Skip to main content

ic_asset_certification/
asset_router.rs

1use crate::{
2    Asset, AssetCertificationError, AssetCertificationResult, AssetConfig, AssetEncoding,
3    AssetFallbackConfig, AssetMap, AssetRedirectKind, CertifiedAssetResponse,
4    NormalizedAssetConfig, RequestKey,
5};
6use ic_http_certification::{
7    utils::add_v2_certificate_header, DefaultCelBuilder, DefaultResponseCertification, Hash,
8    HttpCertification, HttpCertificationPath, HttpCertificationTree, HttpCertificationTreeEntry,
9    HttpRequest, HttpResponse, StatusCode, CERTIFICATE_EXPRESSION_HEADER_NAME,
10};
11use std::{borrow::Cow, cell::RefCell, cmp, collections::HashMap, rc::Rc};
12
13/// A router for certifying and serving static [Assets](Asset).
14///
15/// [Asset] certification is configured using the [AssetConfig] enum.
16///
17/// # Examples
18///
19/// ```
20/// use ic_http_certification::{HttpRequest, StatusCode};
21/// use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRouter, AssetRedirectKind, AssetEncoding};
22///
23/// let mut asset_router = AssetRouter::default();
24///
25/// let index_html_body = b"<html><body><h1>Hello World!</h1></body></html>".as_slice();
26/// let app_js_body = b"console.log('Hello World!');".as_slice();
27/// let app_css_body = b"html,body{min-height:100vh;}".as_slice();
28///
29/// let assets = vec![
30///     Asset::new("index.html", index_html_body),
31///     Asset::new("js/app-488df671.js", app_js_body),
32///     Asset::new("css/app-ba74b708.css", app_css_body),
33/// ];
34///
35/// let asset_configs = vec![
36///     AssetConfig::File {
37///         path: "index.html".to_string(),
38///         content_type: Some("text/html".to_string()),
39///         headers: vec![(
40///             "cache-control".to_string(),
41///             "public, no-cache, no-store".to_string(),
42///         )],
43///         fallback_for: vec![AssetFallbackConfig {
44///             status_code: Some(StatusCode::OK),
45///             scope: "/".to_string(),
46///         }],
47///         aliased_by: vec!["/".to_string()],
48///         encodings: vec![
49///             AssetEncoding::Brotli.default_config(),
50///             AssetEncoding::Gzip.default_config(),
51///         ],
52///     },
53///     AssetConfig::Pattern {
54///         pattern: "**/*.js".to_string(),
55///         content_type: Some("text/javascript".to_string()),
56///         headers: vec![(
57///             "cache-control".to_string(),
58///             "public, max-age=31536000, immutable".to_string(),
59///         )],
60///         encodings: vec![
61///             AssetEncoding::Brotli.default_config(),
62///             AssetEncoding::Gzip.default_config(),
63///         ],
64///     },
65///     AssetConfig::Pattern {
66///         pattern: "**/*.css".to_string(),
67///         content_type: Some("text/css".to_string()),
68///         headers: vec![(
69///             "cache-control".to_string(),
70///             "public, max-age=31536000, immutable".to_string(),
71///         )],
72///         encodings: vec![
73///             AssetEncoding::Brotli.default_config(),
74///             AssetEncoding::Gzip.default_config(),
75///         ],
76///     },
77///     AssetConfig::Redirect {
78///         from: "/old-url".to_string(),
79///         to: "/".to_string(),
80///         kind: AssetRedirectKind::Permanent,
81///         headers: vec![(
82///             "content-type".to_string(),
83///             "text/plain; charset=utf-8".to_string(),
84///         )],
85///     },
86///     AssetConfig::Redirect {
87///         from: "/css/app.css".to_string(),
88///         to: "/css/app-ba74b708.css".to_string(),
89///         kind: AssetRedirectKind::Temporary,
90///         headers: vec![(
91///             "content-type".to_string(),
92///             "text/plain; charset=utf-8".to_string(),
93///         )],
94///     },
95/// ];
96///
97/// asset_router
98///     .certify_assets(assets, asset_configs)
99///     .unwrap();
100///
101/// let index_html_request = HttpRequest::get("/").build();
102///
103/// // this should normally be retrieved using `ic_cdk::api::data_certificate()`.
104/// let data_certificate = vec![1, 2, 3];
105/// let index_html_response = asset_router
106///     .serve_asset(&data_certificate, &index_html_request)
107///     .unwrap();
108/// ```
109///
110/// It's also possible to initialize the [AssetRouter] with an external
111/// [HttpCertificationTree], for cases where the tree needs to be used to
112/// certify other HTTP responses.
113///
114/// ```
115/// use std::{cell::RefCell, rc::Rc};
116/// use ic_http_certification::HttpCertificationTree;
117/// use ic_asset_certification::AssetRouter;
118///
119/// let mut http_certification_tree: Rc<RefCell<HttpCertificationTree>> = Default::default();
120/// let mut asset_router = AssetRouter::with_tree(http_certification_tree.clone());
121/// ```
122#[derive(Debug)]
123pub struct AssetRouter<'content> {
124    tree: Rc<RefCell<HttpCertificationTree>>,
125    responses: HashMap<RequestKey, CertifiedAssetResponse<'content>>,
126    fallback_responses: HashMap<RequestKey, CertifiedAssetResponse<'content>>,
127}
128
129#[derive(Debug, PartialEq)]
130struct RangeRequestValues {
131    pub range_begin: usize,
132    #[allow(unused)]
133    pub range_end: Option<usize>,
134}
135
136/// The chunk size that will be used when splitting assets larger than 2mb down into smaller chunks.
137pub const ASSET_CHUNK_SIZE: usize = 2_000_000;
138
139fn encoding_str(maybe_encoding: Option<AssetEncoding>) -> Option<String> {
140    maybe_encoding.map(|enc| enc.to_string())
141}
142
143fn parse_range_header_str(range_str: &str) -> Result<RangeRequestValues, String> {
144    // expected format: `bytes=<range-begin>-[<range-end>]`
145    let str_value = range_str.trim();
146    if !str_value.starts_with("bytes=") {
147        return Err(format!("Invalid Range header '{range_str}'").to_string());
148    }
149    let str_value = str_value.trim_start_matches("bytes=");
150    let range_header_parts = str_value.split('-').collect::<Vec<_>>();
151    if range_header_parts.is_empty() || range_header_parts.len() != 2 {
152        return Err(format!("Invalid Range header '{range_str}'").to_string());
153    }
154
155    let range_begin = range_header_parts[0]
156        .parse::<usize>()
157        .map_err(|e| format!("Malformed range_begin in Range header '{range_str}': {e}"))?;
158    let range_end = match range_header_parts[1] {
159        "" => None,
160        _ => Some(
161            range_header_parts[1]
162                .parse::<usize>()
163                .map_err(|e| format!("Malformed range_end in Range header '{range_str}': {e}"))?,
164        ),
165    };
166
167    if range_begin > range_end.unwrap_or(usize::MAX) {
168        return Err(format!("Invalid values in Range header '{range_str}'").to_string());
169    }
170    Ok(RangeRequestValues {
171        range_begin,
172        range_end,
173    })
174}
175
176impl<'content> AssetRouter<'content> {
177    /// Creates a new [AssetRouter].
178    pub fn new() -> Self {
179        AssetRouter {
180            tree: Default::default(),
181            responses: HashMap::new(),
182            fallback_responses: HashMap::new(),
183        }
184    }
185
186    /// Creates a new [AssetRouter] using the provided
187    /// [HttpCertificationTree](ic_http_certification::HttpCertificationTree)
188    /// for certifying assets.
189    pub fn with_tree(tree: Rc<RefCell<HttpCertificationTree>>) -> Self {
190        AssetRouter {
191            tree,
192            responses: HashMap::new(),
193            fallback_responses: HashMap::new(),
194        }
195    }
196
197    fn maybe_get_range_begin(request: &HttpRequest) -> AssetCertificationResult<Option<usize>> {
198        if let Some(range_str) = Self::get_range_header(request) {
199            parse_range_header_str(range_str)
200                .map(|e| Some(e.range_begin))
201                .map_err(AssetCertificationError::RequestError)
202        } else {
203            Ok(None)
204        }
205    }
206
207    /// Returns the corresponding
208    /// [HttpResponse](ic_http_certification::HttpResponse) for the provided
209    /// [HttpRequest](ic_http_certification::HttpRequest) if it is found
210    /// in the router.
211    ///
212    /// # Arguments
213    ///
214    /// * `data_certificate` - A byte slice representing the data certificate used for asset certification.
215    ///   This should be retrieved using `ic_cdk::api::data_certificate()`.
216    /// * `request` - A reference to an [HttpRequest](ic_http_certification::HttpRequest) object representing the incoming HTTP request.
217    ///
218    /// If an exact match is not found, then a fallback will
219    /// be searched for. See the
220    /// [fallback_for](AssetConfig::File::fallback_for) configuration
221    /// option for more information on fallbacks.
222    ///
223    /// Returns [None] if no suitable
224    /// [HttpResponse](ic_http_certification::HttpResponse) is found for the
225    /// given [HttpRequest](ic_http_certification::HttpRequest).
226    pub fn serve_asset(
227        &self,
228        data_certificate: &[u8],
229        request: &HttpRequest,
230    ) -> AssetCertificationResult<HttpResponse<'content>> {
231        let preferred_encodings = self.get_preferred_encodings(request);
232        let request_url = request.get_path()?;
233        let maybe_range_begin = Self::maybe_get_range_begin(request)?;
234        let mut cert_response = self
235            .get_asset_for_request(&request_url, preferred_encodings, maybe_range_begin)
236            .cloned()?;
237        let witness = self
238            .tree
239            .borrow()
240            .witness(&cert_response.tree_entry, &request_url)?;
241        let expr_path = cert_response.tree_entry.path.to_expr_path();
242        add_v2_certificate_header(
243            data_certificate,
244            &mut cert_response.response,
245            &witness,
246            &expr_path,
247        );
248        Ok(cert_response.response.clone())
249    }
250
251    /// Returns all standard assets stored in the router.
252    ///
253    /// See the [get_fallback_assets()](AssetRouter::get_fallback_assets)
254    /// function for fallback assets.
255    ///
256    /// See the [AssetMap] struct for more information on the returned type.
257    pub fn get_assets(&self) -> &impl AssetMap<'content> {
258        &self.responses
259    }
260
261    /// Returns all fallback assets stored in the router.
262    ///
263    /// See the [get_assets()](AssetRouter::get_assets)
264    /// function for standard assets.
265    ///
266    /// See the [AssetMap] struct for more information on the returned type.
267    pub fn get_fallback_assets(&self) -> &impl AssetMap<'content> {
268        &self.fallback_responses
269    }
270
271    /// Certifies multiple assets and inserts them into the router, to be served
272    /// later by the [serve_asset](AssetRouter::serve_asset) method.
273    ///
274    /// The asset certification is configured using the provided [AssetConfig]
275    /// enum.
276    ///
277    /// If no configuration matches an individual asset, the asset will be
278    /// served and certified as-is, without headers.
279    ///
280    /// After performing this operation, one must set the canister's certified data (`ic_cdk::api::set_certified_data()`)
281    /// to the new [root hash](AssetRouter::root_hash) of the tree.
282    pub fn certify_assets<'path>(
283        &mut self,
284        assets: impl IntoIterator<Item = Asset<'content, 'path>>,
285        asset_configs: impl IntoIterator<Item = AssetConfig>,
286    ) -> AssetCertificationResult {
287        let asset_configs: Vec<NormalizedAssetConfig> = asset_configs
288            .into_iter()
289            .map(TryInto::try_into)
290            .collect::<AssetCertificationResult<_>>()?;
291
292        let asset_map = assets
293            .into_iter()
294            .map(|asset| (asset.path.clone(), asset))
295            .collect::<HashMap<_, _>>();
296
297        for asset in asset_map.values() {
298            let asset_config = asset_configs.iter().find(|e| e.matches_asset(asset));
299            for (encoding, postfix) in asset_config
300                .map(|e| match e {
301                    NormalizedAssetConfig::File { encodings, .. } => encodings.clone(),
302                    NormalizedAssetConfig::Pattern { encodings, .. } => encodings.clone(),
303                    _ => vec![],
304                })
305                .unwrap_or_default()
306            {
307                let encoded_asset_path = format!("{}{}", asset.path, postfix);
308                let encoded_asset = asset_map.get(encoded_asset_path.as_str()).cloned();
309                if let Some(mut encoded_asset) = encoded_asset {
310                    encoded_asset.url.clone_from(&asset.url);
311
312                    self.certify_asset_impl(encoded_asset, asset_config, Some(encoding))?;
313                }
314            }
315
316            self.certify_asset_impl(asset.clone(), asset_config, None)?;
317        }
318
319        for asset_config in asset_configs {
320            if let NormalizedAssetConfig::Redirect {
321                from,
322                to,
323                kind,
324                headers,
325            } = asset_config
326            {
327                self.insert_redirect(from, to, kind, headers)?;
328            }
329        }
330
331        Ok(())
332    }
333
334    /// Deletes multiple assets from the router, including any certification for those assets.
335    ///
336    /// Depending on the configuration provided to the [certify_assets](AssetRouter::certify_assets) function,
337    /// multiple responses may be generated for the same asset. To ensure that all generated responses are deleted,
338    /// this function accepts the same configuration.
339    ///
340    /// After performing this operation, one must set the canister's certified data (`ic_cdk::api::set_certified_data()`)
341    /// to the new [root hash](AssetRouter::root_hash) of the tree.
342    pub fn delete_assets<'path>(
343        &mut self,
344        assets: impl IntoIterator<Item = Asset<'content, 'path>>,
345        asset_configs: impl IntoIterator<Item = AssetConfig>,
346    ) -> AssetCertificationResult {
347        let asset_configs: Vec<NormalizedAssetConfig> = asset_configs
348            .into_iter()
349            .map(TryInto::try_into)
350            .collect::<AssetCertificationResult<_>>()?;
351
352        let asset_map = assets
353            .into_iter()
354            .map(|asset| (asset.path.clone(), asset))
355            .collect::<HashMap<_, _>>();
356
357        for asset in asset_map.values() {
358            let asset_config = asset_configs.iter().find(|e| e.matches_asset(asset));
359
360            for (encoding, postfix) in asset_config
361                .map(|e| match e {
362                    NormalizedAssetConfig::File { encodings, .. } => encodings.clone(),
363                    NormalizedAssetConfig::Pattern { encodings, .. } => encodings.clone(),
364                    _ => vec![],
365                })
366                .unwrap_or_default()
367            {
368                let encoded_asset_path = format!("{}{}", asset.path, postfix);
369                let encoded_asset = asset_map.get(encoded_asset_path.as_str()).cloned();
370
371                if let Some(mut encoded_asset) = encoded_asset {
372                    encoded_asset.url.clone_from(&asset.url);
373
374                    self.delete_asset_impl(encoded_asset, asset_config, Some(encoding))?;
375                }
376            }
377
378            self.delete_asset_impl(asset.clone(), asset_config, None)?;
379        }
380
381        for asset_config in asset_configs {
382            if let NormalizedAssetConfig::Redirect {
383                from,
384                to,
385                kind,
386                headers,
387            } = asset_config
388            {
389                self.delete_redirect(from, to, kind, headers)?;
390            }
391        }
392
393        Ok(())
394    }
395
396    /// Deletes multiple assets from the router by path, including any certification for those assets.
397    ///
398    /// Depending on the configuration provided to the [certify_assets](AssetRouter::certify_assets) function,
399    /// multiple responses may be generated for the same asset. These assets may exist on different paths,
400    /// for example if the `alias` configuration is used. If `alias` paths are not passed to this function,
401    /// they will not be deleted.
402    ///
403    /// If multiple encodings exist for a path, all encodings will be deleted.
404    ///
405    /// Fallbacks are also not deleted, to delete them, use the
406    /// [delete_fallback_assets_by_path](AssetRouter::delete_fallback_assets_by_path) function.
407    ///
408    /// After performing this operation, one must set the canister's certified data (`ic_cdk::api::set_certified_data()`)
409    /// to the new [root hash](AssetRouter::root_hash) of the tree.
410    pub fn delete_assets_by_path<'path>(
411        &mut self,
412        asset_paths: impl IntoIterator<Item = &'path str>,
413    ) {
414        for asset_path in asset_paths {
415            self.responses
416                .remove(&RequestKey::new(asset_path, None, None));
417            self.tree
418                .borrow_mut()
419                .delete_by_path(&HttpCertificationPath::exact(asset_path));
420        }
421    }
422
423    /// Deletes multiple fallback assets from the router by path, including certification for those assets.
424    ///
425    /// This function will only delete fallbacks, to delete standard assets, use the
426    /// [delete_assets_by_path](AssetRouter::delete_assets_by_path) function.
427    ///
428    /// After performing this operation, one must set the canister's certified data (`ic_cdk::api::set_certified_data()`)
429    /// to the new [root hash](AssetRouter::root_hash) of the tree.
430    pub fn delete_fallback_assets_by_path<'path>(
431        &mut self,
432        asset_paths: impl IntoIterator<Item = &'path str>,
433    ) {
434        for asset_path in asset_paths {
435            self.fallback_responses
436                .remove(&RequestKey::new(asset_path, None, None));
437            self.tree
438                .borrow_mut()
439                .delete_by_path(&HttpCertificationPath::wildcard(asset_path));
440        }
441    }
442
443    /// Deletes all assets from the router, including any certification for those assets.
444    ///
445    /// After performing this operation, one must set the canister's certified data (`ic_cdk::api::set_certified_data()`)
446    /// to the new [root hash](AssetRouter::root_hash) of the tree.
447    pub fn delete_all_assets(&mut self) {
448        self.responses.clear();
449        self.fallback_responses.clear();
450        self.tree.borrow_mut().clear();
451    }
452
453    /// Returns the root hash of the underlying
454    /// [HttpCertificationTree](ic_http_certification::HttpCertificationTree).
455    pub fn root_hash(&self) -> Hash {
456        self.tree.borrow().root_hash()
457    }
458
459    fn get_asset_for_request<'a>(
460        &self,
461        req_path: &'a str,
462        preferred_encodings: Vec<&'a str>,
463        maybe_range_begin: Option<usize>,
464    ) -> AssetCertificationResult<&CertifiedAssetResponse<'content>> {
465        if let Some(response) =
466            self.get_encoded_asset(&preferred_encodings, req_path, maybe_range_begin)
467        {
468            return Ok(response);
469        }
470
471        if let Some(response) =
472            self.responses
473                .get(&RequestKey::new(req_path, None, maybe_range_begin))
474        {
475            if response.response.body().len() > ASSET_CHUNK_SIZE {
476                if let Some(first_chunk_response) =
477                    self.responses
478                        .get(&RequestKey::new(req_path, None, Some(0)))
479                {
480                    return Ok(first_chunk_response);
481                }
482            } else {
483                return Ok(response);
484            }
485        }
486
487        let mut url_scopes = req_path.split('/').collect::<Vec<_>>();
488        url_scopes.pop();
489
490        while !url_scopes.is_empty() {
491            let mut scope = url_scopes.join("/");
492            scope.push('/');
493
494            if let Some(response) = self.get_encoded_fallback_asset(&preferred_encodings, &scope) {
495                return Ok(response);
496            }
497
498            if let Some(response) = self
499                .fallback_responses
500                .get(&RequestKey::new(&scope, None, None))
501            {
502                return Ok(response);
503            }
504
505            scope.pop();
506
507            if let Some(response) = self.get_encoded_fallback_asset(&preferred_encodings, &scope) {
508                return Ok(response);
509            }
510
511            if let Some(response) = self
512                .fallback_responses
513                .get(&RequestKey::new(&scope, None, None))
514            {
515                return Ok(response);
516            }
517
518            url_scopes.pop();
519        }
520        Err(AssetCertificationError::NoAssetMatchingRequestUrl {
521            request_url: req_path.to_string(),
522        })
523    }
524
525    fn certify_asset_impl<'path>(
526        &mut self,
527        asset: Asset<'content, 'path>,
528        asset_config: Option<&NormalizedAssetConfig>,
529        encoding: Option<AssetEncoding>,
530    ) -> AssetCertificationResult {
531        match asset_config {
532            Some(NormalizedAssetConfig::Pattern {
533                content_type,
534                headers,
535                ..
536            }) => {
537                self.insert_static_asset(asset, content_type.clone(), headers.clone(), encoding)?;
538            }
539            Some(NormalizedAssetConfig::File {
540                content_type,
541                headers,
542                fallback_for,
543                aliased_by,
544                ..
545            }) => {
546                self.insert_static_asset(
547                    asset.clone(),
548                    content_type.clone(),
549                    headers.clone(),
550                    encoding,
551                )?;
552
553                for fallback_for in fallback_for.iter() {
554                    self.insert_fallback_asset(
555                        asset.clone(),
556                        content_type.clone(),
557                        headers.clone(),
558                        fallback_for.clone(),
559                        encoding,
560                    )?;
561                }
562
563                for aliased_by in aliased_by.iter() {
564                    let mut aliased_asset = asset.clone();
565                    aliased_asset.url = Cow::Owned(aliased_by.clone());
566
567                    self.insert_static_asset(
568                        aliased_asset,
569                        content_type.clone(),
570                        headers.clone(),
571                        encoding,
572                    )?;
573                }
574            }
575            _ => {
576                self.insert_static_asset(asset, None, vec![], encoding)?;
577            }
578        }
579
580        Ok(())
581    }
582
583    fn delete_asset_impl<'path>(
584        &mut self,
585        asset: Asset<'content, 'path>,
586        asset_config: Option<&NormalizedAssetConfig>,
587        encoding: Option<AssetEncoding>,
588    ) -> AssetCertificationResult {
589        match asset_config {
590            Some(NormalizedAssetConfig::Pattern {
591                content_type,
592                headers,
593                ..
594            }) => {
595                self.delete_static_asset(asset, content_type.clone(), headers.clone(), encoding)?;
596            }
597            Some(NormalizedAssetConfig::File {
598                content_type,
599                headers,
600                fallback_for,
601                aliased_by,
602                ..
603            }) => {
604                self.delete_static_asset(
605                    asset.clone(),
606                    content_type.clone(),
607                    headers.clone(),
608                    encoding,
609                )?;
610
611                for fallback_for in fallback_for.iter() {
612                    self.delete_fallback_asset(
613                        asset.clone(),
614                        content_type.clone(),
615                        headers.clone(),
616                        fallback_for.clone(),
617                        encoding,
618                    )?;
619                }
620
621                for aliased_by in aliased_by.iter() {
622                    let mut aliased_asset = asset.clone();
623                    aliased_asset.url = Cow::Owned(aliased_by.clone());
624
625                    self.delete_static_asset(
626                        aliased_asset,
627                        content_type.clone(),
628                        headers.clone(),
629                        encoding,
630                    )?;
631                }
632            }
633            _ => {
634                self.delete_static_asset(asset, None, vec![], encoding)?;
635            }
636        }
637
638        Ok(())
639    }
640
641    fn insert_static_asset<'path>(
642        &mut self,
643        asset: Asset<'content, 'path>,
644        content_type: Option<String>,
645        additional_headers: Vec<(String, String)>,
646        encoding: Option<AssetEncoding>,
647    ) -> AssetCertificationResult<()> {
648        let asset_url = asset.url.to_string();
649        let total_length = asset.content.len();
650
651        if total_length > ASSET_CHUNK_SIZE {
652            let mut range_begin = 0;
653            while range_begin < asset.content.len() {
654                let response = Self::prepare_static_asset(
655                    asset.clone(),
656                    content_type.clone(),
657                    additional_headers.clone(),
658                    encoding,
659                    Some(range_begin),
660                )?;
661                self.tree.borrow_mut().insert(&response.tree_entry);
662                self.responses.insert(
663                    RequestKey::new(&asset_url, encoding_str(encoding), Some(range_begin)),
664                    response,
665                );
666                range_begin += ASSET_CHUNK_SIZE;
667            }
668        }
669
670        let response =
671            Self::prepare_static_asset(asset, content_type, additional_headers, encoding, None)?;
672
673        self.tree.borrow_mut().insert(&response.tree_entry);
674        self.responses.insert(
675            RequestKey::new(&asset_url, encoding_str(encoding), None),
676            response,
677        );
678        Ok(())
679    }
680
681    fn delete_static_asset<'path>(
682        &mut self,
683        asset: Asset<'content, 'path>,
684        content_type: Option<String>,
685        additional_headers: Vec<(String, String)>,
686        encoding: Option<AssetEncoding>,
687    ) -> AssetCertificationResult<()> {
688        let asset_url = asset.url.to_string();
689        let response =
690            Self::prepare_static_asset(asset, content_type, additional_headers, encoding, None)?;
691
692        self.tree.borrow_mut().delete(&response.tree_entry);
693        self.responses
694            .remove(&RequestKey::new(&asset_url, encoding_str(encoding), None));
695
696        if response.response.body().len() > ASSET_CHUNK_SIZE {
697            // Delete also chunks.
698            let mut range_begin: usize = 0;
699            while range_begin < response.response.body().len() {
700                self.responses.remove(&RequestKey::new(
701                    &asset_url,
702                    encoding_str(encoding),
703                    Some(range_begin),
704                ));
705                range_begin += ASSET_CHUNK_SIZE;
706            }
707        }
708
709        Ok(())
710    }
711
712    fn prepare_static_asset<'path>(
713        asset: Asset<'content, 'path>,
714        content_type: Option<String>,
715        additional_headers: Vec<(String, String)>,
716        encoding: Option<AssetEncoding>,
717        range_begin: Option<usize>,
718    ) -> AssetCertificationResult<CertifiedAssetResponse<'content>> {
719        let asset_url = asset.url.to_string();
720
721        let (response, certification) = Self::prepare_asset_response_and_certification(
722            asset,
723            additional_headers,
724            content_type,
725            encoding,
726            range_begin,
727            None,
728        )?;
729
730        let tree_entry =
731            HttpCertificationTreeEntry::new(HttpCertificationPath::exact(asset_url), certification);
732
733        Ok(CertifiedAssetResponse {
734            response,
735            tree_entry,
736        })
737    }
738
739    fn insert_fallback_asset<'path>(
740        &mut self,
741        asset: Asset<'content, 'path>,
742        content_type: Option<String>,
743        additional_headers: Vec<(String, String)>,
744        fallback_for: AssetFallbackConfig,
745        encoding: Option<AssetEncoding>,
746    ) -> AssetCertificationResult<()> {
747        let response = Self::prepare_fallback_asset(
748            asset,
749            additional_headers,
750            content_type,
751            fallback_for.clone(),
752            encoding,
753        )?;
754
755        self.tree.borrow_mut().insert(&response.tree_entry);
756        self.fallback_responses.insert(
757            RequestKey::new(&fallback_for.scope, encoding_str(encoding), None),
758            response,
759        );
760        Ok(())
761    }
762
763    fn delete_fallback_asset<'path>(
764        &mut self,
765        asset: Asset<'content, 'path>,
766        content_type: Option<String>,
767        additional_headers: Vec<(String, String)>,
768        fallback_for: AssetFallbackConfig,
769        encoding: Option<AssetEncoding>,
770    ) -> AssetCertificationResult<()> {
771        let response = Self::prepare_fallback_asset(
772            asset,
773            additional_headers,
774            content_type,
775            fallback_for.clone(),
776            encoding,
777        )?;
778
779        self.tree.borrow_mut().delete(&response.tree_entry);
780        self.fallback_responses.remove(&RequestKey::new(
781            &fallback_for.scope,
782            encoding_str(encoding),
783            None,
784        ));
785        Ok(())
786    }
787
788    fn prepare_fallback_asset<'path>(
789        asset: Asset<'content, 'path>,
790        additional_headers: Vec<(String, String)>,
791        content_type: Option<String>,
792        fallback_for: AssetFallbackConfig,
793        encoding: Option<AssetEncoding>,
794    ) -> AssetCertificationResult<CertifiedAssetResponse<'content>> {
795        let (response, certification) = Self::prepare_asset_response_and_certification(
796            asset,
797            additional_headers,
798            content_type,
799            encoding,
800            None,
801            fallback_for.status_code,
802        )?;
803
804        let tree_entry = HttpCertificationTreeEntry::new(
805            HttpCertificationPath::wildcard(fallback_for.scope.clone()),
806            certification,
807        );
808
809        Ok(CertifiedAssetResponse {
810            response,
811            tree_entry,
812        })
813    }
814
815    fn insert_redirect(
816        &mut self,
817        from: String,
818        to: String,
819        kind: AssetRedirectKind,
820        additional_headers: Vec<(String, String)>,
821    ) -> AssetCertificationResult<()> {
822        let response = Self::prepare_redirect(from.clone(), to, kind, additional_headers)?;
823
824        self.tree.borrow_mut().insert(&response.tree_entry);
825
826        self.responses
827            .insert(RequestKey::new(&from, None, None), response);
828
829        Ok(())
830    }
831
832    fn delete_redirect(
833        &mut self,
834        from: String,
835        to: String,
836        kind: AssetRedirectKind,
837        addtional_headers: Vec<(String, String)>,
838    ) -> AssetCertificationResult<()> {
839        let response = Self::prepare_redirect(from.clone(), to, kind, addtional_headers)?;
840
841        self.tree.borrow_mut().delete(&response.tree_entry);
842        self.responses.remove(&RequestKey::new(&from, None, None));
843
844        Ok(())
845    }
846
847    fn prepare_redirect(
848        from: String,
849        to: String,
850        kind: AssetRedirectKind,
851        addtional_headers: Vec<(String, String)>,
852    ) -> AssetCertificationResult<CertifiedAssetResponse<'content>> {
853        let status_code = match kind {
854            AssetRedirectKind::Permanent => StatusCode::MOVED_PERMANENTLY,
855            AssetRedirectKind::Temporary => StatusCode::TEMPORARY_REDIRECT,
856        };
857
858        let mut headers = vec![("location".to_string(), to)];
859        headers.extend(addtional_headers);
860
861        let (response, certification) = Self::prepare_response_and_certification(
862            from.clone(),
863            status_code,
864            Cow::Owned(vec![]),
865            headers,
866            vec![],
867        )?;
868
869        Ok(CertifiedAssetResponse {
870            response,
871            tree_entry: HttpCertificationTreeEntry::new(
872                HttpCertificationPath::exact(from),
873                certification,
874            ),
875        })
876    }
877
878    fn prepare_asset_response_and_certification<'path>(
879        asset: Asset<'content, 'path>,
880        additional_headers: Vec<(String, String)>,
881        content_type: Option<String>,
882        encoding: Option<AssetEncoding>,
883        range_begin: Option<usize>,
884        status_code: Option<StatusCode>,
885    ) -> AssetCertificationResult<(HttpResponse<'content>, HttpCertification)> {
886        let mut content = asset.content;
887        let mut status_code = status_code.unwrap_or(StatusCode::OK);
888        let mut headers = vec![];
889        headers.extend(additional_headers);
890
891        if let Some(content_type) = content_type {
892            headers.push(("content-type".to_string(), content_type));
893        }
894
895        if let Some(encoding) = encoding {
896            headers.push(("content-encoding".to_string(), encoding.to_string()));
897        }
898
899        let mut request_headers = vec![];
900        if let Some(range_begin) = range_begin {
901            let total_length = content.len();
902            let range_end = cmp::min(range_begin + ASSET_CHUNK_SIZE, total_length) - 1;
903            content = content[range_begin..(range_end + 1)].to_owned().into();
904            status_code = StatusCode::PARTIAL_CONTENT;
905            headers.push((
906                http::header::CONTENT_RANGE.to_string(),
907                format!("bytes {range_begin}-{range_end}/{total_length}"),
908            ));
909
910            // The `Range` request header will not be sent with the first request,
911            // so we don't include it in certification for the first chunk.
912            if range_begin != 0 {
913                request_headers.push((
914                    http::header::RANGE.to_string(),
915                    format!("bytes={range_begin}-"),
916                ));
917            }
918        };
919
920        Self::prepare_response_and_certification(
921            asset.url.to_string(),
922            status_code,
923            content,
924            headers,
925            request_headers,
926        )
927    }
928
929    fn prepare_response_and_certification(
930        url: String,
931        status_code: StatusCode,
932        body: Cow<'content, [u8]>,
933        additional_response_headers: Vec<(String, String)>,
934        certified_request_headers: Vec<(String, String)>,
935    ) -> AssetCertificationResult<(HttpResponse<'content>, HttpCertification)> {
936        let mut headers = vec![("content-length".to_string(), body.len().to_string())];
937
938        headers.extend(additional_response_headers);
939        let cel_expr = DefaultCelBuilder::full_certification()
940            .with_request_headers(
941                certified_request_headers
942                    .iter()
943                    .map(|(s, _)| s.as_str())
944                    .collect::<Vec<&str>>(),
945            )
946            .with_response_certification(DefaultResponseCertification::response_header_exclusions(
947                vec![],
948            ))
949            .build();
950        let cel_expr_str = cel_expr.to_string();
951        headers.push((CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(), cel_expr_str));
952
953        let request = HttpRequest::get(url)
954            .with_headers(certified_request_headers.clone())
955            .build();
956
957        let response = HttpResponse::builder()
958            .with_status_code(status_code)
959            .with_body(body)
960            .with_headers(headers)
961            .build();
962
963        let certification = HttpCertification::full(&cel_expr, &request, &response, None)?;
964
965        Ok((response, certification))
966    }
967
968    fn get_encoded_asset(
969        &self,
970        preferred_encodings: &[&str],
971        url: &str,
972        maybe_range_begin: Option<usize>,
973    ) -> Option<&CertifiedAssetResponse<'content>> {
974        for encoding in preferred_encodings {
975            if let Some(response) = self.responses.get(&RequestKey::new(
976                url,
977                Some(encoding.to_string()),
978                maybe_range_begin,
979            )) {
980                if response.response.body().len() > ASSET_CHUNK_SIZE {
981                    if let Some(first_chunk_response) = self.responses.get(&RequestKey::new(
982                        url,
983                        Some(encoding.to_string()),
984                        Some(0),
985                    )) {
986                        return Some(first_chunk_response);
987                    } else {
988                        return None;
989                    }
990                } else {
991                    return Some(response);
992                }
993            }
994        }
995
996        None
997    }
998
999    fn get_encoded_fallback_asset(
1000        &self,
1001        preferred_encodings: &[&str],
1002        scope: &str,
1003    ) -> Option<&CertifiedAssetResponse<'content>> {
1004        for encoding in preferred_encodings {
1005            if let Some(response) = self.fallback_responses.get(&RequestKey::new(
1006                scope,
1007                Some(encoding.to_string()),
1008                None,
1009            )) {
1010                return Some(response);
1011            }
1012        }
1013
1014        None
1015    }
1016
1017    fn get_range_header<'a>(request: &'a HttpRequest) -> Option<&'a str> {
1018        for (name, value) in request.headers().iter() {
1019            if name.to_lowercase().eq(&http::header::RANGE.as_str()) {
1020                return Some(value);
1021            }
1022        }
1023        None
1024    }
1025
1026    fn get_preferred_encodings<'a>(&self, request: &'a HttpRequest) -> Vec<&'a str> {
1027        for (name, value) in request.headers().iter() {
1028            if name.to_lowercase() == "accept-encoding" {
1029                return Self::prioritized_encodings(value)
1030                    .iter()
1031                    .map(|(encoding, _quality)| *encoding)
1032                    .collect();
1033            }
1034        }
1035
1036        vec![]
1037    }
1038
1039    fn prioritized_encodings(encodings: &str) -> Vec<(&str, f32)> {
1040        let mut encodings = encodings
1041            .split(',')
1042            .filter_map(|encoding| {
1043                encoding
1044                    .split(';')
1045                    .collect::<Vec<_>>()
1046                    .first()
1047                    .map(|s| s.trim())
1048                    .map(|s| (s, Self::default_encoding_quality(s)))
1049            })
1050            .collect::<Vec<_>>();
1051
1052        // this `unwrap()` call is safe as long as the values returned by
1053        // `default_encoding_quality` are comparable (not NaN)
1054        encodings.sort_unstable_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap());
1055
1056        encodings
1057    }
1058
1059    fn default_encoding_quality(encoding: &str) -> f32 {
1060        if encoding.eq_ignore_ascii_case("br") {
1061            return 1.0;
1062        }
1063
1064        if encoding.eq_ignore_ascii_case("zstd") {
1065            return 0.9;
1066        }
1067
1068        if encoding.eq_ignore_ascii_case("gzip") {
1069            return 0.8;
1070        }
1071
1072        if encoding.eq_ignore_ascii_case("deflate") {
1073            return 0.7;
1074        }
1075
1076        if encoding.eq_ignore_ascii_case("identity") {
1077            return 0.5;
1078        }
1079
1080        0.6
1081    }
1082}
1083
1084impl Default for AssetRouter<'_> {
1085    fn default() -> Self {
1086        Self::new()
1087    }
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092    use super::*;
1093    use crate::AssetFallbackConfig;
1094    use assert_matches::assert_matches;
1095    use ic_certification::{hash_tree::SubtreeLookupResult, HashTree};
1096    use ic_http_certification::{
1097        cel::DefaultFullCelExpressionBuilder, HeaderField, CERTIFICATE_HEADER_NAME,
1098    };
1099    use ic_response_verification::CertificateHeader;
1100    use ic_response_verification_test_utils::{base64_decode, hash};
1101    use rand_chacha::rand_core::{RngCore, SeedableRng};
1102    use rand_chacha::ChaCha20Rng;
1103    use rstest::*;
1104    use std::vec;
1105
1106    const ONE_CHUNK_ASSET_LEN: usize = ASSET_CHUNK_SIZE;
1107    const TWO_CHUNKS_ASSET_LEN: usize = ASSET_CHUNK_SIZE + 1;
1108    const SIX_CHUNKS_ASSET_LEN: usize = 5 * ASSET_CHUNK_SIZE + 12;
1109    const TEN_CHUNKS_ASSET_LEN: usize = 10 * ASSET_CHUNK_SIZE;
1110
1111    const ONE_CHUNK_ASSET_NAME: &str = "long_asset_one_chunk";
1112    const TWO_CHUNKS_ASSET_NAME: &str = "long_asset_two_chunks";
1113    const SIX_CHUNKS_ASSET_NAME: &str = "long_asset_six_chunks";
1114    const TEN_CHUNKS_ASSET_NAME: &str = "long_asset_ten_chunks";
1115
1116    #[rstest]
1117    #[case(0, None)]
1118    #[case(ASSET_CHUNK_SIZE, None)]
1119    #[case(ASSET_CHUNK_SIZE*4, None)]
1120    #[case(0, Some(0))]
1121    #[case(100, Some(2000))]
1122    #[case(10_000, Some(300_000))]
1123    #[case(ASSET_CHUNK_SIZE, Some(2 * ASSET_CHUNK_SIZE - 1))]
1124    fn should_parse_range_header_str(#[case] range_begin: usize, #[case] range_end: Option<usize>) {
1125        let input = if let Some(range_end) = range_end {
1126            format!("bytes={range_begin}-{range_end}")
1127        } else {
1128            format!("bytes={range_begin}-")
1129        };
1130        let result = parse_range_header_str(&input);
1131        let output = result.unwrap_or_else(|e| panic!("failed parsing '{input}': {e:?}"));
1132        assert_eq!(
1133            RangeRequestValues {
1134                range_begin,
1135                range_end
1136            },
1137            output
1138        );
1139    }
1140
1141    #[rstest]
1142    #[case("")]
1143    #[case("byte=1-2")]
1144    #[case("bites=2-4")]
1145    #[case("bytes 7-11")]
1146    #[case("bytes=12345")]
1147    #[case("something else")]
1148    #[case("bytes=-5-19")]
1149    fn should_fail_parse_range_header_str_on_invalid_input(#[case] malformed_input: &str) {
1150        let result = parse_range_header_str(malformed_input);
1151        assert_matches!(result, Err(e) if e.to_string().contains("Invalid Range header"));
1152    }
1153
1154    #[rstest]
1155    #[case("bytes=100-end")]
1156    #[case("bytes=dead-beef")]
1157    fn should_fail_parse_range_header_str_on_malformed_input(#[case] malformed_input: &str) {
1158        let result = parse_range_header_str(malformed_input);
1159        assert_matches!(result, Err(e) if e.to_string().contains("Malformed range_"));
1160    }
1161
1162    #[rstest]
1163    #[case("bytes=100-20")]
1164    #[case("bytes=20-19")]
1165    fn should_fail_parse_range_header_str_on_invalid_values(#[case] malformed_input: &str) {
1166        let result = parse_range_header_str(malformed_input);
1167        assert_matches!(result, Err(e) if e.to_string().contains("Invalid values in Range header"));
1168    }
1169
1170    #[rstest]
1171    #[case("/")]
1172    #[case("https://internetcomputer.org/")]
1173    fn test_index_html(mut asset_router: AssetRouter, #[case] req_url: &str) {
1174        let request = HttpRequest::get(req_url).build();
1175
1176        let mut expected_response = expected_index_html_response();
1177
1178        let response = asset_router
1179            .serve_asset(&data_certificate(), &request)
1180            .unwrap();
1181        let (witness, expr_path) = extract_witness_expr_path(&response);
1182        add_v2_certificate_header(
1183            &data_certificate(),
1184            &mut expected_response,
1185            &witness,
1186            &expr_path,
1187        );
1188
1189        assert_eq!(expr_path, vec!["http_expr", "", "<$>"]);
1190        assert_matches!(
1191            witness.lookup_subtree(&expr_path),
1192            SubtreeLookupResult::Found(_)
1193        );
1194        assert_eq!(response, expected_response);
1195
1196        asset_router
1197            .delete_assets(
1198                vec![Asset::new("index.html", index_html_body())],
1199                vec![index_html_config()],
1200            )
1201            .unwrap();
1202
1203        let result = asset_router.serve_asset(&data_certificate(), &request);
1204        assert_matches!(
1205            result,
1206            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1207                request_url,
1208             }) if request_url == request.get_path().unwrap()
1209        );
1210    }
1211
1212    #[test]
1213    fn test_one_chunk_long_asset_served_in_full() {
1214        let asset_name = ONE_CHUNK_ASSET_NAME;
1215        let long_asset_router =
1216            long_asset_router_with_params(&[asset_name], &[AssetEncoding::Identity]);
1217        let req_url = format!("/{asset_name}");
1218        let asset_body = long_asset_body(asset_name);
1219        // Request the entire "one-chunk"-asset, should obtain it in full.
1220        let request = HttpRequest::get(&req_url).build();
1221        let mut expected_response = build_200_response(
1222            asset_body,
1223            asset_cel_expr(),
1224            vec![
1225                (
1226                    "cache-control".to_string(),
1227                    "public, no-cache, no-store".to_string(),
1228                ),
1229                ("content-type".to_string(), "text/html".to_string()),
1230            ],
1231        );
1232
1233        let response = long_asset_router
1234            .serve_asset(&data_certificate(), &request)
1235            .unwrap();
1236        let (witness, expr_path) = extract_witness_expr_path(&response);
1237        add_v2_certificate_header(
1238            &data_certificate(),
1239            &mut expected_response,
1240            &witness,
1241            &expr_path,
1242        );
1243
1244        assert_eq!(expr_path, vec!["http_expr", &req_url[1..], "<$>"]);
1245        assert_matches!(
1246            witness.lookup_subtree(&expr_path),
1247            SubtreeLookupResult::Found(_)
1248        );
1249        assert_eq!(response, expected_response);
1250    }
1251
1252    #[rstest]
1253    #[case(TWO_CHUNKS_ASSET_NAME)]
1254    #[case(SIX_CHUNKS_ASSET_NAME)]
1255    #[case(TEN_CHUNKS_ASSET_NAME)]
1256    fn test_long_asset_served_in_chunks(#[case] asset_name: &str) {
1257        let long_asset_router =
1258            long_asset_router_with_params(&[asset_name], &[AssetEncoding::Identity]);
1259        let req_url = format!("/{asset_name}");
1260        let asset_body = long_asset_body(asset_name);
1261        let asset_len = asset_body.len();
1262        // Request the entire asset, should obtain the first chunk.
1263        let request = HttpRequest::get(&req_url).build();
1264        let mut expected_response = build_206_response(
1265            asset_body[0..ASSET_CHUNK_SIZE].to_vec(),
1266            asset_cel_expr(),
1267            vec![
1268                (
1269                    "cache-control".to_string(),
1270                    "public, no-cache, no-store".to_string(),
1271                ),
1272                ("content-type".to_string(), "text/html".to_string()),
1273                (
1274                    "content-range".to_string(),
1275                    format!("bytes 0-{}/{}", ASSET_CHUNK_SIZE - 1, asset_len),
1276                ),
1277            ],
1278        );
1279
1280        let response = long_asset_router
1281            .serve_asset(&data_certificate(), &request)
1282            .unwrap();
1283        let (witness, expr_path) = extract_witness_expr_path(&response);
1284        add_v2_certificate_header(
1285            &data_certificate(),
1286            &mut expected_response,
1287            &witness,
1288            &expr_path,
1289        );
1290
1291        assert_eq!(expr_path, vec!["http_expr", &req_url[1..], "<$>"]);
1292        assert_matches!(
1293            witness.lookup_subtree(&expr_path),
1294            SubtreeLookupResult::Found(_)
1295        );
1296        assert_eq!(response, expected_response);
1297
1298        // Request the subsequent chunks, should obtain them.
1299        let expected_number_of_chunks =
1300            (asset_len as f32 / ASSET_CHUNK_SIZE as f32).ceil() as usize;
1301        let mut asset_len_so_far = response.body().len();
1302        let mut number_of_chunks_so_far = 1;
1303        while asset_len_so_far < asset_len {
1304            let chunk_request = HttpRequest::get(&req_url)
1305                .with_headers(vec![(
1306                    "range".to_string(),
1307                    format!("bytes={asset_len_so_far}-"),
1308                )])
1309                .build();
1310            let expected_range_end = cmp::min(asset_len_so_far + ASSET_CHUNK_SIZE, asset_len) - 1;
1311            let mut expected_response = build_206_response(
1312                asset_body[asset_len_so_far..=expected_range_end].to_vec(),
1313                asset_range_chunk_cel_expr(),
1314                vec![
1315                    (
1316                        "cache-control".to_string(),
1317                        "public, no-cache, no-store".to_string(),
1318                    ),
1319                    ("content-type".to_string(), "text/html".to_string()),
1320                    (
1321                        "content-range".to_string(),
1322                        format!("bytes {asset_len_so_far}-{expected_range_end}/{asset_len}"),
1323                    ),
1324                ],
1325            );
1326            let response = long_asset_router
1327                .serve_asset(&data_certificate(), &chunk_request)
1328                .unwrap();
1329            let (witness, expr_path) = extract_witness_expr_path(&response);
1330            assert_matches!(
1331                witness.lookup_subtree(&expr_path),
1332                SubtreeLookupResult::Found(_)
1333            );
1334            add_v2_certificate_header(
1335                &data_certificate(),
1336                &mut expected_response,
1337                &witness,
1338                &expr_path,
1339            );
1340            assert_eq!(response, expected_response);
1341            asset_len_so_far += response.body().len();
1342            number_of_chunks_so_far += 1;
1343        }
1344        assert_eq!(number_of_chunks_so_far, expected_number_of_chunks)
1345    }
1346
1347    #[rstest]
1348    #[case(TWO_CHUNKS_ASSET_NAME)]
1349    #[case(SIX_CHUNKS_ASSET_NAME)]
1350    #[case(TEN_CHUNKS_ASSET_NAME)]
1351    fn test_long_asset_deletion_removes_chunks(#[case] asset_name: &str) {
1352        let mut long_asset_router =
1353            long_asset_router_with_params(&[asset_name], &[AssetEncoding::Identity]);
1354        let req_url = format!("/{asset_name}");
1355        let asset_body = long_asset_body(asset_name);
1356        let asset_len = asset_body.len();
1357        let mut all_requests = vec![];
1358        // Request the entire asset and the chunks, all should succeed.
1359        // First the asset...
1360        let request = HttpRequest::get(&req_url).build();
1361        let response = long_asset_router
1362            .serve_asset(&data_certificate(), &request)
1363            .unwrap();
1364        let (witness, expr_path) = extract_witness_expr_path(&response);
1365
1366        assert_eq!(expr_path, vec!["http_expr", &req_url[1..], "<$>"]);
1367        assert_matches!(
1368            witness.lookup_subtree(&expr_path),
1369            SubtreeLookupResult::Found(_)
1370        );
1371        assert_eq!(response.status_code(), StatusCode::PARTIAL_CONTENT);
1372        all_requests.push(request);
1373
1374        // ... then the subsequent chunks.
1375        let expected_number_of_chunks =
1376            (asset_len as f32 / ASSET_CHUNK_SIZE as f32).ceil() as usize;
1377        let mut asset_len_so_far = response.body().len();
1378        let mut number_of_chunks_so_far = 1;
1379        while asset_len_so_far < asset_len {
1380            let chunk_request = HttpRequest::get(&req_url)
1381                .with_headers(vec![(
1382                    "range".to_string(),
1383                    format!("bytes={asset_len_so_far}-"),
1384                )])
1385                .build();
1386            let response = long_asset_router
1387                .serve_asset(&data_certificate(), &chunk_request)
1388                .unwrap();
1389            let (witness, expr_path) = extract_witness_expr_path(&response);
1390            assert_matches!(
1391                witness.lookup_subtree(&expr_path),
1392                SubtreeLookupResult::Found(_)
1393            );
1394            assert_eq!(response.status_code(), StatusCode::PARTIAL_CONTENT);
1395            asset_len_so_far += response.body().len();
1396            number_of_chunks_so_far += 1;
1397            all_requests.push(chunk_request);
1398        }
1399        assert_eq!(number_of_chunks_so_far, expected_number_of_chunks);
1400        assert_eq!(all_requests.len(), expected_number_of_chunks);
1401
1402        // Delete the asset.
1403        long_asset_router
1404            .delete_assets(
1405                vec![Asset::new(&req_url, asset_body)],
1406                vec![long_asset_config(asset_name)],
1407            )
1408            .expect("Asset deletion failed");
1409
1410        // Re-request the asset and the chunks, all should fail.
1411        for request in all_requests {
1412            let result = long_asset_router.serve_asset(&data_certificate(), &request);
1413            assert_matches!(
1414                result,
1415                Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1416                    request_url,
1417                 }) if request_url == request.get_path().unwrap()
1418            );
1419        }
1420    }
1421
1422    #[rstest]
1423    #[case(SIX_CHUNKS_ASSET_NAME, "deflate", AssetEncoding::Deflate)]
1424    #[case(SIX_CHUNKS_ASSET_NAME, "deflate, identity", AssetEncoding::Deflate)]
1425    #[case(SIX_CHUNKS_ASSET_NAME, "gzip", AssetEncoding::Gzip)]
1426    #[case(SIX_CHUNKS_ASSET_NAME, "gzip, identity", AssetEncoding::Gzip)]
1427    #[case(SIX_CHUNKS_ASSET_NAME, "gzip, deflate", AssetEncoding::Gzip)]
1428    #[case(SIX_CHUNKS_ASSET_NAME, "gzip, deflate, identity", AssetEncoding::Gzip)]
1429    #[case(SIX_CHUNKS_ASSET_NAME, "br", AssetEncoding::Brotli)]
1430    #[case(
1431        SIX_CHUNKS_ASSET_NAME,
1432        "br, gzip, deflate, identity",
1433        AssetEncoding::Brotli
1434    )]
1435    #[case(
1436        SIX_CHUNKS_ASSET_NAME,
1437        "gzip, deflate, identity, br",
1438        AssetEncoding::Brotli
1439    )]
1440    fn test_encoded_long_asset_served_in_encoded_chunks(
1441        #[case] asset_name: &str,
1442        #[case] accept_encoding: &str,
1443        #[case] expected_encoding: AssetEncoding,
1444    ) {
1445        let (_, expected_encoding_suffix) = expected_encoding.default_config();
1446        let long_asset_router = long_asset_router_with_params(
1447            &[asset_name],
1448            &[AssetEncoding::Identity, expected_encoding],
1449        );
1450        let req_url = format!("/{asset_name}");
1451        let encoded_asset_name = format!("{asset_name}{expected_encoding_suffix}");
1452        let asset_body = long_asset_body(&encoded_asset_name);
1453        let asset_len = asset_body.len();
1454
1455        let request = HttpRequest::get(&req_url)
1456            .with_headers(vec![(
1457                "accept-encoding".to_string(),
1458                accept_encoding.to_string(),
1459            )])
1460            .build();
1461        let mut expected_response = build_206_response(
1462            asset_body[0..ASSET_CHUNK_SIZE].to_vec(),
1463            asset_cel_expr(),
1464            vec![
1465                (
1466                    "cache-control".to_string(),
1467                    "public, no-cache, no-store".to_string(),
1468                ),
1469                ("content-type".to_string(), "text/html".to_string()),
1470                (
1471                    "content-encoding".to_string(),
1472                    expected_encoding.to_string(),
1473                ),
1474                (
1475                    "content-range".to_string(),
1476                    format!("bytes 0-{}/{}", ASSET_CHUNK_SIZE - 1, asset_len),
1477                ),
1478            ],
1479        );
1480        let response = long_asset_router
1481            .serve_asset(&data_certificate(), &request)
1482            .unwrap();
1483        let (witness, expr_path) = extract_witness_expr_path(&response);
1484        add_v2_certificate_header(
1485            &data_certificate(),
1486            &mut expected_response,
1487            &witness,
1488            &expr_path,
1489        );
1490
1491        assert_eq!(
1492            expr_path,
1493            HttpCertificationPath::exact(req_url.clone()).to_expr_path()
1494        );
1495        assert_matches!(
1496            witness.lookup_subtree(&expr_path),
1497            SubtreeLookupResult::Found(_)
1498        );
1499        assert_eq!(response, expected_response);
1500
1501        // Request the subsequent chunks, should obtain them.
1502        let expected_number_of_chunks =
1503            (asset_len as f32 / ASSET_CHUNK_SIZE as f32).ceil() as usize;
1504        let mut asset_len_so_far = response.body().len();
1505        let mut number_of_chunks_so_far = 1;
1506        while asset_len_so_far < asset_len {
1507            let chunk_request = HttpRequest::get(&req_url)
1508                .with_headers(vec![
1509                    ("range".to_string(), format!("bytes={asset_len_so_far}-")),
1510                    ("accept-encoding".to_string(), accept_encoding.to_string()),
1511                ])
1512                .build();
1513            let expected_range_end = cmp::min(asset_len_so_far + ASSET_CHUNK_SIZE, asset_len) - 1;
1514            let mut expected_response = build_206_response(
1515                asset_body[asset_len_so_far..=expected_range_end].to_vec(),
1516                encoded_range_chunk_asset_cel_expr(),
1517                vec![
1518                    (
1519                        "cache-control".to_string(),
1520                        "public, no-cache, no-store".to_string(),
1521                    ),
1522                    ("content-type".to_string(), "text/html".to_string()),
1523                    (
1524                        "content-encoding".to_string(),
1525                        expected_encoding.to_string(),
1526                    ),
1527                    (
1528                        "content-range".to_string(),
1529                        format!("bytes {asset_len_so_far}-{expected_range_end}/{asset_len}"),
1530                    ),
1531                ],
1532            );
1533            let response = long_asset_router
1534                .serve_asset(&data_certificate(), &chunk_request)
1535                .unwrap();
1536            let (witness, expr_path) = extract_witness_expr_path(&response);
1537            assert_matches!(
1538                witness.lookup_subtree(&expr_path),
1539                SubtreeLookupResult::Found(_)
1540            );
1541            add_v2_certificate_header(
1542                &data_certificate(),
1543                &mut expected_response,
1544                &witness,
1545                &expr_path,
1546            );
1547            assert_eq!(response, expected_response);
1548            asset_len_so_far += response.body().len();
1549            number_of_chunks_so_far += 1;
1550        }
1551        assert_eq!(number_of_chunks_so_far, expected_number_of_chunks)
1552    }
1553
1554    #[rstest]
1555    #[case(TWO_CHUNKS_ASSET_NAME, AssetEncoding::Brotli)]
1556    #[case(TWO_CHUNKS_ASSET_NAME, AssetEncoding::Gzip)]
1557    #[case(TWO_CHUNKS_ASSET_NAME, AssetEncoding::Deflate)]
1558    fn test_encoded_long_asset_deletion_removes_encoded_chunks(
1559        #[case] asset_name: &str,
1560        #[case] encoding: AssetEncoding,
1561    ) {
1562        let (_, encoding_suffix) = encoding.default_config();
1563        let mut long_asset_router =
1564            long_asset_router_with_params(&[asset_name], &[AssetEncoding::Identity, encoding]);
1565        let req_url = format!("/{asset_name}");
1566        let encoded_asset_name = format!("{asset_name}{encoding_suffix}");
1567        let encoded_asset_body = long_asset_body(&encoded_asset_name);
1568        let asset_len = encoded_asset_body.len();
1569        let mut all_requests = vec![];
1570        // Request the entire asset and the chunks, all should succeed.
1571        // First the asset...
1572        let request = HttpRequest::get(&req_url)
1573            .with_headers(vec![("accept-encoding".to_string(), encoding.to_string())])
1574            .build();
1575        let response = long_asset_router
1576            .serve_asset(&data_certificate(), &request)
1577            .unwrap();
1578        let (witness, expr_path) = extract_witness_expr_path(&response);
1579
1580        assert_eq!(expr_path, vec!["http_expr", &req_url[1..], "<$>"]);
1581        assert_matches!(
1582            witness.lookup_subtree(&expr_path),
1583            SubtreeLookupResult::Found(_)
1584        );
1585        assert_eq!(response.status_code(), StatusCode::PARTIAL_CONTENT);
1586        all_requests.push(request);
1587
1588        // ... then the subsequent chunks.
1589        let expected_number_of_chunks =
1590            (asset_len as f32 / ASSET_CHUNK_SIZE as f32).ceil() as usize;
1591        let mut asset_len_so_far = response.body().len();
1592        let mut number_of_chunks_so_far = 1;
1593        while asset_len_so_far < asset_len {
1594            let chunk_request = HttpRequest::get(&req_url)
1595                .with_headers(vec![
1596                    ("range".to_string(), format!("bytes={asset_len_so_far}-")),
1597                    ("accept-encoding".to_string(), encoding.to_string()),
1598                ])
1599                .build();
1600            let response = long_asset_router
1601                .serve_asset(&data_certificate(), &chunk_request)
1602                .unwrap();
1603            let (witness, expr_path) = extract_witness_expr_path(&response);
1604            assert_matches!(
1605                witness.lookup_subtree(&expr_path),
1606                SubtreeLookupResult::Found(_)
1607            );
1608            assert_eq!(response.status_code(), StatusCode::PARTIAL_CONTENT);
1609            asset_len_so_far += response.body().len();
1610            number_of_chunks_so_far += 1;
1611            all_requests.push(chunk_request);
1612        }
1613        assert_eq!(number_of_chunks_so_far, expected_number_of_chunks);
1614        assert_eq!(all_requests.len(), expected_number_of_chunks);
1615
1616        // Delete the asset.
1617        long_asset_router
1618            .delete_assets(
1619                vec![
1620                    Asset::new(&req_url, long_asset_body(asset_name)),
1621                    Asset::new(format!("/{encoded_asset_name}"), encoded_asset_body),
1622                ],
1623                vec![long_asset_config(&req_url)],
1624            )
1625            .expect("Asset deletion failed");
1626
1627        // Re-request the asset and the chunks, all should fail.
1628        for request in all_requests {
1629            let result = long_asset_router.serve_asset(&data_certificate(), &request);
1630            assert_matches!(
1631                result,
1632                Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1633                    request_url,
1634                 }) if request_url == request.get_path().unwrap()
1635            );
1636        }
1637    }
1638
1639    #[rstest]
1640    #[case(index_html_zz_body(), "/", "deflate", "deflate")]
1641    #[case(index_html_zz_body(), "/", "deflate, identity", "deflate")]
1642    #[case(index_html_gz_body(), "/", "gzip", "gzip")]
1643    #[case(index_html_gz_body(), "/", "gzip, identity", "gzip")]
1644    #[case(index_html_gz_body(), "/", "gzip, deflate", "gzip")]
1645    #[case(index_html_gz_body(), "/", "gzip, deflate, identity", "gzip")]
1646    #[case(index_html_br_body(), "/", "br", "br")]
1647    #[case(index_html_br_body(), "/", "br, gzip, deflate, identity", "br")]
1648    #[case(index_html_br_body(), "/", "gzip, deflate, identity, br", "br")]
1649    #[case(index_html_zz_body(), "/index.html", "deflate", "deflate")]
1650    #[case(index_html_zz_body(), "/index.html", "deflate, identity", "deflate")]
1651    #[case(index_html_gz_body(), "/index.html", "gzip", "gzip")]
1652    #[case(index_html_gz_body(), "/index.html", "gzip, identity", "gzip")]
1653    #[case(index_html_gz_body(), "/index.html", "gzip, deflate", "gzip")]
1654    #[case(index_html_gz_body(), "/index.html", "gzip, deflate, identity", "gzip")]
1655    #[case(index_html_br_body(), "/index.html", "br", "br")]
1656    #[case(
1657        index_html_br_body(),
1658        "/index.html",
1659        "br, gzip, deflate, identity",
1660        "br"
1661    )]
1662    #[case(
1663        index_html_br_body(),
1664        "/index.html",
1665        "gzip, deflate, identity, br",
1666        "br"
1667    )]
1668    fn test_encoded_index_html(
1669        #[case] expected_body: Vec<u8>,
1670        #[case] req_url: &str,
1671        #[case] accept_encoding: &str,
1672        #[case] expected_encoding: &str,
1673        mut asset_router: AssetRouter,
1674    ) {
1675        let request = HttpRequest::get(req_url)
1676            .with_headers(vec![(
1677                "accept-encoding".to_string(),
1678                accept_encoding.to_string(),
1679            )])
1680            .build();
1681        let mut expected_response = build_200_response(
1682            expected_body,
1683            encoded_asset_cel_expr(),
1684            vec![
1685                (
1686                    "cache-control".to_string(),
1687                    "public, no-cache, no-store".to_string(),
1688                ),
1689                ("content-type".to_string(), "text/html".to_string()),
1690                (
1691                    "content-encoding".to_string(),
1692                    expected_encoding.to_string(),
1693                ),
1694            ],
1695        );
1696        let response = asset_router
1697            .serve_asset(&data_certificate(), &request)
1698            .unwrap();
1699        let (witness, expr_path) = extract_witness_expr_path(&response);
1700        add_v2_certificate_header(
1701            &data_certificate(),
1702            &mut expected_response,
1703            &witness,
1704            &expr_path,
1705        );
1706
1707        assert_eq!(
1708            expr_path,
1709            HttpCertificationPath::exact(req_url).to_expr_path()
1710        );
1711        assert_matches!(
1712            witness.lookup_subtree(&expr_path),
1713            SubtreeLookupResult::Found(_)
1714        );
1715        assert_eq!(response, expected_response);
1716
1717        asset_router
1718            .delete_assets(
1719                vec![
1720                    Asset::new("index.html", index_html_body()),
1721                    Asset::new("index.html.gz", index_html_gz_body()),
1722                    Asset::new("index.html.zz", index_html_zz_body()),
1723                    Asset::new("index.html.br", index_html_br_body()),
1724                ],
1725                vec![index_html_config()],
1726            )
1727            .unwrap();
1728
1729        let result = asset_router.serve_asset(&data_certificate(), &request);
1730        assert_matches!(
1731            result,
1732            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1733                request_url,
1734             }) if request_url == req_url
1735        );
1736    }
1737
1738    #[rstest]
1739    #[case(index_html_zz_body(), "/something", "deflate", "deflate")]
1740    #[case(index_html_zz_body(), "/something", "deflate, identity", "deflate")]
1741    #[case(index_html_gz_body(), "/something", "gzip", "gzip")]
1742    #[case(index_html_gz_body(), "/something", "gzip, identity", "gzip")]
1743    #[case(index_html_gz_body(), "/something", "gzip, deflate", "gzip")]
1744    #[case(index_html_gz_body(), "/something", "gzip, deflate, identity", "gzip")]
1745    #[case(index_html_br_body(), "/something", "br", "br")]
1746    #[case(
1747        index_html_br_body(),
1748        "/something",
1749        "br, gzip, deflate, identity",
1750        "br"
1751    )]
1752    #[case(
1753        index_html_br_body(),
1754        "/something",
1755        "gzip, deflate, identity, br",
1756        "br"
1757    )]
1758    #[case(index_html_zz_body(), "/assets/css/app.css", "deflate", "deflate")]
1759    #[case(
1760        index_html_zz_body(),
1761        "/assets/css/app.css",
1762        "deflate, identity",
1763        "deflate"
1764    )]
1765    #[case(index_html_gz_body(), "/assets/css/app.css", "gzip", "gzip")]
1766    #[case(index_html_gz_body(), "/assets/css/app.css", "gzip, identity", "gzip")]
1767    #[case(index_html_gz_body(), "/assets/css/app.css", "gzip, deflate", "gzip")]
1768    #[case(
1769        index_html_gz_body(),
1770        "/assets/css/app.css",
1771        "gzip, deflate, identity",
1772        "gzip"
1773    )]
1774    #[case(index_html_br_body(), "/assets/css/app.css", "br", "br")]
1775    #[case(
1776        index_html_br_body(),
1777        "/assets/css/app.css",
1778        "br, gzip, deflate, identity",
1779        "br"
1780    )]
1781    #[case(
1782        index_html_br_body(),
1783        "/assets/css/app.css",
1784        "gzip, deflate, identity, br",
1785        "br"
1786    )]
1787    fn test_encoded_index_html_fallback(
1788        #[case] expected_body: Vec<u8>,
1789        #[case] req_url: &str,
1790        #[case] accept_encoding: &str,
1791        #[case] expected_encoding: &str,
1792        mut asset_router: AssetRouter,
1793    ) {
1794        let request = HttpRequest::get(req_url)
1795            .with_headers(vec![(
1796                "accept-encoding".to_string(),
1797                accept_encoding.to_string(),
1798            )])
1799            .build();
1800        let mut expected_response = build_200_response(
1801            expected_body,
1802            encoded_asset_cel_expr(),
1803            vec![
1804                (
1805                    "cache-control".to_string(),
1806                    "public, no-cache, no-store".to_string(),
1807                ),
1808                ("content-type".to_string(), "text/html".to_string()),
1809                (
1810                    "content-encoding".to_string(),
1811                    expected_encoding.to_string(),
1812                ),
1813            ],
1814        );
1815
1816        let response = asset_router
1817            .serve_asset(&data_certificate(), &request)
1818            .unwrap();
1819        let (witness, expr_path) = extract_witness_expr_path(&response);
1820        add_v2_certificate_header(
1821            &data_certificate(),
1822            &mut expected_response,
1823            &witness,
1824            &expr_path,
1825        );
1826
1827        let requested_expr_path = HttpCertificationPath::exact(req_url).to_expr_path();
1828        assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
1829        assert_matches!(
1830            witness.lookup_subtree(&expr_path),
1831            SubtreeLookupResult::Found(_)
1832        );
1833        assert_matches!(
1834            witness.lookup_subtree(&requested_expr_path),
1835            SubtreeLookupResult::Absent
1836        );
1837        assert_eq!(response, expected_response);
1838
1839        asset_router
1840            .delete_assets(
1841                vec![
1842                    Asset::new("index.html", index_html_body()),
1843                    Asset::new("index.html.gz", index_html_gz_body()),
1844                    Asset::new("index.html.zz", index_html_zz_body()),
1845                    Asset::new("index.html.br", index_html_br_body()),
1846                ],
1847                vec![index_html_config()],
1848            )
1849            .unwrap();
1850
1851        let result = asset_router.serve_asset(&data_certificate(), &request);
1852        assert_matches!(
1853            result,
1854            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1855                request_url,
1856             }) if request_url == req_url
1857        );
1858    }
1859
1860    #[rstest]
1861    #[case("/something", "/something")]
1862    #[case("https://internetcomputer.org/something", "/something")]
1863    fn test_index_html_root_fallback(
1864        mut asset_router: AssetRouter,
1865        #[case] req_url: &str,
1866        #[case] req_path: &str,
1867    ) {
1868        let mut expected_response = expected_index_html_response();
1869
1870        let request = HttpRequest::get(req_url).build();
1871        let requested_expr_path = HttpCertificationPath::exact(req_path).to_expr_path();
1872
1873        let response = asset_router
1874            .serve_asset(&data_certificate(), &request)
1875            .unwrap();
1876        let (witness, expr_path) = extract_witness_expr_path(&response);
1877        add_v2_certificate_header(
1878            &data_certificate(),
1879            &mut expected_response,
1880            &witness,
1881            &expr_path,
1882        );
1883
1884        assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
1885        assert_matches!(
1886            witness.lookup_subtree(&expr_path),
1887            SubtreeLookupResult::Found(_)
1888        );
1889        assert_matches!(
1890            witness.lookup_subtree(&requested_expr_path),
1891            SubtreeLookupResult::Absent
1892        );
1893        assert_eq!(response, expected_response);
1894
1895        asset_router
1896            .delete_assets(
1897                vec![
1898                    Asset::new("index.html", index_html_body()),
1899                    Asset::new("index.html.gz", index_html_gz_body()),
1900                    Asset::new("index.html.zz", index_html_zz_body()),
1901                    Asset::new("index.html.br", index_html_br_body()),
1902                ],
1903                vec![index_html_config()],
1904            )
1905            .unwrap();
1906
1907        let result = asset_router.serve_asset(&data_certificate(), &request);
1908        assert_matches!(
1909            result,
1910            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1911                request_url,
1912             }) if request_url == req_path
1913        );
1914    }
1915
1916    #[rstest]
1917    #[case("/assets/css/app.css", "/assets/css/app.css")]
1918    #[case(
1919        "https://internetcomputer.org/assets/css/app.css",
1920        "/assets/css/app.css"
1921    )]
1922    fn test_index_html_nested_fallback(
1923        mut asset_router: AssetRouter,
1924        #[case] req_url: &str,
1925        #[case] req_path: &str,
1926    ) {
1927        let mut expected_response = expected_index_html_response();
1928
1929        let request = HttpRequest::get(req_url).build();
1930        let requested_expr_path = HttpCertificationPath::exact(req_path).to_expr_path();
1931
1932        let response = asset_router
1933            .serve_asset(&data_certificate(), &request)
1934            .unwrap();
1935        let (witness, expr_path) = extract_witness_expr_path(&response);
1936        add_v2_certificate_header(
1937            &data_certificate(),
1938            &mut expected_response,
1939            &witness,
1940            &expr_path,
1941        );
1942
1943        assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
1944        assert_matches!(
1945            witness.lookup_subtree(&expr_path),
1946            SubtreeLookupResult::Found(_)
1947        );
1948        assert_matches!(
1949            witness.lookup_subtree(&requested_expr_path),
1950            SubtreeLookupResult::Absent
1951        );
1952        assert_eq!(response, expected_response);
1953
1954        asset_router
1955            .delete_assets(
1956                vec![
1957                    Asset::new("index.html", index_html_body()),
1958                    Asset::new("index.html.gz", index_html_gz_body()),
1959                    Asset::new("index.html.zz", index_html_zz_body()),
1960                    Asset::new("index.html.br", index_html_br_body()),
1961                ],
1962                vec![index_html_config()],
1963            )
1964            .unwrap();
1965
1966        let result = asset_router.serve_asset(&data_certificate(), &request);
1967        assert_matches!(
1968            result,
1969            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1970                request_url,
1971             }) if request_url == req_path
1972        );
1973    }
1974
1975    #[rstest]
1976    #[case("/css/app-ba74b708.css")]
1977    #[case("https://internetcomputer.org/css/app-ba74b708.css")]
1978    fn text_app_css(mut asset_router: AssetRouter, #[case] req_url: &str) {
1979        let request = HttpRequest::get(req_url).build();
1980        let mut expected_response = build_200_response(
1981            app_css_body(),
1982            asset_cel_expr(),
1983            vec![
1984                (
1985                    "cache-control".to_string(),
1986                    "public, max-age=31536000, immutable".to_string(),
1987                ),
1988                ("content-type".to_string(), "text/css".to_string()),
1989            ],
1990        );
1991
1992        let response = asset_router
1993            .serve_asset(&data_certificate(), &request)
1994            .unwrap();
1995        let (witness, expr_path) = extract_witness_expr_path(&response);
1996        add_v2_certificate_header(
1997            &data_certificate(),
1998            &mut expected_response,
1999            &witness,
2000            &expr_path,
2001        );
2002
2003        assert_eq!(
2004            expr_path,
2005            vec!["http_expr", "css", "app-ba74b708.css", "<$>"]
2006        );
2007        assert_matches!(
2008            witness.lookup_subtree(&expr_path),
2009            SubtreeLookupResult::Found(_)
2010        );
2011        assert_eq!(response, expected_response);
2012
2013        asset_router
2014            .delete_assets(
2015                vec![Asset::new("css/app-ba74b708.css", app_css_body())],
2016                vec![css_config()],
2017            )
2018            .unwrap();
2019        let mut expected_response = build_response(
2020            StatusCode::NOT_FOUND,
2021            not_found_html_body(),
2022            asset_cel_expr(),
2023            vec![
2024                (
2025                    "cache-control".to_string(),
2026                    "public, no-cache, no-store".to_string(),
2027                ),
2028                ("content-type".to_string(), "text/html".to_string()),
2029            ],
2030        );
2031
2032        let response = asset_router
2033            .serve_asset(&data_certificate(), &request)
2034            .unwrap();
2035        let (witness, expr_path) = extract_witness_expr_path(&response);
2036        add_v2_certificate_header(
2037            &data_certificate(),
2038            &mut expected_response,
2039            &witness,
2040            &expr_path,
2041        );
2042
2043        assert_eq!(expr_path, vec!["http_expr", "css", "<*>"]);
2044        assert_matches!(
2045            witness.lookup_subtree(&expr_path),
2046            SubtreeLookupResult::Found(_)
2047        );
2048        assert_eq!(response, expected_response);
2049
2050        asset_router
2051            .delete_assets(
2052                vec![
2053                    Asset::new("not-found.html", not_found_html_body()),
2054                    Asset::new("not-found.html.gz", not_found_html_gz_body()),
2055                    Asset::new("not-found.html.zz", not_found_html_zz_body()),
2056                    Asset::new("not-found.html.br", not_found_html_br_body()),
2057                ],
2058                vec![not_found_html_config()],
2059            )
2060            .unwrap();
2061        let mut expected_response = expected_index_html_response();
2062
2063        let response = asset_router
2064            .serve_asset(&data_certificate(), &request)
2065            .unwrap();
2066        let (witness, expr_path) = extract_witness_expr_path(&response);
2067        add_v2_certificate_header(
2068            &data_certificate(),
2069            &mut expected_response,
2070            &witness,
2071            &expr_path,
2072        );
2073
2074        assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
2075        assert_matches!(
2076            witness.lookup_subtree(&expr_path),
2077            SubtreeLookupResult::Found(_)
2078        );
2079        assert_eq!(response, expected_response);
2080
2081        asset_router
2082            .delete_assets(
2083                vec![
2084                    Asset::new("index.html", index_html_body()),
2085                    Asset::new("index.html.gz", index_html_gz_body()),
2086                    Asset::new("index.html.zz", index_html_zz_body()),
2087                    Asset::new("index.html.br", index_html_br_body()),
2088                ],
2089                vec![index_html_config()],
2090            )
2091            .unwrap();
2092
2093        let result = asset_router.serve_asset(&data_certificate(), &request);
2094        assert_matches!(
2095            result,
2096            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2097                request_url,
2098             }) if request_url == request.get_path().unwrap()
2099        );
2100    }
2101
2102    #[rstest]
2103    #[case("/css/core-8d4jhgy2.js")]
2104    #[case("https://internetcomputer.org/css/core-8d4jhgy2.js")]
2105    fn test_not_found_css(mut asset_router: AssetRouter, #[case] req_url: &str) {
2106        let request = HttpRequest::get(req_url).build();
2107        let mut expected_response = build_response(
2108            StatusCode::NOT_FOUND,
2109            not_found_html_body(),
2110            asset_cel_expr(),
2111            vec![
2112                (
2113                    "cache-control".to_string(),
2114                    "public, no-cache, no-store".to_string(),
2115                ),
2116                ("content-type".to_string(), "text/html".to_string()),
2117            ],
2118        );
2119
2120        let response = asset_router
2121            .serve_asset(&data_certificate(), &request)
2122            .unwrap();
2123        let (witness, expr_path) = extract_witness_expr_path(&response);
2124        add_v2_certificate_header(
2125            &data_certificate(),
2126            &mut expected_response,
2127            &witness,
2128            &expr_path,
2129        );
2130
2131        assert_eq!(expr_path, vec!["http_expr", "css", "<*>"]);
2132        assert_matches!(
2133            witness.lookup_subtree(&expr_path),
2134            SubtreeLookupResult::Found(_)
2135        );
2136        assert_eq!(response, expected_response);
2137
2138        asset_router
2139            .delete_assets(
2140                vec![
2141                    Asset::new("not-found.html", not_found_html_body()),
2142                    Asset::new("not-found.html.gz", not_found_html_gz_body()),
2143                    Asset::new("not-found.html.zz", not_found_html_zz_body()),
2144                    Asset::new("not-found.html.br", not_found_html_br_body()),
2145                ],
2146                vec![not_found_html_config()],
2147            )
2148            .unwrap();
2149        let mut expected_response = expected_index_html_response();
2150
2151        let response = asset_router
2152            .serve_asset(&data_certificate(), &request)
2153            .unwrap();
2154        let (witness, expr_path) = extract_witness_expr_path(&response);
2155        add_v2_certificate_header(
2156            &data_certificate(),
2157            &mut expected_response,
2158            &witness,
2159            &expr_path,
2160        );
2161
2162        assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
2163        assert_matches!(
2164            witness.lookup_subtree(&expr_path),
2165            SubtreeLookupResult::Found(_)
2166        );
2167        assert_eq!(response, expected_response);
2168
2169        asset_router
2170            .delete_assets(
2171                vec![
2172                    Asset::new("index.html", index_html_body()),
2173                    Asset::new("index.html.gz", index_html_gz_body()),
2174                    Asset::new("index.html.zz", index_html_zz_body()),
2175                    Asset::new("index.html.br", index_html_br_body()),
2176                ],
2177                vec![index_html_config()],
2178            )
2179            .unwrap();
2180
2181        let result = asset_router.serve_asset(&data_certificate(), &request);
2182        assert_matches!(
2183            result,
2184            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2185                request_url,
2186             }) if request_url == request.get_path().unwrap()
2187        );
2188    }
2189
2190    #[rstest]
2191    #[case("/js/app-488df671.js")]
2192    #[case("https://internetcomputer.org/js/app-488df671.js")]
2193    fn test_app_js(mut asset_router: AssetRouter, #[case] req_url: &str) {
2194        let request = HttpRequest::get(req_url).build();
2195        let mut expected_response = build_200_response(
2196            app_js_body(),
2197            asset_cel_expr(),
2198            vec![
2199                (
2200                    "cache-control".to_string(),
2201                    "public, max-age=31536000, immutable".to_string(),
2202                ),
2203                ("content-type".to_string(), "text/javascript".to_string()),
2204            ],
2205        );
2206
2207        let response = asset_router
2208            .serve_asset(&data_certificate(), &request)
2209            .unwrap();
2210        let (witness, expr_path) = extract_witness_expr_path(&response);
2211        add_v2_certificate_header(
2212            &data_certificate(),
2213            &mut expected_response,
2214            &witness,
2215            &expr_path,
2216        );
2217
2218        assert_eq!(expr_path, vec!["http_expr", "js", "app-488df671.js", "<$>"]);
2219        assert_matches!(
2220            witness.lookup_subtree(&expr_path),
2221            SubtreeLookupResult::Found(_)
2222        );
2223        assert_eq!(response, expected_response);
2224
2225        asset_router
2226            .delete_assets(
2227                vec![
2228                    Asset::new("js/app-488df671.js", app_js_body()),
2229                    Asset::new("js/app-488df671.js.gz", app_js_gz_body()),
2230                    Asset::new("js/app-488df671.js.zz", app_js_zz_body()),
2231                    Asset::new("js/app-488df671.js.br", app_js_br_body()),
2232                ],
2233                vec![js_config()],
2234            )
2235            .unwrap();
2236        let mut expected_response = build_response(
2237            StatusCode::NOT_FOUND,
2238            not_found_html_body(),
2239            asset_cel_expr(),
2240            vec![
2241                (
2242                    "cache-control".to_string(),
2243                    "public, no-cache, no-store".to_string(),
2244                ),
2245                ("content-type".to_string(), "text/html".to_string()),
2246            ],
2247        );
2248
2249        let response = asset_router
2250            .serve_asset(&data_certificate(), &request)
2251            .unwrap();
2252        let (witness, expr_path) = extract_witness_expr_path(&response);
2253        add_v2_certificate_header(
2254            &data_certificate(),
2255            &mut expected_response,
2256            &witness,
2257            &expr_path,
2258        );
2259
2260        assert_eq!(expr_path, vec!["http_expr", "js", "<*>"]);
2261        assert_matches!(
2262            witness.lookup_subtree(&expr_path),
2263            SubtreeLookupResult::Found(_)
2264        );
2265        assert_eq!(response, expected_response);
2266
2267        asset_router
2268            .delete_assets(
2269                vec![
2270                    Asset::new("not-found.html", not_found_html_body()),
2271                    Asset::new("not-found.html.gz", not_found_html_gz_body()),
2272                    Asset::new("not-found.html.zz", not_found_html_zz_body()),
2273                    Asset::new("not-found.html.br", not_found_html_br_body()),
2274                ],
2275                vec![not_found_html_config()],
2276            )
2277            .unwrap();
2278        let mut expected_response = expected_index_html_response();
2279
2280        let response = asset_router
2281            .serve_asset(&data_certificate(), &request)
2282            .unwrap();
2283        let (witness, expr_path) = extract_witness_expr_path(&response);
2284        add_v2_certificate_header(
2285            &data_certificate(),
2286            &mut expected_response,
2287            &witness,
2288            &expr_path,
2289        );
2290
2291        assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
2292        assert_matches!(
2293            witness.lookup_subtree(&expr_path),
2294            SubtreeLookupResult::Found(_)
2295        );
2296        assert_eq!(response, expected_response);
2297
2298        asset_router
2299            .delete_assets(
2300                vec![
2301                    Asset::new("index.html", index_html_body()),
2302                    Asset::new("index.html.gz", index_html_gz_body()),
2303                    Asset::new("index.html.zz", index_html_zz_body()),
2304                    Asset::new("index.html.br", index_html_br_body()),
2305                ],
2306                vec![index_html_config()],
2307            )
2308            .unwrap();
2309
2310        let result = asset_router.serve_asset(&data_certificate(), &request);
2311        assert_matches!(
2312            result,
2313            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2314                request_url,
2315             }) if request_url == request.get_path().unwrap()
2316        );
2317    }
2318
2319    #[rstest]
2320    #[case(
2321        app_js_zz_body(),
2322        not_found_html_zz_body(),
2323        index_html_zz_body(),
2324        "deflate",
2325        "deflate"
2326    )]
2327    #[case(
2328        app_js_zz_body(),
2329        not_found_html_zz_body(),
2330        index_html_zz_body(),
2331        "deflate, identity",
2332        "deflate"
2333    )]
2334    #[case(
2335        app_js_zz_body(),
2336        not_found_html_zz_body(),
2337        index_html_zz_body(),
2338        "identity, deflate",
2339        "deflate"
2340    )]
2341    #[case(
2342        app_js_gz_body(),
2343        not_found_html_gz_body(),
2344        index_html_gz_body(),
2345        "gzip",
2346        "gzip"
2347    )]
2348    #[case(
2349        app_js_gz_body(),
2350        not_found_html_gz_body(),
2351        index_html_gz_body(),
2352        "gzip, identity",
2353        "gzip"
2354    )]
2355    #[case(
2356        app_js_gz_body(),
2357        not_found_html_gz_body(),
2358        index_html_gz_body(),
2359        "identity, gzip",
2360        "gzip"
2361    )]
2362    #[case(
2363        app_js_gz_body(),
2364        not_found_html_gz_body(),
2365        index_html_gz_body(),
2366        "gzip, deflate",
2367        "gzip"
2368    )]
2369    #[case(
2370        app_js_gz_body(),
2371        not_found_html_gz_body(),
2372        index_html_gz_body(),
2373        "deflate, gzip",
2374        "gzip"
2375    )]
2376    #[case(
2377        app_js_gz_body(),
2378        not_found_html_gz_body(),
2379        index_html_gz_body(),
2380        "gzip, deflate, identity",
2381        "gzip"
2382    )]
2383    #[case(
2384        app_js_gz_body(),
2385        not_found_html_gz_body(),
2386        index_html_gz_body(),
2387        "gzip, identity, deflate",
2388        "gzip"
2389    )]
2390    #[case(
2391        app_js_gz_body(),
2392        not_found_html_gz_body(),
2393        index_html_gz_body(),
2394        "identity, gzip, deflate",
2395        "gzip"
2396    )]
2397    #[case(
2398        app_js_gz_body(),
2399        not_found_html_gz_body(),
2400        index_html_gz_body(),
2401        "identity, deflate, gzip",
2402        "gzip"
2403    )]
2404    #[case(
2405        app_js_gz_body(),
2406        not_found_html_gz_body(),
2407        index_html_gz_body(),
2408        "deflate, gzip, identity",
2409        "gzip"
2410    )]
2411    #[case(
2412        app_js_gz_body(),
2413        not_found_html_gz_body(),
2414        index_html_gz_body(),
2415        "deflate, identity, gzip",
2416        "gzip"
2417    )]
2418    #[case(
2419        app_js_br_body(),
2420        not_found_html_br_body(),
2421        index_html_br_body(),
2422        "br",
2423        "br"
2424    )]
2425    #[case(
2426        app_js_br_body(),
2427        not_found_html_br_body(),
2428        index_html_br_body(),
2429        "br, gzip, deflate, identity",
2430        "br"
2431    )]
2432    #[case(
2433        app_js_br_body(),
2434        not_found_html_br_body(),
2435        index_html_br_body(),
2436        "gzip, deflate, identity, br",
2437        "br"
2438    )]
2439    fn test_encoded_app_js(
2440        #[case] expected_body: Vec<u8>,
2441        #[case] expected_not_found_body: Vec<u8>,
2442        #[case] expected_index_body: Vec<u8>,
2443        #[case] accept_encoding: &str,
2444        #[case] expected_encoding: &str,
2445        mut asset_router: AssetRouter,
2446    ) {
2447        let request = HttpRequest::get("/js/app-488df671.js")
2448            .with_headers(vec![(
2449                "accept-encoding".to_string(),
2450                accept_encoding.to_string(),
2451            )])
2452            .build();
2453
2454        let mut expected_response = build_200_response(
2455            expected_body,
2456            encoded_asset_cel_expr(),
2457            vec![
2458                (
2459                    "cache-control".to_string(),
2460                    "public, max-age=31536000, immutable".to_string(),
2461                ),
2462                ("content-type".to_string(), "text/javascript".to_string()),
2463                (
2464                    "content-encoding".to_string(),
2465                    expected_encoding.to_string(),
2466                ),
2467            ],
2468        );
2469
2470        let response = asset_router
2471            .serve_asset(&data_certificate(), &request)
2472            .unwrap();
2473        let (witness, expr_path) = extract_witness_expr_path(&response);
2474        add_v2_certificate_header(
2475            &data_certificate(),
2476            &mut expected_response,
2477            &witness,
2478            &expr_path,
2479        );
2480
2481        assert_eq!(expr_path, vec!["http_expr", "js", "app-488df671.js", "<$>"]);
2482        assert_matches!(
2483            witness.lookup_subtree(&expr_path),
2484            SubtreeLookupResult::Found(_)
2485        );
2486        assert_eq!(response, expected_response);
2487
2488        asset_router
2489            .delete_assets(
2490                vec![
2491                    Asset::new("js/app-488df671.js", app_js_body()),
2492                    Asset::new("js/app-488df671.js.gz", app_js_gz_body()),
2493                    Asset::new("js/app-488df671.js.zz", app_js_zz_body()),
2494                    Asset::new("js/app-488df671.js.br", app_js_br_body()),
2495                ],
2496                vec![js_config()],
2497            )
2498            .unwrap();
2499        let mut expected_response = build_response(
2500            StatusCode::NOT_FOUND,
2501            expected_not_found_body,
2502            encoded_asset_cel_expr(),
2503            vec![
2504                (
2505                    "cache-control".to_string(),
2506                    "public, no-cache, no-store".to_string(),
2507                ),
2508                ("content-type".to_string(), "text/html".to_string()),
2509                (
2510                    "content-encoding".to_string(),
2511                    expected_encoding.to_string(),
2512                ),
2513            ],
2514        );
2515
2516        let response = asset_router
2517            .serve_asset(&data_certificate(), &request)
2518            .unwrap();
2519        let (witness, expr_path) = extract_witness_expr_path(&response);
2520        add_v2_certificate_header(
2521            &data_certificate(),
2522            &mut expected_response,
2523            &witness,
2524            &expr_path,
2525        );
2526
2527        assert_eq!(expr_path, vec!["http_expr", "js", "<*>"]);
2528        assert_matches!(
2529            witness.lookup_subtree(&expr_path),
2530            SubtreeLookupResult::Found(_)
2531        );
2532        assert_eq!(response, expected_response);
2533
2534        asset_router
2535            .delete_assets(
2536                vec![
2537                    Asset::new("not-found.html", not_found_html_body()),
2538                    Asset::new("not-found.html.gz", not_found_html_gz_body()),
2539                    Asset::new("not-found.html.zz", not_found_html_zz_body()),
2540                    Asset::new("not-found.html.br", not_found_html_br_body()),
2541                ],
2542                vec![not_found_html_config()],
2543            )
2544            .unwrap();
2545        let mut expected_response = build_200_response(
2546            expected_index_body,
2547            encoded_asset_cel_expr(),
2548            vec![
2549                (
2550                    "cache-control".to_string(),
2551                    "public, no-cache, no-store".to_string(),
2552                ),
2553                ("content-type".to_string(), "text/html".to_string()),
2554                (
2555                    "content-encoding".to_string(),
2556                    expected_encoding.to_string(),
2557                ),
2558            ],
2559        );
2560
2561        let response = asset_router
2562            .serve_asset(&data_certificate(), &request)
2563            .unwrap();
2564        let (witness, expr_path) = extract_witness_expr_path(&response);
2565        add_v2_certificate_header(
2566            &data_certificate(),
2567            &mut expected_response,
2568            &witness,
2569            &expr_path,
2570        );
2571
2572        assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
2573        assert_matches!(
2574            witness.lookup_subtree(&expr_path),
2575            SubtreeLookupResult::Found(_)
2576        );
2577        assert_eq!(response, expected_response);
2578
2579        asset_router
2580            .delete_assets(
2581                vec![
2582                    Asset::new("index.html", index_html_body()),
2583                    Asset::new("index.html.gz", index_html_gz_body()),
2584                    Asset::new("index.html.zz", index_html_zz_body()),
2585                    Asset::new("index.html.br", index_html_br_body()),
2586                ],
2587                vec![index_html_config()],
2588            )
2589            .unwrap();
2590
2591        let result = asset_router.serve_asset(&data_certificate(), &request);
2592        assert_matches!(
2593            result,
2594            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2595                request_url,
2596             }) if request_url == request.get_path().unwrap()
2597        );
2598    }
2599
2600    #[rstest]
2601    #[case("/js/core-7dk12y45.js")]
2602    #[case("https://internetcomputer.org/js/core-7dk12y45.js")]
2603    fn test_not_found_js(mut asset_router: AssetRouter, #[case] req_url: &str) {
2604        let request = HttpRequest::get(req_url).build();
2605        let mut expected_response = build_response(
2606            StatusCode::NOT_FOUND,
2607            not_found_html_body(),
2608            asset_cel_expr(),
2609            vec![
2610                (
2611                    "cache-control".to_string(),
2612                    "public, no-cache, no-store".to_string(),
2613                ),
2614                ("content-type".to_string(), "text/html".to_string()),
2615            ],
2616        );
2617
2618        let response = asset_router
2619            .serve_asset(&data_certificate(), &request)
2620            .unwrap();
2621        let (witness, expr_path) = extract_witness_expr_path(&response);
2622        add_v2_certificate_header(
2623            &data_certificate(),
2624            &mut expected_response,
2625            &witness,
2626            &expr_path,
2627        );
2628
2629        assert_eq!(expr_path, vec!["http_expr", "js", "<*>"]);
2630        assert_matches!(
2631            witness.lookup_subtree(&expr_path),
2632            SubtreeLookupResult::Found(_)
2633        );
2634        assert_eq!(response, expected_response);
2635
2636        asset_router
2637            .delete_assets(
2638                vec![
2639                    Asset::new("not-found.html", not_found_html_body()),
2640                    Asset::new("not-found.html.gz", not_found_html_gz_body()),
2641                    Asset::new("not-found.html.zz", not_found_html_zz_body()),
2642                    Asset::new("not-found.html.br", not_found_html_br_body()),
2643                ],
2644                vec![not_found_html_config()],
2645            )
2646            .unwrap();
2647
2648        let mut expected_response = expected_index_html_response();
2649
2650        let response = asset_router
2651            .serve_asset(&data_certificate(), &request)
2652            .unwrap();
2653        let (witness, expr_path) = extract_witness_expr_path(&response);
2654        add_v2_certificate_header(
2655            &data_certificate(),
2656            &mut expected_response,
2657            &witness,
2658            &expr_path,
2659        );
2660
2661        assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
2662        assert_matches!(
2663            witness.lookup_subtree(&expr_path),
2664            SubtreeLookupResult::Found(_)
2665        );
2666        assert_eq!(response, expected_response);
2667
2668        asset_router
2669            .delete_assets(
2670                vec![
2671                    Asset::new("index.html", index_html_body()),
2672                    Asset::new("index.html.gz", index_html_gz_body()),
2673                    Asset::new("index.html.zz", index_html_zz_body()),
2674                    Asset::new("index.html.br", index_html_br_body()),
2675                ],
2676                vec![index_html_config()],
2677            )
2678            .unwrap();
2679
2680        let result = asset_router.serve_asset(&data_certificate(), &request);
2681        assert_matches!(
2682            result,
2683            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2684                request_url,
2685             }) if request_url == request.get_path().unwrap()
2686        );
2687    }
2688
2689    #[rstest]
2690    #[case("/404")]
2691    #[case("https://internetcomputer.org/404")]
2692    #[case("/404/")]
2693    #[case("https://internetcomputer.org/404/")]
2694    #[case("/404.html")]
2695    #[case("https://internetcomputer.org/404.html")]
2696    #[case("/not-found")]
2697    #[case("https://internetcomputer.org/not-found")]
2698    #[case("/not-found/")]
2699    #[case("https://internetcomputer.org/not-found/")]
2700    #[case("/not-found/index.html")]
2701    #[case("https://internetcomputer.org/not-found/index.html")]
2702    fn test_not_found_alias(mut asset_router: AssetRouter, #[case] req_url: &str) {
2703        let request = HttpRequest::get(req_url).build();
2704        let mut expected_response = build_200_response(
2705            not_found_html_body(),
2706            asset_cel_expr(),
2707            vec![
2708                (
2709                    "cache-control".to_string(),
2710                    "public, no-cache, no-store".to_string(),
2711                ),
2712                ("content-type".to_string(), "text/html".to_string()),
2713            ],
2714        );
2715
2716        let response = asset_router
2717            .serve_asset(&data_certificate(), &request)
2718            .unwrap();
2719        let (witness, expr_path) = extract_witness_expr_path(&response);
2720        add_v2_certificate_header(
2721            &data_certificate(),
2722            &mut expected_response,
2723            &witness,
2724            &expr_path,
2725        );
2726
2727        assert_eq!(
2728            expr_path,
2729            HttpCertificationPath::exact(request.get_path().unwrap()).to_expr_path()
2730        );
2731        assert_matches!(
2732            witness.lookup_subtree(&expr_path),
2733            SubtreeLookupResult::Found(_)
2734        );
2735        assert_eq!(response, expected_response);
2736
2737        asset_router
2738            .delete_assets(
2739                vec![
2740                    Asset::new("not-found.html", not_found_html_body()),
2741                    Asset::new("not-found.html.gz", not_found_html_gz_body()),
2742                    Asset::new("not-found.html.zz", not_found_html_zz_body()),
2743                    Asset::new("not-found.html.br", not_found_html_br_body()),
2744                ],
2745                vec![not_found_html_config()],
2746            )
2747            .unwrap();
2748
2749        let mut expected_response = expected_index_html_response();
2750
2751        let response = asset_router
2752            .serve_asset(&data_certificate(), &request)
2753            .unwrap();
2754        let (witness, expr_path) = extract_witness_expr_path(&response);
2755        add_v2_certificate_header(
2756            &data_certificate(),
2757            &mut expected_response,
2758            &witness,
2759            &expr_path,
2760        );
2761
2762        assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
2763        assert_matches!(
2764            witness.lookup_subtree(&expr_path),
2765            SubtreeLookupResult::Found(_)
2766        );
2767        assert_eq!(response, expected_response);
2768
2769        asset_router
2770            .delete_assets(
2771                vec![
2772                    Asset::new("index.html", index_html_body()),
2773                    Asset::new("index.html.gz", index_html_gz_body()),
2774                    Asset::new("index.html.zz", index_html_zz_body()),
2775                    Asset::new("index.html.br", index_html_br_body()),
2776                ],
2777                vec![index_html_config()],
2778            )
2779            .unwrap();
2780
2781        let result = asset_router.serve_asset(&data_certificate(), &request);
2782        assert_matches!(
2783            result,
2784            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2785                request_url,
2786             }) if request_url == request.get_path().unwrap()
2787        );
2788    }
2789
2790    #[rstest]
2791    fn test_delete_all_assets() {
2792        let mut asset_router = asset_router();
2793
2794        let request = HttpRequest::get("/index.html").build();
2795
2796        let mut expected_response = expected_index_html_response();
2797
2798        let response = asset_router
2799            .serve_asset(&data_certificate(), &request)
2800            .unwrap();
2801        let (witness, expr_path) = extract_witness_expr_path(&response);
2802        add_v2_certificate_header(
2803            &data_certificate(),
2804            &mut expected_response,
2805            &witness,
2806            &expr_path,
2807        );
2808
2809        assert_eq!(response, expected_response);
2810
2811        asset_router.delete_all_assets();
2812
2813        assert_matches!(
2814            asset_router.serve_asset(
2815                &data_certificate(),
2816                &request,
2817            ),
2818            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2819                request_url,
2820            }) if request_url == "/index.html"
2821        );
2822
2823        let assets: Vec<_> = asset_router.get_assets().iter().collect();
2824        assert!(assets.is_empty());
2825    }
2826
2827    #[rstest]
2828    fn test_delete_by_path() {
2829        let mut asset_router = asset_router();
2830
2831        let index_request = HttpRequest::get("/index.html").build();
2832        let mut expected_index_response = expected_index_html_response();
2833        let index_response = asset_router
2834            .serve_asset(&data_certificate(), &index_request)
2835            .unwrap();
2836        let (witness, expr_path) = extract_witness_expr_path(&index_response);
2837        add_v2_certificate_header(
2838            &data_certificate(),
2839            &mut expected_index_response,
2840            &witness,
2841            &expr_path,
2842        );
2843        assert_eq!(index_response, expected_index_response);
2844
2845        let alias_index_request = HttpRequest::get("/").build();
2846        let mut expected_alias_index_response = expected_index_html_response();
2847        let alias_index_response = asset_router
2848            .serve_asset(&data_certificate(), &alias_index_request)
2849            .unwrap();
2850        let (witness, expr_path) = extract_witness_expr_path(&alias_index_response);
2851        add_v2_certificate_header(
2852            &data_certificate(),
2853            &mut expected_alias_index_response,
2854            &witness,
2855            &expr_path,
2856        );
2857        assert_eq!(alias_index_response, expected_alias_index_response);
2858
2859        let fallback_index_request = HttpRequest::get("/non-existent").build();
2860        let mut expected_fallback_index_response = expected_index_html_response();
2861        let fallback_index_response = asset_router
2862            .serve_asset(&data_certificate(), &fallback_index_request)
2863            .unwrap();
2864        let (witness, expr_path) = extract_witness_expr_path(&fallback_index_response);
2865        add_v2_certificate_header(
2866            &data_certificate(),
2867            &mut expected_fallback_index_response,
2868            &witness,
2869            &expr_path,
2870        );
2871        assert_eq!(fallback_index_response, expected_fallback_index_response);
2872
2873        asset_router.delete_fallback_assets_by_path(vec!["/"]);
2874
2875        let index_request = HttpRequest::get("/index.html").build();
2876        let mut expected_index_response = expected_index_html_response();
2877        let index_response = asset_router
2878            .serve_asset(&data_certificate(), &index_request)
2879            .unwrap();
2880        let (witness, expr_path) = extract_witness_expr_path(&index_response);
2881        add_v2_certificate_header(
2882            &data_certificate(),
2883            &mut expected_index_response,
2884            &witness,
2885            &expr_path,
2886        );
2887        assert_eq!(index_response, expected_index_response);
2888
2889        let alias_index_request = HttpRequest::get("/").build();
2890        let mut expected_alias_index_response = expected_index_html_response();
2891        let alias_index_response = asset_router
2892            .serve_asset(&data_certificate(), &alias_index_request)
2893            .unwrap();
2894        let (witness, expr_path) = extract_witness_expr_path(&alias_index_response);
2895        add_v2_certificate_header(
2896            &data_certificate(),
2897            &mut expected_alias_index_response,
2898            &witness,
2899            &expr_path,
2900        );
2901        assert_eq!(alias_index_response, expected_alias_index_response);
2902
2903        assert_matches!(
2904            asset_router.serve_asset(
2905                &data_certificate(),
2906                &fallback_index_request,
2907            ),
2908            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2909                request_url,
2910            }) if request_url == "/non-existent"
2911        );
2912
2913        asset_router.delete_assets_by_path(vec!["/index.html", "/"]);
2914
2915        assert_matches!(
2916            asset_router.serve_asset(
2917                &data_certificate(),
2918                &index_request,
2919            ),
2920            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2921                request_url,
2922            }) if request_url == "/index.html"
2923        );
2924        assert_matches!(
2925            asset_router.serve_asset(
2926                &data_certificate(),
2927                &alias_index_request,
2928            ),
2929            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2930                request_url,
2931            }) if request_url == "/"
2932        );
2933        assert_matches!(
2934            asset_router.serve_asset(
2935                &data_certificate(),
2936                &fallback_index_request,
2937            ),
2938            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2939                request_url,
2940            }) if request_url == "/non-existent"
2941        );
2942    }
2943
2944    #[rstest]
2945    fn test_asset_map(mut asset_router: AssetRouter) {
2946        let index_html_response = asset_router.get_assets().get("/index.html", None, None);
2947        assert_matches!(
2948            index_html_response,
2949            Some(index_html_response) if index_html_response == &expected_index_html_response()
2950        );
2951
2952        let index_html_fallback_response = asset_router.get_fallback_assets().get("/", None, None);
2953        assert_matches!(
2954            index_html_fallback_response,
2955            Some(index_html_fallback_response) if index_html_fallback_response == &expected_index_html_response()
2956        );
2957
2958        let index_html_gz_response =
2959            asset_router
2960                .get_assets()
2961                .get("/index.html", Some(AssetEncoding::Gzip), None);
2962        assert_matches!(
2963            index_html_gz_response,
2964            Some(index_html_gz_response) if index_html_gz_response == &expected_index_html_gz_response()
2965        );
2966
2967        let index_html_gz_fallback_response =
2968            asset_router
2969                .get_fallback_assets()
2970                .get("/", Some(AssetEncoding::Gzip), None);
2971        assert_matches!(
2972            index_html_gz_fallback_response,
2973            Some(index_html_gz_fallback_response) if index_html_gz_fallback_response == &expected_index_html_gz_response()
2974        );
2975
2976        let index_html_zz_response =
2977            asset_router
2978                .get_assets()
2979                .get("/index.html", Some(AssetEncoding::Deflate), None);
2980        assert_matches!(
2981            index_html_zz_response,
2982            Some(index_html_zz_response) if index_html_zz_response == &expected_index_html_zz_response()
2983        );
2984
2985        let index_html_zz_fallback_response =
2986            asset_router
2987                .get_fallback_assets()
2988                .get("/", Some(AssetEncoding::Deflate), None);
2989        assert_matches!(
2990            index_html_zz_fallback_response,
2991            Some(index_html_zz_fallback_response) if index_html_zz_fallback_response == &expected_index_html_zz_response()
2992        );
2993
2994        let index_html_br_response =
2995            asset_router
2996                .get_assets()
2997                .get("/index.html", Some(AssetEncoding::Brotli), None);
2998        assert_matches!(
2999            index_html_br_response,
3000            Some(index_html_br_response) if index_html_br_response == &expected_index_html_br_response()
3001        );
3002
3003        let index_html_br_fallback_response =
3004            asset_router
3005                .get_fallback_assets()
3006                .get("/", Some(AssetEncoding::Brotli), None);
3007        assert_matches!(
3008            index_html_br_fallback_response,
3009            Some(index_html_br_fallback_response) if index_html_br_fallback_response == &expected_index_html_br_response()
3010        );
3011
3012        asset_router
3013            .delete_assets(
3014                vec![
3015                    Asset::new("index.html", index_html_body()),
3016                    Asset::new("index.html.gz", index_html_gz_body()),
3017                    Asset::new("index.html.zz", index_html_zz_body()),
3018                    Asset::new("index.html.br", index_html_br_body()),
3019                ],
3020                vec![index_html_config()],
3021            )
3022            .unwrap();
3023
3024        let index_html_response = asset_router.get_assets().get("/index.html", None, None);
3025        assert_matches!(index_html_response, None);
3026
3027        let index_html_fallback_response = asset_router.get_fallback_assets().get("/", None, None);
3028        assert_matches!(index_html_fallback_response, None);
3029
3030        let index_html_gz_response =
3031            asset_router
3032                .get_assets()
3033                .get("/index.html", Some(AssetEncoding::Gzip), None);
3034        assert_matches!(index_html_gz_response, None);
3035
3036        let index_html_gz_fallback_response =
3037            asset_router
3038                .get_fallback_assets()
3039                .get("/", Some(AssetEncoding::Gzip), None);
3040        assert_matches!(index_html_gz_fallback_response, None);
3041
3042        let index_html_zz_response =
3043            asset_router
3044                .get_assets()
3045                .get("/index.html", Some(AssetEncoding::Deflate), None);
3046        assert_matches!(index_html_zz_response, None);
3047
3048        let index_html_zz_fallback_response =
3049            asset_router
3050                .get_fallback_assets()
3051                .get("/", Some(AssetEncoding::Deflate), None);
3052        assert_matches!(index_html_zz_fallback_response, None);
3053
3054        let index_html_br_response =
3055            asset_router
3056                .get_assets()
3057                .get("/index.html", Some(AssetEncoding::Brotli), None);
3058        assert_matches!(index_html_br_response, None);
3059
3060        let index_html_br_fallback_response =
3061            asset_router
3062                .get_fallback_assets()
3063                .get("/", Some(AssetEncoding::Brotli), None);
3064        assert_matches!(index_html_br_fallback_response, None);
3065    }
3066
3067    #[rstest]
3068    fn test_asset_map_chunked_responses() {
3069        let mut asset_router = long_asset_router_with_params(
3070            &[TWO_CHUNKS_ASSET_NAME],
3071            &[AssetEncoding::Gzip, AssetEncoding::Identity],
3072        );
3073
3074        let full_body = long_asset_body(TWO_CHUNKS_ASSET_NAME);
3075        let (_, gzip_suffix) = AssetEncoding::Gzip.default_config();
3076        let full_gz_body = long_asset_body(&format!("{TWO_CHUNKS_ASSET_NAME}{gzip_suffix}"));
3077
3078        let first_chunk_body = &full_body[0..ASSET_CHUNK_SIZE];
3079        let first_chunk_response =
3080            asset_router
3081                .get_assets()
3082                .get(format!("/{TWO_CHUNKS_ASSET_NAME}"), None, Some(0));
3083        let expected_first_chunk_response = build_206_response(
3084            first_chunk_body.to_vec(),
3085            asset_cel_expr(),
3086            vec![
3087                (
3088                    "cache-control".to_string(),
3089                    "public, no-cache, no-store".to_string(),
3090                ),
3091                ("content-type".to_string(), "text/html".to_string()),
3092                (
3093                    "content-range".to_string(),
3094                    format!("bytes 0-{}/{}", first_chunk_body.len() - 1, full_body.len()),
3095                ),
3096            ],
3097        );
3098        assert_matches!(
3099            first_chunk_response,
3100            Some(first_chunk_response) if first_chunk_response == &expected_first_chunk_response
3101        );
3102
3103        let first_chunk_gzip_body = &full_gz_body[0..ASSET_CHUNK_SIZE];
3104        let first_chunk_gzip_response = asset_router.get_assets().get(
3105            format!("/{TWO_CHUNKS_ASSET_NAME}"),
3106            Some(AssetEncoding::Gzip),
3107            Some(0),
3108        );
3109        let expected_first_chunk_gzip_response = build_206_response(
3110            first_chunk_gzip_body.to_vec(),
3111            encoded_asset_cel_expr(),
3112            vec![
3113                (
3114                    "cache-control".to_string(),
3115                    "public, no-cache, no-store".to_string(),
3116                ),
3117                ("content-type".to_string(), "text/html".to_string()),
3118                ("content-encoding".to_string(), "gzip".to_string()),
3119                (
3120                    "content-range".to_string(),
3121                    format!(
3122                        "bytes 0-{}/{}",
3123                        first_chunk_gzip_body.len() - 1,
3124                        full_gz_body.len()
3125                    ),
3126                ),
3127            ],
3128        );
3129        assert_matches!(
3130            first_chunk_gzip_response,
3131            Some(first_chunk_gzip_response) if first_chunk_gzip_response == &expected_first_chunk_gzip_response
3132        );
3133
3134        let second_chunk_body = &full_body[ASSET_CHUNK_SIZE..full_body.len()];
3135        let second_chunk_response = asset_router.get_assets().get(
3136            format!("/{TWO_CHUNKS_ASSET_NAME}"),
3137            None,
3138            Some(ASSET_CHUNK_SIZE),
3139        );
3140        let expected_second_chunk_response = build_206_response(
3141            second_chunk_body.to_vec(),
3142            asset_range_chunk_cel_expr(),
3143            vec![
3144                (
3145                    "cache-control".to_string(),
3146                    "public, no-cache, no-store".to_string(),
3147                ),
3148                ("content-type".to_string(), "text/html".to_string()),
3149                (
3150                    "content-range".to_string(),
3151                    format!(
3152                        "bytes {}-{}/{}",
3153                        first_chunk_body.len(),
3154                        first_chunk_body.len() + second_chunk_body.len() - 1,
3155                        full_body.len()
3156                    ),
3157                ),
3158            ],
3159        );
3160        assert_matches!(
3161            second_chunk_response,
3162            Some(second_chunk_response) if second_chunk_response == &expected_second_chunk_response
3163        );
3164
3165        let second_chunk_gzip_body = &full_gz_body[ASSET_CHUNK_SIZE..full_gz_body.len()];
3166        let second_chunk_gzip_response = asset_router.get_assets().get(
3167            format!("/{TWO_CHUNKS_ASSET_NAME}"),
3168            Some(AssetEncoding::Gzip),
3169            Some(ASSET_CHUNK_SIZE),
3170        );
3171        let expected_second_chunk_gzip_response = build_206_response(
3172            second_chunk_gzip_body.to_vec(),
3173            asset_range_chunk_cel_expr(),
3174            vec![
3175                (
3176                    "cache-control".to_string(),
3177                    "public, no-cache, no-store".to_string(),
3178                ),
3179                ("content-type".to_string(), "text/html".to_string()),
3180                ("content-encoding".to_string(), "gzip".to_string()),
3181                (
3182                    "content-range".to_string(),
3183                    format!(
3184                        "bytes {}-{}/{}",
3185                        first_chunk_gzip_body.len(),
3186                        first_chunk_gzip_body.len() + second_chunk_gzip_body.len() - 1,
3187                        full_gz_body.len()
3188                    ),
3189                ),
3190            ],
3191        );
3192
3193        assert_matches!(
3194            second_chunk_gzip_response,
3195            Some(second_chunk_gzip_response) if second_chunk_gzip_response == &expected_second_chunk_gzip_response
3196        );
3197
3198        asset_router
3199            .delete_assets(
3200                vec![
3201                    long_asset(TWO_CHUNKS_ASSET_NAME.to_string()),
3202                    long_asset(format!("{TWO_CHUNKS_ASSET_NAME}{gzip_suffix}")),
3203                ],
3204                vec![
3205                    long_asset_config(TWO_CHUNKS_ASSET_NAME),
3206                    long_asset_config(&format!("{TWO_CHUNKS_ASSET_NAME}{gzip_suffix}")),
3207                ],
3208            )
3209            .unwrap();
3210
3211        let first_chunk_response =
3212            asset_router
3213                .get_assets()
3214                .get(format!("/{TWO_CHUNKS_ASSET_NAME}"), None, Some(0));
3215        assert_matches!(first_chunk_response, None);
3216
3217        let first_chunk_gzip_response = asset_router.get_assets().get(
3218            format!("/{TWO_CHUNKS_ASSET_NAME}"),
3219            Some(AssetEncoding::Gzip),
3220            Some(0),
3221        );
3222        assert_matches!(first_chunk_gzip_response, None);
3223
3224        let second_chunk_response = asset_router.get_assets().get(
3225            format!("/{TWO_CHUNKS_ASSET_NAME}"),
3226            None,
3227            Some(ASSET_CHUNK_SIZE),
3228        );
3229        assert_matches!(second_chunk_response, None);
3230
3231        let second_chunk_gzip_response = asset_router.get_assets().get(
3232            format!("/{TWO_CHUNKS_ASSET_NAME}"),
3233            Some(AssetEncoding::Gzip),
3234            Some(ASSET_CHUNK_SIZE),
3235        );
3236        assert_matches!(second_chunk_gzip_response, None);
3237    }
3238
3239    #[rstest]
3240    fn test_asset_map_iter() {
3241        let asset_router =
3242            long_asset_router_with_params(&[TWO_CHUNKS_ASSET_NAME], &[AssetEncoding::Identity]);
3243        let full_body = long_asset_body(TWO_CHUNKS_ASSET_NAME);
3244
3245        let assets: Vec<_> = asset_router.get_assets().iter().collect();
3246        assert!(assets.len() == 3);
3247
3248        let first_chunk_body = &full_body[0..ASSET_CHUNK_SIZE];
3249        let expected_first_chunk_response = build_206_response(
3250            first_chunk_body.to_vec(),
3251            asset_cel_expr(),
3252            vec![
3253                (
3254                    "cache-control".to_string(),
3255                    "public, no-cache, no-store".to_string(),
3256                ),
3257                ("content-type".to_string(), "text/html".to_string()),
3258                (
3259                    "content-range".to_string(),
3260                    format!("bytes 0-{}/{}", first_chunk_body.len() - 1, full_body.len()),
3261                ),
3262            ],
3263        );
3264
3265        assert!(assets.contains(&(
3266            (&format!("/{TWO_CHUNKS_ASSET_NAME}"), None, Some(0)),
3267            &expected_first_chunk_response
3268        )));
3269
3270        let second_chunk_body = &full_body[ASSET_CHUNK_SIZE..full_body.len()];
3271        let expected_second_chunk_response = build_206_response(
3272            second_chunk_body.to_vec(),
3273            asset_range_chunk_cel_expr(),
3274            vec![
3275                (
3276                    "cache-control".to_string(),
3277                    "public, no-cache, no-store".to_string(),
3278                ),
3279                ("content-type".to_string(), "text/html".to_string()),
3280                (
3281                    "content-range".to_string(),
3282                    format!(
3283                        "bytes {}-{}/{}",
3284                        first_chunk_body.len(),
3285                        first_chunk_body.len() + second_chunk_body.len() - 1,
3286                        full_body.len()
3287                    ),
3288                ),
3289            ],
3290        );
3291        assert!(assets.contains(&(
3292            (
3293                &format!("/{TWO_CHUNKS_ASSET_NAME}"),
3294                None,
3295                Some(ASSET_CHUNK_SIZE)
3296            ),
3297            &expected_second_chunk_response
3298        )));
3299
3300        let expected_full_response = build_200_response(
3301            full_body,
3302            asset_cel_expr(),
3303            vec![
3304                (
3305                    "cache-control".to_string(),
3306                    "public, no-cache, no-store".to_string(),
3307                ),
3308                ("content-type".to_string(), "text/html".to_string()),
3309            ],
3310        );
3311        assert!(assets.contains(&(
3312            (&format!("/{TWO_CHUNKS_ASSET_NAME}"), None, None),
3313            &expected_full_response
3314        )));
3315    }
3316
3317    #[rstest]
3318    fn test_redirects(mut asset_router: AssetRouter) {
3319        let cel_expr = DefaultFullCelExpressionBuilder::default()
3320            .with_response_certification(DefaultResponseCertification::response_header_exclusions(
3321                vec![],
3322            ))
3323            .build()
3324            .to_string();
3325
3326        let css_request = HttpRequest::get("/css/app.css").build();
3327        let old_url_request = HttpRequest::get("/old-url").build();
3328
3329        let mut expected_css_response = HttpResponse::builder()
3330            .with_status_code(StatusCode::TEMPORARY_REDIRECT)
3331            .with_headers(vec![
3332                ("content-length".to_string(), "0".to_string()),
3333                ("location".to_string(), "/css/app-ba74b708.css".to_string()),
3334                (
3335                    CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
3336                    cel_expr.clone(),
3337                ),
3338                (
3339                    "content-type".to_string(),
3340                    "text/plain; charset=utf-8".to_string(),
3341                ),
3342            ])
3343            .build();
3344        let mut expected_old_url_response = HttpResponse::builder()
3345            .with_status_code(StatusCode::MOVED_PERMANENTLY)
3346            .with_headers(vec![
3347                ("content-length".to_string(), "0".to_string()),
3348                ("location".to_string(), "/".to_string()),
3349                (CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(), cel_expr),
3350                (
3351                    "content-type".to_string(),
3352                    "text/plain; charset=utf-8".to_string(),
3353                ),
3354            ])
3355            .build();
3356
3357        let css_response = asset_router
3358            .serve_asset(&data_certificate(), &css_request)
3359            .unwrap();
3360        let (css_witness, css_expr_path) = extract_witness_expr_path(&css_response);
3361        add_v2_certificate_header(
3362            &data_certificate(),
3363            &mut expected_css_response,
3364            &css_witness,
3365            &css_expr_path,
3366        );
3367        let old_url_response = asset_router
3368            .serve_asset(&data_certificate(), &old_url_request)
3369            .unwrap();
3370        let (old_url_witness, old_url_expr_path) = extract_witness_expr_path(&old_url_response);
3371        add_v2_certificate_header(
3372            &data_certificate(),
3373            &mut expected_old_url_response,
3374            &old_url_witness,
3375            &old_url_expr_path,
3376        );
3377
3378        assert_eq!(css_expr_path, vec!["http_expr", "css", "app.css", "<$>"]);
3379        assert_matches!(
3380            css_witness.lookup_subtree(&css_expr_path),
3381            SubtreeLookupResult::Found(_)
3382        );
3383        assert_eq!(css_response, expected_css_response);
3384
3385        assert_eq!(old_url_expr_path, vec!["http_expr", "old-url", "<$>"]);
3386        assert_matches!(
3387            old_url_witness.lookup_subtree(&old_url_expr_path),
3388            SubtreeLookupResult::Found(_)
3389        );
3390        assert_eq!(old_url_response, expected_old_url_response);
3391
3392        asset_router
3393            .delete_assets(
3394                vec![],
3395                vec![old_url_redirect_config(), css_redirect_config()],
3396            )
3397            .unwrap();
3398        let mut expected_css_response = build_response(
3399            StatusCode::NOT_FOUND,
3400            not_found_html_body(),
3401            asset_cel_expr(),
3402            vec![
3403                (
3404                    "cache-control".to_string(),
3405                    "public, no-cache, no-store".to_string(),
3406                ),
3407                ("content-type".to_string(), "text/html".to_string()),
3408            ],
3409        );
3410        let mut expected_old_url_response = expected_index_html_response();
3411
3412        let css_response = asset_router
3413            .serve_asset(&data_certificate(), &css_request)
3414            .unwrap();
3415        let (css_witness, css_expr_path) = extract_witness_expr_path(&css_response);
3416        add_v2_certificate_header(
3417            &data_certificate(),
3418            &mut expected_css_response,
3419            &css_witness,
3420            &css_expr_path,
3421        );
3422        let old_url_response = asset_router
3423            .serve_asset(&data_certificate(), &old_url_request)
3424            .unwrap();
3425        let (old_url_witness, old_url_expr_path) = extract_witness_expr_path(&old_url_response);
3426        add_v2_certificate_header(
3427            &data_certificate(),
3428            &mut expected_old_url_response,
3429            &old_url_witness,
3430            &old_url_expr_path,
3431        );
3432
3433        assert_eq!(css_expr_path, vec!["http_expr", "css", "<*>"]);
3434        assert_matches!(
3435            css_witness.lookup_subtree(&css_expr_path),
3436            SubtreeLookupResult::Found(_)
3437        );
3438        assert_eq!(css_response, expected_css_response);
3439
3440        assert_eq!(old_url_expr_path, vec!["http_expr", "", "<*>"]);
3441        assert_matches!(
3442            old_url_witness.lookup_subtree(&old_url_expr_path),
3443            SubtreeLookupResult::Found(_)
3444        );
3445        assert_eq!(old_url_response, expected_old_url_response);
3446
3447        asset_router
3448            .delete_assets(
3449                vec![
3450                    Asset::new("not-found.html", not_found_html_body()),
3451                    Asset::new("not-found.html.gz", not_found_html_gz_body()),
3452                    Asset::new("not-found.html.zz", not_found_html_zz_body()),
3453                    Asset::new("not-found.html.br", not_found_html_br_body()),
3454                ],
3455                vec![not_found_html_config()],
3456            )
3457            .unwrap();
3458        let mut expected_css_response = expected_index_html_response();
3459        let mut expected_old_url_response = expected_index_html_response();
3460
3461        let css_response = asset_router
3462            .serve_asset(&data_certificate(), &css_request)
3463            .unwrap();
3464        let (css_witness, css_expr_path) = extract_witness_expr_path(&css_response);
3465        add_v2_certificate_header(
3466            &data_certificate(),
3467            &mut expected_css_response,
3468            &css_witness,
3469            &css_expr_path,
3470        );
3471        let old_url_response = asset_router
3472            .serve_asset(&data_certificate(), &old_url_request)
3473            .unwrap();
3474        let (old_url_witness, old_url_expr_path) = extract_witness_expr_path(&old_url_response);
3475        add_v2_certificate_header(
3476            &data_certificate(),
3477            &mut expected_old_url_response,
3478            &old_url_witness,
3479            &old_url_expr_path,
3480        );
3481
3482        assert_eq!(css_expr_path, vec!["http_expr", "", "<*>"]);
3483        assert_matches!(
3484            css_witness.lookup_subtree(&css_expr_path),
3485            SubtreeLookupResult::Found(_)
3486        );
3487        assert_eq!(css_response, expected_css_response);
3488
3489        assert_eq!(old_url_expr_path, vec!["http_expr", "", "<*>"]);
3490        assert_matches!(
3491            old_url_witness.lookup_subtree(&old_url_expr_path),
3492            SubtreeLookupResult::Found(_)
3493        );
3494        assert_eq!(old_url_response, expected_old_url_response);
3495
3496        asset_router
3497            .delete_assets(
3498                vec![
3499                    Asset::new("index.html", index_html_body()),
3500                    Asset::new("index.html.gz", index_html_gz_body()),
3501                    Asset::new("index.html.zz", index_html_zz_body()),
3502                    Asset::new("index.html.br", index_html_br_body()),
3503                ],
3504                vec![index_html_config()],
3505            )
3506            .unwrap();
3507
3508        let css_result = asset_router.serve_asset(&data_certificate(), &css_request);
3509        let old_url_result = asset_router.serve_asset(&data_certificate(), &old_url_request);
3510
3511        assert_matches!(
3512            css_result,
3513            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
3514                request_url,
3515             }) if request_url == css_request.get_path().unwrap()
3516        );
3517        assert_matches!(
3518            old_url_result,
3519            Err(AssetCertificationError::NoAssetMatchingRequestUrl {
3520                request_url,
3521             }) if request_url == old_url_request.get_path().unwrap()
3522        );
3523    }
3524
3525    #[rstest]
3526    fn test_init_with_tree(index_html_body: Vec<u8>, asset_cel_expr: String) {
3527        let http_certification_tree: Rc<RefCell<HttpCertificationTree>> = Default::default();
3528        let mut asset_router = AssetRouter::with_tree(http_certification_tree.clone());
3529
3530        let index_html_asset = Asset::new("index.html", &index_html_body);
3531        let index_html_config = AssetConfig::File {
3532            path: "index.html".to_string(),
3533            content_type: Some("text/html".to_string()),
3534            headers: vec![(
3535                "cache-control".to_string(),
3536                "public, no-cache, no-store".to_string(),
3537            )],
3538            fallback_for: vec![AssetFallbackConfig {
3539                scope: "/".to_string(),
3540                status_code: Some(StatusCode::OK),
3541            }],
3542            aliased_by: vec!["/".to_string()],
3543            encodings: vec![],
3544        };
3545
3546        asset_router
3547            .certify_assets(vec![index_html_asset], vec![index_html_config])
3548            .unwrap();
3549
3550        let request = HttpRequest::get("/").build();
3551
3552        let mut expected_response = build_200_response(
3553            index_html_body.clone(),
3554            asset_cel_expr,
3555            vec![
3556                (
3557                    "cache-control".to_string(),
3558                    "public, no-cache, no-store".to_string(),
3559                ),
3560                ("content-type".to_string(), "text/html".to_string()),
3561            ],
3562        );
3563
3564        let response = asset_router
3565            .serve_asset(&data_certificate(), &request)
3566            .unwrap();
3567        let (witness, expr_path) = extract_witness_expr_path(&response);
3568        add_v2_certificate_header(
3569            &data_certificate(),
3570            &mut expected_response,
3571            &witness,
3572            &expr_path,
3573        );
3574
3575        assert_eq!(expr_path, vec!["http_expr", "", "<$>"]);
3576        assert_matches!(
3577            witness.lookup_subtree(&expr_path),
3578            SubtreeLookupResult::Found(_)
3579        );
3580        assert_eq!(response, expected_response);
3581        assert_eq!(
3582            asset_router.root_hash(),
3583            http_certification_tree.borrow().root_hash()
3584        );
3585    }
3586
3587    fn long_asset_body(asset_name: &str) -> Vec<u8> {
3588        let asset_length = match asset_name {
3589            s if s.contains(ONE_CHUNK_ASSET_NAME) => ONE_CHUNK_ASSET_LEN,
3590            s if s.contains(TWO_CHUNKS_ASSET_NAME) => TWO_CHUNKS_ASSET_LEN,
3591            s if s.contains(SIX_CHUNKS_ASSET_NAME) => SIX_CHUNKS_ASSET_LEN,
3592            s if s.contains(TEN_CHUNKS_ASSET_NAME) => TEN_CHUNKS_ASSET_LEN,
3593            _ => ASSET_CHUNK_SIZE * 3 + 1,
3594        };
3595        let mut rng = ChaCha20Rng::from_seed(hash(asset_name));
3596        let mut body = vec![0u8; asset_length];
3597        rng.fill_bytes(&mut body);
3598        body
3599    }
3600
3601    #[fixture]
3602    fn index_html_body() -> Vec<u8> {
3603        b"<html><body><h1>Hello World!</h1></body></html>".to_vec()
3604    }
3605
3606    #[fixture]
3607    fn expected_index_html_response() -> HttpResponse<'static> {
3608        build_200_response(
3609            index_html_body(),
3610            asset_cel_expr(),
3611            vec![
3612                (
3613                    "cache-control".to_string(),
3614                    "public, no-cache, no-store".to_string(),
3615                ),
3616                ("content-type".to_string(), "text/html".to_string()),
3617            ],
3618        )
3619    }
3620
3621    // Gzip compressed version of `index_html_body`,
3622    // compressed using https://www.zickty.com/texttogzip/,
3623    // and then converted to bytes using https://conv.darkbyte.ru/.
3624    #[fixture]
3625    fn index_html_gz_body() -> Vec<u8> {
3626        vec![
3627            31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 179, 201, 40, 201, 205, 177, 179, 73, 202, 79, 169,
3628            180, 179, 201, 48, 180, 243, 72, 205, 201, 201, 87, 8, 207, 47, 202, 73, 81, 180, 209,
3629            7, 10, 216, 232, 67, 228, 244, 193, 10, 1, 28, 178, 8, 152, 47, 0, 0, 0,
3630        ]
3631    }
3632
3633    #[fixture]
3634    fn expected_index_html_gz_response() -> HttpResponse<'static> {
3635        build_200_response(
3636            index_html_gz_body(),
3637            asset_cel_expr(),
3638            vec![
3639                (
3640                    "cache-control".to_string(),
3641                    "public, no-cache, no-store".to_string(),
3642                ),
3643                ("content-type".to_string(), "text/html".to_string()),
3644                ("content-encoding".to_string(), "gzip".to_string()),
3645            ],
3646        )
3647    }
3648
3649    // Deflate compressed version of `index_html_body`,
3650    // compressed using https://www.zickty.com/texttogzip/,
3651    // and then converted to bytes using https://conv.darkbyte.ru/.
3652    #[fixture]
3653    fn index_html_zz_body() -> Vec<u8> {
3654        vec![
3655            78, 9, 3, 9, 28, 9, 1, 3, 49, 4, 9, 4, 3, 9, 30, 4, 3, 48, 9, 9, 57, 8, 2, 49, 51, 4,
3656            1, 7, 0, 8, 8, 43, 4, 4, 1, 0, 1, 7, 8, 0, 9,
3657        ]
3658    }
3659
3660    #[fixture]
3661    fn expected_index_html_zz_response() -> HttpResponse<'static> {
3662        build_200_response(
3663            index_html_zz_body(),
3664            asset_cel_expr(),
3665            vec![
3666                (
3667                    "cache-control".to_string(),
3668                    "public, no-cache, no-store".to_string(),
3669                ),
3670                ("content-type".to_string(), "text/html".to_string()),
3671                ("content-encoding".to_string(), "deflate".to_string()),
3672            ],
3673        )
3674    }
3675
3676    // Deflate compressed version of `index_html_body`,
3677    // compressed using https://facia.dev/tools/compress-decompress/brotli-compress/,
3678    // and then converted to bytes using https://conv.darkbyte.ru/.
3679    #[fixture]
3680    fn index_html_br_body() -> Vec<u8> {
3681        vec![
3682            1, 2, 0, 8, 1, 9, 36, 2, 72, 65, 4, 25, 0, 5, 84, 0, 5, 18, 1, 64, 14, 5, 4, 1, 9, 0,
3683            3, 4, 42, 2, 1, 59, 13, 14, 3, 19, 69, 18,
3684        ]
3685    }
3686
3687    #[fixture]
3688    fn expected_index_html_br_response() -> HttpResponse<'static> {
3689        build_200_response(
3690            index_html_br_body(),
3691            asset_cel_expr(),
3692            vec![
3693                (
3694                    "cache-control".to_string(),
3695                    "public, no-cache, no-store".to_string(),
3696                ),
3697                ("content-type".to_string(), "text/html".to_string()),
3698                ("content-encoding".to_string(), "br".to_string()),
3699            ],
3700        )
3701    }
3702
3703    #[fixture]
3704    fn app_js_body() -> Vec<u8> {
3705        b"console.log('Hello World!');".to_vec()
3706    }
3707
3708    // Gzip compressed version of `app_js_body`,
3709    // compressed using https://www.zickty.com/texttogzip/,
3710    // and then converted to bytes using https://conv.darkbyte.ru/.
3711    #[fixture]
3712    fn app_js_gz_body() -> Vec<u8> {
3713        vec![
3714            31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 206, 207, 43, 206, 207, 73, 213, 203, 201, 79,
3715            215, 80, 247, 72, 205, 201, 201, 87, 8, 207, 47, 202, 73, 81, 84, 215, 180, 6, 0, 186,
3716            42, 111, 142, 28, 0, 0, 0,
3717        ]
3718    }
3719
3720    // Deflate compressed version of `app_js_body`,
3721    // compressed using https://www.zickty.com/texttogzip/,
3722    // and then converted to bytes using https://conv.darkbyte.ru/.
3723    #[fixture]
3724    fn app_js_zz_body() -> Vec<u8> {
3725        vec![
3726            120, 156, 75, 206, 207, 43, 206, 207, 73, 213, 203, 201, 79, 215, 80, 247, 72, 205,
3727            201, 201, 87, 8, 207, 47, 202, 73, 81, 84, 215, 180, 6, 0, 148, 149, 9, 123,
3728        ]
3729    }
3730
3731    // Brotli compressed version of `app_js_body`,
3732    // compressed using https://facia.dev/tools/compress-decompress/brotli-compress/,
3733    // and then converted to bytes using https://conv.darkbyte.ru/.
3734    #[fixture]
3735    fn app_js_br_body() -> Vec<u8> {
3736        vec![
3737            8, 0, 80, 63, 6, 6, 73, 6, 6, 65, 2, 6, 6, 67, 28, 27, 48, 65, 6, 6, 6, 20, 57, 6, 72,
3738            6, 64, 21, 27, 29, 3, 3,
3739        ]
3740    }
3741
3742    #[fixture]
3743    fn app_css_body() -> Vec<u8> {
3744        b"html,body{min-height:100vh;}".to_vec()
3745    }
3746
3747    #[fixture]
3748    fn not_found_html_body() -> Vec<u8> {
3749        b"<html><body><h1>404 Not Found!</h1></body></html>".to_vec()
3750    }
3751
3752    // Gzip compressed version of `not_found_html_body`,
3753    // compressed using https://www.zickty.com/texttogzip/,
3754    // and then converted to bytes using https://conv.darkbyte.ru/.
3755    #[fixture]
3756    fn not_found_html_gz_body() -> Vec<u8> {
3757        vec![
3758            49, 19, 9, 8, 0, 0, 0, 0, 0, 0, 3, 23, 9, 32, 1, 64, 32, 1, 32, 5, 23, 7, 23, 9, 115,
3759            32, 2, 121, 22, 9, 24, 0, 23, 9, 32, 1, 72, 24, 0, 81, 73, 72, 129, 36, 0, 32, 3, 71,
3760            129, 17, 2, 32, 3, 71, 32, 5, 117, 129, 24, 0, 32, 9, 7, 16, 33, 7, 35, 2, 103, 16, 0,
3761            36, 5, 25, 3, 116, 1, 37, 4, 36, 9, 17, 0, 114, 73, 0, 0, 0,
3762        ]
3763    }
3764
3765    // Deflate compressed version of `not_found_html_body`,
3766    // compressed using https://www.zickty.com/texttogzip/,
3767    // and then converted to bytes using https://conv.darkbyte.ru/.
3768    #[fixture]
3769    fn not_found_html_zz_body() -> Vec<u8> {
3770        vec![
3771            120, 156, 179, 201, 40, 201, 205, 177, 179, 73, 202, 79, 169, 180, 179, 201, 48, 180,
3772            51, 49, 48, 81, 240, 203, 47, 81, 112, 203, 47, 205, 75, 81, 180, 209, 7, 10, 217, 232,
3773            67, 100, 245, 193, 74, 1, 133, 210, 15, 136,
3774        ]
3775    }
3776
3777    // Brotli compressed version of `not_found_html_body`,
3778    // compressed using https://facia.dev/tools/compress-decompress/brotli-compress/,
3779    // and then converted to bytes using https://conv.darkbyte.ru/.
3780    #[fixture]
3781    fn not_found_html_br_body() -> Vec<u8> {
3782        vec![
3783            27, 48, 0, 248, 45, 14, 108, 27, 88, 249, 245, 45, 213, 3, 233, 193, 146, 199, 9, 173,
3784            64, 104, 10, 230, 173, 67, 124, 216, 218, 84, 12, 93, 47, 66, 139, 48, 3, 233, 78, 128,
3785            105, 198, 36, 242, 83, 62, 179, 122, 129, 33, 16, 12,
3786        ]
3787    }
3788
3789    #[fixture]
3790    fn asset_cel_expr() -> String {
3791        DefaultFullCelExpressionBuilder::default()
3792            .with_response_certification(DefaultResponseCertification::response_header_exclusions(
3793                vec![],
3794            ))
3795            .build()
3796            .to_string()
3797    }
3798
3799    #[fixture]
3800    fn asset_range_chunk_cel_expr() -> String {
3801        DefaultFullCelExpressionBuilder::default()
3802            .with_request_headers(vec!["range"])
3803            .with_response_certification(DefaultResponseCertification::response_header_exclusions(
3804                vec![],
3805            ))
3806            .build()
3807            .to_string()
3808    }
3809
3810    #[fixture]
3811    fn encoded_asset_cel_expr() -> String {
3812        DefaultFullCelExpressionBuilder::default()
3813            .with_response_certification(DefaultResponseCertification::response_header_exclusions(
3814                vec![],
3815            ))
3816            .build()
3817            .to_string()
3818    }
3819
3820    #[fixture]
3821    fn encoded_range_chunk_asset_cel_expr() -> String {
3822        DefaultFullCelExpressionBuilder::default()
3823            .with_request_headers(vec!["range"])
3824            .with_response_certification(DefaultResponseCertification::response_header_exclusions(
3825                vec![],
3826            ))
3827            .build()
3828            .to_string()
3829    }
3830
3831    #[fixture]
3832    fn index_html_config() -> AssetConfig {
3833        AssetConfig::File {
3834            path: "index.html".to_string(),
3835            content_type: Some("text/html".to_string()),
3836            headers: vec![(
3837                "cache-control".to_string(),
3838                "public, no-cache, no-store".to_string(),
3839            )],
3840            fallback_for: vec![AssetFallbackConfig {
3841                scope: "/".to_string(),
3842                status_code: Some(StatusCode::OK),
3843            }],
3844            aliased_by: vec!["/".to_string()],
3845            encodings: vec![
3846                AssetEncoding::Gzip.default_config(),
3847                AssetEncoding::Deflate.default_config(),
3848                AssetEncoding::Brotli.default_config(),
3849            ],
3850        }
3851    }
3852
3853    #[fixture]
3854    fn js_config() -> AssetConfig {
3855        AssetConfig::Pattern {
3856            pattern: r"**/*.js".to_string(),
3857            content_type: Some("text/javascript".to_string()),
3858            headers: vec![(
3859                "cache-control".to_string(),
3860                "public, max-age=31536000, immutable".to_string(),
3861            )],
3862            encodings: vec![
3863                AssetEncoding::Gzip.default_config(),
3864                AssetEncoding::Deflate.default_config(),
3865                AssetEncoding::Brotli.default_config(),
3866            ],
3867        }
3868    }
3869
3870    #[fixture]
3871    fn css_config() -> AssetConfig {
3872        AssetConfig::Pattern {
3873            pattern: "**/*.css".to_string(),
3874            content_type: Some("text/css".to_string()),
3875            headers: vec![(
3876                "cache-control".to_string(),
3877                "public, max-age=31536000, immutable".to_string(),
3878            )],
3879            encodings: vec![
3880                AssetEncoding::Gzip.default_config(),
3881                AssetEncoding::Deflate.default_config(),
3882                AssetEncoding::Brotli.default_config(),
3883            ],
3884        }
3885    }
3886
3887    #[fixture]
3888    fn not_found_html_config() -> AssetConfig {
3889        AssetConfig::File {
3890            path: "not-found.html".to_string(),
3891            content_type: Some("text/html".to_string()),
3892            headers: vec![(
3893                "cache-control".to_string(),
3894                "public, no-cache, no-store".to_string(),
3895            )],
3896            fallback_for: vec![
3897                AssetFallbackConfig {
3898                    scope: "/js".to_string(),
3899                    status_code: Some(StatusCode::NOT_FOUND),
3900                },
3901                AssetFallbackConfig {
3902                    scope: "/css".to_string(),
3903                    status_code: Some(StatusCode::NOT_FOUND),
3904                },
3905            ],
3906            aliased_by: vec![
3907                "/404".to_string(),
3908                "/404/".to_string(),
3909                "/404.html".to_string(),
3910                "/not-found".to_string(),
3911                "/not-found/".to_string(),
3912                "/not-found/index.html".to_string(),
3913            ],
3914            encodings: vec![
3915                AssetEncoding::Gzip.default_config(),
3916                AssetEncoding::Deflate.default_config(),
3917                AssetEncoding::Brotli.default_config(),
3918            ],
3919        }
3920    }
3921
3922    #[fixture]
3923    fn old_url_redirect_config() -> AssetConfig {
3924        AssetConfig::Redirect {
3925            from: "/old-url".to_string(),
3926            to: "/".to_string(),
3927            kind: AssetRedirectKind::Permanent,
3928            headers: vec![(
3929                "content-type".to_string(),
3930                "text/plain; charset=utf-8".to_string(),
3931            )],
3932        }
3933    }
3934
3935    #[fixture]
3936    fn css_redirect_config() -> AssetConfig {
3937        AssetConfig::Redirect {
3938            from: "/css/app.css".to_string(),
3939            to: "/css/app-ba74b708.css".to_string(),
3940            kind: AssetRedirectKind::Temporary,
3941            headers: vec![(
3942                "content-type".to_string(),
3943                "text/plain; charset=utf-8".to_string(),
3944            )],
3945        }
3946    }
3947
3948    fn long_asset_config(path: &str) -> AssetConfig {
3949        AssetConfig::File {
3950            path: path.to_string(),
3951            content_type: Some("text/html".to_string()),
3952            headers: vec![(
3953                "cache-control".to_string(),
3954                "public, no-cache, no-store".to_string(),
3955            )],
3956            fallback_for: vec![],
3957            aliased_by: vec![],
3958            encodings: vec![
3959                AssetEncoding::Brotli.default_config(),
3960                AssetEncoding::Deflate.default_config(),
3961                AssetEncoding::Gzip.default_config(),
3962            ],
3963        }
3964    }
3965
3966    #[fixture]
3967    fn asset_router<'a>() -> AssetRouter<'a> {
3968        let mut asset_router = AssetRouter::default();
3969
3970        let assets = vec![
3971            Asset::new("index.html", index_html_body()),
3972            Asset::new("index.html.gz", index_html_gz_body()),
3973            Asset::new("index.html.zz", index_html_zz_body()),
3974            Asset::new("index.html.br", index_html_br_body()),
3975            Asset::new("js/app-488df671.js", app_js_body()),
3976            Asset::new("js/app-488df671.js.gz", app_js_gz_body()),
3977            Asset::new("js/app-488df671.js.zz", app_js_zz_body()),
3978            Asset::new("js/app-488df671.js.br", app_js_br_body()),
3979            Asset::new("css/app-ba74b708.css", app_css_body()),
3980            Asset::new("not-found.html", not_found_html_body()),
3981            Asset::new("not-found.html.gz", not_found_html_gz_body()),
3982            Asset::new("not-found.html.zz", not_found_html_zz_body()),
3983            Asset::new("not-found.html.br", not_found_html_br_body()),
3984        ];
3985
3986        let asset_configs = vec![
3987            index_html_config(),
3988            js_config(),
3989            css_config(),
3990            not_found_html_config(),
3991            old_url_redirect_config(),
3992            css_redirect_config(),
3993        ];
3994
3995        asset_router.certify_assets(assets, asset_configs).unwrap();
3996
3997        asset_router
3998    }
3999
4000    fn long_asset(name: String) -> Asset<'static, 'static> {
4001        let body = long_asset_body(&name);
4002        Asset::new(name, body)
4003    }
4004
4005    fn long_asset_router_with_params<'a>(
4006        asset_names: &[&str],
4007        encodings: &[AssetEncoding],
4008    ) -> AssetRouter<'a> {
4009        let mut asset_router = AssetRouter::default();
4010        let mut assets = vec![];
4011        let mut asset_configs = vec![];
4012
4013        for name in asset_names {
4014            for encoding in encodings {
4015                let (_, encoding_suffix) = encoding.default_config();
4016                let full_name = format!("{name}{encoding_suffix}");
4017                assets.push(long_asset(full_name));
4018            }
4019            asset_configs.push(long_asset_config(name));
4020        }
4021        asset_router.certify_assets(assets, asset_configs).unwrap();
4022        asset_router
4023    }
4024
4025    fn build_200_response<'a>(
4026        body: Vec<u8>,
4027        cel_expr: String,
4028        headers: Vec<HeaderField>,
4029    ) -> HttpResponse<'a> {
4030        build_response(StatusCode::OK, body, cel_expr, headers)
4031    }
4032
4033    fn build_206_response<'a>(
4034        body: Vec<u8>,
4035        cel_expr: String,
4036        headers: Vec<HeaderField>,
4037    ) -> HttpResponse<'a> {
4038        build_response(StatusCode::PARTIAL_CONTENT, body, cel_expr, headers)
4039    }
4040
4041    // A certificate taken from a real response on mainnet. It doesn't matter what it contains,
4042    // as long as it's a valid certificate. If we ever decide to run response verification in these
4043    // tests then the content of the certificate will matter.
4044    #[fixture]
4045    fn data_certificate() -> Vec<u8> {
4046        base64_decode("2dn3o2R0cmVlgwGDAYMBggRYIKZa+jjiJCKIY6ieu3PP5Vz5wLXmyPh1bDmIzXg5dl6LgwJIY2FuaXN0ZXKDAYMBggRYIM/g0MyVpl3VttUo8bFaIM3krNFLeWDQlazn4vVmbs12gwGDAYMBgwGDAYIEWCCrjGIsFv7RroK/KT/vV4dT/8o6c6Q8uFH3A372mLl7I4MBggRYIIILxwOzUO8JeUy1GuQk1oRnBKc7mlApt5csrszervQDgwGDAYIEWCBlMbtdoKygLVFQVorKucJVgVLHtLscN5S8BBykjfmQ+4MBgwGCBFgg680ttF9A023RfxGHUK7ceDdxxyHb6Cbg4qjinLrq+6mDAYIEWCCURcqsdMxTygYlQwS+KseXWp9QWrCCtb446pyKsj3lOoMCSgAAAAABgAmUAQGDAYMBgwJOY2VydGlmaWVkX2RhdGGCA1ggk/T8pnqZAQeSmaKDG8U/GSSBWQTEZfior9A2Wo/FZTSCBFgggt6vD1DPdgxNs+gKICbk3nRcQI5Tkp5syXYnM9GFJmCCBFggP4ZGeJfdyQHomGieqWx6e2QVSVgmctlGDoHCTcTrXWOCBFggILD+6U1L9j+vw02XY6JkB17xt0k6Esl/mcWDSifUPGyCBFggmUXnX4i82sN5AJqyUuIq4i9ErZ47rVK637kTCrOT7eSCBFggNqmXjq9JIe/CbAZW3wpfG12ofFrV8a7+tL5SL0tQ5ymCBFggFI5GZ/bYvjr0BzJiwPwH1yI7Rmd6jam1yj81cX8EqCWCBFggHHprIPk9m3zr3vFaSCA2JJiHSbLLfHzd+a0Rs90Ay7eCBFgg/5TQFWqdhgGiUF6Vy8qFpoKghCMgdXuYrc6pm7nSdpSCBFggzWD0/+Mf+jkRU4F1Xegk5vpPLOuDIfmneS92N03AdpyCBFggtNKNmO74J7jAsbkLgC5DQTEvIelYUdINiDtCOwUPLOWDAYIEWCC8BmcZ/cRsbnrLwEIfk310vWtBYg1iZAh9uLo+rxEtQYMCRHRpbWWCA0nZ/7r03qCj+hdpc2lnbmF0dXJlWDCU4tWKn7kUwxBiXeWihdpAEsfRN8YXvlz8/U7/NzTeb9t3mIdUlyj+YcQVEu9nxBdqZGVsZWdhdGlvbqJpc3VibmV0X2lkWB0QtkczSlQGmHeWsvi2sUzTLwh20/1PyEUhBYMlAmtjZXJ0aWZpY2F0ZVkCfdnZ96JkdHJlZYMBggRYIEe+kyDbAopvNlKUKa90j7vq/mnGs3p+1NUR03ZZjauCgwGDAYIEWCDSkWhnwtRPFDffqILBSu0cTmpQgjY9a5IEFfY8yKUxfIMCRnN1Ym5ldIMBgwGDAYMBgwGCBFgg0dOP/K78SbZBfvb185tLrkYoD1X09di+aBvUgAg+iJeDAYMCWB0QtkczSlQGmHeWsvi2sUzTLwh20/1PyEUhBYMlAoMBgwJPY2FuaXN0ZXJfcmFuZ2VzggNYG9nZ94GCSgAAAAABgAAAAQFKAAAAAAGP//8BAYMCSnB1YmxpY19rZXmCA1iFMIGCMB0GDSsGAQQBgtx8BQMBAgEGDCsGAQQBgtx8BQMCAQNhAKkibyL1MUdfYEy6XJin1b3PH7O2dsscd7G1feb0chACzBZOV+iXsL2AkjXe5cpqmwj6EZ3voSNz0aXeskewXOtFE4acjAp8pqDHx5EzUer76iqMIwraljfu94OFUhGBZIIEWCDGWnxJxMbpfrLKS1SIeWornghRMHDsKLoA4Ht6k/jqo4IEWCC8jzyQpYOJ/fqhwEmB2toxxu/hn7B9fcDMuS1/S/bYA4IEWCCI/qDbafOPnPP7qI+KBA88rcmud3L6GkBqbqRk+oWLnoIEWCBpYe8TfCruCwRnCC7208EsA+kwE7YCpMtiFCcOSEhj8YIEWCCbMGsgdP3vD+1UB+zEnG32tlisH7P9tl/aAiIVQIKEM4MCRHRpbWWCA0nEoN6ai4me+hdpc2lnbmF0dXJlWDCxK5IGNwccdX4Xs5HPctDb3AjfHV2QPScDneQJ7VFFOVN1+47TiJCYsFDPVSkKXVs=")
4047    }
4048
4049    fn build_response<'a>(
4050        status_code: StatusCode,
4051        body: Vec<u8>,
4052        cel_expr: String,
4053        headers: Vec<HeaderField>,
4054    ) -> HttpResponse<'a> {
4055        let combined_headers = headers
4056            .into_iter()
4057            .chain(vec![
4058                ("content-length".to_string(), body.len().to_string()),
4059                (CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(), cel_expr),
4060            ])
4061            .collect();
4062
4063        HttpResponse::builder()
4064            .with_status_code(status_code)
4065            .with_body(body)
4066            .with_headers(combined_headers)
4067            .build()
4068    }
4069
4070    fn extract_witness_expr_path(response: &HttpResponse) -> (HashTree, Vec<String>) {
4071        let (_, certificate_header_str) = response
4072            .headers()
4073            .iter()
4074            .find(|(name, _)| name.to_lowercase() == CERTIFICATE_HEADER_NAME.to_lowercase())
4075            .unwrap();
4076
4077        let certificate_header = CertificateHeader::from(certificate_header_str).unwrap();
4078        (
4079            certificate_header.tree,
4080            certificate_header.expr_path.unwrap(),
4081        )
4082    }
4083}