use crate::Result;
use crate::block::BlockDevice;
use super::bmbt::{BmbtLayout, Extent};
pub const XFS_SYMLINK_MAGIC: u32 = 0x5853_4C4D;
pub const XFS_SYMLINK_HDR_SIZE: usize = 56;
pub fn decode_local(lit: &[u8], size: u64) -> Result<String> {
let n = size as usize;
if n > lit.len() {
return Err(crate::Error::InvalidImage(format!(
"xfs: local symlink size {n} exceeds literal area {}",
lit.len()
)));
}
let bytes = &lit[..n];
let s = std::str::from_utf8(bytes)
.map_err(|_| crate::Error::InvalidImage("xfs: non-UTF-8 inline symlink target".into()))?
.to_string();
Ok(s)
}
pub fn decode_remote(
dev: &mut dyn BlockDevice,
layout: &BmbtLayout,
extents: &[Extent],
size: u64,
) -> Result<String> {
if size == 0 {
return Ok(String::new());
}
let bs = layout.blocksize as u64;
let agblklog = layout.agblklog as u32;
let agblocks = layout.agblocks as u64;
let mut out = Vec::with_capacity(size as usize);
let mut remaining = size as usize;
for ext in extents {
if remaining == 0 {
break;
}
if ext.unwritten {
return Err(crate::Error::Unsupported(
"xfs: unwritten extents in symlink target".into(),
));
}
for blkidx in 0..ext.blockcount as u64 {
if remaining == 0 {
break;
}
let fsb = ext.startblock + blkidx;
let ag = fsb >> agblklog;
let agblk = fsb & ((1u64 << agblklog) - 1);
let byte_off = ag * agblocks * bs + agblk * bs;
let mut block = vec![0u8; bs as usize];
dev.read_at(byte_off, &mut block)?;
let payload: &[u8] = if layout.is_v5 {
if block.len() < XFS_SYMLINK_HDR_SIZE {
return Err(crate::Error::InvalidImage(
"xfs: v5 symlink block shorter than header".into(),
));
}
let magic = u32::from_be_bytes(block[0..4].try_into().unwrap());
if magic != XFS_SYMLINK_MAGIC {
return Err(crate::Error::InvalidImage(format!(
"xfs: v5 symlink block magic {magic:#010x}, want {XFS_SYMLINK_MAGIC:#010x}"
)));
}
&block[XFS_SYMLINK_HDR_SIZE..]
} else {
&block[..]
};
let take = remaining.min(payload.len());
out.extend_from_slice(&payload[..take]);
remaining -= take;
}
}
if remaining != 0 {
return Err(crate::Error::InvalidImage(format!(
"xfs: symlink target ran short by {remaining} bytes"
)));
}
let s = String::from_utf8(out)
.map_err(|_| crate::Error::InvalidImage("xfs: non-UTF-8 remote symlink target".into()))?;
Ok(s)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
#[test]
fn local_decode_basic() {
let lit = b"/etc/hostname\0\0\0\0";
let s = decode_local(lit, 13).unwrap();
assert_eq!(s, "/etc/hostname");
}
#[test]
fn local_decode_oversize_errors() {
let lit = b"abc";
assert!(decode_local(lit, 100).is_err());
}
#[test]
fn remote_v5_round_trip() {
let layout = BmbtLayout {
blocksize: 4096,
agblocks: 256,
agblklog: 8,
is_v5: true,
};
let total = 256u64 * 4096 * 2;
let mut dev = MemoryBackend::new(total);
let target = "/usr/lib/foo";
let mut block = vec![0u8; 4096];
block[0..4].copy_from_slice(&XFS_SYMLINK_MAGIC.to_be_bytes());
block[XFS_SYMLINK_HDR_SIZE..XFS_SYMLINK_HDR_SIZE + target.len()]
.copy_from_slice(target.as_bytes());
dev.write_at(5 * 4096, &block).unwrap();
let extents = vec![Extent {
offset: 0,
startblock: 5,
blockcount: 1,
unwritten: false,
}];
let s = decode_remote(&mut dev, &layout, &extents, target.len() as u64).unwrap();
assert_eq!(s, target);
}
#[test]
fn remote_v4_no_header() {
let layout = BmbtLayout {
blocksize: 512,
agblocks: 64,
agblklog: 6,
is_v5: false,
};
let total = 64u64 * 512 * 2;
let mut dev = MemoryBackend::new(total);
let target = "../relative/path";
let mut block = vec![0u8; 512];
block[..target.len()].copy_from_slice(target.as_bytes());
dev.write_at(3 * 512, &block).unwrap();
let extents = vec![Extent {
offset: 0,
startblock: 3,
blockcount: 1,
unwritten: false,
}];
let s = decode_remote(&mut dev, &layout, &extents, target.len() as u64).unwrap();
assert_eq!(s, target);
}
#[test]
fn remote_v5_bad_magic_errors() {
let layout = BmbtLayout {
blocksize: 4096,
agblocks: 256,
agblklog: 8,
is_v5: true,
};
let mut dev = MemoryBackend::new(256 * 4096);
dev.write_at(5 * 4096, &vec![0u8; 4096]).unwrap();
let extents = vec![Extent {
offset: 0,
startblock: 5,
blockcount: 1,
unwritten: false,
}];
let r = decode_remote(&mut dev, &layout, &extents, 10);
assert!(matches!(r, Err(crate::Error::InvalidImage(_))));
}
}