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.
Files changed (26) hide show
  1. {flashpod-0.2.1 → flashpod-0.2.3}/PKG-INFO +1 -1
  2. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/ipod_flash.py +45 -11
  3. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/platform/macos.py +16 -20
  4. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/platform/windows.py +78 -33
  5. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod.egg-info/PKG-INFO +1 -1
  6. {flashpod-0.2.1 → flashpod-0.2.3}/pyproject.toml +1 -1
  7. {flashpod-0.2.1 → flashpod-0.2.3}/LICENSE +0 -0
  8. {flashpod-0.2.1 → flashpod-0.2.3}/README.md +0 -0
  9. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/__init__.py +0 -0
  10. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/__main__.py +0 -0
  11. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/cli.py +0 -0
  12. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/contrib/99-flashpod-firewire-ipod.rules +0 -0
  13. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/fat32.py +0 -0
  14. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/fatfs.py +0 -0
  15. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/firmware/firmware.json +0 -0
  16. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/itunesdb.py +0 -0
  17. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/platform/__init__.py +0 -0
  18. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/platform/base.py +0 -0
  19. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/platform/linux.py +0 -0
  20. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod/resources.py +0 -0
  21. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod.egg-info/SOURCES.txt +0 -0
  22. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod.egg-info/dependency_links.txt +0 -0
  23. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod.egg-info/entry_points.txt +0 -0
  24. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod.egg-info/requires.txt +0 -0
  25. {flashpod-0.2.1 → flashpod-0.2.3}/flashpod.egg-info/top_level.txt +0 -0
  26. {flashpod-0.2.1 → flashpod-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashpod
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Command-line iPod sync + card-flashing tooling for early FireWire-era iPods, pure Python
5
5
  Author: David Barnhart
6
6
  License-Expression: MIT
@@ -397,10 +397,44 @@ def device_mountpoints(dev):
397
397
  # ----------------------------------------------------------------------------
398
398
  # Interactive selection + confirmation
399
399
  # ----------------------------------------------------------------------------
400
- def choose_device():
401
- cands = list_candidates()
402
- if not cands:
403
- sys.exit(color("No removable/USB disks found. Plug in the card and retry.", C_RED))
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
- while True:
446
- sel = input(color("Select device number (or 'q' to quit): ", C_CYN)).strip()
447
- if sel.lower() in ("q", "quit", ""):
448
- sys.exit("Aborted.")
449
- if sel.isdigit() and int(sel) < len(cands):
450
- return "/dev/" + cands[int(sel)]["name"]
451
- print(color(" invalid selection", C_RED), file=sys.stderr)
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
- disks = self._external_disks()
77
- if not disks:
78
- sys.exit(color("No external disks found. Plug in the card reader and retry.",
79
- ipod_flash.C_RED))
80
- print(color("\nAttached external disks:\n", ipod_flash.C_CYN), file=sys.stderr)
81
- for i, (d, info) in enumerate(disks):
82
- size = int(info.get("TotalSize") or info.get("Size") or 0)
83
- name = (info.get("MediaName") or info.get("IORegistryEntryName")
84
- or "").strip() or "disk"
85
- print(" [%d] /dev/%-7s %10s %s" %
86
- (i, d, ipod_flash.fmt_size(size), name), file=sys.stderr)
87
- print(file=sys.stderr)
88
- while True:
89
- sel = input(color("Select device number (or 'q' to quit): ",
90
- ipod_flash.C_CYN)).strip()
91
- if sel.lower() in ("q", "quit", ""):
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 = ctypes.c_void_p(-1).value if False else (2 ** 64 - 1)
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 = ctypes.windll.kernel32.CreateFileW(
78
- ctypes.c_wchar_p(path), access,
79
- FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None)
80
- if h == INVALID_HANDLE_VALUE or h is None:
81
- raise OSError("CreateFileW failed for %s (err %d)"
82
- % (path, ctypes.windll.kernel32.GetLastError()))
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 = ctypes.windll.kernel32.DeviceIoControl(
89
- handle, code, None, 0, buf, out_size,
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 (err %d)"
93
- % (code, ctypes.windll.kernel32.GetLastError()))
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
- cands = [d for d in self._disks()
116
- if not d[4] and d[3] in ("USB", "SD", "MMC", "1394")]
117
- if not cands:
118
- cands = [d for d in self._disks() if not d[4]]
119
- if not cands:
120
- sys.exit(color("No removable disks found. Plug in the card and retry.",
121
- ipod_flash.C_RED))
122
- print(color("\nAttached removable disks:\n", ipod_flash.C_CYN), file=sys.stderr)
123
- for i, (num, size, name, bus, _sys) in enumerate(cands):
124
- print(" [%d] \\\\.\\PhysicalDrive%-3d %10s %s (%s)" %
125
- (i, num, ipod_flash.fmt_size(size), name.strip(), bus), file=sys.stderr)
126
- print(file=sys.stderr)
127
- while True:
128
- sel = input(color("Select device number (or 'q' to quit): ",
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
- h = self._open_handle(dev, write=("w" in mode or "+" in mode or "a" in mode))
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashpod
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Command-line iPod sync + card-flashing tooling for early FireWire-era iPods, pure Python
5
5
  Author: David Barnhart
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flashpod"
7
- version = "0.2.1"
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