kellnr-registry 5.14.1

Kellnr is a self-hosted registry for Rust crates with support for rustdocs and crates.io caching.
Documentation
use std::sync::Arc;

use axum::body::{Body, Bytes};
use axum::extract::FromRequest;
use axum::http::Request;
use kellnr_appstate::AppStateData;
use kellnr_common::publish_metadata::PublishMetadata;
use kellnr_error::api_error::ApiError;
use kellnr_settings::constants::MIN_BODY_CRATE_AND_DOC_BYTES;
use serde::Deserialize;

use crate::registry_error::RegistryError;

#[derive(Debug, Deserialize)]
pub struct EmptyCrateData {
    pub name: String,
}

#[derive(Debug, PartialEq, Eq)]
pub struct PubData {
    pub metadata_length: u32,
    pub metadata: PublishMetadata,
    pub crate_length: u32,
    pub cratedata: Arc<[u8]>,
}

fn convert_raw_metadata_to_string(raw_data: &[u8]) -> Result<String, RegistryError> {
    Ok(String::from_utf8((raw_data).to_vec())?)
}

fn deserialize_metadata(raw_data: &[u8]) -> Result<PublishMetadata, RegistryError> {
    let metadata_string = convert_raw_metadata_to_string(raw_data)?;
    Ok(serde_json::from_str(&metadata_string)?)
}

fn convert_length(raw_data: &[u8]) -> Result<u32, RegistryError> {
    match TryInto::try_into(raw_data) {
        Ok(i) => Ok(u32::from_le_bytes(i)),
        Err(e) => Err(RegistryError::InvalidMetadataLength(e)),
    }
}

impl FromRequest<AppStateData, Body> for PubData {
    type Rejection = ApiError;

    async fn from_request(
        req: Request<Body>,
        state: &AppStateData,
    ) -> Result<Self, Self::Rejection> {
        let data_bytes: Vec<u8> = Bytes::from_request(req, state)
            .await
            .map_err(RegistryError::ExtractBytesFailed)?
            .to_vec();

        if data_bytes.len() < MIN_BODY_CRATE_AND_DOC_BYTES {
            return Err(RegistryError::InvalidMinLength(
                data_bytes.len(),
                MIN_BODY_CRATE_AND_DOC_BYTES,
            )
            .into());
        }

        let metadata_length = convert_length(&data_bytes[0..4])?;
        let metadata_end = 4 + (metadata_length as usize);

        if metadata_end >= data_bytes.len() {
            return Err(RegistryError::InvalidMetadataSize.into());
        }

        let metadata: PublishMetadata = deserialize_metadata(&data_bytes[4..metadata_end])?;
        let crate_length = convert_length(&data_bytes[metadata_end..(metadata_end + 4)])?;
        let crate_end = metadata_end + 4 + (crate_length as usize);
        let cratedata = Arc::from(data_bytes[metadata_end + 4..crate_end].to_vec());

        let pub_data = PubData {
            metadata_length,
            metadata,
            crate_length,
            cratedata,
        };

        Ok(pub_data)
    }
}

#[cfg(test)]
mod bin_tests {
    use std::convert::TryFrom;
    use std::path::Path;

    use kellnr_common::original_name::OriginalName;
    use kellnr_common::publish_metadata::PublishMetadata;
    use kellnr_common::version::Version;
    use kellnr_settings::Settings;
    use kellnr_storage::cached_crate_storage::DynStorage;
    use kellnr_storage::fs_storage::FSStorage;
    use kellnr_storage::kellnr_crate_storage::KellnrCrateStorage;

    use crate::pub_data::PubData;

    struct TestData {
        settings: Settings,
        crate_storage: KellnrCrateStorage,
    }

    impl Drop for TestData {
        fn drop(&mut self) {
            self.clean();
        }
    }

    impl TestData {
        fn from(data_dir: &str) -> TestData {
            let settings = Settings {
                registry: kellnr_settings::Registry {
                    data_dir: data_dir.to_owned(),
                    session_age_seconds: 60,
                    ..kellnr_settings::Registry::default()
                },
                setup: kellnr_settings::Setup {
                    admin_pwd: String::new(),
                    ..kellnr_settings::Setup::default()
                },
                ..Settings::default()
            };
            let storage = Box::new(FSStorage::new(&settings.crates_path()).unwrap()) as DynStorage;
            let crate_storage = KellnrCrateStorage::new(&settings, storage);
            TestData {
                settings,
                crate_storage,
            }
        }
        fn clean(&self) {
            rm_rf::ensure_removed(&self.settings.registry.data_dir)
                .expect("Cannot remove test bin directory.");
        }
    }

    #[tokio::test]
    async fn add_crate_binary() {
        let pub_data = PubData {
            crate_length: 5,
            cratedata: vec![0x00, 0x11, 0x22, 0x33, 0x44].into(),
            metadata_length: 0,
            metadata: PublishMetadata::minimal("test", "0.1.0"),
        };

        let test_storage = TestData::from("test_add_crate_binary");
        let name = OriginalName::try_from("test").unwrap();
        let version = Version::try_from("0.1.0").unwrap();
        let result = test_storage
            .crate_storage
            .put(&name, &version, pub_data.cratedata.clone())
            .await;

        let get_res = test_storage
            .crate_storage
            .get(&name, &version)
            .await
            .expect("Couldn't find file...");

        assert!(result.is_ok());
        assert_eq!(vec![0x00, 0x11, 0x22, 0x33, 0x44], get_res);

        test_storage.clean();
    }

    #[tokio::test]
    async fn add_crate_binary_with_upper_case_name() {
        let pub_data = PubData {
            crate_length: 5,
            cratedata: vec![0x00, 0x11, 0x22, 0x33, 0x44].into(),
            metadata_length: 0,
            metadata: PublishMetadata::minimal("Test_Add_crate_binary_Upper-Case", "0.1.0"),
        };

        let test_storage = TestData::from("Test_Add_crate_binary_Upper-Case");
        let name = OriginalName::try_from(pub_data.metadata.name).unwrap();
        let version = Version::try_from("0.1.0").unwrap();
        let result = test_storage
            .crate_storage
            .put(&name, &version, pub_data.cratedata.clone())
            .await;

        let get_res = test_storage
            .crate_storage
            .get(&name, &version)
            .await
            .expect("Couldn't find file...");

        assert!(result.is_ok());

        assert_eq!(vec![0x00, 0x11, 0x22, 0x33, 0x44], get_res);
        test_storage.clean();
    }

    #[tokio::test]
    async fn add_duplicate_crate_binary() {
        let pub_data = PubData {
            crate_length: 5,
            cratedata: vec![0x00, 0x11, 0x22, 0x33, 0x44].into(),
            metadata_length: 0,
            metadata: PublishMetadata::minimal("test", "0.1.0"),
        };

        let test_bin = TestData::from("test_add_duplicate_crate_binary");
        let name = OriginalName::try_from("test").unwrap();
        let version = Version::try_from("0.1.0").unwrap();

        let _ = test_bin
            .crate_storage
            .put(&name, &version, pub_data.cratedata.clone())
            .await;
        let result = test_bin
            .crate_storage
            .put(&name, &version, pub_data.cratedata.clone())
            .await;

        assert!(result.is_err());
        assert_eq!(
            "Crate with version already exists: test-0.1.0",
            result.unwrap_err().to_string()
        );
        test_bin.clean();
    }

    #[tokio::test]
    async fn create_rand_doc_queue_path() {
        let test_bin = TestData::from("test_doc_queue");

        let rand_path = test_bin
            .crate_storage
            .create_rand_doc_queue_path()
            .await
            .unwrap();

        assert!(rand_path.exists());
        assert!(
            rand_path.starts_with(
                test_bin
                    .crate_storage
                    .doc_queue_path
                    .to_string_lossy()
                    .to_string()
            )
        );
        test_bin.clean();
    }

    #[tokio::test]
    async fn delete_crate() {
        let pub_data = PubData {
            crate_length: 5,
            cratedata: vec![0x00, 0x11, 0x22, 0x33, 0x44].into(),
            metadata_length: 0,
            metadata: PublishMetadata::minimal("test", "0.1.0"),
        };
        let test_storage = TestData::from("delete_crate");
        let name = OriginalName::try_from("test").unwrap();
        let version = Version::try_from("0.1.0").unwrap();
        test_storage
            .crate_storage
            .put(&name, &version, pub_data.cratedata.clone())
            .await
            .unwrap();
        let crate_path = Path::new(&test_storage.settings.bin_path()).join("test-0.1.0.crate");

        test_storage
            .crate_storage
            .delete(&name, &version)
            .await
            .unwrap();

        assert!(!crate_path.exists());
        test_storage.clean();
    }

    #[tokio::test]
    async fn delete_crate_invalidates_cache() {
        let pub_data = PubData {
            crate_length: 5,
            cratedata: vec![0x00, 0x11, 0x22, 0x33, 0x44].into(),
            metadata_length: 0,
            metadata: PublishMetadata::minimal("test", "0.2.0"),
        };

        let test_storage = TestData::from("delete_crate_invalidates_cache");
        let name = OriginalName::try_from("test").unwrap();
        let version = Version::try_from("0.2.0").unwrap();

        test_storage
            .crate_storage
            .put(&name, &version, pub_data.cratedata.clone())
            .await
            .unwrap();

        let crate_path = Path::new(&test_storage.settings.bin_path()).join("test-0.2.0.crate");
        let crate_path = crate_path.to_string_lossy().to_string();

        assert!(
            test_storage
                .crate_storage
                .get(&name, &version)
                .await
                .is_some()
        );
        assert!(test_storage.crate_storage.cache_has_path(&name, &version));

        test_storage
            .crate_storage
            .delete(&name, &version)
            .await
            .unwrap();

        assert!(!test_storage.crate_storage.cache_has_path(&name, &version));

        assert!(
            test_storage
                .crate_storage
                .get(&name, &version)
                .await
                .is_none()
        );

        assert!(!std::path::PathBuf::from(crate_path).exists());
        test_storage.clean();
    }
}