1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
use super::parse_error::ParseError;
use chrono::{DateTime, Utc};
use iri_string::types::IriString;
use oxilangtag::{LanguageTag, LanguageTagParseError};
use std::cmp::Ordering;
use valuable::{Valuable, Value, Visit};

macro_rules! IriStringImpl {
    ($structname:ident) => {
        impl $structname {
            pub(crate) fn new(uri: &str) -> Result<Self, ParseError> {
                let uri = uri.trim().parse::<IriString>()?;

                if uri.scheme_str() == "http" {
                    return Err(ParseError::InsecureHTTP);
                }

                let log_value = uri.as_str().to_string();

                Ok(Self { uri, log_value })
            }
        }

        impl Valuable for $structname {
            fn as_value(&self) -> Value<'_> {
                self.log_value.as_value()
            }

            fn visit(&self, _visit: &mut dyn Visit) {}
        }
    };
}

/// An [Acknowledgments field](https://www.rfc-editor.org/rfc/rfc9116#name-acknowledgments) links to a page where security researchers are recognized
#[derive(Debug, PartialEq)]
pub struct AcknowledgmentsField {
    /// The URI of the link according to [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986)
    pub uri: IriString,

    log_value: String,
}
IriStringImpl!(AcknowledgmentsField);

/// A [Canonical field](https://www.rfc-editor.org/rfc/rfc9116#name-canonical) contains a canonical URI for the security.txt file
#[derive(Debug, PartialEq)]
pub struct CanonicalField {
    /// The URI of the link according to [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986)
    pub uri: IriString,

    log_value: String,
}
IriStringImpl!(CanonicalField);

/// A [Contact field](https://www.rfc-editor.org/rfc/rfc9116#name-contact) contains contact information to use for reporting vulnerabilities
#[derive(Debug, PartialEq)]
pub struct ContactField {
    /// The URI of the link according to [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986)
    pub uri: IriString,

    log_value: String,
}
IriStringImpl!(ContactField);

/// An [Encryption field](https://www.rfc-editor.org/rfc/rfc9116#name-encryption) links to a key to be used for encrypted communication
#[derive(Debug, PartialEq)]
pub struct EncryptionField {
    /// The URI of the link according to [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986)
    pub uri: IriString,

    log_value: String,
}
IriStringImpl!(EncryptionField);

/// The [Expires field](https://www.rfc-editor.org/rfc/rfc9116#name-expires) represents the date and time after which the security.txt file is considered stale
#[derive(Debug, PartialEq)]
pub struct ExpiresField {
    /// The date and time from which the security.txt file is considered stale
    pub datetime: DateTime<Utc>,

    log_value: String,
}

impl ExpiresField {
    pub(crate) fn new(datetime: &str, now: DateTime<Utc>) -> Result<Self, ParseError> {
        let datetime: DateTime<Utc> = datetime.trim().parse()?;

        if datetime < now {
            return Err(ParseError::ExpiresFieldExpired);
        }

        let log_value = datetime.to_rfc3339();

        Ok(Self { datetime, log_value })
    }
}

impl PartialOrd for ExpiresField {
    #[inline]
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.datetime.partial_cmp(&other.datetime)
    }
}

impl Valuable for ExpiresField {
    fn as_value(&self) -> Value<'_> {
        self.log_value.as_value()
    }

    fn visit(&self, _visit: &mut dyn Visit) {}
}

/// A [Hiring field](https://www.rfc-editor.org/rfc/rfc9116#name-hiring) links to the vendor's security-related job positions
#[derive(Debug, PartialEq)]
pub struct HiringField {
    /// The URI of the link according to [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986)
    pub uri: IriString,

    log_value: String,
}
IriStringImpl!(HiringField);

/// A [Policy field](https://www.rfc-editor.org/rfc/rfc9116#name-policy) links to the security policy page
#[derive(Debug, PartialEq)]
pub struct PolicyField {
    /// The URI of the link according to [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986)
    pub uri: IriString,

    log_value: String,
}
IriStringImpl!(PolicyField);

/// The [Preferred-Languages field](https://www.rfc-editor.org/rfc/rfc9116#name-preferred-languages) lists the preferred languages for security reports
#[derive(Debug, PartialEq)]
pub struct PreferredLanguagesField {
    /// The set of preferred languages according to [RFC 5646](https://www.rfc-editor.org/rfc/rfc5646)
    pub languages: Vec<LanguageTag<String>>,

    log_value: String,
}

impl PreferredLanguagesField {
    pub(crate) fn new(languages: &str) -> Result<Self, ParseError> {
        let languages = languages
            .split(',')
            .map(str::trim)
            .map(LanguageTag::parse_and_normalize)
            .collect::<Result<Vec<LanguageTag<String>>, LanguageTagParseError>>()?;

        if languages.is_empty() {
            return Err(ParseError::IllegalField);
        }

        let log_value = languages.join(", ");

        Ok(Self { languages, log_value })
    }
}

impl Valuable for PreferredLanguagesField {
    fn as_value(&self) -> Value<'_> {
        self.log_value.as_value()
    }

    fn visit(&self, _visit: &mut dyn Visit) {}
}

/// The "Extension" field acts as a catch-all for any fields not explicitly supported by this library
///
/// This feature accommodates [section 2.4 on Extensibility](https://www.rfc-editor.org/rfc/rfc9116#name-extensibility) in the specification.
#[derive(Debug, PartialEq, Valuable)]
pub struct ExtensionField {
    /// Name of the extension field
    pub name: String,
    /// Value of the extension field
    pub value: String,
}

impl ExtensionField {
    pub(crate) fn new(name: String, value: String) -> Result<Self, ParseError> {
        Ok(Self { name, value })
    }
}