use std::path::Path;
use kernel::{Address, AxCode, AxError, Locator};
use storage::Cas;
pub const REST_DIR: &str = ".rest";
pub struct OffloadSite<'a> {
pub cas: &'a mut Cas,
pub city_root: &'a Path,
pub room: &'a Address,
pub origin: storage::BlockOrigin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OffloadRecord {
pub substitute: Vec<u8>,
pub original: Locator,
pub rest_path: String,
pub original_len: u64,
}
fn hint_line(total: u64, rest_path: &str, locator: &Locator) -> String {
format!("\n[offloaded: total {total} bytes; rest at {rest_path}; original {locator}]")
}
fn materialized(
bytes: &[u8],
site: &OffloadSite<'_>,
locator: &Locator,
) -> Result<String, AxError> {
let name = rest_file_name(locator);
let room = site.room.as_str();
materialize(bytes, &site.city_root.join(room).join(REST_DIR), &name)?;
Ok(format!("{room}/{REST_DIR}/{name}"))
}
fn rest_file_name(locator: &Locator) -> String {
let mut tail: String = locator
.to_string()
.chars()
.rev()
.take(16)
.collect::<String>()
.chars()
.rev()
.collect();
tail.retain(|c| c.is_ascii_alphanumeric());
format!("rest-{tail}.dat")
}
fn materialize(bytes: &[u8], dir: &Path, name: &str) -> Result<(), AxError> {
let io = |err: std::io::Error| {
AxError::failure(
AxCode::StorageFatal,
"materialize rest file",
err.to_string(),
)
.with_recovery(
"free space on the disk holding the city, or fix the permissions of the \
run's environment directory, then ask for the result again",
)
};
let path = dir.join(name);
if path.exists() {
return Ok(());
}
std::fs::create_dir_all(dir).map_err(io)?;
std::fs::write(&path, bytes).map_err(io)?;
let mut perms = std::fs::metadata(&path).map_err(io)?.permissions();
perms.set_readonly(true);
std::fs::set_permissions(&path, perms).map_err(io)?;
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Tee {
pub(crate) original: Locator,
pub(crate) rest_path: String,
}
pub(crate) fn tee(bytes: &[u8], site: &mut OffloadSite<'_>) -> Result<Tee, AxError> {
let hash = site
.cas
.put_for(bytes, &site.origin)
.map_err(storage::StorageError::into_ax)?;
let original = Locator::cas(hash);
let rest_path = materialized(bytes, site, &original)?;
Ok(Tee {
original,
rest_path,
})
}
pub fn offload(
bytes: &[u8],
cap_bytes: u64,
site: &mut OffloadSite<'_>,
) -> Result<OffloadRecord, AxError> {
let original_len = u64::try_from(bytes.len()).map_err(|_| {
AxError::failure(AxCode::InvalidArgs, "offload result", "length exceeds u64").with_recovery(
"ask the tool for less at a time: this result is larger than a byte \
count this city can hold",
)
})?;
if original_len <= cap_bytes {
return Err(AxError::failure(
AxCode::InvalidArgs,
"offload result",
"lossless input: only lossy transforms store (invariant 3)",
)
.with_recovery(
"pass this result through unchanged; it already fits the cap, and offload \
stores only what it has to cut",
));
}
let Tee {
original,
rest_path,
} = tee(bytes, site)?;
let hint = hint_line(original_len, &rest_path, &original);
let hint_len = u64::try_from(hint.len()).map_err(|_| {
AxError::failure(AxCode::InvalidArgs, "offload result", "hint exceeds u64").with_recovery(
"report this against runtime::offload: the hint line names one path \
and two byte counts and cannot reach this length",
)
})?;
let head_budget = cap_bytes.checked_sub(hint_len).ok_or_else(|| {
AxError::failure(
AxCode::InvalidArgs,
"offload result",
format!("cap {cap_bytes} cannot even hold the {hint_len}-byte hint"),
)
.with_recovery("raise the cap or skip offload for this result")
})?;
let head_len = usize::try_from(head_budget.min(original_len)).map_err(|_| {
AxError::failure(AxCode::InvalidArgs, "offload result", "head exceeds usize").with_recovery(
"lower `[tool] result_cap_bytes`: the kept head would be longer than \
this machine can address",
)
})?;
let mut substitute = bytes.get(..head_len).unwrap_or_default().to_vec();
substitute.extend_from_slice(hint.as_bytes());
let substitute_len = u64::try_from(substitute.len()).map_err(|_| {
AxError::failure(
AxCode::InvalidArgs,
"offload result",
"substitute exceeds u64",
)
.with_recovery(
"lower `[tool] result_cap_bytes`: the shortened result is longer than a \
byte count this city can hold",
)
})?;
if substitute_len > cap_bytes || substitute_len > original_len {
return Err(AxError::failure(
AxCode::InvalidArgs,
"offload result",
"substitute outgrew its bounds (invariant 2)",
)
.with_recovery(
"report this against runtime::offload: the shortened result must stay \
within both the cap and the original length, and it did not",
));
}
Ok(OffloadRecord {
substitute,
original,
rest_path,
original_len,
})
}
pub fn rematerialize(locator: &Locator, site: &mut OffloadSite<'_>) -> Result<String, AxError> {
let Locator::Cas { hash, .. } = locator else {
return Err(AxError::failure(
AxCode::InvalidArgs,
"rematerialize rest file",
"locator is not cas-addressed",
)
.with_recovery(
"pass the `cas:` locator the offload record carries under `original`; a \
`file:` locator names a path that cleanup already removed",
));
};
let bytes = site.cas.get(hash).map_err(storage::StorageError::into_ax)?;
materialized(&bytes, site, locator)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
reason = "test code"
)]
pub(crate) mod tests {
use super::*;
pub(crate) fn origin() -> storage::BlockOrigin {
storage::BlockOrigin {
run: kernel::RunId::from_bytes([7; 16]),
building: kernel::Address::parse("lab").unwrap(),
}
}
fn site(dir: &tempfile::TempDir) -> (Cas, Address) {
let cas = Cas::open(&dir.path().join("cas")).unwrap();
(cas, Address::parse("room").unwrap())
}
#[test]
fn a7_roundtrip_all_four_assertions() {
let dir = tempfile::tempdir().unwrap();
let (mut cas, room) = site(&dir);
let mut s = OffloadSite {
cas: &mut cas,
city_root: dir.path(),
room: &room,
origin: origin(),
};
let original: Vec<u8> = (0..40_000u32)
.map(|i| u8::try_from(i % 251).unwrap())
.collect();
let record = offload(&original, 4_096, &mut s).unwrap();
assert!(record.substitute.len() <= 4_096);
assert!(record.substitute.len() < original.len());
assert!(
String::from_utf8_lossy(&record.substitute).contains("[offloaded: total 40000 bytes")
);
assert!(
String::from_utf8_lossy(&record.substitute).contains("; rest at room/.rest/rest-"),
"the hint names the rest file by its address in the room"
);
assert_eq!(
std::fs::read(dir.path().join(&record.rest_path)).unwrap(),
original
);
let Locator::Cas { hash, .. } = &record.original else {
panic!("expected a cas locator");
};
assert_eq!(s.cas.get(hash).unwrap(), original);
let mut perms = std::fs::metadata(dir.path().join(&record.rest_path))
.unwrap()
.permissions();
#[allow(clippy::permissions_set_readonly_false, reason = "test cleanup")]
perms.set_readonly(false);
std::fs::set_permissions(dir.path().join(&record.rest_path), perms).unwrap();
std::fs::remove_file(dir.path().join(&record.rest_path)).unwrap();
let back = rematerialize(&record.original, &mut s).unwrap();
assert_eq!(std::fs::read(dir.path().join(&back)).unwrap(), original);
}
#[test]
fn the_rest_address_is_one_the_read_tool_admits_and_resolves() {
let dir = tempfile::tempdir().unwrap();
let (mut cas, room) = site(&dir);
let mut s = OffloadSite {
cas: &mut cas,
city_root: dir.path(),
room: &room,
origin: origin(),
};
let original = vec![3u8; 30_000];
let record = offload(&original, 2_048, &mut s).unwrap();
let admitted =
crate::tools::admit(&record.rest_path, "read", &|_| kernel::ReadVerdict::Open).unwrap();
assert_eq!(
std::fs::read(dir.path().join(admitted.as_str())).unwrap(),
original,
"the read tool resolves the rest address from the city root"
);
}
#[test]
fn the_original_is_pinned_for_the_run_and_building_it_was_cut_for() {
let dir = tempfile::tempdir().unwrap();
let (mut cas, room) = site(&dir);
let mut s = OffloadSite {
cas: &mut cas,
city_root: dir.path(),
room: &room,
origin: origin(),
};
let record = offload(&vec![3u8; 30_000], 2_048, &mut s).unwrap();
let Locator::Cas { hash, .. } = &record.original else {
panic!("expected a cas locator");
};
assert_eq!(s.cas.origins(hash).unwrap(), vec![origin()]);
}
#[test]
fn same_bytes_offload_to_the_same_locator() {
let dir = tempfile::tempdir().unwrap();
let (mut cas, room) = site(&dir);
let mut s = OffloadSite {
cas: &mut cas,
city_root: dir.path(),
room: &room,
origin: origin(),
};
let original = vec![7u8; 30_000];
let one = offload(&original, 2_048, &mut s).unwrap();
let two = offload(&original, 2_048, &mut s).unwrap();
assert_eq!(
one.original, two.original,
"CAS dedup makes offload idempotent"
);
assert_eq!(one.rest_path, two.rest_path);
}
#[test]
fn lossless_input_and_hopeless_caps_are_refused() {
let dir = tempfile::tempdir().unwrap();
let (mut cas, room) = site(&dir);
let mut s = OffloadSite {
cas: &mut cas,
city_root: dir.path(),
room: &room,
origin: origin(),
};
let err = offload(b"small", 4_096, &mut s).unwrap_err();
assert_eq!(*err.code(), AxCode::InvalidArgs);
let big = vec![1u8; 20_000];
let err = offload(&big, 16, &mut s).unwrap_err();
assert!(err.subject().contains("hint"));
}
}