diff --git a/Cargo.toml b/Cargo.toml index 826a00a..6bfd83b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,9 @@ name = "libpacstall" version = "0.1.0" edition = "2021" authors = [ - "Sourajyoti Basak ", - "David Brochero ", - "Paul Cosma " + "Sourajyoti Basak ", + "David Brochero ", + "Paul Cosma ", ] description = "Backend API library for Pacstall" repository = "https://github.com/pacstall/libpacstall" @@ -14,9 +14,28 @@ keywords = ["aur", "pacstall", "package-manager", "linux", "apt"] categories = ["caching", "config", "parsing", "os::linux-apis"] [dependencies] -figment = { version = "0.10.6", features = ["env", "test", "toml" ] } +figment = { version = "0.10.6", features = ["env", "test", "toml"] } +miette = "5.5.0" num_cpus = "1.13.1" +regex = "1.7.0" +semver = "1.0.14" serde = { version = "1.0.144", features = ["derive"] } +spdx = "0.10.0" +strum = { version = "0.24.1", features = ["derive"] } +thiserror = "1.0.37" +tree-sitter = "0.19" +tree-sitter-bash = "0.19.0" [dev-dependencies] -rstest = "0.15.0" +miette = { version = "5.5.0", features = ["fancy"] } +criterion = { version = "0.4.0", features = ["html_reports"] } +rstest = "0.16.0" +proptest = "1.0.0" + + +[[example]] +name = "parser" + +[[bench]] +name = "parser" +harness = false diff --git a/benches/parser.rs b/benches/parser.rs new file mode 100644 index 0000000..3928895 --- /dev/null +++ b/benches/parser.rs @@ -0,0 +1,138 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use libpacstall::parser::pacbuild::PacBuild; + +pub fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("parser", |b| b.iter(|| PacBuild::from_source(black_box(r#"pkgname='potato' # can also be an array, probably shouldn't be though +pkgver='1.0.0' # this is the variable pkgver, can also be a function that will return dynamic version +epoch='0' # force package to be seen as newer no matter what +pkgdesc='Pretty obvious' +url='https://potato.com' +license="Apache-2.0 OR MIT" +arch=('any' 'x86_64') +maintainer=('Henryws ' 'Wizard-28 ') +repology=("project: $pkgname") +source=( + "https://potato.com/$pkgver.tar.gz" + "potato.tar.gz::https://potato.com/$pkgver.tar.gz" # with a forced download name + "$pkgname::git+https://github.com/pacstall/pacstall" # git repo + "$pkgname::https://github.com/pacstall/pacstall/releases/download/2.0.1/pacstall-2.0.1.deb::repology" # use changelog with repology + "$pkgname::git+https://github.com/pacstall/pacstall#branch=master" # git repo with branch + "$pkgname::git+file://home/henry/pacstall/pacstall" # local git repo + "magnet://xt=urn:btih:c4769d7000244e4cae9c054a83e38f168bf4f69f&dn=archlinux-2022.09.03-x86_64.iso" # magnet link + "ftp://ftp.gnu.org/gnu/$pkgname/$pkgname-$pkgver.tar.xz" # ftp + "patch-me-harder.patch::https://potato.com/patch-me.patch" +) # also source_x86_64=(), source_i386=() + +noextract=( + "$pkgver.tar.gz" +) + +sha256sums=( + 'e69fcf51c211772d4f193f3dc59b1e91607bea7e53999f1d5e03ba401e5da969' + 'SKIP' + 'SKIP' + 'etc' +) # can also do sha256sums_x86_64=(), repeat for sha384, sha512, and b2 + +optdepends=( + 'same as pacstall: yes' +) # rince and repeat optdepends_$arch=() + +depends=( + 'hashbrowns>=1.8.0' + 'mashed-potatos<=1.9.0' + 'gravy=2.3.0' + 'applesauce>3.0.0' + 'chicken<2.0.0' + 'libappleslices.so' + 'libdeepfryer.so=3' +) + +makedepends=( + 'whisk' + 'onions' +) + +checkdepends=( + 'customer_satisfaction' +) + +ppa=('mcdonalds/ppa') + +provides=( + 'mashed-potatos' + 'aaaaaaaaaaaaaaaaaaaaaaaaaa' +) + +conflicts=( + 'KFC' + 'potato_rights' +) # can also do conflicts_$arch=() + +replaces=( + 'kidney_beans' +) + +backup=( + 'etc/potato/prepare.conf' +) + +options=( + '!strip' + '!docs' + 'etc' +) + +groups=('potato-clan') + +incompatible=('debian::jessy' 'ubuntu::20.04') + +prepare() { + cd "$pkgname-$pkgver" + patch -p1 -i "$srcdir/patch-me-harder.patch" +} + +build() { + cd "$pkgname-$pkgver" + ./configure --prefix=/usr + make +} + +check() { + cd "$pkgname-$pkgver" + make -k check +} + +package() { + cd "$pkgname-$pkgver" + make DESTDIR="$pkgdir/" install +} + +pre_install() { + echo "potato" +} + +post_install() { + echo "potato" +} + +pre_upgrade() { + echo "potato" +} + +post_upgrade() { + echo "potato" +} + +pre_remove() { + echo "potato" +} + +post_remove() { + echo "potato" +}"# +)))); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/examples/parser.rs b/examples/parser.rs new file mode 100644 index 0000000..62251db --- /dev/null +++ b/examples/parser.rs @@ -0,0 +1,23 @@ +use libpacstall::parser::pacbuild::PacBuild; +use miette::Result; + +fn main() -> Result<()> { + let k = 0; + dbg!(PacBuild::from_source( + r#" +pkgname='te' +pkgver="1.0" +epoch="21" +arch=("any") +#maintainer=("foo <" "bar <>" "biz ") +license="MIT" +ppa=("lol/kol") +#depends=("foods" "bar: h") +#optdepends=("foo: ldsfadvnvbnvbnvnvnnvbnfds") +repology=("project: foo" "visiblename: distrotube") +sources=("git+file:///home/wizard/tmp/::repology") +"# + .trim(), + )?); + Ok(()) +} diff --git a/proptest-regressions/parser/pacbuild.txt b/proptest-regressions/parser/pacbuild.txt new file mode 100644 index 0000000..60f759c --- /dev/null +++ b/proptest-regressions/parser/pacbuild.txt @@ -0,0 +1,22 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc d852d8ab1865b7cd6cd0a169b0f9623924b087f06f45c37b71dd755c2905d17b # shrinks to version = "¡" +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc c0a8aeb72996e589b4531ff434ba0cf3a78d285d0d331e57fa01ec711cf65b74 # shrinks to name = "¡" +cc f1c3f577e82492bf2b437a5ea82df991eeff728a04009f6306f3f3bcfc5251d8 # shrinks to name = "-" +cc e5174aa9101b6422efc1327ce3176df895036712ab6d21082731d199cfeac9b2 # shrinks to name = ".a" +cc 4a837b559db189d9f3b4be4819eb293b163a433e3e9d50f476844096ff5e3c86 # shrinks to name = "z" +cc 3e96b46990a80964a0d69f13de029db178dab8dac811353ad973c5fee2b85ac3 # shrinks to name = "" +cc 6d1545dad9462e6e30fdf91f1a25462532b2e40327bb36a8c7a9bd152a37610e # shrinks to name = "0" +cc de06beeedfa5666db998956db96183abf32ffa4a551fe22b0826242bf7535ccb # shrinks to name = ":", version_req = "0" +cc b1b6390f4ec735609c5351febfee2a0d8a3b1b7895c3d03ace0e888a4b4efc53 # shrinks to name = "\t", email = "a@a.a" +cc 506f616e095e77967291c2782e927324b41cef3302f04809a2230c5c057e7ca4 # shrinks to value = "" diff --git a/src/config.rs b/src/config.rs index fc8f430..1dcb415 100644 --- a/src/config.rs +++ b/src/config.rs @@ -397,7 +397,7 @@ mod tests { let selected_editor_path = &home.join(".selected_editor"); writeln!( - File::create(&selected_editor_path)?, + File::create(selected_editor_path)?, r#" # This is a mock file, if this persists on your system contact the Pacstall developers. SELECTED_EDITOR="/usr/bin/nvim" diff --git a/src/lib.rs b/src/lib.rs index 7b8a174..c07184f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,4 +5,6 @@ html_logo_url = "https://raw.githubusercontent.com/pacstall/website/master/client/public/pacstall.svg" )] #![allow(clippy::must_use_candidate)] + pub mod config; +pub mod parser; diff --git a/src/parser/errors.rs b/src/parser/errors.rs new file mode 100644 index 0000000..9d12d0f --- /dev/null +++ b/src/parser/errors.rs @@ -0,0 +1,42 @@ +use miette::{Diagnostic, Report, SourceSpan}; +use thiserror::Error; + +#[derive(Debug, Diagnostic, Error)] +#[error("Parser error")] +pub struct ParseError { + /// Source code. + #[source_code] + pub input: String, + + #[related] + pub related: Vec, +} + +#[derive(Debug, Diagnostic, Clone, Eq, PartialEq, Error)] +#[error("Invalid field")] +pub struct FieldError { + /// States the issues with the field. + pub field_label: String, + + /// Span of the field which has the error. + #[label("{field_label}")] + pub field_span: SourceSpan, + + /// Span of the erroneous source code. + #[label("here")] + pub error_span: SourceSpan, + + /// Suggestion for fixing the parser error. + #[help] + pub help: String, +} + +#[derive(Debug, Diagnostic, Clone, Eq, PartialEq, Error)] +#[error("Missing field")] +pub struct MissingField { + pub label: &'static str, +} + +#[derive(Debug, Diagnostic, Clone, Eq, PartialEq, Error)] +#[error("Bad syntax")] +pub struct BadSyntax; diff --git a/src/parser/mod.rs b/src/parser/mod.rs new file mode 100644 index 0000000..55db91e --- /dev/null +++ b/src/parser/mod.rs @@ -0,0 +1,2 @@ +pub mod errors; +pub mod pacbuild; diff --git a/src/parser/pacbuild.rs b/src/parser/pacbuild.rs new file mode 100644 index 0000000..b24c6f8 --- /dev/null +++ b/src/parser/pacbuild.rs @@ -0,0 +1,2045 @@ +#![allow(clippy::match_on_vec_items)] +use std::collections::HashMap; +use std::path::PathBuf; +use std::process::Command; + +use miette::{Context, IntoDiagnostic, Report, SourceSpan}; +use regex::Regex; +use semver::VersionReq; +use spdx::Expression; +use strum::{Display, EnumString}; +use tree_sitter::{Node, Parser, Query, QueryCursor}; + +use super::errors::{BadSyntax, FieldError, MissingField, ParseError}; + +#[derive(Debug, PartialEq, Eq)] +pub struct Pkgname(String); + +impl Pkgname { + pub(crate) fn new( + name: &str, + field_node: &Node, + value_node: &Node, + ) -> Result { + if name.trim().is_empty() { + return Err(FieldError { + field_label: "Cannot be empty".into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte(), + value_node.end_byte() - value_node.start_byte(), + ) + .into(), + help: "Remove this empty field".into(), + }); + } + for (index, character) in name.chars().enumerate() { + if index == 0 { + if character == '-' { + return Err(FieldError { + field_label: "Cannot start with a hyphen ( - )".into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: (value_node.start_byte() + 1).into(), + help: format!( + "Use \x1b[0;32mpkgname=\"{}\"\x1b[0m instead", + &name[1..name.len()] + ), + }); + } + + if character == '.' { + return Err(FieldError { + field_label: "Cannot start with a period ( . )".to_owned(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: (value_node.start_byte() + 1).into(), + help: format!( + "Use \x1b[0;32mpkgname=\"{}\"\x1b[0m instead", + &name[1..name.len()] + ), + }); + } + } + + let check = |character: char| { + character.is_ascii_alphabetic() && character.is_lowercase() + || character.is_ascii_digit() + || character == '@' + || character == '.' + || character == '_' + || character == '+' + || character == '-' + }; + + if !check(character) { + return Err(FieldError { + field_label: "Can only contain lowercase, alphanumerics or @._+-".to_owned(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: (value_node.start_byte() + 1 + index).into(), + help: format!("Use \x1b[0;32mpkgname=\"{}\"\x1b[0m instead", { + let mut name = name.to_owned(); + name.retain(check); + name + }), + }); + } + } + + Ok(Self(name.to_string())) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum PkgverType { + Variable(Pkgver), + Function(String), +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Pkgver(String); + +impl Pkgver { + pub fn new(version: &str, field_node: &Node, value_node: &Node) -> Result { + for (index, character) in version.chars().enumerate() { + if !(character.is_ascii_alphanumeric() || character == '.' || character == '_') { + return Err(FieldError { + field_label: "Can only contain alphanumerics, periods and underscores" + .to_owned(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: (value_node.start_byte() + 1 + index).into(), + help: "Remove the invalid characters".into(), + }); + } + } + + Ok(Self(version.into())) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Maintainer { + name: String, + emails: Option>, +} + +impl Maintainer { + // FIXME: Proptest + pub fn new(maintainer: &str, field_node: &Node, value_node: &Node) -> Result { + let mut split: Vec = maintainer + .split_whitespace() + .map(ToString::to_string) + .collect(); + + Ok(Self { + name: match split.first() { + Some(name) => name.trim().into(), + None => { + return Err(FieldError { + field_label: "Needs a maintainer name".to_owned(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: (value_node.start_byte() + 1).into(), + help: "Add a maintainer name. This is usually your GitHub username".into(), + }); + }, + }, + emails: { + if split.len() > 1 { + let mut emails = vec![]; + for email in &mut split[1..] { + if !email.ends_with('>') { + return Err(FieldError { + field_label: "Missing trailing >".to_owned(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: (value_node.end_byte() - 2).into(), + help: "Add a trailing > to the email address".into(), + }); + } + let email = email.trim_matches(['<', '>'].as_ref()); + if email.is_empty() { + return Err(FieldError { + field_label: "Email address cannot be empty".to_owned(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte() + split[0].len() + 1, + value_node.end_byte() + - (value_node.start_byte() + split[0].len() + 2), + ) + .into(), + help: "Add the email address".into(), + }); + } + + emails.push((*email).to_string()); + } + + Some(emails) + } else { + None + } + }, + }) + } +} + +impl ToString for Maintainer { + fn to_string(&self) -> String { + match &self.emails { + Some(emails) => { + let mut maintainer_string = self.name.clone(); + + for email in emails { + maintainer_string.push_str(&format!(" <{email}>")); + } + maintainer_string + }, + None => self.name.clone(), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Dependency { + pub name: String, + pub version_req: Option, +} + +impl Dependency { + fn new(dependency: &str, field_node: &Node, value_node: &Node) -> Result { + let split: Vec<&str> = dependency.split(':').collect(); + + let name = split[0].to_owned(); + + if !name.is_ascii() { + return Err(FieldError { + field_label: "Name has to be valid ASCII".to_owned(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte() + 1, + value_node.end_byte() - value_node.start_byte(), + ) + .into(), + help: "Try romanizing your dependency name.".to_owned(), + }); + } + + let version_req = match split.get(1) { + Some(req) => match VersionReq::parse(req.trim()) { + Ok(req) => Some(req), + Err(error) => { + dbg!(req); + return Err(FieldError { + field_label: error.to_string(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte() + 1 + name.len() + 2, + value_node.end_byte() - value_node.start_byte() - name.len() - 4, + ) + .into(), + help: "The version requirements syntax is defined here: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html".into(), + }); + }, + }, + None => None, + }; + + Ok(Self { name, version_req }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct OptionalDependency { + pub name: String, + pub description: Option, +} + +impl OptionalDependency { + fn new( + optional_dependency: &str, + field_node: &Node, + value_node: &Node, + ) -> Result { + // package:i386: desc + + // let Some(name, description) = optional_dependency.rsplit_once(":") else + // }; + + if optional_dependency.is_empty() { + return Err(FieldError { + field_label: "Cannot be empty".into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte(), + value_node.end_byte() - value_node.start_byte(), + ) + .into(), + help: "Remove this empty field".into(), + }); + } + + let (name, description) = match optional_dependency.rsplit_once(':') { + Some((name, raw_description)) => { + // l:d l: d + // Remove the first leading space (` `) from the raw description, which is part + // of the syntax + let description = &raw_description[1..]; + let trim_start_description = description.trim_start(); + let trim_end_description = description.trim_end(); + let trimmed_description = description.trim(); + + // Succeeds if the syntactic leading space wasn't present in the raw + // description + dbg!(description, raw_description); + if raw_description.starts_with(' ') + && raw_description.chars().nth(1).unwrap() != ' ' + { + return Err(FieldError { + field_label: "The syntactic leading space is missing".to_owned(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte() + 1 + name.len() + 2, + description.len() - trim_start_description.len(), + ) + .into(), + help: format!( + "Use this instead: \x1b[0;32m\"{name}: {trimmed_description}\"\x1b[0m" + ), + }); + } + + // Check for leading spaces + if trim_start_description != description { + return Err(FieldError { + field_label: "Extra leading spaces are invalid".to_owned(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte() + 1 + name.len() + 2, + description.len() - trim_start_description.len(), + ) + .into(), + help: format!( + "Use this instead: \x1b[0;32m\"{name}: {trimmed_description}\"\x1b[0m" + ), + }); + } + + // Check for trailing spaces + if description.trim_end() != description { + return Err(FieldError { + field_label: "Trailing spaces are invalid".to_owned(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + + error_span: ( + value_node.start_byte() + + 1 + + name.len() + + 2 + + trimmed_description.len(), + description.len() - trim_end_description.len(), + ) + .into(), + help: format!( + "Use this instead: \x1b[0;32m\"{name}: {trimmed_description}\"\x1b[0m" + ), + }); + } + + (name.to_owned(), Some(description.to_owned())) + }, + None => (optional_dependency.to_owned(), None), + }; + + Ok(Self { name, description }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct PPA { + pub owner: String, + pub package: String, +} + +impl PPA { + pub fn new(ppa: &str, field_node: &Node, value_node: &Node) -> Result { + let split: Vec<&str> = ppa.split('/').collect(); + + if split.len() == 1 { + return Err(FieldError { + field_label: "Must contain the PPA in the format: owner/package".to_owned(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte() + 1, + value_node.end_byte() - (value_node.start_byte() + 2), + ) + .into(), + help: "Add the PPA in proper format. Example: kelleyk/emacs".into(), + }); + } + + Ok(Self { + owner: split[0].into(), + package: split[1].into(), + }) + } +} + +impl ToString for PPA { + fn to_string(&self) -> String { format!("{}/{}", self.owner, self.package) } +} + +#[derive(Debug, PartialEq, Eq, EnumString, Display)] +#[strum(serialize_all = "lowercase")] +pub enum RepologyStatus { + Newest, + Devel, + Unique, + Outdated, + Legacy, + Rolling, + NoScheme, + Incorrect, + Untrusted, + Ignored, +} + +#[derive(Debug, PartialEq, Eq, Display)] +#[strum(serialize_all = "lowercase")] +pub enum RepologyFilter { + Project(String), + Repo(String), + SubRepo(String), + Name(String), + SrcName(String), + BinName(String), + VisibleName(String), + Version(String), + OrigVersion(String), + Status(RepologyStatus), + Summary(String), +} + +impl RepologyFilter { + #[allow(clippy::too_many_lines)] + fn new( + repology_filter: &str, + field_node: &Node, + value_node: &Node, + ) -> Result { + let split: Vec<&str> = repology_filter.split(':').collect(); + + if split.len() != 2 { + return Err(FieldError { + field_label: "Must contain the repology filter in the format: `filter: value`" + .into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte() + 1, + value_node.end_byte() - value_node.start_byte() - 2, + ) + .into(), + help: "Add the repology filter in proper format. Example: `project: emacs`".into(), + }); + } + + // Verify the filter is properly formatted + if split[0].chars().any(char::is_whitespace) { + return Err(FieldError { + field_label: "Filter must not contain whitespaces".into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: (value_node.start_byte() + 1, split[0].len()).into(), + help: format!( + "Maybe you meant this instead: `{}`", + split[0].replace(' ', "") + ), + }); + } + + // Verify that the value is properly formatted + if !split[1].starts_with(' ') { + return Err(FieldError { + field_label: "Value must start with a space".into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: (value_node.start_byte() + split[0].len() + 2, 1).into(), + help: format!("Use this: `{}: {}`", split[0], split[1].trim()), + }); + } + + let Some(value) = split[1].get(1..) else { + return Err(FieldError { + field_label: "Value cannot be empty".into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: (value_node.start_byte() + split[0].len() + 2, 1).into(), + help: "Add the repology filter in proper format. Example: `project: emacs`".into(), + }); + }; + + let value = value.to_owned(); + + if value.trim().is_empty() { + return Err(FieldError { + field_label: "Value cannot be empty".into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte() + split[0].len() + 2, + value.len() + 1, + ) + .into(), + help: "Add the repology filter in proper format. Example: `project: emacs`".into(), + }); + } + + if value.chars().any(char::is_whitespace) { + return Err(FieldError { + field_label: "Value must not contain whitespaces".into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: (value_node.start_byte() + split[0].len() + 2, split[1].len()).into(), + help: format!( + "Use this: `{}: {}`", + split[0], + split[1] + .chars() + .filter(|c| c.is_whitespace()) + .collect::() + ), + }); + } + + let filter = match split[0] { + "project" => Self::Project(value), + "repo" => Self::Repo(value), + "subrepo" => Self::SubRepo(value), + "name" => Self::Name(value), + "srcname" => Self::SrcName(value), + "binname" => Self::BinName(value), + "visiblename" => Self::VisibleName(value), + "version" => Self::Version(value), + "origversion" => Self::OrigVersion(value), + "status" => Self::Status(match split[1].parse() { + Ok(status) => status, + Err(_) => { + return Err(FieldError { + field_label: "Invalid status".into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: (value_node.start_byte() + split[0].len() + 2, split[1].len()) + .into(), + help: "Use one of `newest`, `devel`, `unique`, `outdated`, `legacy`, \ + `rolling`, `noscheme`, `incorrect`, `untrusted`, `ignored`" + .into(), + }) + }, + }), + "summary" => Self::Summary(value), + _ => { + return Err(FieldError { + field_label: "Invalid filter".into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: (value_node.start_byte() + 1, split[0].len()).into(), + help: "Use one of `project`, `repo`, `subrepo`, `name`, `srcname`, `binname`, \ + `visiblename`, `version`, `origversion`, `status`, `summary`" + .to_owned(), + }); + }, + }; + + Ok(filter) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum GitFragment { + Branch(String), + Commit(String), + Tag(String), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum GitSource { + File(PathBuf), + HTTPS(String), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum SourceLink { + HTTPS(String), + Git { + source_type: GitSource, + fragment: Option, + query_signed: bool, + }, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Source { + pub name: Option, + pub link: SourceLink, + pub repology: bool, +} + +impl Source { + #[allow(clippy::too_many_lines)] + fn new(source: &str, field_node: &Node, value_node: &Node) -> Result { + let field_span: SourceSpan = ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(); + let split: Vec<&str> = source.split("::").collect(); + + let mut raw_repology = None; + let mut name = None; + let mut link = String::new(); + let mut repology = false; + match split.len() { + 1 => { + link = split[0].to_owned(); + }, + 2 => { + if split[0].contains("://") { + link = split[0].to_owned(); + raw_repology = Some(split[1]); + } else { + name = Some(split[0].to_owned()); + link = split[1].to_owned(); + } + }, + 3 => { + name = Some(split[0].to_owned()); + link = split[1].to_owned(); + raw_repology = Some(split[2]); + + repology = true; + }, + _ => todo!(), + }; + + // Repology checks + if let Some(raw_repology) = raw_repology { + if raw_repology != "repology" { + if raw_repology.chars().any(char::is_whitespace) { + let whitespace_characters = raw_repology + .chars() + .skip_while(|c| !c.is_whitespace()) + .take_while(|c| c.is_whitespace()) + .count(); + + let characters_until_whitespaces = raw_repology + .chars() + .take_while(|c| !c.is_whitespace()) + .count(); + + return Err(FieldError { + field_label: "Invalid whitespaces".into(), + field_span, + error_span: ( + value_node.start_byte() + + ((source.len() - raw_repology.len()) + 1) + + characters_until_whitespaces, + whitespace_characters, + ) + .into(), + help: format!( + "Remove the invalid whitespaces. You probably meant this instead: \ + `{}::{}::repology`", + split[0], split[1] + ), + }); + } + + return Err(FieldError { + field_label: if raw_repology.is_empty() { + "Missing repology key".into() + } else { + "Invalid key".into() + }, + field_span, + error_span: ( + value_node.start_byte() + (source.len() - raw_repology.len() + 1), + raw_repology.len(), + ) + .into(), + help: format!( + "Maybe you meant to use the repology key, like this: `{}::{}::repology`", + split[0], split[1] + ), + }); + } + repology = true; + } + + // Link checks + let whitespace_characters = link + .chars() + .skip_while(|c| !c.is_whitespace()) + .take_while(|c| c.is_whitespace()) + .count(); + + if whitespace_characters > 0 { + let characters_until_whitespaces = + link.chars().take_while(|c| !c.is_whitespace()).count(); + + return Err(FieldError { + field_label: "Invalid whitespaces".into(), + field_span, + error_span: ( + value_node.start_byte() + + 1 + + name.map_or(0, |name| name.len() + 2) + + characters_until_whitespaces, + whitespace_characters, + ) + .into(), + help: format!( + "Remove the invalid whitespaces. You probably meant this instead: `{}`", + link.chars() + .filter(|c| !c.is_whitespace()) + .collect::() + ), + }); + } + + let protocol_split = link.split("://").collect::>(); + + if protocol_split.len() != 2 { + return Err(FieldError { + field_label: "No protocol specified".into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte(), + value_node.end_byte() - value_node.start_byte(), + ) + .into(), + help: "Use one of `https`, `git`, `magnet`, `ftp`".into(), + }); + } + + let (protocol, link_without_protocol) = (protocol_split[0], protocol_split[1]); + + let link = link_without_protocol + .find(['#', '?']) + .map_or(link_without_protocol, |i| &link_without_protocol[..i]); + + let protocol = match protocol { + "https" => SourceLink::HTTPS(link.to_owned()), + git if git.starts_with("git") => SourceLink::Git { + source_type: { + let split: Vec<_> = protocol.split('+').collect(); + + if split.len() != 2 { + return Err(FieldError { + field_label: "No git protocol + specified" + .into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte(), + value_node.end_byte() - value_node.start_byte(), + ) + .into(), + help: "Specify a git protocol like: + `git+https` or `git+file`" + .into(), + }); + } + + match split[1] { + "https" => GitSource::HTTPS(link.to_owned()), + "file" => { + let repo_dir = PathBuf::from(link); + if !repo_dir.exists() { + todo!("Repository doesn't exist"); + } + if !repo_dir.is_dir() { + todo!("Repository isn't a directory"); + } + GitSource::File(repo_dir) + }, + _ => { + return Err(FieldError { + field_label: "Invalid git + protocol" + .into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte(), + value_node.end_byte() - value_node.start_byte(), + ) + .into(), + help: "Specify a git protocol like: + `git+https` or `git+file`" + .into(), + }); + }, + } + }, + fragment: { + match link_without_protocol.matches('#').count() { + 2.. => todo!("Invalid number of #"), + 1 => { + let fragment = &link_without_protocol + .get( + link_without_protocol.find('#').unwrap() + ..link_without_protocol + .find('?') + .unwrap_or(link_without_protocol.len() - 1), + ) + .unwrap_or_else(|| todo!("Invalid sequence, ? before #")); + + let split: Vec<&str> = fragment.split('=').collect(); + + if split.len() > 2 { + todo!("Invalid number of ="); + } + + let (fragment_type, value) = (&split[0][1..], split[1].to_owned()); + + match fragment_type { + "branch" => Some(GitFragment::Branch(value)), + "tag" => Some(GitFragment::Tag(value)), + "commit" => Some(GitFragment::Commit(value)), + _ => todo!("Invalid fragment"), + } + }, + 0 => None, + _ => unreachable!("Broke math"), + } + }, + query_signed: { + match link_without_protocol.matches('?').count() { + 2.. => todo!("Invalid number of ?"), + 1 => { + let query = + &link_without_protocol[link_without_protocol.find('?').unwrap() + 1 + ..=std::cmp::max( + link_without_protocol.find('#').unwrap_or(0), + link_without_protocol.len() - 1, + )]; + + match query { + "signed" => true, + _ => todo!("Invalid query"), + } + }, + 0 => false, + _ => unreachable!("Broke math"), + } + }, + }, + _ => { + return Err(FieldError { + field_label: "Invalid protocol".into(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte(), + value_node.end_byte() - value_node.start_byte(), + ) + .into(), + help: "Specify a git protocol like: `https`, `git+https`, `git+file`, \ + `magnet` or `ftp`" + .into(), + }); + }, + }; + + match &protocol { + SourceLink::Git { + source_type: GitSource::File(_), + fragment: _, + query_signed: _, + } => {}, + _ => { + if !Regex::new( + r"(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)", + ) + .unwrap() + .is_match(link_without_protocol) + { + todo!("Invalid URL SIR"); + } + }, + } + + Ok(Self { + repology, + name, + link: protocol, + }) + } +} + +#[derive(Debug, PartialEq)] +pub struct PacBuild { + pub pkgname: Vec, + pub pkgver: PkgverType, + pub epoch: Option, + pub pkgdesc: Option, + pub url: Option, + pub license: Option, + pub custom_variables: Option>, + + pub arch: Vec, + pub maintainer: Option>, + pub noextract: Option>, + pub sha256sums: Option>>>, + pub sha348sums: Option>>>, + pub sha512sums: Option>>>, + pub b2sums: Option>>>, + pub depends: Option>, + pub optdepends: Option>, + pub ppa: Option>, + pub repology: Option>, + pub sources: Vec, + + pub prepare: Option, + pub build: Option, + pub check: Option, + pub package: Option, + pub pre_install: Option, + pub post_install: Option, + pub pre_upgrade: Option, + pub post_upgrade: Option, + pub pre_remove: Option, + pub post_remove: Option, + pub custom_functions: Option>, +} + +impl PacBuild { + fn cleanup_rawstring(raw_string: &str) -> &str { + let len = raw_string.len(); + if len <= 2 { + "" + } else { + &raw_string[1..len - 1] + } + } + + #[allow(clippy::too_many_lines)] + pub fn from_source(source_code: &str) -> Result { + let mut pkgname: Option> = None; + let mut pkgver: Option = None; + let mut epoch: Option = None; + let mut pkgdesc: Option = None; + let mut url: Option = None; + let mut license: Option = None; + let mut custom_variables: Option> = None; + + let mut arch: Option> = None; + let mut maintainer: Option> = None; + let mut noextract: Option> = None; + let mut sha256sums: Option>>> = None; + let mut sha348sums: Option>>> = None; + let mut sha512sums: Option>>> = None; + let mut b2sums: Option>>> = None; + let mut depends: Option> = None; + let mut optdepends: Option> = None; + let mut ppa: Option> = None; + let mut repology: Option> = None; + let mut sources: Option> = None; + + let mut prepare: Option = None; + let mut build: Option = None; + let mut check: Option = None; + let mut package: Option = None; + let mut pre_install: Option = None; + let mut post_install: Option = None; + let mut pre_upgrade: Option = None; + let mut post_upgrade: Option = None; + let mut pre_remove: Option = None; + let mut post_remove: Option = None; + let mut custom_functions: Option> = None; + + #[allow(clippy::similar_names)] + let sourced_code = Command::new("env") + .args([ + "-i", + "bash", + "-c", + &format!(r#"VARS="$(compgen -v)"; {source_code}; VARS="`grep -vFe "$VARS" <<< "$(compgen -v)"|grep -v ^VARS |grep -v ^PIPESTATUS`"; declare -p $VARS|cut -d " " -f 3-; declare -f"#), + ]) + .output() + .unwrap(); + + // dbg!(String::from_utf8(sourced_code.stderr.clone()).unwrap()); + + let mut errors: Vec = vec![]; + + if !sourced_code.status.success() { + errors.push(Report::new_boxed(Box::new(BadSyntax {}))); + } + + let mut parser = Parser::new(); + + parser.set_language(tree_sitter_bash::language()).unwrap(); + + let Some(tree) = parser.parse(sourced_code.stdout.clone(), None) else { { + return Err(ParseError { + input: source_code.into(), + related: vec![Report::new_boxed(Box::new(BadSyntax {}))], + }) + } }; + + let mut query = QueryCursor::new(); + + for (query_match, index) in query.captures( + &Query::new( + tree_sitter_bash::language(), + "(program (variable_assignment + name: (variable_name) @variable_name + value: [ + (array (concatenation ( + (word) + (word) + [(raw_string) (string) (word)] @assoc_array + ))) + [(raw_string) (string) (word)] @value])) + (function_definition + name: (word) @function_name + ) @function", + ) + .unwrap(), + tree.root_node(), + |_| sourced_code.stdout.clone(), + ) { + if index == 1 { + for capture in query_match.captures { + match capture.index { + // Variable name + 0 => { + let field_node = query_match.captures[0].node; + let name = field_node.utf8_text(&sourced_code.stdout).unwrap(); + + let index = query_match.captures[1].index; + + match index { + // It's a normal variable. + 2 => { + let value_node = query_match.captures[1].node; + let value = Self::cleanup_rawstring( + value_node.utf8_text(&sourced_code.stdout).unwrap(), + ); + + match name { + "pkgname" => { + match Pkgname::new(value, &field_node, &value_node) { + Ok(name) => pkgname = Some(vec![name]), + Err(error) => { + errors.push(Report::new_boxed(Box::new(error))); + }, + }; + }, + "pkgver" => { + match Pkgver::new(value, &field_node, &value_node) { + Ok(ver) => pkgver = Some(PkgverType::Variable(ver)), + Err(error) => { + errors.push(Report::new_boxed(Box::new(error))); + }, + }; + }, + "epoch" => { + match value.parse() { + Ok(value) => epoch = Some(value), + Err(_error) => errors.push(Report::new_boxed( + Box::new(FieldError { + field_label: "Can only be a non-negative \ + integer" + .to_owned(), + field_span: ( + field_node.start_byte(), + field_node.end_byte() + - field_node.start_byte(), + ) + .into(), + error_span: ( + value_node.start_byte() + 1, + value_node.end_byte() + - value_node.start_byte() + - 2, + ) + .into(), + help: "Use a non-negative epoch".into(), + }), + )), + }; + }, + "pkgdesc" => { + pkgdesc = Some(value.into()); + }, + "license" => { + match Expression::parse(value) + .into_diagnostic() + .context("Invalid license field") + { + Ok(expr) => license = Some(expr), + Err(error) => errors.push(error), + }; + }, + "url" => { + url = Some(value.into()); + }, + _ => match &mut custom_variables { + Some(custom_variables) => { + custom_variables.insert(name.into(), value.into()); + }, + None => { + custom_variables = Some(HashMap::from([( + name.into(), + value.into(), + )])); + }, + }, + } + }, + // Array + 1 => { + let value_node = query_match.captures[1].node; + let value = Self::cleanup_rawstring( + value_node.utf8_text(&sourced_code.stdout).unwrap(), + ); + + match name { + "arch" => match &mut arch { + Some(arch) => arch.push(value.into()), + None => arch = Some(vec![value.into()]), + }, + "maintainer" => match &mut maintainer { + Some(maintainer_vec) => match Maintainer::new( + value, + &field_node, + &value_node, + ) { + Ok(maintainer) => maintainer_vec.push(maintainer), + Err(error) => { + errors.push(Report::new_boxed(Box::new(error))); + }, + }, + None => { + match Maintainer::new( + value, + &field_node, + &value_node, + ) { + Ok(a_maintainer) => { + maintainer = Some(vec![a_maintainer]); + }, + Err(error) => errors + .push(Report::new_boxed(Box::new(error))), + }; + }, + }, + "noextract" => match &mut noextract { + Some(noextract) => noextract.push(value.into()), + None => noextract = Some(vec![value.into()]), + }, + shasum if shasum.starts_with("sha256sums") => { + let checksum_arch = + shasum.strip_prefix("sha256sums_").unwrap_or("any"); + + match &mut sha256sums { + Some(sha256sums) => { + match sha256sums.get_mut(checksum_arch) { + Some(hashes) => { + hashes.push(if value == "SKIP" { + None + } else { + Some(value.into()) + }); + }, + None => { + sha256sums.insert( + checksum_arch.into(), + vec![if value == "SKIP" { + None + } else { + Some(value.into()) + }], + ); + }, + }; + }, + None => { + sha256sums = Some(HashMap::from([( + checksum_arch.into(), + vec![if value == "SKIP" { + None + } else { + Some(value.into()) + }], + )])); + }, + } + }, + shasum if shasum.starts_with("sha348sums") => { + let checksum_arch = + shasum.strip_prefix("sha348sums_").unwrap_or("any"); + + match &mut sha348sums { + Some(sha348sums) => { + match sha348sums.get_mut(checksum_arch) { + Some(hashes) => { + hashes.push(if value == "SKIP" { + None + } else { + Some(value.into()) + }); + }, + None => { + sha348sums.insert( + checksum_arch.into(), + vec![if value == "SKIP" { + None + } else { + Some(value.into()) + }], + ); + }, + }; + }, + None => { + sha348sums = Some(HashMap::from([( + checksum_arch.into(), + vec![if value == "SKIP" { + None + } else { + Some(value.into()) + }], + )])); + }, + } + }, + shasum if shasum.starts_with("sha512sums") => { + let checksum_arch = + shasum.strip_prefix("sha512sums_").unwrap_or("any"); + + match &mut sha512sums { + Some(sha512sums) => { + match sha512sums.get_mut(checksum_arch) { + Some(hashes) => { + hashes.push(if value == "SKIP" { + None + } else { + Some(value.into()) + }); + }, + None => { + sha512sums.insert( + checksum_arch.into(), + vec![if value == "SKIP" { + None + } else { + Some(value.into()) + }], + ); + }, + }; + }, + None => { + sha512sums = Some(HashMap::from([( + checksum_arch.into(), + vec![if value == "SKIP" { + None + } else { + Some(value.into()) + }], + )])); + }, + } + }, + shasum if shasum.starts_with("b2sums") => { + let checksum_arch = + shasum.strip_prefix("b2sums_").unwrap_or("any"); + + match &mut b2sums { + Some(b2sums) => { + match b2sums.get_mut(checksum_arch) { + Some(hashes) => { + hashes.push(if value == "SKIP" { + None + } else { + Some(value.into()) + }); + }, + None => { + b2sums.insert( + checksum_arch.into(), + vec![if value == "SKIP" { + None + } else { + Some(value.into()) + }], + ); + }, + }; + }, + None => { + b2sums = Some(HashMap::from([( + checksum_arch.into(), + vec![if value == "SKIP" { + None + } else { + Some(value.into()) + }], + )])); + }, + } + }, + "depends" => match &mut depends { + Some(depends_vec) => { + match Dependency::new( + value, + &field_node, + &value_node, + ) { + Ok(dependency) => depends_vec.push(dependency), + Err(error) => errors + .push(Report::new_boxed(Box::new(error))), + } + }, + None => { + match Dependency::new( + value, + &field_node, + &value_node, + ) { + Ok(dependency) => { + depends = Some(vec![dependency]); + }, + Err(error) => errors + .push(Report::new_boxed(Box::new(error))), + } + }, + }, + "optdepends" => match &mut optdepends { + Some(optdepends_vec) => { + match OptionalDependency::new( + value, + &field_node, + &value_node, + ) { + Ok(optional_dependency) => { + optdepends_vec.push(optional_dependency); + }, + Err(error) => errors + .push(Report::new_boxed(Box::new(error))), + } + }, + None => { + match OptionalDependency::new( + value, + &field_node, + &value_node, + ) { + Ok(optional_dependency) => { + optdepends = + Some(vec![optional_dependency]); + }, + Err(error) => errors + .push(Report::new_boxed(Box::new(error))), + } + }, + }, + "ppa" => match &mut ppa { + Some(ppa_vec) => { + match PPA::new(value, &field_node, &value_node) { + Ok(ppa) => ppa_vec.push(ppa), + Err(error) => errors + .push(Report::new_boxed(Box::new(error))), + } + }, + None => { + match PPA::new(value, &field_node, &value_node) { + Ok(a_ppa) => ppa = Some(vec![a_ppa]), + Err(error) => errors + .push(Report::new_boxed(Box::new(error))), + }; + }, + }, + "repology" => match &mut repology { + Some(repology_vec) => { + match RepologyFilter::new( + value, + &field_node, + &value_node, + ) { + Ok(repology_filter) => { + repology_vec.push(repology_filter); + }, + Err(error) => errors + .push(Report::new_boxed(Box::new(error))), + } + }, + None => { + match RepologyFilter::new( + value, + &field_node, + &value_node, + ) { + Ok(repology_filter) => { + repology = Some(vec![repology_filter]); + }, + Err(error) => errors + .push(Report::new_boxed(Box::new(error))), + }; + }, + }, + "sources" => match &mut sources { + Some(sources_vec) => { + match Source::new(value, &field_node, &value_node) { + Ok(source) => sources_vec.push(source), + Err(error) => errors + .push(Report::new_boxed(Box::new(error))), + } + }, + None => { + match Source::new(value, &field_node, &value_node) { + Ok(source) => sources = Some(vec![source]), + Err(error) => errors + .push(Report::new_boxed(Box::new(error))), + }; + }, + }, + _ => {}, + } + }, + _ => {}, + } + }, + // Function definition + 4 => { + let name = query_match.captures[1] + .node + .utf8_text(&sourced_code.stdout) + .unwrap(); + let function = query_match.captures[0] + .node + .utf8_text(&sourced_code.stdout) + .unwrap(); + + match name { + "prepare" => prepare = Some(function.into()), + "build" => build = Some(function.into()), + "check" => check = Some(function.into()), + "package" => package = Some(function.into()), + "pre_install" => pre_install = Some(function.into()), + "post_install" => post_install = Some(function.into()), + "pre_upgrade" => pre_upgrade = Some(function.into()), + "post_upgrade" => post_upgrade = Some(function.into()), + "pre_remove" => pre_remove = Some(function.into()), + "post_remove" => post_remove = Some(function.into()), + _ => match &mut custom_functions { + Some(custom_functions) => { + custom_functions.insert(name.into(), function.into()); + }, + None => { + custom_functions = + Some(HashMap::from([(name.into(), function.into())])); + }, + }, + }; + }, + _ => {}, + }; + } + } + } + + if !errors.is_empty() { + return Err(ParseError { + input: String::from_utf8(sourced_code.stdout).unwrap(), + related: errors, + }); + } + + let Some(pkgname) = pkgname else { + return Err(ParseError { + input: String::from_utf8(sourced_code.stdout).unwrap(), + related: { + errors.push(Report::new_boxed(Box::new(MissingField { + label: "pkgname is missing", + }))); + errors + }, + }); + }; + + let Some(pkgver) = pkgver else { + return Err(ParseError { + input: String::from_utf8(sourced_code.stdout).unwrap(), + related: { + errors.push(Report::new_boxed(Box::new(MissingField { + label: "pkgver is missing", + }))); + errors + }, + }); + }; + + let Some(arch) = arch else { + return Err(ParseError { + input: String::from_utf8(sourced_code.stdout).unwrap(), + related: { + errors.push(Report::new_boxed(Box::new(MissingField { + label: "pkgver is missing", + }))); + errors + }, + }); + }; + + let Some(sources) = sources else { + return Err(ParseError { + input: String::from_utf8(sourced_code.stdout).unwrap(), + related: { errors.push(Report::new_boxed(Box::new(MissingField { label: "source is missing"}))); errors}, + }); + }; + + // TODO: Possibly check if checksum and sources lengths match + + let pkgbuild = Self { + pkgname, + pkgver, + epoch, + pkgdesc, + url, + license, + custom_variables, + arch, + maintainer, + noextract, + sha256sums, + sha348sums, + sha512sums, + b2sums, + depends, + optdepends, + ppa, + repology, + sources, + prepare, + build, + check, + package, + pre_install, + post_install, + pre_upgrade, + post_upgrade, + pre_remove, + post_remove, + custom_functions, + }; + + Ok(pkgbuild) + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use super::*; + + proptest! { + #[test] + fn test_pkgname(name in r#"[a-z0-9@_+]+[a-z0-9@._+-]+"#) { + let mut parser = Parser::new(); + parser.set_language(tree_sitter_bash::language()).unwrap(); + let tree = parser.parse(b"test", None).unwrap(); + let parent = tree.root_node(); + + let pkgname = Pkgname::new(&name, &parent, &parent).unwrap(); + assert_eq!(pkgname.0, name); + } + + #[test] + fn test_invalid_pkgname(name in r"[.-][^a-z0-9@._+-]+") { + let mut parser = Parser::new(); + parser.set_language(tree_sitter_bash::language()).unwrap(); + let tree = parser.parse(b"test", None).unwrap(); + let parent = tree.root_node(); + + let result = Pkgname::new(&name, &parent, &parent); + assert!(result.is_err()); + } + + #[test] + fn test_pkgver(version in r"[a-zA-Z0-9._]+") { + let mut parser = Parser::new(); + + parser.set_language(tree_sitter_bash::language()).unwrap(); + let tree = parser.parse(b"test", None).unwrap(); + let parent = tree.root_node(); + + let pkgver = Pkgver::new(&version, &parent, &parent).unwrap(); + assert_eq!(pkgver.0, version); + } + + #[test] + fn test_invalid_pkgver(version in r"[^a-zA-Z0-9._]") { + let mut parser = Parser::new(); + parser.set_language(tree_sitter_bash::language()).unwrap(); + let tree = parser.parse(b"test", None).unwrap(); + let parent = tree.root_node(); + + let result = Pkgver::new(&version, &parent, &parent); + assert!(result.is_err()); + } + + #[test] + fn test_dependency(name in r#"[\x00-\x7F&&[^:]]+"#, version_req in r#"(?:(>=|<=|>|<|=|\^|~))?((0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})(?:-((?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:, (?:(>=|<=|>|<|=|\^|~))?((0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})(?:-((?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)){0,31}"#) { + let mut parser = Parser::new(); + parser.set_language(tree_sitter_bash::language()).unwrap(); + let tree = parser.parse(b"test", None).unwrap(); + let parent = tree.root_node(); + + let dependency_without_version_req = Dependency::new(&name, &parent, &parent).unwrap(); + assert_eq!(dependency_without_version_req.name, name); + assert_eq!(dependency_without_version_req.version_req, None); + + let dependency_without_version_req = Dependency::new(&(name.clone() + ": " + &version_req), &parent, &parent).unwrap(); + assert_eq!(dependency_without_version_req.name, name); + assert_eq!(dependency_without_version_req.version_req, Some(VersionReq::parse(&version_req).unwrap())); + } + + + // #[test] + // fn test_invalid_dependency(name in r#"[^\x00-\x7F]*"#, version_req in r#"[^(?:(>=|<=|>|<|=|\^|~))?((0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})(?:-((?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:, (?:(>=|<=|>|<|=|\^|~))?((0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})(?:-((?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)){0,31}]"#) { + // let mut parser = Parser::new(); + // parser.set_language(tree_sitter_bash::language()).unwrap(); + // let tree = parser.parse(b"test", None).unwrap(); + // let parent = tree.root_node(); + + // assert!(Dependency::new(&name, &parent, &parent).is_err()); + + // let dependency_without_version_req = Dependency::new(&(name.to_owned() + ": " + &version_req), &parent, &parent).unwrap(); + // assert_eq!(dependency_without_version_req.name, name); + // assert_eq!(dependency_without_version_req.version_req, Some(VersionReq::parse(&version_req).unwrap())); + // } + + // #[test] + // fn test_maintainer(name in r"\S.*\S", email in r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+") { + // let mut parser = Parser::new(); + // parser.set_language(tree_sitter_bash::language()).unwrap(); + // let tree = parser.parse(b"test", None).unwrap(); + // let parent = tree.root_node(); + + + // let maintainer_without_email = Maintainer::new(&name, &parent, &parent).unwrap(); + // assert_eq!(maintainer_without_email.name, name); + // } + + #[test] + fn test_repology(value in r"[^\s:]+") { + let mut parser = Parser::new(); + parser.set_language(tree_sitter_bash::language()).unwrap(); + let tree = parser.parse(b"test", None).unwrap(); + let parent = tree.root_node(); + + let repology_filter = RepologyFilter::new(&format!("name: {value}"), &parent, &parent).unwrap(); + if let RepologyFilter::Name(name) = repology_filter { + assert_eq!(name, value); + } + } + + + #[test] + fn test_invalid_repology(value in r"[\s:]*") { + let mut parser = Parser::new(); + parser.set_language(tree_sitter_bash::language()).unwrap(); + let tree = parser.parse(b"test", None).unwrap(); + let parent = tree.root_node(); + + assert!(RepologyFilter::new(&format!("name:{value}"), &parent, &parent).is_err()); + assert!(RepologyFilter::new(&format!("name: {value}"), &parent, &parent).is_err()); + } + + } +} + +// // #[rstest] +// // #[case("-package12@._+-")] +// // #[case(".package12@._+-")] +// // #[case("Package12@._+-")] +// // #[should_panic] +// // fn invalid_pkgnames(#[case] test_case: &str) { +// // Pkgname::new(test_case).unwrap(); +// // } + +// // #[test] +// // fn valid_pkgname() { +// // Pkgname::new("package12@._+-").unwrap(); +// // } + +// // #[test] +// // #[allow(clippy::too_many_lines)] +// // fn test_parser() { +// // let source_code = r#"pkgname='potato' # can also be an array, +// probably shouldn't be though // pkgver='1.0.0' # this is the variable +// pkgver, can also be a function that will return dynamic version // +// epoch=' 0' # force package to be seen as newer no matter what // +// pkgdesc='Pretty obvious' // url='https://potato.com' +// // license="GPL-3.0-or-later WITH Classpath-exception-2.0 OR MIT AND AAL" +// // arch=('any' 'x86_64') +// // maintainer=('Henryws ' 'wizard-28 +// ') // repology=([project]="$pkgname") +// // provides=('foo' 'bar') +// // mascot="ferris" +// // source=( +// // "https://potato.com/$pkgver.tar.gz" +// // "potato.tar.gz::https://potato.com/$pkgver.tar.gz" # with a forced download name +// // "$pkgname::git+https://github.com/pacstall/pacstall" # git repo +// // "$pkgname::https://github.com/pacstall/pacstall/releases/download/2.0.1/pacstall-2.0.1.deb::repology" # use changelog with repology +// // "$pkgname::git+https://github.com/pacstall/pacstall#branch=master" # git repo with branch +// // "$pkgname::git+file://home/henry/pacstall/pacstall" # local git repo +// // "magnet://xt=urn:btih:c4769d7000244e4cae9c054a83e38f168bf4f69f& +// dn=archlinux-2022.09.03-x86_64.iso" # magnet link // "ftp://ftp.gnu.org/gnu/$pkgname/$pkgname-$pkgver.tar.xz" # ftp +// // "patch-me-harder.patch::https://potato.com/patch-me.patch" +// // ) # also source_x86_64=(), source_i386=() + +// // noextract=( +// // "$pkgver.tar.gz" +// // ) + +// // sha256sums=( +// // "any_sha256sum_1" +// // 'SKIP' +// // 'SKIP' +// // ) + +// // sha256sums_x86_64=( +// // "x86_64_sha256sum_1" +// // "SKIP" +// // "x86_64_sha256sum_2" +// // "SKIP" +// // ) + +// // sha256sums_aarch64=( +// // "aarch64_sha256sum_1" +// // ) + +// // sha348sums=( +// // "sha348sum_1" +// // "SKIP" +// // ) + +// // sha512sums=( +// // "sha512sum_1" +// // "SKIP" +// // ) + +// // b2sums=( +// // "b2sum_1" +// // "SKIP" +// // ) + +// // optdepends=( +// // 'same as pacstall: yes' +// // ) # rince and repeat optdepends_$arch=() + +// // depends=( +// // 'hashbrowns>=1.8.0' +// // 'mashed-potatos<=1.9.0' +// // 'gravy=2.3.0' +// // 'applesauce>3.0.0' +// // 'chicken<2.0.0' +// // 'libappleslices.so' +// // 'libdeepfryer.so=3' +// // ) + +// // makedepends=( +// // 'whisk' +// // 'onions' +// // ) + +// // checkdepends=( +// // 'customer_satisfaction' +// // ) + +// // ppa=('mcdonalds/ppa') + +// // provides=( +// // 'mashed-potatos' +// // 'aaaaaaaaaaaaaaaaaaaaaaaaaa' +// // ) + +// // conflicts=( +// // 'KFC' +// // 'potato_rights' +// // ) # can also do conflicts_$arch=() + +// // replaces=( +// // 'kidney_beans' +// // ) + +// // backup=( +// // 'etc/potato/prepare.conf' +// // ) + +// // options=( +// // '!strip' +// // '!docs' +// // 'etc' +// // ) + +// // groups=('potato-clan') + +// // prepare() { +// // cd "$pkgname-$pkgver" +// // patch -p1 -i "$srcdir/patch-me-harder.patch" +// // } + +// // func() { +// // true +// // } + +// // build() { +// // cd "$pkgname-$pkgver" +// // ./configure --prefix=/usr +// // make +// // } + +// // check() { +// // cd "$pkgname-$pkgver" +// // make -k check +// // } + +// // package() { +// // cd "$pkgname-$pkgver" +// // make DESTDIR="$pkgdir/" install +// // } + +// // pre_install() { +// // echo "potato" +// // } + +// // post_install() { +// // echo "potato" +// // } + +// // pre_upgrade() { +// // echo "potato" +// // } + +// // post_upgrade() { +// // echo "potato" +// // } + +// // pre_remove() { +// // echo "potato" +// // } + +// // post_remove() { +// // echo "potato" +// // }"# +// // .trim(); + +// // let pacbuild = PacBuild::from_source(source_code).unwrap(); + +// // assert_eq!(pacbuild.pkgname, +// vec![Pkgname::new("potato").unwrap()]); // assert_eq!( +// // pacbuild.pkgver, +// // PkgverType::Variable(Pkgver::new("1.0.0").unwrap()) +// // ); +// // assert_eq!(pacbuild.epoch, Some(0)); +// // assert_eq!(pacbuild.pkgdesc, Some("Pretty obvious".into())); +// // assert_eq!(pacbuild.url, Some("https://potato.com".into())); +// // assert_eq!( +// // pacbuild.license, +// // Some( +// // Expression::parse("GPL-3.0-or-later WITH +// Classpath-exception-2.0 OR MIT AND AAL") // .unwrap() +// // ) +// // ); +// // assert_eq!( +// // pacbuild.custom_variables, +// // Some(HashMap::from([("mascot".into(), "ferris".into())])) +// // ); + +// // assert_eq!(pacbuild.arch, vec!["any", "x86_64"]); +// // assert_eq!( +// // pacbuild.maintainer, +// // Some(vec![ +// // Maintainer { +// // name: "Henryws".into(), +// // email: Some("hwengerstickel@pm.me".into()) +// // }, +// // Maintainer { +// // name: "wizard-28".into(), +// // email: Some("wiz28@pm.me".into()) +// // } +// // ]) +// // ); +// // assert_eq!(pacbuild.noextract, +// Some(vec!["1.0.0.tar.gz".into()])); // assert_eq!( +// // pacbuild.sha256sums, +// // Some(HashMap::from([ +// // ( +// // "any".into(), +// // vec![Some("any_sha256sum_1".into()), None, None] +// // ), +// // ( +// // "x86_64".into(), +// // vec![ +// // Some("x86_64_sha256sum_1".into()), +// // None, +// // Some("x86_64_sha256sum_2".into()), +// // None +// // ] +// // ), +// // ("aarch64".into(), +// vec![Some("aarch64_sha256sum_1".into())]) // ])) +// // ); +// // assert_eq!( +// // pacbuild.sha348sums, +// // Some(HashMap::from([( +// // "any".into(), +// // vec![Some("sha348sum_1".into()), None] +// // )])) +// // ); +// // assert_eq!( +// // pacbuild.sha512sums, +// // Some(HashMap::from([( +// // "any".into(), +// // vec![Some("sha512sum_1".into()), None] +// // )])) +// // ); +// // assert_eq!( +// // pacbuild.b2sums, +// // Some(HashMap::from([( +// // "any".into(), +// // vec![Some("b2sum_1".into()), None] +// // )])) +// // ); +// // assert_eq!( +// // pacbuild.prepare, +// // Some( +// // "prepare () \n{ \n cd \"$pkgname-$pkgver\";\n +// patch -p1 -i \ // \"$srcdir/patch-me-harder.patch\"\n}" +// // .into() +// // ) +// // ); +// // assert_eq!( +// // pacbuild.build, +// // Some( +// // "build () \n{ \n cd \"$pkgname-$pkgver\";\n +// ./configure --prefix=/usr;\n \ // make\n}" +// // .into() +// // ) +// // ); +// // assert_eq!( +// // pacbuild.check, +// // Some("check () \n{ \n cd \"$pkgname-$pkgver\";\n +// make -k check\n}".into()) // ); +// // // pub package: Option, +// // // pub pre_install: Option, +// // // pub post_install: Option, +// // // pub pre_upgrade: Option, +// // // pub post_upgrade: Option, +// // // pub pre_remove: Option, +// // // pub post_remove: Option, +// // // pub custom_functions: Option>, +// // } +// }