rama_http/service/fs/serve_dir/mod.rs
1use crate::dep::http_body::{self, Body as HttpBody};
2use crate::headers::encoding::{SupportedEncodings, parse_accept_encoding_headers};
3use crate::layer::set_status::SetStatus;
4use crate::{Body, HeaderValue, Method, Request, Response, StatusCode, header};
5use bytes::Bytes;
6use percent_encoding::percent_decode;
7use rama_core::error::{BoxError, OpaqueError};
8use rama_core::{Context, Service};
9use std::fmt;
10use std::str::FromStr;
11use std::{
12 convert::Infallible,
13 path::{Component, Path, PathBuf},
14};
15
16pub(crate) mod future;
17mod headers;
18mod open_file;
19
20#[cfg(test)]
21mod tests;
22
23// default capacity 64KiB
24const DEFAULT_CAPACITY: usize = 65536;
25
26/// Service that serves files from a given directory and all its sub directories.
27///
28/// The `Content-Type` will be guessed from the file extension.
29///
30/// An empty response with status `404 Not Found` will be returned if:
31///
32/// - The file doesn't exist
33/// - Any segment of the path contains `..`
34/// - Any segment of the path contains a backslash
35/// - On unix, any segment of the path referenced as directory is actually an
36/// existing file (`/file.html/something`)
37/// - We don't have necessary permissions to read the file
38#[derive(Clone, Debug)]
39pub struct ServeDir<F = DefaultServeDirFallback> {
40 base: PathBuf,
41 buf_chunk_size: usize,
42 precompressed_variants: Option<PrecompressedVariants>,
43 // This is used to specialise implementation for
44 // single files
45 variant: ServeVariant,
46 fallback: Option<F>,
47 call_fallback_on_method_not_allowed: bool,
48}
49
50impl ServeDir<DefaultServeDirFallback> {
51 /// Create a new [`ServeDir`].
52 pub fn new<P>(path: P) -> Self
53 where
54 P: AsRef<Path>,
55 {
56 let mut base = PathBuf::from(".");
57 base.push(path.as_ref());
58
59 Self {
60 base,
61 buf_chunk_size: DEFAULT_CAPACITY,
62 precompressed_variants: None,
63 variant: ServeVariant::Directory {
64 serve_mode: Default::default(),
65 },
66 fallback: None,
67 call_fallback_on_method_not_allowed: false,
68 }
69 }
70
71 pub(crate) fn new_single_file<P>(path: P, mime: HeaderValue) -> Self
72 where
73 P: AsRef<Path>,
74 {
75 Self {
76 base: path.as_ref().to_owned(),
77 buf_chunk_size: DEFAULT_CAPACITY,
78 precompressed_variants: None,
79 variant: ServeVariant::SingleFile { mime },
80 fallback: None,
81 call_fallback_on_method_not_allowed: false,
82 }
83 }
84}
85
86impl<F> ServeDir<F> {
87 /// Set the [`DirectoryServeMode`].
88 pub fn with_directory_serve_mode(mut self, mode: DirectoryServeMode) -> Self {
89 match &mut self.variant {
90 ServeVariant::Directory { serve_mode } => {
91 *serve_mode = mode;
92 self
93 }
94 ServeVariant::SingleFile { mime: _ } => self,
95 }
96 }
97
98 /// Set the [`DirectoryServeMode`].
99 pub fn set_directory_serve_mode(&mut self, mode: DirectoryServeMode) -> &mut Self {
100 match &mut self.variant {
101 ServeVariant::Directory { serve_mode } => {
102 *serve_mode = mode;
103 self
104 }
105 ServeVariant::SingleFile { mime: _ } => self,
106 }
107 }
108
109 /// Set a specific read buffer chunk size.
110 ///
111 /// The default capacity is 64kb.
112 pub fn with_buf_chunk_size(mut self, chunk_size: usize) -> Self {
113 self.buf_chunk_size = chunk_size;
114 self
115 }
116
117 /// Set a specific read buffer chunk size.
118 ///
119 /// The default capacity is 64kb.
120 pub fn set_buf_chunk_size(&mut self, chunk_size: usize) -> &mut Self {
121 self.buf_chunk_size = chunk_size;
122 self
123 }
124
125 /// Informs the service that it should also look for a precompressed gzip
126 /// version of _any_ file in the directory.
127 ///
128 /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
129 /// a client with an `Accept-Encoding` header that allows the gzip encoding
130 /// will receive the file `dir/foo.txt.gz` instead of `dir/foo.txt`.
131 /// If the precompressed file is not available, or the client doesn't support it,
132 /// the uncompressed version will be served instead.
133 /// Both the precompressed version and the uncompressed version are expected
134 /// to be present in the directory. Different precompressed variants can be combined.
135 pub fn precompressed_gzip(mut self) -> Self {
136 self.precompressed_variants
137 .get_or_insert(Default::default())
138 .gzip = true;
139 self
140 }
141
142 /// Informs the service that it should also look for a precompressed gzip
143 /// version of _any_ file in the directory.
144 ///
145 /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
146 /// a client with an `Accept-Encoding` header that allows the gzip encoding
147 /// will receive the file `dir/foo.txt.gz` instead of `dir/foo.txt`.
148 /// If the precompressed file is not available, or the client doesn't support it,
149 /// the uncompressed version will be served instead.
150 /// Both the precompressed version and the uncompressed version are expected
151 /// to be present in the directory. Different precompressed variants can be combined.
152 pub fn set_precompressed_gzip(&mut self) -> &mut Self {
153 self.precompressed_variants
154 .get_or_insert(Default::default())
155 .gzip = true;
156 self
157 }
158
159 /// Informs the service that it should also look for a precompressed brotli
160 /// version of _any_ file in the directory.
161 ///
162 /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
163 /// a client with an `Accept-Encoding` header that allows the brotli encoding
164 /// will receive the file `dir/foo.txt.br` instead of `dir/foo.txt`.
165 /// If the precompressed file is not available, or the client doesn't support it,
166 /// the uncompressed version will be served instead.
167 /// Both the precompressed version and the uncompressed version are expected
168 /// to be present in the directory. Different precompressed variants can be combined.
169 pub fn precompressed_br(mut self) -> Self {
170 self.precompressed_variants
171 .get_or_insert(Default::default())
172 .br = true;
173 self
174 }
175
176 /// Informs the service that it should also look for a precompressed brotli
177 /// version of _any_ file in the directory.
178 ///
179 /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
180 /// a client with an `Accept-Encoding` header that allows the brotli encoding
181 /// will receive the file `dir/foo.txt.br` instead of `dir/foo.txt`.
182 /// If the precompressed file is not available, or the client doesn't support it,
183 /// the uncompressed version will be served instead.
184 /// Both the precompressed version and the uncompressed version are expected
185 /// to be present in the directory. Different precompressed variants can be combined.
186 pub fn set_precompressed_br(&mut self) -> &mut Self {
187 self.precompressed_variants
188 .get_or_insert(Default::default())
189 .br = true;
190 self
191 }
192
193 /// Informs the service that it should also look for a precompressed deflate
194 /// version of _any_ file in the directory.
195 ///
196 /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
197 /// a client with an `Accept-Encoding` header that allows the deflate encoding
198 /// will receive the file `dir/foo.txt.zz` instead of `dir/foo.txt`.
199 /// If the precompressed file is not available, or the client doesn't support it,
200 /// the uncompressed version will be served instead.
201 /// Both the precompressed version and the uncompressed version are expected
202 /// to be present in the directory. Different precompressed variants can be combined.
203 pub fn precompressed_deflate(mut self) -> Self {
204 self.precompressed_variants
205 .get_or_insert(Default::default())
206 .deflate = true;
207 self
208 }
209
210 /// Informs the service that it should also look for a precompressed deflate
211 /// version of _any_ file in the directory.
212 ///
213 /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
214 /// a client with an `Accept-Encoding` header that allows the deflate encoding
215 /// will receive the file `dir/foo.txt.zz` instead of `dir/foo.txt`.
216 /// If the precompressed file is not available, or the client doesn't support it,
217 /// the uncompressed version will be served instead.
218 /// Both the precompressed version and the uncompressed version are expected
219 /// to be present in the directory. Different precompressed variants can be combined.
220 pub fn set_precompressed_deflate(&mut self) -> &mut Self {
221 self.precompressed_variants
222 .get_or_insert(Default::default())
223 .deflate = true;
224 self
225 }
226
227 /// Informs the service that it should also look for a precompressed zstd
228 /// version of _any_ file in the directory.
229 ///
230 /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
231 /// a client with an `Accept-Encoding` header that allows the zstd encoding
232 /// will receive the file `dir/foo.txt.zst` instead of `dir/foo.txt`.
233 /// If the precompressed file is not available, or the client doesn't support it,
234 /// the uncompressed version will be served instead.
235 /// Both the precompressed version and the uncompressed version are expected
236 /// to be present in the directory. Different precompressed variants can be combined.
237 pub fn precompressed_zstd(mut self) -> Self {
238 self.precompressed_variants
239 .get_or_insert(Default::default())
240 .zstd = true;
241 self
242 }
243
244 /// Informs the service that it should also look for a precompressed zstd
245 /// version of _any_ file in the directory.
246 ///
247 /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
248 /// a client with an `Accept-Encoding` header that allows the zstd encoding
249 /// will receive the file `dir/foo.txt.zst` instead of `dir/foo.txt`.
250 /// If the precompressed file is not available, or the client doesn't support it,
251 /// the uncompressed version will be served instead.
252 /// Both the precompressed version and the uncompressed version are expected
253 /// to be present in the directory. Different precompressed variants can be combined.
254 pub fn set_precompressed_zstd(&mut self) -> &mut Self {
255 self.precompressed_variants
256 .get_or_insert(Default::default())
257 .zstd = true;
258 self
259 }
260
261 /// Set the fallback service.
262 ///
263 /// This service will be called if there is no file at the path of the request.
264 ///
265 /// The status code returned by the fallback will not be altered. Use
266 /// [`ServeDir::not_found_service`] to set a fallback and always respond with `404 Not Found`.
267 ///
268 /// # Example
269 ///
270 /// This can be used to respond with a different file:
271 ///
272 /// ```rust,no_run
273 /// use rama_core::{
274 /// rt::Executor,
275 /// Layer, layer::TraceErrLayer,
276 /// };
277 /// use rama_tcp::server::TcpListener;
278 /// use rama_http_backend::server::HttpServer;
279 /// use rama_http::service::fs::{ServeDir, ServeFile};
280 ///
281 /// #[tokio::main]
282 /// async fn main() {
283 /// let exec = Executor::default();
284 ///
285 /// let listener = TcpListener::bind("127.0.0.1:8080")
286 /// .await
287 /// .expect("bind TCP Listener");
288 ///
289 /// // This will serve files in the "assets" directory and
290 /// // its subdirectories, and use assets/not_found.html as the fallback page
291 /// let fs_server = ServeDir::new("assets").fallback(ServeFile::new("assets/not_found.html"));
292 /// let http_fs_server = HttpServer::auto(exec).service(fs_server);
293 ///
294 /// // Serve the HTTP server over TCP
295 /// listener
296 /// .serve(TraceErrLayer::new().into_layer(http_fs_server))
297 /// .await;
298 /// }
299 /// ```
300 pub fn fallback<F2>(self, new_fallback: F2) -> ServeDir<F2> {
301 ServeDir {
302 base: self.base,
303 buf_chunk_size: self.buf_chunk_size,
304 precompressed_variants: self.precompressed_variants,
305 variant: self.variant,
306 fallback: Some(new_fallback),
307 call_fallback_on_method_not_allowed: self.call_fallback_on_method_not_allowed,
308 }
309 }
310
311 /// Set the fallback service and override the fallback's status code to `404 Not Found`.
312 ///
313 /// This service will be called if there is no file at the path of the request.
314 ///
315 /// # Example
316 ///
317 /// This can be used to respond with a different file:
318 ///
319 /// ```rust,no_run
320 /// use rama_core::{
321 /// rt::Executor,
322 /// layer::TraceErrLayer,
323 /// Layer,
324 /// };
325 /// use rama_tcp::server::TcpListener;
326 /// use rama_http_backend::server::HttpServer;
327 /// use rama_http::service::fs::{ServeDir, ServeFile};
328 ///
329 /// #[tokio::main]
330 /// async fn main() {
331 /// let exec = Executor::default();
332 ///
333 /// let listener = TcpListener::bind("127.0.0.1:8080")
334 /// .await
335 /// .expect("bind TCP Listener");
336 ///
337 /// // This will serve files in the "assets" directory and
338 /// // its subdirectories, and use assets/not_found.html as the not_found page
339 /// let fs_server = ServeDir::new("assets").not_found_service(ServeFile::new("assets/not_found.html"));
340 /// let http_fs_server = HttpServer::auto(exec).service(fs_server);
341 ///
342 /// // Serve the HTTP server over TCP
343 /// listener
344 /// .serve(TraceErrLayer::new().into_layer(http_fs_server))
345 /// .await;
346 /// }
347 /// ```
348 ///
349 /// Setups like this are often found in single page applications.
350 pub fn not_found_service<F2>(self, new_fallback: F2) -> ServeDir<SetStatus<F2>> {
351 self.fallback(SetStatus::new(new_fallback, StatusCode::NOT_FOUND))
352 }
353
354 /// Customize whether or not to call the fallback for requests that aren't `GET` or `HEAD`.
355 ///
356 /// Defaults to not calling the fallback and instead returning `405 Method Not Allowed`.
357 pub fn call_fallback_on_method_not_allowed(mut self, call_fallback: bool) -> Self {
358 self.call_fallback_on_method_not_allowed = call_fallback;
359 self
360 }
361
362 /// Customize whether or not to call the fallback for requests that aren't `GET` or `HEAD`.
363 ///
364 /// Defaults to not calling the fallback and instead returning `405 Method Not Allowed`.
365 pub fn set_call_fallback_on_method_not_allowed(&mut self, call_fallback: bool) -> &mut Self {
366 self.call_fallback_on_method_not_allowed = call_fallback;
367 self
368 }
369
370 /// Call the service and get a future that contains any `std::io::Error` that might have
371 /// happened.
372 ///
373 /// By default `<ServeDir as Service<_>>::call` will handle IO errors and convert them into
374 /// responses. It does that by converting [`std::io::ErrorKind::NotFound`] and
375 /// [`std::io::ErrorKind::PermissionDenied`] to `404 Not Found` and any other error to `500
376 /// Internal Server Error`. The error will also be logged with `tracing`.
377 ///
378 /// If you want to manually control how the error response is generated you can make a new
379 /// service that wraps a `ServeDir` and calls `try_call` instead of `call`.
380 ///
381 /// # Example
382 ///
383 /// ```rust,no_run
384 /// use rama_core::{
385 /// rt::Executor,
386 /// service::service_fn,
387 /// layer::TraceErrLayer,
388 /// Context, Layer,
389 /// };
390 /// use rama_tcp::server::TcpListener;
391 /// use rama_http_backend::server::HttpServer;
392 /// use rama_http::service::fs::ServeDir;
393 /// use rama_http::{Body, Request, Response, StatusCode};
394 /// use std::convert::Infallible;
395 ///
396 /// #[tokio::main]
397 /// async fn main() {
398 /// let exec = Executor::default();
399 ///
400 /// let listener = TcpListener::bind("127.0.0.1:8080")
401 /// .await
402 /// .expect("bind TCP Listener");
403 ///
404 /// // This will serve files in the "assets" directory and
405 /// // its subdirectories, and use assets/not_found.html as the fallback page
406 /// let http_fs_server = HttpServer::auto(exec).service(service_fn(serve_dir));
407 ///
408 /// // Serve the HTTP server over TCP
409 /// listener
410 /// .serve(TraceErrLayer::new().into_layer(http_fs_server))
411 /// .await;
412 /// }
413 ///
414 /// async fn serve_dir<State>(
415 /// ctx: Context<State>,
416 /// request: Request,
417 /// ) -> Result<Response<Body>, Infallible>
418 /// where
419 /// State: Clone + Send + Sync + 'static,
420 /// {
421 /// let service = ServeDir::new("assets");
422 ///
423 /// match service.try_call(ctx, request).await {
424 /// Ok(response) => Ok(response),
425 /// Err(_) => {
426 /// let body = Body::from("Something went wrong...");
427 /// let response = Response::builder()
428 /// .status(StatusCode::INTERNAL_SERVER_ERROR)
429 /// .body(body)
430 /// .unwrap();
431 /// Ok(response)
432 /// }
433 /// }
434 /// }
435 /// ```
436 pub async fn try_call<State, ReqBody, FResBody>(
437 &self,
438 ctx: Context<State>,
439 req: Request<ReqBody>,
440 ) -> Result<Response, std::io::Error>
441 where
442 State: Clone + Send + Sync + 'static,
443 F: Service<State, Request<ReqBody>, Response = Response<FResBody>, Error = Infallible>
444 + Clone,
445 FResBody: http_body::Body<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
446 {
447 if req.method() != Method::GET && req.method() != Method::HEAD {
448 if self.call_fallback_on_method_not_allowed {
449 if let Some(fallback) = self.fallback.as_ref() {
450 return future::serve_fallback(fallback, ctx, req).await;
451 }
452 } else {
453 return Ok(future::method_not_allowed());
454 }
455 }
456
457 // `ServeDir` doesn't care about the request body but the fallback might. So move out the
458 // body and pass it to the fallback, leaving an empty body in its place
459 //
460 // this is necessary because we cannot clone bodies
461 let (mut parts, body) = req.into_parts();
462 // same goes for extensions
463 let extensions = std::mem::take(&mut parts.extensions);
464 let req = Request::from_parts(parts, Body::empty());
465
466 let fallback_and_request = self.fallback.as_ref().map(|fallback| {
467 let mut fallback_req = Request::new(body);
468 *fallback_req.method_mut() = req.method().clone();
469 *fallback_req.uri_mut() = req.uri().clone();
470 *fallback_req.headers_mut() = req.headers().clone();
471 *fallback_req.extensions_mut() = extensions;
472
473 (fallback, ctx, fallback_req)
474 });
475
476 let path_to_file = match self
477 .variant
478 .build_and_validate_path(&self.base, req.uri().path())
479 {
480 Some(path_to_file) => path_to_file,
481 None => {
482 return if let Some((fallback, ctx, request)) = fallback_and_request {
483 future::serve_fallback(fallback, ctx, request).await
484 } else {
485 Ok(future::not_found())
486 };
487 }
488 };
489
490 let buf_chunk_size = self.buf_chunk_size;
491 let range_header = req
492 .headers()
493 .get(header::RANGE)
494 .and_then(|value| value.to_str().ok())
495 .map(|s| s.to_owned());
496
497 let negotiated_encodings: Vec<_> = parse_accept_encoding_headers(
498 req.headers(),
499 self.precompressed_variants.unwrap_or_default(),
500 )
501 .collect();
502
503 let variant = self.variant.clone();
504
505 let open_file_result = open_file::open_file(
506 variant,
507 path_to_file,
508 req,
509 negotiated_encodings,
510 range_header,
511 buf_chunk_size,
512 )
513 .await;
514
515 future::consume_open_file_result(open_file_result, fallback_and_request).await
516 }
517}
518
519impl<State, ReqBody, F, FResBody> Service<State, Request<ReqBody>> for ServeDir<F>
520where
521 State: Clone + Send + Sync + 'static,
522 ReqBody: Send + 'static,
523 F: Service<State, Request<ReqBody>, Response = Response<FResBody>, Error = Infallible> + Clone,
524 FResBody: HttpBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
525{
526 type Response = Response;
527 type Error = Infallible;
528
529 async fn serve(
530 &self,
531 ctx: Context<State>,
532 req: Request<ReqBody>,
533 ) -> Result<Self::Response, Self::Error> {
534 let result = self.try_call(ctx, req).await;
535 Ok(result.unwrap_or_else(|err| {
536 tracing::error!(error = %err, "Failed to read file");
537
538 let body = Body::empty();
539 Response::builder()
540 .status(StatusCode::INTERNAL_SERVER_ERROR)
541 .body(body)
542 .unwrap()
543 }))
544 }
545}
546
547#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
548pub enum DirectoryServeMode {
549 #[default]
550 /// If the requested path is a directory append `index.html`.
551 ///
552 /// This is useful for static sites
553 AppendIndexHtml,
554 /// If the requested path is a directory
555 /// handle it as resource "not found" (404).
556 NotFound,
557 /// Show the file tree of the directory as file tree
558 /// which can be navigated.
559 HtmlFileList,
560}
561
562impl fmt::Display for DirectoryServeMode {
563 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
564 write!(
565 f,
566 "{}",
567 match self {
568 DirectoryServeMode::AppendIndexHtml => "append-index",
569 DirectoryServeMode::NotFound => "not-found",
570 DirectoryServeMode::HtmlFileList => "html-file-list",
571 }
572 )
573 }
574}
575
576impl FromStr for DirectoryServeMode {
577 type Err = OpaqueError;
578
579 fn from_str(s: &str) -> Result<Self, Self::Err> {
580 rama_utils::macros::match_ignore_ascii_case_str! {
581 match(s) {
582 "append-index" | "append_index" => Ok(Self::AppendIndexHtml),
583 "not-found" | "not_found" => Ok(Self::NotFound),
584 "html-file-list" | "html_file_list" => Ok(Self::HtmlFileList),
585 _ => Err(OpaqueError::from_display("invalid DirectoryServeMode str")),
586 }
587 }
588 }
589}
590
591impl TryFrom<&str> for DirectoryServeMode {
592 type Error = OpaqueError;
593
594 #[inline]
595 fn try_from(value: &str) -> Result<Self, Self::Error> {
596 value.parse()
597 }
598}
599
600// Allow the ServeDir service to be used in the ServeFile service
601// with almost no overhead
602#[derive(Clone, Debug)]
603enum ServeVariant {
604 Directory { serve_mode: DirectoryServeMode },
605 SingleFile { mime: HeaderValue },
606}
607
608impl ServeVariant {
609 fn build_and_validate_path(&self, base_path: &Path, requested_path: &str) -> Option<PathBuf> {
610 match self {
611 ServeVariant::Directory { serve_mode: _ } => {
612 let path = requested_path.trim_start_matches('/');
613
614 let path_decoded = percent_decode(path.as_ref()).decode_utf8().ok()?;
615 let path_decoded = Path::new(&*path_decoded);
616
617 let mut path_to_file = base_path.to_path_buf();
618 for component in path_decoded.components() {
619 match component {
620 Component::Normal(comp) => {
621 // protect against paths like `/foo/c:/bar/baz` (#204)
622 if Path::new(&comp)
623 .components()
624 .all(|c| matches!(c, Component::Normal(_)))
625 {
626 path_to_file.push(comp)
627 } else {
628 return None;
629 }
630 }
631 Component::CurDir => {}
632 Component::Prefix(_) | Component::RootDir | Component::ParentDir => {
633 return None;
634 }
635 }
636 }
637 Some(path_to_file)
638 }
639 ServeVariant::SingleFile { mime: _ } => Some(base_path.to_path_buf()),
640 }
641 }
642}
643
644/// The default fallback service used with [`ServeDir`].
645#[derive(Debug, Clone, Copy)]
646pub struct DefaultServeDirFallback(Infallible);
647
648impl<State, ReqBody> Service<State, Request<ReqBody>> for DefaultServeDirFallback
649where
650 State: Clone + Send + Sync + 'static,
651 ReqBody: Send + 'static,
652{
653 type Response = Response;
654 type Error = Infallible;
655
656 async fn serve(
657 &self,
658 _ctx: Context<State>,
659 _req: Request<ReqBody>,
660 ) -> Result<Self::Response, Self::Error> {
661 match self.0 {}
662 }
663}
664
665#[derive(Clone, Copy, Debug, Default)]
666struct PrecompressedVariants {
667 gzip: bool,
668 deflate: bool,
669 br: bool,
670 zstd: bool,
671}
672
673impl SupportedEncodings for PrecompressedVariants {
674 fn gzip(&self) -> bool {
675 self.gzip
676 }
677
678 fn deflate(&self) -> bool {
679 self.deflate
680 }
681
682 fn br(&self) -> bool {
683 self.br
684 }
685
686 fn zstd(&self) -> bool {
687 self.zstd
688 }
689}