Skip to content

Commit 2d098aa

Browse files
committed
builder: Make add_pax() return an error in GNU mode
Previously, add_pax() accepted calls regardless of ExtensionMode but finish() only emitted PAX records in Pax mode -- silently discarding any PAX data added in Gnu mode. The doc comment incorrectly claimed PAX extensions would be emitted regardless of mode. Return HeaderError::IncompatibleMode instead, so callers get a clear error rather than quietly losing xattrs or other PAX metadata. This is a breaking API change: add_pax() now returns Result<&mut Self> instead of &mut Self. Assisted-by: OpenCode (Claude Opus 4) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 62ee267 commit 2d098aa

3 files changed

Lines changed: 72 additions & 11 deletions

File tree

fuzz/generate_corpus.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,9 @@ fn main() {
272272
.unwrap()
273273
.size(0)
274274
.unwrap();
275-
builder.add_pax("SCHILY.xattr.user.test", b"value123");
275+
builder
276+
.add_pax("SCHILY.xattr.user.test", b"value123")
277+
.unwrap();
276278
let hdr = builder.finish_bytes();
277279
let mut archive = hdr;
278280
archive.extend_from_slice(&EOA);
@@ -292,10 +294,12 @@ fn main() {
292294
.unwrap()
293295
.mtime(9999999999)
294296
.unwrap();
295-
builder.add_pax(
296-
"SCHILY.xattr.security.selinux",
297-
b"system_u:object_r:usr_t:s0",
298-
);
297+
builder
298+
.add_pax(
299+
"SCHILY.xattr.security.selinux",
300+
b"system_u:object_r:usr_t:s0",
301+
)
302+
.unwrap();
299303
let hdr = builder.finish_bytes();
300304
let mut archive = hdr;
301305
archive.extend_from_slice(b"payload");
@@ -512,7 +516,7 @@ fn main() {
512516
.unwrap()
513517
.mtime(1700000000)
514518
.unwrap();
515-
builder.add_pax("mtime", b"1700000000.123456789");
519+
builder.add_pax("mtime", b"1700000000.123456789").unwrap();
516520
let hdr = builder.finish_bytes();
517521
let mut archive = hdr;
518522
archive.extend_from_slice(&EOA);

src/builder.rs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -863,11 +863,23 @@ impl EntryBuilder {
863863
/// Add a custom PAX extension record.
864864
///
865865
/// This is useful for adding metadata that doesn't fit in standard
866-
/// header fields. The PAX extension will be emitted regardless of
867-
/// the extension mode setting.
868-
pub fn add_pax(&mut self, key: &str, value: impl AsRef<[u8]>) -> &mut Self {
866+
/// header fields (e.g. extended attributes, high-precision timestamps).
867+
///
868+
/// # Errors
869+
///
870+
/// Returns [`HeaderError::IncompatibleMode`] if the extension mode is
871+
/// [`ExtensionMode::Gnu`]. PAX records are only emitted in PAX mode;
872+
/// call [`Self::set_extension_mode`] first or use [`Self::new_ustar`]
873+
/// to create a PAX-mode builder.
874+
pub fn add_pax(&mut self, key: &str, value: impl AsRef<[u8]>) -> Result<&mut Self> {
875+
if self.mode != ExtensionMode::Pax {
876+
return Err(HeaderError::IncompatibleMode {
877+
required: ExtensionMode::Pax,
878+
current: self.mode,
879+
});
880+
}
869881
self.pax_mut().add(key, value);
870-
self
882+
Ok(self)
871883
}
872884

873885
/// Get or create the PAX builder for this entry.
@@ -1861,6 +1873,7 @@ mod tests {
18611873
.size(0)
18621874
.unwrap()
18631875
.add_pax("SCHILY.xattr.user.test", b"value")
1876+
.unwrap()
18641877
.entry_type(EntryType::Regular);
18651878

18661879
assert!(builder.needs_extension()); // Due to custom PAX
@@ -1876,6 +1889,40 @@ mod tests {
18761889
assert!(pax_str.contains("SCHILY.xattr.user.test=value"));
18771890
}
18781891

1892+
#[test]
1893+
fn test_add_pax_rejects_gnu_mode() {
1894+
let mut builder = EntryBuilder::new_gnu();
1895+
let result = builder.add_pax("SCHILY.xattr.user.test", b"value");
1896+
assert!(result.is_err());
1897+
let err = result.unwrap_err();
1898+
assert!(
1899+
err.to_string().contains("Pax"),
1900+
"error should mention Pax mode: {err}"
1901+
);
1902+
}
1903+
1904+
#[test]
1905+
fn test_add_pax_after_mode_switch() {
1906+
// Switching from GNU to PAX mode should allow add_pax().
1907+
let mut builder = EntryBuilder::new_gnu();
1908+
assert!(builder.add_pax("key", b"val").is_err());
1909+
1910+
builder.set_extension_mode(ExtensionMode::Pax);
1911+
builder
1912+
.path(b"file.txt")
1913+
.mode(0o644)
1914+
.unwrap()
1915+
.size(0)
1916+
.unwrap()
1917+
.add_pax("SCHILY.xattr.user.test", b"value")
1918+
.unwrap();
1919+
1920+
let blocks = builder.finish();
1921+
// Should have PAX 'x' header + PAX data + main header
1922+
assert!(blocks.len() >= 3);
1923+
assert_eq!(blocks[0].entry_type(), EntryType::XHeader);
1924+
}
1925+
18791926
#[test]
18801927
fn test_entry_builder_extension_mode_switching() {
18811928
let long_path = "x/".repeat(60);

src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ pub enum HeaderError {
9494
detail: String,
9595
},
9696

97+
/// Operation requires a different extension mode.
98+
#[error("operation requires {required:?} extension mode, but builder is in {current:?} mode")]
99+
IncompatibleMode {
100+
/// The mode the operation requires.
101+
required: crate::builder::ExtensionMode,
102+
/// The mode the builder is currently in.
103+
current: crate::builder::ExtensionMode,
104+
},
105+
97106
/// The header checksum does not match the computed value.
98107
#[error("checksum mismatch: expected {expected}, computed {computed}")]
99108
ChecksumMismatch {
@@ -4103,7 +4112,8 @@ mod tests {
41034112
.mtime(params.mtime)
41044113
.unwrap()
41054114
.entry_type(EntryType::Regular)
4106-
.add_pax(&params.xattr_key, params.xattr_value.as_bytes());
4115+
.add_pax(&params.xattr_key, params.xattr_value.as_bytes())
4116+
.unwrap();
41074117

41084118
builder.finish()
41094119
}

0 commit comments

Comments
 (0)