-
-
Notifications
You must be signed in to change notification settings - Fork 244
Expand file tree
/
Copy pathnormalize.rs
More file actions
153 lines (132 loc) · 5.5 KB
/
normalize.rs
File metadata and controls
153 lines (132 loc) · 5.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
use std::borrow::Cow;
#[cfg(not(windows))]
use std::fs;
use std::fs::File;
use std::io::Write as _;
#[cfg(not(windows))]
use std::os::unix::fs::PermissionsExt as _;
use std::path::{Path, PathBuf};
use crate::constants::VERSION;
use crate::utils::fs::TempFile;
use anyhow::{Context as _, Result};
use itertools::Itertools as _;
use log::debug;
use symbolic::common::ByteView;
use walkdir::WalkDir;
use zip::write::SimpleFileOptions;
use zip::{DateTime, ZipWriter};
fn get_version() -> Cow<'static, str> {
// Integration tests can override the version for consistent test results.
// This ensures deterministic checksums in test fixtures by using a fixed version.
std::env::var("SENTRY_CLI_INTEGRATION_TEST_VERSION_OVERRIDE")
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(VERSION))
}
fn sort_entries(path: &Path) -> Result<impl Iterator<Item = (PathBuf, PathBuf)>> {
Ok(WalkDir::new(path)
.into_iter()
.filter_map(Result::ok)
.filter(|entry| {
let path = entry.path();
// Include both regular files and symlinks
path.is_file() || path.is_symlink()
})
.map(|entry| {
let entry_path = entry.into_path();
let relative_path = entry_path.strip_prefix(path)?.to_owned();
Ok((entry_path, relative_path))
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.sorted_by(|(_, a), (_, b)| a.cmp(b)))
}
fn add_entries_to_zip(
zip: &mut ZipWriter<File>,
entries: impl Iterator<Item = (PathBuf, PathBuf)>,
directory_name: &str,
) -> Result<i32> {
let mut file_count = 0;
// Need to set the last modified time to a fixed value to ensure consistent checksums
// This is important as an optimization to avoid re-uploading the same chunks if they're already on the server
// but the last modified time being different will cause checksums to be different.
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.last_modified_time(DateTime::default());
for (entry_path, relative_path) in entries {
#[cfg(not(windows))]
// On Unix, we need to preserve the file permissions.
let options = options.unix_permissions(fs::metadata(&entry_path)?.permissions().mode());
let zip_path = format!("{directory_name}/{}", relative_path.to_string_lossy());
if entry_path.is_symlink() {
// Handle symlinks by reading the target path and writing it as a symlink
let target = std::fs::read_link(&entry_path)?;
let target_str = target.to_string_lossy();
// Create a symlink entry in the zip
zip.add_symlink(zip_path.as_str(), &target_str, options)
.with_context(|| format!("Failed to add symlink '{zip_path}' to zip archive"))?;
} else {
// Handle regular files
zip.start_file(zip_path.as_str(), options)
.with_context(|| format!("Failed to add file '{zip_path}' to zip archive"))?;
let file_byteview = ByteView::open(&entry_path)?;
zip.write_all(file_byteview.as_slice())?;
}
file_count += 1;
}
Ok(file_count)
}
fn metadata_file_options() -> SimpleFileOptions {
SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.last_modified_time(DateTime::default())
}
pub fn write_version_metadata<W: std::io::Write + std::io::Seek>(
zip: &mut ZipWriter<W>,
plugin_name: Option<&str>,
plugin_version: Option<&str>,
) -> Result<()> {
let version = get_version();
zip.start_file(".sentry-cli-metadata.txt", metadata_file_options())?;
writeln!(zip, "sentry-cli-version: {version}")?;
// Write plugin info if available
if let (Some(name), Some(version)) = (plugin_name, plugin_version) {
writeln!(zip, "{name}: {version}")?;
}
Ok(())
}
// For XCArchive directories, we'll zip the entire directory
// It's important to not change the contents of the directory or the size
// analysis will be wrong and the code signature will break.
pub fn normalize_directory(
path: &Path,
parsed_assets_path: &Path,
plugin_name: Option<&str>,
plugin_version: Option<&str>,
) -> Result<TempFile> {
debug!("Creating normalized zip for directory: {}", path.display());
let temp_file = TempFile::create()?;
let mut zip = ZipWriter::new(temp_file.open()?);
let directory_name = path.file_name().expect("Failed to get basename");
// Collect and sort entries for deterministic ordering
// This is important to ensure stable sha1 checksums for the zip file as
// an optimization is used to avoid re-uploading the same chunks if they're already on the server.
let entries = sort_entries(path)?;
let mut file_count = add_entries_to_zip(&mut zip, entries, &directory_name.to_string_lossy())?;
// Add parsed assets to the zip in a "ParsedAssets" directory
if parsed_assets_path.exists() {
debug!(
"Adding parsed assets from: {}",
parsed_assets_path.display()
);
let parsed_assets_entries = sort_entries(parsed_assets_path)?;
file_count += add_entries_to_zip(
&mut zip,
parsed_assets_entries,
&format!("{}/ParsedAssets", directory_name.to_string_lossy()),
)?;
}
write_version_metadata(&mut zip, plugin_name, plugin_version)?;
zip.finish()?;
debug!("Successfully created normalized zip for directory with {file_count} files");
Ok(temp_file)
}