Skip to main content

boox_note_parser/
resource.rs

1use 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}