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,306 @@
1
+ //! Native fast path for `roo-oxide`.
2
+ //!
3
+ //! [`reader`] turns worksheet XML into raw cell records without touching Ruby.
4
+ //! [`convert`] turns those records into roo's cells,
5
+ //! and [`store`] keeps them to build one cell at a time. [`shared_strings`]
6
+ //! reads a workbook's shared strings the way roo does.
7
+
8
+ pub mod archive;
9
+ pub mod convert;
10
+ pub mod numeric;
11
+ pub mod reader;
12
+ pub mod shared_strings;
13
+ pub mod sheet_parts;
14
+ pub mod store;
15
+
16
+ use std::cell::{Cell as StdCell, RefCell};
17
+ use std::ffi::c_void;
18
+ use std::panic::{self, AssertUnwindSafe};
19
+ use std::sync::atomic::{AtomicBool, Ordering};
20
+ use std::sync::mpsc::{self, Receiver, RecvTimeoutError, TryRecvError};
21
+ use std::time::Duration;
22
+
23
+ use magnus::value::Lazy;
24
+ use magnus::{function, method, prelude::*, Error, ExceptionClass, RArray, RHash, Ruby};
25
+
26
+ use archive::Source;
27
+ use convert::Converter;
28
+ use reader::{Mode, RawRow, Unsupported};
29
+
30
+ static UNSUPPORTED: Lazy<ExceptionClass> = Lazy::new(|r| {
31
+ r.eval("Roo::Oxide::UnsupportedError")
32
+ .expect("roo/oxide defines its error before the extension loads")
33
+ });
34
+
35
+ pub(crate) fn unsupported(ruby: &Ruby, error: Unsupported) -> Error {
36
+ Error::new(ruby.get_inner(&UNSUPPORTED), error.0)
37
+ }
38
+
39
+ /// Runs `work` with the GVL released. `work` must not touch any Ruby object,
40
+ /// and should return soon after the flag it is given turns true: Ruby sets
41
+ /// it when it has something to deliver to this thread (`Thread#raise`,
42
+ /// `Timeout`, Ctrl-C). Such an exception comes back as `Err` instead of
43
+ /// unwinding through the caller's Rust frames.
44
+ pub(crate) fn without_gvl<F: FnOnce(&AtomicBool) -> R, R>(work: F) -> Result<R, Error> {
45
+ struct Call<'a, F, R> {
46
+ work: Option<F>,
47
+ interrupted: &'a AtomicBool,
48
+ result: Option<std::thread::Result<R>>,
49
+ }
50
+
51
+ unsafe extern "C" fn trampoline<F: FnOnce(&AtomicBool) -> R, R>(
52
+ data: *mut c_void,
53
+ ) -> *mut c_void {
54
+ let call = unsafe { &mut *(data as *mut Call<'_, F, R>) };
55
+ let work = call.work.take().expect("called once");
56
+ let interrupted = call.interrupted;
57
+ call.result = Some(panic::catch_unwind(AssertUnwindSafe(|| work(interrupted))));
58
+ std::ptr::null_mut()
59
+ }
60
+
61
+ unsafe extern "C" fn unblock(data: *mut c_void) {
62
+ unsafe { &*(data as *const AtomicBool) }.store(true, Ordering::Release);
63
+ }
64
+
65
+ let interrupted = AtomicBool::new(false);
66
+ let mut call = Call {
67
+ work: Some(work),
68
+ interrupted: &interrupted,
69
+ result: None,
70
+ };
71
+ let data = &mut call as *mut Call<'_, F, R> as *mut c_void;
72
+ let flag = &interrupted as *const AtomicBool as *mut c_void;
73
+ magnus::rb_sys::protect(|| unsafe {
74
+ rb_sys::rb_thread_call_without_gvl(Some(trampoline::<F, R>), data, Some(unblock), flag);
75
+ rb_sys::Qnil as rb_sys::VALUE
76
+ })?;
77
+ match call.result.expect("trampoline ran") {
78
+ Ok(result) => Ok(result),
79
+ Err(payload) => panic::resume_unwind(payload),
80
+ }
81
+ }
82
+
83
+ enum Wait {
84
+ Batch(Batch),
85
+ /// Ruby interrupted the wait without raising; wait again.
86
+ Interrupted,
87
+ /// The reading thread ended without sending a last batch.
88
+ Stopped,
89
+ }
90
+
91
+ /// Waits for the next batch with the GVL released.
92
+ fn wait_for(batches: &Receiver<Batch>) -> Result<Wait, Error> {
93
+ without_gvl(|interrupted| loop {
94
+ match batches.recv_timeout(Duration::from_millis(20)) {
95
+ Ok(batch) => return Wait::Batch(batch),
96
+ Err(RecvTimeoutError::Disconnected) => return Wait::Stopped,
97
+ Err(RecvTimeoutError::Timeout) if interrupted.load(Ordering::Acquire) => {
98
+ return Wait::Interrupted
99
+ }
100
+ Err(RecvTimeoutError::Timeout) => {}
101
+ }
102
+ })
103
+ }
104
+
105
+ /// One streamed `<row>`, converted to cells only when roo asks for them.
106
+ #[magnus::wrap(class = "Roo::Oxide::Native::Row", free_immediately, size)]
107
+ pub struct Row(RawRow);
108
+
109
+ impl Row {
110
+ pub fn raw(&self) -> &RawRow {
111
+ &self.0
112
+ }
113
+ }
114
+
115
+ type Batch = Result<Vec<RawRow>, Unsupported>;
116
+
117
+ enum Batches {
118
+ Thread(Receiver<Batch>),
119
+ Inline {
120
+ sheet: Box<reader::SheetReader<archive::Input>>,
121
+ batch: usize,
122
+ },
123
+ }
124
+
125
+ /// Reads small ZIP entries on demand without the GVL. Large worksheets are
126
+ /// read one batch ahead on a worker, so a busy Ruby thread does not slow
127
+ /// every batch down.
128
+ #[magnus::wrap(class = "Roo::Oxide::Native::Reader", free_immediately, size)]
129
+ struct Reader {
130
+ /// `None` once closed.
131
+ batches: RefCell<Option<Batches>>,
132
+ /// The rows of the last batch not handed to Ruby yet.
133
+ pending: RefCell<std::vec::IntoIter<RawRow>>,
134
+ /// Set while a Ruby thread waits on `batches` without the GVL; the
135
+ /// receiver must not be used from two threads at once.
136
+ in_use: StdCell<bool>,
137
+ done: StdCell<bool>,
138
+ rows_read: StdCell<usize>,
139
+ }
140
+
141
+ impl Reader {
142
+ /// `batch` is the number of cells parsed per batch.
143
+ fn new(ruby: &Ruby, source: Source, stream: bool, batch: usize) -> Result<Self, Error> {
144
+ let mode = if stream { Mode::Stream } else { Mode::Document };
145
+ let mut sheet = source.open(ruby, mode)?;
146
+ let batches = if source.small() {
147
+ Batches::Inline {
148
+ sheet: Box::new(sheet),
149
+ batch,
150
+ }
151
+ } else {
152
+ let (sender, batches) = mpsc::sync_channel::<Batch>(1);
153
+ std::thread::Builder::new()
154
+ .name("roo-oxide".into())
155
+ .spawn(move || loop {
156
+ let rows = sheet.next_rows(batch);
157
+ let last = !matches!(&rows, Ok(rows) if !rows.is_empty());
158
+ if sender.send(rows).is_err() || last {
159
+ return;
160
+ }
161
+ })
162
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
163
+ Batches::Thread(batches)
164
+ };
165
+ Ok(Self {
166
+ batches: RefCell::new(Some(batches)),
167
+ pending: RefCell::new(Vec::new().into_iter()),
168
+ in_use: StdCell::new(false),
169
+ done: StdCell::new(false),
170
+ rows_read: StdCell::new(0),
171
+ })
172
+ }
173
+
174
+ fn read_batch(&self, ruby: &Ruby) -> Result<Vec<RawRow>, Error> {
175
+ if self.done.get() {
176
+ return Ok(Vec::new());
177
+ }
178
+ if self.in_use.replace(true) {
179
+ return Err(Error::new(
180
+ ruby.exception_runtime_error(),
181
+ "reader is already in use",
182
+ ));
183
+ }
184
+ let received = match self.batches.borrow_mut().as_mut() {
185
+ None => Ok(Wait::Batch(Ok(Vec::new()))),
186
+ Some(Batches::Inline { sheet, batch }) => {
187
+ without_gvl(|_| Wait::Batch(sheet.next_rows(*batch)))
188
+ }
189
+ Some(Batches::Thread(batches)) => match batches.try_recv() {
190
+ Ok(batch) => Ok(Wait::Batch(batch)),
191
+ Err(TryRecvError::Disconnected) => Ok(Wait::Stopped),
192
+ Err(TryRecvError::Empty) => loop {
193
+ match wait_for(batches) {
194
+ Ok(Wait::Interrupted) => continue,
195
+ other => break other,
196
+ }
197
+ },
198
+ },
199
+ };
200
+ self.in_use.set(false);
201
+ let rows = match received? {
202
+ Wait::Batch(batch) => batch.map_err(|e| unsupported(ruby, e))?,
203
+ Wait::Stopped | Wait::Interrupted => {
204
+ return Err(unsupported(
205
+ ruby,
206
+ Unsupported("the reader thread stopped".into()),
207
+ ));
208
+ }
209
+ };
210
+ if rows.is_empty() {
211
+ self.done.set(true);
212
+ }
213
+ Ok(rows)
214
+ }
215
+
216
+ /// Stops the reading thread, which closes the file and frees the rows it
217
+ /// read ahead, without waiting for garbage collection.
218
+ fn close(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
219
+ if rb_self.in_use.get() {
220
+ return Err(Error::new(
221
+ ruby.exception_runtime_error(),
222
+ "reader is already in use",
223
+ ));
224
+ }
225
+ rb_self.batches.borrow_mut().take();
226
+ rb_self.pending.replace(Vec::new().into_iter());
227
+ rb_self.done.set(true);
228
+ Ok(())
229
+ }
230
+
231
+ /// Document mode: adds the next batch of rows to `cells`. Returns
232
+ /// `false` once the sheet is exhausted.
233
+ fn fill_cells(
234
+ ruby: &Ruby,
235
+ rb_self: &Self,
236
+ cells: RHash,
237
+ converter: &Converter,
238
+ ) -> Result<bool, Error> {
239
+ let rows = rb_self.read_batch(ruby)?;
240
+ for row in &rows {
241
+ let row_number = rb_self.rows_read.get() + 1;
242
+ rb_self.rows_read.set(row_number);
243
+ converter.add_row(ruby, cells, row, row_number)?;
244
+ }
245
+ Ok(!rows.is_empty())
246
+ }
247
+
248
+ /// Stream mode: the next row of the sheet, `None` at its end. Rows wait
249
+ /// in their batch until asked for, so Ruby holds one at a time.
250
+ fn next_raw_row(&self, ruby: &Ruby) -> Result<Option<RawRow>, Error> {
251
+ if let Some(row) = self.pending.borrow_mut().next() {
252
+ return Ok(Some(row));
253
+ }
254
+ let mut rows = self.read_batch(ruby)?.into_iter();
255
+ let row = rows.next();
256
+ self.pending.replace(rows);
257
+ Ok(row)
258
+ }
259
+
260
+ /// Stream mode: the next row, or `nil` at the end of the sheet.
261
+ fn next_row(ruby: &Ruby, rb_self: &Self) -> Result<Option<Row>, Error> {
262
+ Ok(rb_self.next_raw_row(ruby)?.map(Row))
263
+ }
264
+
265
+ /// Stream mode: the values of the next row, as
266
+ /// [`Converter::row_values`] gives them, or `nil` at the end of the
267
+ /// sheet. Each row is converted only when asked for, as roo reads it,
268
+ /// and its parsed record is freed right after.
269
+ fn next_values(
270
+ ruby: &Ruby,
271
+ rb_self: &Self,
272
+ converter: &Converter,
273
+ ) -> Result<Option<RArray>, Error> {
274
+ match rb_self.next_raw_row(ruby)? {
275
+ Some(row) => converter.row_values(ruby, &row).map(Some),
276
+ None => Ok(None),
277
+ }
278
+ }
279
+ }
280
+
281
+ #[magnus::init]
282
+ fn init(ruby: &Ruby) -> Result<(), Error> {
283
+ let native = ruby
284
+ .define_module("Roo")?
285
+ .define_module("Oxide")?
286
+ .define_module("Native")?;
287
+
288
+ let reader = native.define_class("Reader", ruby.class_object())?;
289
+ reader.define_singleton_method("new", function!(Reader::new, 3))?;
290
+ reader.define_method("fill_cells", method!(Reader::fill_cells, 2))?;
291
+ reader.define_method("next_row", method!(Reader::next_row, 0))?;
292
+ reader.define_method("next_values", method!(Reader::next_values, 1))?;
293
+ reader.define_method("close", method!(Reader::close, 0))?;
294
+
295
+ native.define_class("Row", ruby.class_object())?;
296
+
297
+ let converter = native.define_class("Converter", ruby.class_object())?;
298
+ converter.define_singleton_method("new", function!(Converter::new, 2))?;
299
+ converter.define_method("row_cells", method!(Converter::row_cells, 2))?;
300
+
301
+ store::define(ruby, native)?;
302
+ shared_strings::define(native)?;
303
+ sheet_parts::define(native)?;
304
+ archive::define(ruby, native)?;
305
+ Ok(())
306
+ }
@@ -0,0 +1,174 @@
1
+ //! roo's text-to-number conversions, for the plainly written numbers that
2
+ //! worksheets store: `-?\d+(\.\d+)?([eE][-+]?\d+)?`. Any other text gives
3
+ //! `None`, and the caller leaves the conversion to roo's own code.
4
+
5
+ use magnus::{IntoValue, Ruby, Value};
6
+
7
+ #[derive(Clone, Copy, Debug, PartialEq)]
8
+ pub enum Number {
9
+ Integer(i64),
10
+ Float(f64),
11
+ }
12
+
13
+ impl IntoValue for Number {
14
+ fn into_value_with(self, ruby: &Ruby) -> Value {
15
+ match self {
16
+ Number::Integer(integer) => ruby.integer_from_i64(integer).into_value_with(ruby),
17
+ Number::Float(float) => ruby.float_from_f64(float).into_value_with(ruby),
18
+ }
19
+ }
20
+ }
21
+
22
+ /// Plain number text, split where Ruby's conversions tell it apart.
23
+ struct Plain<'a> {
24
+ /// The sign and the digits before any `.` or exponent.
25
+ integer: &'a str,
26
+ fraction: bool,
27
+ exponent: bool,
28
+ /// The digits before the exponent.
29
+ mantissa: &'a [u8],
30
+ }
31
+
32
+ impl<'a> Plain<'a> {
33
+ fn parse(text: &'a str) -> Option<Self> {
34
+ let bytes = text.as_bytes();
35
+ let mut at = usize::from(bytes.first() == Some(&b'-'));
36
+ let digits = |at: &mut usize| {
37
+ let start = *at;
38
+ while bytes.get(*at).is_some_and(u8::is_ascii_digit) {
39
+ *at += 1;
40
+ }
41
+ *at > start
42
+ };
43
+ if !digits(&mut at) {
44
+ return None;
45
+ }
46
+ let integer_end = at;
47
+ let fraction = bytes.get(at) == Some(&b'.');
48
+ if fraction {
49
+ at += 1;
50
+ if !digits(&mut at) {
51
+ return None;
52
+ }
53
+ }
54
+ let mantissa_end = at;
55
+ let exponent = matches!(bytes.get(at), Some(b'e' | b'E'));
56
+ if exponent {
57
+ at += 1;
58
+ if matches!(bytes.get(at), Some(b'+' | b'-')) {
59
+ at += 1;
60
+ }
61
+ if !digits(&mut at) {
62
+ return None;
63
+ }
64
+ }
65
+ (at == bytes.len()).then(|| Self {
66
+ integer: &text[..integer_end],
67
+ fraction,
68
+ exponent,
69
+ mantissa: &bytes[..mantissa_end],
70
+ })
71
+ }
72
+
73
+ /// The integer part, when it fits an `i64`.
74
+ fn integer(&self) -> Option<i64> {
75
+ self.integer.parse().ok()
76
+ }
77
+ }
78
+
79
+ /// `Cell::Number#create_numeric(text)` under a format that matches `/%/` or
80
+ /// `/\.0/` (`always_float`) or one that does not.
81
+ pub fn create_numeric(text: &str, always_float: bool) -> Option<Number> {
82
+ let plain = Plain::parse(text)?;
83
+ if always_float || plain.fraction || plain.exponent {
84
+ // Ruby's `strtod` stops reading fraction digits after about 60 digits,
85
+ // so a longer mantissa can round differently.
86
+ if plain.mantissa.len() > 40 {
87
+ return None;
88
+ }
89
+ let float: f64 = text.parse().ok()?;
90
+ // Ruby's `Float()` warns when the text is out of range (infinite, or
91
+ // zero although a digit is not); subnormal results are left to Ruby
92
+ // too.
93
+ let nonzero = plain.mantissa.iter().any(|b| matches!(b, b'1'..=b'9'));
94
+ let in_range = float.is_normal() || (float == 0.0 && !nonzero);
95
+ in_range.then_some(Number::Float(float))
96
+ } else {
97
+ plain.integer().map(Number::Integer)
98
+ }
99
+ }
100
+
101
+ /// Text that `Float()` and `Integer(text, 10)` both read as a finite number,
102
+ /// and `String#to_f` reads whole.
103
+ pub fn plain_number(text: &str) -> bool {
104
+ Plain::parse(text).is_some() && text.parse::<f64>().is_ok_and(f64::is_finite)
105
+ }
106
+
107
+ /// `String#to_i` of plain number text.
108
+ pub fn leading_integer(text: &str) -> Option<i64> {
109
+ Plain::parse(text)?.integer()
110
+ }
111
+
112
+ #[cfg(test)]
113
+ mod tests {
114
+ use super::{create_numeric, leading_integer, Number};
115
+
116
+ #[test]
117
+ fn integers_and_floats_follow_the_text_and_the_format() {
118
+ assert_eq!(create_numeric("12", false), Some(Number::Integer(12)));
119
+ assert_eq!(create_numeric("-0", false), Some(Number::Integer(0)));
120
+ assert_eq!(create_numeric("007", false), Some(Number::Integer(7)));
121
+ assert_eq!(create_numeric("12", true), Some(Number::Float(12.0)));
122
+ assert_eq!(create_numeric("1.5", false), Some(Number::Float(1.5)));
123
+ assert_eq!(create_numeric("1E3", false), Some(Number::Float(1000.0)));
124
+ assert_eq!(create_numeric("2.5e-3", false), Some(Number::Float(0.0025)));
125
+ let Some(Number::Float(negative_zero)) = create_numeric("-0", true) else {
126
+ panic!("-0 under a float format is a Float");
127
+ };
128
+ assert!(negative_zero.is_sign_negative());
129
+ assert_eq!(create_numeric("0e999", false), Some(Number::Float(0.0)));
130
+ }
131
+
132
+ #[test]
133
+ fn leaves_other_text_to_roo() {
134
+ for text in [
135
+ "",
136
+ "-",
137
+ "+5",
138
+ " 1",
139
+ "1 ",
140
+ "1_000",
141
+ "0x1A",
142
+ "1.",
143
+ ".5",
144
+ "1.e5",
145
+ "1e",
146
+ "1e+",
147
+ "inf",
148
+ "NaN",
149
+ "#N/A",
150
+ "1e400",
151
+ "1e-400",
152
+ "4.9e-324",
153
+ "12855504354071923631583389444689181878463593399757479065157632.5",
154
+ ] {
155
+ assert_eq!(create_numeric(text, false), None, "{text:?}");
156
+ assert_eq!(create_numeric(text, true), None, "{text:?}");
157
+ }
158
+ assert_eq!(create_numeric("9223372036854775808", false), None);
159
+ assert_eq!(
160
+ create_numeric("-9223372036854775808", false),
161
+ Some(Number::Integer(i64::MIN))
162
+ );
163
+ }
164
+
165
+ #[test]
166
+ fn leading_integer_is_string_to_i() {
167
+ assert_eq!(leading_integer("45000"), Some(45000));
168
+ assert_eq!(leading_integer("45000.9"), Some(45000));
169
+ assert_eq!(leading_integer("1E3"), Some(1));
170
+ assert_eq!(leading_integer("-0.5"), Some(0));
171
+ assert_eq!(leading_integer("#N/A"), None);
172
+ assert_eq!(leading_integer("12345678901234567890"), None);
173
+ }
174
+ }
@@ -0,0 +1,199 @@
1
+ //! The attributes of a start tag, read in one pass.
2
+ //!
3
+ //! quick-xml leaves most of an attribute list unchecked, so this reads it
4
+ //! along the XML grammar `(S Name S? '=' S? AttValue)* S?` and rejects
5
+ //! everything else, as libxml2 does: a missing `=` or quote, an unquoted
6
+ //! value, `<` in a value, attributes without whitespace between them, and a
7
+ //! name used twice.
8
+
9
+ use std::borrow::Cow;
10
+ use std::collections::HashSet;
11
+ use std::ops::Range;
12
+
13
+ use quick_xml::name::QName;
14
+ use quick_xml::XmlVersion;
15
+
16
+ use super::{check_chars, check_name, Result, Unsupported};
17
+
18
+ /// An attribute as written, before references are resolved.
19
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
20
+ pub struct Attribute<'a> {
21
+ pub key: &'a str,
22
+ pub value: &'a str,
23
+ }
24
+
25
+ impl<'a> Attribute<'a> {
26
+ /// The value with references resolved and whitespace normalized, as
27
+ /// libxml2 reads it. Fails on a reference XML does not define.
28
+ pub fn normalized_value(&self) -> Result<Cow<'a, str>> {
29
+ if !self
30
+ .value
31
+ .bytes()
32
+ .any(|b| matches!(b, b'&' | b'\t' | b'\n' | b'\r'))
33
+ {
34
+ return Ok(Cow::Borrowed(self.value));
35
+ }
36
+ let attribute = quick_xml::events::attributes::Attribute {
37
+ key: QName(self.key),
38
+ value: Cow::Borrowed(self.value),
39
+ };
40
+ Ok(attribute.normalized_value(XmlVersion::Implicit1_0)?)
41
+ }
42
+ }
43
+
44
+ /// The attributes of the last start tag read, as byte ranges of its
45
+ /// attribute list, so a tag is read once. The buffer is reused from tag to
46
+ /// tag.
47
+ #[derive(Default)]
48
+ pub struct TagAttributes {
49
+ spans: Vec<(Range<usize>, Range<usize>)>,
50
+ prefixed: bool,
51
+ references: bool,
52
+ }
53
+
54
+ impl TagAttributes {
55
+ /// Reads and checks `raw`, what quick-xml's `BytesStart::attributes_raw`
56
+ /// returns, in place of the tag read before.
57
+ pub fn read(&mut self, raw: &str) -> Result<()> {
58
+ self.prefixed = false;
59
+ self.references = false;
60
+ if self.read_common(raw) {
61
+ return Ok(());
62
+ }
63
+ self.spans.clear();
64
+ check_chars(raw)?;
65
+ let bytes = raw.as_bytes();
66
+ let mut position = 0;
67
+ loop {
68
+ let start = skip_whitespace(bytes, position);
69
+ if start == bytes.len() {
70
+ break;
71
+ }
72
+ if start == position {
73
+ return Err(Unsupported(
74
+ "attributes without whitespace between them".into(),
75
+ ));
76
+ }
77
+ let key_end = start
78
+ + bytes[start..]
79
+ .iter()
80
+ .position(|&b| b == b'=' || is_whitespace(b))
81
+ .unwrap_or(bytes.len() - start);
82
+ let key = &raw[start..key_end];
83
+ check_name(key)?;
84
+ self.prefixed |= key.contains(':');
85
+ let equals = skip_whitespace(bytes, key_end);
86
+ if bytes.get(equals) != Some(&b'=') {
87
+ return Err(Unsupported(format!("no `=` after the attribute {key}")));
88
+ }
89
+ let quote_at = skip_whitespace(bytes, equals + 1);
90
+ let quote = match bytes.get(quote_at) {
91
+ Some(&quote @ (b'"' | b'\'')) => quote,
92
+ _ => return Err(Unsupported(format!("an unquoted value for {key}"))),
93
+ };
94
+ let value_start = quote_at + 1;
95
+ let mut value_end = value_start;
96
+ loop {
97
+ match bytes.get(value_end) {
98
+ Some(&b) if b == quote => break,
99
+ Some(b'<') => return Err(Unsupported("`<` in an attribute value".into())),
100
+ Some(b'&') => self.references = true,
101
+ Some(_) => {}
102
+ None => return Err(Unsupported(format!("an unclosed value for {key}"))),
103
+ }
104
+ value_end += 1;
105
+ }
106
+ self.spans.push((start..key_end, value_start..value_end));
107
+ position = value_end + 1;
108
+ }
109
+ self.check_unique(bytes)
110
+ }
111
+
112
+ /// Reads `raw` when it holds only ` r="…"`, ` s="…"`, and ` t="…"`, each at most once,
113
+ /// with printable ASCII values without references.
114
+ pub(super) fn read_common(&mut self, raw: &str) -> bool {
115
+ self.spans.clear();
116
+ let bytes = raw.as_bytes();
117
+ let mut seen = 0u8;
118
+ let mut position = 0;
119
+ while position < bytes.len() {
120
+ let &[b' ', key, b'=', b'"', ..] = &bytes[position..] else {
121
+ return false;
122
+ };
123
+ let bit = match key {
124
+ b'r' => 1,
125
+ b's' => 2,
126
+ b't' => 4,
127
+ _ => return false,
128
+ };
129
+ if seen & bit != 0 {
130
+ return false;
131
+ }
132
+ seen |= bit;
133
+ let value_start = position + 4;
134
+ let Some(length) = bytes[value_start..].iter().position(|&b| b == b'"') else {
135
+ return false;
136
+ };
137
+ let value_end = value_start + length;
138
+ if !bytes[value_start..value_end]
139
+ .iter()
140
+ .all(|&b| matches!(b, b' '..=b'~') && b != b'&' && b != b'<')
141
+ {
142
+ return false;
143
+ }
144
+ self.spans
145
+ .push((position + 1..position + 2, value_start..value_end));
146
+ position = value_end + 1;
147
+ }
148
+ true
149
+ }
150
+
151
+ /// Whether a key has a namespace prefix.
152
+ pub fn prefixed(&self) -> bool {
153
+ self.prefixed
154
+ }
155
+
156
+ /// Whether a value holds a reference.
157
+ pub fn references(&self) -> bool {
158
+ self.references
159
+ }
160
+
161
+ /// The attributes [`read`](Self::read) found in `raw`.
162
+ pub fn iter<'a>(&'a self, raw: &'a str) -> impl Iterator<Item = Attribute<'a>> + 'a {
163
+ self.spans.iter().map(move |(key, value)| Attribute {
164
+ key: &raw[key.clone()],
165
+ value: &raw[value.clone()],
166
+ })
167
+ }
168
+
169
+ fn check_unique(&self, bytes: &[u8]) -> Result<()> {
170
+ let key = |(key, _): &(Range<usize>, Range<usize>)| &bytes[key.clone()];
171
+ let unique = if self.spans.len() <= 8 {
172
+ self.spans.iter().enumerate().all(|(i, span)| {
173
+ let key = key(span);
174
+ self.spans[..i]
175
+ .iter()
176
+ .all(|earlier| bytes[earlier.0.clone()] != *key)
177
+ })
178
+ } else {
179
+ let mut seen = HashSet::with_capacity(self.spans.len());
180
+ self.spans.iter().all(|span| seen.insert(key(span)))
181
+ };
182
+ if unique {
183
+ Ok(())
184
+ } else {
185
+ Err(Unsupported("an attribute named twice".into()))
186
+ }
187
+ }
188
+ }
189
+
190
+ fn is_whitespace(b: u8) -> bool {
191
+ matches!(b, b' ' | b'\t' | b'\r' | b'\n')
192
+ }
193
+
194
+ fn skip_whitespace(bytes: &[u8], from: usize) -> usize {
195
+ from + bytes[from..]
196
+ .iter()
197
+ .take_while(|&&b| is_whitespace(b))
198
+ .count()
199
+ }