From 5ebbabcd47dbcb66c81f24d8bcc2de4fcefb0538 Mon Sep 17 00:00:00 2001 From: Raphael Fakhri <153192858+RaphaelFakhri@users.noreply.github.com> Date: Mon, 19 Oct 2026 06:36:54 +0000 Subject: [PATCH] Clear stale path bytes in install_only tar headers The install_only conversion rewrites each entry path from python/install/... to python/... with tar::Header::set_path. set_path does not clear the name field past the new NUL terminator, and does not clear the ustar prefix when the new path fits in the name field. Each header kept the tail of the original path after the NUL, which some tar readers include in the entry name. Clear the name and prefix fields before setting the new path. Fixes #686 --- src/release.rs | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/release.rs b/src/release.rs index e92c73b65..cf488f8df 100644 --- a/src/release.rs +++ b/src/release.rs @@ -555,6 +555,13 @@ pub fn convert_to_install_only(reader: impl BufRead, writer: W) -> Res let new_path = PathBuf::from("python").join(path.strip_prefix("python/install/")?); let mut header = entry.header().clone(); + // `set_path` leaves the rest of the original path in place after the + // new path's NUL terminator, and leaves a stale ustar prefix when the + // new path fits in the name field. Clear both first. + header.as_old_mut().name.fill(0); + if let Some(ustar) = header.as_ustar_mut() { + ustar.prefix.fill(0); + } header.set_path(&new_path)?; header.set_cksum(); @@ -756,6 +763,94 @@ mod tests { assert!(!should_strip(Path::new("tcl90.dll"), &unsigned)); assert!(!should_strip(Path::new("tcl9tk90.dll"), &unsigned)); } + + fn full_archive(entries: &[(&str, &[u8])]) -> Vec { + let python_json = serde_json::json!({ + "build_info": { + "core": {"objs": [], "links": []}, + "extensions": {}, + "inittab_object": "", + "inittab_source": "", + "inittab_cflags": [], + "object_file_format": "elf", + }, + "build_options": "pgo+lto", + "crt_features": [], + "libpython_link_mode": "static", + "optimizations": "pgo+lto", + "python_bytecode_magic_number": "", + "python_config_vars": {}, + "python_exe": "install/bin/python3", + "python_extension_module_loading": [], + "python_implementation_cache_tag": "cpython-313", + "python_implementation_hex_version": 0, + "python_implementation_name": "cpython", + "python_implementation_version": [], + "python_major_minor_version": "3.13", + "python_paths_abstract": {}, + "python_paths": {"stdlib": "install/lib/python3.13"}, + "python_platform_tag": "", + "python_stdlib_test_packages": [], + "python_suffixes": {}, + "python_symbol_visibility": "", + "python_tag": "cp313", + "python_version": "3.13.0", + "target_triple": "x86_64-unknown-linux-gnu", + "run_tests": "", + "version": "8", + }); + let python_json = serde_json::to_vec(&python_json).unwrap(); + + let mut builder = tar::Builder::new(vec![]); + for (path, data) in std::iter::once(("python/PYTHON.json", python_json.as_slice())) + .chain(entries.iter().copied()) + { + let mut header = tar::Header::new_ustar(); + header.set_path(path).unwrap(); + header.set_size(data.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + builder.append(&header, data).unwrap(); + } + + zstd::encode_all(builder.into_inner().unwrap().as_slice(), 0).unwrap() + } + + #[test] + fn install_only_clears_stale_header_path_bytes() { + let long_name = format!( + "python/install/lib/python3.13/site-packages/{}.py", + "x".repeat(60) + ); + let full = full_archive(&[("python/install/bin/idle3.13", b""), (&long_name, b"")]); + + let install_only = convert_to_install_only(full.as_slice(), vec![]).unwrap(); + let mut tar = vec![]; + flate2::read::GzDecoder::new(install_only.as_slice()) + .read_to_end(&mut tar) + .unwrap(); + + let mut names = vec![]; + for header in tar.as_chunks::<512>().0 { + let header = tar::Header::from_byte_slice(header); + let Some(ustar) = header.as_ustar() else { + continue; + }; + + let len = ustar.name.iter().position(|b| *b == 0).unwrap_or(100); + assert!(ustar.name[len..].iter().all(|b| *b == 0)); + assert!(ustar.prefix.iter().all(|b| *b == 0)); + names.push(String::from_utf8(ustar.name[..len].to_vec()).unwrap()); + } + + assert_eq!( + names, + [ + "python/bin/idle3.13".to_string(), + long_name.replace("python/install/", "python/"), + ] + ); + } } /// Create an install-only .tar.gz archive from a .tar.zst archive.