@@ -603,7 +603,7 @@ fn read_archive(bytes: &[u8]) -> Result<Vec<PackageMember>, AdapterError> {
603603 . by_index ( index)
604604 . map_err ( |error| AdapterError :: Zip ( error. to_string ( ) ) ) ?;
605605 let name = file. name ( ) . to_owned ( ) ;
606- if !portable_member_name ( & file , & name) {
606+ if !portable_member_name ( & name) {
607607 return Err ( AdapterError :: UnsafeMemberName ( name) ) ;
608608 }
609609 if !names. insert ( name. clone ( ) ) {
@@ -656,12 +656,32 @@ fn read_archive(bytes: &[u8]) -> Result<Vec<PackageMember>, AdapterError> {
656656 Ok ( members)
657657}
658658
659- fn portable_member_name < R : Read > ( file : & zip:: read:: ZipFile < ' _ , R > , name : & str ) -> bool {
660- name. is_ascii ( )
661- && !name. contains ( '\\' )
662- && file
663- . enclosed_name ( )
664- . is_some_and ( |path| path. to_string_lossy ( ) == name)
659+ fn portable_member_name ( name : & str ) -> bool {
660+ !name. is_empty ( )
661+ && name. is_ascii ( )
662+ && name. bytes ( ) . all ( |byte| {
663+ byte >= b' '
664+ && byte != 0x7f
665+ && !matches ! ( byte, b'\\' | b'<' | b'>' | b':' | b'"' | b'|' | b'?' | b'*' )
666+ } )
667+ && name. split ( '/' ) . all ( |segment| {
668+ !segment. is_empty ( )
669+ && !matches ! ( segment, "." | ".." )
670+ && !segment. ends_with ( [ '.' , ' ' ] )
671+ && !windows_reserved_segment ( segment)
672+ } )
673+ }
674+
675+ fn windows_reserved_segment ( segment : & str ) -> bool {
676+ let stem = segment. split_once ( '.' ) . map_or ( segment, |( stem, _) | stem) ;
677+ [ "CON" , "PRN" , "AUX" , "NUL" ]
678+ . into_iter ( )
679+ . any ( |reserved| stem. eq_ignore_ascii_case ( reserved) )
680+ || ( stem. len ( ) == 4
681+ && [ "COM" , "LPT" ]
682+ . into_iter ( )
683+ . any ( |reserved| stem[ ..3 ] . eq_ignore_ascii_case ( reserved) )
684+ && matches ! ( stem. as_bytes( ) [ 3 ] , b'1' ..=b'9' ) )
665685}
666686
667687fn write_archive ( members : & [ PackageMember ] ) -> Result < Vec < u8 > , AdapterError > {
@@ -1952,6 +1972,40 @@ mod tests {
19521972 ) ) ;
19531973 }
19541974
1975+ #[ test]
1976+ fn member_names_are_host_independent_and_traversal_safe ( ) {
1977+ for name in [
1978+ "manifest.json" ,
1979+ "files/00000000-0000-0000-0000-000000000001.json" ,
1980+ "objects/design probe.bin" ,
1981+ ] {
1982+ assert ! (
1983+ portable_member_name( name) ,
1984+ "expected {name:?} to be portable"
1985+ ) ;
1986+ }
1987+ for name in [
1988+ "" ,
1989+ "/manifest.json" ,
1990+ "files/" ,
1991+ "files//shape.json" ,
1992+ "files/./shape.json" ,
1993+ "files/../manifest.json" ,
1994+ "files\\ shape.json" ,
1995+ "C:/shape.json" ,
1996+ "files/shape?.json" ,
1997+ "files/shape. " ,
1998+ "files/CON.json" ,
1999+ "files/lpt9" ,
2000+ "files/shape\u{7f} .json" ,
2001+ ] {
2002+ assert ! (
2003+ !portable_member_name( name) ,
2004+ "expected {name:?} to be rejected"
2005+ ) ;
2006+ }
2007+ }
2008+
19552009 #[ test]
19562010 fn writer_rejects_duplicate_member_names ( ) {
19572011 let cursor = Cursor :: new ( Vec :: new ( ) ) ;
0 commit comments