warehouse/src/new_crate.rs

254 lines
7.7 KiB
Rust

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<u8>,
}
#[async_trait]
impl FromRequest<Body> for NewCrateRequest {
type Rejection = Errors;
async fn from_request(req: &mut RequestParts<Body>) -> Result<Self, Self::Rejection> {
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::<NewCrateJsonData>(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<Dependency2>,
features: HashMap<String, Vec<String>>,
authors: Vec<String>,
description: Option<String>,
documentation: Option<String>,
homepage: Option<String>,
readme: Option<String>,
readme_file: Option<String>,
keywords: Vec<String>,
categories: Vec<String>,
license: Option<String>,
license_file: Option<String>,
repository: Option<String>,
badges: HashMap<String, HashMap<String, String>>,
links: Option<String>,
}
#[derive(Deserialize)]
struct Dependency2 {
#[serde(flatten)]
base: BaseDependency,
explicit_name_in_toml: Option<String>,
}
#[derive(Serialize)]
pub struct NewCrateResponse {
warnings: Warnings,
}
#[derive(Serialize)]
struct Warnings {
invalid_categories: Vec<String>,
invalid_badges: Vec<String>,
other: Vec<String>,
}
// FIXME: i'm not gonna touch this code for fear of breaking it, you can handle it later ig
#[allow(clippy::too_many_lines)]
pub async fn new_crate(
mut request: NewCrateRequest,
Auth(auth_user): Auth,
Extension(state): Extension<Arc<State>>,
) -> RespResult<Json<NewCrateResponse>> {
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::<Vec<_>>(),
)
.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, features, version_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
)
.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)
.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(),
},
}))
}