Skip to main content

jw_hwp_core/
summary.rs

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}