Skip to content
6 changes: 6 additions & 0 deletions .devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/devcontainers/features/nix:1": {}
}
}
15 changes: 13 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -609,4 +621,3 @@ lazy val rchain = (project in file("."))
rspaceBench,
shared
)

73 changes: 73 additions & 0 deletions casper/src/test/resources/StringEscapeSpec.rho
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion node/src/main/scala/coop/rchain/node/web/VersionInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 0 additions & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,3 @@ addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0")
addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.2")

addDependencyTreePlugin

Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}