1use {
11 crate::ControlFile,
12 os_str_bytes::OsStrBytes,
13 std::{
14 io::{BufWriter, Cursor, Read, Write},
15 path::Path,
16 time::SystemTime,
17 },
18 tugger_file_manifest::{FileEntry, FileManifest, FileManifestError},
19};
20
21#[derive(Debug)]
23pub enum DebError {
24 IoError(std::io::Error),
25 PathError(String),
26 FileManifestError(FileManifestError),
27}
28
29impl std::fmt::Display for DebError {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 Self::IoError(inner) => write!(f, "I/O error: {}", inner),
33 Self::PathError(msg) => write!(f, "path error: {}", msg),
34 Self::FileManifestError(inner) => write!(f, "file manifest error: {}", inner),
35 }
36 }
37}
38
39impl std::error::Error for DebError {}
40
41impl From<std::io::Error> for DebError {
42 fn from(e: std::io::Error) -> Self {
43 Self::IoError(e)
44 }
45}
46
47impl<W> From<std::io::IntoInnerError<W>> for DebError {
48 fn from(e: std::io::IntoInnerError<W>) -> Self {
49 Self::IoError(e.into())
50 }
51}
52
53impl From<FileManifestError> for DebError {
54 fn from(e: FileManifestError) -> Self {
55 Self::FileManifestError(e)
56 }
57}
58
59pub enum DebCompression {
61 Uncompressed,
63 Gzip,
65 Xz(u32),
67 Zstandard(i32),
69}
70
71impl DebCompression {
72 pub fn extension(&self) -> &'static str {
74 match self {
75 Self::Uncompressed => "",
76 Self::Gzip => ".gz",
77 Self::Xz(_) => ".xz",
78 Self::Zstandard(_) => ".zst",
79 }
80 }
81
82 pub fn compress(&self, reader: &mut impl Read) -> Result<Vec<u8>, DebError> {
84 let mut buffer = vec![];
85
86 match self {
87 Self::Uncompressed => {
88 std::io::copy(reader, &mut buffer)?;
89 }
90 Self::Gzip => {
91 let header = libflate::gzip::HeaderBuilder::new().finish();
92
93 let mut encoder = libflate::gzip::Encoder::with_options(
94 &mut buffer,
95 libflate::gzip::EncodeOptions::new().header(header),
96 )?;
97 std::io::copy(reader, &mut encoder)?;
98 encoder.finish().into_result()?;
99 }
100 Self::Xz(level) => {
101 let mut encoder = xz2::write::XzEncoder::new(buffer, *level);
102 std::io::copy(reader, &mut encoder)?;
103 buffer = encoder.finish()?;
104 }
105 Self::Zstandard(level) => {
106 let mut encoder = zstd::Encoder::new(buffer, *level)?;
107 std::io::copy(reader, &mut encoder)?;
108 buffer = encoder.finish()?;
109 }
110 }
111
112 Ok(buffer)
113 }
114}
115
116pub struct DebBuilder<'control> {
118 control_builder: ControlTarBuilder<'control>,
119
120 compression: DebCompression,
121
122 install_files: FileManifest,
124
125 mtime: Option<SystemTime>,
126}
127
128impl<'control> DebBuilder<'control> {
129 pub fn new(control_file: ControlFile<'control>) -> Self {
131 Self {
132 control_builder: ControlTarBuilder::new(control_file),
133 compression: DebCompression::Gzip,
134 install_files: FileManifest::default(),
135 mtime: None,
136 }
137 }
138
139 pub fn set_compression(mut self, compression: DebCompression) -> Self {
143 self.compression = compression;
144 self
145 }
146
147 fn mtime(&self) -> u64 {
148 self.mtime
149 .unwrap_or_else(std::time::SystemTime::now)
150 .duration_since(std::time::UNIX_EPOCH)
151 .expect("times before UNIX epoch not accepted")
152 .as_secs()
153 }
154
155 pub fn set_mtime(mut self, time: Option<SystemTime>) -> Self {
162 self.mtime = time;
163 self.control_builder = self.control_builder.set_mtime(time);
164 self
165 }
166
167 pub fn extra_control_tar_file(
169 mut self,
170 path: impl AsRef<Path>,
171 entry: impl Into<FileEntry>,
172 ) -> Result<Self, DebError> {
173 self.control_builder = self.control_builder.add_extra_file(path, entry)?;
174 Ok(self)
175 }
176
177 pub fn install_file(
185 mut self,
186 path: impl AsRef<Path> + Clone,
187 entry: impl Into<FileEntry> + Clone,
188 ) -> Result<Self, DebError> {
189 let entry = entry.into();
190
191 let data = entry.resolve_content()?;
192 let mut cursor = Cursor::new(&data);
193 self.control_builder = self
194 .control_builder
195 .add_data_file(path.clone(), &mut cursor)?;
196
197 self.install_files.add_file_entry(path, entry)?;
198
199 Ok(self)
200 }
201
202 pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), DebError> {
206 let mut ar_builder = ar::Builder::new(writer);
207
208 let data: &[u8] = b"2.0\n";
210 let mut header = ar::Header::new(b"debian-binary".to_vec(), data.len() as _);
211 header.set_mode(0o644);
212 header.set_mtime(self.mtime());
213 header.set_uid(0);
214 header.set_gid(0);
215 ar_builder.append(&header, data)?;
216
217 let mut control_writer = BufWriter::new(Vec::new());
219 self.control_builder.write(&mut control_writer)?;
220 let control_tar = control_writer.into_inner()?;
221 let control_tar = self
222 .compression
223 .compress(&mut std::io::Cursor::new(control_tar))?;
224
225 let mut header = ar::Header::new(
226 format!("control.tar{}", self.compression.extension()).into_bytes(),
227 control_tar.len() as _,
228 );
229 header.set_mode(0o644);
230 header.set_mtime(self.mtime());
231 header.set_uid(0);
232 header.set_gid(0);
233 ar_builder.append(&header, &*control_tar)?;
234
235 let mut data_writer = BufWriter::new(Vec::new());
237 write_deb_tar(&mut data_writer, &self.install_files, self.mtime())?;
238 let data_tar = data_writer.into_inner()?;
239 let data_tar = self
240 .compression
241 .compress(&mut std::io::Cursor::new(data_tar))?;
242
243 let mut header = ar::Header::new(
244 format!("data.tar{}", self.compression.extension()).into_bytes(),
245 data_tar.len() as _,
246 );
247 header.set_mode(0o644);
248 header.set_mtime(self.mtime());
249 header.set_uid(0);
250 header.set_gid(0);
251 ar_builder.append(&header, &*data_tar)?;
252
253 Ok(())
254 }
255}
256
257fn new_tar_header(mtime: u64) -> Result<tar::Header, DebError> {
258 let mut header = tar::Header::new_gnu();
259 header.set_uid(0);
260 header.set_gid(0);
261 header.set_username("root")?;
262 header.set_groupname("root")?;
263 header.set_mtime(mtime);
264
265 Ok(header)
266}
267
268fn set_header_path(
269 builder: &mut tar::Builder<impl Write>,
270 header: &mut tar::Header,
271 path: &Path,
272 is_directory: bool,
273) -> Result<(), DebError> {
274 assert!(header.as_ustar().is_none());
282
283 let value = format!(
284 "./{}{}",
285 path.display(),
286 if is_directory { "/" } else { "" }
287 );
288 let value_bytes = value.as_bytes();
289
290 let name_buffer = &mut header.as_old_mut().name;
291
292 if value_bytes.len() <= name_buffer.len() {
294 name_buffer[0..value_bytes.len()].copy_from_slice(value_bytes);
295 } else {
296 let mut header2 = tar::Header::new_gnu();
299 let name = b"././@LongLink";
300 header2.as_gnu_mut().unwrap().name[..name.len()].clone_from_slice(&name[..]);
301 header2.set_mode(0o644);
302 header2.set_uid(0);
303 header2.set_gid(0);
304 header2.set_mtime(0);
305 header2.set_size(value_bytes.len() as u64 + 1);
306 header2.set_entry_type(tar::EntryType::new(b'L'));
307 header2.set_cksum();
308 let mut data = value_bytes.chain(std::io::repeat(0).take(1));
309 builder.append(&header2, &mut data)?;
310
311 let truncated_bytes = &value_bytes[0..name_buffer.len()];
312 name_buffer[0..truncated_bytes.len()].copy_from_slice(truncated_bytes);
313 }
314
315 Ok(())
316}
317
318pub struct ControlTarBuilder<'a> {
320 control: ControlFile<'a>,
322 extra_files: FileManifest,
324 md5sums: Vec<Vec<u8>>,
326 mtime: Option<SystemTime>,
328}
329
330impl<'a> ControlTarBuilder<'a> {
331 pub fn new(control_file: ControlFile<'a>) -> Self {
333 Self {
334 control: control_file,
335 extra_files: FileManifest::default(),
336 md5sums: vec![],
337 mtime: None,
338 }
339 }
340
341 pub fn add_extra_file(
347 mut self,
348 path: impl AsRef<Path>,
349 entry: impl Into<FileEntry>,
350 ) -> Result<Self, DebError> {
351 self.extra_files.add_file_entry(path, entry)?;
352
353 Ok(self)
354 }
355
356 pub fn add_data_file<P: AsRef<Path>, R: Read>(
367 mut self,
368 path: P,
369 reader: &mut R,
370 ) -> Result<Self, DebError> {
371 let mut context = md5::Context::new();
372
373 let mut buffer = [0; 32768];
374
375 loop {
376 let read = reader.read(&mut buffer)?;
377 if read == 0 {
378 break;
379 }
380
381 context.consume(&buffer[0..read]);
382 }
383
384 let digest = context.compute();
385
386 let mut entry = Vec::new();
387 entry.write_all(&digest.to_ascii_lowercase())?;
388 entry.write_all(b" ")?;
389 entry.write_all(path.as_ref().to_raw_bytes().as_ref())?;
390 entry.write_all(b"\n")?;
391
392 self.md5sums.push(entry);
393
394 Ok(self)
395 }
396
397 fn mtime(&self) -> u64 {
398 self.mtime
399 .unwrap_or_else(std::time::SystemTime::now)
400 .duration_since(std::time::UNIX_EPOCH)
401 .expect("times before UNIX epoch not accepted")
402 .as_secs()
403 }
404
405 pub fn set_mtime(mut self, time: Option<SystemTime>) -> Self {
406 self.mtime = time;
407 self
408 }
409
410 pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), DebError> {
412 let mut control_buffer = BufWriter::new(Vec::new());
413 self.control.write(&mut control_buffer)?;
414 let control_data = control_buffer.into_inner()?;
415
416 let mut manifest = self.extra_files.clone();
417 manifest.add_file_entry("control", control_data)?;
418 manifest.add_file_entry("md5sums", self.md5sums.concat::<u8>())?;
419
420 write_deb_tar(writer, &manifest, self.mtime())
421 }
422}
423
424pub fn write_deb_tar<W: Write>(
426 writer: W,
427 files: &FileManifest,
428 mtime: u64,
429) -> Result<(), DebError> {
430 let mut builder = tar::Builder::new(writer);
431
432 let mut header = new_tar_header(mtime)?;
434 header.set_path(Path::new("./"))?;
435 header.set_mode(0o755);
436 header.set_size(0);
437 header.set_cksum();
438 builder.append(&header, &*vec![])?;
439
440 for directory in files.relative_directories() {
442 let mut header = new_tar_header(mtime)?;
443 set_header_path(&mut builder, &mut header, &directory, true)?;
444 header.set_mode(0o755);
445 header.set_size(0);
446 header.set_cksum();
447 builder.append(&header, &*vec![])?;
448 }
449
450 for (rel_path, content) in files.iter_entries() {
452 let data = content.resolve_content()?;
453
454 let mut header = new_tar_header(mtime)?;
455 set_header_path(&mut builder, &mut header, rel_path, false)?;
456 header.set_mode(if content.is_executable() {
457 0o755
458 } else {
459 0o644
460 });
461 header.set_size(data.len() as _);
462 header.set_cksum();
463 builder.append(&header, &*data)?;
464 }
465
466 builder.finish()?;
467
468 Ok(())
469}
470
471#[cfg(test)]
472mod tests {
473 use {
474 super::*,
475 crate::ControlParagraph,
476 anyhow::{anyhow, Result},
477 std::path::PathBuf,
478 };
479
480 #[test]
481 fn test_write_control_tar_simple() -> Result<()> {
482 let mut control_para = ControlParagraph::default();
483 control_para.add_field_from_string("Package".into(), "mypackage".into())?;
484 control_para.add_field_from_string("Architecture".into(), "amd64".into())?;
485
486 let mut control = ControlFile::default();
487 control.add_paragraph(control_para);
488
489 let builder = ControlTarBuilder::new(control)
490 .set_mtime(Some(SystemTime::UNIX_EPOCH))
491 .add_extra_file("prerm", FileEntry::new_from_data(vec![42], true))?
492 .add_data_file("usr/bin/myapp", &mut std::io::Cursor::new("data"))?;
493
494 let mut buffer = vec![];
495 builder.write(&mut buffer)?;
496
497 let mut archive = tar::Archive::new(std::io::Cursor::new(buffer));
498
499 for (i, entry) in archive.entries()?.enumerate() {
500 let entry = entry?;
501
502 let path = match i {
503 0 => Path::new("./"),
504 1 => Path::new("./control"),
505 2 => Path::new("./md5sums"),
506 3 => Path::new("./prerm"),
507 _ => return Err(anyhow!("unexpected archive entry")),
508 };
509
510 assert_eq!(entry.path()?, path, "entry {} path matches", i);
511 }
512
513 Ok(())
514 }
515
516 #[test]
517 fn test_write_data_tar_one_file() -> Result<()> {
518 let mut manifest = FileManifest::default();
519 manifest.add_file_entry("foo/bar.txt", FileEntry::new_from_data(vec![42], true))?;
520
521 let mut buffer = vec![];
522 write_deb_tar(&mut buffer, &manifest, 2)?;
523
524 let mut archive = tar::Archive::new(std::io::Cursor::new(buffer));
525
526 for (i, entry) in archive.entries()?.enumerate() {
527 let entry = entry?;
528
529 let path = match i {
530 0 => Path::new("./"),
531 1 => Path::new("./foo/"),
532 2 => Path::new("./foo/bar.txt"),
533 _ => return Err(anyhow!("unexpected archive entry")),
534 };
535
536 assert_eq!(entry.path()?, path, "entry {} path matches", i);
537 }
538
539 Ok(())
540 }
541
542 #[test]
543 fn test_write_data_tar_long_path() -> Result<()> {
544 let long_path = PathBuf::from(format!("f{}.txt", "u".repeat(200)));
545
546 let mut manifest = FileManifest::default();
547
548 manifest.add_file_entry(&long_path, vec![42])?;
549
550 let mut buffer = vec![];
551 write_deb_tar(&mut buffer, &manifest, 2)?;
552
553 let mut archive = tar::Archive::new(std::io::Cursor::new(buffer));
554
555 for (i, entry) in archive.entries()?.enumerate() {
556 let entry = entry?;
557
558 if i != 1 {
559 continue;
560 }
561
562 assert_eq!(
563 entry.path()?,
564 Path::new(&format!("./f{}.txt", "u".repeat(200)))
565 );
566 }
567
568 Ok(())
569 }
570
571 #[test]
572 fn test_write_deb() -> Result<()> {
573 let mut control_para = ControlParagraph::default();
574 control_para.add_field_from_string("Package".into(), "mypackage".into())?;
575 control_para.add_field_from_string("Architecture".into(), "amd64".into())?;
576
577 let mut control = ControlFile::default();
578 control.add_paragraph(control_para);
579
580 let builder = DebBuilder::new(control)
581 .set_compression(DebCompression::Zstandard(3))
582 .install_file("usr/bin/myapp", FileEntry::new_from_data(vec![42], true))?;
583
584 let mut buffer = vec![];
585 builder.write(&mut buffer)?;
586
587 let mut archive = ar::Archive::new(std::io::Cursor::new(buffer));
588 {
589 let entry = archive.next_entry().unwrap().unwrap();
590 assert_eq!(entry.header().identifier(), b"debian-binary");
591 }
592 {
593 let entry = archive.next_entry().unwrap().unwrap();
594 assert_eq!(entry.header().identifier(), b"control.tar.zst");
595 }
596 {
597 let entry = archive.next_entry().unwrap().unwrap();
598 assert_eq!(entry.header().identifier(), b"data.tar.zst");
599 }
600
601 assert!(archive.next_entry().is_none());
602
603 Ok(())
604 }
605}