Skip to content

fix(store-service): report a symlink's target length as its fs.stat size (#447) - #448

Closed
leepokai wants to merge 1 commit into
Ephemeral-AI-Lab:mainfrom
leepokai:fix/stat-symlink-size
Closed

leepokai wants to merge 1 commit into
Ephemeral-AI-Lab:mainfrom
leepokai:fix/stat-symlink-size

Conversation

@leepokai

Copy link
Copy Markdown
Contributor

Fixes #447.

fs.stat reported a symlink's size as the length of the object that stores its target, which is the target plus 27 bytes of envelope and header. A mounted Workspace reports the target's length. The repro and cause are in the issue.

Fix

  • size() in layerfs-store-service/src/read/snapshot.rs now decodes the stored target with SymlinkTarget::decode, the same way layerfs-workspace base/view.rs does, and returns its byte length.
  • The operator file history and stats use the same helper, so they now count a symlink the same way.
  • The fs.stat reference in the layerfs-js-sdk skill now says what size means for a file, a symlink and a folder.

Nothing changes in the stored format.

Tests

  • New layerfs-api test stat_symlink, covering a link and a dangling link. On main it fails with left: Some(Int(32)) right: Some(Int(5)). It passes with the fix.

Checks, run on Linux arm64 (Ubuntu 22.04 VM) with Rust 1.85.1:

Check Result
cargo test -p layerfs-api --test stat_symlink 1 passed
cargo test -p layerfs-store-service passed
every layerfs-api test target, one at a time, 120 s timeout each 46 targets, all passed
cargo clippy -p layerfs-store-service -p layerfs-api --all-targets -- -D warnings clean
cargo fmt --all -- --check clean

I also confirmed the mount side by running kernel_mount under FUSE with a temporary local probe, which is not part of this PR. For all five fixture symlinks, lstat size equals the readlink length.

Production LOC: 102,160 -> 102,161 (delta +1), in layerfs-store-service. The counting method is in the commit message.

fs.stat answered a symlink's size as the length of the canonical object
that stores its target: the object envelope plus the LFS4LNK header, 27
bytes more than the target (a link to `a.txt` reported 32). A mounted
Workspace reports the target's length (base/view.rs stat_resolved and the
FUSE attributes), so the two views of one symlink disagreed.

snapshot::size now decodes the stored target with SymlinkTarget::decode,
as the Workspace base view does, and returns its byte length. The
operator file history and stats use the same helper and now count a
symlink the same way.

Tests: new layerfs-api tests/stat_symlink.rs (a link and a dangling
link); it fails on main with `left: Some(Int(32)) right: Some(Int(5))`
and passes with the fix. The fs.stat reference in the layerfs-js-sdk
skill now says what `size` means for each type.

Verification (Linux arm64 VM, Rust 1.85.1): cargo fmt --all --check;
clippy -p layerfs-store-service -p layerfs-api --all-targets
-D warnings; test -p layerfs-store-service and each of the 46
layerfs-api test targets under a 120 s timeout: all passed. check_product_boundary.py reports the
same 4 violations as main, none in the changed files.

No tests deleted.

Production LOC: 102160 -> 102161 (delta +1)
Scope: combined core+api. Core: 96788 -> 96789 (layerfs-store-service
10009 -> 10010); api: 5372 -> 5372. Method: tools/production_loc.py
--root <extracted tree> --json on git archive of the first parent and of
the staged tree. Reference absent (retired R9c; last counted 65,417 at
tree 498dd19).
yifanxuaaa added a commit that referenced this pull request Oct 26, 2026
fs.stat answered a symlink's size as the length of the object storing its
target (target + 27 bytes); it now decodes the target and returns its
length, as a mounted Workspace's lstat does. The operator storage and
revision answers use the same helper, so they count a symlink the same
way. No stored format changes.

Rebased by the maintainer onto main after #465 moved the crates to
engine/; the new test moved with it to engine/crates/layerfs-api/tests/.
Checked on macOS (cargo 1.85.1): stat_symlink passes and fails on main's
snapshot.rs with 32 vs 5; layerfs-store-service tests; layerfs-api
sdk_method_golden, diff_resume, sdk_compare, write_refusals,
sdk_reads_during_publish and init; fmt and Clippy -D warnings clean.

Production LOC: 102164 -> 102165 (delta +1)
  First-parent comparison (main a944016 -> this merge). core 38241,
  engine 61290 -> 61291 (layerfs-store-service 10009 -> 10010), api 2633;
  reference absent (retired R9c; last counted 65,417 at tree
  498dd19). Counted with tools/production_loc.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yifanxuaaa

Copy link
Copy Markdown
Contributor

Merged into main as 3828268, thanks! #465 moved the crates to engine/, so I rebased your commit onto main (ea052b1, your authorship kept): the new test now lives at engine/crates/layerfs-api/tests/stat_symlink.rs and the LOC line is recounted (102164 -> 102165, +1 in layerfs-store-service under engine). This PR's branch still holds the pre-rebase commit, so GitHub shows it as closed rather than merged.

@yifanxuaaa yifanxuaaa closed this Oct 26, 2026
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.

fs.stat: a symlink's size is its stored object length (target + 27 bytes); is the target length intended?

2 participants