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}