Repository: thecoshman/http (language: rust)

Issue:
WebDAV support
After D'Oh's RFSAPI, so two versions from now, but definitely a thing to consider.

Breaking it up would probably be a good idea too

[Ref](http://webdav.org)


Discussion / hints:
Yeah, sure. Why not make this thing even more over powered than it needs to be. As you say, probably worth trying to break it down into smaller features... but I guess you either support WebDAV or not. Unless there is like some core/minimal version to support and then further support can be added later.

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index e116719..5038a1d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -37,7 +37,7 @@ script:
   -
   - if [ "$LANGUAGE" == "Rust" ] && [ "$DEPLOY" ] && [ "$TRAVIS_TAG" ]; then cargo build --verbose --release; fi
   - if [ "$LANGUAGE" == "Rust" ] && [ "$CLIPPY" ]; then
-      cargo install -f clippy;
+      rustup component add clippy-preview || cargo install --git https://github.com/rust-lang/rust-clippy clippy -f;
       cargo clippy;
     fi
   -
diff --git a/Cargo.toml b/Cargo.toml
index e054936..78d02e8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,12 +4,12 @@ name = "https"
 description = "Host These Things Please - a basic http server for hosting a folder fast and simply"
 repository = "https://github.com/thecoshman/http"
 readme = "README.md"
-keywords = ["http", "server", "https", "file", "directory"]
+keywords = ["http", "server", "https", "webdav", "directory"]
 categories = ["network-programming", "web-programming::http-server"]
 license = "MIT"
 build = "build.rs"
 # Remember to also update in appveyor.yml
-version = "1.7.0"
+version = "1.8.0"
 # Remember to also update in http.md
 authors = ["thecoshman <rust@thecoshman.com>",
            "nabijaczleweli <nabijaczleweli@gmail.com>",
@@ -22,12 +22,15 @@ lazy_static = "1.4"
 serde_json = "0.9"
 mime_guess = "1.8"
 tabwriter = "1.1"
+itertools = "0.8"
 lazysort = "0.2"
 brotli2 = "0.3"
 unicase = "2.4"
+walkdir = "2.2"
 base64 = "0.10"
 flate2 = "1.0"
 rfsapi = "0.1"
+xml-rs = "0.8"
 bzip2 = "0.3"
 ctrlc = "3.1"
 regex = "1.2"
@@ -45,6 +48,13 @@ default-features = false
 version = "0.6"
 features = ["hyper-native-tls"]
 
+[target.'cfg(target_os = "windows")'.dependencies.winapi]
+version = "0.3"
+features = ["fileapi"]
+
+[target.'cfg(not(target_os = "windows"))'.dependencies.os-str-generic]
+version = "0.2"
+
 [target.'cfg(not(any(target_os = "windows", target_os = "macos")))'.dependencies.libc]
 version = "0.2"
 
diff --git a/README.md b/README.md
index 73048ec..acf5625 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ See [the manpage](http.md) for full list.
   * [x] Full Range header support
   * [x] Hosting with an <sub>(optional)</sub> optionally autogenerated TLS certificate
   * [x] Arbitrarily nested username/password authentication
+  * [x] [WebDAV/RFC2518](https://tools.ietf.org/html/rfc2518) support, tested with the Linux [`davfs2`](http://savannah.nongnu.org/projects/davfs2) helper, Windows network filesystem support (out-of-box), and the Total Commander [WebDAV plugin](https://www.ghisler.com/plugins.htm)
   * [x] [RFSAPI](https://github.com/nabijaczleweli/rfsapi-rs) support (explorable from commandline with [D'Oh](https://github.com/thecoshman/doh))
 
 ## [Manpage](http.md)
diff --git a/appveyor.yml b/appveyor.yml
index 00eece9..6e6fa01 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 1.7.0-{build}
+version: 1.8.0-{build}
 
 skip_tags: false
 
@@ -22,21 +22,21 @@ build: off
 build_script:
   - git submodule update --init --recursive
   - cargo build --verbose --release
-  - cp target\release\http.exe http-v1.7.0.exe
-  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.7.0.exe
-  - makensis -DHTTP_VERSION=v1.7.0 "/X!include \"EnvVarUpdate.nsh\"" install.nsi
+  - cp target\release\http.exe http-v1.8.0.exe
+  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.8.0.exe
+  - makensis -DHTTP_VERSION=v1.8.0 "/X!include \"EnvVarUpdate.nsh\"" install.nsi
 
 test: off
 test_script:
   - cargo test --verbose --release
 
 artifacts:
-  - path: http-v1.7.0.exe
-  - path: http v1.7.0 installer.exe
+  - path: http-v1.8.0.exe
+  - path: http v1.8.0 installer.exe
 
 deploy:
   provider: GitHub
-  artifact: /http.*v1.7.0.*\.exe/
+  artifact: /http.*v1.8.0.*\.exe/
   auth_token:
     secure: ZTXvCrv9y01s7Hd60w8W7NaouPnPoaw9YJt9WhWQ2Pep8HLvCikt9Exjkz8SGP9P
   on:
diff --git a/http.md b/http.md
index 690f816..f3da3de 100644
--- a/http.md
+++ b/http.md
@@ -163,6 +163,12 @@ pass parameters like what port to use.
       N >= 2 – suppress startup except for auth data, if present
       N >= 3 – suppress all startup messages
 
+  -d --webdav
+
+    Handle WebDAV requests.
+
+    False by default.
+
 ## EXAMPLES
 
   `http`
diff --git a/src/main.rs b/src/main.rs
index d4b8a5a..da4d213 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,17 +1,23 @@
 extern crate hyper_native_tls;
 extern crate percent_encoding;
 extern crate trivial_colours;
+#[cfg(not(target_os = "windows"))]
+extern crate os_str_generic;
 #[macro_use]
 extern crate lazy_static;
 extern crate serde_json;
 extern crate mime_guess;
+extern crate itertools;
 extern crate tabwriter;
 extern crate lazysort;
 extern crate brotli2;
 extern crate unicase;
+extern crate walkdir;
 extern crate base64;
 extern crate flate2;
 extern crate rfsapi;
+#[cfg(target_os = "windows")]
+extern crate winapi;
 extern crate bzip2;
 extern crate ctrlc;
 extern crate serde;
@@ -24,6 +30,7 @@ extern crate libc;
 extern crate rand;
 extern crate time;
 extern crate md6;
+extern crate xml;
 
 mod error;
 mod options;
diff --git a/src/ops.rs b/src/ops/mod.rs
similarity index 90%
rename from src/ops.rs
rename to src/ops/mod.rs
index 9a9e690..97b1fb3 100644
--- a/src/ops.rs
+++ b/src/ops/mod.rs
@@ -1,6 +1,5 @@
 use md6;
 use std::iter;
-use time::now;
 use serde_json;
 use std::borrow::Cow;
 use std::net::IpAddr;
@@ -14,26 +13,29 @@ use std::fs::{self, File};
 use std::default::Default;
 use rand::{Rng, thread_rng};
 use iron::modifiers::Header;
+use iron::url::Url as GenericUrl;
 use self::super::{Options, Error};
 use mime_guess::guess_mime_type_opt;
 use hyper_native_tls::NativeTlsServer;
 use std::collections::{BTreeMap, HashMap};
 use std::io::{self, SeekFrom, Write, Read, Seek};
-use trivial_colours::{Reset as CReset, Colour as C};
 use std::process::{ExitStatus, Command, Child, Stdio};
 use rfsapi::{RawFsApiHeader, FilesetData, RawFileData};
 use rand::distributions::uniform::Uniform as UniformDistribution;
 use rand::distributions::Alphanumeric as AlphanumericDistribution;
 use iron::{headers, status, method, mime, IronResult, Listening, Response, TypeMap, Request, Handler, Iron};
-use self::super::util::{WwwAuthenticate, url_path, file_hash, is_symlink, encode_str, encode_file, file_length, hash_string, html_response, file_binary,
-                        client_mobile, percent_decode, file_icon_suffix, is_actually_file, is_descendant_of, response_encoding, detect_file_as_dir,
-                        encoding_extension, file_time_modified, get_raw_fs_metadata, human_readable_size, is_nonexistant_descendant_of, USER_AGENT, ERROR_HTML,
-                        INDEX_EXTENSIONS, MIN_ENCODING_GAIN, MAX_ENCODING_SIZE, MIN_ENCODING_SIZE, DIRECTORY_LISTING_HTML, MOBILE_DIRECTORY_LISTING_HTML,
-                        BLACKLISTED_ENCODING_EXTENSIONS};
+use self::super::util::{WwwAuthenticate, CommaList, Dav, url_path, file_hash, is_symlink, encode_str, encode_file, file_length, hash_string, html_response,
+                        file_binary, client_mobile, percent_decode, file_icon_suffix, is_actually_file, is_descendant_of, response_encoding, detect_file_as_dir,
+                        encoding_extension, file_time_modified, file_time_modified_p, get_raw_fs_metadata, human_readable_size, is_nonexistant_descendant_of,
+                        USER_AGENT, ERROR_HTML, INDEX_EXTENSIONS, MIN_ENCODING_GAIN, MAX_ENCODING_SIZE, MIN_ENCODING_SIZE, DAV_LEVEL_1_METHODS,
+                        DIRECTORY_LISTING_HTML, MOBILE_DIRECTORY_LISTING_HTML, BLACKLISTED_ENCODING_EXTENSIONS};
 
 
 macro_rules! log {
     ($do_log:expr, $fmt:expr) => {
+        use time::now;
+        use trivial_colours::{Reset as CReset, Colour as C};
+
         if $do_log {
             print!("{}[{}]{} ", C::Cyan, now().strftime("%F %T").unwrap(), CReset);
             println!(concat!($fmt, "{black:.0}{red:.0}{green:.0}{yellow:.0}{blue:.0}{magenta:.0}{cyan:.0}{white:.0}{reset:.0}"),
@@ -49,6 +51,9 @@ macro_rules! log {
         }
     };
     ($do_log:expr, $fmt:expr, $($arg:tt)*) => {
+        use time::now;
+        use trivial_colours::{Reset as CReset, Colour as C};
+
         if $do_log {
             print!("{}[{}]{} ", C::Cyan, now().strftime("%F %T").unwrap(), CReset);
             println!(concat!($fmt, "{black:.0}{red:.0}{green:.0}{yellow:.0}{blue:.0}{magenta:.0}{cyan:.0}{white:.0}{reset:.0}"),
@@ -66,6 +71,8 @@ macro_rules! log {
     };
 }
 
+mod webdav;
+
 
 // TODO: ideally this String here would be Encoding instead but hyper is bad
 type CacheT<Cnt> = HashMap<([u8; 32], String), Cnt>;
@@ -76,6 +83,7 @@ pub struct HttpHandler {
     pub sandbox_symlinks: bool,
     pub check_indices: bool,
     pub log: bool,
+    pub webdav: bool,
     pub global_auth_data: Option<(String, Option<String>)>,
     pub path_auth_data: BTreeMap<String, Option<(String, Option<String>)>>,
     pub writes_temp_dir: Option<(String, PathBuf)>,
@@ -109,6 +117,7 @@ impl HttpHandler {
             sandbox_symlinks: opts.sandbox_symlinks,
             check_indices: opts.check_indices,
             log: opts.loglevel < LogLevel::NoServeStatus,
+            webdav: opts.webdav,
             global_auth_data: global_auth_data,
             path_auth_data: path_auth_data,
             writes_temp_dir: HttpHandler::temp_subdir(&opts.temp_directory, opts.allow_writes, "writes"),
@@ -119,7 +128,7 @@ impl HttpHandler {
     }
 
     pub fn clean_temp_dirs(temp_dir: &(String, PathBuf), loglevel: LogLevel) {
-        for (temp_name, temp_dir) in ["writes", "encoded", "tls"].into_iter().flat_map(|tn| HttpHandler::temp_subdir(temp_dir, true, tn)) {
+        for (temp_name, temp_dir) in ["writes", "encoded", "tls"].iter().flat_map(|tn| HttpHandler::temp_subdir(temp_dir, true, tn)) {
             if temp_dir.exists() && fs::remove_dir_all(&temp_dir).is_ok() {
                 log!(loglevel < LogLevel::NoServeStatus, "Deleted temp dir {magenta}{}{reset}", temp_name);
             }
@@ -151,7 +160,7 @@ impl Handler for HttpHandler {
             }
         }
 
-        match req.method {
+        let mut resp = match req.method {
             method::Options => self.handle_options(req),
             method::Get => self.handle_get(req),
             method::Put => self.handle_put(req),
@@ -163,8 +172,27 @@ impl Handler for HttpHandler {
                 })
             }
             method::Trace => self.handle_trace(req),
+            method::Extension(ref ext) => {
+                if self.webdav {
+                    match &ext[..] {
+                        "COPY" => self.handle_webdav_copy(req),
+                        "MKCOL" => self.handle_webdav_mkcol(req),
+                        "MOVE" => self.handle_webdav_move(req),
+                        "PROPFIND" => self.handle_webdav_propfind(req),
+                        "PROPPATCH" => self.handle_webdav_proppatch(req),
+
+                        _ => self.handle_bad_method(req),
+                    }
+                } else {
+                    self.handle_bad_method(req)
+                }
+            }
             _ => self.handle_bad_method(req),
+        }?;
+        if self.webdav {
+            resp.headers.set(Dav::LEVEL_1);
         }
+        Ok(resp)
     }
 }
 
@@ -240,9 +268,19 @@ impl HttpHandler {
 
     fn handle_options(&self, req: &mut Request) -> IronResult<Response> {
         log!(self.log, "{green}{}{reset} asked for {red}OPTIONS{reset}", req.remote_addr);
-        Ok(Response::with((status::NoContent,
-                           Header(headers::Server(USER_AGENT.to_string())),
-                           Header(headers::Allow(vec![method::Options, method::Get, method::Put, method::Delete, method::Head, method::Trace])))))
+
+        let mut allowed_methods = Vec::with_capacity(6 +
+                                                     if self.webdav {
+            DAV_LEVEL_1_METHODS.len()
+        } else {
+            0
+        });
+        allowed_methods.extend_from_slice(&[method::Options, method::Get, method::Put, method::Delete, method::Head, method::Trace]);
+        if self.webdav {
+            allowed_methods.extend_from_slice(&DAV_LEVEL_1_METHODS);
+        }
+
+        Ok(Response::with((status::NoContent, Header(headers::Server(USER_AGENT.to_string())), Header(headers::Allow(allowed_methods)))))
     }
 
     fn handle_get(&self, req: &mut Request) -> IronResult<Response> {
@@ -290,7 +328,12 @@ impl HttpHandler {
                                                 html_response(ERROR_HTML, &["400 Bad Request", "The request URL was invalid.", cause]))
     }
 
+    #[inline(always)]
     fn handle_nonexistant(&self, req: &mut Request, req_p: PathBuf) -> IronResult<Response> {
+        self.handle_nonexistant_status(req, req_p, status::NotFound)
+    }
+
+    fn handle_nonexistant_status(&self, req: &mut Request, req_p: PathBuf, status: status::Status) -> IronResult<Response> {
         log!(self.log,
              "{green}{}{reset} requested to {red}{}{reset} nonexistant entity {magenta}{}{reset}",
              req.remote_addr,
@@ -299,9 +342,9 @@ impl HttpHandler {
 
         let url_p = url_path(&req.url);
         self.handle_generated_response_encoding(req,
-                                                status::NotFound,
+                                                status,
                                                 html_response(ERROR_HTML,
-                                                              &["404 Not Found", &format!("The requested entity \"{}\" doesn't exist.", url_p), ""]))
+                                                              &[&status.to_string()[..], &format!("The requested entity \"{}\" doesn't exist.", url_p), ""]))
     }
 
     fn handle_get_raw_fs_file(&self, req: &mut Request, req_p: PathBuf) -> IronResult<Response> {
@@ -370,7 +413,7 @@ impl HttpHandler {
 
         Ok(Response::with((status::PartialContent,
                            (Header(headers::Server(USER_AGENT.to_string())),
-                            Header(headers::LastModified(headers::HttpDate(file_time_modified(&req_p)))),
+                            Header(headers::LastModified(headers::HttpDate(file_time_modified_p(&req_p)))),
                             Header(headers::ContentRange(headers::ContentRangeSpec::Bytes {
                                 range: Some((from, to)),
                                 instance_length: Some(file_length(&f.metadata().expect("Failed to get requested file metadata"), &req_p)),
@@ -416,13 +459,14 @@ impl HttpHandler {
 
     fn handle_get_file_opened_range(&self, req_p: PathBuf, s: SeekFrom, b_from: u64, clen: u64, mt: Mime) -> IronResult<Response> {
         let mut f = File::open(&req_p).expect("Failed to open requested file");
-        let flen = file_length(&f.metadata().expect("Failed to get requested file metadata"), &req_p);
+        let fmeta = f.metadata().expect("Failed to get requested file metadata");
+        let flen = file_length(&fmeta, &req_p);
         f.seek(s).expect("Failed to seek requested file");
 
         Ok(Response::with((status::PartialContent,
                            f,
                            (Header(headers::Server(USER_AGENT.to_string())),
-                            Header(headers::LastModified(headers::HttpDate(file_time_modified(&req_p)))),
+                            Header(headers::LastModified(headers::HttpDate(file_time_modified(&fmeta)))),
                             Header(headers::ContentRange(headers::ContentRangeSpec::Bytes {
                                 range: Some((b_from, flen - 1)),
                                 instance_length: Some(flen),
@@ -457,7 +501,7 @@ impl HttpHandler {
 
         Ok(Response::with((status::NoContent,
                            Header(headers::Server(USER_AGENT.to_string())),
-                           Header(headers::LastModified(headers::HttpDate(file_time_modified(&req_p)))),
+                           Header(headers::LastModified(headers::HttpDate(file_time_modified_p(&req_p)))),
                            Header(headers::ContentRange(headers::ContentRangeSpec::Bytes {
                                range: Some((from, to)),
                                instance_length: Some(file_length(&req_p.metadata().expect("Failed to get requested file metadata"), &req_p)),
@@ -486,7 +530,7 @@ impl HttpHandler {
         } else {
             Ok(Response::with((status::Ok,
                                (Header(headers::Server(USER_AGENT.to_string())),
-                                Header(headers::LastModified(headers::HttpDate(file_time_modified(&req_p)))),
+                                Header(headers::LastModified(headers::HttpDate(file_time_modified(&metadata)))),
                                 Header(headers::AcceptRanges(vec![headers::RangeUnit::Bytes]))),
                                req_p.as_path(),
                                Header(headers::ContentLength(file_length(&metadata, &req_p))),
@@ -520,7 +564,7 @@ impl HttpHandler {
                     Some(&(ref resp_p, false)) => {
                         return Ok(Response::with((status::Ok,
                                                   Header(headers::Server(USER_AGENT.to_string())),
-                                                  Header(headers::LastModified(headers::HttpDate(file_time_modified(resp_p)))),
+                                                  Header(headers::LastModified(headers::HttpDate(file_time_modified_p(resp_p)))),
                                                   Header(headers::AcceptRanges(vec![headers::RangeUnit::Bytes])),
                                                   resp_p.as_path(),
                                                   mt)));
@@ -571,7 +615,7 @@ impl HttpHandler {
 
         Ok(Response::with((status::Ok,
                            (Header(headers::Server(USER_AGENT.to_string())),
-                            Header(headers::LastModified(headers::HttpDate(file_time_modified(&req_p)))),
+                            Header(headers::LastModified(headers::HttpDate(file_time_modified_p(&req_p)))),
                             Header(headers::AcceptRanges(vec![headers::RangeUnit::Bytes]))),
                            req_p.as_path(),
                            Header(headers::ContentLength(file_length(&req_p.metadata().expect("Failed to get requested file metadata"), &req_p))),
@@ -595,11 +639,10 @@ impl HttpHandler {
                     let fp = f.path();
                     let mut symlink = false;
                     !((!self.follow_symlinks &&
-                       (|| {
+                       {
                         symlink = is_symlink(&fp);
                         symlink
-                    })()) ||
-                      (self.follow_symlinks && self.sandbox_symlinks && symlink && !is_descendant_of(fp, &self.hosted_directory.1)))
+                    }) || (self.follow_symlinks && self.sandbox_symlinks && symlink && !is_descendant_of(fp, &self.hosted_directory.1)))
                 })
                                                 .map(|f| {
                     let is_file = is_actually_file(&f.file_type().expect("Failed to get file type"));
@@ -609,7 +652,7 @@ impl HttpHandler {
                         RawFileData {
                             mime_type: "text/directory".parse().unwrap(),
                             name: f.file_name().into_string().expect("Failed to get file name"),
-                            last_modified: file_time_modified(&f.path()),
+                            last_modified: file_time_modified_p(&f.path()),
                             size: 0,
                             is_file: false,
                         }
@@ -682,8 +725,7 @@ impl HttpHandler {
             format!("<a href=\"/{up_path}{up_path_slash}\" class=\"list entry top\"><span class=\"back_arrow_icon\">Parent directory</span></a> \
                      <a href=\"/{up_path}{up_path_slash}\" class=\"list entry bottom\"><span class=\"marker\">@</span>\
                        <span class=\"datetime\">{} UTC</span></a>",
-                    file_time_modified(req_p.parent()
-                            .expect("Failed to get requested directory's parent directory"))
+                    file_time_modified_p(req_p.parent().expect("Failed to get requested directory's parent directory"))
                         .strftime("%F %T")
                         .unwrap(),
                     up_path = slash_idx.map(|i| &rel_noslash[0..i]).unwrap_or(""),
@@ -696,10 +738,10 @@ impl HttpHandler {
                 let fp = f.path();
                 let mut symlink = false;
                 !((!self.follow_symlinks &&
-                   (|| {
+                   {
                     symlink = is_symlink(&fp);
                     symlink
-                })()) || (self.follow_symlinks && self.sandbox_symlinks && symlink && !is_descendant_of(fp, &self.hosted_directory.1)))
+                }) || (self.follow_symlinks && self.sandbox_symlinks && symlink && !is_descendant_of(fp, &self.hosted_directory.1)))
             })
             .sorted_by(|lhs, rhs| {
                 (is_actually_file(&lhs.file_type().expect("Failed to get file type")),
@@ -709,6 +751,7 @@ impl HttpHandler {
             })
             .fold("".to_string(), |cur, f| {
                 let is_file = is_actually_file(&f.file_type().expect("Failed to get file type"));
+                let fmeta = f.metadata().expect("Failed to get requested file metadata");
                 let fname = f.file_name().into_string().expect("Failed to get file name");
                 let path = f.path();
 
@@ -721,10 +764,10 @@ impl HttpHandler {
                         path.file_name().map(|p| p.to_str().expect("Filename not UTF-8").replace('.', "_")).as_ref().unwrap_or(&fname),
                         fname,
                         if is_file { "" } else { "/" },
-                        file_time_modified(&path).strftime("%F %T").unwrap(),
+                        file_time_modified(&fmeta).strftime("%F %T").unwrap(),
                         if is_file { "<span class=\"size\">" } else { "" },
                         if is_file {
-                            human_readable_size(file_length(&f.metadata().expect("Failed to get requested file metadata"), &path))
+                            human_readable_size(file_length(&fmeta, &path))
                         } else {
                             String::new()
                         },
@@ -771,7 +814,7 @@ impl HttpHandler {
                          <td><a href=\"/{up_path}{up_path_slash}\">Parent directory</a></td> \
                          <td><a href=\"/{up_path}{up_path_slash}\" class=\"datetime\">{}</a></td> \
                          <td><a href=\"/{up_path}{up_path_slash}\">&nbsp;</a></td></tr>",
-                    file_time_modified(req_p.parent().expect("Failed to get requested directory's parent directory")).strftime("%F %T").unwrap(),
+                    file_time_modified_p(req_p.parent().expect("Failed to get requested directory's parent directory")).strftime("%F %T").unwrap(),
                     up_path = slash_idx.map(|i| &rel_noslash[0..i]).unwrap_or(""),
                     up_path_slash = if slash_idx.is_some() { "/" } else { "" })
         };
@@ -782,10 +825,10 @@ impl HttpHandler {
                 let fp = f.path();
                 let mut symlink = false;
                 !((!self.follow_symlinks &&
-                   (|| {
+                   {
                     symlink = is_symlink(&fp);
                     symlink
-                })()) || (self.follow_symlinks && self.sandbox_symlinks && symlink && !is_descendant_of(fp, &self.hosted_directory.1)))
+                }) || (self.follow_symlinks && self.sandbox_symlinks && symlink && !is_descendant_of(fp, &self.hosted_directory.1)))
             })
             .sorted_by(|lhs, rhs| {
                 (is_actually_file(&lhs.file_type().expect("Failed to get file type")),
@@ -795,9 +838,10 @@ impl HttpHandler {
             })
             .fold("".to_string(), |cur, f| {
                 let is_file = is_actually_file(&f.file_type().expect("Failed to get file type"));
+                let fmeta = f.metadata().expect("Failed to get requested file metadata");
                 let fname = f.file_name().into_string().expect("Failed to get file name");
                 let path = f.path();
-                let len = file_length(&f.metadata().expect("Failed to get requested file metadata"), &path);
+                let len = file_length(&fmeta, &path);
 
                 format!("{}<tr><td><a href=\"{path}{fname}\" id=\"{}\" class=\"{}{}_icon\"></a></td> \
                            <td><a href=\"{path}{fname}\">{}{}</a></td> <td><a href=\"{path}{fname}\" class=\"datetime\">{}</a></td> \
@@ -808,7 +852,7 @@ impl HttpHandler {
                         file_icon_suffix(&path, is_file),
                         fname,
                         if is_file { "" } else { "/" },
-                        file_time_modified(&path).strftime("%F %T").unwrap(),
+                        file_time_modified(&fmeta).strftime("%F %T").unwrap(),
                         if is_file { "<abbr title=\"" } else { "&nbsp;" },
                         if is_file {
                             len.to_string()
@@ -857,7 +901,14 @@ impl HttpHandler {
         if url_err {
             self.handle_invalid_url(req, "<p>Percent-encoding decoded to invalid UTF-8.</p>")
         } else if req_p.is_dir() {
-            self.handle_disallowed_method(req, &[method::Options, method::Get, method::Delete, method::Head, method::Trace], "directory")
+            self.handle_disallowed_method(req,
+                                          &[&[method::Options, method::Get, method::Delete, method::Head, method::Trace],
+                                            if self.webdav {
+                                                &DAV_LEVEL_1_METHODS[..]
+                                            } else {
+                                                &[]
+                                            }],
+                                          "directory")
         } else if detect_file_as_dir(&req_p) {
             self.handle_invalid_url(req, "<p>Attempted to use file as directory.</p>")
         } else if req.headers.has::<headers::ContentRange>() {
@@ -872,8 +923,9 @@ impl HttpHandler {
         }
     }
 
-    fn handle_disallowed_method(&self, req: &mut Request, allowed: &[method::Method], tpe: &str) -> IronResult<Response> {
+    fn handle_disallowed_method(&self, req: &mut Request, allowed: &[&[method::Method]], tpe: &str) -> IronResult<Response> {
         let allowed_s = allowed.iter()
+            .flat_map(|mms| mms.iter())
             .enumerate()
             .fold("".to_string(), |cur, (i, m)| {
                 cur + &m.to_string() +
@@ -900,7 +952,7 @@ impl HttpHandler {
                           &["405 Method Not Allowed", &format!("Can't {} on a {}.", req.method, tpe), &format!("<p>Allowed methods: {}</p>", allowed_s)]);
         self.handle_generated_response_encoding(req, status::MethodNotAllowed, resp_text)
             .map(|mut r| {
-                r.headers.set(headers::Allow(allowed.to_vec()));
+                r.headers.set(headers::Allow(allowed.iter().flat_map(|mms| mms.iter()).cloned().collect()));
                 r
             })
     }
@@ -958,7 +1010,7 @@ impl HttpHandler {
             return self.handle_forbidden_method(req, "-w", "write requests");
         }
 
-        let (req_p, symlink, url_err) = self.parse_requested_path_custom_symlink(req, false);
+        let (req_p, symlink, url_err) = self.parse_requested_path_custom_symlink(req.url.as_ref(), false);
 
         if url_err {
             self.handle_invalid_url(req, "<p>Percent-encoding decoded to invalid UTF-8.</p>")
@@ -971,11 +1023,11 @@ impl HttpHandler {
     }
 
     fn handle_delete_path(&self, req: &mut Request, req_p: PathBuf, symlink: bool) -> IronResult<Response> {
-        let file = is_actually_file(&req_p.metadata().expect("failed to get file metadata").file_type());
+        let ft = req_p.metadata().expect("failed to get file metadata").file_type();
         log!(self.log,
              "{green}{}{reset} deleted {blue}{} {magenta}{}{reset}",
              req.remote_addr,
-             if file {
+             if is_actually_file(&ft) {
                  "file"
              } else if symlink {
                  "symlink"
@@ -984,7 +1036,7 @@ impl HttpHandler {
              },
              req_p.display());
 
-        if file {
+        if is_actually_file(&ft) {
             fs::remove_file(req_p).expect("Failed to remove requested file");
         } else {
             fs::remove_dir_all(req_p).expect(if symlink {
@@ -1038,8 +1090,15 @@ impl HttpHandler {
              req.remote_addr,
              req.method);
 
-        let last_p = format!("<p>Unsupported request method: {}.<br />\nSupported methods: OPTIONS, GET, PUT, DELETE, HEAD and TRACE.</p>",
-                             req.method);
+        let last_p = format!("<p>Unsupported request method: {}.<br />\nSupported methods: {}{}OPTIONS, GET, PUT, DELETE, HEAD, and TRACE.</p>",
+                             req.method,
+                             CommaList(if self.webdav {
+                                     &DAV_LEVEL_1_METHODS[..]
+                                 } else {
+                                     &[][..]
+                                 }
+                                 .iter()),
+                             if self.webdav { ", " } else { "" });
         self.handle_generated_response_encoding(req,
                                                 status::NotImplemented,
                                                 html_response(ERROR_HTML, &["501 Not Implemented", "This operation was not implemented.", &last_p]))
@@ -1101,15 +1160,12 @@ impl HttpHandler {
     }
 
     fn parse_requested_path(&self, req: &Request) -> (PathBuf, bool, bool) {
-        self.parse_requested_path_custom_symlink(req, true)
+        self.parse_requested_path_custom_symlink(req.url.as_ref(), true)
     }
 
-    fn parse_requested_path_custom_symlink(&self, req: &Request, follow_symlinks: bool) -> (PathBuf, bool, bool) {
-        req.url
-            .as_ref()
-            .path_segments()
+    fn parse_requested_path_custom_symlink(&self, req_url: &GenericUrl, follow_symlinks: bool) -> (PathBuf, bool, bool) {
+        req_url.path_segments()
             .unwrap()
-            .into_iter()
             .filter(|p| !p.is_empty())
             .fold((self.hosted_directory.1.clone(), false, false), |(mut cur, mut sk, mut err), pp| {
                 if let Some(pp) = percent_decode(pp) {
@@ -1145,6 +1201,7 @@ impl Clone for HttpHandler {
             sandbox_symlinks: self.sandbox_symlinks,
             check_indices: self.check_indices,
             log: self.log,
+            webdav: self.webdav,
             global_auth_data: self.global_auth_data.clone(),
             path_auth_data: self.path_auth_data.clone(),
             writes_temp_dir: self.writes_temp_dir.clone(),
diff --git a/src/ops/webdav.rs b/src/ops/webdav.rs
new file mode 100644
index 0000000..3fdc098
--- /dev/null
+++ b/src/ops/webdav.rs
@@ -0,0 +1,753 @@
+//! Basic WebDAV handling is based heavily on
+//! https://github.com/tylerwhall/hyperdav-server/blob/415f512ac030478593ad389a3267aeed7441d826/src/lib.rs,
+//! and extended on
+//! https://github.com/miquels/webdav-handler-rs @ 02433c1acfccd848a7de26889f6857cbad559076,
+//! adhering to
+//! https://tools.ietf.org/html/rfc2518
+
+
+use self::super::super::util::{BorrowXmlName, Destination, CommaList, Overwrite, Depth, win32_file_attributes, file_time_accessed, file_time_modified,
+                               file_time_created, client_microsoft, is_actually_file, is_descendant_of, file_executable, html_response, file_length,
+                               file_binary, copy_dir, WEBDAV_ALLPROP_PROPERTIES_NON_WINDOWS, WEBDAV_ALLPROP_PROPERTIES_WINDOWS, WEBDAV_XML_NAMESPACE_MICROSOFT,
+                               WEBDAV_XML_NAMESPACE_APACHE, WEBDAV_PROPNAME_PROPERTIES, WEBDAV_XML_NAMESPACE_DAV, WEBDAV_XML_NAMESPACES, ERROR_HTML};
+use std::io::{ErrorKind as IoErrorKind, Result as IoResult, Error as IoError, Write, Read};
+use xml::reader::{EventReader as XmlReader, XmlEvent as XmlREvent, Error as XmlRError};
+use xml::writer::{EventWriter as XmlWriter, XmlEvent as XmlWEvent, Error as XmlWError};
+use xml::{EmitterConfig as XmlEmitterConfig, ParserConfig as XmlParserConfig};
+use xml::writer::events::StartElementBuilder as XmlWEventStartElementBuilder;
+use xml::common::{TextPosition as XmlTextPosition, XmlVersion, Position};
+use xml::name::{OwnedName as OwnedXmlName, Name as XmlName};
+use iron::{status, IronResult, Response, Request};
+use mime_guess::guess_mime_type_opt;
+use iron::url::Url as GenericUrl;
+use std::path::{PathBuf, Path};
+use std::fs::{self, Metadata};
+use self::super::HttpHandler;
+use itertools::Itertools;
+use std::borrow::Borrow;
+use iron::mime::Mime;
+use std::fmt;
+
+
+lazy_static! {
+    static ref DEFAULT_XML_PARSER_CONFIG: XmlParserConfig = XmlParserConfig { trim_whitespace: true, ..Default::default() };
+    static ref DEFAULT_XML_EMITTER_CONFIG: XmlEmitterConfig = XmlEmitterConfig { perform_indent: cfg!(debug_assertions), ..Default::default() };
+}
+
+
+impl HttpHandler {
+    pub(super) fn handle_webdav_propfind(&self, req: &mut Request) -> IronResult<Response> {
+        let (req_p, symlink, url_err) = self.parse_requested_path(req);
+
+        if url_err {
+            return self.handle_invalid_url(req, "<p>Percent-encoding decoded to invalid UTF-8.</p>");
+        }
+
+        if !req_p.exists() || (symlink && !self.follow_symlinks) ||
+           (symlink && self.follow_symlinks && self.sandbox_symlinks && !is_descendant_of(&req_p, &self.hosted_directory.1)) {
+            return self.handle_nonexistant(req, req_p);
+        }
+
+
+        let depth = req.headers.get::<Depth>().copied().unwrap_or(Depth::Zero);
+
+        let props = match parse_propfind(req) {
+            Ok(props) => props,
+            Err(e) => {
+                match match e {
+                    Ok(pe) => Ok(pe),
+                    Err(xre) => {
+                        if xre.position() == XmlTextPosition::new() && xre.msg().contains("no root element") {
+                            Err(PropfindVariant::AllProp)
+                        } else {
+                            Ok(xre.to_string())
+                        }
+                    }
+                } {
+                    Ok(e) => {
+                        log!(self.log,
+                             "{green}{}{reset} tried to {red}PROPFIND{reset} {yellow}{}{reset} with invalid XML",
+                             req.remote_addr,
+                             req_p.display());
+                        return self.handle_generated_response_encoding(req,
+                                                                       status::BadRequest,
+                                                                       html_response(ERROR_HTML, &["400 Bad Request", &format!("Invalid XML: {}", e), ""]));
+                    }
+                    Err(props) => props,
+                }
+            }
+        };
+
+        log!(self.log,
+             "{green}{}{reset} requested {red}PROPFIND{reset} of {} on {yellow}{}{reset} at depth {}",
+             req.remote_addr,
+             props,
+             req_p.display(),
+             depth);
+
+        let url = req.url.as_ref().as_str().to_string();
+        let resp = match props {
+            PropfindVariant::AllProp => {
+                self.handle_webdav_propfind_write_output(req,
+                                                         url,
+                                                         &req_p,
+                                                         if client_microsoft(&req.headers) {
+                                                             WEBDAV_ALLPROP_PROPERTIES_WINDOWS
+                                                         } else {
+                                                             WEBDAV_ALLPROP_PROPERTIES_NON_WINDOWS
+                                                         },
+                                                         false,
+                                                         depth)
+            }
+            PropfindVariant::PropName => self.handle_webdav_propfind_write_output(req, url, &req_p, WEBDAV_PROPNAME_PROPERTIES, true, depth),
+            PropfindVariant::Props(props) => self.handle_webdav_propfind_write_output(req, url, &req_p, &[&props[..]], false, depth),
+        };
+
+        match resp.expect("Couldn't write PROPFIND XML") {
+            Ok(xml_resp) => Ok(Response::with((status::MultiStatus, xml_resp, "text/xml;charset=utf-8".parse::<Mime>().unwrap()))),
+            Err(resp) => resp,
+        }
+    }
+
+    /// Adapted from
+    /// https://github.com/tylerwhall/hyperdav-server/blob/415f512ac030478593ad389a3267aeed7441d826/src/lib.rs#L459
+    fn handle_webdav_propfind_write_output<'n, N: BorrowXmlName<'n>>(&self, req: &mut Request, url: String, path: &Path, props: &[&'n [N]], just_names: bool,
+                                                                     depth: Depth)
+                                                                     -> Result<Result<Vec<u8>, IronResult<Response>>, XmlWError> {
+        let mut out = intialise_xml_output()?;
+        out.write(namespaces_for_props("D:multistatus", props.iter().flat_map(|pp| pp.iter())))?;
+
+        let meta = path.metadata().expect("Failed to get requested file metadata");
+        handle_propfind_path(&mut out, &url, &path, &meta, props, just_names)?;
+
+        if meta.is_dir() {
+            if let Some(ir) = self.handle_webdav_propfind_path_recursive(req, &mut out, url, &path, props, just_names, depth)? {
+                return Ok(Err(ir));
+            }
+        }
+
+        out.write(XmlWEvent::end_element())?;
+
+        Ok(Ok(out.into_inner()))
+    }
+
+    fn handle_webdav_propfind_path_recursive<'n, W: Write, N: BorrowXmlName<'n>>(&self, req: &mut Request, out: &mut XmlWriter<W>, root_url: String,
+                                                                                 root_path: &Path, props: &[&'n [N]], just_names: bool, depth: Depth)
+                                                                                 -> Result<Option<IronResult<Response>>, XmlWError> {
+        if let Some(next_depth) = depth.lower() {
+            for f in root_path.read_dir().expect("Failed to read requested directory").map(|p| p.expect("Failed to iterate over requested directory")) {
+                let mut url = root_url.clone();
+                if !url.ends_with('/') {
+                    url.push('/');
+                }
+                url.push_str(f.file_name().to_str().expect("Filename not UTF-8"));
+
+                let mut path = f.path();
+                let mut symlink = false;
+                while let Ok(newlink) = path.read_link() {
+                    symlink = true;
+                    path = newlink;
+                }
+
+                if !(!path.exists() || (symlink && !self.follow_symlinks) ||
+                     (symlink && self.follow_symlinks && self.sandbox_symlinks && !is_descendant_of(&path, &self.hosted_directory.1))) {
+                    handle_propfind_path(out,
+                                         &url,
+                                         &path,
+                                         &path.metadata().expect("Failed to get requested file metadata"),
+                                         props,
+                                         just_names)?;
+                    self.handle_webdav_propfind_path_recursive(req, out, url, &path, props, just_names, next_depth)?;
+                }
+            }
+        }
+
+        Ok(None)
+    }
+
+    /// NB: we don't allow modifying any properties, so we 409 Conflict all of them
+    pub(super) fn handle_webdav_proppatch(&self, req: &mut Request) -> IronResult<Response> {
+        let (req_p, symlink, url_err) = self.parse_requested_path(req);
+
+        if url_err {
+            return self.handle_invalid_url(req, "<p>Percent-encoding decoded to invalid UTF-8.</p>");
+        }
+
+        if self.writes_temp_dir.is_none() {
+            return self.handle_forbidden_method(req, "-w", "write requests");
+        }
+
+        if !req_p.exists() || (symlink && !self.follow_symlinks) ||
+           (symlink && self.follow_symlinks && self.sandbox_symlinks && !is_descendant_of(&req_p, &self.hosted_directory.1)) {
+            return self.handle_nonexistant(req, req_p);
+        }
+
+        let props = match parse_proppatch(req) {
+            Ok(props) => props,
+            Err(e) => {
+                log!(self.log,
+                     "{green}{}{reset} tried to {red}PROPPATCH{reset} {yellow}{}{reset} with invalid XML",
+                     req.remote_addr,
+                     req_p.display());
+                return self.handle_generated_response_encoding(req,
+                                                               status::BadRequest,
+                                                               html_response(ERROR_HTML, &["400 Bad Request", &format!("Invalid XML: {}", e), ""]));
+            }
+        };
+
+        log!(self.log,
+             "{green}{}{reset} requested {red}PROPPATCH{reset} of {} on {yellow}{}{reset}",
+             req.remote_addr,
+             CommaList(props.iter().map(|p| &p.0.local_name)),
+             req_p.display());
+
+        match write_proppatch_output(&props, req.url.as_ref()).expect("Couldn't write PROPPATCH XML") {
+            Ok(xml_resp) => Ok(Response::with((status::MultiStatus, xml_resp, "text/xml;charset=utf-8".parse::<Mime>().unwrap()))),
+            Err(resp) => resp,
+        }
+    }
+
+    pub(super) fn handle_webdav_mkcol(&self, req: &mut Request) -> IronResult<Response> {
+        let (req_p, symlink, url_err) = self.parse_requested_path(req);
+
+        log!(self.log,
+             "{green}{}{reset} requested to {red}MKCOL{reset} at {yellow}{}{reset}",
+             req.remote_addr,
+             req_p.display());
+
+        if url_err {
+            return self.handle_invalid_url(req, "<p>Percent-encoding decoded to invalid UTF-8.</p>");
+        }
+
+        if self.writes_temp_dir.is_none() {
+            return self.handle_forbidden_method(req, "-w", "write requests");
+        }
+
+        if !req_p.parent().map(|pp| pp.exists()).unwrap_or(true) || (symlink && !self.follow_symlinks) ||
+           (symlink && self.follow_symlinks && self.sandbox_symlinks && !is_descendant_of(&req_p, &self.hosted_directory.1)) {
+            return self.handle_nonexistant_status(req, req_p, status::Conflict);
+        }
+
+        if req.body.read_exact(&mut [0]).is_ok() {
+            return Ok(Response::with(status::UnsupportedMediaType));
+        }
+
+        match fs::create_dir(&req_p) {
+            Ok(()) => Ok(Response::with(status::Created)),
+            Err(e) => {
+                match e.kind() {
+                    IoErrorKind::NotFound => self.handle_nonexistant_status(req, req_p, status::Conflict),
+                    IoErrorKind::AlreadyExists => Ok(Response::with((status::MethodNotAllowed, "File exists"))),
+                    _ => Ok(Response::with(status::Forbidden)),
+                }
+            }
+        }
+    }
+
+    #[inline(always)]
+    pub(crate) fn handle_webdav_copy(&self, req: &mut Request) -> IronResult<Response> {
+        self.handle_webdav_copy_move(req, false, None)
+    }
+
+    #[inline(always)]
+    pub(crate) fn handle_webdav_move(&self, req: &mut Request) -> IronResult<Response> {
+        let mut sp = (PathBuf::new(), false);
+        let resp = self.handle_webdav_copy_move(req, true, Some(&mut sp))?;
+
+        if resp.status == Some(status::Created) || resp.status == Some(status::NoContent) {
+            let (req_p, is_file) = sp;
+
+            let removal = if is_file {
+                fs::remove_file(req_p)
+            } else {
+                fs::remove_dir_all(req_p)
+            };
+            if removal.is_err() {
+                return Ok(Response::with(status::Locked));
+            }
+        }
+
+        Ok(resp)
+    }
+
+    fn handle_webdav_copy_move(&self, req: &mut Request, is_move: bool, source_path: Option<&mut (PathBuf, bool)>) -> IronResult<Response> {
+        let (req_p, symlink, url_err) = self.parse_requested_path(req);
+
+        if url_err {
+            return self.handle_invalid_url(req, "<p>Percent-encoding decoded to invalid UTF-8.</p>");
+        }
+        let (dest_p, dest_symlink) = match req.headers.get::<Destination>() {
+            Some(dest) => {
+                let (dest_p, dest_symlink, dest_url_err) = self.parse_requested_path_custom_symlink(&dest.0, true);
+
+                if dest_url_err {
+                    return self.handle_invalid_url(req, "<p>Percent-encoding decoded destination to invalid UTF-8.</p>");
+                }
+
+                (dest_p, dest_symlink)
+            }
+            None => return self.handle_invalid_url(req, "<p>Destination URL invalid or nonexistant.</p>"),
+        };
+
+        let depth = req.headers.get::<Depth>().copied().unwrap_or(Depth::Infinity);
+        let overwrite = req.headers.get::<Overwrite>().copied().unwrap_or_default().0;
+
+        log!(self.log,
+             "{green}{}{reset} requested to {}{red}{}{reset} {yellow}{}{reset} to {yellow}{}{reset} at depth {}",
+             req.remote_addr,
+             if overwrite { "overwrite-" } else { "" },
+             if !is_move { "COPY" } else { "MOVE" },
+             req_p.display(),
+             dest_p.display(),
+             depth);
+
+        if self.writes_temp_dir.is_none() {
+            return self.handle_forbidden_method(req, "-w", "write requests");
+        }
+
+        if req_p == dest_p {
+            return Ok(Response::with(status::Forbidden));
+        }
+
+        if !req_p.exists() || (symlink && !self.follow_symlinks) ||
+           (symlink && self.follow_symlinks && self.sandbox_symlinks && !is_descendant_of(&req_p, &self.hosted_directory.1)) {
+            return self.handle_nonexistant(req, req_p);
+        }
+
+        if !dest_p.parent().map(|pp| pp.exists()).unwrap_or(true) || (dest_symlink && !self.follow_symlinks) ||
+           (dest_symlink && self.follow_symlinks && self.sandbox_symlinks && !is_descendant_of(&dest_p, &self.hosted_directory.1)) {
+            return Ok(Response::with(status::Conflict));
+        }
+
+        let mut overwritten = false;
+        if dest_p.exists() {
+            if !overwrite {
+                return Ok(Response::with(status::PreconditionFailed));
+            }
+
+            if !is_actually_file(&dest_p.metadata().expect("Failed to get destination file metadata").file_type()) {
+                // NB: this disallows overwriting non-empty directories
+                if fs::remove_dir(&dest_p).is_err() {
+                    return Ok(Response::with(status::Locked));
+                }
+            }
+
+            overwritten = true;
+        }
+
+        let source_file = is_actually_file(&req_p.metadata().expect("Failed to get requested file metadata").file_type());
+        if let Some(sp) = source_path {
+            *sp = (req_p.clone(), source_file);
+        }
+        if source_file {
+            copy_response(fs::copy(req_p, dest_p).map(|_| ()), overwritten)
+        } else {
+            match depth {
+                Depth::Zero if !is_move => copy_response(fs::create_dir(dest_p), overwritten),
+                Depth::Infinity => {
+                    match copy_dir(&req_p, &dest_p) {
+                        Ok(errors) => {
+                            if errors.is_empty() {
+                                copy_response(Ok(()), overwritten)
+                            } else {
+                                Ok(Response::with((status::MultiStatus,
+                                                   copy_response_multierror(&errors, req.url.as_ref()).expect("Couldn't write PROPFIND XML"))))
+                            }
+                        }
+                        Err(err) => copy_response(Err(err), overwritten),
+                    }
+                }
+                _ => {
+                    self.handle_generated_response_encoding(req,
+                                                            status::BadRequest,
+                                                            html_response(ERROR_HTML, &["400 Bad Request", &format!("Invalid depth: {}", depth), ""]))
+                }
+            }
+        }
+    }
+}
+
+
+/// https://tools.ietf.org/html/rfc2518#section-12.14
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+enum PropfindVariant {
+    AllProp,
+    PropName,
+    Props(Vec<OwnedXmlName>),
+}
+
+impl fmt::Display for PropfindVariant {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            PropfindVariant::AllProp => f.write_str("all props"),
+            PropfindVariant::PropName => f.write_str("prop names"),
+            PropfindVariant::Props(props) => {
+                let mut itr = props.iter();
+                if let Some(name) = itr.next() {
+                    name.borrow().repr_display().fmt(f)?;
+
+                    for name in itr {
+                        f.write_str(", ")?;
+                        name.borrow().repr_display().fmt(f)?;
+                    }
+                }
+
+                Ok(())
+            }
+        }
+    }
+}
+
+
+/// https://tools.ietf.org/html/rfc2518#section-12.14
+///
+/// Adapted from
+/// https://github.com/tylerwhall/hyperdav-server/blob/415f512ac030478593ad389a3267aeed7441d826/src/lib.rs#L158
+fn parse_propfind(req: &mut Request) -> Result<PropfindVariant, Result<String, XmlRError>> {
+    #[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
+    enum State {
+        Start,
+        PropFind,
+        Prop,
+        InProp,
+    }
+
+
+    let mut xml = XmlReader::new_with_config(&mut req.body, DEFAULT_XML_PARSER_CONFIG.clone());
+    let mut state = State::Start;
+    let mut props = vec![];
+
+    loop {
+        let event = xml.next().map_err(Err)?;
+
+        match (state, event) {
+            (State::Start, XmlREvent::StartDocument { .. }) => (),
+            (State::Start, XmlREvent::StartElement { ref name, .. }) if name.local_name == "propfind" => state = State::PropFind,
+
+            (State::PropFind, XmlREvent::StartElement { ref name, .. }) if name.local_name == "allprop" => return Ok(PropfindVariant::AllProp),
+            (State::PropFind, XmlREvent::StartElement { ref name, .. }) if name.local_name == "propname" => return Ok(PropfindVariant::PropName),
+            (State::PropFind, XmlREvent::StartElement { ref name, .. }) if name.local_name == "prop" => state = State::Prop,
+
+            (State::Prop, XmlREvent::StartElement { name, .. }) => {
+                state = State::InProp;
+                props.push(name);
+            }
+            (State::Prop, XmlREvent::EndElement { .. }) => return Ok(PropfindVariant::Props(props)),
+
+            (State::InProp, XmlREvent::EndElement { .. }) => state = State::Prop,
+
+            (st, ev) => return Err(Ok(format!("Unexpected event {:?} during state {:?}", ev, st))),
+        }
+    }
+}
+
+/// Adapted from
+/// https://github.com/tylerwhall/hyperdav-server/blob/415f512ac030478593ad389a3267aeed7441d826/src/lib.rs#L306
+fn handle_propfind_path<'n, W: Write, N: BorrowXmlName<'n>>(out: &mut XmlWriter<W>, url: &str, path: &Path, meta: &Metadata, props: &[&'n [N]],
+                                                            just_names: bool)
+                                                            -> Result<(), XmlWError> {
+    out.write(XmlWEvent::start_element("D:response"))?;
+
+    out.write(XmlWEvent::start_element("D:href"))?;
+    out.write(XmlWEvent::characters(url))?;
+    out.write(XmlWEvent::end_element())?; // href
+
+    let prop_count = props.iter().map(|pp| pp.len()).sum();
+    let mut failed_props = Vec::with_capacity(prop_count);
+    out.write(XmlWEvent::start_element("D:propstat"))?;
+    out.write(XmlWEvent::start_element("D:prop"))?;
+    for prop in props.iter().flat_map(|pp| pp.iter()) {
+        let prop = prop.borrow_xml_name();
+
+        let mut write_name = false;
+        if !just_names && !handle_prop_path(out, path, meta, prop)? {
+            failed_props.push(prop);
+            write_name = true;
+        }
+
+        if just_names || write_name {
+            start_client_prop_element(out, prop)?;
+            out.write(XmlWEvent::end_element())?;
+        }
+    }
+    out.write(XmlWEvent::end_element())?; // prop
+    out.write(XmlWEvent::start_element("D:status"))?;
+    if failed_props.len() >= prop_count {
+        // If they all failed, make this a failure response and return
+        out.write(XmlWEvent::characters("HTTP/1.1 404 Not Found"))?;
+        out.write(XmlWEvent::end_element())?; // status
+        out.write(XmlWEvent::end_element())?; // propstat
+        out.write(XmlWEvent::end_element())?; // response
+        return Ok(());
+    }
+
+    out.write(XmlWEvent::characters("HTTP/1.1 200 OK"))?;
+    out.write(XmlWEvent::end_element())?; // status
+    out.write(XmlWEvent::end_element())?; // propstat
+
+    if !failed_props.is_empty() {
+        // Handle the failed properties
+        out.write(XmlWEvent::start_element("D:propstat"))?;
+        out.write(XmlWEvent::start_element("D:prop"))?;
+        for prop in failed_props {
+            start_client_prop_element(out, prop)?;
+            out.write(XmlWEvent::end_element())?;
+        }
+        out.write(XmlWEvent::end_element())?; // prop
+        out.write(XmlWEvent::start_element("D:status"))?;
+        out.write(XmlWEvent::characters("HTTP/1.1 404 Not Found"))?;
+        out.write(XmlWEvent::end_element())?; // status
+        out.write(XmlWEvent::end_element())?; // propstat
+    }
+
+    out.write(XmlWEvent::end_element())?; // response
+
+    Ok(())
+}
+
+/// Adapted from
+/// https://github.com/tylerwhall/hyperdav-server/blob/415f512ac030478593ad389a3267aeed7441d826/src/lib.rs#L245
+/// extended properties adapted from
+/// https://github.com/miquels/webdav-handler-rs/blob/02433c1acfccd848a7de26889f6857cbad559076/src/handle_props.rs#L655
+fn handle_prop_path<W: Write>(out: &mut XmlWriter<W>, path: &Path, meta: &Metadata, prop: XmlName) -> Result<bool, XmlWError> {
+    if prop.namespace == Some(WEBDAV_XML_NAMESPACE_DAV.1) {
+        match prop.local_name {
+            "creationdate" => {
+                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "creationdate")))?;
+                out.write(XmlWEvent::characters(&file_time_created(meta).rfc3339().to_string()))?;
+            }
+
+            "getcontentlength" => {
+                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "getcontentlength")))?;
+                out.write(XmlWEvent::characters(&file_length(&meta, &path).to_string()))?;
+            }
+
+            "getcontenttype" => {
+                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "getcontenttype")))?;
+                let mime_type = guess_mime_type_opt(&path).unwrap_or_else(|| if file_binary(&path) {
+                    "application/octet-stream".parse().unwrap()
+                } else {
+                    "text/plain".parse().unwrap()
+                });
+                out.write(XmlWEvent::characters(&mime_type.to_string()))?;
+            }
+
+            "getlastmodified" => {
+                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "getlastmodified")))?;
+                out.write(XmlWEvent::characters(&file_time_modified(meta).rfc3339().to_string()))?;
+            }
+
+            "resourcetype" => {
+                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "resourcetype")))?;
+                if !is_actually_file(&meta.file_type()) {
+                    out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "collection")))?;
+                    out.write(XmlWEvent::end_element())?;
+                }
+            }
+
+            _ => return Ok(false),
+        }
+    } else if prop.namespace == Some(WEBDAV_XML_NAMESPACE_MICROSOFT.1) {
+        match prop.local_name {
+            "Win32CreationTime" => {
+                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_MICROSOFT.0, "Win32CreationTime")))?;
+                out.write(XmlWEvent::characters(&file_time_created(meta).rfc3339().to_string()))?;
+            }
+
+            "Win32FileAttributes" => {
+                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_MICROSOFT.0, "Win32FileAttributes")))?;
+
+                let attr = win32_file_attributes(meta, path);
+                out.write(XmlWEvent::characters(&format!("{:08x}", attr)))?;
+            }
+
+            "Win32LastAccessTime" => {
+                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_MICROSOFT.0, "Win32FileAttributes")))?;
+                out.write(XmlWEvent::characters(&file_time_accessed(meta).rfc3339().to_string()))?;
+            }
+
+            "Win32LastModifiedTime" => {
+                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_MICROSOFT.0, "Win32LastModifiedTime")))?;
+                out.write(XmlWEvent::characters(&file_time_modified(meta).rfc3339().to_string()))?;
+            }
+
+            _ => return Ok(false),
+        }
+    } else if prop.namespace == Some(WEBDAV_XML_NAMESPACE_APACHE.1) {
+        match prop.local_name {
+            "executable" => {
+                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_APACHE.0, "executable")))?;
+                out.write(XmlWEvent::characters(if file_executable(&meta) { "T" } else { "F" }))?;
+            }
+
+            _ => return Ok(false),
+        }
+    } else {
+        return Ok(false);
+    }
+
+    out.write(XmlWEvent::end_element())?;
+    Ok(true)
+}
+
+/// Adapted from
+/// https://github.com/tylerwhall/hyperdav-server/blob/415f512ac030478593ad389a3267aeed7441d826/src/lib.rs#L214
+fn start_client_prop_element<W: Write>(out: &mut XmlWriter<W>, prop: XmlName) -> Result<(), XmlWError> {
+    if let Some(prop_namespace) = prop.namespace {
+        if let Some((prefix, _)) = WEBDAV_XML_NAMESPACES.iter().find(|(_, ns)| *ns == prop_namespace) {
+            return out.write(XmlWEvent::start_element(XmlName { prefix: Some(prefix), ..prop }));
+        }
+
+        if prop.prefix.map(|prop_prefix| WEBDAV_XML_NAMESPACES.iter().any(|(pf, _)| *pf == prop_prefix)).unwrap_or(true) {
+            return out.write(XmlWEvent::start_element(XmlName { prefix: Some("U"), ..prop }).ns("U", prop_namespace));
+        }
+    }
+
+    out.write(XmlWEvent::start_element(prop))
+}
+
+/// https://tools.ietf.org/html/rfc2518#section-12.13
+fn parse_proppatch(req: &mut Request) -> Result<Vec<(OwnedXmlName, bool)>, String> {
+    #[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
+    enum State {
+        Start,
+        PropertyUpdate,
+        Action,
+        Prop,
+        InProp,
+    }
+
+
+    let mut xml = XmlReader::new_with_config(&mut req.body, DEFAULT_XML_PARSER_CONFIG.clone());
+    let mut state = State::Start;
+    let mut props = vec![];
+    let mut propname = None;
+    let mut is_remove = false;
+
+    loop {
+        let event = xml.next().map_err(|e| e.to_string())?;
+
+        match (state, event) {
+            (State::Start, XmlREvent::StartDocument { .. }) => (),
+            (State::Start, XmlREvent::StartElement { ref name, .. }) if name.local_name == "propertyupdate" => state = State::PropertyUpdate,
+
+            (State::PropertyUpdate, XmlREvent::StartElement { ref name, .. }) if name.local_name == "set" => {
+                state = State::Action;
+                is_remove = false;
+            }
+            (State::PropertyUpdate, XmlREvent::StartElement { ref name, .. }) if name.local_name == "remove" => {
+                state = State::Action;
+                is_remove = true;
+            }
+            (State::PropertyUpdate, XmlREvent::EndElement { .. }) => return Ok(props),
+
+            (State::Action, XmlREvent::StartElement { ref name, .. }) if name.local_name == "prop" => state = State::Prop,
+            (State::Action, XmlREvent::EndElement { .. }) => state = State::PropertyUpdate,
+
+            (State::Prop, XmlREvent::StartElement { name, .. }) => {
+                state = State::InProp;
+                propname = Some(name.clone());
+                props.push((name, is_remove));
+            }
+            (State::Prop, XmlREvent::EndElement { .. }) => state = State::Action,
+
+            (State::InProp, XmlREvent::EndElement { name, .. }) => {
+                if Some(name) == propname {
+                    state = State::Prop;
+                }
+            }
+            (State::InProp, _) => {}
+
+            (st, ev) => return Err(format!("Unexpected event {:?} during state {:?}", ev, st)),
+        }
+    }
+}
+
+fn write_proppatch_output(props: &[(OwnedXmlName, bool)], req_url: &GenericUrl) -> Result<Result<Vec<u8>, IronResult<Response>>, XmlWError> {
+    let mut out = intialise_xml_output()?;
+    out.write(namespaces_for_props("D:multistatus", props.iter().map(|pp| &pp.0)))?;
+
+    out.write(XmlWEvent::start_element("D:href"))?;
+    out.write(XmlWEvent::characters(req_url.as_str()))?;
+    out.write(XmlWEvent::end_element())?;
+
+    out.write(XmlWEvent::start_element("D:propstat"))?;
+
+    for (name, _) in props {
+        out.write(XmlWEvent::start_element("D:prop"))?;
+
+        start_client_prop_element(&mut out, name.borrow())?;
+        out.write(XmlWEvent::end_element())?;
+
+        out.write(XmlWEvent::end_element())?;
+    }
+
+    out.write(XmlWEvent::start_element("D:status"))?;
+    out.write(XmlWEvent::characters("HTTP/1.1 409 Conflict"))?;
+    out.write(XmlWEvent::end_element())?;
+
+    out.write(XmlWEvent::end_element())?;
+
+    out.write(XmlWEvent::end_element())?;
+
+    Ok(Ok(out.into_inner()))
+}
+
+fn copy_response(op_result: IoResult<()>, overwritten: bool) -> IronResult<Response> {
+    match op_result {
+        Ok(_) => {
+            if overwritten {
+                Ok(Response::with(status::NoContent))
+            } else {
+                Ok(Response::with(status::Created))
+            }
+        }
+        Err(_) => Ok(Response::with(status::InsufficientStorage)),
+    }
+}
+
+fn copy_response_multierror(errors: &[(IoError, String)], req_url: &GenericUrl) -> Result<Vec<u8>, XmlWError> {
+    let mut out = intialise_xml_output()?;
+    out.write(XmlWEvent::start_element("D:multistatus").ns(WEBDAV_XML_NAMESPACE_DAV.0, WEBDAV_XML_NAMESPACE_DAV.1))?;
+    out.write(XmlWEvent::start_element("D:response"))?;
+
+    for (_, subp) in errors {
+        out.write(XmlWEvent::start_element("D:href"))?;
+        out.write(XmlWEvent::characters(req_url.join(subp).expect("Couldn't append errored path to url").as_str()))?;
+        out.write(XmlWEvent::end_element())?;
+    }
+
+    out.write(XmlWEvent::start_element("D:status"))?;
+    out.write(XmlWEvent::characters("HTTP/1.1 507 Insufficient Storage"))?;
+    out.write(XmlWEvent::end_element())?;
+
+    out.write(XmlWEvent::end_element())?;
+
+    out.write(XmlWEvent::end_element())?;
+
+    Ok(out.into_inner())
+}
+
+fn intialise_xml_output() -> Result<XmlWriter<Vec<u8>>, XmlWError> {
+    let mut out = XmlWriter::new_with_config(vec![], DEFAULT_XML_EMITTER_CONFIG.clone());
+
+    out.write(XmlWEvent::StartDocument {
+            version: XmlVersion::Version10,
+            encoding: Some("utf-8"),
+            standalone: None,
+        })?;
+
+    Ok(out)
+}
+
+fn namespaces_for_props<'n, N: 'n + BorrowXmlName<'n>, Ni: Iterator<Item = &'n N>>(elem_name: &str, props: Ni) -> XmlWEventStartElementBuilder {
+    let mut bldr = XmlWEvent::start_element(elem_name).ns(WEBDAV_XML_NAMESPACES[0].0, WEBDAV_XML_NAMESPACES[0].1);
+
+    for prop_namespace in props.map(|p| p.borrow().borrow_xml_name()).flat_map(|p| p.namespace).unique() {
+        if let Some((prefix, namespace)) = WEBDAV_XML_NAMESPACES[1..].iter().find(|(_, ns)| *ns == prop_namespace) {
+            bldr = bldr.ns(*prefix, *namespace);
+        }
+    }
+
+    bldr
+}
diff --git a/src/options.rs b/src/options.rs
index 7f0aa8b..63d3397 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -61,6 +61,8 @@ pub struct Options {
     ///   * >= 2 – suppress startup except for auth data, if present
     ///   * >= 3 – suppress all startup messages
     pub loglevel: LogLevel,
+    /// Whether to handle WebDAV requests. Default: false
+    pub webdav: bool,
     /// Data for HTTPS, identity file and password. Default: `None`
     pub tls_data: Option<((String, PathBuf), String)>,
     /// Whether to generate a one-off certificate. Default: false
@@ -92,6 +94,7 @@ impl Options {
             .arg(Arg::from_usage("-i --no-indices 'Always generate dir listings even if index files are available. Default: false'"))
             .arg(Arg::from_usage("-e --no-encode 'Do not encode filesystem files. Default: false'"))
             .arg(Arg::from_usage("-q --quiet... 'Suppress increasing amounts of output'"))
+            .arg(Arg::from_usage("-d --webdav 'Handle WebDAV requests. Default: false'"))
             .arg(Arg::from_usage("--ssl [TLS_IDENTITY] 'Data for HTTPS, identity file. Password in HTTP_SSL_PASS env var, otherwise empty'")
                 .validator(Options::identity_validator))
             .arg(Arg::from_usage("--gen-ssl 'Generate a one-off TLS certificate'").conflicts_with("ssl"))
@@ -166,7 +169,8 @@ impl Options {
             allow_writes: matches.is_present("allow-write"),
             encode_fs: !matches.is_present("no-encode"),
             loglevel: matches.occurrences_of("quiet").into(),
-            tls_data: matches.value_of("ssl").map(|id| ((id.to_string(), fs::canonicalize(id).unwrap()), env::var("HTTP_SSL_PASS").unwrap_or(String::new()))),
+            webdav: matches.is_present("webdav"),
+            tls_data: matches.value_of("ssl").map(|id| ((id.to_string(), fs::canonicalize(id).unwrap()), env::var("HTTP_SSL_PASS").unwrap_or_default())),
             generate_tls: matches.is_present("gen-ssl"),
             path_auth_data: path_auth_data,
             generate_path_auth: generate_path_auth,
diff --git a/src/util/content_encoding.rs b/src/util/content_encoding.rs
index 3f47484..82a9229 100644
--- a/src/util/content_encoding.rs
+++ b/src/util/content_encoding.rs
@@ -17,7 +17,7 @@ lazy_static! {
     /// The list of content encodings we handle.
     pub static ref SUPPORTED_ENCODINGS: Vec<Encoding> = {
         let es = vec![Encoding::Gzip, Encoding::Deflate, Encoding::EncodingExt("br".to_string()), Encoding::EncodingExt("bzip2".to_string())];
-        [es.clone(), es.into_iter().map(|e| Encoding::EncodingExt(format!("x-{}", e))).collect()].into_iter().flat_map(|e| e.clone()).collect()
+        [es.clone(), es.into_iter().map(|e| Encoding::EncodingExt(format!("x-{}", e))).collect()].iter().flat_map(|e| e.clone()).collect()
     };
 
     /// The list of extensions not to encode.
@@ -46,7 +46,7 @@ pub fn response_encoding(requested: &mut [QualityItem<Encoding>]) -> Option<Enco
 /// Encode a string slice using a specified encoding or `None` if encoding failed or is not recognised.
 pub fn encode_str(dt: &str, enc: &Encoding) -> Option<Vec<u8>> {
     type EncodeT = fn(&str) -> Option<Vec<u8>>;
-    static STR_ENCODING_FNS: &'static [EncodeT] = &[encode_str_gzip, encode_str_deflate, encode_str_brotli, encode_str_bzip2];
+    static STR_ENCODING_FNS: &[EncodeT] = &[encode_str_gzip, encode_str_deflate, encode_str_brotli, encode_str_bzip2];
 
     encoding_idx(enc).and_then(|fi| STR_ENCODING_FNS[fi](dt))
 }
@@ -55,7 +55,7 @@ pub fn encode_str(dt: &str, enc: &Encoding) -> Option<Vec<u8>> {
 /// `false` if encoding failed, is not recognised or an I/O error occurred.
 pub fn encode_file(p: &Path, op: &Path, enc: &Encoding) -> bool {
     type EncodeT = fn(File, File) -> bool;
-    static FILE_ENCODING_FNS: &'static [EncodeT] = &[encode_file_gzip, encode_file_deflate, encode_file_brotli, encode_file_bzip2];
+    static FILE_ENCODING_FNS: &[EncodeT] = &[encode_file_gzip, encode_file_deflate, encode_file_brotli, encode_file_bzip2];
 
     encoding_idx(enc)
         .map(|fi| {
@@ -69,7 +69,7 @@ pub fn encode_file(p: &Path, op: &Path, enc: &Encoding) -> bool {
 
 /// Encoding extension to use for encoded files, for example "gz" for gzip, or `None` if the encoding is not recognised.
 pub fn encoding_extension(enc: &Encoding) -> Option<&'static str> {
-    static ENCODING_EXTS: &'static [&'static str] = &["gz", "dflt", "br", "bz2"];
+    static ENCODING_EXTS: &[&str] = &["gz", "dflt", "br", "bz2"];
 
     encoding_idx(enc).map(|ei| ENCODING_EXTS[ei])
 }
diff --git a/src/util/mod.rs b/src/util/mod.rs
index def756a..5c746b5 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -2,37 +2,41 @@
 
 
 mod os;
+mod webdav;
 mod content_encoding;
 
 use base64;
 use std::path::Path;
 use percent_encoding;
+use walkdir::WalkDir;
 use std::borrow::Cow;
 use rfsapi::RawFileData;
-use std::{cmp, f64, fmt};
-use iron::headers::UserAgent;
+use std::time::SystemTime;
+use std::{cmp, f64, fmt, str};
 use std::collections::HashMap;
 use time::{self, Duration, Tm};
 use iron::{mime, Headers, Url};
-use std::io::{BufReader, BufRead};
 use base64::display::Base64Display;
-use std::fs::{self, FileType, File};
-use iron::headers::{HeaderFormat, Header};
 use iron::error::HttpResult as HyperResult;
+use std::fs::{self, FileType, Metadata, File};
+use iron::headers::{HeaderFormat, UserAgent, Header};
 use mime_guess::{guess_mime_type_opt, get_mime_type_str};
+use xml::name::{OwnedName as OwnedXmlName, Name as XmlName};
+use std::io::{ErrorKind as IoErrorKind, BufReader, BufRead, Result as IoResult, Error as IoError};
 
 pub use self::os::*;
+pub use self::webdav::*;
 pub use self::content_encoding::*;
 
 
 /// The generic HTML page to use as response to errors.
-pub static ERROR_HTML: &'static str = include_str!("../../assets/error.html");
+pub static ERROR_HTML: &str = include_str!("../../assets/error.html");
 
 /// The HTML page to use as template for a requested directory's listing.
-pub static DIRECTORY_LISTING_HTML: &'static str = include_str!("../../assets/directory_listing.html");
+pub static DIRECTORY_LISTING_HTML: &str = include_str!("../../assets/directory_listing.html");
 
 /// The HTML page to use as template for a requested directory's listing for mobile devices.
-pub static MOBILE_DIRECTORY_LISTING_HTML: &'static str = include_str!("../../assets/directory_listing_mobile.html");
+pub static MOBILE_DIRECTORY_LISTING_HTML: &str = include_str!("../../assets/directory_listing_mobile.html");
 
 lazy_static! {
     /// Collection of data to be injected into generated responses.
@@ -80,10 +84,10 @@ pub static PORT_SCAN_LOWEST: u16 = 8000;
 pub static PORT_SCAN_HIGHEST: u16 = 9999;
 
 /// The app name and version to use with User-Agent or Server response header.
-pub static USER_AGENT: &'static str = concat!("http/", env!("CARGO_PKG_VERSION"));
+pub static USER_AGENT: &str = concat!("http/", env!("CARGO_PKG_VERSION"));
 
 /// Index file extensions to look for if `-i` was not specified.
-pub static INDEX_EXTENSIONS: &'static [&'static str] = &["html", "htm", "shtml"];
+pub static INDEX_EXTENSIONS: &[&str] = &["html", "htm", "shtml"];
 
 
 /// The [WWW-Authenticate header](https://tools.ietf.org/html/rfc7235#section-4.1), without parsing.
@@ -109,6 +113,46 @@ impl HeaderFormat for WwwAuthenticate {
     }
 }
 
+#[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
+pub struct CommaList<D: fmt::Display, I: Iterator<Item = D>>(pub I);
+
+impl<D: fmt::Display, I: Iterator<Item = D> + Clone> fmt::Display for CommaList<D, I> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let mut itr = self.0.clone();
+        if let Some(item) = itr.next() {
+            item.fmt(f)?;
+
+            for item in itr {
+                f.write_str(", ")?;
+                item.fmt(f)?;
+            }
+        }
+
+        Ok(())
+    }
+}
+
+
+/// `xml`'s `OwnedName::borrow()` returns a value not a reference, so it cannot be used with the libstd `Borrow` trait
+pub trait BorrowXmlName<'n> {
+    fn borrow_xml_name(&'n self) -> XmlName<'n>;
+}
+
+impl<'n> BorrowXmlName<'n> for XmlName<'n> {
+    #[inline(always)]
+    fn borrow_xml_name(&'n self) -> XmlName<'n> {
+        *self
+    }
+}
+
+impl<'n> BorrowXmlName<'n> for OwnedXmlName {
+    #[inline(always)]
+    fn borrow_xml_name(&'n self) -> XmlName<'n> {
+        self.borrow()
+    }
+}
+
+
 
 /// Uppercase the first character of the supplied string.
 ///
@@ -143,7 +187,10 @@ pub fn uppercase_first(s: &str) -> String {
 /// assert!(!file_binary("Cargo.toml"));
 /// ```
 pub fn file_binary<P: AsRef<Path>>(path: P) -> bool {
-    let path = path.as_ref();
+    file_binary_impl(path.as_ref())
+}
+
+fn file_binary_impl(path: &Path) -> bool {
     path.metadata()
         .map(|m| is_device(&m.file_type()) || File::open(path).and_then(|f| BufReader::new(f).read_line(&mut String::new())).is_err())
         .unwrap_or(true)
@@ -203,8 +250,37 @@ pub fn percent_decode(s: &str) -> Option<Cow<str>> {
 }
 
 /// Get the timestamp of the file's last modification as a `time::Tm` in UTC.
-pub fn file_time_modified(f: &Path) -> Tm {
-    match f.metadata().expect("Failed to get file metadata").modified().expect("Failed to get file last modified date").elapsed() {
+pub fn file_time_modified_p(f: &Path) -> Tm {
+    file_time_modified(&f.metadata().expect("Failed to get file metadata"))
+}
+
+/// Get the timestamp of the file's last modification as a `time::Tm` in UTC.
+pub fn file_time_created_p(f: &Path) -> Tm {
+    file_time_created(&f.metadata().expect("Failed to get file metadata"))
+}
+
+/// Get the timestamp of the file's last access as a `time::Tm` in UTC.
+pub fn file_time_accessed_p(f: &Path) -> Tm {
+    file_time_accessed(&f.metadata().expect("Failed to get file metadata"))
+}
+
+/// Get the timestamp of the file's last modification as a `time::Tm` in UTC.
+pub fn file_time_modified(m: &Metadata) -> Tm {
+    file_time_impl(m.modified().expect("Failed to get file last modified date"))
+}
+
+/// Get the timestamp of the file's last modification as a `time::Tm` in UTC.
+pub fn file_time_created(m: &Metadata) -> Tm {
+    file_time_impl(m.created().expect("Failed to get file created date"))
+}
+
+/// Get the timestamp of the file's last access as a `time::Tm` in UTC.
+pub fn file_time_accessed(m: &Metadata) -> Tm {
+    file_time_impl(m.accessed().expect("Failed to get file accessed date"))
+}
+
+fn file_time_impl(time: SystemTime) -> Tm {
+    match time.elapsed() {
         Ok(dur) => time::now_utc() - Duration::from_std(dur).unwrap(),
         Err(ste) => time::now_utc() + Duration::from_std(ste.duration()).unwrap(),
     }
@@ -327,6 +403,13 @@ pub fn client_mobile(hdr: &Headers) -> bool {
     hdr.get::<UserAgent>().map(|s| s.contains("Mobi") || s.contains("mobi")).unwrap_or(false)
 }
 
+/// Check if, given the request headers, the client should be treated as Microsoft software.
+///
+/// Based on https://github.com/miquels/webdav-handler-rs/blob/02433c1acfccd848a7de26889f6857cbad559076/src/handle_props.rs#L529
+pub fn client_microsoft(hdr: &Headers) -> bool {
+    hdr.get::<UserAgent>().map(|s| s.contains("Microsoft") || s.contains("microsoft")).unwrap_or(false)
+}
+
 /// Get the suffix for the icon to use to represent the given file.
 pub fn file_icon_suffix<P: AsRef<Path>>(f: P, is_file: bool) -> &'static str {
     if is_file {
@@ -347,7 +430,11 @@ pub fn file_icon_suffix<P: AsRef<Path>>(f: P, is_file: bool) -> &'static str {
 ///
 /// The specified path must point to a file.
 pub fn get_raw_fs_metadata<P: AsRef<Path>>(f: P) -> RawFileData {
-    let f = f.as_ref();
+    get_raw_fs_metadata_impl(f.as_ref())
+}
+
+fn get_raw_fs_metadata_impl(f: &Path) -> RawFileData {
+    let meta = f.metadata().expect("Failed to get requested file metadata");
     RawFileData {
         mime_type: guess_mime_type_opt(f).unwrap_or_else(|| if file_binary(f) {
             "application/octet-stream".parse().unwrap()
@@ -355,8 +442,63 @@ pub fn get_raw_fs_metadata<P: AsRef<Path>>(f: P) -> RawFileData {
             "text/plain".parse().unwrap()
         }),
         name: f.file_name().unwrap().to_str().expect("Failed to get requested file name").to_string(),
-        last_modified: file_time_modified(f),
-        size: f.metadata().expect("Failed to get requested file metadata").len(),
+        last_modified: file_time_modified(&meta),
+        size: file_length(&meta, &f),
         is_file: true,
     }
 }
+
+/// Recursively copy a directory
+///
+/// Stolen from https://github.com/mdunsmuir/copy_dir/blob/0.1.2/src/lib.rs
+pub fn copy_dir(from: &Path, to: &Path) -> IoResult<Vec<(IoError, String)>> {
+    macro_rules! push_error {
+        ($vec:ident, $path:ident, $expr:expr) => {
+            match $expr {
+                Ok(_) => (),
+                Err(e) => $vec.push((e, $path.to_string_lossy().into_owned())),
+            }
+        };
+    }
+
+    let mut errors = Vec::new();
+
+    fs::create_dir(&to)?;
+
+    // The approach taken by this code (i.e. walkdir) will not gracefully
+    // handle copying a directory into itself, so we're going to simply
+    // disallow it by checking the paths. This is a thornier problem than I
+    // wish it was, and I'd like to find a better solution, but for now I
+    // would prefer to return an error rather than having the copy blow up
+    // in users' faces. Ultimately I think a solution to this will involve
+    // not using walkdir at all, and might come along with better handling
+    // of hard links.
+    if from.canonicalize().and_then(|fc| to.canonicalize().map(|tc| (fc, tc))).map(|(fc, tc)| tc.starts_with(fc))? {
+        fs::remove_dir(&to)?;
+
+        return Err(IoError::new(IoErrorKind::Other, "cannot copy to a path prefixed by the source path"));
+    }
+
+    for entry in WalkDir::new(&from).min_depth(1).into_iter().flatten() {
+        let source_metadata = match entry.metadata() {
+            Ok(md) => md,
+            Err(err) => {
+                errors.push((err.into(), entry.path().to_string_lossy().into_owned()));
+                continue;
+            }
+        };
+
+        let relative_path = entry.path().strip_prefix(&from).expect("strip_prefix failed; this is a probably a bug in copy_dir");
+
+        let target_path = to.join(relative_path);
+
+        if !is_actually_file(&source_metadata.file_type()) {
+            push_error!(errors, relative_path, fs::create_dir(&target_path));
+            push_error!(errors, relative_path, fs::set_permissions(&target_path, source_metadata.permissions()));
+        } else {
+            push_error!(errors, relative_path, fs::copy(entry.path(), &target_path));
+        }
+    }
+
+    Ok(errors)
+}
diff --git a/src/util/os/mod.rs b/src/util/os/mod.rs
index 4103c02..42cf160 100644
--- a/src/util/os/mod.rs
+++ b/src/util/os/mod.rs
@@ -1,9 +1,17 @@
+#[cfg(target_os = "windows")]
+mod windows;
+#[cfg(not(target_os = "windows"))]
+mod non_windows;
 #[cfg(any(target_os = "windows", target_os = "macos"))]
 mod windows_macos;
 #[cfg(not(any(target_os = "windows", target_os = "macos")))]
 mod non_windows_non_macos;
 
+#[cfg(target_os = "windows")]
+pub use self::windows::*;
+#[cfg(not(target_os = "windows"))]
+pub use self::non_windows::*;
 #[cfg(any(target_os = "windows", target_os = "macos"))]
-pub use self::windows_macos::{file_length, is_device};
+pub use self::windows_macos::*;
 #[cfg(not(any(target_os = "windows", target_os = "macos")))]
-pub use self::non_windows_non_macos::{file_length, is_device};
+pub use self::non_windows_non_macos::*;
diff --git a/src/util/os/non_windows.rs b/src/util/os/non_windows.rs
new file mode 100644
index 0000000..e443936
--- /dev/null
+++ b/src/util/os/non_windows.rs
@@ -0,0 +1,37 @@
+use self::super::super::is_actually_file;
+use os_str_generic::OsStrGenericExt;
+use std::fs::Metadata;
+use std::path::Path;
+
+
+const FILE_ATTRIBUTE_READONLY: u32 = 0x01;
+const FILE_ATTRIBUTE_HIDDEN: u32 = 0x02;
+const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
+const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x20;
+
+
+/// Get windows-style attributes for the specified file
+///
+/// https://docs.microsoft.com/en-gb/windows/win32/fileio/file-attribute-constants
+pub fn win32_file_attributes(meta: &Metadata, path: &Path) -> u32 {
+    let mut attr = 0;
+
+    if meta.permissions().readonly() {
+        attr |= FILE_ATTRIBUTE_READONLY;
+    }
+
+    if path.file_name().map(|n| n.starts_with(".")).unwrap_or(false) {
+        attr |= FILE_ATTRIBUTE_HIDDEN;
+    }
+
+    if !is_actually_file(&meta.file_type()) {
+        attr |= FILE_ATTRIBUTE_DIRECTORY;
+    } else {
+        // this is the 'Archive' bit, which is set by
+        // default on _all_ files on creation and on
+        // modification.
+        attr |= FILE_ATTRIBUTE_ARCHIVE;
+    }
+
+    attr
+}
diff --git a/src/util/os/non_windows_non_macos.rs b/src/util/os/non_windows_non_macos.rs
index 1d635ae..2c3248d 100644
--- a/src/util/os/non_windows_non_macos.rs
+++ b/src/util/os/non_windows_non_macos.rs
@@ -1,5 +1,5 @@
+use std::os::unix::fs::{PermissionsExt, FileTypeExt};
 use libc::{O_RDONLY, c_ulong, close, ioctl, open};
-use std::os::unix::fs::FileTypeExt;
 use std::os::unix::ffi::OsStrExt;
 use std::fs::{FileType, Metadata};
 use std::ffi::CString;
@@ -9,17 +9,31 @@ use std::path::Path;
 include!(concat!(env!("OUT_DIR"), "/ioctl-data/ioctl.rs"));
 
 
+// Stolen from https://unix.superglobalmegacorp.com/Net2/newsrc/sys/stat.h.html
+/// X for owner
+const S_IXUSR: u32 = 0o000100;
+/// X for group
+const S_IXGRP: u32 = 0o000010;
+/// X for other
+const S_IXOTH: u32 = 0o000001;
+
+
 /// OS-specific check for fileness
 pub fn is_device(tp: &FileType) -> bool {
     tp.is_block_device() || tp.is_char_device() || tp.is_fifo() || tp.is_socket()
 }
 
 /// Check file length responsibly
+#[inline(always)]
 pub fn file_length<P: AsRef<Path>>(meta: &Metadata, path: &P) -> u64 {
+    file_length_impl(meta, path.as_ref())
+}
+
+fn file_length_impl(meta: &Metadata, path: &Path) -> u64 {
     if is_device(&meta.file_type()) {
         let mut block_count: c_ulong = 0;
 
-        let path_c = CString::new(path.as_ref().as_os_str().as_bytes()).unwrap();
+        let path_c = CString::new(path.as_os_str().as_bytes()).unwrap();
         let dev_file = unsafe { open(path_c.as_ptr(), O_RDONLY) };
         if dev_file >= 0 {
             let ok = unsafe { ioctl(dev_file, BLKGETSIZE, &mut block_count as *mut c_ulong) } == 0;
@@ -33,3 +47,8 @@ pub fn file_length<P: AsRef<Path>>(meta: &Metadata, path: &P) -> u64 {
 
     meta.len()
 }
+
+/// Check if file is marked executable
+pub fn file_executable(meta: &Metadata) -> bool {
+    (meta.permissions().mode() & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0
+}
diff --git a/src/util/os/windows.rs b/src/util/os/windows.rs
new file mode 100644
index 0000000..0686de2
--- /dev/null
+++ b/src/util/os/windows.rs
@@ -0,0 +1,15 @@
+use winapi::um::fileapi::GetFileAttributesW;
+use std::os::windows::ffi::OsStrExt;
+use std::fs::Metadata;
+use std::path::Path;
+
+
+/// Get windows-style attributes for the specified file
+///
+/// https://docs.microsoft.com/en-gb/windows/win32/fileio/file-attribute-constants
+pub fn win32_file_attributes(_: &Metadata, path: &Path) -> u32 {
+    let mut buf: Vec<_> = path.as_os_str().encode_wide().collect();
+    buf.push(0);
+
+    unsafe { GetFileAttributesW(buf.as_ptr()) }
+}
diff --git a/src/util/os/windows_macos.rs b/src/util/os/windows_macos.rs
index 3d2b6e8..7f10521 100644
--- a/src/util/os/windows_macos.rs
+++ b/src/util/os/windows_macos.rs
@@ -3,11 +3,19 @@ use std::path::Path;
 
 
 /// OS-specific check for fileness
+#[inline(always)]
 pub fn is_device(_: &FileType) -> bool {
     false
 }
 
 /// Check file length responsibly
+#[inline(always)]
 pub fn file_length<P: AsRef<Path>>(meta: &Metadata, _: &P) -> u64 {
     meta.len()
 }
+
+/// Check if file is marked executable
+#[inline(always)]
+pub fn file_executable(_: &Metadata) -> bool {
+    true
+}
diff --git a/src/util/webdav.rs b/src/util/webdav.rs
new file mode 100644
index 0000000..243707a
--- /dev/null
+++ b/src/util/webdav.rs
@@ -0,0 +1,232 @@
+use iron::method;
+use std::{fmt, str};
+use xml::name::Name as XmlName;
+use iron::url::Url as GenericUrl;
+use iron::headers::{HeaderFormat, Header};
+use iron::error::{HttpResult as HyperResult, HttpError as HyperError};
+
+
+macro_rules! xml_name {
+    ($ns:expr, $ln:expr) => {
+        XmlName {
+            local_name: $ln,
+            namespace: Some($ns.1),
+            prefix: Some($ns.0),
+        }
+    }
+}
+
+
+lazy_static! {
+    /// HTTP methods we support for WebDAV level 1, as specified in https://tools.ietf.org/html/rfc2518, without locks
+    pub static ref DAV_LEVEL_1_METHODS: Vec<method::Method> =
+        ["COPY", "MKCOL", "MOVE", "PROPFIND", "PROPPATCH"].iter().map(|m| method::Extension(m.to_string())).collect();
+}
+
+/// Prefix and namespace URI for generic WebDAV elements
+pub static WEBDAV_XML_NAMESPACE_DAV: (&str, &str) = ("D", "DAV:");
+
+/// Prefix and namespace URI for elements specific to Windows clients
+pub static WEBDAV_XML_NAMESPACE_MICROSOFT: (&str, &str) = ("Z", "urn:schemas-microsoft-com:");
+
+/// Prefix and namespace URI for elements for Apache emulation
+pub static WEBDAV_XML_NAMESPACE_APACHE: (&str, &str) = ("A", "http://apache.org/dav/props/");
+
+/// All first-class-recognised prefix/namespace pairs
+///
+/// `WEBDAV_XML_NAMESPACE_DAV` needs to be the first here
+pub static WEBDAV_XML_NAMESPACES: &[&(&str, &str)] = &[&WEBDAV_XML_NAMESPACE_DAV, &WEBDAV_XML_NAMESPACE_MICROSOFT, &WEBDAV_XML_NAMESPACE_APACHE];
+
+/// Properties to return on empty body or [`<allprop />`](https://tools.ietf.org/html/rfc2518#section-12.14.1)
+/// for non-Windows clients
+///
+/// Based on https://github.com/miquels/webdav-handler-rs/blob/02433c1acfccd848a7de26889f6857cbad559076/src/handle_props.rs#L52
+pub static WEBDAV_ALLPROP_PROPERTIES_NON_WINDOWS: &[&[XmlName]] = &[&[xml_name!(WEBDAV_XML_NAMESPACE_DAV, "creationdate"),
+                                                                      xml_name!(WEBDAV_XML_NAMESPACE_DAV, "getcontentlength"),
+                                                                      xml_name!(WEBDAV_XML_NAMESPACE_DAV, "getcontenttype"),
+                                                                      xml_name!(WEBDAV_XML_NAMESPACE_DAV, "getlastmodified"),
+                                                                      xml_name!(WEBDAV_XML_NAMESPACE_DAV, "resourcetype")]];
+
+/// Properties to return on empty body or [`<allprop />`](https://tools.ietf.org/html/rfc2518#section-12.14.1)
+/// for Windows clients
+///
+/// Based on https://github.com/miquels/webdav-handler-rs/blob/02433c1acfccd848a7de26889f6857cbad559076/src/handle_props.rs#L66
+pub static WEBDAV_ALLPROP_PROPERTIES_WINDOWS: &[&[XmlName]] = &[&WEBDAV_ALLPROP_PROPERTIES_NON_WINDOWS[0],
+                                                                &[xml_name!(WEBDAV_XML_NAMESPACE_MICROSOFT, "Win32CreationTime"),
+                                                                  xml_name!(WEBDAV_XML_NAMESPACE_MICROSOFT, "Win32FileAttributes"),
+                                                                  xml_name!(WEBDAV_XML_NAMESPACE_MICROSOFT, "Win32LastAccessTime"),
+                                                                  xml_name!(WEBDAV_XML_NAMESPACE_MICROSOFT, "Win32LastModifiedTime")]];
+
+/// Properties listed for a [`<propname />`](https://tools.ietf.org/html/rfc2518#section-12.14.2) request
+///
+/// Based on https://github.com/miquels/webdav-handler-rs/blob/02433c1acfccd848a7de26889f6857cbad559076/src/handle_props.rs#L34
+pub static WEBDAV_PROPNAME_PROPERTIES: &[&[XmlName]] = &[&WEBDAV_ALLPROP_PROPERTIES_NON_WINDOWS[0],
+                                                         &[xml_name!(WEBDAV_XML_NAMESPACE_APACHE, "executable"),
+                                                           xml_name!(WEBDAV_XML_NAMESPACE_MICROSOFT, "Win32LastAccessTime")]];
+
+
+
+/// The [DAV header](https://tools.ietf.org/html/rfc2518#section-9.1), without parsing.
+///
+/// We don't ever receive this header, only ever send it, so this is fine.
+#[derive(Debug, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
+pub struct Dav(pub &'static [&'static str]);
+
+impl Dav {
+    pub const LEVEL_1: Dav = Dav(&["1"]);
+}
+
+impl Header for Dav {
+    fn header_name() -> &'static str {
+        "DAV"
+    }
+
+    /// Dummy impl returning an empty value, since we're only ever sending these
+    fn parse_header(_: &[Vec<u8>]) -> HyperResult<Dav> {
+        Ok(Dav(&[]))
+    }
+}
+
+impl HeaderFormat for Dav {
+    fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.write_str(self.0[0])?;
+        for lvl in self.0.iter().skip(1) {
+            f.write_str(", ")?;
+            f.write_str(lvl)?;
+        }
+        Ok(())
+    }
+}
+
+/// The [Depth header](https://tools.ietf.org/html/rfc2518#section-9.2).
+#[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
+pub enum Depth {
+    Zero,
+    One,
+    Infinity,
+}
+
+impl Depth {
+    /// Get a depth lower than this one by one, if it exists
+    pub fn lower(self) -> Option<Depth> {
+        match self {
+            Depth::Zero => None,
+            Depth::One => Some(Depth::Zero),
+            Depth::Infinity => Some(Depth::Infinity),
+        }
+    }
+}
+
+impl Header for Depth {
+    fn header_name() -> &'static str {
+        "Depth"
+    }
+
+    fn parse_header(raw: &[Vec<u8>]) -> HyperResult<Depth> {
+        if raw.len() != 1 {
+            return Err(HyperError::Header);
+        }
+
+        Ok(match &unsafe { raw.get_unchecked(0) }[..] {
+            b"0" => Depth::Zero,
+            b"1" => Depth::One,
+            b"infinity" => Depth::Infinity,
+            _ => return Err(HyperError::Header),
+        })
+    }
+}
+
+impl HeaderFormat for Depth {
+    fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Depth::Zero => f.write_str("0"),
+            Depth::One => f.write_str("1"),
+            Depth::Infinity => f.write_str("infinity"),
+        }
+    }
+}
+
+impl fmt::Display for Depth {
+    #[inline(always)]
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.fmt_header(f)
+    }
+}
+
+/// The [Destination header](https://tools.ietf.org/html/rfc2518#section-9.3).
+#[derive(Debug, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
+pub struct Destination(pub GenericUrl);
+
+impl Header for Destination {
+    fn header_name() -> &'static str {
+        "Destination"
+    }
+
+    fn parse_header(raw: &[Vec<u8>]) -> HyperResult<Destination> {
+        if raw.len() != 1 {
+            return Err(HyperError::Header);
+        }
+
+        let url = str::from_utf8(&unsafe { raw.get_unchecked(0) }).map_err(|_| HyperError::Header)?;
+        GenericUrl::parse(url).map(Destination).map_err(HyperError::Uri)
+    }
+}
+
+impl HeaderFormat for Destination {
+    fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.write_str(self.0.as_str())
+    }
+}
+
+impl fmt::Display for Destination {
+    #[inline(always)]
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.fmt_header(f)
+    }
+}
+
+/// The [Overwrite header](https://tools.ietf.org/html/rfc2518#section-9.6).
+#[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
+pub struct Overwrite(pub bool);
+
+impl Header for Overwrite {
+    fn header_name() -> &'static str {
+        "Overwrite"
+    }
+
+    fn parse_header(raw: &[Vec<u8>]) -> HyperResult<Overwrite> {
+        if raw.len() != 1 {
+            return Err(HyperError::Header);
+        }
+
+        let val = unsafe { raw.get_unchecked(0) };
+        if val.len() != 1 {
+            return Err(HyperError::Header);
+        }
+        match unsafe { val.get_unchecked(0) } {
+            b'T' => Ok(Overwrite(true)),
+            b'F' => Ok(Overwrite(false)),
+            _ => Err(HyperError::Header),
+        }
+    }
+}
+
+impl HeaderFormat for Overwrite {
+    fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.write_str(if self.0 { "T" } else { "F" })
+    }
+}
+
+impl fmt::Display for Overwrite {
+    #[inline(always)]
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.fmt_header(f)
+    }
+}
+
+impl Default for Overwrite {
+    #[inline(always)]
+    fn default() -> Overwrite {
+        Overwrite(true)
+    }
+}


Test patch: