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