ic_asset_certification/asset_config.rs
1use crate::{Asset, AssetCertificationError};
2use globset::{Glob, GlobMatcher};
3use ic_http_certification::StatusCode;
4use std::fmt::{Display, Formatter};
5
6/// Certification configuration for [assets](Asset). This configuration
7/// is passed alongside the [assets](Asset) to the
8/// [AssetRouter](crate::AssetRouter).
9///
10/// The configuration can target a specific [File](AssetConfig::File) or an
11/// array of files using a [Pattern](AssetConfig::Pattern).
12///
13/// Configuration can specify the content type and
14/// headers to include for certification and to be served by the
15/// [AssetRouter](crate::AssetRouter) for each asset matching the configuration.
16///
17/// # Examples
18///
19/// ## JavaScript file
20///
21/// This example configures an individual JavaScript file to be served by the
22/// [AssetRouter](crate::AssetRouter) on the `/app.js` path. The content type is
23/// set to `text/javascript` and a `cache-control` header is added.
24///
25/// ```
26/// use ic_http_certification::StatusCode;
27/// use ic_asset_certification::{AssetConfig, AssetEncoding};
28///
29/// let config = AssetConfig::File {
30/// path: "app.js".to_string(),
31/// content_type: Some("text/javascript".to_string()),
32/// headers: vec![
33/// ("Cache-Control".to_string(), "public, max-age=31536000, immutable".to_string()),
34/// ],
35/// fallback_for: vec![],
36/// aliased_by: vec![],
37/// encodings: vec![
38/// AssetEncoding::Brotli.default_config(),
39/// AssetEncoding::Gzip.default_config(),
40/// ],
41/// };
42/// ```
43///
44/// ## Index HTML file with fallback
45///
46/// This example configures an individual HTML file to be served by the
47/// [AssetRouter](crate::AssetRouter) on the `/index.html` path. In addition,
48/// it is configured as the fallback for the `/` scope. This means that any
49/// request that does not exactly match an asset, will be given this response.
50/// The content type is set to `text/html` and a `cache-control` header is added.
51///
52/// ```
53/// use ic_http_certification::StatusCode;
54/// use ic_asset_certification::{AssetConfig, AssetFallbackConfig, AssetEncoding};
55///
56/// let config = AssetConfig::File {
57/// path: "index.html".to_string(),
58/// content_type: Some("text/html".to_string()),
59/// headers: vec![
60/// ("Cache-Control".to_string(), "public, no-cache, no-store".to_string()),
61/// ],
62/// fallback_for: vec![AssetFallbackConfig {
63/// scope: "/".to_string(),
64/// status_code: Some(StatusCode::OK),
65/// }],
66/// aliased_by: vec!["/".to_string()],
67/// encodings: vec![
68/// AssetEncoding::Brotli.default_config(),
69/// AssetEncoding::Gzip.default_config(),
70/// ],
71/// };
72/// ```
73///
74/// ## 404 HTML file with multiple fallbacks and aliases
75///
76/// This example configures an individual HTML file to be served by the
77/// [AssetRouter](crate::AssetRouter) on the `/404.html` path.
78///
79/// In addition, it is configured as the fallback for the `/js`, and `/css`
80/// scopes. This means that any request that does not exactly match an asset in
81/// the `/js` or `/css` directories, will be given this response. The content
82/// type is set to `text/html` and a `cache-control` header is added.
83///
84/// The asset is also aliased by multiple paths. This means that any request
85/// made to one of these aliases will be served the asset at `/404.html`.
86/// The asset is aliased by the following paths:
87/// - `/404`
88/// - `/404/`
89/// - `/404.html`
90/// - `/not-found`
91/// - `/not-found/`
92/// - `/not-found/index.html`
93///
94/// ```
95/// use ic_http_certification::StatusCode;
96/// use ic_asset_certification::{AssetConfig, AssetFallbackConfig, AssetEncoding};
97///
98/// let config = AssetConfig::File {
99/// path: "404.html".to_string(),
100/// content_type: Some("text/html".to_string()),
101/// headers: vec![
102/// ("Cache-Control".to_string(), "public, no-cache, no-store".to_string()),
103/// ],
104/// fallback_for: vec![
105/// AssetFallbackConfig {
106/// scope: "/css".to_string(),
107/// status_code: Some(StatusCode::NOT_FOUND),
108/// },
109/// AssetFallbackConfig {
110/// scope: "/js".to_string(),
111/// status_code: Some(StatusCode::NOT_FOUND),
112/// },
113/// ],
114/// aliased_by: vec![
115/// "/404".to_string(),
116/// "/404/".to_string(),
117/// "/404.html".to_string(),
118/// "/not-found".to_string(),
119/// "/not-found/".to_string(),
120/// "/not-found/index.html".to_string(),
121/// ],
122/// encodings: vec![
123/// AssetEncoding::Brotli.default_config(),
124/// AssetEncoding::Gzip.default_config(),
125/// ],
126/// };
127/// ```
128///
129/// ## CSS files using a glob pattern
130///
131/// This example configures all CSS files to be served by the
132/// [AssetRouter](crate::AssetRouter) using a glob pattern. The content type is
133/// set to `text/css` and a `cache-control` header is added.
134///
135/// ```
136/// use ic_http_certification::StatusCode;
137/// use ic_asset_certification::{AssetConfig, AssetEncoding};
138///
139/// let config = AssetConfig::Pattern {
140/// pattern: "**/*.css".to_string(),
141/// content_type: Some("text/css".to_string()),
142/// headers: vec![
143/// ("Cache-Control".to_string(), "public, max-age=31536000, immutable".to_string()),
144/// ],
145/// encodings: vec![
146/// AssetEncoding::Brotli.default_config(),
147/// AssetEncoding::Gzip.default_config(),
148/// ],
149/// };
150/// ```
151///
152/// ## Temporary redirect
153///
154/// This example configures a redirect from `/old` to `/new`. The redirect is
155/// configured as a temporary redirect (307).
156///
157/// ```
158/// use ic_asset_certification::{AssetConfig, AssetRedirectKind};
159///
160/// let config = AssetConfig::Redirect {
161/// from: "/old".to_string(),
162/// to: "/new".to_string(),
163/// kind: AssetRedirectKind::Temporary,
164/// headers: vec![(
165/// "content-type".to_string(),
166/// "text/plain; charset=utf-8".to_string(),
167/// )],
168/// };
169/// ```
170///
171/// ## Permanent redirect
172///
173/// This example configures a redirect from `/old` to `/new`. The redirect is
174/// configured as a permanent redirect (301).
175///
176/// ```
177/// use ic_asset_certification::{AssetConfig, AssetRedirectKind};
178///
179/// let config = AssetConfig::Redirect {
180/// from: "/old".to_string(),
181/// to: "/new".to_string(),
182/// kind: AssetRedirectKind::Permanent,
183/// headers: vec![(
184/// "content-type".to_string(),
185/// "text/plain; charset=utf-8".to_string(),
186/// )],
187/// };
188/// ```
189#[derive(Debug, Clone)]
190pub enum AssetConfig {
191 /// Matches a specific file.
192 File {
193 /// The path to the file. This path must exactly match the
194 /// path of an [Asset] provided to the [AssetRouter](crate::AssetRouter)
195 /// with this config.
196 path: String,
197
198 /// The content type of the file (e.g. "text/javascript").
199 ///
200 /// Providing this option will auto-insert a `Content-Type` header with
201 /// the provided value. If this value is not provided, the
202 /// `Content-Type` header will not be inserted.
203 ///
204 /// If the `Content-Type` header is not sent to the browser, the browser
205 /// will try to guess the content type based on the file extension,
206 /// unless a `X-Content-Type-Options: nosniff` header is sent.
207 ///
208 /// Not certifying the `Content-Type` header will also allow a malicious
209 /// replica to insert its own `Content-Type` header, which could lead
210 /// to a security vulnerability.
211 content_type: Option<String>,
212
213 /// Additional headers to be inserted into the response. Each additional
214 /// header added will be included in certification and served by the
215 /// [AssetRouter](crate::AssetRouter) for matching [Assets](Asset).
216 headers: Vec<(String, String)>,
217
218 /// Configure this asset as a fallback for a set of scopes.
219 ///
220 /// When serving assets, if a requested path does not exactly match any
221 /// assets then the [AssetRouter](crate::AssetRouter) will search for an
222 /// asset configured with a fallback scope that most closely matches
223 /// the requested asset's path.
224 ///
225 /// For example, if a request is made for `/app.js` and no asset with
226 /// that exact path is found, the router will attempt to serve an asset
227 /// configured with a fallback scope of `/`.
228 ///
229 /// This will be done recursively until no more fallback scopes are
230 /// possible to find. For example, if a request is made for
231 /// `/assets/js/app/core/index.js` and no asset with that exact path is
232 /// found, the [AssetRouter](crate::AssetRouter) will search for assets
233 /// configured with the following fallback scopes, in order:
234 /// - `/assets/js/app/core`
235 /// - `/assets/js/app`
236 /// - `/assets/js`
237 /// - `/assets`
238 /// - `/`
239 ///
240 /// If multiple fallback assets are configured, the first one found will
241 /// be used. If no asset is found with any of these fallback scopes, no
242 /// response will be returned.
243 fallback_for: Vec<AssetFallbackConfig>,
244
245 /// A list of aliases for this asset. If a request is made for one of
246 /// these aliases, the asset will be served as if the request was made
247 /// for the original path.
248 ///
249 /// For example, if an asset is configured with the path `index.html` and
250 /// the alias `/`, a request for `/` will be served the
251 /// asset at `index.html`.
252 aliased_by: Vec<String>,
253
254 /// A list of encodings to serve the asset with. Each listing includes
255 /// the encoding of an asset, and the file extension for the encoded
256 /// asset. The router will search for an asset with the provided file
257 /// extension and certify all matching encoded assets, if found.
258 ///
259 /// A list of alternative encodings that can be used to serve the asset.
260 ///
261 /// Each entry is a tuple of the [encoding name](AssetEncoding) and the
262 /// file extension used in the file path. For example, to include Brotli
263 /// and Gzip encodings:
264 /// `vec![AssetEncoding::Brotli.default_config(), AssetEncoding::Gzip.default_config()]`
265 ///
266 /// Each encoding referenced must be provided to the asset router as a
267 /// separate file with the same filename as the original file, but with
268 /// an additional file extension matching the configuration. For
269 /// example, if the current matched file is named `file.html`, then the
270 /// asset router will look for `file.html.br` and `file.html.gz`.
271 ///
272 /// If the file is found, the asset will be certified and served with
273 /// the provided encoding according to the `Accept-Encoding`. Encodings
274 /// are prioritized in the following order:
275 /// - Brotli
276 /// - Zstd
277 /// - Gzip
278 /// - Deflate
279 /// - Identity
280 ///
281 /// The asset router will return the highest priority encoding that has
282 /// been certified and is supported by the client.
283 encodings: Vec<(AssetEncoding, String)>,
284 },
285
286 /// Matches files using a glob pattern.
287 Pattern {
288 /// A glob pattern to match files against.
289 ///
290 /// Standard Unix-style glob syntax is supported:
291 /// - `?` matches any single character.
292 /// - `*` matches zero or more characters.
293 /// - `**` recursively matches directories but is only legal in three
294 /// situations.
295 /// - If the glob starts with `**/`, then it matches all directories.
296 /// For example, `**/foo` matches `foo` and `bar/foo` but not
297 /// `foo/bar`.
298 /// - If the glob ends with `/**`, then it matches all sub-entries.
299 /// For example, `foo/**` matches `foo/a` and `foo/a/b`, but not
300 /// `foo`.
301 /// - If the glob contains `/**/` anywhere within the pattern, then it
302 /// matches zero or more directories.
303 /// - Using `**` anywhere else is illegal.
304 /// - The glob `**` is allowed and means "match everything".
305 /// - `{a,b}` matches `a` or `b` where `a` and `b` are arbitrary glob
306 /// patterns. (N.B. Nesting {...} is not currently allowed.)
307 /// - `[ab]` matches `a` or `b` where `a` and `b` are characters.
308 /// - `[!ab]` to match any character except for `a` and `b`.
309 /// - Metacharacters such as `*` and `?` can be escaped with character
310 /// class notation. e.g., `[*]` matches `*`.
311 pattern: String,
312
313 /// The content type of the file (e.g. "text/javascript").
314 ///
315 /// Providing this option will auto-insert a `Content-Type` header with
316 /// the provided value. If this value is not provided, the
317 /// `Content-Type` header will not be inserted.
318 ///
319 /// If the `Content-Type` header is not sent to the browser, the browser
320 /// will try to guess the content type based on the file extension,
321 /// unless a `X-Content-Type-Options: nosniff` header is sent.
322 ///
323 /// Not certifying the `Content-Type` header will also allow a malicious
324 /// replica to insert its own `Content-Type` header, which could lead
325 /// to a security vulnerability.
326 content_type: Option<String>,
327
328 /// Additional headers to be inserted into the response. Each additional
329 /// header added will be included in certification and served by the
330 /// [AssetRouter](crate::AssetRouter) for matching [Assets](Asset).
331 headers: Vec<(String, String)>,
332
333 /// A list of encodings to serve the asset with. Each listing includes
334 /// the encoding of an asset, and the file extension for the encoded
335 /// asset. The router will search for an asset with the provided file
336 /// extension and certify all matching encoded assets, if found.
337 ///
338 /// A list of alternative encodings that can be used to serve the asset.
339 ///
340 /// Each entry is a tuple of the [encoding name](AssetEncoding) and the
341 /// file extension used in the file path. For example, to include Brotli
342 /// and Gzip encodings:
343 /// `vec![AssetEncoding::Brotli.default_config(), AssetEncoding::Gzip.default_config()]`
344 ///
345 /// Each encoding referenced must be provided to the asset router as a
346 /// separate file with the same filename as the original file, but with
347 /// an additional file extension matching the configuration. For
348 /// example, if the current matched file is named `file.html`, then the
349 /// asset router will look for `file.html.br` and `file.html.gz`.
350 ///
351 /// If the file is found, the asset will be certified and served with
352 /// the provided encoding according to the `Accept-Encoding`. Encodings
353 /// are prioritized in the following order:
354 /// - Brotli
355 /// - Zstd
356 /// - Gzip
357 /// - Deflate
358 /// - Identity
359 ///
360 /// The asset router will return the highest priority encoding that has
361 /// been certified and is supported by the client.
362 encodings: Vec<(AssetEncoding, String)>,
363 },
364
365 /// Redirects the request to another URL. This config type is not matched
366 /// against any assets.
367 Redirect {
368 /// The URL to redirect from.
369 from: String,
370
371 /// The URL to redirect to.
372 to: String,
373
374 /// The kind redirect to configure.
375 kind: AssetRedirectKind,
376
377 /// Additional headers to be inserted into the response. Each additional
378 /// header added will be included in certification and served by the
379 /// [AssetRouter](crate::AssetRouter) for matching [Assets](Asset).
380 ///
381 /// Note that the `Location` header will be automatically added to the
382 /// response with the value of the `to` field.
383 headers: Vec<(String, String)>,
384 },
385}
386
387/// Configuration for an asset to be used as a fallback for a specific scope.
388///
389/// See the [fallback_for](AssetConfig::File::fallback_for) configuration
390/// of the [AssetConfig] interface for more information.
391#[derive(Debug, Clone)]
392pub struct AssetFallbackConfig {
393 /// The scope to use this asset as a fallback for.
394 ///
395 /// See the [fallback_for](AssetConfig::File::fallback_for)
396 /// configuration of the [AssetConfig] interface for more information.
397 pub scope: String,
398
399 /// The HTTP status code to return when serving the asset.
400 /// If this value is not provided, the default status code will be 200.
401 pub status_code: Option<StatusCode>,
402}
403
404/// The type of redirect to use. Redirects can be either
405/// [permanent](AssetRedirectKind::Permanent) or
406/// [temporary](AssetRedirectKind::Temporary).
407#[derive(Debug, Clone)]
408pub enum AssetRedirectKind {
409 /// A permanent redirect (301).
410 ///
411 /// The browser will cache this redirect and will not make a request to the
412 /// old location again. This is useful when the resource has permanently
413 /// moved to a new location. The browser will update its bookmarks and
414 /// search engine results.
415 ///
416 /// See the
417 /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301)
418 /// for more information.
419 Permanent,
420
421 /// A temporary redirect (307).
422 ///
423 /// The browser will not cache this redirect and will make a request to the
424 /// old location again. This is useful when the resource has temporarily
425 /// moved to a new location. The browser will not update its bookmarks and
426 /// search engine results.
427 ///
428 /// See the
429 /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307)
430 /// for more information.
431 Temporary,
432}
433
434/// The encoding of an asset.
435#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
436pub enum AssetEncoding {
437 /// The asset is not encoded.
438 Identity,
439
440 /// The asset is encoded with the Brotli algorithm.
441 Brotli,
442
443 /// The asset is encoded with the Zstd algorithm.
444 Zstd,
445
446 /// The asset is encoded with the Gzip algorithm.
447 Gzip,
448
449 /// The asset is encoded with the Deflate algorithm.
450 Deflate,
451}
452
453impl AssetEncoding {
454 /// Returns the default encoding and file extension for the encoding.
455 /// The default encoding is the encoding that is used when the client
456 /// does not specify an encoding in the `Accept-Encoding` header.
457 ///
458 /// The default encoding and file extension are:
459 /// - [Brotli](AssetEncoding::Brotli): `br`
460 /// - [Zstd](AssetEncoding::Zstd): `zst`
461 /// - [Gzip](AssetEncoding::Gzip): `gz`
462 /// - [Deflate](AssetEncoding::Deflate): `zz`
463 ///
464 /// # Examples
465 ///
466 /// ```
467 /// use ic_asset_certification::AssetEncoding;
468 ///
469 /// let (encoding, extension) = AssetEncoding::Brotli.default_config();
470 /// assert_eq!(encoding, AssetEncoding::Brotli);
471 /// assert_eq!(extension, ".br");
472 ///
473 /// let (encoding, extension) = AssetEncoding::Zstd.default_config();
474 /// assert_eq!(encoding, AssetEncoding::Zstd);
475 /// assert_eq!(extension, ".zst");
476 ///
477 /// let (encoding, extension) = AssetEncoding::Gzip.default_config();
478 /// assert_eq!(encoding, AssetEncoding::Gzip);
479 /// assert_eq!(extension, ".gz");
480 ///
481 /// let (encoding, extension) = AssetEncoding::Deflate.default_config();
482 /// assert_eq!(encoding, AssetEncoding::Deflate);
483 /// assert_eq!(extension, ".zz");
484 ///
485 /// let (encoding, extension) = AssetEncoding::Identity.default_config();
486 /// assert_eq!(encoding, AssetEncoding::Identity);
487 /// assert_eq!(extension, "");
488 /// ```
489 pub fn default_config(self) -> (AssetEncoding, String) {
490 let file_extension = match self {
491 AssetEncoding::Identity => "".to_string(),
492 AssetEncoding::Brotli => ".br".to_string(),
493 AssetEncoding::Zstd => ".zst".to_string(),
494 AssetEncoding::Gzip => ".gz".to_string(),
495 AssetEncoding::Deflate => ".zz".to_string(),
496 };
497
498 (self, file_extension)
499 }
500
501 /// Returns an encoding with a custom file extension. This is useful
502 /// when the default file extension assigned by [default_config](AssetEncoding::default_config)
503 /// is not desired.
504 ///
505 /// # Examples
506 ///
507 /// ```
508 /// use ic_asset_certification::AssetEncoding;
509 ///
510 /// let (encoding, extension) = AssetEncoding::Brotli.custom_config("brotli".to_string());
511 ///
512 /// assert_eq!(encoding, AssetEncoding::Brotli);
513 /// assert_eq!(extension, "brotli");
514 /// ```
515 pub fn custom_config(self, extension: String) -> (AssetEncoding, String) {
516 (self, extension)
517 }
518}
519
520impl Display for AssetEncoding {
521 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
522 let str = match self {
523 AssetEncoding::Identity => "identity".to_string(),
524 AssetEncoding::Brotli => "br".to_string(),
525 AssetEncoding::Zstd => "zstd".to_string(),
526 AssetEncoding::Gzip => "gzip".to_string(),
527 AssetEncoding::Deflate => "deflate".to_string(),
528 };
529
530 write!(f, "{str}")
531 }
532}
533
534#[derive(Debug, Clone)]
535pub(crate) enum NormalizedAssetConfig {
536 File {
537 path: String,
538 content_type: Option<String>,
539 headers: Vec<(String, String)>,
540 fallback_for: Vec<AssetFallbackConfig>,
541 aliased_by: Vec<String>,
542 encodings: Vec<(AssetEncoding, String)>,
543 },
544 Pattern {
545 pattern: GlobMatcher,
546 content_type: Option<String>,
547 headers: Vec<(String, String)>,
548 encodings: Vec<(AssetEncoding, String)>,
549 },
550 Redirect {
551 from: String,
552 to: String,
553 kind: AssetRedirectKind,
554 headers: Vec<(String, String)>,
555 },
556}
557
558impl TryFrom<AssetConfig> for NormalizedAssetConfig {
559 type Error = AssetCertificationError;
560
561 fn try_from(config: AssetConfig) -> Result<Self, Self::Error> {
562 match config {
563 AssetConfig::File {
564 path,
565 content_type,
566 headers,
567 fallback_for,
568 aliased_by,
569 encodings,
570 } => Ok(NormalizedAssetConfig::File {
571 path,
572 content_type,
573 headers,
574 fallback_for,
575 aliased_by,
576 encodings,
577 }),
578 AssetConfig::Pattern {
579 pattern,
580 content_type,
581 headers,
582 encodings,
583 } => Ok(NormalizedAssetConfig::Pattern {
584 pattern: Glob::new(&pattern)?.compile_matcher(),
585 content_type,
586 headers,
587 encodings,
588 }),
589 AssetConfig::Redirect {
590 from,
591 to,
592 kind,
593 headers,
594 } => Ok(NormalizedAssetConfig::Redirect {
595 from,
596 to,
597 kind,
598 headers,
599 }),
600 }
601 }
602}
603
604impl NormalizedAssetConfig {
605 pub(crate) fn matches_asset(&self, asset: &Asset) -> bool {
606 match self {
607 Self::File { path, .. } => path == asset.path.as_ref(),
608 Self::Pattern { pattern, .. } => pattern.is_match(asset.path.as_ref()),
609 Self::Redirect { .. } => false,
610 }
611 }
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617 use crate::Asset;
618 use rstest::*;
619
620 #[rstest]
621 #[case("index.html", "index.html", true)]
622 #[case("app.js", "app.js", true)]
623 #[case("index.js", "app.css", false)]
624 #[case("index.css", "index.js", false)]
625 fn matches_asset_file(
626 #[case] asset_path: &str,
627 #[case] config_path: &str,
628 #[case] expected: bool,
629 ) {
630 let asset = Asset::new(asset_path, vec![]);
631 let config: NormalizedAssetConfig = AssetConfig::File {
632 path: config_path.to_string(),
633 content_type: None,
634 headers: vec![],
635 fallback_for: vec![],
636 aliased_by: vec![],
637 encodings: vec![],
638 }
639 .try_into()
640 .unwrap();
641
642 assert_eq!(config.matches_asset(&asset), expected);
643 }
644
645 #[rstest]
646 // index.html, *
647 #[case("index.html", "*", true)]
648 #[case("index.html", "**", true)]
649 #[case("index.html", "**/*", true)]
650 #[case("index.html", "**/**", true)]
651 // app.js, *
652 #[case("app.js", "*", true)]
653 #[case("app.js", "**", true)]
654 #[case("app.js", "**/*", true)]
655 #[case("app.js", "**/**", true)]
656 // index.html, *.html
657 #[case("index.html", "*.html", true)]
658 #[case("index.html", "**.html", true)]
659 #[case("index.html", "**/*.html", true)]
660 #[case("index.html", "**/**.html", true)]
661 // app.js, *.html
662 #[case("app.js", "*.html", false)]
663 #[case("app.js", "**.html", false)]
664 #[case("app.js", "**/*.html", false)]
665 #[case("app.js", "**/**.html", false)]
666 // app.js, *.js
667 #[case("app.js", "*.js", true)]
668 #[case("app.js", "**.js", true)]
669 #[case("app.js", "**/*.js", true)]
670 #[case("app.js", "**/**.js", true)]
671 // index.html, *.{js,html}
672 #[case("index.html", "*.{js,html}", true)]
673 #[case("index.html", "**.{js,html}", true)]
674 #[case("index.html", "**/*.{js,html}", true)]
675 #[case("index.html", "**/**.{js,html}", true)]
676 // app.js, *.{js,html}
677 #[case("app.js", "*.{js,html}", true)]
678 #[case("app.js", "**.{js,html}", true)]
679 #[case("app.js", "**/*.{js,html}", true)]
680 #[case("app.js", "**/**.{js,html}", true)]
681 // index.html, assets/*.html
682 #[case("index.html", "assets/*.html", false)]
683 #[case("index.html", "assets/**.html", false)]
684 #[case("index.html", "assets/**/*.html", false)]
685 // app.js, assets/*.js
686 #[case("app.js", "assets/*.js", false)]
687 #[case("app.js", "assets/**.js", false)]
688 #[case("app.js", "assets/**/*.js", false)]
689 // assets/index.html, *
690 #[case("assets/index.html", "*", true)]
691 #[case("assets/index.html", "**", true)]
692 #[case("assets/index.html", "**/*", true)]
693 #[case("assets/index.html", "**/**", true)]
694 // assets/app.js, *
695 #[case("assets/app.js", "*", true)]
696 #[case("assets/app.js", "**", true)]
697 #[case("assets/app.js", "**/*", true)]
698 #[case("assets/app.js", "**/**", true)]
699 // assets/index.html, *.html
700 #[case("assets/index.html", "*.html", true)]
701 #[case("assets/index.html", "**.html", true)]
702 #[case("assets/index.html", "**/*.html", true)]
703 // assets/app.js, *.js
704 #[case("assets/app.js", "*.js", true)]
705 #[case("assets/app.js", "**.js", true)]
706 #[case("assets/app.js", "**/*.js", true)]
707 // assets/index.html, assets/*.html
708 #[case("assets/index.html", "assets/*.html", true)]
709 #[case("assets/index.html", "assets/**.html", true)]
710 #[case("assets/index.html", "assets/**/*.html", true)]
711 // assets/app.js, assets/*.js
712 #[case("assets/app.js", "assets/*.js", true)]
713 #[case("assets/app.js", "assets/**.js", true)]
714 #[case("assets/app.js", "assets/**/*.js", true)]
715 // assets/index.html, assets/*.{js,html}
716 #[case("assets/index.html", "assets/*.{js,html}", true)]
717 #[case("assets/index.html", "assets/**.{js,html}", true)]
718 #[case("assets/index.html", "assets/**/*.{js,html}", true)]
719 #[case("assets/index.html", "assets/**/**.{js,html}", true)]
720 // assets/app.js, assets/*.{js,html}
721 #[case("assets/app.js", "assets/*.{js,html}", true)]
722 #[case("assets/app.js", "assets/**.{js,html}", true)]
723 #[case("assets/app.js", "assets/**/*.{js,html}", true)]
724 #[case("assets/app.js", "assets/**/**.{js,html}", true)]
725 fn matches_asset_pattern(
726 #[case] asset_path: &str,
727 #[case] config_pattern: &str,
728 #[case] expected: bool,
729 ) {
730 let asset = Asset::new(asset_path, vec![]);
731 let config: NormalizedAssetConfig = AssetConfig::Pattern {
732 pattern: config_pattern.to_string(),
733 content_type: None,
734 headers: vec![],
735 encodings: vec![],
736 }
737 .try_into()
738 .unwrap();
739
740 assert_eq!(config.matches_asset(&asset), expected);
741 }
742
743 #[rstest]
744 #[case("index.html")]
745 #[case("app.js")]
746 #[case("index.js")]
747 #[case("index.css")]
748 fn does_not_match_asset_redirect(#[case] asset_path: &str) {
749 let asset = Asset::new(asset_path, vec![]);
750 let config: NormalizedAssetConfig = AssetConfig::Redirect {
751 from: asset_path.to_string(),
752 to: asset_path.to_string(),
753 kind: AssetRedirectKind::Permanent,
754 headers: vec![(
755 "content-type".to_string(),
756 "text/plain; charset=utf-8".to_string(),
757 )],
758 }
759 .try_into()
760 .unwrap();
761
762 assert!(!config.matches_asset(&asset));
763 }
764
765 #[rstest]
766 fn asset_encoding_to_string() {
767 assert_eq!(AssetEncoding::Brotli.to_string(), "br");
768 assert_eq!(AssetEncoding::Zstd.to_string(), "zstd");
769 assert_eq!(AssetEncoding::Gzip.to_string(), "gzip");
770 assert_eq!(AssetEncoding::Deflate.to_string(), "deflate");
771 assert_eq!(AssetEncoding::Identity.to_string(), "identity");
772 }
773}