1use crate::container::Container;
2use crate::error::Error;
3use crate::ole_property::{parse, PropValue};
4
5pub const STREAM_NAME: &str = "/\u{0005}HwpSummaryInformation";
6
7pub const PIDSI_TITLE: u32 = 0x02;
8pub const PIDSI_SUBJECT: u32 = 0x03;
9pub const PIDSI_AUTHOR: u32 = 0x04;
10pub const PIDSI_KEYWORDS: u32 = 0x05;
11pub const PIDSI_COMMENTS: u32 = 0x06;
12pub const PIDSI_LAST_AUTHOR: u32 = 0x08;
13pub const PIDSI_REVISION: u32 = 0x09;
14pub const PIDSI_LAST_PRINTED: u32 = 0x0B;
15pub const PIDSI_CREATE_DTM: u32 = 0x0C;
16pub const PIDSI_LAST_SAVE_DTM: u32 = 0x0D;
17pub const PIDSI_PAGE_COUNT: u32 = 0x0E;
18pub const HWPPIDSI_DATE_STR: u32 = 0x14;
19pub const HWPPIDSI_PARACOUNT: u32 = 0x15;
20
21#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
22pub struct Metadata {
23 pub title: Option<String>,
24 pub subject: Option<String>,
25 pub author: Option<String>,
26 pub keywords: Option<String>,
27 pub comments: Option<String>,
28 pub last_author: Option<String>,
29 pub revision: Option<String>,
30 pub date_string: Option<String>,
31 pub page_count: Option<i32>,
32 pub para_count: Option<i32>,
33 pub created_at: Option<String>,
34 pub modified_at: Option<String>,
35 pub printed_at: Option<String>,
36}
37
38pub fn read(container: &mut Container) -> Result<Metadata, Error> {
39 let bytes = match container.read_raw_stream(STREAM_NAME) {
40 Ok(b) => b,
41 Err(Error::MissingStream(_)) => return Ok(Metadata::default()),
42 Err(e) => return Err(e),
43 };
44 let sections = parse(&bytes)?;
45 let mut md = Metadata::default();
46 for section in sections {
47 for (pid, val) in section.properties {
48 match (pid, &val) {
49 (PIDSI_TITLE, PropValue::String(s)) => md.title = Some(s.clone()),
50 (PIDSI_SUBJECT, PropValue::String(s)) => md.subject = Some(s.clone()),
51 (PIDSI_AUTHOR, PropValue::String(s)) => md.author = Some(s.clone()),
52 (PIDSI_KEYWORDS, PropValue::String(s)) => md.keywords = Some(s.clone()),
53 (PIDSI_COMMENTS, PropValue::String(s)) => md.comments = Some(s.clone()),
54 (PIDSI_LAST_AUTHOR, PropValue::String(s)) => md.last_author = Some(s.clone()),
55 (PIDSI_REVISION, PropValue::String(s)) => md.revision = Some(s.clone()),
56 (HWPPIDSI_DATE_STR, PropValue::String(s)) => md.date_string = Some(s.clone()),
57 (PIDSI_PAGE_COUNT, PropValue::I4(v)) => md.page_count = Some(*v),
58 (HWPPIDSI_PARACOUNT, PropValue::I4(v)) => md.para_count = Some(*v),
59 (PIDSI_CREATE_DTM, PropValue::FileTime(t)) => md.created_at = filetime_to_iso(*t),
60 (PIDSI_LAST_SAVE_DTM, PropValue::FileTime(t)) => {
61 md.modified_at = filetime_to_iso(*t)
62 }
63 (PIDSI_LAST_PRINTED, PropValue::FileTime(t)) => md.printed_at = filetime_to_iso(*t),
64 _ => {}
65 }
66 }
67 }
68 Ok(md)
69}
70
71pub fn filetime_to_iso(ticks_100ns: u64) -> Option<String> {
72 if ticks_100ns == 0 {
73 return None;
74 }
75 const UNIX_EPOCH_OFFSET_SECONDS: u64 = 11_644_473_600;
76 const TICKS_PER_SEC: u64 = 10_000_000;
77 let secs_since_filetime_epoch = ticks_100ns / TICKS_PER_SEC;
78 if secs_since_filetime_epoch < UNIX_EPOCH_OFFSET_SECONDS {
79 return None;
80 }
81 let secs = secs_since_filetime_epoch - UNIX_EPOCH_OFFSET_SECONDS;
82 Some(format_unix_seconds(secs))
83}
84
85fn format_unix_seconds(secs: u64) -> String {
86 let days = (secs / 86_400) as i64;
87 let rem = secs % 86_400;
88 let (h, m, s) = (
89 (rem / 3600) as u32,
90 ((rem / 60) % 60) as u32,
91 (rem % 60) as u32,
92 );
93 let z = days + 719_468;
94 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
95 let doe = (z - era * 146_097) as u64;
96 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
97 let y = yoe as i64 + era * 400;
98 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
99 let mp = (5 * doy + 2) / 153;
100 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
101 let mo = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
102 let yr = y + if mo <= 2 { 1 } else { 0 };
103 format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", yr, mo, d, h, m, s)
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn filetime_zero_is_none() {
112 assert_eq!(filetime_to_iso(0), None);
113 }
114
115 #[test]
116 fn filetime_unix_epoch_is_1970() {
117 let ft = 11_644_473_600u64 * 10_000_000;
118 assert_eq!(filetime_to_iso(ft).as_deref(), Some("1970-01-01T00:00:00Z"));
119 }
120
121 #[test]
122 fn filetime_known_date() {
123 let secs_since_unix = 1_582_979_696u64;
124 let ft = (secs_since_unix + 11_644_473_600) * 10_000_000;
125 assert_eq!(filetime_to_iso(ft).as_deref(), Some("2020-02-29T12:34:56Z"));
126 }
127}