use super::misc::mktag;
use crate::error::{Error, Result};
use crate::tag::Tag;
use crate::value::Value;
use std::collections::{HashMap, HashSet};
const DEFAULT_GROUP1: &str = "MXF";
const INSTANCE_UID: &str = "060e2b34010101010101150200000000";
#[rustfmt::skip]
const LOCAL_SETS: &[(&str, &str)] = &[
("060e2b34025301010d01010101010200", "StructuralComponent"),
("060e2b34025301010d01010101010f00", "SequenceSet"),
("060e2b34025301010d01010101011400", "TimecodeComponent"),
("060e2b34025301010d01010101011800", "ContentStorageSet"),
("060e2b34025301010d01010101012300", "EssenceContainerDataSet"),
("060e2b34025301010d01010101012500", "FileDescriptor"),
("060e2b34025301010d01010101012700", "GenericPictureEssenceDescriptor"),
("060e2b34025301010d01010101012800", "CDCIEssenceDescriptor"),
("060e2b34025301010d01010101012900", "RGBAEssenceDescriptor"),
("060e2b34025301010d01010101012f00", "Preface"),
("060e2b34025301010d01010101013000", "Identification"),
("060e2b34025301010d01010101013200", "NetworkLocator"),
("060e2b34025301010d01010101013300", "TextLocator"),
("060e2b34025301010d01010101013400", "GenericPackage"),
("060e2b34025301010d01010101013600", "MaterialPackage"),
("060e2b34025301010d01010101013700", "SourcePackage"),
("060e2b34025301010d01010101013800", "GenericTrack"),
("060e2b34025301010d01010101013900", "EventTrack"),
("060e2b34025301010d01010101013a00", "StaticTrack"),
("060e2b34025301010d01010101013b00", "Track"),
("060e2b34025301010d01010101014100", "DMSegment"),
("060e2b34025301010d01010101014200", "GenericSoundEssenceDescriptor"),
("060e2b34025301010d01010101014300", "GenericDataEssenceDescriptor"),
("060e2b34025301010d01010101014400", "MultipleDescriptor"),
("060e2b34025301010d01010101014500", "DMSourceClip"),
("060e2b34025301010d01010101014700", "AES3PCMDescriptor"),
("060e2b34025301010d01010101014800", "WaveAudioDescriptor"),
("060e2b34025301010d01010101015100", "MPEG2VideoDescriptor"),
("060e2b34025301010d01010101015a00", "JPEG2000PictureSubDescriptor"),
("060e2b34025301010d01010101015b00", "VBIDataDescriptor"),
("060e2b34025301010d01040000000000", "DMSet"),
("060e2b34025301010d01040100000000", "DMFramework"),
("060e2b34025301010d01040101010100", "ProductionFramework"),
("060e2b34025301010d01040101010200", "ClipFramework"),
("060e2b34025301010d01040101010300", "SceneFramework"),
("060e2b34025301010d01040101100100", "Titles"),
("060e2b34025301010d01040101110100", "Identification"),
("060e2b34025301010d01040101120100", "GroupRelationship"),
("060e2b34025301010d01040101130100", "Branding"),
("060e2b34025301010d01040101140100", "Event"),
("060e2b34025301010d01040101140200", "Publication"),
("060e2b34025301010d01040101150100", "Award"),
("060e2b34025301010d01040101160100", "CaptionDescription"),
("060e2b34025301010d01040101170100", "Annotation"),
("060e2b34025301010d01040101170200", "SettingPeriod"),
("060e2b34025301010d01040101170300", "Scripting"),
("060e2b34025301010d01040101170400", "Classification"),
("060e2b34025301010d01040101170500", "Shot"),
("060e2b34025301010d01040101170600", "KeyPoint"),
("060e2b34025301010d01040101170800", "CueWords"),
("060e2b34025301010d01040101180100", "Participant"),
("060e2b34025301010d01040101190100", "ContactsList"),
("060e2b34025301010d010401011a0200", "Person"),
("060e2b34025301010d010401011a0300", "Organisation"),
("060e2b34025301010d010401011a0400", "Location"),
("060e2b34025301010d010401011b0100", "Address"),
("060e2b34025301010d010401011b0200", "Communications"),
("060e2b34025301010d010401011c0100", "Contract"),
("060e2b34025301010d010401011c0200", "Rights"),
("060e2b34025301010d010401011d0100", "PictureFormat"),
("060e2b34025301010d010401011e0100", "DeviceParameters"),
("060e2b34025301010d010401011f0100", "NameValue"),
("060e2b34025301010d01040101200100", "Processing"),
("060e2b34025301010d01040101200200", "Projects"),
("060e2b34025301010d01040102010000", "CryptographicFramework"),
("060e2b34025301010d01040102020000", "CryptographicContext"),
];
const USER_ORGANIZATION_SET: &str = "UserOrganizationPublicUse";
const USER_ORGANIZATION_PREFIX: &str = "060e2b34025301010d";
#[rustfmt::skip]
const UNKNOWN_SETS: &[&str] = &[
"060e2b34025301010d01010101011100",
"060e2b34025301010d01020101100000", "060e2b34025301010d01020101100100", ];
#[rustfmt::skip]
const STRONG_REFS: &[(&str, bool)] = &[
("060e2b34010101020301021003000000", true), ("060e2b34010101020301021004000000", true), ("060e2b3401010102030201020c000000", true), ("060e2b34010101020520090d00000000", true), ("060e2b34010101020601010402010000", false), ("060e2b34010101020601010402020000", false), ("060e2b34010101020601010402030000", false), ("060e2b34010101020601010402040000", false), ("060e2b34010101020601010402050000", false), ("060e2b34010101020601010402060000", false), ("060e2b34010101020601010402070000", false), ("060e2b34010101020601010402080000", false), ("060e2b34010101020601010402090000", false), ("060e2b340101010206010104020a0000", false), ("060e2b340101010206010104020b0000", false), ("060e2b34010101020601010405010000", true), ("060e2b34010101020601010405020000", true), ("060e2b34010101020601010405030000", true), ("060e2b34010101020601010405040000", true), ("060e2b34010101020601010405050000", true), ("060e2b34010101020601010405060000", true), ("060e2b34010101020601010405070000", true), ("060e2b34010101020601010405080000", true), ("060e2b34010101020601010405090000", true), ("060e2b34010101020601010406010000", true), ("060e2b34010101020601010406020000", true), ("060e2b34010101020601010406030000", true), ("060e2b34010101020601010406040000", true), ("060e2b34010101020601010406050000", true), ("060e2b34010101020601010406060000", true), ("060e2b34010101020601010406070000", true), ("060e2b34010101020601010406080000", true), ("060e2b34010101020601010406090000", true), ("060e2b340101010206010104060a0000", true), ("060e2b34010101020601010702000000", true), ("060e2b34010101020601010707000000", true), ("060e2b34010101020601010708000000", true), ("060e2b340101010406010104060b0000", true), ("060e2b340101010506010104020c0000", false), ("060e2b34010101050601010402400500", false), ("060e2b34010101050601010402401c00", false), ("060e2b34010101050601010402401d00", false), ("060e2b34010101050601010402402000", false), ("060e2b34010101050601010402402100", false), ("060e2b34010101050601010402402200", false), ("060e2b34010101050601010402402301", false), ("060e2b34010101050601010402402302", false), ("060e2b34010101050601010405400400", true), ("060e2b34010101050601010405400500", true), ("060e2b34010101050601010405400600", true), ("060e2b34010101050601010405400700", true), ("060e2b34010101050601010405400800", true), ("060e2b34010101050601010405400900", true), ("060e2b34010101050601010405400a00", true), ("060e2b34010101050601010405400b00", true), ("060e2b34010101050601010405400c00", true), ("060e2b34010101050601010405400d00", true), ("060e2b34010101050601010405400e01", true), ("060e2b34010101050601010405400e02", true), ("060e2b34010101050601010405400f00", true), ("060e2b34010101050601010405401000", true), ("060e2b34010101050601010405401101", true), ("060e2b34010101050601010405401102", true), ("060e2b34010101050601010405401200", true), ("060e2b34010101050601010405401300", true), ("060e2b34010101050601010405401400", true), ("060e2b34010101050601010405401500", true), ("060e2b34010101050601010405401600", true), ("060e2b34010101050601010405401700", true), ("060e2b34010101050601010405401800", true), ("060e2b34010101050601010405401900", true), ("060e2b34010101050601010405401a00", true), ("060e2b34010101050601010405401b00", true), ("060e2b34010101050601010405401e00", true), ("060e2b34010101050601010405401f01", true), ("060e2b34010101050601010405401f02", true), ("060e2b34010101050601010405401f03", true), ("060e2b340101010506010104060c0000", true), ("060e2b340101010506010104060d0000", true), ("060e2b34010101070301021007000000", true), ("060e2b34010101070301021008000000", true), ("060e2b34010101070302010216000000", true), ("060e2b340101010706010104050a0000", true), ("060e2b340101010706010104050b0000", true), ("060e2b34010101070601010405401f04", true), ("060e2b34010101080601010405400d01", true), ("060e2b340101010806010104060e0000", true), ("060e2b340101010806010104060f0000", true), ("060e2b340101010906010104020d0000", false), ("060e2b34010101090601010406100000", true), ("060e2b340101010a0601010716000000", false), ("060e2b340101010a0601010717000000", false), ("060e2b340101010c06010104020e0000", false), ("060e2b340101010c06010104020f0000", false), ("060e2b340101010c0601010402100000", false), ("060e2b340101010c0601010402110000", false), ("060e2b340101010c0601010402120000", false), ("060e2b340101010c0601010406110000", true), ("060e2b340101010c0601010406120000", true), ("060e2b340101010c0601010406130000", true), ("060e2b340101010c0601010406140000", true), ];
#[rustfmt::skip]
const COMPONENT_DATA_DEF: &[(&str, &str)] = &[
("060e2b34.0401.0101.01030201.01000000", "SMPTE 12M Timecode Track"),
("060e2b34.0401.0101.01030201.02000000", "SMPTE 12M Timecode Track with active user bits"),
("060e2b34.0401.0101.01030201.03000000", "SMPTE 309M Timecode Track"),
("060e2b34.0401.0101.01030201.10000000", "Descriptive Metadata Track"),
("060e2b34.0401.0101.01030202.01000000", "Picture Essence Track"),
("060e2b34.0401.0101.01030202.02000000", "Sound Essence Track"),
("060e2b34.0401.0101.01030202.03000000", "Data Essence Track"),
];
#[derive(Clone, Copy, PartialEq)]
enum Kind {
Timestamp,
Utf16,
VersionType,
ProductVersion,
Boolean,
Int16u,
Int32u,
Rational,
Int64sDuration,
LengthDuration,
ComponentDataDef,
}
#[rustfmt::skip]
const TAGS: &[(&str, &str, Kind)] = &[
("060e2b34010101010404010105000000", "DropFrame", Kind::Boolean),
("060e2b34010101010406010100000000", "SampleRate", Kind::Rational),
("060e2b34010101010406010200000000", "EssenceLength", Kind::LengthDuration),
("060e2b34010101020104010300000000", "TrackNumber", Kind::Int32u),
("060e2b34010101020107010100000000", "TrackID", Kind::Int32u),
("060e2b34010101020107010201000000", "TrackName", Kind::Utf16),
("060e2b34010101020301020105000000", "SDKVersion", Kind::VersionType),
("060e2b34010101020404010102060000", "RoundedTimecodeTimebase", Kind::Int16u),
("060e2b34010101020407010000000000", "ComponentDataDefinition", Kind::ComponentDataDef),
("060e2b34010101020520070102010000", "ApplicationSupplierName", Kind::Utf16),
("060e2b34010101020520070103010000", "ApplicationName", Kind::Utf16),
("060e2b34010101020520070105010000", "ApplicationVersionString", Kind::Utf16),
("060e2b34010101020520070106010000", "ApplicationPlatform", Kind::Utf16),
("060e2b3401010102052007010a000000", "ToolkitVersion", Kind::ProductVersion),
("060e2b34010101020530040500000000", "EditRate", Kind::Rational),
("060e2b34010101020702010301030000", "Origin", Kind::Int64sDuration),
("060e2b34010101020702010301050000", "StartTimecode", Kind::Int64sDuration),
("060e2b34010101020702011001030000", "CreateDate", Kind::Timestamp),
("060e2b34010101020702011002030000", "ModifyDate", Kind::Timestamp),
("060e2b34010101020702011002040000", "ContainerLastModifyDate", Kind::Timestamp),
("060e2b34010101020702011002050000", "PackageLastModifyDate", Kind::Timestamp),
("060e2b34010101020702020101030000", "Duration", Kind::LengthDuration),
("060e2b34010101040103040400000000", "EssenceStreamID", Kind::Int32u),
("060e2b34010101040402030104000000", "LockedIndicator", Kind::Boolean),
("060e2b34010101040402030304000000", "BitsPerAudioSample", Kind::Int32u),
("060e2b34010101050402010104000000", "ChannelCount", Kind::Int32u),
("060e2b34010101050402030101010000", "AudioSampleRate", Kind::Rational),
("060e2b34010101050402030201000000", "BlockAlign", Kind::Int16u),
("060e2b34010101050402030305000000", "AverageBytesPerSecond", Kind::Int32u),
("060e2b34010101050601010305000000", "LinkedTrackID", Kind::Int32u),
];
struct Emitted {
tag: Tag,
uid: Option<[u8; 16]>,
duration: Option<f64>,
}
struct Obj {
name: &'static str,
strong_refs: Vec<[u8; 16]>,
tags: Vec<usize>,
track_id: Option<u32>,
edit_rate: Option<f64>,
did_groups: bool,
}
#[derive(Default)]
struct Parser {
emitted: Vec<Emitted>,
objects: Vec<Obj>,
by_uid: HashMap<[u8; 16], usize>,
group1: HashMap<u32, String>,
num_tracks: u32,
prefaces: Vec<[u8; 16]>,
edit_rates: HashMap<String, f64>,
best_duration: HashMap<&'static str, [u8; 16]>,
in_source: bool,
}
pub fn read_mxf(data: &[u8], extract_embedded: u8) -> Result<Vec<Tag>> {
let magic = b"\x06\x0e\x2b\x34";
let start = data
.windows(4)
.position(|w| w == magic.as_ref())
.ok_or_else(|| Error::InvalidData("not an MXF file".into()))?;
let data = &data[start..];
let mut p = Parser::default();
let mut registry: HashMap<[u8; 2], [u8; 16]> = HashMap::new();
let mut pos = 0;
while pos + 17 <= data.len() {
if &data[pos..pos + 4] != b"\x06\x0e\x2b\x34" {
pos += 1;
continue;
}
let key = &data[pos..pos + 16];
let len_byte = data[pos + 16];
let (val_len, ber_size) = if len_byte < 0x80 {
(len_byte as usize, 1usize)
} else {
let n = (len_byte & 0x7f) as usize;
if pos + 17 + n > data.len() {
break;
}
let mut l = 0usize;
for i in 0..n {
l = (l << 8) | (data[pos + 17 + i] as usize);
}
(l, 1 + n)
};
let val_start = pos + 16 + ber_size;
if val_start + val_len > data.len() {
break;
}
let val = &data[val_start..val_start + val_len];
if key[4] == 0x02 && key[5] == 0x05 && key[12] == 0x01 && key[13] == 0x02 {
if val.len() >= 4 && !p.emitted.iter().any(|e| e.tag.name == "MXFVersion") {
let major = u16::from_be_bytes([val[0], val[1]]);
let minor = u16::from_be_bytes([val[2], val[3]]);
p.emitted.push(Emitted {
tag: mktag(
DEFAULT_GROUP1,
"MXFVersion",
"MXF Version",
Value::String(format!("{major}.{minor}")),
),
uid: None,
duration: None,
});
}
}
else if key[4] == 0x02 && key[5] == 0x05 && key[12] == 0x01 && key[13] == 0x05 {
read_primer(val, &mut registry);
}
else if let Some(set_name) = local_set_name(&hex32(key)) {
parse_local_set(val, set_name, ®istry, &mut p);
}
pos = val_start + val_len;
}
for root in p.prefaces.clone() {
set_groups(&mut p, root);
}
convert_durations(&mut p);
let mut tags = finish(p);
if extract_embedded == 0 {
let mut last: HashMap<&str, usize> = HashMap::new();
for (i, t) in tags.iter().enumerate() {
last.insert(t.name.as_str(), i);
}
let keep: Vec<bool> = tags
.iter()
.enumerate()
.map(|(i, t)| last.get(t.name.as_str()) == Some(&i))
.collect();
let mut it = keep.into_iter();
tags.retain(|_| it.next().unwrap_or(true));
}
Ok(tags)
}
fn read_primer(val: &[u8], registry: &mut HashMap<[u8; 2], [u8; 16]>) {
if val.len() < 8 {
return;
}
let count = u32::from_be_bytes([val[0], val[1], val[2], val[3]]) as usize;
let item_size = u32::from_be_bytes([val[4], val[5], val[6], val[7]]) as usize;
if item_size < 18 {
return;
}
for i in 0..count {
let off = 8 + i * item_size;
if off + 18 > val.len() {
break;
}
let mut ltag = [0u8; 2];
ltag.copy_from_slice(&val[off..off + 2]);
registry.insert(ltag, uid16(&val[off + 2..off + 18]));
}
}
fn local_set_name(ul: &str) -> Option<&'static str> {
if let Ok(i) = LOCAL_SETS.binary_search_by_key(&ul, |e| e.0) {
return Some(LOCAL_SETS[i].1);
}
if ul.starts_with(USER_ORGANIZATION_PREFIX) && !UNKNOWN_SETS.contains(&ul) {
return Some(USER_ORGANIZATION_SET);
}
None
}
fn parse_local_set(
data: &[u8],
set_name: &'static str,
registry: &HashMap<[u8; 2], [u8; 16]>,
p: &mut Parser,
) {
let mut strong_refs: Vec<[u8; 16]> = Vec::new();
let mut indices: Vec<usize> = Vec::new();
let mut instance: Option<[u8; 16]> = None;
let mut edit_rate: Option<f64> = None;
let mut track_id: Option<u32> = None;
let mut pos = 0;
while pos + 4 <= data.len() {
let ltag = [data[pos], data[pos + 1]];
let llen = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
pos += 4;
if pos + llen > data.len() {
break;
}
let val = &data[pos..pos + llen];
pos += llen;
let Some(ul) = registry.get(<ag) else {
continue;
};
let ul = hex32(ul);
if ul == INSTANCE_UID {
if val.len() == 16 {
instance = Some(uid16(val));
}
continue;
}
if let Ok(i) = STRONG_REFS.binary_search_by_key(&ul.as_str(), |e| e.0) {
strong_refs.extend(read_strong_refs(val, STRONG_REFS[i].1));
continue;
}
let Ok(i) = TAGS.binary_search_by_key(&ul.as_str(), |e| e.0) else {
continue;
};
let (_, name, kind) = TAGS[i];
let Some((text, duration)) = decode(kind, val) else {
continue;
};
if name == "EditRate" {
edit_rate = rational(val);
} else if name.ends_with("TrackID") {
if let Some(id) = int32u(val) {
track_id = Some(id);
if !p.group1.contains_key(&id) {
p.num_tracks += 1;
p.group1.insert(id, format!("Track{}", p.num_tracks));
}
}
}
indices.push(p.emitted.len());
p.emitted.push(Emitted {
tag: mktag(DEFAULT_GROUP1, name, name, Value::String(text)),
uid: None,
duration,
});
}
let Some(uid) = instance else { return };
let idx = match p.by_uid.get(&uid) {
Some(&i) => i,
None => {
p.objects.push(Obj {
name: set_name,
strong_refs: Vec::new(),
tags: Vec::new(),
track_id: None,
edit_rate: None,
did_groups: false,
});
let i = p.objects.len() - 1;
p.by_uid.insert(uid, i);
i
}
};
let obj = &mut p.objects[idx];
obj.name = set_name;
obj.strong_refs.extend(strong_refs);
obj.tags.extend(indices.iter().copied());
if track_id.is_some() {
obj.track_id = track_id;
}
if let Some(r) = edit_rate {
if r != 0.0 {
obj.edit_rate = Some(r);
}
}
for i in indices {
p.emitted[i].uid = Some(uid);
}
if set_name == "Preface" {
p.prefaces.push(uid);
}
}
fn set_groups(p: &mut Parser, root: [u8; 16]) {
enum Step {
Visit([u8; 16], Option<u32>),
LeaveSource,
}
let mut stack = vec![Step::Visit(root, None)];
while let Some(step) = stack.pop() {
let (uid, inherited) = match step {
Step::LeaveSource => {
p.in_source = false;
continue;
}
Step::Visit(uid, inherited) => (uid, inherited),
};
let Some(&i) = p.by_uid.get(&uid) else {
continue;
};
if p.objects[i].did_groups {
continue;
}
p.objects[i].did_groups = true;
let track_id = p.objects[i].track_id.or(inherited);
let mut g1 = None;
if let Some(id) = track_id {
p.objects[i].track_id = Some(id);
g1 = p.group1.get(&id).cloned();
if let (Some(rate), Some(g)) = (p.objects[i].edit_rate, g1.as_ref()) {
p.edit_rates.insert(g.clone(), rate);
}
if p.objects[i].name == "TimecodeComponent" {
let in_what = if p.in_source { "Source" } else { "Other" };
p.best_duration.insert(in_what, uid);
}
}
let set_source = p.objects[i].name == "SourcePackage";
if set_source {
p.in_source = true;
}
if let Some(g) = g1 {
for ti in p.objects[i].tags.clone() {
p.emitted[ti].tag.group.family1 = g.clone();
}
}
if set_source {
stack.push(Step::LeaveSource);
}
for r in p.objects[i].strong_refs.clone().into_iter().rev() {
stack.push(Step::Visit(r, track_id));
}
}
}
fn convert_durations(p: &mut Parser) {
for e in &mut p.emitted {
let Some(d) = e.duration else { continue };
let d = match p.edit_rates.get(e.tag.group.family1.as_str()) {
Some(rate) if *rate != 0.0 => d / rate,
_ => d,
};
e.duration = Some(d);
let text = convert_duration(d);
e.tag.raw_value = Value::String(text.clone());
e.tag.print_value = text;
}
}
fn finish(p: Parser) -> Vec<Tag> {
let best_uid = p
.best_duration
.get("Source")
.or_else(|| p.best_duration.get("Other"))
.copied();
let mut best_value = None;
let mut seen: HashSet<(&str, [u8; 16])> = HashSet::new();
let mut keep = vec![true; p.emitted.len()];
for (i, e) in p.emitted.iter().enumerate().rev() {
let Some(uid) = e.uid else { continue };
if !seen.insert((e.tag.name.as_str(), uid)) {
keep[i] = false;
} else if Some(uid) == best_uid && e.tag.name == "Duration" {
best_value = e.duration;
}
}
let mut tags: Vec<Tag> = p
.emitted
.iter()
.zip(&keep)
.filter(|(_, &k)| k)
.map(|(e, _)| e.tag.clone())
.collect();
if let Some(v) = best_value {
if v.abs() <= 1e18 {
tags.push(mktag(
DEFAULT_GROUP1,
"Duration",
"Duration",
Value::String(convert_duration(v)),
));
}
}
tags
}
fn read_strong_refs(val: &[u8], collection: bool) -> Vec<[u8; 16]> {
if collection && val.len() > 16 {
let count = u32::from_be_bytes([val[0], val[1], val[2], val[3]]) as usize;
let size = u32::from_be_bytes([val[4], val[5], val[6], val[7]]) as usize;
let mut out = Vec::new();
for i in 0..count {
let off = 8 + i * size;
if size < 16 || off + size > val.len() {
break;
}
out.push(uid16(&val[off..off + 16]));
}
return out;
}
if val.len() == 16 {
return vec![uid16(val)];
}
Vec::new()
}
fn decode(kind: Kind, val: &[u8]) -> Option<(String, Option<f64>)> {
let text = match kind {
Kind::Timestamp => decode_timestamp(val),
Kind::Utf16 => decode_utf16(val),
Kind::VersionType => val
.iter()
.map(|b| b.to_string())
.collect::<Vec<_>>()
.join("."),
Kind::ProductVersion => decode_product_version(val),
Kind::Boolean => if val == b"\0" { "False" } else { "True" }.to_string(),
Kind::Int16u => {
if val.len() < 2 {
return None;
}
u16::from_be_bytes([val[0], val[1]]).to_string()
}
Kind::Int32u => int32u(val)?.to_string(),
Kind::Rational => format_number(rational(val)?),
Kind::ComponentDataDef => decode_component_data_def(val),
Kind::Int64sDuration | Kind::LengthDuration => {
if val.len() < 8 {
return None;
}
let raw = <[u8; 8]>::try_from(&val[..8]).ok()?;
let n = if kind == Kind::LengthDuration {
u64::from_be_bytes(raw) as f64
} else {
i64::from_be_bytes(raw) as f64
};
if n > 1e18 {
return None;
}
return Some((convert_duration(n), Some(n)));
}
};
Some((text, None))
}
fn int32u(val: &[u8]) -> Option<u32> {
if val.len() < 4 {
return None;
}
Some(u32::from_be_bytes([val[0], val[1], val[2], val[3]]))
}
fn rational(val: &[u8]) -> Option<f64> {
if val.len() < 8 {
return None;
}
let num = i32::from_be_bytes([val[0], val[1], val[2], val[3]]) as f64;
let den = i32::from_be_bytes([val[4], val[5], val[6], val[7]]) as f64;
if den == 0.0 {
return None;
}
Some(num / den)
}
fn format_number(v: f64) -> String {
if v == 0.0 {
return "0".to_string();
}
if !v.is_finite() {
return format!("{v}");
}
let exp = v.abs().log10().floor() as i32;
if !(-4..15).contains(&exp) {
return format!("{v}");
}
let precision = (14 - exp).max(0) as usize;
let s = format!("{v:.precision$}");
if s.contains('.') {
s.trim_end_matches('0').trim_end_matches('.').to_string()
} else {
s
}
}
fn convert_duration(time: f64) -> String {
if time == 0.0 {
return "0 s".to_string();
}
let (mut sign, mut time) = if time > 0.0 {
(String::new(), time)
} else {
("-".to_string(), -time)
};
if time < 30.0 {
return format!("{sign}{time:.2} s");
}
time += 0.5; let mut h = (time / 3600.0).trunc();
time -= h * 3600.0;
let m = (time / 60.0).trunc();
time -= m * 60.0;
if h > 24.0 {
let d = (h / 24.0).trunc();
h -= d * 24.0;
sign = format!("{sign}{d} days ");
}
format!("{sign}{}:{:02}:{:02}", h as i64, m as i64, time as i64)
}
fn decode_timestamp(val: &[u8]) -> String {
let fields: Vec<u32> = if val.len() >= 2 {
std::iter::once(u16::from_be_bytes([val[0], val[1]]) as u32)
.chain(val[2..].iter().map(|&b| b as u32))
.collect()
} else {
Vec::new()
};
let max = [3000u32, 12, 31, 24, 59, 59, 249];
let mut checked = 0;
for f in &fields {
if checked >= max.len() || *f > max[checked] {
break;
}
checked += 1;
}
if checked < max.len() {
let hex: String = val.iter().map(|b| format!("{b:02x}")).collect();
return format!("Invalid (0x{hex})");
}
format!(
"{:04}:{:02}:{:02} {:02}:{:02}:{:02}.{:03}",
fields[0],
fields[1],
fields[2],
fields[3],
fields[4],
fields[5],
fields[6] * 4,
)
}
fn decode_product_version(val: &[u8]) -> String {
let mut a: Vec<u16> = val
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
while a.len() < 5 {
a.push(0);
}
let release = match a[4] {
0 => "unknown".to_string(),
1 => "released".to_string(),
2 => "debug".to_string(),
3 => "patched".to_string(),
4 => "beta".to_string(),
5 => "private build".to_string(),
n => format!("unknown {n}"),
};
format!("{}.{}.{}.{} {}", a[0], a[1], a[2], a[3], release)
}
fn decode_utf16(val: &[u8]) -> String {
let chars: Vec<u16> = val
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&chars)
.trim_end_matches('\0')
.to_string()
}
fn decode_component_data_def(val: &[u8]) -> String {
if val.len() != 16 {
return val.iter().map(|b| format!("{b:02x}")).collect();
}
let text = if val[0] & 0x80 == 0 {
ul_dotted(val)
} else {
let mut swapped = [0u8; 16];
swapped[..8].copy_from_slice(&val[8..]);
swapped[8..].copy_from_slice(&val[..8]);
guid(&swapped)
};
COMPONENT_DATA_DEF
.iter()
.find(|(k, _)| *k == text)
.map(|(_, v)| (*v).to_string())
.unwrap_or(text)
}
fn ul_dotted(b: &[u8]) -> String {
let h: String = b.iter().map(|x| format!("{x:02x}")).collect();
format!(
"{}.{}.{}.{}.{}",
&h[0..8],
&h[8..12],
&h[12..16],
&h[16..24],
&h[24..32]
)
}
fn guid(b: &[u8]) -> String {
let h: String = b.iter().map(|x| format!("{x:02x}")).collect();
format!(
"{}-{}-{}-{}-{}",
&h[0..8],
&h[8..12],
&h[12..16],
&h[16..20],
&h[20..32]
)
}
fn hex32(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn uid16(b: &[u8]) -> [u8; 16] {
let mut out = [0u8; 16];
out.copy_from_slice(&b[..16]);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tables_are_sorted() {
assert!(LOCAL_SETS.windows(2).all(|w| w[0].0 < w[1].0));
assert!(STRONG_REFS.windows(2).all(|w| w[0].0 < w[1].0));
assert!(TAGS.windows(2).all(|w| w[0].0 < w[1].0));
}
#[test]
fn duration_formatting_matches_exiftool() {
assert_eq!(convert_duration(0.0), "0 s");
assert_eq!(convert_duration(1.5), "1.50 s");
assert_eq!(convert_duration(-1.5), "-1.50 s");
assert_eq!(convert_duration(90.0), "0:01:30");
assert_eq!(convert_duration(90000.0), "1 days 1:00:00");
}
#[test]
fn numbers_print_like_perl() {
assert_eq!(format_number(1.0), "1");
assert_eq!(format_number(7872.0), "7872");
assert_eq!(format_number(30000.0 / 1001.0), "29.97002997003");
}
}