From a0416cf6a91d6b22260e7101aef1289cd235c2b7 Mon Sep 17 00:00:00 2001
From: Raine <raine@ixvd.net>
Date: Sat, 14 Oct 2023 22:39:40 +0200
Subject: [PATCH] feat: add package identifiers

---
 pkgr/Cargo.toml                |   6 +-
 pkgr/src/commands/mod.rs       |   8 +++
 pkgr/src/main.rs               |  57 +++++++++++++++++
 pkgr/src/package/identifier.rs | 114 +++++++++++++++++++++++++++++++++
 pkgr/src/package/mod.rs        |   1 +
 5 files changed, 185 insertions(+), 1 deletion(-)
 create mode 100644 pkgr/src/commands/mod.rs
 create mode 100644 pkgr/src/package/identifier.rs
 create mode 100644 pkgr/src/package/mod.rs

diff --git a/pkgr/Cargo.toml b/pkgr/Cargo.toml
index df095d2..92a0e91 100644
--- a/pkgr/Cargo.toml
+++ b/pkgr/Cargo.toml
@@ -6,4 +6,8 @@ edition = "2021"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-manifest = { path = "../manifest" }
\ No newline at end of file
+fern = "0.6.2"
+getopts = "0.2.21"
+log = "0.4.19"
+regex = "1.9.1"
+manifest = { path = "../manifest" }
diff --git a/pkgr/src/commands/mod.rs b/pkgr/src/commands/mod.rs
new file mode 100644
index 0000000..61e3fe8
--- /dev/null
+++ b/pkgr/src/commands/mod.rs
@@ -0,0 +1,8 @@
+use crate::package::identifier::PackageIdentifier;
+
+pub enum Command {
+    Grab(PackageIdentifier), // grab a package from a remote source
+    Remove(PackageIdentifier), // remove a package from the local source
+    List, // list all packages in the local source
+    Update,
+}
\ No newline at end of file
diff --git a/pkgr/src/main.rs b/pkgr/src/main.rs
index f79c691..8d75350 100644
--- a/pkgr/src/main.rs
+++ b/pkgr/src/main.rs
@@ -1,2 +1,59 @@
+use std::env;
+use std::process::exit;
+use std::str::FromStr;
+
+use getopts::Options;
+use log::{info, SetLoggerError};
+
+mod commands;
+mod package;
+
 fn main() {
+    setup_logger()
+        .expect("Unable to setup logger.");
+
+    let args: Vec<String> = env::args().collect();
+    let program = args[0].clone();
+
+    let mut opts = Options::new();
+    opts.optflag("h", "help", "Show help message.");
+    let matches = opts.parse(&args[1..]).unwrap_or_else(|e| panic!("{}", e.to_string()));
+
+    if matches.opt_present("h") {
+        print_usage(&program, opts);
+        exit(1);
+    }
+
+    let command = if !matches.free.is_empty() {
+        matches.free[0].clone()
+    } else {
+        print_usage(&program, opts);
+        exit(1);
+    };
+
+    info!("Identifier: {}", package::identifier::PackageIdentifier::from_str(&command).unwrap());
 }
+
+fn print_usage(program: &str, opts: Options) {
+    let mut brief = format!("Usage: {} <COMMAND> [options]\n\n", program);
+    brief += &*format!("Commands: grab \n");
+    brief += &*format!("          remove\n");
+    print!("{}", opts.usage(&brief.trim_end()));
+}
+
+fn setup_logger() -> Result<(), SetLoggerError> {
+    fern::Dispatch::new()
+        .format(|out, message, record| {
+            out.finish(format_args!(
+                "{} {}",
+                match record.level().to_string().chars().nth(0).unwrap_or('I') {
+                    'E' | 'W' => "!!",
+                    _ => "**",
+                },
+                message
+            ))
+        })
+        .level(log::LevelFilter::Info)
+        .chain(std::io::stdout())
+        .apply()
+}
\ No newline at end of file
diff --git a/pkgr/src/package/identifier.rs b/pkgr/src/package/identifier.rs
new file mode 100644
index 0000000..01f9f6b
--- /dev/null
+++ b/pkgr/src/package/identifier.rs
@@ -0,0 +1,114 @@
+use std::str::FromStr;
+
+use regex::Regex;
+
+#[derive(Debug)]
+pub enum PackageIdentifierError {
+    InvalidPackageLocator(String),
+    InvalidURI(String),
+}
+
+impl std::fmt::Display for PackageIdentifierError {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        match self {
+            PackageIdentifierError::InvalidPackageLocator(s) => write!(f, "Invalid package locator: {}", s),
+            PackageIdentifierError::InvalidURI(s) => write!(f, "Invalid URI: {}", s),
+        }
+    }
+}
+
+#[derive(Debug)]
+pub enum PackageIdentifier {
+    PackageLocator(PackageLocator),
+    URI(String),
+}
+
+impl std::fmt::Display for PackageIdentifier {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        match self {
+            PackageIdentifier::PackageLocator(pl) => write!(f, "{}", pl),
+            PackageIdentifier::URI(uri) => write!(f, "{}", uri),
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct PackageLocator {
+    pub name: String,
+    pub version: Option<u32>,
+    pub tags: Option<Vec<String>>,
+}
+
+impl std::fmt::Display for PackageLocator {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        let mut s = self.name.clone();
+        if let Some(version) = self.version {
+            s += &*format!("@{}", version);
+        }
+        if let Some(tags) = &self.tags {
+            s += &*format!(":{}", tags.join(","));
+        }
+        write!(f, "{}", s)
+    }
+}
+
+impl FromStr for PackageIdentifier {
+    type Err = PackageIdentifierError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let uri_re = regex::Regex::new(r"^[a-zA-Z0-9]+://").unwrap();
+        if uri_re.is_match(s) {
+            // there needs to be stuff after the protocol
+            let split = s.split("://").collect::<Vec<&str>>();
+            if split.len() != 2 || split[1].is_empty() {
+                return Err(PackageIdentifierError::InvalidURI(s.to_string()));
+            }
+            Ok(PackageIdentifier::URI(s.to_string()))
+        } else {
+            let pl = match PackageLocator::from_str(s) {
+                Ok(pl) => pl,
+                Err(e) => return Err(PackageIdentifierError::InvalidPackageLocator(e.to_string())),
+            };
+            Ok(PackageIdentifier::PackageLocator(pl))
+        }
+    }
+}
+
+impl FromStr for PackageLocator {
+    type Err = PackageIdentifierError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        #[allow(unused_assignments)] // false positive
+        let mut name = None;
+        let mut version = None;
+        let mut tags = None;
+
+        if s.is_empty() {
+            return Err(PackageIdentifierError::InvalidPackageLocator(s.to_string()));
+        }
+
+        let name_re = Regex::new("^([A-Za-z][A-Za-z0-9_.]+)").unwrap();
+        let version_re = Regex::new("@([0-9]+)").unwrap();
+        let tags_re = Regex::new(":([a-zA-Z0-9,._]+)").unwrap();
+
+        if let Some(caps) = name_re.captures(s) {
+            name = Some(caps.get(1).unwrap().as_str().to_string());
+        } else {
+            return Err(PackageIdentifierError::InvalidPackageLocator(s.to_string()));
+        }
+
+        if let Some(caps) = version_re.captures(s) {
+            version = Some(caps.get(1).unwrap().as_str().parse::<u32>().unwrap());
+        }
+
+        if let Some(caps) = tags_re.captures(s) {
+            tags = Some(caps.get(1).unwrap().as_str().split(",").map(|s| s.to_string()).collect());
+        }
+
+        Ok(PackageLocator {
+            name: name.unwrap(),
+            version,
+            tags,
+        })
+    }
+}
\ No newline at end of file
diff --git a/pkgr/src/package/mod.rs b/pkgr/src/package/mod.rs
new file mode 100644
index 0000000..8f83cb4
--- /dev/null
+++ b/pkgr/src/package/mod.rs
@@ -0,0 +1 @@
+pub mod identifier;
\ No newline at end of file