use crate::util::u16le;
#[derive(Debug, Clone, Default)]
struct StyleDescription {
sti: u16,
sgc: u8,
istd_base: u16,
name: String,
}
#[derive(Debug, Default)]
pub(crate) struct StyleSheet {
heading: Vec<Option<u8>>,
names: Vec<String>,
}
impl StyleSheet {
pub(crate) fn heading_level(&self, istd: u16) -> Option<u8> {
self.heading.get(istd as usize).copied().flatten()
}
pub(crate) fn name(&self, istd: u16) -> Option<&str> {
self.names
.get(istd as usize)
.map(String::as_str)
.filter(|s| !s.is_empty())
}
pub(crate) fn parse(table: &[u8], fc_stshf: usize, lcb_stshf: usize) -> StyleSheet {
let empty = StyleSheet::default();
if lcb_stshf < 8 {
return empty;
}
let Some(stsh) = table.get(fc_stshf..fc_stshf.saturating_add(lcb_stshf)) else {
return empty;
};
let (Some(cb_stshi), Some(cstd), Some(cb_std_base)) =
(u16le(stsh, 0), u16le(stsh, 2), u16le(stsh, 4))
else {
return empty;
};
let base_len = if cb_std_base == 18 { 18 } else { 10 } as usize;
let names_written = u16le(stsh, 6).unwrap_or(0) & 1 != 0;
let mut p = 2usize.saturating_add(cb_stshi as usize);
let mut descs: Vec<Option<StyleDescription>> = Vec::with_capacity(cstd as usize);
for _ in 0..cstd {
let Some(cb_std) = u16le(stsh, p) else { break };
p += 2;
if cb_std == 0 {
descs.push(None); continue;
}
let cb_std = cb_std as usize;
let Some(std) = stsh.get(p..p.saturating_add(cb_std)) else {
break;
};
p += cb_std;
descs.push(parse_std(std, base_len, names_written));
}
let n = descs.len();
let mut heading = vec![None; n];
let mut names = vec![String::new(); n];
let mut visited = vec![0u32; n];
for istd in 0..n {
let gen = istd as u32 + 1;
heading[istd] = resolve_level(&descs, istd, &mut visited, gen, 0);
if let Some(d) = &descs[istd] {
names[istd] = d.name.clone();
}
}
StyleSheet { heading, names }
}
}
fn parse_std(std: &[u8], base_len: usize, names_written: bool) -> Option<StyleDescription> {
let sti = u16le(std, 0)? & 0x0FFF;
let grf = u16le(std, 2)?;
let sgc = (grf & 0x000F) as u8;
let istd_base = (grf >> 4) & 0x0FFF;
let mut name = String::new();
if names_written {
if let Some(cch) = u16le(std, base_len) {
let start = base_len + 2;
if let Some(bytes) = std.get(start..start + cch as usize * 2) {
name = utf16le(bytes);
}
}
}
Some(StyleDescription {
sti,
sgc,
istd_base,
name,
})
}
const MAX_STYLE_BASE_DEPTH: usize = 64;
fn resolve_level(
descs: &[Option<StyleDescription>],
istd: usize,
visited: &mut [u32],
gen: u32,
depth: usize,
) -> Option<u8> {
if depth > MAX_STYLE_BASE_DEPTH
|| istd >= descs.len()
|| visited.get(istd).copied().unwrap_or(gen) == gen
{
return None;
}
visited[istd] = gen;
let Some(d) = &descs[istd] else { return None };
if (1..=9).contains(&d.sti) {
return Some(d.sti as u8);
}
if d.sti == 0 {
return None; }
if d.sgc == 1 && d.istd_base != 0x0FFF && d.istd_base as usize != istd {
if let Some(n) = resolve_level(descs, d.istd_base as usize, visited, gen, depth + 1) {
return Some(n);
}
}
heading_from_name(&d.name)
}
pub(crate) fn heading_from_name(name: &str) -> Option<u8> {
let t = name.trim();
if let Some(rest) = t.strip_prefix("제목") {
if let Ok(n) = rest.trim().parse::<u8>() {
if (1..=9).contains(&n) {
return Some(n);
}
}
}
let lower = t.to_lowercase();
if let Some(rest) = lower.strip_prefix("heading") {
if let Ok(n) = rest.trim().parse::<u8>() {
if (1..=9).contains(&n) {
return Some(n);
}
}
}
None
}
fn utf16le(b: &[u8]) -> String {
let units: Vec<u16> = b
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&units)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn heading_name_matching() {
assert_eq!(heading_from_name("Heading 1"), Some(1));
assert_eq!(heading_from_name("heading3"), Some(3));
assert_eq!(heading_from_name("제목 2"), Some(2));
assert_eq!(heading_from_name("제목3"), Some(3));
assert_eq!(heading_from_name("Normal"), None);
assert_eq!(heading_from_name("본문"), None);
}
#[test]
fn resolve_by_sti_and_base_chain() {
let descs = vec![
Some(StyleDescription {
sti: 0,
sgc: 1,
istd_base: 0x0FFF,
name: "Normal".into(),
}),
Some(StyleDescription {
sti: 1,
sgc: 1,
istd_base: 0,
name: "Heading 1".into(),
}),
Some(StyleDescription {
sti: 0x0FFE,
sgc: 1,
istd_base: 1,
name: "MyHead".into(),
}),
Some(StyleDescription {
sti: 0x0FFE,
sgc: 1,
istd_base: 0x0FFF,
name: "제목 4".into(),
}),
];
let n = descs.len();
let lvl = |i| resolve_level(&descs, i, &mut vec![0u32; n], 1, 0);
assert_eq!(lvl(0), None);
assert_eq!(lvl(1), Some(1));
assert_eq!(lvl(2), Some(1));
assert_eq!(lvl(3), Some(4));
}
#[test]
fn cycle_guard() {
let descs = vec![
Some(StyleDescription {
sti: 0x0FFE,
sgc: 1,
istd_base: 1,
name: String::new(),
}),
Some(StyleDescription {
sti: 0x0FFE,
sgc: 1,
istd_base: 0,
name: String::new(),
}),
];
assert_eq!(resolve_level(&descs, 0, &mut [0u32; 2], 1, 0), None);
}
#[test]
fn long_acyclic_base_chain_does_not_stack_overflow() {
let n = 200_000usize;
let mut descs: Vec<Option<StyleDescription>> = (0..n)
.map(|i| {
Some(StyleDescription {
sti: 0x0FFE,
sgc: 1,
istd_base: (i + 1) as u16, name: String::new(),
})
})
.collect();
descs[n - 1] = Some(StyleDescription {
sti: 0x0FFE,
sgc: 1,
istd_base: 0x0FFF,
name: "Heading 3".into(),
});
let mut visited = vec![0u32; n];
assert_eq!(resolve_level(&descs, 0, &mut visited, 1, 0), None);
}
#[test]
fn huge_style_count_resolves_in_linear_time() {
let cstd = 65535u16;
let mut stsh = Vec::new();
stsh.extend_from_slice(&6u16.to_le_bytes()); stsh.extend_from_slice(&cstd.to_le_bytes()); stsh.extend_from_slice(&10u16.to_le_bytes()); stsh.extend_from_slice(&0u16.to_le_bytes()); stsh.resize(stsh.len() + cstd as usize * 2, 0u8); let ss = StyleSheet::parse(&stsh, 0, stsh.len());
assert_eq!(ss.heading.len(), cstd as usize);
assert!(ss.heading.iter().all(|h| h.is_none()));
}
}