1use std::{
4 fs,
5 io::{self, Read, Write},
6 path::{Path, PathBuf},
7 time::UNIX_EPOCH,
8};
9
10use computer_protocol::{SKIP_REPORT_ENTRY, SkipList};
11use tar::{Builder, EntryType, Header};
12
13use crate::{Failure, Progress, rules::Platform};
14
15#[derive(Debug)]
17pub struct Packed {
18 pub progress: Progress,
19 pub skipped: SkipList,
20}
21
22struct Exact<'a, R> {
27 inner: R,
28 left: u64,
29 copied: &'a mut u64,
30}
31
32impl<R: Read> Read for Exact<'_, R> {
33 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
34 if self.left == 0 {
35 return Ok(0);
36 }
37 let want = usize::try_from(self.left).map_or(buf.len(), |left| left.min(buf.len()));
38 let n = self.inner.read(&mut buf[..want])?;
39 if n == 0 {
40 return Err(io::Error::new(
41 io::ErrorKind::UnexpectedEof,
42 "the file got shorter while it was being copied",
43 ));
44 }
45 self.left -= n as u64;
46 *self.copied += n as u64;
47 Ok(n)
48 }
49}
50
51struct Packer<W: Write> {
52 builder: Builder<W>,
53 platform: Platform,
54 progress: Progress,
55 skipped: SkipList,
56}
57
58pub fn pack<W: Write>(source: &Path, out: W, platform: Platform) -> Result<Packed, Failure> {
70 let mut packer = Packer {
71 builder: Builder::new(out),
72 platform,
73 progress: Progress::default(),
74 skipped: SkipList::default(),
75 };
76 match packer.run(source) {
77 Ok(()) => Ok(Packed {
78 progress: packer.progress,
79 skipped: packer.skipped,
80 }),
81 Err(error) => Err(Failure::new(
82 format!("sending {} failed: {error}", source.display()),
83 packer.progress,
84 )),
85 }
86}
87
88impl<W: Write> Packer<W> {
89 fn run(&mut self, source: &Path) -> io::Result<()> {
90 let meta = fs::metadata(source).map_err(|error| {
91 if error.kind() == io::ErrorKind::NotFound {
92 io::Error::new(error.kind(), "it does not exist")
93 } else {
94 error
95 }
96 })?;
97 let name = root_name(source)?;
98 let name = Path::new(&name);
99 if meta.is_dir() {
100 self.add_dir(source, name, &meta)?;
101 } else if meta.is_file() {
102 self.add_file(source, name)?;
103 } else {
104 return Err(io::Error::other(
105 "it is a pipe, device, or socket, not a file or folder",
106 ));
107 }
108 self.finish()
109 }
110
111 fn finish(&mut self) -> io::Result<()> {
112 let json = serde_json::to_vec(&self.skipped).map_err(io::Error::other)?;
113 let mut header = header(EntryType::Regular, 0o644, 0);
114 header.set_size(json.len() as u64);
115 self.builder
116 .append_data(&mut header, SKIP_REPORT_ENTRY, json.as_slice())?;
117 self.builder.finish()?;
118 self.builder.get_mut().flush()
119 }
120
121 fn add_dir(&mut self, dir: &Path, rel: &Path, meta: &fs::Metadata) -> io::Result<()> {
122 let mut head = header(EntryType::Directory, 0o755, mtime(meta));
123 self.builder.append_data(&mut head, rel, io::empty())?;
124 self.progress.folders += 1;
125
126 let listing = match fs::read_dir(dir) {
127 Ok(listing) => listing,
128 Err(error) => {
129 self.skip(rel, format!("the folder cannot be read: {error}"));
130 return Ok(());
131 }
132 };
133 let mut children = Vec::new();
134 for child in listing {
135 match child {
136 Ok(child) => children.push(child),
137 Err(error) => self.skip(rel, format!("an entry cannot be read: {error}")),
138 }
139 }
140 children.sort_by_key(fs::DirEntry::file_name);
141 for child in children {
142 let Some(name) = child.file_name().to_str().map(str::to_owned) else {
143 self.skip(
144 &rel.join(child.file_name()),
145 "the name is not valid UTF-8".to_owned(),
146 );
147 continue;
148 };
149 let path = child.path();
150 let rel = rel.join(&name);
151 let meta = match fs::symlink_metadata(&path) {
152 Ok(meta) => meta,
153 Err(error) => {
154 self.skip(&rel, format!("it cannot be read: {error}"));
155 continue;
156 }
157 };
158 let kind = meta.file_type();
159 if kind.is_dir() {
160 self.add_dir(&path, &rel, &meta)?;
161 } else if kind.is_file() {
162 self.add_file(&path, &rel)?;
163 } else if kind.is_symlink() {
164 self.add_link(&path, &rel, &meta)?;
165 } else {
166 self.skip(&rel, "it is a pipe, device, or socket".to_owned());
167 }
168 }
169 Ok(())
170 }
171
172 fn add_file(&mut self, path: &Path, rel: &Path) -> io::Result<()> {
173 let file = match fs::File::open(path) {
174 Ok(file) => file,
175 Err(error) if rel.components().count() > 1 => {
176 self.skip(rel, format!("it cannot be opened: {error}"));
177 return Ok(());
178 }
179 Err(error) => return Err(error),
180 };
181 let meta = file.metadata()?;
182 let mut head = header(EntryType::Regular, file_mode(&meta), mtime(&meta));
183 head.set_size(meta.len());
184 let mut copied = 0;
185 let data = Exact {
186 inner: file,
187 left: meta.len(),
188 copied: &mut copied,
189 };
190 let sent = self.builder.append_data(&mut head, rel, data);
191 self.progress.bytes += copied;
192 sent.map_err(|error| io::Error::new(error.kind(), format!("{}: {error}", rel.display())))?;
193 self.progress.files += 1;
194 Ok(())
195 }
196
197 fn add_link(&mut self, path: &Path, rel: &Path, meta: &fs::Metadata) -> io::Result<()> {
198 let target = match fs::read_link(path) {
199 Ok(target) => target,
200 Err(error) => {
201 self.skip(rel, format!("the link cannot be read: {error}"));
202 return Ok(());
203 }
204 };
205 if self.platform == Platform::Windows && (target.has_root() || has_prefix(&target)) {
206 self.skip(
207 rel,
208 "the link points to an absolute Windows path that does not exist on the computer"
209 .to_owned(),
210 );
211 return Ok(());
212 }
213 let mut head = header(EntryType::Symlink, 0o777, mtime(meta));
214 self.builder.append_link(&mut head, rel, &target)?;
215 self.progress.files += 1;
216 Ok(())
217 }
218
219 fn skip(&mut self, rel: &Path, reason: String) {
220 self.skipped.push(rel.display().to_string(), reason);
221 }
222}
223
224fn has_prefix(path: &Path) -> bool {
225 matches!(
226 path.components().next(),
227 Some(std::path::Component::Prefix(_))
228 )
229}
230
231pub fn root_name(source: &Path) -> io::Result<String> {
237 let given: Option<PathBuf> = source.file_name().map(PathBuf::from);
238 let name = match given {
239 Some(name) => name,
240 None => fs::canonicalize(source)?
241 .file_name()
242 .map(PathBuf::from)
243 .ok_or_else(|| io::Error::other("it has no name, give a path to a file or folder"))?,
244 };
245 name.into_os_string()
246 .into_string()
247 .map_err(|_| io::Error::other("its name is not valid UTF-8"))
248}
249
250fn header(kind: EntryType, mode: u32, mtime: u64) -> Header {
251 let mut header = Header::new_gnu();
252 header.set_entry_type(kind);
253 header.set_mode(mode);
254 header.set_mtime(mtime);
255 header.set_size(0);
256 header
257}
258
259fn mtime(meta: &fs::Metadata) -> u64 {
260 meta.modified()
261 .ok()
262 .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
263 .map_or(0, |since| since.as_secs())
264}
265
266#[cfg(unix)]
267fn file_mode(meta: &fs::Metadata) -> u32 {
268 use std::os::unix::fs::PermissionsExt;
269 meta.permissions().mode() & 0o777
270}
271
272#[cfg(not(unix))]
273fn file_mode(meta: &fs::Metadata) -> u32 {
274 if meta.permissions().readonly() {
275 0o444
276 } else {
277 0o644
278 }
279}