nix_nar/
enc.rs

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
13/// Encoder which can archive a given path as a NAR file.
14pub struct Encoder {
15    stack: Vec<CurrentActivity>,
16    internal_buffer_size: usize,
17}
18
19/// Builder for [`Encoder`].
20pub 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    /// Create a new encoder for file hierarchy at the given path.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the path is not valid UTF-8.
55    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    /// Create a builder for this encoder that can take additional
69    /// options.
70    pub fn builder<P: AsRef<Path>>(path: P) -> EncoderBuilder<P> {
71        EncoderBuilder {
72            path,
73            internal_buffer_size: 1024,
74        }
75    }
76
77    /// Archive to the given path.
78    ///
79    /// This is equivalent to creating the file, and [`io::copy`]ing
80    /// to it.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if the path already exists or an I/O error occurs.
85    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    /// Build the encoder.
100    ///
101    /// # Errors
102    ///
103    /// Returns an error if the path is not valid UTF-8.
104    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    /// Configure the internal buffer size.  This should be at least
115    /// 200 bytes larger than the longest filename.
116    ///
117    /// # Panics
118    ///
119    /// Panics if the given number is smaller than 200.
120    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    /// Execute a write-into-buffer operation.  If the given buffer is
202    /// big enough, then we just write into that.  Otherwise, we
203    /// create a temporary buffer, we write into that, we copy as much
204    /// data as we can to the given buffer, and store the remainer
205    /// into a `CurrentActivity::WriteMoreBytes` on the `self.stack`.
206    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                // TODO This is one vec copy too many.
219                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}