//! onset-core for the hosted service, through a C ABI. //! //! The Worker owns storage. It loads objects into this module's cache //! (`cache_put`), then calls an operation. Each project gets its own cache //! slot (`cache_use`), since one isolate can serve several projects. An operation that reaches an //! object the cache doesn't have answers `{"need": [ids]}`; the Worker loads //! them and calls again (operations are deterministic). Answers are JSON in //! the result buffer (`result_ptr`/`result_len`); objects an operation made //! come back as a bundle from `take_new`. use std::cell::RefCell; use std::collections::{HashMap, HashSet, VecDeque}; use std::io::Read; use serde::Deserialize; use serde_json::{Value, json}; use sha1::{Digest, Sha1}; use onset_core::bundle::{self, Frame, RAW, ZSTD}; use onset_core::merge::{self, Mode}; use onset_core::object::{Kind, Object, decode_chunks}; use onset_core::tree::{self, EntryKind}; use onset_core::{Error, Id, MemSource, Source, filter}; thread_local! { static CACHES: RefCell> = RefCell::new(HashMap::new()); static SLOT: std::cell::Cell = const { std::cell::Cell::new(0) }; static NEW: RefCell = RefCell::new(MemSource::default()); static RESULT: RefCell> = const { RefCell::new(Vec::new()) }; } #[unsafe(no_mangle)] pub extern "C" fn alloc(len: usize) -> *mut u8 { let mut buf = Vec::::with_capacity(len.max(1)); let ptr = buf.as_mut_ptr(); std::mem::forget(buf); ptr } /// # Safety /// `ptr` and `len` must come from `alloc`. #[unsafe(no_mangle)] pub unsafe extern "C" fn dealloc(ptr: *mut u8, len: usize) { unsafe { drop(Vec::from_raw_parts(ptr, 0, len.max(1))) }; } #[unsafe(no_mangle)] pub extern "C" fn result_ptr() -> *const u8 { RESULT.with(|r| r.borrow().as_ptr()) } #[unsafe(no_mangle)] pub extern "C" fn result_len() -> usize { RESULT.with(|r| r.borrow().len()) } /// SHA-256 of the input: the 32-byte digest, at once. (Web Crypto's /// digest is asynchronous, and a landing waited on it twice.) #[unsafe(no_mangle)] pub extern "C" fn sha256(ptr: *const u8, len: usize) -> i32 { use sha2::{Digest, Sha256}; set_result(Sha256::digest(input(ptr, len)).to_vec()); 0 } fn set_result(bytes: Vec) { RESULT.with(|r| *r.borrow_mut() = bytes); } fn answer(value: Value) -> i32 { set_result(serde_json::to_vec(&value).unwrap_or_default()); 0 } fn fail(message: String) -> i32 { set_result(serde_json::to_vec(&json!({ "error": message })).unwrap_or_default()); -1 } fn input<'a>(ptr: *const u8, len: usize) -> &'a [u8] { // SAFETY: the Worker passes a buffer it allocated with `alloc` and filled. unsafe { std::slice::from_raw_parts(ptr, len) } } struct Cache; impl Source for Cache { fn get(&self, id: &Id) -> onset_core::Result> { if let Some(o) = NEW.with(|n| n.borrow().objects.get(id).cloned()) { return Ok(Some(o)); } Ok(CACHES.with(|c| c.borrow().get(&SLOT.get()).and_then(|s| s.objects.get(id).cloned()))) } } /// The biggest object opened here. Files over 1 MiB are stored as chunks /// of at most 256 KiB, so only a tree of a folder with hundreds of /// thousands of files comes near it; the isolate has 128 MB. const MAX_OBJECT: u64 = 32 << 20; fn decode(frame: &Frame) -> Result, String> { if frame.raw_len > MAX_OBJECT { return Err(format!("object {} is over {} MiB", frame.id.short(), MAX_OBJECT >> 20)); } match frame.codec { RAW => Ok(frame.payload.clone()), ZSTD => { // What a frame says it holds isn't trusted: room for what it // claims grows as it's read, and reading stops just past it. let mut out = Vec::with_capacity((frame.raw_len as usize).min(frame.payload.len().saturating_mul(4)).min(1 << 20)); ruzstd::decoding::StreamingDecoder::new(&frame.payload[..]) .map_err(|e| format!("damaged object {}: {e}", frame.id.short()))? .take(frame.raw_len + 1) .read_to_end(&mut out) .map_err(|e| format!("damaged object {}: {e}", frame.id.short()))?; Ok(out) } _ => Err("unknown codec".into()), } } #[derive(Default)] struct Slot { objects: HashMap, bytes: usize, } fn insert(id: Id, object: Object) { let size = object.data.len() + 64; CACHES.with(|c| { let mut caches = c.borrow_mut(); let slot = caches.entry(SLOT.get()).or_default(); if slot.objects.insert(id, object).is_none() { slot.bytes += size; } }); } /// Checks every object in a bundle, caching them when `cache`. Answers each /// frame's place in the bundle, for the Worker's index. fn take_bundle(bytes: &[u8], cache: bool) -> Result, String> { let frames = bundle::read(bytes).map_err(|e| e.to_string())?; let mut out = Vec::with_capacity(frames.len()); let mut at = bundle::MAGIC.len(); for frame in frames { let len = 50 + frame.payload.len(); let data = decode(&frame)?; bundle::verify(&frame, &data).map_err(|e| e.to_string())?; out.push(json!({ "id": frame.id.hex(), "kind": frame.kind as u8, "off": at, "len": len, "raw": frame.raw_len })); at += len; if cache { insert(frame.id, Object::checked(frame.kind, data, frame.id)); } } Ok(out) } /// Verifies a bundle (an upload) and answers where each frame is. #[unsafe(no_mangle)] pub extern "C" fn verify(ptr: *const u8, len: usize) -> i32 { match take_bundle(input(ptr, len), false) { Ok(frames) => answer(json!({ "frames": frames })), Err(e) => fail(e), } } /// Caches every object in a bundle (frames loaded from storage). #[unsafe(no_mangle)] pub extern "C" fn cache_put(ptr: *const u8, len: usize) -> i32 { match take_bundle(input(ptr, len), true) { Ok(frames) => answer(json!({ "cached": frames.len() })), Err(e) => fail(e), } } /// Selects the cache slot (one per project) later calls use. #[unsafe(no_mangle)] pub extern "C" fn cache_use(slot: u32) { SLOT.set(slot); } /// Empties one slot. #[unsafe(no_mangle)] pub extern "C" fn cache_drop(slot: u32) { CACHES.with(|c| c.borrow_mut().remove(&slot)); } /// Cached bytes in every slot, in KiB. #[unsafe(no_mangle)] pub extern "C" fn cache_kib() -> u32 { CACHES.with(|c| (c.borrow().values().map(|s| s.bytes).sum::() / 1024) as u32) } /// The objects the last operation made, as a bundle (and forgets them). #[unsafe(no_mangle)] pub extern "C" fn take_new() -> i32 { let objects = NEW.with(|n| std::mem::take(&mut *n.borrow_mut())); let mut out = Vec::new(); bundle::write_header(&mut out); for (id, o) in objects.objects { // Its ID is the map's key: hashing the object again costs as much as making it. bundle::write_frame(&mut out, &Frame { id, kind: o.kind, codec: RAW, raw_len: o.data.len() as u64, payload: o.data.clone() }); insert(id, o); } bundle::write_end(&mut out); set_result(out); 0 } fn parse Deserialize<'de>>(ptr: *const u8, len: usize) -> Result { serde_json::from_slice(input(ptr, len)).map_err(|e| format!("bad request: {e}")) } fn id(text: &str) -> Result { Id::parse(text).ok_or_else(|| format!("not an object id: {text}")) } /// Runs an operation; a missing object becomes `{"need": [id]}`. fn run(op: impl FnOnce() -> Result) -> i32 { match op() { Ok(v) => answer(v), Err(Error::Missing(missing)) => answer(json!({ "need": [missing.hex()] })), Err(e) => fail(e.to_string()), } } #[derive(Deserialize)] struct Roots { roots: Vec, } /// Trees (and chunk lists) under `roots` that the cache doesn't have yet, /// nearest first: the Worker loads these before an operation that walks /// whole trees. #[unsafe(no_mangle)] pub extern "C" fn need_trees(ptr: *const u8, len: usize) -> i32 { let req: Roots = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let mut need = Vec::new(); let mut seen = HashSet::new(); let mut queue: VecDeque = VecDeque::new(); for r in &req.roots { match id(r) { Ok(i) => queue.push_back(i), Err(e) => return fail(e), } } while let Some(t) = queue.pop_front() { if !seen.insert(t) { continue; } match Cache.get(&t) { Ok(Some(o)) if o.kind == Kind::Tree => { if let Ok(entries) = tree::decode(&o.data) { for e in entries { if e.kind == EntryKind::Tree { queue.push_back(e.id); } } } } Ok(Some(_)) => {} _ => need.push(t.hex()), } if need.len() >= 2000 { break; } } answer(json!({ "need": need })) } #[derive(Deserialize)] struct MergeReq { base: String, ours: String, theirs: String, #[serde(default)] labels: Vec, } /// A landing's merge: `{"tree", "conflicts"}`. #[unsafe(no_mangle)] pub extern "C" fn merge(ptr: *const u8, len: usize) -> i32 { let req: MergeReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let (base, ours, theirs) = match (id(&req.base), id(&req.ours), id(&req.theirs)) { (Ok(a), Ok(b), Ok(c)) => (a, b, c), _ => return fail("bad ids".into()), }; let labels = (req.labels.first().cloned().unwrap_or_else(|| "main".into()), req.labels.get(1).cloned().unwrap_or_else(|| "change".into())); run(|| { let m = merge::trees(&Cache, Some(&base), &ours, &theirs, Mode::Land, (&labels.0, &labels.1))?; if m.clean() { NEW.with(|n| *n.borrow_mut() = m.objects); } Ok(json!({ "tree": m.tree.hex(), "conflicts": m.conflicts })) }) } #[derive(Deserialize)] struct FilterReq { tree: String, hidden: Vec, } #[unsafe(no_mangle)] pub extern "C" fn filter_tree(ptr: *const u8, len: usize) -> i32 { let req: FilterReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let t = match id(&req.tree) { Ok(t) => t, Err(e) => return fail(e), }; run(|| { let (out, objects) = filter::filter(&Cache, &t, &req.hidden)?; NEW.with(|n| { let mut n = n.borrow_mut(); for o in objects { n.put(o); } }); Ok(json!({ "tree": out.hex() })) }) } #[derive(Deserialize)] struct GraftReq { visible: String, full: String, hidden: Vec, } #[unsafe(no_mangle)] pub extern "C" fn graft_tree(ptr: *const u8, len: usize) -> i32 { let req: GraftReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let (v, f) = match (id(&req.visible), id(&req.full)) { (Ok(a), Ok(b)) => (a, b), _ => return fail("bad ids".into()), }; run(|| match filter::graft(&Cache, &v, &f, &req.hidden)? { Ok((out, objects)) => { NEW.with(|n| { let mut n = n.borrow_mut(); for o in objects { n.put(o); } }); Ok(json!({ "tree": out.hex() })) } Err(path) => Ok(json!({ "refused": path })), }) } #[derive(Deserialize)] struct EditReq { tree: String, /// `[path, kind (1 file, 2 executable), content in base64]`. #[serde(default)] put: Vec<(String, u8, String)>, #[serde(default)] delete: Vec, /// Also say which of the paths are in `tree` already (`existing`). #[serde(default)] existing: bool, } /// A tree with files written and deleted: `{"tree", "existing"?}`, or `{"bad_path"}`. #[unsafe(no_mangle)] pub extern "C" fn edit_tree(ptr: *const u8, len: usize) -> i32 { use base64::Engine; let req: EditReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let t = match id(&req.tree) { Ok(t) => t, Err(e) => return fail(e), }; let mut put = Vec::with_capacity(req.put.len()); for (path, kind, b64) in &req.put { let Ok(content) = base64::engine::general_purpose::STANDARD.decode(b64) else { return fail(format!("{path}: content isn't base64")) }; put.push(tree::Put { path: path.clone(), kind: if *kind == 2 { EntryKind::Exec } else { EntryKind::File }, content }); } run(|| match tree::edit(&Cache, &t, &put, &req.delete)? { Ok((root, objects)) => { let mut existing = Vec::new(); if req.existing { for path in put.iter().map(|p| p.path.as_str()).chain(req.delete.iter().map(String::as_str)) { if tree::lookup(&Cache, &t, path)?.is_some() { existing.push(path.to_string()); } } } NEW.with(|n| { let mut n = n.borrow_mut(); for o in objects { n.put(o); } }); Ok(json!({ "tree": root.hex(), "existing": existing })) } Err(path) => Ok(json!({ "bad_path": path })), }) } #[derive(Deserialize)] struct ScanReq { old: String, new: String, } /// The first path `new` adds over `old` that no project may hold (Onset's or /// git's own folder, a backslash): `{"bad": path | null}`. #[unsafe(no_mangle)] pub extern "C" fn unsafe_path(ptr: *const u8, len: usize) -> i32 { let req: ScanReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let (old, new) = match (id(&req.old), id(&req.new)) { (Ok(a), Ok(b)) => (a, b), _ => return fail("bad ids".into()), }; run(|| Ok(json!({ "bad": tree::unsafe_path(&Cache, &old, &new)? }))) } /// Secrets in lines `new` adds over `old`: `{"hits": [{path, line, kind}]}`. #[unsafe(no_mangle)] pub extern "C" fn secrets(ptr: *const u8, len: usize) -> i32 { let req: ScanReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let (old, new) = match (id(&req.old), id(&req.new)) { (Ok(a), Ok(b)) => (a, b), _ => return fail("bad ids".into()), }; run(|| Ok(json!({ "hits": onset_core::secrets::scan(&Cache, &old, &new)? }))) } #[derive(Deserialize)] struct PathReq { tree: String, #[serde(default)] path: String, } /// The entry at a path: `{"entry": {kind, id} | null}` (the root is a tree). #[unsafe(no_mangle)] pub extern "C" fn lookup(ptr: *const u8, len: usize) -> i32 { let req: PathReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let root = match id(&req.tree) { Ok(t) => t, Err(e) => return fail(e), }; run(|| { let mut at = (EntryKind::Tree, root); for part in req.path.split('/').filter(|p| !p.is_empty()) { if at.0 != EntryKind::Tree { return Ok(json!({ "entry": null })); } match tree::read(&Cache, &at.1)?.into_iter().find(|e| e.name == part) { Some(e) => at = (e.kind, e.id), None => return Ok(json!({ "entry": null })), } } Ok(json!({ "entry": { "kind": at.0.name(), "id": at.1.hex() } })) }) } #[derive(Deserialize)] struct RenamedFromReq { old: String, new: String, path: String, } /// Where `path` in tree `new` moved from in tree `old`: `{"from": path or null}`. #[unsafe(no_mangle)] pub extern "C" fn renamed_from(ptr: *const u8, len: usize) -> i32 { let req: RenamedFromReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let (old, new) = match (id(&req.old), id(&req.new)) { (Ok(o), Ok(n)) => (o, n), (Err(e), _) | (_, Err(e)) => return fail(e), }; run(|| Ok(json!({ "from": onset_core::rename::renamed_from(&Cache, &old, &new, &req.path)? }))) } #[derive(Deserialize)] struct PurgeReq { trees: Vec, paths: Vec, } /// Trees without `paths` (on purge): `{"map": {old: new}, "gone": [ids /// that were at them]}` for trees that had any, or `{"need": [ids]}`: every /// folder on the way that the cache doesn't have yet, for all of them at once. #[unsafe(no_mangle)] pub extern "C" fn purge_trees(ptr: *const u8, len: usize) -> i32 { use std::collections::BTreeSet; use onset_core::purge; let req: PurgeReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let mut trees = Vec::with_capacity(req.trees.len()); for t in &req.trees { match id(t) { Ok(t) => trees.push(t), Err(e) => return fail(e), } } let mut need = BTreeSet::new(); for t in &trees { if let Err(e) = purge::needs(&Cache, t, &req.paths, &mut need) { return fail(e.to_string()); } if need.len() >= 2000 { break; } } if !need.is_empty() { return answer(json!({ "need": need.iter().map(|i| i.hex()).collect::>() })); } run(|| { let mut gone = BTreeSet::new(); let mut map = serde_json::Map::new(); for t in &trees { let (after, objects) = purge::without(&Cache, t, &req.paths, &mut gone)?; if after != *t { NEW.with(|n| { let mut n = n.borrow_mut(); for o in objects { n.put(o); } }); map.insert(t.hex(), Value::String(after.hex())); } } Ok(json!({ "map": map, "gone": gone.iter().map(|i| i.hex()).collect::>() })) }) } #[derive(Deserialize)] struct LiveReq { trees: Vec, gone: Vec, } /// One level of a walk over what's kept (after on purge): the folders in /// `trees` (loaded first) and which of `gone` they hold: /// `{"trees": [ids], "hits": [ids]}`, or `{"need": [ids]}`. #[unsafe(no_mangle)] pub extern "C" fn live_level(ptr: *const u8, len: usize) -> i32 { let req: LiveReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let gone: HashSet<&str> = req.gone.iter().map(String::as_str).collect(); let (mut next, mut hits, mut need) = (Vec::new(), HashSet::new(), Vec::new()); for t in &req.trees { let t = match id(t) { Ok(t) => t, Err(e) => return fail(e), }; match Cache.get(&t) { Ok(Some(o)) if o.kind == Kind::Tree => { let Ok(entries) = tree::decode(&o.data) else { return fail(format!("damaged tree {}", t.short())) }; for e in entries { let hex = e.id.hex(); if gone.contains(hex.as_str()) { hits.insert(hex.clone()); } if e.kind == EntryKind::Tree { next.push(hex); } } } Ok(Some(_)) => {} _ => need.push(t.hex()), } } if !need.is_empty() { return answer(json!({ "need": need })); } answer(json!({ "trees": next, "hits": hits.into_iter().collect::>() })) } #[derive(Deserialize)] struct HistoryReq { /// Versions of main, newest first: [n, tree]. versions: Vec<(u64, String)>, path: String, /// The oldest of `versions` is version 1. from_start: bool, } /// The versions that changed a path, newest first: /// `{"changes": [[n, "added" | "modified" | "removed", id | null]]}`, or /// `{"need": [ids]}`: every folder one level needs. #[unsafe(no_mangle)] pub extern "C" fn path_history(ptr: *const u8, len: usize) -> i32 { use onset_core::history::{self, Found, Kind as Change}; let req: HistoryReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let mut ns = Vec::with_capacity(req.versions.len()); let mut trees = Vec::with_capacity(req.versions.len()); for (n, t) in &req.versions { match id(t) { Ok(t) => { ns.push(*n); trees.push(t); } Err(e) => return fail(e), } } run(|| { Ok(match history::entries(&Cache, &trees, &req.path)? { Found::Need(need) => json!({ "need": need.iter().map(|i| i.hex()).collect::>() }), Found::All(found) => { let changes: Vec = history::changes(&ns, &found, req.from_start) .into_iter() .map(|(n, kind, after)| { let kind = match kind { Change::Added => "added", Change::Modified => "modified", Change::Removed => "removed", }; json!([n, kind, after.map(|e| e.id.hex())]) }) .collect(); json!({ "changes": changes }) } }) }) } #[derive(Deserialize)] struct TouchedReq { /// [before (null: nothing), after] trees. pairs: Vec<(Option, String)>, most: usize, } /// The paths each pair of trees changes (folders added or removed whole as /// the folder): `{"touched": [[paths] | null]}` (null: more than `most`), or /// `{"need": [ids]}`. #[unsafe(no_mangle)] pub extern "C" fn touched(ptr: *const u8, len: usize) -> i32 { use onset_core::history::{self, Touched}; let req: TouchedReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let mut pairs = Vec::with_capacity(req.pairs.len()); for (a, b) in &req.pairs { let a = match a.as_deref().map(id).transpose() { Ok(a) => a, Err(e) => return fail(e), }; match id(b) { Ok(b) => pairs.push((a, b)), Err(e) => return fail(e), } } run(|| { Ok(match history::touched(&Cache, &pairs, req.most)? { Touched::Need(need) => json!({ "need": need.iter().map(|i| i.hex()).collect::>() }), Touched::All(found) => json!({ "touched": found }), }) }) } #[derive(Deserialize)] struct BlameReq { /// The versions that changed the file, newest first: [n, file id or null]. points: Vec<(u64, Option)>, } /// Which version last changed each line of the newest content: /// `{"owners": [[n, lines]...], "base64": content}` (runs of lines), or /// `{"need": [ids]}`. #[unsafe(no_mangle)] pub extern "C" fn blame(ptr: *const u8, len: usize) -> i32 { use base64::Engine; let req: BlameReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let mut ids = Vec::with_capacity(req.points.len()); for (_, f) in &req.points { match f.as_deref().map(id) { Some(Ok(i)) => ids.push(Some(i)), Some(Err(e)) => return fail(e), None => ids.push(None), } } run(|| { // Every file, then every big file's parts, asked for at once. let mut need: Vec = ids.iter().flatten().filter(|i| Cache.get(i).ok().flatten().is_none()).map(|i| i.hex()).collect(); if need.is_empty() { for i in ids.iter().flatten() { if let Some(o) = Cache.get(i)? && o.kind == Kind::Chunks { need.extend(decode_chunks(&o.data)?.into_iter().filter(|(c, _)| Cache.get(c).ok().flatten().is_none()).map(|(c, _)| c.hex())); } } } if !need.is_empty() { need.sort(); need.dedup(); return Ok(json!({ "need": need })); } let mut contents = Vec::with_capacity(ids.len()); for i in &ids { contents.push(match i { Some(i) => onset_core::chunk::read_file(&Cache, i)?, None => Vec::new(), }); } let points: Vec<(u64, &[u8])> = req.points.iter().zip(&contents).map(|((n, _), c)| (*n, c.as_slice())).collect(); let mut runs: Vec<(u64, u32)> = Vec::new(); for n in onset_core::history::blame(&points) { match runs.last_mut() { Some((last, count)) if *last == n => *count += 1, _ => runs.push((n, 1)), } } let newest = contents.first().map(|c| base64::engine::general_purpose::STANDARD.encode(c)).unwrap_or_default(); Ok(json!({ "owners": runs, "base64": newest })) }) } /// A folder's entries: `{"entries": [[name, kind]]}`. #[unsafe(no_mangle)] pub extern "C" fn list_dir(ptr: *const u8, len: usize) -> i32 { let req: PathReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let t = match id(&req.tree) { Ok(t) => t, Err(e) => return fail(e), }; run(|| Ok(json!({ "entries": tree::read(&Cache, &t)?.iter().map(|e| json!([e.name, e.kind.name()])).collect::>() }))) } #[derive(Deserialize)] struct ReadReq { id: String, #[serde(default)] max: Option, } /// A file's content: `{"base64", "size"}`, or `{"too_big": size}` over `max` bytes. #[unsafe(no_mangle)] pub extern "C" fn read_file(ptr: *const u8, len: usize) -> i32 { use base64::Engine; let req: ReadReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let f = match id(&req.id) { Ok(t) => t, Err(e) => return fail(e), }; run(|| { let size = onset_core::chunk::file_size(&Cache, &f)?; if req.max.is_some_and(|m| size as usize > m) { return Ok(json!({ "too_big": size })); } let bytes = onset_core::chunk::read_file(&Cache, &f)?; Ok(json!({ "base64": base64::engine::general_purpose::STANDARD.encode(&bytes), "size": bytes.len() })) }) } /// The first hidden path a tree holds: `{"found": path | null}`. #[unsafe(no_mangle)] pub extern "C" fn hidden_in(ptr: *const u8, len: usize) -> i32 { let req: FilterReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let t = match id(&req.tree) { Ok(t) => t, Err(e) => return fail(e), }; run(|| Ok(json!({ "found": filter::holds_hidden(&Cache, &t, &req.hidden)? }))) } #[derive(Deserialize)] struct SinceReq { tree: String, #[serde(default)] have: Option, } /// Every object under `tree` that `have` doesn't share, chunks included. #[unsafe(no_mangle)] pub extern "C" fn new_since(ptr: *const u8, len: usize) -> i32 { let req: SinceReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let t = match id(&req.tree) { Ok(t) => t, Err(e) => return fail(e), }; let have = match req.have.as_deref().map(id) { Some(Ok(h)) => Some(h), Some(Err(e)) => return fail(e), None => None, }; run(|| { let mut ids = Vec::new(); tree::new_since(&Cache, have.as_ref(), &t, &mut ids)?; let mut out = Vec::with_capacity(ids.len()); let mut need = Vec::new(); for i in ids { out.push(i.hex()); // Chunk lists name chunks; the Worker knows a file's kind from // its index, so it asks for those lists separately. if let Some(o) = Cache.get(&i)? && o.kind == Kind::Chunks { for (c, _) in decode_chunks(&o.data)? { out.push(c.hex()); } } else if Cache.get(&i)?.is_none() { need.push(i.hex()); } } Ok(json!({ "ids": out, "files_not_loaded": need })) }) } #[derive(Deserialize)] struct FlattenReq { tree: String, } /// Every file under a tree: `[[path, kind, id]]`. #[unsafe(no_mangle)] pub extern "C" fn flatten(ptr: *const u8, len: usize) -> i32 { let req: FlattenReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let t = match id(&req.tree) { Ok(t) => t, Err(e) => return fail(e), }; run(|| { let files = tree::flatten(&Cache, &t)?; Ok(json!({ "files": files.iter().map(|(p, e)| json!([p, e.kind as u8, e.id.hex()])).collect::>() })) }) } // The git view: versions written as git objects, streamed as one pack. // // The Worker plans a version (`git_plan`) and looks up git IDs it already // knows. It starts a pack (`pack_begin`), hashes each new file (`blob_begin` // with write 0, `blob_feed` one stored frame at a time, `blob_end`), writes // the trees and the commit (`git_trees`), which names the commit it pushes, // then writes each new file into the pack (`blob_begin` with write 1) and // closes it (`pack_end`). Pack bytes collect in a buffer the Worker drains // with `pack_take`. struct GitPack { sha: Sha1, out: Vec, blob: Option, /// Native ID → git ID of everything this pack's trees name: the Worker /// sends each once per pack, not with every version. known: HashMap, /// With deltas on: each folder's tree as last written to this pack (git /// ID, bytes, and how many deltas deep it is), so the next version's /// tree there can be a delta on it. bases: Option, u8)>>, } /// Deltas made on deltas go this deep at most (git's own default is 50). const MAX_DELTA_DEPTH: u8 = 50; /// Trees smaller than this are written whole. const DELTA_MIN: usize = 1024; struct GitBlob { sha: Sha1, /// False: only hashing (the git ID is needed before the pack is sent). write: bool, size: u64, fed: u64, } thread_local! { static PACK: RefCell> = const { RefCell::new(None) }; /// One zlib stream, reused for every object: making one costs more /// than deflating a small object. static ZLIB: RefCell = RefCell::new(flate2::Compress::new(flate2::Compression::fast(), true)); } /// Deflates `data` onto the zlib stream (started with `zlib_start`); `end` /// finishes the stream. fn deflate(data: &[u8], end: bool) -> Vec { ZLIB.with(|z| { let mut z = z.borrow_mut(); let flush = if end { flate2::FlushCompress::Finish } else { flate2::FlushCompress::None }; let start = z.total_in(); let mut out = Vec::with_capacity(data.len() / 2 + 64); loop { let used = (z.total_in() - start) as usize; let status = z.compress_vec(&data[used..], &mut out, flush).expect("zlib"); let used = (z.total_in() - start) as usize; if (end && status == flate2::Status::StreamEnd) || (!end && used == data.len() && out.len() < out.capacity()) { return out; } out.reserve(out.capacity().max(64)); } }) } fn zlib_start() { ZLIB.with(|z| z.borrow_mut().reset()); } fn emit(pack: &mut GitPack, bytes: &[u8]) { pack.sha.update(bytes); pack.out.extend_from_slice(bytes); } /// A pack entry's header: type and size. fn entry_header(kind: u8, size: u64) -> Vec { let mut out = Vec::with_capacity(10); let mut byte = (kind << 4) | (size & 0x0f) as u8; let mut rest = size >> 4; while rest > 0 { out.push(byte | 0x80); byte = (rest & 0x7f) as u8; rest >>= 7; } out.push(byte); out } fn hex(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{b:02x}")).collect() } fn git_header(kind: &str, size: u64) -> Vec { format!("{kind} {size}\0").into_bytes() } /// Adds a whole object (trees, commits) to the pack; answers its git ID. fn emit_object(pack: &mut GitPack, kind: &str, type_code: u8, data: &[u8]) -> String { hex(&emit_raw(pack, kind, type_code, data)) } fn git_id(kind: &str, data: &[u8]) -> [u8; 20] { let mut h = Sha1::new(); h.update(git_header(kind, data.len() as u64)); h.update(data); h.finalize().into() } fn emit_raw(pack: &mut GitPack, kind: &str, type_code: u8, data: &[u8]) -> [u8; 20] { let id = git_id(kind, data); zlib_start(); let deflated = deflate(data, true); emit(pack, &entry_header(type_code, data.len() as u64)); emit(pack, &deflated); id } /// Adds the tree for folder `path` to the pack: as a delta on the folder's /// tree written earlier in this pack when deltas are on and that's much /// smaller (a version usually changes one entry of a big folder), else whole. fn emit_tree(pack: &mut GitPack, path: &str, data: Vec) -> [u8; 20] { let Some(bases) = pack.bases.as_mut() else { return emit_raw(pack, "tree", 2, &data) }; if data.len() < DELTA_MIN { return emit_raw_tree(pack, path, data, 0); } let made = bases.get(path).filter(|(_, _, depth)| *depth < MAX_DELTA_DEPTH).and_then(|(base_id, base, depth)| { onset_core::git::make_delta(base, &data).map(|delta| (*base_id, delta, depth + 1)) }); let Some((base_id, delta, depth)) = made else { return emit_raw_tree(pack, path, data, 0) }; let id = git_id("tree", &data); zlib_start(); let deflated = deflate(&delta, true); // REF_DELTA (7): the base's git ID, then the deflated delta. emit(pack, &entry_header(7, delta.len() as u64)); emit(pack, &base_id); emit(pack, &deflated); pack.bases.as_mut().unwrap().insert(path.to_string(), (id, data, depth)); id } fn emit_raw_tree(pack: &mut GitPack, path: &str, data: Vec, depth: u8) -> [u8; 20] { let id = emit_raw(pack, "tree", 2, &data); if data.len() >= DELTA_MIN { pack.bases.as_mut().unwrap().insert(path.to_string(), (id, data, depth)); } id } fn with_pack(f: impl FnOnce(&mut GitPack) -> Result) -> i32 { let result = PACK.with(|p| match p.borrow_mut().as_mut() { Some(pack) => f(pack), None => Err("no pack started".into()), }); match result { Ok(v) => answer(v), Err(e) => fail(e), } } /// Starts a pack of `count` objects; with `deltas`, a folder's tree can be /// written as a delta on its tree written earlier in the same pack. #[unsafe(no_mangle)] pub extern "C" fn pack_begin(count: u32, deltas: u32) -> i32 { let mut pack = GitPack { sha: Sha1::new(), out: Vec::new(), blob: None, known: HashMap::new(), bases: (deltas != 0).then(HashMap::new) }; let mut head = b"PACK".to_vec(); head.extend_from_slice(&2u32.to_be_bytes()); head.extend_from_slice(&count.to_be_bytes()); emit(&mut pack, &head); PACK.with(|p| *p.borrow_mut() = Some(pack)); answer(json!({})) } /// Pack bytes made so far (and forgets them). #[unsafe(no_mangle)] pub extern "C" fn pack_take() -> i32 { let bytes = PACK.with(|p| p.borrow_mut().as_mut().map(|pack| std::mem::take(&mut pack.out)).unwrap_or_default()); set_result(bytes); 0 } /// Ends the pack: the last bytes, with the checksum. #[unsafe(no_mangle)] pub extern "C" fn pack_end() -> i32 { let Some(mut pack) = PACK.with(|p| p.borrow_mut().take()) else { return fail("no pack started".into()) }; if pack.blob.is_some() { return fail("a file is still open".into()); } let sum = pack.sha.finalize(); pack.out.extend_from_slice(&sum); set_result(pack.out); 0 } /// Starts a file of `size` bytes (a JS number: files under 2^53 bytes). /// With `write` 0 the file is only hashed; with 1 it goes into the pack. #[unsafe(no_mangle)] pub extern "C" fn blob_begin(size: f64, write: u32) -> i32 { let size = size as u64; with_pack(|pack| { if pack.blob.is_some() { return Err("a file is already open".into()); } let mut sha = Sha1::new(); sha.update(git_header("blob", size)); if write != 0 { emit(pack, &entry_header(3, size)); zlib_start(); } pack.blob = Some(GitBlob { sha, write: write != 0, size, fed: 0 }); Ok(json!({})) }) } /// One stored frame of the open file: the whole blob, or its next chunk. #[unsafe(no_mangle)] pub extern "C" fn blob_feed(ptr: *const u8, len: usize) -> i32 { let frame = match one_frame(input(ptr, len)) { Ok(f) => f, Err(e) => return fail(e), }; with_pack(|pack| { let data = decode(&frame)?; bundle::verify(&frame, &data).map_err(|e| e.to_string())?; let blob = pack.blob.as_mut().ok_or("no file open")?; blob.sha.update(&data); blob.fed += data.len() as u64; if blob.write { let out = deflate(&data, false); emit(pack, &out); } Ok(json!({})) }) } #[unsafe(no_mangle)] pub extern "C" fn blob_end() -> i32 { with_pack(|pack| { let blob = pack.blob.take().ok_or("no file open")?; if blob.fed != blob.size { return Err(format!("file was {} bytes, expected {}", blob.fed, blob.size)); } if blob.write { let rest = deflate(&[], true); emit(pack, &rest); } Ok(json!({ "git": hex(&blob.sha.finalize()) })) }) } /// One frame as stored in a pack (no bundle header). fn one_frame(bytes: &[u8]) -> Result { let mut b = Vec::with_capacity(bundle::MAGIC.len() + bytes.len() + 32); bundle::write_header(&mut b); b.extend_from_slice(bytes); bundle::write_end(&mut b); let mut frames = bundle::read(&b).map_err(|e| e.to_string())?; if frames.len() != 1 { return Err("expected one object".into()); } Ok(frames.remove(0)) } /// A big file's chunk list (one stored frame): `{"chunks": [[id, size]], "size"}`. #[unsafe(no_mangle)] pub extern "C" fn chunk_list(ptr: *const u8, len: usize) -> i32 { let result = (|| { let frame = one_frame(input(ptr, len))?; let data = decode(&frame)?; bundle::verify(&frame, &data).map_err(|e| e.to_string())?; if frame.kind != Kind::Chunks { return Err("not a chunk list".to_string()); } let chunks = decode_chunks(&data).map_err(|e| e.to_string())?; let size: u64 = chunks.iter().map(|(_, n)| n).sum(); Ok(json!({ "chunks": chunks.iter().map(|(c, n)| json!([c.hex(), n])).collect::>(), "size": size })) })(); match result { Ok(v) => answer(v), Err(e) => fail(e), } } #[derive(Deserialize)] struct PlanReq { tree: String, #[serde(default)] have: Option, /// IDs whose git form isn't known: treated as new. #[serde(default)] force: Vec, /// The first version of a pack: entries looked up for earlier ones /// aren't answered again until the next pack starts. #[serde(default)] start: bool, } thread_local! { /// Entries `git_plan` answered for lookup since the pack's first version. static LOOKED: RefCell> = RefCell::new(HashSet::new()); } #[derive(Default)] struct Plan { trees: Vec, files: Vec, lookup: Vec, seen: HashSet, looked: HashSet, } fn plan_tree(t: &Id, have: Option<&Id>, force: &HashSet, plan: &mut Plan) -> Result<(), Error> { if !plan.seen.insert(*t) { return Ok(()); } plan.trees.push(*t); // Both folders' entries are sorted by name (as encode writes them): // walked side by side, without copying names. let tree_of = |id: &Id| -> Result { let o = Cache.must(id)?; if o.kind != Kind::Tree { return Err(Error::Corrupt(format!("{} isn't a tree", id.short()))); } Ok(o) }; let new = tree_of(t)?; let old = have.map(tree_of).transpose()?; if let (Some(entries), Some(before)) = (tree::raw_entries(&new.data)?, tree::raw_entries(old.as_ref().map_or(&[][..], |o| &o.data))?) { let mut at = 0; for e in &entries { while at < before.len() && before[at].name.as_bytes() < e.name.as_bytes() { at += 1; } let b = before.get(at).filter(|b| b.name == e.name); if e.kind == EntryKind::Gitlink { continue; } if b.is_some_and(|b| b.id == e.id) && !force.contains(&e.id) { if plan.looked.insert(e.id) { plan.lookup.push(e.id); } continue; } if e.kind == EntryKind::Tree { let old_sub = if force.contains(&e.id) { None } else { b.filter(|b| b.kind == EntryKind::Tree).map(|b| b.id) }; plan_tree(&e.id, old_sub.as_ref(), force, plan)?; } else if plan.seen.insert(e.id) { plan.files.push(e.id); } } return Ok(()); } let old: HashMap = match have { Some(h) => tree::read(&Cache, h)?.into_iter().map(|e| (e.name.clone(), e)).collect(), None => HashMap::new(), }; for e in tree::read(&Cache, t)? { if e.kind == EntryKind::Gitlink { // A submodule's commit isn't an object the pack carries. continue; } let before = old.get(&e.name); if before.is_some_and(|b| b.id == e.id) && !force.contains(&e.id) { if plan.looked.insert(e.id) { plan.lookup.push(e.id); } continue; } if e.kind == EntryKind::Tree { // A forced tree is rebuilt whole. let old_sub = if force.contains(&e.id) { None } else { before.filter(|b| b.kind == EntryKind::Tree).map(|b| b.id) }; plan_tree(&e.id, old_sub.as_ref(), force, plan)?; } else if plan.seen.insert(e.id) { plan.files.push(e.id); } } Ok(()) } #[derive(Deserialize)] struct PathsReq { /// (the version before, a version): top folders. pairs: Vec<(Option, String)>, } /// The folders along `new`'s changed paths (against `old`) that the cache /// doesn't have, as deep as what it has lets the walk see. fn missing_on_paths(old: Option<&Id>, new: &Id, out: &mut Vec, seen: &mut HashSet) -> Result<(), Error> { if old == Some(new) || !seen.insert(*new) { return Ok(()); } let Some(n) = Cache.get(new)? else { out.push(*new); return Ok(()); }; let o = match old { Some(id) => match Cache.get(id)? { Some(o) => Some(o), None => { if seen.insert(*id) { out.push(*id); } return Ok(()); } }, None => None, }; if n.kind != Kind::Tree || o.as_ref().is_some_and(|o| o.kind != Kind::Tree) { return Ok(()); } let (Some(entries), Some(before)) = (tree::raw_entries(&n.data)?, tree::raw_entries(o.as_ref().map_or(&[][..], |o| &o.data))?) else { return Ok(()) }; let mut at = 0; for e in entries.iter().filter(|e| e.kind == EntryKind::Tree) { while at < before.len() && before[at].name.as_bytes() < e.name.as_bytes() { at += 1; } let b = before.get(at).filter(|b| b.name == e.name && b.kind == EntryKind::Tree).map(|b| b.id); missing_on_paths(b.as_ref(), &e.id, out, seen)?; } Ok(()) } /// Folders a batch of versions will read for git that aren't loaded: the /// Worker loads them all at once, a level a round, before planning. #[unsafe(no_mangle)] pub extern "C" fn paths_missing(ptr: *const u8, len: usize) -> i32 { let req: PathsReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let mut out = Vec::new(); let mut seen = HashSet::new(); for (old, new) in &req.pairs { let (Some(new), old) = (Id::parse(new), old.as_deref().and_then(Id::parse)) else { return fail("bad tree id".into()) }; if let Err(e) = missing_on_paths(old.as_ref(), &new, &mut out, &mut seen) { return fail(e.to_string()); } } answer(json!({ "missing": out.iter().map(|i| i.hex()).collect::>() })) } /// What a version adds over the one before it, for git: new trees, new /// files, and unchanged entries whose git IDs the Worker looks up. #[unsafe(no_mangle)] pub extern "C" fn git_plan(ptr: *const u8, len: usize) -> i32 { let req: PlanReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let root = match id(&req.tree) { Ok(t) => t, Err(e) => return fail(e), }; let have = match req.have.as_deref().map(id) { Some(Ok(h)) => Some(h), Some(Err(e)) => return fail(e), None => None, }; let force: HashSet = req.force.iter().filter_map(|f| Id::parse(f)).collect(); run(|| { let mut plan = Plan::default(); if have == Some(root) && !force.contains(&root) { plan.lookup.push(root); } else { let have = if force.contains(&root) { None } else { have }; plan_tree(&root, have.as_ref(), &force, &mut plan)?; } // A big folder's unchanged entries are answered once a pack. LOOKED.with(|l| { let mut looked = l.borrow_mut(); if req.start { looked.clear(); } plan.lookup.retain(|id| looked.insert(*id)); }); let list = |ids: &[Id]| ids.iter().map(|i| i.hex()).collect::>(); Ok(json!({ "trees": list(&plan.trees), "files": list(&plan.files), "lookup": list(&plan.lookup) })) }) } #[derive(Deserialize)] struct TreesReq { tree: String, /// The plan's new trees: each is written to the pack once. trees: Vec, /// Native ID → git ID for everything else the trees name that wasn't /// sent for this pack before (the pack keeps them). known: HashMap, #[serde(default)] parents: Vec, message: String, author: String, time: i64, } fn git_tree(t: &Id, path: &str, fresh: &HashSet, made: &mut HashMap, pack: &mut GitPack) -> Result<[u8; 20], Error> { if let Some(g) = made.get(t) { return Ok(*g); } if !fresh.contains(t) { return pack.known.get(t).copied().ok_or_else(|| Error::Corrupt(format!("git id of tree {} isn't known", t.short()))); } let entries = tree::read(&Cache, t)?; let mut rows: Vec<(&str, bool, &'static [u8], [u8; 20])> = Vec::with_capacity(entries.len()); for e in &entries { let (mode, gid): (&'static [u8], [u8; 20]) = match e.kind { EntryKind::Tree => { let sub = if path.is_empty() { e.name.clone() } else { format!("{path}/{}", e.name) }; (b"40000", git_tree(&e.id, &sub, fresh, made, pack)?) } EntryKind::Gitlink => (b"160000", gitlink(&e.id)?), kind => { let gid = pack.known.get(&e.id).copied().ok_or_else(|| Error::Corrupt(format!("git id of file {} isn't known", e.id.short())))?; (match kind { EntryKind::Exec => b"100755", EntryKind::Link => b"120000", _ => b"100644", }, gid) } }; rows.push((&e.name, e.kind == EntryKind::Tree, mode, gid)); } // Git sorts entries by name, with folders compared as if they end in '/'. fn key(name: &str, folder: bool) -> impl Iterator + '_ { name.bytes().chain(folder.then_some(b'/')) } rows.sort_by(|a, b| key(a.0, a.1).cmp(key(b.0, b.1))); let mut data = Vec::with_capacity(rows.iter().map(|r| r.0.len() + r.2.len() + 22).sum()); for (name, _, mode, gid) in &rows { data.extend_from_slice(mode); data.push(b' '); data.extend_from_slice(name.as_bytes()); data.push(0); data.extend_from_slice(gid); } let g = emit_tree(pack, path, data); made.insert(*t, g); pack.known.insert(*t, g); Ok(g) } /// The commit a submodule entry points at (its content, 40 hex digits). fn gitlink(id: &Id) -> Result<[u8; 20], Error> { let text = String::from_utf8(onset_core::chunk::read_file(&Cache, id)?).map_err(|_| Error::Corrupt("bad submodule entry".into()))?; git_bytes(text.trim()).ok_or_else(|| Error::Corrupt("bad submodule entry".into())) } /// A git ID's 20 bytes from its 40 hex digits. fn git_bytes(hex: &str) -> Option<[u8; 20]> { if hex.len() != 40 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) { return None; } let mut out = [0u8; 20]; for (i, o) in out.iter_mut().enumerate() { *o = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).ok()?; } Some(out) } /// Writes the version's new trees and its commit to the pack. #[unsafe(no_mangle)] pub extern "C" fn git_trees(ptr: *const u8, len: usize) -> i32 { let req: TreesReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let root = match id(&req.tree) { Ok(t) => t, Err(e) => return fail(e), }; let fresh: HashSet = req.trees.iter().filter_map(|t| Id::parse(t)).collect(); // Every new tree is loaded first, so nothing reaches the pack twice, // and so are the submodule entries in them. let mut need: Vec = fresh.iter().filter(|t| Cache.get(t).ok().flatten().is_none()).map(|t| t.hex()).collect(); if need.is_empty() { for t in &fresh { if let Ok(entries) = tree::read(&Cache, t) { need.extend(entries.iter().filter(|e| e.kind == EntryKind::Gitlink && Cache.get(&e.id).ok().flatten().is_none()).map(|e| e.id.hex())); } } } if !need.is_empty() { return answer(json!({ "need": need })); } with_pack(|pack| { for (native, git) in &req.known { match (Id::parse(native), git_bytes(git)) { (Some(n), Some(g)) => { pack.known.insert(n, g); } _ => return Err(format!("bad git id for {native}")), } } let mut made = HashMap::new(); let tree_id = hex(&git_tree(&root, "", &fresh, &mut made, pack).map_err(|e| e.to_string())?); if made.len() != fresh.len() { return Err(format!("wrote {} trees, planned {}", made.len(), fresh.len())); } let (name, email) = match req.author.split_once(" <") { Some((n, e)) => (n.to_string(), e.trim_end_matches('>').to_string()), None => (req.author.clone(), "vcs@localhost".into()), }; let mut commit = format!("tree {tree_id}\n"); for p in &req.parents { if p.len() != 40 || !p.bytes().all(|b| b.is_ascii_hexdigit()) { return Err(format!("bad parent {p}")); } commit.push_str(&format!("parent {p}\n")); } commit.push_str(&format!("author {name} <{email}> {} +0000\ncommitter {name} <{email}> {} +0000\n\n{}", req.time, req.time, req.message)); if !commit.ends_with('\n') { commit.push('\n'); } let commit_id = emit_object(pack, "commit", 1, commit.as_bytes()); let trees: HashMap = made.into_iter().map(|(k, v)| (k.hex(), hex(&v))).collect(); Ok(json!({ "commit": commit_id, "tree": tree_id, "trees": trees })) }) } // Git main's new commits, coming in: a pack fetched from the git view is // read once, then its commits and trees are turned into versions. /// The most a fetched pack's objects can add up to. const GIT_IN_MAX: usize = 48 << 20; thread_local! { static GIT_IN: RefCell> = const { RefCell::new(None) }; } /// Reads a pack (raw bytes): `{"objects": n}`. #[unsafe(no_mangle)] pub extern "C" fn git_in_read(ptr: *const u8, len: usize) -> i32 { match onset_core::git::read_pack(input(ptr, len), GIT_IN_MAX) { Ok(pack) => { let n = pack.objects.len(); GIT_IN.with(|g| *g.borrow_mut() = Some(pack)); answer(json!({ "objects": n })) } Err(e) => fail(e.to_string()), } } #[unsafe(no_mangle)] pub extern "C" fn git_in_drop() { GIT_IN.with(|g| *g.borrow_mut() = None); } fn with_git_in(f: impl FnOnce(&onset_core::git::Pack) -> Result) -> i32 { let result = GIT_IN.with(|g| match g.borrow().as_ref() { Some(pack) => f(pack), None => Err("no git pack read".into()), }); match result { Ok(v) => answer(v), Err(e) => fail(e), } } #[derive(Deserialize)] struct GitInCommitsReq { from: String, to: String, } /// The commits that took git main from `from` to `to`, oldest first: /// each on the first-parent line, or only `to` when `from` is in its /// history some other way. `{"rewritten": true}` when it isn't (a force push). #[unsafe(no_mangle)] pub extern "C" fn git_in_commits(ptr: *const u8, len: usize) -> i32 { use onset_core::git; let req: GitInCommitsReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let (Some(from), Some(to)) = (git::parse_id(&req.from), git::parse_id(&req.to)) else { return fail("bad git ids".into()) }; with_git_in(|pack| { let view = |id: &git::GitId, c: &git::Commit| json!({ "id": git::hex(id), "tree": git::hex(&c.tree), "parents": c.parents.iter().map(git::hex).collect::>(), "author": c.author, "time": c.time, "title": c.title, "body": c.body }); if let Some(line) = git::first_parents(pack, &from, &to, 1000).map_err(|e| e.to_string())? { return Ok(json!({ "commits": line.iter().map(|(id, c)| view(id, c)).collect::>() })); } if !git::descends(pack, &from, &to).map_err(|e| e.to_string())? { return Ok(json!({ "rewritten": true })); } let Some((git::GitKind::Commit, data)) = pack.objects.get(&to) else { return Err("git main's commit isn't in the pack".into()) }; let c = git::Commit::parse(data).map_err(|e| e.to_string())?; Ok(json!({ "commits": [view(&to, &c)] })) }) } #[derive(Deserialize)] struct GitInTreeReq { tree: String, /// git ID → Onset ID for trees and files outside the pack. #[serde(default)] known: HashMap, } /// A git tree as an Onset tree: `{"tree", "ids": [[onset, git]]}` with its new /// objects made, or `{"unknown": [git ids]}` to look up first. #[unsafe(no_mangle)] pub extern "C" fn git_in_tree(ptr: *const u8, len: usize) -> i32 { use onset_core::git; let req: GitInTreeReq = match parse(ptr, len) { Ok(r) => r, Err(e) => return fail(e), }; let Some(root) = git::parse_id(&req.tree) else { return fail("bad git id".into()) }; let known: HashMap = req.known.iter().filter_map(|(g, v)| Some((git::parse_id(g)?, Id::parse(v)?))).collect(); with_git_in(|pack| { let mut to = git::ToOnset::new(pack, &known); let tree = to.tree(root, true).map_err(|e| e.to_string())?.unwrap_or_else(tree::empty_id); if !to.unknown.is_empty() { return Ok(json!({ "unknown": to.unknown.iter().map(git::hex).collect::>() })); } NEW.with(|n| { let mut n = n.borrow_mut(); for o in to.objects.drain(..) { n.put(o); } }); Ok(json!({ "tree": tree.hex(), "ids": to.ids.iter().map(|(v, g)| json!([v.hex(), git::hex(g)])).collect::>() })) }) } #[cfg(test)] mod tests { use super::*; use std::io::Write; use onset_core::tree::{Entry, Files}; fn sha1_git(kind: &str, data: &[u8]) -> String { let mut h = Sha1::new(); h.update(git_header(kind, data.len() as u64)); h.update(data); hex(&h.finalize()) } #[test] fn git_ids_match_git() { // `printf 'hello\n' | git hash-object --stdin` assert_eq!(sha1_git("blob", b"hello\n"), "ce013625030ba8dba906f756967f9e9ca394464a"); } fn feed(o: &Object) { let mut b = Vec::new(); bundle::write_frame(&mut b, &Frame::raw(o)); assert_eq!(blob_feed(b.as_ptr(), b.len()), 0); } fn result() -> Vec { RESULT.with(|r| r.borrow().clone()) } fn call_json(f: extern "C" fn(*const u8, usize) -> i32, v: Value) -> Value { let b = serde_json::to_vec(&v).unwrap(); assert_eq!(f(b.as_ptr(), b.len()), 0, "{}", String::from_utf8_lossy(&result())); serde_json::from_slice(&result()).unwrap() } /// A version's pack is one git accepts, with the IDs git computes. #[test] fn git_pack_is_git() { let small = Object::blob(b"fn main() {}\n".to_vec()); let big: Vec = (0..3_000_000u32).map(|i| (i.wrapping_mul(2654435761) >> 13) as u8).collect(); let big_parts = onset_core::chunk::file_objects(&big); let big_id = big_parts[0].id(); let mut files = Files::new(); files.insert("src/main.rs".into(), Entry { kind: EntryKind::File, id: small.id() }); files.insert("run.sh".into(), Entry { kind: EntryKind::Exec, id: small.id() }); files.insert("data/big.bin".into(), Entry { kind: EntryKind::File, id: big_id }); let (root, trees) = tree::build(&files); cache_drop(0); for t in &trees { insert(t.id(), t.clone()); } let plan = call_json(git_plan, json!({ "tree": root.hex() })); let new_trees: Vec = serde_json::from_value(plan["trees"].clone()).unwrap(); let new_files: Vec = serde_json::from_value(plan["files"].clone()).unwrap(); assert_eq!(new_trees.len(), 3); assert_eq!(new_files.len(), 2); assert_eq!(pack_begin((new_trees.len() + new_files.len() + 1) as u32, 1), 0); let mut known = HashMap::new(); let list = big_parts.iter().find(|o| o.kind == Kind::Chunks).unwrap(); let chunks: HashMap = big_parts.iter().map(|o| (o.id(), o)).collect(); let files_pass = |write: u32, pack: &mut Vec| -> Vec { let mut ids = Vec::new(); assert_eq!(blob_begin(small.data.len() as f64, write), 0); feed(&small); assert_eq!(blob_end(), 0); let v: Value = serde_json::from_slice(&result()).unwrap(); ids.push(v["git"].as_str().unwrap().to_string()); assert_eq!(blob_begin(big.len() as f64, write), 0); for (c, _) in decode_chunks(&list.data).unwrap() { feed(chunks[&c]); pack_take(); pack.extend(result()); } assert_eq!(blob_end(), 0); let v: Value = serde_json::from_slice(&result()).unwrap(); ids.push(v["git"].as_str().unwrap().to_string()); ids }; let mut pack = Vec::new(); let hashed = files_pass(0, &mut pack); known.insert(small.id().hex(), hashed[0].clone()); known.insert(big_id.hex(), hashed[1].clone()); let done = call_json( git_trees, json!({ "tree": root.hex(), "trees": new_trees, "known": known, "parents": [], "message": "First\n", "author": "Ada ", "time": 1700000000 }), ); let written = files_pass(1, &mut pack); assert_eq!(hashed, written); pack_take(); pack.extend(result()); assert_eq!(pack_end(), 0); pack.extend(result()); let dir = std::env::temp_dir().join(format!("onset-wasm-git-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let git = |args: &[&str], input: Option<&[u8]>| -> String { let mut c = std::process::Command::new("git"); c.args(args).current_dir(&dir).stdin(std::process::Stdio::piped()).stdout(std::process::Stdio::piped()); let mut child = c.spawn().unwrap(); if let Some(i) = input { child.stdin.take().unwrap().write_all(i).unwrap(); } else { drop(child.stdin.take()); } let out = child.wait_with_output().unwrap(); assert!(out.status.success(), "git {args:?} failed"); String::from_utf8(out.stdout).unwrap() }; git(&["init", "-q", "--bare", "."], None); git(&["index-pack", "--stdin", "--strict"], Some(&pack)); let commit = done["commit"].as_str().unwrap(); git(&["update-ref", "refs/heads/main", commit], None); git(&["fsck", "--strict", "--no-dangling"], None); let listing = git(&["ls-tree", "-r", "main"], None); assert!(listing.contains("100755 blob") && listing.contains("run.sh"), "{listing}"); assert_eq!(git(&["cat-file", "-p", "main:src/main.rs"], None), "fn main() {}\n"); assert_eq!(git(&["cat-file", "-s", "main:data/big.bin"], None).trim(), big.len().to_string()); let _ = std::fs::remove_dir_all(&dir); } }