#![cfg(target_os = "windows")]
use std::io::{self, Read, Write};
use std::path::Path;
use base64::Engine as _;
pub(crate) const BACKUP_DATA: u32 = 0x0000_0001;
pub(crate) const BACKUP_EA_DATA: u32 = 0x0000_0002;
pub(crate) const BACKUP_SECURITY_DATA: u32 = 0x0000_0003;
pub(crate) const BACKUP_REPARSE_DATA: u32 = 0x0000_0008;
pub(crate) fn write_stream_header(
writer: &mut impl Write,
stream_id: u32,
attributes: u32,
size: u64,
) -> io::Result<()> {
writer.write_all(&stream_id.to_le_bytes())?;
writer.write_all(&attributes.to_le_bytes())?;
writer.write_all(&size.to_le_bytes())?;
writer.write_all(&0u32.to_le_bytes())?; Ok(())
}
const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x0000_0020;
#[derive(Default)]
struct PaxMetadata {
security_descriptor: Option<Vec<u8>>,
reparse_data: Option<Vec<u8>>,
extended_attributes: Option<Vec<u8>>,
file_attributes: Option<u32>,
}
fn collect_pax_metadata<R: Read>(entry: &mut tar::Entry<'_, R>) -> io::Result<PaxMetadata> {
let mut out = PaxMetadata::default();
let Some(pax) = entry.pax_extensions()? else {
return Ok(out);
};
let engine = base64::engine::general_purpose::STANDARD;
for ext in pax {
let ext = ext?;
let key = ext.key().unwrap_or("");
let val = ext.value_bytes();
match key {
"MSWINDOWS.rawsd" => {
out.security_descriptor = Some(engine.decode(val).map_err(|e| {
io::Error::other(format!("PAX MSWINDOWS.rawsd base64 decode: {e}"))
})?);
}
"MSWINDOWS.reparse" => {
out.reparse_data = Some(engine.decode(val).map_err(|e| {
io::Error::other(format!("PAX MSWINDOWS.reparse base64 decode: {e}"))
})?);
}
"MSWINDOWS.eas" => {
out.extended_attributes = Some(engine.decode(val).map_err(|e| {
io::Error::other(format!("PAX MSWINDOWS.eas base64 decode: {e}"))
})?);
}
"MSWINDOWS.fileattr" => {
let s = std::str::from_utf8(val)
.map_err(|e| io::Error::other(format!("PAX MSWINDOWS.fileattr utf8: {e}")))?;
let n = s
.trim()
.parse::<u32>()
.map_err(|e| io::Error::other(format!("PAX MSWINDOWS.fileattr parse: {e}")))?;
out.file_attributes = Some(n);
}
_ => {}
}
}
Ok(out)
}
pub fn write_oci_entry_to_backup_stream<R: Read>(
entry: &mut tar::Entry<'_, R>,
dest: &Path,
) -> io::Result<()> {
let pax = collect_pax_metadata(entry)?;
let body_size = entry.size();
let mut writer = crate::windows::layer::create_long_path_file(dest)?;
let attrs = pax.file_attributes.unwrap_or(FILE_ATTRIBUTE_ARCHIVE);
writer.write_all(&attrs.to_le_bytes())?;
if let Some(sd) = pax.security_descriptor {
write_stream_header(&mut writer, BACKUP_SECURITY_DATA, 0, sd.len() as u64)?;
writer.write_all(&sd)?;
}
if let Some(rp) = pax.reparse_data {
write_stream_header(&mut writer, BACKUP_REPARSE_DATA, 0, rp.len() as u64)?;
writer.write_all(&rp)?;
}
if let Some(eas) = pax.extended_attributes {
write_stream_header(&mut writer, BACKUP_EA_DATA, 0, eas.len() as u64)?;
writer.write_all(&eas)?;
}
if body_size > 0 {
write_stream_header(&mut writer, BACKUP_DATA, 0, body_size)?;
io::copy(entry, &mut writer)?;
}
Ok(())
}
pub fn write_oci_entry_as_base_layer<R: Read>(
entry: &mut tar::Entry<'_, R>,
dest: &Path,
) -> io::Result<()> {
let pax = collect_pax_metadata(entry)?;
let body_size = entry.size();
let mut writer = crate::windows::layer::BackupStreamWriter::create_new(dest)?;
if let Some(sd) = pax.security_descriptor {
write_stream_header(&mut writer, BACKUP_SECURITY_DATA, 0, sd.len() as u64)?;
writer.write_all(&sd)?;
}
if let Some(rp) = pax.reparse_data {
write_stream_header(&mut writer, BACKUP_REPARSE_DATA, 0, rp.len() as u64)?;
writer.write_all(&rp)?;
}
if let Some(eas) = pax.extended_attributes {
write_stream_header(&mut writer, BACKUP_EA_DATA, 0, eas.len() as u64)?;
writer.write_all(&eas)?;
}
if body_size > 0 {
write_stream_header(&mut writer, BACKUP_DATA, 0, body_size)?;
io::copy(entry, &mut writer)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
struct Capture {
buf: Vec<u8>,
}
impl Write for Capture {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buf.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn header_layout_is_20_bytes_le_with_zero_name_size() {
let mut cap = Capture { buf: Vec::new() };
write_stream_header(&mut cap, BACKUP_DATA, 0, 0xAA_BB_CC_DD).unwrap();
assert_eq!(cap.buf.len(), 20, "WIN32_STREAM_ID prefix is 20 bytes");
assert_eq!(&cap.buf[0..4], &1u32.to_le_bytes());
assert_eq!(&cap.buf[4..8], &0u32.to_le_bytes());
assert_eq!(&cap.buf[8..16], &0xAA_BB_CC_DDu64.to_le_bytes());
assert_eq!(&cap.buf[16..20], &0u32.to_le_bytes());
}
#[test]
fn header_encodes_security_stream_id() {
let mut cap = Capture { buf: Vec::new() };
write_stream_header(&mut cap, BACKUP_SECURITY_DATA, 0, 16).unwrap();
assert_eq!(&cap.buf[0..4], &3u32.to_le_bytes());
assert_eq!(&cap.buf[8..16], &16u64.to_le_bytes());
}
#[test]
fn header_encodes_reparse_stream_id() {
let mut cap = Capture { buf: Vec::new() };
write_stream_header(&mut cap, BACKUP_REPARSE_DATA, 0, 24).unwrap();
assert_eq!(&cap.buf[0..4], &8u32.to_le_bytes());
assert_eq!(&cap.buf[8..16], &24u64.to_le_bytes());
}
#[test]
fn header_encodes_ea_stream_id() {
let mut cap = Capture { buf: Vec::new() };
write_stream_header(&mut cap, BACKUP_EA_DATA, 0, 7).unwrap();
assert_eq!(&cap.buf[0..4], &2u32.to_le_bytes());
assert_eq!(&cap.buf[8..16], &7u64.to_le_bytes());
}
#[test]
fn write_oci_entry_writes_attr_header_then_body_for_minimal_file() {
let mut tar_bytes: Vec<u8> = Vec::new();
{
let mut builder = tar::Builder::new(&mut tar_bytes);
builder
.append_pax_extensions([("MSWINDOWS.fileattr", b"32".as_slice())])
.unwrap();
let body: &[u8] = b"hi";
let mut header = tar::Header::new_ustar();
header.set_path("Files/test.txt").unwrap();
header.set_size(body.len() as u64);
header.set_entry_type(tar::EntryType::Regular);
header.set_mode(0o644);
header.set_cksum();
builder.append(&header, body).unwrap();
builder.finish().unwrap();
}
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("test.txt");
{
let mut archive = tar::Archive::new(io::Cursor::new(&tar_bytes));
let mut entries = archive.entries().unwrap();
let mut entry = loop {
let e = entries
.next()
.expect("expected a regular file entry after the PAX header")
.unwrap();
if e.header().entry_type() == tar::EntryType::Regular {
break e;
}
};
write_oci_entry_to_backup_stream(&mut entry, &dest).unwrap();
}
let out = std::fs::read(&dest).unwrap();
assert_eq!(
out.len(),
4 + 20 + 2,
"attr(4) + WIN32_STREAM_ID(20) + body(2)"
);
assert_eq!(&out[0..4], &[0x20, 0x00, 0x00, 0x00]);
assert_eq!(&out[4..8], &1u32.to_le_bytes());
assert_eq!(&out[8..12], &0u32.to_le_bytes());
assert_eq!(&out[12..20], &2u64.to_le_bytes());
assert_eq!(&out[20..24], &0u32.to_le_bytes());
assert_eq!(&out[24..26], b"hi");
}
}