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}