use alloc::sync::Arc;
use core::fmt::Write as _;
use libvctrl::{Decoder, EntryKind, Hash, ObjectStore, VctrlError};
use std::io::{BufRead, Write as IoWrite};
#[derive(Debug, Clone, Copy)]
pub enum CatFileMode {
PrettyPrint,
ObjectType,
ObjectSize,
Exists,
Raw(ObjectType),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjectType {
Blob,
Tree,
Commit,
Tag,
}
pub fn cat_file<D: Decoder>(
store: &dyn ObjectStore,
decoder: &D,
object_name: &str,
mode: CatFileMode,
writer: &mut impl IoWrite,
) -> Result<(), VctrlError> {
let hash = parse_hash(object_name)?;
let mut encoded = Vec::new();
let _ = store
.get(&hash)?
.read_to_end(&mut encoded)
.map_err(|e| VctrlError::IoError(Arc::new(e)))?;
match mode {
CatFileMode::Exists => Ok(()),
CatFileMode::ObjectType => {
let obj_type = decode_type(decoder, &encoded)?;
let type_str = obj_type_to_str(obj_type);
writeln!(writer, "{type_str}").map_err(|e| VctrlError::IoError(Arc::new(e)))?;
Ok(())
}
CatFileMode::ObjectSize => {
let _obj_type = decode_type(decoder, &encoded)?;
let size = encoded.len();
writeln!(writer, "{size}").map_err(|e| VctrlError::IoError(Arc::new(e)))?;
Ok(())
}
CatFileMode::PrettyPrint => {
let content = pretty_print(decoder, &encoded)?;
writer
.write_all(content.as_bytes())
.map_err(|e| VctrlError::IoError(Arc::new(e)))?;
Ok(())
}
CatFileMode::Raw(expected_type) => {
let actual_type = decode_type(decoder, &encoded)?;
if actual_type != expected_type {
let actual_type_str = obj_type_to_str(actual_type);
let expected_type_str = obj_type_to_str(expected_type);
return Err(VctrlError::Other(format!(
"object {object_name} is a {actual_type_str}, not a {expected_type_str}"
)));
}
writer
.write_all(&encoded)
.map_err(|e| VctrlError::IoError(Arc::new(e)))?;
Ok(())
}
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Default)]
pub struct BatchOptions {
pub format: Option<String>,
pub nul_terminated: bool,
pub follow_symlinks: bool,
pub buffer: bool,
pub print_contents: bool,
}
pub fn cat_file_batch<D: Decoder>(
store: &dyn ObjectStore,
decoder: &D,
input: &mut impl BufRead,
output: &mut impl IoWrite,
options: &BatchOptions,
) -> Result<(), VctrlError> {
let delimiter: u8 = if options.nul_terminated { 0 } else { b'\n' };
let mut line = String::new();
let mut out_buf: Vec<u8> = Vec::new();
loop {
line.clear();
if input
.read_line(&mut line)
.map_err(|e| VctrlError::IoError(Arc::new(e)))?
== 0
{
break;
}
let trimmed = if options.nul_terminated {
line.trim_end_matches('\0')
} else {
line.trim_end()
};
let object_name = trimmed;
if let Ok((info, content)) = handle_one_object(store, decoder, object_name, options) {
out_buf.extend_from_slice(info.as_bytes());
out_buf.push(delimiter);
if let Some(c) = content {
out_buf.extend_from_slice(&c);
if options.nul_terminated {
out_buf.push(0);
} else {
out_buf.push(b'\n');
}
}
if !options.buffer {
output
.write_all(&out_buf)
.map_err(|e| VctrlError::IoError(Arc::new(e)))?;
out_buf.clear();
}
} else {
let error_line = format!("{object_name} missing");
out_buf.extend_from_slice(error_line.as_bytes());
out_buf.push(delimiter);
if !options.buffer {
output
.write_all(&out_buf)
.map_err(|e| VctrlError::IoError(Arc::new(e)))?;
out_buf.clear();
}
}
}
if !out_buf.is_empty() {
output
.write_all(&out_buf)
.map_err(|e| VctrlError::IoError(Arc::new(e)))?;
}
Ok(())
}
fn handle_one_object<D: Decoder>(
store: &dyn ObjectStore,
decoder: &D,
object_name: &str,
options: &BatchOptions,
) -> Result<(String, Option<Vec<u8>>), VctrlError> {
let hash = parse_hash(object_name)?;
let mut encoded = Vec::new();
let _ = store
.get(&hash)?
.read_to_end(&mut encoded)
.map_err(|e| VctrlError::IoError(Arc::new(e)))?;
let obj_type = decode_type(decoder, &encoded)?;
let obj_size = encoded.len() as u64;
let info = options.format.as_ref().map_or_else(
|| {
let type_str = obj_type_to_str(obj_type);
format!("{hash} {type_str} {obj_size}")
},
|fmt| format_batch_info(fmt, &hash, obj_type, obj_size, None),
);
let content = if options.print_contents {
Some(pretty_print(decoder, &encoded)?.into_bytes())
} else {
None
};
Ok((info, content))
}
fn parse_hash(s: &str) -> Result<Hash, VctrlError> {
if s.len() != 128 {
let actual_len = s.len();
return Err(VctrlError::Other(format!(
"invalid hash length: {actual_len} (expected 128)"
)));
}
let mut bytes = [0u8; 64];
for (i, byte) in bytes.iter_mut().enumerate() {
let start = i
.checked_mul(2)
.ok_or_else(|| VctrlError::Other("hash index overflow".into()))?;
let end = start
.checked_add(2)
.ok_or_else(|| VctrlError::Other("hash index overflow".into()))?;
let hex_byte = s
.get(start..end)
.ok_or_else(|| VctrlError::Other("hash slice out of bounds".into()))?;
*byte = u8::from_str_radix(hex_byte, 16)
.map_err(|e| VctrlError::Other(format!("invalid hex character in hash: {e}")))?;
}
Hash::from_bytes(&bytes)
}
fn decode_type<D: Decoder>(decoder: &D, encoded: &[u8]) -> Result<ObjectType, VctrlError> {
if decoder.decode_blob(encoded).is_ok() {
return Ok(ObjectType::Blob);
}
if decoder.decode_tree(encoded).is_ok() {
return Ok(ObjectType::Tree);
}
if decoder.decode_commit(encoded).is_ok() {
return Ok(ObjectType::Commit);
}
if decoder.decode_tag(encoded).is_ok() {
return Ok(ObjectType::Tag);
}
Err(VctrlError::CorruptedData("unknown object type".into()))
}
fn pretty_print<D: Decoder>(decoder: &D, encoded: &[u8]) -> Result<String, VctrlError> {
if let Ok(blob) = decoder.decode_blob(encoded) {
return Ok(String::from_utf8_lossy(blob.data()).to_string());
}
if let Ok(tree) = decoder.decode_tree(encoded) {
let mut out = String::new();
for entry in tree.entries() {
let _ = writeln!(
&mut out,
"{:06o} {:?} {} {}",
entry_mode(entry.kind()),
entry.kind(),
entry.hash(),
entry.name()
);
}
return Ok(out);
}
if let Ok(commit) = decoder.decode_commit(encoded) {
let mut out = String::new();
let _ = writeln!(&mut out, "tree {}", commit.tree());
for parent in commit.parents() {
let _ = writeln!(&mut out, "parent {parent}");
}
let _ = writeln!(
&mut out,
"author {} <{}>",
commit.author().name(),
commit.author().email()
);
let _ = writeln!(
&mut out,
"committer {} <{}>",
commit.committer().name(),
commit.committer().email()
);
let _ = writeln!(&mut out);
let _ = writeln!(&mut out, "{}", commit.message());
return Ok(out);
}
if let Ok(tag) = decoder.decode_tag(encoded) {
let mut out = String::new();
let _ = writeln!(&mut out, "object {}", tag.target());
let _ = writeln!(&mut out, "type commit");
let _ = writeln!(&mut out, "tag {}", tag.name());
if let Some(tagger) = tag.tagger() {
let _ = writeln!(&mut out, "tagger {} <{}>", tagger.name(), tagger.email());
}
let _ = writeln!(&mut out);
let _ = writeln!(&mut out, "{}", tag.message());
return Ok(out);
}
Err(VctrlError::CorruptedData("unknown object type".into()))
}
const fn obj_type_to_str(t: ObjectType) -> &'static str {
match t {
ObjectType::Blob => "blob",
ObjectType::Tree => "tree",
ObjectType::Commit => "commit",
ObjectType::Tag => "tag",
}
}
const fn entry_mode(kind: EntryKind) -> u32 {
match kind {
EntryKind::Blob => 0o100_644,
EntryKind::Executable => 0o100_755,
EntryKind::Symlink => 0o120_000,
EntryKind::Tree => 0o040_000,
EntryKind::Submodule => 0o160_000,
_ => 0,
}
}
fn format_batch_info(
format: &str,
hash: &Hash,
obj_type: ObjectType,
size: u64,
_mode: Option<u32>,
) -> String {
let mut output = format.to_owned();
output = output.replace("%(objectname)", &hash.to_string());
output = output.replace("%(objecttype)", obj_type_to_str(obj_type));
output = output.replace("%(objectsize)", &size.to_string());
output
}