]>
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, | |
4b012472 | 560 | default_features: bool, |
c0240ec0 | 561 | public: bool, |
0a29b90c FG |
562 | } |
563 | ||
564 | /// Entry with data that corresponds to [`tar::EntryType`]. | |
565 | #[non_exhaustive] | |
566 | enum EntryData { | |
567 | Regular(String), | |
568 | Symlink(PathBuf), | |
569 | } | |
570 | ||
571 | /// A file to be created in a package. | |
572 | struct PackageFile { | |
573 | path: String, | |
574 | contents: EntryData, | |
575 | /// The Unix mode for the file. Note that when extracted on Windows, this | |
576 | /// is mostly ignored since it doesn't have the same style of permissions. | |
577 | mode: u32, | |
578 | /// If `true`, the file is created in the root of the tarfile, used for | |
579 | /// testing invalid packages. | |
580 | extra: bool, | |
581 | } | |
582 | ||
583 | const DEFAULT_MODE: u32 = 0o644; | |
584 | ||
585 | /// Initializes the on-disk registry and sets up the config so that crates.io | |
586 | /// is replaced with the one on disk. | |
587 | pub fn init() -> TestRegistry { | |
588 | RegistryBuilder::new().build() | |
589 | } | |
590 | ||
591 | /// Variant of `init` that initializes the "alternative" registry and crates.io | |
592 | /// replacement. | |
593 | pub fn alt_init() -> TestRegistry { | |
594 | init(); | |
595 | RegistryBuilder::new().alternative().build() | |
596 | } | |
597 | ||
598 | pub struct HttpServerHandle { | |
599 | addr: SocketAddr, | |
600 | handle: Option<JoinHandle<()>>, | |
601 | } | |
602 | ||
603 | impl HttpServerHandle { | |
604 | pub fn index_url(&self) -> Url { | |
605 | Url::parse(&format!("sparse+http://{}/index/", self.addr.to_string())).unwrap() | |
606 | } | |
607 | ||
608 | pub fn api_url(&self) -> Url { | |
609 | Url::parse(&format!("http://{}/", self.addr.to_string())).unwrap() | |
610 | } | |
611 | ||
612 | pub fn dl_url(&self) -> Url { | |
613 | Url::parse(&format!("http://{}/dl", self.addr.to_string())).unwrap() | |
614 | } | |
615 | ||
616 | fn stop(&self) { | |
617 | if let Ok(mut stream) = TcpStream::connect(self.addr) { | |
618 | // shutdown the server | |
619 | let _ = stream.write_all(b"stop"); | |
620 | let _ = stream.flush(); | |
621 | } | |
622 | } | |
623 | } | |
624 | ||
625 | impl Drop for HttpServerHandle { | |
626 | fn drop(&mut self) { | |
627 | self.stop(); | |
628 | } | |
629 | } | |
630 | ||
631 | /// Request to the test http server | |
632 | #[derive(Clone)] | |
633 | pub struct Request { | |
634 | pub url: Url, | |
635 | pub method: String, | |
636 | pub body: Option<Vec<u8>>, | |
637 | pub authorization: Option<String>, | |
638 | pub if_modified_since: Option<String>, | |
639 | pub if_none_match: Option<String>, | |
640 | } | |
641 | ||
642 | impl fmt::Debug for Request { | |
643 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
644 | // body is not included as it can produce long debug outputs | |
645 | f.debug_struct("Request") | |
646 | .field("url", &self.url) | |
647 | .field("method", &self.method) | |
648 | .field("authorization", &self.authorization) | |
649 | .field("if_modified_since", &self.if_modified_since) | |
650 | .field("if_none_match", &self.if_none_match) | |
651 | .finish() | |
652 | } | |
653 | } | |
654 | ||
655 | /// Response from the test http server | |
656 | pub struct Response { | |
657 | pub code: u32, | |
658 | pub headers: Vec<String>, | |
659 | pub body: Vec<u8>, | |
660 | } | |
661 | ||
662 | pub struct HttpServer { | |
663 | listener: TcpListener, | |
664 | registry_path: PathBuf, | |
665 | dl_path: PathBuf, | |
666 | api_path: PathBuf, | |
667 | addr: SocketAddr, | |
668 | token: Token, | |
669 | auth_required: bool, | |
670 | custom_responders: HashMap<String, RequestCallback>, | |
671 | not_found_handler: RequestCallback, | |
672 | delayed_index_update: usize, | |
673 | } | |
674 | ||
675 | /// A helper struct that collects the arguments for [`HttpServer::check_authorized`]. | |
676 | /// Based on looking at the request, these are the fields that the authentication header should attest to. | |
677 | struct Mutation<'a> { | |
678 | mutation: &'a str, | |
679 | name: Option<&'a str>, | |
680 | vers: Option<&'a str>, | |
681 | cksum: Option<&'a str>, | |
682 | } | |
683 | ||
684 | impl HttpServer { | |
685 | pub fn new( | |
686 | registry_path: PathBuf, | |
687 | dl_path: PathBuf, | |
688 | api_path: PathBuf, | |
689 | token: Token, | |
690 | auth_required: bool, | |
691 | custom_responders: HashMap<String, RequestCallback>, | |
692 | not_found_handler: RequestCallback, | |
693 | delayed_index_update: usize, | |
694 | ) -> HttpServerHandle { | |
695 | let listener = TcpListener::bind("127.0.0.1:0").unwrap(); | |
696 | let addr = listener.local_addr().unwrap(); | |
697 | let server = HttpServer { | |
698 | listener, | |
699 | registry_path, | |
700 | dl_path, | |
701 | api_path, | |
702 | addr, | |
703 | token, | |
704 | auth_required, | |
705 | custom_responders, | |
706 | not_found_handler, | |
707 | delayed_index_update, | |
708 | }; | |
709 | let handle = Some(thread::spawn(move || server.start())); | |
710 | HttpServerHandle { addr, handle } | |
711 | } | |
712 | ||
713 | fn start(&self) { | |
714 | let mut line = String::new(); | |
715 | 'server: loop { | |
716 | let (socket, _) = self.listener.accept().unwrap(); | |
717 | let mut buf = BufReader::new(socket); | |
718 | line.clear(); | |
719 | if buf.read_line(&mut line).unwrap() == 0 { | |
720 | // Connection terminated. | |
721 | continue; | |
722 | } | |
723 | // Read the "GET path HTTP/1.1" line. | |
724 | let mut parts = line.split_ascii_whitespace(); | |
725 | let method = parts.next().unwrap().to_ascii_lowercase(); | |
726 | if method == "stop" { | |
727 | // Shutdown the server. | |
728 | return; | |
729 | } | |
730 | let addr = self.listener.local_addr().unwrap(); | |
731 | let url = format!( | |
732 | "http://{}/{}", | |
733 | addr, | |
734 | parts.next().unwrap().trim_start_matches('/') | |
735 | ); | |
736 | let url = Url::parse(&url).unwrap(); | |
737 | ||
738 | // Grab headers we care about. | |
739 | let mut if_modified_since = None; | |
740 | let mut if_none_match = None; | |
741 | let mut authorization = None; | |
742 | let mut content_len = None; | |
743 | loop { | |
744 | line.clear(); | |
745 | if buf.read_line(&mut line).unwrap() == 0 { | |
746 | continue 'server; | |
747 | } | |
748 | if line == "\r\n" { | |
749 | // End of headers. | |
750 | line.clear(); | |
751 | break; | |
752 | } | |
753 | let (name, value) = line.split_once(':').unwrap(); | |
754 | let name = name.trim().to_ascii_lowercase(); | |
755 | let value = value.trim().to_string(); | |
756 | match name.as_str() { | |
757 | "if-modified-since" => if_modified_since = Some(value), | |
758 | "if-none-match" => if_none_match = Some(value), | |
759 | "authorization" => authorization = Some(value), | |
760 | "content-length" => content_len = Some(value), | |
761 | _ => {} | |
762 | } | |
763 | } | |
764 | ||
765 | let mut body = None; | |
766 | if let Some(con_len) = content_len { | |
767 | let len = con_len.parse::<u64>().unwrap(); | |
768 | let mut content = vec![0u8; len as usize]; | |
769 | buf.read_exact(&mut content).unwrap(); | |
770 | body = Some(content) | |
771 | } | |
772 | ||
773 | let req = Request { | |
774 | authorization, | |
775 | if_modified_since, | |
776 | if_none_match, | |
777 | method, | |
778 | url, | |
779 | body, | |
780 | }; | |
781 | println!("req: {:#?}", req); | |
782 | let response = self.route(&req); | |
783 | let buf = buf.get_mut(); | |
784 | write!(buf, "HTTP/1.1 {}\r\n", response.code).unwrap(); | |
785 | write!(buf, "Content-Length: {}\r\n", response.body.len()).unwrap(); | |
781aab86 | 786 | write!(buf, "Connection: close\r\n").unwrap(); |
0a29b90c FG |
787 | for header in response.headers { |
788 | write!(buf, "{}\r\n", header).unwrap(); | |
789 | } | |
790 | write!(buf, "\r\n").unwrap(); | |
791 | buf.write_all(&response.body).unwrap(); | |
792 | buf.flush().unwrap(); | |
793 | } | |
794 | } | |
795 | ||
781aab86 | 796 | fn check_authorized(&self, req: &Request, mutation: Option<Mutation<'_>>) -> bool { |
0a29b90c FG |
797 | let (private_key, private_key_subject) = if mutation.is_some() || self.auth_required { |
798 | match &self.token { | |
799 | Token::Plaintext(token) => return Some(token) == req.authorization.as_ref(), | |
800 | Token::Keys(private_key, private_key_subject) => { | |
801 | (private_key.as_str(), private_key_subject) | |
802 | } | |
803 | } | |
804 | } else { | |
805 | assert!(req.authorization.is_none(), "unexpected token"); | |
806 | return true; | |
807 | }; | |
808 | ||
809 | macro_rules! t { | |
810 | ($e:expr) => { | |
811 | match $e { | |
812 | Some(e) => e, | |
813 | None => return false, | |
814 | } | |
815 | }; | |
816 | } | |
817 | ||
818 | let secret: AsymmetricSecretKey<pasetors::version3::V3> = private_key.try_into().unwrap(); | |
819 | let public: AsymmetricPublicKey<pasetors::version3::V3> = (&secret).try_into().unwrap(); | |
820 | let pub_key_id: pasetors::paserk::Id = (&public).into(); | |
821 | let mut paserk_pub_key_id = String::new(); | |
822 | FormatAsPaserk::fmt(&pub_key_id, &mut paserk_pub_key_id).unwrap(); | |
823 | // https://github.com/rust-lang/rfcs/blob/master/text/3231-cargo-asymmetric-tokens.md#how-the-registry-server-will-validate-an-asymmetric-token | |
824 | ||
825 | // - The PASETO is in v3.public format. | |
826 | let authorization = t!(&req.authorization); | |
827 | let untrusted_token = t!( | |
828 | UntrustedToken::<pasetors::Public, pasetors::version3::V3>::try_from(authorization) | |
829 | .ok() | |
830 | ); | |
831 | ||
832 | // - The PASETO validates using the public key it looked up based on the key ID. | |
833 | #[derive(serde::Deserialize, Debug)] | |
834 | struct Footer<'a> { | |
835 | url: &'a str, | |
836 | kip: &'a str, | |
837 | } | |
781aab86 FG |
838 | let footer: Footer<'_> = |
839 | t!(serde_json::from_slice(untrusted_token.untrusted_footer()).ok()); | |
0a29b90c FG |
840 | if footer.kip != paserk_pub_key_id { |
841 | return false; | |
842 | } | |
843 | let trusted_token = | |
844 | t!( | |
845 | pasetors::version3::PublicToken::verify(&public, &untrusted_token, None, None,) | |
846 | .ok() | |
847 | ); | |
848 | ||
849 | // - The URL matches the registry base URL | |
850 | if footer.url != "https://github.com/rust-lang/crates.io-index" | |
851 | && footer.url != &format!("sparse+http://{}/index/", self.addr.to_string()) | |
852 | { | |
0a29b90c FG |
853 | return false; |
854 | } | |
855 | ||
856 | // - The PASETO is still within its valid time period. | |
857 | #[derive(serde::Deserialize)] | |
858 | struct Message<'a> { | |
859 | iat: &'a str, | |
860 | sub: Option<&'a str>, | |
861 | mutation: Option<&'a str>, | |
862 | name: Option<&'a str>, | |
863 | vers: Option<&'a str>, | |
864 | cksum: Option<&'a str>, | |
865 | _challenge: Option<&'a str>, // todo: PASETO with challenges | |
866 | v: Option<u8>, | |
867 | } | |
781aab86 | 868 | let message: Message<'_> = t!(serde_json::from_str(trusted_token.payload()).ok()); |
0a29b90c FG |
869 | let token_time = t!(OffsetDateTime::parse(message.iat, &Rfc3339).ok()); |
870 | let now = OffsetDateTime::now_utc(); | |
871 | if (now - token_time) > Duration::MINUTE { | |
872 | return false; | |
873 | } | |
874 | if private_key_subject.as_deref() != message.sub { | |
0a29b90c FG |
875 | return false; |
876 | } | |
877 | // - If the claim v is set, that it has the value of 1. | |
878 | if let Some(v) = message.v { | |
879 | if v != 1 { | |
0a29b90c FG |
880 | return false; |
881 | } | |
882 | } | |
883 | // - If the server issues challenges, that the challenge has not yet been answered. | |
884 | // todo: PASETO with challenges | |
885 | // - If the operation is a mutation: | |
886 | if let Some(mutation) = mutation { | |
887 | // - That the operation matches the mutation field and is one of publish, yank, or unyank. | |
888 | if message.mutation != Some(mutation.mutation) { | |
0a29b90c FG |
889 | return false; |
890 | } | |
891 | // - That the package, and version match the request. | |
892 | if message.name != mutation.name { | |
0a29b90c FG |
893 | return false; |
894 | } | |
895 | if message.vers != mutation.vers { | |
0a29b90c FG |
896 | return false; |
897 | } | |
898 | // - If the mutation is publish, that the version has not already been published, and that the hash matches the request. | |
899 | if mutation.mutation == "publish" { | |
900 | if message.cksum != mutation.cksum { | |
0a29b90c FG |
901 | return false; |
902 | } | |
903 | } | |
904 | } else { | |
905 | // - If the operation is a read, that the mutation field is not set. | |
906 | if message.mutation.is_some() | |
907 | || message.name.is_some() | |
908 | || message.vers.is_some() | |
909 | || message.cksum.is_some() | |
910 | { | |
911 | return false; | |
912 | } | |
913 | } | |
914 | true | |
915 | } | |
916 | ||
917 | /// Route the request | |
918 | fn route(&self, req: &Request) -> Response { | |
919 | // Check for custom responder | |
920 | if let Some(responder) = self.custom_responders.get(req.url.path()) { | |
921 | return responder(&req, self); | |
922 | } | |
923 | let path: Vec<_> = req.url.path()[1..].split('/').collect(); | |
924 | match (req.method.as_str(), path.as_slice()) { | |
925 | ("get", ["index", ..]) => { | |
926 | if !self.check_authorized(req, None) { | |
927 | self.unauthorized(req) | |
928 | } else { | |
929 | self.index(&req) | |
930 | } | |
931 | } | |
932 | ("get", ["dl", ..]) => { | |
933 | if !self.check_authorized(req, None) { | |
934 | self.unauthorized(req) | |
935 | } else { | |
936 | self.dl(&req) | |
937 | } | |
938 | } | |
939 | // publish | |
940 | ("put", ["api", "v1", "crates", "new"]) => self.check_authorized_publish(req), | |
941 | // The remainder of the operators in the test framework do nothing other than responding 'ok'. | |
942 | // | |
943 | // Note: We don't need to support anything real here because there are no tests that | |
944 | // currently require anything other than publishing via the http api. | |
945 | ||
946 | // yank / unyank | |
947 | ("delete" | "put", ["api", "v1", "crates", crate_name, version, mutation]) => { | |
948 | if !self.check_authorized( | |
949 | req, | |
950 | Some(Mutation { | |
951 | mutation, | |
952 | name: Some(crate_name), | |
953 | vers: Some(version), | |
954 | cksum: None, | |
955 | }), | |
956 | ) { | |
957 | self.unauthorized(req) | |
958 | } else { | |
959 | self.ok(&req) | |
960 | } | |
961 | } | |
962 | // owners | |
963 | ("get" | "put" | "delete", ["api", "v1", "crates", crate_name, "owners"]) => { | |
964 | if !self.check_authorized( | |
965 | req, | |
966 | Some(Mutation { | |
967 | mutation: "owners", | |
968 | name: Some(crate_name), | |
969 | vers: None, | |
970 | cksum: None, | |
971 | }), | |
972 | ) { | |
973 | self.unauthorized(req) | |
974 | } else { | |
975 | self.ok(&req) | |
976 | } | |
977 | } | |
978 | _ => self.not_found(&req), | |
979 | } | |
980 | } | |
981 | ||
982 | /// Unauthorized response | |
983 | pub fn unauthorized(&self, _req: &Request) -> Response { | |
984 | Response { | |
985 | code: 401, | |
986 | headers: vec![ | |
987 | r#"WWW-Authenticate: Cargo login_url="https://test-registry-login/me""#.to_string(), | |
988 | ], | |
989 | body: b"Unauthorized message from server.".to_vec(), | |
990 | } | |
991 | } | |
992 | ||
993 | /// Not found response | |
994 | pub fn not_found(&self, req: &Request) -> Response { | |
995 | (self.not_found_handler)(req, self) | |
996 | } | |
997 | ||
998 | /// Respond OK without doing anything | |
999 | pub fn ok(&self, _req: &Request) -> Response { | |
1000 | Response { | |
1001 | code: 200, | |
1002 | headers: vec![], | |
1003 | body: br#"{"ok": true, "msg": "completed!"}"#.to_vec(), | |
1004 | } | |
1005 | } | |
1006 | ||
1007 | /// Return an internal server error (HTTP 500) | |
1008 | pub fn internal_server_error(&self, _req: &Request) -> Response { | |
1009 | Response { | |
1010 | code: 500, | |
1011 | headers: vec![], | |
1012 | body: br#"internal server error"#.to_vec(), | |
1013 | } | |
1014 | } | |
1015 | ||
1016 | /// Serve the download endpoint | |
1017 | pub fn dl(&self, req: &Request) -> Response { | |
1018 | let file = self | |
1019 | .dl_path | |
1020 | .join(req.url.path().strip_prefix("/dl/").unwrap()); | |
1021 | println!("{}", file.display()); | |
1022 | if !file.exists() { | |
1023 | return self.not_found(req); | |
1024 | } | |
1025 | return Response { | |
1026 | body: fs::read(&file).unwrap(), | |
1027 | code: 200, | |
1028 | headers: vec![], | |
1029 | }; | |
1030 | } | |
1031 | ||
1032 | /// Serve the registry index | |
1033 | pub fn index(&self, req: &Request) -> Response { | |
1034 | let file = self | |
1035 | .registry_path | |
1036 | .join(req.url.path().strip_prefix("/index/").unwrap()); | |
1037 | if !file.exists() { | |
1038 | return self.not_found(req); | |
1039 | } else { | |
1040 | // Now grab info about the file. | |
1041 | let data = fs::read(&file).unwrap(); | |
1042 | let etag = Sha256::new().update(&data).finish_hex(); | |
1043 | let last_modified = format!("{:?}", file.metadata().unwrap().modified().unwrap()); | |
1044 | ||
1045 | // Start to construct our response: | |
1046 | let mut any_match = false; | |
1047 | let mut all_match = true; | |
1048 | if let Some(expected) = &req.if_none_match { | |
1049 | if &etag != expected { | |
1050 | all_match = false; | |
1051 | } else { | |
1052 | any_match = true; | |
1053 | } | |
1054 | } | |
1055 | if let Some(expected) = &req.if_modified_since { | |
1056 | // NOTE: Equality comparison is good enough for tests. | |
1057 | if &last_modified != expected { | |
1058 | all_match = false; | |
1059 | } else { | |
1060 | any_match = true; | |
1061 | } | |
1062 | } | |
1063 | ||
1064 | if any_match && all_match { | |
1065 | return Response { | |
1066 | body: Vec::new(), | |
1067 | code: 304, | |
1068 | headers: vec![], | |
1069 | }; | |
1070 | } else { | |
1071 | return Response { | |
1072 | body: data, | |
1073 | code: 200, | |
1074 | headers: vec![ | |
1075 | format!("ETag: \"{}\"", etag), | |
1076 | format!("Last-Modified: {}", last_modified), | |
1077 | ], | |
1078 | }; | |
1079 | } | |
1080 | } | |
1081 | } | |
1082 | ||
1083 | pub fn check_authorized_publish(&self, req: &Request) -> Response { | |
1084 | if let Some(body) = &req.body { | |
1085 | // Mimic the publish behavior for local registries by writing out the request | |
1086 | // so tests can verify publishes made to either registry type. | |
1087 | let path = self.api_path.join("api/v1/crates/new"); | |
1088 | t!(fs::create_dir_all(path.parent().unwrap())); | |
1089 | t!(fs::write(&path, body)); | |
1090 | ||
1091 | // Get the metadata of the package | |
1092 | let (len, remaining) = body.split_at(4); | |
1093 | let json_len = u32::from_le_bytes(len.try_into().unwrap()); | |
1094 | let (json, remaining) = remaining.split_at(json_len as usize); | |
1095 | let new_crate = serde_json::from_slice::<crates_io::NewCrate>(json).unwrap(); | |
1096 | // Get the `.crate` file | |
1097 | let (len, remaining) = remaining.split_at(4); | |
1098 | let file_len = u32::from_le_bytes(len.try_into().unwrap()); | |
1099 | let (file, _remaining) = remaining.split_at(file_len as usize); | |
1100 | let file_cksum = cksum(&file); | |
1101 | ||
1102 | if !self.check_authorized( | |
1103 | req, | |
1104 | Some(Mutation { | |
1105 | mutation: "publish", | |
1106 | name: Some(&new_crate.name), | |
1107 | vers: Some(&new_crate.vers), | |
1108 | cksum: Some(&file_cksum), | |
1109 | }), | |
1110 | ) { | |
1111 | return self.unauthorized(req); | |
1112 | } | |
1113 | ||
1114 | let dst = self | |
1115 | .dl_path | |
1116 | .join(&new_crate.name) | |
1117 | .join(&new_crate.vers) | |
1118 | .join("download"); | |
1119 | ||
1120 | if self.delayed_index_update == 0 { | |
1121 | save_new_crate(dst, new_crate, file, file_cksum, &self.registry_path); | |
1122 | } else { | |
1123 | let delayed_index_update = self.delayed_index_update; | |
1124 | let registry_path = self.registry_path.clone(); | |
1125 | let file = Vec::from(file); | |
1126 | thread::spawn(move || { | |
1127 | thread::sleep(std::time::Duration::new(delayed_index_update as u64, 0)); | |
1128 | save_new_crate(dst, new_crate, &file, file_cksum, ®istry_path); | |
1129 | }); | |
1130 | } | |
1131 | ||
1132 | self.ok(&req) | |
1133 | } else { | |
1134 | Response { | |
1135 | code: 400, | |
1136 | headers: vec![], | |
1137 | body: b"The request was missing a body".to_vec(), | |
1138 | } | |
1139 | } | |
1140 | } | |
1141 | } | |
1142 | ||
1143 | fn save_new_crate( | |
1144 | dst: PathBuf, | |
1145 | new_crate: crates_io::NewCrate, | |
1146 | file: &[u8], | |
1147 | file_cksum: String, | |
1148 | registry_path: &Path, | |
1149 | ) { | |
1150 | // Write the `.crate` | |
1151 | t!(fs::create_dir_all(dst.parent().unwrap())); | |
1152 | t!(fs::write(&dst, file)); | |
1153 | ||
1154 | let deps = new_crate | |
1155 | .deps | |
1156 | .iter() | |
1157 | .map(|dep| { | |
1158 | let (name, package) = match &dep.explicit_name_in_toml { | |
1159 | Some(explicit) => (explicit.to_string(), Some(dep.name.to_string())), | |
1160 | None => (dep.name.to_string(), None), | |
1161 | }; | |
1162 | serde_json::json!({ | |
1163 | "name": name, | |
1164 | "req": dep.version_req, | |
1165 | "features": dep.features, | |
4b012472 | 1166 | "default_features": dep.default_features, |
0a29b90c FG |
1167 | "target": dep.target, |
1168 | "optional": dep.optional, | |
1169 | "kind": dep.kind, | |
1170 | "registry": dep.registry, | |
1171 | "package": package, | |
4b012472 FG |
1172 | "artifact": dep.artifact, |
1173 | "bindep_target": dep.bindep_target, | |
1174 | "lib": dep.lib, | |
0a29b90c FG |
1175 | }) |
1176 | }) | |
1177 | .collect::<Vec<_>>(); | |
1178 | ||
1179 | let line = create_index_line( | |
1180 | serde_json::json!(new_crate.name), | |
1181 | &new_crate.vers, | |
1182 | deps, | |
1183 | &file_cksum, | |
1184 | new_crate.features, | |
1185 | false, | |
1186 | new_crate.links, | |
4b012472 | 1187 | new_crate.rust_version.as_deref(), |
49aad941 | 1188 | None, |
0a29b90c FG |
1189 | ); |
1190 | ||
1191 | write_to_index(registry_path, &new_crate.name, line, false); | |
1192 | } | |
1193 | ||
1194 | impl Package { | |
1195 | /// Creates a new package builder. | |
1196 | /// Call `publish()` to finalize and build the package. | |
1197 | pub fn new(name: &str, vers: &str) -> Package { | |
1198 | let config = paths::home().join(".cargo/config"); | |
1199 | if !config.exists() { | |
1200 | init(); | |
1201 | } | |
1202 | Package { | |
1203 | name: name.to_string(), | |
1204 | vers: vers.to_string(), | |
1205 | deps: Vec::new(), | |
1206 | files: Vec::new(), | |
1207 | yanked: false, | |
1208 | features: BTreeMap::new(), | |
1209 | local: false, | |
1210 | alternative: false, | |
1211 | invalid_json: false, | |
1212 | proc_macro: false, | |
1213 | links: None, | |
1214 | rust_version: None, | |
1215 | cargo_features: Vec::new(), | |
1216 | v: None, | |
1217 | } | |
1218 | } | |
1219 | ||
1220 | /// Call with `true` to publish in a "local registry". | |
1221 | /// | |
1222 | /// See `source-replacement.html#local-registry-sources` for more details | |
1223 | /// on local registries. See `local_registry.rs` for the tests that use | |
1224 | /// this. | |
1225 | pub fn local(&mut self, local: bool) -> &mut Package { | |
1226 | self.local = local; | |
1227 | self | |
1228 | } | |
1229 | ||
1230 | /// Call with `true` to publish in an "alternative registry". | |
1231 | /// | |
1232 | /// The name of the alternative registry is called "alternative". | |
1233 | /// | |
1234 | /// See `src/doc/src/reference/registries.md` for more details on | |
1235 | /// alternative registries. See `alt_registry.rs` for the tests that use | |
1236 | /// this. | |
1237 | pub fn alternative(&mut self, alternative: bool) -> &mut Package { | |
1238 | self.alternative = alternative; | |
1239 | self | |
1240 | } | |
1241 | ||
1242 | /// Adds a file to the package. | |
1243 | pub fn file(&mut self, name: &str, contents: &str) -> &mut Package { | |
1244 | self.file_with_mode(name, DEFAULT_MODE, contents) | |
1245 | } | |
1246 | ||
1247 | /// Adds a file with a specific Unix mode. | |
1248 | pub fn file_with_mode(&mut self, path: &str, mode: u32, contents: &str) -> &mut Package { | |
1249 | self.files.push(PackageFile { | |
1250 | path: path.to_string(), | |
1251 | contents: EntryData::Regular(contents.into()), | |
1252 | mode, | |
1253 | extra: false, | |
1254 | }); | |
1255 | self | |
1256 | } | |
1257 | ||
1258 | /// Adds a symlink to a path to the package. | |
1259 | pub fn symlink(&mut self, dst: &str, src: &str) -> &mut Package { | |
1260 | self.files.push(PackageFile { | |
1261 | path: dst.to_string(), | |
1262 | contents: EntryData::Symlink(src.into()), | |
1263 | mode: DEFAULT_MODE, | |
1264 | extra: false, | |
1265 | }); | |
1266 | self | |
1267 | } | |
1268 | ||
1269 | /// Adds an "extra" file that is not rooted within the package. | |
1270 | /// | |
1271 | /// Normal files are automatically placed within a directory named | |
1272 | /// `$PACKAGE-$VERSION`. This allows you to override that behavior, | |
1273 | /// typically for testing invalid behavior. | |
1274 | pub fn extra_file(&mut self, path: &str, contents: &str) -> &mut Package { | |
1275 | self.files.push(PackageFile { | |
1276 | path: path.to_string(), | |
1277 | contents: EntryData::Regular(contents.to_string()), | |
1278 | mode: DEFAULT_MODE, | |
1279 | extra: true, | |
1280 | }); | |
1281 | self | |
1282 | } | |
1283 | ||
1284 | /// Adds a normal dependency. Example: | |
49aad941 | 1285 | /// ```toml |
0a29b90c FG |
1286 | /// [dependencies] |
1287 | /// foo = {version = "1.0"} | |
1288 | /// ``` | |
1289 | pub fn dep(&mut self, name: &str, vers: &str) -> &mut Package { | |
1290 | self.add_dep(&Dependency::new(name, vers)) | |
1291 | } | |
1292 | ||
1293 | /// Adds a dependency with the given feature. Example: | |
49aad941 | 1294 | /// ```toml |
0a29b90c FG |
1295 | /// [dependencies] |
1296 | /// foo = {version = "1.0", "features": ["feat1", "feat2"]} | |
1297 | /// ``` | |
1298 | pub fn feature_dep(&mut self, name: &str, vers: &str, features: &[&str]) -> &mut Package { | |
1299 | self.add_dep(Dependency::new(name, vers).enable_features(features)) | |
1300 | } | |
1301 | ||
1302 | /// Adds a platform-specific dependency. Example: | |
1303 | /// ```toml | |
1304 | /// [target.'cfg(windows)'.dependencies] | |
1305 | /// foo = {version = "1.0"} | |
1306 | /// ``` | |
1307 | pub fn target_dep(&mut self, name: &str, vers: &str, target: &str) -> &mut Package { | |
1308 | self.add_dep(Dependency::new(name, vers).target(target)) | |
1309 | } | |
1310 | ||
1311 | /// Adds a dependency to the alternative registry. | |
1312 | pub fn registry_dep(&mut self, name: &str, vers: &str) -> &mut Package { | |
1313 | self.add_dep(Dependency::new(name, vers).registry("alternative")) | |
1314 | } | |
1315 | ||
1316 | /// Adds a dev-dependency. Example: | |
49aad941 | 1317 | /// ```toml |
0a29b90c FG |
1318 | /// [dev-dependencies] |
1319 | /// foo = {version = "1.0"} | |
1320 | /// ``` | |
1321 | pub fn dev_dep(&mut self, name: &str, vers: &str) -> &mut Package { | |
1322 | self.add_dep(Dependency::new(name, vers).dev()) | |
1323 | } | |
1324 | ||
1325 | /// Adds a build-dependency. Example: | |
49aad941 | 1326 | /// ```toml |
0a29b90c FG |
1327 | /// [build-dependencies] |
1328 | /// foo = {version = "1.0"} | |
1329 | /// ``` | |
1330 | pub fn build_dep(&mut self, name: &str, vers: &str) -> &mut Package { | |
1331 | self.add_dep(Dependency::new(name, vers).build()) | |
1332 | } | |
1333 | ||
1334 | pub fn add_dep(&mut self, dep: &Dependency) -> &mut Package { | |
1335 | self.deps.push(dep.clone()); | |
1336 | self | |
1337 | } | |
1338 | ||
1339 | /// Specifies whether or not the package is "yanked". | |
1340 | pub fn yanked(&mut self, yanked: bool) -> &mut Package { | |
1341 | self.yanked = yanked; | |
1342 | self | |
1343 | } | |
1344 | ||
1345 | /// Specifies whether or not this is a proc macro. | |
1346 | pub fn proc_macro(&mut self, proc_macro: bool) -> &mut Package { | |
1347 | self.proc_macro = proc_macro; | |
1348 | self | |
1349 | } | |
1350 | ||
1351 | /// Adds an entry in the `[features]` section. | |
1352 | pub fn feature(&mut self, name: &str, deps: &[&str]) -> &mut Package { | |
1353 | let deps = deps.iter().map(|s| s.to_string()).collect(); | |
1354 | self.features.insert(name.to_string(), deps); | |
1355 | self | |
1356 | } | |
1357 | ||
1358 | /// Specify a minimal Rust version. | |
1359 | pub fn rust_version(&mut self, rust_version: &str) -> &mut Package { | |
1360 | self.rust_version = Some(rust_version.into()); | |
1361 | self | |
1362 | } | |
1363 | ||
1364 | /// Causes the JSON line emitted in the index to be invalid, presumably | |
1365 | /// causing Cargo to skip over this version. | |
1366 | pub fn invalid_json(&mut self, invalid: bool) -> &mut Package { | |
1367 | self.invalid_json = invalid; | |
1368 | self | |
1369 | } | |
1370 | ||
1371 | pub fn links(&mut self, links: &str) -> &mut Package { | |
1372 | self.links = Some(links.to_string()); | |
1373 | self | |
1374 | } | |
1375 | ||
1376 | pub fn cargo_feature(&mut self, feature: &str) -> &mut Package { | |
1377 | self.cargo_features.push(feature.to_owned()); | |
1378 | self | |
1379 | } | |
1380 | ||
1381 | /// Sets the index schema version for this package. | |
1382 | /// | |
fe692bf9 | 1383 | /// See `cargo::sources::registry::IndexPackage` for more information. |
0a29b90c FG |
1384 | pub fn schema_version(&mut self, version: u32) -> &mut Package { |
1385 | self.v = Some(version); | |
1386 | self | |
1387 | } | |
1388 | ||
1389 | /// Creates the package and place it in the registry. | |
1390 | /// | |
1391 | /// This does not actually use Cargo's publishing system, but instead | |
1392 | /// manually creates the entry in the registry on the filesystem. | |
1393 | /// | |
1394 | /// Returns the checksum for the package. | |
1395 | pub fn publish(&self) -> String { | |
1396 | self.make_archive(); | |
1397 | ||
1398 | // Figure out what we're going to write into the index. | |
1399 | let deps = self | |
1400 | .deps | |
1401 | .iter() | |
1402 | .map(|dep| { | |
1403 | // In the index, the `registry` is null if it is from the same registry. | |
1404 | // In Cargo.toml, it is None if it is from crates.io. | |
1405 | let registry_url = match (self.alternative, dep.registry.as_deref()) { | |
1406 | (false, None) => None, | |
1407 | (false, Some("alternative")) => Some(alt_registry_url().to_string()), | |
1408 | (true, None) => { | |
1409 | Some("https://github.com/rust-lang/crates.io-index".to_string()) | |
1410 | } | |
1411 | (true, Some("alternative")) => None, | |
1412 | _ => panic!("registry_dep currently only supports `alternative`"), | |
1413 | }; | |
781aab86 FG |
1414 | let artifact = if let Some(artifact) = &dep.artifact { |
1415 | serde_json::json!([artifact]) | |
1416 | } else { | |
1417 | serde_json::json!(null) | |
1418 | }; | |
0a29b90c FG |
1419 | serde_json::json!({ |
1420 | "name": dep.name, | |
1421 | "req": dep.vers, | |
1422 | "features": dep.features, | |
4b012472 | 1423 | "default_features": dep.default_features, |
0a29b90c | 1424 | "target": dep.target, |
781aab86 FG |
1425 | "artifact": artifact, |
1426 | "bindep_target": dep.bindep_target, | |
1427 | "lib": dep.lib, | |
0a29b90c FG |
1428 | "optional": dep.optional, |
1429 | "kind": dep.kind, | |
1430 | "registry": registry_url, | |
1431 | "package": dep.package, | |
c0240ec0 | 1432 | "public": dep.public, |
0a29b90c FG |
1433 | }) |
1434 | }) | |
1435 | .collect::<Vec<_>>(); | |
1436 | let cksum = { | |
1437 | let c = t!(fs::read(&self.archive_dst())); | |
1438 | cksum(&c) | |
1439 | }; | |
1440 | let name = if self.invalid_json { | |
1441 | serde_json::json!(1) | |
1442 | } else { | |
1443 | serde_json::json!(self.name) | |
1444 | }; | |
1445 | let line = create_index_line( | |
1446 | name, | |
1447 | &self.vers, | |
1448 | deps, | |
1449 | &cksum, | |
1450 | self.features.clone(), | |
1451 | self.yanked, | |
1452 | self.links.clone(), | |
49aad941 | 1453 | self.rust_version.as_deref(), |
0a29b90c FG |
1454 | self.v, |
1455 | ); | |
1456 | ||
1457 | let registry_path = if self.alternative { | |
1458 | alt_registry_path() | |
1459 | } else { | |
1460 | registry_path() | |
1461 | }; | |
1462 | ||
1463 | write_to_index(®istry_path, &self.name, line, self.local); | |
1464 | ||
1465 | cksum | |
1466 | } | |
1467 | ||
1468 | fn make_archive(&self) { | |
1469 | let dst = self.archive_dst(); | |
1470 | t!(fs::create_dir_all(dst.parent().unwrap())); | |
1471 | let f = t!(File::create(&dst)); | |
1472 | let mut a = Builder::new(GzEncoder::new(f, Compression::default())); | |
1473 | ||
1474 | if !self | |
1475 | .files | |
1476 | .iter() | |
1477 | .any(|PackageFile { path, .. }| path == "Cargo.toml") | |
1478 | { | |
1479 | self.append_manifest(&mut a); | |
1480 | } | |
1481 | if self.files.is_empty() { | |
1482 | self.append( | |
1483 | &mut a, | |
1484 | "src/lib.rs", | |
1485 | DEFAULT_MODE, | |
1486 | &EntryData::Regular("".into()), | |
1487 | ); | |
1488 | } else { | |
1489 | for PackageFile { | |
1490 | path, | |
1491 | contents, | |
1492 | mode, | |
1493 | extra, | |
1494 | } in &self.files | |
1495 | { | |
1496 | if *extra { | |
1497 | self.append_raw(&mut a, path, *mode, contents); | |
1498 | } else { | |
1499 | self.append(&mut a, path, *mode, contents); | |
1500 | } | |
1501 | } | |
1502 | } | |
1503 | } | |
1504 | ||
1505 | fn append_manifest<W: Write>(&self, ar: &mut Builder<W>) { | |
1506 | let mut manifest = String::new(); | |
1507 | ||
1508 | if !self.cargo_features.is_empty() { | |
1509 | let mut features = String::new(); | |
1510 | serde::Serialize::serialize( | |
1511 | &self.cargo_features, | |
1512 | toml::ser::ValueSerializer::new(&mut features), | |
1513 | ) | |
1514 | .unwrap(); | |
1515 | manifest.push_str(&format!("cargo-features = {}\n\n", features)); | |
1516 | } | |
1517 | ||
1518 | manifest.push_str(&format!( | |
1519 | r#" | |
1520 | [package] | |
1521 | name = "{}" | |
1522 | version = "{}" | |
1523 | authors = [] | |
1524 | "#, | |
1525 | self.name, self.vers | |
1526 | )); | |
1527 | ||
1528 | if let Some(version) = &self.rust_version { | |
1529 | manifest.push_str(&format!("rust-version = \"{}\"", version)); | |
1530 | } | |
1531 | ||
4b012472 FG |
1532 | if !self.features.is_empty() { |
1533 | let features: Vec<String> = self | |
1534 | .features | |
1535 | .iter() | |
1536 | .map(|(feature, features)| { | |
1537 | if features.is_empty() { | |
1538 | format!("{} = []", feature) | |
1539 | } else { | |
1540 | format!( | |
1541 | "{} = [{}]", | |
1542 | feature, | |
1543 | features | |
1544 | .iter() | |
1545 | .map(|s| format!("\"{}\"", s)) | |
1546 | .collect::<Vec<_>>() | |
1547 | .join(", ") | |
1548 | ) | |
1549 | } | |
1550 | }) | |
1551 | .collect(); | |
1552 | ||
1553 | manifest.push_str(&format!("\n[features]\n{}", features.join("\n"))); | |
1554 | } | |
1555 | ||
0a29b90c FG |
1556 | for dep in self.deps.iter() { |
1557 | let target = match dep.target { | |
1558 | None => String::new(), | |
1559 | Some(ref s) => format!("target.'{}'.", s), | |
1560 | }; | |
1561 | let kind = match &dep.kind[..] { | |
1562 | "build" => "build-", | |
1563 | "dev" => "dev-", | |
1564 | _ => "", | |
1565 | }; | |
1566 | manifest.push_str(&format!( | |
1567 | r#" | |
1568 | [{}{}dependencies.{}] | |
1569 | version = "{}" | |
1570 | "#, | |
1571 | target, kind, dep.name, dep.vers | |
1572 | )); | |
4b012472 FG |
1573 | if dep.optional { |
1574 | manifest.push_str("optional = true\n"); | |
1575 | } | |
781aab86 | 1576 | if let Some(artifact) = &dep.artifact { |
0a29b90c | 1577 | manifest.push_str(&format!("artifact = \"{}\"\n", artifact)); |
781aab86 FG |
1578 | } |
1579 | if let Some(target) = &dep.bindep_target { | |
1580 | manifest.push_str(&format!("target = \"{}\"\n", target)); | |
1581 | } | |
1582 | if dep.lib { | |
1583 | manifest.push_str("lib = true\n"); | |
0a29b90c FG |
1584 | } |
1585 | if let Some(registry) = &dep.registry { | |
1586 | assert_eq!(registry, "alternative"); | |
1587 | manifest.push_str(&format!("registry-index = \"{}\"", alt_registry_url())); | |
1588 | } | |
4b012472 FG |
1589 | if !dep.default_features { |
1590 | manifest.push_str("default-features = false\n"); | |
1591 | } | |
1592 | if !dep.features.is_empty() { | |
1593 | let mut features = String::new(); | |
1594 | serde::Serialize::serialize( | |
1595 | &dep.features, | |
1596 | toml::ser::ValueSerializer::new(&mut features), | |
1597 | ) | |
1598 | .unwrap(); | |
1599 | manifest.push_str(&format!("features = {}\n", features)); | |
1600 | } | |
1601 | if let Some(package) = &dep.package { | |
1602 | manifest.push_str(&format!("package = \"{}\"\n", package)); | |
1603 | } | |
0a29b90c FG |
1604 | } |
1605 | if self.proc_macro { | |
1606 | manifest.push_str("[lib]\nproc-macro = true\n"); | |
1607 | } | |
1608 | ||
1609 | self.append( | |
1610 | ar, | |
1611 | "Cargo.toml", | |
1612 | DEFAULT_MODE, | |
1613 | &EntryData::Regular(manifest.into()), | |
1614 | ); | |
1615 | } | |
1616 | ||
1617 | fn append<W: Write>(&self, ar: &mut Builder<W>, file: &str, mode: u32, contents: &EntryData) { | |
1618 | self.append_raw( | |
1619 | ar, | |
1620 | &format!("{}-{}/{}", self.name, self.vers, file), | |
1621 | mode, | |
1622 | contents, | |
1623 | ); | |
1624 | } | |
1625 | ||
1626 | fn append_raw<W: Write>( | |
1627 | &self, | |
1628 | ar: &mut Builder<W>, | |
1629 | path: &str, | |
1630 | mode: u32, | |
1631 | contents: &EntryData, | |
1632 | ) { | |
1633 | let mut header = Header::new_ustar(); | |
1634 | let contents = match contents { | |
1635 | EntryData::Regular(contents) => contents.as_str(), | |
1636 | EntryData::Symlink(src) => { | |
1637 | header.set_entry_type(tar::EntryType::Symlink); | |
1638 | t!(header.set_link_name(src)); | |
1639 | "" // Symlink has no contents. | |
1640 | } | |
1641 | }; | |
1642 | header.set_size(contents.len() as u64); | |
1643 | t!(header.set_path(path)); | |
1644 | header.set_mode(mode); | |
1645 | header.set_cksum(); | |
1646 | t!(ar.append(&header, contents.as_bytes())); | |
1647 | } | |
1648 | ||
1649 | /// Returns the path to the compressed package file. | |
1650 | pub fn archive_dst(&self) -> PathBuf { | |
1651 | if self.local { | |
1652 | registry_path().join(format!("{}-{}.crate", self.name, self.vers)) | |
1653 | } else if self.alternative { | |
1654 | alt_dl_path() | |
1655 | .join(&self.name) | |
1656 | .join(&self.vers) | |
1657 | .join("download") | |
1658 | } else { | |
1659 | dl_path().join(&self.name).join(&self.vers).join("download") | |
1660 | } | |
1661 | } | |
1662 | } | |
1663 | ||
1664 | pub fn cksum(s: &[u8]) -> String { | |
1665 | Sha256::new().update(s).finish_hex() | |
1666 | } | |
1667 | ||
1668 | impl Dependency { | |
1669 | pub fn new(name: &str, vers: &str) -> Dependency { | |
1670 | Dependency { | |
1671 | name: name.to_string(), | |
1672 | vers: vers.to_string(), | |
1673 | kind: "normal".to_string(), | |
1674 | artifact: None, | |
781aab86 FG |
1675 | bindep_target: None, |
1676 | lib: false, | |
0a29b90c FG |
1677 | target: None, |
1678 | features: Vec::new(), | |
1679 | package: None, | |
1680 | optional: false, | |
1681 | registry: None, | |
4b012472 | 1682 | default_features: true, |
c0240ec0 | 1683 | public: false, |
0a29b90c FG |
1684 | } |
1685 | } | |
1686 | ||
1687 | /// Changes this to `[build-dependencies]`. | |
1688 | pub fn build(&mut self) -> &mut Self { | |
1689 | self.kind = "build".to_string(); | |
1690 | self | |
1691 | } | |
1692 | ||
1693 | /// Changes this to `[dev-dependencies]`. | |
1694 | pub fn dev(&mut self) -> &mut Self { | |
1695 | self.kind = "dev".to_string(); | |
1696 | self | |
1697 | } | |
1698 | ||
1699 | /// Changes this to `[target.$target.dependencies]`. | |
1700 | pub fn target(&mut self, target: &str) -> &mut Self { | |
1701 | self.target = Some(target.to_string()); | |
1702 | self | |
1703 | } | |
1704 | ||
1705 | /// Change the artifact to be of the given kind, like "bin", or "staticlib", | |
1706 | /// along with a specific target triple if provided. | |
1707 | pub fn artifact(&mut self, kind: &str, target: Option<String>) -> &mut Self { | |
781aab86 FG |
1708 | self.artifact = Some(kind.to_string()); |
1709 | self.bindep_target = target; | |
0a29b90c FG |
1710 | self |
1711 | } | |
1712 | ||
1713 | /// Adds `registry = $registry` to this dependency. | |
1714 | pub fn registry(&mut self, registry: &str) -> &mut Self { | |
1715 | self.registry = Some(registry.to_string()); | |
1716 | self | |
1717 | } | |
1718 | ||
1719 | /// Adds `features = [ ... ]` to this dependency. | |
1720 | pub fn enable_features(&mut self, features: &[&str]) -> &mut Self { | |
1721 | self.features.extend(features.iter().map(|s| s.to_string())); | |
1722 | self | |
1723 | } | |
1724 | ||
1725 | /// Adds `package = ...` to this dependency. | |
1726 | pub fn package(&mut self, pkg: &str) -> &mut Self { | |
1727 | self.package = Some(pkg.to_string()); | |
1728 | self | |
1729 | } | |
1730 | ||
1731 | /// Changes this to an optional dependency. | |
1732 | pub fn optional(&mut self, optional: bool) -> &mut Self { | |
1733 | self.optional = optional; | |
1734 | self | |
1735 | } | |
4b012472 | 1736 | |
c0240ec0 FG |
1737 | /// Changes this to an public dependency. |
1738 | pub fn public(&mut self, public: bool) -> &mut Self { | |
1739 | self.public = public; | |
1740 | self | |
1741 | } | |
1742 | ||
4b012472 FG |
1743 | /// Adds `default-features = false` if the argument is `false`. |
1744 | pub fn default_features(&mut self, default_features: bool) -> &mut Self { | |
1745 | self.default_features = default_features; | |
1746 | self | |
1747 | } | |
0a29b90c | 1748 | } |