ic_asset_certification/
lib.rs

1//! # Asset certification
2//!
3//! Asset certification is a specialized form of
4//! [HTTP certification](https://internetcomputer.org/docs/current/developer-docs/http-compatible-canisters/custom-http-canisters)
5//! purpose-built for certifying static assets in [ICP](https://internetcomputer.org/) canisters.
6//!
7//! The `ic-asset-certification` crate provides the necessary functionality to
8//! certify and serve static assets from Rust canisters.
9//!
10//! This is implemented in the following steps:
11//!
12//! 1. [Preparing assets](#preparing-assets).
13//! 2. [Configuring asset certification](#configuring-asset-certification).
14//! 3. [Inserting assets into the asset router](#inserting-assets-into-the-asset-router).
15//! 4. [Serving assets](#serving-assets).
16//! 5. [Deleting assets](#deleting-assets).
17//! 6. [Querying assets](#querying-assets).
18//!
19//! For canisters that need it, it's also possible to [delete assets](#deleting-assets).
20//!
21//! ## Preparing assets
22//!
23//! This library is unopinionated about where assets come from, so that is not
24//! covered in detail here. However, there are three main options:
25//!
26//! - Embedding assets in the canister at compile time:
27//!   - [include_bytes!](https://doc.rust-lang.org/std/macro.include_bytes.html)
28//!   - [include_dir!](https://docs.rs/include_dir/latest/include_dir/index.html)
29//! - Uploading assets via canister endpoints at runtime:
30//!   - The [`dfx` asset canister](https://github.com/dfinity/sdk/blob/master/docs/design/asset-canister-interface.md) is a good example of this approach.
31//! - Generating assets dynamically in code, at runtime.
32//!
33//! With the assets in memory, they can be converted into the [Asset] type:
34//!
35//! ```rust
36//! use ic_asset_certification::Asset;
37//!
38//! let asset = Asset::new(
39//!     "index.html",
40//!     b"<html><body><h1>Hello World!</h1></body></html>".as_slice(),
41//! );
42//! ```
43//!
44//! It is recommended to use references when including assets directly into the
45//! canister to avoid duplicating the content. This is particularly important for
46//! larger assets.
47//!
48//! ```rust
49//! use ic_asset_certification::Asset;
50//!
51//! let pretty_big_asset = include_bytes!("lib.rs");
52//! let asset = Asset::new(
53//!     "assets/pretty-big-asset.gz",
54//!     pretty_big_asset.as_slice(),
55//! );
56//! ```
57//!
58//! In some cases, it may be necessary to use owned values, such as when assets are
59//! dynamically generated or modified at runtime.
60//!
61//! ```rust
62//! use ic_asset_certification::Asset;
63//!
64//! let name = "World";
65//! let asset = Asset::new(
66//!     "index.html",
67//!     format!("<html><body><h1>Hello {name}!</h1></body></html>").into_bytes(),
68//! );
69//! ```
70//!
71//! ## Configuring asset certification
72//!
73//! [AssetConfig] defines the configuration for any files that will be certified.
74//! The configuration can either be matched to an individual file by [path](AssetConfig::File) or to
75//! many files by a [glob](AssetConfig::Pattern).
76//!
77//! In both cases, the following options can be configured for each asset:
78//!
79//! - `content_type`
80//!   - Providing this option will certify and serve a `Content-Type` header with
81//!     the provided value.
82//!   - If this value is not provided, the `Content-Type` header will not be
83//!     inserted.
84//!   - If the `Content-Type` header is not sent to the browser, the browser will
85//!     try to guess the content type based on the file extension, unless an
86//!     `X-Content-Type-Options: nosniff` header is sent.
87//!   - Not certifying the `Content-Type` header will also allow a malicious replica
88//!     to insert its own `Content-Type` header, which could lead to a security
89//!     vulnerability.
90//! - `headers`
91//!   - Any additional headers provided will be certified and served with the
92//!     asset.
93//!   - It's important to include any headers that can affect browser behavior,
94//!     particularly [security headers](https://owasp.org/www-project-secure-headers/index.html).
95//! - `encodings`
96//!     - A list of alternative encodings that can be used to serve the asset.
97//!     - Each entry is a tuple of the [encoding name](AssetEncoding) and the file
98//!       extension used in the file path, that can be conveniently created with
99//!       the `default_config` factory method. For example, to include Brotli and Gzip encodings:
100//!       `vec![AssetEncoding::Brotli.default_config(), AssetEncoding::Gzip.default_config()]`.
101//!     - The default file extensions for each encoding are:
102//!         - Brotli: `br`
103//!         - Gzip: `gz`
104//!         - Deflate: `zz`
105//!         - Zstd: `zst`
106//!     - Alternatively, a custom file extension can be provided for each encoding
107//!       by using the `custom_config` factory method. For example, to include a custom
108//!       file extension for Brotli and Gzip encodings:
109//!       `vec![AssetEncoding::Brotli.custom_config("brotli"), AssetEncoding::Gzip.custom_config("gzip")]`.
110//!     - Each encoding referenced must be provided to the asset router as a
111//!       separate file with the same filename as the original file, but with an
112//!       additional file extension matching the configuration. For example, if the
113//!       current matched file is named `file.html`, then the asset router will
114//!       look for `file.html.br` and `file.html.gz`.
115//!     - If the file is found, the asset will be certified and served with the
116//!       provided encoding according to the `Accept-Encoding`.
117//!     - Encodings are prioritized in the following order:
118//!         - Brotli
119//!         - Zstd
120//!         - Gzip
121//!         - Deflate
122//!         - Identity
123//!     - The asset router will return the highest priority encoding that has been
124//!       certified and is supported by the client.
125//!
126//! ### Configuring individual files
127//!
128//! When configuring an individual file, the [path](AssetConfig::File::path) property is provided and must
129//! match the path passed into the [Asset] constructor in the previous step.
130//!
131//! In addition to the common configuration options, individual assets also have
132//! the option of registering the asset as a [fallback response](AssetConfig::File::fallback_for) for a particular
133//! scope. This can be used to configure 404 pages or single-page application
134//! entry points, for example.
135//!
136//! When serving assets, if a requested path does not exactly match any assets, then
137//! a search is conducted for an asset configured with the fallback scope that most
138//! closely matches the requested asset's path.
139//!
140//! For example, if a request is made for `/app.js` and no asset with that exact
141//! path is found, an attempt will be made to serve an asset configured with a
142//! fallback scope of `/`.
143//!
144//! This will be done recursively until it's no longer
145//! possible to find a valid fallback. For example, if a request is made for
146//! `/assets/js/app/core/index.js` and no asset with that exact path is found, then
147//! the search will check for assets configured with the following fallback scopes,
148//! in order:
149//!
150//! - `/assets/js/app/core`
151//! - `/assets/js/app`
152//! - `/assets/js`
153//! - `/assets`
154//! - `/`
155//!
156//! If multiple fallback assets are configured, the first one found will be used,
157//! since that will be the most specific one available for that path. If no asset is
158//! found with any of these fallback scopes, no response will be returned.
159//!
160//! It's also possible to register aliases for an asset. This can be useful for
161//! configuring multiple paths that should serve the same asset. For example, if an
162//! asset is configured with the path `index.html`, it can be aliased by the path
163//! `/`.
164//!
165//! The following example configures an individual HTML file to be served by the
166//! on the `/index.html` path, in addition to serving as the fallback for the `/`
167//! scope and setting `/` as an alias for this asset.
168//!
169//! ```rust
170//! use ic_http_certification::StatusCode;
171//! use ic_asset_certification::{AssetConfig, AssetFallbackConfig, AssetEncoding};
172//!
173//! let config = AssetConfig::File {
174//!     path: "index.html".to_string(),
175//!     content_type: Some("text/html".to_string()),
176//!     headers: vec![
177//!         ("Cache-Control".to_string(), "public, no-cache, no-store".to_string()),
178//!     ],
179//!     fallback_for: vec![AssetFallbackConfig {
180//!         scope: "/".to_string(),
181//!         status_code: Some(StatusCode::OK),
182//!     }],
183//!     aliased_by: vec!["/".to_string()],
184//!     encodings: vec![
185//!         AssetEncoding::Brotli.default_config(),
186//!         AssetEncoding::Gzip.default_config(),
187//!     ],
188//! };
189//! ```
190//!
191//! It's also possible to configure multiple fallbacks for a single asset. The
192//! following example configures an individual HTML file to be served on the
193//! `/404.html` path, in addition to serving as the fallback for the `/js` and `/css`
194//! scopes.
195//!
196//! Any request to paths starting in `/js` and `/css` directories that don't exactly
197//! match an asset will be routed to the `/404.html` asset.
198//!
199//! Multiple aliases are also configured for this asset, namely:
200//! - `/404`,
201//! - `/404/`,
202//! - `/404.html`
203//! - `/not-found`
204//! - `/not-found/`
205//! - `/not-found/index.html`
206//!
207//! Requests to any of those aliases will serve the `/404.html` asset.
208//!
209//! ```rust
210//! use ic_http_certification::StatusCode;
211//! use ic_asset_certification::{AssetConfig, AssetFallbackConfig, AssetEncoding};
212//!
213//! let config = AssetConfig::File {
214//!     path: "404.html".to_string(),
215//!     content_type: Some("text/html".to_string()),
216//!     headers: vec![
217//!         ("Cache-Control".to_string(), "public, no-cache, no-store".to_string()),
218//!     ],
219//!     fallback_for: vec![
220//!         AssetFallbackConfig {
221//!             scope: "/css".to_string(),
222//!             status_code: Some(StatusCode::NOT_FOUND),
223//!         },
224//!         AssetFallbackConfig {
225//!             scope: "/js".to_string(),
226//!             status_code: Some(StatusCode::NOT_FOUND),
227//!         },
228//!     ],
229//!     aliased_by: vec![
230//!         "/404".to_string(),
231//!         "/404/".to_string(),
232//!         "/404.html".to_string(),
233//!         "/not-found".to_string(),
234//!         "/not-found/".to_string(),
235//!         "/not-found/index.html".to_string(),
236//!     ],
237//!     encodings: vec![
238//!         AssetEncoding::Brotli.default_config(),
239//!         AssetEncoding::Gzip.default_config(),
240//!     ],
241//! };
242//! ```
243//!
244//! ### Configuring file patterns
245//!
246//! When configuring file patterns, the `pattern` property is provided. This
247//! property is a glob pattern that will be used to match multiple files.
248//!
249//! Standard Unix-style glob syntax is supported:
250//!
251//! - `?` matches any single character.
252//! - `*` matches zero or more characters.
253//! - `**` recursively matches directories but is only legal in three
254//!   situations.
255//!   - If the glob starts with '**\/`, then it matches all directories.
256//!   For example, `**\/foo` matches `foo` and `bar\/foo` but not
257//!   `foo\/bar\`.
258//! - If the glob ends with `\/**`, then it matches all sub-entries.
259//!   For example, `foo\/\**` matches `foo\/a` and `foo\/a\/b`, but not
260//!   `foo`.
261//! - If the glob contains `\/\**\/` anywhere within the pattern, then it
262//!   matches zero or more directories.
263//! - Using `**` anywhere else is illegal.
264//! - The glob `**` is allowed and means "match everything".
265//! - `{a,b}` matches `a` or `b` where `a` and `b` are arbitrary glob
266//! patterns. (N.B. Nesting {...} is not currently allowed.)
267//! - `[ab]` matches `a` or `b` where `a` and `b` are characters.
268//! - `[!ab]` to match any character except for `a` and `b`.
269//! - Metacharacters such as `*` and `?` can be escaped with character
270//! class notation, e.g., `[*]` matches `*`.
271//!
272//! For example, the following pattern will match all `.js` files in the `js`
273//! directory:
274//!
275//! ```rust
276//! use ic_http_certification::StatusCode;
277//! use ic_asset_certification::{AssetConfig, AssetEncoding};
278//!
279//! let config = AssetConfig::Pattern {
280//!     pattern: "js/*.js".to_string(),
281//!     content_type: Some("application/javascript".to_string()),
282//!     headers: vec![
283//!         ("Cache-Control".to_string(), "public, max-age=31536000, immutable".to_string()),
284//!     ],
285//!     encodings: vec![
286//!         AssetEncoding::Brotli.default_config(),
287//!         AssetEncoding::Gzip.default_config(),
288//!     ],
289//! };
290//! ```
291//!
292//! ### Configuring redirects
293//!
294//! Redirects can be configured using the [AssetConfig::Redirect] variant. This
295//! variant takes `from` and `to` paths, and a redirect [kind](AssetRedirectKind).
296//! When a request is made to the `from` path, the client will be redirected to the
297//! `to` path. The [AssetConfig::Redirect] config is not matched against any [Asset]s.
298//!
299//! Redirects can be configured as either [permanent](AssetRedirectKind::Permanent)
300//! or [temporary](AssetRedirectKind::Temporary).
301//!
302//! The browser will cache permanent redirects and will not request the old
303//! location again. This is useful when the resource has permanently moved to a new
304//! location. The browser will update its bookmarks and search engine results.
305//!
306//! See the
307//! [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301)
308//! for more information on permanent redirects.
309//!
310//! The browser will not cache temporary redirects and will request
311//! the old location again. This is useful when the resource has temporarily moved
312//! to a new location. The browser will not update its bookmarks and search engine
313//! results.
314//!
315//! See the
316//! [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307)
317//! for more information on temporary redirects.
318//!
319//! The following example configures a permanent redirect from `/old` to `/new`:
320//!
321//! ```rust
322//! use ic_asset_certification::{AssetConfig, AssetRedirectKind};
323//!
324//! let config = AssetConfig::Redirect {
325//!     from: "/old".to_string(),
326//!     to: "/new".to_string(),
327//!     kind: AssetRedirectKind::Permanent,
328//!     headers: vec![(
329//!         "content-type".to_string(),
330//!         "text/plain; charset=utf-8".to_string(),
331//!     )],
332//! };
333//! ```
334//!
335//! ## Inserting assets into the asset router
336//!
337//! The [AssetRouter] is responsible for certifying responses and routing requests to
338//! the appropriate response.
339//!
340//! Assets can be inserted using the
341//! [certify_assets](AssetRouter::certify_assets) method:
342//!
343//! ```rust
344//! use ic_http_certification::StatusCode;
345//! use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRouter, AssetRedirectKind, AssetEncoding};
346//!
347//! let mut asset_router = AssetRouter::default();
348//!
349//! let assets = vec![
350//!     Asset::new(
351//!         "index.html",
352//!         b"<html><body><h1>Hello World!</h1></body></html>".as_slice(),
353//!     ),
354//!     Asset::new(
355//!         "index.html.gz",
356//!         &[0, 1, 2, 3, 4, 5]
357//!     ),
358//!     Asset::new(
359//!         "index.html.br",
360//!         &[6, 7, 8, 9, 10, 11]
361//!     ),
362//!     Asset::new(
363//!         "app.js",
364//!         b"console.log('Hello World!');".as_slice(),
365//!     ),
366//!     Asset::new(
367//!         "app.js.gz",
368//!         &[12, 13, 14, 15, 16, 17],
369//!     ),
370//!     Asset::new(
371//!         "app.js.br",
372//!         &[18, 19, 20, 21, 22, 23],
373//!     ),
374//!     Asset::new(
375//!         "css/app-ba74b708.css",
376//!         b"html,body{min-height:100vh;}".as_slice(),
377//!     ),
378//!     Asset::new(
379//!         "css/app-ba74b708.css.gz",
380//!         &[24, 25, 26, 27, 28, 29],
381//!     ),
382//!     Asset::new(
383//!         "css/app-ba74b708.css.br",
384//!         &[30, 31, 32, 33, 34, 35],
385//!     ),
386//! ];
387//!
388//! let asset_configs = vec![
389//!     AssetConfig::File {
390//!         path: "index.html".to_string(),
391//!         content_type: Some("text/html".to_string()),
392//!         headers: vec![(
393//!             "cache-control".to_string(),
394//!             "public, no-cache, no-store".to_string(),
395//!         )],
396//!         fallback_for: vec![AssetFallbackConfig {
397//!             scope: "/".to_string(),
398//!             status_code: Some(StatusCode::OK),
399//!         }],
400//!         aliased_by: vec!["/".to_string()],
401//!         encodings: vec![
402//!             AssetEncoding::Brotli.default_config(),
403//!             AssetEncoding::Gzip.default_config(),
404//!         ],
405//!     },
406//!     AssetConfig::Pattern {
407//!         pattern: "**/*.js".to_string(),
408//!         content_type: Some("text/javascript".to_string()),
409//!         headers: vec![(
410//!             "cache-control".to_string(),
411//!             "public, max-age=31536000, immutable".to_string(),
412//!         )],
413//!         encodings: vec![
414//!             AssetEncoding::Brotli.default_config(),
415//!             AssetEncoding::Gzip.default_config(),
416//!         ],
417//!     },
418//!     AssetConfig::Pattern {
419//!         pattern: "**/*.css".to_string(),
420//!         content_type: Some("text/css".to_string()),
421//!         headers: vec![(
422//!             "cache-control".to_string(),
423//!             "public, max-age=31536000, immutable".to_string(),
424//!         )],
425//!         encodings: vec![
426//!             AssetEncoding::Brotli.default_config(),
427//!             AssetEncoding::Gzip.default_config(),
428//!         ],
429//!     },
430//!     AssetConfig::Redirect {
431//!         from: "/old".to_string(),
432//!         to: "/new".to_string(),
433//!         kind: AssetRedirectKind::Permanent,
434//!         headers: vec![(
435//!             "content-type".to_string(),
436//!             "text/plain; charset=utf-8".to_string(),
437//!         )],
438//!     },
439//! ];
440//!
441//! asset_router.certify_assets(assets, asset_configs).unwrap();
442//! ```
443//!
444//! After certifying assets, make sure to set the canister's
445//! certified data:
446//!
447//! ```ignore
448//! use ic_cdk::api::set_certified_data;
449//!
450//! set_certified_data(&asset_router.root_hash());
451//! ```
452//!
453//! It's also possible to initialize the router with an
454//! [HttpCertificationTree](ic_http_certification::HttpCertificationTree). This is
455//! useful when direct access to the
456//! [HttpCertificationTree](ic_http_certification::HttpCertificationTree) is required
457//! for certifying [HttpRequest](ic_http_certification::HttpRequest)s and
458//! [HttpResponse](ic_http_certification::HttpResponse)s outside of the [AssetRouter].
459//!
460//! ```rust
461//! use std::{cell::RefCell, rc::Rc};
462//! use ic_http_certification::HttpCertificationTree;
463//! use ic_asset_certification::AssetRouter;
464//!
465//! let mut http_certification_tree: Rc<RefCell<HttpCertificationTree>> = Default::default();
466//! let mut asset_router = AssetRouter::with_tree(http_certification_tree.clone());
467//! ```
468//!
469//! ## Serving assets
470//!
471//! Assets can be served by calling the `serve_asset` method on the `AssetRouter`.
472//! This method will return a response, a witness, and an expression path, which can be used
473//! alongside the canister's data certificate to add the required certificate header to the response.
474//!
475//! ```rust
476//! use ic_http_certification::{HttpRequest, utils::add_v2_certificate_header, StatusCode};
477//! use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRouter};
478//!
479//! let mut asset_router = AssetRouter::default();
480//!
481//! let asset = Asset::new(
482//!     "index.html",
483//!     b"<html><body><h1>Hello World!</h1></body></html>".as_slice(),
484//! );
485//!
486//! let asset_config = AssetConfig::File {
487//!     path: "index.html".to_string(),
488//!     content_type: Some("text/html".to_string()),
489//!     headers: vec![
490//!         ("Cache-Control".to_string(), "public, no-cache, no-store".to_string()),
491//!     ],
492//!     fallback_for: vec![AssetFallbackConfig {
493//!         scope: "/".to_string(),
494//!         status_code: Some(StatusCode::OK),
495//!     }],
496//!     aliased_by: vec!["/".to_string()],
497//!     encodings: vec![],
498//! };
499//!
500//! let http_request = HttpRequest::get("/").build();
501//!
502//! asset_router.certify_assets(vec![asset], vec![asset_config]).unwrap();
503//!
504//! // This should normally be retrieved using `ic_cdk::api::data_certificate()`.
505//! let data_certificate = vec![1, 2, 3];
506//! let response = asset_router.serve_asset(&data_certificate, &http_request).unwrap();
507//!```
508//!
509//! ## Deleting assets
510//!
511//! There are three ways to delete assets from the asset router:
512//! 1. [By configuration](#deleting-assets-by-configuration).
513//! 1. [By path](#deleting-assets-by-path).
514//! 1. [All at once](#deleting-all-assets).
515//!
516//! ### Deleting assets by configuration
517//!
518//! Deleting assets by configuration is similar to [certifying them](#inserting-assets-into-the-asset-router).
519//!
520//! Depending on the configuration provided to the [certify_assets](AssetRouter::certify_assets) function,
521//! multiple responses may be generated for the same asset. To ensure that all generated responses are deleted,
522//! the [delete_assets](AssetRouter::delete_assets) function accepts the same configuration.
523//!
524//! If a configuration different from the one used to certify assets in the first place is provided,
525//! one of two things can happen:
526//!
527//! 1. If the configuration includes a file that was not certified in the first place, it will be silently ignored.
528//! For example, if the configuration provided to `certify_assets` includes the Brotli and Gzip encodings, but the
529//! configuration provided to `delete_assets` includes Brotli, Gzip, and Deflate. the Brotli and Gzip encoded files will be deleted, while the Deflate file is ignored, since it doesn't exist.
530//!
531//! 2. If the configuration excludes a file that was certified, it will not be deleted. For example, if the configuration,
532//! provided to `certify_assets` includes the Brotli and Gzip encodings, but the configuration provided to `delete_assets`
533//! only includes Brotli, then the Gzip file will not be deleted.
534//!
535//! Assuming the same base example used above to demonstrate certifying assets:
536//!
537//! ```rust
538//! use ic_http_certification::StatusCode;
539//! use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRouter, AssetRedirectKind, AssetEncoding};
540//!
541//! let mut asset_router = AssetRouter::default();
542//!
543//! let assets = vec![
544//!     Asset::new(
545//!         "index.html",
546//!         b"<html><body><h1>Hello World!</h1></body></html>".as_slice(),
547//!     ),
548//!     Asset::new(
549//!         "index.html.gz",
550//!         &[0, 1, 2, 3, 4, 5]
551//!     ),
552//!     Asset::new(
553//!         "index.html.br",
554//!         &[6, 7, 8, 9, 10, 11]
555//!     ),
556//!     Asset::new(
557//!         "app.js",
558//!         b"console.log('Hello World!');".as_slice(),
559//!     ),
560//!     Asset::new(
561//!         "app.js.gz",
562//!         &[12, 13, 14, 15, 16, 17],
563//!     ),
564//!     Asset::new(
565//!         "app.js.br",
566//!         &[18, 19, 20, 21, 22, 23],
567//!     ),
568//!     Asset::new(
569//!         "css/app-ba74b708.css",
570//!         b"html,body{min-height:100vh;}".as_slice(),
571//!     ),
572//!     Asset::new(
573//!         "css/app-ba74b708.css.gz",
574//!         &[24, 25, 26, 27, 28, 29],
575//!     ),
576//!     Asset::new(
577//!         "css/app-ba74b708.css.br",
578//!         &[30, 31, 32, 33, 34, 35],
579//!     ),
580//! ];
581//!
582//! let asset_configs = vec![
583//!     AssetConfig::File {
584//!         path: "index.html".to_string(),
585//!         content_type: Some("text/html".to_string()),
586//!         headers: vec![(
587//!             "cache-control".to_string(),
588//!             "public, no-cache, no-store".to_string(),
589//!         )],
590//!         fallback_for: vec![AssetFallbackConfig {
591//!             scope: "/".to_string(),
592//!             status_code: Some(StatusCode::OK),
593//!         }],
594//!         aliased_by: vec!["/".to_string()],
595//!         encodings: vec![
596//!             AssetEncoding::Brotli.default_config(),
597//!             AssetEncoding::Gzip.default_config(),
598//!         ],
599//!     },
600//!     AssetConfig::Pattern {
601//!         pattern: "**/*.js".to_string(),
602//!         content_type: Some("text/javascript".to_string()),
603//!         headers: vec![(
604//!             "cache-control".to_string(),
605//!             "public, max-age=31536000, immutable".to_string(),
606//!         )],
607//!         encodings: vec![
608//!             AssetEncoding::Brotli.default_config(),
609//!             AssetEncoding::Gzip.default_config(),
610//!         ],
611//!     },
612//!     AssetConfig::Pattern {
613//!         pattern: "**/*.css".to_string(),
614//!         content_type: Some("text/css".to_string()),
615//!         headers: vec![(
616//!             "cache-control".to_string(),
617//!             "public, max-age=31536000, immutable".to_string(),
618//!         )],
619//!         encodings: vec![
620//!             AssetEncoding::Brotli.default_config(),
621//!             AssetEncoding::Gzip.default_config(),
622//!         ],
623//!     },
624//!     AssetConfig::Redirect {
625//!         from: "/old".to_string(),
626//!         to: "/new".to_string(),
627//!         kind: AssetRedirectKind::Permanent,
628//!         headers: vec![(
629//!             "content-type".to_string(),
630//!             "text/plain; charset=utf-8".to_string(),
631//!         )],
632//!     },
633//! ];
634//!
635//! asset_router.certify_assets(assets, asset_configs).unwrap();
636//! ```
637//!
638//! To delete the `index.html` asset, along with the fallback configuration for the `/` scope, the alias `/` and the alternative encodings:
639//!
640//! ```rust
641//! # use ic_http_certification::StatusCode;
642//! # use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRouter, AssetRedirectKind, AssetEncoding};
643//!
644//! # let mut asset_router = AssetRouter::default();
645//!
646//! asset_router
647//!     .delete_assets(
648//!         vec![
649//!             Asset::new(
650//!                 "index.html",
651//!                 b"<html><body><h1>Hello World!</h1></body></html>".as_slice(),
652//!             ),
653//!             Asset::new("index.html.gz", &[0, 1, 2, 3, 4, 5]),
654//!             Asset::new("index.html.br", &[6, 7, 8, 9, 10, 11]),
655//!         ],
656//!         vec![AssetConfig::File {
657//!             path: "index.html".to_string(),
658//!             content_type: Some("text/html".to_string()),
659//!             headers: vec![(
660//!                 "cache-control".to_string(),
661//!                 "public, no-cache, no-store".to_string(),
662//!             )],
663//!             fallback_for: vec![AssetFallbackConfig {
664//!                 scope: "/".to_string(),
665//!                 status_code: Some(StatusCode::OK),
666//!             }],
667//!             aliased_by: vec!["/".to_string()],
668//!             encodings: vec![
669//!                 AssetEncoding::Brotli.default_config(),
670//!                 AssetEncoding::Gzip.default_config(),
671//!             ],
672//!         }],
673//!     )
674//!     .unwrap();
675//! ```
676//!
677//! To delete the `app.js`asset, along with the alternative encodings:
678//!
679//! ```rust
680//! # use ic_http_certification::StatusCode;
681//! # use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRouter, AssetRedirectKind, AssetEncoding};
682//!
683//! # let mut asset_router = AssetRouter::default();
684//!
685//! asset_router
686//!     .delete_assets(
687//!         vec![
688//!             Asset::new("app.js", b"console.log('Hello World!');".as_slice()),
689//!             Asset::new("app.js.gz", &[12, 13, 14, 15, 16, 17]),
690//!             Asset::new("app.js.br", &[18, 19, 20, 21, 22, 23]),
691//!         ],
692//!         vec![AssetConfig::Pattern {
693//!             pattern: "**/*.js".to_string(),
694//!             content_type: Some("text/javascript".to_string()),
695//!             headers: vec![(
696//!                 "cache-control".to_string(),
697//!                 "public, max-age=31536000, immutable".to_string(),
698//!             )],
699//!             encodings: vec![
700//!                 AssetEncoding::Brotli.default_config(),
701//!                 AssetEncoding::Gzip.default_config(),
702//!             ],
703//!         }],
704//!     )
705//!     .unwrap();
706//! ```
707//!
708//! To delete the `css/app-ba74b708.css` asset, along with the alternative encodings:
709//!
710//! ```rust
711//! # use ic_http_certification::StatusCode;
712//! # use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRouter, AssetRedirectKind, AssetEncoding};
713//!
714//! # let mut asset_router = AssetRouter::default();
715//!
716//! asset_router.delete_assets(
717//!     vec![
718//!         Asset::new(
719//!             "css/app-ba74b708.css",
720//!             b"html,body{min-height:100vh;}".as_slice(),
721//!         ),
722//!         Asset::new(
723//!             "css/app-ba74b708.css.gz",
724//!             &[24, 25, 26, 27, 28, 29],
725//!         ),
726//!         Asset::new(
727//!             "css/app-ba74b708.css.br",
728//!             &[30, 31, 32, 33, 34, 35],
729//!         ),
730//!     ],
731//!     vec![
732//!         AssetConfig::Pattern {
733//!             pattern: "**/*.css".to_string(),
734//!             content_type: Some("text/css".to_string()),
735//!             headers: vec![(
736//!                 "cache-control".to_string(),
737//!                 "public, max-age=31536000, immutable".to_string(),
738//!             )],
739//!             encodings: vec![
740//!                 AssetEncoding::Brotli.default_config(),
741//!                 AssetEncoding::Gzip.default_config(),
742//!             ],
743//!         },
744//!     ]
745//! ).unwrap();
746//! ```
747//!
748//! And finally, to delete the `/old` redirect:
749//!
750//! ```rust
751//! # use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRouter, AssetRedirectKind, AssetEncoding};
752//!
753//! # let mut asset_router = AssetRouter::default();
754//!
755//! asset_router
756//!     .delete_assets(
757//!         vec![],
758//!         vec![AssetConfig::Redirect {
759//!             from: "/old".to_string(),
760//!             to: "/new".to_string(),
761//!             kind: AssetRedirectKind::Permanent,
762//!             headers: vec![(
763//!                 "content-type".to_string(),
764//!                 "text/plain; charset=utf-8".to_string(),
765//!              )],
766//!         }],
767//!     )
768//!     .unwrap();
769//! ```
770//!
771//! After deleting any assets, make sure to set the canister's
772//! certified data again:
773//!
774//! ```ignore
775//! use ic_cdk::api::set_certified_data;
776//!
777//! set_certified_data(&asset_router.root_hash());
778//! ```
779//!
780//! ### Deleting assets by path
781//!
782//! To delete assets by path, use the
783//! [delete_assets_by_path](AssetRouter::delete_assets_by_path) function.
784//!
785//! Depending on the configuration provided to the [certify_assets](AssetRouter::certify_assets) function,
786//! multiple responses may be generated for the same asset. These assets may exist on different paths,
787//! for example, if the `alias` configuration is used. If `alias` paths are not passed to this function,
788//! they will not be deleted.
789//!
790//! If multiple encodings exist for a path, all encodings will be deleted.
791//!
792//! Fallbacks are also not deleted; to delete them, use the
793//! [delete_fallback_assets_by_path](AssetRouter::delete_fallback_assets_by_path) function.
794//!
795//! Assuming the same base example used above to demonstrate certifying assets:
796//!
797//! ```rust
798//! use ic_http_certification::StatusCode;
799//! use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRouter, AssetRedirectKind, AssetEncoding};
800//!
801//! let mut asset_router = AssetRouter::default();
802//!
803//! let assets = vec![
804//!     Asset::new(
805//!         "index.html",
806//!         b"<html><body><h1>Hello World!</h1></body></html>".as_slice(),
807//!     ),
808//!     Asset::new(
809//!         "index.html.gz",
810//!         &[0, 1, 2, 3, 4, 5]
811//!     ),
812//!     Asset::new(
813//!         "index.html.br",
814//!         &[6, 7, 8, 9, 10, 11]
815//!     ),
816//!     Asset::new(
817//!         "app.js",
818//!         b"console.log('Hello World!');".as_slice(),
819//!     ),
820//!     Asset::new(
821//!         "app.js.gz",
822//!         &[12, 13, 14, 15, 16, 17],
823//!     ),
824//!     Asset::new(
825//!         "app.js.br",
826//!         &[18, 19, 20, 21, 22, 23],
827//!     ),
828//!     Asset::new(
829//!         "css/app-ba74b708.css",
830//!         b"html,body{min-height:100vh;}".as_slice(),
831//!     ),
832//!     Asset::new(
833//!         "css/app-ba74b708.css.gz",
834//!         &[24, 25, 26, 27, 28, 29],
835//!     ),
836//!     Asset::new(
837//!         "css/app-ba74b708.css.br",
838//!         &[30, 31, 32, 33, 34, 35],
839//!     ),
840//! ];
841//!
842//! let asset_configs = vec![
843//!     AssetConfig::File {
844//!         path: "index.html".to_string(),
845//!         content_type: Some("text/html".to_string()),
846//!         headers: vec![(
847//!             "cache-control".to_string(),
848//!             "public, no-cache, no-store".to_string(),
849//!         )],
850//!         fallback_for: vec![AssetFallbackConfig {
851//!             scope: "/".to_string(),
852//!             status_code: Some(StatusCode::OK),
853//!         }],
854//!         aliased_by: vec!["/".to_string()],
855//!         encodings: vec![
856//!             AssetEncoding::Brotli.default_config(),
857//!             AssetEncoding::Gzip.default_config(),
858//!         ],
859//!     },
860//!     AssetConfig::Pattern {
861//!         pattern: "**/*.js".to_string(),
862//!         content_type: Some("text/javascript".to_string()),
863//!         headers: vec![(
864//!             "cache-control".to_string(),
865//!             "public, max-age=31536000, immutable".to_string(),
866//!         )],
867//!         encodings: vec![
868//!             AssetEncoding::Brotli.default_config(),
869//!             AssetEncoding::Gzip.default_config(),
870//!         ],
871//!     },
872//!     AssetConfig::Pattern {
873//!         pattern: "**/*.css".to_string(),
874//!         content_type: Some("text/css".to_string()),
875//!         headers: vec![(
876//!             "cache-control".to_string(),
877//!             "public, max-age=31536000, immutable".to_string(),
878//!         )],
879//!         encodings: vec![
880//!             AssetEncoding::Brotli.default_config(),
881//!             AssetEncoding::Gzip.default_config(),
882//!         ],
883//!     },
884//!     AssetConfig::Redirect {
885//!         from: "/old".to_string(),
886//!         to: "/new".to_string(),
887//!         kind: AssetRedirectKind::Permanent,
888//!         headers: vec![("content-type".to_string(), "text/plain".to_string())],
889//!     },
890//! ];
891//!
892//! asset_router.certify_assets(assets, asset_configs).unwrap();
893//! ```
894//!
895//! To delete the `index.html` asset, along with the fallback configuration for the `/` scope, the alias `/` and the alternative encodings:
896//!
897//! ```rust
898//! # use ic_http_certification::StatusCode;
899//! # use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRouter, AssetRedirectKind, AssetEncoding};
900//!
901//! # let mut asset_router = AssetRouter::default();
902//!
903//! asset_router
904//!     .delete_assets_by_path(
905//!         vec![
906//!             "/index.html", // deletes the index.html asset, along with all encodings
907//!             "/" // deletes the `/` alias for index.html, along with all encodings
908//!         ],
909//!     );
910//!
911//! asset_router
912//!     .delete_fallback_assets_by_path(
913//!        vec![
914//!           "/" // deletes the fallback configuration for the `/` scope, along with all encodings
915//!       ]
916//!    );
917//! ```
918//!
919//! To delete the `app.js`asset, along with the alternative encodings:
920//!
921//! ```rust
922//! # use ic_http_certification::StatusCode;
923//! # use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRouter, AssetRedirectKind, AssetEncoding};
924//!
925//! # let mut asset_router = AssetRouter::default();
926//!
927//! asset_router.delete_assets_by_path(vec!["/app.js"]);
928//! ```
929//!
930//! To delete the `css/app-ba74b708.css` asset, along with the alternative encodings:
931//!
932//! ```rust
933//! # use ic_http_certification::StatusCode;
934//! # use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRouter, AssetRedirectKind, AssetEncoding};
935//!
936//! # let mut asset_router = AssetRouter::default();
937//!
938//! asset_router.delete_assets_by_path(vec!["/css/app-ba74b708.css"]);
939//! ```
940//!
941//! And finally, to delete the `/old` redirect:
942//!
943//! ```rust
944//! # use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRouter, AssetRedirectKind, AssetEncoding};
945//!
946//! # let mut asset_router = AssetRouter::default();
947//!
948//! asset_router.delete_assets_by_path(vec!["/old"]);
949//! ```
950//!
951//! After deleting any assets, make sure to set the canister's
952//! certified data again:
953//!
954//! ```ignore
955//! use ic_cdk::api::set_certified_data;
956//!
957//! set_certified_data(&asset_router.root_hash());
958//! ```
959//!
960//! ### Deleting all assets
961//!
962//! It's also possible to delete all assets and their certification in one go:
963//!
964//! ```rust
965//! # use ic_asset_certification::AssetRouter;
966//!
967//! # let mut asset_router = AssetRouter::default();
968//!
969//! asset_router.delete_all_assets();
970//! ```
971//!
972//! After deleting any assets, make sure to set the canister's
973//! certified data again:
974//!
975//! ```ignore
976//! use ic_cdk::api::set_certified_data;
977//!
978//! set_certified_data(&asset_router.root_hash());
979//! ```
980//!
981//! ## Querying assets
982//!
983//! The [AssetRouter] has two functions to retrieve an [AssetMap] containing assets.
984//!
985//! The [get_assets()](AssetRouter::get_assets) function returns all standard assets, while the
986//! [get_fallback_assets()](AssetRouter::get_fallback_assets) function returns all fallback assets.
987//!
988//! The [AssetMap] can be used to query assets by `path`, `encoding`, and `starting_range`.
989//! For standard assets, the path refers to the asset's path, e.g., `/index.html`.
990//!
991//! For fallback assets, the path refers to the scope that the fallback is valid for, e.g., `/`.
992//! See the [fallback_for](crate::AssetConfig::File::fallback_for) config option for more information
993//! on fallback scopes.
994//!
995//! For all types of assets, the encoding refers to the encoding of the asset; see [AssetEncoding].
996//!
997//! Assets greater than 2 MiB are split into multiple ranges; the starting range allows retrieval of
998//! individual chunks of these large assets. The first range is `Some(0)`, the second range is
999//! `Some(ASSET_CHUNK_SIZE)`, the third range is `Some(ASSET_CHUNK_SIZE * 2)`, and so on. The entire asset can
1000//! also be retrieved by passing `None` as the `starting_range`.
1001//! See [ASSET_CHUNK_SIZE] for the size of each chunk.
1002
1003#![deny(missing_docs, missing_debug_implementations, rustdoc::all, clippy::all)]
1004
1005mod asset;
1006mod asset_config;
1007mod asset_map;
1008mod asset_router;
1009mod error;
1010mod types;
1011
1012pub use asset::*;
1013pub use asset_config::*;
1014pub use asset_map::*;
1015pub use asset_router::*;
1016pub use error::*;
1017pub(crate) use types::*;