diskstack 1.0.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.
- diskstack-1.0.0/CHANGELOG.md +62 -0
- diskstack-1.0.0/LICENSE +21 -0
- diskstack-1.0.0/MANIFEST.in +10 -0
- diskstack-1.0.0/NOTICE +55 -0
- diskstack-1.0.0/PKG-INFO +329 -0
- diskstack-1.0.0/README.md +294 -0
- diskstack-1.0.0/diskstack/__init__.py +5 -0
- diskstack-1.0.0/diskstack/_vendor/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/COPYING +24 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/__init__.py +2 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/amiga/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/amiga/amigados.py +221 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/apple2/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/apple2/apple2_gcr.py +223 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/bitcell.py +117 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/codec.py +362 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/commodore/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/commodore/c64_gcr.py +229 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/datageneral/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/datageneral/datageneral.py +211 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/hp/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/hp/hp_mmfm.py +258 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/ibm/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/ibm/ibm.py +1005 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/macintosh/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/macintosh/mac_gcr.py +238 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/micropolis/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/micropolis/micropolis.py +233 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/northstar/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/codec/northstar/northstar.py +210 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs.cfg +81 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_acorn.cfg +119 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_akai.cfg +27 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_amiga.cfg +17 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_apple2.cfg +31 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_apricot.cfg +25 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_atari.cfg +33 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_atarist.cfg +76 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_coco.cfg +98 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_commodore.cfg +127 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_datageneral.cfg +8 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_dec.cfg +37 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_dragon.cfg +61 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_eagle.cfg +25 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_ensoniq.cfg +39 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_epson.cfg +95 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_gem.cfg +15 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_hp.cfg +25 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_ibm.cfg +140 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_kaypro.cfg +58 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_luxor.cfg +96 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_mac.cfg +71 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_micropolis.cfg +73 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_mm1.cfg +69 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_msx.cfg +45 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_northstar.cfg +37 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_occ1.cfg +23 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_olivetti.cfg +18 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_pc98.cfg +69 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_raw.cfg +25 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_rm.cfg +98 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_sci.cfg +15 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_sega.cfg +13 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_sharp.cfg +13 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_thomson.cfg +78 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_tsc.cfg +81 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_xerox.cfg +59 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/data/diskdefs_zx.cfg +245 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/error.py +19 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/flux.py +315 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/a2r.py +133 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/acorn.py +28 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/adf.py +15 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/apple2.py +18 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/apridisk.py +107 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/caps.py +464 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/d64.py +51 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/d81.py +28 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/d88.py +211 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/dcp.py +45 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/dim.py +53 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/dmk.py +166 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/dsk.py +31 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/edsk.py +591 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/fd.py +23 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/fdi.py +41 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/hdm.py +15 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/hfe.py +742 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/image.py +130 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/imd.py +201 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/img.py +111 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/kryoflux.py +304 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/mgt.py +15 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/msa.py +153 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/nfd.py +113 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/nsi.py +47 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/scp.py +471 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/sf7.py +15 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/sharp2d.py +15 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/td0.py +141 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/image/xdf.py +15 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/optimised/__init__.py +22 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/tools/__init__.py +0 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/tools/util.py +133 -0
- diskstack-1.0.0/diskstack/_vendor/greaseweazle/track.py +494 -0
- diskstack-1.0.0/diskstack/cache.py +210 -0
- diskstack-1.0.0/diskstack/candidates.py +396 -0
- diskstack-1.0.0/diskstack/cli.py +348 -0
- diskstack-1.0.0/diskstack/errors.py +5 -0
- diskstack-1.0.0/diskstack/filler.py +29 -0
- diskstack-1.0.0/diskstack/formats.py +381 -0
- diskstack-1.0.0/diskstack/parallel.py +73 -0
- diskstack-1.0.0/diskstack/report.py +353 -0
- diskstack-1.0.0/diskstack/stack.py +317 -0
- diskstack-1.0.0/diskstack.egg-info/PKG-INFO +329 -0
- diskstack-1.0.0/diskstack.egg-info/SOURCES.txt +139 -0
- diskstack-1.0.0/diskstack.egg-info/dependency_links.txt +1 -0
- diskstack-1.0.0/diskstack.egg-info/entry_points.txt +2 -0
- diskstack-1.0.0/diskstack.egg-info/requires.txt +10 -0
- diskstack-1.0.0/diskstack.egg-info/top_level.txt +1 -0
- diskstack-1.0.0/examples/README.md +9 -0
- diskstack-1.0.0/examples/console-session.txt +49 -0
- diskstack-1.0.0/examples/diskstack-report.json +28489 -0
- diskstack-1.0.0/pyproject.toml +65 -0
- diskstack-1.0.0/setup.cfg +4 -0
- diskstack-1.0.0/tests/conftest.py +70 -0
- diskstack-1.0.0/tests/fixtures/make_fixtures.py +194 -0
- diskstack-1.0.0/tests/test_amiga.py +154 -0
- diskstack-1.0.0/tests/test_cache.py +289 -0
- diskstack-1.0.0/tests/test_cli.py +296 -0
- diskstack-1.0.0/tests/test_filler.py +83 -0
- diskstack-1.0.0/tests/test_images.py +169 -0
- diskstack-1.0.0/tests/test_parallel.py +38 -0
- diskstack-1.0.0/tests/test_report.py +238 -0
- diskstack-1.0.0/tests/test_scp_flux.py +54 -0
- diskstack-1.0.0/tests/test_stacking.py +81 -0
- diskstack-1.0.0/tests/test_vote.py +217 -0
- diskstack-1.0.0/tools/vendor.py +213 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 - 2026-09-12
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- Merges two or more dumps of one floppy disk into a single sector image.
|
|
8
|
+
- Reads flux (`.scp`, KryoFlux `.raw` streams, `.hfe`) and sector images
|
|
9
|
+
(`.img`, `.ima`, `.st`, `.adf`, `.imd`), in any mix.
|
|
10
|
+
- Decodes every revolution of every flux capture, not just the first two.
|
|
11
|
+
- Groups read attempts by the decoded `(cylinder, head, sector id)`, so
|
|
12
|
+
captures with different index phases and motor speeds still line up.
|
|
13
|
+
- Three tiers per sector: a passing CRC wins, else the attempts are rebuilt
|
|
14
|
+
into candidate payloads that have to satisfy a check value off the disk,
|
|
15
|
+
else unresolved.
|
|
16
|
+
- Three reconstruction routes, reported per sector as `method`: a byte-wise
|
|
17
|
+
majority, a majority of one payload per input file so a capture with more
|
|
18
|
+
revolutions cannot outvote the rest, and each read on its own, which
|
|
19
|
+
recovers a sector whose CRC bytes rather than payload were damaged.
|
|
20
|
+
- Treats Greaseweazle's `-=[BAD SECTOR]=-` filler as a failed read.
|
|
21
|
+
- Prints a `gw read --tracks=` command naming only the tracks still unresolved.
|
|
22
|
+
- Writes `diskstack-report.json` with per-sector provenance, schema 1.
|
|
23
|
+
- IBM FM/MFM and AmigaDOS MFM formats, auto-detected.
|
|
24
|
+
- Decodes flux one track per core, `-j/--jobs` to change it, falling back to a
|
|
25
|
+
single process if the worker pool cannot start.
|
|
26
|
+
- Flags a sector as unstable when one capture read it two different ways across
|
|
27
|
+
its own revolutions, and says so beside the re-read command, because weak
|
|
28
|
+
bits do not settle down however many more passes you make.
|
|
29
|
+
- Refuses to write the merged image or the report over one of the inputs.
|
|
30
|
+
- Warns when a sector image is not the size the chosen format implies, since
|
|
31
|
+
the extra sectors get ignored and a missing tail counts as unread.
|
|
32
|
+
- Takes only the sectors a truncated `.img` or `.adf` actually holds. The
|
|
33
|
+
readers pad a short image to the format length and mark what they invented
|
|
34
|
+
CRC-clean, which would otherwise win the merge with zeroes.
|
|
35
|
+
- Rejects an output path it cannot write before reading any input rather than
|
|
36
|
+
after decoding all of them.
|
|
37
|
+
- Calls a sector verified only when some read of it satisfied a check value off
|
|
38
|
+
the disk, and ends a merge of sector images by saying nothing confirmed them
|
|
39
|
+
instead of claiming a CRC did.
|
|
40
|
+
- Flags a sector contested when two dumps both read it cleanly and disagreed,
|
|
41
|
+
keeps the best-ranked dump's copy, and names those sectors on stdout, since
|
|
42
|
+
nothing in a sector image can arbitrate between them.
|
|
43
|
+
- Applies a repeated `--pll` to every flux container. KryoFlux streams and HFE
|
|
44
|
+
images previously took the first setting and ignored the rest.
|
|
45
|
+
- Refuses a report path equal to the merged image.
|
|
46
|
+
- Counts a KryoFlux input as the whole stream set rather than the single file
|
|
47
|
+
named on the command line.
|
|
48
|
+
- Compares each run against the report it replaces and says how many sectors
|
|
49
|
+
the new capture recovered, or that nothing moved, which is the loop's stop
|
|
50
|
+
condition. Also in the report as `since_last_report`.
|
|
51
|
+
- Stops calling a sector unstable when a repeated `--pll` read one revolution
|
|
52
|
+
two ways. Weak bits are revolutions disagreeing with each other, not two
|
|
53
|
+
decodes of the same revolution.
|
|
54
|
+
- Rejects an input it cannot open, including a KryoFlux name that does not fit
|
|
55
|
+
the per-track pattern, before decoding any of the others.
|
|
56
|
+
- Measures a KryoFlux set by the track files the reader will open, so a second
|
|
57
|
+
capture sitting in the same directory is not counted in.
|
|
58
|
+
- Keeps decoded flux and the detected format in `.diskstack-cache` beside the
|
|
59
|
+
output, so adding a capture to the stack only decodes the new one. Three
|
|
60
|
+
captures of a 360K disk go from 6.5 seconds to 0.4 on a repeat run.
|
|
61
|
+
`--cache DIR` moves it, `--no-cache` turns it off, and every input says in
|
|
62
|
+
the report whether it was decoded or reused.
|
diskstack-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christian Bosch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
diskstack-1.0.0/NOTICE
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
diskstack bundles part of Greaseweazle.
|
|
2
|
+
|
|
3
|
+
https://github.com/keirf/greaseweazle
|
|
4
|
+
commit 26690f89967d519e0106ab9566019a026b920bb4 (master, post-v1.23)
|
|
5
|
+
|
|
6
|
+
Greaseweazle is released into the public domain under the Unlicense. The full
|
|
7
|
+
text is in diskstack/_vendor/greaseweazle/COPYING.
|
|
8
|
+
|
|
9
|
+
Greaseweazle is not published on PyPI, so its flux and sector decoders are
|
|
10
|
+
vendored rather than depended on. tools/vendor.py regenerates the whole tree
|
|
11
|
+
from that commit; nothing under diskstack/_vendor/ is edited by hand.
|
|
12
|
+
|
|
13
|
+
What is vendored: src/greaseweazle/ minus usb.py, cli.py and tools/. TrackSet,
|
|
14
|
+
range_str and columnify are extracted from tools/util.py into a standalone
|
|
15
|
+
module because the rest of that file drives the USB hardware and needs
|
|
16
|
+
pyserial.
|
|
17
|
+
|
|
18
|
+
Local changes, all applied automatically by tools/vendor.py:
|
|
19
|
+
|
|
20
|
+
* imports rewritten from `greaseweazle.*` to
|
|
21
|
+
`diskstack._vendor.greaseweazle.*`
|
|
22
|
+
|
|
23
|
+
* greaseweazle/__init__.py is generated. It carries the vendored commit as
|
|
24
|
+
__version__ and imports codec.codec, because codec/codec.py imports every
|
|
25
|
+
codec at module scope and those codecs import back into codec.ibm.ibm, so
|
|
26
|
+
importing e.g. image.imd first lands on a partially-initialised module.
|
|
27
|
+
Upstream never hits this because its CLI always reaches codec.codec first.
|
|
28
|
+
|
|
29
|
+
* codec/ibm/ibm.py: the two `print("Unknown mark %02x")` warnings are
|
|
30
|
+
dropped. Damaged tracks produce them constantly and they would corrupt the
|
|
31
|
+
table diskstack prints on stdout.
|
|
32
|
+
|
|
33
|
+
* optimised/__init__.py: the two warnings about the missing C extension are
|
|
34
|
+
dropped, for the same reason. The pure-Python fallback is used and is fast
|
|
35
|
+
enough (about 75 ms per three-revolution track).
|
|
36
|
+
|
|
37
|
+
* image/imd.py: the reader now records the per-sector data-error flag
|
|
38
|
+
(rec & 4) and the data-unavailable code (rec == 0) as a failed CRC instead
|
|
39
|
+
of discarding both. An IMD full of error sectors is exactly the input
|
|
40
|
+
diskstack exists to merge.
|
|
41
|
+
|
|
42
|
+
* tools/util.py: TrackSet.TrackIter gets an __iter__ returning itself. An
|
|
43
|
+
iterator has to be iterable, and Python 3.13 enforces it, so without this
|
|
44
|
+
every `for track in diskdef.tracks` raises TypeError there.
|
|
45
|
+
|
|
46
|
+
* codec/codec.py: importlib.resources.open_text is replaced with
|
|
47
|
+
importlib.resources.files(...).joinpath(...).open(), which is the form
|
|
48
|
+
that still works once the package is installed as a wheel.
|
|
49
|
+
|
|
50
|
+
The test fixtures download Transylvania.img from the fluxfox test corpus:
|
|
51
|
+
|
|
52
|
+
https://github.com/dbalsom/fluxfox/tree/main/tests/images/transylvania
|
|
53
|
+
|
|
54
|
+
See the LICENSE.txt in that directory for its terms. Nothing from it is
|
|
55
|
+
redistributed here; tests/fixtures/make_fixtures.py fetches it on demand.
|
diskstack-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diskstack
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Merge several dumps of one floppy disk into the best possible image, and say what is still missing
|
|
5
|
+
Author-email: Christian Bosch <cbosch101@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Booyaka101/diskstack
|
|
8
|
+
Project-URL: Repository, https://github.com/Booyaka101/diskstack
|
|
9
|
+
Project-URL: Issues, https://github.com/Booyaka101/diskstack/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Booyaka101/diskstack/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: floppy,flux,greaseweazle,supercard-pro,scp,preservation,disk-image,data-recovery,retrocomputing,amiga,mfm
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: System :: Archiving
|
|
20
|
+
Classifier: Topic :: System :: Recovery Tools
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
License-File: NOTICE
|
|
25
|
+
License-File: diskstack/_vendor/greaseweazle/COPYING
|
|
26
|
+
Requires-Dist: click>=8.1
|
|
27
|
+
Requires-Dist: bitarray>=2.9
|
|
28
|
+
Requires-Dist: crcmod>=1.7
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.16; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# diskstack
|
|
37
|
+
|
|
38
|
+
Merge several dumps of one floppy disk into the best possible image, and get
|
|
39
|
+
told exactly what is still missing.
|
|
40
|
+
|
|
41
|
+
## The loop
|
|
42
|
+
|
|
43
|
+
Read the disk twice, stack the dumps, and diskstack hands you back a
|
|
44
|
+
Greaseweazle command that re-reads only the tracks that are still bad:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
$ diskstack disk_a.scp disk_b.scp -o merged.img
|
|
48
|
+
|
|
49
|
+
720 sectors: 714 clean (from 2 files), 6 unresolved
|
|
50
|
+
|
|
51
|
+
Tracks needing attention
|
|
52
|
+
cyl head clean voted unresolved missing sector ids still bad
|
|
53
|
+
17 0 7 0 2 0 4,7
|
|
54
|
+
18 0 7 0 2 0 4,7
|
|
55
|
+
19 0 7 0 2 0 4,7
|
|
56
|
+
|
|
57
|
+
Re-read just these tracks, then run diskstack again with the new capture added:
|
|
58
|
+
gw read retry.scp --tracks=c=17-19:h=0
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Three tracks instead of eighty, so the drive spends seconds on the media
|
|
62
|
+
rather than minutes. Feed the new capture back in:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
$ gw read disk_c.scp --tracks=c=17-19:h=0
|
|
66
|
+
$ diskstack disk_a.scp disk_b.scp disk_c.scp -o merged.img
|
|
67
|
+
|
|
68
|
+
720 sectors: 714 clean (from 3 files), 6 recovered by vote
|
|
69
|
+
Since the last report: 6 recovered.
|
|
70
|
+
|
|
71
|
+
Every sector confirmed by CRC. Nothing left to re-read.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That run produced a file byte-identical to the original disk image. Repeat
|
|
75
|
+
until the unresolved count reaches zero or stops moving. diskstack reads the
|
|
76
|
+
report it is about to overwrite, so it tells you which of the two happened:
|
|
77
|
+
either how many sectors the new capture recovered, or that nothing changed and
|
|
78
|
+
another pass at the same settings is unlikely to. It also keeps the decoded
|
|
79
|
+
flux, so the second run only decodes the capture you just added: re-running
|
|
80
|
+
the three-capture merge above takes 0.4 seconds against 6.5 the first time.
|
|
81
|
+
The full session is in
|
|
82
|
+
[examples/console-session.txt](examples/console-session.txt) and the report it
|
|
83
|
+
wrote is in [examples/diskstack-report.json](examples/diskstack-report.json).
|
|
84
|
+
|
|
85
|
+
## Install
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
pip install diskstack
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Python 3.11 or newer. No hardware, no network, no configuration.
|
|
92
|
+
|
|
93
|
+
Until the PyPI upload lands, `pip install
|
|
94
|
+
git+https://github.com/Booyaka101/diskstack` gets you the same thing.
|
|
95
|
+
|
|
96
|
+
## Use
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
diskstack disk_a.scp disk_b.scp disk_c.img -o merged.img
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Inputs are two or more dumps of the same disk, in any mix of:
|
|
103
|
+
|
|
104
|
+
| what | extensions |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| flux | `.scp` (SuperCard Pro), `.raw` (KryoFlux streams), `.hfe` (HxC) |
|
|
107
|
+
| sector images | `.img`, `.ima`, `.st`, `.adf`, `.imd` |
|
|
108
|
+
|
|
109
|
+
For KryoFlux, name any one of the per-track stream files and the rest of the
|
|
110
|
+
set is picked up from the directory.
|
|
111
|
+
|
|
112
|
+
Output is a sector image: `.img`, `.ima`, `.st`, `.adf` or `.imd`. Flux goes in
|
|
113
|
+
but never comes out. Along with it you get `diskstack-report.json` next to the
|
|
114
|
+
image and the table above on stdout.
|
|
115
|
+
|
|
116
|
+
Formats understood are IBM FM and MFM (PC 160K through 2.88M, Atari ST) and
|
|
117
|
+
AmigaDOS MFM. `diskstack --list-formats` prints the list. Detection is
|
|
118
|
+
automatic; `--format ibm.720` overrides it.
|
|
119
|
+
|
|
120
|
+
The exit code is 0 when every sector is confirmed and 2 when something is still
|
|
121
|
+
unresolved, so a retry loop can be scripted.
|
|
122
|
+
|
|
123
|
+
## How it decides
|
|
124
|
+
|
|
125
|
+
Every sector of every revolution of every input is a separate read attempt.
|
|
126
|
+
Attempts are grouped by the `(cylinder, head, sector id)` in their ID address
|
|
127
|
+
mark, never by where they sit in the flux, because two captures of the same
|
|
128
|
+
disk do not line up: the index phase differs and so does the motor speed.
|
|
129
|
+
|
|
130
|
+
Per sector, in order:
|
|
131
|
+
|
|
132
|
+
1. **clean** - an attempt whose own CRC or checksum passes. Taken as is.
|
|
133
|
+
2. **recovered by vote** - no attempt passed, so the attempts get recombined
|
|
134
|
+
into candidate payloads and the first one that satisfies a check value off
|
|
135
|
+
the disk wins. Three routes are tried, best evidence first:
|
|
136
|
+
`majority` votes byte by byte across every attempt, `per_source_majority`
|
|
137
|
+
collapses each input file to one payload first so a capture with more
|
|
138
|
+
revolutions cannot outvote the files that read the sector correctly, and
|
|
139
|
+
`cross_check` tries each individual read as it stands, which is what
|
|
140
|
+
recovers a sector whose payload was fine and whose own CRC bytes were the
|
|
141
|
+
damaged part. The CRC bytes came off the same damaged track, so they get
|
|
142
|
+
voted on too and the voted check value is tried alongside the ones read.
|
|
143
|
+
Ties go to the dump that read the rest of the disk best. The report says
|
|
144
|
+
which route it was.
|
|
145
|
+
3. **unresolved** - the vote still failed. The largest cluster of attempts that
|
|
146
|
+
agree exactly is written out, the sector is listed in the table, and its
|
|
147
|
+
track goes into the re-read command.
|
|
148
|
+
|
|
149
|
+
A sector that is good in any input is never worse in the output. There is a
|
|
150
|
+
test for that.
|
|
151
|
+
|
|
152
|
+
Only a handful of reconstructions are tried, deliberately. A CRC16 accepts the
|
|
153
|
+
wrong payload once in 65536 tries, so every extra candidate thrown at the check
|
|
154
|
+
value buys recovery at the price of a small chance of confidently writing out
|
|
155
|
+
garbage. Three routes that each mean something beats a brute-force sweep over
|
|
156
|
+
the contested bytes.
|
|
157
|
+
|
|
158
|
+
The check value has to come off the disk, which means a stack of only sector
|
|
159
|
+
images can never reach step 2. `.img` and `.adf` files carry data and nothing
|
|
160
|
+
to verify it with, so identical bytes in two images prove only that both dumps
|
|
161
|
+
read the same thing. Mixing in one flux capture is what gives the vote
|
|
162
|
+
something to check against.
|
|
163
|
+
|
|
164
|
+
That distinction runs through the whole report. A sector is `verified` when
|
|
165
|
+
some read of it satisfied a check value off the disk, and a merge of sector
|
|
166
|
+
images alone never gets that, so the closing line says the sectors came from
|
|
167
|
+
inputs that carry no check value instead of claiming a CRC confirmed them. A
|
|
168
|
+
sector is `contested` when two dumps both looked good and held different
|
|
169
|
+
bytes. Nothing in a sector image can arbitrate that, so diskstack keeps the
|
|
170
|
+
best-ranked dump's copy, prints which sectors it happened to, and leaves the
|
|
171
|
+
judgement to you. It is what deliberate weak bits look like, and also what two
|
|
172
|
+
dumps of two different disks look like.
|
|
173
|
+
|
|
174
|
+
A sector the merge could not confirm is marked `unstable` if one capture read
|
|
175
|
+
it two different ways across its own revolutions. That is what weak bits look
|
|
176
|
+
like, and they are usually deliberate, so more passes over the disk will not
|
|
177
|
+
settle them. The stdout note says so next to the re-read command. Two `--pll`
|
|
178
|
+
settings reading one revolution two ways is a different thing, a decode that
|
|
179
|
+
went marginal rather than a disk that moved, and does not count.
|
|
180
|
+
|
|
181
|
+
Sectors Greaseweazle filled with `-=[BAD SECTOR]=-` are treated as failed
|
|
182
|
+
reads, not as data, so a `.img` from an earlier bad session still contributes
|
|
183
|
+
its good sectors. `--keep-filler` turns that off.
|
|
184
|
+
|
|
185
|
+
A truncated `.img` or `.adf` contributes only the sectors the file actually
|
|
186
|
+
holds bytes for. The readers pad a short image out to the format's length and
|
|
187
|
+
mark every sector they invented as CRC-clean, so without that check a dump that
|
|
188
|
+
stopped halfway would win the merge with 512 bytes of nothing per sector.
|
|
189
|
+
|
|
190
|
+
## Options
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
-o, --output PATH Merged sector image to write (.img, .ima,
|
|
194
|
+
.st, .adf, .imd). [required]
|
|
195
|
+
-r, --report PATH JSON report path [default: diskstack-
|
|
196
|
+
report.json beside the output]
|
|
197
|
+
--no-report Do not write the JSON report.
|
|
198
|
+
-f, --format TEXT Disk format, e.g. ibm.720 or amiga.amigados.
|
|
199
|
+
Detected from the inputs when not given.
|
|
200
|
+
--list-formats Print the disk formats diskstack understands
|
|
201
|
+
and exit.
|
|
202
|
+
--pll SPEC Flux PLL settings, e.g.
|
|
203
|
+
period=5:phase=60:lowpass=1.5. Repeat to
|
|
204
|
+
decode each capture several ways and let the
|
|
205
|
+
results compete.
|
|
206
|
+
--revs N Use only the first N revolutions of each
|
|
207
|
+
flux capture [default: all of them] [x>=1]
|
|
208
|
+
--keep-filler Trust sectors written as '-=[BAD SECTOR]=-'
|
|
209
|
+
filler instead of treating them as failed
|
|
210
|
+
reads.
|
|
211
|
+
--fill-unresolved [best|filler|zero]
|
|
212
|
+
What to write for a sector no input
|
|
213
|
+
confirmed: its best guess, bad-sector
|
|
214
|
+
filler, or zeroes. [default: best]
|
|
215
|
+
--no-vote Skip the byte-wise majority vote; keep only
|
|
216
|
+
sectors whose own CRC passes.
|
|
217
|
+
-j, --jobs N Worker processes for decoding flux [default:
|
|
218
|
+
one per core, up to 8] [x>=0]
|
|
219
|
+
--cache DIR Where to keep decoded flux between runs
|
|
220
|
+
[default: .diskstack-cache beside the
|
|
221
|
+
output]
|
|
222
|
+
--no-cache Decode every flux capture again instead of
|
|
223
|
+
reusing the decode a previous run stored.
|
|
224
|
+
--retry-name NAME Filename used in the printed gw read
|
|
225
|
+
command. [default: retry.scp]
|
|
226
|
+
-q, --quiet Print the summary and the re-read command,
|
|
227
|
+
no tables.
|
|
228
|
+
-V, --version Show the version and exit.
|
|
229
|
+
-h, --help Show this message and exit.
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Decoding flux is the slow part, so it runs one track per core by default. A
|
|
233
|
+
three-capture 40-cylinder merge takes about six seconds on eight cores against
|
|
234
|
+
half a minute on one. `--jobs 1` forces the single-process path, which is also
|
|
235
|
+
what happens automatically if the worker pool cannot start.
|
|
236
|
+
|
|
237
|
+
`--pll` is worth knowing about. Repeating it decodes each flux capture several
|
|
238
|
+
ways, and since two PLL settings disagree about different marginal bitcells,
|
|
239
|
+
they become independent attempts that the vote can use:
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
diskstack a.scp b.scp -o merged.img --pll period=5:phase=60 --pll lowpass=1.5
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
It applies to every flux container, `.scp`, `.raw` and `.hfe` alike.
|
|
246
|
+
|
|
247
|
+
Decoded flux is kept in `.diskstack-cache` beside the output, along with the
|
|
248
|
+
detected disk format, so a re-run only pays for the captures it has not seen.
|
|
249
|
+
An entry is named for a hash of the input's bytes and of every setting that
|
|
250
|
+
changes the decode, which means a fresh dump written over the same filename is
|
|
251
|
+
a new entry rather than a stale hit, and a capture that moved or was renamed
|
|
252
|
+
still counts as one it has seen. `--cache DIR` puts it somewhere else,
|
|
253
|
+
`--no-cache` turns it off, and deleting the directory costs nothing but the
|
|
254
|
+
next decode. It drops its least recently used entries once it passes 512 MB;
|
|
255
|
+
three captures of a 360K disk take 377 KB.
|
|
256
|
+
|
|
257
|
+
## The report
|
|
258
|
+
|
|
259
|
+
`diskstack-report.json` carries one entry per sector of the disk: its status,
|
|
260
|
+
how many attempts it got, how many agreed, and which file and revolution each
|
|
261
|
+
contributing read came from. Plus per-input totals and the re-read trackspecs.
|
|
262
|
+
`schema` is versioned and starts at 1.
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
{
|
|
266
|
+
"cyl": 17, "head": 0, "sec_id": 4, "size": 512,
|
|
267
|
+
"status": "unresolved", "attempts": 6, "good": 0, "agreement": 3,
|
|
268
|
+
"discarded": 0, "unstable": false, "contested": false, "verified": false,
|
|
269
|
+
"method": null,
|
|
270
|
+
"sources": [{"path": "capture_a.scp", "rev": 0}]
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`method` names the route that rebuilt a recovered sector and is null for any
|
|
275
|
+
other status. `discarded` counts attempts thrown away because they decoded to
|
|
276
|
+
the wrong length for the sector. `verified` and `contested` are the two above,
|
|
277
|
+
and `totals` counts both across the disk.
|
|
278
|
+
|
|
279
|
+
`since_last_report` holds the `recovered`, `lost` and `still_bad` counts
|
|
280
|
+
against the report this run replaced, or null on the first run and whenever
|
|
281
|
+
the old report covers a different set of sectors. Each input carries `cached`,
|
|
282
|
+
saying whether its sectors were decoded on this run or reused from an earlier
|
|
283
|
+
one.
|
|
284
|
+
|
|
285
|
+
## What it does not do
|
|
286
|
+
|
|
287
|
+
v1 is deliberately narrow. No GCR, so no Apple II and no Commodore. No writing
|
|
288
|
+
flux back out. No hardware access, so nothing here talks to a Greaseweazle
|
|
289
|
+
or a KryoFlux. No copy-protection preservation: this produces sector images,
|
|
290
|
+
which is the wrong container for weak bits and long tracks. No network calls.
|
|
291
|
+
|
|
292
|
+
## Validation, honestly
|
|
293
|
+
|
|
294
|
+
The three-capture test builds genuine SuperCard Pro flux from a real PC floppy
|
|
295
|
+
image, damages each capture a different way, gives each its own index phase and
|
|
296
|
+
its own motor speed within 1.5 percent, and asserts the merged output is
|
|
297
|
+
byte-identical to the original. The AmigaDOS codec gets the same treatment one
|
|
298
|
+
track at a time, since the fixture disk is a PC one. That is a real decode of
|
|
299
|
+
real flux through Greaseweazle's own decoders, but the damage is synthetic. It
|
|
300
|
+
has not yet been run against dumps of a genuinely deteriorated disk.
|
|
301
|
+
|
|
302
|
+
If you have a disk that needs several passes, I would like to see the report.
|
|
303
|
+
Open an issue with the `diskstack-report.json` and what the disk is.
|
|
304
|
+
|
|
305
|
+
## Tests
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
pip install -e ".[dev]"
|
|
309
|
+
python tests/fixtures/make_fixtures.py # downloads 360 KB, writes 55 MB
|
|
310
|
+
python -m pytest tests -q
|
|
311
|
+
ruff check .
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The fixture builder needs the network once. Everything else runs offline.
|
|
315
|
+
|
|
316
|
+
## Built on Greaseweazle
|
|
317
|
+
|
|
318
|
+
The flux and sector decoders are [Greaseweazle's](
|
|
319
|
+
https://github.com/keirf/greaseweazle), vendored under
|
|
320
|
+
`diskstack/_vendor/greaseweazle` because Greaseweazle is not published on PyPI.
|
|
321
|
+
It is public domain under the Unlicense. `tools/vendor.py` regenerates the tree
|
|
322
|
+
from a pinned commit and applies every local change; `NOTICE` lists them.
|
|
323
|
+
|
|
324
|
+
Runtime dependencies are `click`, `bitarray` and `crcmod`. The last two are
|
|
325
|
+
what the vendored decoders need.
|
|
326
|
+
|
|
327
|
+
## License
|
|
328
|
+
|
|
329
|
+
MIT. See `LICENSE`, and `NOTICE` for the vendored code.
|