1use crate::{Request, Response, route};
4use git_zero_core::{
5 Error, ObjectId, Repository, Result,
6 io::Write,
7 pack::{self, IndexEntry},
8};
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum LooseObjects {
13 Enabled,
14 Disabled,
15}
16
17pub struct Publication<'a, R> {
32 repository: &'a R,
33 entries: &'a [IndexEntry],
34 checksum: ObjectId,
35 pack_size: u64,
36 index_size: u64,
37 loose: LooseObjects,
38 listing: [u8; 80],
39 listing_length: usize,
40}
41
42impl<'a, R: Repository> Publication<'a, R> {
43 pub fn new(
44 repository: &'a R,
45 entries: &'a mut [IndexEntry],
46 scratch: &mut [u8],
47 loose: LooseObjects,
48 ) -> Result<Self> {
49 let mut pack_output = Count(0);
50 let mut index_output = Count(0);
51 let checksum = pack::write_indexed_with_scratch(
52 repository,
53 None,
54 entries,
55 scratch,
56 &mut pack_output,
57 &mut index_output,
58 )?;
59 let hex = checksum.to_hex();
60 let mut listing = [0; 80];
61 let prefix = b"P pack-";
62 listing[..prefix.len()].copy_from_slice(prefix);
63 listing[prefix.len()..prefix.len() + hex.len()].copy_from_slice(&hex);
64 let suffix = b".pack\n\n";
65 let end = prefix.len() + hex.len();
66 listing[end..end + suffix.len()].copy_from_slice(suffix);
67 Ok(Self {
68 repository,
69 entries: &entries[..repository.object_count()],
70 checksum,
71 pack_size: pack_output.0,
72 index_size: index_output.0,
73 loose,
74 listing,
75 listing_length: end + suffix.len(),
76 })
77 }
78
79 pub fn checksum(&self) -> ObjectId {
80 self.checksum
81 }
82 pub fn pack_size(&self) -> u64 {
83 self.pack_size
84 }
85 pub fn index_size(&self) -> u64 {
86 self.index_size
87 }
88
89 pub fn prepare(&self, request: Request<'_>) -> Result<Option<PublishedResponse<'_, R>>> {
96 let route = route::validate(request)?;
97 if let Some(response) = route.rejection {
98 return Ok(Some(PublishedResponse::new(
99 self.repository,
100 response,
101 Body::Empty,
102 0,
103 )));
104 }
105 let (body, content_type, length) = match route.path {
106 "objects/info/packs" => (
107 Body::Bytes {
108 bytes: &self.listing[..self.listing_length],
109 position: 0,
110 },
111 "text/plain; charset=utf-8",
112 self.listing_length as u64,
113 ),
114 path if path.starts_with("objects/pack/") => {
115 let name = &path.as_bytes()[13..];
116 let hex = self.checksum.to_hex();
117 let Some(suffix) = name
118 .strip_prefix(b"pack-")
119 .and_then(|name| name.strip_prefix(hex.as_bytes()))
120 else {
121 return Ok(Some(self.not_found()));
122 };
123 match suffix {
124 b".pack" => (
125 Body::Pack(pack::Encoder::new(self.repository, None)?),
126 "application/x-git-packed-objects",
127 self.pack_size,
128 ),
129 b".idx" => (
130 Body::Index(pack::IndexEncoder::new(self.entries, self.checksum)?),
131 "application/x-git-packed-objects-toc",
132 self.index_size,
133 ),
134 _ => return Ok(Some(self.not_found())),
135 }
136 }
137 path if path.starts_with("objects/") && self.loose == LooseObjects::Disabled => {
138 return Ok(Some(self.not_found()));
139 }
140 _ => return Ok(None),
141 };
142 Ok(Some(PublishedResponse::new(
143 self.repository,
144 Response {
145 status: 200,
146 content_type,
147 },
148 body,
149 length,
150 )))
151 }
152
153 fn not_found(&self) -> PublishedResponse<'_, R> {
154 PublishedResponse::new(
155 self.repository,
156 Response {
157 status: 404,
158 content_type: "text/plain; charset=utf-8",
159 },
160 Body::Empty,
161 0,
162 )
163 }
164
165 pub fn serve(
168 &self,
169 request: Request<'_>,
170 scratch: &mut [u8],
171 output: &mut impl Write,
172 ) -> Result<Option<Response>> {
173 let Some(mut cursor) = self.prepare(request)? else {
174 return Ok(None);
175 };
176 loop {
177 let length = cursor.read(scratch)?;
178 if length == 0 {
179 return Ok(Some(cursor.response()));
180 }
181 output.write_all(&scratch[..length])?;
182 }
183 }
184}
185
186struct Count(u64);
187impl Write for Count {
188 fn write_all(&mut self, bytes: &[u8]) -> Result<()> {
189 self.0 = self
190 .0
191 .checked_add(bytes.len() as u64)
192 .ok_or(Error::LimitExceeded)?;
193 Ok(())
194 }
195}
196
197#[allow(clippy::large_enum_variant)]
198enum Body<'a> {
199 Empty,
200 Bytes { bytes: &'a [u8], position: usize },
201 Pack(pack::Encoder<'a>),
202 Index(pack::IndexEncoder<'a>),
203}
204
205pub struct PublishedResponse<'a, R> {
208 repository: &'a R,
209 response: Response,
210 body: Body<'a>,
211 size: u64,
212 cancelled: bool,
213}
214impl<'a, R: Repository> PublishedResponse<'a, R> {
215 fn new(repository: &'a R, response: Response, body: Body<'a>, size: u64) -> Self {
216 Self {
217 repository,
218 response,
219 body,
220 size,
221 cancelled: false,
222 }
223 }
224 pub fn response(&self) -> Response {
225 self.response
226 }
227 pub fn size(&self) -> u64 {
228 self.size
229 }
230 pub fn cancel(&mut self) {
231 self.cancelled = true;
232 }
233
234 pub fn read(&mut self, output: &mut [u8]) -> Result<usize> {
237 if self.cancelled {
238 return Err(Error::InvalidRequest);
239 }
240 if output.is_empty() && !matches!(self.body, Body::Empty) {
242 return Err(Error::BufferTooSmall);
243 }
244 let result = match &mut self.body {
245 Body::Empty => Ok(0),
246 Body::Bytes { bytes, position } => {
247 let length = output.len().min(bytes.len() - *position);
248 output[..length].copy_from_slice(&bytes[*position..*position + length]);
249 *position += length;
250 Ok(length)
251 }
252 Body::Pack(encoder) => encoder.read(self.repository, output),
253 Body::Index(encoder) => encoder.read(output),
254 };
255 if result.is_err() {
256 self.cancel();
257 }
258 result
259 }
260}