boox_note_parser/
resource.rs1use chrono::{DateTime, Utc};
2
3use crate::{
4 error::{Error, Result},
5 id::ResourceUuid,
6 utils::convert_timestamp_to_datetime,
7};
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct ResourceRecord {
11 pub path: String,
12 pub resource_id: ResourceUuid,
13 pub timestamp_millis: u64,
14 pub timestamp: DateTime<Utc>,
15 pub size_bytes: u64,
16 pub payload: ResourcePayload,
17}
18
19impl ResourceRecord {
20 pub fn read(
21 path: impl Into<String>,
22 mut reader: impl std::io::Read,
23 size_bytes: u64,
24 ) -> Result<Self> {
25 let path = path.into();
26 let file_name = file_name_from_path(&path)?;
27 let (resource_id, timestamp_millis) = parse_resource_file_name(file_name)?;
28 let timestamp = convert_timestamp_to_datetime(timestamp_millis)?;
29
30 let payload = if size_bytes == 0 {
31 ResourcePayload::Empty
32 } else {
33 let mut buf = Vec::new();
34 reader.read_to_end(&mut buf)?;
35 if buf.is_empty() {
36 ResourcePayload::Empty
37 } else {
38 ResourcePayload::Raw(buf)
39 }
40 };
41
42 Ok(Self {
43 path,
44 resource_id,
45 timestamp_millis,
46 timestamp,
47 size_bytes,
48 payload,
49 })
50 }
51}
52
53#[derive(Debug, Clone, PartialEq)]
54pub enum ResourcePayload {
55 Empty,
56 Raw(Vec<u8>),
57}
58
59pub fn parse_resource_file_name(file_name: &str) -> Result<(ResourceUuid, u64)> {
60 let mut segments = file_name.split('#');
61 let resource_id = segments.next();
62 let timestamp = segments.next();
63 let extra = segments.next();
64
65 if resource_id.is_none() || timestamp.is_none() || extra.is_some() {
66 return Err(Error::InvalidArchiveEntryName(file_name.to_string()));
67 }
68
69 let resource_id = ResourceUuid::from_str(resource_id.unwrap())?;
70 let timestamp_millis = timestamp.unwrap().parse::<u64>().map_err(|e| {
71 Error::InvalidTimestampFormat(format!(
72 "Failed to parse resource timestamp in '{}': {}",
73 file_name, e
74 ))
75 })?;
76
77 Ok((resource_id, timestamp_millis))
78}
79
80fn file_name_from_path(path: &str) -> Result<&str> {
81 path.rsplit('/')
82 .next()
83 .filter(|name| !name.is_empty())
84 .ok_or_else(|| Error::InvalidArchiveEntryName(path.to_string()))
85}
86
87#[cfg(test)]
88mod tests {
89 use std::io::Cursor;
90
91 use super::{ResourcePayload, ResourceRecord, parse_resource_file_name};
92 use crate::id::ResourceUuid;
93
94 #[test]
95 fn parses_resource_filename() {
96 let (resource_id, timestamp) =
97 parse_resource_file_name("b199553d-a169-4f7f-ae01-f9312c9cbd3c#1753456222446").unwrap();
98
99 assert_eq!(
100 resource_id,
101 ResourceUuid::from_str("b199553d-a169-4f7f-ae01-f9312c9cbd3c").unwrap()
102 );
103 assert_eq!(timestamp, 1753456222446);
104 }
105
106 #[test]
107 fn parses_empty_resource_payload() {
108 let record = ResourceRecord::read(
109 "resource/pb/b199553d-a169-4f7f-ae01-f9312c9cbd3c#1753456222446",
110 Cursor::new(Vec::<u8>::new()),
111 0,
112 )
113 .unwrap();
114
115 assert!(matches!(record.payload, ResourcePayload::Empty));
116 assert_eq!(record.size_bytes, 0);
117 }
118}