Skip to content

Commit 09ab514

Browse files
committed
rechunker: Duplicate xattrs when more than 256 links
This is to avoid creating (10s of) thousands of links to a single file which will cause the rechunker to fail after passing the filesystem's link limit. This will create a duplicate xattrs file to link to for each chunk of 256 links. Assisted-by: Claude Code Signed-off-by: ckyrouac <[email protected]>
1 parent df2da1a commit 09ab514

File tree

1 file changed

+132
-9
lines changed

1 file changed

+132
-9
lines changed

crates/ostree-ext/src/tar/export.rs

Lines changed: 132 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ struct OstreeTarWriter<'a, W: std::io::Write> {
126126
wrote_dirmeta: HashSet<String>,
127127
wrote_content: HashSet<String>,
128128
wrote_xattrs: HashSet<String>,
129+
/// Tracks the number of links to each xattrs object (checksum -> (link count, suffix))
130+
xattrs_link_counts: std::collections::HashMap<String, (u32, u32)>,
129131
}
130132

131133
pub(crate) fn object_path(objtype: ostree::ObjectType, checksum: &str) -> Utf8PathBuf {
@@ -141,9 +143,13 @@ pub(crate) fn object_path(objtype: ostree::ObjectType, checksum: &str) -> Utf8Pa
141143
format!("{OSTREEDIR}/repo/objects/{first}/{rest}.{suffix}").into()
142144
}
143145

144-
fn v1_xattrs_object_path(checksum: &str) -> Utf8PathBuf {
146+
fn v1_xattrs_object_path_with_suffix(checksum: &str, suffix: u32) -> Utf8PathBuf {
145147
let (first, rest) = checksum.split_at(2);
146-
format!("{OSTREEDIR}/repo/objects/{first}/{rest}.file-xattrs").into()
148+
if suffix == 0 {
149+
format!("{OSTREEDIR}/repo/objects/{first}/{rest}.file-xattrs").into()
150+
} else {
151+
format!("{OSTREEDIR}/repo/objects/{first}/{rest}.file-xattrs.{suffix}").into()
152+
}
147153
}
148154

149155
fn v1_xattrs_link_object_path(checksum: &str) -> Utf8PathBuf {
@@ -197,6 +203,7 @@ impl<'a, W: std::io::Write> OstreeTarWriter<'a, W> {
197203
wrote_dirtree: HashSet::new(),
198204
wrote_content: HashSet::new(),
199205
wrote_xattrs: HashSet::new(),
206+
xattrs_link_counts: std::collections::HashMap::new(),
200207
};
201208
Ok(r)
202209
}
@@ -383,6 +390,8 @@ impl<'a, W: std::io::Write> OstreeTarWriter<'a, W> {
383390
/// Return whether content was written.
384391
#[context("Writing xattrs")]
385392
fn append_ostree_xattrs(&mut self, checksum: &str, xattrs: &glib::Variant) -> Result<bool> {
393+
const XATTRS_LINK_LIMIT: u32 = 256;
394+
386395
let xattrs_data = xattrs.data_as_bytes();
387396
let xattrs_data = xattrs_data.as_ref();
388397

@@ -391,13 +400,40 @@ impl<'a, W: std::io::Write> OstreeTarWriter<'a, W> {
391400
hex::encode(digest)
392401
};
393402

394-
let path = v1_xattrs_object_path(&xattrs_checksum);
395-
// Write xattrs content into a separate `.file-xattrs` object.
396-
if !self.wrote_xattrs.contains(&xattrs_checksum) {
397-
let inserted = self.wrote_xattrs.insert(xattrs_checksum);
398-
debug_assert!(inserted);
399-
self.append_default_data(&path, xattrs_data)?;
403+
// Check if we've already written this xattrs object or need to create a new one
404+
let (link_count, suffix) = self
405+
.xattrs_link_counts
406+
.get(&xattrs_checksum)
407+
.copied()
408+
.unwrap_or((0, 0));
409+
410+
// Determine which suffix to use based on link count
411+
let (current_suffix, should_create_new) = if link_count >= XATTRS_LINK_LIMIT {
412+
(suffix + 1, true)
413+
} else {
414+
(suffix, link_count == 0)
415+
};
416+
417+
let path = v1_xattrs_object_path_with_suffix(&xattrs_checksum, current_suffix);
418+
419+
// Write xattrs content into a separate `.file-xattrs` object if needed
420+
if should_create_new {
421+
// For the first object (suffix 0) or when we need a new suffixed object
422+
let key = format!("{}.{}", xattrs_checksum, current_suffix);
423+
if !self.wrote_xattrs.contains(&key) {
424+
let inserted = self.wrote_xattrs.insert(key);
425+
debug_assert!(inserted);
426+
self.append_default_data(&path, xattrs_data)?;
427+
// Reset link count for new suffix
428+
self.xattrs_link_counts
429+
.insert(xattrs_checksum.clone(), (1, current_suffix));
430+
}
431+
} else {
432+
// Increment link count for existing xattrs object
433+
self.xattrs_link_counts
434+
.insert(xattrs_checksum.clone(), (link_count + 1, current_suffix));
400435
}
436+
401437
// Write a `.file-xattrs-link` which links the file object to
402438
// the corresponding detached xattrs.
403439
{
@@ -936,8 +972,13 @@ mod tests {
936972
fn test_v1_xattrs_object_path() {
937973
let checksum = "b8627e3ef0f255a322d2bd9610cfaaacc8f122b7f8d17c0e7e3caafa160f9fc7";
938974
let expected = "sysroot/ostree/repo/objects/b8/627e3ef0f255a322d2bd9610cfaaacc8f122b7f8d17c0e7e3caafa160f9fc7.file-xattrs";
939-
let output = v1_xattrs_object_path(checksum);
975+
let output = v1_xattrs_object_path_with_suffix(checksum, 0);
940976
assert_eq!(&output, expected);
977+
978+
// Test with suffix
979+
let expected_with_suffix = "sysroot/ostree/repo/objects/b8/627e3ef0f255a322d2bd9610cfaaacc8f122b7f8d17c0e7e3caafa160f9fc7.file-xattrs.1";
980+
let output_with_suffix = v1_xattrs_object_path_with_suffix(checksum, 1);
981+
assert_eq!(&output_with_suffix, expected_with_suffix);
941982
}
942983

943984
#[test]
@@ -947,4 +988,86 @@ mod tests {
947988
let output = v1_xattrs_link_object_path(checksum);
948989
assert_eq!(&output, expected);
949990
}
991+
992+
#[test]
993+
fn test_append_ostree_xattrs() {
994+
// Create a temporary in-memory tar builder
995+
let mut tar_data = Vec::new();
996+
let mut tar_builder = tar::Builder::new(&mut tar_data);
997+
998+
// Create a minimal OstreeTarWriter for testing
999+
// We bypass the complex commit creation and just create the struct directly
1000+
let tmp_dir = tempfile::tempdir().unwrap();
1001+
let repo_path = tmp_dir.path().join("repo");
1002+
let repo = ostree::Repo::new_for_path(&repo_path);
1003+
repo.create(ostree::RepoMode::Archive, gio::Cancellable::NONE)
1004+
.unwrap();
1005+
1006+
// Create a dummy commit - we just need something valid
1007+
let empty_variant = glib::Variant::from(());
1008+
1009+
// Manually create an OstreeTarWriter instance
1010+
let mut writer = OstreeTarWriter {
1011+
repo: &repo,
1012+
commit_checksum: "dummy",
1013+
commit_object: empty_variant,
1014+
out: &mut tar_builder,
1015+
options: ExportOptions::default(),
1016+
wrote_initdirs: false,
1017+
structure_only: false,
1018+
wrote_vartmp: false,
1019+
wrote_dirtree: HashSet::new(),
1020+
wrote_dirmeta: HashSet::new(),
1021+
wrote_content: HashSet::new(),
1022+
wrote_xattrs: HashSet::new(),
1023+
xattrs_link_counts: std::collections::HashMap::new(),
1024+
};
1025+
1026+
// Create xattrs in the expected format for ostree (a(ayay))
1027+
// Format: array of (name bytes with null terminator, value bytes)
1028+
let xattrs_data: Vec<(Vec<u8>, Vec<u8>)> =
1029+
vec![(b"user.test\0".to_vec(), b"test_value".to_vec())];
1030+
let xattrs_variant = glib::Variant::from(xattrs_data);
1031+
1032+
// Test append_ostree_xattrs with a test checksum
1033+
let test_checksum = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
1034+
let result = writer
1035+
.append_ostree_xattrs(test_checksum, &xattrs_variant)
1036+
.unwrap();
1037+
assert!(result);
1038+
1039+
// Verify that xattrs were written
1040+
let xattrs_checksum = {
1041+
let xattrs_data = xattrs_variant.data_as_bytes();
1042+
let digest =
1043+
openssl::hash::hash(openssl::hash::MessageDigest::sha256(), xattrs_data.as_ref())
1044+
.unwrap();
1045+
hex::encode(digest)
1046+
};
1047+
assert!(writer
1048+
.wrote_xattrs
1049+
.contains(&format!("{}.0", xattrs_checksum)));
1050+
1051+
// Test with multiple calls to check link count behavior
1052+
for i in 1..300 {
1053+
let checksum = format!("test{:064}", i);
1054+
writer
1055+
.append_ostree_xattrs(&checksum, &xattrs_variant)
1056+
.unwrap();
1057+
}
1058+
1059+
// Verify that multiple xattrs objects were created when limit was exceeded
1060+
// Should have created suffix 0 and suffix 1 objects
1061+
assert!(writer
1062+
.wrote_xattrs
1063+
.contains(&format!("{}.0", xattrs_checksum)));
1064+
assert!(writer
1065+
.wrote_xattrs
1066+
.contains(&format!("{}.1", xattrs_checksum)));
1067+
1068+
// Verify the link counts
1069+
let (link_count, suffix) = writer.xattrs_link_counts.get(&xattrs_checksum).unwrap();
1070+
assert_eq!(*suffix, 1);
1071+
assert!(*link_count == 44);
1072+
}
9501073
}

0 commit comments

Comments
 (0)