calcsheet-native 0.1.0

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,470 @@
1
+ //! The Rust core of calcsheet-native, loaded by `lib/calcsheet/native.rb`.
2
+
3
+ mod csv;
4
+ mod gvl;
5
+ mod ods;
6
+ mod xlsx;
7
+ mod xml;
8
+
9
+ use std::cell::RefCell;
10
+ use std::sync::Arc;
11
+
12
+ use magnus::{Error, IntoValue, RArray, RString, Ruby, Value, function, method, prelude::*};
13
+
14
+ use crate::csv::{Field, RowSep, Stop, Tokenizer};
15
+ use crate::ods::TableReader;
16
+ use crate::xlsx::{SheetReader, StringsReader, StringsStop};
17
+
18
+ /// The version of the Rust core, which must equal the version of the gem.
19
+ fn core_version() -> &'static str {
20
+ env!("CARGO_PKG_VERSION")
21
+ }
22
+
23
+ /// The CSV tokenizer of `Calcsheet::Native::CsvReader`: it is fed UTF-8 text and hands out records in batches,
24
+ /// parsed without the GVL.
25
+ #[magnus::wrap(class = "Calcsheet::Native::CsvTokenizer", free_immediately, size)]
26
+ struct CsvTokenizer(RefCell<Tokenizer>);
27
+
28
+ impl CsvTokenizer {
29
+ /// `col_sep` and `quote_char` are one ASCII character, `row_sep` "\n", "\r" or "\r\n".
30
+ fn new(
31
+ ruby: &Ruby,
32
+ col_sep: String,
33
+ quote_char: String,
34
+ row_sep: String,
35
+ ) -> Result<Self, Error> {
36
+ let byte = |name: &str, value: &str| match value.as_bytes() {
37
+ [b] if b.is_ascii() => Ok(*b),
38
+ _ => Err(Error::new(
39
+ ruby.exception_arg_error(),
40
+ format!("{name}: one ASCII character expected, got {value:?}"),
41
+ )),
42
+ };
43
+ let row_sep = match row_sep.as_str() {
44
+ "\n" => RowSep::Lf,
45
+ "\r" => RowSep::Cr,
46
+ "\r\n" => RowSep::CrLf,
47
+ other => {
48
+ return Err(Error::new(
49
+ ruby.exception_arg_error(),
50
+ format!("row_sep: \"\\n\", \"\\r\" or \"\\r\\n\" expected, got {other:?}"),
51
+ ));
52
+ }
53
+ };
54
+ Ok(CsvTokenizer(RefCell::new(Tokenizer::new(
55
+ byte("col_sep", &col_sep)?,
56
+ byte("quote_char", &quote_char)?,
57
+ row_sep,
58
+ ))))
59
+ }
60
+
61
+ fn tokenizer(&self, ruby: &Ruby) -> Result<std::cell::RefMut<'_, Tokenizer>, Error> {
62
+ self.0.try_borrow_mut().map_err(|_| {
63
+ Error::new(
64
+ ruby.exception_runtime_error(),
65
+ "a CSV tokenizer is used by two threads at once",
66
+ )
67
+ })
68
+ }
69
+
70
+ /// Appends a chunk of UTF-8 text.
71
+ fn feed(ruby: &Ruby, rb_self: &Self, chunk: RString) -> Result<(), Error> {
72
+ let mut tokenizer = rb_self.tokenizer(ruby)?;
73
+ // SAFETY: the bytes are copied before any Ruby code runs.
74
+ tokenizer.feed(unsafe { chunk.as_slice() });
75
+ Ok(())
76
+ }
77
+
78
+ /// No more text comes.
79
+ fn finish(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
80
+ rb_self.tokenizer(ruby)?.finish();
81
+ Ok(())
82
+ }
83
+
84
+ /// Up to `max` records: `[fields, breaks, strays]`, an Array of the fields (Arrays of Strings and nil), one of
85
+ /// Integers and one of booleans; an empty batch at the end of the text. nil when the text ends inside a record
86
+ /// (feed more, or finish), false when the next record is malformed (see #error).
87
+ fn records(ruby: &Ruby, rb_self: &Self, max: usize) -> Result<Value, Error> {
88
+ let mut tokenizer = rb_self.tokenizer(ruby)?;
89
+ let (records, stop) = gvl::without_gvl(|| tokenizer.batch(max)).map_err(|panic| {
90
+ Error::new(
91
+ ruby.exception_runtime_error(),
92
+ format!("calcsheet-native: {panic}"),
93
+ )
94
+ })?;
95
+ if records.is_empty() {
96
+ match stop {
97
+ Stop::NeedMore => return Ok(ruby.qnil().as_value()),
98
+ Stop::Error(_) => return Ok(ruby.qfalse().as_value()),
99
+ Stop::End | Stop::Full => {}
100
+ }
101
+ }
102
+
103
+ let text = tokenizer.buffer();
104
+ let utf8 = ruby.utf8_encindex();
105
+ let all = ruby.ary_new_capa(records.len());
106
+ let breaks = ruby.ary_new_capa(records.len());
107
+ let strays = ruby.ary_new_capa(records.len());
108
+ for record in records {
109
+ let fields = ruby.ary_new_capa(record.fields.len());
110
+ for field in record.fields {
111
+ match field {
112
+ Field::Nil => fields.push(ruby.qnil())?,
113
+ Field::Span(start, end) => {
114
+ fields.push(ruby.enc_str_new(&text[start..end], utf8))?
115
+ }
116
+ Field::Owned(bytes) => fields.push(ruby.enc_str_new(bytes, utf8))?,
117
+ }
118
+ }
119
+ all.push(fields)?;
120
+ breaks.push(record.breaks)?;
121
+ strays.push(record.stray)?;
122
+ }
123
+ Ok(ruby
124
+ .ary_new_from_values(&[all.as_value(), breaks.as_value(), strays.as_value()])
125
+ .as_value())
126
+ }
127
+
128
+ /// `[message, line]` of the malformed record that #records stopped at: the arguments of CSV::MalformedCSVError.
129
+ fn error(ruby: &Ruby, rb_self: &Self) -> Result<Option<(String, usize)>, Error> {
130
+ let tokenizer = rb_self.tokenizer(ruby)?;
131
+ Ok(tokenizer
132
+ .pending_error()
133
+ .map(|message| (message, tokenizer.line())))
134
+ }
135
+ }
136
+
137
+ /// The cells of an XLSX sheet part for `Calcsheet::Native::XlsxReader`: it is fed the inflated XML and hands out
138
+ /// rows in batches, parsed without the GVL, or tells that calcsheet's own parser must read the sheet.
139
+ #[magnus::wrap(class = "Calcsheet::Native::XlsxSheet", free_immediately, size)]
140
+ struct XlsxSheet(RefCell<SheetReader>);
141
+
142
+ impl XlsxSheet {
143
+ fn new(max_cell_bytes: Option<usize>) -> Self {
144
+ XlsxSheet(RefCell::new(SheetReader::new(max_cell_bytes)))
145
+ }
146
+
147
+ fn reader(&self, ruby: &Ruby) -> Result<std::cell::RefMut<'_, SheetReader>, Error> {
148
+ self.0.try_borrow_mut().map_err(|_| {
149
+ Error::new(
150
+ ruby.exception_runtime_error(),
151
+ "an XLSX sheet reader is used by two threads at once",
152
+ )
153
+ })
154
+ }
155
+
156
+ fn feed(ruby: &Ruby, rb_self: &Self, chunk: RString) -> Result<(), Error> {
157
+ // SAFETY: the bytes are copied before any Ruby code runs.
158
+ rb_self.reader(ruby)?.feed(unsafe { chunk.as_slice() });
159
+ Ok(())
160
+ }
161
+
162
+ fn finish(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
163
+ rb_self.reader(ruby)?.finish();
164
+ Ok(())
165
+ }
166
+
167
+ /// The shared strings table read in Rust.
168
+ fn table(ruby: &Ruby, rb_self: &Self, table: &StringTable) -> Result<(), Error> {
169
+ rb_self.reader(ruby)?.set_table(Arc::clone(&table.0));
170
+ Ok(())
171
+ }
172
+
173
+ /// The shared strings table that calcsheet loaded: the buffer and packed offsets of its SharedStrings.
174
+ fn strings(
175
+ ruby: &Ruby,
176
+ rb_self: &Self,
177
+ buffer: RString,
178
+ offsets: RString,
179
+ ) -> Result<(), Error> {
180
+ // SAFETY: the bytes are copied before any Ruby code runs.
181
+ rb_self
182
+ .reader(ruby)?
183
+ .set_strings(unsafe { buffer.as_slice() }, unsafe { offsets.as_slice() });
184
+ Ok(())
185
+ }
186
+
187
+ /// Up to `max` rows: `[numbers, ends, cells, done]`, the row numbers, the end of each row's cells in `cells`, the
188
+ /// cells as six values each (column, kind, style, text, raw, type text) and whether the sheet ended. nil when the
189
+ /// text fed so far ends inside a row and no row is complete (feed more, or finish); false when calcsheet's
190
+ /// parser must read the sheet; `:strings` when a cell needs the shared strings and no row is complete (see
191
+ /// #strings). A shared string is frozen, as calcsheet's SharedStrings hands it out.
192
+ fn rows(ruby: &Ruby, rb_self: &Self, max: usize) -> Result<Value, Error> {
193
+ let mut reader = rb_self.reader(ruby)?;
194
+ let (rows, stop) = gvl::without_gvl(|| reader.rows(max)).map_err(|panic| {
195
+ Error::new(
196
+ ruby.exception_runtime_error(),
197
+ format!("calcsheet-native: {panic}"),
198
+ )
199
+ })?;
200
+ let done = match stop {
201
+ xlsx::Stop::Unsupported => return Ok(ruby.qfalse().as_value()),
202
+ xlsx::Stop::NeedMore if rows.is_empty() => return Ok(ruby.qnil().as_value()),
203
+ xlsx::Stop::NeedStrings if rows.is_empty() => {
204
+ return Ok(ruby.to_symbol("strings").as_value());
205
+ }
206
+ xlsx::Stop::End => true,
207
+ xlsx::Stop::NeedMore | xlsx::Stop::NeedStrings | xlsx::Stop::Full => false,
208
+ };
209
+ let utf8 = ruby.utf8_encindex();
210
+ let numbers = ruby.ary_new_capa(rows.len());
211
+ let ends = ruby.ary_new_capa(rows.len());
212
+ let cells = ruby.ary_new();
213
+ let mut count = 0;
214
+ for row in rows {
215
+ numbers.push(row.number)?;
216
+ for cell in row.cells {
217
+ cells.push(cell.column)?;
218
+ cells.push(cell.kind as u8)?;
219
+ cells.push(cell.style)?;
220
+ let text = ruby.enc_str_new(reader.text(&cell), utf8);
221
+ if cell.kind == xlsx::Kind::Shared {
222
+ text.freeze();
223
+ }
224
+ cells.push(text)?;
225
+ cells.push(cell.raw)?;
226
+ cells.push(cell.type_text.map(|text| ruby.enc_str_new(text, utf8)))?;
227
+ count += 6;
228
+ }
229
+ ends.push(count)?;
230
+ }
231
+ Ok(ruby
232
+ .ary_new_from_values(&[
233
+ numbers.as_value(),
234
+ ends.as_value(),
235
+ cells.as_value(),
236
+ done.into_value_with(ruby),
237
+ ])
238
+ .as_value())
239
+ }
240
+ }
241
+
242
+ /// The cells of one ODS table for `Calcsheet::Native::OdsReader`: it is fed the table's XML from calcsheet's Scanner
243
+ /// and hands out rows in batches, parsed without the GVL, or tells that calcsheet's parser must read the table.
244
+ #[magnus::wrap(class = "Calcsheet::Native::OdsTable", free_immediately, size)]
245
+ struct OdsTable(RefCell<TableReader>);
246
+
247
+ impl OdsTable {
248
+ /// `prefixes`: those of the table, office, text and calcext namespaces; `formats`: the names of the cell styles
249
+ /// with a format, in the order of the formats the Ruby side holds.
250
+ fn new(
251
+ prefixes: (String, String, String, String),
252
+ formats: RArray,
253
+ max_cell_bytes: Option<usize>,
254
+ first_row: u64,
255
+ start_row: u64,
256
+ ) -> Result<Self, Error> {
257
+ let formats = formats
258
+ .to_vec::<String>()?
259
+ .into_iter()
260
+ .map(String::into_bytes)
261
+ .collect();
262
+ let (table, office, text, calcext) = &prefixes;
263
+ Ok(OdsTable(RefCell::new(TableReader::new(
264
+ [table, office, text, calcext],
265
+ formats,
266
+ max_cell_bytes,
267
+ first_row,
268
+ start_row,
269
+ ))))
270
+ }
271
+
272
+ fn reader(&self, ruby: &Ruby) -> Result<std::cell::RefMut<'_, TableReader>, Error> {
273
+ self.0.try_borrow_mut().map_err(|_| {
274
+ Error::new(
275
+ ruby.exception_runtime_error(),
276
+ "an ODS table reader is used by two threads at once",
277
+ )
278
+ })
279
+ }
280
+
281
+ fn feed(ruby: &Ruby, rb_self: &Self, chunk: RString) -> Result<(), Error> {
282
+ // SAFETY: the bytes are copied before any Ruby code runs.
283
+ rb_self.reader(ruby)?.feed(unsafe { chunk.as_slice() });
284
+ Ok(())
285
+ }
286
+
287
+ fn finish(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
288
+ rb_self.reader(ruby)?.finish();
289
+ Ok(())
290
+ }
291
+
292
+ /// Up to `max` rows with cells: `[firsts, lasts, ends, cells, column_formats, done]`, the first and last number
293
+ /// of each row (a repeated row), the end of each row's cells in `cells`, the cells as six values each (column,
294
+ /// repeat, kind, missing, value, format), the column formats so far as `[first, last, format]` triples flattened,
295
+ /// and whether the table ended. nil when the text fed so far ends inside a row and no row is complete; false when
296
+ /// calcsheet's parser must read the table.
297
+ fn rows(ruby: &Ruby, rb_self: &Self, max: usize) -> Result<Value, Error> {
298
+ let mut reader = rb_self.reader(ruby)?;
299
+ let (rows, stop) = gvl::without_gvl(|| reader.rows(max)).map_err(|panic| {
300
+ Error::new(
301
+ ruby.exception_runtime_error(),
302
+ format!("calcsheet-native: {panic}"),
303
+ )
304
+ })?;
305
+ let done = match stop {
306
+ ods::Stop::Unsupported => return Ok(ruby.qfalse().as_value()),
307
+ ods::Stop::NeedMore if rows.is_empty() => return Ok(ruby.qnil().as_value()),
308
+ ods::Stop::End => true,
309
+ ods::Stop::NeedMore | ods::Stop::Full => false,
310
+ };
311
+ let utf8 = ruby.utf8_encindex();
312
+ let firsts = ruby.ary_new_capa(rows.len());
313
+ let lasts = ruby.ary_new_capa(rows.len());
314
+ let ends = ruby.ary_new_capa(rows.len());
315
+ let cells = ruby.ary_new();
316
+ let mut count = 0;
317
+ for row in rows {
318
+ firsts.push(row.number)?;
319
+ lasts.push(row.last)?;
320
+ for cell in row.cells {
321
+ cells.push(cell.column)?;
322
+ cells.push(cell.repeat)?;
323
+ cells.push(cell.kind as u8)?;
324
+ cells.push(cell.missing)?;
325
+ cells.push(ruby.enc_str_new(reader.value(&cell), utf8))?;
326
+ cells.push(cell.format)?;
327
+ count += 6;
328
+ }
329
+ ends.push(count)?;
330
+ }
331
+ let formats = ruby.ary_new_capa(reader.column_formats.len() * 3);
332
+ for &(first, last, format) in &reader.column_formats {
333
+ formats.push(first)?;
334
+ formats.push(last)?;
335
+ formats.push(format)?;
336
+ }
337
+ Ok(ruby
338
+ .ary_new_from_values(&[
339
+ firsts.as_value(),
340
+ lasts.as_value(),
341
+ ends.as_value(),
342
+ cells.as_value(),
343
+ formats.as_value(),
344
+ done.into_value_with(ruby),
345
+ ])
346
+ .as_value())
347
+ }
348
+ }
349
+
350
+ /// The shared strings table read in Rust: `Calcsheet::Native::SharedStrings` hands its strings out as calcsheet's
351
+ /// SharedStrings does, and the sheet readers share it.
352
+ #[magnus::wrap(class = "Calcsheet::Native::StringTable", free_immediately, size)]
353
+ struct StringTable(Arc<xlsx::Strings>);
354
+
355
+ impl StringTable {
356
+ fn size(&self) -> usize {
357
+ self.0.size()
358
+ }
359
+
360
+ fn bytesize(&self) -> usize {
361
+ self.0.buffer.len()
362
+ }
363
+
364
+ /// SharedStrings#unchecked: a new frozen String tagged UTF-8, not validated.
365
+ fn unchecked(ruby: &Ruby, rb_self: &Self, index: usize) -> Result<RString, Error> {
366
+ if index >= rb_self.0.size() {
367
+ return Err(Error::new(
368
+ ruby.exception_index_error(),
369
+ format!("no shared string {index}"),
370
+ ));
371
+ }
372
+ let text = ruby.enc_str_new(rb_self.0.get(index), ruby.utf8_encindex());
373
+ text.freeze();
374
+ Ok(text)
375
+ }
376
+ }
377
+
378
+ /// The shared strings of an XLSX file for `Calcsheet::Native::XlsxReader`: fed the XML of `xl/sharedStrings.xml`, it
379
+ /// gives a StringTable, or tells that calcsheet must load the strings.
380
+ #[magnus::wrap(class = "Calcsheet::Native::XlsxStrings", free_immediately, size)]
381
+ struct XlsxStrings(RefCell<StringsReader>);
382
+
383
+ impl XlsxStrings {
384
+ fn new(max_cell_bytes: Option<usize>) -> Self {
385
+ XlsxStrings(RefCell::new(StringsReader::new(max_cell_bytes)))
386
+ }
387
+
388
+ fn reader(&self, ruby: &Ruby) -> Result<std::cell::RefMut<'_, StringsReader>, Error> {
389
+ self.0.try_borrow_mut().map_err(|_| {
390
+ Error::new(
391
+ ruby.exception_runtime_error(),
392
+ "a shared strings reader is used by two threads at once",
393
+ )
394
+ })
395
+ }
396
+
397
+ fn feed(ruby: &Ruby, rb_self: &Self, chunk: RString) -> Result<(), Error> {
398
+ // SAFETY: the bytes are copied before any Ruby code runs.
399
+ rb_self.reader(ruby)?.feed(unsafe { chunk.as_slice() });
400
+ Ok(())
401
+ }
402
+
403
+ fn finish(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
404
+ rb_self.reader(ruby)?.finish();
405
+ Ok(())
406
+ }
407
+
408
+ /// The StringTable once the XML is read; nil when it needs more, false when calcsheet must load the table.
409
+ fn read(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
410
+ let mut reader = rb_self.reader(ruby)?;
411
+ let stop = gvl::without_gvl(|| reader.read()).map_err(|panic| {
412
+ Error::new(
413
+ ruby.exception_runtime_error(),
414
+ format!("calcsheet-native: {panic}"),
415
+ )
416
+ })?;
417
+ match stop {
418
+ StringsStop::NeedMore => Ok(ruby.qnil().as_value()),
419
+ StringsStop::Unsupported => Ok(ruby.qfalse().as_value()),
420
+ StringsStop::Done => {
421
+ let mut strings = xlsx::Strings {
422
+ buffer: std::mem::take(&mut reader.buffer),
423
+ offsets: std::mem::take(&mut reader.offsets),
424
+ };
425
+ strings.buffer.shrink_to_fit();
426
+ strings.offsets.shrink_to_fit();
427
+ Ok(StringTable(Arc::new(strings)).into_value_with(ruby))
428
+ }
429
+ }
430
+ }
431
+ }
432
+
433
+ #[magnus::init]
434
+ fn init(ruby: &Ruby) -> Result<(), Error> {
435
+ let native = ruby.define_module("Calcsheet")?.define_module("Native")?;
436
+ native.define_singleton_method("core_version", function!(core_version, 0))?;
437
+
438
+ let tokenizer = native.define_class("CsvTokenizer", ruby.class_object())?;
439
+ tokenizer.define_singleton_method("new", function!(CsvTokenizer::new, 3))?;
440
+ tokenizer.define_method("feed", method!(CsvTokenizer::feed, 1))?;
441
+ tokenizer.define_method("finish", method!(CsvTokenizer::finish, 0))?;
442
+ tokenizer.define_method("records", method!(CsvTokenizer::records, 1))?;
443
+ tokenizer.define_method("error", method!(CsvTokenizer::error, 0))?;
444
+
445
+ let sheet = native.define_class("XlsxSheet", ruby.class_object())?;
446
+ sheet.define_singleton_method("new", function!(XlsxSheet::new, 1))?;
447
+ sheet.define_method("feed", method!(XlsxSheet::feed, 1))?;
448
+ sheet.define_method("finish", method!(XlsxSheet::finish, 0))?;
449
+ sheet.define_method("rows", method!(XlsxSheet::rows, 1))?;
450
+ sheet.define_method("strings", method!(XlsxSheet::strings, 2))?;
451
+ sheet.define_method("table", method!(XlsxSheet::table, 1))?;
452
+
453
+ let string_table = native.define_class("StringTable", ruby.class_object())?;
454
+ string_table.define_method("size", method!(StringTable::size, 0))?;
455
+ string_table.define_method("bytesize", method!(StringTable::bytesize, 0))?;
456
+ string_table.define_method("unchecked", method!(StringTable::unchecked, 1))?;
457
+
458
+ let strings = native.define_class("XlsxStrings", ruby.class_object())?;
459
+ strings.define_singleton_method("new", function!(XlsxStrings::new, 1))?;
460
+ strings.define_method("feed", method!(XlsxStrings::feed, 1))?;
461
+ strings.define_method("finish", method!(XlsxStrings::finish, 0))?;
462
+ strings.define_method("read", method!(XlsxStrings::read, 0))?;
463
+
464
+ let table = native.define_class("OdsTable", ruby.class_object())?;
465
+ table.define_singleton_method("new", function!(OdsTable::new, 5))?;
466
+ table.define_method("feed", method!(OdsTable::feed, 1))?;
467
+ table.define_method("finish", method!(OdsTable::finish, 0))?;
468
+ table.define_method("rows", method!(OdsTable::rows, 1))?;
469
+ Ok(())
470
+ }