index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2022-10-11 23:27:16.0 +00:00:00
committer GitHub <noreply@github.com> 2022-10-11 23:27:16.0 +00:00:00
commit
997e49342478ffde2c96570f76c4fc8b458ed62a [patch]
tree
a656c7248f5db2a600680d17879033e2ce1c0db4
parent
3c863fdceed386969e22cf5080699c4218b55709
download
997e49342478ffde2c96570f76c4fc8b458ed62a.tar.gz

feat: directory listing format support (#151)

option: `--directory-listing-format`
formats supported: `html`, `json`
default: `html`

resolves #128

Diff

 .github/workflows/devel.yml |   2 +-
 Cargo.lock                  |  18 +++-
 Cargo.toml                  |   1 +-
 src/directory_listing.rs    | 273 ++++++++++++++++++++++++++++++++-------------
 src/handler.rs              |   8 +-
 src/server.rs               |   5 +-
 src/settings/cli.rs         |  13 ++-
 src/settings/file.rs        |   2 +-
 src/settings/mod.rs         |   5 +-
 src/static_files.rs         |   3 +-
 tests/compression_static.rs |   7 +-
 tests/dir_listing.rs        |  73 +++++++++++-
 tests/static_files.rs       |  28 +++++-
 tests/toml/config.toml      |  10 +-
 14 files changed, 364 insertions(+), 84 deletions(-)

diff --git a/.github/workflows/devel.yml b/.github/workflows/devel.yml
index d4b6f8d..4825534 100644
--- a/.github/workflows/devel.yml
+++ b/.github/workflows/devel.yml
@@ -136,7 +136,7 @@ jobs:
        echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV

    - name: Cache cargo registry and git trees
      uses: Swatinem/rust-cache@v1
      uses: Swatinem/rust-cache@v2

    - name: Show command used for Cargo
      run: |
diff --git a/Cargo.lock b/Cargo.lock
index 3f99e10..230c01a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -874,6 +874,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"

[[package]]
name = "ryu"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"

[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -919,6 +925,17 @@ dependencies = [
]

[[package]]
name = "serde_json"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074"
dependencies = [
 "itoa",
 "ryu",
 "serde",
]

[[package]]
name = "serde_repr"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1037,6 +1054,7 @@ dependencies = [
 "rustls-pemfile",
 "serde",
 "serde_ignored",
 "serde_json",
 "serde_repr",
 "signal-hook",
 "signal-hook-tokio",
diff --git a/Cargo.toml b/Cargo.toml
index 0fe0235..3ff5f3a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -71,6 +71,7 @@ windows-sys = { version = "0.36.1", features = [ "Win32_Foundation", "Win32_Netw

[dev-dependencies]
bytes = "1.1"
serde_json = "1.0"

[profile.release]
codegen-units = 1
diff --git a/src/directory_listing.rs b/src/directory_listing.rs
index 188a35b..c9ab5bb 100644
--- a/src/directory_listing.rs
+++ b/src/directory_listing.rs
@@ -11,11 +11,22 @@ use std::future::Future;
use std::io;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use structopt::clap::arg_enum;

use crate::Result;

arg_enum! {
    #[derive(Debug, Serialize, Deserialize, Clone)]
    #[serde(rename_all = "lowercase")]
    /// Directory listing output format for file entries.
    pub enum DirListFmt {
        Html,
        Json,
    }
}

/// Provides directory listing support for the current request.
/// Note that this function highly depends on `static_files::get_composed_metadata()` function
/// Note that this function highly depends on `static_files::composed_file_metadata()` function
/// which must be called first. See `static_files::handle()` for more details.
pub fn auto_index<'a>(
    method: &'a Method,
@@ -23,20 +34,28 @@ pub fn auto_index<'a>(
    uri_query: Option<&'a str>,
    filepath: &'a Path,
    dir_listing_order: u8,
    dir_listing_format: &'a DirListFmt,
) -> impl Future<Output = Result<Response<Body>, StatusCode>> + Send + 'a {
    let is_head = method == Method::HEAD;

    // Note: it's safe to call `parent()` here since `filepath`
    // value always refer to a path with file ending and under
    // a root directory boundary.
    // See `get_composed_metadata()` function which sanitizes the requested
    // See `composed_file_metadata()` function which sanitizes the requested
    // path before to be delegated here.
    let parent = filepath.parent().unwrap_or(filepath);

    tokio::fs::read_dir(parent).then(move |res| match res {
        Ok(entries) => Either::Left(async move {
            match read_dir_entries(entries, current_path, uri_query, is_head, dir_listing_order)
                .await
        Ok(dir_reader) => Either::Left(async move {
            match read_dir_entries(
                dir_reader,
                current_path,
                uri_query,
                is_head,
                dir_listing_order,
                dir_listing_format,
            )
            .await
            {
                Ok(resp) => Ok(resp),
                Err(err) => {
@@ -76,23 +95,43 @@ pub fn auto_index<'a>(
const STYLE: &str = r#"<style>html{background-color:#fff;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;min-width:20rem;text-rendering:optimizeLegibility;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%}body{padding:1rem;font-family:Consolas,'Liberation Mono',Menlo,monospace;font-size:.875rem;max-width:70rem;margin:0 auto;color:#4a4a4a;font-weight:400;line-height:1.5}h1{margin:0;padding:0;font-size:1.375rem;line-height:1.25;margin-bottom:0.5rem;}table{width:100%;border-spacing: 0;}table th,table td{padding:.2rem .5rem;white-space:nowrap;vertical-align:top}table th a,table td a{display:inline-block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:95%;vertical-align:top}table tr:hover td{background-color:#f5f5f5}footer{padding-top:0.5rem}table tr th{text-align:left;}</style>"#;
const FOOTER: &str = r#"<footer>Powered by <a target="_blank" href="https://github.com/joseluisq/static-web-server">static-web-server</a> | MIT &amp; Apache 2.0</footer>"#;

const DATETIME_FORMAT_UTC: &str = "%FT%TZ";
const DATETIME_FORMAT_LOCAL: &str = "%F %T";

/// Defines a file entry and its properties.
struct FileEntry {
    name: String,
    name_encoded: String,
    modified: Option<DateTime<Local>>,
    filesize: u64,
    uri: Option<String>,
}

/// Defines sorting attributes for file entries.
struct SortingAttr<'a> {
    name: &'a str,
    last_modified: &'a str,
    size: &'a str,
}

/// It reads a list of directory entries and create an index page content.
/// Otherwise it returns a status error.
async fn read_dir_entries(
    mut file_entries: tokio::fs::ReadDir,
    mut dir_reader: tokio::fs::ReadDir,
    base_path: &str,
    uri_query: Option<&str>,
    is_head: bool,
    mut dir_listing_order: u8,
    mut order_code: u8,
    content_format: &DirListFmt,
) -> Result<Response<Body>> {
    let mut dirs_count: usize = 0;
    let mut files_count: usize = 0;
    let mut files_found: Vec<(String, String, u64, Option<String>)> = vec![];
    let mut file_entries: Vec<FileEntry> = vec![];

    while let Some(entry) = file_entries.next_entry().await? {
        let meta = entry.metadata().await?;
    while let Some(dir_entry) = dir_reader.next_entry().await? {
        let meta = dir_entry.metadata().await?;

        let name = entry
        let name = dir_entry
            .file_name()
            .into_string()
            .map_err(|err| anyhow::anyhow!(err.into_string().unwrap_or_default()))?;
@@ -107,7 +146,7 @@ async fn read_dir_entries(
            filesize = meta.len();
            files_count += 1;
        } else if meta.file_type().is_symlink() {
            let m = tokio::fs::symlink_metadata(entry.path().canonicalize()?).await?;
            let m = tokio::fs::symlink_metadata(dir_entry.path().canonicalize()?).await?;
            if m.is_dir() {
                name_encoded += "/";
                dirs_count += 1;
@@ -134,6 +173,7 @@ async fn read_dir_entries(
            if base_path != parent_dir {
                base_dir = base_path.strip_prefix(parent_dir)?;
            }

            let mut base_str = String::new();
            if !base_dir.starts_with("/") {
                let base_dir = base_dir.to_str().unwrap_or_default();
@@ -142,18 +182,25 @@ async fn read_dir_entries(
                }
                base_str.push('/');
            }

            base_str.push_str(&name_encoded);
            uri = Some(base_str);
        }

        let modified = match parse_last_modified(meta.modified()?) {
            Ok(local_dt) => local_dt.format("%F %T").to_string(),
            Ok(local_dt) => Some(local_dt),
            Err(err) => {
                tracing::error!("error determining file last modified: {:?}", err);
                String::from("-")
                None
            }
        };
        files_found.push((name_encoded, modified, filesize, uri));
        file_entries.push(FileEntry {
            name,
            name_encoded,
            modified,
            filesize,
            uri,
        });
    }

    // Check the query request uri for a sorting type. E.g https://blah/?sort=5
@@ -164,7 +211,7 @@ async fn read_dir_entries(
            if let Some(sort) = parts.next() {
                if sort.0 == "sort" && !sort.1.trim().is_empty() {
                    match sort.1.parse::<u8>() {
                        Ok(order_code) => dir_listing_order = order_code,
                        Ok(code) => order_code = code,
                        Err(err) => {
                            tracing::debug!(
                                "sorting: query value error when converting to u8: {:?}",
@@ -177,63 +224,145 @@ async fn read_dir_entries(
        }
    }

    let html = create_auto_index(
        base_path,
        dirs_count,
        files_count,
        dir_listing_order,
        &mut files_found,
    )?;

    let mut resp = Response::new(Body::empty());

    // Handle directory listing content format
    let content = match content_format {
        DirListFmt::Json => {
            // JSON
            resp.headers_mut()
                .typed_insert(ContentType::from(mime::APPLICATION_JSON));

            json_auto_index(&mut file_entries, order_code)?
        }
        // HTML (default)
        _ => {
            resp.headers_mut()
                .typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8));

            html_auto_index(
                base_path,
                dirs_count,
                files_count,
                &mut file_entries,
                order_code,
            )?
        }
    };

    resp.headers_mut()
        .typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8));
    resp.headers_mut()
        .typed_insert(ContentLength(html.len() as u64));
        .typed_insert(ContentLength(content.len() as u64));

    // We skip the body for HEAD requests
    if is_head {
        return Ok(resp);
    }

    *resp.body_mut() = Body::from(html);
    *resp.body_mut() = Body::from(content);

    Ok(resp)
}

/// Create an auto index html content.
fn create_auto_index(
    base_path: &str,
/// Create an auto index in JSON format.
fn json_auto_index(entries: &mut [FileEntry], order_code: u8) -> Result<String> {
    sort_file_entries(entries, order_code);

    let mut json = String::from('[');

    for entry in entries {
        let file_size = &entry.filesize;
        let file_name = &entry.name;
        let is_empty = *file_size == 0_u64;
        let file_type = if is_empty { "directory" } else { "file" };
        let file_modified = &entry.modified;

        json.push('{');
        json.push_str(format!("\"name\":{},", json_quote_str(file_name.as_str())).as_str());
        json.push_str(format!("\"type\":\"{}\",", file_type).as_str());

        let file_modified_str = file_modified.map_or("".to_owned(), |local_dt| {
            local_dt
                .with_timezone(&Utc)
                .format(DATETIME_FORMAT_UTC)
                .to_string()
        });
        json.push_str(format!("\"mtime\":\"{}\"", file_modified_str).as_str());

        if !is_empty {
            json.push_str(format!(",\"size\":{}", file_size).as_str());
        }
        json.push_str("},");
    }

    json.pop();
    json.push(']');

    Ok(json)
}

/// Quotes a string value.
fn json_quote_str(s: &str) -> String {
    let mut r = String::from("\"");
    for c in s.chars() {
        match c {
            '\\' => r.push_str("\\\\"),
            '\u{0008}' => r.push_str("\\b"),
            '\u{000c}' => r.push_str("\\f"),
            '\n' => r.push_str("\\n"),
            '\r' => r.push_str("\\r"),
            '\t' => r.push_str("\\t"),
            '"' => r.push_str("\\\""),
            c if c.is_control() => r.push_str(format!("\\u{:04x}", c as u32).as_str()),
            c => r.push(c),
        };
    }
    r.push('\"');
    r
}

/// Create an auto index in HTML format.
fn html_auto_index<'a>(
    base_path: &'a str,
    dirs_count: usize,
    files_count: usize,
    dir_listing_order: u8,
    files_found: &mut Vec<(String, String, u64, Option<String>)>,
    entries: &'a mut [FileEntry],
    order_code: u8,
) -> Result<String> {
    // Sorting the files by an specific order code and create the table header
    let table_header = create_table_header(sort_files(files_found, dir_listing_order));
    let sort_attrs = sort_file_entries(entries, order_code);

    // Prepare table row
    // Create the table header specifying every order code column
    let table_header = format!(
        r#"<thead><tr><th><a href="?sort={}">Name</a></th><th style="width:160px;"><a href="?sort={}">Last modified</a></th><th style="width:120px;text-align:right;"><a href="?sort={}">Size</a></th></tr></thead>"#,
        sort_attrs.name, sort_attrs.last_modified, sort_attrs.size,
    );

    // Prepare table row template
    let mut table_row = String::new();
    if base_path != "/" {
        table_row = String::from(r#"<tr><td colspan="3"><a href="../">../</a></td></tr>"#);
    }

    for file in files_found {
        let (file_name, file_modified, file_size, uri) = file;
        let mut filesize_str = file_size
    for entry in entries {
        let file_name = &entry.name_encoded;
        let file_modified = &entry.modified;
        let file_uri = &entry.uri.clone().unwrap_or_else(|| file_name.to_owned());
        let file_name_decoded = percent_decode_str(file_name).decode_utf8()?.to_string();
        let mut filesize = entry
            .filesize
            .file_size(file_size_opts::DECIMAL)
            .map_err(anyhow::Error::msg)?;

        if *file_size == 0_u64 {
            filesize_str = String::from("-");
        if entry.filesize == 0_u64 {
            filesize = String::from("-");
        }

        let file_uri = uri.clone().unwrap_or_else(|| file_name.to_owned());
        let file_name_decoded = percent_decode_str(file_name).decode_utf8()?.to_string();
        let file_modified_str = file_modified.map_or("-".to_owned(), |local_dt| {
            local_dt.format(DATETIME_FORMAT_LOCAL).to_string()
        });

        table_row = format!(
            "{}<tr><td><a href=\"{}\">{}</a></td><td>{}</td><td align=\"right\">{}</td></tr>",
            table_row, file_uri, file_name_decoded, file_modified, filesize_str
            table_row, file_uri, file_name_decoded, file_modified_str, filesize
        );
    }

@@ -256,66 +385,56 @@ fn create_auto_index(
    Ok(html_page)
}

/// Create a table header providing the sorting attributes.
fn create_table_header(sorting_attrs: (String, String, String)) -> String {
    let (name, last_modified, size) = sorting_attrs;
    format!(
        r#"<thead><tr><th><a href="?sort={}">Name</a></th><th style="width:160px;"><a href="?sort={}">Last modified</a></th><th style="width:120px;text-align:right;"><a href="?sort={}">Size</a></th></tr></thead>"#,
        name, last_modified, size,
    )
}

/// Sort a list of files by an specific order code.
fn sort_files(
    files: &mut [(String, String, u64, Option<String>)],
    order_code: u8,
) -> (String, String, String) {
/// Sort a list of file entries by a specific order code.
fn sort_file_entries(files: &mut [FileEntry], order_code: u8) -> SortingAttr<'_> {
    // Default sorting type values
    let mut name = "0".to_owned();
    let mut last_modified = "2".to_owned();
    let mut size = "4".to_owned();
    let mut name = "0";
    let mut last_modified = "2";
    let mut size = "4";

    files.sort_by(|a, b| match order_code {
        // Name (asc, desc)
        0 => {
            name = "1".to_owned();
            a.0.to_lowercase().cmp(&b.0.to_lowercase())
            name = "1";
            a.name.to_lowercase().cmp(&b.name.to_lowercase())
        }
        1 => {
            name = "0".to_owned();
            b.0.to_lowercase().cmp(&a.0.to_lowercase())
            name = "0";
            b.name.to_lowercase().cmp(&a.name.to_lowercase())
        }

        // Modified (asc, desc)
        2 => {
            last_modified = "3".to_owned();
            a.1.cmp(&b.1)
            last_modified = "3";
            a.modified.cmp(&b.modified)
        }
        3 => {
            last_modified = "2".to_owned();
            b.1.cmp(&a.1)
            last_modified = "2";
            b.modified.cmp(&a.modified)
        }

        // File size (asc, desc)
        4 => {
            size = "5".to_owned();
            a.2.cmp(&b.2)
            size = "5";
            a.filesize.cmp(&b.filesize)
        }
        5 => {
            size = "4".to_owned();
            b.2.cmp(&a.2)
            size = "4";
            b.filesize.cmp(&a.filesize)
        }

        // Unordered
        _ => Ordering::Equal,
    });

    (name, last_modified, size)
    SortingAttr {
        name,
        last_modified,
        size,
    }
}

fn parse_last_modified(
    modified: SystemTime,
) -> Result<DateTime<Local>, Box<dyn std::error::Error>> {
fn parse_last_modified(modified: SystemTime) -> Result<DateTime<Local>> {
    let since_epoch = modified.duration_since(UNIX_EPOCH)?;
    // HTTP times don't have nanosecond precision, so we truncate
    // the modification time.
diff --git a/src/handler.rs b/src/handler.rs
index ec1ee08..6f5f9bd 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -3,8 +3,9 @@ use hyper::{header::WWW_AUTHENTICATE, Body, Method, Request, Response, StatusCod
use std::{future::Future, net::IpAddr, net::SocketAddr, path::PathBuf, sync::Arc};

use crate::{
    basic_auth, compression, control_headers, cors, custom_headers, error_page, fallback_page,
    redirects, rewrites, security_headers,
    basic_auth, compression, control_headers, cors, custom_headers,
    directory_listing::DirListFmt,
    error_page, fallback_page, redirects, rewrites, security_headers,
    settings::Advanced,
    static_files::{self, HandleOpts},
    Error, Result,
@@ -18,6 +19,7 @@ pub struct RequestHandlerOpts {
    pub compression_static: bool,
    pub dir_listing: bool,
    pub dir_listing_order: u8,
    pub dir_listing_format: DirListFmt,
    pub cors: Option<cors::Configured>,
    pub security_headers: bool,
    pub cache_control_headers: bool,
@@ -53,6 +55,7 @@ impl RequestHandler {
        let uri_query = uri.query();
        let dir_listing = self.opts.dir_listing;
        let dir_listing_order = self.opts.dir_listing_order;
        let dir_listing_format = &self.opts.dir_listing_format;
        let log_remote_addr = self.opts.log_remote_address;
        let redirect_trailing_slash = self.opts.redirect_trailing_slash;
        let compression_static = self.opts.compression_static;
@@ -190,6 +193,7 @@ impl RequestHandler {
                uri_query,
                dir_listing,
                dir_listing_order,
                dir_listing_format,
                redirect_trailing_slash,
                compression_static,
            })
diff --git a/src/server.rs b/src/server.rs
index cd3a608..2d931be 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -150,6 +150,10 @@ impl Server {
        let dir_listing_order = general.directory_listing_order;
        tracing::info!("directory listing order code: {}", dir_listing_order);

        // Directory listing format
        let dir_listing_format = general.directory_listing_format;
        tracing::info!("directory listing format: {}", dir_listing_format);

        // Cache control headers option
        let cache_control_headers = general.cache_control_headers;
        tracing::info!("cache control headers: enabled={}", cache_control_headers);
@@ -191,6 +195,7 @@ impl Server {
                compression_static,
                dir_listing,
                dir_listing_order,
                dir_listing_format,
                cors,
                security_headers,
                cache_control_headers,
diff --git a/src/settings/cli.rs b/src/settings/cli.rs
index 2614ec5..5c9ae26 100644
--- a/src/settings/cli.rs
+++ b/src/settings/cli.rs
@@ -3,6 +3,8 @@
use std::path::PathBuf;
use structopt::StructOpt;

use crate::directory_listing::DirListFmt;

/// General server configuration available in CLI and config file options.
#[derive(Debug, StructOpt)]
#[structopt(about, author)]
@@ -155,6 +157,17 @@ pub struct General {

    #[structopt(
        long,
        required_if("directory_listing", "true"),
        possible_values = &DirListFmt::variants(),
        default_value = "html",
        env = "SERVER_DIRECTORY_LISTING_FORMAT",
        case_insensitive = true
    )]
    /// Specify a content format for directory listing entries. Formats supported: "html" or "json". Default "html".
    pub directory_listing_format: DirListFmt,

    #[structopt(
        long,
        parse(try_from_str),
        required_if("http2", "true"),
        default_value_if("http2", Some("true"), "true"),
diff --git a/src/settings/file.rs b/src/settings/file.rs
index d65f370..e2ecc60 100644
--- a/src/settings/file.rs
+++ b/src/settings/file.rs
@@ -6,6 +6,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
use std::path::Path;
use std::{collections::BTreeSet, path::PathBuf};

use crate::directory_listing::DirListFmt;
use crate::{helpers, Context, Result};

#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -116,6 +117,7 @@ pub struct General {
    // Directory listing
    pub directory_listing: Option<bool>,
    pub directory_listing_order: Option<u8>,
    pub directory_listing_format: Option<DirListFmt>,

    // Basich Authentication
    pub basic_auth: Option<String>,
diff --git a/src/settings/mod.rs b/src/settings/mod.rs
index c61dcd6..39ae749 100644
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -79,6 +79,7 @@ impl Settings {
        let mut cors_expose_headers = opts.cors_expose_headers;
        let mut directory_listing = opts.directory_listing;
        let mut directory_listing_order = opts.directory_listing_order;
        let mut directory_listing_format = opts.directory_listing_format;
        let mut basic_auth = opts.basic_auth;
        let mut fd = opts.fd;
        let mut threads_multiplier = opts.threads_multiplier;
@@ -165,6 +166,9 @@ impl Settings {
                    if let Some(v) = general.directory_listing_order {
                        directory_listing_order = v
                    }
                    if let Some(v) = general.directory_listing_format {
                        directory_listing_format = v
                    }
                    if let Some(ref v) = general.basic_auth {
                        basic_auth = v.to_owned()
                    }
@@ -308,6 +312,7 @@ impl Settings {
                cors_expose_headers,
                directory_listing,
                directory_listing_order,
                directory_listing_format,
                basic_auth,
                fd,
                threads_multiplier,
diff --git a/src/static_files.rs b/src/static_files.rs
index b6c65dd..80f126d 100644
--- a/src/static_files.rs
+++ b/src/static_files.rs
@@ -23,6 +23,7 @@ use tokio::fs::File as TkFile;
use tokio::io::AsyncSeekExt;
use tokio_util::io::poll_read_buf;

use crate::directory_listing::DirListFmt;
use crate::{compression_static, directory_listing, Result};

/// Defines all options needed by the static-files handler.
@@ -34,6 +35,7 @@ pub struct HandleOpts<'a> {
    pub uri_query: Option<&'a str>,
    pub dir_listing: bool,
    pub dir_listing_order: u8,
    pub dir_listing_format: &'a DirListFmt,
    pub redirect_trailing_slash: bool,
    pub compression_static: bool,
}
@@ -108,6 +110,7 @@ pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<(Response<Body>, bool),
            opts.uri_query,
            file_path.as_ref(),
            opts.dir_listing_order,
            opts.dir_listing_format,
        )
        .await?;

diff --git a/tests/compression_static.rs b/tests/compression_static.rs
index cebc38c..c697c6f 100644
--- a/tests/compression_static.rs
+++ b/tests/compression_static.rs
@@ -10,7 +10,10 @@ mod tests {
    use http::Method;
    use std::path::PathBuf;

    use static_web_server::static_files::{self, HandleOpts};
    use static_web_server::{
        directory_listing::DirListFmt,
        static_files::{self, HandleOpts},
    };

    fn public_dir() -> PathBuf {
        PathBuf::from("docker/public/")
@@ -37,6 +40,7 @@ mod tests {
            uri_query: None,
            dir_listing: false,
            dir_listing_order: 6,
            dir_listing_format: &DirListFmt::Html,
            redirect_trailing_slash: true,
            compression_static: true,
        })
@@ -89,6 +93,7 @@ mod tests {
            uri_query: None,
            dir_listing: false,
            dir_listing_order: 6,
            dir_listing_format: &DirListFmt::Html,
            redirect_trailing_slash: true,
            compression_static: true,
        })
diff --git a/tests/dir_listing.rs b/tests/dir_listing.rs
index d70b345..a27fd33 100644
--- a/tests/dir_listing.rs
+++ b/tests/dir_listing.rs
@@ -7,9 +7,13 @@
mod tests {
    use headers::HeaderMap;
    use http::{Method, StatusCode};
    use serde::{Deserialize, Serialize};
    use std::path::{Path, PathBuf};

    use static_web_server::static_files::{self, HandleOpts};
    use static_web_server::{
        directory_listing::DirListFmt,
        static_files::{self, HandleOpts},
    };

    const METHODS: [Method; 8] = [
        Method::CONNECT,
@@ -40,6 +44,7 @@ mod tests {
                uri_query: None,
                dir_listing: true,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -68,6 +73,7 @@ mod tests {
                uri_query: None,
                dir_listing: true,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -106,6 +112,7 @@ mod tests {
                uri_query: None,
                dir_listing: true,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: false,
                compression_static: false,
            })
@@ -144,6 +151,7 @@ mod tests {
                uri_query: None,
                dir_listing: true,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: false,
                compression_static: false,
            })
@@ -172,6 +180,7 @@ mod tests {
                uri_query: None,
                dir_listing: true,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -200,4 +209,66 @@ mod tests {
            }
        }
    }

    #[tokio::test]
    async fn dir_listing_json_format() {
        #[derive(Serialize, Deserialize)]
        struct FileEntry {
            name: String,
            #[serde(rename = "type")]
            typed: String,
            mtime: String,
            size: Option<usize>,
        }

        for method in METHODS {
            match static_files::handle(&HandleOpts {
                method: &method,
                headers: &HeaderMap::new(),
                base_path: &root_dir("tests/fixtures/public/"),
                uri_path: "/",
                uri_query: None,
                dir_listing: true,
                dir_listing_order: 1,
                dir_listing_format: &DirListFmt::Json,
                redirect_trailing_slash: true,
                compression_static: false,
            })
            .await
            {
                Ok((mut res, _)) => {
                    assert_eq!(res.status(), 200);
                    assert_eq!(res.headers()["content-type"], "application/json");

                    let body = hyper::body::to_bytes(res.body_mut())
                        .await
                        .expect("unexpected bytes error during `body` conversion");
                    let body_str = std::str::from_utf8(&body).unwrap();

                    if method == Method::GET {
                        let entries: Vec<FileEntry> = serde_json::from_str(body_str).unwrap();
                        assert_eq!(entries.len(), 2);

                        let first_entry = entries.first().unwrap();
                        assert_eq!(first_entry.name, "spécial directöry");
                        assert_eq!(first_entry.typed, "directory");
                        assert_eq!(first_entry.mtime.is_empty(), false);
                        assert!(first_entry.size.is_none());

                        let last_entry = entries.last().unwrap();
                        assert_eq!(last_entry.name, "index.html.gz");
                        assert_eq!(last_entry.typed, "file");
                        assert_eq!(last_entry.mtime.is_empty(), false);
                        assert!(last_entry.size.unwrap() > 300);
                    } else {
                        assert!(body_str.is_empty());
                    }
                }
                Err(status) => {
                    assert!(method != Method::GET && method != Method::HEAD);
                    assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
                }
            }
        }
    }
}
diff --git a/tests/static_files.rs b/tests/static_files.rs
index 9908119..725f9ee 100644
--- a/tests/static_files.rs
+++ b/tests/static_files.rs
@@ -13,6 +13,7 @@ mod tests {

    use static_web_server::{
        compression,
        directory_listing::DirListFmt,
        static_files::{self, HandleOpts},
    };

@@ -30,6 +31,7 @@ mod tests {
            uri_query: None,
            dir_listing: false,
            dir_listing_order: 6,
            dir_listing_format: &DirListFmt::Html,
            redirect_trailing_slash: true,
            compression_static: false,
        })
@@ -70,6 +72,7 @@ mod tests {
            uri_query: None,
            dir_listing: false,
            dir_listing_order: 6,
            dir_listing_format: &DirListFmt::Html,
            redirect_trailing_slash: true,
            compression_static: false,
        })
@@ -111,6 +114,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -136,6 +140,7 @@ mod tests {
            uri_query: None,
            dir_listing: false,
            dir_listing_order: 0,
            dir_listing_format: &DirListFmt::Html,
            redirect_trailing_slash: true,
            compression_static: false,
        })
@@ -162,6 +167,7 @@ mod tests {
            uri_query: None,
            dir_listing: false,
            dir_listing_order: 0,
            dir_listing_format: &DirListFmt::Html,
            redirect_trailing_slash: true,
            compression_static: false,
        })
@@ -187,6 +193,7 @@ mod tests {
            uri_query: None,
            dir_listing: false,
            dir_listing_order: 0,
            dir_listing_format: &DirListFmt::Html,
            redirect_trailing_slash: false,
            compression_static: false,
        })
@@ -217,6 +224,7 @@ mod tests {
                    uri_query: None,
                    dir_listing: false,
                    dir_listing_order: 6,
                    dir_listing_format: &DirListFmt::Html,
                    redirect_trailing_slash: true,
                    compression_static: false,
                })
@@ -262,6 +270,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -289,6 +298,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -319,6 +329,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -349,6 +360,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -382,6 +394,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -413,6 +426,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -442,6 +456,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -470,6 +485,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -512,6 +528,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -571,6 +588,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -633,6 +651,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -675,6 +694,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -718,6 +738,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -753,6 +774,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -798,6 +820,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -840,6 +863,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -885,6 +909,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -928,6 +953,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -966,6 +992,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
@@ -1015,6 +1042,7 @@ mod tests {
                uri_query: None,
                dir_listing: false,
                dir_listing_order: 6,
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: false,
            })
diff --git a/tests/toml/config.toml b/tests/toml/config.toml
index d096246..eafaa3e 100644
--- a/tests/toml/config.toml
+++ b/tests/toml/config.toml
@@ -3,7 +3,7 @@
#### Address & Root dir
host = "::"
port = 8787
root = "docker/public"
root = "tests/fixtures/public"

#### Logging
log-level = "trace"
@@ -28,7 +28,13 @@ security-headers = true
cors-allow-origins = ""

#### Directory listing
directory-listing = false
directory-listing = true

#### Directory listing sorting code
directory-listing-order = 1

#### Directory listing content format
directory-listing-format = "json"

#### Basich Authentication
basic-auth = ""