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
154
155
156
157
158
//! Shared tar-archive helpers for `crate::export` and
//! [`crate::remote_cache`] — the only two crate sites that build
//! tar archives in memory.
//!
//! Both modules previously open-coded the same five-line sequence:
//! `Header::new_gnu` + `set_size` + `set_mode` + `set_cksum` +
//! `append`/`append_data`. [`pack_tar_entry`] consolidates the
//! sequence so call sites express what they want (a named entry
//! with a mode and a payload) rather than how to assemble a tar
//! header.
//!
//! `tar::Builder::append_data` already calls `Header::set_cksum`
//! after rewriting the path, so the helper does NOT call it
//! separately — the previous open-coded `set_cksum()` calls in
//! `remote_cache.rs` were redundant double-cksums that the helper
//! drops.
//!
//! Mode is required (not defaulted) because the two callers use
//! distinct modes deliberately: `export` writes 0o755 so the
//! `.run` extractor preserves executable bits on the embedded
//! `ktstr`/`scheduler` binaries, while `remote_cache` writes
//! 0o644 for cached metadata/image files that are never executed
//! directly. Defaulting either mode would silently regress the
//! other site.
use std::io::{Read, Write};
/// Pack one named entry into a tar archive in memory.
///
/// Builds a fresh GNU header, writes `size` and `mode`, and
/// streams `reader` (which must yield exactly `size` bytes per
/// the tar format contract) under `name`. The path is encoded by
/// `append_data` so callers do NOT need to pre-set it on the
/// header.
///
/// Returns the underlying I/O error from `append_data` on
/// failure — typical causes are a write error against the
/// builder's sink or a `reader` that yields fewer bytes than
/// `size` claims.
pub fn pack_tar_entry<W: Write, R: Read>(
builder: &mut tar::Builder<W>,
name: &str,
mode: u32,
size: u64,
reader: R,
) -> std::io::Result<()> {
let mut header = tar::Header::new_gnu();
header.set_size(size);
header.set_mode(mode);
builder.append_data(&mut header, name, reader)
}
#[cfg(test)]
mod tests {
use super::*;
/// Pack one entry, finish the archive, then unpack and verify
/// every property the helper sets — name, mode, size, and the
/// payload bytes. Pins the helper's contract end-to-end so a
/// future regression that swaps `set_size`/`set_mode` order, or
/// drops one of the calls, surfaces here. Routed through Verdict
/// so failure messages carry the field labels for diagnosis.
#[test]
fn pack_tar_entry_roundtrips_through_archive() {
use crate::assert::Verdict;
const NAME: &str = "ktstr";
const MODE: u32 = 0o755;
let payload: &[u8] = b"hello tar world\n";
// Pack into an in-memory archive.
let mut buf: Vec<u8> = Vec::new();
{
let mut builder = tar::Builder::new(&mut buf);
pack_tar_entry(&mut builder, NAME, MODE, payload.len() as u64, payload)
.expect("pack_tar_entry must succeed for valid inputs");
builder.finish().expect("tar finish");
}
// Unpack and verify each header field plus the payload.
let mut archive = tar::Archive::new(&buf[..]);
let mut entries = archive.entries().expect("read entries");
let mut entry = entries
.next()
.expect("at least one entry")
.expect("entry header readable");
let header = entry.header().clone();
let path_str = header
.path()
.expect("path bytes must decode")
.to_string_lossy()
.to_string();
let mode = header.mode().expect("mode must decode");
let size = header.size().expect("size must decode");
let mut got = Vec::new();
entry.read_to_end(&mut got).expect("payload readable");
let mut v = Verdict::new();
crate::claim!(v, path_str).eq(NAME.to_string());
crate::claim!(v, mode).eq(MODE);
crate::claim!(v, size).eq(payload.len() as u64);
// ClaimBuilder requires T: Display; Vec<u8> doesn't impl
// Display. Compare via hex strings (already a dep) so the
// verdict carries a readable diff on mismatch.
let got_hex = hex::encode(&got);
let want_hex = hex::encode(payload);
crate::claim!(v, got_hex).eq(want_hex);
let r = v.into_result();
assert!(
r.is_pass(),
"tar roundtrip claims must all pass: {:?}",
r.outcomes,
);
// Confirm only one entry was packed.
assert!(
entries.next().is_none(),
"pack_tar_entry must produce exactly one entry per call",
);
}
/// Distinct modes are preserved per call. The module doc says
/// export uses 0o755 and remote_cache uses 0o644 — a regression
/// that defaulted the mode (e.g. dropping `set_mode`) would
/// produce identical modes regardless of the argument.
#[test]
fn pack_tar_entry_preserves_distinct_modes_across_calls() {
use crate::assert::Verdict;
let mut buf: Vec<u8> = Vec::new();
{
let mut builder = tar::Builder::new(&mut buf);
pack_tar_entry(&mut builder, "a", 0o755, 1, &b"x"[..]).unwrap();
pack_tar_entry(&mut builder, "b", 0o644, 1, &b"y"[..]).unwrap();
builder.finish().unwrap();
}
let mut archive = tar::Archive::new(&buf[..]);
let mut iter = archive.entries().unwrap();
let a = iter.next().unwrap().unwrap();
let b = iter.next().unwrap().unwrap();
let a_mode = a.header().mode().unwrap();
let b_mode = b.header().mode().unwrap();
let mut v = Verdict::new();
crate::claim!(v, a_mode).eq(0o755u32);
crate::claim!(v, b_mode).eq(0o644u32);
crate::claim!(v, a_mode).ne(b_mode);
let r = v.into_result();
assert!(
r.is_pass(),
"per-call mode must be preserved distinctly: {:?}",
r.outcomes,
);
}
}