Keep PyInstaller modules within their PYZ archive (from #707)

This commit is contained in:
Afloat
2026-11-04 01:13:52 +07:00
committed by bflatastic
parent 03a2d7a304
commit 6cedc883e9
2 changed files with 53 additions and 2 deletions
+7 -2
View File
@@ -218,6 +218,7 @@ struct PyzRead {
/// How many modules its table holds, which may be more than `entries`.
total: usize,
encrypted: bool,
outside: usize,
}
fn looks_zlib(b: &[u8]) -> bool {
@@ -254,6 +255,7 @@ fn read_pyz(data: &[u8], member: &PackedEntry, room: usize, key_seen: bool) -> R
let unzipped = !samples.is_empty() && samples.iter().all(|b| !looks_zlib(b));
let encrypted = key_seen || key_inside || unzipped;
let kind = format!("{PYZ_KIND}{magic:04x}");
let outside = modules.iter().filter(|m| !in_range(m)).count();
let entries = modules
.iter()
.map(|m| PackedEntry {
@@ -261,11 +263,11 @@ fn read_pyz(data: &[u8], member: &PackedEntry, room: usize, key_seen: bool) -> R
offset: member.offset.saturating_add(m.at),
stored_size: m.len,
size: None,
compression: if encrypted { Compression::Unreadable } else { Compression::Zlib },
compression: if encrypted || !in_range(m) { Compression::Unreadable } else { Compression::Zlib },
kind: Some(kind.clone()),
})
.collect();
Ok(PyzRead { magic, entries, total, encrypted })
Ok(PyzRead { magic, entries, total, encrypted, outside })
}
impl PackedFormat for PyInstaller {
@@ -312,6 +314,9 @@ impl PackedFormat for PyInstaller {
"encrypted": read.encrypted,
}));
truncated |= read.total > read.entries.len();
if read.outside > 0 {
notes.push(format!("{} module(s) in {} lie outside that PYZ archive and cannot be read.", read.outside, m.entry.path));
}
nested.extend(read.entries);
}
Err(why) => {
+46
View File
@@ -0,0 +1,46 @@
use craft_anything_formats::packed::{Compression, PackedFormat};
use craft_anything_formats::pyinstaller::PyInstaller;
use craft_anything_testkit::packed::{code_311, marshal as m, pyinstaller};
fn sample() -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let code = code_311("<module>", "<module>", "good.py", 1, &[], &[], &[]);
let compressed = miniz_oxide::deflate::compress_to_vec_zlib(&code, 6);
let mut pyz = b"PYZ\0".to_vec();
pyz.extend_from_slice(&3531u16.to_le_bytes());
pyz.extend_from_slice(&[0x0d, 0x0a]);
pyz.extend_from_slice(&[0; 8]);
pyz.extend_from_slice(&compressed);
let table_at = pyz.len();
pyz[8..12].copy_from_slice(&(table_at as u32).to_be_bytes());
let table = |outside: i32| {
m::list(vec![
m::tuple(vec![m::string("good"), m::tuple(vec![m::int(0), m::int(16), m::int(compressed.len() as i32)])]),
m::tuple(vec![m::string("outside"), m::tuple(vec![m::int(0), m::int(outside), m::int(compressed.len() as i32)])]),
])
};
let full_len = table_at + table(0).len();
pyz.extend_from_slice(&table(full_len as i32));
let container = pyinstaller(&[("PYZ.pyz", b'z', &pyz, false), ("neighbor", b'x', &compressed, false)], true, 312);
(container, compressed, code)
}
#[test]
fn a_module_cannot_read_compressed_bytes_from_the_next_container_member() {
let (data, _, _) = sample();
let listing = PyInstaller.list(&data).unwrap();
let bad = listing.entries.iter().find(|e| e.path == "PYZ.pyz.modules/outside.pyc").unwrap();
assert_eq!(bad.compression, Compression::Unreadable);
assert!(PyInstaller.read(&data, bad, 1000).is_err());
assert!(listing.notes.iter().any(|n| n.contains("outside") || n.contains("bounds")));
}
#[test]
fn sound_modules_and_neighboring_members_still_read() {
let (data, compressed, code) = sample();
let listing = PyInstaller.list(&data).unwrap();
let good = listing.entries.iter().find(|e| e.path == "PYZ.pyz.modules/good.pyc").unwrap();
let recovered = PyInstaller.read(&data, good, 1000).unwrap();
assert_eq!(&recovered[16..], code);
let neighbor = listing.entries.iter().find(|e| e.path == "neighbor").unwrap();
assert_eq!(PyInstaller.read(&data, neighbor, 1000).unwrap(), compressed);
}