Skip to content

stat: GNU parity for device numbers and the default record - #101

Open
kelp wants to merge 6 commits into
mainfrom
fix/stat-dev-minor-truncation
Open

stat: GNU parity for device numbers and the default record#101
kelp wants to merge 6 commits into
mainfrom
fix/stat-dev-minor-truncation

Conversation

@kelp

@kelp kelp commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Two related GNU-parity fixes in stat's default output, both found by
diffing our record against /usr/bin/stat on Linux.

Device minor numbers above 255 (#92)

%t/%T, the -t terse fields, and the default Device: line each
carried their own copy of the dev_t bit layout. The Linux copy masked
the minor with 0xff, dropping the high bits the kernel packs further
up, so stat -c '%t %T' /dev/binder gave a 4 where GNU gives a 104.
All three now call devMajor/devMinor, leaving makeDev and those two
helpers as the only code that knows the layout. macOS was already
correct and is unchanged.

Also adds GNU's Device type: MAJ,MIN field, which we never emitted.
Character and block special files get it, and their Links column
widens to five to match.

Default-record separators (#98)

GNU separates Size: from Blocks: with a tab, and follows the
Blocks, IO Block and Inode fields with literal spaces. We folded
each separator into the preceding column's pad width. That renders
identically for everyday values, which is why it went unnoticed, but the
separator disappears entirely once a field outgrows its pad:

  • a 10 GB file printed 10737418240Blocks: — no separator at all
  • an inode of 11+ digits left one space before Links:, not two

The issue reported only the missing tab; the other two divergences are
the same defect and turned up while checking whether the rest of the
line was genuinely equivalent or merely coincidentally aligned. Fix is
three format literals, matched against coreutils 9.5 default_format.

Still divergent, out of scope here: GNU's %N shell-quoting of file
names (File: 'my file'). We never quote.

Tests

Red was verified on both platforms before each fix, failing for the
right reason — for #98, expected '\x09' after the size field,
found '\x42', the B of Blocks.

Check macOS Linux
zig build test 2668/2724 pass, 56 skip, 0 fail 2672/2724 pass, 52 skip, 0 fail
Integration 48/48 utilities 48/48 utilities

Byte-level check on Linux — diff of cat -A output against
/usr/bin/stat is identical for /etc/passwd, /dev/null and /tmp.

Closes #92
Closes #98

🤖 Generated with Claude Code

https://claude.ai/code/session_01VapqXpB1wtGk5Cb9KeMSok

kelp and others added 6 commits July 26, 2026 20:14
Nine failing tests for issue #92. Unit tests build a synthetic
StatResult so the renderers can be driven with a minor above 255
without a real device node; integration tests diff our output
against GNU stat on /dev/null and on a scanned /dev node whose
minor exceeds 255.

Red on Linux: %T, -t field 10, and the default Device line all
truncate minor 260 to 4, and no file type gets GNU's
"Device type: M,m" suffix. Red on macOS for the suffix only; the
Darwin encoding tests pass, pinning that path as untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VapqXpB1wtGk5Cb9KeMSok
%t/%T, the -t terse fields, and the default Device line each kept
their own copy of the dev_t bit layout. The Linux copy masked the
minor with 0xff, dropping the high bits the kernel packs further
up, so any device with a minor above 255 rendered wrong:
stat -c '%t %T' /dev/binder gave "a 4" where GNU gives "a 104".

All three now call devMajor/devMinor, leaving makeDev and those
two helpers as the only code that knows the layout. macOS masks
were already identical to the helpers, so that path is unchanged.

Also adds GNU's "Device type: MAJ,MIN" field, which the default
output never emitted. Character and block special files get it,
and their Links column widens to five to match; every other file
type renders as before.

Fixes #92

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VapqXpB1wtGk5Cb9KeMSok
Eight failing tests for issue #98. GNU's default_format puts a
literal tab after the Size field and literal spaces after Blocks,
IO Block, and Inode. Our format strings fold those separators into
the pad widths, so they render identically for everyday values and
vanish entirely once a field overflows its pad.

Three unit tests cover the Size line and are ungated: one pins
GNU's exact bytes for a 1383-byte file, two drive synthetic sizes
and block counts past the pad. Two more cover the Device line with
an 11-digit inode, Linux-gated like their issue-92 neighbours
because the dev_t layout differs. The integration helper diffs the
whole record against GNU for a regular file, a directory, and
/dev/null, replacing the grep '^Device:' scoping those earlier
tests needed.

Red on both platforms: expected '\x09' after the size field, found
'\x42' -- the B of Blocks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VapqXpB1wtGk5Cb9KeMSok
The green phase fell back to a briefing embedded from an earlier dd
run when none was passed in, so a bug fix could be implemented
against another bug's notes. Re-scout instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VapqXpB1wtGk5Cb9KeMSok
GNU's default_format puts a literal tab after the Size field and
literal spaces after Blocks, IO Block, and Inode. We folded each
separator into the preceding pad width, which renders identically
for everyday values and drops the separator entirely once a field
overflows its pad: a 10GB file printed "10737418240Blocks:", and
an 11-digit inode left one space before Links where GNU leaves two.

Three format literals now match coreutils 9.5 byte for byte. On
Linux, stat FILE is identical to /usr/bin/stat FILE for a regular
file, a directory, and /dev/null, which lets the whole record be
diffed against GNU instead of just the Device line.

Fixes #98

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VapqXpB1wtGk5Cb9KeMSok
The EXAMPLES record rendered the separator as spaces, which is what
the code used to emit. Also notes the fix in the changelog.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VapqXpB1wtGk5Cb9KeMSok
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stat: default output omits GNU's tab after the Size field stat: GNU %t/%T use a Linux major/minor mask that doesn't match how dev is composed

1 participant