Artifact 09b9cadc58b2cd75d0d327b78daf21b54b3aaf76e5dc90f801ed3fb99c807205:
- File
internal/deck/encode.go
— part of check-in
[1bfa6b2094]
at
2026-07-23 15:34:10
on branch trunk
— deck: complete the §4.3 escape table in FossilEncode/FossilDecode (#137)
FossilEncode/FossilDecode implemented only 3 of the 8 escapes the artifact/sync format §4.3 mandates (\s, \n, \\). A field value carrying a real TAB, CR, VT, FF or NUL byte -- as canonical fossil writes it -- was mis-decoded and no longer re-marshalled to its source wire bytes, changing the artifact hash. Unrecognized escapes were also left literal instead of dropping the backslash per spec.
Add the remaining escapes (\t \r \v \f \0) to both directions and make an unrecognized escape drop its backslash, matching canonical fossil fossilize/defossilize (verified against fossil 2.28). Every caller -- A/C/H/L/J/F/T/U cards and the xfer cards -- shares these functions and gets the fix.
Closes #119 (user: [email protected], size: 1832) [annotate] [blame] [check-ins using]
package deck import "strings" // FossilEncode escapes a string for use as an artifact/sync card field value, // per §4.3 of the format spec. It mirrors canonical fossil's fossilize(): the // eight bytes newline, space, tab, carriage return, vertical tab, form feed, // NUL and backslash are each written as a two-character backslash escape; all // other bytes pass through unchanged. func FossilEncode(s string) string { var b strings.Builder b.Grow(len(s)) for i := 0; i < len(s); i++ { switch s[i] { case '\n': b.WriteString(`\n`) case ' ': b.WriteString(`\s`) case '\t': b.WriteString(`\t`) case '\r': b.WriteString(`\r`) case '\v': b.WriteString(`\v`) case '\f': b.WriteString(`\f`) case 0: b.WriteString(`\0`) case '\\': b.WriteString(`\\`) default: b.WriteByte(s[i]) } } return b.String() } // FossilDecode reverses FossilEncode, per §4.3. It mirrors canonical fossil's // defossilize(): each recognized escape maps back to its raw byte, and an // unrecognized escape drops the backslash and keeps the following byte literally. // A lone trailing backslash (which a conforming encoder never emits) is left // as-is rather than consuming a nonexistent following byte. func FossilDecode(s string) string { var b strings.Builder b.Grow(len(s)) for i := 0; i < len(s); i++ { if s[i] != '\\' || i+1 >= len(s) { b.WriteByte(s[i]) continue } i++ switch s[i] { case 'n': b.WriteByte('\n') case 's': b.WriteByte(' ') case 't': b.WriteByte('\t') case 'r': b.WriteByte('\r') case 'v': b.WriteByte('\v') case 'f': b.WriteByte('\f') case '0': b.WriteByte(0) case '\\': b.WriteByte('\\') default: // §4.3: an unrecognized escape drops the backslash and keeps the byte. b.WriteByte(s[i]) } } return b.String() }