use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use concinnity_core::blob::{
CacheEntry, CacheEntryKind, CacheMeta, HEADER_SIZE, encode_cnb_prefix, parse_cnb,
parse_payload_section_start,
};
#[derive(Clone, Copy)]
struct Span {
offset: u64,
len: u64,
}
pub(super) struct Index {
path: PathBuf,
payload_start: u64,
entries: HashMap<(CacheEntryKind, String), Span>,
}
impl Index {
pub(super) fn read(path: &Path, token: u32) -> Self {
read_index(path, token).unwrap_or_else(|| Self::empty(path))
}
fn empty(path: &Path) -> Self {
Self {
path: path.to_path_buf(),
payload_start: 0,
entries: HashMap::new(),
}
}
pub(super) fn get(&self, kind: CacheEntryKind, key: &str) -> Option<Vec<u8>> {
let span = *self.entries.get(&(kind, key.to_owned()))?;
read_span(&self.path, self.payload_start, span).ok()
}
pub(super) fn contains(&self, kind: CacheEntryKind, key: &str) -> bool {
self.entries.contains_key(&(kind, key.to_owned()))
}
#[cfg(test)]
pub(super) fn len(&self) -> usize {
self.entries.len()
}
}
fn read_index(path: &Path, token: u32) -> Option<Index> {
let mut file = File::open(path).ok()?;
let size = file.metadata().ok()?.len();
let mut header = [0u8; HEADER_SIZE];
file.read_exact(&mut header).ok()?;
let payload_start = parse_payload_section_start::<CacheMeta>(&header).ok()?;
if payload_start > size {
return None;
}
let mut prefix = vec![0u8; usize::try_from(payload_start).ok()?];
prefix[..HEADER_SIZE].copy_from_slice(&header);
file.read_exact(&mut prefix[HEADER_SIZE..]).ok()?;
let (meta, _) = parse_cnb::<CacheMeta>(token, &prefix).ok()?;
Some(Index {
path: path.to_path_buf(),
payload_start,
entries: meta
.entries
.into_iter()
.filter(|entry| entry.offset.saturating_add(entry.len) <= size - payload_start)
.map(|entry| {
(
(entry.kind, entry.key),
Span {
offset: entry.offset,
len: entry.len,
},
)
})
.collect(),
})
}
fn read_span(path: &Path, payload_start: u64, span: Span) -> io::Result<Vec<u8>> {
let mut file = File::open(path)?;
file.seek(SeekFrom::Start(payload_start + span.offset))?;
let mut bytes = vec![0u8; usize::try_from(span.len).map_err(io::Error::other)?];
file.read_exact(&mut bytes)?;
Ok(bytes)
}
pub(super) fn write(
path: &Path,
index: &Index,
stored: &[(CacheEntryKind, &str, &[u8])],
token: u32,
) -> bool {
let plan = plan(index, stored);
let meta = CacheMeta {
toolchain: String::new(),
entries: plan.iter().map(|p| p.entry.clone()).collect(),
};
let Ok(prefix) = encode_cnb_prefix(token, &meta) else {
return false;
};
concinnity_host::store::atomic::replace(path, |out| {
out.write_all(&prefix)?;
let mut previous = None;
for planned in &plan {
match &planned.source {
Source::Stored(bytes) => out.write_all(bytes)?,
Source::Carried(span) => {
let file = match &mut previous {
Some(file) => file,
slot => slot.insert(File::open(&index.path)?),
};
file.seek(SeekFrom::Start(index.payload_start + span.offset))?;
io::copy(&mut Read::by_ref(file).take(span.len), out)?;
}
}
}
Ok(())
})
}
enum Source<'a> {
Carried(Span),
Stored(&'a [u8]),
}
struct Planned<'a> {
entry: CacheEntry,
source: Source<'a>,
}
fn plan<'a>(index: &Index, stored: &[(CacheEntryKind, &'a str, &'a [u8])]) -> Vec<Planned<'a>> {
let replaced: HashSet<(CacheEntryKind, &str)> =
stored.iter().map(|(kind, key, _)| (*kind, *key)).collect();
let mut carried: Vec<(CacheEntryKind, &String, Span)> = index
.entries
.iter()
.filter(|((kind, key), _)| !replaced.contains(&(*kind, key.as_str())))
.map(|((kind, key), span)| (*kind, key, *span))
.collect();
carried.sort_by(|a, b| (a.1, a.0 as u8).cmp(&(b.1, b.0 as u8)));
let sources =
carried
.iter()
.map(|(kind, key, span)| (*kind, key.as_str(), span.len, Source::Carried(*span)))
.chain(stored.iter().map(|(kind, key, bytes)| {
(*kind, *key, bytes.len() as u64, Source::Stored(bytes))
}));
let mut offset = 0u64;
sources
.map(|(kind, key, len, source)| {
let entry = CacheEntry {
kind,
key: key.to_owned(),
offset,
len,
};
offset += len;
Planned { entry, source }
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
const PAYLOAD: CacheEntryKind = CacheEntryKind::Payload;
const EXPANSION: CacheEntryKind = CacheEntryKind::Expansion;
const TOKEN: u32 = 0xC0FFEE;
fn segment_path(dir: &tempfile::TempDir) -> PathBuf {
dir.path().join("cache").join("1")
}
fn written(path: &Path, items: &[(CacheEntryKind, &str, &[u8])]) -> bool {
let index = Index::read(path, TOKEN);
write(path, &index, items, TOKEN)
}
#[test]
fn an_entry_round_trips_through_a_segment() {
let dir = tempfile::tempdir().unwrap();
let path = segment_path(&dir);
assert!(written(&path, &[(PAYLOAD, "cafe", &[1, 2, 3])]));
let index = Index::read(&path, TOKEN);
assert_eq!(index.get(PAYLOAD, "cafe"), Some(vec![1, 2, 3]));
assert_eq!(index.get(PAYLOAD, "f00d"), None);
}
#[test]
fn a_payload_and_an_expansion_may_share_one_key() {
let dir = tempfile::tempdir().unwrap();
let path = segment_path(&dir);
written(
&path,
&[(PAYLOAD, "cafe", &[1, 2, 3]), (EXPANSION, "cafe", &[9])],
);
let index = Index::read(&path, TOKEN);
assert_eq!(index.get(PAYLOAD, "cafe"), Some(vec![1, 2, 3]));
assert_eq!(index.get(EXPANSION, "cafe"), Some(vec![9]));
}
#[test]
fn a_later_build_carries_the_earlier_entries_through() {
let dir = tempfile::tempdir().unwrap();
let path = segment_path(&dir);
written(&path, &[(PAYLOAD, "aa", &[1]), (PAYLOAD, "bb", &[2, 2])]);
let first = Index::read(&path, TOKEN);
assert!(write(&path, &first, &[(PAYLOAD, "cc", &[3; 300])], TOKEN));
let second = Index::read(&path, TOKEN);
assert_eq!(second.len(), 3);
assert_eq!(second.get(PAYLOAD, "aa"), Some(vec![1]));
assert_eq!(second.get(PAYLOAD, "bb"), Some(vec![2, 2]));
assert_eq!(second.get(PAYLOAD, "cc"), Some(vec![3; 300]));
}
#[test]
fn a_restored_key_replaces_what_the_segment_held() {
let dir = tempfile::tempdir().unwrap();
let path = segment_path(&dir);
written(&path, &[(PAYLOAD, "aa", &[1]), (PAYLOAD, "bb", &[2])]);
let first = Index::read(&path, TOKEN);
write(&path, &first, &[(PAYLOAD, "aa", &[7, 7, 7])], TOKEN);
let second = Index::read(&path, TOKEN);
assert_eq!(second.len(), 2);
assert_eq!(second.get(PAYLOAD, "aa"), Some(vec![7, 7, 7]));
assert_eq!(second.get(PAYLOAD, "bb"), Some(vec![2]));
}
#[test]
fn one_set_of_entries_writes_one_image() {
let dir = tempfile::tempdir().unwrap();
let items: &[(CacheEntryKind, &str, &[u8])] = &[
(PAYLOAD, "bb", &[2, 2]),
(EXPANSION, "aa", &[1]),
(PAYLOAD, "aa", &[3, 3, 3]),
];
let one = segment_path(&dir);
written(&one, items);
let two = dir.path().join("second").join("1");
write(&two, &Index::read(&one, TOKEN), &[], TOKEN);
let three = dir.path().join("third").join("1");
write(&three, &Index::read(&two, TOKEN), &[], TOKEN);
assert_eq!(std::fs::read(&two).unwrap(), std::fs::read(&three).unwrap());
}
#[test]
fn an_absent_segment_reads_empty_and_is_recreated() {
let dir = tempfile::tempdir().unwrap();
let path = segment_path(&dir);
assert_eq!(Index::read(&path, TOKEN).get(PAYLOAD, "cafe"), None);
written(&path, &[(PAYLOAD, "cafe", &[1])]);
std::fs::remove_dir_all(path.parent().unwrap()).unwrap();
assert_eq!(Index::read(&path, TOKEN).get(PAYLOAD, "cafe"), None);
written(&path, &[(PAYLOAD, "cafe", &[1])]);
assert_eq!(
Index::read(&path, TOKEN).get(PAYLOAD, "cafe"),
Some(vec![1])
);
}
#[test]
fn a_segment_of_another_binary_reads_empty() {
let dir = tempfile::tempdir().unwrap();
let path = segment_path(&dir);
written(&path, &[(PAYLOAD, "cafe", &[1, 2, 3])]);
assert_eq!(Index::read(&path, TOKEN + 1).get(PAYLOAD, "cafe"), None);
assert_eq!(
Index::read(&path, TOKEN).get(PAYLOAD, "cafe"),
Some(vec![1, 2, 3])
);
write(
&path,
&Index::read(&path, TOKEN + 1),
&[(PAYLOAD, "f00d", &[4])],
TOKEN + 1,
);
let reread = Index::read(&path, TOKEN + 1);
assert_eq!(reread.len(), 1);
assert_eq!(reread.get(PAYLOAD, "f00d"), Some(vec![4]));
}
#[test]
fn a_corrupt_segment_reads_empty_and_is_replaced() {
let dir = tempfile::tempdir().unwrap();
let path = segment_path(&dir);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, b"not a segment at all").unwrap();
assert_eq!(Index::read(&path, TOKEN).get(PAYLOAD, "cafe"), None);
written(&path, &[(PAYLOAD, "cafe", &[7])]);
assert_eq!(
Index::read(&path, TOKEN).get(PAYLOAD, "cafe"),
Some(vec![7])
);
}
#[test]
fn an_entry_pointing_past_the_file_is_dropped() {
let dir = tempfile::tempdir().unwrap();
let path = segment_path(&dir);
written(&path, &[(PAYLOAD, "aa", &[1]), (PAYLOAD, "bb", &[2; 64])]);
let image = std::fs::read(&path).unwrap();
std::fs::write(&path, &image[..image.len() - 8]).unwrap();
let index = Index::read(&path, TOKEN);
assert_eq!(index.get(PAYLOAD, "aa"), Some(vec![1]));
assert_eq!(index.get(PAYLOAD, "bb"), None);
assert_eq!(index.len(), 1);
}
#[test]
fn the_runtime_segment_is_left_alone() {
let dir = tempfile::tempdir().unwrap();
let path = segment_path(&dir);
let sibling = path.parent().unwrap().join("0");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&sibling, b"runtime segment").unwrap();
written(&path, &[(PAYLOAD, "cafe", &[1])]);
assert_eq!(std::fs::read(&sibling).unwrap(), b"runtime segment");
}
#[test]
fn a_large_entry_survives_being_carried_through() {
let dir = tempfile::tempdir().unwrap();
let path = segment_path(&dir);
let big: Vec<u8> = (0..512 * 1024).map(|i| (i % 251) as u8).collect();
written(&path, &[(PAYLOAD, "big", &big), (PAYLOAD, "small", &[9])]);
let first = Index::read(&path, TOKEN);
write(&path, &first, &[(PAYLOAD, "later", &[8])], TOKEN);
let second = Index::read(&path, TOKEN);
assert_eq!(second.get(PAYLOAD, "big"), Some(big));
assert_eq!(second.get(PAYLOAD, "small"), Some(vec![9]));
assert_eq!(second.get(PAYLOAD, "later"), Some(vec![8]));
}
}