use crate::{Request, Response, route};
use git_zero_core::{
Error, ObjectId, Repository, Result,
io::Write,
pack::{self, IndexEntry},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LooseObjects {
Enabled,
Disabled,
}
pub struct Publication<'a, R> {
repository: &'a R,
entries: &'a [IndexEntry],
checksum: ObjectId,
pack_size: u64,
index_size: u64,
loose: LooseObjects,
listing: [u8; 80],
listing_length: usize,
}
impl<'a, R: Repository> Publication<'a, R> {
pub fn new(
repository: &'a R,
entries: &'a mut [IndexEntry],
scratch: &mut [u8],
loose: LooseObjects,
) -> Result<Self> {
let mut pack_output = Count(0);
let mut index_output = Count(0);
let checksum = pack::write_indexed_with_scratch(
repository,
None,
entries,
scratch,
&mut pack_output,
&mut index_output,
)?;
let hex = checksum.to_hex();
let mut listing = [0; 80];
let prefix = b"P pack-";
listing[..prefix.len()].copy_from_slice(prefix);
listing[prefix.len()..prefix.len() + hex.len()].copy_from_slice(&hex);
let suffix = b".pack\n\n";
let end = prefix.len() + hex.len();
listing[end..end + suffix.len()].copy_from_slice(suffix);
Ok(Self {
repository,
entries: &entries[..repository.object_count()],
checksum,
pack_size: pack_output.0,
index_size: index_output.0,
loose,
listing,
listing_length: end + suffix.len(),
})
}
pub fn checksum(&self) -> ObjectId {
self.checksum
}
pub fn pack_size(&self) -> u64 {
self.pack_size
}
pub fn index_size(&self) -> u64 {
self.index_size
}
pub fn prepare(&self, request: Request<'_>) -> Result<Option<PublishedResponse<'_, R>>> {
let route = route::validate(request)?;
if let Some(response) = route.rejection {
return Ok(Some(PublishedResponse::new(
self.repository,
response,
Body::Empty,
0,
)));
}
let (body, content_type, length) = match route.path {
"objects/info/packs" => (
Body::Bytes {
bytes: &self.listing[..self.listing_length],
position: 0,
},
"text/plain; charset=utf-8",
self.listing_length as u64,
),
path if path.starts_with("objects/pack/") => {
let name = &path.as_bytes()[13..];
let hex = self.checksum.to_hex();
let Some(suffix) = name
.strip_prefix(b"pack-")
.and_then(|name| name.strip_prefix(hex.as_bytes()))
else {
return Ok(Some(self.not_found()));
};
match suffix {
b".pack" => (
Body::Pack(pack::Encoder::new(self.repository, None)?),
"application/x-git-packed-objects",
self.pack_size,
),
b".idx" => (
Body::Index(pack::IndexEncoder::new(self.entries, self.checksum)?),
"application/x-git-packed-objects-toc",
self.index_size,
),
_ => return Ok(Some(self.not_found())),
}
}
path if path.starts_with("objects/") && self.loose == LooseObjects::Disabled => {
return Ok(Some(self.not_found()));
}
_ => return Ok(None),
};
Ok(Some(PublishedResponse::new(
self.repository,
Response {
status: 200,
content_type,
},
body,
length,
)))
}
fn not_found(&self) -> PublishedResponse<'_, R> {
PublishedResponse::new(
self.repository,
Response {
status: 404,
content_type: "text/plain; charset=utf-8",
},
Body::Empty,
0,
)
}
pub fn serve(
&self,
request: Request<'_>,
scratch: &mut [u8],
output: &mut impl Write,
) -> Result<Option<Response>> {
let Some(mut cursor) = self.prepare(request)? else {
return Ok(None);
};
loop {
let length = cursor.read(scratch)?;
if length == 0 {
return Ok(Some(cursor.response()));
}
output.write_all(&scratch[..length])?;
}
}
}
struct Count(u64);
impl Write for Count {
fn write_all(&mut self, bytes: &[u8]) -> Result<()> {
self.0 = self
.0
.checked_add(bytes.len() as u64)
.ok_or(Error::LimitExceeded)?;
Ok(())
}
}
#[allow(clippy::large_enum_variant)]
enum Body<'a> {
Empty,
Bytes { bytes: &'a [u8], position: usize },
Pack(pack::Encoder<'a>),
Index(pack::IndexEncoder<'a>),
}
pub struct PublishedResponse<'a, R> {
repository: &'a R,
response: Response,
body: Body<'a>,
size: u64,
cancelled: bool,
}
impl<'a, R: Repository> PublishedResponse<'a, R> {
fn new(repository: &'a R, response: Response, body: Body<'a>, size: u64) -> Self {
Self {
repository,
response,
body,
size,
cancelled: false,
}
}
pub fn response(&self) -> Response {
self.response
}
pub fn size(&self) -> u64 {
self.size
}
pub fn cancel(&mut self) {
self.cancelled = true;
}
pub fn read(&mut self, output: &mut [u8]) -> Result<usize> {
if self.cancelled {
return Err(Error::InvalidRequest);
}
if output.is_empty() && !matches!(self.body, Body::Empty) {
return Err(Error::BufferTooSmall);
}
let result = match &mut self.body {
Body::Empty => Ok(0),
Body::Bytes { bytes, position } => {
let length = output.len().min(bytes.len() - *position);
output[..length].copy_from_slice(&bytes[*position..*position + length]);
*position += length;
Ok(length)
}
Body::Pack(encoder) => encoder.read(self.repository, output),
Body::Index(encoder) => encoder.read(output),
};
if result.is_err() {
self.cancel();
}
result
}
}