1mod proto;
2
3#[cfg(test)]
4mod tests;
5
6pub mod version;
7
8use crate::proto::{signed::Signed, versions::Versions};
9use bytes::buf::Buf;
10use flate2::read::GzDecoder;
11use http::{Method, StatusCode};
12use lazy_static::lazy_static;
13use prost::Message;
14use regex::Regex;
15use ring::digest::{Context, SHA256};
16use serde::Deserialize;
17use serde_json::json;
18use std::{
19 collections::HashMap,
20 convert::{TryFrom, TryInto},
21 fmt::Display,
22 io::{BufReader, Read},
23};
24use thiserror::Error;
25use version::{Range, Version};
26use x509_parser::prelude::FromDer;
27
28#[derive(Debug, Clone)]
29pub struct Config {
30 pub api_base: http::Uri,
32 pub repository_base: http::Uri,
34}
35
36impl Config {
37 pub fn new() -> Self {
38 Self {
39 api_base: http::Uri::from_static("https://hex.pm/api/"),
40 repository_base: http::Uri::from_static("https://repo.hex.pm/"),
41 }
42 }
43
44 fn api_request(
45 &self,
46 method: http::Method,
47 path_suffix: &str,
48 api_key: Option<&str>,
49 ) -> http::request::Builder {
50 make_request(self.api_base.clone(), method, path_suffix, api_key)
51 .header("content-type", "application/json")
52 .header("accept", "application/json")
53 }
54
55 fn repository_request(
56 &self,
57 method: http::Method,
58 path_suffix: &str,
59 api_key: Option<&str>,
60 ) -> http::request::Builder {
61 make_request(self.repository_base.clone(), method, path_suffix, api_key)
62 }
63}
64impl Default for Config {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70fn make_request(
71 base: http::Uri,
72 method: http::Method,
73 path_suffix: &str,
74 api_key: Option<&str>,
75) -> http::request::Builder {
76 let mut parts = base.into_parts();
77 parts.path_and_query = Some(
78 match parts.path_and_query {
79 Some(path) => format!("{}{}", path, path_suffix).try_into(),
80 None => path_suffix.try_into(),
81 }
82 .expect("api_uri path"),
83 );
84 let uri = http::Uri::from_parts(parts).expect("api_uri building");
85 let mut builder = http::Request::builder()
86 .method(method)
87 .uri(uri)
88 .header("user-agent", USER_AGENT);
89 if let Some(key) = api_key {
90 builder = builder.header("authorization", key);
91 }
92 builder
93}
94
95pub fn create_api_key_request(
103 username: &str,
104 password: &str,
105 key_name: &str,
106 config: &Config,
107) -> http::Request<Vec<u8>> {
108 let body = json!({
109 "name": key_name,
110 "permissions": [{
111 "domain": "api",
112 "resource": "write",
113 }],
114 });
115 let creds = http_auth_basic::Credentials::new(username, password).as_http_header();
116 config
117 .api_request(Method::POST, "keys", None)
118 .header("authorization", creds)
119 .body(body.to_string().into_bytes())
120 .expect("create_api_key_request request")
121}
122
123pub fn create_api_key_response(response: http::Response<Vec<u8>>) -> Result<String, ApiError> {
125 #[derive(Deserialize)]
126 struct Resp {
127 secret: String,
128 }
129 let (parts, body) = response.into_parts();
130 match parts.status {
131 StatusCode::CREATED => Ok(serde_json::from_slice::<Resp>(&body)?.secret),
132 StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited),
133 StatusCode::UNAUTHORIZED => Err(ApiError::InvalidCredentials),
134 status => Err(ApiError::unexpected_response(status, body)),
135 }
136}
137
138pub fn remove_api_key_request(
146 name_of_key_to_delete: &str,
147 api_key: &str,
148 config: &Config,
149) -> http::Request<Vec<u8>> {
150 config
151 .api_request(
152 Method::DELETE,
153 &format!("keys/{}", name_of_key_to_delete),
154 Some(api_key),
155 )
156 .body(vec![])
157 .expect("remove_api_key_request request")
158}
159
160pub fn remove_api_key_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> {
162 let (parts, body) = response.into_parts();
163 match parts.status {
164 StatusCode::NO_CONTENT | StatusCode::OK => Ok(()),
165 StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited),
166 StatusCode::UNAUTHORIZED => Err(ApiError::InvalidCredentials),
167 status => Err(ApiError::unexpected_response(status, body)),
168 }
169}
170
171pub fn retire_release_request(
179 package: &str,
180 version: &str,
181 reason: RetirementReason,
182 message: Option<&str>,
183 api_key: &str,
184 config: &Config,
185) -> http::Request<Vec<u8>> {
186 let body = json!({
187 "reason": reason.to_str(),
188 "message": message,
189 });
190 config
191 .api_request(
192 Method::POST,
193 &format!("packages/{}/releases/{}/retire", package, version),
194 Some(api_key),
195 )
196 .body(body.to_string().into_bytes())
197 .expect("retire_release_request request")
198}
199
200pub fn retire_release_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> {
202 let (parts, body) = response.into_parts();
203 match parts.status {
204 StatusCode::NO_CONTENT | StatusCode::OK => Ok(()),
205 StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited),
206 StatusCode::UNAUTHORIZED => Err(ApiError::InvalidCredentials),
207 status => Err(ApiError::unexpected_response(status, body)),
208 }
209}
210
211pub fn unretire_release_request(
219 package: &str,
220 version: &str,
221 api_key: &str,
222 config: &Config,
223) -> http::Request<Vec<u8>> {
224 config
225 .api_request(
226 Method::DELETE,
227 &format!("packages/{}/releases/{}/retire", package, version),
228 Some(api_key),
229 )
230 .body(vec![])
231 .expect("unretire_release_request request")
232}
233
234pub fn unretire_release_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> {
236 let (parts, body) = response.into_parts();
237 match parts.status {
238 StatusCode::NO_CONTENT | StatusCode::OK => Ok(()),
239 StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited),
240 StatusCode::UNAUTHORIZED => Err(ApiError::InvalidCredentials),
241 status => Err(ApiError::unexpected_response(status, body)),
242 }
243}
244
245pub fn get_repository_versions_request(
249 api_key: Option<&str>,
250 config: &Config,
251) -> http::Request<Vec<u8>> {
252 config
253 .repository_request(Method::GET, "versions", api_key)
254 .header("accept", "application/json")
255 .body(vec![])
256 .expect("get_repository_versions_request request")
257}
258
259pub fn get_repository_versions_response(
263 response: http::Response<Vec<u8>>,
264 public_key: &[u8],
265) -> Result<HashMap<String, Vec<Version>>, ApiError> {
266 let (parts, body) = response.into_parts();
267
268 match parts.status {
269 StatusCode::OK => (),
270 status => return Err(ApiError::unexpected_response(status, body)),
271 };
272
273 let mut decoder = GzDecoder::new(body.reader());
274 let mut body = Vec::new();
275 decoder.read_to_end(&mut body)?;
276
277 let signed = Signed::decode(body.as_slice())?;
278
279 let payload =
280 verify_payload(signed, public_key).map_err(|_| ApiError::IncorrectPayloadSignature)?;
281
282 let versions = Versions::decode(payload.as_slice())?
283 .packages
284 .into_iter()
285 .map(|n| {
286 let parse_version = |v: &str| {
287 let err = |_| ApiError::InvalidVersionFormat(v.to_string());
288 Version::parse(v).map_err(err)
289 };
290 let versions = n
291 .versions
292 .iter()
293 .map(|v| parse_version(v.as_str()))
294 .collect::<Result<Vec<Version>, ApiError>>()?;
295 Ok((n.name, versions))
296 })
297 .collect::<Result<HashMap<_, _>, ApiError>>()?;
298
299 Ok(versions)
300}
301
302pub fn get_package_request(
310 name: &str,
311 api_key: Option<&str>,
312 config: &Config,
313) -> http::Request<Vec<u8>> {
314 config
315 .repository_request(Method::GET, &format!("packages/{}", name), api_key)
316 .header("accept", "application/json")
317 .body(vec![])
318 .expect("get_package_request request")
319}
320
321pub fn get_package_response(
324 response: http::Response<Vec<u8>>,
325 public_key: &[u8],
326) -> Result<Package, ApiError> {
327 let (parts, body) = response.into_parts();
328
329 match parts.status {
330 StatusCode::OK => (),
331 StatusCode::FORBIDDEN => return Err(ApiError::NotFound),
332 StatusCode::NOT_FOUND => return Err(ApiError::NotFound),
333 status => {
334 return Err(ApiError::unexpected_response(status, body));
335 }
336 };
337
338 let mut decoder = GzDecoder::new(body.reader());
339 let mut body = Vec::new();
340 decoder.read_to_end(&mut body)?;
341
342 let signed = Signed::decode(body.as_slice())?;
343
344 let payload =
345 verify_payload(signed, public_key).map_err(|_| ApiError::IncorrectPayloadSignature)?;
346
347 let package = proto::package::Package::decode(payload.as_slice())?;
348 let releases = package
349 .releases
350 .clone()
351 .into_iter()
352 .map(proto_to_release)
353 .collect::<Result<Vec<_>, _>>()?;
354 let package = Package {
355 name: package.name,
356 repository: package.repository,
357 releases,
358 };
359
360 Ok(package)
361}
362
363pub fn get_package_tarball_request(
366 name: &str,
367 version: &str,
368 api_key: Option<&str>,
369 config: &Config,
370) -> http::Request<Vec<u8>> {
371 config
372 .repository_request(
373 Method::GET,
374 &format!("tarballs/{}-{}.tar", name, version),
375 api_key,
376 )
377 .header("accept", "application/x-tar")
378 .body(vec![])
379 .expect("get_package_tarball_request request")
380}
381
382pub fn get_package_tarball_response(
385 response: http::Response<Vec<u8>>,
386 checksum: &[u8],
387) -> Result<Vec<u8>, ApiError> {
388 let (parts, body) = response.into_parts();
389 match parts.status {
390 StatusCode::OK => (),
391 StatusCode::FORBIDDEN => return Err(ApiError::NotFound),
392 StatusCode::NOT_FOUND => return Err(ApiError::NotFound),
393 status => {
394 return Err(ApiError::unexpected_response(status, body));
395 }
396 };
397 let body = read_and_check_body(body.reader(), checksum)?;
398 Ok(body)
399}
400
401pub fn remove_docs_request(
407 package_name: &str,
408 version: &str,
409 api_key: &str,
410 config: &Config,
411) -> Result<http::Request<Vec<u8>>, ApiError> {
412 validate_package_and_version(package_name, version)?;
413
414 Ok(config
415 .api_request(
416 Method::DELETE,
417 &format!("packages/{}/releases/{}/docs", package_name, version),
418 Some(api_key),
419 )
420 .body(vec![])
421 .expect("remove_docs_request request"))
422}
423
424pub fn remove_docs_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> {
425 let (parts, body) = response.into_parts();
426 match parts.status {
427 StatusCode::NO_CONTENT => Ok(()),
428 StatusCode::NOT_FOUND => Err(ApiError::NotFound),
429 StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited),
430 StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey),
431 StatusCode::FORBIDDEN => Err(ApiError::Forbidden),
432 status => Err(ApiError::unexpected_response(status, body)),
433 }
434}
435
436pub fn publish_docs_request(
442 package_name: &str,
443 version: &str,
444 gzipped_tarball: Vec<u8>,
445 api_key: &str,
446 config: &Config,
447) -> Result<http::Request<Vec<u8>>, ApiError> {
448 validate_package_and_version(package_name, version)?;
449
450 Ok(config
451 .api_request(
452 Method::POST,
453 &format!("packages/{}/releases/{}/docs", package_name, version),
454 Some(api_key),
455 )
456 .header("content-encoding", "x-gzip")
457 .header("content-type", "application/x-tar")
458 .body(gzipped_tarball)
459 .expect("publish_docs_request request"))
460}
461
462pub fn publish_docs_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> {
463 let (parts, body) = response.into_parts();
464 match parts.status {
465 StatusCode::CREATED => Ok(()),
466 StatusCode::NOT_FOUND => Err(ApiError::NotFound),
467 StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited),
468 StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey),
469 StatusCode::FORBIDDEN => Err(ApiError::Forbidden),
470 status => Err(ApiError::unexpected_response(status, body)),
471 }
472}
473
474pub fn publish_package_request(
480 release_tarball: Vec<u8>,
481 api_key: &str,
482 config: &Config,
483 replace: bool,
484) -> http::Request<Vec<u8>> {
485 config
487 .api_request(
488 Method::POST,
489 format!("publish?replace={}", replace).as_str(),
490 Some(api_key),
491 )
492 .header("content-type", "application/x-tar")
493 .body(release_tarball)
494 .expect("publish_package_request request")
495}
496
497pub fn publish_package_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> {
498 let (parts, body) = response.into_parts();
500 match parts.status {
501 StatusCode::OK | StatusCode::CREATED => Ok(()),
502 StatusCode::NOT_FOUND => Err(ApiError::NotFound),
503 StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited),
504 StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey),
505 StatusCode::FORBIDDEN => Err(ApiError::Forbidden),
506 StatusCode::UNPROCESSABLE_ENTITY => {
507 let body = &String::from_utf8_lossy(&body).to_string();
508 if body.contains("--replace") {
509 return Err(ApiError::NotReplacing);
510 }
511 Err(ApiError::LateModification)
512 }
513 status => Err(ApiError::unexpected_response(status, body)),
514 }
515}
516
517pub fn revert_release_request(
523 package_name: &str,
524 version: &str,
525 api_key: &str,
526 config: &Config,
527) -> Result<http::Request<Vec<u8>>, ApiError> {
528 validate_package_and_version(package_name, version)?;
529
530 Ok(config
531 .api_request(
532 Method::DELETE,
533 &format!("packages/{}/releases/{}", package_name, version),
534 Some(api_key),
535 )
536 .body(vec![])
537 .expect("publish_package_request request"))
538}
539
540pub fn revert_release_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> {
541 let (parts, body) = response.into_parts();
542 match parts.status {
543 StatusCode::NO_CONTENT => Ok(()),
544 StatusCode::NOT_FOUND => Err(ApiError::NotFound),
545 StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited),
546 StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey),
547 StatusCode::FORBIDDEN => Err(ApiError::Forbidden),
548 status => Err(ApiError::unexpected_response(status, body)),
549 }
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
554pub enum OwnerLevel {
555 Maintainer,
557 Full,
559}
560
561impl Display for OwnerLevel {
562 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
563 match self {
564 OwnerLevel::Maintainer => write!(f, "maintainer"),
565 OwnerLevel::Full => write!(f, "full"),
566 }
567 }
568}
569
570pub fn add_owner_request(
576 package_name: &str,
577 owner: &str,
578 level: OwnerLevel,
579 api_key: &str,
580 config: &Config,
581) -> http::Request<Vec<u8>> {
582 let body = json!({
583 "level": level.to_string(),
584 "transfer": false,
585 });
586
587 config
588 .api_request(
589 Method::PUT,
590 &format!("packages/{}/owners/{}", package_name, owner),
591 Some(api_key),
592 )
593 .body(body.to_string().into_bytes())
594 .expect("add_owner_request request")
595}
596
597pub fn add_owner_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> {
598 let (parts, body) = response.into_parts();
599 match parts.status {
600 StatusCode::NO_CONTENT => Ok(()),
601 StatusCode::NOT_FOUND => Err(ApiError::NotFound),
602 StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited),
603 StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey),
604 StatusCode::FORBIDDEN => Err(ApiError::Forbidden),
605 status => Err(ApiError::unexpected_response(status, body)),
606 }
607}
608
609pub fn transfer_owner_request(
615 package_name: &str,
616 owner: &str,
617 api_key: &str,
618 config: &Config,
619) -> http::Request<Vec<u8>> {
620 let body = json!({
621 "level": OwnerLevel::Full.to_string(),
622 "transfer": true,
623 });
624
625 config
626 .api_request(
627 Method::PUT,
628 &format!("packages/{}/owners/{}", package_name, owner),
629 Some(api_key),
630 )
631 .body(body.to_string().into_bytes())
632 .expect("transfer_owner_request request")
633}
634
635pub fn transfer_owner_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> {
636 let (parts, body) = response.into_parts();
637 match parts.status {
638 StatusCode::NO_CONTENT => Ok(()),
639 StatusCode::NOT_FOUND => Err(ApiError::NotFound),
640 StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited),
641 StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey),
642 StatusCode::FORBIDDEN => Err(ApiError::Forbidden),
643 status => Err(ApiError::unexpected_response(status, body)),
644 }
645}
646
647pub fn remove_owner_request(
653 package_name: &str,
654 owner: &str,
655 api_key: &str,
656 config: &Config,
657) -> http::Request<Vec<u8>> {
658 config
659 .api_request(
660 Method::DELETE,
661 &format!("packages/{}/owners/{}", package_name, owner),
662 Some(api_key),
663 )
664 .body(vec![])
665 .expect("remove_owner_request request")
666}
667
668pub fn remove_owner_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> {
669 let (parts, body) = response.into_parts();
670 match parts.status {
671 StatusCode::NO_CONTENT => Ok(()),
672 StatusCode::NOT_FOUND => Err(ApiError::NotFound),
673 StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited),
674 StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey),
675 StatusCode::FORBIDDEN => Err(ApiError::Forbidden),
676 status => Err(ApiError::unexpected_response(status, body)),
677 }
678}
679
680#[derive(Error, Debug)]
681pub enum ApiError {
682 #[error(transparent)]
683 Json(#[from] serde_json::Error),
684
685 #[error(transparent)]
686 Io(#[from] std::io::Error),
687
688 #[error("the rate limit for the Hex API has been exceeded for this IP")]
689 RateLimited,
690
691 #[error("invalid username and password combination")]
692 InvalidCredentials,
693
694 #[error("an unexpected response was sent by Hex: {0}: {1}")]
695 UnexpectedResponse(StatusCode, String),
696
697 #[error("the given package name {0} is not valid")]
698 InvalidPackageNameFormat(String),
699
700 #[error("the payload signature does not match the downloaded payload")]
701 IncorrectPayloadSignature,
702
703 #[error(transparent)]
704 InvalidProtobuf(#[from] prost::DecodeError),
705
706 #[error("unexpected version format {0}")]
707 InvalidVersionFormat(String),
708
709 #[error("resource was not found")]
710 NotFound,
711
712 #[error("the version requirement format {0} is not valid")]
713 InvalidVersionRequirementFormat(String),
714
715 #[error("the downloaded data did not have the expected checksum")]
716 IncorrectChecksum,
717
718 #[error("the given API key was not valid")]
719 InvalidApiKey,
720
721 #[error("this account is not authorized for this action")]
722 Forbidden,
723
724 #[error("must explicitly express your intention to replace the release")]
725 NotReplacing,
726
727 #[error("can only modify a release up to one hour after publication")]
728 LateModification,
729}
730
731impl ApiError {
732 fn unexpected_response(status: StatusCode, body: Vec<u8>) -> Self {
733 ApiError::UnexpectedResponse(status, String::from_utf8_lossy(&body).to_string())
734 }
735
736 pub fn is_not_found(&self) -> bool {
740 matches!(self, Self::NotFound)
741 }
742}
743
744fn read_and_check_body(reader: impl std::io::Read, checksum: &[u8]) -> Result<Vec<u8>, ApiError> {
746 use std::io::Read;
747 let mut reader = BufReader::new(reader);
748 let mut context = Context::new(&SHA256);
749 let mut buffer = [0; 1024];
750 let mut body = Vec::new();
751
752 loop {
753 let count = reader.read(&mut buffer)?;
754 if count == 0 {
755 break;
756 }
757 let bytes = &buffer[..count];
758 context.update(bytes);
759 body.extend_from_slice(bytes);
760 }
761
762 let digest = context.finish();
763 if digest.as_ref() == checksum {
764 Ok(body)
765 } else {
766 Err(ApiError::IncorrectChecksum)
767 }
768}
769
770fn proto_to_retirement_status(
771 status: Option<proto::package::RetirementStatus>,
772) -> Option<RetirementStatus> {
773 status.map(|stat| RetirementStatus {
774 message: stat.message().into(),
775 reason: proto_to_retirement_reason(stat.reason()),
776 })
777}
778
779fn proto_to_retirement_reason(reason: proto::package::RetirementReason) -> RetirementReason {
780 use proto::package::RetirementReason::*;
781 match reason {
782 RetiredOther => RetirementReason::Other,
783 RetiredInvalid => RetirementReason::Invalid,
784 RetiredSecurity => RetirementReason::Security,
785 RetiredDeprecated => RetirementReason::Deprecated,
786 RetiredRenamed => RetirementReason::Renamed,
787 }
788}
789
790fn proto_to_dep(dep: proto::package::Dependency) -> Result<(String, Dependency), ApiError> {
791 let app = dep.app;
792 let repository = dep.repository;
793 let requirement = Range::new(dep.requirement.clone())
794 .map_err(|_| ApiError::InvalidVersionFormat(dep.requirement))?;
795 Ok((
796 dep.package,
797 Dependency {
798 requirement,
799 optional: dep.optional.is_some(),
800 app,
801 repository,
802 },
803 ))
804}
805
806fn proto_to_release(release: proto::package::Release) -> Result<Release<()>, ApiError> {
807 let dependencies = release
808 .dependencies
809 .clone()
810 .into_iter()
811 .map(proto_to_dep)
812 .collect::<Result<HashMap<_, _>, _>>()?;
813 let version = Version::try_from(release.version.as_str())
814 .expect("Failed to parse version format from Hex");
815 Ok(Release {
816 version,
817 outer_checksum: release.outer_checksum.unwrap_or_default(),
818 retirement_status: proto_to_retirement_status(release.retired),
819 requirements: dependencies,
820 meta: (),
821 })
822}
823
824#[derive(Debug, PartialEq, Eq, Clone)]
825pub struct Package {
826 pub name: String,
827 pub repository: String,
828 pub releases: Vec<Release<()>>,
829}
830
831#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize)]
832pub struct Release<Meta> {
833 pub version: Version,
835 pub requirements: HashMap<String, Dependency>,
837 pub retirement_status: Option<RetirementStatus>,
840 #[serde(alias = "checksum", deserialize_with = "deserialize_checksum")]
843 pub outer_checksum: Vec<u8>,
844 pub meta: Meta,
846}
847
848fn deserialize_checksum<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
849where
850 D: serde::Deserializer<'de>,
851{
852 let s: &str = serde::de::Deserialize::deserialize(deserializer)?;
853 base16::decode(s).map_err(serde::de::Error::custom)
854}
855
856impl<Meta> Release<Meta> {
857 pub fn is_retired(&self) -> bool {
858 self.retirement_status.is_some()
859 }
860}
861
862#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize)]
863pub struct ReleaseMeta {
864 pub app: String,
865 pub build_tools: Vec<String>,
866}
867
868#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize)]
869pub struct RetirementStatus {
870 pub reason: RetirementReason,
871 pub message: String,
872}
873
874#[derive(Debug, PartialEq, Eq, Clone)]
875pub enum RetirementReason {
876 Other,
877 Invalid,
878 Security,
879 Deprecated,
880 Renamed,
881}
882
883impl<'de> serde::Deserialize<'de> for RetirementReason {
884 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
885 where
886 D: serde::Deserializer<'de>,
887 {
888 let s: &str = serde::de::Deserialize::deserialize(deserializer)?;
889 match s {
890 "other" => Ok(RetirementReason::Other),
891 "invalid" => Ok(RetirementReason::Invalid),
892 "security" => Ok(RetirementReason::Security),
893 "deprecated" => Ok(RetirementReason::Deprecated),
894 "renamed" => Ok(RetirementReason::Renamed),
895 _ => Err(serde::de::Error::custom("unknown retirement reason type")),
896 }
897 }
898}
899
900impl RetirementReason {
901 pub fn to_str(&self) -> &'static str {
902 match self {
903 RetirementReason::Other => "other",
904 RetirementReason::Invalid => "invalid",
905 RetirementReason::Security => "security",
906 RetirementReason::Deprecated => "deprecated",
907 RetirementReason::Renamed => "renamed",
908 }
909 }
910}
911
912#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize)]
913pub struct Dependency {
914 pub requirement: Range,
916 pub optional: bool,
919 pub app: Option<String>,
922 pub repository: Option<String>,
924}
925
926static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), " (", env!("CARGO_PKG_VERSION"), ")");
927
928fn validate_package_and_version(package: &str, version: &str) -> Result<(), ApiError> {
929 lazy_static! {
930 static ref PACKAGE_PATTERN: Regex = Regex::new(r"^[a-z]\w*$").unwrap();
931 static ref VERSION_PATTERN: Regex = Regex::new(r"^[a-zA-Z-0-9\._-]+$").unwrap();
932 }
933 if !PACKAGE_PATTERN.is_match(package) {
934 return Err(ApiError::InvalidPackageNameFormat(package.to_string()));
935 }
936 if !VERSION_PATTERN.is_match(version) {
937 return Err(ApiError::InvalidVersionFormat(version.to_string()));
938 }
939 Ok(())
940}
941
942fn verify_payload(mut signed: Signed, pem_public_key: &[u8]) -> Result<Vec<u8>, ApiError> {
954 let (_, pem) = x509_parser::pem::parse_x509_pem(pem_public_key)
955 .map_err(|_| ApiError::IncorrectPayloadSignature)?;
956 let (_, spki) = x509_parser::prelude::SubjectPublicKeyInfo::from_der(&pem.contents)
957 .map_err(|_| ApiError::IncorrectPayloadSignature)?;
958 let payload = std::mem::take(&mut signed.payload);
959 let verification = ring::signature::UnparsedPublicKey::new(
960 &ring::signature::RSA_PKCS1_2048_8192_SHA512,
961 &spki.subject_public_key,
962 )
963 .verify(payload.as_slice(), signed.signature());
964
965 if verification.is_ok() {
966 Ok(payload)
967 } else {
968 Err(ApiError::IncorrectPayloadSignature)
969 }
970}
971
972pub fn get_package_release_request(
975 name: &str,
976 version: &str,
977 api_key: Option<&str>,
978 config: &Config,
979) -> http::Request<Vec<u8>> {
980 config
981 .api_request(
982 Method::GET,
983 &format!("packages/{}/releases/{}", name, version),
984 api_key,
985 )
986 .header("accept", "application/json")
987 .body(vec![])
988 .expect("get_package_release request")
989}
990
991pub fn get_package_release_response(
994 response: http::Response<Vec<u8>>,
995) -> Result<Release<ReleaseMeta>, ApiError> {
996 let (parts, body) = response.into_parts();
997
998 match parts.status {
999 StatusCode::OK => Ok(serde_json::from_slice(&body)?),
1000 StatusCode::NOT_FOUND => Err(ApiError::NotFound),
1001 StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited),
1002 StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey),
1003 StatusCode::FORBIDDEN => Err(ApiError::Forbidden),
1004 status => Err(ApiError::unexpected_response(status, body)),
1005 }
1006}