Skip to content

Commit

Permalink
Separate parser from in-contract macro
Browse files Browse the repository at this point in the history
  • Loading branch information
tlambertz committed Mar 29, 2022
1 parent 68a8af4 commit e27419b
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 230 deletions.
2 changes: 1 addition & 1 deletion query-security-txt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ license = "MIT OR Apache-2.0"
bincode = "1.3.3"
clap = { version = "3.1.6", features = ["derive"] }
color-eyre = "0.6.1"
solana-security-txt = { version = "0.1.4", path = "../security-txt" }
solana-security-txt = { version = "0.1.4", path = "../security-txt", features = ["parser"] }
solana-client = "1.10.0"
solana-sdk = "1.10.0"
7 changes: 5 additions & 2 deletions security-txt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ readme = "../README.md"
repository = "https://github.com/neodyme-labs/solana-security-txt"
license = "MIT OR Apache-2.0"

[features]
# when no features are enabled, this crate has no dependencies. The parser requires two, but won't be used on-chain.
parser = ["thiserror", "twoway"]

[dependencies]
thiserror = "1.0.30"
twoway = "0.2.2"
thiserror = {version = "=1.0.30", optional = true}
twoway = {version = "=0.2.2", optional = true}
231 changes: 4 additions & 227 deletions security-txt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@
//!
//! The string the macro builds begins with the start marker `=======BEGIN SECURITY.TXT V1=======\0`, and ends with the end marker `=======END SECURITY.TXT V1=======\0`. In between is a list of an even amount of strings, delimited by nullbytes. Every two strings form a key-value-pair.
use core::fmt;
use std::{collections::HashMap, fmt::Display};

use thiserror::Error;
use twoway::find_bytes;
#[cfg(feature = "parser")]
mod parser;
#[cfg(feature = "parser")]
pub use crate::parser::*;

pub const SECURITY_TXT_BEGIN: &str = "=======BEGIN SECURITY.TXT V1=======\0";
pub const SECURITY_TXT_END: &str = "=======END SECURITY.TXT V1=======\0";
Expand All @@ -83,226 +83,3 @@ macro_rules! security_txt {
};
}

#[derive(Error, Debug)]
pub enum SecurityTxtError {
#[error("security.txt doesn't start with the right string")]
InvalidSecurityTxtBegin,
#[error("Couldn't find end string")]
EndNotFound,
#[error("Couldn't find start string")]
StartNotFound,
#[error("Invalid field: `{0:?}`")]
InvalidField(Vec<u8>),
#[error("Unknown field: `{0}`")]
UnknownField(String),
#[error("Invalid value `{0:?}` for field `{1}`")]
InvalidValue(Vec<u8>, String),
#[error("Invalid contact `{0}`")]
InvalidContact(String),
#[error("Missing field: `{0}`")]
MissingField(String),
#[error("Duplicate field: `{0}`")]
DuplicateField(String),
#[error("Uneven amount of parts")]
Uneven,
}

pub enum Contact {
Email(String),
Discord(String),
Telegram(String),
Twitter(String),
Link(String),
Other(String),
}

impl Display for Contact {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Contact::Discord(s) => write!(f, "Discord: {}", s),
Contact::Email(s) => write!(f, "Email: {}", s),
Contact::Telegram(s) => write!(f, "Telegram: {}", s),
Contact::Twitter(s) => write!(f, "Twitter: {}", s),
Contact::Link(s) => write!(f, "Link: {}", s),
Contact::Other(s) => write!(f, "Other: {}", s),
}
}
}

impl Contact {
pub fn from_str(s: &str) -> Result<Self, SecurityTxtError> {
let (typ, value) = s
.split_once(":")
.ok_or_else(|| SecurityTxtError::InvalidContact(s.to_string()))?;
let (contact_type, contact_info) = (typ.trim(), value.trim());
match contact_type.to_ascii_lowercase().as_str() {
"email" => Ok(Contact::Email(contact_info.to_string())),
"discord" => Ok(Contact::Discord(contact_info.to_string())),
"telegram" => Ok(Contact::Telegram(contact_info.to_string())),
"twitter" => Ok(Contact::Twitter(contact_info.to_string())),
"link" => Ok(Contact::Link(contact_info.to_string())),
"other" => Ok(Contact::Other(contact_info.to_string())),
_ => Err(SecurityTxtError::InvalidContact(s.to_string())),
}
}
}

pub struct SecurityTxt {
pub name: String,
pub project_url: String,
pub source_code: Option<String>,
pub expiry: Option<String>,
pub preferred_languages: Vec<String>,
pub contacts: Vec<Contact>,
pub auditors: Vec<String>,
pub encryption: Option<String>,
pub acknowledgements: Option<String>,
pub policy: String,
}

impl Display for SecurityTxt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Name: {}", self.name)?;
writeln!(f, "Project URL: {}", self.project_url)?;

if let Some(expiry) = &self.expiry {
writeln!(f, "Expires at: {}", expiry)?;
}

if let Some(source_code) = &self.source_code {
writeln!(f, "Source code: {}", source_code)?;
}

if !self.contacts.is_empty() {
writeln!(f, "\nContacts:")?;
for contact in &self.contacts {
writeln!(f, " {}", contact)?;
}
}

if !self.preferred_languages.is_empty() {
writeln!(f, "\nPreferred Languages:")?;
for languages in &self.preferred_languages {
writeln!(f, " {}", languages)?;
}
}

if let Some(encryption) = &self.encryption {
writeln!(f, "\nEncryption:")?;
writeln!(f, "{}", encryption)?;
}

if let Some(acknowledegments) = &self.acknowledgements {
writeln!(f, "\nAcknowledgements:")?;
writeln!(f, "{}", acknowledegments)?;
}

if !self.auditors.is_empty() {
writeln!(f, "\nAuditors:")?;
for auditor in &self.auditors {
writeln!(f, " {}", auditor)?;
}
}

writeln!(f, "\nPolicy:")?;
writeln!(f, "{}", self.policy)?;

Ok(())
}
}

/// Parses a security.txt. Might not consume all of `data`.
pub fn parse(mut data: &[u8]) -> Result<SecurityTxt, SecurityTxtError> {
if !data.starts_with(SECURITY_TXT_BEGIN.as_bytes()) {
return Err(SecurityTxtError::InvalidSecurityTxtBegin);
}

let end = match find_bytes(data, SECURITY_TXT_END.as_bytes()) {
Some(i) => i,
None => return Err(SecurityTxtError::EndNotFound),
};

data = &data[SECURITY_TXT_BEGIN.len()..end];

let mut attributes = HashMap::<String, String>::default();
let mut field: Option<String> = None;
for part in data.split(|&b| b == 0) {
if let Some(ref f) = field {
let value = std::str::from_utf8(part)
.map_err(|_| SecurityTxtError::InvalidValue(part.to_vec(), f.clone()))?;
attributes.insert(f.clone(), value.to_string());
field = None;
} else {
field = Some({
let field = std::str::from_utf8(part)
.map_err(|_| SecurityTxtError::InvalidField(part.to_vec()))?
.to_string();
if attributes.contains_key(&field) {
return Err(SecurityTxtError::DuplicateField(field));
}
field
});
}
}

let name = attributes
.remove("name")
.ok_or_else(|| SecurityTxtError::MissingField("name".to_string()))?;
let project_url = attributes
.remove("project_url")
.ok_or_else(|| SecurityTxtError::MissingField("project_url".to_string()))?;
let source_code = attributes.remove("source_code");
let expiry = attributes.remove("expiry");
let preferred_languages: Vec<_> = attributes
.remove("preferred_languages")
.ok_or_else(|| SecurityTxtError::MissingField("preferred_languages".to_string()))?
.split(',')
.map(|s| s.trim().to_string())
.collect();
let contacts: Result<Vec<_>, SecurityTxtError> = attributes
.remove("contacts")
.ok_or_else(|| SecurityTxtError::MissingField("contacts".to_string()))?
.split(",")
.map(|s| Contact::from_str(s.trim()))
.collect();
let contacts = contacts?;
let auditors: Vec<_> = attributes
.remove("auditors")
.unwrap_or_default()
.split(",")
.map(|s| s.trim().to_string())
.collect();
let encryption = attributes.remove("encryption");
let acknowledgements = attributes.remove("acknowledgements");
let policy = attributes
.remove("policy")
.ok_or_else(|| SecurityTxtError::MissingField("policy".to_string()))?;

if !attributes.is_empty() {
return Err(SecurityTxtError::UnknownField(
attributes.keys().next().unwrap().clone(),
));
}

Ok(SecurityTxt {
name,
project_url,
source_code,
expiry,
preferred_languages,
contacts,
auditors,
encryption,
acknowledgements,
policy,
})
}

/// Finds and parses the security.txt in the haystack
pub fn find_and_parse(data: &[u8]) -> Result<SecurityTxt, SecurityTxtError> {
let start = match find_bytes(data, SECURITY_TXT_BEGIN.as_bytes()) {
Some(i) => i,
None => return Err(SecurityTxtError::StartNotFound),
};
parse(&data[start..])
}
Loading

0 comments on commit e27419b

Please sign in to comment.