Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion sdk/go/dstack/compose_hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,35 @@ func toDeterministicJSON(v interface{}) (string, error) {
return "", err
}
// Encode appends a newline that Marshal does not.
return strings.TrimSuffix(buf.String(), "\n"), nil
return unescapeLineSeparators(strings.TrimSuffix(buf.String(), "\n")), nil
}

// unescapeLineSeparators undoes the one escape SetEscapeHTML(false) leaves on:
// encoding/json always writes U+2028 and U+2029 as `\u2028` and `\u2029`, which
// Rust, Python and JS write raw. It walks escape pairs so that an escaped
// backslash followed by the text `u2028` is left alone.
func unescapeLineSeparators(s string) string {
if !strings.Contains(s, `\u202`) {
return s
}
var b strings.Builder
b.Grow(len(s))
for i := 0; i < len(s); i++ {
switch {
case strings.HasPrefix(s[i:], `\u2028`):
b.WriteRune('\u2028')
i += 5
case strings.HasPrefix(s[i:], `\u2029`):
b.WriteRune('\u2029')
i += 5
case s[i] == '\\':
b.WriteString(s[i : i+2])
i++
default:
b.WriteByte(s[i])
}
}
return b.String()
}

// GetComposeHash computes the SHA256 hash of the application composition
Expand Down
19 changes: 19 additions & 0 deletions sdk/go/dstack/compose_hash_parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,25 @@ func TestHTMLCharactersAreNotEscaped(t *testing.T) {
}
}

// encoding/json escapes U+2028 and U+2029 even with HTML escaping off; the
// other SDKs write them raw. The fixture also carries the six characters
// `\u2028`, whose backslash is escaped, so they must come out as `\\u2028`.
func TestLineSeparatorsAreNotEscaped(t *testing.T) {
compose := AppCompose{
Runner: "docker-compose",
DockerComposeFile: "a\u2028b\u2029c \\u2028",
}
got, err := GetComposeHash(compose)
if err != nil {
t.Fatal(err)
}
canonical := "{\"docker_compose_file\":\"a\u2028b\u2029c \\\\u2028\",\"runner\":\"docker-compose\"}"
sum := sha256.Sum256([]byte(canonical))
if want := hex.EncodeToString(sum[:]); got != want {
t.Fatalf("the canonical form diverged from the other SDKs.\n got=%s\nwant=%s", got, want)
}
}

// A typed field and an Extra key of the same name must not both be emitted.
func TestADeclaredFieldWinsOverExtra(t *testing.T) {
compose := AppCompose{
Expand Down
Loading