use crate::error::CopcError;
use crate::las_header::LasVersion;
pub const EVLR_HEADER_SIZE: usize = 60;
#[derive(Debug, Clone)]
pub struct ExtendedVlr {
pub reserved: u16,
pub user_id: [u8; 16],
pub record_id: u16,
pub record_length: u64,
pub description: [u8; 32],
pub data: Vec<u8>,
}
impl ExtendedVlr {
pub fn user_id_str(&self) -> String {
trim_null_utf8(&self.user_id)
}
pub fn description_str(&self) -> String {
trim_null_utf8(&self.description)
}
}
fn trim_null_utf8(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes)
.trim_end_matches('\0')
.to_string()
}
pub fn version_supports_evlr(version: &LasVersion) -> bool {
matches!(version, LasVersion::V14)
}
fn read_u16_le(data: &[u8], offset: usize) -> Result<u16, CopcError> {
let end = offset
.checked_add(2)
.ok_or_else(|| CopcError::InvalidFormat("EVLR u16 read offset overflow".into()))?;
if end > data.len() {
return Err(CopcError::InvalidFormat(format!(
"EVLR truncated: need {end} bytes but only {} available",
data.len()
)));
}
Ok(u16::from_le_bytes([data[offset], data[offset + 1]]))
}
fn read_u64_le(data: &[u8], offset: usize) -> Result<u64, CopcError> {
let end = offset
.checked_add(8)
.ok_or_else(|| CopcError::InvalidFormat("EVLR u64 read offset overflow".into()))?;
if end > data.len() {
return Err(CopcError::InvalidFormat(format!(
"EVLR truncated: need {end} bytes but only {} available",
data.len()
)));
}
Ok(u64::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
]))
}
pub fn parse_evlr(data: &[u8], offset: usize) -> Result<(ExtendedVlr, usize), CopcError> {
let header_end = offset
.checked_add(EVLR_HEADER_SIZE)
.ok_or_else(|| CopcError::InvalidFormat("EVLR header offset overflow".into()))?;
if header_end > data.len() {
return Err(CopcError::InvalidFormat(format!(
"EVLR header truncated at offset {offset} (need {header_end} bytes, have {})",
data.len()
)));
}
let reserved = read_u16_le(data, offset)?;
let mut user_id = [0u8; 16];
user_id.copy_from_slice(&data[offset + 2..offset + 18]);
let record_id = read_u16_le(data, offset + 18)?;
let record_length = read_u64_le(data, offset + 20)?;
let mut description = [0u8; 32];
description.copy_from_slice(&data[offset + 28..offset + 60]);
let data_start = header_end;
let len_usize = usize::try_from(record_length).map_err(|_| {
CopcError::InvalidFormat(format!(
"EVLR record_length {record_length} exceeds platform usize"
))
})?;
let data_end = data_start
.checked_add(len_usize)
.ok_or_else(|| CopcError::InvalidFormat("EVLR payload offset overflow".into()))?;
if data_end > data.len() {
return Err(CopcError::InvalidFormat(format!(
"EVLR payload truncated: need {data_end} bytes but only {} available",
data.len()
)));
}
Ok((
ExtendedVlr {
reserved,
user_id,
record_id,
record_length,
description,
data: data[data_start..data_end].to_vec(),
},
data_end,
))
}
pub fn parse_evlr_chain(
bytes: &[u8],
start_of_first_evlr: u64,
number_of_evlrs: u32,
) -> Result<Vec<ExtendedVlr>, CopcError> {
if number_of_evlrs == 0 {
return Ok(Vec::new());
}
let mut offset = usize::try_from(start_of_first_evlr).map_err(|_| {
CopcError::InvalidFormat(format!(
"EVLR start offset {start_of_first_evlr} exceeds platform usize"
))
})?;
if offset > bytes.len() {
return Err(CopcError::InvalidFormat(format!(
"EVLR start offset {offset} exceeds file length {}",
bytes.len()
)));
}
let mut evlrs = Vec::with_capacity(number_of_evlrs as usize);
for i in 0..number_of_evlrs {
let (evlr, next_offset) = parse_evlr(bytes, offset).map_err(|e| {
CopcError::InvalidFormat(format!("Failed to parse EVLR #{i} at offset {offset}: {e}"))
})?;
evlrs.push(evlr);
offset = next_offset;
}
Ok(evlrs)
}
#[cfg(test)]
mod tests {
use super::*;
fn append_evlr(buf: &mut Vec<u8>, user_id: &str, record_id: u16, payload: &[u8]) {
buf.extend_from_slice(&0u16.to_le_bytes()); let uid = user_id.as_bytes();
let mut uid_buf = [0u8; 16];
let len = uid.len().min(16);
uid_buf[..len].copy_from_slice(&uid[..len]);
buf.extend_from_slice(&uid_buf);
buf.extend_from_slice(&record_id.to_le_bytes());
buf.extend_from_slice(&(payload.len() as u64).to_le_bytes());
buf.extend_from_slice(&[0u8; 32]); buf.extend_from_slice(payload);
}
#[test]
fn test_evlr_header_size_constant() {
assert_eq!(EVLR_HEADER_SIZE, 60);
}
#[test]
fn test_parse_single_evlr() {
let mut buf = Vec::new();
append_evlr(&mut buf, "test_user", 42, b"hello");
let (evlr, next) = parse_evlr(&buf, 0).expect("parse");
assert_eq!(evlr.record_id, 42);
assert_eq!(evlr.record_length, 5);
assert_eq!(evlr.data, b"hello");
assert_eq!(next, 60 + 5);
assert_eq!(evlr.user_id_str(), "test_user");
}
#[test]
fn test_chain_zero_records() {
let evlrs = parse_evlr_chain(&[], 0, 0).expect("zero");
assert!(evlrs.is_empty());
}
#[test]
fn test_chain_two_records() {
let mut buf = vec![0u8; 8]; let start = buf.len() as u64;
append_evlr(&mut buf, "first", 1, b"aaaa");
append_evlr(&mut buf, "second", 2, b"bbbbbb");
let evlrs = parse_evlr_chain(&buf, start, 2).expect("two");
assert_eq!(evlrs.len(), 2);
assert_eq!(evlrs[0].record_id, 1);
assert_eq!(evlrs[0].data, b"aaaa");
assert_eq!(evlrs[1].record_id, 2);
assert_eq!(evlrs[1].data, b"bbbbbb");
}
#[test]
fn test_chain_truncated_errors_no_panic() {
let mut buf = Vec::new();
append_evlr(&mut buf, "x", 1, b"payload");
buf.truncate(30); assert!(parse_evlr_chain(&buf, 0, 1).is_err());
}
#[test]
fn test_start_offset_out_of_range_errors() {
let buf = vec![0u8; 10];
assert!(parse_evlr_chain(&buf, 1000, 1).is_err());
}
#[test]
fn test_version_guard() {
assert!(version_supports_evlr(&LasVersion::V14));
assert!(!version_supports_evlr(&LasVersion::V13));
assert!(!version_supports_evlr(&LasVersion::V10));
}
}