add_determinism/add_det/handlers/
zip.rs

1/* SPDX-License-Identifier: GPL-3.0-or-later */
2
3use 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    // Share the implementation for .zip and .jar, but define two
18    // separate handlers which can be enabled independently.
19    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            // Open output again to adjust timestamps
99            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                            // We write at offset header_start + 10
133                            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                            // We write at offset header_start + 12
148                            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                // The file's modification timestamp indicates that it
161                // was created during the build. This means that it
162                // most likely contains uid and gid information that
163                // reflects the build environment. This will happen
164                // even for files which were copied from build sources
165                // and have mtime < $SOURCE_DATE_EPOCH. Our rewriting
166                // of the zip file would drop this metadata. Let's
167                // check if the rewritten file has different size,
168                // which indicates that we dropped some metadata.
169
170                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}