use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug)]
pub enum RevisionError {
NotAGitRepository(String),
RevisionNotFound(String),
}
impl RevisionError {
pub fn message(&self) -> &str {
match self {
RevisionError::NotAGitRepository(m) => m,
RevisionError::RevisionNotFound(m) => m,
}
}
}
fn run_git(args: &[&str], cwd: &Path) -> Result<Output, RevisionError> {
Command::new("git")
.args(args)
.current_dir(cwd)
.stdin(Stdio::null())
.output()
.map_err(|_| {
RevisionError::NotAGitRepository("git executable not found".to_string())
})
}
pub fn repository_root(directory: &str) -> Result<String, RevisionError> {
let out = run_git(&["rev-parse", "--show-toplevel"], Path::new(directory))?;
if !out.status.success() {
return Err(RevisionError::NotAGitRepository(format!(
"not a git repository: {directory}"
)));
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
pub struct MaterializedRevision {
root: PathBuf,
pub corpus: PathBuf,
}
impl Drop for MaterializedRevision {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
fn make_temp_dir() -> io::Result<PathBuf> {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let base = std::env::temp_dir();
let pid = std::process::id();
loop {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let candidate = base.join(format!("decided-watchkeeper-{pid}-{n}"));
match std::fs::create_dir(&candidate) {
Ok(()) => return Ok(candidate),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
}
}
}
pub fn materialize_revision(
repo_root: &str,
rev: &str,
subpath: &str,
) -> Result<MaterializedRevision, RevisionError> {
let root = Path::new(repo_root);
let verify = run_git(
&["rev-parse", "--verify", "--quiet", &format!("{rev}^{{commit}}")],
root,
)?;
if !verify.status.success() {
return Err(RevisionError::RevisionNotFound(format!(
"unknown revision: {rev}"
)));
}
let pathspec = if subpath.is_empty() || subpath == "." {
"."
} else {
subpath
};
let archive = run_git(&["archive", "--format=tar", rev, "--", pathspec], root)?;
let tmp = make_temp_dir().map_err(|e| {
RevisionError::NotAGitRepository(format!("not a git repository: {e}"))
})?;
let guard_root = tmp.clone();
if archive.status.success() {
extract_tar(&archive.stdout, &tmp);
}
let corpus = if pathspec == "." {
tmp
} else {
guard_root.join(subpath)
};
let _ = std::fs::create_dir_all(&corpus);
Ok(MaterializedRevision {
root: guard_root,
corpus,
})
}
fn octal_field(bytes: &[u8]) -> u64 {
let mut out: u64 = 0;
for &b in bytes {
if matches!(b, b'0'..=b'7') {
out = out * 8 + u64::from(b - b'0');
}
}
out
}
fn cstr_field(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).into_owned()
}
fn pax_path(data: &[u8]) -> Option<String> {
let mut i = 0;
while i < data.len() {
let space = data[i..].iter().position(|&b| b == b' ')?;
let len: usize = std::str::from_utf8(&data[i..i + space])
.ok()?
.parse()
.ok()?;
if len == 0 || i + len > data.len() {
return None;
}
let record = &data[i + space + 1..i + len];
if let Some(eq) = record.iter().position(|&b| b == b'=') {
let key = &record[..eq];
if key == b"path" {
let mut value = &record[eq + 1..];
if value.last() == Some(&b'\n') {
value = &value[..value.len() - 1];
}
return Some(String::from_utf8_lossy(value).into_owned());
}
}
i += len;
}
None
}
fn safe_relative(name: &str) -> bool {
!name.starts_with('/')
&& !name
.split('/')
.any(|c| c == ".." || c.chars().any(|ch| ch == '\\'))
}
fn extract_tar(data: &[u8], target: &Path) {
let mut offset = 0usize;
let mut pending_path: Option<String> = None;
while offset + 512 <= data.len() {
let header = &data[offset..offset + 512];
offset += 512;
if header.iter().all(|&b| b == 0) {
break; }
let size = octal_field(&header[124..136]) as usize;
let padded = size.div_ceil(512) * 512;
if offset + size > data.len() {
break; }
let body = &data[offset..offset + size];
let typeflag = header[156];
match typeflag {
b'g' => {} b'x' => {
if let Some(p) = pax_path(body) {
pending_path = Some(p);
}
}
b'L' => {
pending_path = Some(cstr_field(body));
}
_ => {
let mut name = match pending_path.take() {
Some(p) => p,
None => {
let base = cstr_field(&header[0..100]);
let prefix = cstr_field(&header[345..500]);
if prefix.is_empty() {
base
} else {
format!("{prefix}/{base}")
}
}
};
let is_dir_name = name.ends_with('/');
while name.ends_with('/') {
name.pop();
}
if !name.is_empty() && safe_relative(&name) {
let dest = target.join(&name);
match typeflag {
b'5' => {
let _ = std::fs::create_dir_all(&dest);
}
b'0' | 0 | b'7' if !is_dir_name => {
if let Some(parent) = dest.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(&dest, body);
}
b'2' => {
if let Some(parent) = dest.parent() {
let _ = std::fs::create_dir_all(parent);
}
#[cfg(unix)]
{
let link = cstr_field(&header[157..257]);
let _ = std::os::unix::fs::symlink(&link, &dest);
}
}
_ => {} }
}
}
}
offset += padded;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn octal_parses_padded_fields() {
assert_eq!(octal_field(b"0000644\0"), 0o644);
assert_eq!(octal_field(b"00000000173 "), 0o173);
}
#[test]
fn pax_path_record() {
let payload = b"33 path=decisions/some-long-name\n";
assert_eq!(pax_path(payload).as_deref(), Some("decisions/some-long-name"));
}
#[test]
fn rejects_escaping_names() {
assert!(!safe_relative("/abs"));
assert!(!safe_relative("a/../b"));
assert!(safe_relative("decisions/d1.md"));
}
}