diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 000000000..0e53de9a4 --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,6 @@ +{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/nix:1": {} + } +} diff --git a/build.sbt b/build.sbt index 86d855677..81eb512ad 100644 --- a/build.sbt +++ b/build.sbt @@ -5,6 +5,7 @@ import NativePackagerHelper._ import com.typesafe.sbt.packager.docker._ import sys.process._ import javax.print.attribute.standard.RequestingUserName +import java.io.IOException //allow stopping sbt tasks using ctrl+c without killing sbt itself Global / cancelable := true @@ -21,6 +22,17 @@ Global / PB.protocVersion := "3.24.3" // ThisBuild / libraryDependencies += compilerPlugin("io.tryp" % "splain" % "0.5.8" cross CrossVersion.patch) +// Helper function to safely get git commit hash +def getSafeGitCommit(): String = { + try { + val commit = "git rev-parse HEAD".!!.trim + if (commit.nonEmpty && commit.length == 40) commit else "unknown" + } catch { + case _: IOException => "unknown" + case _: Exception => "unknown" + } +} + inThisBuild(List( publish / skip := true, publishMavenStyle := true, @@ -330,7 +342,7 @@ lazy val node = (project in file("node")) scalapb.gen(grpc = false) -> (sourceManaged in Compile).value / "protobuf", grpcmonix.generators.gen() -> (sourceManaged in Compile).value / "protobuf" ), - buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion, git.gitHeadCommit), + buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion, "gitHeadCommit" -> getSafeGitCommit()), buildInfoPackage := "coop.rchain.node", mainClass in Compile := Some("coop.rchain.node.Main"), discoveredMainClasses in Compile := Seq(), @@ -609,4 +621,3 @@ lazy val rchain = (project in file(".")) rspaceBench, shared ) - diff --git a/casper/src/test/resources/StringEscapeSpec.rho b/casper/src/test/resources/StringEscapeSpec.rho new file mode 100644 index 000000000..591cbddb1 --- /dev/null +++ b/casper/src/test/resources/StringEscapeSpec.rho @@ -0,0 +1,73 @@ +new + rl(`rho:registry:lookup`), RhoSpecCh, + test_newline_escape, test_tab_escape, test_quote_escape, + test_backslash_escape, test_unicode_escape, test_combined_escapes, + test_no_escape_needed +in { + rl!(`rho:id:zphjgsfy13h1k85isc8rtwtgt3t9zzt5pjd5ihykfmyapfc4wt3x5h`, *RhoSpecCh) | + for(@(_, RhoSpec) <- RhoSpecCh) { + @RhoSpec!("testSuite", + [ + ("Newline escape sequences are unescaped correctly", *test_newline_escape), + ("Tab escape sequences are unescaped correctly", *test_tab_escape), + ("Quote escape sequences are unescaped correctly", *test_quote_escape), + ("Backslash escape sequences are unescaped correctly", *test_backslash_escape), + ("Combined escape sequences are unescaped correctly", *test_combined_escapes), + ("Strings without escapes remain unchanged", *test_no_escape_needed) + ]) + } | + + contract test_newline_escape(rhoSpec, _, ackCh) = { + new ch, privateAck in { + ch!("Hello\nWorld".toUtf8Bytes().bytesToHex()) | + for (@msg <- ch) { + rhoSpec!("assert", ("Hello\nWorld".toUtf8Bytes().bytesToHex(), "==", msg), "Newline escaped string unescaped correctly", *ackCh) + } + } + } | + + contract test_tab_escape(rhoSpec, _, ackCh) = { + new ch, privateAck in { + ch!("Column1\tColumn2".toUtf8Bytes().bytesToHex()) | + for (@msg <- ch) { + rhoSpec!("assert", ("Column1\tColumn2".toUtf8Bytes().bytesToHex(), "==", msg), "Tab escaped string unescaped correctly", *ackCh) + } + } + } | + + contract test_quote_escape(rhoSpec, _, ackCh) = { + new ch, privateAck in { + ch!("She said \"Hello\"".toUtf8Bytes().bytesToHex()) | + for (@msg <- ch) { + rhoSpec!("assert", ("She said \"Hello\"".toUtf8Bytes().bytesToHex(), "==", msg), "Quote escaped string unescaped correctly", *ackCh) + } + } + } | + + contract test_backslash_escape(rhoSpec, _, ackCh) = { + new ch, privateAck in { + ch!("Path: C:\\Users\\Documents".toUtf8Bytes().bytesToHex()) | + for (@msg <- ch) { + rhoSpec!("assert", ("Path: C:\\Users\\Documents".toUtf8Bytes().bytesToHex(), "==", msg), "Backslash escaped string unescaped correctly", *ackCh) + } + } + } | + + contract test_combined_escapes(rhoSpec, _, ackCh) = { + new ch, privateAck in { + ch!("Line1\nTab\tQuote\"Backslash\\End".toUtf8Bytes().bytesToHex()) | + for (@msg <- ch) { + rhoSpec!("assert", ("Line1\nTab\tQuote\"Backslash\\End".toUtf8Bytes().bytesToHex(), "==", msg), "Multiple escape sequences handled correctly", *ackCh) + } + } + } | + + contract test_no_escape_needed(rhoSpec, _, ackCh) = { + new ch, privateAck in { + ch!("SimpleStringWithoutEscapes".toUtf8Bytes().bytesToHex()) | + for (@msg <- ch) { + rhoSpec!("assert", ("SimpleStringWithoutEscapes".toUtf8Bytes().bytesToHex(), "==", msg), "String without escapes passes through unchanged", *ackCh) + } + } + } +} diff --git a/casper/src/test/scala/coop/rchain/casper/genesis/contracts/StringEscapeTest.scala b/casper/src/test/scala/coop/rchain/casper/genesis/contracts/StringEscapeTest.scala new file mode 100644 index 000000000..86eddc6e4 --- /dev/null +++ b/casper/src/test/scala/coop/rchain/casper/genesis/contracts/StringEscapeTest.scala @@ -0,0 +1,12 @@ +package coop.rchain.casper.genesis.contracts + +import coop.rchain.casper.helper.RhoSpec +import coop.rchain.models.NormalizerEnv +import coop.rchain.rholang.build.CompiledRholangSource + +class StringEscapeSpec + extends RhoSpec( + CompiledRholangSource("StringEscapeSpec.rho", NormalizerEnv.Empty), + Seq.empty, + GENESIS_TEST_TIMEOUT + ) diff --git a/node/src/main/scala/coop/rchain/node/configuration/commandline/Options.scala b/node/src/main/scala/coop/rchain/node/configuration/commandline/Options.scala index 679c84247..862105674 100644 --- a/node/src/main/scala/coop/rchain/node/configuration/commandline/Options.scala +++ b/node/src/main/scala/coop/rchain/node/configuration/commandline/Options.scala @@ -113,7 +113,7 @@ final case class Options(arguments: Seq[String]) extends ScallopConf(arguments) val width = 120 version( - s"RChain node | gRPC client \nversion ${BuildInfo.version} (${BuildInfo.gitHeadCommit.getOrElse("commit # unknown")})" + s"RChain node | gRPC client \nversion ${BuildInfo.version} (${BuildInfo.gitHeadCommit})" ) printedName = "rchain" helpWidth(width) diff --git a/node/src/main/scala/coop/rchain/node/web/VersionInfo.scala b/node/src/main/scala/coop/rchain/node/web/VersionInfo.scala index a31982ea9..f5a84ab5a 100644 --- a/node/src/main/scala/coop/rchain/node/web/VersionInfo.scala +++ b/node/src/main/scala/coop/rchain/node/web/VersionInfo.scala @@ -7,7 +7,7 @@ import org.http4s.HttpRoutes object VersionInfo { val get: String = - s"RChain Node ${BuildInfo.version} (${BuildInfo.gitHeadCommit.getOrElse("commit # unknown")})" + s"RChain Node ${BuildInfo.version} (${BuildInfo.gitHeadCommit})" def service[F[_]: Sync]: HttpRoutes[F] = { val dsl = org.http4s.dsl.Http4sDsl[F] diff --git a/project/plugins.sbt b/project/plugins.sbt index 794f9be0a..d77ee85d4 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -24,4 +24,3 @@ addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.2") addDependencyTreePlugin - diff --git a/rholang/src/main/scala/coop/rchain/rholang/interpreter/compiler/normalizer/GroundNormalizeMatcher.scala b/rholang/src/main/scala/coop/rchain/rholang/interpreter/compiler/normalizer/GroundNormalizeMatcher.scala index d03b45881..390d5e362 100644 --- a/rholang/src/main/scala/coop/rchain/rholang/interpreter/compiler/normalizer/GroundNormalizeMatcher.scala +++ b/rholang/src/main/scala/coop/rchain/rholang/interpreter/compiler/normalizer/GroundNormalizeMatcher.scala @@ -41,6 +41,46 @@ object GroundNormalizeMatcher { // a custom string token def stripString(raw: String): String = { require(raw.length >= 2) - raw.substring(1, raw.length - 1) + val unquoted = raw.substring(1, raw.length - 1) + unescapeString(unquoted) + } + + // Process escape sequences in strings + private def unescapeString(s: String): String = { + val sb = new StringBuilder() + var i = 0 + while (i < s.length) { + if (s(i) == '\\' && i + 1 < s.length) { + s(i + 1) match { + case '"' => sb.append('"'); i += 2 + case '\\' => sb.append('\\'); i += 2 + case 'n' => sb.append('\n'); i += 2 + case 't' => sb.append('\t'); i += 2 + case 'r' => sb.append('\r'); i += 2 + case 'b' => sb.append('\b'); i += 2 + case 'f' => sb.append('\f'); i += 2 + case 'u' if i + 5 < s.length => + try { + val hexCode = s.substring(i + 2, i + 6) + val codePoint = Integer.parseInt(hexCode, 16) + sb.append(codePoint.toChar) + i += 6 + } catch { + case _: NumberFormatException => + // For robustness, treat invalid Unicode escapes as literal characters + sb.append('\\') + sb.append('u') + i += 2 + } + case _ => + sb.append(s(i)) + i += 1 + } + } else { + sb.append(s(i)) + i += 1 + } + } + sb.toString() } }