microsandbox_core/oci/
reference.rs

1use crate::error::MicrosandboxError;
2use getset::{Getters, Setters};
3use microsandbox_utils::{env, DEFAULT_OCI_REFERENCE_REPO_NAMESPACE, DEFAULT_OCI_REFERENCE_TAG};
4use oci_spec::image::Digest;
5use regex::Regex;
6use serde;
7use std::{fmt, str::FromStr};
8
9//--------------------------------------------------------------------------------------------------
10// Types
11//--------------------------------------------------------------------------------------------------
12
13/// Represents an OCI-compliant image reference.
14///
15/// This struct includes the registry, repository, and a selector that combines a tag and an optional digest.
16/// If no registry or tag is provided in the input string, default values will be used.
17#[derive(Debug, Clone, PartialEq, Eq, Getters, Setters)]
18#[getset(get = "pub with_prefix", set = "pub with_prefix")]
19#[derive(serde::Serialize, serde::Deserialize)]
20#[serde(try_from = "String")]
21#[serde(into = "String")]
22pub struct Reference {
23    /// The registry where the image is hosted.
24    registry: String,
25
26    /// The repository name of the image.
27    repository: String,
28
29    /// The selector specifying either a tag and an optional digest, or a digest only.
30    selector: ReferenceSelector,
31}
32
33/// Represents the selector part of an OCI image reference.
34///
35/// It can either be a tag (with an optional digest) or a standalone digest.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum ReferenceSelector {
38    /// Tag variant containing the image tag and an optional digest.
39    Tag {
40        /// The image tag.
41        tag: String,
42
43        /// The optional digest.
44        digest: Option<Digest>,
45    },
46    /// Digest variant containing only a digest.
47    Digest(Digest),
48}
49
50//--------------------------------------------------------------------------------------------------
51// Methods
52//--------------------------------------------------------------------------------------------------
53
54impl ReferenceSelector {
55    /// Creates a new ReferenceSelector with the specified tag and no digest.
56    pub fn tag(tag: impl Into<String>) -> Self {
57        Self::Tag {
58            tag: tag.into(),
59            digest: None,
60        }
61    }
62
63    /// Creates a new ReferenceSelector with both a tag and an associated digest.
64    pub fn tag_with_digest(tag: impl Into<String>, digest: impl Into<Digest>) -> Self {
65        Self::Tag {
66            tag: tag.into(),
67            digest: Some(digest.into()),
68        }
69    }
70
71    /// Creates a new ReferenceSelector using the specified digest.
72    pub fn digest(digest: impl Into<Digest>) -> Self {
73        Self::Digest(digest.into())
74    }
75}
76
77//--------------------------------------------------------------------------------------------------
78// Trait Implementations
79//--------------------------------------------------------------------------------------------------
80
81impl FromStr for Reference {
82    type Err = MicrosandboxError;
83
84    /// Parses a string slice into an OCI image Reference.
85    ///
86    /// Supported formats include:
87    /// - "registry/repository:tag"
88    /// - "repository:tag"
89    /// - "repository"
90    /// - "registry/repository@digest"
91    /// - "registry/repository:tag@digest"
92    ///
93    /// If the registry is omitted, it defaults to the value from [`get_oci_registry`].
94    /// If the tag is omitted, it defaults to [`DEFAULT_OCI_REFERENCE_TAG`].
95    ///
96    /// ## Returns
97    ///
98    /// Returns a [`MicrosandboxError::ImageReferenceError`] for parse failures.
99    fn from_str(s: &str) -> Result<Self, Self::Err> {
100        let s = s.trim();
101        let default_registry = env::get_oci_registry();
102
103        if s.is_empty() {
104            return Err(MicrosandboxError::ImageReferenceError(
105                "input string is empty".into(),
106            ));
107        }
108
109        if let Some(at_idx) = s.find('@') {
110            let potential_digest = &s[at_idx + 1..];
111            if potential_digest.contains(":") {
112                // Treat as digest branch
113                let (pre, digest_part) = s.split_at(at_idx);
114                let digest_str = &digest_part[1..]; // Skip '@'
115                let parsed_digest = digest_str.parse::<Digest>().map_err(|e| {
116                    MicrosandboxError::ImageReferenceError(format!("invalid digest: {}", e))
117                })?;
118
119                let (registry, remainder) = extract_registry_and_path(pre, &default_registry);
120                let (repository, tag) = extract_repository_and_tag(remainder)?;
121
122                // Validate registry, repository and tag
123                validate_registry(&registry)?;
124                validate_repository(&repository)?;
125                validate_tag(&tag)?;
126
127                Ok(Reference {
128                    registry,
129                    repository,
130                    selector: ReferenceSelector::tag_with_digest(tag, parsed_digest),
131                })
132            } else {
133                return Err(MicrosandboxError::ImageReferenceError(format!(
134                    "invalid digest: {}",
135                    potential_digest
136                )));
137            }
138        } else {
139            let (registry, remainder) = extract_registry_and_path(s, &default_registry);
140            let (repository, tag) = extract_repository_and_tag(remainder)?;
141
142            // Validate registry, repository and tag
143            validate_registry(&registry)?;
144            validate_repository(&repository)?;
145            validate_tag(&tag)?;
146
147            Ok(Reference {
148                registry,
149                repository,
150                selector: ReferenceSelector::tag(tag),
151            })
152        }
153    }
154}
155
156impl fmt::Display for Reference {
157    /// Formats the OCI image Reference into a string.
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        write!(f, "{}/{}", self.registry, self.repository)?;
160        match &self.selector {
161            ReferenceSelector::Tag {
162                tag,
163                digest: Some(d),
164            } => write!(f, ":{}@{}", tag, d),
165            ReferenceSelector::Tag { tag, digest: None } => write!(f, ":{}", tag),
166            ReferenceSelector::Digest(d) => write!(f, "@{}", d),
167        }
168    }
169}
170
171impl From<Reference> for String {
172    fn from(reference: Reference) -> String {
173        reference.to_string()
174    }
175}
176
177impl TryFrom<String> for Reference {
178    type Error = MicrosandboxError;
179
180    fn try_from(s: String) -> Result<Self, Self::Error> {
181        s.parse()
182    }
183}
184
185//--------------------------------------------------------------------------------------------------
186// Functions
187//--------------------------------------------------------------------------------------------------
188
189/// Validates the given registry string.
190///
191/// This function checks that the registry contains only alphanumeric characters, dashes, dots,
192/// and optionally a port number. It returns Ok(()) if the registry is valid, or an ImageReferenceError otherwise.
193fn validate_registry(registry: &str) -> Result<(), MicrosandboxError> {
194    let re = Regex::new(r"^[a-zA-Z0-9.-]+(:[0-9]+)?$").unwrap();
195    if re.is_match(registry) {
196        Ok(())
197    } else {
198        Err(MicrosandboxError::ImageReferenceError(format!(
199            "invalid registry: {}",
200            registry
201        )))
202    }
203}
204
205/// Validates the repository name.
206///
207/// The repository name must match a specific pattern that allows lowercase letters, numbers,
208/// and certain punctuation (._-) as well as slashes. Returns Ok if valid, and an error if invalid.
209fn validate_repository(repository: &str) -> Result<(), MicrosandboxError> {
210    let repo_re =
211        Regex::new(r"^([a-z0-9]+(?:[._-][a-z0-9]+)*)(/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$").unwrap();
212    if repo_re.is_match(repository) {
213        Ok(())
214    } else {
215        Err(MicrosandboxError::ImageReferenceError(format!(
216            "invalid repository: {}",
217            repository
218        )))
219    }
220}
221
222/// Validates the tag string.
223///
224/// Ensures that the tag starts with a word character and is followed by up to 127 characters
225/// that can be alphanumeric, underscores, dashes, or dots. Returns Ok if the tag is valid, or an error otherwise.
226fn validate_tag(tag: &str) -> Result<(), MicrosandboxError> {
227    let tag_re = Regex::new(r"^\w[\w.-]{0,127}$").unwrap();
228    if tag_re.is_match(tag) {
229        Ok(())
230    } else {
231        Err(MicrosandboxError::ImageReferenceError(format!(
232            "invalid tag: {}",
233            tag
234        )))
235    }
236}
237
238/// Extracts the registry and the remaining path from the OCI reference string.
239/// If the registry is not specified, returns the provided default registry.
240fn extract_registry_and_path<'a>(reference: &'a str, default_registry: &str) -> (String, &'a str) {
241    let segments: Vec<&str> = reference.splitn(2, '/').collect();
242    if segments.len() > 1
243        && (segments[0].contains('.') || segments[0].contains(':') || segments[0] == "localhost")
244    {
245        (segments[0].to_string(), segments[1])
246    } else {
247        (default_registry.to_string(), reference)
248    }
249}
250
251/// Extracts the repository and tag from the given path string.
252/// If the repository part does not contain a '/', the default namespace is prepended.
253/// If no tag is provided, the default tag is used.
254fn extract_repository_and_tag(path: &str) -> Result<(String, String), MicrosandboxError> {
255    if let Some(idx) = path.rfind(':') {
256        let repo_part = &path[..idx];
257        let tag_part = &path[idx + 1..];
258        if repo_part.is_empty() {
259            return Err(MicrosandboxError::ImageReferenceError(
260                "repository is empty".into(),
261            ));
262        }
263        let repository = if !repo_part.contains('/') {
264            format!("{}/{}", DEFAULT_OCI_REFERENCE_REPO_NAMESPACE, repo_part)
265        } else {
266            repo_part.to_string()
267        };
268        Ok((repository, tag_part.to_string()))
269    } else {
270        let repository = if !path.contains('/') {
271            format!("{}/{}", DEFAULT_OCI_REFERENCE_REPO_NAMESPACE, path)
272        } else {
273            path.to_string()
274        };
275        Ok((repository, DEFAULT_OCI_REFERENCE_TAG.to_string()))
276    }
277}
278
279//--------------------------------------------------------------------------------------------------
280// Tests
281//--------------------------------------------------------------------------------------------------
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_reference_valid_reference_with_registry_and_tag() {
289        let s = "docker.io/library/alpine:3.12";
290        let reference = s.parse::<Reference>().unwrap();
291        assert_eq!(reference.registry, "docker.io");
292        assert_eq!(reference.repository, "library/alpine");
293        match reference.selector {
294            ReferenceSelector::Tag {
295                ref tag,
296                ref digest,
297            } => {
298                assert_eq!(tag, "3.12");
299                assert!(digest.is_none());
300            }
301            _ => panic!("Expected Tag variant without digest"),
302        }
303        assert_eq!(reference.to_string(), "docker.io/library/alpine:3.12");
304    }
305
306    #[test]
307    fn test_reference_default_registry_and_tag() {
308        let s = "library/alpine";
309        let reference = s.parse::<Reference>().unwrap();
310        let expected_registry = env::get_oci_registry();
311        assert_eq!(reference.registry, expected_registry);
312        assert_eq!(reference.repository, "library/alpine");
313        match reference.selector {
314            ReferenceSelector::Tag {
315                ref tag,
316                ref digest,
317            } => {
318                assert_eq!(tag, DEFAULT_OCI_REFERENCE_TAG);
319                assert!(digest.is_none());
320            }
321            _ => panic!("Expected Tag variant without digest"),
322        }
323        let expected_string = format!(
324            "{}/library/alpine:{}",
325            expected_registry, DEFAULT_OCI_REFERENCE_TAG
326        );
327        assert_eq!(reference.to_string(), expected_string);
328    }
329
330    #[test]
331    fn test_reference_without_tag() {
332        let s = "docker.io/library/alpine";
333        let reference = s.parse::<Reference>().unwrap();
334        assert_eq!(reference.registry, "docker.io");
335        assert_eq!(reference.repository, "library/alpine");
336        match reference.selector {
337            ReferenceSelector::Tag {
338                ref tag,
339                ref digest,
340            } => {
341                assert_eq!(tag, DEFAULT_OCI_REFERENCE_TAG);
342                assert!(digest.is_none());
343            }
344            _ => panic!("Expected Tag variant without digest"),
345        }
346        let expected = format!("docker.io/library/alpine:{}", DEFAULT_OCI_REFERENCE_TAG);
347        assert_eq!(reference.to_string(), expected);
348    }
349
350    #[test]
351    fn test_reference_with_digest_and_tag() {
352        let valid_digest = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
353        let s = format!("registry.example.com/myrepo:mytag@sha256:{}", valid_digest);
354        let reference = s.parse::<Reference>().unwrap();
355        assert_eq!(reference.registry, "registry.example.com");
356        assert_eq!(reference.repository, "library/myrepo");
357        match reference.selector {
358            ReferenceSelector::Tag {
359                ref tag,
360                ref digest,
361            } => {
362                assert_eq!(tag, "mytag");
363                let d = digest.as_ref().expect("Expected a digest");
364                assert_eq!(d.to_string(), format!("sha256:{}", valid_digest));
365            }
366            _ => panic!("Expected Tag variant with digest"),
367        }
368        let expected = format!(
369            "registry.example.com/library/myrepo:mytag@sha256:{}",
370            valid_digest
371        );
372        assert_eq!(reference.to_string(), expected);
373    }
374
375    #[test]
376    fn test_reference_with_digest_only() {
377        let valid_digest = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
378        let s = format!("registry.example.com/myrepo@sha256:{}", valid_digest);
379        let reference = s.parse::<Reference>().unwrap();
380        assert_eq!(reference.registry, "registry.example.com");
381        assert_eq!(reference.repository, "library/myrepo");
382        match reference.selector {
383            ReferenceSelector::Tag {
384                ref tag,
385                ref digest,
386            } => {
387                assert_eq!(tag, DEFAULT_OCI_REFERENCE_TAG);
388                let d = digest.as_ref().expect("Expected a digest");
389                assert_eq!(d.to_string(), format!("sha256:{}", valid_digest));
390            }
391            _ => panic!("Expected Tag variant with digest"),
392        }
393        let expected = format!(
394            "registry.example.com/library/myrepo:{}@sha256:{}",
395            DEFAULT_OCI_REFERENCE_TAG, valid_digest
396        );
397        assert_eq!(reference.to_string(), expected);
398    }
399
400    #[test]
401    fn test_reference_registry_with_port() {
402        let s = "registry.example.com:5000/myrepo:1.0";
403        let reference = s.parse::<Reference>().unwrap();
404        assert_eq!(reference.registry, "registry.example.com:5000");
405        assert_eq!(reference.repository, "library/myrepo");
406        match reference.selector {
407            ReferenceSelector::Tag {
408                ref tag,
409                ref digest,
410            } => {
411                assert_eq!(tag, "1.0");
412                assert!(digest.is_none());
413            }
414            _ => panic!("Expected Tag variant without digest"),
415        }
416        assert_eq!(
417            reference.to_string(),
418            "registry.example.com:5000/library/myrepo:1.0"
419        );
420    }
421
422    #[test]
423    fn test_reference_single_segment_registry() {
424        let s = "docker.io/alpine";
425        let reference = s.parse::<Reference>().unwrap();
426        assert_eq!(reference.registry, "docker.io");
427        assert_eq!(
428            reference.repository,
429            format!("{}/alpine", DEFAULT_OCI_REFERENCE_REPO_NAMESPACE)
430        );
431        match reference.selector {
432            ReferenceSelector::Tag {
433                ref tag,
434                ref digest,
435            } => {
436                assert_eq!(tag, DEFAULT_OCI_REFERENCE_TAG);
437                assert!(digest.is_none());
438            }
439            _ => panic!("Expected Tag variant"),
440        }
441        assert_eq!(
442            reference.to_string(),
443            format!(
444                "docker.io/{}/alpine:{}",
445                DEFAULT_OCI_REFERENCE_REPO_NAMESPACE, DEFAULT_OCI_REFERENCE_TAG
446            )
447        );
448    }
449
450    #[test]
451    fn test_reference_no_registry_single_segment() {
452        let s = "alpine";
453        let reference = s.parse::<Reference>().unwrap();
454        let default_registry = env::get_oci_registry();
455        assert_eq!(reference.registry, default_registry);
456        assert_eq!(
457            reference.repository,
458            format!("{}/alpine", DEFAULT_OCI_REFERENCE_REPO_NAMESPACE)
459        );
460        match reference.selector {
461            ReferenceSelector::Tag {
462                ref tag,
463                ref digest,
464            } => {
465                assert_eq!(tag, DEFAULT_OCI_REFERENCE_TAG);
466                assert!(digest.is_none());
467            }
468            _ => panic!("Expected Tag variant"),
469        }
470        let expected = format!(
471            "{}/{}:{}",
472            default_registry,
473            format!("{}/alpine", DEFAULT_OCI_REFERENCE_REPO_NAMESPACE),
474            DEFAULT_OCI_REFERENCE_TAG
475        );
476        assert_eq!(reference.to_string(), expected);
477    }
478
479    #[test]
480    fn test_reference_no_registry_multi_segment() {
481        let s = "myorg/myrepo:stable";
482        let reference = s.parse::<Reference>().unwrap();
483        let default_registry = env::get_oci_registry();
484        assert_eq!(reference.registry, default_registry);
485        assert_eq!(reference.repository, "myorg/myrepo");
486        match reference.selector {
487            ReferenceSelector::Tag {
488                ref tag,
489                ref digest,
490            } => {
491                assert_eq!(tag, "stable");
492                assert!(digest.is_none());
493            }
494            _ => panic!("Expected Tag variant"),
495        }
496        let expected = format!("{}/myorg/myrepo:stable", default_registry);
497        assert_eq!(reference.to_string(), expected);
498    }
499
500    #[test]
501    fn test_reference_digest_single_segment() {
502        let valid_digest = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
503        let s = format!("docker.io/alpine@sha256:{}", valid_digest);
504        let reference = s.parse::<Reference>().unwrap();
505        assert_eq!(reference.registry, "docker.io");
506        assert_eq!(
507            reference.repository,
508            format!("{}/alpine", DEFAULT_OCI_REFERENCE_REPO_NAMESPACE)
509        );
510        match reference.selector {
511            ReferenceSelector::Tag {
512                ref tag,
513                ref digest,
514            } => {
515                assert_eq!(tag, DEFAULT_OCI_REFERENCE_TAG);
516                let d = digest.as_ref().expect("Expected digest");
517                assert_eq!(d.to_string(), format!("sha256:{}", valid_digest));
518            }
519            _ => panic!("Expected Tag variant with digest"),
520        }
521        let expected = format!(
522            "docker.io/{}/alpine:{}@sha256:{}",
523            DEFAULT_OCI_REFERENCE_REPO_NAMESPACE, DEFAULT_OCI_REFERENCE_TAG, valid_digest
524        );
525        assert_eq!(reference.to_string(), expected);
526    }
527
528    #[test]
529    fn test_reference_digest_multi_segment() {
530        let valid_digest = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
531        let s = format!("docker.io/myorg/myrepo:stable@sha256:{}", valid_digest);
532        let reference = s.parse::<Reference>().unwrap();
533        assert_eq!(reference.registry, "docker.io");
534        assert_eq!(reference.repository, "myorg/myrepo");
535        match reference.selector {
536            ReferenceSelector::Tag {
537                ref tag,
538                ref digest,
539            } => {
540                assert_eq!(tag, "stable");
541                let d = digest.as_ref().expect("Expected digest");
542                assert_eq!(d.to_string(), format!("sha256:{}", valid_digest));
543            }
544            _ => panic!("Expected Tag variant with digest"),
545        }
546        let expected = format!("docker.io/myorg/myrepo:stable@sha256:{}", valid_digest);
547        assert_eq!(reference.to_string(), expected);
548    }
549
550    #[test]
551    fn test_reference_complex_path() {
552        let s = "registry.io/v2/image:tag";
553        let reference = s.parse::<Reference>().unwrap();
554        assert_eq!(reference.registry, "registry.io");
555        assert_eq!(reference.repository, "v2/image");
556        match reference.selector {
557            ReferenceSelector::Tag {
558                ref tag,
559                ref digest,
560            } => {
561                assert_eq!(tag, "tag");
562                assert!(digest.is_none());
563            }
564            _ => panic!("Expected Tag variant"),
565        }
566        assert_eq!(reference.to_string(), "registry.io/v2/image:tag");
567    }
568
569    #[test]
570    fn test_reference_multi_slash_repository() {
571        let s = "docker.io/a/b/c:1.0";
572        let reference = s.parse::<Reference>().unwrap();
573        assert_eq!(reference.registry, "docker.io");
574        assert_eq!(reference.repository, "a/b/c");
575        match reference.selector {
576            ReferenceSelector::Tag {
577                ref tag,
578                ref digest,
579            } => {
580                assert_eq!(tag, "1.0");
581                assert!(digest.is_none());
582            }
583            _ => panic!("Expected Tag variant"),
584        }
585        assert_eq!(reference.to_string(), "docker.io/a/b/c:1.0");
586    }
587
588    #[test]
589    fn test_empty_input() {
590        let s = "";
591        let err = s.parse::<Reference>().unwrap_err();
592        let err_str = err.to_string();
593        assert!(err_str.contains("input string is empty"));
594    }
595
596    #[test]
597    fn test_empty_repository() {
598        let s = "registry.example.com/:tag";
599        let err = s.parse::<Reference>().unwrap_err();
600        let err_str = err.to_string();
601        assert!(err_str.contains("repository is empty"));
602    }
603
604    #[test]
605    fn test_reference_registry_ip_port_single_segment() {
606        let s = "192.168.1.1:5000/ubuntu:18.04";
607        let reference = s.parse::<Reference>().unwrap();
608        assert_eq!(reference.registry, "192.168.1.1:5000");
609        assert_eq!(
610            reference.repository,
611            format!("{}/ubuntu", DEFAULT_OCI_REFERENCE_REPO_NAMESPACE)
612        );
613        match reference.selector {
614            ReferenceSelector::Tag {
615                ref tag,
616                ref digest,
617            } => {
618                assert_eq!(tag, "18.04");
619                assert!(digest.is_none());
620            }
621            _ => panic!("Expected Tag variant"),
622        }
623        let expected = format!(
624            "192.168.1.1:5000/{}/ubuntu:18.04",
625            DEFAULT_OCI_REFERENCE_REPO_NAMESPACE
626        );
627        assert_eq!(reference.to_string(), expected);
628    }
629
630    #[test]
631    fn test_reference_registry_ip_port_multi_segment() {
632        let s = "192.168.1.1:5000/org/repo:version";
633        let reference = s.parse::<Reference>().unwrap();
634        assert_eq!(reference.registry, "192.168.1.1:5000");
635        assert_eq!(reference.repository, "org/repo");
636        match reference.selector {
637            ReferenceSelector::Tag {
638                ref tag,
639                ref digest,
640            } => {
641                assert_eq!(tag, "version");
642                assert!(digest.is_none());
643            }
644            _ => panic!("Expected Tag variant"),
645        }
646        let expected = "192.168.1.1:5000/org/repo:version".to_string();
647        assert_eq!(reference.to_string(), expected);
648    }
649
650    #[test]
651    fn test_reference_invalid_registry() {
652        // Registry contains an invalid character '!' and is forced as a registry by containing a dot
653        let s = "inva!id-registry.com/library/alpine:3.12";
654        let err = s.parse::<Reference>().unwrap_err();
655        assert!(err.to_string().contains("invalid registry"));
656    }
657
658    #[test]
659    fn test_reference_invalid_repository() {
660        // Repository contains uppercase letters which are invalid
661        let s = "docker.io/Library/alpine:3.12";
662        let err = s.parse::<Reference>().unwrap_err();
663        assert!(err.to_string().contains("invalid repository"));
664    }
665
666    #[test]
667    fn test_reference_invalid_tag() {
668        // Tag contains an invalid character '!'
669        let s = "docker.io/library/alpine:t!ag";
670        let err = s.parse::<Reference>().unwrap_err();
671        let err_str = err.to_string();
672        assert!(err_str.contains("invalid tag"));
673    }
674
675    #[test]
676    fn test_reference_tag_length_exceeds_limit() {
677        // Create a tag of length 129 (exceeds max length of 128 characters)
678        let long_tag = "a".repeat(129);
679        let s = format!("docker.io/library/alpine:{}", long_tag);
680        let err = s.parse::<Reference>().unwrap_err();
681        assert!(err.to_string().contains("invalid tag"));
682    }
683}