-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit af5af73
Showing
10 changed files
with
516 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/target | ||
Cargo.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
version = "Two" | ||
use_field_init_shorthand = true | ||
merge_imports = true | ||
wrap_comments = true | ||
use_try_shorthand = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "cargo-registry" | ||
version = "0.1.0" | ||
authors = ["Daniel Eades <[email protected]>"] | ||
edition = "2018" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
serde = {version = "1.0.104", features =["derive"] } | ||
url = { version = "2.1.1", features = ["serde"] } | ||
semver = { version = "0.9.0", features = ["serde"] } | ||
serde_json = "1.0.45" | ||
async-std = "1.4.0" | ||
thiserror = "1.0.10" | ||
|
||
[dev-dependencies] | ||
test-case = "1.0.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
use serde::{Deserialize, Serialize}; | ||
use std::fmt; | ||
use url::Url; | ||
|
||
#[derive(Serialize, Deserialize)] | ||
pub struct Config { | ||
dl: Url, | ||
|
||
#[serde(skip_serializing_if = "Option::is_none")] | ||
api: Option<Url>, | ||
|
||
#[serde(skip_serializing_if = "Vec::is_empty")] | ||
allowed_registries: Vec<Url>, | ||
} | ||
|
||
impl Config { | ||
pub fn new(crate_download: Url) -> Self { | ||
Self { | ||
dl: crate_download, | ||
api: None, | ||
allowed_registries: Vec::default(), | ||
} | ||
} | ||
} | ||
|
||
impl fmt::Display for Config { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
write!(f, "{}", &serde_json::to_string_pretty(self).unwrap()) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::Config; | ||
use url::Url; | ||
|
||
#[test] | ||
fn new() { | ||
let url = Url::parse("https://crates.io/api/v1/crates/{crate}/{version}/download") | ||
.expect("URL is invalid!"); | ||
|
||
let _ = Config::new(url); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
pub enum Error { | ||
Io(std::io::Error), | ||
} | ||
|
||
impl From<std::io::Error> for Error { | ||
fn from(e: std::io::Error) -> Self { | ||
Self::Io(e) | ||
} | ||
} | ||
|
||
pub type Result<T> = std::result::Result<T, Error>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
use super::{validate::ValidationError, Config, Metadata}; | ||
use async_std::{ | ||
fs::File, | ||
io::prelude::WriteExt, | ||
path::{Path, PathBuf}, | ||
}; | ||
use std::io; | ||
use thiserror::Error; | ||
use url::Url; | ||
|
||
mod index_file; | ||
use index_file::IndexFile; | ||
|
||
pub struct Index { | ||
root: PathBuf, | ||
config: Config, | ||
} | ||
|
||
impl Index { | ||
/// Create a new `Index`. | ||
/// | ||
/// # Parameters | ||
/// | ||
/// - *root*: The path on the filesystem at which the root of the index is | ||
/// located | ||
/// - *download*- This is the URL for downloading crates listed in the | ||
/// index. The value may have the markers {crate} and {version} which are | ||
/// replaced with the name and version of the crate to download. If the | ||
/// markers are not present, then the value /{crate}/{version}/download is | ||
/// appended to the end. | ||
/// | ||
/// This method does not touch the filesystem. use [`init()`](Index::init) | ||
/// to initialise the index in the filesystem. | ||
pub fn new(root: impl Into<PathBuf>, download: Url) -> Self { | ||
let root = root.into(); | ||
let config = Config::new(download); | ||
Self { root, config } | ||
} | ||
|
||
/// Initialise an index at the root path. | ||
/// | ||
/// # Example | ||
/// ```no_run | ||
/// use cargo_registry::{Index, Url}; | ||
/// # async { | ||
/// let root = "/index"; | ||
/// let download_url = Url::parse("https://crates.io/api/v1/crates/").unwrap(); | ||
/// | ||
/// let index = Index::new(root, download_url); | ||
/// index.init().await?; | ||
/// # Ok::<(), std::io::Error>(()) | ||
/// # }; | ||
/// ``` | ||
pub async fn init(&self) -> io::Result<()> { | ||
async_std::fs::DirBuilder::new() | ||
.recursive(true) | ||
.create(&self.root) | ||
.await?; | ||
let mut file = File::create(&self.root.join("config.json")).await?; | ||
file.write_all(self.config.to_string().as_bytes()).await?; | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Insert crate ['Metadata'] into the index. | ||
/// | ||
/// # Errors | ||
/// | ||
/// This method can fail if the metadata is deemed to be invalid, or if the | ||
/// filesystem cannot be written to. | ||
pub async fn insert(&self, crate_metadata: Metadata) -> Result<(), IndexError> { | ||
// get the full path to the index file | ||
let path = self.get_path(crate_metadata.name()); | ||
|
||
// create the parent directories to the file | ||
create_parents(&path).await?; | ||
|
||
// open the index file for editing | ||
let mut file = IndexFile::open(&path).await?; | ||
|
||
// insert the new metadata | ||
file.insert(crate_metadata).await?; | ||
|
||
Ok(()) | ||
} | ||
|
||
fn get_path(&self, name: &str) -> PathBuf { | ||
let stem = get_path(name); | ||
self.root.join(stem) | ||
} | ||
} | ||
|
||
#[derive(Debug, Error)] | ||
pub enum IndexError { | ||
#[error("Validation Error")] | ||
Validation(#[from] ValidationError), | ||
|
||
#[error("IO Error")] | ||
Io(#[from] io::Error), | ||
} | ||
|
||
fn get_path(name: &str) -> PathBuf { | ||
let mut path = PathBuf::new(); | ||
|
||
let name_lowercase = name.to_ascii_lowercase(); | ||
|
||
match name.len() { | ||
1 => { | ||
path.push("1"); | ||
path.push(name); | ||
path | ||
} | ||
2 => { | ||
path.push("2"); | ||
path.push(name); | ||
path | ||
} | ||
3 => { | ||
path.push("3"); | ||
path.push(&name_lowercase[0..1]); | ||
path.push(name); | ||
path | ||
} | ||
_ => { | ||
path.push(&name_lowercase[0..2]); | ||
path.push(&name_lowercase[2..4]); | ||
path.push(name); | ||
path | ||
} | ||
} | ||
} | ||
|
||
async fn create_parents(path: &Path) -> io::Result<()> { | ||
async_std::fs::DirBuilder::new() | ||
.recursive(true) | ||
.create(path.parent().unwrap()) | ||
.await | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
|
||
use super::Metadata; | ||
use test_case::test_case; | ||
|
||
#[test] | ||
fn deserialize() { | ||
let example1 = r#" | ||
{ | ||
"name": "foo", | ||
"vers": "0.1.0", | ||
"deps": [ | ||
{ | ||
"name": "rand", | ||
"req": "^0.6", | ||
"features": ["i128_support"], | ||
"optional": false, | ||
"default_features": true, | ||
"target": null, | ||
"kind": "normal", | ||
"registry": null, | ||
"package": null | ||
} | ||
], | ||
"cksum": "d867001db0e2b6e0496f9fac96930e2d42233ecd3ca0413e0753d4c7695d289c", | ||
"features": { | ||
"extras": ["rand/simd_support"] | ||
}, | ||
"yanked": false, | ||
"links": null | ||
} | ||
"#; | ||
|
||
let _: Metadata = serde_json::from_str(example1).unwrap(); | ||
|
||
let example2 = r#" | ||
{ | ||
"name": "my_serde", | ||
"vers": "1.0.11", | ||
"deps": [ | ||
{ | ||
"name": "serde", | ||
"req": "^1.0", | ||
"registry": "https://github.com/rust-lang/crates.io-index", | ||
"features": [], | ||
"optional": true, | ||
"default_features": true, | ||
"target": null, | ||
"kind": "normal" | ||
} | ||
], | ||
"cksum": "f7726f29ddf9731b17ff113c461e362c381d9d69433f79de4f3dd572488823e9", | ||
"features": { | ||
"default": [ | ||
"std" | ||
], | ||
"derive": [ | ||
"serde_derive" | ||
], | ||
"std": [ | ||
] | ||
}, | ||
"yanked": false | ||
} | ||
"#; | ||
|
||
let _: Metadata = serde_json::from_str(example2).unwrap(); | ||
} | ||
|
||
#[test_case("x" => "1/x" ; "one-letter crate name")] | ||
#[test_case("xx" => "2/xx" ; "two-letter crate name")] | ||
#[test_case("xxx" =>"3/x/xxx" ; "three-letter crate name")] | ||
#[test_case("abcd" => "ab/cd/abcd" ; "four-letter crate name")] | ||
#[test_case("abcde" => "ab/cd/abcde" ; "five-letter crate name")] | ||
#[test_case("aBcD" => "ab/cd/aBcD" ; "mixed-case crate name")] | ||
fn get_path(name: &str) -> String { | ||
crate::index::get_path(name).to_str().unwrap().to_string() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
use super::{IndexError, Metadata}; | ||
use crate::validate::{validate_version, ValidationError}; | ||
use async_std::{ | ||
fs::{File, OpenOptions}, | ||
io::{ | ||
prelude::{BufReadExt, WriteExt}, | ||
BufReader, | ||
}, | ||
path::Path, | ||
stream::StreamExt, | ||
}; | ||
use semver::Version; | ||
use std::io; | ||
|
||
pub struct IndexFile { | ||
file: File, | ||
entries: Vec<Metadata>, | ||
} | ||
|
||
impl IndexFile { | ||
pub async fn open(path: &Path) -> io::Result<IndexFile> { | ||
let file = OpenOptions::new() | ||
.append(true) | ||
.create(true) | ||
.open(path) | ||
.await?; | ||
|
||
let mut lines = BufReader::new(&file).lines(); | ||
|
||
let mut entries = Vec::new(); | ||
|
||
while let Some(line) = lines.next().await { | ||
let metadata: Metadata = serde_json::from_str(&line?).expect("JSON encoding error"); | ||
entries.push(metadata); | ||
} | ||
|
||
Ok(Self { file, entries }) | ||
} | ||
|
||
pub async fn insert(&mut self, metadata: Metadata) -> std::result::Result<(), IndexError> { | ||
self.validate(&metadata)?; | ||
|
||
let mut string = metadata.to_string(); | ||
string.push('\r'); | ||
|
||
self.file.write_all(string.as_bytes()).await?; | ||
self.entries.push(metadata); | ||
Ok(()) | ||
} | ||
|
||
pub fn current_version(&self) -> Option<&Version> { | ||
self.entries.last().map(Metadata::version) | ||
} | ||
|
||
fn validate(&self, metadata: &Metadata) -> std::result::Result<(), ValidationError> { | ||
self.validate_version(metadata.version())?; | ||
Ok(()) | ||
} | ||
|
||
fn validate_version( | ||
&self, | ||
given_version: &Version, | ||
) -> std::result::Result<(), ValidationError> { | ||
if let Some(current_version) = self.current_version() { | ||
validate_version(current_version, given_version) | ||
} else { | ||
Ok(()) | ||
} | ||
} | ||
} | ||
|
||
impl<'a> IntoIterator for &'a IndexFile { | ||
type Item = &'a Metadata; | ||
type IntoIter = impl Iterator<Item = Self::Item> + 'a; | ||
fn into_iter(self) -> Self::IntoIter { | ||
self.entries.iter() | ||
} | ||
} | ||
|
||
impl IntoIterator for IndexFile { | ||
type Item = Metadata; | ||
type IntoIter = impl Iterator<Item = Self::Item>; | ||
fn into_iter(self) -> Self::IntoIter { | ||
self.entries.into_iter() | ||
} | ||
} |
Oops, something went wrong.