Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
danieleades committed Feb 5, 2020
0 parents commit af5af73
Show file tree
Hide file tree
Showing 10 changed files with 516 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
5 changes: 5 additions & 0 deletions .rustfmt.toml
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
18 changes: 18 additions & 0 deletions Cargo.toml
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"
44 changes: 44 additions & 0 deletions src/config.rs
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);
}
}
11 changes: 11 additions & 0 deletions src/error.rs
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>;
220 changes: 220 additions & 0 deletions src/index.rs
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()
}
}
86 changes: 86 additions & 0 deletions src/index/index_file.rs
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()
}
}
Loading

0 comments on commit af5af73

Please sign in to comment.