use crate::types::{
CompressionOption, ENCODING_VERSION, EncodingInfo, EncodingOptions, FieldType, MAGIC_HEADER,
PointField,
};
use crate::{Error, Result};
pub fn encoding_info_to_yaml(info: &EncodingInfo) -> String {
let mut yaml = String::new();
yaml.push_str(&format!("version: {}\n", info.version));
yaml.push_str(&format!("width: {}\n", info.width));
yaml.push_str(&format!("height: {}\n", info.height));
yaml.push_str(&format!("point_step: {}\n", info.point_step));
yaml.push_str(&format!("encoding_opt: {}\n", info.encoding_opt.as_str()));
yaml.push_str(&format!(
"compression_opt: {}\n",
info.compression_opt.as_str()
));
if !info.encoding_config.is_empty() {
yaml.push_str(&format!("encoding_config: {}\n", info.encoding_config));
}
yaml.push_str("fields:\n");
for field in &info.fields {
yaml.push_str(&format!(" - name: {}\n", field.name));
yaml.push_str(&format!(" offset: {}\n", field.offset));
yaml.push_str(&format!(" type: {}\n", field.field_type.as_str()));
match field.resolution {
Some(r) => yaml.push_str(&format!(" resolution: {}\n", r)),
None => yaml.push_str(" resolution: null\n"),
}
}
yaml
}
pub fn encode_header(info: &EncodingInfo) -> Vec<u8> {
let yaml = encoding_info_to_yaml(info);
let mut out = Vec::with_capacity(MAGIC_HEADER.len() + 2 + 1 + yaml.len() + 1);
out.extend_from_slice(MAGIC_HEADER);
out.push(b'0' + (ENCODING_VERSION / 10));
out.push(b'0' + (ENCODING_VERSION % 10));
out.push(b'\n');
out.extend_from_slice(yaml.as_bytes());
out.push(0u8); out
}
pub fn decode_header(data: &[u8]) -> Result<(EncodingInfo, usize)> {
if data.len() < MAGIC_HEADER.len() + 2 {
return Err(Error::TooSmall);
}
if &data[..MAGIC_HEADER.len()] != MAGIC_HEADER {
let got = String::from_utf8_lossy(&data[..MAGIC_HEADER.len()]).into_owned();
return Err(Error::InvalidMagic { got });
}
let mut pos = MAGIC_HEADER.len();
let v0 = data[pos] - b'0';
let v1 = data[pos + 1] - b'0';
let version = v0 * 10 + v1;
pos += 2;
if !(2..=ENCODING_VERSION).contains(&version) {
return Err(Error::UnsupportedVersion {
got: version,
current: ENCODING_VERSION,
});
}
if pos + 1 < data.len() && data[pos] == b'\n' && data[pos + 1] != b'{' {
pos += 1; let remaining = &data[pos..];
let null_pos = remaining
.iter()
.position(|&b| b == 0)
.ok_or_else(|| Error::MalformedHeader("missing null terminator".into()))?;
let yaml_str = std::str::from_utf8(&remaining[..null_pos])
.map_err(|e| Error::MalformedHeader(e.to_string()))?;
let mut info = encoding_info_from_yaml(yaml_str)?;
info.version = version;
pos += null_pos + 1;
return Ok((info, pos));
}
let mut info = EncodingInfo {
version,
..Default::default()
};
let read_u32 = |data: &[u8], pos: &mut usize| -> Result<u32> {
if *pos + 4 > data.len() {
return Err(Error::Truncated("binary header (u32)".into()));
}
let v = u32::from_le_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]);
*pos += 4;
Ok(v)
};
let read_u16 = |data: &[u8], pos: &mut usize| -> Result<u16> {
if *pos + 2 > data.len() {
return Err(Error::Truncated("binary header (u16)".into()));
}
let v = u16::from_le_bytes([data[*pos], data[*pos + 1]]);
*pos += 2;
Ok(v)
};
let read_u8 = |data: &[u8], pos: &mut usize| -> Result<u8> {
if *pos >= data.len() {
return Err(Error::Truncated("binary header (u8)".into()));
}
let v = data[*pos];
*pos += 1;
Ok(v)
};
let read_f32 = |data: &[u8], pos: &mut usize| -> Result<f32> {
if *pos + 4 > data.len() {
return Err(Error::Truncated("binary header (f32)".into()));
}
let v = f32::from_le_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]);
*pos += 4;
Ok(v)
};
let read_string = |data: &[u8], pos: &mut usize| -> Result<String> {
let len = {
if *pos + 2 > data.len() {
return Err(Error::Truncated("binary header (string len)".into()));
}
let v = u16::from_le_bytes([data[*pos], data[*pos + 1]]) as usize;
*pos += 2;
v
};
if *pos + len > data.len() {
return Err(Error::Truncated("binary header (string data)".into()));
}
let s = std::str::from_utf8(&data[*pos..*pos + len])
.map_err(|e| Error::MalformedHeader(e.to_string()))?
.to_string();
*pos += len;
Ok(s)
};
info.width = read_u32(data, &mut pos)?;
info.height = read_u32(data, &mut pos)?;
info.point_step = read_u32(data, &mut pos)?;
let enc_stage = read_u8(data, &mut pos)?;
info.encoding_opt = match enc_stage {
0 => EncodingOptions::None,
1 => EncodingOptions::Lossy,
2 => EncodingOptions::Lossless,
_ => {
return Err(Error::MalformedHeader(format!(
"unknown encoding option: {}",
enc_stage
)));
}
};
let comp_stage = read_u8(data, &mut pos)?;
info.compression_opt = match comp_stage {
0 => CompressionOption::None,
1 => CompressionOption::Lz4,
2 => CompressionOption::Zstd,
_ => {
return Err(Error::MalformedHeader(format!(
"unknown compression option: {}",
comp_stage
)));
}
};
let fields_count = read_u16(data, &mut pos)?;
for _ in 0..fields_count {
let name = read_string(data, &mut pos)?;
let offset = read_u32(data, &mut pos)?;
let type_byte = read_u8(data, &mut pos)?;
let field_type = FieldType::from_yaml(&type_byte.to_string()).unwrap_or(FieldType::Unknown);
let res = read_f32(data, &mut pos)?;
let resolution = if res > 0.0 { Some(res) } else { None };
info.fields.push(PointField {
name,
offset,
field_type,
resolution,
});
}
Ok((info, pos))
}
pub fn encoding_info_from_yaml(yaml: &str) -> Result<EncodingInfo> {
let mut info = EncodingInfo::default();
let mut in_fields = false;
let mut current_field: Option<PointField> = None;
for line in yaml.lines() {
let trimmed = line.trim_start();
if trimmed.is_empty() {
continue;
}
let indent = line.len() - trimmed.len();
if indent == 0 && !trimmed.starts_with('-') {
in_fields = false;
if let Some(field) = current_field.take() {
info.fields.push(field);
}
if let Some((key, val)) = parse_kv(trimmed) {
match key {
"version" => {
info.version = val
.parse::<u8>()
.map_err(|e| Error::MalformedHeader(e.to_string()))?;
}
"width" => {
info.width = val
.parse::<u32>()
.map_err(|e| Error::MalformedHeader(e.to_string()))?;
}
"height" => {
info.height = val
.parse::<u32>()
.map_err(|e| Error::MalformedHeader(e.to_string()))?;
}
"point_step" => {
info.point_step = val
.parse::<u32>()
.map_err(|e| Error::MalformedHeader(e.to_string()))?;
}
"encoding_opt" => {
info.encoding_opt = EncodingOptions::from_yaml(val).ok_or_else(|| {
Error::MalformedHeader(format!("unknown encoding_opt: {}", val))
})?;
}
"compression_opt" => {
info.compression_opt =
CompressionOption::from_yaml(val).ok_or_else(|| {
Error::MalformedHeader(format!("unknown compression_opt: {}", val))
})?;
}
"encoding_config" => {
info.encoding_config = val.to_string();
}
"fields" => {
in_fields = true;
}
_ => {}
}
}
} else if in_fields {
if let Some(rest) = trimmed.strip_prefix("- ") {
if let Some(field) = current_field.take() {
info.fields.push(field);
}
current_field = Some(PointField {
name: String::new(),
offset: 0,
field_type: FieldType::Unknown,
resolution: None,
});
if let Some((key, val)) = parse_kv(rest) {
if let Some(ref mut field) = current_field {
apply_field_kv(field, key, val)?;
}
}
} else if indent >= 4 {
if let Some((key, val)) = parse_kv(trimmed) {
if let Some(ref mut field) = current_field {
apply_field_kv(field, key, val)?;
}
}
}
}
}
if let Some(field) = current_field.take() {
info.fields.push(field);
}
Ok(info)
}
fn parse_kv(s: &str) -> Option<(&str, &str)> {
if let Some(colon_pos) = s.find(": ") {
let key = s[..colon_pos].trim();
let val = s[colon_pos + 2..].trim();
Some((key, val))
} else if let Some(stripped) = s.strip_suffix(':') {
let key = stripped.trim();
Some((key, ""))
} else {
None
}
}
fn apply_field_kv(field: &mut PointField, key: &str, val: &str) -> Result<()> {
match key {
"name" => field.name = val.to_string(),
"offset" => {
field.offset = val
.parse::<u32>()
.map_err(|e| Error::MalformedHeader(e.to_string()))?;
}
"type" => {
field.field_type = FieldType::from_yaml(val)
.ok_or_else(|| Error::MalformedHeader(format!("unknown field type: {}", val)))?;
}
"resolution" => {
if val != "null" {
field.resolution = Some(
val.parse::<f32>()
.map_err(|e| Error::MalformedHeader(e.to_string()))?,
);
} else {
field.resolution = None;
}
}
_ => {}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::*;
fn make_test_info() -> EncodingInfo {
EncodingInfo {
fields: vec![
PointField {
name: "x".to_string(),
offset: 0,
field_type: FieldType::Float32,
resolution: Some(0.001),
},
PointField {
name: "y".to_string(),
offset: 4,
field_type: FieldType::Float32,
resolution: Some(0.001),
},
PointField {
name: "z".to_string(),
offset: 8,
field_type: FieldType::Float32,
resolution: Some(0.001),
},
PointField {
name: "intensity".to_string(),
offset: 12,
field_type: FieldType::Uint8,
resolution: None,
},
],
width: 100,
height: 1,
point_step: 13,
encoding_opt: EncodingOptions::Lossy,
compression_opt: CompressionOption::Zstd,
encoding_config: String::new(),
version: ENCODING_VERSION,
}
}
#[test]
fn test_yaml_roundtrip() {
let info = make_test_info();
let yaml = encoding_info_to_yaml(&info);
let parsed = encoding_info_from_yaml(&yaml).unwrap();
assert_eq!(parsed.width, info.width);
assert_eq!(parsed.height, info.height);
assert_eq!(parsed.point_step, info.point_step);
assert_eq!(parsed.encoding_opt, info.encoding_opt);
assert_eq!(parsed.compression_opt, info.compression_opt);
assert_eq!(parsed.fields.len(), info.fields.len());
for (a, b) in info.fields.iter().zip(parsed.fields.iter()) {
assert_eq!(a.name, b.name);
assert_eq!(a.offset, b.offset);
assert_eq!(a.field_type, b.field_type);
assert_eq!(a.resolution, b.resolution);
}
}
#[test]
fn test_header_encode_decode() {
let info = make_test_info();
let header_bytes = encode_header(&info);
assert!(header_bytes.starts_with(b"CLOUDINI_V03\n"));
let (decoded, consumed) = decode_header(&header_bytes).unwrap();
assert_eq!(consumed, header_bytes.len());
assert_eq!(decoded.width, info.width);
assert_eq!(decoded.encoding_opt, info.encoding_opt);
assert_eq!(decoded.compression_opt, info.compression_opt);
assert_eq!(decoded.fields.len(), info.fields.len());
}
}