1use std::sync::Arc;
26
27use crate::error::{DocError, Result};
28use crate::model::{Block, BlockKind, Document, Page};
29use crate::plugin::{FormatPlugin, ParseCapability, RawDoc, RawPage, RawTextItem};
30use crate::registry::Registry;
31
32pub const MAGIC: &[u8] = b"{\\rtf";
34
35pub const SKIP_DESTS: &[&str] = &[
37 "fonttbl",
38 "colortbl",
39 "stylesheet",
40 "info",
41 "pict",
42 "object",
43 "header",
44 "footer",
45 "headerl",
46 "headerr",
47 "headerf",
48 "footerl",
49 "footerr",
50 "footerf",
51 "footnote",
52 "annotation",
53 "atnid",
54 "atnauthor",
55 "fldinst",
56 "themedata",
57 "datastore",
58 "latentstyles",
59 "listtable",
60 "listoverridetable",
61 "rsidtbl",
62 "generator",
63 "xmlnstbl",
64 "filetbl",
65 "revtbl",
66 "pn",
67 "sp",
68 "sn",
69 "sv",
70 "shpinst",
71 "background",
72 "do",
73 "buptim",
74 "userprops",
75];
76
77fn hexval(b: u8) -> Option<u8> {
78 match b {
79 b'0'..=b'9' => Some(b - b'0'),
80 b'a'..=b'f' => Some(b - b'a' + 10),
81 b'A'..=b'F' => Some(b - b'A' + 10),
82 _ => None,
83 }
84}
85
86fn push_char(buf: &mut Vec<u8>, ch: char) {
87 let mut tmp = [0u8; 4];
88 buf.extend_from_slice(ch.encode_utf8(&mut tmp).as_bytes());
89}
90
91fn cp1252_char(b: u8) -> char {
95 match b {
96 0x80 => '\u{20AC}',
97 0x81 => '\u{FFFD}',
98 0x82 => '\u{201A}',
99 0x83 => '\u{0192}',
100 0x84 => '\u{201E}',
101 0x85 => '\u{2026}',
102 0x86 => '\u{2020}',
103 0x87 => '\u{2021}',
104 0x88 => '\u{02C6}',
105 0x89 => '\u{2030}',
106 0x8A => '\u{0160}',
107 0x8B => '\u{2039}',
108 0x8C => '\u{0152}',
109 0x8D => '\u{FFFD}',
110 0x8E => '\u{017D}',
111 0x8F => '\u{FFFD}',
112 0x90 => '\u{FFFD}',
113 0x91 => '\u{2018}',
114 0x92 => '\u{2019}',
115 0x93 => '\u{201C}',
116 0x94 => '\u{201D}',
117 0x95 => '\u{2022}',
118 0x96 => '\u{2013}',
119 0x97 => '\u{2014}',
120 0x98 => '\u{02DC}',
121 0x99 => '\u{2122}',
122 0x9A => '\u{0161}',
123 0x9B => '\u{203A}',
124 0x9C => '\u{0153}',
125 0x9D => '\u{FFFD}',
126 0x9E => '\u{017E}',
127 0x9F => '\u{0178}',
128 _ => b as char, }
130}
131
132fn flush(buf: &mut Vec<u8>, pending_high: &mut Option<u32>, out: &mut Vec<String>) -> Result<()> {
133 if pending_high.is_some() {
134 push_char(buf, '\u{FFFD}');
135 *pending_high = None;
136 }
137 let text = std::str::from_utf8(buf).map_err(|_| {
138 DocError::corrupt("RTF 正文含非法 UTF-8 字节(ansicpg65001 下 \\'hh 序列不完整)")
139 })?;
140 let trimmed = text.trim();
141 let paragraph = trimmed.to_string();
142 buf.clear();
143 if !paragraph.is_empty() {
144 out.push(paragraph);
145 }
146 Ok(())
147}
148
149pub fn extract_paragraphs(raw: &[u8]) -> Result<Vec<String>> {
151 let data = raw.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(raw);
152 let mut start = 0;
153 while start < data.len() && matches!(data[start], b' ' | b'\t' | b'\r' | b'\n') {
154 start += 1;
155 }
156 let data = &data[start..];
157 if !data.starts_with(MAGIC) {
158 return Err(DocError::corrupt("不是合法的 RTF(缺少 {\\rtf 头)"));
159 }
160
161 let n = data.len();
162 let mut paragraphs: Vec<String> = Vec::new();
163 let mut buf: Vec<u8> = Vec::new();
164 let mut codepage: Option<i32> = None;
165 let mut uc: i32 = 1;
166 let mut skip_left: i32 = 0;
167 let mut pending_high: Option<u32> = None;
168 let mut depth: usize = 0;
169 let mut skip_from: Option<usize> = None;
170 let mut dest_pending: Vec<bool> = Vec::new();
171
172 let mut i = 0usize;
173 while i < n {
174 let c = data[i];
175
176 if c == b'{' {
177 depth += 1;
178 dest_pending.push(true);
179 i += 1;
180 continue;
181 }
182
183 if c == b'}' {
184 if let Some(s) = skip_from {
185 if s >= depth {
186 skip_from = None;
187 }
188 }
189 if depth > 0 {
190 depth -= 1;
191 dest_pending.pop();
192 }
193 i += 1;
194 continue;
195 }
196
197 if c == b'\\' {
198 if i + 1 >= n {
199 i += 1;
200 continue;
201 }
202 let nxt = data[i + 1];
203
204 if nxt.is_ascii_alphabetic() {
206 let mut j = i + 1;
207 while j < n && data[j].is_ascii_alphabetic() {
208 j += 1;
209 }
210 let word = String::from_utf8_lossy(&data[i + 1..j]).to_ascii_lowercase();
211 let mut k = j;
212 let mut neg = false;
213 if k < n && data[k] == b'-' {
214 neg = true;
215 k += 1;
216 }
217 let digits_at = k;
218 while k < n && data[k].is_ascii_digit() {
219 k += 1;
220 }
221 let mut param: Option<i32> = None;
222 if k > digits_at {
223 let v: i32 = String::from_utf8_lossy(&data[digits_at..k])
224 .parse()
225 .unwrap_or(0);
226 param = Some(if neg { -v } else { v });
227 }
228 if k < n && data[k] == b' ' {
229 k += 1;
230 }
231 i = k;
232
233 let first_in_group = dest_pending.last().copied().unwrap_or(false);
234 if first_in_group {
235 if let Some(last) = dest_pending.last_mut() {
236 *last = false;
237 }
238 if SKIP_DESTS.contains(&word.as_str()) {
239 if skip_from.is_none() {
240 skip_from = Some(depth);
241 }
242 continue;
243 }
244 }
245 if skip_from.is_some() {
246 if word == "bin" {
247 if let Some(p) = param {
248 if p > 0 {
249 i = (i + p as usize).min(n);
250 }
251 }
252 }
253 continue;
254 }
255
256 match word.as_str() {
257 "par" | "row" | "sect" => {
258 flush(&mut buf, &mut pending_high, &mut paragraphs)?;
259 }
260 "line" => buf.push(b'\n'),
261 "tab" | "cell" => buf.push(b'\t'),
262 "uc" => uc = param.unwrap_or(0).max(0),
263 "ansicpg" => {
264 if let Some(p) = param {
265 codepage = Some(p);
266 }
267 }
268 "bin" => {
269 if let Some(p) = param {
270 if p > 0 {
271 i = (i + p as usize).min(n);
272 }
273 }
274 }
275 "u" => {
276 let mut raw_cp = param.unwrap_or(0);
277 if raw_cp < 0 {
278 raw_cp += 0x10000;
279 }
280 let cp = raw_cp as u32;
281 if (0xD800..=0xDBFF).contains(&cp) {
282 if pending_high.is_some() {
283 push_char(&mut buf, '\u{FFFD}');
284 }
285 pending_high = Some(cp);
286 } else if (0xDC00..=0xDFFF).contains(&cp) && pending_high.is_some() {
287 let hi = pending_high.take().unwrap_or(0xD800);
288 let full = 0x10000 + ((hi - 0xD800) << 10) + (cp - 0xDC00);
289 push_char(&mut buf, char::from_u32(full).unwrap_or('\u{FFFD}'));
290 } else {
291 if pending_high.is_some() {
292 push_char(&mut buf, '\u{FFFD}');
293 pending_high = None;
294 }
295 let ch = if (0xDC00..=0xDFFF).contains(&cp) {
296 '\u{FFFD}'
297 } else {
298 char::from_u32(cp).unwrap_or('\u{FFFD}')
299 };
300 push_char(&mut buf, ch);
301 }
302 skip_left = uc;
303 }
304 _ => {}
305 }
306 continue;
307 }
308
309 if nxt == b'\n' || nxt == b'\r' {
311 let mut j = i + 1;
312 while j < n && (data[j] == b'\n' || data[j] == b'\r') {
313 j += 1;
314 }
315 i = j;
316 if skip_from.is_none() {
317 flush(&mut buf, &mut pending_high, &mut paragraphs)?;
318 }
319 continue;
320 }
321
322 if nxt == b'\'' {
323 if i + 3 >= n {
324 return Err(DocError::corrupt("RTF 的 \\'hh 转义不完整"));
325 }
326 let hi = hexval(data[i + 2])
327 .ok_or_else(|| DocError::corrupt("RTF 的 \\'hh 转义含非十六进制字符"))?;
328 let lo = hexval(data[i + 3])
329 .ok_or_else(|| DocError::corrupt("RTF 的 \\'hh 转义含非十六进制字符"))?;
330 i += 4;
331 if skip_from.is_some() {
332 continue;
333 }
334 if skip_left > 0 {
335 skip_left -= 1;
336 continue;
337 }
338 let byte = hi * 16 + lo;
339 match codepage {
340 Some(65001) => buf.push(byte), Some(cp) if cp != 1252 => {
342 return Err(DocError::corrupt(format!(
343 "RTF 声明了不支持的代码页 {cp}(仅支持 1252 / 65001);请另存为 Unicode RTF"
344 )));
345 }
346 _ => push_char(&mut buf, cp1252_char(byte)),
347 }
348 continue;
349 }
350
351 if nxt == b'*' {
352 let first_in_group = dest_pending.last().copied().unwrap_or(false);
353 if first_in_group {
354 if let Some(last) = dest_pending.last_mut() {
355 *last = false;
356 }
357 if skip_from.is_none() {
358 skip_from = Some(depth);
359 }
360 }
361 i += 2;
362 continue;
363 }
364
365 if skip_from.is_some() {
366 i += 2;
367 continue;
368 }
369
370 match nxt {
371 b'\\' => buf.push(b'\\'),
372 b'{' => buf.push(b'{'),
373 b'}' => buf.push(b'}'),
374 b'~' => push_char(&mut buf, '\u{00A0}'),
375 b'_' => buf.push(b'-'),
376 _ => {}
377 }
378 i += 2;
379 continue;
380 }
381
382 if c < 0x20 {
384 i += 1;
385 continue;
386 }
387 if skip_from.is_some() {
388 i += 1;
389 continue;
390 }
391 if skip_left > 0 {
392 skip_left -= 1;
393 i += 1;
394 continue;
395 }
396 buf.push(c);
397 i += 1;
398 }
399
400 flush(&mut buf, &mut pending_high, &mut paragraphs)?;
401 if paragraphs.is_empty() {
402 return Err(DocError::corrupt("RTF 未找到任何正文文本"));
403 }
404 Ok(paragraphs)
405}
406
407pub struct RtfParseCapability;
409
410impl ParseCapability for RtfParseCapability {
411 fn id(&self) -> &str {
412 "rtf.read.rtf"
413 }
414 fn formats(&self) -> &[&str] {
415 &["rtf"]
416 }
417 fn parse(&self, path: &str) -> Result<RawDoc> {
418 let raw = std::fs::read(path)
419 .map_err(|e| DocError::corrupt(format!("无法读取 RTF 文件: {e}")))?;
420 let items = extract_paragraphs(&raw)?
421 .into_iter()
422 .map(|text| RawTextItem {
423 text,
424 ..Default::default()
425 })
426 .collect();
427 Ok(RawDoc {
428 pages: vec![RawPage {
429 name: None,
430 items,
431 ..Default::default()
432 }],
433 ..Default::default()
434 })
435 }
436}
437
438pub struct RtfPlugin;
440
441impl FormatPlugin for RtfPlugin {
442 fn id(&self) -> &str {
443 "rtf"
444 }
445 fn formats(&self) -> &[&str] {
446 &["rtf"]
447 }
448 fn default_parse_chain(&self) -> Vec<&'static str> {
449 vec!["rtf.read.rtf"]
450 }
451 fn default_process_chain(&self) -> Vec<&'static str> {
452 vec![]
453 }
454 fn assemble(&self, raw: &RawDoc) -> Document {
455 let items: Vec<&str> = raw
456 .pages
457 .first()
458 .map(|p| p.items.iter().map(|i| i.text.as_str()).collect())
459 .unwrap_or_default();
460 let blocks: Vec<Block> = items
461 .iter()
462 .filter(|t| !t.trim().is_empty())
463 .map(|t| Block {
464 kind: BlockKind::Paragraph,
465 content: (*t).to_string(),
466 ..Default::default()
467 })
468 .collect();
469 Document {
470 pages: vec![Page {
471 name: None,
472 blocks,
473 text: items.join("\n\n"),
474 ..Default::default()
475 }],
476 ..Default::default()
477 }
478 }
479}
480
481pub fn register_defaults(reg: &mut Registry) {
483 let _ = reg.register_parse(Arc::new(RtfParseCapability), None);
484 reg.register_plugin(Arc::new(RtfPlugin));
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn parses_paragraphs_and_skips_destinations() {
493 let src = br"{\rtf1\ansi\ansicpg1252{\fonttbl{\f0 Arial;}}{\*\generator Foo;}\pard
494first paragraph\par
495{\b bold} text with \{braces\} and \\slash\par
496}";
497 let paras = extract_paragraphs(src).unwrap();
498 assert_eq!(
499 paras,
500 vec!["first paragraph", "bold text with {braces} and \\slash"]
501 );
502 }
503
504 #[test]
505 fn unicode_and_hex_escapes_decode() {
506 let src = br"{\rtf1\u20320?\u22909?\par caf\'e9 \'93quoted\'94\par}";
507 let paras = extract_paragraphs(src).unwrap();
508 assert_eq!(paras, vec!["你好", "caf\u{e9} \u{201c}quoted\u{201d}"]);
509 }
510
511 #[test]
512 fn surrogate_pairs_merge_and_uc_skips_fallback() {
513 let src = br"{\rtf1\uc1\u-10179?\u-8704?\par}";
515 let paras = extract_paragraphs(src).unwrap();
516 assert_eq!(paras, vec!["\u{1F600}"]);
517 }
518
519 #[test]
520 fn controls_map_to_tabs_and_soft_breaks() {
521 let src = br"{\rtf1 a\tab b\line c\par\cell d\row}";
522 let paras = extract_paragraphs(src).unwrap();
523 assert_eq!(paras, vec!["a\tb\nc", "d"]);
524 }
525
526 #[test]
527 fn missing_header_is_corrupt() {
528 let err = extract_paragraphs(b"hello world").unwrap_err();
529 assert_eq!(err.code.as_str(), "corrupt");
530 }
531
532 #[test]
533 fn empty_body_is_corrupt() {
534 let err = extract_paragraphs(br"{\rtf1\ansi{\fonttbl{\f0 Arial;}}}").unwrap_err();
535 assert_eq!(err.code.as_str(), "corrupt");
536 }
537
538 #[test]
539 fn unsupported_codepage_with_raw_bytes_is_corrupt() {
540 let err = extract_paragraphs(br"{\rtf1\ansicpg936 \'d6\'d0\par}").unwrap_err();
542 assert_eq!(err.code.as_str(), "corrupt");
543 assert!(err.to_string().contains("936"));
544 }
545
546 #[test]
547 fn registry_resolves_rtf_format() {
548 let mut reg = Registry::new();
549 crate::plugins::register_defaults(&mut reg);
550 let pol = crate::policy::BackendPolicy::default();
551 let chain = reg.resolve_parse_chain("rtf", &pol, &["rtf.read.rtf"]);
552 assert_eq!(chain.len(), 1, "rtf 解析链为空");
553 assert_eq!(chain[0].id(), "rtf.read.rtf");
554 }
555
556 #[test]
557 fn engine_parses_rtf_end_to_end() {
558 let p = std::env::temp_dir().join("docparser_e2e.rtf");
559 std::fs::write(
560 &p,
561 br"{\rtf1\ansi{\fonttbl{\f0 Arial;}}Hello\par World\par}",
562 )
563 .unwrap();
564 let mut reg = Registry::new();
565 crate::plugins::register_defaults(&mut reg);
566 let doc = crate::engine::DocumentEngine::new(reg)
567 .parse(p.to_str().unwrap())
568 .unwrap_or_else(|e| panic!("rtf 解析失败: {e}"));
569 assert_eq!(doc.pages.len(), 1);
570 assert_eq!(doc.pages[0].text, "Hello\n\nWorld");
571 assert_eq!(doc.pages[0].blocks.len(), 2);
572 std::fs::remove_file(&p).ok();
573 }
574
575 #[test]
576 fn plugin_makes_one_block_per_paragraph() {
577 let raw = RawDoc {
578 pages: vec![RawPage {
579 items: vec![
580 RawTextItem {
581 text: "p1".into(),
582 ..Default::default()
583 },
584 RawTextItem {
585 text: "p2".into(),
586 ..Default::default()
587 },
588 ],
589 ..Default::default()
590 }],
591 ..Default::default()
592 };
593 let doc = RtfPlugin.assemble(&raw);
594 assert_eq!(doc.pages.len(), 1);
595 assert_eq!(doc.pages[0].blocks.len(), 2);
596 assert_eq!(doc.pages[0].text, "p1\n\np2");
597 }
598}