xfh 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. xfh-0.1.0/LICENSE +26 -0
  2. xfh-0.1.0/NOTICE +10 -0
  3. xfh-0.1.0/PKG-INFO +140 -0
  4. xfh-0.1.0/README.md +104 -0
  5. xfh-0.1.0/docs/codec-support.md +32 -0
  6. xfh-0.1.0/docs/format.md +35 -0
  7. xfh-0.1.0/docs/oracle-matrix.md +121 -0
  8. xfh-0.1.0/docs/release-checklist.md +77 -0
  9. xfh-0.1.0/docs/research/initial-findings.md +107 -0
  10. xfh-0.1.0/pyproject.toml +77 -0
  11. xfh-0.1.0/src/xfh/__init__.py +33 -0
  12. xfh-0.1.0/src/xfh/__main__.py +5 -0
  13. xfh-0.1.0/src/xfh/api.py +171 -0
  14. xfh-0.1.0/src/xfh/cli.py +101 -0
  15. xfh-0.1.0/src/xfh/codecs/__init__.py +63 -0
  16. xfh-0.1.0/src/xfh/codecs/_prefix.py +41 -0
  17. xfh-0.1.0/src/xfh/codecs/_streams.py +102 -0
  18. xfh-0.1.0/src/xfh/codecs/acca.py +71 -0
  19. xfh-0.1.0/src/xfh/codecs/artm.py +91 -0
  20. xfh-0.1.0/src/xfh/codecs/blzw.py +103 -0
  21. xfh-0.1.0/src/xfh/codecs/crm.py +145 -0
  22. xfh-0.1.0/src/xfh/codecs/dlta.py +19 -0
  23. xfh-0.1.0/src/xfh/codecs/fast.py +22 -0
  24. xfh-0.1.0/src/xfh/codecs/fbr2.py +40 -0
  25. xfh-0.1.0/src/xfh/codecs/hfmn.py +56 -0
  26. xfh-0.1.0/src/xfh/codecs/huff.py +53 -0
  27. xfh-0.1.0/src/xfh/codecs/ilzr.py +37 -0
  28. xfh-0.1.0/src/xfh/codecs/impl.py +114 -0
  29. xfh-0.1.0/src/xfh/codecs/lhlb.py +124 -0
  30. xfh-0.1.0/src/xfh/codecs/lz_small.py +105 -0
  31. xfh-0.1.0/src/xfh/codecs/lzw_variants.py +96 -0
  32. xfh-0.1.0/src/xfh/codecs/mash.py +54 -0
  33. xfh-0.1.0/src/xfh/codecs/none.py +14 -0
  34. xfh-0.1.0/src/xfh/codecs/nuke.py +100 -0
  35. xfh-0.1.0/src/xfh/codecs/pwpk.py +104 -0
  36. xfh-0.1.0/src/xfh/codecs/rake.py +137 -0
  37. xfh-0.1.0/src/xfh/codecs/rdcn.py +44 -0
  38. xfh-0.1.0/src/xfh/codecs/rle.py +69 -0
  39. xfh-0.1.0/src/xfh/codecs/shri.py +305 -0
  40. xfh-0.1.0/src/xfh/codecs/smpl.py +32 -0
  41. xfh-0.1.0/src/xfh/codecs/sqsh.py +112 -0
  42. xfh-0.1.0/src/xfh/codecs/standard.py +56 -0
  43. xfh-0.1.0/src/xfh/codecs/wrappers.py +84 -0
  44. xfh-0.1.0/src/xfh/codecs/zeno.py +79 -0
  45. xfh-0.1.0/src/xfh/container.py +258 -0
  46. xfh-0.1.0/src/xfh/errors.py +31 -0
  47. xfh-0.1.0/src/xfh/limits.py +20 -0
  48. xfh-0.1.0/src/xfh/models.py +57 -0
  49. xfh-0.1.0/tests/__init__.py +1 -0
  50. xfh-0.1.0/tests/fixtures/diskexpander/cancel-packed.hex +9 -0
  51. xfh-0.1.0/tests/fixtures/diskexpander/cancel-plain.hex +13 -0
  52. xfh-0.1.0/tests/fixtures/oracle/acca100-text.hex +1 -0
  53. xfh-0.1.0/tests/fixtures/oracle/blzw060-zero256.hex +3 -0
  54. xfh-0.1.0/tests/fixtures/oracle/bytes.hex +6 -0
  55. xfh-0.1.0/tests/fixtures/oracle/bzp2040-repeat1k.hex +1 -0
  56. xfh-0.1.0/tests/fixtures/oracle/cbr0050-zero256.hex +2 -0
  57. xfh-0.1.0/tests/fixtures/oracle/crm2-independent-repeat1k.hex +1 -0
  58. xfh-0.1.0/tests/fixtures/oracle/crms-independent-repeat1k.hex +1 -0
  59. xfh-0.1.0/tests/fixtures/oracle/dlta100-text.hex +1 -0
  60. xfh-0.1.0/tests/fixtures/oracle/duke050-bytes.hex +3 -0
  61. xfh-0.1.0/tests/fixtures/oracle/fast050-repeat1k.hex +5 -0
  62. xfh-0.1.0/tests/fixtures/oracle/fbr2100-zero256.hex +1 -0
  63. xfh-0.1.0/tests/fixtures/oracle/frle016-zero256.hex +2 -0
  64. xfh-0.1.0/tests/fixtures/oracle/gzip065-repeat1k.hex +1 -0
  65. xfh-0.1.0/tests/fixtures/oracle/hfmn000-text.hex +1 -0
  66. xfh-0.1.0/tests/fixtures/oracle/huff050-bytes.hex +7 -0
  67. xfh-0.1.0/tests/fixtures/oracle/ilzr050-repeat1k.hex +1 -0
  68. xfh-0.1.0/tests/fixtures/oracle/impl100-repeat1k.hex +1 -0
  69. xfh-0.1.0/tests/fixtures/oracle/lhlb100-repeat1k.hex +1 -0
  70. xfh-0.1.0/tests/fixtures/oracle/lzbs100-repeat1k.hex +1 -0
  71. xfh-0.1.0/tests/fixtures/oracle/lzw2100-repeat1k.hex +1 -0
  72. xfh-0.1.0/tests/fixtures/oracle/lzw3100-repeat1k.hex +1 -0
  73. xfh-0.1.0/tests/fixtures/oracle/lzw4100-repeat1k.hex +1 -0
  74. xfh-0.1.0/tests/fixtures/oracle/lzw5100-repeat1k.hex +1 -0
  75. xfh-0.1.0/tests/fixtures/oracle/manifest.json +121 -0
  76. xfh-0.1.0/tests/fixtures/oracle/mash100-repeat1k.hex +1 -0
  77. xfh-0.1.0/tests/fixtures/oracle/none050-repeat1k.hex +23 -0
  78. xfh-0.1.0/tests/fixtures/oracle/nuke050-repeat1k.hex +3 -0
  79. xfh-0.1.0/tests/fixtures/oracle/pwpk-ppmc-repeat1k.hex +1 -0
  80. xfh-0.1.0/tests/fixtures/oracle/rake100-repeat1k.hex +2 -0
  81. xfh-0.1.0/tests/fixtures/oracle/rdcn100-repeat1k.hex +9 -0
  82. xfh-0.1.0/tests/fixtures/oracle/repeat1k.hex +22 -0
  83. xfh-0.1.0/tests/fixtures/oracle/rlen050-text.hex +5 -0
  84. xfh-0.1.0/tests/fixtures/oracle/sdhc047-repeat1k.hex +17 -0
  85. xfh-0.1.0/tests/fixtures/oracle/shri100-repeat1k.hex +2 -0
  86. xfh-0.1.0/tests/fixtures/oracle/shri100-repeat64k.hex +6 -0
  87. xfh-0.1.0/tests/fixtures/oracle/slz3100-repeat1k.hex +1 -0
  88. xfh-0.1.0/tests/fixtures/oracle/sqsh100-repeat1k.hex +1 -0
  89. xfh-0.1.0/tests/fixtures/oracle/text.hex +1 -0
  90. xfh-0.1.0/tests/fixtures/oracle/zeno050-repeat1k.hex +1 -0
  91. xfh-0.1.0/tests/fixtures/oracle/zero256.hex +2 -0
  92. xfh-0.1.0/tests/helpers.py +54 -0
  93. xfh-0.1.0/tests/test_api.py +54 -0
  94. xfh-0.1.0/tests/test_cli.py +60 -0
  95. xfh-0.1.0/tests/test_codecs.py +336 -0
  96. xfh-0.1.0/tests/test_fixture_manifest.py +130 -0
  97. xfh-0.1.0/tests/test_legacy.py +53 -0
  98. xfh-0.1.0/tests/test_oracle_tools.py +40 -0
  99. xfh-0.1.0/tests/test_properties.py +70 -0
  100. xfh-0.1.0/tools/abk2png.ts +43 -0
  101. xfh-0.1.0/tools/amigadates.ts +67 -0
  102. xfh-0.1.0/tools/amigaget.ts +56 -0
  103. xfh-0.1.0/tools/amigapeek.ts +42 -0
  104. xfh-0.1.0/tools/fsuae/README.md +49 -0
  105. xfh-0.1.0/tools/fsuae/Startup-Sequence +9 -0
  106. xfh-0.1.0/tools/fsuae/oracle.py +501 -0
  107. xfh-0.1.0/tools/fsuae/probe-stage +19 -0
  108. xfh-0.1.0/tools/fsuae/query-stage +6 -0
  109. xfh-0.1.0/tools/fsuae/run.py +61 -0
  110. xfh-0.1.0/tools/try_xpkf.py +34 -0
  111. xfh-0.1.0/tools/verify_fixture_manifest.py +150 -0
xfh-0.1.0/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2026, xfh contributors
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice,
10
+ this list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26
+ POSSIBILITY OF SUCH DAMAGE.
xfh-0.1.0/NOTICE ADDED
@@ -0,0 +1,10 @@
1
+ Parts of the XPK codec implementations are derived from Ancient:
2
+
3
+ Copyright (c) 2017-2025, Teemu Suutari
4
+ https://github.com/temisu/ancient
5
+
6
+ Ancient is distributed under the BSD 2-Clause License. Its copyright and
7
+ license terms are retained in the relevant source modules.
8
+
9
+ Historical XFH and XPK source code is used as format documentation and test
10
+ oracle material only. It is not included in this distribution.
xfh-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: xfh
3
+ Version: 0.1.0
4
+ Summary: Recover Amiga XPK files, including files written through XFH
5
+ Keywords: amiga,compression,data-recovery,xpk,xfh
6
+ Author: xfh contributors
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-Expression: BSD-2-Clause
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: System :: Archiving :: Compression
21
+ License-File: LICENSE
22
+ License-File: NOTICE
23
+ Requires-Dist: build>=1.2 ; extra == "dev"
24
+ Requires-Dist: coverage>=7.6 ; extra == "dev"
25
+ Requires-Dist: flit_core>=3.10 ; extra == "dev"
26
+ Requires-Dist: hypothesis>=6.100 ; extra == "dev"
27
+ Requires-Dist: pre-commit>=4 ; extra == "dev"
28
+ Requires-Dist: pytest>=8 ; extra == "dev"
29
+ Requires-Dist: pytest-cov>=6 ; extra == "dev"
30
+ Requires-Dist: ruff>=0.9 ; extra == "dev"
31
+ Requires-Dist: twine>=6 ; extra == "dev"
32
+ Project-URL: Issues, https://github.com/bitplane/xfh/issues
33
+ Project-URL: Repository, https://github.com/bitplane/xfh
34
+ Provides-Extra: dev
35
+
36
+ # xfh
37
+
38
+ `xfh` is a pure-Python data-recovery library and command-line tool for files
39
+ compressed with the Amiga XPK system, including files written transparently
40
+ through DiskExpander.
41
+
42
+ The project is under active format-recovery work. It currently supports the
43
+ later `XPKF` container, the DiskExpander 2.1 wrapper, and 41 codec identifiers
44
+ in pure Python. Historical compatibility identifiers `CBR1` (CBR0) and `FRHT`
45
+ (RAKE) are accepted as exact stream-format aliases. See the
46
+ [codec support table](https://github.com/bitplane/xfh/blob/master/docs/codec-support.md)
47
+ for the evidence and limitations behind each codec.
48
+
49
+ ## Installation
50
+
51
+ `xfh` requires Python 3.10 or newer and has no runtime dependencies. After the
52
+ first release is published to PyPI, install it with:
53
+
54
+ ```console
55
+ python -m pip install xfh
56
+ ```
57
+
58
+ Until then, install a checkout directly:
59
+
60
+ ```console
61
+ python -m pip install .
62
+ ```
63
+
64
+ ## Quick start
65
+
66
+ Inspect a stream before writing anything, strictly verify it, and then unpack
67
+ it:
68
+
69
+ ```console
70
+ xfh info packed-file
71
+ xfh verify packed-file
72
+ xfh unpack packed-file recovered-file
73
+ ```
74
+
75
+ Existing output files are not overwritten unless `--force` is supplied.
76
+ Decompression is limited to 256 MiB by default; use `--max-output-size` to set
77
+ an appropriate byte limit for a known larger file.
78
+
79
+ For a damaged stream, salvage mode writes only the prefix decoded before the
80
+ first failing chunk. Its exit status is `4` when recovery is incomplete:
81
+
82
+ ```console
83
+ xfh unpack damaged-file recovered-prefix --salvage --report recovery.json
84
+ ```
85
+
86
+ The public Python API accepts bytes-like input:
87
+
88
+ ```python
89
+ from pathlib import Path
90
+
91
+ import xfh
92
+
93
+ packed = Path("packed-file").read_bytes()
94
+ info = xfh.inspect(packed)
95
+ plain = xfh.decompress(packed, limits=xfh.Limits(max_output_size=512 * 1024 * 1024))
96
+ Path("recovered-file").write_bytes(plain)
97
+
98
+ print(info.codec, len(plain))
99
+ ```
100
+
101
+ Use `xfh.decompress_file()` when writing to disk: it uses an atomic destination
102
+ write and refuses to replace an existing file by default.
103
+
104
+ ## Recovery and stability
105
+
106
+ Strict decoding validates the container, declared sizes, checksums, chunk
107
+ output sizes, and initial-byte prefix where the format provides them. It either
108
+ returns the complete declared output or raises an `xfh.XfhError` subclass.
109
+ Password-protected streams are detected but not decoded.
110
+
111
+ Salvage is deliberately opt-in. Its output can be useful evidence, but an
112
+ incomplete result is not proof that every returned byte represents an intact
113
+ original file. Keep the source media and packed files unchanged, work on
114
+ copies, review the recovery report, and validate recovered data with tools
115
+ specific to its file type.
116
+
117
+ This is alpha software reconstructed from historical implementations and
118
+ surviving samples. Codec support varies in strength from headless-UAE packer
119
+ fixtures to source-derived decoder tests; the support table records that
120
+ distinction. The API and command-line interface may change before 1.0.
121
+
122
+ ## Development
123
+
124
+ Development follows the template workflow:
125
+
126
+ ```console
127
+ make dev
128
+ make test
129
+ make coverage
130
+ make lint
131
+ make dist
132
+ ```
133
+
134
+ See [the original investigation](https://github.com/bitplane/xfh/blob/master/docs/research/initial-findings.md)
135
+ for the provenance of the project and
136
+ [the format notes](https://github.com/bitplane/xfh/blob/master/docs/format.md)
137
+ for current support boundaries. The
138
+ [oracle matrix](https://github.com/bitplane/xfh/blob/master/docs/oracle-matrix.md)
139
+ records results from the isolated original Amiga implementation.
140
+
xfh-0.1.0/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # xfh
2
+
3
+ `xfh` is a pure-Python data-recovery library and command-line tool for files
4
+ compressed with the Amiga XPK system, including files written transparently
5
+ through DiskExpander.
6
+
7
+ The project is under active format-recovery work. It currently supports the
8
+ later `XPKF` container, the DiskExpander 2.1 wrapper, and 41 codec identifiers
9
+ in pure Python. Historical compatibility identifiers `CBR1` (CBR0) and `FRHT`
10
+ (RAKE) are accepted as exact stream-format aliases. See the
11
+ [codec support table](https://github.com/bitplane/xfh/blob/master/docs/codec-support.md)
12
+ for the evidence and limitations behind each codec.
13
+
14
+ ## Installation
15
+
16
+ `xfh` requires Python 3.10 or newer and has no runtime dependencies. After the
17
+ first release is published to PyPI, install it with:
18
+
19
+ ```console
20
+ python -m pip install xfh
21
+ ```
22
+
23
+ Until then, install a checkout directly:
24
+
25
+ ```console
26
+ python -m pip install .
27
+ ```
28
+
29
+ ## Quick start
30
+
31
+ Inspect a stream before writing anything, strictly verify it, and then unpack
32
+ it:
33
+
34
+ ```console
35
+ xfh info packed-file
36
+ xfh verify packed-file
37
+ xfh unpack packed-file recovered-file
38
+ ```
39
+
40
+ Existing output files are not overwritten unless `--force` is supplied.
41
+ Decompression is limited to 256 MiB by default; use `--max-output-size` to set
42
+ an appropriate byte limit for a known larger file.
43
+
44
+ For a damaged stream, salvage mode writes only the prefix decoded before the
45
+ first failing chunk. Its exit status is `4` when recovery is incomplete:
46
+
47
+ ```console
48
+ xfh unpack damaged-file recovered-prefix --salvage --report recovery.json
49
+ ```
50
+
51
+ The public Python API accepts bytes-like input:
52
+
53
+ ```python
54
+ from pathlib import Path
55
+
56
+ import xfh
57
+
58
+ packed = Path("packed-file").read_bytes()
59
+ info = xfh.inspect(packed)
60
+ plain = xfh.decompress(packed, limits=xfh.Limits(max_output_size=512 * 1024 * 1024))
61
+ Path("recovered-file").write_bytes(plain)
62
+
63
+ print(info.codec, len(plain))
64
+ ```
65
+
66
+ Use `xfh.decompress_file()` when writing to disk: it uses an atomic destination
67
+ write and refuses to replace an existing file by default.
68
+
69
+ ## Recovery and stability
70
+
71
+ Strict decoding validates the container, declared sizes, checksums, chunk
72
+ output sizes, and initial-byte prefix where the format provides them. It either
73
+ returns the complete declared output or raises an `xfh.XfhError` subclass.
74
+ Password-protected streams are detected but not decoded.
75
+
76
+ Salvage is deliberately opt-in. Its output can be useful evidence, but an
77
+ incomplete result is not proof that every returned byte represents an intact
78
+ original file. Keep the source media and packed files unchanged, work on
79
+ copies, review the recovery report, and validate recovered data with tools
80
+ specific to its file type.
81
+
82
+ This is alpha software reconstructed from historical implementations and
83
+ surviving samples. Codec support varies in strength from headless-UAE packer
84
+ fixtures to source-derived decoder tests; the support table records that
85
+ distinction. The API and command-line interface may change before 1.0.
86
+
87
+ ## Development
88
+
89
+ Development follows the template workflow:
90
+
91
+ ```console
92
+ make dev
93
+ make test
94
+ make coverage
95
+ make lint
96
+ make dist
97
+ ```
98
+
99
+ See [the original investigation](https://github.com/bitplane/xfh/blob/master/docs/research/initial-findings.md)
100
+ for the provenance of the project and
101
+ [the format notes](https://github.com/bitplane/xfh/blob/master/docs/format.md)
102
+ for current support boundaries. The
103
+ [oracle matrix](https://github.com/bitplane/xfh/blob/master/docs/oracle-matrix.md)
104
+ records results from the isolated original Amiga implementation.
@@ -0,0 +1,32 @@
1
+ # Codec support and evidence
2
+
3
+ `xfh` currently recognizes 41 XPK identifiers. “Amiga pack” means at least one
4
+ fixture was created and unpacked by the original Amiga libraries under
5
+ headless UAE, then decoded byte-exactly by Python. “Derived” means the decoder
6
+ is covered by source-derived or transformed fixtures but an original packer
7
+ artifact is not yet available.
8
+
9
+ | Evidence | Codecs |
10
+ | --- | --- |
11
+ | Original Amiga pack/unpack | ACCA, BLZW, BZP2, CBR0, DLTA, DUKE, FAST, FBR2, FRLE, GZIP, HFMN, HUFF, ILZR, IMPL, LHLB, LZBS, LZW2, LZW3, LZW4, LZW5, MASH, NONE, NUKE, RAKE, RDCN, RLEN, SDHC, SHRI, SLZ3, SQSH, ZENO |
12
+ | Exact historical aliases | CBR1, FRHT |
13
+ | Derived or transformed | ARTM, CRM2, CRMS, CYB2, PWPK, SHR3, SMPL, TDCS |
14
+
15
+ The important remaining evidence gaps are:
16
+
17
+ - CYB2 needs a genuine file created through XpkCybPrefs; its standalone
18
+ library does not expose packing modes and rejects hand-built containers.
19
+ - TDCS needs an original library or authentic packed sample.
20
+ - ARTM and SHR3 need preserved original libraries.
21
+ - SMPL’s original packer selected raw chunks for every tested vector, so its
22
+ compressed-stream test remains source-derived.
23
+
24
+ PWPK has a positive stream produced by the original Amiga PPMC tool and
25
+ cross-checked with Ancient. CRM2 and CRMS have positive streams generated and
26
+ decoded by an independent Crunch-Mania implementation. They remain in the
27
+ derived row because the committed XPK envelopes were constructed outside the
28
+ original XPK master library.
29
+
30
+ The preservation archive contains further codecs that are not implemented
31
+ yet. Wrapper/backend formats are being prioritized before encryption,
32
+ authentication, and preference-system formats.
@@ -0,0 +1,35 @@
1
+ # Format notes
2
+
3
+ ## XPKF
4
+
5
+ `xfh` validates the 36-byte global header, optional extended header, short and
6
+ long chunk headers, header XORs, payload XORs, four-byte alignment, declared
7
+ sizes, and the terminal chunk before decoding. Integers are big-endian.
8
+
9
+ ## DiskExpander 2.1 stream
10
+
11
+ The recovered files begin with `01 80 63 68 05 61 01 0a`, followed by the
12
+ unpacked size, a packed-length field (measured from byte 8), and an eight-byte
13
+ NUL-padded sublibrary name such as `xpkNUKE`. It is not a later `XPKF`
14
+ stream.
15
+
16
+ The second length is the absolute offset of a chunk-boundary table at the end
17
+ of the file. The table is a sequence of big-endian 32-bit offsets. It begins
18
+ with 24, ends with its own file offset, and therefore describes every chunk as
19
+ a pair of adjacent boundaries. Each chunk starts with a big-endian 16-bit
20
+ compressed-stream length followed by exactly that many codec bytes.
21
+ The length value `0xffff` is a raw-block sentinel; in that case exactly
22
+ 10,000 uncompressed bytes follow.
23
+
24
+ DiskExpander's `Block 10` setting divides unpacked data into decimal 10,000-byte
25
+ chunks; the final chunk contains the remainder. Its bundled `xpkNUKE.library`
26
+ identifies itself as NUKE 1.0 and is byte-identical to the public XPK 2.4-era
27
+ library. Passing only the bytes after each two-byte chunk length to the NUKE
28
+ decoder recovers the known 130-byte `Cancel.gads` sample to its 204-byte
29
+ plaintext exactly.
30
+
31
+ The identification comes from the recovered startup command
32
+ `DiskExpander ... Compressor NUKE Block 10 Table 1000 ...`, the `$VER:
33
+ DiskExpander 2.1` strings in its executable and handlers, and matching wrapper
34
+ magic in those handlers. It is distinct from XFH and from the later `XPKF`
35
+ container.
@@ -0,0 +1,121 @@
1
+ # Original-code oracle matrix
2
+
3
+ The isolated oracle uses FS-UAE 3.2.35, Workbench 3.1, xpkmaster 4.16
4
+ (21-Aug-1997), and recovered compressor libraries. ROMs, Amiga binaries,
5
+ private files, logs, and the complete workspace are not distributed.
6
+
7
+ `xQuery` reported these mode ranges:
8
+
9
+ | Codec | Default | Ranges |
10
+ | --- | ---: | --- |
11
+ | NONE | 50 | 0–100 |
12
+ | NUKE | 50 | 0–100 |
13
+ | FAST | 50 | 0–79, 80–100 |
14
+ | RAKE | 100 | 0–25, 26–50, 51–75, 76–100 |
15
+ | HUFF | 50 | 0–100 |
16
+ | SHRI | 100 | 0–14, 15–28, 29–42, 43–56, 57–70, 71–84, 85–100 |
17
+ | DLTA | 100 | 0–100 |
18
+ | SMPL | 50 | 0–100 |
19
+ | HFMN | 0 | 0–100 |
20
+ | MASH | 100 | 0–100 |
21
+ | SQSH | 100 | 0–100 |
22
+ | LZBS | 100 | 0, 1–10, 11–20, 21–30, 31–40, 41–50, 51–60, 61–70, 71–80, 81–90, 91–100 |
23
+ | SLZ3 | 100 | 0–100 |
24
+ | SDHC | 50 | 0–7, 8–15, 16–23, 24–31, 32–39, 40–47, 48–55, 56–63, 64–71, 72–79, 80–87, 88–100 |
25
+ | LHLB | 100 | 0–100 |
26
+ | BZP2 | 40 | 0–19, 20–29, 30–39, 40–49, 50–59, 60–69, 70–79, 80–89, 90–100 |
27
+ | GZIP | 65 | 0–9, 10–19, 20–29, 30–39, 40–49, 50–59, 60–69, 70–79, 80–89, 90–100 |
28
+ | IMPL | 100 | 0–10, 11–30, 31–50, 51–75, 76–98, 99–100 |
29
+
30
+ The matrix exercised every numeric mode on a canonical text vector and every
31
+ range boundary/default on deterministic pattern and size vectors:
32
+
33
+ - 990 byte-exact pack/unpack successes;
34
+ - 36 empty-input cases exposing an original-tool defect: a 36-byte XPKF header
35
+ is emitted, then rejected by the same master library;
36
+ - one HUFF case which hangs after reporting successful compression, followed
37
+ by 11 deliberately unrun HUFF cases;
38
+ - zero round-trip mismatches among completed non-empty cases.
39
+
40
+ All 1,026 emitted containers begin with `XPKF`. Small representative fixtures
41
+ for all six codecs are committed as reviewable hexadecimal files and validate
42
+ the current pure-Python implementations.
43
+
44
+ A second isolated run added `CBR0`, `RLEN`, `FRLE`, `RDCN`, `BLZW`, and
45
+ `DUKE`. Across the completed non-empty cases, all 372 original packed/plain
46
+ artifacts decode byte-exactly in Python. Compact acceptance containers for
47
+ each codec are committed as hexadecimal fixtures. The historical packers are
48
+ run in separate or short-lived emulator sessions because repeated use can
49
+ crash the original Amiga process; this is an oracle limitation rather than an
50
+ accepted decoder failure.
51
+
52
+ A third set covers `DLTA`, `SMPL`, `HFMN`, `MASH`, and `SQSH`. Short isolated
53
+ runs produced 35 byte-exact original pack/unpack artifacts across modes 0,
54
+ default, and 100. Python directly decoded all 20 generated compressed DLTA,
55
+ HFMN, MASH, and SQSH containers. The SMPL packer selected raw XPK chunks for
56
+ all 15 vectors, so its decoder additionally has a source-derived synthetic
57
+ prefix-code test rather than claiming compressed oracle coverage. DLTA hangs
58
+ on a one-byte input and HFMN hangs on one broad byte-pattern input; those
59
+ original-packer defects are excluded from the safe fixture stage.
60
+
61
+ Every compact committed acceptance container is recorded in the public
62
+ manifest with its input, output, size, and SHA-256 hashes. Known hanging
63
+ packer/vector pairs are machine-readable exclusions and are never emitted
64
+ into a guest stage. `oracle.py verify-python` performs the direct comparison
65
+ and records its results as JSON.
66
+
67
+ The CyberYAFA `LZW2`–`LZW5` variants are now backed by preserved original
68
+ libraries and headless UAE fixtures across every reported mode boundary.
69
+ `SHR3` remains source-derived because no corresponding original library has
70
+ been located; it is additionally tested by converting both packed chunks from
71
+ the 64 KiB SHRI continuation fixture to its headerless chunk representation.
72
+
73
+ Preserved original `ACCA`, `FBR2`, `ILZR`, and `ZENO` libraries were also
74
+ located and exercised under UAE. Together with the LZW family, 157 generated
75
+ artifacts round-trip in the original master library and decode byte-exactly in
76
+ Python. Compact fixtures, private library hashes, and the preservation archive
77
+ hash are linked in the public manifest; the binaries themselves are not
78
+ redistributed. `ARTM` remains source-derived because its original library has
79
+ not been located. Compact source-derived XPK containers for these codecs were
80
+ also independently compared byte-for-byte with Ancient 2.3.0 where Ancient
81
+ supports the emitted variant.
82
+
83
+ The next batch adds `LZBS`, `SLZ3`, `TDCS`, `LHLB`, `SDHC`, and `CYB2`.
84
+ Preserved `LZBS`, `SLZ3`, and `SDHC` packers produced 83 completed artifacts
85
+ across every reported mode boundary; Python decoded all of them byte-exactly.
86
+ Low-mode LZBS output also confirmed that its original depacker stops a final
87
+ literal run at the declared output size even when the packer rounds that run
88
+ up. Representative original-Amiga fixtures for all three codecs are committed.
89
+
90
+ `LHLB` initially appeared unpack-only because its required `lh.library` was
91
+ missing. Installing the original Aminet dependency exposes its 0–100 packing
92
+ range; a mode-100 artifact now round-trips in the Amiga master library and
93
+ decodes byte-exactly in Python. The dependency hash is recorded in the public
94
+ manifest, but the binary is not redistributed.
95
+
96
+ `CYB2` still reports no packing modes. Its preserved library also requires
97
+ `xpkcybhandle.library`; with that dependency and its `CYB1` companion installed,
98
+ the original library continues to reject source-constructed CYB2 containers
99
+ before unpacking. Consequently CYB2 remains source-derived rather than being
100
+ mislabelled as original-Amiga verified. `TDCS` has no library in the
101
+ preservation archive. Both retain synthetic success and malformed-stream
102
+ coverage.
103
+
104
+ Preserved BZP2, GZIP, and IMPL packers generated representative compressed
105
+ fixtures that round-trip under headless UAE and decode byte-exactly in Python.
106
+ The complete boundary/default matrix now contains 36 BZP2, 42 GZIP, 24 IMPL,
107
+ and six LHLB byte-exact successes with no Python mismatches.
108
+
109
+ PWPK, CRM2, and CRMS expose no packing modes in the preserved XPK libraries.
110
+ An original Amiga PPMC utility generated the positive PowerPacker stream used
111
+ by the PWPK fixture, which was also checked with Ancient. CRM2 and CRMS use
112
+ positive streams generated and decoded by an independent Crunch-Mania
113
+ implementation. Their XPK envelopes remain source-derived and are labelled
114
+ accordingly.
115
+
116
+ FS-UAE runs behind Xvfb with Mesa software rendering, so oracle generation
117
+ does not map a window onto the host desktop.
118
+
119
+ The recovered private wrapper beginning `01 80 63 68 05 61 01 0a` was not
120
+ recognized as packed data by the original master library. That result rules
121
+ out using generated XPKF files to infer its outer framing.
@@ -0,0 +1,77 @@
1
+ # Release checklist
2
+
3
+ This checklist covers an `xfh` release. CI modernization is intentionally
4
+ deferred to `example-python-project` so it can be back-ported consistently
5
+ across repositories.
6
+
7
+ ## Prepare
8
+
9
+ - Confirm the intended version and supported Python versions.
10
+ - Confirm the `xfh` distribution name is still available on PyPI. A missing
11
+ project page does not reserve a name; only a successful upload does.
12
+ - Create the PyPI project/account configuration, enable two-factor
13
+ authentication, and prefer a narrowly scoped trusted publisher or project
14
+ token.
15
+ - Update `project.version` in `pyproject.toml`.
16
+ - Check that the codec support table and recovery limitations match the code.
17
+ - Review `git diff`, `git status`, and the commits included since the previous
18
+ release.
19
+
20
+ ## Validate
21
+
22
+ Run the repository checks from a clean checkout:
23
+
24
+ ```console
25
+ make coverage
26
+ make lint
27
+ make dist
28
+ ```
29
+
30
+ Then verify the built artifacts rather than the source checkout:
31
+
32
+ ```console
33
+ python -m venv ~/tmp/xfh-release-venv
34
+ ~/tmp/xfh-release-venv/bin/python -m pip install dist/xfh-*.whl
35
+ ~/tmp/xfh-release-venv/bin/xfh --help
36
+ ~/tmp/xfh-release-venv/bin/python -c "import xfh; print(xfh.__file__)"
37
+ ```
38
+
39
+ - Inspect the wheel and source archive file lists for missing or private data.
40
+ - Confirm `twine check dist/*` passes.
41
+ - Exercise `info`, `verify`, and `unpack` against a known fixture with the
42
+ installed wheel.
43
+ - Confirm strict failure, output-size limiting, overwrite refusal, and salvage
44
+ exit status/reporting still behave as documented.
45
+
46
+ ## Publish
47
+
48
+ - Upload to TestPyPI first when changing packaging or publishing credentials.
49
+ - Install the exact TestPyPI artifact in a fresh environment and repeat the
50
+ smoke test.
51
+ - Tag the reviewed release commit with the exact project version, such as
52
+ `0.1.0`, and push the tag.
53
+ - Upload exactly the already-reviewed artifacts to PyPI; do not rebuild between
54
+ TestPyPI and PyPI.
55
+ - Create a GitHub release from the tag with concise release notes.
56
+
57
+ The first PyPI upload bootstraps the project and therefore needs a temporary
58
+ account-wide token:
59
+
60
+ ```console
61
+ read -rsp "PyPI token: " PYPI_TOKEN && echo
62
+ export PYPI_TOKEN
63
+ make release
64
+ unset PYPI_TOKEN
65
+ ```
66
+
67
+ After that upload, replace it with a token scoped to the new `xfh` project.
68
+ For a TestPyPI upload, additionally set `PYPI_REPOSITORY_URL` to the TestPyPI
69
+ legacy upload endpoint.
70
+
71
+ ## Verify
72
+
73
+ - Confirm the PyPI metadata, project links, license, Python requirement, wheel,
74
+ and source archive render correctly.
75
+ - Install from PyPI in a fresh environment and repeat the fixture smoke test.
76
+ - Confirm the GitHub tag and release point to the published source.
77
+ - Announce material recovery limitations alongside the release.
@@ -0,0 +1,107 @@
1
+ # Initial XFH / xpkNUKE recovery notes
2
+
3
+ ## The job
4
+
5
+ `~/Documents/amiga/amiga.ddr.img` is a 3-partition Amiga drive image. Its
6
+ third partition, **dh2 `SHITE`**, ran **XFH** — the AmigaOS *xpk File
7
+ Handler*, a DOS handler that transparently compresses everything written to
8
+ a directory or partition via `xpkmaster.library`. **2,356 of its 5,276
9
+ files** are compressed with the **xpkNUKE** sublibrary and cannot currently
10
+ be read.
11
+
12
+ | partition | sector range | crunched | total |
13
+ |---|---|---|---|
14
+ | dh1 `WORKBENCH` | 1536 + 82944 | 1 | 4,459 |
15
+ | dh0 `WORK` | 84480 + 411648 | 0 | 7,433 |
16
+ | **dh2 `SHITE`** | **496128 + 217344** | **2,356** | **5,276** |
17
+
18
+ Goal: read them. The headline prize is `kaos/shit/` — 63 files of digitised
19
+ animation frames from 1991–1995 by Matt Benson, Dave Gary, James Benson and
20
+ John Hayes, including a claymation wombat with six arms, one of which falls
21
+ off mid-animation. But it is 2,356 files in total, across `Other`, `Games`,
22
+ `t`, `GadToolsBox`, `Legion`, `GMS` and more.
23
+
24
+ ## Why it is XFH and not someone crunching files
25
+
26
+ - **252 crunched files in `t/`** — the temp directory. Nobody hand-crunches
27
+ temp files.
28
+ - `.info` icons of ~300 bytes are crunched, where compression is pointless.
29
+ - It spans a dozen unrelated drawers, and is confined to one partition.
30
+
31
+ No `xpkmaster.library` and no XFH handler survive on the drive (only
32
+ `xfdmaster`, bundled with virus checkers, which is the unrelated *decrunch*
33
+ library). So the handler binary is not available to disassemble.
34
+
35
+ ## The container, as far as it is decoded
36
+
37
+ Verified byte-exactly against `samples/matt1.info` (310 bytes on disk):
38
+
39
+ ```
40
+ +0 01 80 63 68 05 61 01 0a constant magic, identical in every file
41
+ +8 ULONG unpacked length (460 for matt1.info)
42
+ +12 ULONG length of everything from +8 onward
43
+ (302; and 302 + 8 = 310 = file size)
44
+ +16 "xpkNUKE\0" 8 bytes, NUL-padded
45
+ +24 compressed data
46
+ ```
47
+
48
+ Sample sizes:
49
+
50
+ | file | on disk | unpacked (from +8) |
51
+ |---|---|---|
52
+ | `matt1.info` | 310 | 460 |
53
+ | `wombat1.pic` | 12,492 | 19,844 |
54
+ | `wombat` | 112,134 | 122,780 |
55
+ | `wombat2` | 190,746 | 206,006 |
56
+
57
+ **Unsolved:** for `wombat` the arithmetic leaves 560 bytes unaccounted for
58
+ (file − 8 − declared length). 560 = 35 × 16, and an XPK chunk header is 16
59
+ bytes, so the payload is probably chunked with per-chunk headers. The small
60
+ single-chunk files fit the simple layout; the large ones do not.
61
+
62
+ ## What has been ruled out
63
+
64
+ **`ancient` (temisu, `apt install ancient`, installed) does NOT read these.**
65
+ It implements xpkNUKE correctly, but expects an `XPKF` *stream*. These are
66
+ XFH's container. Keep it installed anyway — it handles DMS and LhA, which
67
+ the corpus is full of.
68
+
69
+ `tools/try_xpkf.py` wraps the payload from +24 in a synthetic `XPKF` header
70
+ and brute-forces all 256 header-checksum values, looking for one `ancient`
71
+ will accept. It finds none, so the payload is not a bare XPK chunk stream —
72
+ or the assumed 36-byte `XPKF` header layout is wrong. Worth retrying with
73
+ other header lengths and with reconstructed chunk headers.
74
+
75
+ ## The lever: known plaintext, in quantity
76
+
77
+ **382 filenames exist both crunched on dh2 and uncrunched on dh0/dh1.**
78
+ Pick one whose uncrunched size matches the crunched file's declared unpacked
79
+ length and you have a verified plaintext/ciphertext pair to develop and
80
+ check against. Reproduce with:
81
+
82
+ ```bash
83
+ # tools/amigapeek.ts <image> <path-filter> <first-sector> <blocks>
84
+ # prints "size path <first 24 bytes as text>" per file
85
+ npx tsx tools/amigapeek.ts ~/Documents/amiga/amiga.ddr.img "" 496128 217344 > peek-dh2.txt
86
+ npx tsx tools/amigapeek.ts ~/Documents/amiga/amiga.ddr.img "" 84480 411648 > peek-dh0.txt
87
+ grep xpkNUKE peek-dh2.txt | awk '{print $2}' | awk -F/ '{print $NF}' | LC_ALL=C sort -u > c.txt
88
+ grep -v xpkNUKE peek-dh0.txt | awk '{print $2}' | awk -F/ '{print $NF}' | LC_ALL=C sort -u > u.txt
89
+ LC_ALL=C comm -12 c.txt u.txt
90
+ ```
91
+
92
+ ## Tools here
93
+
94
+ Run from `~/src/tmp/amos/amos-ts` (they import from its `src/`):
95
+
96
+ - `tools/amigaget.ts <img> <path> <out> [sector] [blocks]` — extract one file
97
+ - `tools/amigapeek.ts <img> <filter> [sector] [blocks]` — list files + first bytes
98
+ - `tools/amigadates.ts <img> [--sector N] [--blocks N]` — real AmigaDOS
99
+ datestamps, tick precision
100
+ - `tools/abk2png.ts <file.abk> <prefix>` — render an AMOS sprite/icon bank
101
+ (handles the `AmBs` multi-bank wrapper)
102
+ - `tools/try_xpkf.py <file>` — the failed XPKF-wrapping experiment
103
+
104
+ ## Where this is written up
105
+
106
+ `~/src/tmp/amos/dating-amos-games.md`, section "Blocked: dh2 ran an XPK
107
+ filesystem packer".