use core::ffi::CStr;
#[cfg(feature = "server")]
use git_zero::http::{self, Request, Response};
use git_zero::{
Error, Head, ObjectFormat, ObjectId, ObjectInfo, ObjectKind, Repository, Result, io::Write,
pack, repository::validate_refs,
};
use rustix::{
fd::{AsFd, OwnedFd},
fs::{self, FileType, Mode, OFlags},
io::{self, Errno},
};
#[cfg(feature = "server")]
mod publication;
#[cfg(feature = "server")]
pub use publication::{FilePublication, FilePublishedResponse};
pub struct FileObject {
file: Region,
id: ObjectId,
kind: ObjectKind,
}
impl FileObject {
pub fn open_at(
directory: impl AsFd,
path: &CStr,
kind: ObjectKind,
id: ObjectId,
) -> Result<Self> {
let fd = fs::openat(
directory,
path,
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
Mode::empty(),
)
.map_err(|error| {
if error == Errno::NOENT {
Error::ObjectNotFound
} else {
Error::Backend
}
})?;
Self::from_fd(fd, kind, id)
}
pub fn from_fd(fd: OwnedFd, kind: ObjectKind, id: ObjectId) -> Result<Self> {
let size = file_size(&fd)?;
Self::from_range(fd, 0, ObjectInfo { kind, size }, id)
}
pub fn from_range(fd: OwnedFd, offset: u64, info: ObjectInfo, id: ObjectId) -> Result<Self> {
let end = offset.checked_add(info.size).ok_or(Error::InvalidObject)?;
if end > i64::MAX as u64 || end > file_size(&fd)? {
return Err(Error::InvalidObject);
}
Ok(Self {
file: Region {
fd,
offset,
size: info.size,
},
id,
kind: info.kind,
})
}
pub fn id(&self) -> ObjectId {
self.id
}
pub fn info(&self) -> ObjectInfo {
ObjectInfo {
kind: self.kind,
size: self.file.size,
}
}
}
pub struct FileRepository<'a> {
objects: &'a [FileObject],
head: ObjectId,
head_ref: &'a str,
detached: bool,
refs: &'a [&'a str],
prepacked: Option<Region>,
}
impl<'a> FileRepository<'a> {
pub fn new(head: ObjectId, objects: &'a [FileObject]) -> Self {
Self {
objects,
head,
head_ref: "refs/heads/main",
detached: false,
refs: &["refs/heads/main"],
prepacked: None,
}
}
pub fn with_refs(mut self, head_ref: &'a str, refs: &'a [&'a str]) -> Result<Self> {
self.head_ref = head_ref;
self.detached = false;
self.refs = refs;
validate_refs(&self)?;
Ok(self)
}
pub fn with_detached_head(mut self, refs: &'a [&'a str]) -> Result<Self> {
self.detached = true;
self.refs = refs;
validate_refs(&self)?;
Ok(self)
}
pub fn with_prepacked(
mut self,
fd: OwnedFd,
object_scratch: &mut [u8],
compare_scratch: &mut [u8],
) -> Result<Self> {
let size = file_size(&fd).map_err(pack_error)?;
let file = Region {
fd,
offset: 0,
size,
};
let mut compare = Compare {
file: &file,
position: 0,
scratch: compare_scratch,
};
pack::write_with_scratch(&self, object_scratch, &mut compare)?;
if compare.position != file.size {
return Err(Error::CorruptPack);
}
self.prepacked = Some(file);
Ok(self)
}
pub fn has_prepacked(&self) -> bool {
self.prepacked.is_some()
}
pub fn write_pack_with_scratch(
&self,
scratch: &mut [u8],
output: &mut impl Write,
) -> Result<()> {
if let Some(file) = &self.prepacked {
file.copy_to(scratch, output)
} else {
pack::write_with_scratch(self, scratch, output).map(|_| ())
}
}
#[cfg(feature = "server")]
pub fn serve_with_scratch(
&self,
request: Request<'_>,
scratch: &mut [u8],
output: &mut impl Write,
) -> Result<Response> {
http::serve_with_pack(self, request, scratch, output, |scratch, output| {
self.write_pack_with_scratch(scratch, output)
})
}
}
impl Repository for FileRepository<'_> {
fn object_format(&self) -> ObjectFormat {
self.head.format()
}
fn head(&self) -> ObjectId {
self.head
}
fn head_state(&self) -> Head<'_> {
if self.detached {
Head::Detached(self.head)
} else {
Head::Symbolic {
target: self.head_ref,
id: self.head,
}
}
}
fn object_count(&self) -> usize {
self.objects.len()
}
fn object_info(&self, index: usize) -> Result<ObjectInfo> {
self.objects
.get(index)
.map(FileObject::info)
.ok_or(Error::ObjectNotFound)
}
fn object_id(&self, index: usize) -> Option<ObjectId> {
self.objects.get(index).map(FileObject::id)
}
fn read_object(&self, index: usize, offset: u64, storage: &mut [u8]) -> Result<usize> {
self.objects
.get(index)
.ok_or(Error::ObjectNotFound)?
.file
.read(offset, storage)
}
fn head_ref(&self) -> &str {
self.head_ref
}
fn reference_count(&self) -> usize {
self.refs.len()
}
fn reference_name(&self, index: usize) -> Option<&str> {
self.refs.get(index).copied()
}
}
pub(crate) fn file_size(fd: &impl AsFd) -> Result<u64> {
let stat = fs::fstat(fd).map_err(|_| Error::Backend)?;
if !FileType::from_raw_mode(stat.st_mode).is_file() {
return Err(Error::InvalidObject);
}
u64::try_from(stat.st_size).map_err(|_| Error::InvalidObject)
}
fn pack_error(error: Error) -> Error {
match error {
Error::InvalidObject => Error::CorruptPack,
other => other,
}
}
struct Region {
fd: OwnedFd,
offset: u64,
size: u64,
}
impl Region {
fn copy_to(&self, scratch: &mut [u8], output: &mut impl Write) -> Result<()> {
let mut offset = 0;
while offset < self.size {
if scratch.is_empty() {
return Err(Error::BufferTooSmall);
}
let length = self.read(offset, scratch)?;
output.write_all(&scratch[..length])?;
offset += length as u64;
}
Ok(())
}
fn read(&self, offset: u64, storage: &mut [u8]) -> Result<usize> {
if offset >= self.size || storage.is_empty() {
return Ok(0);
}
let capacity = (self.size - offset).min(storage.len() as u64) as usize;
loop {
match io::pread(&self.fd, &mut storage[..capacity], self.offset + offset) {
Ok(0) => return Err(Error::InvalidObject), Ok(length) => return Ok(length),
Err(Errno::INTR) => continue,
Err(_) => return Err(Error::Backend),
}
}
}
}
struct Compare<'a> {
file: &'a Region,
position: u64,
scratch: &'a mut [u8],
}
impl Write for Compare<'_> {
fn write_all(&mut self, mut bytes: &[u8]) -> Result<()> {
while !bytes.is_empty() {
if self.scratch.is_empty() {
return Err(Error::BufferTooSmall);
}
let capacity = self.scratch.len().min(bytes.len());
let length = self
.file
.read(self.position, &mut self.scratch[..capacity])
.map_err(pack_error)?;
if length == 0 || self.scratch[..length] != bytes[..length] {
return Err(Error::CorruptPack);
}
self.position += length as u64;
bytes = &bytes[length..];
}
Ok(())
}
}