@@ -630,4 +630,73 @@ mod tests {
630630 assert ! ( !destdir. join( "blah" ) . exists( ) ) ;
631631 Ok ( ( ) )
632632 }
633+
634+ /// Regression test: PAX `path` headers (used for non-ASCII filenames)
635+ /// must not bypass the /etc -> /usr/etc remap, since PAX takes
636+ /// precedence over basic tar headers per POSIX.
637+ #[ tokio:: test]
638+ async fn tar_filter_pax_etc_remap ( ) -> Result < ( ) > {
639+ let tempd = tempfile:: tempdir ( ) ?;
640+ let src_tar_path = tempd. path ( ) . join ( "src.tar" ) ;
641+ let pax_path = "etc/ssl/certs/Főtanúsítvány.pem" ;
642+
643+ // Build a tar with an explicit PAX `path` under etc/, matching how
644+ // Docker/BuildKit produces layers for non-ASCII filenames.
645+ {
646+ let mut builder = tar:: Builder :: new ( std:: fs:: File :: create ( & src_tar_path) ?) ;
647+ let data = b"cert" ;
648+ let mut header = tar:: Header :: new_gnu ( ) ;
649+ header. set_size ( data. len ( ) as u64 ) ;
650+ header. set_mode ( 0o644 ) ;
651+ header. set_entry_type ( tar:: EntryType :: Regular ) ;
652+ header. set_cksum ( ) ;
653+ builder. append_pax_extensions ( [ ( "path" , pax_path. as_bytes ( ) ) ] . into_iter ( ) ) ?;
654+ builder. append_data ( & mut header, pax_path, & data[ ..] ) ?;
655+ builder. into_inner ( ) ?;
656+ }
657+
658+ let mut dest = Vec :: new ( ) ;
659+ let src = tokio:: io:: BufReader :: new ( tokio:: fs:: File :: open ( & src_tar_path) . await ?) ;
660+ let cap_tmpdir = Dir :: open_ambient_dir ( & tempd, cap_std:: ambient_authority ( ) ) ?;
661+ filter_tar_async (
662+ src,
663+ oci_image:: MediaType :: ImageLayer ,
664+ & mut dest,
665+ & Default :: default ( ) ,
666+ cap_tmpdir,
667+ )
668+ . await ?;
669+
670+ // Check the raw PAX headers in the output. We cannot use unpack()
671+ // because the Rust tar crate resolves PAX-vs-GNU conflicts
672+ // differently than libarchive/ostree (which gives PAX precedence).
673+ let mut found_remapped = false ;
674+ let mut archive = tar:: Archive :: new ( Cursor :: new ( dest. as_slice ( ) ) ) ;
675+ for entry in archive. entries ( ) ? {
676+ let mut entry = entry?;
677+ let entry_path = entry. path ( ) ?;
678+ let entry_path = entry_path. to_string_lossy ( ) ;
679+ let entry_path = entry_path. trim_start_matches ( "./" ) ;
680+ if entry_path == format ! ( "usr/{pax_path}" ) {
681+ found_remapped = true ;
682+ }
683+ if let Some ( pax) = entry. pax_extensions ( ) ? {
684+ for ext in pax. flatten ( ) {
685+ if let Ok ( "path" | "linkpath" ) = ext. key ( ) {
686+ let value = String :: from_utf8_lossy ( ext. value_bytes ( ) ) ;
687+ let clean = value. trim_start_matches ( "./" ) . trim_end_matches ( '\0' ) ;
688+ assert ! (
689+ !clean. starts_with( "etc/" ) && clean != "etc" ,
690+ "PAX header still contains unremapped /etc path: {value}"
691+ ) ;
692+ }
693+ }
694+ }
695+ }
696+ assert ! (
697+ found_remapped,
698+ "Expected remapped file at usr/{pax_path} not found in output"
699+ ) ;
700+ Ok ( ( ) )
701+ }
633702}
0 commit comments