icu_pattern/
double.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 [`DoublePlaceholder`] pattern backend.
6
7use core::convert::Infallible;
8use core::{cmp::Ordering, str::FromStr};
9use either::Either;
10use writeable::adapters::WriteableAsTryWriteableInfallible;
11use writeable::Writeable;
12
13use crate::common::*;
14use crate::Error;
15
16#[cfg(feature = "alloc")]
17use alloc::{boxed::Box, string::String};
18
19/// A two-value enum for the [`DoublePlaceholder`] pattern backend.
20///
21/// # Examples
22///
23/// ```
24/// use core::cmp::Ordering;
25/// use core::str::FromStr;
26/// use icu_pattern::DoublePlaceholderKey;
27/// use icu_pattern::DoublePlaceholderPattern;
28/// use icu_pattern::PatternItem;
29///
30/// // Parse the string syntax
31/// let pattern = DoublePlaceholderPattern::try_from_str(
32///     "Hello, {0} and {1}!",
33///     Default::default(),
34/// )
35/// .unwrap();
36///
37/// assert_eq!(
38///     pattern.iter().cmp(
39///         [
40///             PatternItem::Literal("Hello, "),
41///             PatternItem::Placeholder(DoublePlaceholderKey::Place0),
42///             PatternItem::Literal(" and "),
43///             PatternItem::Placeholder(DoublePlaceholderKey::Place1),
44///             PatternItem::Literal("!")
45///         ]
46///         .into_iter()
47///     ),
48///     Ordering::Equal
49/// );
50/// ```
51#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
52#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
53#[allow(clippy::exhaustive_enums)] // Defined to have two entries
54pub enum DoublePlaceholderKey {
55    /// The placeholder `{0}`.
56    Place0,
57    /// The placeholder `{1}`.
58    Place1,
59}
60
61impl FromStr for DoublePlaceholderKey {
62    type Err = Error;
63    fn from_str(s: &str) -> Result<Self, Self::Err> {
64        match s {
65            "0" => Ok(Self::Place0),
66            "1" => Ok(Self::Place1),
67            _ => Err(Error::InvalidPlaceholder),
68        }
69    }
70}
71
72impl<W0, W1> PlaceholderValueProvider<DoublePlaceholderKey> for (W0, W1)
73where
74    W0: Writeable,
75    W1: Writeable,
76{
77    type Error = Infallible;
78
79    type W<'a>
80        = WriteableAsTryWriteableInfallible<Either<&'a W0, &'a W1>>
81    where
82        Self: 'a;
83
84    type L<'a, 'l>
85        = &'l str
86    where
87        Self: 'a;
88
89    #[inline]
90    fn value_for(&self, key: DoublePlaceholderKey) -> Self::W<'_> {
91        let writeable = match key {
92            DoublePlaceholderKey::Place0 => Either::Left(&self.0),
93            DoublePlaceholderKey::Place1 => Either::Right(&self.1),
94        };
95        WriteableAsTryWriteableInfallible(writeable)
96    }
97    #[inline]
98    fn map_literal<'a, 'l>(&'a self, literal: &'l str) -> Self::L<'a, 'l> {
99        literal
100    }
101}
102
103impl<W> PlaceholderValueProvider<DoublePlaceholderKey> for [W; 2]
104where
105    W: Writeable,
106{
107    type Error = Infallible;
108
109    type W<'a>
110        = WriteableAsTryWriteableInfallible<&'a W>
111    where
112        Self: 'a;
113
114    type L<'a, 'l>
115        = &'l str
116    where
117        Self: 'a;
118
119    #[inline]
120    fn value_for(&self, key: DoublePlaceholderKey) -> Self::W<'_> {
121        let [item0, item1] = self;
122        let writeable = match key {
123            DoublePlaceholderKey::Place0 => item0,
124            DoublePlaceholderKey::Place1 => item1,
125        };
126        WriteableAsTryWriteableInfallible(writeable)
127    }
128    #[inline]
129    fn map_literal<'a, 'l>(&'a self, literal: &'l str) -> Self::L<'a, 'l> {
130        literal
131    }
132}
133
134/// Internal representation of a placeholder
135#[derive(Debug, Copy, Clone)]
136struct DoublePlaceholderInfo {
137    /// The placeholder key: 0 or 1
138    pub key: DoublePlaceholderKey,
139    /// An offset field. This can take one of two forms:
140    /// - Encoded form: 1 + offset from start of literals
141    /// - Decoded form: offset from start of store
142    pub offset: usize,
143}
144
145impl DoublePlaceholderInfo {
146    pub fn from_char(ch: char) -> Self {
147        Self {
148            key: if ((ch as usize) & 0x1) == 0 {
149                DoublePlaceholderKey::Place0
150            } else {
151                DoublePlaceholderKey::Place1
152            },
153            offset: (ch as usize) >> 1,
154        }
155    }
156    #[cfg(feature = "alloc")]
157    pub fn try_to_char(self) -> Result<char, Error> {
158        let usize_val = match self.key {
159            DoublePlaceholderKey::Place0 => 0,
160            DoublePlaceholderKey::Place1 => 1,
161        } | (self.offset << 1);
162        u32::try_from(usize_val)
163            .ok()
164            .and_then(|x| char::try_from(x).ok())
165            .ok_or(Error::InvalidPattern)
166    }
167    /// Creates a PlaceholderInfo for an empty Place0
168    pub fn no_place0() -> Self {
169        Self {
170            key: DoublePlaceholderKey::Place0,
171            offset: 0,
172        }
173    }
174    /// Changes Place0 to Place1 and vice-versa
175    pub fn swap(self) -> Self {
176        Self {
177            key: match self.key {
178                DoublePlaceholderKey::Place0 => DoublePlaceholderKey::Place1,
179                DoublePlaceholderKey::Place1 => DoublePlaceholderKey::Place0,
180            },
181            offset: self.offset,
182        }
183    }
184    /// Sets the offset to 0 (ignored placeholder), retaining the key
185    #[cfg(feature = "alloc")]
186    pub fn clear(self) -> Self {
187        Self {
188            key: self.key,
189            offset: 0,
190        }
191    }
192}
193
194/// Backend for patterns containing zero, one, or two placeholders.
195///
196/// This empty type is not constructible.
197///
198/// # Placeholder Keys
199///
200/// The placeholder is either [`DoublePlaceholderKey::Place0`] or [`DoublePlaceholderKey::Place1`].
201///
202/// In [`Pattern::interpolate()`], pass a two-element array or tuple.
203///
204/// # Encoding Details
205///
206/// The first two code points contain the indices of the first and second placeholders with
207/// the following encoding:
208///
209/// 1. First bit: 0 for [`DoublePlaceholderKey::Place0`], 1 for [`DoublePlaceholderKey::Place1`].
210/// 2. Second and higher bits: 1 plus the byte offset of the placeholder counting from after
211///    the placeholder index code points. If zero, skip this placeholder.
212///
213/// # Examples
214///
215/// Parsing a pattern into the encoding:
216///
217/// ```
218/// use core::str::FromStr;
219/// use icu_pattern::DoublePlaceholder;
220/// use icu_pattern::Pattern;
221///
222/// // Parse the string syntax and check the resulting data store:
223/// let pattern = Pattern::<DoublePlaceholder>::try_from_str(
224///     "Hello, {0} and {1}!",
225///     Default::default(),
226/// )
227/// .unwrap();
228///
229/// assert_eq!("\x10\x1BHello,  and !", &pattern.store);
230/// ```
231///
232/// Explanation of the lead code points:
233///
234/// 1. `\x10` is placeholder 0 at index 7: ((7 + 1) << 1) | 0
235/// 2. `\x1B` is placeholder 1 at index 12: ((12 + 1) << 1) | 1
236///
237/// Example patterns supported by this backend:
238///
239/// ```
240/// use core::str::FromStr;
241/// use icu_pattern::DoublePlaceholder;
242/// use icu_pattern::Pattern;
243/// use icu_pattern::QuoteMode;
244///
245/// // Single numeric placeholder (note, "5" is used):
246/// assert_eq!(
247///     Pattern::<DoublePlaceholder>::try_from_str(
248///         "{0} days ago",
249///         Default::default()
250///     )
251///     .unwrap()
252///     .interpolate_to_string([5, 7]),
253///     "5 days ago",
254/// );
255///
256/// // No placeholder (note, the placeholder value is never accessed):
257/// assert_eq!(
258///     Pattern::<DoublePlaceholder>::try_from_str(
259///         "yesterday",
260///         Default::default()
261///     )
262///     .unwrap()
263///     .interpolate_to_string(["foo", "bar"]),
264///     "yesterday",
265/// );
266///
267/// // Escaped placeholder and a placeholder value 1 (note, "bar" is used):
268/// assert_eq!(
269///     Pattern::<DoublePlaceholder>::try_from_str(
270///         "'{0}' {1}",
271///         QuoteMode::QuotingSupported.into()
272///     )
273///     .unwrap()
274///     .interpolate_to_string(("foo", "bar")),
275///     "{0} bar",
276/// );
277///
278/// // Pattern with the placeholders in the opposite order:
279/// assert_eq!(
280///     Pattern::<DoublePlaceholder>::try_from_str(
281///         "A {1} B {0} C",
282///         Default::default()
283///     )
284///     .unwrap()
285///     .interpolate_to_string(("D", "E")),
286///     "A E B D C",
287/// );
288///
289/// // No literals, only placeholders:
290/// assert_eq!(
291///     Pattern::<DoublePlaceholder>::try_from_str(
292///         "{1}{0}",
293///         Default::default()
294///     )
295///     .unwrap()
296///     .interpolate_to_string((1, "A")),
297///     "A1",
298/// );
299/// ```
300///
301/// [`Pattern::interpolate()`]: crate::Pattern::interpolate
302#[derive(Debug, Copy, Clone, PartialEq, Eq)]
303#[allow(clippy::exhaustive_enums)] // Empty Enum
304pub enum DoublePlaceholder {}
305
306impl crate::private::Sealed for DoublePlaceholder {}
307
308impl PatternBackend for DoublePlaceholder {
309    type PlaceholderKey<'a> = DoublePlaceholderKey;
310    #[cfg(feature = "alloc")]
311    type PlaceholderKeyCow<'a> = DoublePlaceholderKey;
312    type Error<'a> = Infallible;
313    type Store = str;
314    type Iter<'a> = DoublePlaceholderPatternIterator<'a>;
315
316    fn validate_store(store: &Self::Store) -> Result<(), Error> {
317        let mut chars = store.chars();
318        let ph_first_char = chars.next().ok_or(Error::InvalidPattern)?;
319        let ph_second_char = chars.next().ok_or(Error::InvalidPattern)?;
320        let initial_offset = ph_first_char.len_utf8() + ph_second_char.len_utf8();
321        let ph_first = DoublePlaceholderInfo::from_char(ph_first_char);
322        let ph_second = DoublePlaceholderInfo::from_char(ph_second_char);
323        if ph_first.key == ph_second.key {
324            return Err(Error::InvalidPattern);
325        }
326        if ph_first.offset > ph_second.offset && ph_second.offset > 0 {
327            return Err(Error::InvalidPattern);
328        }
329        if ph_second.offset > store.len() - initial_offset + 1 {
330            return Err(Error::InvalidPattern);
331        }
332        if (ph_second_char as usize) >= 0xD800 {
333            return Err(Error::InvalidPattern);
334        }
335        Ok(())
336    }
337
338    fn iter_items(store: &Self::Store) -> Self::Iter<'_> {
339        let mut chars = store.chars();
340        let (mut ph_first, ph_first_len) = match chars.next() {
341            Some(ch) => (DoublePlaceholderInfo::from_char(ch), ch.len_utf8()),
342            None => {
343                debug_assert!(false);
344                (DoublePlaceholderInfo::no_place0(), 0)
345            }
346        };
347        let (mut ph_second, ph_second_len) = match chars.next() {
348            Some(ch) => (DoublePlaceholderInfo::from_char(ch), ch.len_utf8()),
349            None => {
350                debug_assert!(false);
351                (ph_first.swap(), ph_first_len)
352            }
353        };
354        let initial_offset = ph_first_len + ph_second_len;
355        ph_first.offset += initial_offset - 1;
356        ph_second.offset += initial_offset - 1;
357        DoublePlaceholderPatternIterator {
358            store,
359            ph_first,
360            ph_second,
361            current_offset: initial_offset,
362        }
363    }
364
365    #[cfg(feature = "alloc")]
366    fn try_from_items<
367        'cow,
368        'ph,
369        I: Iterator<Item = Result<PatternItemCow<'cow, Self::PlaceholderKey<'ph>>, Error>>,
370    >(
371        items: I,
372    ) -> Result<Box<str>, Error> {
373        let mut result = String::new();
374        let mut first_ph = None;
375        let mut second_ph = None;
376        for item in items {
377            match item? {
378                PatternItemCow::Literal(s) => result.push_str(&s),
379                PatternItemCow::Placeholder(ph_key) => {
380                    if second_ph.is_some() {
381                        return Err(Error::InvalidPattern);
382                    }
383                    let placeholder_offset = result.len() + 1;
384                    if placeholder_offset >= 0xD800 {
385                        return Err(Error::InvalidPattern);
386                    }
387                    let ph_info = DoublePlaceholderInfo {
388                        key: ph_key,
389                        offset: placeholder_offset,
390                    };
391                    if first_ph.is_none() {
392                        first_ph.replace(ph_info);
393                    } else {
394                        second_ph.replace(ph_info);
395                    }
396                }
397            }
398        }
399        let (first_ph, second_ph) = match (first_ph, second_ph) {
400            (Some(a), Some(b)) => (a, b),
401            (Some(a), None) => (a, a.swap().clear()),
402            (None, None) => (
403                DoublePlaceholderInfo::no_place0(),
404                DoublePlaceholderInfo::no_place0().swap(),
405            ),
406            (None, Some(_)) => unreachable!("first_ph always populated before second_ph"),
407        };
408        if first_ph.key == second_ph.key {
409            return Err(Error::InvalidPattern);
410        }
411
412        result.insert(0, second_ph.try_to_char()?);
413        result.insert(0, first_ph.try_to_char()?);
414
415        Ok(result.into_boxed_str())
416    }
417
418    fn empty() -> &'static Self::Store {
419        "\0\u{1}"
420    }
421}
422
423#[doc(hidden)] // TODO(#4467): Should be internal
424#[derive(Debug)]
425pub struct DoublePlaceholderPatternIterator<'a> {
426    store: &'a str,
427    ph_first: DoublePlaceholderInfo,
428    ph_second: DoublePlaceholderInfo,
429    current_offset: usize,
430}
431
432// Note: This impl is not exported via public bounds, but it might be in the
433// future, and the compiler might be able to find it. The code is also
434// reachable from `Iterator::size_hint`.
435impl ExactSizeIterator for DoublePlaceholderPatternIterator<'_> {
436    fn len(&self) -> usize {
437        let mut chars = self.store.chars();
438        let (mut ph_first, ph_first_len) = match chars.next() {
439            Some(ch) => (DoublePlaceholderInfo::from_char(ch), ch.len_utf8()),
440            None => {
441                debug_assert!(false);
442                (DoublePlaceholderInfo::no_place0(), 0)
443            }
444        };
445        let (mut ph_second, ph_second_len) = match chars.next() {
446            Some(ch) => (DoublePlaceholderInfo::from_char(ch), ch.len_utf8()),
447            None => {
448                debug_assert!(false);
449                (ph_first.swap(), ph_first_len)
450            }
451        };
452        let initial_offset = ph_first_len + ph_second_len;
453        ph_first.offset += initial_offset - 1;
454        ph_second.offset += initial_offset - 1;
455        let store_len = self.store.len();
456
457        #[allow(clippy::comparison_chain)]
458        if ph_first.offset < initial_offset {
459            // No placeholders
460            if initial_offset < store_len {
461                // No placeholder, non-empty literal
462                1
463            } else {
464                // No placeholder, empty literal
465                0
466            }
467        } else if ph_first.offset == initial_offset {
468            // At least 1 placeholder, empty prefix
469            if ph_second.offset < initial_offset {
470                // Single placeholder
471                if ph_first.offset < store_len {
472                    // Single placeholder, empty prefix, non-empty suffix
473                    2
474                } else {
475                    // Single placeholder, empty prefix, empty suffix
476                    1
477                }
478            } else if ph_second.offset == ph_first.offset {
479                // Two placeholders, empty infix
480                if ph_first.offset < store_len {
481                    // Two placeholders, empty prefix, empty infix, non-empty suffix
482                    3
483                } else {
484                    // Two placeholders, empty prefix, empty infix, empty suffix
485                    2
486                }
487            } else if ph_second.offset < store_len {
488                // Two placeholders, empty prefix, non-empty infix, non-empty suffix
489                4
490            } else {
491                // Two placeholders, empty prefix, non-empty infix, empty suffix
492                3
493            }
494        } else {
495            // At least 1 placeholder, non-empty prefix
496            if ph_second.offset < initial_offset {
497                // Single placeholder
498                if ph_first.offset < store_len {
499                    // Single placeholder, non-empty prefix, non-empty suffix
500                    3
501                } else {
502                    // Single placeholder, non-empty prefix, empty suffix
503                    2
504                }
505            } else if ph_second.offset == ph_first.offset {
506                // Two placeholders, empty infix
507                if ph_first.offset < store_len {
508                    // Two placeholders, non-empty prefix, empty infix, non-empty suffix
509                    4
510                } else {
511                    // Two placeholders, non-empty prefix, empty infix, empty suffix
512                    3
513                }
514            } else if ph_second.offset < store_len {
515                // Two placeholders, non-empty prefix, non-empty infix, non-empty suffix
516                5
517            } else {
518                // Two placeholders, non-empty prefix, non-empty infix, empty suffix
519                4
520            }
521        }
522    }
523}
524
525impl<'a> Iterator for DoublePlaceholderPatternIterator<'a> {
526    type Item = PatternItem<'a, DoublePlaceholderKey>;
527    fn next(&mut self) -> Option<Self::Item> {
528        match self.current_offset.cmp(&self.ph_first.offset) {
529            Ordering::Less => {
530                // Prefix
531                let literal_str = match self.store.get(self.current_offset..self.ph_first.offset) {
532                    Some(s) => s,
533                    None => {
534                        debug_assert!(false, "offsets are in range");
535                        ""
536                    }
537                };
538                self.current_offset = self.ph_first.offset;
539                Some(PatternItem::Literal(literal_str))
540            }
541            Ordering::Equal => {
542                // Placeholder
543                self.ph_first.offset = 0;
544                Some(PatternItem::Placeholder(self.ph_first.key))
545            }
546            Ordering::Greater => match self.current_offset.cmp(&self.ph_second.offset) {
547                Ordering::Less => {
548                    // Infix
549                    let literal_str =
550                        match self.store.get(self.current_offset..self.ph_second.offset) {
551                            Some(s) => s,
552                            None => {
553                                debug_assert!(false, "offsets are in range");
554                                ""
555                            }
556                        };
557                    self.current_offset = self.ph_second.offset;
558                    Some(PatternItem::Literal(literal_str))
559                }
560                Ordering::Equal => {
561                    // Placeholder
562                    self.ph_second.offset = 0;
563                    Some(PatternItem::Placeholder(self.ph_second.key))
564                }
565                Ordering::Greater => {
566                    // Suffix or end of string
567                    let literal_str = match self.store.get(self.current_offset..) {
568                        Some(s) => s,
569                        None => {
570                            debug_assert!(false, "offsets are in range");
571                            ""
572                        }
573                    };
574                    if literal_str.is_empty() {
575                        // End of string
576                        None
577                    } else {
578                        // Suffix
579                        self.current_offset = self.store.len();
580                        Some(PatternItem::Literal(literal_str))
581                    }
582                }
583            },
584        }
585    }
586
587    fn size_hint(&self) -> (usize, Option<usize>) {
588        let len = self.len();
589        (len, Some(len))
590    }
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596    use crate::DoublePlaceholderPattern;
597
598    #[test]
599    fn test_permutations() {
600        #[derive(Debug)]
601        struct TestCase<'a> {
602            pattern: &'a str,
603            store: &'a str,
604            /// Interpolation with 0=apple, 1=orange
605            interpolated: &'a str,
606        }
607        let cases = [
608            TestCase {
609                pattern: "",
610                store: "\x00\x01",
611                interpolated: "",
612            },
613            TestCase {
614                pattern: "{0}",
615                store: "\x02\x01",
616                interpolated: "apple",
617            },
618            TestCase {
619                pattern: "X{0}",
620                store: "\x04\x01X",
621                interpolated: "Xapple",
622            },
623            TestCase {
624                pattern: "{0}Y",
625                store: "\x02\x01Y",
626                interpolated: "appleY",
627            },
628            TestCase {
629                pattern: "X{0}Y",
630                store: "\x04\x01XY",
631                interpolated: "XappleY",
632            },
633            TestCase {
634                pattern: "{1}",
635                store: "\x03\x00",
636                interpolated: "orange",
637            },
638            TestCase {
639                pattern: "X{1}",
640                store: "\x05\x00X",
641                interpolated: "Xorange",
642            },
643            TestCase {
644                pattern: "{1}Y",
645                store: "\x03\x00Y",
646                interpolated: "orangeY",
647            },
648            TestCase {
649                pattern: "X{1}Y",
650                store: "\x05\x00XY",
651                interpolated: "XorangeY",
652            },
653            TestCase {
654                pattern: "{0}{1}",
655                store: "\x02\x03",
656                interpolated: "appleorange",
657            },
658            TestCase {
659                pattern: "X{0}{1}",
660                store: "\x04\x05X",
661                interpolated: "Xappleorange",
662            },
663            TestCase {
664                pattern: "{0}Y{1}",
665                store: "\x02\x05Y",
666                interpolated: "appleYorange",
667            },
668            TestCase {
669                pattern: "{0}{1}Z",
670                store: "\x02\x03Z",
671                interpolated: "appleorangeZ",
672            },
673            TestCase {
674                pattern: "X{0}Y{1}",
675                store: "\x04\x07XY",
676                interpolated: "XappleYorange",
677            },
678            TestCase {
679                pattern: "X{0}{1}Z",
680                store: "\x04\x05XZ",
681                interpolated: "XappleorangeZ",
682            },
683            TestCase {
684                pattern: "{0}Y{1}Z",
685                store: "\x02\x05YZ",
686                interpolated: "appleYorangeZ",
687            },
688            TestCase {
689                pattern: "X{0}Y{1}Z",
690                store: "\x04\x07XYZ",
691                interpolated: "XappleYorangeZ",
692            },
693            TestCase {
694                pattern: "{1}{0}",
695                store: "\x03\x02",
696                interpolated: "orangeapple",
697            },
698            TestCase {
699                pattern: "X{1}{0}",
700                store: "\x05\x04X",
701                interpolated: "Xorangeapple",
702            },
703            TestCase {
704                pattern: "{1}Y{0}",
705                store: "\x03\x04Y",
706                interpolated: "orangeYapple",
707            },
708            TestCase {
709                pattern: "{1}{0}Z",
710                store: "\x03\x02Z",
711                interpolated: "orangeappleZ",
712            },
713            TestCase {
714                pattern: "X{1}Y{0}",
715                store: "\x05\x06XY",
716                interpolated: "XorangeYapple",
717            },
718            TestCase {
719                pattern: "X{1}{0}Z",
720                store: "\x05\x04XZ",
721                interpolated: "XorangeappleZ",
722            },
723            TestCase {
724                pattern: "{1}Y{0}Z",
725                store: "\x03\x04YZ",
726                interpolated: "orangeYappleZ",
727            },
728            TestCase {
729                pattern: "X{1}Y{0}Z",
730                store: "\x05\x06XYZ",
731                interpolated: "XorangeYappleZ",
732            },
733            TestCase {
734                pattern: "01234567890123456789012345678901234567890123456789012345678901234567890123456789{0}Y{1}Z",
735                store: "\u{A2}\u{A5}01234567890123456789012345678901234567890123456789012345678901234567890123456789YZ",
736                interpolated: "01234567890123456789012345678901234567890123456789012345678901234567890123456789appleYorangeZ",
737            },
738        ];
739        for cas in cases {
740            let TestCase {
741                pattern,
742                store,
743                interpolated,
744            } = cas;
745            let parsed =
746                DoublePlaceholderPattern::try_from_str(pattern, Default::default()).unwrap();
747            let actual = parsed
748                .interpolate(["apple", "orange"])
749                .write_to_string()
750                .into_owned();
751            assert_eq!(&parsed.store, store, "{cas:?}");
752            assert_eq!(actual, interpolated, "{cas:?}");
753        }
754    }
755
756    #[test]
757    fn test_invalid() {
758        let cases = [
759            "",               // too short
760            "\x00",           // too short
761            "\x00\x00",       // duplicate placeholders
762            "\x04\x03",       // first offset is after second offset
763            "\x04\x05",       // second offset out of range (also first offset)
764            "\x04\u{10001}@", // second offset too large for UTF-8
765        ];
766        let long_str = "0123456789".repeat(1000000);
767        for cas in cases {
768            let cas = cas.replace('@', &long_str);
769            assert!(DoublePlaceholder::validate_store(&cas).is_err(), "{cas:?}");
770        }
771    }
772}