diskstack 1.0.0__py3-none-any.whl
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/__init__.py +5 -0
- diskstack/_vendor/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/COPYING +24 -0
- diskstack/_vendor/greaseweazle/__init__.py +2 -0
- diskstack/_vendor/greaseweazle/codec/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/amiga/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/amiga/amigados.py +221 -0
- diskstack/_vendor/greaseweazle/codec/apple2/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/apple2/apple2_gcr.py +223 -0
- diskstack/_vendor/greaseweazle/codec/bitcell.py +117 -0
- diskstack/_vendor/greaseweazle/codec/codec.py +362 -0
- diskstack/_vendor/greaseweazle/codec/commodore/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/commodore/c64_gcr.py +229 -0
- diskstack/_vendor/greaseweazle/codec/datageneral/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/datageneral/datageneral.py +211 -0
- diskstack/_vendor/greaseweazle/codec/hp/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/hp/hp_mmfm.py +258 -0
- diskstack/_vendor/greaseweazle/codec/ibm/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/ibm/ibm.py +1005 -0
- diskstack/_vendor/greaseweazle/codec/macintosh/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/macintosh/mac_gcr.py +238 -0
- diskstack/_vendor/greaseweazle/codec/micropolis/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/micropolis/micropolis.py +233 -0
- diskstack/_vendor/greaseweazle/codec/northstar/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/northstar/northstar.py +210 -0
- diskstack/_vendor/greaseweazle/data/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/data/diskdefs.cfg +81 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_acorn.cfg +119 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_akai.cfg +27 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_amiga.cfg +17 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_apple2.cfg +31 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_apricot.cfg +25 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_atari.cfg +33 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_atarist.cfg +76 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_coco.cfg +98 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_commodore.cfg +127 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_datageneral.cfg +8 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_dec.cfg +37 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_dragon.cfg +61 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_eagle.cfg +25 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_ensoniq.cfg +39 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_epson.cfg +95 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_gem.cfg +15 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_hp.cfg +25 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_ibm.cfg +140 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_kaypro.cfg +58 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_luxor.cfg +96 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_mac.cfg +71 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_micropolis.cfg +73 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_mm1.cfg +69 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_msx.cfg +45 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_northstar.cfg +37 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_occ1.cfg +23 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_olivetti.cfg +18 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_pc98.cfg +69 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_raw.cfg +25 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_rm.cfg +98 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_sci.cfg +15 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_sega.cfg +13 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_sharp.cfg +13 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_thomson.cfg +78 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_tsc.cfg +81 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_xerox.cfg +59 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_zx.cfg +245 -0
- diskstack/_vendor/greaseweazle/error.py +19 -0
- diskstack/_vendor/greaseweazle/flux.py +315 -0
- diskstack/_vendor/greaseweazle/image/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/image/a2r.py +133 -0
- diskstack/_vendor/greaseweazle/image/acorn.py +28 -0
- diskstack/_vendor/greaseweazle/image/adf.py +15 -0
- diskstack/_vendor/greaseweazle/image/apple2.py +18 -0
- diskstack/_vendor/greaseweazle/image/apridisk.py +107 -0
- diskstack/_vendor/greaseweazle/image/caps.py +464 -0
- diskstack/_vendor/greaseweazle/image/d64.py +51 -0
- diskstack/_vendor/greaseweazle/image/d81.py +28 -0
- diskstack/_vendor/greaseweazle/image/d88.py +211 -0
- diskstack/_vendor/greaseweazle/image/dcp.py +45 -0
- diskstack/_vendor/greaseweazle/image/dim.py +53 -0
- diskstack/_vendor/greaseweazle/image/dmk.py +166 -0
- diskstack/_vendor/greaseweazle/image/dsk.py +31 -0
- diskstack/_vendor/greaseweazle/image/edsk.py +591 -0
- diskstack/_vendor/greaseweazle/image/fd.py +23 -0
- diskstack/_vendor/greaseweazle/image/fdi.py +41 -0
- diskstack/_vendor/greaseweazle/image/hdm.py +15 -0
- diskstack/_vendor/greaseweazle/image/hfe.py +742 -0
- diskstack/_vendor/greaseweazle/image/image.py +130 -0
- diskstack/_vendor/greaseweazle/image/imd.py +201 -0
- diskstack/_vendor/greaseweazle/image/img.py +111 -0
- diskstack/_vendor/greaseweazle/image/kryoflux.py +304 -0
- diskstack/_vendor/greaseweazle/image/mgt.py +15 -0
- diskstack/_vendor/greaseweazle/image/msa.py +153 -0
- diskstack/_vendor/greaseweazle/image/nfd.py +113 -0
- diskstack/_vendor/greaseweazle/image/nsi.py +47 -0
- diskstack/_vendor/greaseweazle/image/scp.py +471 -0
- diskstack/_vendor/greaseweazle/image/sf7.py +15 -0
- diskstack/_vendor/greaseweazle/image/sharp2d.py +15 -0
- diskstack/_vendor/greaseweazle/image/td0.py +141 -0
- diskstack/_vendor/greaseweazle/image/xdf.py +15 -0
- diskstack/_vendor/greaseweazle/optimised/__init__.py +22 -0
- diskstack/_vendor/greaseweazle/tools/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/tools/util.py +133 -0
- diskstack/_vendor/greaseweazle/track.py +494 -0
- diskstack/cache.py +210 -0
- diskstack/candidates.py +396 -0
- diskstack/cli.py +348 -0
- diskstack/errors.py +5 -0
- diskstack/filler.py +29 -0
- diskstack/formats.py +381 -0
- diskstack/parallel.py +73 -0
- diskstack/report.py +353 -0
- diskstack/stack.py +317 -0
- diskstack-1.0.0.dist-info/METADATA +329 -0
- diskstack-1.0.0.dist-info/RECORD +119 -0
- diskstack-1.0.0.dist-info/WHEEL +5 -0
- diskstack-1.0.0.dist-info/entry_points.txt +2 -0
- diskstack-1.0.0.dist-info/licenses/LICENSE +21 -0
- diskstack-1.0.0.dist-info/licenses/NOTICE +55 -0
- diskstack-1.0.0.dist-info/licenses/diskstack/_vendor/greaseweazle/COPYING +24 -0
- diskstack-1.0.0.dist-info/top_level.txt +1 -0
diskstack/cli.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Command line: read the dumps, stack them, write the image and the report."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from concurrent.futures.process import BrokenProcessPool
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional, Sequence, Tuple
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from diskstack import (__version__, cache, candidates, formats, parallel,
|
|
15
|
+
report, stack)
|
|
16
|
+
from diskstack._vendor.greaseweazle.track import PLL
|
|
17
|
+
from diskstack.errors import DiskStackError
|
|
18
|
+
from diskstack.filler import make_filler
|
|
19
|
+
|
|
20
|
+
FILL_CHOICES = ('best', 'filler', 'zero')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Decode:
|
|
25
|
+
"""Everything that decides what reading one input produces."""
|
|
26
|
+
|
|
27
|
+
fmt: object
|
|
28
|
+
fmt_name: str
|
|
29
|
+
plls: List[Optional[PLL]] = field(default_factory=lambda: [None])
|
|
30
|
+
revs: Optional[int] = None
|
|
31
|
+
detect_filler: bool = True
|
|
32
|
+
jobs: int = 1
|
|
33
|
+
cache_dir: Optional[Path] = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parse_plls(specs: Sequence[str]) -> List[Optional[PLL]]:
|
|
37
|
+
"""Turn ``--pll`` strings into PLL objects; no flag means the default PLL."""
|
|
38
|
+
if not specs:
|
|
39
|
+
return [None]
|
|
40
|
+
plls = []
|
|
41
|
+
for spec in specs:
|
|
42
|
+
try:
|
|
43
|
+
plls.append(PLL(spec))
|
|
44
|
+
except (ValueError, AttributeError) as exc:
|
|
45
|
+
raise DiskStackError(
|
|
46
|
+
f"Bad --pll '{spec}'. Expected key=value pairs joined by ':', "
|
|
47
|
+
f"from period=<pct>, phase=<pct>, lowpass=<us>, "
|
|
48
|
+
f"e.g. --pll period=5:phase=60") from exc
|
|
49
|
+
return plls
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@contextmanager
|
|
53
|
+
def reading(path: Path, steps: int, quiet: bool):
|
|
54
|
+
"""Show progress while a file is read. Decoding flux takes real time."""
|
|
55
|
+
if quiet:
|
|
56
|
+
yield None
|
|
57
|
+
return
|
|
58
|
+
with click.progressbar(length=steps, file=sys.stderr,
|
|
59
|
+
label=f'Reading {path.name}') as bar:
|
|
60
|
+
yield lambda: bar.update(1)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def decode_one(path: Path, opts: Decode, pll: Optional[PLL], on_track
|
|
64
|
+
) -> Tuple[List[candidates.Candidate], candidates.SourceInfo]:
|
|
65
|
+
"""One pass over one file, falling back to one process if workers fail."""
|
|
66
|
+
try:
|
|
67
|
+
return candidates.load(path, opts.fmt, pll=pll, revs=opts.revs,
|
|
68
|
+
detect_filler=opts.detect_filler,
|
|
69
|
+
jobs=opts.jobs, on_track=on_track)
|
|
70
|
+
except (BrokenProcessPool, ImportError, OSError, RuntimeError) as exc:
|
|
71
|
+
if opts.jobs <= 1:
|
|
72
|
+
raise
|
|
73
|
+
click.echo(f'Parallel decode unavailable ({exc}); using one process.',
|
|
74
|
+
err=True)
|
|
75
|
+
return candidates.load(path, opts.fmt, pll=pll, revs=opts.revs,
|
|
76
|
+
detect_filler=opts.detect_filler, jobs=1,
|
|
77
|
+
on_track=on_track)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cache_name(path: Path, opts: Decode, pll: Optional[PLL]) -> Optional[str]:
|
|
81
|
+
"""Entry this decode belongs under, or None if it is not worth caching.
|
|
82
|
+
|
|
83
|
+
Only flux is: reading a sector image is a file read, so an entry for one
|
|
84
|
+
would cost about what it saves.
|
|
85
|
+
"""
|
|
86
|
+
if opts.cache_dir is None or not formats.is_flux(path):
|
|
87
|
+
return None
|
|
88
|
+
try:
|
|
89
|
+
return cache.key(formats.input_files(path, formats.kind_of(path)),
|
|
90
|
+
cache.settings(opts.fmt_name, pll, opts.revs,
|
|
91
|
+
opts.detect_filler))
|
|
92
|
+
except OSError:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def read_one(path: Path, opts: Decode, pll: Optional[PLL], on_track
|
|
97
|
+
) -> Tuple[List[candidates.Candidate], candidates.SourceInfo]:
|
|
98
|
+
"""One pass over one file, taken from the cache when it is already there."""
|
|
99
|
+
name = cache_name(path, opts, pll)
|
|
100
|
+
if name is not None:
|
|
101
|
+
hit = cache.load(opts.cache_dir, name, path)
|
|
102
|
+
if hit is not None:
|
|
103
|
+
if on_track is not None:
|
|
104
|
+
for _ in candidates.track_list(opts.fmt):
|
|
105
|
+
on_track()
|
|
106
|
+
return hit
|
|
107
|
+
cands, info = decode_one(path, opts, pll, on_track)
|
|
108
|
+
if name is not None:
|
|
109
|
+
cache.store(opts.cache_dir, name, cands, info)
|
|
110
|
+
return cands, info
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def disk_format(paths: Sequence[Path], pll: Optional[PLL],
|
|
114
|
+
cache_dir: Optional[Path]) -> Tuple[str, str]:
|
|
115
|
+
"""The disk format, remembered from the last run over the same inputs.
|
|
116
|
+
|
|
117
|
+
Detection decodes the first cylinders of a flux capture, which with the
|
|
118
|
+
decodes themselves cached is the slowest thing left in a repeat run.
|
|
119
|
+
"""
|
|
120
|
+
name = None
|
|
121
|
+
if cache_dir is not None:
|
|
122
|
+
files = [f for path in paths
|
|
123
|
+
for f in formats.input_files(path, formats.kind_of(path))]
|
|
124
|
+
try:
|
|
125
|
+
name = cache.key(files, cache.settings('detect', pll, None, True))
|
|
126
|
+
except OSError:
|
|
127
|
+
name = None
|
|
128
|
+
known = (cache.recall(cache_dir, name) or {}) if name else {}
|
|
129
|
+
if (isinstance(known.get('format'), str)
|
|
130
|
+
and isinstance(known.get('detail'), str)):
|
|
131
|
+
return known['format'], known['detail']
|
|
132
|
+
fmt_name, detail = formats.detect_format(paths, pll)
|
|
133
|
+
if name is not None:
|
|
134
|
+
cache.note(cache_dir, name, {'format': fmt_name, 'detail': detail})
|
|
135
|
+
return fmt_name, detail
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def load_source(path: Path, opts: Decode, on_track=None
|
|
139
|
+
) -> Tuple[List[candidates.Candidate], candidates.SourceInfo]:
|
|
140
|
+
"""Read one input file, decoding flux once per requested PLL setting.
|
|
141
|
+
|
|
142
|
+
Two PLLs disagreeing about a marginal bitcell give two independent
|
|
143
|
+
attempts at the same sector, which is exactly what the vote wants.
|
|
144
|
+
"""
|
|
145
|
+
cands, info = read_one(path, opts, opts.plls[0], on_track)
|
|
146
|
+
if info.kind not in formats.FLUX_KINDS:
|
|
147
|
+
return cands, info
|
|
148
|
+
for pll in opts.plls[1:]:
|
|
149
|
+
more, extra = read_one(path, opts, pll, on_track)
|
|
150
|
+
cands += more
|
|
151
|
+
parallel.merge_info(info, extra)
|
|
152
|
+
info.cached = info.cached and extra.cached
|
|
153
|
+
return cands, info
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def apply_fill(result: stack.StackResult, how: str) -> None:
|
|
157
|
+
"""Replace the payload of sectors diskstack could not confirm."""
|
|
158
|
+
if how == 'best':
|
|
159
|
+
return
|
|
160
|
+
for res in result.resolutions:
|
|
161
|
+
if res.resolved:
|
|
162
|
+
continue
|
|
163
|
+
res.data = (bytes(res.size) if how == 'zero'
|
|
164
|
+
else make_filler(res.size))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _list_formats(ctx, param, value):
|
|
168
|
+
if not value or ctx.resilient_parsing:
|
|
169
|
+
return
|
|
170
|
+
click.echo('\n'.join(formats.supported_formats()))
|
|
171
|
+
ctx.exit()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@click.command(context_settings={'help_option_names': ['-h', '--help']})
|
|
175
|
+
@click.argument('inputs', nargs=-1, required=True,
|
|
176
|
+
type=click.Path(path_type=Path))
|
|
177
|
+
@click.option('-o', '--output', required=True, type=click.Path(path_type=Path),
|
|
178
|
+
help='Merged sector image to write (.img, .ima, .st, .adf, '
|
|
179
|
+
'.imd).')
|
|
180
|
+
@click.option('-r', '--report', 'report_path', type=click.Path(path_type=Path),
|
|
181
|
+
help='JSON report path [default: diskstack-report.json beside '
|
|
182
|
+
'the output]')
|
|
183
|
+
@click.option('--no-report', is_flag=True, help='Do not write the JSON report.')
|
|
184
|
+
@click.option('-f', '--format', 'fmt_name',
|
|
185
|
+
help='Disk format, e.g. ibm.720 or amiga.amigados. Detected '
|
|
186
|
+
'from the inputs when not given.')
|
|
187
|
+
@click.option('--list-formats', is_flag=True, is_eager=True, expose_value=False,
|
|
188
|
+
callback=_list_formats,
|
|
189
|
+
help='Print the disk formats diskstack understands and exit.')
|
|
190
|
+
@click.option('--pll', 'pll_specs', multiple=True, metavar='SPEC',
|
|
191
|
+
help='Flux PLL settings, e.g. period=5:phase=60:lowpass=1.5. '
|
|
192
|
+
'Repeat to decode each capture several ways and let the '
|
|
193
|
+
'results compete.')
|
|
194
|
+
@click.option('--revs', type=click.IntRange(min=1), metavar='N',
|
|
195
|
+
help='Use only the first N revolutions of each flux capture '
|
|
196
|
+
'[default: all of them]')
|
|
197
|
+
@click.option('--keep-filler', is_flag=True,
|
|
198
|
+
help="Trust sectors written as '-=[BAD SECTOR]=-' filler "
|
|
199
|
+
'instead of treating them as failed reads.')
|
|
200
|
+
@click.option('--fill-unresolved', type=click.Choice(FILL_CHOICES),
|
|
201
|
+
default='best', show_default=True,
|
|
202
|
+
help='What to write for a sector no input confirmed: its best '
|
|
203
|
+
'guess, bad-sector filler, or zeroes.')
|
|
204
|
+
@click.option('--no-vote', is_flag=True,
|
|
205
|
+
help='Skip the byte-wise majority vote; keep only sectors whose '
|
|
206
|
+
'own CRC passes.')
|
|
207
|
+
@click.option('-j', '--jobs', type=click.IntRange(min=0), metavar='N',
|
|
208
|
+
default=0, help='Worker processes for decoding flux '
|
|
209
|
+
'[default: one per core, up to '
|
|
210
|
+
f'{parallel.MAX_JOBS}]')
|
|
211
|
+
@click.option('--cache', 'cache_dir', type=click.Path(path_type=Path),
|
|
212
|
+
metavar='DIR',
|
|
213
|
+
help='Where to keep decoded flux between runs [default: '
|
|
214
|
+
'.diskstack-cache beside the output]')
|
|
215
|
+
@click.option('--no-cache', is_flag=True,
|
|
216
|
+
help='Decode every flux capture again instead of reusing the '
|
|
217
|
+
'decode a previous run stored.')
|
|
218
|
+
@click.option('--retry-name', default='retry.scp', show_default=True,
|
|
219
|
+
metavar='NAME', help='Filename used in the printed gw read '
|
|
220
|
+
'command.')
|
|
221
|
+
@click.option('-q', '--quiet', is_flag=True,
|
|
222
|
+
help='Print the summary and the re-read command, no tables.')
|
|
223
|
+
@click.version_option(__version__, '-V', '--version', prog_name='diskstack')
|
|
224
|
+
def main(**opts):
|
|
225
|
+
"""Merge several dumps of one floppy disk into the best possible image.
|
|
226
|
+
|
|
227
|
+
INPUTS are two or more files of the same disk, any mix of flux captures
|
|
228
|
+
(.scp, .raw, .hfe) and decoded sector images (.img, .ima, .st, .adf,
|
|
229
|
+
.imd):
|
|
230
|
+
|
|
231
|
+
diskstack disk_a.scp disk_b.scp disk_c.img -o merged.img
|
|
232
|
+
|
|
233
|
+
Every sector is taken from whichever read got a passing CRC. Sectors no
|
|
234
|
+
read got right are rebuilt from the attempts and kept only if the result
|
|
235
|
+
satisfies a check value off the disk, and diskstack prints a gw read
|
|
236
|
+
command naming only the tracks that are still bad, so the next pass over
|
|
237
|
+
the disk is short.
|
|
238
|
+
"""
|
|
239
|
+
try:
|
|
240
|
+
run(**opts)
|
|
241
|
+
except DiskStackError as exc:
|
|
242
|
+
raise click.ClickException(str(exc)) from exc
|
|
243
|
+
except KeyboardInterrupt:
|
|
244
|
+
click.echo('Interrupted.', err=True)
|
|
245
|
+
raise SystemExit(130) from None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def run(inputs, output, report_path, no_report, fmt_name, pll_specs, revs,
|
|
249
|
+
keep_filler, fill_unresolved, no_vote, jobs, cache_dir, no_cache,
|
|
250
|
+
retry_name, quiet):
|
|
251
|
+
paths = [Path(p) for p in inputs]
|
|
252
|
+
if len(paths) < 2:
|
|
253
|
+
raise DiskStackError(
|
|
254
|
+
'diskstack needs two or more dumps of the same disk; '
|
|
255
|
+
f'got {len(paths)}.\nWith one dump there is nothing to stack.')
|
|
256
|
+
seen = set()
|
|
257
|
+
for path in paths:
|
|
258
|
+
resolved = path.resolve()
|
|
259
|
+
if resolved in seen:
|
|
260
|
+
raise DiskStackError(f'{path}: listed twice. Each dump can only '
|
|
261
|
+
f'count once towards the vote.')
|
|
262
|
+
seen.add(resolved)
|
|
263
|
+
formats.check_readable(path)
|
|
264
|
+
if output.resolve() in seen:
|
|
265
|
+
raise DiskStackError(f'{output}: that is one of the inputs. Writing '
|
|
266
|
+
f'the merge over a dump would destroy it.')
|
|
267
|
+
if report_path is not None and report_path.resolve() in seen:
|
|
268
|
+
raise DiskStackError(f'{report_path}: that is one of the inputs.')
|
|
269
|
+
if report_path is not None and report_path.resolve() == output.resolve():
|
|
270
|
+
raise DiskStackError(f'{report_path}: that is the merged image. The '
|
|
271
|
+
f'report would be written over it.')
|
|
272
|
+
if cache_dir is not None and cache_dir.exists() and not cache_dir.is_dir():
|
|
273
|
+
raise DiskStackError(f'{cache_dir}: --cache wants a directory to keep '
|
|
274
|
+
f'decoded flux in.')
|
|
275
|
+
formats.check_writable(output)
|
|
276
|
+
|
|
277
|
+
plls = _parse_plls(pll_specs)
|
|
278
|
+
cache_dir = (None if no_cache
|
|
279
|
+
else cache_dir or output.parent / cache.DIR_NAME)
|
|
280
|
+
|
|
281
|
+
if fmt_name:
|
|
282
|
+
fmt, detail = formats.get_format(fmt_name), 'given with --format'
|
|
283
|
+
else:
|
|
284
|
+
fmt_name, detail = disk_format(paths, plls[0], cache_dir)
|
|
285
|
+
fmt = formats.get_format(fmt_name)
|
|
286
|
+
|
|
287
|
+
opts = Decode(fmt=fmt, fmt_name=fmt_name, plls=plls, revs=revs,
|
|
288
|
+
detect_filler=not keep_filler,
|
|
289
|
+
jobs=jobs or parallel.default_jobs(), cache_dir=cache_dir)
|
|
290
|
+
|
|
291
|
+
all_cands, sources = [], []
|
|
292
|
+
tracks = len(candidates.track_list(fmt))
|
|
293
|
+
for path in paths:
|
|
294
|
+
steps = tracks * len(plls) if formats.is_flux(path) else 1
|
|
295
|
+
with reading(path, steps, quiet) as on_track:
|
|
296
|
+
cands, info = load_source(path, opts, on_track)
|
|
297
|
+
if on_track is not None and not formats.is_flux(path):
|
|
298
|
+
on_track()
|
|
299
|
+
all_cands += cands
|
|
300
|
+
sources.append(info)
|
|
301
|
+
|
|
302
|
+
if not all_cands:
|
|
303
|
+
raise DiskStackError(
|
|
304
|
+
'No sectors could be read from any input. Either these are not '
|
|
305
|
+
f'{fmt_name} disks or every track failed to decode.\n'
|
|
306
|
+
'Try --format, or --pll lowpass=1.5 for a weak capture.')
|
|
307
|
+
|
|
308
|
+
expected = candidates.expected_sectors(fmt)
|
|
309
|
+
sizes = {key: candidates.sector_size(fmt, *key) for key in expected}
|
|
310
|
+
for info in sources:
|
|
311
|
+
if info.kind in formats.FLAT_KINDS:
|
|
312
|
+
info.expected_size = sum(sizes.values())
|
|
313
|
+
result = stack.stack(all_cands, expected, sizes,
|
|
314
|
+
[info.path for info in sources], vote=not no_vote)
|
|
315
|
+
apply_fill(result, fill_unresolved)
|
|
316
|
+
|
|
317
|
+
formats.write_image(output, fmt, result.sector_data(),
|
|
318
|
+
result.bad_sectors())
|
|
319
|
+
|
|
320
|
+
progress = None
|
|
321
|
+
if not no_report:
|
|
322
|
+
target = report_path or output.parent / 'diskstack-report.json'
|
|
323
|
+
# Read before writing: the file about to be overwritten is the last
|
|
324
|
+
# run of this same command, which is what the loop wants compared.
|
|
325
|
+
previous = report.read_previous(target)
|
|
326
|
+
if previous is not None:
|
|
327
|
+
progress = report.compare(previous, result)
|
|
328
|
+
data = report.build(result, sources, fmt_name, detail, output,
|
|
329
|
+
retry_name, progress)
|
|
330
|
+
try:
|
|
331
|
+
report.write(data, target)
|
|
332
|
+
except OSError as exc:
|
|
333
|
+
raise DiskStackError(f'{target}: {exc.strerror or exc}') from exc
|
|
334
|
+
|
|
335
|
+
click.echo(report.render(result, sources, fmt_name, detail, retry_name,
|
|
336
|
+
tables=not quiet, progress=progress))
|
|
337
|
+
click.echo('')
|
|
338
|
+
written = [f'{output} ({output.stat().st_size} bytes)']
|
|
339
|
+
if not no_report:
|
|
340
|
+
written.append(str(target))
|
|
341
|
+
click.echo('Wrote ' + ' and '.join(written))
|
|
342
|
+
|
|
343
|
+
if result.counts()[stack.UNRESOLVED] or result.counts()[stack.MISSING]:
|
|
344
|
+
raise SystemExit(2)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
if __name__ == '__main__':
|
|
348
|
+
main()
|
diskstack/errors.py
ADDED
diskstack/filler.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Detection of Greaseweazle's bad-sector filler inside decoded images.
|
|
2
|
+
|
|
3
|
+
A sector image has nowhere to record "this sector was unreadable", so
|
|
4
|
+
Greaseweazle writes ``-=[BAD SECTOR]=-`` repeated across the whole sector
|
|
5
|
+
instead. Reading such an image back gives a sector that looks perfectly
|
|
6
|
+
valid, which is the complaint in greaseweazle issues #97 and #565. diskstack
|
|
7
|
+
treats any sector that is exactly that repeated pattern as a failed read, so
|
|
8
|
+
another dump can supply the real bytes.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
FILLER = b'-=[BAD SECTOR]=-'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_filler(data: bytes) -> bool:
|
|
15
|
+
"""True if ``data`` is Greaseweazle's bad-sector filler and nothing else."""
|
|
16
|
+
n = len(data)
|
|
17
|
+
if n < len(FILLER) or n % len(FILLER) != 0:
|
|
18
|
+
return False
|
|
19
|
+
return bytes(data) == FILLER * (n // len(FILLER))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def make_filler(size: int) -> bytes:
|
|
23
|
+
"""Bad-sector filler for a sector of ``size`` bytes.
|
|
24
|
+
|
|
25
|
+
Matches Greaseweazle's own ``FILLER * (size // 16)``, which leaves a short
|
|
26
|
+
tail of zeroes when the sector size is not a multiple of 16.
|
|
27
|
+
"""
|
|
28
|
+
dat = FILLER * (size // len(FILLER))
|
|
29
|
+
return dat + bytes(size - len(dat))
|