use crate::error::{Error, Result};
const EXTERNAL_LINK_VERSION: u8 = 0;
const EXTERNAL_LINK_FLAGS_ALL: u8 = 0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkType {
Hard,
Soft,
External,
UserDefined(u8),
}
#[derive(Debug, Clone)]
pub struct LinkMessage {
pub name: String,
pub link_type: LinkType,
pub creation_order: Option<u64>,
pub char_encoding: u8,
pub hard_link_addr: Option<u64>,
pub soft_link_target: Option<String>,
pub external_link: Option<(String, String)>,
}
impl LinkMessage {
pub fn decode(data: &[u8], sizeof_addr: u8) -> Result<Self> {
Self::decode_impl(data, sizeof_addr)
}
fn decode_impl(data: &[u8], sizeof_addr: u8) -> Result<Self> {
let mut pos = 0;
let version = read_u8(data, &mut pos, "link message version")?;
if version != 1 {
return Err(Error::InvalidFormat(format!(
"link message version {version}"
)));
}
let flags = read_u8(data, &mut pos, "link message flags")?;
let size_of_len_of_link_name = flags & 0x03; let has_creation_order = flags & 0x04 != 0;
let has_link_type = flags & 0x08 != 0;
let has_char_encoding = flags & 0x10 != 0;
let link_type = if has_link_type {
let t = read_u8(data, &mut pos, "link message link type")?;
match t {
0 => LinkType::Hard,
1 => LinkType::Soft,
64 => LinkType::External,
other => LinkType::UserDefined(other),
}
} else {
LinkType::Hard };
let creation_order = if has_creation_order {
let val = read_le_u64(data, &mut pos, 8, "link message creation order")?;
Some(val)
} else {
None
};
let char_encoding = if has_char_encoding {
read_u8(data, &mut pos, "link message character encoding")?
} else {
0 };
if char_encoding > 1 {
return Err(Error::InvalidFormat(format!(
"invalid link character encoding {char_encoding}"
)));
}
let name_len_size = 1 << size_of_len_of_link_name; let name_len = read_le_u64(data, &mut pos, name_len_size, "link message name length")?
.try_into()
.map_err(|_| Error::InvalidFormat("link name length overflows usize".into()))?;
ensure_available(data, pos, name_len, "link name")?;
let name = String::from_utf8_lossy(&data[pos..pos + name_len]).to_string();
pos += name_len;
let mut hard_link_addr = None;
let mut soft_link_target = None;
let mut external_link = None;
match link_type {
LinkType::Hard => {
hard_link_addr = Some(read_le_u64(
data,
&mut pos,
sizeof_addr as usize,
"hard link address",
)?);
}
LinkType::Soft => {
let target_len =
read_le_u64(data, &mut pos, 2, "soft link target length")? as usize;
ensure_available(data, pos, target_len, "soft link target")?;
soft_link_target =
Some(String::from_utf8_lossy(&data[pos..pos + target_len]).to_string());
}
LinkType::External => {
let info_len =
read_le_u64(data, &mut pos, 2, "external link info length")? as usize;
if info_len < 2 {
return Err(Error::InvalidFormat(
"external link info is too short".into(),
));
}
ensure_available(data, pos, info_len, "external link info")?;
let (filename, obj_path) = unpack_external_link_value(&data[pos..pos + info_len])?;
trace_external_link_resolve(&filename, &obj_path);
external_link = Some((filename, obj_path));
}
LinkType::UserDefined(_) => {
}
}
Ok(LinkMessage {
name,
link_type,
creation_order,
char_encoding,
hard_link_addr,
soft_link_target,
external_link,
})
}
}
#[cfg(feature = "tracehash")]
fn trace_external_link_resolve(filename: &str, obj_path: &str) {
let mut th = tracehash::th_call!("hdf5.external_link.resolve");
th.input_bytes(filename.as_bytes());
th.input_bytes(obj_path.as_bytes());
th.output_value(&(true));
th.output_value(filename.as_bytes());
th.output_value(obj_path.as_bytes());
th.finish();
}
#[cfg(not(feature = "tracehash"))]
fn trace_external_link_resolve(_filename: &str, _obj_path: &str) {}
fn ensure_available(data: &[u8], pos: usize, len: usize, context: &str) -> Result<()> {
let end = pos
.checked_add(len)
.ok_or_else(|| Error::InvalidFormat(format!("{context} length overflow")))?;
if end > data.len() {
return Err(Error::InvalidFormat(format!("{context} is truncated")));
}
Ok(())
}
fn read_u8(data: &[u8], pos: &mut usize, context: &str) -> Result<u8> {
ensure_available(data, *pos, 1, context)?;
let value = data[*pos];
*pos += 1;
Ok(value)
}
fn read_le_u64(data: &[u8], pos: &mut usize, size: usize, context: &str) -> Result<u64> {
if !(1..=8).contains(&size) {
return Err(Error::InvalidFormat(format!(
"{context} has invalid byte width {size}"
)));
}
ensure_available(data, *pos, size, context)?;
let mut val = 0u64;
for i in 0..size {
val |= (data[*pos + i] as u64) << (i * 8);
}
*pos += size;
Ok(val)
}
fn unpack_external_link_value(data: &[u8]) -> Result<(String, String)> {
if data.is_empty() {
return Err(Error::InvalidFormat(
"not a valid external link buffer".into(),
));
}
let header = data[0];
let ext_version = (header >> 4) & 0x0f;
let ext_flags = header & 0x0f;
if ext_version > EXTERNAL_LINK_VERSION {
return Err(Error::InvalidFormat(format!(
"external link version {ext_version} exceeds supported version {EXTERNAL_LINK_VERSION}"
)));
}
if ext_flags & !EXTERNAL_LINK_FLAGS_ALL != 0 {
return Err(Error::InvalidFormat(format!(
"external link flags {ext_flags:#x} are invalid"
)));
}
if data.len() <= 2 {
return Err(Error::InvalidFormat(
"not a valid external link buffer".into(),
));
}
if data[data.len() - 1] != 0 {
return Err(Error::InvalidFormat(
"external link buffer is not NULL-terminated".into(),
));
}
let payload = &data[1..];
let filename_end = payload.iter().position(|&b| b == 0).ok_or_else(|| {
Error::InvalidFormat("external link buffer does not contain an object path".into())
})?;
if filename_end + 1 >= payload.len() {
return Err(Error::InvalidFormat(
"external link buffer does not contain an object path".into(),
));
}
let obj_payload = &payload[filename_end + 1..];
let obj_end = obj_payload.iter().position(|&b| b == 0).ok_or_else(|| {
Error::InvalidFormat("external link buffer does not contain an object path".into())
})?;
if obj_end == 0 {
return Err(Error::InvalidFormat(
"external link buffer does not contain an object path".into(),
));
}
let filename = String::from_utf8_lossy(&payload[..filename_end]).to_string();
let obj_path = String::from_utf8_lossy(&obj_payload[..obj_end]).to_string();
Ok((filename, obj_path))
}