Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / compiler-core / src / hex.rs
15 kB 453 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2021 The Gleam contributors 3 4use camino::Utf8Path; 5use clap::ValueEnum; 6use debug_ignore::DebugIgnore; 7use flate2::read::GzDecoder; 8use futures::future; 9use hexpm::{ApiError, WriteActionCredentials, version::Version}; 10use strum::{EnumString, VariantNames}; 11use tar::Archive; 12 13use crate::{ 14 Error, Result, 15 io::{FileSystemReader, FileSystemWriter, HttpClient, TarUnpacker}, 16 manifest::{ManifestPackage, ManifestPackageSource}, 17 paths::{self, ProjectPaths}, 18}; 19 20pub const HEXPM_PUBLIC_KEY: &[u8] = b"-----BEGIN PUBLIC KEY----- 21MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApqREcFDt5vV21JVe2QNB 22Edvzk6w36aNFhVGWN5toNJRjRJ6m4hIuG4KaXtDWVLjnvct6MYMfqhC79HAGwyF+ 23IqR6Q6a5bbFSsImgBJwz1oadoVKD6ZNetAuCIK84cjMrEFRkELtEIPNHblCzUkkM 243rS9+DPlnfG8hBvGi6tvQIuZmXGCxF/73hU0/MyGhbmEjIKRtG6b0sJYKelRLTPW 25XgK7s5pESgiwf2YC/2MGDXjAJfpfCd0RpLdvd4eRiXtVlE9qO9bND94E7PgQ/xqZ 26J1i2xWFndWa6nfFnRxZmCStCOZWYYPlaxr+FZceFbpMwzTNs4g3d4tLNUcbKAIH4 270wIDAQAB 28-----END PUBLIC KEY----- 29"; 30 31fn key_name(hostname: &str) -> String { 32 format!("gleam-{hostname}") 33} 34 35pub async fn publish_package<Http: HttpClient>( 36 release_tarball: Vec<u8>, 37 version: String, 38 name: &str, 39 api_key: &WriteActionCredentials, 40 config: &hexpm::Config, 41 replace: bool, 42 http: &Http, 43) -> Result<()> { 44 tracing::info!("Publishing package, replace: {}", replace); 45 let request = hexpm::api_publish_package_request(release_tarball, api_key, config, replace); 46 let response = http.send(request).await?; 47 hexpm::api_publish_package_response(response).map_err(|e| match e { 48 ApiError::NotReplacing => Error::HexPublishReplaceRequired { version }, 49 ApiError::Forbidden => Error::HexPublishAccessDenied { 50 name: name.into(), 51 version, 52 }, 53 ApiError::Json(_) 54 | ApiError::Io(_) 55 | ApiError::RateLimited 56 | ApiError::InvalidCredentials 57 | ApiError::UnexpectedResponse(..) 58 | ApiError::InvalidPackageNameFormat(_) 59 | ApiError::IncorrectPayloadSignature 60 | ApiError::InvalidProtobuf(_) 61 | ApiError::InvalidVersionFormat(_) 62 | ApiError::NotFound 63 | ApiError::InvalidVersionRequirementFormat(_) 64 | ApiError::IncorrectChecksum 65 | ApiError::OAuthTimeout 66 | ApiError::OAuthAccessDenied 67 | ApiError::ExpiredToken 68 | ApiError::OAuthRefreshTokenRejected 69 | ApiError::IncorrectOneTimePassword 70 | ApiError::LateDeletion 71 | ApiError::LateModification => Error::hex(e), 72 }) 73} 74 75pub async fn add_owner<Http: HttpClient>( 76 api_key: &WriteActionCredentials, 77 package_name: String, 78 new_owner_username_or_email: String, 79 level: hexpm::OwnerLevel, 80 config: &hexpm::Config, 81 http: &Http, 82) -> Result<()> { 83 tracing::info!( 84 "Adding {} as owner of `{}`", 85 new_owner_username_or_email, 86 package_name 87 ); 88 let request = hexpm::api_add_owner_request( 89 &package_name, 90 &new_owner_username_or_email, 91 level, 92 api_key, 93 config, 94 ); 95 let response = http.send(request).await?; 96 hexpm::api_add_owner_response(response).map_err(Error::hex) 97} 98 99pub async fn transfer_owner<Http: HttpClient>( 100 api_key: &WriteActionCredentials, 101 package_name: String, 102 new_owner_username_or_email: String, 103 config: &hexpm::Config, 104 http: &Http, 105) -> Result<()> { 106 tracing::info!( 107 "Transferring ownership of `{}` to {}", 108 package_name, 109 new_owner_username_or_email 110 ); 111 let request = hexpm::api_transfer_owner_request( 112 &package_name, 113 &new_owner_username_or_email, 114 api_key, 115 config, 116 ); 117 let response = http.send(request).await?; 118 hexpm::api_transfer_owner_response(response).map_err(Error::hex) 119} 120 121#[derive(Debug, EnumString, VariantNames, ValueEnum, Clone, Copy, PartialEq, Eq)] 122#[strum(serialize_all = "lowercase")] 123pub enum RetirementReason { 124 Other, 125 Invalid, 126 Security, 127 Deprecated, 128 Renamed, 129} 130 131impl RetirementReason { 132 pub fn to_library_enum(&self) -> hexpm::RetirementReason { 133 match self { 134 RetirementReason::Other => hexpm::RetirementReason::Other, 135 RetirementReason::Invalid => hexpm::RetirementReason::Invalid, 136 RetirementReason::Security => hexpm::RetirementReason::Security, 137 RetirementReason::Deprecated => hexpm::RetirementReason::Deprecated, 138 RetirementReason::Renamed => hexpm::RetirementReason::Renamed, 139 } 140 } 141} 142 143pub async fn retire_release<Http: HttpClient>( 144 package: &str, 145 version: &str, 146 reason: RetirementReason, 147 message: Option<&str>, 148 api_key: &WriteActionCredentials, 149 config: &hexpm::Config, 150 http: &Http, 151) -> Result<()> { 152 tracing::info!(package=%package, version=%version, "retiring_hex_release"); 153 let request = hexpm::api_retire_release_request( 154 package, 155 version, 156 reason.to_library_enum(), 157 message, 158 api_key, 159 config, 160 ); 161 let response = http.send(request).await?; 162 hexpm::api_retire_release_response(response).map_err(Error::hex) 163} 164 165pub async fn unretire_release<Http: HttpClient>( 166 package: &str, 167 version: &str, 168 api_key: &WriteActionCredentials, 169 config: &hexpm::Config, 170 http: &Http, 171) -> Result<()> { 172 tracing::info!(package=%package, version=%version, "retiring_hex_release"); 173 let request = hexpm::api_unretire_release_request(package, version, api_key, config); 174 let response = http.send(request).await?; 175 hexpm::api_unretire_release_response(response).map_err(Error::hex) 176} 177 178pub async fn remove_api_key<Http: HttpClient>( 179 hostname: &str, 180 config: &hexpm::Config, 181 auth_key: &WriteActionCredentials, 182 http: &Http, 183) -> Result<()> { 184 tracing::info!("Deleting API key from Hex"); 185 let request = hexpm::api_remove_api_key_request(&key_name(hostname), auth_key, config); 186 let response = http.send(request).await?; 187 hexpm::api_remove_api_key_response(response).map_err(Error::hex) 188} 189 190#[derive(Debug)] 191pub struct Downloader { 192 fs_reader: DebugIgnore<Box<dyn FileSystemReader>>, 193 fs_writer: DebugIgnore<Box<dyn FileSystemWriter>>, 194 http: DebugIgnore<Box<dyn HttpClient>>, 195 untar: DebugIgnore<Box<dyn TarUnpacker>>, 196 hex_config: hexpm::Config, 197 credentials: DebugIgnore<Option<hexpm::Credentials>>, 198 paths: ProjectPaths, 199} 200 201impl Downloader { 202 pub fn new( 203 fs_reader: Box<dyn FileSystemReader>, 204 fs_writer: Box<dyn FileSystemWriter>, 205 http: Box<dyn HttpClient>, 206 untar: Box<dyn TarUnpacker>, 207 credentials: Option<hexpm::Credentials>, 208 paths: ProjectPaths, 209 ) -> Self { 210 Self { 211 fs_reader: DebugIgnore(fs_reader), 212 fs_writer: DebugIgnore(fs_writer), 213 http: DebugIgnore(http), 214 untar: DebugIgnore(untar), 215 hex_config: hexpm::Config::new(), 216 credentials: DebugIgnore(credentials), 217 paths, 218 } 219 } 220 221 pub async fn ensure_package_downloaded( 222 &self, 223 package: &ManifestPackage, 224 ) -> Result<bool, Error> { 225 let outer_checksum = match &package.source { 226 ManifestPackageSource::Hex { outer_checksum } => outer_checksum, 227 ManifestPackageSource::Git { .. } | ManifestPackageSource::Local { .. } => { 228 panic!("Attempt to download non-hex package from hex") 229 } 230 }; 231 232 let tarball_path = paths::global_package_cache_package_tarball(outer_checksum); 233 if self.fs_reader.is_file(&tarball_path) { 234 tracing::info!( 235 package = package.name.as_str(), 236 version = %package.version, 237 "package_in_cache" 238 ); 239 return Ok(false); 240 } 241 tracing::info!( 242 package = &package.name.as_str(), 243 version = %package.version, 244 "downloading_package_to_cache" 245 ); 246 247 let request = hexpm::repository_get_package_tarball_request( 248 &package.name, 249 &package.version.to_string(), 250 self.credentials.as_ref(), 251 &self.hex_config, 252 ); 253 let response = self.http.send(request).await?; 254 255 let tarball = hexpm::repository_get_package_tarball_response(response, &outer_checksum.0) 256 .map_err(|error| Error::DownloadPackageError { 257 package_name: package.name.to_string(), 258 package_version: package.version.to_string(), 259 error: error.to_string(), 260 })?; 261 self.fs_writer.write_bytes(&tarball_path, &tarball)?; 262 Ok(true) 263 } 264 265 pub async fn ensure_package_in_build_directory( 266 &self, 267 package: &ManifestPackage, 268 ) -> Result<bool> { 269 let _ = self.ensure_package_downloaded(package).await?; 270 self.extract_package_from_cache(package) 271 } 272 273 // It would be really nice if this was async but the library is sync 274 pub fn extract_package_from_cache(&self, package: &ManifestPackage) -> Result<bool> { 275 let contents_path = Utf8Path::new("contents.tar.gz"); 276 let destination = self.paths.build_packages_package(&package.name); 277 278 let outer_checksum = match &package.source { 279 ManifestPackageSource::Hex { outer_checksum } => outer_checksum, 280 ManifestPackageSource::Git { .. } | ManifestPackageSource::Local { .. } => { 281 panic!("Attempt to download non-hex package from hex") 282 } 283 }; 284 285 // If the directory already exists then there's nothing for us to do 286 if self.fs_reader.is_directory(&destination) { 287 tracing::info!( 288 package = package.name.as_str(), 289 "Package already in build directory" 290 ); 291 return Ok(false); 292 } 293 294 tracing::info!(package = package.name.as_str(), "writing_package_to_target"); 295 let tarball = paths::global_package_cache_package_tarball(outer_checksum); 296 let reader = self.fs_reader.reader(&tarball)?; 297 let mut archive = Archive::new(reader); 298 299 // Find the source code from within the outer tarball 300 for entry in self.untar.entries(&mut archive)? { 301 let file = entry.map_err(Error::expand_tar)?; 302 303 let path = file.header().path().map_err(Error::expand_tar)?; 304 if path.as_ref() == contents_path { 305 // Expand this inner source code and write to the file system 306 let archive = Archive::new(GzDecoder::new(file)); 307 let result = self.untar.unpack(&destination, archive); 308 309 // If we failed to expand the tarball remove any source code 310 // that was partially written so that we don't mistakenly think 311 // the operation succeeded next time we run. 312 return match result { 313 Ok(()) => Ok(true), 314 Err(err) => { 315 self.fs_writer.delete_directory(&destination)?; 316 Err(err) 317 } 318 }; 319 } 320 } 321 322 Err(Error::ExpandTar { 323 error: "Unable to locate Hex package contents.tar.gz".into(), 324 }) 325 } 326 327 pub async fn download_hex_packages<'a, Packages: Iterator<Item = &'a ManifestPackage>>( 328 &self, 329 packages: Packages, 330 project_name: &str, 331 ) -> Result<()> { 332 let futures = packages 333 .filter(|package| project_name != package.name) 334 .map(|package| self.ensure_package_in_build_directory(package)); 335 336 // Run the futures to download the packages concurrently 337 let results = future::join_all(futures).await; 338 339 // Count the number of packages downloaded while checking for errors 340 for result in results { 341 let _ = result?; 342 } 343 Ok(()) 344 } 345} 346 347pub async fn publish_documentation<Http: HttpClient>( 348 name: &str, 349 version: &Version, 350 archive: Vec<u8>, 351 api_key: &WriteActionCredentials, 352 config: &hexpm::Config, 353 http: &Http, 354) -> Result<()> { 355 tracing::info!("publishing_documentation"); 356 let request = 357 hexpm::api_publish_docs_request(name, &version.to_string(), archive, api_key, config) 358 .map_err(Error::hex)?; 359 let response = http.send(request).await?; 360 hexpm::api_publish_docs_response(response).map_err(Error::hex) 361} 362 363pub async fn get_package_release<Http: HttpClient>( 364 name: &str, 365 version: &Version, 366 credentials: Option<&hexpm::Credentials>, 367 config: &hexpm::Config, 368 http: &Http, 369) -> Result<hexpm::Release<hexpm::ReleaseMeta>> { 370 let version = version.to_string(); 371 tracing::info!( 372 name = name, 373 version = version.as_str(), 374 "looking_up_package_release" 375 ); 376 let request = hexpm::api_get_package_release_request(name, &version, credentials, config); 377 let response = http.send(request).await?; 378 hexpm::api_get_package_release_response(response).map_err(Error::hex) 379} 380 381#[cfg(test)] 382mod tests { 383 use super::*; 384 use std::sync::Mutex; 385 386 /// A fake `HttpClient` that records the `authorization` header of the last 387 /// request it was sent and replies with a canned package release. 388 #[derive(Default)] 389 struct AuthorizationCapturingHttpClient { 390 last_authorization: Mutex<Option<String>>, 391 } 392 393 #[async_trait::async_trait] 394 impl HttpClient for AuthorizationCapturingHttpClient { 395 async fn send( 396 &self, 397 request: http::Request<Vec<u8>>, 398 ) -> Result<http::Response<Vec<u8>>, Error> { 399 let authorization = request 400 .headers() 401 .get("authorization") 402 .map(|value| value.to_str().unwrap().to_string()); 403 *self.last_authorization.lock().unwrap() = authorization; 404 405 let body = serde_json::json!({ 406 "version": "1.0.0", 407 "checksum": "960090c2fb391784bb34267b099dc9315cc1b1f6013e7415bc763cef1905d7d3", 408 "requirements": {}, 409 "meta": { "app": "gleam_stdlib", "build_tools": ["gleam"] } 410 }) 411 .to_string() 412 .into_bytes(); 413 414 Ok(http::Response::builder().status(200).body(body).unwrap()) 415 } 416 } 417 418 #[test] 419 fn get_package_release_authenticates_with_api_key() { 420 let http = AuthorizationCapturingHttpClient::default(); 421 let credentials = hexpm::Credentials::ApiKey("secret-key".into()); 422 423 let _ = futures::executor::block_on(get_package_release( 424 "gleam_stdlib", 425 &Version::new(1, 0, 0), 426 Some(&credentials), 427 &hexpm::Config::new(), 428 &http, 429 )) 430 .unwrap(); 431 432 assert_eq!( 433 http.last_authorization.lock().unwrap().as_deref(), 434 Some("secret-key") 435 ); 436 } 437 438 #[test] 439 fn get_package_release_is_anonymous_without_api_key() { 440 let http = AuthorizationCapturingHttpClient::default(); 441 442 let _ = futures::executor::block_on(get_package_release( 443 "gleam_stdlib", 444 &Version::new(1, 0, 0), 445 None, 446 &hexpm::Config::new(), 447 &http, 448 )) 449 .unwrap(); 450 451 assert_eq!(http.last_authorization.lock().unwrap().as_deref(), None); 452 } 453}