]>
Commit | Line | Data |
---|---|---|
0a29b90c FG |
1 | use crate::git::repo; |
2 | use crate::paths; | |
3 | use crate::publish::{create_index_line, write_to_index}; | |
4 | use cargo_util::paths::append; | |
5 | use cargo_util::Sha256; | |
6 | use flate2::write::GzEncoder; | |
7 | use flate2::Compression; | |
8 | use pasetors::keys::{AsymmetricPublicKey, AsymmetricSecretKey}; | |
9 | use pasetors::paserk::FormatAsPaserk; | |
10 | use pasetors::token::UntrustedToken; | |
11 | use std::collections::{BTreeMap, HashMap}; | |
12 | use std::fmt; | |
13 | use std::fs::{self, File}; | |
14 | use std::io::{BufRead, BufReader, Read, Write}; | |
15 | use std::net::{SocketAddr, TcpListener, TcpStream}; | |
16 | use std::path::{Path, PathBuf}; | |
17 | use std::thread::{self, JoinHandle}; | |
18 | use tar::{Builder, Header}; | |
19 | use time::format_description::well_known::Rfc3339; | |
20 | use time::{Duration, OffsetDateTime}; | |
21 | use url::Url; | |
22 | ||
23 | /// Gets the path to the local index pretending to be crates.io. This is a Git repo | |
24 | /// initialized with a `config.json` file pointing to `dl_path` for downloads | |
25 | /// and `api_path` for uploads. | |
26 | pub fn registry_path() -> PathBuf { | |
27 | generate_path("registry") | |
28 | } | |
29 | /// Gets the path for local web API uploads. Cargo will place the contents of a web API | |
30 | /// request here. For example, `api/v1/crates/new` is the result of publishing a crate. | |
31 | pub fn api_path() -> PathBuf { | |
32 | generate_path("api") | |
33 | } | |
34 | /// Gets the path where crates can be downloaded using the web API endpoint. Crates | |
35 | /// should be organized as `{name}/{version}/download` to match the web API | |
36 | /// endpoint. This is rarely used and must be manually set up. | |
37 | fn dl_path() -> PathBuf { | |
38 | generate_path("dl") | |
39 | } | |
40 | /// Gets the alternative-registry version of `registry_path`. | |
41 | fn alt_registry_path() -> PathBuf { | |
42 | generate_path("alternative-registry") | |
43 | } | |
44 | /// Gets the alternative-registry version of `registry_url`. | |
45 | fn alt_registry_url() -> Url { | |
46 | generate_url("alternative-registry") | |
47 | } | |
48 | /// Gets the alternative-registry version of `dl_path`. | |
49 | pub fn alt_dl_path() -> PathBuf { | |
50 | generate_path("alternative-dl") | |
51 | } | |
52 | /// Gets the alternative-registry version of `api_path`. | |
53 | pub fn alt_api_path() -> PathBuf { | |
54 | generate_path("alternative-api") | |
55 | } | |
56 | fn generate_path(name: &str) -> PathBuf { | |
57 | paths::root().join(name) | |
58 | } | |
59 | fn generate_url(name: &str) -> Url { | |
60 | Url::from_file_path(generate_path(name)).ok().unwrap() | |
61 | } | |
62 | ||
63 | #[derive(Clone)] | |
64 | pub enum Token { | |
65 | Plaintext(String), | |
66 | Keys(String, Option<String>), | |
67 | } | |
68 | ||
69 | impl Token { | |
70 | /// This is a valid PASETO secret key. | |
71 | /// This one is already publicly available as part of the text of the RFC so is safe to use for tests. | |
72 | pub fn rfc_key() -> Token { | |
73 | Token::Keys( | |
74 | "k3.secret.fNYVuMvBgOlljt9TDohnaYLblghqaHoQquVZwgR6X12cBFHZLFsaU3q7X3k1Zn36" | |
75 | .to_string(), | |
76 | Some("sub".to_string()), | |
77 | ) | |
78 | } | |
79 | } | |
80 | ||
81 | type RequestCallback = Box<dyn Send + Fn(&Request, &HttpServer) -> Response>; | |
82 | ||
83 | /// A builder for initializing registries. | |
84 | pub struct RegistryBuilder { | |
85 | /// If set, configures an alternate registry with the given name. | |
86 | alternative: Option<String>, | |
87 | /// The authorization token for the registry. | |
88 | token: Option<Token>, | |
89 | /// If set, the registry requires authorization for all operations. | |
90 | auth_required: bool, | |
91 | /// If set, serves the index over http. | |
92 | http_index: bool, | |
93 | /// If set, serves the API over http. | |
94 | http_api: bool, | |
95 | /// If set, config.json includes 'api' | |
96 | api: bool, | |
97 | /// Write the token in the configuration. | |
98 | configure_token: bool, | |
99 | /// Write the registry in configuration. | |
100 | configure_registry: bool, | |
101 | /// API responders. | |
102 | custom_responders: HashMap<String, RequestCallback>, | |
103 | /// Handler for 404 responses. | |
104 | not_found_handler: RequestCallback, | |
105 | /// If nonzero, the git index update to be delayed by the given number of seconds. | |
106 | delayed_index_update: usize, | |
add651ee FG |
107 | /// Credential provider in configuration |
108 | credential_provider: Option<String>, | |
0a29b90c FG |
109 | } |
110 | ||
111 | pub struct TestRegistry { | |
112 | server: Option<HttpServerHandle>, | |
113 | index_url: Url, | |
114 | path: PathBuf, | |
115 | api_url: Url, | |
116 | dl_url: Url, | |
117 | token: Token, | |
118 | } | |
119 | ||
120 | impl TestRegistry { | |
121 | pub fn index_url(&self) -> &Url { | |
122 | &self.index_url | |
123 | } | |
124 | ||
125 | pub fn api_url(&self) -> &Url { | |
126 | &self.api_url | |
127 | } | |
128 | ||
129 | pub fn token(&self) -> &str { | |
130 | match &self.token { | |
131 | Token::Plaintext(s) => s, | |
132 | Token::Keys(_, _) => panic!("registry was not configured with a plaintext token"), | |
133 | } | |
134 | } | |
135 | ||
136 | pub fn key(&self) -> &str { | |
137 | match &self.token { | |
138 | Token::Plaintext(_) => panic!("registry was not configured with a secret key"), | |
139 | Token::Keys(s, _) => s, | |
140 | } | |
141 | } | |
142 | ||
143 | /// Shutdown the server thread and wait for it to stop. | |
144 | /// `Drop` automatically stops the server, but this additionally | |
145 | /// waits for the thread to stop. | |
146 | pub fn join(self) { | |
147 | if let Some(mut server) = self.server { | |
148 | server.stop(); | |
149 | let handle = server.handle.take().unwrap(); | |
150 | handle.join().unwrap(); | |
151 | } | |
152 | } | |
153 | } | |
154 | ||
155 | impl RegistryBuilder { | |
156 | #[must_use] | |
157 | pub fn new() -> RegistryBuilder { | |
158 | let not_found = |_req: &Request, _server: &HttpServer| -> Response { | |
159 | Response { | |
160 | code: 404, | |
161 | headers: vec![], | |
162 | body: b"not found".to_vec(), | |
163 | } | |
164 | }; | |
165 | RegistryBuilder { | |
166 | alternative: None, | |
167 | token: None, | |
168 | auth_required: false, | |
169 | http_api: false, | |
170 | http_index: false, | |
171 | api: true, | |
172 | configure_registry: true, | |
173 | configure_token: true, | |
174 | custom_responders: HashMap::new(), | |
175 | not_found_handler: Box::new(not_found), | |
176 | delayed_index_update: 0, | |
add651ee | 177 | credential_provider: None, |
0a29b90c FG |
178 | } |
179 | } | |
180 | ||
181 | /// Adds a custom HTTP response for a specific url | |
182 | #[must_use] | |
183 | pub fn add_responder<R: 'static + Send + Fn(&Request, &HttpServer) -> Response>( | |
184 | mut self, | |
185 | url: impl Into<String>, | |
186 | responder: R, | |
187 | ) -> Self { | |
188 | self.custom_responders | |
189 | .insert(url.into(), Box::new(responder)); | |
190 | self | |
191 | } | |
192 | ||
193 | #[must_use] | |
194 | pub fn not_found_handler<R: 'static + Send + Fn(&Request, &HttpServer) -> Response>( | |
195 | mut self, | |
196 | responder: R, | |
197 | ) -> Self { | |
198 | self.not_found_handler = Box::new(responder); | |
199 | self | |
200 | } | |
201 | ||
202 | /// Configures the git index update to be delayed by the given number of seconds. | |
203 | #[must_use] | |
204 | pub fn delayed_index_update(mut self, delay: usize) -> Self { | |
205 | self.delayed_index_update = delay; | |
206 | self | |
207 | } | |
208 | ||
209 | /// Sets whether or not to initialize as an alternative registry. | |
210 | #[must_use] | |
211 | pub fn alternative_named(mut self, alt: &str) -> Self { | |
212 | self.alternative = Some(alt.to_string()); | |
213 | self | |
214 | } | |
215 | ||
216 | /// Sets whether or not to initialize as an alternative registry. | |
217 | #[must_use] | |
218 | pub fn alternative(self) -> Self { | |
219 | self.alternative_named("alternative") | |
220 | } | |
221 | ||
222 | /// Prevents placing a token in the configuration | |
223 | #[must_use] | |
224 | pub fn no_configure_token(mut self) -> Self { | |
225 | self.configure_token = false; | |
226 | self | |
227 | } | |
228 | ||
229 | /// Prevents adding the registry to the configuration. | |
230 | #[must_use] | |
231 | pub fn no_configure_registry(mut self) -> Self { | |
232 | self.configure_registry = false; | |
233 | self | |
234 | } | |
235 | ||
236 | /// Sets the token value | |
237 | #[must_use] | |
238 | pub fn token(mut self, token: Token) -> Self { | |
239 | self.token = Some(token); | |
240 | self | |
241 | } | |
242 | ||
243 | /// Sets this registry to require the authentication token for | |
244 | /// all operations. | |
245 | #[must_use] | |
246 | pub fn auth_required(mut self) -> Self { | |
247 | self.auth_required = true; | |
248 | self | |
249 | } | |
250 | ||
251 | /// Operate the index over http | |
252 | #[must_use] | |
253 | pub fn http_index(mut self) -> Self { | |
254 | self.http_index = true; | |
255 | self | |
256 | } | |
257 | ||
258 | /// Operate the api over http | |
259 | #[must_use] | |
260 | pub fn http_api(mut self) -> Self { | |
261 | self.http_api = true; | |
262 | self | |
263 | } | |
264 | ||
265 | /// The registry has no api. | |
266 | #[must_use] | |
267 | pub fn no_api(mut self) -> Self { | |
268 | self.api = false; | |
269 | self | |
270 | } | |
271 | ||
add651ee FG |
272 | /// The credential provider to configure for this registry. |
273 | #[must_use] | |
274 | pub fn credential_provider(mut self, provider: &[&str]) -> Self { | |
275 | self.credential_provider = Some(format!("['{}']", provider.join("','"))); | |
276 | self | |
277 | } | |
278 | ||
0a29b90c FG |
279 | /// Initializes the registry. |
280 | #[must_use] | |
281 | pub fn build(self) -> TestRegistry { | |
282 | let config_path = paths::home().join(".cargo/config"); | |
283 | t!(fs::create_dir_all(config_path.parent().unwrap())); | |
284 | let prefix = if let Some(alternative) = &self.alternative { | |
285 | format!("{alternative}-") | |
286 | } else { | |
287 | String::new() | |
288 | }; | |
289 | let registry_path = generate_path(&format!("{prefix}registry")); | |
290 | let index_url = generate_url(&format!("{prefix}registry")); | |
291 | let api_url = generate_url(&format!("{prefix}api")); | |
292 | let dl_url = generate_url(&format!("{prefix}dl")); | |
293 | let dl_path = generate_path(&format!("{prefix}dl")); | |
294 | let api_path = generate_path(&format!("{prefix}api")); | |
295 | let token = self | |
296 | .token | |
297 | .unwrap_or_else(|| Token::Plaintext(format!("{prefix}sekrit"))); | |
298 | ||
299 | let (server, index_url, api_url, dl_url) = if !self.http_index && !self.http_api { | |
300 | // No need to start the HTTP server. | |
301 | (None, index_url, api_url, dl_url) | |
302 | } else { | |
303 | let server = HttpServer::new( | |
304 | registry_path.clone(), | |
305 | dl_path, | |
306 | api_path.clone(), | |
307 | token.clone(), | |
308 | self.auth_required, | |
309 | self.custom_responders, | |
310 | self.not_found_handler, | |
311 | self.delayed_index_update, | |
312 | ); | |
313 | let index_url = if self.http_index { | |
314 | server.index_url() | |
315 | } else { | |
316 | index_url | |
317 | }; | |
318 | let api_url = if self.http_api { | |
319 | server.api_url() | |
320 | } else { | |
321 | api_url | |
322 | }; | |
323 | let dl_url = server.dl_url(); | |
324 | (Some(server), index_url, api_url, dl_url) | |
325 | }; | |
326 | ||
327 | let registry = TestRegistry { | |
328 | api_url, | |
329 | index_url, | |
330 | server, | |
331 | dl_url, | |
332 | path: registry_path, | |
333 | token, | |
334 | }; | |
335 | ||
336 | if self.configure_registry { | |
337 | if let Some(alternative) = &self.alternative { | |
338 | append( | |
339 | &config_path, | |
340 | format!( | |
341 | " | |
342 | [registries.{alternative}] | |
343 | index = '{}'", | |
344 | registry.index_url | |
345 | ) | |
346 | .as_bytes(), | |
347 | ) | |
348 | .unwrap(); | |
add651ee FG |
349 | if let Some(p) = &self.credential_provider { |
350 | append( | |
351 | &config_path, | |
352 | &format!( | |
353 | " | |
354 | credential-provider = {p} | |
355 | " | |
356 | ) | |
357 | .as_bytes(), | |
358 | ) | |
359 | .unwrap() | |
360 | } | |
0a29b90c FG |
361 | } else { |
362 | append( | |
363 | &config_path, | |
364 | format!( | |
365 | " | |
366 | [source.crates-io] | |
367 | replace-with = 'dummy-registry' | |
368 | ||
369 | [registries.dummy-registry] | |
370 | index = '{}'", | |
371 | registry.index_url | |
372 | ) | |
373 | .as_bytes(), | |
374 | ) | |
375 | .unwrap(); | |
add651ee FG |
376 | |
377 | if let Some(p) = &self.credential_provider { | |
378 | append( | |
379 | &config_path, | |
380 | &format!( | |
381 | " | |
382 | [registry] | |
383 | credential-provider = {p} | |
384 | " | |
385 | ) | |
386 | .as_bytes(), | |
387 | ) | |
388 | .unwrap() | |
389 | } | |
0a29b90c FG |
390 | } |
391 | } | |
392 | ||
393 | if self.configure_token { | |
394 | let credentials = paths::home().join(".cargo/credentials.toml"); | |
395 | match ®istry.token { | |
396 | Token::Plaintext(token) => { | |
397 | if let Some(alternative) = &self.alternative { | |
398 | append( | |
399 | &credentials, | |
400 | format!( | |
401 | r#" | |
402 | [registries.{alternative}] | |
403 | token = "{token}" | |
404 | "# | |
405 | ) | |
406 | .as_bytes(), | |
407 | ) | |
408 | .unwrap(); | |
409 | } else { | |
410 | append( | |
411 | &credentials, | |
412 | format!( | |
413 | r#" | |
414 | [registry] | |
415 | token = "{token}" | |
416 | "# | |
417 | ) | |
418 | .as_bytes(), | |
419 | ) | |
420 | .unwrap(); | |
421 | } | |
422 | } | |
423 | Token::Keys(key, subject) => { | |
424 | let mut out = if let Some(alternative) = &self.alternative { | |
425 | format!("\n[registries.{alternative}]\n") | |
426 | } else { | |
427 | format!("\n[registry]\n") | |
428 | }; | |
429 | out += &format!("secret-key = \"{key}\"\n"); | |
430 | if let Some(subject) = subject { | |
431 | out += &format!("secret-key-subject = \"{subject}\"\n"); | |
432 | } | |
433 | ||
434 | append(&credentials, out.as_bytes()).unwrap(); | |
435 | } | |
436 | } | |
437 | } | |
438 | ||
439 | let auth = if self.auth_required { | |
440 | r#","auth-required":true"# | |
441 | } else { | |
442 | "" | |
443 | }; | |
444 | let api = if self.api { | |
445 | format!(r#","api":"{}""#, registry.api_url) | |
446 | } else { | |
447 | String::new() | |
448 | }; | |
449 | // Initialize a new registry. | |
450 | repo(®istry.path) | |
451 | .file( | |
452 | "config.json", | |
453 | &format!(r#"{{"dl":"{}"{api}{auth}}}"#, registry.dl_url), | |
454 | ) | |
455 | .build(); | |
456 | fs::create_dir_all(api_path.join("api/v1/crates")).unwrap(); | |
457 | ||
458 | registry | |
459 | } | |
460 | } | |
461 | ||
462 | /// A builder for creating a new package in a registry. | |
463 | /// | |
464 | /// This uses "source replacement" using an automatically generated | |
465 | /// `.cargo/config` file to ensure that dependencies will use these packages | |
466 | /// instead of contacting crates.io. See `source-replacement.md` for more | |
467 | /// details on how source replacement works. | |
468 | /// | |
469 | /// Call `publish` to finalize and create the package. | |
470 | /// | |
471 | /// If no files are specified, an empty `lib.rs` file is automatically created. | |
472 | /// | |
473 | /// The `Cargo.toml` file is automatically generated based on the methods | |
474 | /// called on `Package` (for example, calling `dep()` will add to the | |
475 | /// `[dependencies]` automatically). You may also specify a `Cargo.toml` file | |
476 | /// to override the generated one. | |
477 | /// | |
478 | /// This supports different registry types: | |
479 | /// - Regular source replacement that replaces `crates.io` (the default). | |
480 | /// - A "local registry" which is a subset for vendoring (see | |
481 | /// `Package::local`). | |
482 | /// - An "alternative registry" which requires specifying the registry name | |
483 | /// (see `Package::alternative`). | |
484 | /// | |
485 | /// This does not support "directory sources". See `directory.rs` for | |
486 | /// `VendorPackage` which implements directory sources. | |
487 | /// | |
488 | /// # Example | |
49aad941 FG |
489 | /// ```no_run |
490 | /// use cargo_test_support::registry::Package; | |
491 | /// use cargo_test_support::project; | |
492 | /// | |
0a29b90c FG |
493 | /// // Publish package "a" depending on "b". |
494 | /// Package::new("a", "1.0.0") | |
495 | /// .dep("b", "1.0.0") | |
496 | /// .file("src/lib.rs", r#" | |
497 | /// extern crate b; | |
498 | /// pub fn f() -> i32 { b::f() * 2 } | |
499 | /// "#) | |
500 | /// .publish(); | |
501 | /// | |
502 | /// // Publish package "b". | |
503 | /// Package::new("b", "1.0.0") | |
504 | /// .file("src/lib.rs", r#" | |
505 | /// pub fn f() -> i32 { 12 } | |
506 | /// "#) | |
507 | /// .publish(); | |
508 | /// | |
509 | /// // Create a project that uses package "a". | |
510 | /// let p = project() | |
511 | /// .file("Cargo.toml", r#" | |
512 | /// [package] | |
513 | /// name = "foo" | |
514 | /// version = "0.0.1" | |
515 | /// | |
516 | /// [dependencies] | |
517 | /// a = "1.0" | |
518 | /// "#) | |
519 | /// .file("src/main.rs", r#" | |
520 | /// extern crate a; | |
521 | /// fn main() { println!("{}", a::f()); } | |
522 | /// "#) | |
523 | /// .build(); | |
524 | /// | |
525 | /// p.cargo("run").with_stdout("24").run(); | |
526 | /// ``` | |
527 | #[must_use] | |
528 | pub struct Package { | |
529 | name: String, | |
530 | vers: String, | |
531 | deps: Vec<Dependency>, | |
532 | files: Vec<PackageFile>, | |
533 | yanked: bool, | |
534 | features: FeatureMap, | |
535 | local: bool, | |
536 | alternative: bool, | |
537 | invalid_json: bool, | |
538 | proc_macro: bool, | |
539 | links: Option<String>, | |
540 | rust_version: Option<String>, | |
541 | cargo_features: Vec<String>, | |
542 | v: Option<u32>, | |
543 | } | |
544 | ||
545 | pub(crate) type FeatureMap = BTreeMap<String, Vec<String>>; | |
546 | ||
547 | #[derive(Clone)] | |
548 | pub struct Dependency { | |
549 | name: String, | |
550 | vers: String, | |
551 | kind: String, | |
781aab86 FG |
552 | artifact: Option<String>, |
553 | bindep_target: Option<String>, | |
554 | lib: bool, | |
0a29b90c FG |
555 | target: Option<String>, |
556 | features: Vec<String>, | |
557 | registry: Option<String>, | |
558 | package: Option<String>, | |
559 | optional: bool, | |
560 | } | |
561 | ||
562 | /// Entry with data that corresponds to [`tar::EntryType`]. | |
563 | #[non_exhaustive] | |
564 | enum EntryData { | |
565 | Regular(String), | |
566 | Symlink(PathBuf), | |
567 | } | |
568 | ||
569 | /// A file to be created in a package. | |
570 | struct PackageFile { | |
571 | path: String, | |
572 | contents: EntryData, | |
573 | /// The Unix mode for the file. Note that when extracted on Windows, this | |
574 | /// is mostly ignored since it doesn't have the same style of permissions. | |
575 | mode: u32, | |
576 | /// If `true`, the file is created in the root of the tarfile, used for | |
577 | /// testing invalid packages. | |
578 | extra: bool, | |
579 | } | |
580 | ||
581 | const DEFAULT_MODE: u32 = 0o644; | |
582 | ||
583 | /// Initializes the on-disk registry and sets up the config so that crates.io | |
584 | /// is replaced with the one on disk. | |
585 | pub fn init() -> TestRegistry { | |
586 | RegistryBuilder::new().build() | |
587 | } | |
588 | ||
589 | /// Variant of `init` that initializes the "alternative" registry and crates.io | |
590 | /// replacement. | |
591 | pub fn alt_init() -> TestRegistry { | |
592 | init(); | |
593 | RegistryBuilder::new().alternative().build() | |
594 | } | |
595 | ||
596 | pub struct HttpServerHandle { | |
597 | addr: SocketAddr, | |
598 | handle: Option<JoinHandle<()>>, | |
599 | } | |
600 | ||
601 | impl HttpServerHandle { | |
602 | pub fn index_url(&self) -> Url { | |
603 | Url::parse(&format!("sparse+http://{}/index/", self.addr.to_string())).unwrap() | |
604 | } | |
605 | ||
606 | pub fn api_url(&self) -> Url { | |
607 | Url::parse(&format!("http://{}/", self.addr.to_string())).unwrap() | |
608 | } | |
609 | ||
610 | pub fn dl_url(&self) -> Url { | |
611 | Url::parse(&format!("http://{}/dl", self.addr.to_string())).unwrap() | |
612 | } | |
613 | ||
614 | fn stop(&self) { | |
615 | if let Ok(mut stream) = TcpStream::connect(self.addr) { | |
616 | // shutdown the server | |
617 | let _ = stream.write_all(b"stop"); | |
618 | let _ = stream.flush(); | |
619 | } | |
620 | } | |
621 | } | |
622 | ||
623 | impl Drop for HttpServerHandle { | |
624 | fn drop(&mut self) { | |
625 | self.stop(); | |
626 | } | |
627 | } | |
628 | ||
629 | /// Request to the test http server | |
630 | #[derive(Clone)] | |
631 | pub struct Request { | |
632 | pub url: Url, | |
633 | pub method: String, | |
634 | pub body: Option<Vec<u8>>, | |
635 | pub authorization: Option<String>, | |
636 | pub if_modified_since: Option<String>, | |
637 | pub if_none_match: Option<String>, | |
638 | } | |
639 | ||
640 | impl fmt::Debug for Request { | |
641 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
642 | // body is not included as it can produce long debug outputs | |
643 | f.debug_struct("Request") | |
644 | .field("url", &self.url) | |
645 | .field("method", &self.method) | |
646 | .field("authorization", &self.authorization) | |
647 | .field("if_modified_since", &self.if_modified_since) | |
648 | .field("if_none_match", &self.if_none_match) | |
649 | .finish() | |
650 | } | |
651 | } | |
652 | ||
653 | /// Response from the test http server | |
654 | pub struct Response { | |
655 | pub code: u32, | |
656 | pub headers: Vec<String>, | |
657 | pub body: Vec<u8>, | |
658 | } | |
659 | ||
660 | pub struct HttpServer { | |
661 | listener: TcpListener, | |
662 | registry_path: PathBuf, | |
663 | dl_path: PathBuf, | |
664 | api_path: PathBuf, | |
665 | addr: SocketAddr, | |
666 | token: Token, | |
667 | auth_required: bool, | |
668 | custom_responders: HashMap<String, RequestCallback>, | |
669 | not_found_handler: RequestCallback, | |
670 | delayed_index_update: usize, | |
671 | } | |
672 | ||
673 | /// A helper struct that collects the arguments for [`HttpServer::check_authorized`]. | |
674 | /// Based on looking at the request, these are the fields that the authentication header should attest to. | |
675 | struct Mutation<'a> { | |
676 | mutation: &'a str, | |
677 | name: Option<&'a str>, | |
678 | vers: Option<&'a str>, | |
679 | cksum: Option<&'a str>, | |
680 | } | |
681 | ||
682 | impl HttpServer { | |
683 | pub fn new( | |
684 | registry_path: PathBuf, | |
685 | dl_path: PathBuf, | |
686 | api_path: PathBuf, | |
687 | token: Token, | |
688 | auth_required: bool, | |
689 | custom_responders: HashMap<String, RequestCallback>, | |
690 | not_found_handler: RequestCallback, | |
691 | delayed_index_update: usize, | |
692 | ) -> HttpServerHandle { | |
693 | let listener = TcpListener::bind("127.0.0.1:0").unwrap(); | |
694 | let addr = listener.local_addr().unwrap(); | |
695 | let server = HttpServer { | |
696 | listener, | |
697 | registry_path, | |
698 | dl_path, | |
699 | api_path, | |
700 | addr, | |
701 | token, | |
702 | auth_required, | |
703 | custom_responders, | |
704 | not_found_handler, | |
705 | delayed_index_update, | |
706 | }; | |
707 | let handle = Some(thread::spawn(move || server.start())); | |
708 | HttpServerHandle { addr, handle } | |
709 | } | |
710 | ||
711 | fn start(&self) { | |
712 | let mut line = String::new(); | |
713 | 'server: loop { | |
714 | let (socket, _) = self.listener.accept().unwrap(); | |
715 | let mut buf = BufReader::new(socket); | |
716 | line.clear(); | |
717 | if buf.read_line(&mut line).unwrap() == 0 { | |
718 | // Connection terminated. | |
719 | continue; | |
720 | } | |
721 | // Read the "GET path HTTP/1.1" line. | |
722 | let mut parts = line.split_ascii_whitespace(); | |
723 | let method = parts.next().unwrap().to_ascii_lowercase(); | |
724 | if method == "stop" { | |
725 | // Shutdown the server. | |
726 | return; | |
727 | } | |
728 | let addr = self.listener.local_addr().unwrap(); | |
729 | let url = format!( | |
730 | "http://{}/{}", | |
731 | addr, | |
732 | parts.next().unwrap().trim_start_matches('/') | |
733 | ); | |
734 | let url = Url::parse(&url).unwrap(); | |
735 | ||
736 | // Grab headers we care about. | |
737 | let mut if_modified_since = None; | |
738 | let mut if_none_match = None; | |
739 | let mut authorization = None; | |
740 | let mut content_len = None; | |
741 | loop { | |
742 | line.clear(); | |
743 | if buf.read_line(&mut line).unwrap() == 0 { | |
744 | continue 'server; | |
745 | } | |
746 | if line == "\r\n" { | |
747 | // End of headers. | |
748 | line.clear(); | |
749 | break; | |
750 | } | |
751 | let (name, value) = line.split_once(':').unwrap(); | |
752 | let name = name.trim().to_ascii_lowercase(); | |
753 | let value = value.trim().to_string(); | |
754 | match name.as_str() { | |
755 | "if-modified-since" => if_modified_since = Some(value), | |
756 | "if-none-match" => if_none_match = Some(value), | |
757 | "authorization" => authorization = Some(value), | |
758 | "content-length" => content_len = Some(value), | |
759 | _ => {} | |
760 | } | |
761 | } | |
762 | ||
763 | let mut body = None; | |
764 | if let Some(con_len) = content_len { | |
765 | let len = con_len.parse::<u64>().unwrap(); | |
766 | let mut content = vec![0u8; len as usize]; | |
767 | buf.read_exact(&mut content).unwrap(); | |
768 | body = Some(content) | |
769 | } | |
770 | ||
771 | let req = Request { | |
772 | authorization, | |
773 | if_modified_since, | |
774 | if_none_match, | |
775 | method, | |
776 | url, | |
777 | body, | |
778 | }; | |
779 | println!("req: {:#?}", req); | |
780 | let response = self.route(&req); | |
781 | let buf = buf.get_mut(); | |
782 | write!(buf, "HTTP/1.1 {}\r\n", response.code).unwrap(); | |
783 | write!(buf, "Content-Length: {}\r\n", response.body.len()).unwrap(); | |
781aab86 | 784 | write!(buf, "Connection: close\r\n").unwrap(); |
0a29b90c FG |
785 | for header in response.headers { |
786 | write!(buf, "{}\r\n", header).unwrap(); | |
787 | } | |
788 | write!(buf, "\r\n").unwrap(); | |
789 | buf.write_all(&response.body).unwrap(); | |
790 | buf.flush().unwrap(); | |
791 | } | |
792 | } | |
793 | ||
781aab86 | 794 | fn check_authorized(&self, req: &Request, mutation: Option<Mutation<'_>>) -> bool { |
0a29b90c FG |
795 | let (private_key, private_key_subject) = if mutation.is_some() || self.auth_required { |
796 | match &self.token { | |
797 | Token::Plaintext(token) => return Some(token) == req.authorization.as_ref(), | |
798 | Token::Keys(private_key, private_key_subject) => { | |
799 | (private_key.as_str(), private_key_subject) | |
800 | } | |
801 | } | |
802 | } else { | |
803 | assert!(req.authorization.is_none(), "unexpected token"); | |
804 | return true; | |
805 | }; | |
806 | ||
807 | macro_rules! t { | |
808 | ($e:expr) => { | |
809 | match $e { | |
810 | Some(e) => e, | |
811 | None => return false, | |
812 | } | |
813 | }; | |
814 | } | |
815 | ||
816 | let secret: AsymmetricSecretKey<pasetors::version3::V3> = private_key.try_into().unwrap(); | |
817 | let public: AsymmetricPublicKey<pasetors::version3::V3> = (&secret).try_into().unwrap(); | |
818 | let pub_key_id: pasetors::paserk::Id = (&public).into(); | |
819 | let mut paserk_pub_key_id = String::new(); | |
820 | FormatAsPaserk::fmt(&pub_key_id, &mut paserk_pub_key_id).unwrap(); | |
821 | // https://github.com/rust-lang/rfcs/blob/master/text/3231-cargo-asymmetric-tokens.md#how-the-registry-server-will-validate-an-asymmetric-token | |
822 | ||
823 | // - The PASETO is in v3.public format. | |
824 | let authorization = t!(&req.authorization); | |
825 | let untrusted_token = t!( | |
826 | UntrustedToken::<pasetors::Public, pasetors::version3::V3>::try_from(authorization) | |
827 | .ok() | |
828 | ); | |
829 | ||
830 | // - The PASETO validates using the public key it looked up based on the key ID. | |
831 | #[derive(serde::Deserialize, Debug)] | |
832 | struct Footer<'a> { | |
833 | url: &'a str, | |
834 | kip: &'a str, | |
835 | } | |
781aab86 FG |
836 | let footer: Footer<'_> = |
837 | t!(serde_json::from_slice(untrusted_token.untrusted_footer()).ok()); | |
0a29b90c FG |
838 | if footer.kip != paserk_pub_key_id { |
839 | return false; | |
840 | } | |
841 | let trusted_token = | |
842 | t!( | |
843 | pasetors::version3::PublicToken::verify(&public, &untrusted_token, None, None,) | |
844 | .ok() | |
845 | ); | |
846 | ||
847 | // - The URL matches the registry base URL | |
848 | if footer.url != "https://github.com/rust-lang/crates.io-index" | |
849 | && footer.url != &format!("sparse+http://{}/index/", self.addr.to_string()) | |
850 | { | |
0a29b90c FG |
851 | return false; |
852 | } | |
853 | ||
854 | // - The PASETO is still within its valid time period. | |
855 | #[derive(serde::Deserialize)] | |
856 | struct Message<'a> { | |
857 | iat: &'a str, | |
858 | sub: Option<&'a str>, | |
859 | mutation: Option<&'a str>, | |
860 | name: Option<&'a str>, | |
861 | vers: Option<&'a str>, | |
862 | cksum: Option<&'a str>, | |
863 | _challenge: Option<&'a str>, // todo: PASETO with challenges | |
864 | v: Option<u8>, | |
865 | } | |
781aab86 | 866 | let message: Message<'_> = t!(serde_json::from_str(trusted_token.payload()).ok()); |
0a29b90c FG |
867 | let token_time = t!(OffsetDateTime::parse(message.iat, &Rfc3339).ok()); |
868 | let now = OffsetDateTime::now_utc(); | |
869 | if (now - token_time) > Duration::MINUTE { | |
870 | return false; | |
871 | } | |
872 | if private_key_subject.as_deref() != message.sub { | |
0a29b90c FG |
873 | return false; |
874 | } | |
875 | // - If the claim v is set, that it has the value of 1. | |
876 | if let Some(v) = message.v { | |
877 | if v != 1 { | |
0a29b90c FG |
878 | return false; |
879 | } | |
880 | } | |
881 | // - If the server issues challenges, that the challenge has not yet been answered. | |
882 | // todo: PASETO with challenges | |
883 | // - If the operation is a mutation: | |
884 | if let Some(mutation) = mutation { | |
885 | // - That the operation matches the mutation field and is one of publish, yank, or unyank. | |
886 | if message.mutation != Some(mutation.mutation) { | |
0a29b90c FG |
887 | return false; |
888 | } | |
889 | // - That the package, and version match the request. | |
890 | if message.name != mutation.name { | |
0a29b90c FG |
891 | return false; |
892 | } | |
893 | if message.vers != mutation.vers { | |
0a29b90c FG |
894 | return false; |
895 | } | |
896 | // - If the mutation is publish, that the version has not already been published, and that the hash matches the request. | |
897 | if mutation.mutation == "publish" { | |
898 | if message.cksum != mutation.cksum { | |
0a29b90c FG |
899 | return false; |
900 | } | |
901 | } | |
902 | } else { | |
903 | // - If the operation is a read, that the mutation field is not set. | |
904 | if message.mutation.is_some() | |
905 | || message.name.is_some() | |
906 | || message.vers.is_some() | |
907 | || message.cksum.is_some() | |
908 | { | |
909 | return false; | |
910 | } | |
911 | } | |
912 | true | |
913 | } | |
914 | ||
915 | /// Route the request | |
916 | fn route(&self, req: &Request) -> Response { | |
917 | // Check for custom responder | |
918 | if let Some(responder) = self.custom_responders.get(req.url.path()) { | |
919 | return responder(&req, self); | |
920 | } | |
921 | let path: Vec<_> = req.url.path()[1..].split('/').collect(); | |
922 | match (req.method.as_str(), path.as_slice()) { | |
923 | ("get", ["index", ..]) => { | |
924 | if !self.check_authorized(req, None) { | |
925 | self.unauthorized(req) | |
926 | } else { | |
927 | self.index(&req) | |
928 | } | |
929 | } | |
930 | ("get", ["dl", ..]) => { | |
931 | if !self.check_authorized(req, None) { | |
932 | self.unauthorized(req) | |
933 | } else { | |
934 | self.dl(&req) | |
935 | } | |
936 | } | |
937 | // publish | |
938 | ("put", ["api", "v1", "crates", "new"]) => self.check_authorized_publish(req), | |
939 | // The remainder of the operators in the test framework do nothing other than responding 'ok'. | |
940 | // | |
941 | // Note: We don't need to support anything real here because there are no tests that | |
942 | // currently require anything other than publishing via the http api. | |
943 | ||
944 | // yank / unyank | |
945 | ("delete" | "put", ["api", "v1", "crates", crate_name, version, mutation]) => { | |
946 | if !self.check_authorized( | |
947 | req, | |
948 | Some(Mutation { | |
949 | mutation, | |
950 | name: Some(crate_name), | |
951 | vers: Some(version), | |
952 | cksum: None, | |
953 | }), | |
954 | ) { | |
955 | self.unauthorized(req) | |
956 | } else { | |
957 | self.ok(&req) | |
958 | } | |
959 | } | |
960 | // owners | |
961 | ("get" | "put" | "delete", ["api", "v1", "crates", crate_name, "owners"]) => { | |
962 | if !self.check_authorized( | |
963 | req, | |
964 | Some(Mutation { | |
965 | mutation: "owners", | |
966 | name: Some(crate_name), | |
967 | vers: None, | |
968 | cksum: None, | |
969 | }), | |
970 | ) { | |
971 | self.unauthorized(req) | |
972 | } else { | |
973 | self.ok(&req) | |
974 | } | |
975 | } | |
976 | _ => self.not_found(&req), | |
977 | } | |
978 | } | |
979 | ||
980 | /// Unauthorized response | |
981 | pub fn unauthorized(&self, _req: &Request) -> Response { | |
982 | Response { | |
983 | code: 401, | |
984 | headers: vec![ | |
985 | r#"WWW-Authenticate: Cargo login_url="https://test-registry-login/me""#.to_string(), | |
986 | ], | |
987 | body: b"Unauthorized message from server.".to_vec(), | |
988 | } | |
989 | } | |
990 | ||
991 | /// Not found response | |
992 | pub fn not_found(&self, req: &Request) -> Response { | |
993 | (self.not_found_handler)(req, self) | |
994 | } | |
995 | ||
996 | /// Respond OK without doing anything | |
997 | pub fn ok(&self, _req: &Request) -> Response { | |
998 | Response { | |
999 | code: 200, | |
1000 | headers: vec![], | |
1001 | body: br#"{"ok": true, "msg": "completed!"}"#.to_vec(), | |
1002 | } | |
1003 | } | |
1004 | ||
1005 | /// Return an internal server error (HTTP 500) | |
1006 | pub fn internal_server_error(&self, _req: &Request) -> Response { | |
1007 | Response { | |
1008 | code: 500, | |
1009 | headers: vec![], | |
1010 | body: br#"internal server error"#.to_vec(), | |
1011 | } | |
1012 | } | |
1013 | ||
1014 | /// Serve the download endpoint | |
1015 | pub fn dl(&self, req: &Request) -> Response { | |
1016 | let file = self | |
1017 | .dl_path | |
1018 | .join(req.url.path().strip_prefix("/dl/").unwrap()); | |
1019 | println!("{}", file.display()); | |
1020 | if !file.exists() { | |
1021 | return self.not_found(req); | |
1022 | } | |
1023 | return Response { | |
1024 | body: fs::read(&file).unwrap(), | |
1025 | code: 200, | |
1026 | headers: vec![], | |
1027 | }; | |
1028 | } | |
1029 | ||
1030 | /// Serve the registry index | |
1031 | pub fn index(&self, req: &Request) -> Response { | |
1032 | let file = self | |
1033 | .registry_path | |
1034 | .join(req.url.path().strip_prefix("/index/").unwrap()); | |
1035 | if !file.exists() { | |
1036 | return self.not_found(req); | |
1037 | } else { | |
1038 | // Now grab info about the file. | |
1039 | let data = fs::read(&file).unwrap(); | |
1040 | let etag = Sha256::new().update(&data).finish_hex(); | |
1041 | let last_modified = format!("{:?}", file.metadata().unwrap().modified().unwrap()); | |
1042 | ||
1043 | // Start to construct our response: | |
1044 | let mut any_match = false; | |
1045 | let mut all_match = true; | |
1046 | if let Some(expected) = &req.if_none_match { | |
1047 | if &etag != expected { | |
1048 | all_match = false; | |
1049 | } else { | |
1050 | any_match = true; | |
1051 | } | |
1052 | } | |
1053 | if let Some(expected) = &req.if_modified_since { | |
1054 | // NOTE: Equality comparison is good enough for tests. | |
1055 | if &last_modified != expected { | |
1056 | all_match = false; | |
1057 | } else { | |
1058 | any_match = true; | |
1059 | } | |
1060 | } | |
1061 | ||
1062 | if any_match && all_match { | |
1063 | return Response { | |
1064 | body: Vec::new(), | |
1065 | code: 304, | |
1066 | headers: vec![], | |
1067 | }; | |
1068 | } else { | |
1069 | return Response { | |
1070 | body: data, | |
1071 | code: 200, | |
1072 | headers: vec![ | |
1073 | format!("ETag: \"{}\"", etag), | |
1074 | format!("Last-Modified: {}", last_modified), | |
1075 | ], | |
1076 | }; | |
1077 | } | |
1078 | } | |
1079 | } | |
1080 | ||
1081 | pub fn check_authorized_publish(&self, req: &Request) -> Response { | |
1082 | if let Some(body) = &req.body { | |
1083 | // Mimic the publish behavior for local registries by writing out the request | |
1084 | // so tests can verify publishes made to either registry type. | |
1085 | let path = self.api_path.join("api/v1/crates/new"); | |
1086 | t!(fs::create_dir_all(path.parent().unwrap())); | |
1087 | t!(fs::write(&path, body)); | |
1088 | ||
1089 | // Get the metadata of the package | |
1090 | let (len, remaining) = body.split_at(4); | |
1091 | let json_len = u32::from_le_bytes(len.try_into().unwrap()); | |
1092 | let (json, remaining) = remaining.split_at(json_len as usize); | |
1093 | let new_crate = serde_json::from_slice::<crates_io::NewCrate>(json).unwrap(); | |
1094 | // Get the `.crate` file | |
1095 | let (len, remaining) = remaining.split_at(4); | |
1096 | let file_len = u32::from_le_bytes(len.try_into().unwrap()); | |
1097 | let (file, _remaining) = remaining.split_at(file_len as usize); | |
1098 | let file_cksum = cksum(&file); | |
1099 | ||
1100 | if !self.check_authorized( | |
1101 | req, | |
1102 | Some(Mutation { | |
1103 | mutation: "publish", | |
1104 | name: Some(&new_crate.name), | |
1105 | vers: Some(&new_crate.vers), | |
1106 | cksum: Some(&file_cksum), | |
1107 | }), | |
1108 | ) { | |
1109 | return self.unauthorized(req); | |
1110 | } | |
1111 | ||
1112 | let dst = self | |
1113 | .dl_path | |
1114 | .join(&new_crate.name) | |
1115 | .join(&new_crate.vers) | |
1116 | .join("download"); | |
1117 | ||
1118 | if self.delayed_index_update == 0 { | |
1119 | save_new_crate(dst, new_crate, file, file_cksum, &self.registry_path); | |
1120 | } else { | |
1121 | let delayed_index_update = self.delayed_index_update; | |
1122 | let registry_path = self.registry_path.clone(); | |
1123 | let file = Vec::from(file); | |
1124 | thread::spawn(move || { | |
1125 | thread::sleep(std::time::Duration::new(delayed_index_update as u64, 0)); | |
1126 | save_new_crate(dst, new_crate, &file, file_cksum, ®istry_path); | |
1127 | }); | |
1128 | } | |
1129 | ||
1130 | self.ok(&req) | |
1131 | } else { | |
1132 | Response { | |
1133 | code: 400, | |
1134 | headers: vec![], | |
1135 | body: b"The request was missing a body".to_vec(), | |
1136 | } | |
1137 | } | |
1138 | } | |
1139 | } | |
1140 | ||
1141 | fn save_new_crate( | |
1142 | dst: PathBuf, | |
1143 | new_crate: crates_io::NewCrate, | |
1144 | file: &[u8], | |
1145 | file_cksum: String, | |
1146 | registry_path: &Path, | |
1147 | ) { | |
1148 | // Write the `.crate` | |
1149 | t!(fs::create_dir_all(dst.parent().unwrap())); | |
1150 | t!(fs::write(&dst, file)); | |
1151 | ||
1152 | let deps = new_crate | |
1153 | .deps | |
1154 | .iter() | |
1155 | .map(|dep| { | |
1156 | let (name, package) = match &dep.explicit_name_in_toml { | |
1157 | Some(explicit) => (explicit.to_string(), Some(dep.name.to_string())), | |
1158 | None => (dep.name.to_string(), None), | |
1159 | }; | |
1160 | serde_json::json!({ | |
1161 | "name": name, | |
1162 | "req": dep.version_req, | |
1163 | "features": dep.features, | |
1164 | "default_features": true, | |
1165 | "target": dep.target, | |
1166 | "optional": dep.optional, | |
1167 | "kind": dep.kind, | |
1168 | "registry": dep.registry, | |
1169 | "package": package, | |
1170 | }) | |
1171 | }) | |
1172 | .collect::<Vec<_>>(); | |
1173 | ||
1174 | let line = create_index_line( | |
1175 | serde_json::json!(new_crate.name), | |
1176 | &new_crate.vers, | |
1177 | deps, | |
1178 | &file_cksum, | |
1179 | new_crate.features, | |
1180 | false, | |
1181 | new_crate.links, | |
1182 | None, | |
49aad941 | 1183 | None, |
0a29b90c FG |
1184 | ); |
1185 | ||
1186 | write_to_index(registry_path, &new_crate.name, line, false); | |
1187 | } | |
1188 | ||
1189 | impl Package { | |
1190 | /// Creates a new package builder. | |
1191 | /// Call `publish()` to finalize and build the package. | |
1192 | pub fn new(name: &str, vers: &str) -> Package { | |
1193 | let config = paths::home().join(".cargo/config"); | |
1194 | if !config.exists() { | |
1195 | init(); | |
1196 | } | |
1197 | Package { | |
1198 | name: name.to_string(), | |
1199 | vers: vers.to_string(), | |
1200 | deps: Vec::new(), | |
1201 | files: Vec::new(), | |
1202 | yanked: false, | |
1203 | features: BTreeMap::new(), | |
1204 | local: false, | |
1205 | alternative: false, | |
1206 | invalid_json: false, | |
1207 | proc_macro: false, | |
1208 | links: None, | |
1209 | rust_version: None, | |
1210 | cargo_features: Vec::new(), | |
1211 | v: None, | |
1212 | } | |
1213 | } | |
1214 | ||
1215 | /// Call with `true` to publish in a "local registry". | |
1216 | /// | |
1217 | /// See `source-replacement.html#local-registry-sources` for more details | |
1218 | /// on local registries. See `local_registry.rs` for the tests that use | |
1219 | /// this. | |
1220 | pub fn local(&mut self, local: bool) -> &mut Package { | |
1221 | self.local = local; | |
1222 | self | |
1223 | } | |
1224 | ||
1225 | /// Call with `true` to publish in an "alternative registry". | |
1226 | /// | |
1227 | /// The name of the alternative registry is called "alternative". | |
1228 | /// | |
1229 | /// See `src/doc/src/reference/registries.md` for more details on | |
1230 | /// alternative registries. See `alt_registry.rs` for the tests that use | |
1231 | /// this. | |
1232 | pub fn alternative(&mut self, alternative: bool) -> &mut Package { | |
1233 | self.alternative = alternative; | |
1234 | self | |
1235 | } | |
1236 | ||
1237 | /// Adds a file to the package. | |
1238 | pub fn file(&mut self, name: &str, contents: &str) -> &mut Package { | |
1239 | self.file_with_mode(name, DEFAULT_MODE, contents) | |
1240 | } | |
1241 | ||
1242 | /// Adds a file with a specific Unix mode. | |
1243 | pub fn file_with_mode(&mut self, path: &str, mode: u32, contents: &str) -> &mut Package { | |
1244 | self.files.push(PackageFile { | |
1245 | path: path.to_string(), | |
1246 | contents: EntryData::Regular(contents.into()), | |
1247 | mode, | |
1248 | extra: false, | |
1249 | }); | |
1250 | self | |
1251 | } | |
1252 | ||
1253 | /// Adds a symlink to a path to the package. | |
1254 | pub fn symlink(&mut self, dst: &str, src: &str) -> &mut Package { | |
1255 | self.files.push(PackageFile { | |
1256 | path: dst.to_string(), | |
1257 | contents: EntryData::Symlink(src.into()), | |
1258 | mode: DEFAULT_MODE, | |
1259 | extra: false, | |
1260 | }); | |
1261 | self | |
1262 | } | |
1263 | ||
1264 | /// Adds an "extra" file that is not rooted within the package. | |
1265 | /// | |
1266 | /// Normal files are automatically placed within a directory named | |
1267 | /// `$PACKAGE-$VERSION`. This allows you to override that behavior, | |
1268 | /// typically for testing invalid behavior. | |
1269 | pub fn extra_file(&mut self, path: &str, contents: &str) -> &mut Package { | |
1270 | self.files.push(PackageFile { | |
1271 | path: path.to_string(), | |
1272 | contents: EntryData::Regular(contents.to_string()), | |
1273 | mode: DEFAULT_MODE, | |
1274 | extra: true, | |
1275 | }); | |
1276 | self | |
1277 | } | |
1278 | ||
1279 | /// Adds a normal dependency. Example: | |
49aad941 | 1280 | /// ```toml |
0a29b90c FG |
1281 | /// [dependencies] |
1282 | /// foo = {version = "1.0"} | |
1283 | /// ``` | |
1284 | pub fn dep(&mut self, name: &str, vers: &str) -> &mut Package { | |
1285 | self.add_dep(&Dependency::new(name, vers)) | |
1286 | } | |
1287 | ||
1288 | /// Adds a dependency with the given feature. Example: | |
49aad941 | 1289 | /// ```toml |
0a29b90c FG |
1290 | /// [dependencies] |
1291 | /// foo = {version = "1.0", "features": ["feat1", "feat2"]} | |
1292 | /// ``` | |
1293 | pub fn feature_dep(&mut self, name: &str, vers: &str, features: &[&str]) -> &mut Package { | |
1294 | self.add_dep(Dependency::new(name, vers).enable_features(features)) | |
1295 | } | |
1296 | ||
1297 | /// Adds a platform-specific dependency. Example: | |
1298 | /// ```toml | |
1299 | /// [target.'cfg(windows)'.dependencies] | |
1300 | /// foo = {version = "1.0"} | |
1301 | /// ``` | |
1302 | pub fn target_dep(&mut self, name: &str, vers: &str, target: &str) -> &mut Package { | |
1303 | self.add_dep(Dependency::new(name, vers).target(target)) | |
1304 | } | |
1305 | ||
1306 | /// Adds a dependency to the alternative registry. | |
1307 | pub fn registry_dep(&mut self, name: &str, vers: &str) -> &mut Package { | |
1308 | self.add_dep(Dependency::new(name, vers).registry("alternative")) | |
1309 | } | |
1310 | ||
1311 | /// Adds a dev-dependency. Example: | |
49aad941 | 1312 | /// ```toml |
0a29b90c FG |
1313 | /// [dev-dependencies] |
1314 | /// foo = {version = "1.0"} | |
1315 | /// ``` | |
1316 | pub fn dev_dep(&mut self, name: &str, vers: &str) -> &mut Package { | |
1317 | self.add_dep(Dependency::new(name, vers).dev()) | |
1318 | } | |
1319 | ||
1320 | /// Adds a build-dependency. Example: | |
49aad941 | 1321 | /// ```toml |
0a29b90c FG |
1322 | /// [build-dependencies] |
1323 | /// foo = {version = "1.0"} | |
1324 | /// ``` | |
1325 | pub fn build_dep(&mut self, name: &str, vers: &str) -> &mut Package { | |
1326 | self.add_dep(Dependency::new(name, vers).build()) | |
1327 | } | |
1328 | ||
1329 | pub fn add_dep(&mut self, dep: &Dependency) -> &mut Package { | |
1330 | self.deps.push(dep.clone()); | |
1331 | self | |
1332 | } | |
1333 | ||
1334 | /// Specifies whether or not the package is "yanked". | |
1335 | pub fn yanked(&mut self, yanked: bool) -> &mut Package { | |
1336 | self.yanked = yanked; | |
1337 | self | |
1338 | } | |
1339 | ||
1340 | /// Specifies whether or not this is a proc macro. | |
1341 | pub fn proc_macro(&mut self, proc_macro: bool) -> &mut Package { | |
1342 | self.proc_macro = proc_macro; | |
1343 | self | |
1344 | } | |
1345 | ||
1346 | /// Adds an entry in the `[features]` section. | |
1347 | pub fn feature(&mut self, name: &str, deps: &[&str]) -> &mut Package { | |
1348 | let deps = deps.iter().map(|s| s.to_string()).collect(); | |
1349 | self.features.insert(name.to_string(), deps); | |
1350 | self | |
1351 | } | |
1352 | ||
1353 | /// Specify a minimal Rust version. | |
1354 | pub fn rust_version(&mut self, rust_version: &str) -> &mut Package { | |
1355 | self.rust_version = Some(rust_version.into()); | |
1356 | self | |
1357 | } | |
1358 | ||
1359 | /// Causes the JSON line emitted in the index to be invalid, presumably | |
1360 | /// causing Cargo to skip over this version. | |
1361 | pub fn invalid_json(&mut self, invalid: bool) -> &mut Package { | |
1362 | self.invalid_json = invalid; | |
1363 | self | |
1364 | } | |
1365 | ||
1366 | pub fn links(&mut self, links: &str) -> &mut Package { | |
1367 | self.links = Some(links.to_string()); | |
1368 | self | |
1369 | } | |
1370 | ||
1371 | pub fn cargo_feature(&mut self, feature: &str) -> &mut Package { | |
1372 | self.cargo_features.push(feature.to_owned()); | |
1373 | self | |
1374 | } | |
1375 | ||
1376 | /// Sets the index schema version for this package. | |
1377 | /// | |
fe692bf9 | 1378 | /// See `cargo::sources::registry::IndexPackage` for more information. |
0a29b90c FG |
1379 | pub fn schema_version(&mut self, version: u32) -> &mut Package { |
1380 | self.v = Some(version); | |
1381 | self | |
1382 | } | |
1383 | ||
1384 | /// Creates the package and place it in the registry. | |
1385 | /// | |
1386 | /// This does not actually use Cargo's publishing system, but instead | |
1387 | /// manually creates the entry in the registry on the filesystem. | |
1388 | /// | |
1389 | /// Returns the checksum for the package. | |
1390 | pub fn publish(&self) -> String { | |
1391 | self.make_archive(); | |
1392 | ||
1393 | // Figure out what we're going to write into the index. | |
1394 | let deps = self | |
1395 | .deps | |
1396 | .iter() | |
1397 | .map(|dep| { | |
1398 | // In the index, the `registry` is null if it is from the same registry. | |
1399 | // In Cargo.toml, it is None if it is from crates.io. | |
1400 | let registry_url = match (self.alternative, dep.registry.as_deref()) { | |
1401 | (false, None) => None, | |
1402 | (false, Some("alternative")) => Some(alt_registry_url().to_string()), | |
1403 | (true, None) => { | |
1404 | Some("https://github.com/rust-lang/crates.io-index".to_string()) | |
1405 | } | |
1406 | (true, Some("alternative")) => None, | |
1407 | _ => panic!("registry_dep currently only supports `alternative`"), | |
1408 | }; | |
781aab86 FG |
1409 | let artifact = if let Some(artifact) = &dep.artifact { |
1410 | serde_json::json!([artifact]) | |
1411 | } else { | |
1412 | serde_json::json!(null) | |
1413 | }; | |
0a29b90c FG |
1414 | serde_json::json!({ |
1415 | "name": dep.name, | |
1416 | "req": dep.vers, | |
1417 | "features": dep.features, | |
1418 | "default_features": true, | |
1419 | "target": dep.target, | |
781aab86 FG |
1420 | "artifact": artifact, |
1421 | "bindep_target": dep.bindep_target, | |
1422 | "lib": dep.lib, | |
0a29b90c FG |
1423 | "optional": dep.optional, |
1424 | "kind": dep.kind, | |
1425 | "registry": registry_url, | |
1426 | "package": dep.package, | |
1427 | }) | |
1428 | }) | |
1429 | .collect::<Vec<_>>(); | |
1430 | let cksum = { | |
1431 | let c = t!(fs::read(&self.archive_dst())); | |
1432 | cksum(&c) | |
1433 | }; | |
1434 | let name = if self.invalid_json { | |
1435 | serde_json::json!(1) | |
1436 | } else { | |
1437 | serde_json::json!(self.name) | |
1438 | }; | |
1439 | let line = create_index_line( | |
1440 | name, | |
1441 | &self.vers, | |
1442 | deps, | |
1443 | &cksum, | |
1444 | self.features.clone(), | |
1445 | self.yanked, | |
1446 | self.links.clone(), | |
49aad941 | 1447 | self.rust_version.as_deref(), |
0a29b90c FG |
1448 | self.v, |
1449 | ); | |
1450 | ||
1451 | let registry_path = if self.alternative { | |
1452 | alt_registry_path() | |
1453 | } else { | |
1454 | registry_path() | |
1455 | }; | |
1456 | ||
1457 | write_to_index(®istry_path, &self.name, line, self.local); | |
1458 | ||
1459 | cksum | |
1460 | } | |
1461 | ||
1462 | fn make_archive(&self) { | |
1463 | let dst = self.archive_dst(); | |
1464 | t!(fs::create_dir_all(dst.parent().unwrap())); | |
1465 | let f = t!(File::create(&dst)); | |
1466 | let mut a = Builder::new(GzEncoder::new(f, Compression::default())); | |
1467 | ||
1468 | if !self | |
1469 | .files | |
1470 | .iter() | |
1471 | .any(|PackageFile { path, .. }| path == "Cargo.toml") | |
1472 | { | |
1473 | self.append_manifest(&mut a); | |
1474 | } | |
1475 | if self.files.is_empty() { | |
1476 | self.append( | |
1477 | &mut a, | |
1478 | "src/lib.rs", | |
1479 | DEFAULT_MODE, | |
1480 | &EntryData::Regular("".into()), | |
1481 | ); | |
1482 | } else { | |
1483 | for PackageFile { | |
1484 | path, | |
1485 | contents, | |
1486 | mode, | |
1487 | extra, | |
1488 | } in &self.files | |
1489 | { | |
1490 | if *extra { | |
1491 | self.append_raw(&mut a, path, *mode, contents); | |
1492 | } else { | |
1493 | self.append(&mut a, path, *mode, contents); | |
1494 | } | |
1495 | } | |
1496 | } | |
1497 | } | |
1498 | ||
1499 | fn append_manifest<W: Write>(&self, ar: &mut Builder<W>) { | |
1500 | let mut manifest = String::new(); | |
1501 | ||
1502 | if !self.cargo_features.is_empty() { | |
1503 | let mut features = String::new(); | |
1504 | serde::Serialize::serialize( | |
1505 | &self.cargo_features, | |
1506 | toml::ser::ValueSerializer::new(&mut features), | |
1507 | ) | |
1508 | .unwrap(); | |
1509 | manifest.push_str(&format!("cargo-features = {}\n\n", features)); | |
1510 | } | |
1511 | ||
1512 | manifest.push_str(&format!( | |
1513 | r#" | |
1514 | [package] | |
1515 | name = "{}" | |
1516 | version = "{}" | |
1517 | authors = [] | |
1518 | "#, | |
1519 | self.name, self.vers | |
1520 | )); | |
1521 | ||
1522 | if let Some(version) = &self.rust_version { | |
1523 | manifest.push_str(&format!("rust-version = \"{}\"", version)); | |
1524 | } | |
1525 | ||
1526 | for dep in self.deps.iter() { | |
1527 | let target = match dep.target { | |
1528 | None => String::new(), | |
1529 | Some(ref s) => format!("target.'{}'.", s), | |
1530 | }; | |
1531 | let kind = match &dep.kind[..] { | |
1532 | "build" => "build-", | |
1533 | "dev" => "dev-", | |
1534 | _ => "", | |
1535 | }; | |
1536 | manifest.push_str(&format!( | |
1537 | r#" | |
1538 | [{}{}dependencies.{}] | |
1539 | version = "{}" | |
1540 | "#, | |
1541 | target, kind, dep.name, dep.vers | |
1542 | )); | |
781aab86 | 1543 | if let Some(artifact) = &dep.artifact { |
0a29b90c | 1544 | manifest.push_str(&format!("artifact = \"{}\"\n", artifact)); |
781aab86 FG |
1545 | } |
1546 | if let Some(target) = &dep.bindep_target { | |
1547 | manifest.push_str(&format!("target = \"{}\"\n", target)); | |
1548 | } | |
1549 | if dep.lib { | |
1550 | manifest.push_str("lib = true\n"); | |
0a29b90c FG |
1551 | } |
1552 | if let Some(registry) = &dep.registry { | |
1553 | assert_eq!(registry, "alternative"); | |
1554 | manifest.push_str(&format!("registry-index = \"{}\"", alt_registry_url())); | |
1555 | } | |
1556 | } | |
1557 | if self.proc_macro { | |
1558 | manifest.push_str("[lib]\nproc-macro = true\n"); | |
1559 | } | |
1560 | ||
1561 | self.append( | |
1562 | ar, | |
1563 | "Cargo.toml", | |
1564 | DEFAULT_MODE, | |
1565 | &EntryData::Regular(manifest.into()), | |
1566 | ); | |
1567 | } | |
1568 | ||
1569 | fn append<W: Write>(&self, ar: &mut Builder<W>, file: &str, mode: u32, contents: &EntryData) { | |
1570 | self.append_raw( | |
1571 | ar, | |
1572 | &format!("{}-{}/{}", self.name, self.vers, file), | |
1573 | mode, | |
1574 | contents, | |
1575 | ); | |
1576 | } | |
1577 | ||
1578 | fn append_raw<W: Write>( | |
1579 | &self, | |
1580 | ar: &mut Builder<W>, | |
1581 | path: &str, | |
1582 | mode: u32, | |
1583 | contents: &EntryData, | |
1584 | ) { | |
1585 | let mut header = Header::new_ustar(); | |
1586 | let contents = match contents { | |
1587 | EntryData::Regular(contents) => contents.as_str(), | |
1588 | EntryData::Symlink(src) => { | |
1589 | header.set_entry_type(tar::EntryType::Symlink); | |
1590 | t!(header.set_link_name(src)); | |
1591 | "" // Symlink has no contents. | |
1592 | } | |
1593 | }; | |
1594 | header.set_size(contents.len() as u64); | |
1595 | t!(header.set_path(path)); | |
1596 | header.set_mode(mode); | |
1597 | header.set_cksum(); | |
1598 | t!(ar.append(&header, contents.as_bytes())); | |
1599 | } | |
1600 | ||
1601 | /// Returns the path to the compressed package file. | |
1602 | pub fn archive_dst(&self) -> PathBuf { | |
1603 | if self.local { | |
1604 | registry_path().join(format!("{}-{}.crate", self.name, self.vers)) | |
1605 | } else if self.alternative { | |
1606 | alt_dl_path() | |
1607 | .join(&self.name) | |
1608 | .join(&self.vers) | |
1609 | .join("download") | |
1610 | } else { | |
1611 | dl_path().join(&self.name).join(&self.vers).join("download") | |
1612 | } | |
1613 | } | |
1614 | } | |
1615 | ||
1616 | pub fn cksum(s: &[u8]) -> String { | |
1617 | Sha256::new().update(s).finish_hex() | |
1618 | } | |
1619 | ||
1620 | impl Dependency { | |
1621 | pub fn new(name: &str, vers: &str) -> Dependency { | |
1622 | Dependency { | |
1623 | name: name.to_string(), | |
1624 | vers: vers.to_string(), | |
1625 | kind: "normal".to_string(), | |
1626 | artifact: None, | |
781aab86 FG |
1627 | bindep_target: None, |
1628 | lib: false, | |
0a29b90c FG |
1629 | target: None, |
1630 | features: Vec::new(), | |
1631 | package: None, | |
1632 | optional: false, | |
1633 | registry: None, | |
1634 | } | |
1635 | } | |
1636 | ||
1637 | /// Changes this to `[build-dependencies]`. | |
1638 | pub fn build(&mut self) -> &mut Self { | |
1639 | self.kind = "build".to_string(); | |
1640 | self | |
1641 | } | |
1642 | ||
1643 | /// Changes this to `[dev-dependencies]`. | |
1644 | pub fn dev(&mut self) -> &mut Self { | |
1645 | self.kind = "dev".to_string(); | |
1646 | self | |
1647 | } | |
1648 | ||
1649 | /// Changes this to `[target.$target.dependencies]`. | |
1650 | pub fn target(&mut self, target: &str) -> &mut Self { | |
1651 | self.target = Some(target.to_string()); | |
1652 | self | |
1653 | } | |
1654 | ||
1655 | /// Change the artifact to be of the given kind, like "bin", or "staticlib", | |
1656 | /// along with a specific target triple if provided. | |
1657 | pub fn artifact(&mut self, kind: &str, target: Option<String>) -> &mut Self { | |
781aab86 FG |
1658 | self.artifact = Some(kind.to_string()); |
1659 | self.bindep_target = target; | |
0a29b90c FG |
1660 | self |
1661 | } | |
1662 | ||
1663 | /// Adds `registry = $registry` to this dependency. | |
1664 | pub fn registry(&mut self, registry: &str) -> &mut Self { | |
1665 | self.registry = Some(registry.to_string()); | |
1666 | self | |
1667 | } | |
1668 | ||
1669 | /// Adds `features = [ ... ]` to this dependency. | |
1670 | pub fn enable_features(&mut self, features: &[&str]) -> &mut Self { | |
1671 | self.features.extend(features.iter().map(|s| s.to_string())); | |
1672 | self | |
1673 | } | |
1674 | ||
1675 | /// Adds `package = ...` to this dependency. | |
1676 | pub fn package(&mut self, pkg: &str) -> &mut Self { | |
1677 | self.package = Some(pkg.to_string()); | |
1678 | self | |
1679 | } | |
1680 | ||
1681 | /// Changes this to an optional dependency. | |
1682 | pub fn optional(&mut self, optional: bool) -> &mut Self { | |
1683 | self.optional = optional; | |
1684 | self | |
1685 | } | |
1686 | } |