document_svg/document/
access.rs1use std::fs::{self, File};
10use std::io::Read;
11use std::path::Path;
12
13use crate::convert::{ConvertOptions, PageConsumer};
14use crate::document::html::{HtmlBlock, render_blocks_to_pages};
15use crate::error::{Error, Result};
16use crate::table::{TableAlign, TableData};
17
18const MAX_ACCESS_BYTES: u64 = 2 * 1024 * 1024 * 1024;
19const ACCESS_HEADER_BYTES: usize = 4096;
20const ACE_MARKER: &[u8] = b"Standard ACE DB";
21const JET_MARKER: &[u8] = b"Standard Jet DB";
22const MSISAM_MARKER: &[u8] = b"MSISAM Database";
23
24pub(crate) fn looks_like_prefix(prefix: &[u8]) -> bool {
25 prefix.len() >= 4
26 && prefix[..4] == [0, 1, 0, 0]
27 && (prefix
28 .windows(ACE_MARKER.len())
29 .any(|window| window == ACE_MARKER)
30 || prefix
31 .windows(JET_MARKER.len())
32 .any(|window| window == JET_MARKER)
33 || prefix
34 .windows(MSISAM_MARKER.len())
35 .any(|window| window == MSISAM_MARKER))
36}
37
38struct AccessPageSink<'a> {
39 inner: &'a mut dyn PageConsumer,
40 warnings: &'a [String],
41}
42
43impl PageConsumer for AccessPageSink<'_> {
44 fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
45 page.source_format = "access".into();
46 if page.title.is_empty() {
47 page.title = "Microsoft Access database header".into();
48 }
49 page.description = "ACE/Jet header metadata is rendered as bounded inert rows; database objects and executable content are not opened".into();
50 for warning in self.warnings {
51 page.warn(warning.clone());
52 }
53 self.inner.consume(page)
54 }
55}
56
57pub(crate) fn convert(
58 path: &Path,
59 options: &ConvertOptions,
60 sink: &mut dyn PageConsumer,
61) -> Result<Vec<String>> {
62 let metadata = fs::metadata(path)?;
63 let max_bytes = options.max_input_bytes.min(MAX_ACCESS_BYTES);
64 if metadata.len() > max_bytes {
65 return Err(Error::LimitExceeded(format!(
66 "Access input exceeds maximum bytes ({max_bytes})"
67 )));
68 }
69 let mut file = File::open(path)?;
70 let read_len = metadata.len().min(ACCESS_HEADER_BYTES as u64) as usize;
71 let mut header = vec![0u8; read_len];
72 file.read_exact(&mut header)?;
73 if !looks_like_prefix(&header) {
74 return Err(Error::InvalidInput(
75 "Access input has no recognized ACE/Jet database header".into(),
76 ));
77 }
78 let (engine, page_size) = if header
79 .windows(ACE_MARKER.len())
80 .any(|window| window == ACE_MARKER)
81 {
82 ("ACE / ACCDB", "4096 bytes (ACE default)")
83 } else if header
84 .windows(JET_MARKER.len())
85 .any(|window| window == JET_MARKER)
86 {
87 ("Jet / MDB", "2048 or 4096 bytes (Jet generation dependent)")
88 } else {
89 ("MSISAM", "engine-defined")
90 };
91 let rows = vec![
92 vec!["Storage engine".into(), engine.into()],
93 vec!["Header marker offset".into(), "4".into()],
94 vec!["Page size".into(), page_size.into()],
95 vec!["File bytes".into(), metadata.len().to_string()],
96 vec![
97 "AccessVersion marker".into(),
98 if header
99 .windows(b"AccessVersion".len())
100 .any(|window| window == b"AccessVersion")
101 {
102 "present"
103 } else {
104 "not in bounded header"
105 }
106 .into(),
107 ],
108 vec!["Header bytes read".into(), read_len.to_string()],
109 ];
110 let warnings = vec![
111 "Access tables, rows, columns, indexes, queries, forms, reports, relationships, attachments, and linked databases are not decoded".into(),
112 "VBA/data macros, OLE/ActiveX payloads, passwords, encryption, external paths, and database operations remain inert and are never bypassed or executed".into(),
113 ];
114 let blocks = vec![
115 HtmlBlock::Heading {
116 level: 1,
117 text: "Microsoft Access database header".into(),
118 },
119 HtmlBlock::Paragraph {
120 text: "Only the ACE/Jet engine marker and bounded first-page metadata are inspected. Database pages and executable content are never expanded.".into(),
121 },
122 HtmlBlock::Table(TableData {
123 headers: vec!["Metric".into(), "Value".into()],
124 rows,
125 alignments: vec![TableAlign::Left, TableAlign::Right],
126 raw_source: String::new(),
127 }),
128 ];
129 let mut page_sink = AccessPageSink {
130 inner: sink,
131 warnings: &warnings,
132 };
133 render_blocks_to_pages(&blocks, &mut page_sink, options)?;
134 Ok(warnings)
135}