1use core::ffi::CStr;
3#[cfg(feature = "server")]
4use git_zero::http::{self, Request, Response};
5
6use git_zero::{
7 Error, Head, ObjectFormat, ObjectId, ObjectInfo, ObjectKind, Repository, Result, io::Write,
8 pack, repository::validate_refs,
9};
10use rustix::{
11 fd::{AsFd, OwnedFd},
12 fs::{self, FileType, Mode, OFlags},
13 io::{self, Errno},
14};
15
16#[cfg(feature = "server")]
17mod publication;
18#[cfg(feature = "server")]
19pub use publication::{FilePublication, FilePublishedResponse};
20
21pub struct FileObject {
30 file: Region,
31 id: ObjectId,
32 kind: ObjectKind,
33}
34
35impl FileObject {
36 pub fn open_at(
40 directory: impl AsFd,
41 path: &CStr,
42 kind: ObjectKind,
43 id: ObjectId,
44 ) -> Result<Self> {
45 let fd = fs::openat(
46 directory,
47 path,
48 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
49 Mode::empty(),
50 )
51 .map_err(|error| {
52 if error == Errno::NOENT {
53 Error::ObjectNotFound
54 } else {
55 Error::Backend
56 }
57 })?;
58 Self::from_fd(fd, kind, id)
59 }
60
61 pub fn from_fd(fd: OwnedFd, kind: ObjectKind, id: ObjectId) -> Result<Self> {
63 let size = file_size(&fd)?;
64 Self::from_range(fd, 0, ObjectInfo { kind, size }, id)
65 }
66
67 pub fn from_range(fd: OwnedFd, offset: u64, info: ObjectInfo, id: ObjectId) -> Result<Self> {
70 let end = offset.checked_add(info.size).ok_or(Error::InvalidObject)?;
71 if end > i64::MAX as u64 || end > file_size(&fd)? {
72 return Err(Error::InvalidObject);
73 }
74 Ok(Self {
75 file: Region {
76 fd,
77 offset,
78 size: info.size,
79 },
80 id,
81 kind: info.kind,
82 })
83 }
84
85 pub fn id(&self) -> ObjectId {
86 self.id
87 }
88 pub fn info(&self) -> ObjectInfo {
89 ObjectInfo {
90 kind: self.kind,
91 size: self.file.size,
92 }
93 }
94}
95
96pub struct FileRepository<'a> {
104 objects: &'a [FileObject],
105 head: ObjectId,
106 head_ref: &'a str,
107 detached: bool,
108 refs: &'a [&'a str],
109 prepacked: Option<Region>,
110}
111
112impl<'a> FileRepository<'a> {
113 pub fn new(head: ObjectId, objects: &'a [FileObject]) -> Self {
114 Self {
115 objects,
116 head,
117 head_ref: "refs/heads/main",
118 detached: false,
119 refs: &["refs/heads/main"],
120 prepacked: None,
121 }
122 }
123
124 pub fn with_refs(mut self, head_ref: &'a str, refs: &'a [&'a str]) -> Result<Self> {
126 self.head_ref = head_ref;
127 self.detached = false;
128 self.refs = refs;
129 validate_refs(&self)?;
130 Ok(self)
131 }
132
133 pub fn with_detached_head(mut self, refs: &'a [&'a str]) -> Result<Self> {
136 self.detached = true;
137 self.refs = refs;
138 validate_refs(&self)?;
139 Ok(self)
140 }
141
142 pub fn with_prepacked(
152 mut self,
153 fd: OwnedFd,
154 object_scratch: &mut [u8],
155 compare_scratch: &mut [u8],
156 ) -> Result<Self> {
157 let size = file_size(&fd).map_err(pack_error)?;
158 let file = Region {
159 fd,
160 offset: 0,
161 size,
162 };
163 let mut compare = Compare {
164 file: &file,
165 position: 0,
166 scratch: compare_scratch,
167 };
168 pack::write_with_scratch(&self, object_scratch, &mut compare)?;
169 if compare.position != file.size {
170 return Err(Error::CorruptPack);
171 }
172 self.prepacked = Some(file);
173 Ok(self)
174 }
175
176 pub fn has_prepacked(&self) -> bool {
177 self.prepacked.is_some()
178 }
179
180 pub fn write_pack_with_scratch(
183 &self,
184 scratch: &mut [u8],
185 output: &mut impl Write,
186 ) -> Result<()> {
187 if let Some(file) = &self.prepacked {
188 file.copy_to(scratch, output)
189 } else {
190 pack::write_with_scratch(self, scratch, output).map(|_| ())
191 }
192 }
193
194 #[cfg(feature = "server")]
198 pub fn serve_with_scratch(
199 &self,
200 request: Request<'_>,
201 scratch: &mut [u8],
202 output: &mut impl Write,
203 ) -> Result<Response> {
204 http::serve_with_pack(self, request, scratch, output, |scratch, output| {
205 self.write_pack_with_scratch(scratch, output)
206 })
207 }
208}
209
210impl Repository for FileRepository<'_> {
211 fn object_format(&self) -> ObjectFormat {
212 self.head.format()
213 }
214 fn head(&self) -> ObjectId {
215 self.head
216 }
217 fn head_state(&self) -> Head<'_> {
218 if self.detached {
219 Head::Detached(self.head)
220 } else {
221 Head::Symbolic {
222 target: self.head_ref,
223 id: self.head,
224 }
225 }
226 }
227 fn object_count(&self) -> usize {
228 self.objects.len()
229 }
230 fn object_info(&self, index: usize) -> Result<ObjectInfo> {
231 self.objects
232 .get(index)
233 .map(FileObject::info)
234 .ok_or(Error::ObjectNotFound)
235 }
236 fn object_id(&self, index: usize) -> Option<ObjectId> {
237 self.objects.get(index).map(FileObject::id)
238 }
239 fn read_object(&self, index: usize, offset: u64, storage: &mut [u8]) -> Result<usize> {
240 self.objects
241 .get(index)
242 .ok_or(Error::ObjectNotFound)?
243 .file
244 .read(offset, storage)
245 }
246 fn head_ref(&self) -> &str {
247 self.head_ref
248 }
249 fn reference_count(&self) -> usize {
250 self.refs.len()
251 }
252 fn reference_name(&self, index: usize) -> Option<&str> {
253 self.refs.get(index).copied()
254 }
255}
256
257pub(crate) fn file_size(fd: &impl AsFd) -> Result<u64> {
258 let stat = fs::fstat(fd).map_err(|_| Error::Backend)?;
259 if !FileType::from_raw_mode(stat.st_mode).is_file() {
260 return Err(Error::InvalidObject);
261 }
262 u64::try_from(stat.st_size).map_err(|_| Error::InvalidObject)
263}
264
265fn pack_error(error: Error) -> Error {
266 match error {
267 Error::InvalidObject => Error::CorruptPack,
268 other => other,
269 }
270}
271
272struct Region {
273 fd: OwnedFd,
274 offset: u64,
275 size: u64,
276}
277
278impl Region {
279 fn copy_to(&self, scratch: &mut [u8], output: &mut impl Write) -> Result<()> {
280 let mut offset = 0;
281 while offset < self.size {
282 if scratch.is_empty() {
283 return Err(Error::BufferTooSmall);
284 }
285 let length = self.read(offset, scratch)?;
286 output.write_all(&scratch[..length])?;
287 offset += length as u64;
288 }
289 Ok(())
290 }
291
292 fn read(&self, offset: u64, storage: &mut [u8]) -> Result<usize> {
293 if offset >= self.size || storage.is_empty() {
294 return Ok(0);
295 }
296 let capacity = (self.size - offset).min(storage.len() as u64) as usize;
297 loop {
298 match io::pread(&self.fd, &mut storage[..capacity], self.offset + offset) {
299 Ok(0) => return Err(Error::InvalidObject), Ok(length) => return Ok(length),
301 Err(Errno::INTR) => continue,
302 Err(_) => return Err(Error::Backend),
303 }
304 }
305 }
306}
307
308struct Compare<'a> {
309 file: &'a Region,
310 position: u64,
311 scratch: &'a mut [u8],
312}
313impl Write for Compare<'_> {
314 fn write_all(&mut self, mut bytes: &[u8]) -> Result<()> {
315 while !bytes.is_empty() {
316 if self.scratch.is_empty() {
317 return Err(Error::BufferTooSmall);
318 }
319 let capacity = self.scratch.len().min(bytes.len());
320 let length = self
321 .file
322 .read(self.position, &mut self.scratch[..capacity])
323 .map_err(pack_error)?;
324 if length == 0 || self.scratch[..length] != bytes[..length] {
325 return Err(Error::CorruptPack);
326 }
327 self.position += length as u64;
328 bytes = &bytes[length..];
329 }
330 Ok(())
331 }
332}