document_svg/document/
sqlite.rs1use std::path::Path;
9use std::time::{Duration, Instant};
10
11use rusqlite::limits::Limit;
12use rusqlite::{Connection, OpenFlags, types::ValueRef};
13
14use crate::convert::{ConvertOptions, PageConsumer};
15use crate::document::html::{HtmlBlock, render_blocks_to_pages_with_warnings};
16use crate::error::{Error, Result};
17use crate::table::{TableAlign, TableData};
18
19const MAX_SQLITE_INPUT_BYTES: u64 = 256 * 1024 * 1024;
20const MAX_SQLITE_TABLES: usize = 100;
21const MAX_SQLITE_ROWS_PER_TABLE: usize = 2_000;
22const MAX_SQLITE_COLUMNS: usize = 128;
23const MAX_SQLITE_CELL_CHARS: usize = 512;
24const MAX_SQLITE_TEXT_BYTES: usize = 64 * 1024 * 1024;
25const MAX_SQLITE_QUERY_SECONDS: u64 = 5;
26
27pub(crate) fn looks_like_prefix(bytes: &[u8]) -> bool {
28 bytes.starts_with(b"SQLite format 3\0")
29}
30
31pub(crate) fn convert(
32 path: &Path,
33 options: &ConvertOptions,
34 sink: &mut dyn PageConsumer,
35) -> Result<Vec<String>> {
36 let metadata = std::fs::metadata(path)?;
37 let limit = options.max_input_bytes.min(MAX_SQLITE_INPUT_BYTES);
38 if metadata.len() > limit {
39 return Err(Error::LimitExceeded(format!(
40 "SQLite input exceeds maximum bytes ({limit})"
41 )));
42 }
43 check_sidecar(path, "-wal", limit)?;
44 check_sidecar(path, "-shm", limit.min(16 * 1024 * 1024))?;
45 let flags = OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX;
46 let connection = Connection::open_with_flags(path, flags).map_err(|error| {
47 Error::InvalidInput(format!("cannot open SQLite database read-only: {error}"))
48 })?;
49 connection
50 .busy_timeout(Duration::from_secs(1))
51 .map_err(sql_error)?;
52 let started = Instant::now();
53 connection
54 .progress_handler(
55 1000,
56 Some(move || started.elapsed() >= Duration::from_secs(MAX_SQLITE_QUERY_SECONDS)),
57 )
58 .map_err(sql_error)?;
59 connection
60 .set_limit(Limit::SQLITE_LIMIT_LENGTH, 8 * 1024 * 1024)
61 .map_err(sql_error)?;
62 connection
63 .set_limit(Limit::SQLITE_LIMIT_SQL_LENGTH, 1024 * 1024)
64 .map_err(sql_error)?;
65 connection
66 .set_limit(Limit::SQLITE_LIMIT_COLUMN, MAX_SQLITE_COLUMNS as i32)
67 .map_err(sql_error)?;
68 let mut warnings = Vec::new();
69 let mut blocks = Vec::new();
70 let tables = list_tables(&connection)?;
71 if tables.is_empty() {
72 return Err(Error::InvalidInput(
73 "SQLite database contains no ordinary user tables".into(),
74 ));
75 }
76 if tables.len() > MAX_SQLITE_TABLES {
77 warnings.push(format!(
78 "SQLite contains more than {MAX_SQLITE_TABLES} user tables; remaining tables were omitted"
79 ));
80 }
81 let mut text_bytes = 0usize;
82 for table_name in tables.into_iter().take(MAX_SQLITE_TABLES) {
83 let (table, table_warnings) = read_table(&connection, &table_name, &mut text_bytes)?;
84 warnings.extend(table_warnings);
85 blocks.push(HtmlBlock::Heading {
86 level: 2,
87 text: table_name,
88 });
89 blocks.push(HtmlBlock::Table(table));
90 }
91 if text_bytes > MAX_SQLITE_TEXT_BYTES {
92 return Err(Error::LimitExceeded(format!(
93 "SQLite rendered text exceeds {MAX_SQLITE_TEXT_BYTES} bytes"
94 )));
95 }
96 render_blocks_to_pages_with_warnings(&blocks, sink, options, &warnings)?;
97 Ok(dedup_warnings(warnings))
98}
99
100fn list_tables(connection: &Connection) -> Result<Vec<String>> {
101 let mut statement = connection
102 .prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name LIMIT ?1")
103 .map_err(sql_error)?;
104 let names = statement
105 .query_map([MAX_SQLITE_TABLES.saturating_add(1) as i64], |row| {
106 row.get::<_, String>(0)
107 })
108 .map_err(sql_error)?
109 .collect::<std::result::Result<Vec<_>, _>>()
110 .map_err(sql_error)?;
111 Ok(names)
112}
113
114fn read_table(
115 connection: &Connection,
116 table_name: &str,
117 text_bytes: &mut usize,
118) -> Result<(TableData, Vec<String>)> {
119 let quoted = quote_identifier(table_name);
120 let sql = format!(
121 "SELECT * FROM {quoted} LIMIT {}",
122 MAX_SQLITE_ROWS_PER_TABLE + 1
123 );
124 let mut statement = connection.prepare(&sql).map_err(sql_error)?;
125 let column_count = statement.column_count().min(MAX_SQLITE_COLUMNS);
126 let headers = statement
127 .column_names()
128 .into_iter()
129 .take(column_count)
130 .map(str::to_owned)
131 .collect::<Vec<_>>();
132 let mut rows = Vec::new();
133 let mut warnings = Vec::new();
134 let mut query = statement.query([]).map_err(sql_error)?;
135 while let Some(row) = query.next().map_err(sql_error)? {
136 if rows.len() >= MAX_SQLITE_ROWS_PER_TABLE {
137 warnings.push(format!(
138 "SQLite table '{table_name}' exceeded {MAX_SQLITE_ROWS_PER_TABLE} sampled rows; remaining rows were omitted"
139 ));
140 break;
141 }
142 let mut values = Vec::with_capacity(column_count);
143 for index in 0..column_count {
144 let value = match row.get_ref(index).map_err(sql_error)? {
145 ValueRef::Null => String::new(),
146 ValueRef::Integer(value) => value.to_string(),
147 ValueRef::Real(value) => value.to_string(),
148 ValueRef::Text(value) => String::from_utf8_lossy(value).into_owned(),
149 ValueRef::Blob(value) => format!("<BLOB {} bytes>", value.len()),
150 };
151 let value = truncate_cell(&value);
152 *text_bytes = text_bytes.saturating_add(value.len());
153 values.push(value);
154 }
155 rows.push(values);
156 }
157 Ok((
158 TableData {
159 headers,
160 rows,
161 alignments: vec![TableAlign::Left; column_count],
162 raw_source: String::new(),
163 },
164 warnings,
165 ))
166}
167
168fn quote_identifier(value: &str) -> String {
169 format!("\"{}\"", value.replace('"', "\"\""))
170}
171
172fn truncate_cell(value: &str) -> String {
173 value.chars().take(MAX_SQLITE_CELL_CHARS).collect()
174}
175
176fn sql_error(error: rusqlite::Error) -> Error {
177 Error::InvalidInput(format!("SQLite preview query failed: {error}"))
178}
179
180fn check_sidecar(path: &Path, suffix: &str, limit: u64) -> Result<()> {
181 let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
182 return Ok(());
183 };
184 let sidecar = path.with_file_name(format!("{file_name}{suffix}"));
185 let Ok(metadata) = std::fs::metadata(&sidecar) else {
186 return Ok(());
187 };
188 if !metadata.is_file() {
189 return Err(Error::InvalidInput(format!(
190 "SQLite sidecar '{suffix}' is not a regular file"
191 )));
192 }
193 if metadata.len() > limit {
194 return Err(Error::LimitExceeded(format!(
195 "SQLite sidecar '{suffix}' exceeds maximum bytes ({limit})"
196 )));
197 }
198 Ok(())
199}
200
201fn dedup_warnings(mut warnings: Vec<String>) -> Vec<String> {
202 warnings.sort();
203 warnings.dedup();
204 warnings
205}