Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/itunescdb-only-library.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@podkit/core': patch
'podkit': patch
---

Recognise an iPod library that only has the compressed `iTunesCDB`, as Apple software leaves it on an iPod nano 6th/7th generation. Such devices were reported as uninitialised, and `podkit device scan`/`sync` suggested `podkit device init`.
5 changes: 5 additions & 0 deletions .changeset/macos-compiled-binary-signature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'podkit': patch
---

Re-sign the compiled macOS binary ad hoc when Bun leaves an invalid signature behind, so the CLI no longer exits with status 137 on launch on Apple Silicon.
7 changes: 7 additions & 0 deletions .changeset/nano-7g-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@podkit/libgpod-node': patch
'@podkit/devices-ipod': minor
'podkit': minor
---

podkit now syncs the iPod nano 7th generation, confirmed on a device with firmware 1.0.4. Its libgpod build writes the hidden category containers and iTunes' schema level that the nano 7th generation needs to show a library, and keeps the plays, skips, ratings and bookmarks the device records across syncs. The nano 6th generation stays read-only: its library writes are covered only by synthetic tests, which its unsupported-device message now says.
5 changes: 5 additions & 0 deletions .changeset/nano-firmware-categories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@podkit/libgpod-node': patch
---

For the iPod nano 6th and 7th generation, libgpod now writes the hidden category containers (Music, Videos, …) and iTunes' database schema level that the nano 7th generation firmware needs to show a library, and keeps the Videos category's identity in the `iTunesCDB`. Such a write fails, instead of leaving a library the device cannot browse, when the device's SQLite post-process commands are missing or fail or when a database cannot be written completely.
7 changes: 7 additions & 0 deletions .changeset/nano-native-hashab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@podkit/libgpod-node': patch
'@podkit/devices-ipod': patch
'podkit': patch
---

podkit's libgpod build now signs iPod nano 6th and 7th generation databases itself, instead of needing an external `hashab` module. Signing alone does not make them writable: the nano 7th generation shows no music from a library written this way, although its signatures are valid, so both stay read-only with this change. Their support notes and unsupported-device messages now say so, instead of naming the missing module.
8 changes: 4 additions & 4 deletions .github/workflows/build-platform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
uses: actions/cache@v4
with:
path: ${{ env.STATIC_DEPS_DIR }}
key: static-deps-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('tools/prebuild/build-static-deps.sh') }}
key: static-deps-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('tools/prebuild/**') }}

# Install build toolchain (only when building prebuilds from scratch)

Expand Down Expand Up @@ -250,7 +250,7 @@ jobs:
uses: actions/cache@v4
with:
path: ${{ env.STATIC_DEPS_DIR }}
key: static-deps-linux-x64-${{ hashFiles('tools/prebuild/build-static-deps.sh') }}
key: static-deps-linux-x64-${{ hashFiles('tools/prebuild/**') }}

- name: Install Alpine build deps (full)
if: steps.prebuild-cache.outputs.cache-hit != 'true' && steps.static-cache.outputs.cache-hit != 'true'
Expand Down Expand Up @@ -448,7 +448,7 @@ jobs:
uses: actions/cache@v4
with:
path: static-deps
key: static-deps-linux-arm64-${{ hashFiles('tools/prebuild/build-static-deps.sh') }}
key: static-deps-linux-arm64-${{ hashFiles('tools/prebuild/**') }}

- name: Build prebuild in Alpine
if: steps.prebuild-cache.outputs.cache-hit != 'true'
Expand Down Expand Up @@ -685,7 +685,7 @@ jobs:
uses: actions/cache@v4
with:
path: ${{ env.STATIC_DEPS_DIR }}
key: static-deps-linux-${{ matrix.arch }}-gnu-${{ hashFiles('tools/prebuild/build-static-deps.sh', 'tools/prebuild/build-linux-glibc.sh') }}
key: static-deps-linux-${{ matrix.arch }}-gnu-${{ hashFiles('tools/prebuild/**') }}

# Full build toolchain. ubuntu:20.04 ships meson/ninja/cmake too old for
# recent glib/gdk-pixbuf/libxml2, so those come from pip, not apt.
Expand Down
16 changes: 14 additions & 2 deletions .github/workflows/prebuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
uses: actions/cache@v4
with:
path: ${{ env.STATIC_DEPS_DIR }}
key: static-deps-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('tools/prebuild/build-static-deps.sh', 'tools/prebuild/build-linux-glibc.sh') }}
key: static-deps-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('tools/prebuild/**') }}

# ---- Cache MISS: install full build toolchain and build from source ----

Expand Down Expand Up @@ -129,6 +129,13 @@ jobs:
# Linux glibc verification happens inside tools/prebuild/build-linux-glibc.sh
# (the shared script). No additional step needed here.

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Test libgpod nano HashAB signing
run: bash tools/nano7/test-libgpod.sh

- name: Run tests
working-directory: packages/libgpod-node
run: bun run test:unit
Expand Down Expand Up @@ -164,7 +171,7 @@ jobs:
uses: actions/cache@v4
with:
path: ${{ env.STATIC_DEPS_DIR }}
key: static-deps-linux-x64-musl-${{ hashFiles('tools/prebuild/build-static-deps.sh') }}
key: static-deps-linux-x64-musl-${{ hashFiles('tools/prebuild/**') }}

- name: Install Alpine build deps (full)
if: steps.cache.outputs.cache-hit != 'true'
Expand Down Expand Up @@ -217,6 +224,9 @@ jobs:
echo "ERROR: Found unexpected dynamic dependencies"; exit 1
fi

- name: Test libgpod nano HashAB signing
run: bash tools/nano7/test-libgpod.sh

- name: Run tests
working-directory: packages/libgpod-node
run: bun run test:unit
Expand Down Expand Up @@ -259,6 +269,8 @@ jobs:
curl -fsSL https://bun.sh/install | bash
bun install

bash tools/nano7/test-libgpod.sh

cd packages/libgpod-node
npx prebuildify --napi --strip
mv prebuilds/linux-arm64 prebuilds/linux-arm64-musl
Expand Down
9 changes: 7 additions & 2 deletions devices/ipod.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ iPod_Control/
| Shuffle 1-2G | Yes | No | No | No | — | None |
| Nano 1-2G | Yes | No | Yes | No | — | 176x132 |
| Nano 3-5G | Yes | Yes | Yes | Yes | ipod-nano-3g | Varies |
| Nano 7G | Yes | Yes | Yes | Yes | ipod-classic | 240x432 |
| Video 5G | Yes | Yes | Yes | Yes | ipod-video-5g | 320x240 |
| Video 5.5G | Yes | Yes | Yes | Yes | ipod-video-5g | 320x240 |
| Classic 6G | Yes | Yes | Yes | Yes | ipod-classic | 320x240 |
Expand All @@ -293,8 +294,7 @@ iPod_Control/
| Generation | Reason |
|------------|--------|
| Shuffle 3-4G | Buttonless; writing their iTunesSD playback database is unverified on hardware |
| Nano 6G | Writes an iTunesDB format libgpod cannot produce; read is untested |
| Nano 7G | Reads and archives cleanly (hardware-confirmed); writing needs the hashAB signature libgpod cannot produce without an external blob podkit doesn't ship |
| Nano 6G | Reads and archives (hardware-confirmed); its library writes are covered only by synthetic tests, not by hardware |
| Touch (all) | iOS protocol + cryptographic iTunesDB signing |
| iPhone (all) | iOS protocol + cryptographic iTunesDB signing |
| iPad | iOS protocol + cryptographic iTunesDB signing |
Expand Down Expand Up @@ -348,3 +348,8 @@ Fully implemented in podkit. Key implementation files:
- **Video handler:** `packages/podkit-core/src/sync/handlers/video-handler.ts`
- **Generation metadata:** `packages/podkit-core/src/ipod/generation.ts`
- **Model identification:** `packages/podkit-core/src/device/ipod-models.ts`

## Nano 6G/7G signing

The nano 6th and 7th generation sign `iTunesCDB` and `Locations.itdb.cbk` with hashAB, which podkit's libgpod builds compute ([ADR-030](../docs/adr/adr-030-native-nano-hashab.md); checks in `tools/nano7/`).
For them it also writes iTunes' category containers and schema level into the SQLite library and keeps the play data the device records (`tools/nano7/README.md`); the nano 7G syncs, the nano 6G stays read-only.
75 changes: 75 additions & 0 deletions docs/adr/adr-030-native-nano-hashab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
title: 'ADR-030: Link an Open HashAB Signer Into libgpod'
description: Build libgpod with the Unlicense dstaley/hashab C sources instead of loading an external hashab blob, so the iPod nano 6th/7th generation databases can be signed. Signing alone does not make those generations syncable.
sidebar:
order: 31
---

# ADR-030: Link an Open HashAB Signer Into libgpod

## Status

**Proposed** (2026-10-27)

## Context

The iPod nano 6th and 7th generation (SysInfoExtended `DBVersion` 5) sign
`iTunesCDB` and `iTunes Library.itlp/Locations.itdb.cbk` with HashAB.
libgpod 0.8.3 already writes both files, including the SQLite databases and
the artwork, but `itdb_hashAB.c` computes the signature only through a
`hashab` module loaded from `LIBGPOD_BLOB_DIR`, and fails the write without
it. podkit ships no such module.

[dstaley/hashab](https://github.com/dstaley/hashab) is an Unlicense C
implementation of the same `calcHashAB(target, sha1, uuid, rnd)` function,
with 100 reference vectors. It takes the eight-byte FireWire GUID of a nano,
not an iOS device identifier.

## Decision

- Vendor the signer unmodified in `tools/prebuild/vendor/hashab` at a pinned
commit, with its licence and provenance.
- `tools/prebuild/prepare-libgpod-nano.sh` applies
`tools/prebuild/libgpod-nano.patch` and copies the sources into
`src/nano_hashab/` before `autoreconf`, in both the macOS developer build
and the static prebuild dependencies. The patch replaces the module loader
with a direct call and builds the sources as an internal convenience
library, with `-fwrapv` as upstream does, and with their global symbols
renamed and hidden so that they neither clash with nor leak from libgpod.
- Before signing, the patch requires a 16-digit hexadecimal `FireWireGUID`
(an optional `0x` prefix is accepted) and otherwise fails the write with an
error. libgpod's CDB normalisation, SQLite generation and artwork writing
are unchanged.
- The static dependency cache records a fingerprint of the patch and the
vendored sources, and the CI cache keys hash all of `tools/prebuild/`, so a
change to either rebuilds libgpod.
- No runtime dependency, subprocess or N-API surface is added. Python is used
only by the developer checks in `tools/nano7/`.

## Verification

- The 100 upstream vectors and a third-party reference
`Locations.itdb`/`.cbk` pair reproduce byte for byte.
- `tools/nano7/test-libgpod.sh`, run in the prebuild workflow, lets the
patched libgpod create, reparse, resave and delete a synthetic track for a
nano 7th generation (from the repository's USB SysInfoExtended capture) and
a nano 6th generation (synthetic capabilities), and checks SQLite integrity
and both signatures. Malformed and iOS-sized GUIDs fail; a 5th generation
iPod still gets an unsigned `iTunesDB`.
- On a nano 7th generation (firmware 1.0.4), Apple-written `iTunesCDB` and
`Locations.itdb.cbk` verify with libgpod's normalisation (hashing scheme 3)
and the device GUID.

## Consequences

Signing is necessary but not sufficient. On a nano 7th generation (firmware
1.0.4), a library written by this libgpod has valid signatures, yet the
firmware shows no music. `nano_6g` and `nano_7g` therefore keep
`support.access = 'read-only'`; the nano 6th generation's writes are covered
only by synthetic tests. Enabling sync for either generation needs a complete
library write that is verified on the device.

The nano 7th generation became syncable once libgpod also writes the
firmware's category containers and iTunes' schema level and keeps the play
data the device records (`tools/nano7/README.md`); the nano 6th generation
stays read-only.
1 change: 1 addition & 0 deletions docs/adr/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ This section contains Architecture Decision Records (ADRs) documenting significa
| [ADR-026](./adr-026-dual-libc-linux-distribution.md) | Dual-libc Linux Distribution (glibc + musl) | Accepted | 2026-08-04 |
| [ADR-027](./adr-027-lima-vm-substrate-consolidation.md) | Lima VM Substrate Consolidation (`@podkit/lima`) | Accepted | 2026-08-24 |
| [ADR-028](./adr-028-substrate-agnostic-device-harness.md) | Substrate-Agnostic Device Harness | Accepted | 2026-09-07 |
| [ADR-030](./adr-030-native-nano-hashab.md) | Link an Open HashAB Signer Into libgpod | Proposed | 2026-10-27 |

## What is an ADR?

Expand Down
4 changes: 2 additions & 2 deletions docs/architecture/device/identity-support-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ is the fallback on USB *transport* error
| iPod nano 1G–2G | **no (2G does not respond; 1G untested)** | **yes** | SCSI (host only) |
| iPod nano 3G | **yes** (first USB-capable) | untested | USB |
| iPod nano 4G–5G | **yes** | yes | USB |
| iPod nano 6G | yes (inquiry) | yes | Inquiry works but the generation is **read-only** (iTunesDB format podkit cannot write) |
| iPod nano 7G | yes (inquiry) | yes | Inquiry works; the generation is **read-only**, hardware-confirmed (hashAB signing unavailable — libgpod needs an external blob podkit doesn't ship — but reads and archives cleanly) |
| iPod nano 6G | yes (inquiry) | yes | Inquiry works but the generation is **read-only**, hardware-confirmed (reads and archives; its library writes are covered only by synthetic tests) |
| iPod nano 7G | yes (inquiry) | yes | USB |
| iPod shuffle 1G–2G | no | **yes** | SCSI (host only) |
| iPod shuffle 3G–4G | yes | yes | Inquiry works but sync is **refused** (bdhs iTunesSD write unverified; read-only tier) |
| iPod touch / iPhone / iPad | no | no | No disk mode at all — out of scope |
Expand Down
19 changes: 6 additions & 13 deletions docs/formats/generations.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ after changing the table, then regenerate this block from
| iPod nano (3rd Generation) | `nano_3g` | syncable | inferred | — |
| iPod nano (4th Generation) | `nano_4g` | syncable | inferred | — |
| iPod nano (5th Generation) | `nano_5g` | syncable | inferred | — |
| iPod nano (6th Generation) | `nano_6g` | read-only | hardware | Reads and archives (259 tracks confirmed on hardware); writing needs an iTunesDB format libgpod cannot produce. |
| iPod nano (7th Generation) | `nano_7g` | read-only | hardware | Reads iTunesDB (1,414 tracks confirmed) and archives cleanly; writing needs the hashAB signature libgpod cannot produce without an external blob podkit does not ship. |
| iPod nano (6th Generation) | `nano_6g` | read-only | hardware | Reads and archives (259 tracks confirmed on hardware); its library writes are covered only by synthetic tests, not by hardware. |
| iPod nano (7th Generation) | `nano_7g` | syncable | hardware | Syncs (confirmed on hardware, firmware 1.0.4); reads iTunesDB (1,414 tracks confirmed on hardware) and archives cleanly. |
| iPod shuffle (1st Generation) | `shuffle_1g` | syncable | inferred | — |
| iPod shuffle (2nd Generation) | `shuffle_2g` | syncable | inferred | — |
| iPod shuffle (3rd Generation) | `shuffle_3g` | read-only | hardware | Reads iTunesDB; writing the bdhs iTunesSD playback DB is unverified on hardware. |
Expand Down Expand Up @@ -111,17 +111,10 @@ after changing the table, then regenerate this block from
shown to leave a playable device, so podkit does not attempt it. Both are
`hardware`-confirmed.
See [itunessd-bdhs.md](itunessd-bdhs.md).
- **nano 6g — `read-only`.** Its write is a format libgpod cannot produce, but
its read is merely *untested* — and a read is non-destructive, so the tier
permits the attempt rather than forbidding a safe operation on a guess.
- **nano 7g — `read-only`, hardware-confirmed.** libgpod opens its classic
iTunesCDB `iTunesDB` fine — a real nano 7G read 1,414 tracks and archived
cleanly. Writing needs a hashAB signature, which libgpod only computes via
an external `hashab` blob loaded through `LIBGPOD_BLOB_DIR`
(`itdb_hashAB.c`); podkit ships no such blob, so the write path fails
closed. This corrects an earlier claim that nano 7g had no libgpod table
entry at all — it reads fine; only the write is blocked, and for a
different reason.
- **nano 6g — `read-only`, hardware-confirmed.** A real nano 6G read and
archived 259 tracks. podkit's libgpod writes its library like the nano 7g's
([ADR-030](../adr/adr-030-native-nano-hashab.md), `tools/nano7/`), but
only synthetic tests cover that.
- **iPod touch (all), not-in-libgpod — `none`.** No mountable database, or
Apple's proprietary sync protocol that podkit cannot speak over disk mode.

Expand Down
2 changes: 2 additions & 0 deletions docs/test-devices.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ archiving work fine.
| Filesystem | Confirms readability | Device also carries `iTunes Library.itlp`, the SQLite database the firmware plays from — separate from the libgpod-readable `iTunesCDB`. |
| Sync (write) | **Genuinely refused** | Not a false refusal this time: `checksumType: 'hashAB'`, and libgpod does not implement hashAB signing. `itdb_hashAB.c:43-68` (`load_libhashab`) `g_module_open`s an external `hashab` blob from `LIBGPOD_BLOB_DIR` and fails closed when the symbol is absent. podkit ships no such blob (not referenced by `tools/prebuild/*.sh` or `tools/libgpod-macos/build.sh`, none installed). An unsigned database is rejected by the firmware. |

The hashAB part of this refusal is superseded by [ADR-030](adr/adr-030-native-nano-hashab.md).

**Why this unit matters:** the prior `access: 'none'` entry for nano 7G was
itself wrong — it read "not in libgpod's `ipod_info_table`" as "no mountable
database," but the two are unrelated. libgpod's classic iTunesCDB reader works
Expand Down
8 changes: 4 additions & 4 deletions packages/devices-ipod/src/classify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,14 @@ describe('classifyAsIpod — unsupported iPod-family devices', () => {
const result = classifyAsIpod({ vendorId: '05ac', productId: '120d' });
expect(result).not.toBeNull();
expect(result!.supported).toBe(false);
expect(result!.unsupportedReason?.headline).toContain('iTunesDB format');
expect(result!.unsupportedReason?.headline).toContain('not verified on hardware');
});

it('classifies iPod nano 7G as unsupported (0x05ac:0x1267)', () => {
it('classifies iPod nano 7G as supported (0x05ac:0x1267)', () => {
const result = classifyAsIpod({ vendorId: '05ac', productId: '1267' });
expect(result).not.toBeNull();
expect(result!.supported).toBe(false);
expect(result!.unsupportedReason?.headline).toMatch(/nano 7th gen/i);
expect(result!.supported).toBe(true);
expect(result!.unsupportedReason).toBeUndefined();
});

it('classifies iPod touch 5G as unsupported via known table (0x05ac:0x12aa)', () => {
Expand Down
Loading