use crate::cache_key::{
InputPrediction, PORTABLE_PREDICTION_SCHEMA, PREDICTION_SCHEMA, Portable, PortablePrediction,
};
use std::path::Path;
pub(crate) const SHARED_ROW_SCHEMA: u32 = 1;
pub(crate) const SHARED_ROW_MAX_BYTES: u64 = 256 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SharedPrediction {
Portable(PortablePrediction),
Plain(PlainRow),
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(crate) struct PlainRow {
pub(crate) schema: u32,
pub(crate) sources: Vec<Portable>,
pub(crate) env_deps: Vec<(String, Portable)>,
pub(crate) tree: Option<String>,
}
fn for_remote_value(value: &str, registry: Option<&Path>) -> Option<Portable> {
if !Path::new(value).has_root() {
return Some(Portable::Literal(value.to_string()));
}
let suffix = crate::cache_key::registry_suffix(value, registry?)?;
Some(Portable::Registry(suffix))
}
fn for_remote_entry(entry: &Portable, registry: Option<&Path>) -> Option<Portable> {
match entry {
Portable::Literal(value) => for_remote_value(value, registry),
relative => Some(relative.clone()),
}
}
pub(crate) fn for_remote_portable(
row: &PortablePrediction,
registry: Option<&Path>,
) -> Option<SharedPrediction> {
Some(SharedPrediction::Portable(PortablePrediction {
schema: row.schema,
sources: row
.sources
.iter()
.map(|source| for_remote_entry(source, registry))
.collect::<Option<_>>()?,
env_deps: row
.env_deps
.iter()
.map(|(name, value)| Some((name.clone(), for_remote_entry(value, registry)?)))
.collect::<Option<_>>()?,
tree: row.tree.clone(),
}))
}
pub(crate) fn for_remote_plain(row: &InputPrediction, registry: &Path) -> Option<SharedPrediction> {
Some(SharedPrediction::Plain(PlainRow {
schema: row.schema,
sources: row
.sources
.iter()
.map(|source| for_remote_value(source.to_str()?, Some(registry)))
.collect::<Option<_>>()?,
env_deps: row
.env_deps
.iter()
.map(|(name, value)| Some((name.clone(), for_remote_value(value, Some(registry))?)))
.collect::<Option<_>>()?,
tree: row.tree.clone(),
}))
}
pub(crate) fn plain_from_remote(row: &PlainRow, registry: &Path) -> Option<InputPrediction> {
let registry = registry.to_str()?;
let place = |entry: &Portable| match entry {
Portable::Literal(value) => Some(value.clone()),
Portable::Registry(suffix) => Some(format!("{registry}{suffix}")),
Portable::OutDir(_) | Portable::Workspace(_) => None,
};
Some(InputPrediction {
schema: row.schema,
sources: row
.sources
.iter()
.map(|source| place(source).map(Into::into))
.collect::<Option<_>>()?,
env_deps: row
.env_deps
.iter()
.map(|(name, value)| Some((name.clone(), place(value)?)))
.collect::<Option<_>>()?,
tree: row.tree.clone(),
})
}
impl SharedPrediction {
fn fits(&self, identity: &str) -> bool {
match self {
SharedPrediction::Portable(row) => {
crate::cache_key::is_portable_identity(identity)
&& row.schema == PORTABLE_PREDICTION_SCHEMA
}
SharedPrediction::Plain(row) => {
crate::cache_key::is_shared_target_identity(identity)
&& row.schema == PREDICTION_SCHEMA
}
}
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct SharedRow {
schema: u32,
identity: String,
row: SharedPrediction,
}
pub(crate) fn object_name(identity: &str) -> String {
format!("{}.json", blake3::hash(identity.as_bytes()).to_hex())
}
pub(crate) fn encode(identity: &str, row: &SharedPrediction) -> Option<Vec<u8>> {
if !row.fits(identity) {
return None;
}
let bytes = serde_json::to_vec(&SharedRow {
schema: SHARED_ROW_SCHEMA,
identity: identity.to_string(),
row: row.clone(),
})
.ok()?;
(bytes.len() as u64 <= SHARED_ROW_MAX_BYTES).then_some(bytes)
}
pub(crate) fn decode(bytes: &[u8], identity: &str) -> Option<SharedPrediction> {
if bytes.len() as u64 > SHARED_ROW_MAX_BYTES {
return None;
}
let shared: SharedRow = serde_json::from_slice(bytes).ok()?;
(shared.schema == SHARED_ROW_SCHEMA && shared.identity == identity && shared.row.fits(identity))
.then_some(shared.row)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache_key::Portable;
const IDENTITY: &str = "shared-workspace-v1:abc";
fn portable() -> PortablePrediction {
PortablePrediction {
schema: PORTABLE_PREDICTION_SCHEMA,
sources: vec![Portable::Literal("kt/src/lib.rs".to_string())],
env_deps: vec![],
tree: "tree".to_string(),
}
}
fn row() -> SharedPrediction {
SharedPrediction::Portable(portable())
}
fn plain() -> SharedPrediction {
SharedPrediction::Plain(PlainRow {
schema: PREDICTION_SCHEMA,
sources: vec![Portable::Registry("/i/kt-1.0.0/src/lib.rs".into())],
env_deps: vec![],
tree: None,
})
}
#[test]
fn a_published_row_carries_no_local_path() {
let registry = Path::new("/home/me/.cargo/registry/src");
let local = InputPrediction {
schema: PREDICTION_SCHEMA,
sources: vec!["/home/me/.cargo/registry/src/i/kt-1.0.0/src/lib.rs".into()],
env_deps: vec![(
"CARGO_MANIFEST_DIR".into(),
"/home/me/.cargo/registry/src/i/kt-1.0.0".into(),
)],
tree: Some("t".into()),
};
let shared = for_remote_plain(&local, registry).unwrap();
let bytes = encode("shared-target-v2:abc", &shared).unwrap();
assert!(
!String::from_utf8_lossy(&bytes).contains("/home/me"),
"{}",
String::from_utf8_lossy(&bytes)
);
let SharedPrediction::Plain(row) = decode(&bytes, "shared-target-v2:abc").unwrap() else {
panic!("a plain row");
};
let elsewhere = plain_from_remote(&row, Path::new("/ci/.cargo/registry/src")).unwrap();
assert_eq!(
elsewhere.sources,
vec![std::path::PathBuf::from(
"/ci/.cargo/registry/src/i/kt-1.0.0/src/lib.rs"
)]
);
assert_eq!(
elsewhere.env_deps[0].1,
"/ci/.cargo/registry/src/i/kt-1.0.0"
);
assert_eq!(elsewhere.tree.as_deref(), Some("t"));
}
#[test]
fn a_row_naming_another_absolute_path_is_not_published() {
let registry = Path::new("/home/me/.cargo/registry/src");
let mut local = InputPrediction {
schema: PREDICTION_SCHEMA,
sources: vec!["/home/me/.cargo/registry/src/i/kt-1.0.0/src/lib.rs".into()],
env_deps: vec![("HOME".into(), "/home/me".into())],
tree: None,
};
assert_eq!(for_remote_plain(&local, registry), None, "an env value");
local.env_deps = vec![("PROFILE".into(), "debug".into())];
local.sources.push("/etc/hosts".into());
assert_eq!(for_remote_plain(&local, registry), None, "a source");
local.sources.pop();
local
.sources
.push("/home/me/.cargo/registry/src/i/kt-1.0.0/../../../../x".into());
assert_eq!(
for_remote_plain(&local, registry),
None,
"a walk out of the registry"
);
let workspace = PortablePrediction {
schema: PORTABLE_PREDICTION_SCHEMA,
sources: vec![
Portable::Literal("kt/src/lib.rs".into()),
Portable::Workspace("/assets/a".into()),
],
env_deps: vec![("OUT".into(), Portable::Literal("/home/me/out".into()))],
tree: "t".into(),
};
assert_eq!(for_remote_portable(&workspace, None), None);
let mut without_path = workspace.clone();
without_path.env_deps.clear();
assert_eq!(
for_remote_portable(&without_path, None),
Some(SharedPrediction::Portable(without_path.clone())),
"relative entries travel as they are"
);
let registry_row = PortablePrediction {
sources: vec![Portable::Literal(
"/home/me/.cargo/registry/src/i/kt-1.0.0/src/lib.rs".into(),
)],
..without_path
};
let Some(SharedPrediction::Portable(shared)) =
for_remote_portable(®istry_row, Some(registry))
else {
panic!("a registry path travels relative");
};
assert_eq!(
shared.sources,
vec![Portable::Registry("/i/kt-1.0.0/src/lib.rs".into())]
);
}
#[test]
fn a_plain_row_with_a_foreign_entry_is_refused_on_arrival() {
let row = PlainRow {
schema: PREDICTION_SCHEMA,
sources: vec![Portable::Workspace("/kt/src/lib.rs".into())],
env_deps: vec![],
tree: None,
};
assert_eq!(plain_from_remote(&row, Path::new("/r")), None);
}
#[test]
fn a_row_round_trips_under_its_own_identity_only() {
let bytes = encode(IDENTITY, &row()).unwrap();
assert_eq!(decode(&bytes, IDENTITY), Some(row()));
assert_eq!(
decode(&bytes, "shared-workspace-v1:other"),
None,
"an object copied under another identity"
);
}
#[test]
fn each_identity_travels_with_its_own_kind_of_row() {
assert!(encode("shared-out-dir-v1:abc", &row()).is_some());
assert_eq!(encode("shared-target-v2:abc", &row()), None);
assert_eq!(encode("abc", &row()), None);
let bytes = encode("shared-target-v2:abc", &plain()).unwrap();
assert_eq!(decode(&bytes, "shared-target-v2:abc"), Some(plain()));
assert_eq!(encode(IDENTITY, &plain()), None);
assert_eq!(encode("abc", &plain()), None);
let bytes = encode(IDENTITY, &row()).unwrap();
assert_eq!(decode(&bytes, "abc"), None);
}
#[test]
fn a_malformed_or_foreign_object_is_no_row() {
assert_eq!(decode(b"not json", IDENTITY), None);
assert_eq!(decode(b"{}", IDENTITY), None);
let mut shared: serde_json::Value =
serde_json::from_slice(&encode(IDENTITY, &row()).unwrap()).unwrap();
shared["schema"] = (SHARED_ROW_SCHEMA + 1).into();
assert_eq!(
decode(&serde_json::to_vec(&shared).unwrap(), IDENTITY),
None
);
shared["schema"] = SHARED_ROW_SCHEMA.into();
shared["row"]["portable"]["schema"] = (PORTABLE_PREDICTION_SCHEMA + 1).into();
assert_eq!(
decode(&serde_json::to_vec(&shared).unwrap(), IDENTITY),
None
);
}
#[test]
fn an_object_past_the_cap_is_no_row() {
let mut large = portable();
let padding = "x".repeat(SHARED_ROW_MAX_BYTES as usize);
large.sources.push(Portable::Literal(padding));
let large = SharedPrediction::Portable(large);
assert_eq!(encode(IDENTITY, &large), None);
let bytes = serde_json::to_vec(&SharedRow {
schema: SHARED_ROW_SCHEMA,
identity: IDENTITY.to_string(),
row: large,
})
.unwrap();
assert_eq!(decode(&bytes, IDENTITY), None);
}
fn row_padded(extra: usize) -> SharedPrediction {
let mut padded = portable();
padded.sources.push(Portable::Literal("x".repeat(extra)));
SharedPrediction::Portable(padded)
}
#[test]
fn an_object_exactly_at_the_cap_is_still_a_row() {
let base = encode(IDENTITY, &row_padded(0)).unwrap().len();
let at_cap = row_padded(SHARED_ROW_MAX_BYTES as usize - base);
let bytes = encode(IDENTITY, &at_cap).unwrap();
assert_eq!(bytes.len() as u64, SHARED_ROW_MAX_BYTES);
assert_eq!(decode(&bytes, IDENTITY), Some(at_cap));
let over = row_padded(SHARED_ROW_MAX_BYTES as usize - base + 1);
assert_eq!(encode(IDENTITY, &over), None);
let over_bytes = serde_json::to_vec(&SharedRow {
schema: SHARED_ROW_SCHEMA,
identity: IDENTITY.to_string(),
row: over,
})
.unwrap();
assert_eq!(over_bytes.len() as u64, SHARED_ROW_MAX_BYTES + 1);
assert_eq!(decode(&over_bytes, IDENTITY), None);
}
#[test]
fn the_row_cap_is_a_quarter_mebibyte() {
assert_eq!(SHARED_ROW_MAX_BYTES, 262_144);
}
#[test]
fn the_object_name_is_the_identity_hash() {
let name = object_name(IDENTITY);
assert!(name.ends_with(".json"));
assert_eq!(name.len(), 64 + ".json".len());
assert_ne!(name, object_name("shared-workspace-v1:abd"));
}
}