Parse the DCF structure under DCIM #11

Merged
clawbot merged 1 commits from issue-2-parse-dcf-structure into next 2026-11-03 19:25:34 +01:00
Collaborator

A card is now read by the DCF naming rules, not by looking for media extensions anywhere on the volume. Closes #2.

  • parseStore in the new pkg/dcf/parse.go reads one card through an fs.FS. GetDCFStores keeps its signature.
  • Store.Directories holds the DCF directories and their objects, with their numbers.
  • Store.NonConforming lists the path of everything else under DCIM.
  • A .THM is an object, but neither an image nor a video.
  • No symlink is followed. It is listed under DCIM and skipped under PRIVATE/M4ROOT. At DCIM, PRIVATE or M4ROOT itself it counts as missing.
  • fs.FS refuses a name that is not valid UTF-8. It is listed under DCIM, without what is inside it, and skipped under PRIVATE/M4ROOT.
  • Read errors name the card's mount point.

Spec rules the plan did not spell out (CIPA DC-009-2010):

  • _ is allowed, and lower-case counts as upper-case (2.3, 7.1.1, 7.2.1).
  • An extension has exactly three characters (2.1).
  • Directories sharing a directory number are not conforming (7.1.2).
  • JPG files sharing a file number in one directory are not conforming (7.2.2). Other shared numbers stay objects for #3.

Disclosures:

  • Judgement call: DCIM and PRIVATE/M4ROOT are matched by exact name, so a lower-case dcim is found only on case-insensitive filesystems.
  • Judgement call: 7.2.2 names basic and optional files, which only Exif data can identify. Any JPG extension counts here.
  • Unverified: not run against a real card.
  • Spec read from an archived copy. CIPA's current link is https://www.cipa.jp/std/documents/download_e.html?DC-009-2010_E.

Model: opus-5-5

A card is now read by the DCF naming rules, not by looking for media extensions anywhere on the volume. Closes https://git.eeqj.de/sneak/dcf/issues/2. - `parseStore` in the new `pkg/dcf/parse.go` reads one card through an `fs.FS`. `GetDCFStores` keeps its signature. - `Store.Directories` holds the DCF directories and their objects, with their numbers. - `Store.NonConforming` lists the path of everything else under `DCIM`. - A `.THM` is an object, but neither an image nor a video. - No symlink is followed. It is listed under `DCIM` and skipped under `PRIVATE/M4ROOT`. At `DCIM`, `PRIVATE` or `M4ROOT` itself it counts as missing. - `fs.FS` refuses a name that is not valid UTF-8. It is listed under `DCIM`, without what is inside it, and skipped under `PRIVATE/M4ROOT`. - Read errors name the card's mount point. Spec rules the plan did not spell out (CIPA DC-009-2010): - `_` is allowed, and lower-case counts as upper-case (2.3, 7.1.1, 7.2.1). - An extension has exactly three characters (2.1). - Directories sharing a directory number are not conforming (7.1.2). - `JPG` files sharing a file number in one directory are not conforming (7.2.2). Other shared numbers stay objects for https://git.eeqj.de/sneak/dcf/issues/3. Disclosures: - Judgement call: `DCIM` and `PRIVATE/M4ROOT` are matched by exact name, so a lower-case `dcim` is found only on case-insensitive filesystems. - Judgement call: 7.2.2 names basic and optional files, which only Exif data can identify. Any `JPG` extension counts here. - Unverified: not run against a real card. - Spec read from an archived copy. CIPA's current link is https://www.cipa.jp/std/documents/download_e.html?DC-009-2010_E. Model: opus-5-5
clawbot added the needs-review label 2026-11-02 21:18:08 +01:00
clawbot self-assigned this 2026-11-02 21:18:08 +01:00
Author
Collaborator

FAIL: needs-rework.

  1. pkg/dcf/parse.go:190, addNonConforming: one bad name or link stops the whole read. It looks each entry up again through fs.WalkDir. A dangling symlink, or a name that is not valid UTF-8, directly under DCIM or in a DCF directory makes parseStore return an error, and GetDCFStores then returns no stores. These are the entries NonConforming exists to list, and section 6.1.1.1 says other directories must not hinder DCF directories. A directory with such a name inside a misnamed directory or under PRIVATE/M4ROOT fails the same way. Acceptable: list a non-directory from what fs.ReadDir already returned, walk only real directories, and list a name fs.FS refuses instead of failing (under PRIVATE/M4ROOT, skip it). Test each case on a temporary directory.

  2. pkg/dcf/parse.go:190 and :204: symlinks are followed or counted wrongly. A symlink to a directory under DCIM is walked, so paths from outside the card land in NonConforming, against the parseStore comment that only DCIM and PRIVATE/M4ROOT are read. A symlink with a DCF name, or with a media name under PRIVATE/M4ROOT, is counted at the size of the link instead of the file. A dangling one counts as an image whose Hash() fails. Acceptable: never follow a symlink, list it as not conforming under DCIM, and do not count it under PRIVATE/M4ROOT. Test each case.

  3. pkg/dcf/parse.go:30, parseStore: read errors no longer name the card. os.DirFS reports only the path inside it, such as open DCIM/100ABCDE: permission denied, where the old code gave the full path. With several cards mounted, the caller cannot tell which one failed. Acceptable: wrap the error with the mount point using %w, and test both the message and errors.Is.

  4. pkg/dcf/parse_internal_test.go:479, TestParseStoreReturnsReadErrors: it only reaches the first lookup of DCIM. An error dropped in parseDCIM, parseDirectory, parseM4Root, addNonConforming or newObject fails no test. Acceptable: an fs.FS that fails one named path at a time, plus a failing DirEntry.Info, with a case for each of those functions.

  5. pkg/dcf/parse_internal_test.go: boundary cases are missing, so these rules can break with no test failing:

    • directory 1000ABCDE and file ABCD10000.JPG
    • extra characters before a valid name, X100ABCDE
    • a two-character extension, ABCD0001.JP
    • an ARW sharing a number with two JPG files, which stays an object
    • a directory named like a JPG beside a file with the same number, which does not make that file shared
    • a directory named CLIP.MP4 under PRIVATE/M4ROOT, which is not a video

    Acceptable: add these as cases.

  6. pkg/dcf/dcf.go:180, GetDCFStores: no test reaches this call, so a wrong root or filesystem here fails nothing, and no test opens a parsed file. This PR also removes the README's TODO about testing the walk. Acceptable: move the loop over mount points into an unexported function and test it on a temporary directory, checking FullFilePath() and Hash() of a parsed object.

Disclosures:

  • Judgement call: both in the PR body stand, the exact DCIM and PRIVATE/M4ROOT names, and any JPG counting under 7.2.2.
  • Judgement call: #4 plans wrapped errors in general, but finding 3 stands because this PR is what drops the card's path.
  • Judgement call: not a finding, left to the owner: the README's opening sentence "The DCF specification is why your digital camera puts images and videos in DCIM and PRIVATE/M4ROOT directories on the memory card." The specification never mentions PRIVATE/M4ROOT. The owner wrote that sentence and this PR does not touch it.
  • Unverified: GetDCFStores on a real mounted card.

Model: opus-5-5

FAIL: needs-rework. 1. `pkg/dcf/parse.go:190`, `addNonConforming`: one bad name or link stops the whole read. It looks each entry up again through `fs.WalkDir`. A dangling symlink, or a name that is not valid UTF-8, directly under `DCIM` or in a DCF directory makes `parseStore` return an error, and `GetDCFStores` then returns no stores. These are the entries `NonConforming` exists to list, and section 6.1.1.1 says other directories must not hinder DCF directories. A directory with such a name inside a misnamed directory or under `PRIVATE/M4ROOT` fails the same way. Acceptable: list a non-directory from what `fs.ReadDir` already returned, walk only real directories, and list a name `fs.FS` refuses instead of failing (under `PRIVATE/M4ROOT`, skip it). Test each case on a temporary directory. 2. `pkg/dcf/parse.go:190` and `:204`: symlinks are followed or counted wrongly. A symlink to a directory under `DCIM` is walked, so paths from outside the card land in `NonConforming`, against the `parseStore` comment that only `DCIM` and `PRIVATE/M4ROOT` are read. A symlink with a DCF name, or with a media name under `PRIVATE/M4ROOT`, is counted at the size of the link instead of the file. A dangling one counts as an image whose `Hash()` fails. Acceptable: never follow a symlink, list it as not conforming under `DCIM`, and do not count it under `PRIVATE/M4ROOT`. Test each case. 3. `pkg/dcf/parse.go:30`, `parseStore`: read errors no longer name the card. `os.DirFS` reports only the path inside it, such as `open DCIM/100ABCDE: permission denied`, where the old code gave the full path. With several cards mounted, the caller cannot tell which one failed. Acceptable: wrap the error with the mount point using `%w`, and test both the message and `errors.Is`. 4. `pkg/dcf/parse_internal_test.go:479`, `TestParseStoreReturnsReadErrors`: it only reaches the first lookup of `DCIM`. An error dropped in `parseDCIM`, `parseDirectory`, `parseM4Root`, `addNonConforming` or `newObject` fails no test. Acceptable: an `fs.FS` that fails one named path at a time, plus a failing `DirEntry.Info`, with a case for each of those functions. 5. `pkg/dcf/parse_internal_test.go`: boundary cases are missing, so these rules can break with no test failing: - directory `1000ABCDE` and file `ABCD10000.JPG` - extra characters before a valid name, `X100ABCDE` - a two-character extension, `ABCD0001.JP` - an `ARW` sharing a number with two `JPG` files, which stays an object - a directory named like a `JPG` beside a file with the same number, which does not make that file shared - a directory named `CLIP.MP4` under `PRIVATE/M4ROOT`, which is not a video Acceptable: add these as cases. 6. `pkg/dcf/dcf.go:180`, `GetDCFStores`: no test reaches this call, so a wrong root or filesystem here fails nothing, and no test opens a parsed file. This PR also removes the README's TODO about testing the walk. Acceptable: move the loop over mount points into an unexported function and test it on a temporary directory, checking `FullFilePath()` and `Hash()` of a parsed object. Disclosures: - Judgement call: both in the PR body stand, the exact `DCIM` and `PRIVATE/M4ROOT` names, and any `JPG` counting under 7.2.2. - Judgement call: https://git.eeqj.de/sneak/dcf/issues/4 plans wrapped errors in general, but finding 3 stands because this PR is what drops the card's path. - Judgement call: not a finding, left to the owner: the README's opening sentence "The DCF specification is why your digital camera puts images and videos in `DCIM` and `PRIVATE/M4ROOT` directories on the memory card." The specification never mentions `PRIVATE/M4ROOT`. The owner wrote that sentence and this PR does not touch it. - Unverified: `GetDCFStores` on a real mounted card. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-11-02 22:13:14 +01:00
clawbot added 1 commit 2026-11-03 19:08:27 +01:00
A card is now read by the naming rules in CIPA DC-009-2010, not by
looking for media extensions anywhere on the volume.

Store.Directories holds the DCF directories and their objects, with
directory and file numbers. Store.NonConforming lists everything else
under DCIM, including symlinks and names fs.FS refuses. Media under
PRIVATE/M4ROOT stays, with no DCF numbers. Media anywhere else is no
longer part of the store. A symlink is never followed, and a read error
names the card's mount point.

Model: opus-5-5
clawbot force-pushed issue-2-parse-dcf-structure from 34cc2c30c1 to 25c6aca864 2026-11-03 19:08:27 +01:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-11-03 19:08:47 +01:00
Author
Collaborator

Rework of #11 (comment), all six fixed as the review called acceptable:

  1. Fixed: only real directories are read. A directory whose name is not valid UTF-8 is listed without what is inside it.
  2. Fixed. Judgement call: a symlink at DCIM, PRIVATE or M4ROOT itself counts as missing, and isDCFMountPoint now agrees, so it does not make a store.
  3. Fixed: the error names the mount point with %w.
  4. Fixed: failingFS and failingEntry in pkg/dcf/parse_internal_test.go, with cases for each listed function.
  5. Fixed: all six are test cases.
  6. Fixed: parseStores in pkg/dcf/dcf.go, tested in pkg/dcf/dcf_internal_test.go.

Model: opus-5-5

Rework of https://git.eeqj.de/sneak/dcf/pulls/11#issuecomment-162862, all six fixed as the review called acceptable: 1. Fixed: only real directories are read. A directory whose name is not valid UTF-8 is listed without what is inside it. 2. Fixed. Judgement call: a symlink at `DCIM`, `PRIVATE` or `M4ROOT` itself counts as missing, and `isDCFMountPoint` now agrees, so it does not make a store. 3. Fixed: the error names the mount point with `%w`. 4. Fixed: `failingFS` and `failingEntry` in `pkg/dcf/parse_internal_test.go`, with cases for each listed function. 5. Fixed: all six are test cases. 6. Fixed: `parseStores` in `pkg/dcf/dcf.go`, tested in `pkg/dcf/dcf_internal_test.go`. Model: opus-5-5
Author
Collaborator

PASS: every finding in #11 (comment) is fixed and tested, and this review of #11 found nothing new that blocks merging it into next.

Model: opus-5-5

PASS: every finding in https://git.eeqj.de/sneak/dcf/pulls/11#issuecomment-162862 is fixed and tested, and this review of https://git.eeqj.de/sneak/dcf/pulls/11 found nothing new that blocks merging it into `next`. Model: opus-5-5
clawbot merged commit 2c61b85a53 into next 2026-11-03 19:25:34 +01:00
clawbot deleted branch issue-2-parse-dcf-structure 2026-11-03 19:25:35 +01:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/dcf#11