Put and Read: content on the socket, for clients that share no files

The system daemon runs as its own user behind ProtectHome, so it can
neither read a file a desktop app wrote for Add nor write one back for
Materialize - redoal on the system varde failed every get with EACCES.
Put imports base64 bytes carried in the request and Read returns a raw
blob's bytes in the reply, both up to MAX_INLINE_BYTES (16 MiB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-10-03 13:08:50 +02:00
co-authored by Claude Opus 5.5
parent c9dbc4a576
commit 687af8bd2a
6 changed files with 129 additions and 0 deletions
+2
View File
@@ -16,6 +16,8 @@ required-features = ["socket-api"]
varde-proto = { workspace = true }
iroh = { workspace = true }
iroh-blobs = { workspace = true }
# Request::Put / Read carry content inline, base64.
base64 = "0.22"
# mDNS/local-network address lookup left iroh core in 1.0.
iroh-mdns-address-lookup = "0.4"
# Interop with iroh-blobs streams and channels; both are already in the
+33
View File
@@ -371,6 +371,39 @@ impl Daemon {
bytes,
})
}
Request::Put { data } => {
use base64::Engine as _;
let bytes = base64::engine::general_purpose::STANDARD
.decode(data.as_bytes())
.map_err(|e| OpError::InvalidArgument(format!("put: data is not base64: {e}")))?;
if bytes.len() as u64 > varde_proto::MAX_INLINE_BYTES {
return Err(OpError::InvalidArgument(format!(
"put: {} bytes, more than {} inline; add it by path",
bytes.len(),
varde_proto::MAX_INLINE_BYTES
)));
}
let (hash, bytes) = self.store.add_bytes(bytes).await?;
self.meta.record_format(&hash.to_hex(), StoredFormat::Raw)?;
Ok(ResponseData::Added {
hash: hash.to_hex().to_string(),
bytes,
})
}
Request::Read { hash } => {
use base64::Engine as _;
let parsed = parse_hash(&hash)?;
if self.meta.format_of(&parsed.to_hex()) == StoredFormat::HashSeq {
return Err(OpError::InvalidArgument(format!(
"read: {} is a collection; materialize it",
parsed.to_hex()
)));
}
let bytes = self.store.read_bytes(parsed, varde_proto::MAX_INLINE_BYTES).await?;
Ok(ResponseData::Bytes {
data: base64::engine::general_purpose::STANDARD.encode(bytes),
})
}
Request::Pin { hash, policy } => {
let parsed = parse_hash(&hash)?;
self.meta.pin(&parsed.to_hex(), policy)?;
+24
View File
@@ -228,6 +228,30 @@ impl BlobStore {
/// Export `hash` to `dest`. Returns bytes written and whether at least
/// one file was reflinked.
/// Import `data` as one raw blob (`Request::Put`).
pub async fn add_bytes(&self, data: Vec<u8>) -> Result<(Hash, u64), OpError> {
let size = data.len() as u64;
let tag = self.store.add_bytes(data).temp_tag().await.map_err(internal)?;
info!(hash = %tag.hash().to_hex(), size, "imported bytes");
Ok((tag.hash(), size))
}
/// The whole content of a complete raw blob (`Request::Read`), when it
/// is no larger than `limit`.
pub async fn read_bytes(&self, hash: Hash, limit: u64) -> Result<Vec<u8>, OpError> {
match self.store.blobs().status(hash).await.map_err(internal)? {
BlobStatus::Complete { size } if size > limit => Err(OpError::InvalidArgument(format!(
"{} is {size} bytes, more than {limit} can be read inline; materialize it",
hash.to_hex()
))),
BlobStatus::Complete { .. } => {
let bytes = self.store.blobs().get_bytes(hash).await.map_err(internal)?;
Ok(bytes.to_vec())
}
_ => Err(OpError::NotFound(format!("{} is not complete in the store", hash.to_hex()))),
}
}
pub async fn materialize(
&self,
hash: Hash,
+42
View File
@@ -276,3 +276,45 @@ fn reflink_when_supported() {
drop(daemon);
let _ = std::fs::remove_dir_all(&base);
}
/// Put and Read carry the bytes on the socket: a client that shares no
/// filesystem with the daemon (a desktop app and the system daemon, which
/// runs as its own user behind ProtectHome) stores and reads back without
/// a path either side must reach. A Put blob reads back by Materialize too.
#[test]
fn bytes_round_trip_inline() {
use base64::Engine as _;
let b64 = base64::engine::general_purpose::STANDARD;
let daemon = support::spawn_daemon();
let mut client = support::Client::connect(&daemon.socket);
let payload: Vec<u8> = (0..50_000u32).flat_map(|i| i.to_le_bytes()).collect();
let reply = client.request(&Request::Put { data: b64.encode(&payload) });
assert!(reply.ok, "put failed: {:?}", reply.error);
let hash = match reply.data {
Some(ResponseData::Added { hash, bytes }) => {
assert_eq!(bytes, payload.len() as u64);
hash
}
other => panic!("unexpected put reply: {other:?}"),
};
let reply = client.request(&Request::Read { hash: hash.clone() });
assert!(reply.ok, "read failed: {:?}", reply.error);
match reply.data {
Some(ResponseData::Bytes { data }) => assert_eq!(b64.decode(data).unwrap(), payload),
other => panic!("unexpected read reply: {other:?}"),
}
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("out.bin");
materialize(&mut client, &hash, &dest, MaterializeMode::Copy).unwrap();
assert_eq!(std::fs::read(&dest).unwrap(), payload);
// Not base64: refused, not stored.
let reply = client.request(&Request::Put { data: "not base64!".into() });
assert_eq!(reply.error.expect("error body").code, ErrorCode::InvalidArgument);
// Unknown hash: not found.
let reply = client.request(&Request::Read { hash: "0".repeat(64) });
assert!(!reply.ok);
}