@@ -31,29 +31,47 @@ fn write_empty(writer: &mut impl fmt::Write) -> fmt::Result {
3131 writer. write_str ( "-" )
3232}
3333
34+ /// Escape a byte slice for a space-delimited dumpfile field.
35+ ///
36+ /// This corresponds to `print_escaped_optional` in the C composefs
37+ /// `composefs-info.c`, combining `ESCAPE_STANDARD | ESCAPE_LONE_DASH`.
38+ /// Empty values map to `-` (the "none" sentinel), and a bare `-` is
39+ /// hex-escaped so it is not confused with the sentinel.
40+ ///
41+ /// Not appropriate for xattr values — use [`write_escaped_raw`] instead.
3442fn write_escaped ( writer : & mut impl fmt:: Write , bytes : & [ u8 ] ) -> fmt:: Result {
3543 if bytes. is_empty ( ) {
3644 return write_empty ( writer) ;
3745 }
3846
39- // A bare `-` must be escaped because the parser uses `-` as
40- // the sentinel for "empty/none" (cf. `optional_str` in the parser) .
47+ // Matches C ESCAPE_LONE_DASH: a bare `-` must be escaped because
48+ // the parser uses `-` as the sentinel for "empty/none".
4149 if bytes == b"-" {
4250 return writer. write_str ( "\\ x2d" ) ;
4351 }
4452
4553 write_escaped_raw ( writer, bytes)
4654}
4755
48- /// Write escaped bytes without special handling for empty or bare `-`.
56+ /// Escape a byte slice without the `-` sentinel logic .
4957///
50- /// Used for xattr values where the `-` sentinel is not applicable:
51- /// xattr values are always present when the key=value pair exists,
52- /// and a value of `-` or empty is perfectly valid.
58+ /// This corresponds to `print_escaped` with `ESCAPE_EQUAL` (but without
59+ /// `ESCAPE_LONE_DASH`) in the C composefs `composefs-info.c`. Used for
60+ /// xattr values where `-` and empty are valid literal values, not
61+ /// sentinels.
62+ ///
63+ /// Note: we unconditionally escape `=` in all fields, whereas the C code
64+ /// only uses `ESCAPE_EQUAL` for xattr keys and values. This is harmless
65+ /// since `\x3d` round-trips correctly, but means our output for paths
66+ /// containing `=` is slightly more verbose than the C output.
5367fn write_escaped_raw ( writer : & mut impl fmt:: Write , bytes : & [ u8 ] ) -> fmt:: Result {
5468 for c in bytes {
5569 let c = * c;
5670
71+ // The set of hex-escaped characters matches C `!isgraph(c)` in the
72+ // POSIX locale (i.e. outside 0x21..=0x7E), plus `=` and `\`.
73+ // The C code uses named escapes for `\\`, `\n`, `\r`, `\t` while
74+ // we hex-escape everything uniformly; both forms parse correctly.
5775 if c < b'!' || c == b'=' || c == b'\\' || c > b'~' {
5876 write ! ( writer, "\\ x{c:02x}" ) ?;
5977 } else {
@@ -234,6 +252,12 @@ pub fn write_leaf(
234252///
235253/// Creates a special entry that links the given path to an existing target path
236254/// that was already written to the dumpfile.
255+ ///
256+ /// The C composefs writes the real metadata (mode, uid, etc.) from the
257+ /// target node; we write placeholder zeros since both parsers ignore all
258+ /// fields except the target path for hardlink entries. The nlink/uid/gid/rdev
259+ /// fields must be valid integers (not `-`) because the parser reads them
260+ /// unconditionally before checking the `@` hardlink prefix.
237261pub fn write_hardlink ( writer : & mut impl fmt:: Write , path : & Path , target : & OsStr ) -> fmt:: Result {
238262 write_escaped ( writer, path. as_os_str ( ) . as_bytes ( ) ) ?;
239263 write ! ( writer, " 0 @120000 0 0 0 0 0.0 " ) ?;
0 commit comments