use crate::{ auth, db::{DbVersionFeature, PgU32}, db_error, index::{update_crate_from_db, BaseDependency}, internal_error, Auth, Errors, RespResult, State, }; use async_trait::async_trait; use axum::{ body::{Body, Bytes}, extract::{FromRequest, RequestParts}, Extension, Json, }; use semver::Version; use serde::{Deserialize, Serialize}; use sqlx::{query, query_as, types::Json as SqlxJson, Connection}; use std::{collections::HashMap, fmt::Write, sync::Arc}; use tokio::fs; pub struct NewCrateRequest { json_data: NewCrateJsonData, crate_file: Vec, } #[async_trait] impl FromRequest for NewCrateRequest { type Rejection = Errors; async fn from_request(req: &mut RequestParts) -> Result { let body = Bytes::from_request(req).await.unwrap(); let mut offset = 0; let json_len_bytes = body .get(offset..offset + 4) .ok_or_else(|| Errors::new("Not enough bytes when reading length of JSON data"))?; let json_len = u32::from_le_bytes(json_len_bytes.try_into().unwrap()) as usize; offset += 4; let json_bytes = body .get(offset..offset + json_len) .ok_or_else(|| Errors::new("Not enough bytes when reading JSON data"))?; let json_data = serde_json::from_slice::(json_bytes) .map_err(|e| Errors::new(format_args!("Error while parsing JSON data: {e}")))?; offset += json_len; let crate_len_bytes = body .get(offset..offset + 4) .ok_or_else(|| Errors::new("Not enough bytes when reading length of .crate file"))?; let crate_len = u32::from_le_bytes(crate_len_bytes.try_into().unwrap()) as usize; offset += 4; let crate_file = body .get(offset..offset + crate_len) .ok_or_else(|| Errors::new("Not enough bytes when reading .crate file"))? .to_vec(); offset += crate_len; if body.len() != offset { return Err(Errors::new("Too much data provided")); } Ok(Self { json_data, crate_file, }) } } #[derive(Deserialize)] struct NewCrateJsonData { name: String, vers: Version, deps: Vec, features: HashMap>, authors: Vec, description: Option, documentation: Option, homepage: Option, readme: Option, readme_file: Option, keywords: Vec, categories: Vec, license: Option, license_file: Option, repository: Option, badges: HashMap>, links: Option, } #[derive(Deserialize)] struct Dependency2 { #[serde(flatten)] base: BaseDependency, explicit_name_in_toml: Option, } #[derive(Serialize)] pub struct NewCrateResponse { warnings: Warnings, } #[derive(Serialize)] struct Warnings { invalid_categories: Vec, invalid_badges: Vec, other: Vec, } pub async fn new_crate( mut request: NewCrateRequest, Auth(auth_user): Auth, Extension(state): Extension>, ) -> RespResult> { if !request.json_data.name.is_ascii() { return Err(Errors::new("Crate name must be ASCII").into()); } request.json_data.name.make_ascii_lowercase(); let mut db = state.db.acquire().await.map_err(db_error)?; let crate_id = { let mut trans = db.begin().await.map_err(db_error)?; let (crate_id,): (PgU32,) = match query_as("SELECT id FROM crates WHERE name = $1 LIMIT 1") .bind(&request.json_data.name) .fetch_optional(&mut *trans) .await .map_err(db_error)? { Some(v) => { auth(&request.json_data.name, &auth_user, &state).await?; v } None => query_as( "INSERT INTO crates (name, publisher, owners) VALUES ($1, $2, $3) RETURNING id", ) .bind(&request.json_data.name) .bind(auth_user.id) .bind(&[auth_user.id][..]) .fetch_one(&mut *trans) .await .map_err(db_error)?, }; let hash = hmac_sha256::Hash::hash(&request.crate_file); let mut cksum = String::new(); for byte in hash { write!(cksum, "{byte:02x}").expect("Formatting to a String failed"); } let (version_id,): (PgU32,) = query_as( r"INSERT INTO versions ( vers, cksum, yanked, links, crate_id, features, authors, description, documentation, homepage, readme, readme_file, keywords, categories, license, license_file, repository, badges ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING id", ) .bind(&request.json_data.vers.to_string()) .bind(&cksum) .bind(false) .bind(&request.json_data.links) .bind(crate_id) .bind( &request .json_data .features .iter() .map(|v| DbVersionFeature { feature: v.0.clone(), enables: v.1.clone(), }) .collect::>(), ) .bind(&request.json_data.authors) .bind(&request.json_data.description) .bind(&request.json_data.documentation) .bind(&request.json_data.homepage) .bind(&request.json_data.readme) .bind(&request.json_data.readme_file) .bind(&request.json_data.keywords) .bind(&request.json_data.categories) .bind(&request.json_data.license) .bind(&request.json_data.license_file) .bind(&request.json_data.repository) .bind(SqlxJson(&request.json_data.badges)) .fetch_one(&mut *trans) .await .map_err(db_error)?; for dep in &request.json_data.deps { query( r"INSERT INTO deps ( name, version_req, optional, default_features, target, kind, registry, package, feature, version_id ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", ) .bind(&dep.base.name) .bind(&dep.base.version_req.to_string()) .bind(&dep.base.optional) .bind(&dep.base.default_features) .bind(&dep.base.target) .bind(&dep.base.kind.to_db_repr()) .bind(&dep.base.registry) .bind(&dep.explicit_name_in_toml) .bind(&dep.base.features) .bind(version_id) .execute(&mut *trans) .await .map_err(db_error)?; } trans.commit().await.map_err(db_error)?; crate_id }; update_crate_from_db( crate_id, &state, &format!( "Add version {} of '{}'", request.json_data.vers, request.json_data.name ), ) .await?; let mut path = state.crate_dir.clone(); path.push(&request.json_data.name); path.push(request.json_data.vers.to_string()); fs::create_dir_all(&path).await.map_err(internal_error)?; path.push("crate.crate"); fs::write(path, request.crate_file) .await .map_err(internal_error)?; Ok(Json(NewCrateResponse { warnings: Warnings { invalid_categories: Vec::new(), invalid_badges: Vec::new(), other: Vec::new(), }, })) }