1use std::{
2 cmp::min,
3 fs::{self, File},
4 io::{self, Read},
5 path::Path,
6};
7
8use camino::{Utf8DirEntry, Utf8Path, Utf8PathBuf};
9use is_executable::IsExecutable;
10
11use crate::{coder, NarError};
12
13pub struct Encoder {
15 stack: Vec<CurrentActivity>,
16 internal_buffer_size: usize,
17}
18
19pub struct EncoderBuilder<P: AsRef<Path>> {
21 path: P,
22 internal_buffer_size: usize,
23}
24
25#[derive(Debug)]
26enum CurrentActivity {
27 StartArchive,
28 StartEntry,
29 Toplevel {
30 path: Utf8PathBuf,
31 },
32 WalkingDir {
33 dir_path: Utf8PathBuf,
34 files_rev: Vec<String>,
35 },
36 EncodingFile {
37 file: File,
38 },
39 WritePadding {
40 padding: u64,
41 },
42 WriteMoreBytes {
43 bytes: Vec<u8>,
44 },
45 CloseDirEntry,
46 CloseEntry,
47}
48
49impl Encoder {
50 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, NarError> {
56 let path = to_utf8_path(path)?;
57 Ok(Self {
58 stack: vec![
59 CurrentActivity::CloseEntry,
60 CurrentActivity::Toplevel { path },
61 CurrentActivity::StartEntry,
62 CurrentActivity::StartArchive,
63 ],
64 internal_buffer_size: 1024,
65 })
66 }
67
68 pub fn builder<P: AsRef<Path>>(path: P) -> EncoderBuilder<P> {
71 EncoderBuilder {
72 path,
73 internal_buffer_size: 1024,
74 }
75 }
76
77 pub fn pack<P: AsRef<Path>>(&mut self, dst: P) -> Result<(), NarError> {
86 let dst = to_utf8_path(dst)?;
87 if dst.symlink_metadata().is_ok() {
88 return Err(NarError::PackError(format!(
89 "Destination {dst} already exists. Delete it first."
90 )));
91 }
92 let mut nar = File::create(&dst)?;
93 io::copy(self, &mut nar)?;
94 Ok(())
95 }
96}
97
98impl<P: AsRef<Path>> EncoderBuilder<P> {
99 pub fn build(&self) -> Result<Encoder, NarError> {
105 let Self {
106 path,
107 internal_buffer_size,
108 } = self;
109 let mut enc = Encoder::new(path)?;
110 enc.internal_buffer_size = *internal_buffer_size;
111 Ok(enc)
112 }
113
114 pub fn internal_buffer_size(&mut self, x: usize) -> &mut Self {
121 assert!(
122 x >= 200,
123 "internal_buffer_size should be at least 200 bytes larger than the longest filename you have"
124 );
125 self.internal_buffer_size = x;
126 self
127 }
128}
129
130impl Encoder {
131 fn start_encoding_file<P: AsRef<Utf8Path>>(
132 &mut self,
133 buf: &mut [u8],
134 path: P,
135 dir_entry: Option<String>,
136 ) -> Result<usize, io::Error> {
137 let path = path.as_ref();
138 let executable = path.as_std_path().is_executable();
139 let file_handle = File::open(path).map_err(annotate_err_with_path(&path))?;
140 let file_len = file_handle.metadata()?.len();
141 let file_len_rounded_up = (file_len + 7) & !7;
142 if file_len_rounded_up > file_len {
143 self.stack.push(CurrentActivity::WritePadding {
144 padding: file_len_rounded_up - file_len,
145 });
146 }
147 self.stack
148 .push(CurrentActivity::EncodingFile { file: file_handle });
149 self.write_with_buffer(buf, move |buf| {
150 let mut len = 0;
151 if let Some(ref file) = dir_entry {
152 len += coder::start_dir_entry(&mut buf[len..], file)?;
153 }
154 len += coder::write_file_regular(&mut buf[len..], executable)?;
155 len += coder::write_u64_le(&mut buf[len..], file_len)?;
156 Ok(len)
157 })
158 }
159
160 fn start_encoding_dir<P: AsRef<Utf8Path>>(
161 &mut self,
162 buf: &mut [u8],
163 path: P,
164 dir_entry: Option<String>,
165 ) -> Result<usize, io::Error> {
166 self.stack.push(CurrentActivity::WalkingDir {
167 files_rev: list_dir_files(path.as_ref())
168 .map_err(annotate_err_with_path(&path))?,
169 dir_path: path.as_ref().into(),
170 });
171 self.write_with_buffer(buf, move |buf| {
172 let mut len = 0;
173 if let Some(ref file) = dir_entry {
174 len += coder::start_dir_entry(&mut buf[len..], file)?;
175 }
176 len += coder::start_dir(&mut buf[len..])?;
177 Ok(len)
178 })
179 }
180
181 fn start_encoding_symlink<P: AsRef<Utf8Path>>(
182 &mut self,
183 buf: &mut [u8],
184 link_path: P,
185 dir_entry: Option<String>,
186 ) -> Result<usize, io::Error> {
187 let link_path = link_path.as_ref();
188 let target_path: Utf8PathBuf = link_path
189 .read_link_utf8()
190 .map_err(annotate_err_with_path(&link_path))?;
191 self.write_with_buffer(buf, move |buf| {
192 let mut len = 0;
193 if let Some(ref file) = dir_entry {
194 len += coder::start_dir_entry(buf, file)?;
195 }
196 len += coder::write_symlink(&mut buf[len..], &target_path)?;
197 Ok(len)
198 })
199 }
200
201 fn write_with_buffer<F>(&mut self, dst_buf: &mut [u8], f: F) -> io::Result<usize>
207 where
208 F: FnOnce(&mut [u8]) -> io::Result<usize>,
209 {
210 if dst_buf.len() >= 1024 {
211 f(dst_buf)
212 } else {
213 let mut buf = vec![0; self.internal_buffer_size];
214 let len = f(&mut buf)?;
215 let to_write_len = min(len, dst_buf.len());
216 dst_buf[..to_write_len].copy_from_slice(&buf[..to_write_len]);
217 if len > to_write_len {
218 self.stack.push(CurrentActivity::WriteMoreBytes {
220 bytes: buf[to_write_len..len].to_vec(),
221 });
222 }
223 Ok(to_write_len)
224 }
225 }
226}
227
228impl Read for Encoder {
229 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
230 match self.stack.pop() {
231 None => Ok(0),
232 Some(CurrentActivity::StartArchive) => {
233 self.write_with_buffer(buf, coder::start_archive)
234 }
235 Some(CurrentActivity::StartEntry) => {
236 self.write_with_buffer(buf, coder::start_entry)
237 }
238 Some(CurrentActivity::CloseDirEntry) => {
239 self.write_with_buffer(buf, coder::close_dir_entry)
240 }
241 Some(CurrentActivity::CloseEntry) => {
242 self.write_with_buffer(buf, coder::close_entry)
243 }
244 Some(CurrentActivity::Toplevel { path }) => {
245 let metadata =
246 fs::symlink_metadata(&path).map_err(annotate_err_with_path(&path))?;
247 if metadata.is_dir() {
248 self.start_encoding_dir(buf, path, None)
249 } else if metadata.is_symlink() {
250 self.start_encoding_symlink(buf, path, None)
251 } else if metadata.is_file() {
252 self.start_encoding_file(buf, path, None)
253 } else {
254 Err(other_io_error(format!("unknown file type {path}")))
255 }
256 }
257 Some(CurrentActivity::WalkingDir {
258 dir_path,
259 mut files_rev,
260 }) => match files_rev.pop() {
261 None => self.read(buf),
262 Some(file) => {
263 let path = dir_path.join(&file);
264
265 self.stack.push(CurrentActivity::WalkingDir {
266 dir_path,
267 files_rev,
268 });
269
270 self.stack.push(CurrentActivity::CloseDirEntry);
271
272 let metadata = fs::symlink_metadata(&path)
273 .map_err(annotate_err_with_path(&file))?;
274 if metadata.is_dir() {
275 self.start_encoding_dir(buf, path, Some(file))
276 } else if metadata.is_symlink() {
277 self.start_encoding_symlink(buf, path, Some(file))
278 } else if metadata.is_file() {
279 self.start_encoding_file(buf, path, Some(file))
280 } else {
281 Err(other_io_error(format!("unknown file type {path}",)))
282 }
283 }
284 },
285 Some(CurrentActivity::EncodingFile { mut file }) => {
286 let len = file.read(buf)?;
287 if len != 0 {
288 self.stack.push(CurrentActivity::EncodingFile { file });
289 Ok(len)
290 } else {
291 self.read(buf)
292 }
293 }
294 Some(CurrentActivity::WritePadding { padding }) => {
295 #[allow(clippy::cast_possible_truncation)]
296 let len = min(padding, buf.len() as u64) as usize;
297 buf.fill(0);
298 if (len as u64) < padding {
299 self.stack.push(CurrentActivity::WritePadding {
300 padding: padding - len as u64,
301 });
302 }
303 Ok(len)
304 }
305 Some(CurrentActivity::WriteMoreBytes { bytes }) => {
306 let len = min(bytes.len(), buf.len());
307 buf[..len].copy_from_slice(&bytes[..len]);
308 if len < bytes.len() {
309 self.stack.push(CurrentActivity::WriteMoreBytes {
310 bytes: bytes[len..].to_vec(),
311 });
312 }
313 Ok(len)
314 }
315 }
316 }
317}
318
319fn list_dir_files(path: &Utf8Path) -> Result<Vec<String>, io::Error> {
320 let mut fs = path
321 .read_dir_utf8()
322 .map_err(annotate_err_with_path(&path))?
323 .collect::<Result<Vec<Utf8DirEntry>, io::Error>>()?
324 .into_iter()
325 .map(|p| p.file_name().into())
326 .collect::<Vec<String>>();
327 fs.sort_by(|a, b| b.cmp(a));
328 Ok(fs)
329}
330
331fn annotate_err_with_path<P: AsRef<Utf8Path>>(
332 path: P,
333) -> impl FnOnce(io::Error) -> io::Error {
334 let path = path.as_ref().to_path_buf();
335 move |err: io::Error| other_io_error(format!("IO error on {path}: {err}"))
336}
337
338fn other_io_error<S: AsRef<str>>(message: S) -> io::Error {
339 io::Error::other(message.as_ref())
340}
341
342fn to_utf8_path<P: AsRef<Path>>(path: P) -> Result<Utf8PathBuf, NarError> {
343 let path = path.as_ref();
344 path.try_into()
345 .map(|x: &Utf8Path| x.to_path_buf())
346 .map_err(|err| {
347 NarError::Utf8PathError(format!(
348 "Failed to convert '{}' to UTF-8: {err}",
349 path.display()
350 ))
351 })
352}