coap_lite/
link_format.rs

1// Copyright 2019 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15
16// Modifications copyright (c) 2021 Jiayi Hu, Martin Disch
17
18//! Mechanisms and constants for encoding and decoding [IETF-RFC6690 CoAP
19//! link-formats].
20//!
21//! [IETF-RFC6690 CoAP link-formats]: https://tools.ietf.org/html/rfc6690
22
23use alloc::{borrow::Cow, string::ToString};
24use core::{
25    fmt::{Display, Write},
26    iter::FusedIterator,
27};
28
29/// Relation Type.
30///
31/// From [IETF-RFC8288], [Section 3.3]:
32///
33/// > The relation type of a link conveyed in the Link header field is
34/// > conveyed in the "rel" parameter's value.  The rel parameter MUST be
35/// > present but MUST NOT appear more than once in a given link-value;
36/// > occurrences after the first MUST be ignored by parsers.
37/// >
38/// > The rel parameter can, however, contain multiple link relation types.
39/// > When this occurs, it establishes multiple links that share the same
40/// > context, target, and target attributes.
41/// >
42/// > The ABNF for the rel parameter values is:
43/// >
44/// > ```abnf
45/// >     relation-type *( 1*SP relation-type )
46/// > ```
47/// >
48/// > where:
49/// >
50/// > ```abnf
51/// >     relation-type  = reg-rel-type / ext-rel-type
52/// >     reg-rel-type   = LOALPHA *( LOALPHA / DIGIT / "." / "-" )
53/// >     ext-rel-type   = URI ; Section 3 of [RFC3986]
54/// > ```
55/// >
56/// > Note that extension relation types are REQUIRED to be absolute URIs
57/// > in Link header fields and MUST be quoted when they contain characters
58/// > not allowed in tokens, such as a semicolon (";") or comma (",") (as
59/// > these characters are used as delimiters in the header field itself).
60///
61/// Optional in [IETF-RFC6690] link format resources.
62///
63/// [IETF-RFC8288]: https://tools.ietf.org/html/rfc8288
64/// [Section 3.3]: https://tools.ietf.org/html/rfc8288#section-3.3
65/// [IETF-RFC6690]: https://tools.ietf.org/html/rfc6690
66pub const LINK_ATTR_REL: &str = "rel";
67
68/// Anchor attribute.
69///
70/// Provides an override of the document context URI when parsing relative URIs
71/// in the links. The value itself may be a relative URI, which is evaluated
72/// against the document context URI.
73///
74/// * <a href="https://tools.ietf.org/html/rfc8288#section-3.2">RFC8288, Section 3.2</a>
75pub const LINK_ATTR_ANCHOR: &str = "anchor";
76
77/// A hint indicating what the language of the result of dereferencing the link
78/// should be.
79///
80/// * <a href="https://tools.ietf.org/html/rfc8288#section-3.4.1">RFC8288, Section 3.4.1</a>
81pub const LINK_ATTR_HREFLANG: &str = "hreflang";
82
83/// Media Attribute. Used to indicate intended destination medium or media for
84/// style information.
85///
86/// * <a href="https://tools.ietf.org/html/rfc8288#section-3.4.1">RFC8288, Section 3.4.1</a>
87pub const LINK_ATTR_MEDIA: &str = "media";
88
89/// Human-readable label describing the resource.
90///
91/// * <a href="https://tools.ietf.org/html/rfc8288#section-3.4.1">RFC8288, Section 3.4.1</a>
92pub const LINK_ATTR_TITLE: &str = "title";
93
94/// Human-readable label describing the resource, along with language
95/// information.
96///
97/// Is is typically formatted as `"utf-8'<LANG_CODE&>'<TITLE_TEXT>"`. For
98/// example:
99///
100/// * `"utf-8'en'£ rates"`
101///
102/// Note that since <a href="https://tools.ietf.org/html/rfc6690">RFC6690</a>
103/// requires the link format serialization to always be in UTF-8 format, the
104/// value of this attribute MUST ALWAYS start with either the string
105/// <code>utf-8</code> or <code>UTF-8</code> and MUST NOT be percent-encoded.
106///
107/// * <a href="https://tools.ietf.org/html/rfc8288#section-3.4.1">RFC8288, Section 3.4.1</a>
108/// * <a href="https://tools.ietf.org/html/rfc8187">RFC8187</a>
109pub const LINK_ATTR_TITLE_STAR: &str = "title*";
110
111/// MIME content type attribute.
112///
113/// This attribute should be avoided in favor of [`LINK_ATTR_CONTENT_FORMAT`].
114///
115/// * <a href="https://tools.ietf.org/html/rfc8288#section-3.4.1">RFC8288, Section 3.4.1</a>
116#[doc(hidden)]
117pub const LINK_ATTR_TYPE: &str = "type";
118
119/// Resource Type Attribute.
120///
121/// The Resource Type `rt` attribute is an opaque string used to assign an
122/// application-specific semantic type to a resource. One can think of this as
123/// a noun describing the resource.
124///
125/// * <a href="https://tools.ietf.org/html/rfc6690#section-3.1">RFC6690, Section 3.1</a>
126pub const LINK_ATTR_RESOURCE_TYPE: &str = "rt";
127
128/// Interface Description Attribute.
129///
130/// The Interface Description `if` attribute is an opaque string used to
131/// provide a name or URI indicating a specific interface definition used to
132/// interact with the target resource. One can think of this as describing
133/// verbs usable on a resource.
134///
135/// * <a href="https://tools.ietf.org/html/rfc6690#section-3.2">RFC6690, Section 3.2</a>
136///
137pub const LINK_ATTR_INTERFACE_DESCRIPTION: &str = "if";
138
139/// The estimated maximum size of the fetched resource.
140///
141/// The maximum size estimate attribute `sz` gives an indication of the maximum
142/// size of the resource representation returned by performing a GET on the
143/// target URI. For links to CoAP resources, this attribute is not expected to
144/// be included for small resources that can comfortably be carried in a single
145/// Maximum Transmission Unit (MTU) but SHOULD be included for resources larger
146/// than that. The maximum size estimate attribute MUST NOT appear more than
147/// once in a link.
148///
149/// * <a href="https://tools.ietf.org/html/rfc6690#section-3.3">RFC6690, Section 3.3</a>
150pub const LINK_ATTR_MAXIMUM_SIZE_ESTIMATE: &str = "sz";
151
152/// The value of this resource expressed as a human-readable string. Must
153/// beless than 63 bytes.
154pub const LINK_ATTR_VALUE: &str = "v";
155
156/// Content-Format Code(s).
157///
158/// Space-separated list of content type integers appropriate for being
159/// specified in an Accept option.
160///
161/// * <a href="https://tools.ietf.org/html/rfc7252#section-7.2.1">RFC7252, Section 7.2.1</a>
162pub const LINK_ATTR_CONTENT_FORMAT: &str = "ct";
163
164/// Identifies this resource as observable if present.
165///
166/// * <a href="https://tools.ietf.org/html/rfc7641#section-6">RFC7641, Section 6</a>
167pub const LINK_ATTR_OBSERVABLE: &str = "obs";
168
169/// Name of the endpoint, max 63 bytes.
170///
171/// * <a href="https://goo.gl/6e2s7C#section-5.3">draft-ietf-core-resource-directory-14</a>
172pub const LINK_ATTR_ENDPOINT_NAME: &str = "ep";
173
174/// Lifetime of the registration in seconds. Valid values are between
175/// 60-4294967295, inclusive.
176///
177/// * <a href="https://goo.gl/6e2s7C#section-5.3">draft-ietf-core-resource-directory-14</a>
178pub const LINK_ATTR_REGISTRATION_LIFETIME: &str = "lt";
179
180/// Sector to which this endpoint belongs. Must be less than 63 bytes.
181///
182/// * <a href="https://goo.gl/6e2s7C#section-5.3">draft-ietf-core-resource-directory-14</a>
183pub const LINK_ATTR_SECTOR: &str = "d";
184
185/// The scheme, address and point and path at which this server is available.
186///
187/// MUST be a valid URI.
188///
189/// * <a href="https://goo.gl/6e2s7C#section-5.3">draft-ietf-core-resource-directory-14</a>
190pub const LINK_ATTR_REGISTRATION_BASE_URI: &str = "base";
191
192/// Name of a group in this RD. Must be less than 63 bytes.
193///
194/// * <a href="https://goo.gl/6e2s7C#section-6.1">draft-ietf-core-resource-directory-14</a>
195pub const LINK_ATTR_GROUP_NAME: &str = "gp";
196
197/// Semantic name of the endpoint. Must be less than 63 bytes.
198///
199/// * <a href="https://goo.gl/6e2s7C#section-10.3.1">draft-ietf-core-resource-directory-14</a>
200pub const LINK_ATTR_ENDPOINT_TYPE: &str = "et";
201
202/// Error type for parsing a link format.
203#[derive(Copy, Clone, Debug, Eq, PartialEq)]
204pub enum ErrorLinkFormat {
205    /// An error was encountered while parsing the link format.
206    ParseError,
207}
208
209const QUOTE_ESCAPE_CHAR: char = '\\';
210const ATTR_SEPARATOR_CHAR: char = ';';
211const LINK_SEPARATOR_CHAR: char = ',';
212
213/// Parsing iterator which parses a string formatted as an [IETF-RFC6690 CoAP
214/// link-format].
215///
216/// As successful parsing is performed, this iterator emits a tuple inside of a
217/// `Result::Ok`. The tuple contains a string slice for the link and a
218/// [`LinkAttributeParser`] to provide access to the link attributes for that
219/// link.
220///
221/// Parsing errors are emitted as a `Result::Err` and are of the error type
222/// [`ErrorLinkFormat`].
223///
224/// [IETF-RFC6690 CoAP link-format]: https://tools.ietf.org/html/rfc6690
225#[derive(Copy, Clone, Debug, Eq, PartialEq)]
226pub struct LinkFormatParser<'a> {
227    pub(super) inner: &'a str,
228}
229
230impl<'a> LinkFormatParser<'a> {
231    /// Creates a new instance of `LinkFormatParser` for the given string
232    /// slice.
233    pub fn new(inner: &'a str) -> LinkFormatParser<'a> {
234        LinkFormatParser { inner }
235    }
236}
237
238impl<'a> Iterator for LinkFormatParser<'a> {
239    /// (uri-ref, link-attribute-iterator)
240    type Item = Result<(&'a str, LinkAttributeParser<'a>), ErrorLinkFormat>;
241
242    #[inline]
243    fn next(&mut self) -> Option<Self::Item> {
244        if self.inner.is_empty() {
245            return None;
246        }
247
248        let mut iter = self.inner.chars();
249
250        // Proceed through whitespace until we get a '<'.
251        loop {
252            match iter.next() {
253                Some(c) if c.is_ascii_whitespace() => continue,
254                Some('<') => break,
255                Some(_) => {
256                    self.inner = "";
257                    return Some(Err(ErrorLinkFormat::ParseError));
258                }
259                None => {
260                    self.inner = "";
261                    return None;
262                }
263            }
264        }
265
266        let link_ref = iter.as_str();
267
268        // Proceed through characters until we get a '>'.
269        for c in iter.by_ref() {
270            if c == '>' {
271                break;
272            }
273        }
274
275        let link_len =
276            iter.as_str().as_ptr() as usize - link_ref.as_ptr() as usize;
277
278        let link_ref = (link_ref[..link_len]).trim_end_matches('>');
279
280        let mut attr_keys = iter.as_str();
281
282        // Skip to the end of the attributes. We leave the
283        // actual attribute parsing to `LinkAttributeParser`.
284        loop {
285            match iter.next() {
286                Some(LINK_SEPARATOR_CHAR) | None => {
287                    break;
288                }
289                Some('"') => {
290                    // Handle quotes.
291                    loop {
292                        match iter.next() {
293                            Some('"') | None => break,
294                            Some(QUOTE_ESCAPE_CHAR) => {
295                                // Slashes always escape the next character,
296                                // since we are scanning and not parsing we
297                                // just skip it.
298                                iter.next();
299                            }
300                            _ => (),
301                        }
302                    }
303                }
304                _ => (),
305            }
306        }
307
308        let attr_len =
309            iter.as_str().as_ptr() as usize - attr_keys.as_ptr() as usize;
310        attr_keys =
311            attr_keys[..attr_len].trim_end_matches(LINK_SEPARATOR_CHAR);
312
313        self.inner = iter.as_str();
314        Some(Ok((
315            link_ref,
316            LinkAttributeParser {
317                inner: attr_keys.trim_matches(ATTR_SEPARATOR_CHAR),
318            },
319        )))
320    }
321}
322
323/// Parsing iterator which parses link attributes for [IETF-RFC6690 CoAP
324/// link-format] processing.
325///
326/// This iterator is emitted by [`LinkFormatParser`] while parsing a CoAP
327/// link-format. It emits a tuple for each attribute, with the first item being
328/// a string slice for the attribute key and the second item being an
329/// [`Unquote`] iterator for obtaining the value. A `String` or `Cow<str>`
330/// version of the value can be easily obtained by calling `to_string()` or
331/// `to_cow()` on the [`Unquote`] instance.
332///
333/// This iterator is permissive and makes a best-effort to parse the link
334/// attributes and does not emit errors while parsing.
335///
336/// [IETF-RFC6690 CoAP link-format]: https://tools.ietf.org/html/rfc6690
337#[derive(Copy, Clone, Debug, Eq, PartialEq)]
338pub struct LinkAttributeParser<'a> {
339    pub(super) inner: &'a str,
340}
341
342impl<'a> Iterator for LinkAttributeParser<'a> {
343    /// (key_ref: &str, value-ref: Unquote)
344    type Item = (&'a str, Unquote<'a>);
345
346    #[inline]
347    fn next(&mut self) -> Option<Self::Item> {
348        if self.inner.is_empty() {
349            return None;
350        }
351
352        let mut iter = self.inner.chars();
353
354        // Skip to the end of the attribute.
355        loop {
356            match iter.next() {
357                Some(ATTR_SEPARATOR_CHAR) | None => {
358                    break;
359                }
360                Some('"') => {
361                    // Handle quotes.
362                    loop {
363                        match iter.next() {
364                            Some('"') | None => {
365                                break;
366                            }
367                            Some(QUOTE_ESCAPE_CHAR) => {
368                                iter.next();
369                            }
370                            _ => (),
371                        }
372                    }
373                }
374                _ => (),
375            }
376        }
377
378        let attr_len =
379            iter.as_str().as_ptr() as usize - self.inner.as_ptr() as usize;
380        let attr_str = &self.inner[..attr_len];
381
382        self.inner = iter.as_str();
383
384        let attr_str = attr_str.trim_end_matches(ATTR_SEPARATOR_CHAR);
385
386        let (key, value) = if let Some(i) = attr_str.find('=') {
387            let (key, value) = attr_str.split_at(i);
388
389            (key, &value[1..])
390        } else {
391            (attr_str, "")
392        };
393
394        Some((key.trim(), Unquote::new(value.trim())))
395    }
396}
397
398/// Character iterator which decodes a [IETF-RFC2616] [`quoted-string`].
399/// Used by [`LinkAttributeParser`].
400///
401/// From [IETF-RFC2616] Section 2.2:
402///
403/// > A string of text is parsed as a single word if it is quoted using
404/// > double-quote marks.
405/// >
406/// > ```abnf
407/// >     quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
408/// >     qdtext         = <any TEXT except <">>
409/// > ```
410/// >
411/// > The backslash character ('\\') MAY be used as a single-character
412/// > quoting mechanism only within quoted-string and comment constructs.
413/// >
414/// > ```abnf
415/// >     quoted-pair    = "\" CHAR
416/// > ```
417///
418/// [IETF-RFC2616]: https://tools.ietf.org/html/rfc2616
419/// [`quoted-string`]: https://tools.ietf.org/html/rfc2616#section-2.2
420#[derive(Clone, Debug)]
421pub struct Unquote<'a> {
422    inner: core::str::Chars<'a>,
423    state: UnquoteState,
424}
425
426#[derive(Copy, Clone, Debug, Eq, PartialEq)]
427enum UnquoteState {
428    NotStarted,
429    NotQuoted,
430    Quoted,
431}
432
433impl Eq for Unquote<'_> {}
434
435impl PartialEq for Unquote<'_> {
436    fn eq(&self, other: &Self) -> bool {
437        let self_s = self.inner.as_str();
438        let other_s = other.inner.as_str();
439        self.state == other.state
440            && self_s.as_ptr() == other_s.as_ptr()
441            && self_s.len() == other_s.len()
442    }
443}
444
445impl<'a> From<Unquote<'a>> for Cow<'a, str> {
446    fn from(iter: Unquote<'a>) -> Self {
447        iter.to_cow()
448    }
449}
450
451impl FusedIterator for Unquote<'_> {}
452
453impl Display for Unquote<'_> {
454    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
455        self.clone().try_for_each(|c| f.write_char(c))
456    }
457}
458
459impl<'a> Unquote<'a> {
460    /// Creates a new instance of the `Unquote` iterator from `quoted_str`.
461    pub fn new(quoted_str: &'a str) -> Unquote<'a> {
462        Unquote {
463            inner: quoted_str.chars(),
464            state: UnquoteState::NotStarted,
465        }
466    }
467
468    /// Converts a fresh, unused instance of `Unquote` into the underlying raw
469    /// string slice.
470    ///
471    /// Calling this method will panic if `next()` has been called.
472    pub fn into_raw_str(self) -> &'a str {
473        assert_eq!(self.state, UnquoteState::NotStarted);
474        self.inner.as_str()
475    }
476
477    /// Returns the unquoted version of this string as a copy-on-write string.
478    pub fn to_cow(&self) -> Cow<'a, str> {
479        let str_ref = self.inner.as_str();
480        if self.is_quoted() {
481            if str_ref.find('\\').is_some() {
482                Cow::from(self.to_string())
483            } else {
484                // String is quoted but has no escapes.
485                Cow::from(&str_ref[1..str_ref.len() - 1])
486            }
487        } else {
488            Cow::from(str_ref)
489        }
490    }
491
492    /// Returns true if the underlying string is quoted, false otherwise.
493    pub fn is_quoted(&self) -> bool {
494        match self.state {
495            UnquoteState::NotStarted => self.inner.as_str().starts_with('"'),
496            UnquoteState::NotQuoted => false,
497            UnquoteState::Quoted => true,
498        }
499    }
500}
501
502impl Iterator for Unquote<'_> {
503    type Item = char;
504
505    #[inline]
506    fn next(&mut self) -> Option<Self::Item> {
507        loop {
508            return match self.state {
509                UnquoteState::NotStarted => match self.inner.next() {
510                    Some('"') => {
511                        self.state = UnquoteState::Quoted;
512                        // Go back to the start of the loop so we can hit
513                        // our "UnquoteState::Quoted" section below.
514                        continue;
515                    }
516                    c => {
517                        self.state = UnquoteState::NotQuoted;
518                        c
519                    }
520                },
521                UnquoteState::NotQuoted => self.inner.next(),
522                UnquoteState::Quoted => match self.inner.next() {
523                    Some('"') => {
524                        // We are finished. Make ourselves empty so we can call
525                        // ourselves "Fused"
526                        self.inner = "".chars();
527                        None
528                    }
529                    Some(QUOTE_ESCAPE_CHAR) => self.inner.next(),
530                    c => c,
531                },
532            };
533        }
534    }
535}
536
537/// Helper for writing [IETF-RFC6690 CoAP link-formats] to anything
538/// implementing [`core::fmt::Write`].
539///
540/// ## Example
541///
542/// ```
543/// use coap_lite::LinkFormatWrite;
544/// use coap_lite::LINK_ATTR_INTERFACE_DESCRIPTION;
545///
546/// // String implements core::fmt::Write
547/// let mut buffer = String::new();
548///
549/// let mut write = LinkFormatWrite::new(&mut buffer);
550///
551/// write.link("/sensor/light")
552///     .attr_quoted(LINK_ATTR_INTERFACE_DESCRIPTION,"sensor")
553///     .finish()
554///     .expect("Error writing link");
555///
556/// assert_eq!(&buffer, r#"</sensor/light>;if="sensor""#);
557/// ```
558///
559/// [IETF-RFC6690 CoAP link-formats]: https://tools.ietf.org/html/rfc6690
560#[derive(Debug)]
561pub struct LinkFormatWrite<'a, T: ?Sized> {
562    write: &'a mut T,
563    is_first: bool,
564    add_newlines: bool,
565    error: Option<core::fmt::Error>,
566}
567
568impl<'a, T: Write + ?Sized> LinkFormatWrite<'a, T> {
569    /// Creates a new instance of `LinkFormatWriter` for a given instance that
570    /// implements [`core::fmt::Write`].
571    pub fn new(write: &'a mut T) -> LinkFormatWrite<'a, T> {
572        LinkFormatWrite {
573            write,
574            is_first: true,
575            add_newlines: false,
576            error: None,
577        }
578    }
579
580    /// Sets whether newlines should be added or not between links, possibly
581    /// improving human readability an the expense of a few extra bytes.
582    pub fn set_add_newlines(&mut self, add_newlines: bool) {
583        self.add_newlines = add_newlines;
584    }
585
586    /// Adds a link to the link format and returns [`LinkAttributeWrite`].
587    ///
588    /// The returned [`LinkAttributeWrite`] instance can then be used to
589    /// associate attributes to the link.
590    pub fn link<'b>(
591        &'b mut self,
592        link: &str,
593    ) -> LinkAttributeWrite<'a, 'b, T> {
594        if self.is_first {
595            self.is_first = false;
596        } else if self.error.is_none() {
597            self.error = self.write.write_char(LINK_SEPARATOR_CHAR).err();
598            if self.add_newlines {
599                self.error = self.write.write_str("\n\r").err();
600            }
601        }
602
603        if self.error.is_none() {
604            self.error = self.write.write_char('<').err();
605        }
606
607        if self.error.is_none() {
608            self.error = write!(self.write, "{}", link).err();
609        }
610
611        if self.error.is_none() {
612            self.error = self.write.write_char('>').err();
613        }
614
615        LinkAttributeWrite(self)
616    }
617
618    /// Consumes this [`LinkFormatWrite`] instance, returning any error that
619    /// might have occurred during writing.
620    pub fn finish(self) -> Result<(), core::fmt::Error> {
621        if let Some(e) = self.error {
622            Err(e)
623        } else {
624            Ok(())
625        }
626    }
627}
628
629/// Helper for writing link format attributes; created by calling
630/// [`LinkFormatWrite::link`].
631#[derive(Debug)]
632pub struct LinkAttributeWrite<'a, 'b, T: ?Sized>(
633    &'b mut LinkFormatWrite<'a, T>,
634);
635
636impl<T: Write + ?Sized> LinkAttributeWrite<'_, '_, T> {
637    /// Prints just the key and an equals sign, prefixed with ';'
638    fn internal_attr_key_eq(&mut self, key: &str) {
639        debug_assert!(key
640            .find(|c: char| c.is_ascii_whitespace() || c == '=')
641            .is_none());
642
643        if self.0.error.is_none() {
644            self.0.error = self.0.write.write_char(ATTR_SEPARATOR_CHAR).err();
645        }
646
647        if self.0.error.is_none() {
648            self.0.error = self.0.write.write_str(key).err();
649        }
650
651        if self.0.error.is_none() {
652            self.0.error = self.0.write.write_char('=').err();
653        }
654    }
655
656    /// Adds an attribute to the link, only quoting the value if it contains
657    /// non-ascii-alphanumeric characters.
658    pub fn attr(mut self, key: &str, value: &str) -> Self {
659        if value.find(|c: char| !c.is_ascii_alphanumeric()).is_some() {
660            return self.attr_quoted(key, value);
661        }
662
663        self.internal_attr_key_eq(key);
664
665        if self.0.error.is_none() {
666            self.0.error = self.0.write.write_str(value).err();
667        }
668
669        self
670    }
671
672    /// Adds an attribute to the link that has u32 value.
673    pub fn attr_u32(mut self, key: &str, value: u32) -> Self {
674        self.internal_attr_key_eq(key);
675
676        if self.0.error.is_none() {
677            self.0.error = write!(self.0.write, "{}", value).err();
678        }
679
680        self
681    }
682
683    /// Adds an attribute to the link that has u16 value.
684    pub fn attr_u16(self, key: &str, value: u16) -> Self {
685        self.attr_u32(key, value as u32)
686    }
687
688    /// Adds an attribute to the link, unconditionally quoting the value.
689    pub fn attr_quoted(mut self, key: &str, value: &str) -> Self {
690        self.internal_attr_key_eq(key);
691
692        if self.0.error.is_none() {
693            self.0.error = self.0.write.write_char('"').err();
694        }
695
696        for c in value.chars() {
697            if (c == '"' || c == '\\') && self.0.error.is_none() {
698                self.0.error =
699                    self.0.write.write_char(QUOTE_ESCAPE_CHAR).err();
700            }
701
702            if self.0.error.is_none() {
703                self.0.error = self.0.write.write_char(c).err();
704            }
705        }
706
707        if self.0.error.is_none() {
708            self.0.error = self.0.write.write_char('"').err();
709        }
710
711        self
712    }
713
714    /// Consumes this [`LinkAttributeWrite`] instance, returning any error that
715    /// might have occurred during writing.
716    pub fn finish(self) -> Result<(), core::fmt::Error> {
717        if let Some(e) = self.0.error {
718            Err(e)
719        } else {
720            Ok(())
721        }
722    }
723}
724
725#[cfg(test)]
726mod test {
727    use super::*;
728    use alloc::string::{String, ToString};
729
730    #[test]
731    fn link_format_write_1() {
732        let mut buffer = String::new();
733
734        let mut write = LinkFormatWrite::new(&mut buffer);
735
736        write
737            .link("/sensor/light")
738            .attr_quoted(LINK_ATTR_INTERFACE_DESCRIPTION, "sensor")
739            .finish()
740            .expect("Write link failed");
741
742        assert_eq!(write.finish(), Ok(()));
743
744        assert_eq!(&buffer, r#"</sensor/light>;if="sensor""#);
745    }
746
747    #[test]
748    fn link_format_write_2() {
749        let mut buffer = String::new();
750
751        let mut write = LinkFormatWrite::new(&mut buffer);
752
753        write
754            .link("/sensor/light")
755            .attr_quoted(LINK_ATTR_INTERFACE_DESCRIPTION, "sensor")
756            .attr(LINK_ATTR_TITLE, "My Light")
757            .finish()
758            .expect("Write link failed");
759
760        write
761            .link("/sensor/temp")
762            .attr_quoted(LINK_ATTR_INTERFACE_DESCRIPTION, "sensor")
763            .attr(LINK_ATTR_TITLE, "My Thermostat")
764            .attr_u32(LINK_ATTR_VALUE, 20)
765            .finish()
766            .expect("Write link failed");
767
768        assert_eq!(write.finish(), Ok(()));
769
770        assert_eq!(
771            &buffer,
772            r#"</sensor/light>;if="sensor";title="My Light",</sensor/temp>;if="sensor";title="My Thermostat";v=20"#
773        );
774    }
775
776    #[test]
777    fn unquote_1() {
778        let unquote = Unquote::new(r#""sensor""#);
779
780        assert_eq!(&unquote.to_string(), "sensor");
781    }
782
783    #[test]
784    fn unquote_2() {
785        let unquote = Unquote::new("sensor");
786
787        assert_eq!(&unquote.to_string(), "sensor");
788    }
789
790    #[test]
791    fn unquote_3() {
792        let unquote = Unquote::new(r#""the \"foo\" bar""#);
793
794        assert_eq!(&unquote.to_string(), r#"the "foo" bar"#);
795    }
796
797    #[test]
798    fn unquote_4() {
799        let unquote = Unquote::new(r#""\"the foo bar\"""#);
800
801        assert_eq!(&unquote.to_string(), r#""the foo bar""#);
802    }
803
804    #[test]
805    fn unquote_5() {
806        let unquote = Unquote::new(r#""the \\\"foo\\\" bar""#);
807
808        assert_eq!(&unquote.to_string(), r#"the \"foo\" bar"#);
809    }
810
811    #[test]
812    fn link_format_parser_1() {
813        let link_format = r#"</sensors>;ct=40"#;
814
815        let mut parser = LinkFormatParser::new(link_format);
816
817        match parser.next() {
818            Some(Ok((link, mut attr_iter))) => {
819                assert_eq!(link, "/sensors");
820                assert_eq!(
821                    attr_iter
822                        .next()
823                        .map(|attr| (attr.0, attr.1.into_raw_str())),
824                    Some(("ct", r#"40"#))
825                );
826                assert_eq!(attr_iter.next(), None);
827            }
828            x => {
829                panic!("{:?}", x);
830            }
831        }
832
833        assert_eq!(parser.next(), None);
834    }
835
836    #[test]
837    fn link_format_parser_2() {
838        let link_format = r#"
839            </sensors/temp>;if="sensor",
840            </sensors/light>;if="sensor""#;
841
842        let mut parser = LinkFormatParser::new(link_format);
843
844        match parser.next() {
845            Some(Ok((link, mut attr_iter))) => {
846                assert_eq!(link, "/sensors/temp");
847                assert_eq!(
848                    attr_iter
849                        .next()
850                        .map(|attr| (attr.0, attr.1.into_raw_str())),
851                    Some(("if", r#""sensor""#))
852                );
853                assert_eq!(attr_iter.next(), None);
854            }
855            x => {
856                panic!("{:?}", x);
857            }
858        }
859
860        match parser.next() {
861            Some(Ok((link, mut attr_iter))) => {
862                assert_eq!(link, "/sensors/light");
863                assert_eq!(
864                    attr_iter
865                        .next()
866                        .map(|attr| (attr.0, attr.1.into_raw_str())),
867                    Some(("if", r#""sensor""#))
868                );
869                assert_eq!(attr_iter.next(), None);
870            }
871            x => {
872                panic!("{:?}", x);
873            }
874        }
875
876        assert_eq!(parser.next(), None);
877    }
878
879    #[test]
880    fn link_format_parser_3() {
881        let link_format = r#"</sensors>;ct=40;title="Sensor Index",
882   </sensors/temp>;rt="temperature-c";if="sensor",
883   </sensors/light>;rt="light-lux";if="sensor",
884   <http://www.example.com/sensors/t123>;anchor="/sensors/temp"
885   ;rel="describedby",
886   </t>;anchor="/sensors/temp";rel="alternate""#;
887
888        let mut parser = LinkFormatParser::new(link_format);
889
890        match parser.next() {
891            Some(Ok((link, mut attr_iter))) => {
892                assert_eq!(link, "/sensors");
893                assert_eq!(
894                    attr_iter
895                        .next()
896                        .map(|attr| (attr.0, attr.1.into_raw_str())),
897                    Some(("ct", r#"40"#))
898                );
899                assert_eq!(
900                    attr_iter
901                        .next()
902                        .map(|attr| (attr.0, attr.1.into_raw_str())),
903                    Some(("title", r#""Sensor Index""#))
904                );
905                assert_eq!(attr_iter.next(), None);
906            }
907            x => {
908                panic!("{:?}", x);
909            }
910        }
911
912        match parser.next() {
913            Some(Ok((link, mut attr_iter))) => {
914                assert_eq!(link, "/sensors/temp");
915                assert_eq!(
916                    attr_iter
917                        .next()
918                        .map(|attr| (attr.0, attr.1.into_raw_str())),
919                    Some(("rt", r#""temperature-c""#))
920                );
921                assert_eq!(
922                    attr_iter
923                        .next()
924                        .map(|attr| (attr.0, attr.1.into_raw_str())),
925                    Some(("if", r#""sensor""#))
926                );
927                assert_eq!(attr_iter.next(), None);
928            }
929            x => {
930                panic!("{:?}", x);
931            }
932        }
933
934        match parser.next() {
935            Some(Ok((link, mut attr_iter))) => {
936                assert_eq!(link, "/sensors/light");
937                assert_eq!(
938                    attr_iter
939                        .next()
940                        .map(|attr| (attr.0, attr.1.into_raw_str())),
941                    Some(("rt", r#""light-lux""#))
942                );
943                assert_eq!(
944                    attr_iter
945                        .next()
946                        .map(|attr| (attr.0, attr.1.into_raw_str())),
947                    Some(("if", r#""sensor""#))
948                );
949                assert_eq!(attr_iter.next(), None);
950            }
951            x => {
952                panic!("{:?}", x);
953            }
954        }
955
956        match parser.next() {
957            Some(Ok((link, mut attr_iter))) => {
958                assert_eq!(link, "http://www.example.com/sensors/t123");
959                assert_eq!(
960                    attr_iter
961                        .next()
962                        .map(|attr| (attr.0, attr.1.into_raw_str())),
963                    Some(("anchor", r#""/sensors/temp""#))
964                );
965                assert_eq!(
966                    attr_iter
967                        .next()
968                        .map(|attr| (attr.0, attr.1.into_raw_str())),
969                    Some(("rel", r#""describedby""#))
970                );
971                assert_eq!(attr_iter.next(), None);
972            }
973            x => {
974                panic!("{:?}", x);
975            }
976        }
977
978        match parser.next() {
979            Some(Ok((link, mut attr_iter))) => {
980                assert_eq!(link, "/t");
981                assert_eq!(
982                    attr_iter
983                        .next()
984                        .map(|attr| (attr.0, attr.1.into_raw_str())),
985                    Some(("anchor", r#""/sensors/temp""#))
986                );
987                assert_eq!(
988                    attr_iter
989                        .next()
990                        .map(|attr| (attr.0, attr.1.into_raw_str())),
991                    Some(("rel", r#""alternate""#))
992                );
993                assert_eq!(attr_iter.next(), None);
994            }
995            x => {
996                panic!("{:?}", x);
997            }
998        }
999
1000        assert_eq!(parser.next(), None);
1001    }
1002}