Skip to content

Commit 0a89b3d

Browse files
authored
Fix: symlinks in normalized upload (#2744)
Sometimes uploads have symlinks in them, we need to preserve the contents of the upload, otherwise the code signature breaks. Additionally, the app size would change if we changed the contents, making our size analysis invalid Closes [EME-266](https://linear.app/getsentry/issue/EME-266/sentry-cli-not-handling-symlinks-properly-when-zipping)
1 parent 7518392 commit 0a89b3d

File tree

2 files changed

+68
-5
lines changed

2 files changed

+68
-5
lines changed

src/commands/build/upload.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@ fn upload_file(
570570
mod tests {
571571
use super::*;
572572
use std::fs;
573+
use std::os::unix::fs::symlink;
573574
use zip::ZipArchive;
574575

575576
#[test]
@@ -651,4 +652,51 @@ mod tests {
651652
);
652653
Ok(())
653654
}
655+
656+
#[test]
657+
fn test_normalize_directory_preserves_symlinks() -> Result<()> {
658+
let temp_dir = crate::utils::fs::TempDir::create()?;
659+
let test_dir = temp_dir.path().join("TestApp.xcarchive");
660+
fs::create_dir_all(test_dir.join("Products"))?;
661+
662+
// Create a regular file
663+
fs::write(test_dir.join("Products").join("app.txt"), "test content")?;
664+
665+
// Create a symlink pointing to the regular file
666+
let symlink_path = test_dir.join("Products").join("app_link.txt");
667+
symlink("app.txt", &symlink_path)?;
668+
669+
let result_zip = normalize_directory(&test_dir, temp_dir.path())?;
670+
let zip_file = fs::File::open(result_zip.path())?;
671+
let mut archive = ZipArchive::new(zip_file)?;
672+
673+
// Check that both the regular file and symlink are in the zip
674+
let mut has_regular_file = false;
675+
let mut has_symlink = false;
676+
677+
for i in 0..archive.len() {
678+
let file = archive.by_index(i)?;
679+
let file_name = file.name();
680+
681+
if file_name == "TestApp.xcarchive/Products/app.txt" {
682+
has_regular_file = true;
683+
// Verify it's actually a regular file, not a symlink
684+
assert!(
685+
!file.is_symlink(),
686+
"app.txt should be a regular file, not a symlink"
687+
);
688+
} else if file_name == "TestApp.xcarchive/Products/app_link.txt" {
689+
has_symlink = true;
690+
// Verify it's actually a symlink
691+
assert!(
692+
file.is_symlink(),
693+
"app_link.txt should be a symlink in the zip"
694+
);
695+
}
696+
}
697+
698+
assert!(has_regular_file, "Regular file should be in zip");
699+
assert!(has_symlink, "Symlink should be preserved in zip");
700+
Ok(())
701+
}
654702
}

src/utils/build/normalize.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ use zip::{DateTime, ZipWriter};
1717

1818
fn sort_entries(path: &Path) -> Result<impl Iterator<Item = (PathBuf, PathBuf)>> {
1919
Ok(WalkDir::new(path)
20-
.follow_links(true)
2120
.into_iter()
2221
.filter_map(Result::ok)
23-
.filter(|entry| entry.path().is_file())
22+
.filter(|entry| {
23+
let path = entry.path();
24+
// Include both regular files and symlinks
25+
path.is_file() || path.is_symlink()
26+
})
2427
.map(|entry| {
2528
let entry_path = entry.into_path();
2629
let relative_path = entry_path.strip_prefix(path)?.to_owned();
@@ -52,16 +55,28 @@ fn add_entries_to_zip(
5255

5356
let zip_path = format!("{directory_name}/{}", relative_path.to_string_lossy());
5457

55-
zip.start_file(zip_path, options)?;
56-
let file_byteview = ByteView::open(&entry_path)?;
57-
zip.write_all(file_byteview.as_slice())?;
58+
if entry_path.is_symlink() {
59+
// Handle symlinks by reading the target path and writing it as a symlink
60+
let target = std::fs::read_link(&entry_path)?;
61+
let target_str = target.to_string_lossy();
62+
63+
// Create a symlink entry in the zip
64+
zip.add_symlink(zip_path, &target_str, options)?;
65+
} else {
66+
// Handle regular files
67+
zip.start_file(zip_path, options)?;
68+
let file_byteview = ByteView::open(&entry_path)?;
69+
zip.write_all(file_byteview.as_slice())?;
70+
}
5871
file_count += 1;
5972
}
6073

6174
Ok(file_count)
6275
}
6376

6477
// For XCArchive directories, we'll zip the entire directory
78+
// It's important to not change the contents of the directory or the size
79+
// analysis will be wrong and the code signature will break.
6580
pub fn normalize_directory(path: &Path, parsed_assets_path: &Path) -> Result<TempFile> {
6681
debug!("Creating normalized zip for directory: {}", path.display());
6782

0 commit comments

Comments
 (0)