flashpod 0.2.1__tar.gz → 0.2.3__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.
- {flashpod-0.2.1 → flashpod-0.2.3}/PKG-INFO +1 -1
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/ipod_flash.py +45 -11
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/platform/macos.py +16 -20
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/platform/windows.py +78 -33
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod.egg-info/PKG-INFO +1 -1
- {flashpod-0.2.1 → flashpod-0.2.3}/pyproject.toml +1 -1
- {flashpod-0.2.1 → flashpod-0.2.3}/LICENSE +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/README.md +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/__init__.py +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/__main__.py +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/cli.py +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/contrib/99-flashpod-firewire-ipod.rules +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/fat32.py +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/fatfs.py +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/firmware/firmware.json +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/itunesdb.py +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/platform/__init__.py +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/platform/base.py +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/platform/linux.py +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/resources.py +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod.egg-info/SOURCES.txt +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod.egg-info/dependency_links.txt +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod.egg-info/entry_points.txt +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod.egg-info/requires.txt +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/flashpod.egg-info/top_level.txt +0 -0
- {flashpod-0.2.1 → flashpod-0.2.3}/setup.cfg +0 -0
|
@@ -397,10 +397,44 @@ def device_mountpoints(dev):
|
|
|
397
397
|
# ----------------------------------------------------------------------------
|
|
398
398
|
# Interactive selection + confirmation
|
|
399
399
|
# ----------------------------------------------------------------------------
|
|
400
|
-
def
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
400
|
+
def pick_device(scan, render, path_of, empty_msg):
|
|
401
|
+
"""Interactive device chooser shared by every platform backend.
|
|
402
|
+
|
|
403
|
+
``scan()`` returns the candidate list (called again on each refresh),
|
|
404
|
+
``render(items)`` prints the table, ``path_of(item)`` yields the device path
|
|
405
|
+
to return.
|
|
406
|
+
|
|
407
|
+
The loop accepts 'r' to re-scan. Without it, a reader plugged in — or a card
|
|
408
|
+
inserted — after the list was drawn stays invisible until you quit and start
|
|
409
|
+
the whole command over, which is a long way back when you've already picked
|
|
410
|
+
a model and firmware.
|
|
411
|
+
"""
|
|
412
|
+
items = scan()
|
|
413
|
+
if not items:
|
|
414
|
+
sys.exit(color(empty_msg, C_RED))
|
|
415
|
+
render(items)
|
|
416
|
+
while True:
|
|
417
|
+
try:
|
|
418
|
+
sel = input(color("Select device number "
|
|
419
|
+
"('r' to rescan, 'q' to quit): ", C_CYN)).strip().lower()
|
|
420
|
+
except (EOFError, KeyboardInterrupt):
|
|
421
|
+
print(file=sys.stderr)
|
|
422
|
+
sys.exit("Aborted.")
|
|
423
|
+
if sel in ("q", "quit", ""):
|
|
424
|
+
sys.exit("Aborted.")
|
|
425
|
+
if sel in ("r", "refresh", "rescan"):
|
|
426
|
+
items = scan()
|
|
427
|
+
if not items:
|
|
428
|
+
print(color(" " + empty_msg, C_YEL), file=sys.stderr)
|
|
429
|
+
else:
|
|
430
|
+
render(items)
|
|
431
|
+
continue
|
|
432
|
+
if sel.isdigit() and int(sel) < len(items):
|
|
433
|
+
return path_of(items[int(sel)])
|
|
434
|
+
print(color(" invalid selection", C_RED), file=sys.stderr)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _render_candidates(cands):
|
|
404
438
|
print(color("\nAttached removable storage:\n", C_CYN), file=sys.stderr)
|
|
405
439
|
# Multi-slot readers expose one /dev/sdX per slot, all with the same
|
|
406
440
|
# identity strings; number the slots so they can be told apart.
|
|
@@ -442,13 +476,13 @@ def choose_device():
|
|
|
442
476
|
if r["warn"]:
|
|
443
477
|
print(color(" " * sub + "⚠ " + r["warn"], C_YEL), file=sys.stderr)
|
|
444
478
|
print(file=sys.stderr)
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def choose_device():
|
|
482
|
+
return pick_device(
|
|
483
|
+
list_candidates, _render_candidates,
|
|
484
|
+
lambda d: "/dev/" + d["name"],
|
|
485
|
+
"No removable/USB disks found. Plug in the card and retry.")
|
|
452
486
|
|
|
453
487
|
def device_label(dev):
|
|
454
488
|
"""Short, typeable name for a device, used in the ERASE confirmation.
|
|
@@ -73,26 +73,22 @@ class MacOSPlatform(Platform):
|
|
|
73
73
|
def choose_device(self):
|
|
74
74
|
from .. import ipod_flash
|
|
75
75
|
color = ipod_flash.color
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
sys.exit("Aborted.")
|
|
93
|
-
if sel.isdigit() and int(sel) < len(disks):
|
|
94
|
-
return "/dev/" + disks[int(sel)][0]
|
|
95
|
-
print(color(" invalid selection", ipod_flash.C_RED), file=sys.stderr)
|
|
76
|
+
|
|
77
|
+
def render(disks):
|
|
78
|
+
print(color("\nAttached external disks:\n", ipod_flash.C_CYN),
|
|
79
|
+
file=sys.stderr)
|
|
80
|
+
for i, (d, info) in enumerate(disks):
|
|
81
|
+
size = int(info.get("TotalSize") or info.get("Size") or 0)
|
|
82
|
+
name = (info.get("MediaName") or info.get("IORegistryEntryName")
|
|
83
|
+
or "").strip() or "disk"
|
|
84
|
+
print(" [%d] /dev/%-7s %10s %s" %
|
|
85
|
+
(i, d, ipod_flash.fmt_size(size), name), file=sys.stderr)
|
|
86
|
+
print(file=sys.stderr)
|
|
87
|
+
|
|
88
|
+
return ipod_flash.pick_device(
|
|
89
|
+
self._external_disks, render,
|
|
90
|
+
lambda item: "/dev/" + item[0],
|
|
91
|
+
"No external disks found. Plug in the card reader and retry.")
|
|
96
92
|
|
|
97
93
|
def device_sectors(self, dev):
|
|
98
94
|
# real file/image -> just its size
|
|
@@ -35,7 +35,38 @@ GENERIC_WRITE = 0x40000000
|
|
|
35
35
|
FILE_SHARE_READ = 0x00000001
|
|
36
36
|
FILE_SHARE_WRITE = 0x00000002
|
|
37
37
|
OPEN_EXISTING = 3
|
|
38
|
-
INVALID_HANDLE_VALUE
|
|
38
|
+
# INVALID_HANDLE_VALUE is (HANDLE)-1, so its unsigned value is pointer-sized:
|
|
39
|
+
# 2**64-1 on 64-bit Python, 2**32-1 on 32-bit. Derive it rather than assuming.
|
|
40
|
+
INVALID_HANDLE_VALUE = (1 << (8 * ctypes.sizeof(ctypes.c_void_p))) - 1
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _k32():
|
|
44
|
+
"""kernel32 with correct prototypes for the handle APIs.
|
|
45
|
+
|
|
46
|
+
Declaring these matters: ctypes defaults restype to ``c_int``, but a HANDLE
|
|
47
|
+
is pointer-sized. On 64-bit Windows that truncates the handle, and — far
|
|
48
|
+
worse — a *failed* CreateFileW returns INVALID_HANDLE_VALUE, which as a
|
|
49
|
+
signed int is -1 and never equals the unsigned constant we compare against.
|
|
50
|
+
The failure then goes unnoticed and the bogus handle reaches
|
|
51
|
+
msvcrt.open_osfhandle, whose fd raises EBADF ("Bad file descriptor") on
|
|
52
|
+
first use — hiding the actual Windows error.
|
|
53
|
+
|
|
54
|
+
Re-declaring on every call is harmless and keeps this import-safe on
|
|
55
|
+
non-Windows (nothing here runs until a method is called).
|
|
56
|
+
"""
|
|
57
|
+
k = ctypes.windll.kernel32
|
|
58
|
+
k.CreateFileW.restype = ctypes.c_void_p
|
|
59
|
+
k.CreateFileW.argtypes = [ctypes.c_wchar_p, ctypes.c_uint32,
|
|
60
|
+
ctypes.c_uint32, ctypes.c_void_p,
|
|
61
|
+
ctypes.c_uint32, ctypes.c_uint32,
|
|
62
|
+
ctypes.c_void_p]
|
|
63
|
+
k.CloseHandle.argtypes = [ctypes.c_void_p]
|
|
64
|
+
k.DeviceIoControl.argtypes = [ctypes.c_void_p, ctypes.c_uint32,
|
|
65
|
+
ctypes.c_void_p, ctypes.c_uint32,
|
|
66
|
+
ctypes.c_void_p, ctypes.c_uint32,
|
|
67
|
+
ctypes.POINTER(ctypes.c_ulong),
|
|
68
|
+
ctypes.c_void_p]
|
|
69
|
+
return k
|
|
39
70
|
|
|
40
71
|
|
|
41
72
|
def _powershell(script):
|
|
@@ -73,24 +104,28 @@ class WindowsPlatform(Platform):
|
|
|
73
104
|
|
|
74
105
|
# -- low-level handle helpers -----------------------------------------
|
|
75
106
|
def _open_handle(self, path, write):
|
|
107
|
+
k = _k32()
|
|
76
108
|
access = GENERIC_READ | (GENERIC_WRITE if write else 0)
|
|
77
|
-
h =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if h
|
|
81
|
-
|
|
82
|
-
|
|
109
|
+
h = k.CreateFileW(path, access,
|
|
110
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE, None,
|
|
111
|
+
OPEN_EXISTING, 0, None)
|
|
112
|
+
if h is None or h == INVALID_HANDLE_VALUE:
|
|
113
|
+
# Report what Windows actually said: error 5 (access denied) and 32
|
|
114
|
+
# (sharing violation) are the usual causes here, and both mean a
|
|
115
|
+
# volume on this disk is mounted and holding the sectors.
|
|
116
|
+
raise OSError("CreateFileW failed for %s: %s"
|
|
117
|
+
% (path, ctypes.WinError(k.GetLastError())))
|
|
83
118
|
return h
|
|
84
119
|
|
|
85
120
|
def _ioctl(self, handle, code, out_size=0):
|
|
121
|
+
k = _k32()
|
|
86
122
|
buf = ctypes.create_string_buffer(out_size) if out_size else None
|
|
87
123
|
returned = ctypes.c_ulong(0)
|
|
88
|
-
ok =
|
|
89
|
-
|
|
90
|
-
ctypes.byref(returned), None)
|
|
124
|
+
ok = k.DeviceIoControl(handle, code, None, 0, buf, out_size,
|
|
125
|
+
ctypes.byref(returned), None)
|
|
91
126
|
if not ok:
|
|
92
|
-
raise OSError("DeviceIoControl 0x%X failed
|
|
93
|
-
% (code, ctypes.
|
|
127
|
+
raise OSError("DeviceIoControl 0x%X failed: %s"
|
|
128
|
+
% (code, ctypes.WinError(k.GetLastError())))
|
|
94
129
|
return buf.raw[:returned.value] if buf else b""
|
|
95
130
|
|
|
96
131
|
# -- device discovery / selection -------------------------------------
|
|
@@ -109,29 +144,31 @@ class WindowsPlatform(Platform):
|
|
|
109
144
|
f[4].strip().lower() == "true" or f[5].strip().lower() == "true"))
|
|
110
145
|
return disks
|
|
111
146
|
|
|
147
|
+
def _removable_disks(self):
|
|
148
|
+
"""Removable/USB disks worth offering, falling back to every non-system
|
|
149
|
+
disk when nothing matches the removable bus types."""
|
|
150
|
+
disks = self._disks()
|
|
151
|
+
cands = [d for d in disks
|
|
152
|
+
if not d[4] and d[3] in ("USB", "SD", "MMC", "1394")]
|
|
153
|
+
return cands or [d for d in disks if not d[4]]
|
|
154
|
+
|
|
112
155
|
def choose_device(self):
|
|
113
156
|
from .. import ipod_flash
|
|
114
157
|
color = ipod_flash.color
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
ipod_flash.C_CYN)).strip()
|
|
130
|
-
if sel.lower() in ("q", "quit", ""):
|
|
131
|
-
sys.exit("Aborted.")
|
|
132
|
-
if sel.isdigit() and int(sel) < len(cands):
|
|
133
|
-
return "\\\\.\\PhysicalDrive%d" % cands[int(sel)][0]
|
|
134
|
-
print(color(" invalid selection", ipod_flash.C_RED), file=sys.stderr)
|
|
158
|
+
|
|
159
|
+
def render(cands):
|
|
160
|
+
print(color("\nAttached removable disks:\n", ipod_flash.C_CYN),
|
|
161
|
+
file=sys.stderr)
|
|
162
|
+
for i, (num, size, name, bus, _sys) in enumerate(cands):
|
|
163
|
+
print(" [%d] \\\\.\\PhysicalDrive%-3d %10s %s (%s)" %
|
|
164
|
+
(i, num, ipod_flash.fmt_size(size), name.strip(), bus),
|
|
165
|
+
file=sys.stderr)
|
|
166
|
+
print(file=sys.stderr)
|
|
167
|
+
|
|
168
|
+
return ipod_flash.pick_device(
|
|
169
|
+
self._removable_disks, render,
|
|
170
|
+
lambda d: "\\\\.\\PhysicalDrive%d" % d[0],
|
|
171
|
+
"No removable disks found. Plug in the card and retry.")
|
|
135
172
|
|
|
136
173
|
def device_sectors(self, dev):
|
|
137
174
|
if os.path.isfile(dev):
|
|
@@ -237,7 +274,15 @@ class WindowsPlatform(Platform):
|
|
|
237
274
|
if os.path.isfile(dev):
|
|
238
275
|
return open(dev, mode)
|
|
239
276
|
import msvcrt
|
|
240
|
-
|
|
277
|
+
write = ("w" in mode or "+" in mode or "a" in mode)
|
|
278
|
+
if write:
|
|
279
|
+
# FSCTL_LOCK_VOLUME only holds while the locking handle is open, so
|
|
280
|
+
# the dismount done before write_layout is already undone by now:
|
|
281
|
+
# writing the new MBR makes Windows rescan and re-mount the data
|
|
282
|
+
# partition, and it then refuses writes into those sectors. Dismount
|
|
283
|
+
# again immediately before each write open.
|
|
284
|
+
self.unmount_all(dev, False)
|
|
285
|
+
h = self._open_handle(dev, write=write)
|
|
241
286
|
fd = msvcrt.open_osfhandle(h, os.O_BINARY)
|
|
242
287
|
return AlignedRawIO(os.fdopen(fd, mode, buffering=0))
|
|
243
288
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flashpod"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "Command-line iPod sync + card-flashing tooling for early FireWire-era iPods, pure Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.6"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|