use crate::note::NOTE_HEADER_BYTES;
use crate::time::Timedate;
pub const ITEM_DESCRIPTOR_BYTES: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldKind {
Text,
TextList,
Rfc822Text,
Number,
NumberRange,
Time,
TimeRange,
Formula,
RichText,
Object,
Html,
MimePart,
Unknown,
}
impl FieldKind {
pub fn label(self) -> &'static str {
match self {
FieldKind::Text => "Text",
FieldKind::TextList => "Text list",
FieldKind::Rfc822Text => "RFC822 text",
FieldKind::Number => "Number",
FieldKind::NumberRange => "Number range",
FieldKind::Time => "Time",
FieldKind::TimeRange => "Time range",
FieldKind::Formula => "Formula",
FieldKind::RichText => "Rich text",
FieldKind::Object => "Attachment / object",
FieldKind::Html => "HTML",
FieldKind::MimePart => "MIME part",
FieldKind::Unknown => "Unknown",
}
}
}
pub fn field_kind(item_class: u8, item_type: u8) -> FieldKind {
match item_class {
0x05 => match item_type {
0x01 => FieldKind::TextList,
0x02 => FieldKind::Rfc822Text,
_ => FieldKind::Text,
},
0x03 => match item_type {
0x01 => FieldKind::NumberRange,
_ => FieldKind::Number,
},
0x04 => match item_type {
0x01 => FieldKind::TimeRange,
_ => FieldKind::Time,
},
0x06 => FieldKind::Formula,
0x00 => match item_type {
0x01 => FieldKind::RichText,
0x03 => FieldKind::Object,
0x15 => FieldKind::Html,
0x18 => FieldKind::MimePart,
_ => FieldKind::Unknown,
},
_ => FieldKind::Unknown,
}
}
#[derive(Debug, Clone, Copy)]
pub struct NoteItem<'a> {
pub name_id: u16,
pub type_flags: u16,
pub value: &'a [u8],
}
impl<'a> NoteItem<'a> {
pub fn as_text(&self) -> String {
self.value
.iter()
.map(|&b| if (0x20..0x7f).contains(&b) { b as char } else { '.' })
.collect()
}
pub fn is_printable_text(&self) -> bool {
!self.value.is_empty()
&& self
.value
.iter()
.all(|&b| (0x20..0x7f).contains(&b) || b == b'\t')
}
pub fn display_value(&self) -> String {
if self.value.is_empty() {
return String::new();
}
if self.is_printable_text() {
return self.as_text();
}
match self.value.len() {
8 => {
if let Ok(td) = Timedate::from_bytes(self.value) {
if let Some(clock) = td.as_clock() {
return clock.to_iso_8601();
}
}
let bytes: [u8; 8] = self.value.try_into().expect("len checked");
let f = f64::from_le_bytes(bytes);
if f == 0.0 || (f.is_finite() && f.abs() >= 1e-4 && f.abs() < 1e15) {
if f.fract() == 0.0 {
return format!("{}", f as i64);
}
return format!("{f}");
}
hex_summary(self.value)
}
4 => format!(
"{}",
u32::from_le_bytes(self.value.try_into().expect("len checked"))
),
2 => {
let v = u16::from_le_bytes([self.value[0], self.value[1]]);
if is_type_word(v) {
String::new()
} else {
format!("{v}")
}
}
1 => format!("{}", self.value[0]),
_ => hex_summary(self.value),
}
}
}
fn is_type_word(v: u16) -> bool {
matches!(
v,
0x0300 | 0x0301 | 0x0400 | 0x0401 | 0x0500 | 0x0501 | 0x0600 | 0x0601 | 0x0700
)
}
impl NoteItem<'_> {
pub fn render(&self, kind: FieldKind) -> String {
if self.value.is_empty() {
return String::new();
}
if self.value.len() == 2 && is_type_word(u16::from_le_bytes([self.value[0], self.value[1]])) {
return String::new();
}
match kind {
FieldKind::Text
| FieldKind::TextList
| FieldKind::Rfc822Text
| FieldKind::Formula
| FieldKind::Html
| FieldKind::MimePart => {
if self.is_printable_text() {
self.as_text()
} else {
hex_summary(self.value)
}
}
FieldKind::Number | FieldKind::NumberRange => {
if self.value.len() >= 8 {
let b: [u8; 8] = self.value[..8].try_into().expect("len checked");
let f = f64::from_le_bytes(b);
if f.is_finite() && f.fract() == 0.0 && f.abs() < 1e15 {
format!("{}", f as i64)
} else if f.is_finite() {
format!("{f}")
} else {
hex_summary(self.value)
}
} else {
self.display_value()
}
}
FieldKind::Time | FieldKind::TimeRange => {
if self.value.len() >= 8 {
if let Ok(td) = Timedate::from_bytes(&self.value[..8]) {
if let Some(c) = td.as_clock() {
return c.to_iso_8601();
}
}
hex_summary(self.value)
} else {
self.display_value()
}
}
FieldKind::RichText => "(rich text)".to_string(),
FieldKind::Object => "(attachment / object)".to_string(),
FieldKind::Unknown => self.display_value(),
}
}
}
fn hex_summary(b: &[u8]) -> String {
let mut s = String::new();
for (i, x) in b.iter().take(16).enumerate() {
if i > 0 {
s.push(' ');
}
s.push_str(&format!("{x:02x}"));
}
if b.len() > 16 {
s.push_str(" ...");
}
s
}
pub fn parse_items(record: &[u8], number_of_note_items: u16) -> Vec<NoteItem<'_>> {
let count = number_of_note_items as usize;
let table_end = NOTE_HEADER_BYTES + count * ITEM_DESCRIPTOR_BYTES;
if record.len() < table_end {
return Vec::new();
}
let mut items = Vec::with_capacity(count);
let mut cursor = table_end;
for i in 0..count {
let d = NOTE_HEADER_BYTES + i * ITEM_DESCRIPTOR_BYTES;
let name_id = u16::from_le_bytes([record[d], record[d + 1]]);
let type_flags = u16::from_le_bytes([record[d + 2], record[d + 3]]);
let value_size = u16::from_le_bytes([record[d + 4], record[d + 5]]) as usize;
let Some(value) = record.get(cursor..cursor + value_size) else {
break;
};
cursor += value_size;
items.push(NoteItem {
name_id,
type_flags,
value,
});
}
items
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic(items: &[(u16, u16, &[u8])]) -> Vec<u8> {
let mut buf = vec![0u8; NOTE_HEADER_BYTES];
for (name_id, type_flags, value) in items {
buf.extend_from_slice(&name_id.to_le_bytes());
buf.extend_from_slice(&type_flags.to_le_bytes());
buf.extend_from_slice(&(value.len() as u16).to_le_bytes());
buf.extend_from_slice(&0u16.to_le_bytes());
}
for (_, _, value) in items {
buf.extend_from_slice(value);
}
buf
}
#[test]
fn parses_packed_text_values() {
let rec = synthetic(&[
(0x09A1, 0x000C, b"613 Goolagong Pde."),
(0x07E5, 0x020C, b"a@b.org"),
(0x0036, 0x0004, b""), ]);
let items = parse_items(&rec, 3);
assert_eq!(items.len(), 3);
assert_eq!(items[0].name_id, 0x09A1);
assert_eq!(items[0].as_text(), "613 Goolagong Pde.");
assert!(items[0].is_printable_text());
assert_eq!(items[1].as_text(), "a@b.org");
assert!(items[2].value.is_empty());
}
#[test]
fn truncated_record_stops_cleanly() {
let mut rec = synthetic(&[(0x0001, 0x000C, b"hello world")]);
rec.truncate(rec.len() - 4); let items = parse_items(&rec, 1);
assert!(items.is_empty());
}
#[test]
fn zero_items_yields_empty() {
let rec = vec![0u8; NOTE_HEADER_BYTES];
assert!(parse_items(&rec, 0).is_empty());
}
#[test]
fn display_value_renders_by_shape() {
let rec = synthetic(&[
(1, 0x0C, b"hello"), (2, 0x04, &0x0500u16.to_le_bytes()), (3, 0x04, &42u16.to_le_bytes()), (4, 0x04, &[0x99; 6]), ]);
let items = parse_items(&rec, 4);
assert_eq!(items[0].display_value(), "hello");
assert_eq!(items[1].display_value(), ""); assert_eq!(items[2].display_value(), "42");
assert_eq!(items[3].display_value(), "99 99 99 99 99 99");
}
}