File size: 8,554 Bytes
2811ca0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | # PK3 extraction: first completed research step
Verified on **2026-10-11**, using only `.cache/UrbanTerror43/`. No original executable or QVM was run. All 39 cached installation files still match the recorded [source snapshot](../inventory/snapshot.json). Original installation copies are removed from `assets/`; extracted member payloads are the durable asset inputs.
## Reproduce and verify
```sh
python3 scripts/setup-repository.py
python3 scripts/test_unpack_pk3.py
python3 scripts/test_storage_policy.py
python3 scripts/unpack_pk3.py extract
python3 scripts/unpack_pk3.py verify
```
The Python 3 standard library is sufficient for extraction. Git LFS and Git Xet are needed for repository storage. `extract` verifies existing outputs before reusing them, refuses altered/unexpected files, and never overwrites a changed payload. `verify` reconstructs member hashes and ZIP metadata from the original archives and compares the entire output/report set. Missing or changed reports fail verification. Custom source/output/report/snapshot roots and explicit decompression limits are available via `--help`; the three roots must be disjoint. The default snapshot requires exactly the supplied 23 PK3s with their recorded sizes/hashes.
## Layout and identities
```text
assets/extracted/<archive filename>/members/<six-digit entry index>/<original path>
docs/extraction/archives/<archive filename>.jsonl
```
For example, `sound/weapons/FRF1/frf1-inside.wav` from `zUrT43_010.pk3` is stored at `assets/extracted/zUrT43_010.pk3/members/000174/sound/weapons/FRF1/frf1-inside.wav`. Its lowercase-name counterpart in `zUrT43_021.pk3` is stored separately under member `000063`. The index is zero-based in central-directory order and includes directory records, so gaps between file indices are intentional.
Each member is identified by the full archive SHA-256, entry index, exact logical path, same-name occurrence number and uncompressed SHA-256. Archive order is only deterministic enumeration, **not an established game loading order**. No winner is selected and no override is flattened. Duplicate ZIP filenames also have separate indexed directories, although none occurs within these 23 archives. Exact duplicate bytes remain separate working-tree files with separate provenance; LFS/Xet may deduplicate their storage.
Git control names are escaped (`.gitattributes` becomes `%2Egitattributes`) to stop game payloads changing repository filters. Platform punctuation/percent signs are percent-encoded; unusually long components use a hash label. The manifest retains the exact decoded original path. Each isolated member directory avoids case-folding and Unicode-normalization collisions. The supplied archives need no Git-control-name escaping. ZIP names use their original flags/CP437 decoding; flags, extras and comments are retained for further format research.
The **743 empty directory entries are preserved as metadata**, because Git does not store empty directories. The **three empty skin files are physically present** and remain file entries, distinguished from directory records. Empty files use the Git LFS specification's empty-pointer form, which is zero bytes; all non-empty assets must be canonical SHA-256 LFS pointers. See the [Git LFS specification](https://github.com/git-lfs/git-lfs/blob/main/docs/spec.md).
## Verified findings
| Measurement | Result |
| --- | ---: |
| PK3 archives | 23 |
| Central-directory entries | 10,578 |
| File occurrences extracted and verified | 9,835 |
| Directory records | 743 |
| Uncompressed file bytes | 3,125,455,778 |
| Distinct exact logical paths | 9,721 |
| Distinct normalized/case-folded logical paths | 9,718 |
| Repeated exact-path groups/events | 114 / 114 |
| Repeated-path groups with differing bytes | 92 |
| Repeated-path groups with identical bytes | 22 |
| Case-collision groups | 3 |
| Distinct payload SHA-256 values | 9,069 |
| Identical-payload groups across any paths | 509 |
| Empty file occurrences | 3 |
All members were decompressed, checked by Python's ZIP CRC verification, hashed and then compared against the extracted files in a separate full verification pass. All source archive hashes match the earlier inventory. Every central-directory record uses flag bits `0` and ZIP creator system `0` (DOS metadata); this records archive metadata and does not establish the host on which the game was built. Compression is ZIP stored (`0`) or DEFLATE (`8`); no encrypted entries, symbolic links, duplicate filenames within an archive, or non-empty directory payloads were found.
The three empty files occur in `zUrT43_008.pk3`: `models/weapons2/negev/negev_hold_female_b.skin`, `negev_hold_female_w.skin`, and `negev_hold_male_b.skin`. Their presence is proven; their runtime role remains to be established.
For FRF1 sounds, `frf1-inside.wav` and `frf1-outside.wav` differ between the uppercase directory in archive `010` and the lowercase directory in archive `021`. `frf1-shelldrop.wav` is byte-identical in the two archives. All six occurrences are retained. Case-insensitive filesystems must not choose a variant for the project; the original engine's VFS behavior remains a research task.
None of the extracted payload hashes equals any of the 39 original installation-file hashes. The original LFS objects can therefore be removed without deleting an object required by these extracted members. This comparison is by SHA-256, rather than guessed filenames.
## Evidence files
- [summary.json](summary.json): counts, layout, extraction bounds, archive identities and extension census.
- `archives/*.jsonl`: an archive header followed by every file/directory record. Records retain original name, occurrence/index, output path, size, compressed size, CRC, SHA-256, compression/flags, date tuple, creator/extractor versions, external/internal attributes, local-header offset, volume, comments and extras.
- [path-variants.jsonl](path-variants.jsonl): every repeated logical path and all its versions; no runtime winner.
- [case-collisions.jsonl](case-collisions.jsonl): all case/normalization collisions and their retained paths/hashes.
- [identical-payloads.jsonl](identical-payloads.jsonl): every shared content hash and all occurrences, including empty skins.
The largest per-archive metadata report is below 1 MiB. Reports are ordinary UTF-8 Git documentation; extracted payloads use the `assets/**` LFS/Xet policy. These are descriptive metadata records, not decoded game logic or copied media inside documentation.
## Safety, validation and remaining work
The extractor preflights archive identities, paths, methods, encryption flags, file types and decompression bounds before producing outputs. It rejects absolute/drive paths, parent traversal, malformed path separators, embedded NUL names, symlink/special entries, filesystem symlink ancestors, root overlap and unexpected output files. Streaming decompression checks length and CRC; per-archive staging is promoted atomically and removed on failure. Defaults allow 1 GiB per member, 8 GiB total expansion, 100,000 entries and a 2,000:1 expansion ratio. These bounds accommodate this verified snapshot and can be explicitly changed for a separately reviewed input.
Synthetic tests cover archive overrides, same-archive duplicate entries, case and Unicode collisions, zero bytes, empty directories, Git control names, path escapes, links, decompression bounds, CRC corruption, changed outputs/reports, input snapshot drift and deterministic reruns. Storage tests also cover the empty-pointer form, original-file reintroduction, bypassed filters and payloads deleted later in history. Batch Git-object reading keeps guards practical for thousands of staged assets while preserving bounded reads.
This step completes provenance and extraction work in AP-003/AP-004/AP-005/AP-008. It establishes the layout and extraction-side safety portions of AP-006/AP-007; metadata-only/selected-member modes and future runtime package readers remain pending. Format parsing, VFS precedence, shaders/material semantics, BSP geometry/collision, MD3 animation, QVM instructions/syscalls, dependency closure, reference gameplay and Solari conversion are not inferred from successful unpacking.
Next research steps: inspect every QVM and native binary/library, validate BSP/MD3/AAS structures, parse original shader scripts with dependency records, then determine original VFS precedence through source evidence and controlled reference runs. Use the preserved member identities in every subsequent finding.
|