diff --git a/build.sbt b/build.sbt index df5058f4c1..019c8b2202 100644 --- a/build.sbt +++ b/build.sbt @@ -244,6 +244,13 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq( // sealed trait: #3349 ProblemFilters.exclude[ReversedMissingMethodProblem]( "fs2.io.net.tls.TLSParameters.withClientAuthType" + ), + // equals/hashCode/toString on file attributes: #3345 + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "fs2.io.file.PosixFileAttributes.fs2$io$file$PosixFileAttributes$$super=uals" + ), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "fs2.io.file.PosixFileAttributes.fs2$io$file$PosixFileAttributes$$super#Code" ) ) diff --git a/io/jvm-native/src/main/scala/fs2/io/file/FilesPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/file/FilesPlatform.scala index 73eacd0be1..590da203af 100644 --- a/io/jvm-native/src/main/scala/fs2/io/file/FilesPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/file/FilesPlatform.scala @@ -421,6 +421,7 @@ private[file] trait FilesCompanionPlatform { with PosixFileAttributes.UnsealedPosixFileAttributes { def owner: Principal = attr.owner def group: Principal = attr.group - def permissions: PosixPermissions = PosixPermissions.fromString(attr.permissions.toString).get + def permissions: PosixPermissions = + PosixPermissions.fromString(PosixFilePermissions.toString(attr.permissions)).get } } diff --git a/io/shared/src/main/scala/fs2/io/file/FileAttributes.scala b/io/shared/src/main/scala/fs2/io/file/FileAttributes.scala index 97c2620e81..9d4eb287de 100644 --- a/io/shared/src/main/scala/fs2/io/file/FileAttributes.scala +++ b/io/shared/src/main/scala/fs2/io/file/FileAttributes.scala @@ -42,6 +42,38 @@ sealed trait BasicFileAttributes { def lastAccessTime: FiniteDuration def lastModifiedTime: FiniteDuration def size: Long + + override def equals(that: Any): Boolean = that match { + case other: BasicFileAttributes => + creationTime == other.creationTime && + fileKey == other.fileKey && + isDirectory == other.isDirectory && + isOther == other.isOther && + isRegularFile == other.isRegularFile && + isSymbolicLink == other.isSymbolicLink && + lastAccessTime == other.lastAccessTime && + lastModifiedTime == other.lastModifiedTime && + size == other.size + case _ => false + } + + override def hashCode: Int = { + import util.hashing.MurmurHash3.{stringHash, mix, finalizeHash} + val h = stringHash("FileAttributes") + mix(h, creationTime.##) + mix(h, fileKey.##) + mix(h, isDirectory.##) + mix(h, isOther.##) + mix(h, isRegularFile.##) + mix(h, isSymbolicLink.##) + mix(h, lastAccessTime.##) + mix(h, lastModifiedTime.##) + mix(h, size.##) + finalizeHash(h, 9) + } + + override def toString: String = + s"BasicFileAttributes($creationTime, $fileKey, $isDirectory, $isOther, $isRegularFile, $isSymbolicLink, $lastAccessTime, $lastModifiedTime, $size)" } object BasicFileAttributes { @@ -54,6 +86,22 @@ object BasicFileAttributes { // the owner/group operations JVM only. sealed trait PosixFileAttributes extends BasicFileAttributes { def permissions: PosixPermissions + + final override def equals(that: Any): Boolean = that match { + case other: PosixFileAttributes => super.equals(other) && permissions == other.permissions + case _ => false + } + + final override def hashCode: Int = { + import util.hashing.MurmurHash3.{stringHash, mix, finalizeHash} + val h = stringHash("PosixFileAttributes") + mix(h, super.hashCode) + mix(h, permissions.##) + finalizeHash(h, 2) + } + + final override def toString: String = + s"PosixFileAttributes($creationTime, $fileKey, $isDirectory, $isOther, $isRegularFile, $isSymbolicLink, $lastAccessTime, $lastModifiedTime, $size, $permissions)" } object PosixFileAttributes { diff --git a/io/shared/src/test/scala/fs2/io/file/FilesSuite.scala b/io/shared/src/test/scala/fs2/io/file/FilesSuite.scala index 60b2d61c4c..bc0048b22b 100644 --- a/io/shared/src/test/scala/fs2/io/file/FilesSuite.scala +++ b/io/shared/src/test/scala/fs2/io/file/FilesSuite.scala @@ -856,4 +856,21 @@ class FilesSuite extends Fs2IoSuite with BaseFileSuite { } } + group("attributes") { + + test("basic attributes are consistent for the same file") { + tempFile.use { p => + val attr = Files[IO].getBasicFileAttributes(p) + (attr, attr).mapN(assertEquals(_, _)) + } + } + + test("posix attributes are consistent for the same file") { + tempFile.use { p => + val attr = Files[IO].getPosixFileAttributes(p) + (attr, attr).mapN(assertEquals(_, _)) + } + } + } + }