Skip to content
Closed
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
4 changes: 3 additions & 1 deletion .agents/skills/layerfs-js-sdk/references/methods/fs-read.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ const page = await layerfs.fs.list("work", "/src");

Describes one path.

**Returns:** `{ kind: "ok", snapshot, root, path, type, inode, size }`.
**Returns:** `{ kind: "ok", snapshot, root, path, type, inode, size }`. `size` is
a file's length in bytes, a symlink's target length (as `lstat` reports it in a
mounted Workspace) and 0 for a folder.

```js
const exists = await layerfs.fs.stat("work", "/package.json").then(() => true, (e) => { if (isRefusal(e, "NoSuchPath")) return false; throw e; });
Expand Down
38 changes: 38 additions & 0 deletions core/crates/layerfs-api/tests/stat_symlink.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! `fs.stat` gives a symlink the size a mounted Workspace gives it: the
//! length of its target, not of the object that stores the target.
#[path = "support/installed_store.rs"]
#[allow(dead_code)]
mod installed;
#[path = "support/sdk.rs"]
#[allow(dead_code)]
mod support;

use layerfs_bridge::control::SdkValue;
use std::fs;
use support::Fixture;

#[test]
fn a_symlink_size_is_its_target_length() {
let fixture = Fixture::new("stat-symlink", |source| {
fs::write(source.join("a.txt"), b"a\n").unwrap();
std::os::unix::fs::symlink("a.txt", source.join("link")).unwrap();
std::os::unix::fs::symlink("/does/not/exist", source.join("dangling")).unwrap();
3
});
for (path, target) in [("/link", "a.txt"), ("/dangling", "/does/not/exist")] {
let stat = fixture.call(
"fs.stat",
vec![
("branch", SdkValue::text("main")),
("path", SdkValue::text(path)),
],
);
assert_eq!(stat.get("type"), Some(&SdkValue::text("symlink")), "{path}");
assert_eq!(
stat.get("size"),
Some(&SdkValue::Int(target.len() as i64)),
"{path}"
);
}
fixture.cleanup();
}
9 changes: 6 additions & 3 deletions core/crates/layerfs-store-service/src/read/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::store::{Store, StorePorts};
use layerfs_bridge::control::{SdkRefusal, SdkValue};
use layerfs_content::compare::{as_text, EntryFacts, TreeTable, MAXIMUM_TEXT_BYTES};
use layerfs_content::filesystem::path::PathName;
use layerfs_content::filesystem::FilesystemRootId;
use layerfs_content::filesystem::{FilesystemRootId, SymlinkTarget};
use layerfs_content::object::inode_leaf::InodeKind;
use layerfs_content::{AuthenticatedObjects, ContentError, ObjectId};
use layerfs_history::{LayerStackRecord, NodeId};
Expand Down Expand Up @@ -129,7 +129,9 @@ pub fn kind_name(kind: InodeKind) -> &'static str {
}

/// Logical size of a file or symlink's content; a directory is 0. A regular
/// file's length is Store metadata: no payload or delta chain is read.
/// file's length is Store metadata: no payload or delta chain is read. A
/// symlink's is its target's length, as a mounted Workspace reports it, not
/// the length of the object that stores the target.
pub fn size(reader: &StorePorts, entry: &EntryFacts) -> Result<u64, SdkRefusal> {
match entry.value.kind {
InodeKind::Directory => Ok(0),
Expand All @@ -140,7 +142,8 @@ pub fn size(reader: &StorePorts, entry: &EntryFacts) -> Result<u64, SdkRefusal>
.ok_or_else(|| args::failed("missing file length")),
InodeKind::Symlink => {
layerfs_content::AuthenticatedObjects::read_canonical(reader, entry.value.content_root)
.map(|canonical| canonical.len() as u64)
.and_then(|canonical| SymlinkTarget::decode(&canonical))
.map(|target| target.as_bytes().len() as u64)
.map_err(args::content)
}
}
Expand Down