tulip_solana_security_txt/
lib.rs1use core::fmt;
62use std::{collections::HashMap, fmt::Display};
63
64use thiserror::Error;
65use twoway::find_bytes;
66
67pub const SECURITY_TXT_BEGIN: &str = "=======BEGIN SECURITY.TXT V1=======\0";
68pub const SECURITY_TXT_END: &str = "=======END SECURITY.TXT V1=======\0";
69
70#[macro_export]
71macro_rules! security_txt {
72 ($($name:ident: $value:expr),*) => {
73 #[allow(dead_code)]
74 #[no_mangle]
75 #[link_section = ".security.txt"]
76 pub static security_txt: &str = concat! {
77 "=======BEGIN SECURITY.TXT V1=======\0",
78 $(stringify!($name), "\0", $value, "\0",)*
79 "=======END SECURITY.TXT V1=======\0"
80 };
81 };
82}
83
84#[derive(Error, Debug)]
85pub enum SecurityTxtError {
86 #[error("security.txt doesn't start with the right string")]
87 InvalidSecurityTxtBegin,
88 #[error("Couldn't find end string")]
89 EndNotFound,
90 #[error("Couldn't find start string")]
91 StartNotFound,
92 #[error("Invalid field: `{0:?}`")]
93 InvalidField(Vec<u8>),
94 #[error("Unknown field: `{0}`")]
95 UnknownField(String),
96 #[error("Invalid value `{0:?}` for field `{1}`")]
97 InvalidValue(Vec<u8>, String),
98 #[error("Invalid contact `{0}`")]
99 InvalidContact(String),
100 #[error("Missing field: `{0}`")]
101 MissingField(String),
102 #[error("Duplicate field: `{0}`")]
103 DuplicateField(String),
104 #[error("Uneven amount of parts")]
105 Uneven,
106}
107
108pub enum Contact {
109 Email(String),
110 Discord(String),
111 Telegram(String),
112 Twitter(String),
113 Link(String),
114 Other(String),
115}
116
117impl Display for Contact {
118 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
119 match self {
120 Contact::Discord(s) => write!(f, "Discord: {}", s),
121 Contact::Email(s) => write!(f, "Email: {}", s),
122 Contact::Telegram(s) => write!(f, "Telegram: {}", s),
123 Contact::Twitter(s) => write!(f, "Twitter: {}", s),
124 Contact::Link(s) => write!(f, "Link: {}", s),
125 Contact::Other(s) => write!(f, "Other: {}", s),
126 }
127 }
128}
129
130impl Contact {
131 pub fn from_str(s: &str) -> Result<Self, SecurityTxtError> {
132 let parts: Vec<_> = s.split(":").collect();
133 if parts.len() != 2 {
134 return Err(SecurityTxtError::InvalidContact(s.to_string()));
135 }
136 let (contact_type, contact_info) = (parts[0].trim(), parts[1].trim());
137 match contact_type.to_ascii_lowercase().as_str() {
138 "email" => Ok(Contact::Email(contact_info.to_string())),
139 "discord" => Ok(Contact::Discord(contact_info.to_string())),
140 "telegram" => Ok(Contact::Telegram(contact_info.to_string())),
141 "twitter" => Ok(Contact::Twitter(contact_info.to_string())),
142 "link" => Ok(Contact::Link(contact_info.to_string())),
143 "other" => Ok(Contact::Other(contact_info.to_string())),
144 _ => Err(SecurityTxtError::InvalidContact(s.to_string())),
145 }
146 }
147}
148
149pub struct SecurityTxt {
150 pub name: String,
151 pub project_url: String,
152 pub source_code: Option<String>,
153 pub expiry: Option<String>,
154 pub preferred_languages: Vec<String>,
155 pub contacts: Vec<Contact>,
156 pub encryption: Option<String>,
157 pub acknowledgements: Option<String>,
158 pub policy: String,
159}
160
161impl Display for SecurityTxt {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 writeln!(f, "Name: {}", self.name)?;
164 writeln!(f, "Project URL: {}", self.project_url)?;
165
166 if let Some(expiry) = &self.expiry {
167 writeln!(f, "Expires at: {}", expiry)?;
168 }
169
170 if let Some(source_code) = &self.source_code {
171 writeln!(f, "Source code: {}", source_code)?;
172 }
173
174 if !self.contacts.is_empty() {
175 writeln!(f, "\nContacts:")?;
176 for contact in &self.contacts {
177 writeln!(f, " {}", contact)?;
178 }
179 }
180
181 if !self.preferred_languages.is_empty() {
182 writeln!(f, "\nPreferred Languages:")?;
183 for languages in &self.preferred_languages {
184 writeln!(f, " {}", languages)?;
185 }
186 }
187
188 if let Some(encryption) = &self.encryption {
189 writeln!(f, "\nEncryption:")?;
190 writeln!(f, "{}", encryption)?;
191 }
192
193 if let Some(acknowledegments) = &self.acknowledgements {
194 writeln!(f, "\nAcknowledgements:")?;
195 writeln!(f, "{}", acknowledegments)?;
196 }
197
198 writeln!(f, "\nPolicy:")?;
199 writeln!(f, "{}", self.policy)?;
200
201 Ok(())
202 }
203}
204
205pub fn parse(mut data: &[u8]) -> Result<SecurityTxt, SecurityTxtError> {
207 if !data.starts_with(SECURITY_TXT_BEGIN.as_bytes()) {
208 return Err(SecurityTxtError::InvalidSecurityTxtBegin);
209 }
210
211 let end = match find_bytes(data, SECURITY_TXT_END.as_bytes()) {
212 Some(i) => i,
213 None => return Err(SecurityTxtError::EndNotFound),
214 };
215
216 data = &data[SECURITY_TXT_BEGIN.len()..end];
217
218 let mut attributes = HashMap::<String, String>::default();
219 let mut field: Option<String> = None;
220 for part in data.split(|&b| b == 0) {
221 if let Some(ref f) = field {
222 let value = std::str::from_utf8(part)
223 .map_err(|_| SecurityTxtError::InvalidValue(part.to_vec(), f.clone()))?;
224 attributes.insert(f.clone(), value.to_string());
225 field = None;
226 } else {
227 field = Some({
228 let field = std::str::from_utf8(part)
229 .map_err(|_| SecurityTxtError::InvalidField(part.to_vec()))?
230 .to_string();
231 if attributes.contains_key(&field) {
232 return Err(SecurityTxtError::DuplicateField(field));
233 }
234 field
235 });
236 }
237 }
238
239 let name = attributes
240 .remove("name")
241 .ok_or_else(|| SecurityTxtError::MissingField("name".to_string()))?;
242 let project_url = attributes
243 .remove("project_url")
244 .ok_or_else(|| SecurityTxtError::MissingField("project_url".to_string()))?;
245 let source_code = attributes.remove("source_code");
246 let expiry = attributes.remove("expiry");
247 let preferred_languages: Vec<_> = attributes
248 .remove("preferred_languages")
249 .ok_or_else(|| SecurityTxtError::MissingField("preferred_languages".to_string()))?
250 .split(',')
251 .map(|s| s.trim().to_string())
252 .collect();
253 let contacts: Result<Vec<_>, SecurityTxtError> = attributes
254 .remove("contacts")
255 .ok_or_else(|| SecurityTxtError::MissingField("contacts".to_string()))?
256 .split(",")
257 .map(|s| Contact::from_str(s.trim()))
258 .collect();
259 let contacts = contacts?;
260 let encryption = attributes.remove("encryption");
261 let acknowledgements = attributes.remove("acknowledgements");
262 let policy = attributes
263 .remove("policy")
264 .ok_or_else(|| SecurityTxtError::MissingField("policy".to_string()))?;
265
266 if !attributes.is_empty() {
267 return Err(SecurityTxtError::UnknownField(
268 attributes.keys().next().unwrap().clone(),
269 ));
270 }
271
272 Ok(SecurityTxt {
273 name,
274 project_url,
275 source_code,
276 expiry,
277 preferred_languages,
278 contacts,
279 encryption,
280 acknowledgements,
281 policy,
282 })
283}
284
285pub fn find_and_parse(data: &[u8]) -> Result<SecurityTxt, SecurityTxtError> {
287 let start = match find_bytes(data, SECURITY_TXT_BEGIN.as_bytes()) {
288 Some(i) => i,
289 None => return Err(SecurityTxtError::StartNotFound),
290 };
291 parse(&data[start..])
292}