• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zbraniecki / icu4x / 12020603084

23 Nov 2024 08:43PM UTC coverage: 75.71% (+0.2%) from 75.477%
12020603084

push

github

sffc
Touch Cargo.lock

55589 of 73424 relevant lines covered (75.71%)

644270.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.7
/components/pattern/src/multi_named.rs
1
// This file is part of ICU4X. For terms of use, please see the file
2
// called LICENSE at the top level of the ICU4X source tree
3
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4

5
//! Code for the [`MultiNamedPlaceholder`] pattern backend.
6

7
#[cfg(feature = "alloc")]
8
use alloc::{borrow::Cow, boxed::Box, collections::BTreeMap, str::FromStr, string::String};
9
use core::fmt;
10
#[cfg(feature = "litemap")]
11
use litemap::LiteMap;
12
use writeable::Writeable;
13

14
use crate::common::*;
15
use crate::Error;
16

17
/// A string wrapper for the [`MultiNamedPlaceholder`] pattern backend.
18
///
19
/// # Examples
20
///
21
/// ```
22
/// use core::cmp::Ordering;
23
/// use core::str::FromStr;
24
/// use icu_pattern::MultiNamedPlaceholderKey;
25
/// use icu_pattern::MultiNamedPlaceholderPattern;
26
/// use icu_pattern::PatternItem;
27
///
28
/// // Parse the string syntax and check the resulting data store:
29
/// let pattern = MultiNamedPlaceholderPattern::try_from_str(
30
///     "Hello, {person0} and {person1}!",
31
///     Default::default(),
32
/// )
33
/// .unwrap();
34
///
35
/// assert_eq!(
36
///     pattern.iter().cmp(
37
///         [
38
///             PatternItem::Literal("Hello, "),
39
///             PatternItem::Placeholder(MultiNamedPlaceholderKey(
40
///                 "person0".into()
41
///             )),
42
///             PatternItem::Literal(" and "),
43
///             PatternItem::Placeholder(MultiNamedPlaceholderKey(
44
///                 "person1".into()
45
///             )),
46
///             PatternItem::Literal("!")
47
///         ]
48
///         .into_iter()
49
///     ),
50
///     Ordering::Equal
51
/// );
52
/// ```
53
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4✔
54
#[repr(transparent)]
55
#[allow(clippy::exhaustive_structs)] // transparent newtype
56
pub struct MultiNamedPlaceholderKey<'a>(pub &'a str);
2✔
57

58
/// Cowable version of [`MultiNamedPlaceholderKey`], used during construction.
59
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
×
60
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
×
61
#[repr(transparent)]
62
#[allow(clippy::exhaustive_structs)] // transparent newtype
63
#[cfg(feature = "alloc")]
64
pub struct MultiNamedPlaceholderKeyCow<'a>(pub Cow<'a, str>);
×
65

66
#[cfg(feature = "alloc")]
67
impl FromStr for MultiNamedPlaceholderKeyCow<'_> {
68
    type Err = Error;
69
    fn from_str(s: &str) -> Result<Self, Self::Err> {
88✔
70
        // Can't borrow the str here unfortunately
71
        Ok(MultiNamedPlaceholderKeyCow(Cow::Owned(String::from(s))))
88✔
72
    }
88✔
73
}
74

75
#[derive(Debug, Clone, PartialEq, Eq)]
×
76
#[non_exhaustive]
77
pub struct MissingNamedPlaceholderError<'a> {
78
    pub name: &'a str,
×
79
}
80

81
impl Writeable for MissingNamedPlaceholderError<'_> {
82
    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
×
83
        sink.write_char('{')?;
×
84
        sink.write_str(self.name)?;
×
85
        sink.write_char('}')?;
×
86
        Ok(())
×
87
    }
×
88
}
89

90
#[cfg(feature = "alloc")]
91
impl<'k, K, W> PlaceholderValueProvider<MultiNamedPlaceholderKey<'k>> for BTreeMap<K, W>
92
where
93
    K: Ord + core::borrow::Borrow<str>,
94
    W: Writeable,
95
{
96
    type Error = MissingNamedPlaceholderError<'k>;
97
    type W<'a>
98
        = Result<&'a W, Self::Error>
99
    where
100
        W: 'a,
101
        Self: 'a;
102
    const LITERAL_PART: writeable::Part = crate::PATTERN_LITERAL_PART;
103
    #[inline]
104
    fn value_for<'a>(
105
        &'a self,
106
        key: MultiNamedPlaceholderKey<'k>,
107
    ) -> (Self::W<'a>, writeable::Part) {
108
        let writeable = match self.get(key.0) {
109
            Some(value) => Ok(value),
110
            None => Err(MissingNamedPlaceholderError { name: key.0 }),
111
        };
112
        (writeable, crate::PATTERN_PLACEHOLDER_PART)
113
    }
114
}
115

116
#[cfg(feature = "litemap")]
117
impl<'k, K, W, S> PlaceholderValueProvider<MultiNamedPlaceholderKey<'k>> for LiteMap<K, W, S>
118
where
119
    K: Ord + core::borrow::Borrow<str>,
120
    W: Writeable,
121
    S: litemap::store::Store<K, W>,
122
{
123
    type Error = MissingNamedPlaceholderError<'k>;
124
    type W<'a>
125
        = Result<&'a W, Self::Error>
126
    where
127
        W: 'a,
128
        Self: 'a;
129
    const LITERAL_PART: writeable::Part = crate::PATTERN_LITERAL_PART;
130
    #[inline]
131
    fn value_for<'a>(
132
        &'a self,
133
        key: MultiNamedPlaceholderKey<'k>,
134
    ) -> (Self::W<'a>, writeable::Part) {
135
        let writeable = match self.get(key.0) {
136
            Some(value) => Ok(value),
137
            None => Err(MissingNamedPlaceholderError { name: key.0 }),
138
        };
139
        (writeable, crate::PATTERN_PLACEHOLDER_PART)
140
    }
141
}
142

143
/// Backend for patterns containing zero or more named placeholders.
144
///
145
/// This empty type is not constructible.
146
///
147
/// # Placeholder Keys
148
///
149
/// The placeholder is [`MultiNamedPlaceholderKey`].
150
///
151
/// In [`Pattern::interpolate()`], pass a map-like structure. Missing keys will be replaced
152
/// with the Unicode replacement character U+FFFD.
153
///
154
/// # Encoding Details
155
///
156
/// The literals and placeholders are stored in context. A placeholder is encoded as a name length
157
/// in octal code points followed by the placeholder name.
158
///
159
/// For example, consider the pattern: "Hello, {user} and {someone_else}!"
160
///
161
/// The encoding for this would be:
162
///
163
/// ```txt
164
/// Hello, \x00\x04user and \x01\x04someone_else!
165
/// ```
166
///
167
/// where `\x00\x04` and `\x01\x04` are a big-endian octal number representing the lengths of
168
/// their respective placeholder names.
169
///
170
/// Consequences of this encoding:
171
///
172
/// 1. The maximum placeholder name length is 64 bytes
173
/// 2. Code points in the range `\x00` through `\x07` are reserved for the placeholder name
174
///
175
/// # Examples
176
///
177
/// Example patterns supported by this backend:
178
///
179
/// ```
180
/// use core::str::FromStr;
181
/// use icu_pattern::MultiNamedPlaceholder;
182
/// use icu_pattern::Pattern;
183
/// use std::collections::BTreeMap;
184
///
185
/// let placeholder_value_map: BTreeMap<&str, &str> = [
186
///     ("num", "5"),
187
///     ("letter", "X"),
188
///     ("", "empty"),
189
///     ("unused", "unused"),
190
/// ]
191
/// .into_iter()
192
/// .collect();
193
///
194
/// // Single placeholder:
195
/// assert_eq!(
196
///     Pattern::<MultiNamedPlaceholder>::try_from_str(
197
///         "{num} days ago",
198
///         Default::default()
199
///     )
200
///     .unwrap()
201
///     .try_interpolate_to_string(&placeholder_value_map)
202
///     .unwrap(),
203
///     "5 days ago",
204
/// );
205
///
206
/// // No placeholder (note, the placeholder value is never accessed):
207
/// assert_eq!(
208
///     Pattern::<MultiNamedPlaceholder>::try_from_str(
209
///         "yesterday",
210
///         Default::default()
211
///     )
212
///     .unwrap()
213
///     .try_interpolate_to_string(&placeholder_value_map)
214
///     .unwrap(),
215
///     "yesterday",
216
/// );
217
///
218
/// // No literals, only placeholders:
219
/// assert_eq!(
220
///     Pattern::<MultiNamedPlaceholder>::try_from_str(
221
///         "{letter}{num}{}",
222
///         Default::default()
223
///     )
224
///     .unwrap()
225
///     .try_interpolate_to_string(&placeholder_value_map)
226
///     .unwrap(),
227
///     "X5empty",
228
/// );
229
/// ```
230
///
231
/// Use [`LiteMap`] for alloc-free formatting:
232
///
233
/// ```
234
/// use core::str::FromStr;
235
/// use icu_pattern::MultiNamedPlaceholderPattern;
236
/// use litemap::LiteMap;
237
/// use writeable::TryWriteable;
238
///
239
/// static placeholder_value_map: LiteMap<&str, usize, &[(&str, usize)]> =
240
///     LiteMap::from_sorted_store_unchecked(&[("seven", 11)]);
241
///
242
/// // Note: String allocates, but this could be a non-allocating sink
243
/// let mut sink = String::new();
244
///
245
/// MultiNamedPlaceholderPattern::try_from_str("{seven}", Default::default())
246
///     .unwrap()
247
///     .try_interpolate(&placeholder_value_map)
248
///     .try_write_to(&mut sink)
249
///     .unwrap()
250
///     .unwrap();
251
///
252
/// assert_eq!(sink, "11");
253
/// ```
254
///
255
/// Missing placeholder values cause an error result to be returned. However,
256
/// based on the design of [`TryWriteable`], the error can be discarded to get
257
/// a best-effort interpolation with potential replacement characters.
258
///
259
/// ```should_panic
260
/// use core::str::FromStr;
261
/// use icu_pattern::MultiNamedPlaceholder;
262
/// use icu_pattern::Pattern;
263
/// use std::collections::BTreeMap;
264
///
265
/// let placeholder_value_map: BTreeMap<&str, &str> =
266
///     [("num", "5"), ("letter", "X")].into_iter().collect();
267
///
268
/// Pattern::<MultiNamedPlaceholder>::try_from_str(
269
///     "Your name is {your_name}",
270
///     Default::default(),
271
/// )
272
/// .unwrap()
273
/// .try_interpolate_to_string(&placeholder_value_map)
274
/// .unwrap();
275
/// ```
276
///
277
/// Recover the best-effort lossy string by directly using [`Pattern::try_interpolate()`]:
278
///
279
/// ```
280
/// use core::str::FromStr;
281
/// use icu_pattern::MissingNamedPlaceholderError;
282
/// use icu_pattern::MultiNamedPlaceholder;
283
/// use icu_pattern::Pattern;
284
/// use std::borrow::Cow;
285
/// use std::collections::BTreeMap;
286
/// use writeable::TryWriteable;
287
///
288
/// let placeholder_value_map: BTreeMap<&str, &str> =
289
///     [("num", "5"), ("letter", "X")].into_iter().collect();
290
///
291
/// let pattern = Pattern::<MultiNamedPlaceholder>::try_from_str(
292
///     "Your name is {your_name}",
293
///     Default::default(),
294
/// )
295
/// .unwrap();
296
///
297
/// let mut buffer = String::new();
298
/// let result = pattern
299
///     .try_interpolate(&placeholder_value_map)
300
///     .try_write_to(&mut buffer)
301
///     .expect("infallible write to String");
302
///
303
/// assert!(matches!(result, Err(MissingNamedPlaceholderError { .. })));
304
/// assert_eq!(result.unwrap_err().name, "your_name");
305
/// assert_eq!(buffer, "Your name is {your_name}");
306
/// ```
307
///
308
/// [`Pattern::interpolate()`]: crate::Pattern::interpolate
309
/// [`Pattern::try_interpolate()`]: crate::Pattern::try_interpolate
310
/// [`TryWriteable`]: writeable::TryWriteable
311
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
×
312
#[allow(clippy::exhaustive_enums)] // Empty Enum
313
pub enum MultiNamedPlaceholder {}
314

315
impl crate::private::Sealed for MultiNamedPlaceholder {}
316

317
impl PatternBackend for MultiNamedPlaceholder {
318
    type PlaceholderKey<'a> = MultiNamedPlaceholderKey<'a>;
319
    #[cfg(feature = "alloc")]
320
    type PlaceholderKeyCow<'a> = MultiNamedPlaceholderKeyCow<'a>;
321
    type Error<'a> = MissingNamedPlaceholderError<'a>;
322
    type Store = str;
323
    type Iter<'a> = MultiNamedPlaceholderPatternIterator<'a>;
324

325
    fn validate_store(store: &Self::Store) -> Result<(), Error> {
32✔
326
        let mut iter = MultiNamedPlaceholderPatternIterator::new(store);
32✔
327
        while iter
187✔
328
            .try_next()
329
            .map_err(|e| match e {
4✔
330
                MultiNamedPlaceholderError::InvalidStore => Error::InvalidPattern,
4✔
331
                MultiNamedPlaceholderError::Unreachable => {
332
                    debug_assert!(false, "unreachable");
×
333
                    Error::InvalidPattern
334
                }
335
            })?
8✔
336
            .is_some()
337
        {}
338
        Ok(())
28✔
339
    }
32✔
340

341
    fn iter_items(store: &Self::Store) -> Self::Iter<'_> {
28✔
342
        MultiNamedPlaceholderPatternIterator::new(store)
28✔
343
    }
28✔
344

345
    #[cfg(feature = "alloc")]
346
    fn try_from_items<
143✔
347
        'cow,
348
        'ph,
349
        I: Iterator<Item = Result<PatternItemCow<'cow, Self::PlaceholderKeyCow<'ph>>, Error>>,
350
    >(
351
        items: I,
352
    ) -> Result<Box<str>, Error> {
353
        let mut string = String::new();
143✔
354
        for item in items {
172✔
355
            match item? {
137✔
356
                PatternItemCow::Literal(s) if s.contains(|x| (x as usize) <= 0x07) => {
135✔
357
                    // TODO: Should this be a different error type?
358
                    return Err(Error::InvalidPattern);
4✔
359
                }
4✔
360
                PatternItemCow::Literal(s) => string.push_str(&s),
56✔
361
                PatternItemCow::Placeholder(ph_key) => {
92✔
362
                    let name_length = ph_key.0.len();
92✔
363
                    if name_length >= 64 {
77✔
364
                        return Err(Error::InvalidPlaceholder);
1✔
365
                    }
366
                    let lead = (name_length >> 3) as u8;
78✔
367
                    let trail = (name_length & 0x7) as u8;
78✔
368
                    string.push(char::from(lead));
76✔
369
                    string.push(char::from(trail));
77✔
370
                    string.push_str(&ph_key.0);
76✔
371
                }
82✔
372
            }
373
        }
52✔
374
        Ok(string.into_boxed_str())
19✔
375
    }
25✔
376

377
    fn empty() -> &'static Self::Store {
1✔
378
        ""
379
    }
1✔
380
}
381

382
#[derive(Debug)]
×
383
pub struct MultiNamedPlaceholderPatternIterator<'a> {
384
    store: &'a str,
×
385
}
386

387
// Note: we don't implement ExactSizeIterator since we don't store that metadata in MultiNamed.
388

389
impl<'a> Iterator for MultiNamedPlaceholderPatternIterator<'a> {
390
    type Item = PatternItem<'a, MultiNamedPlaceholderKey<'a>>;
391
    fn next(&mut self) -> Option<Self::Item> {
185✔
392
        match self.try_next() {
185✔
393
            Ok(next) => next,
185✔
394
            Err(MultiNamedPlaceholderError::InvalidStore) => {
395
                debug_assert!(
×
396
                    false,
397
                    "invalid store with {} bytes remaining",
398
                    self.store.len()
×
399
                );
400
                None
401
            }
402
            Err(MultiNamedPlaceholderError::Unreachable) => {
403
                debug_assert!(false, "unreachable");
×
404
                None
405
            }
406
        }
407
    }
185✔
408
}
409

410
enum MultiNamedPlaceholderError {
411
    InvalidStore,
412
    Unreachable,
413
}
414

415
impl<'a> MultiNamedPlaceholderPatternIterator<'a> {
416
    fn new(store: &'a str) -> Self {
60✔
417
        Self { store }
418
    }
60✔
419

420
    fn try_next(
380✔
421
        &mut self,
422
    ) -> Result<Option<PatternItem<'a, MultiNamedPlaceholderKey<'a>>>, MultiNamedPlaceholderError>
423
    {
424
        match self.store.find(|x| (x as usize) <= 0x07) {
970✔
425
            Some(0) => {
426
                // Placeholder
427
                let Some(&[lead, trail]) = self.store.as_bytes().get(0..2) else {
190✔
428
                    return Err(MultiNamedPlaceholderError::InvalidStore);
2✔
429
                };
430
                debug_assert!(lead <= 7);
182✔
431
                if trail > 7 {
182✔
432
                    return Err(MultiNamedPlaceholderError::InvalidStore);
×
433
                }
434
                let placeholder_len = (lead << 3) + trail;
182✔
435
                let boundary = (placeholder_len as usize) + 2;
182✔
436
                // TODO: use .split_at_checked() when available:
437
                // https://github.com/rust-lang/rust/issues/119128
438
                let Some(placeholder_name) = self.store.get(2..boundary) else {
182✔
439
                    return Err(MultiNamedPlaceholderError::InvalidStore);
2✔
440
                };
441
                let Some(remainder) = self.store.get(boundary..) else {
180✔
442
                    debug_assert!(false, "should be a perfect slice");
×
443
                    return Err(MultiNamedPlaceholderError::Unreachable);
444
                };
445
                self.store = remainder;
180✔
446
                Ok(Some(PatternItem::Placeholder(MultiNamedPlaceholderKey(
180✔
447
                    placeholder_name,
448
                ))))
449
            }
180✔
450
            Some(i) => {
124✔
451
                // Literal
452
                // TODO: use .split_at_checked() when available:
453
                // https://github.com/rust-lang/rust/issues/119128
454
                let Some(literal) = self.store.get(..i) else {
124✔
455
                    debug_assert!(false, "should be a perfect slice");
×
456
                    return Err(MultiNamedPlaceholderError::Unreachable);
457
                };
458
                let Some(remainder) = self.store.get(i..) else {
124✔
459
                    debug_assert!(false, "should be a perfect slice");
×
460
                    return Err(MultiNamedPlaceholderError::Unreachable);
461
                };
462
                self.store = remainder;
124✔
463
                Ok(Some(PatternItem::Literal(literal)))
124✔
464
            }
124✔
465
            None if self.store.is_empty() => {
66✔
466
                // End of string
467
                Ok(None)
57✔
468
            }
469
            None => {
470
                // Closing literal
471
                let literal = self.store;
9✔
472
                self.store = "";
9✔
473
                Ok(Some(PatternItem::Literal(literal)))
9✔
474
            }
9✔
475
        }
476
    }
374✔
477
}
478

479
#[cfg(test)]
480
mod tests {
481
    use super::*;
482
    use crate::{MultiNamedPlaceholder, MultiNamedPlaceholderPattern};
483

484
    #[test]
485
    fn test_invalid() {
2✔
486
        let long_str = "0123456789".repeat(1000000);
1✔
487
        let strings = [
1✔
488
            "{",    // invalid syntax
489
            "{@}",  // placeholder name too long
490
            "\x00", // invalid character
491
            "\x07", // invalid character
492
        ];
493
        for string in strings {
1✔
494
            let string = string.replace('@', &long_str);
4✔
495
            assert!(
4✔
496
                MultiNamedPlaceholderPattern::try_from_str(&string, Default::default()).is_err(),
4✔
497
                "{string:?}"
498
            );
499
        }
5✔
500
        let stores = [
1✔
501
            "\x00",      // too short
502
            "\x02",      // too short
503
            "\x00\x02",  // no placeholder name
504
            "\x00\x02a", // placeholder name too short
505
        ];
506
        for store in stores {
5✔
507
            assert!(
×
508
                MultiNamedPlaceholder::validate_store(store).is_err(),
4✔
509
                "{store:?}"
510
            );
511
        }
1✔
512
    }
2✔
513
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc