1use {
8 crate::{control::ControlFile, deb::DebCompression, error::Result},
9 md5::Digest,
10 os_str_bytes::OsStrBytes,
11 simple_file_manifest::{FileEntry, FileManifest},
12 std::{
13 io::{BufWriter, Cursor, Read, Write},
14 path::Path,
15 time::SystemTime,
16 },
17};
18
19pub struct DebBuilder<'control> {
21 control_builder: ControlTarBuilder<'control>,
22
23 compression: DebCompression,
24
25 install_files: FileManifest,
27
28 mtime: Option<SystemTime>,
29}
30
31impl<'control> DebBuilder<'control> {
32 pub fn new(control_file: ControlFile<'control>) -> Self {
34 Self {
35 control_builder: ControlTarBuilder::new(control_file),
36 compression: DebCompression::Gzip,
37 install_files: FileManifest::default(),
38 mtime: None,
39 }
40 }
41
42 #[must_use]
46 pub fn set_compression(mut self, compression: DebCompression) -> Self {
47 self.compression = compression;
48 self
49 }
50
51 fn mtime(&self) -> u64 {
52 self.mtime
53 .unwrap_or_else(std::time::SystemTime::now)
54 .duration_since(std::time::UNIX_EPOCH)
55 .expect("times before UNIX epoch not accepted")
56 .as_secs()
57 }
58
59 #[must_use]
66 pub fn set_mtime(mut self, time: Option<SystemTime>) -> Self {
67 self.mtime = time;
68 self.control_builder = self.control_builder.set_mtime(time);
69 self
70 }
71
72 pub fn extra_control_tar_file(
74 mut self,
75 path: impl AsRef<Path>,
76 entry: impl Into<FileEntry>,
77 ) -> Result<Self> {
78 self.control_builder = self.control_builder.add_extra_file(path, entry)?;
79 Ok(self)
80 }
81
82 pub fn install_file(
90 mut self,
91 path: impl AsRef<Path> + Clone,
92 entry: impl Into<FileEntry> + Clone,
93 ) -> Result<Self> {
94 let entry = entry.into();
95
96 let data = entry.resolve_content()?;
97 let mut cursor = Cursor::new(&data);
98 self.control_builder = self
99 .control_builder
100 .add_data_file(path.clone(), &mut cursor)?;
101
102 self.install_files.add_file_entry(path, entry)?;
103
104 Ok(self)
105 }
106
107 pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
111 let mut ar_builder = ar::Builder::new(writer);
112
113 let data: &[u8] = b"2.0\n";
115 let mut header = ar::Header::new(b"debian-binary".to_vec(), data.len() as _);
116 header.set_mode(0o644);
117 header.set_mtime(self.mtime());
118 header.set_uid(0);
119 header.set_gid(0);
120 ar_builder.append(&header, data)?;
121
122 let mut control_writer = BufWriter::new(Vec::new());
124 self.control_builder.write(&mut control_writer)?;
125 let control_tar = control_writer.into_inner().map_err(|e| e.into_error())?;
126 let control_tar = self
127 .compression
128 .compress(&mut std::io::Cursor::new(control_tar))?;
129
130 let mut header = ar::Header::new(
131 format!("control.tar{}", self.compression.extension()).into_bytes(),
132 control_tar.len() as _,
133 );
134 header.set_mode(0o644);
135 header.set_mtime(self.mtime());
136 header.set_uid(0);
137 header.set_gid(0);
138 ar_builder.append(&header, &*control_tar)?;
139
140 let mut data_writer = BufWriter::new(Vec::new());
142 write_deb_tar(&mut data_writer, &self.install_files, self.mtime())?;
143 let data_tar = data_writer.into_inner().map_err(|e| e.into_error())?;
144 let data_tar = self
145 .compression
146 .compress(&mut std::io::Cursor::new(data_tar))?;
147
148 let mut header = ar::Header::new(
149 format!("data.tar{}", self.compression.extension()).into_bytes(),
150 data_tar.len() as _,
151 );
152 header.set_mode(0o644);
153 header.set_mtime(self.mtime());
154 header.set_uid(0);
155 header.set_gid(0);
156 ar_builder.append(&header, &*data_tar)?;
157
158 Ok(())
159 }
160}
161
162fn new_tar_header(mtime: u64) -> Result<tar::Header> {
163 let mut header = tar::Header::new_gnu();
164 header.set_uid(0);
165 header.set_gid(0);
166 header.set_username("root")?;
167 header.set_groupname("root")?;
168 header.set_mtime(mtime);
169
170 Ok(header)
171}
172
173fn set_header_path(
174 builder: &mut tar::Builder<impl Write>,
175 header: &mut tar::Header,
176 path: &Path,
177 is_directory: bool,
178) -> Result<()> {
179 assert!(header.as_ustar().is_none());
187
188 let value = format!(
189 "./{}{}",
190 path.display(),
191 if is_directory { "/" } else { "" }
192 );
193 let value_bytes = value.as_bytes();
194
195 let name_buffer = &mut header.as_old_mut().name;
196
197 if value_bytes.len() <= name_buffer.len() {
199 name_buffer[0..value_bytes.len()].copy_from_slice(value_bytes);
200 } else {
201 let mut header2 = tar::Header::new_gnu();
204 let name = b"././@LongLink";
205 header2.as_gnu_mut().unwrap().name[..name.len()].clone_from_slice(&name[..]);
206 header2.set_mode(0o644);
207 header2.set_uid(0);
208 header2.set_gid(0);
209 header2.set_mtime(0);
210 header2.set_size(value_bytes.len() as u64 + 1);
211 header2.set_entry_type(tar::EntryType::new(b'L'));
212 header2.set_cksum();
213 let mut data = value_bytes.chain(std::io::repeat(0).take(1));
214 builder.append(&header2, &mut data)?;
215
216 let truncated_bytes = &value_bytes[0..name_buffer.len()];
217 name_buffer[0..truncated_bytes.len()].copy_from_slice(truncated_bytes);
218 }
219
220 Ok(())
221}
222
223pub struct ControlTarBuilder<'a> {
225 control: ControlFile<'a>,
227 extra_files: FileManifest,
229 md5sums: Vec<Vec<u8>>,
231 mtime: Option<SystemTime>,
233}
234
235impl<'a> ControlTarBuilder<'a> {
236 pub fn new(control_file: ControlFile<'a>) -> Self {
238 Self {
239 control: control_file,
240 extra_files: FileManifest::default(),
241 md5sums: vec![],
242 mtime: None,
243 }
244 }
245
246 pub fn add_extra_file(
252 mut self,
253 path: impl AsRef<Path>,
254 entry: impl Into<FileEntry>,
255 ) -> Result<Self> {
256 self.extra_files.add_file_entry(path, entry)?;
257
258 Ok(self)
259 }
260
261 pub fn add_data_file<P: AsRef<Path>, R: Read>(
272 mut self,
273 path: P,
274 reader: &mut R,
275 ) -> Result<Self> {
276 let mut context = md5::Md5::new();
277
278 let mut buffer = [0; 32768];
279
280 loop {
281 let read = reader.read(&mut buffer)?;
282 if read == 0 {
283 break;
284 }
285
286 context.update(&buffer[0..read]);
287 }
288
289 let digest = context.finalize();
290
291 let mut entry = Vec::new();
292 entry.write_all(&digest.to_ascii_lowercase())?;
293 entry.write_all(b" ")?;
294 entry.write_all(path.as_ref().to_raw_bytes().as_ref())?;
295 entry.write_all(b"\n")?;
296
297 self.md5sums.push(entry);
298
299 Ok(self)
300 }
301
302 fn mtime(&self) -> u64 {
303 self.mtime
304 .unwrap_or_else(std::time::SystemTime::now)
305 .duration_since(std::time::UNIX_EPOCH)
306 .expect("times before UNIX epoch not accepted")
307 .as_secs()
308 }
309
310 #[must_use]
311 pub fn set_mtime(mut self, time: Option<SystemTime>) -> Self {
312 self.mtime = time;
313 self
314 }
315
316 pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
318 let mut control_buffer = BufWriter::new(Vec::new());
319 self.control.write(&mut control_buffer)?;
320 let control_data = control_buffer.into_inner().map_err(|e| e.into_error())?;
321
322 let mut manifest = self.extra_files.clone();
323 manifest.add_file_entry("control", control_data)?;
324 manifest.add_file_entry("md5sums", self.md5sums.concat::<u8>())?;
325
326 write_deb_tar(writer, &manifest, self.mtime())
327 }
328}
329
330pub fn write_deb_tar<W: Write>(writer: W, files: &FileManifest, mtime: u64) -> Result<()> {
332 let mut builder = tar::Builder::new(writer);
333
334 let mut header = new_tar_header(mtime)?;
336 header.set_path(Path::new("./"))?;
337 header.set_mode(0o755);
338 header.set_size(0);
339 header.set_entry_type(tar::EntryType::Directory);
340 header.set_cksum();
341 builder.append(&header, &*vec![])?;
342
343 for directory in files.relative_directories() {
345 let mut header = new_tar_header(mtime)?;
346 set_header_path(&mut builder, &mut header, &directory, true)?;
347 header.set_mode(0o755);
348 header.set_size(0);
349 header.set_entry_type(tar::EntryType::Directory);
350 header.set_cksum();
351 builder.append(&header, &*vec![])?;
352 }
353
354 for (rel_path, content) in files.iter_entries() {
356 let data = content.resolve_content()?;
357
358 let mut header = new_tar_header(mtime)?;
359 set_header_path(&mut builder, &mut header, rel_path, false)?;
360 header.set_mode(if content.is_executable() {
361 0o755
362 } else {
363 0o644
364 });
365 header.set_size(data.len() as _);
366 header.set_cksum();
367 builder.append(&header, &*data)?;
368 }
369
370 builder.finish()?;
371
372 Ok(())
373}
374
375#[cfg(test)]
376mod tests {
377 use {super::*, crate::control::ControlParagraph, std::path::PathBuf};
378
379 #[test]
380 fn test_write_control_tar_simple() -> Result<()> {
381 let mut control_para = ControlParagraph::default();
382 control_para.set_field_from_string("Package".into(), "mypackage".into());
383 control_para.set_field_from_string("Architecture".into(), "amd64".into());
384
385 let mut control = ControlFile::default();
386 control.add_paragraph(control_para);
387
388 let builder = ControlTarBuilder::new(control)
389 .set_mtime(Some(SystemTime::UNIX_EPOCH))
390 .add_extra_file("prerm", FileEntry::new_from_data(vec![42], true))?
391 .add_data_file("usr/bin/myapp", &mut std::io::Cursor::new("data"))?;
392
393 let mut buffer = vec![];
394 builder.write(&mut buffer)?;
395
396 let mut archive = tar::Archive::new(std::io::Cursor::new(buffer));
397
398 for (i, entry) in archive.entries()?.enumerate() {
399 let entry = entry?;
400
401 let path = match i {
402 0 => Path::new("./"),
403 1 => Path::new("./control"),
404 2 => Path::new("./md5sums"),
405 3 => Path::new("./prerm"),
406 _ => panic!("unexpected archive entry"),
407 };
408
409 assert_eq!(entry.path()?, path, "entry {} path matches", i);
410 }
411
412 Ok(())
413 }
414
415 #[test]
416 fn test_write_data_tar_one_file() -> Result<()> {
417 let mut manifest = FileManifest::default();
418 manifest.add_file_entry("foo/bar.txt", FileEntry::new_from_data(vec![42], true))?;
419
420 let mut buffer = vec![];
421 write_deb_tar(&mut buffer, &manifest, 2)?;
422
423 let mut archive = tar::Archive::new(std::io::Cursor::new(buffer));
424
425 for (i, entry) in archive.entries()?.enumerate() {
426 let entry = entry?;
427
428 let path = match i {
429 0 => Path::new("./"),
430 1 => Path::new("./foo/"),
431 2 => Path::new("./foo/bar.txt"),
432 _ => panic!("unexpected archive entry"),
433 };
434
435 assert_eq!(entry.path()?, path, "entry {} path matches", i);
436 }
437
438 Ok(())
439 }
440
441 #[test]
442 fn test_write_data_tar_long_path() -> Result<()> {
443 let long_path = PathBuf::from(format!("f{}.txt", "u".repeat(200)));
444
445 let mut manifest = FileManifest::default();
446
447 manifest.add_file_entry(&long_path, vec![42])?;
448
449 let mut buffer = vec![];
450 write_deb_tar(&mut buffer, &manifest, 2)?;
451
452 let mut archive = tar::Archive::new(std::io::Cursor::new(buffer));
453
454 for (i, entry) in archive.entries()?.enumerate() {
455 let entry = entry?;
456
457 if i != 1 {
458 continue;
459 }
460
461 assert_eq!(
462 entry.path()?,
463 Path::new(&format!("./f{}.txt", "u".repeat(200)))
464 );
465 }
466
467 Ok(())
468 }
469
470 #[test]
471 fn test_write_deb() -> Result<()> {
472 let mut control_para = ControlParagraph::default();
473 control_para.set_field_from_string("Package".into(), "mypackage".into());
474 control_para.set_field_from_string("Architecture".into(), "amd64".into());
475
476 let mut control = ControlFile::default();
477 control.add_paragraph(control_para);
478
479 let builder = DebBuilder::new(control)
480 .set_compression(DebCompression::Zstandard(3))
481 .install_file("usr/bin/myapp", FileEntry::new_from_data(vec![42], true))?;
482
483 let mut buffer = vec![];
484 builder.write(&mut buffer)?;
485
486 let mut archive = ar::Archive::new(std::io::Cursor::new(buffer));
487 {
488 let entry = archive.next_entry().unwrap().unwrap();
489 assert_eq!(entry.header().identifier(), b"debian-binary");
490 }
491 {
492 let entry = archive.next_entry().unwrap().unwrap();
493 assert_eq!(entry.header().identifier(), b"control.tar.zst");
494 }
495 {
496 let entry = archive.next_entry().unwrap().unwrap();
497 assert_eq!(entry.header().identifier(), b"data.tar.zst");
498 }
499
500 assert!(archive.next_entry().is_none());
501
502 Ok(())
503 }
504}