roo-oxide 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,312 @@
1
+ //! The parts of a worksheet besides its cells that roo reads from the whole
2
+ //! document, read in one pass without keeping the rest:
3
+ //!
4
+ //! * `SheetDoc#extract_hyperlinks` selects `/worksheet/hyperlinks/hyperlink`
5
+ //! and reads its `ref`, `id` (written `r:id`), and `location`.
6
+ //! * `SheetDoc#expand_merged_ranges` selects
7
+ //! `/worksheet/mergeCells/mergeCell` and reads its `ref`.
8
+ //!
9
+ //! roo removes the document's namespaces first, so names compare by local
10
+ //! name. Nokogiri recovers from XML that is not well-formed into a tree this
11
+ //! reader does not reproduce, so each event passes through [`SheetReader`]'s
12
+ //! XML checks. Anything it rejects is left to roo.
13
+
14
+ use std::collections::HashMap;
15
+ use std::io::{self, BufRead, Read};
16
+ use std::sync::atomic::{AtomicBool, Ordering};
17
+
18
+ use magnus::{prelude::*, Error, RArray, RHash, RString, Ruby, Value};
19
+ use quick_xml::events::{BytesStart, Event};
20
+ use quick_xml::XmlVersion;
21
+
22
+ use crate::archive::{Input, Source, ZipEntry};
23
+ use crate::reader::{parse_reference, Mode, SheetReader, Unsupported};
24
+ use crate::{unsupported, without_gvl};
25
+
26
+ type Result<T> = std::result::Result<T, Unsupported>;
27
+
28
+ /// A `hyperlink` element's `ref`, `id`, and `location`, `None` for one it
29
+ /// does not have.
30
+ pub type Hyperlink = (Option<String>, Option<String>, Option<String>);
31
+
32
+ #[derive(Clone, Copy, PartialEq, Eq)]
33
+ enum Section {
34
+ Hyperlinks,
35
+ MergeCells,
36
+ Other,
37
+ }
38
+
39
+ pub struct SheetPartsReader<R: BufRead> {
40
+ xml: SheetReader<R>,
41
+ buf: Vec<u8>,
42
+ depth: usize,
43
+ root_is_worksheet: bool,
44
+ section: Section,
45
+ pub hyperlinks: Vec<Hyperlink>,
46
+ /// Each `mergeCell`'s `ref`.
47
+ pub merge_cells: Vec<Option<String>>,
48
+ pub relationships: HashMap<Option<String>, (Option<String>, Option<String>)>,
49
+ root_is_relationships: bool,
50
+ }
51
+
52
+ impl<R: BufRead> SheetPartsReader<R> {
53
+ pub fn new(input: R) -> Self {
54
+ Self {
55
+ xml: SheetReader::new(input, Mode::Document),
56
+ buf: Vec::new(),
57
+ depth: 0,
58
+ root_is_worksheet: false,
59
+ section: Section::Other,
60
+ hyperlinks: Vec::new(),
61
+ merge_cells: Vec::new(),
62
+ relationships: HashMap::new(),
63
+ root_is_relationships: false,
64
+ }
65
+ }
66
+
67
+ /// Reads the next event; `false` at the end of the document.
68
+ pub fn next_event(&mut self) -> Result<bool> {
69
+ let mut buf = std::mem::take(&mut self.buf);
70
+ buf.clear();
71
+ let more = self.read_event(&mut buf);
72
+ self.buf = buf;
73
+ more
74
+ }
75
+
76
+ fn read_event(&mut self, buf: &mut Vec<u8>) -> Result<bool> {
77
+ match self.xml.next_xml_event(buf)? {
78
+ Event::Start(e) => {
79
+ self.depth += 1;
80
+ let (depth, root_is_worksheet, section) =
81
+ (self.depth, self.root_is_worksheet, self.section);
82
+ match (depth, e.local_name().as_ref()) {
83
+ (1, name) => {
84
+ self.root_is_worksheet = name == "worksheet";
85
+ self.root_is_relationships = name == "Relationships";
86
+ }
87
+ (2, "Relationship") if self.root_is_relationships => self.relationship(&e)?,
88
+ (2, "hyperlinks") if root_is_worksheet => self.section = Section::Hyperlinks,
89
+ (2, "mergeCells") if root_is_worksheet => self.section = Section::MergeCells,
90
+ (3, _) if section != Section::Other => self.take(&e)?,
91
+ _ => {}
92
+ }
93
+ }
94
+ Event::Empty(e) if self.depth == 2 && self.section != Section::Other => {
95
+ self.take(&e)?;
96
+ }
97
+ Event::Empty(e) if self.depth == 1 && self.root_is_relationships => {
98
+ if e.local_name().as_ref() == "Relationship" {
99
+ self.relationship(&e)?;
100
+ }
101
+ }
102
+ Event::End(_) => {
103
+ if self.depth == 2 {
104
+ self.section = Section::Other;
105
+ }
106
+ self.depth -= 1;
107
+ }
108
+ Event::Eof => return Ok(false),
109
+ _ => {}
110
+ }
111
+ Ok(true)
112
+ }
113
+
114
+ /// Keeps `e`, a child of the open section, when it is a part.
115
+ fn take(&mut self, e: &BytesStart<'_>) -> Result<()> {
116
+ match (self.section, e.local_name().as_ref()) {
117
+ (Section::Hyperlinks, "hyperlink") => {
118
+ let [reference, id, location] = attributes(e, ["ref", "id", "location"])?;
119
+ self.hyperlinks.push((reference, id, location));
120
+ }
121
+ (Section::MergeCells, "mergeCell") => {
122
+ let [reference] = attributes(e, ["ref"])?;
123
+ self.merge_cells.push(reference);
124
+ }
125
+ _ => {}
126
+ }
127
+ Ok(())
128
+ }
129
+
130
+ fn relationship(&mut self, e: &BytesStart<'_>) -> Result<()> {
131
+ let [id, kind, target] = attributes(e, ["Id", "Type", "Target"])?;
132
+ self.relationships.insert(id, (kind, target));
133
+ Ok(())
134
+ }
135
+ }
136
+
137
+ /// The values Nokogiri's `Node#[]` returns for `names` once the namespaces
138
+ /// are removed: the first attribute with that local name, whatever its
139
+ /// prefix.
140
+ fn attributes<const N: usize>(e: &BytesStart<'_>, names: [&str; N]) -> Result<[Option<String>; N]> {
141
+ let mut values = [const { None }; N];
142
+ for attribute in e.attributes() {
143
+ let attribute = attribute?;
144
+ let key = attribute.key;
145
+ if key.as_namespace_binding().is_some() {
146
+ continue;
147
+ }
148
+ let Some(at) = names
149
+ .iter()
150
+ .position(|name| *name == key.local_name().as_ref())
151
+ else {
152
+ continue;
153
+ };
154
+ if values[at].is_none() {
155
+ let value = attribute.normalized_value(XmlVersion::Implicit1_0)?;
156
+ values[at] = Some(value.into_owned());
157
+ }
158
+ }
159
+ Ok(values)
160
+ }
161
+
162
+ /// Calls `step` until it returns `false` (`Ok(true)`) or `interrupted`
163
+ /// turns true (`Ok(false)`).
164
+ fn until(interrupted: &AtomicBool, mut step: impl FnMut() -> Result<bool>) -> Result<bool> {
165
+ while !interrupted.load(Ordering::Acquire) {
166
+ if !step()? {
167
+ return Ok(true);
168
+ }
169
+ }
170
+ Ok(false)
171
+ }
172
+
173
+ /// Reads and validates worksheet or relationship parts with the GVL released.
174
+ fn read(ruby: &Ruby, source: &Source) -> std::result::Result<SheetPartsReader<Input>, Error> {
175
+ let mut parts = SheetPartsReader::new(source.input(ruby)?);
176
+ while !without_gvl(|interrupted| until(interrupted, || parts.next_event()))?
177
+ .map_err(|e| unsupported(ruby, e))?
178
+ {}
179
+ Ok(parts)
180
+ }
181
+
182
+ /// `Native.sheet_parts(source, targets)`, for a worksheet's path or
183
+ /// `Native::ZipEntry`: `[hyperlinks, merge_cells]`, with resolved targets
184
+ /// keyed by roo coordinates and merged references in document order. Raises
185
+ /// `Roo::Oxide::UnsupportedError` for a sheet roo reads differently.
186
+ fn sheet_parts(
187
+ ruby: &Ruby,
188
+ source: Source,
189
+ targets: RHash,
190
+ ) -> std::result::Result<(RHash, Vec<Option<String>>), Error> {
191
+ let parts = read(ruby, &source)?;
192
+ let hyperlinks = ruby.hash_new();
193
+ let coordinate_class: magnus::RClass = ruby.eval("Roo::Excelx::Coordinate")?;
194
+ let utils: Value = ruby.eval("Roo::Utils")?;
195
+ for (reference, id, location) in parts.hyperlinks {
196
+ let Some(mut target) = targets.get(id) else {
197
+ continue;
198
+ };
199
+ if let Some(location) = location {
200
+ target = target.funcall("+", (ruby.str_new(&format!("#{location}")),))?;
201
+ } else {
202
+ target = target.funcall("dup", ())?;
203
+ }
204
+ match reference.as_deref().and_then(parse_reference) {
205
+ Some((row, column)) => {
206
+ let coordinate = RArray::from_value(coordinate_class.obj_alloc()?)
207
+ .expect("Coordinate is an Array");
208
+ coordinate.push(row)?;
209
+ coordinate.push(column)?;
210
+ coordinate.freeze();
211
+ hyperlinks.aset(coordinate, target)?;
212
+ }
213
+ None => {
214
+ let coordinates: Value =
215
+ utils.funcall("coordinates_in_range", (reference.unwrap_or_default(),))?;
216
+ let coordinates: RArray = coordinates.funcall("to_a", ())?;
217
+ for coordinate in coordinates.into_iter() {
218
+ hyperlinks.aset(coordinate, target)?;
219
+ }
220
+ }
221
+ }
222
+ }
223
+ Ok((hyperlinks, parts.merge_cells))
224
+ }
225
+
226
+ pub fn define(native: magnus::RModule) -> std::result::Result<(), Error> {
227
+ native.define_singleton_method("sheet_parts", magnus::function!(sheet_parts, 2))?;
228
+ native.define_singleton_method("metadata_bytes", magnus::function!(metadata_bytes, 1))?;
229
+ native.define_singleton_method("hyperlink_targets", magnus::function!(hyperlink_targets, 1))?;
230
+ Ok(())
231
+ }
232
+
233
+ fn hyperlink_targets(ruby: &Ruby, source: Source) -> std::result::Result<Option<RHash>, Error> {
234
+ let parts = read(ruby, &source)?;
235
+ if !parts.relationships.values().any(|(kind, _)| {
236
+ kind.as_deref()
237
+ .is_some_and(|kind| kind.contains("hyperlink"))
238
+ }) {
239
+ return Ok(None);
240
+ }
241
+ let targets = ruby.hash_new();
242
+ for (id, (_, target)) in parts.relationships {
243
+ targets.aset(id, target)?;
244
+ }
245
+ Ok(Some(targets))
246
+ }
247
+
248
+ /// Inflates a workbook metadata entry without an intermediate file, using
249
+ /// the same local-header and size checks as native extraction.
250
+ fn metadata_bytes(ruby: &Ruby, entry: &ZipEntry) -> std::result::Result<RString, Error> {
251
+ let entry = entry.clone();
252
+ let bytes = without_gvl(move |interrupted| -> io::Result<Vec<u8>> {
253
+ let mut input = entry.inflated()?;
254
+ let mut bytes = Vec::new();
255
+ let mut chunk = [0; 8192];
256
+ loop {
257
+ if interrupted.load(Ordering::Acquire) {
258
+ return Err(io::Error::from(io::ErrorKind::Interrupted));
259
+ }
260
+ let read = input.read(&mut chunk)?;
261
+ if read == 0 {
262
+ return Ok(bytes);
263
+ }
264
+ bytes.extend_from_slice(&chunk[..read]);
265
+ }
266
+ })?
267
+ .map_err(|error| Error::new(ruby.exception_io_error(), error.to_string()))?;
268
+ Ok(ruby.str_from_slice(&bytes))
269
+ }
270
+
271
+ #[cfg(test)]
272
+ mod tests {
273
+ use super::*;
274
+
275
+ fn parts(xml: &str) -> (Vec<Hyperlink>, Vec<Option<String>>) {
276
+ let mut reader = SheetPartsReader::new(xml.as_bytes());
277
+ while reader.next_event().unwrap() {}
278
+ (reader.hyperlinks, reader.merge_cells)
279
+ }
280
+
281
+ fn some(text: &str) -> Option<String> {
282
+ Some(text.to_owned())
283
+ }
284
+
285
+ #[test]
286
+ fn reads_hyperlinks_and_merged_ranges_under_the_worksheet() {
287
+ let (hyperlinks, merges) = parts(
288
+ r#"<x:worksheet xmlns:x="s" xmlns:r="r"><x:sheetData><row><c r="A1"/></row></x:sheetData>
289
+ <x:mergeCells><x:mergeCell ref="A1:B2"/><mergeCell/><other ref="C1"/></x:mergeCells>
290
+ <hyperlinks><hyperlink ref="A1" r:id="rId1"><hyperlink ref="B9"/></hyperlink>
291
+ <hyperlink location="a&amp;b&#10;c&#x9;d e" ref="C3" id="first" r:id="second"/></hyperlinks>
292
+ <sheetData><hyperlinks><hyperlink ref="Z9"/></hyperlinks></sheetData></x:worksheet>"#,
293
+ );
294
+ assert_eq!(
295
+ hyperlinks,
296
+ [
297
+ (some("A1"), some("rId1"), None),
298
+ (some("C3"), some("first"), some("a&b\nc\td e")),
299
+ ]
300
+ );
301
+ assert_eq!(merges, [some("A1:B2"), None]);
302
+ }
303
+
304
+ #[test]
305
+ fn reads_nothing_under_another_root() {
306
+ let (hyperlinks, merges) = parts(
307
+ r#"<chartsheet><hyperlinks><hyperlink ref="A1"/></hyperlinks>
308
+ <mergeCells><mergeCell ref="A1:B2"/></mergeCells></chartsheet>"#,
309
+ );
310
+ assert!(hyperlinks.is_empty() && merges.is_empty());
311
+ }
312
+ }
@@ -0,0 +1,177 @@
1
+ use std::fmt::Write;
2
+
3
+ use magnus::encoding::EncodingCapable;
4
+ use magnus::value::ReprValue;
5
+ use magnus::{Error, RArray, RHash, RString, Ruby, Symbol, TryConvert, Value};
6
+
7
+ use super::{Content, Store};
8
+ use crate::reader::Kind;
9
+
10
+ impl Store {
11
+ fn formatter_cell(&self, ruby: &Ruby, record: u32) -> Result<Option<(Value, Symbol)>, Error> {
12
+ let raw = &self.records[record as usize];
13
+ let built = ruby.get_inner(self.built).get(raw.built_key());
14
+ if built.is_none() {
15
+ if raw.content == Content::Empty {
16
+ return Ok(None);
17
+ }
18
+ let converter = ruby.get_inner(self.converter);
19
+ let cell = self.formulas.raw(&self.records, record);
20
+ if let Some(kind) = converter.formatter_type(ruby, &cell)? {
21
+ let value = self.value(ruby, record)?;
22
+ let string =
23
+ raw.content == Content::Inline || matches!(raw.kind, Kind::Shared | Kind::Str);
24
+ if string && RString::try_convert(value)?.is_empty() {
25
+ return Ok(None);
26
+ }
27
+ return Ok(Some((value, ruby.sym_new(**kind).into())));
28
+ }
29
+ }
30
+ let cell = match built {
31
+ Some(cell) => cell,
32
+ None => self.build(ruby, record)?,
33
+ };
34
+ if cell.funcall::<_, _, Value>("empty?", ())?.to_bool() {
35
+ return Ok(None);
36
+ }
37
+ Ok(Some((
38
+ cell.funcall("value", ())?,
39
+ cell.funcall("type", ())?,
40
+ )))
41
+ }
42
+
43
+ pub(super) fn matrix_rows(ruby: &Ruby, rb_self: &Self, area: RArray) -> Result<RArray, Error> {
44
+ let first_row: i64 = area.entry(0)?;
45
+ let first_column: i64 = area.entry(1)?;
46
+ let last_row: i64 = area.entry(2)?;
47
+ let last_column: i64 = area.entry(3)?;
48
+ let rows = ruby.ary_new();
49
+ for row in first_row..=last_row {
50
+ rows.push(Self::row(
51
+ ruby,
52
+ rb_self,
53
+ ruby.integer_from_i64(row).as_value(),
54
+ first_column,
55
+ last_column,
56
+ )?)?;
57
+ }
58
+ Ok(rows)
59
+ }
60
+
61
+ pub(super) fn to_yaml(
62
+ ruby: &Ruby,
63
+ rb_self: &Self,
64
+ book: Value,
65
+ prefix: Value,
66
+ area: RArray,
67
+ ) -> Result<RString, Error> {
68
+ let output = ruby.str_new("--- \n");
69
+ let empty_prefix = magnus::RHash::from_value(prefix).is_some_and(|hash| hash.is_empty());
70
+ rb_self.each_formatter_cell(ruby, area, |row, column, value, kind| {
71
+ output.cat(format!("cell_{row}_{column}: \n"));
72
+ if !empty_prefix {
73
+ let prefix: RString = book.funcall("formatter_yaml_prefix", (prefix,))?;
74
+ output.funcall::<_, _, Value>("<<", (prefix,))?;
75
+ }
76
+ output.cat(format!(
77
+ " row: {row} \n col: {column} \n celltype: {} \n value: ",
78
+ kind.name()?
79
+ ));
80
+ let value = if kind == ruby.sym_new("time") {
81
+ book.funcall("integer_to_timestring", (value,))?
82
+ } else {
83
+ value
84
+ };
85
+ output.funcall::<_, _, Value>("<<", (value.to_r_string()?,))?;
86
+ output.cat(" \n");
87
+ Ok(())
88
+ })?;
89
+ Ok(output)
90
+ }
91
+
92
+ pub(super) fn xml_cells(
93
+ ruby: &Ruby,
94
+ rb_self: &Self,
95
+ book: Value,
96
+ area: RArray,
97
+ ) -> Result<RString, Error> {
98
+ let mut output = String::new();
99
+ rb_self.each_formatter_cell(ruby, area, |row, column, value, kind| {
100
+ let text = value.to_r_string()?;
101
+ let bytes = unsafe { text.as_slice() };
102
+ let utf8 =
103
+ text.enc_get() == ruby.utf8_encindex() || text.enc_get() == ruby.usascii_encindex();
104
+ let xml_text = std::str::from_utf8(bytes).ok().filter(|text| {
105
+ text.chars().all(|character| {
106
+ matches!(character, '\t' | '\n')
107
+ || (character >= ' ' && !matches!(character, '\u{fffe}' | '\u{ffff}'))
108
+ })
109
+ });
110
+ if !utf8 || xml_text.is_none() || RHash::from_value(value).is_some() {
111
+ let cell: RString =
112
+ book.funcall("formatter_xml_cell", (value, row, column, kind))?;
113
+ output.push_str(" ");
114
+ output.push_str(&cell.to_string()?);
115
+ output.push('\n');
116
+ return Ok(());
117
+ }
118
+ write!(
119
+ output,
120
+ " <cell row=\"{row}\" column=\"{column}\" type=\"{}\"",
121
+ kind.name()?
122
+ )
123
+ .unwrap();
124
+ if bytes.is_empty() {
125
+ output.push_str("/>\n");
126
+ } else {
127
+ output.push('>');
128
+ for character in xml_text.expect("validated above").chars() {
129
+ match character {
130
+ '&' => output.push_str("&amp;"),
131
+ '<' => output.push_str("&lt;"),
132
+ '>' => output.push_str("&gt;"),
133
+ character if !character.is_ascii() => {
134
+ write!(output, "&#x{:X};", u32::from(character)).unwrap();
135
+ }
136
+ character => output.push(character),
137
+ }
138
+ }
139
+ output.push_str("</cell>\n");
140
+ }
141
+ Ok(())
142
+ })?;
143
+ Ok(ruby.str_new(&output))
144
+ }
145
+
146
+ fn each_formatter_cell(
147
+ &self,
148
+ ruby: &Ruby,
149
+ area: RArray,
150
+ mut visit: impl FnMut(i64, i64, Value, Symbol) -> Result<(), Error>,
151
+ ) -> Result<(), Error> {
152
+ let first_row: i64 = area.entry(0)?;
153
+ let first_column: i64 = area.entry(1)?;
154
+ let last_row: i64 = area.entry(2)?;
155
+ let last_column: i64 = area.entry(3)?;
156
+ let top: i64 = area.entry(4)?;
157
+ let left: i64 = area.entry(5)?;
158
+ let bottom: i64 = area.entry(6)?;
159
+ let right: i64 = area.entry(7)?;
160
+ for &record in &self.index {
161
+ let raw = &self.records[record as usize];
162
+ let (row, column) = (i64::from(raw.row), i64::from(raw.column));
163
+ if row > last_row.min(bottom) {
164
+ break;
165
+ }
166
+ if row < first_row.max(top)
167
+ || !(first_column.max(left)..=last_column.min(right)).contains(&column)
168
+ {
169
+ continue;
170
+ }
171
+ if let Some((value, kind)) = self.formatter_cell(ruby, record)? {
172
+ visit(row, column, value, kind)?;
173
+ }
174
+ }
175
+ Ok(())
176
+ }
177
+ }