From fc0d93d11c416bc602cee022025529a1c95fe385 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 11 Aug 2023 22:33:36 -0400 Subject: [PATCH] Include content hash into File hash value Problem ------- I'd like to reuse JsonFormat structure to calculate hash of an arbitrary value. This idea was already implemented in #26, but I'd like File's hash value to include the content hash. Solution -------- This changes the default JsonFormat of the File to include both the file name and the content hash. --- ...scala => FileIsoStringLongBenchmark.scala} | 7 +- build.sbt | 2 +- .../scala/sjsonnew/BasicJsonProtocol.scala | 2 + .../scala/sjsonnew/FileIsoStringLongs.scala | 31 +++++++ core/src/main/scala/sjsonnew/HashUtil.scala | 37 ++++++++ .../main/scala/sjsonnew/IsoStringLong.scala | 85 +++++++++++++++++++ .../scala/sjsonnew/IsoStringLongFormats.scala | 42 +++++++++ .../scala/sjsonnew/JavaExtraFormats.scala | 59 +------------ .../main/scala/sjsonnew/PathOnlyFormats.scala | 45 ++++++++++ core/src/main/scala/sjsonnew/package.scala | 2 + project/Dependencies.scala | 1 + .../support/murmurhash/MurmurhashSpec.scala | 44 ++++++++++ .../support/spray/JavaExtraFormatsSpec.scala | 31 ------- .../support/spray/PathOnlyFormatsSpec.scala | 55 ++++++++++++ 14 files changed, 350 insertions(+), 93 deletions(-) rename benchmark/src/main/scala/sjsonnew/benchmark/{FileIsoStringBenchmark.scala => FileIsoStringLongBenchmark.scala} (56%) create mode 100644 core/src/main/scala/sjsonnew/FileIsoStringLongs.scala create mode 100644 core/src/main/scala/sjsonnew/IsoStringLong.scala create mode 100644 core/src/main/scala/sjsonnew/IsoStringLongFormats.scala create mode 100644 core/src/main/scala/sjsonnew/PathOnlyFormats.scala create mode 100644 support/spray/src/test/scala/sjsonnew/support/spray/PathOnlyFormatsSpec.scala diff --git a/benchmark/src/main/scala/sjsonnew/benchmark/FileIsoStringBenchmark.scala b/benchmark/src/main/scala/sjsonnew/benchmark/FileIsoStringLongBenchmark.scala similarity index 56% rename from benchmark/src/main/scala/sjsonnew/benchmark/FileIsoStringBenchmark.scala rename to benchmark/src/main/scala/sjsonnew/benchmark/FileIsoStringLongBenchmark.scala index 0a2d524..1fe1ddc 100644 --- a/benchmark/src/main/scala/sjsonnew/benchmark/FileIsoStringBenchmark.scala +++ b/benchmark/src/main/scala/sjsonnew/benchmark/FileIsoStringLongBenchmark.scala @@ -3,14 +3,13 @@ package sjsonnew.benchmark import java.io.File import org.openjdk.jmh.annotations.Benchmark -import sjsonnew.IsoString +import sjsonnew.IsoStringLong -class FileIsoStringBenchmark { +class FileIsoStringLongBenchmark { @Benchmark def fileB: String = { - import sjsonnew.BasicJsonProtocol._ - val isoFile = implicitly[IsoString[File]] + val isoFile = implicitly[IsoStringLong[File]] val f = new File("/tmp") isoFile.to(f) } diff --git a/build.sbt b/build.sbt index 7f1e857..d138985 100644 --- a/build.sbt +++ b/build.sbt @@ -39,7 +39,7 @@ lazy val core = (projectMatrix in file("core")) ) ) .jvmPlatform(scalaVersions = allScalaVersions, settings = Seq( - libraryDependencies ++= testDependencies.value, + libraryDependencies ++= testDependencies.value ++ Seq(zeroAllocationHashing), )) def support(n: String) = diff --git a/core/src/main/scala/sjsonnew/BasicJsonProtocol.scala b/core/src/main/scala/sjsonnew/BasicJsonProtocol.scala index 2837a9c..0fba208 100644 --- a/core/src/main/scala/sjsonnew/BasicJsonProtocol.scala +++ b/core/src/main/scala/sjsonnew/BasicJsonProtocol.scala @@ -29,12 +29,14 @@ trait BasicJsonProtocol with AdditionalFormats with UnionFormats with FlatUnionFormats + with IsoStringLongFormats with IsoFormats with JavaPrimitiveFormats with JavaExtraFormats with CalendarFormats with ImplicitHashWriters with CaseClassFormats + with FileIsoStringLongs with ThrowableFormats object BasicJsonProtocol extends BasicJsonProtocol diff --git a/core/src/main/scala/sjsonnew/FileIsoStringLongs.scala b/core/src/main/scala/sjsonnew/FileIsoStringLongs.scala new file mode 100644 index 0000000..88d3d86 --- /dev/null +++ b/core/src/main/scala/sjsonnew/FileIsoStringLongs.scala @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 Eugene Yokota + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sjsonnew + +import java.io.File +import java.net.URI +import java.nio.file.{ Path, Paths } + +trait FileIsoStringLongs { + implicit lazy val fileStringLongIso: IsoStringLong[File] = IsoStringLong.iso[File]( + (file: File) => (IsoStringLong.fileToString(file), HashUtil.farmHash(file.toPath())), + (p: (String, Long)) => IsoStringLong.uriToFile(new URI(p._1))) + + implicit lazy val pathStringLongIso: IsoStringLong[Path] = IsoStringLong.iso[Path]( + (file: Path) => (file.toString, HashUtil.farmHash(file)), + (p: (String, Long)) => Paths.get(p._1)) +} diff --git a/core/src/main/scala/sjsonnew/HashUtil.scala b/core/src/main/scala/sjsonnew/HashUtil.scala index 8f55c86..ab4a182 100644 --- a/core/src/main/scala/sjsonnew/HashUtil.scala +++ b/core/src/main/scala/sjsonnew/HashUtil.scala @@ -1,5 +1,9 @@ package sjsonnew +import java.io.{ BufferedInputStream, File, FileInputStream, FileNotFoundException, InputStream } +import java.nio.file.{ Files, Path } +import net.openhft.hashing.LongHashFunction + object HashUtil { // https://github.com/addthis/stream-lib/blob/master/src/main/java/com/clearspring/analytics/hash/MurmurHash.java def hashLong(data: Long): Int = @@ -19,4 +23,37 @@ object HashUtil { h ^= h >>> 15 h } + + private[sjsonnew] def farmHash(bytes: Array[Byte]): Long = + LongHashFunction.farmNa().hashBytes(bytes) + + private[sjsonnew] def farmHash(path: Path): Long = { + // allocating many byte arrays for large files may lead to OOME + // but it is more efficient for small files + val largeFileLimit = 10 * 1024 * 1024 + if (!Files.exists(path) || Files.isDirectory(path)) 0L + else if (Files.size(path) < largeFileLimit) farmHash(Files.readAllBytes(path)) + else farmHash(sha256(path.toFile)) + } + + /** Calculates the SHA-1 hash of the given file. */ + def sha256(file: File): Array[Byte] = + try sha256(new BufferedInputStream(new FileInputStream(file))) // apply closes the stream + catch { case _: FileNotFoundException => Array() } + + /** Calculates the SHA-1 hash of the given stream, closing it when finished. */ + def sha256(stream: InputStream): Array[Byte] = { + val BufferSize = 8192 + import java.security.{ DigestInputStream, MessageDigest } + val digest = MessageDigest.getInstance("SHA-256") + try { + val dis = new DigestInputStream(stream, digest) + val buffer = new Array[Byte](BufferSize) + while (dis.read(buffer) >= 0) {} + dis.close() + digest.digest + } finally { + stream.close() + } + } } diff --git a/core/src/main/scala/sjsonnew/IsoStringLong.scala b/core/src/main/scala/sjsonnew/IsoStringLong.scala new file mode 100644 index 0000000..78f5e3f --- /dev/null +++ b/core/src/main/scala/sjsonnew/IsoStringLong.scala @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2023 Eugene Yokota + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sjsonnew + +import java.io.File +import java.net.{ URI, URL } +import java.util.Locale + +trait IsoStringLong[A] { + def to(a: A): (String, Long) + def from(p: (String, Long)): A +} + +object IsoStringLong { + def iso[A](to0: A => (String, Long), from0: ((String, Long)) => A): IsoStringLong[A] = new IsoStringLong[A] { + def to(a: A): (String, Long) = to0(a) + def from(p: (String, Long)): A = from0(p) + } + + private[sjsonnew] lazy val isWindows: Boolean = + System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows") + + private[sjsonnew] final val FileScheme = "file" + + private[sjsonnew] def fileToString(file: File): String = { + val p = file.getPath + if (p.startsWith(File.separatorChar.toString) && isWindows) { + if (p.startsWith("""\\""")) { + // supports \\laptop\My Documents\Some.doc on Windows + new URI(FileScheme, normalizeName(p), null).toASCIIString + } + else { + // supports /tmp on Windows + new URI(FileScheme, "", normalizeName(p), null).toASCIIString + } + } else if (file.isAbsolute) { + //not using f.toURI to avoid filesystem syscalls + //we use empty string as host to force file:// instead of just file: + new URI(FileScheme, "", normalizeName(ensureHeadSlash(file.getAbsolutePath)), null).toASCIIString + } else { + new URI(null, normalizeName(file.getPath), null).toASCIIString + } + } + + private[this] def ensureHeadSlash(name: String) = { + if(name.nonEmpty && name.head != File.separatorChar) s"${File.separatorChar}$name" + else name + } + private[this] def normalizeName(name: String) = { + val sep = File.separatorChar + if (sep == '/') name else name.replace(sep, '/') + } + + private[sjsonnew] def uriToFile(uri: URI): File = { + val part = uri.getSchemeSpecificPart + // scheme might be omitted for relative URI reference. + assert( + Option(uri.getScheme) match { + case None | Some(FileScheme) => true + case _ => false + }, + s"Expected protocol to be '$FileScheme' or empty in URI $uri" + ) + Option(uri.getAuthority) match { + case None if part startsWith "/" => new File(uri) + case _ => + if (!(part startsWith "/") && (part contains ":")) new File("//" + part) + else new File(part) + } + } +} diff --git a/core/src/main/scala/sjsonnew/IsoStringLongFormats.scala b/core/src/main/scala/sjsonnew/IsoStringLongFormats.scala new file mode 100644 index 0000000..8372ef5 --- /dev/null +++ b/core/src/main/scala/sjsonnew/IsoStringLongFormats.scala @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 Eugene Yokota + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sjsonnew + +trait IsoStringLongFormats { + implicit def isoStringLongFormat[A: IsoStringLong]: JsonFormat[A] = new JsonFormat[A] { + val iso = implicitly[IsoStringLong[A]] + def write[J](a: A, builder: Builder[J]): Unit = { + val p = iso.to(a) + builder.beginObject() + builder.addFieldName("first") + builder.writeString(p._1) + builder.addFieldName("second") + builder.writeLong(p._2) + builder.endObject() + } + def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): A = + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val first = unbuilder.readField[String]("first") + val second = unbuilder.readField[Long]("second") + unbuilder.endObject() + iso.from((first, second)) + case None => deserializationError(s"Expected JsObject but got None") + } + } +} diff --git a/core/src/main/scala/sjsonnew/JavaExtraFormats.scala b/core/src/main/scala/sjsonnew/JavaExtraFormats.scala index a6a03d1..e9294e0 100644 --- a/core/src/main/scala/sjsonnew/JavaExtraFormats.scala +++ b/core/src/main/scala/sjsonnew/JavaExtraFormats.scala @@ -19,11 +19,10 @@ package sjsonnew import java.net.{ URI, URL } import java.io.File import java.math.{ BigInteger, BigDecimal => JBigDecimal } -import java.util.{ Locale, Optional, UUID } +import java.util.{ Optional, UUID } trait JavaExtraFormats { this: PrimitiveFormats with AdditionalFormats with IsoFormats => - import JavaExtraFormats._ private[this] type JF[A] = JsonFormat[A] // simple alias for reduced verbosity @@ -40,57 +39,6 @@ trait JavaExtraFormats { implicit val urlStringIso: IsoString[URL] = IsoString.iso[URL]( _.toURI.toASCIIString, (s: String) => (new URI(s)).toURL) - private[this] final val FileScheme = "file" - - implicit val fileStringIso: IsoString[File] = IsoString.iso[File]( - (f: File) => { - val p = f.getPath - if (p.startsWith(File.separatorChar.toString) && isWindows) { - if (p.startsWith("""\\""")) { - // supports \\laptop\My Documents\Some.doc on Windows - new URI(FileScheme, normalizeName(p), null).toASCIIString - } - else { - // supports /tmp on Windows - new URI(FileScheme, "", normalizeName(p), null).toASCIIString - } - } else if (f.isAbsolute) { - //not using f.toURI to avoid filesystem syscalls - //we use empty string as host to force file:// instead of just file: - new URI(FileScheme, "", normalizeName(ensureHeadSlash(f.getAbsolutePath)), null).toASCIIString - } else { - new URI(null, normalizeName(f.getPath), null).toASCIIString - } - }, - (s: String) => uriToFile(new URI(s))) - - private[this] def ensureHeadSlash(name: String) = { - if(name.nonEmpty && name.head != File.separatorChar) s"${File.separatorChar}$name" - else name - } - private[this] def normalizeName(name: String) = { - val sep = File.separatorChar - if (sep == '/') name else name.replace(sep, '/') - } - - private[this] def uriToFile(uri: URI): File = { - val part = uri.getSchemeSpecificPart - // scheme might be omitted for relative URI reference. - assert( - Option(uri.getScheme) match { - case None | Some(FileScheme) => true - case _ => false - }, - s"Expected protocol to be '$FileScheme' or empty in URI $uri" - ) - Option(uri.getAuthority) match { - case None if part startsWith "/" => new File(uri) - case _ => - if (!(part startsWith "/") && (part contains ":")) new File("//" + part) - else new File(part) - } - } - implicit def optionalFormat[A :JF]: JF[Optional[A]] = new OptionalFormat[A] final class OptionalFormat[A :JF] extends JF[Optional[A]] { lazy val elemFormat = implicitly[JF[A]] @@ -112,7 +60,4 @@ trait JavaExtraFormats { } } -object JavaExtraFormats { - private[sjsonnew] lazy val isWindows: Boolean = - System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows") -} +object JavaExtraFormats diff --git a/core/src/main/scala/sjsonnew/PathOnlyFormats.scala b/core/src/main/scala/sjsonnew/PathOnlyFormats.scala new file mode 100644 index 0000000..7e4fc22 --- /dev/null +++ b/core/src/main/scala/sjsonnew/PathOnlyFormats.scala @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 Eugene Yokota + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sjsonnew + +import java.io.File +import java.net.URI +import java.nio.file.{ Path, Paths } + +trait PathOnlyFormats { + implicit val pathOnlyFileFormat: JsonFormat[File] = new JsonFormat[File] { + def write[J](file: File, builder: Builder[J]): Unit = + builder.writeString(IsoStringLong.fileToString(file)) + def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): File = + jsOpt match { + case Some(js) => IsoStringLong.uriToFile(new URI(unbuilder.readString(js))) + case None => deserializationError(s"Expected JsString but got None") + } + } + + implicit val pathOnlyPathFormat: JsonFormat[Path] = new JsonFormat[Path] { + def write[J](file: Path, builder: Builder[J]): Unit = + builder.writeString(file.toString) + def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): Path = + jsOpt match { + case Some(js) => Paths.get(unbuilder.readString(js)) + case None => deserializationError(s"Expected JsString but got None") + } + } +} + +object PathOnlyFormats extends PathOnlyFormats diff --git a/core/src/main/scala/sjsonnew/package.scala b/core/src/main/scala/sjsonnew/package.scala index 565b56e..1844189 100644 --- a/core/src/main/scala/sjsonnew/package.scala +++ b/core/src/main/scala/sjsonnew/package.scala @@ -24,9 +24,11 @@ package object sjsonnew with AdditionalFormats with UnionFormats with FlatUnionFormats + with IsoStringLongFormats with IsoFormats with JavaPrimitiveFormats with ThrowableFormats + with FileIsoStringLongs with ImplicitHashWriters { def deserializationError(msg: String, cause: Throwable = null, fieldNames: List[String] = Nil) = throw new DeserializationException(msg, cause, fieldNames) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index bcf5869..555a46c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -20,4 +20,5 @@ object Dependencies { lazy val jawnSpray = "org.typelevel" %% "jawn-spray" % jawnVersion lazy val shadedJawnParser = "com.eed3si9n" %% "shaded-jawn-parser" % "1.3.2" lazy val lmIvy = "org.scala-sbt" %% "librarymanagement-ivy" % "1.2.4" + lazy val zeroAllocationHashing = "net.openhft" % "zero-allocation-hashing" % "0.10.1" } diff --git a/support/murmurhash/src/test/scala/sjsonnew/support/murmurhash/MurmurhashSpec.scala b/support/murmurhash/src/test/scala/sjsonnew/support/murmurhash/MurmurhashSpec.scala index 79a4971..59e4829 100644 --- a/support/murmurhash/src/test/scala/sjsonnew/support/murmurhash/MurmurhashSpec.scala +++ b/support/murmurhash/src/test/scala/sjsonnew/support/murmurhash/MurmurhashSpec.scala @@ -17,11 +17,15 @@ package sjsonnew package support.murmurhash +import java.io.File +import java.nio.file.Paths import org.scalatest.flatspec.AnyFlatSpec import BUtil._ import LList._ class MurmurhashSpec extends AnyFlatSpec { + import IsoStringLong.isWindows + "The IntJsonFormat" should "convert an Int to an int hash" in { assert(Hasher.hashUnsafe[Int](1) === 1527037976) } @@ -118,4 +122,44 @@ class MurmurhashSpec extends AnyFlatSpec { lazy val a1 = ("a", 1) :*: LNil lazy val ba1 = ("b", a1) :*: LNil lazy val a1Hash = 1371594665 + + "The FileIsoStringLongs" should "convert a File to an int hash" in { + if (isWindows) { + if (scala.util.Properties.versionNumberString.startsWith("2.13.")) { + assert(Hasher.hashUnsafe(new File("LICENSE")) == 1342556559) + assert(Hasher.hashUnsafe(new File("non-existent")) == -857314535) + } else { + assert(Hasher.hashUnsafe(new File("LICENSE")) == 39842659) + assert(Hasher.hashUnsafe(new File("non-existent")) == -863778723) + } + } else { + if (scala.util.Properties.versionNumberString.startsWith("2.13.")) { + assert(Hasher.hashUnsafe(new File("LICENSE")) == 1209992821) + assert(Hasher.hashUnsafe(new File("non-existent")) == -857314535) + } else { + assert(Hasher.hashUnsafe(new File("LICENSE")) == 1642989355) + assert(Hasher.hashUnsafe(new File("non-existent")) == -863778723) + } + } + } + + it should "convert a Path to an int hash" in { + if (isWindows) { + if (scala.util.Properties.versionNumberString.startsWith("2.13.")) { + assert(Hasher.hashUnsafe(Paths.get("LICENSE")) == 1342556559) + assert(Hasher.hashUnsafe(Paths.get("non-existent")) == -857314535) + } else { + assert(Hasher.hashUnsafe(Paths.get("LICENSE")) == 39842659) + assert(Hasher.hashUnsafe(Paths.get("non-existent")) == -863778723) + } + } else { + if (scala.util.Properties.versionNumberString.startsWith("2.13.")) { + assert(Hasher.hashUnsafe(Paths.get("LICENSE")) == 1209992821) + assert(Hasher.hashUnsafe(Paths.get("non-existent")) == -857314535) + } else { + assert(Hasher.hashUnsafe(Paths.get("LICENSE")) == 1642989355) + assert(Hasher.hashUnsafe(Paths.get("non-existent")) == -863778723) + } + } + } } diff --git a/support/spray/src/test/scala/sjsonnew/support/spray/JavaExtraFormatsSpec.scala b/support/spray/src/test/scala/sjsonnew/support/spray/JavaExtraFormatsSpec.scala index e598702..9e5cd91 100644 --- a/support/spray/src/test/scala/sjsonnew/support/spray/JavaExtraFormatsSpec.scala +++ b/support/spray/src/test/scala/sjsonnew/support/spray/JavaExtraFormatsSpec.scala @@ -23,8 +23,6 @@ import java.io.File import java.util.{ Locale, Optional, UUID } object JavaExtraFormatsSpec extends verify.BasicTestSuite with BasicJsonProtocol { - import JavaExtraFormats._ - case class Person(name: Optional[String], value: Optional[Int]) implicit object PersonFormat extends JsonFormat[Person] { def write[J](x: Person, builder: Builder[J]): Unit = { @@ -72,35 +70,6 @@ object JavaExtraFormatsSpec extends verify.BasicTestSuite with BasicJsonProtocol Predef.assert(Converter.fromJsonUnsafe[URL](JsString("http://localhost")) == url) } - test("The fileStringIso") { - val f = new File("/tmp") - val f2 = new File(new File("src"), "main") - // "convert a File to JsString" in { - Predef.assert(Converter.toJsonUnsafe(f) == JsString("file:///tmp")) - - // "convert a relative path to JsString" in { - // https://tools.ietf.org/html/rfc3986#section-4.2 - Predef.assert(Converter.toJsonUnsafe(f2) == JsString("src/main")) - - // "convert the JsString back to the File" in { - Predef.assert(Converter.fromJsonUnsafe[File](JsString("file:///tmp")) == f) - - // "convert the JsString back to the relative path" in { - Predef.assert(Converter.fromJsonUnsafe[File](JsString("src/main")) == f2) - - // "convert an absolute path on Windows" in { - if (isWindows) Predef.assert(Converter.toJsonUnsafe(new File("""C:\Documents and Settings\""")) == JsString("file:///C:/Documents%20and%20Settings")) - else () - - // "convert a relative path on Windows" in { - if (isWindows) Predef.assert(Converter.toJsonUnsafe(new File("""..\My Documents\test""")) == JsString("../My%20Documents/test")) - else () - - // "convert a UNC path on Windows" in { - if (isWindows) Predef.assert(Converter.toJsonUnsafe(new File("""\\laptop\My Documents\Some.doc""")) == JsString("file://laptop/My%20Documents/Some.doc")) - else () - } - test("The optionalFormat") { // "convert Optional.empty to JsNull" in { Predef.assert(Converter.toJsonUnsafe(Optional.empty[Int]) == JsNull) diff --git a/support/spray/src/test/scala/sjsonnew/support/spray/PathOnlyFormatsSpec.scala b/support/spray/src/test/scala/sjsonnew/support/spray/PathOnlyFormatsSpec.scala new file mode 100644 index 0000000..f76d39d --- /dev/null +++ b/support/spray/src/test/scala/sjsonnew/support/spray/PathOnlyFormatsSpec.scala @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2023 Eugene Yokota + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sjsonnew +package support.spray + +import spray.json.{ JsValue, JsNumber, JsString, JsNull, JsTrue, JsFalse, JsObject } +import java.io.File + +object PathOnlyFormatsSpec extends verify.BasicTestSuite { + import IsoStringLong.isWindows + import PathOnlyFormats._ + + test("The PathOnlyFormats can override the default") { + val f = new File("/tmp") + val f2 = new File(new File("src"), "main") + // "convert a File to JsString" in { + Predef.assert(Converter.toJsonUnsafe(f) == JsString("file:///tmp")) + + // "convert a relative path to JsString" in { + // https://tools.ietf.org/html/rfc3986#section-4.2 + Predef.assert(Converter.toJsonUnsafe(f2) == JsString("src/main")) + + // "convert the JsString back to the File" in { + Predef.assert(Converter.fromJsonUnsafe[File](JsString("file:///tmp")) == f) + + // "convert the JsString back to the relative path" in { + Predef.assert(Converter.fromJsonUnsafe[File](JsString("src/main")) == f2) + + // "convert an absolute path on Windows" in { + if (isWindows) Predef.assert(Converter.toJsonUnsafe(new File("""C:\Documents and Settings\""")) == JsString("file:///C:/Documents%20and%20Settings")) + else () + + // "convert a relative path on Windows" in { + if (isWindows) Predef.assert(Converter.toJsonUnsafe(new File("""..\My Documents\test""")) == JsString("../My%20Documents/test")) + else () + + // "convert a UNC path on Windows" in { + if (isWindows) Predef.assert(Converter.toJsonUnsafe(new File("""\\laptop\My Documents\Some.doc""")) == JsString("file://laptop/My%20Documents/Some.doc")) + else () + } +}