add_determinism/add_det/handlers/
zip.rs1use anyhow::{bail, Result};
4use log::{debug, warn};
5use std::fs::File;
6use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
7use std::path::Path;
8use std::sync::Arc;
9use time;
10
11use super::{config, InputOutputHelper};
12
13const FILE_HEADER_MAGIC: [u8; 4] = [0x50, 0x4b, 0x03, 0x04];
14const CENTRAL_HEADER_FILE_MAGIC: [u8; 4] = [0x50, 0x4b, 0x01, 0x02];
15
16pub struct Zip {
17 extension: &'static str,
20
21 config: Arc<config::Config>,
22 unix_epoch: Option<time::OffsetDateTime>,
23 dos_epoch: Option<zip::DateTime>,
24}
25
26impl Zip {
27 fn boxed(config: &Arc<config::Config>, extension: &'static str)
28 -> Box<dyn super::Processor + Send + Sync>
29 {
30 Box::new(Self {
31 extension,
32 config: config.clone(),
33 unix_epoch: None,
34 dos_epoch: None,
35 })
36 }
37
38 pub fn boxed_zip(config: &Arc<config::Config>) -> Box<dyn super::Processor + Send + Sync> {
39 Self::boxed(config, "zip")
40 }
41
42 pub fn boxed_jar(config: &Arc<config::Config>) -> Box<dyn super::Processor + Send + Sync> {
43 Self::boxed(config, "jar")
44 }
45}
46
47impl super::Processor for Zip {
48 fn name(&self) -> &str {
49 self.extension
50 }
51
52 fn initialize(&mut self) -> Result<()> {
53 let unix_epoch = match self.config.source_date_epoch {
54 None => bail!("{} handler requires $SOURCE_DATE_EPOCH to be set", self.extension),
55 Some(v) => time::OffsetDateTime::from_unix_timestamp(v).unwrap(),
56 };
57 let dos_epoch = zip::DateTime::try_from(unix_epoch)?;
58
59 self.unix_epoch = Some(unix_epoch);
60 self.dos_epoch = Some(dos_epoch);
61 Ok(())
62 }
63
64 fn filter(&self, path: &Path) -> Result<bool> {
65 Ok(self.config.ignore_extension ||
66 path.extension().is_some_and(|x| x == self.extension))
67 }
68
69 fn process(&self, input_path: &Path) -> Result<super::ProcessResult> {
70 let mut have_mod = false;
71 let (mut io, input) = InputOutputHelper::open(input_path, self.config.check, true)?;
72 let mut input = zip::ZipArchive::new(input)?;
73
74 io.open_output(true)?;
75
76 let output = BufWriter::new(io.output.as_ref().unwrap().as_file());
77 let mut output = zip::ZipWriter::new(output);
78
79 for i in 0..input.len() {
80 let file = input.by_index(i)?;
81 output.raw_copy_file(file)?;
82 }
83
84 output.finish()?;
85 drop(output);
86
87 if let Some(dos_epoch) = self.dos_epoch {
88 let ts: [u8; 4] = [
89 (dos_epoch.timepart() & 0xFF).try_into().unwrap(),
90 (dos_epoch.timepart() >> 8).try_into().unwrap(),
91 (dos_epoch.datepart() & 0xFF).try_into().unwrap(),
92 (dos_epoch.datepart() >> 8).try_into().unwrap(),
93 ];
94
95 debug!("Epoch converted to zip::DateTime: {dos_epoch:?}");
96 debug!("Epoch as buffer: {ts:?}");
97
98 let output_path = io.output.as_ref().unwrap().path().to_path_buf();
100 let mut output =
101 zip::ZipArchive::new(BufReader::new(File::open(&output_path)?))?;
102
103 let overwrite = io.output.as_mut().unwrap().as_file_mut();
104
105 for i in 0..output.len() {
106 let file = output.by_index(i)?;
107
108 match file.last_modified().to_time() {
109 Err(e) => {
110 warn!("{}: component {}: {}",
111 input_path.display(),
112 file.name(),
113 e);
114 }
115 Ok(mtime) => {
116 debug!("File {}: {}\n {:?} {:?} {}", i, file.name(), mtime, self.unix_epoch,
117 mtime > self.unix_epoch.unwrap());
118
119 if mtime > self.unix_epoch.unwrap() {
120 let header_offset = file.header_start();
121
122 debug!("{}: {}: seeking to 0x{:08x} (local file header)",
123 output_path.display(),
124 file.name(),
125 header_offset);
126
127 overwrite.seek(SeekFrom::Start(header_offset))?;
128 let mut buf = [0; 10];
129 overwrite.read_exact(&mut buf)?;
130 assert_eq!(buf[..4], FILE_HEADER_MAGIC);
131
132 overwrite.write_all(&ts)?;
134
135 let header_offset = file.central_header_start();
136
137 debug!("{}: {}: seeking to 0x{:08x} (central file header)",
138 output_path.display(),
139 file.name(),
140 header_offset);
141
142 overwrite.seek(SeekFrom::Start(header_offset))?;
143 let mut buf = [0; 12];
144 overwrite.read_exact(&mut buf)?;
145 assert_eq!(buf[..4], CENTRAL_HEADER_FILE_MAGIC);
146
147 overwrite.write_all(&ts)?;
149
150 have_mod = true;
151 }
152 }
153 }
154 }
155 }
156
157 if !have_mod &&
158 self.unix_epoch.is_some() &&
159 io.input_metadata.modified()? > self.unix_epoch.unwrap() {
160 have_mod = io.output.as_mut().unwrap().as_file_mut().metadata()?.len() != io.input_metadata.len();
171 }
172
173 io.finalize(have_mod)
174 }
175}