@@ -30,8 +30,9 @@ pub(crate) fn transform_mtree_entries<R: Read>(
3030 filter : & PathFilter < ' _ > ,
3131 time_filters : & TimeFilters ,
3232) -> io:: Result < Vec < io:: Result < Option < NormalEntry > > > > {
33+ let normalized = normalize_mtree_input ( reader) ?;
3334 // Use empty cwd to avoid mtree2 joining paths with current working directory
34- let mtree = MTree :: from_reader_with_cwd ( reader , PathBuf :: new ( ) ) ;
35+ let mtree = MTree :: from_reader_with_cwd ( io :: Cursor :: new ( normalized ) , PathBuf :: new ( ) ) ;
3536 let mut results = Vec :: new ( ) ;
3637
3738 for entry_result in mtree {
@@ -85,6 +86,98 @@ pub(crate) fn transform_mtree_entries<R: Read>(
8586 Ok ( results)
8687}
8788
89+ /// Normalizes mtree input for bsdtar compatibility before parsing with mtree2.
90+ ///
91+ /// - Converts CRLF/CR line endings to LF
92+ /// - Rewrites `content=` keyword to `contents=` (libarchive alias)
93+ fn normalize_mtree_input ( mut reader : impl Read ) -> io:: Result < Vec < u8 > > {
94+ let mut raw = Vec :: new ( ) ;
95+ reader. read_to_end ( & mut raw) ?;
96+ let normalized_line_endings = normalize_line_endings ( & raw ) ;
97+ Ok ( rewrite_content_keyword_alias ( & normalized_line_endings) )
98+ }
99+
100+ fn normalize_line_endings ( input : & [ u8 ] ) -> Vec < u8 > {
101+ let mut out = Vec :: with_capacity ( input. len ( ) ) ;
102+ let mut idx = 0 ;
103+ while idx < input. len ( ) {
104+ if input[ idx] == b'\r' {
105+ if idx + 1 < input. len ( ) && input[ idx + 1 ] == b'\n' {
106+ idx += 1 ;
107+ }
108+ out. push ( b'\n' ) ;
109+ } else {
110+ out. push ( input[ idx] ) ;
111+ }
112+ idx += 1 ;
113+ }
114+ out
115+ }
116+
117+ fn rewrite_content_keyword_alias ( input : & [ u8 ] ) -> Vec < u8 > {
118+ let mut out = Vec :: with_capacity ( input. len ( ) ) ;
119+ let mut line_start = 0 ;
120+ while line_start < input. len ( ) {
121+ let mut line_end = line_start;
122+ while line_end < input. len ( ) && input[ line_end] != b'\n' {
123+ line_end += 1 ;
124+ }
125+
126+ rewrite_content_keyword_line ( & input[ line_start..line_end] , & mut out) ;
127+ if line_end < input. len ( ) {
128+ out. push ( b'\n' ) ;
129+ line_end += 1 ;
130+ }
131+ line_start = line_end;
132+ }
133+ out
134+ }
135+
136+ fn rewrite_content_keyword_line ( line : & [ u8 ] , out : & mut Vec < u8 > ) {
137+ if line. is_empty ( ) || line[ 0 ] == b'#' {
138+ out. extend_from_slice ( line) ;
139+ return ;
140+ }
141+
142+ let mut idx = 0 ;
143+ while idx < line. len ( ) && is_mtree_whitespace ( line[ idx] ) {
144+ idx += 1 ;
145+ }
146+ while idx < line. len ( ) && !is_mtree_whitespace ( line[ idx] ) {
147+ idx += 1 ;
148+ }
149+ out. extend_from_slice ( & line[ ..idx] ) ;
150+
151+ while idx < line. len ( ) {
152+ let ws_start = idx;
153+ while idx < line. len ( ) && is_mtree_whitespace ( line[ idx] ) {
154+ idx += 1 ;
155+ }
156+ out. extend_from_slice ( & line[ ws_start..idx] ) ;
157+
158+ let token_start = idx;
159+ while idx < line. len ( ) && !is_mtree_whitespace ( line[ idx] ) {
160+ idx += 1 ;
161+ }
162+ if token_start == idx {
163+ break ;
164+ }
165+
166+ let token = & line[ token_start..idx] ;
167+ if token. starts_with ( b"content=" ) {
168+ out. extend_from_slice ( b"contents=" ) ;
169+ out. extend_from_slice ( & token[ b"content=" . len ( ) ..] ) ;
170+ } else {
171+ out. extend_from_slice ( token) ;
172+ }
173+ }
174+ }
175+
176+ #[ inline]
177+ fn is_mtree_whitespace ( byte : u8 ) -> bool {
178+ matches ! ( byte, b' ' | b'\t' )
179+ }
180+
88181/// Creates a single archive entry from an mtree entry.
89182fn create_entry_from_mtree (
90183 mtree_entry : & MtreeEntry ,
@@ -434,4 +527,32 @@ mod tests {
434527 assert_eq ! ( entry2. path( ) . to_str( ) , Some ( "file2.txt" ) ) ;
435528 assert ! ( entry2. optional( ) , "file2.txt should be marked as optional" ) ;
436529 }
530+
531+ #[ test]
532+ fn normalize_mtree_input_converts_crlf_and_content_alias ( ) {
533+ let input = b"#mtree\r \n f type=file content=bar/foo\r \n " ;
534+ let normalized = normalize_mtree_input ( & input[ ..] ) . unwrap ( ) ;
535+ assert_eq ! ( normalized, b"#mtree\n f type=file contents=bar/foo\n " ) ;
536+ }
537+
538+ #[ test]
539+ fn normalize_mtree_input_keeps_existing_contents_keyword ( ) {
540+ let input = b"#mtree\n f type=file contents=bar/foo\n " ;
541+ let normalized = normalize_mtree_input ( & input[ ..] ) . unwrap ( ) ;
542+ assert_eq ! ( normalized, input) ;
543+ }
544+
545+ #[ test]
546+ fn normalize_mtree_input_preserves_first_token ( ) {
547+ let input = b"#mtree\n content=file type=file contents=bar/foo\n " ;
548+ let normalized = normalize_mtree_input ( & input[ ..] ) . unwrap ( ) ;
549+ assert_eq ! ( normalized, input) ;
550+ }
551+
552+ #[ test]
553+ fn normalize_mtree_input_handles_wrapped_crlf_line ( ) {
554+ let input = b"#mtree\r \n f uname=\\ \r \n root content=bar/foo\r \n " ;
555+ let normalized = normalize_mtree_input ( & input[ ..] ) . unwrap ( ) ;
556+ assert_eq ! ( normalized, b"#mtree\n f uname=\\ \n root contents=bar/foo\n " ) ;
557+ }
437558}
0 commit comments