1use super::{Compare, Region, file_size, pack_error};
3use git_zero::{
4 Error, Repository, Result,
5 http::{
6 Request, Response,
7 dumb::{LooseObjects, Publication, PublishedResponse},
8 },
9 io::{SliceWriter, Write},
10 pack::IndexEntry,
11};
12use rustix::fd::OwnedFd;
13
14pub struct FilePublication<'a, R> {
30 publication: Publication<'a, R>,
31 pack: Region,
32 index: Region,
33}
34
35impl<'a, R: Repository> FilePublication<'a, R> {
36 pub fn from_fds(
37 repository: &'a R,
38 entries: &'a mut [IndexEntry],
39 pack: OwnedFd,
40 index: OwnedFd,
41 encode_scratch: &mut [u8],
42 compare_scratch: &mut [u8],
43 loose: LooseObjects,
44 ) -> Result<Self> {
45 let pack = Region {
46 size: file_size(&pack).map_err(pack_error)?,
47 fd: pack,
48 offset: 0,
49 };
50 let index = Region {
51 size: file_size(&index).map_err(pack_error)?,
52 fd: index,
53 offset: 0,
54 };
55 let publication = Publication::new(repository, entries, encode_scratch, loose)?;
56 if pack.size != publication.pack_size() || index.size != publication.index_size() {
57 return Err(Error::CorruptPack);
58 }
59 for (file, extension) in [(&pack, b"pack".as_slice()), (&index, b"idx".as_slice())] {
60 let mut bytes = [0; 96];
61 let mut path = SliceWriter::new(&mut bytes);
62 path.write_all(b"objects/pack/pack-")?;
63 path.write_all(publication.checksum().to_hex().as_bytes())?;
64 path.write_all(b".")?;
65 path.write_all(extension)?;
66 let path = core::str::from_utf8(path.as_bytes()).map_err(|_| Error::InvalidRequest)?;
67 let request = Request {
68 method: "GET",
69 path,
70 query: None,
71 body: b"",
72 git_protocol: None,
73 content_type: None,
74 };
75 let mut compare = Compare {
76 file,
77 position: 0,
78 scratch: compare_scratch,
79 };
80 publication.serve(request, encode_scratch, &mut compare)?;
81 if compare.position != file.size {
82 return Err(Error::CorruptPack);
83 }
84 }
85 Ok(Self {
86 publication,
87 pack,
88 index,
89 })
90 }
91
92 pub fn checksum(&self) -> git_zero::ObjectId {
93 self.publication.checksum()
94 }
95 pub fn pack_size(&self) -> u64 {
96 self.pack.size
97 }
98 pub fn index_size(&self) -> u64 {
99 self.index.size
100 }
101
102 pub fn prepare(&self, request: Request<'_>) -> Result<Option<FilePublishedResponse<'_, R>>> {
106 let Some(cursor) = self.publication.prepare(request)? else {
107 return Ok(None);
108 };
109 let response = cursor.response();
110 let size = cursor.size();
111 let body = match response.content_type {
112 "application/x-git-packed-objects" => FileBody::File {
113 file: &self.pack,
114 position: 0,
115 },
116 "application/x-git-packed-objects-toc" => FileBody::File {
117 file: &self.index,
118 position: 0,
119 },
120 _ => FileBody::Generated(cursor),
121 };
122 Ok(Some(FilePublishedResponse {
123 response,
124 size,
125 body,
126 cancelled: false,
127 }))
128 }
129}
130
131#[allow(clippy::large_enum_variant)]
132enum FileBody<'a, R> {
133 File { file: &'a Region, position: u64 },
134 Generated(PublishedResponse<'a, R>),
135}
136
137pub struct FilePublishedResponse<'a, R> {
140 response: Response,
141 size: u64,
142 body: FileBody<'a, R>,
143 cancelled: bool,
144}
145impl<R: Repository> FilePublishedResponse<'_, R> {
146 pub fn response(&self) -> Response {
147 self.response
148 }
149 pub fn size(&self) -> u64 {
150 self.size
151 }
152 pub fn cancel(&mut self) {
153 self.cancelled = true;
154 }
155 pub fn read(&mut self, output: &mut [u8]) -> Result<usize> {
156 if self.cancelled {
157 return Err(Error::InvalidRequest);
158 }
159 if output.is_empty() {
160 return Err(Error::BufferTooSmall);
161 }
162 let result = match &mut self.body {
163 FileBody::Generated(cursor) => cursor.read(output),
164 FileBody::File { file, position } => file.read(*position, output).inspect(|&count| {
165 *position += count as u64;
166 }),
167 };
168 if result.is_err() {
169 self.cancel();
170 }
171 result
172 }
173}