Repository: thecoshman/http (language: rust)

Issue:
Devices wrongly treated as directories
`/dev/sda`/`/dev/mmcblk0`/etc. are all treated as directories, meaning they can't be downloaded.


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 4ac8390..b0c116e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,7 +9,7 @@ categories = ["network-programming", "web-programming::http-server"]
 license = "MIT"
 build = "build.rs"
 # Remember to also update in appveyor.yml
-version = "1.3.1"
+version = "1.3.2"
 # Remember to also update in http.md
 authors = ["thecoshman <thecoshman@gmail.com>",
            "nabijaczleweli <nabijaczleweli@gmail.com>"]
@@ -41,12 +41,24 @@ default-features = false
 version = "0.5"
 features = ["hyper-native-tls"]
 
+[target.'cfg(not(target_os = "windows"))'.dependencies.libc]
+version = "0.2"
+
 [build-dependencies]
 embed-resource = "1.1"
 
+[target.'cfg(not(target_os = "windows"))'.build-dependencies.gcc]
+version = "0.3"
+
 
 [[bin]]
 name = "http"
 path = "src/main.rs"
 test = false
 doc = false
+
+[[bin]]
+name = "httplz"
+path = "src/main.rs"
+test = false
+doc = false
diff --git a/README.md b/README.md
index 876acd6..05f24c6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# http [![Travis build status](https://travis-ci.org/thecoshman/http.svg)](https://travis-ci.org/thecoshman/http) [![AppVeyor build status](https://ci.appveyor.com/api/projects/status/7knk9lnptn1njro5?svg=true)](https://ci.appveyor.com/project/thecoshman/http) [![Licence](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) [![Crates.io version](http://meritbadge.herokuapp.com/https)](https://crates.io/crates/https)
+# http [![Travis build status](https://travis-ci.org/thecoshman/http.svg)](https://travis-ci.org/thecoshman/http) [![AppVeyor build status](https://ci.appveyor.com/api/projects/status/7knk9lnptn1njro5?svg=true)](https://ci.appveyor.com/project/thecoshman/http) [![Licence](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) [![Crates.io version](https://meritbadge.herokuapp.com/https)](https://crates.io/crates/https)
 Host These Things Please - a basic HTTP server for hosting a folder fast and simply
 
 ## Selected features
diff --git a/appveyor.yml b/appveyor.yml
index d54a5e3..ef43e66 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 1.3.1-{build}
+version: 1.3.2-{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.3.1.exe
-  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.3.1.exe
-  - makensis -DHTTP_VERSION=v1.3.1 "/X!include \"EnvVarUpdate.nsh\"" install.nsi
+  - cp target\release\http.exe http-v1.3.2.exe
+  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.3.2.exe
+  - makensis -DHTTP_VERSION=v1.3.2 "/X!include \"EnvVarUpdate.nsh\"" install.nsi
 
 test: off
 test_script:
   - cargo test --verbose --release
 
 artifacts:
-  - path: http-v1.3.1.exe
-  - path: http v1.3.1 installer.exe
+  - path: http-v1.3.2.exe
+  - path: http v1.3.2 installer.exe
 
 deploy:
   provider: GitHub
-  artifact: /http.*v1.3.1.*\.exe/
+  artifact: /http.*v1.3.2.*\.exe/
   auth_token:
     secure: ZTXvCrv9y01s7Hd60w8W7NaouPnPoaw9YJt9WhWQ2Pep8HLvCikt9Exjkz8SGP9P
   on:
diff --git a/build.rs b/build.rs
index 33429fb..7ec64d8 100644
--- a/build.rs
+++ b/build.rs
@@ -1,6 +1,56 @@
 extern crate embed_resource;
+#[cfg(not(target_os = "windows"))]
+extern crate gcc;
+
+#[cfg(not(target_os = "windows"))]
+use std::env;
+#[cfg(not(target_os = "windows"))]
+use std::io::Write;
+#[cfg(not(target_os = "windows"))]
+use std::path::Path;
+#[cfg(not(target_os = "windows"))]
+use std::fs::{self, File};
+
+
+/// The last line of this, after running it through a preprocessor, will expand to the value of `BLKGETSIZE`
+static IOCTL_CHECK_SOURCE: &str = r#"
+#include <linux/fs.h>
+
+BLKGETSIZE
+"#;
+
+/// Replace `{}` with the `BLKGETSIZE` expression from `IOCTL_CHECK_SOURCE`
+static IOCTL_INCLUDE_SKELETON: &str = r#"
+/// Return `device size / 512` (`long *` arg)
+static BLKGETSIZE: c_ulong = {};
+"#;
 
 
 fn main() {
+    embed_resources();
+    get_ioctl_data();
+}
+
+fn embed_resources() {
     embed_resource::compile("http-manifest.rc");
 }
+
+#[cfg(target_os = "windows")]
+fn get_ioctl_data() {
+
+}
+
+#[cfg(not(target_os = "windows"))]
+fn get_ioctl_data() {
+    let ioctl_dir = Path::new(&env::var("OUT_DIR").unwrap()).join("ioctl-data");
+    fs::create_dir_all(&ioctl_dir).unwrap();
+
+    let ioctl_source = ioctl_dir.join("ioctl.c");
+    File::create(&ioctl_source).unwrap().write_all(IOCTL_CHECK_SOURCE.as_bytes()).unwrap();
+
+    let ioctl_preprocessed = String::from_utf8(gcc::Build::new().file(ioctl_source).expand()).unwrap();
+    let blkgetsize_expr = ioctl_preprocessed.lines().next_back().unwrap().replace("U", " as c_ulong");
+
+    let ioctl_include = ioctl_dir.join("ioctl.rs");
+    File::create(&ioctl_include).unwrap().write_all(IOCTL_INCLUDE_SKELETON.replace("{}", &blkgetsize_expr).as_bytes()).unwrap();
+}
diff --git a/src/main.rs b/src/main.rs
index 32a998b..fc4ae22 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,6 +16,8 @@ extern crate serde;
 #[macro_use]
 extern crate clap;
 extern crate iron;
+#[cfg(not(target_os = "windows"))]
+extern crate libc;
 extern crate time;
 extern crate url;
 extern crate md6;
diff --git a/src/ops.rs b/src/ops.rs
index 6a46d7f..010139b 100644
--- a/src/ops.rs
+++ b/src/ops.rs
@@ -21,10 +21,10 @@ use trivial_colours::{Reset as CReset, Colour as C};
 use std::process::{ExitStatus, Command, Child, Stdio};
 use rfsapi::{RawFsApiHeader, FilesetData, RawFileData};
 use iron::{headers, status, method, mime, IronResult, Listening, Response, TypeMap, Request, Handler, Iron};
-use self::super::util::{url_path, file_hash, is_symlink, encode_str, encode_file, hash_string, html_response, file_binary, client_mobile, percent_decode,
-                        file_icon_suffix, response_encoding, detect_file_as_dir, encoding_extension, file_time_modified, get_raw_fs_metadata,
-                        human_readable_size, 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::{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, response_encoding, detect_file_as_dir, encoding_extension, file_time_modified,
+                        get_raw_fs_metadata, human_readable_size, 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};
 
 
 macro_rules! log {
@@ -138,7 +138,7 @@ impl HttpHandler {
 
     fn handle_get(&self, req: &mut Request) -> IronResult<Response> {
         let (req_p, symlink, url_err) = self.parse_requested_path(req);
-        let file = req_p.is_file();
+        let file = is_actually_file(&req_p.metadata().expect("Failed to get file metadata").file_type());
         let range = req.headers.get().map(|r: &headers::Range| (*r).clone());
         let raw_fs = req.headers.get().map(|r: &RawFsApiHeader| r.0).unwrap_or(false);
 
@@ -201,7 +201,7 @@ impl HttpHandler {
         match range {
             headers::Range::Bytes(ref brs) => {
                 if brs.len() == 1 {
-                    let flen = req_p.metadata().expect("Failed to get requested file metadata").len();
+                    let flen = file_length(&req_p.metadata().expect("Failed to get requested file metadata"), &req_p);
                     match brs[0] {
                         // Cases where from is bigger than to are filtered out by iron so can never happen
                         headers::ByteRangeSpec::FromTo(from, to) => self.handle_get_file_closed_range(req, req_p, from, to),
@@ -251,7 +251,7 @@ impl HttpHandler {
                             Header(headers::LastModified(headers::HttpDate(file_time_modified(&req_p)))),
                             Header(headers::ContentRange(headers::ContentRangeSpec::Bytes {
                                 range: Some((from, to)),
-                                instance_length: Some(f.metadata().expect("Failed to get requested file metadata").len()),
+                                instance_length: Some(file_length(&f.metadata().expect("Failed to get requested file metadata"), &req_p)),
                             })),
                             Header(headers::AcceptRanges(vec![headers::RangeUnit::Bytes]))),
                            buf,
@@ -270,7 +270,7 @@ impl HttpHandler {
              from,
              mime_type);
 
-        let flen = req_p.metadata().expect("Failed to get requested file metadata").len();
+        let flen = file_length(&req_p.metadata().expect("Failed to get requested file metadata"), &req_p);
         self.handle_get_file_opened_range(req_p, SeekFrom::Start(from), from, flen - from, mime_type)
     }
 
@@ -286,13 +286,13 @@ impl HttpHandler {
              req_p.display(),
              mime_type);
 
-        let flen = req_p.metadata().expect("Failed to get requested file metadata").len();
+        let flen = file_length(&req_p.metadata().expect("Failed to get requested file metadata"), &req_p);
         self.handle_get_file_opened_range(req_p, SeekFrom::End(-(from as i64)), flen - from, from, mime_type)
     }
 
     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 = f.metadata().expect("Failed to get requested file metadata").len();
+        let flen = file_length(&f.metadata().expect("Failed to get requested file metadata"), &req_p);
         f.seek(s).expect("Failed to seek requested file");
 
         Ok(Response::with((status::PartialContent,
@@ -335,7 +335,7 @@ impl HttpHandler {
                            Header(headers::LastModified(headers::HttpDate(file_time_modified(&req_p)))),
                            Header(headers::ContentRange(headers::ContentRangeSpec::Bytes {
                                range: Some((from, to)),
-                               instance_length: Some(req_p.metadata().expect("Failed to get requested file metadata").len()),
+                               instance_length: Some(file_length(&req_p.metadata().expect("Failed to get requested file metadata"), &req_p)),
                            })),
                            Header(headers::AcceptRanges(vec![headers::RangeUnit::Bytes])),
                            mime_type)))
@@ -352,16 +352,18 @@ impl HttpHandler {
              req_p.display(),
              mime_type);
 
-        let flen = req_p.metadata().expect("Failed to get requested file metadata").len();
+        let metadata = req_p.metadata().expect("Failed to get requested file metadata");
+        let flen = file_length(&metadata, &req_p);
         if self.encoded_temp_dir.is_some() && flen > MIN_ENCODING_SIZE && flen < MAX_ENCODING_SIZE &&
            req_p.extension().and_then(|s| s.to_str()).map(|s| !BLACKLISTED_ENCODING_EXTENSIONS.contains(&UniCase::new(s))).unwrap_or(true) {
             self.handle_get_file_encoded(req, req_p, mime_type)
         } 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::AcceptRanges(vec![headers::RangeUnit::Bytes])),
-                               req_p,
+                               (Header(headers::Server(USER_AGENT.to_string())),
+                                Header(headers::LastModified(headers::HttpDate(file_time_modified(&req_p)))),
+                                Header(headers::AcceptRanges(vec![headers::RangeUnit::Bytes]))),
+                               req_p.as_path(),
+                               Header(headers::ContentLength(file_length(&metadata, &req_p))),
                                mime_type)))
         }
     }
@@ -377,8 +379,9 @@ impl HttpHandler {
                         log!("{} encoded as {} for {:.1}% ratio (cached)",
                              iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
                              encoding,
-                             ((req_p.metadata().expect("Failed to get requested file metadata").len() as f64) /
-                              (resp_p.metadata().expect("Failed to get encoded file metadata").len() as f64)) * 100f64);
+                             ((file_length(&req_p.metadata().expect("Failed to get requested file metadata"), &req_p) as f64) /
+                              (file_length(&resp_p.metadata().expect("Failed to get encoded file metadata"), &resp_p) as f64)) *
+                             100f64);
 
                         return Ok(Response::with((status::Ok,
                                                   Header(headers::Server(USER_AGENT.to_string())),
@@ -408,8 +411,8 @@ impl HttpHandler {
             };
 
             if encode_file(&req_p, &resp_p, &encoding) {
-                let gain = (req_p.metadata().expect("Failed to get requested file metadata").len() as f64) /
-                           (resp_p.metadata().expect("Failed to get encoded file metadata").len() as f64);
+                let gain = (file_length(&req_p.metadata().expect("Failed to get requested file metadata"), &req_p) as f64) /
+                           (file_length(&resp_p.metadata().expect("Failed to get encoded file metadata"), &resp_p) as f64);
                 if gain < MIN_ENCODING_GAIN {
                     let mut cache = self.cache_fs.write().expect("Filesystem cache write lock poisoned");
                     cache.insert(cache_key, (req_p.clone(), false));
@@ -438,10 +441,11 @@ 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::AcceptRanges(vec![headers::RangeUnit::Bytes])),
-                           req_p,
+                           (Header(headers::Server(USER_AGENT.to_string())),
+                            Header(headers::LastModified(headers::HttpDate(file_time_modified(&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))),
                            mt)))
     }
 
@@ -459,7 +463,7 @@ impl HttpHandler {
                                                 .map(|p| p.expect("Failed to iterate over requested directory"))
                                                 .filter(|f| self.follow_symlinks || !is_symlink(f.path()))
                                                 .map(|f| {
-                    let is_file = f.file_type().expect("Failed to get file type").is_file();
+                    let is_file = is_actually_file(&f.file_type().expect("Failed to get file type"));
                     if is_file {
                         get_raw_fs_metadata(f.path())
                     } else {
@@ -546,12 +550,13 @@ impl HttpHandler {
             .map(|p| p.expect("Failed to iterate over requested directory"))
             .filter(|f| self.follow_symlinks || !is_symlink(f.path()))
             .sorted_by(|lhs, rhs| {
-                (lhs.file_type().expect("Failed to get file type").is_file(), lhs.file_name().to_str().expect("Failed to get file name").to_lowercase())
-                    .cmp(&(rhs.file_type().expect("Failed to get file type").is_file(),
+                (is_actually_file(&lhs.file_type().expect("Failed to get file type")),
+                 lhs.file_name().to_str().expect("Failed to get file name").to_lowercase())
+                    .cmp(&(is_actually_file(&rhs.file_type().expect("Failed to get file type")),
                            rhs.file_name().to_str().expect("Failed to get file name").to_lowercase()))
             })
             .fold("".to_string(), |cur, f| {
-                let is_file = f.file_type().expect("Failed to get file type").is_file();
+                let is_file = is_actually_file(&f.file_type().expect("Failed to get file type"));
                 let fname = f.file_name().into_string().expect("Failed to get file name");
                 let path = f.path();
 
@@ -567,7 +572,7 @@ impl HttpHandler {
                         file_time_modified(&path).strftime("%F %T").unwrap(),
                         if is_file { "<span class=\"size\">" } else { "" },
                         if is_file {
-                            human_readable_size(f.metadata().expect("Failed to get file metadata").len())
+                            human_readable_size(file_length(&f.metadata().expect("Failed to get requested file metadata"), &path))
                         } else {
                             String::new()
                         },
@@ -622,15 +627,16 @@ impl HttpHandler {
             .map(|p| p.expect("Failed to iterate over requested directory"))
             .filter(|f| self.follow_symlinks || !is_symlink(f.path()))
             .sorted_by(|lhs, rhs| {
-                (lhs.file_type().expect("Failed to get file type").is_file(), lhs.file_name().to_str().expect("Failed to get file name").to_lowercase())
-                    .cmp(&(rhs.file_type().expect("Failed to get file type").is_file(),
+                (is_actually_file(&lhs.file_type().expect("Failed to get file type")),
+                 lhs.file_name().to_str().expect("Failed to get file name").to_lowercase())
+                    .cmp(&(is_actually_file(&rhs.file_type().expect("Failed to get file type")),
                            rhs.file_name().to_str().expect("Failed to get file name").to_lowercase()))
             })
             .fold("".to_string(), |cur, f| {
-                let is_file = f.file_type().expect("Failed to get file type").is_file();
+                let is_file = is_actually_file(&f.file_type().expect("Failed to get file type"));
                 let fname = f.file_name().into_string().expect("Failed to get file name");
                 let path = f.path();
-                let len = f.metadata().expect("Failed to get file metadata").len();
+                let len = file_length(&f.metadata().expect("Failed to get requested file metadata"), &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> \
@@ -790,7 +796,7 @@ impl HttpHandler {
     fn handle_delete_path(&self, req: &mut Request, req_p: PathBuf, symlink: bool) -> IronResult<Response> {
         log!("{green}{}{reset} deleted {blue}{} {magenta}{}{reset}",
              req.remote_addr,
-             if req_p.is_file() {
+             if is_actually_file(&req_p.metadata().expect("failed to get file metadata").file_type()) {
                  "file"
              } else if symlink {
                  "symlink"
@@ -799,7 +805,7 @@ impl HttpHandler {
              },
              req_p.display());
 
-        if req_p.is_file() {
+        if is_actually_file(&req_p.metadata().expect("failed to get file metadata").file_type()) {
             fs::remove_file(req_p).expect("Failed to remove requested file");
         } else {
             fs::remove_dir_all(req_p).expect(if symlink {
diff --git a/src/util/mod.rs b/src/util/mod.rs
index b26d1f6..9de27b1 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -1,16 +1,17 @@
 //! Module containing various utility functions.
 
 
+mod os;
 mod content_encoding;
 
 use std::f64;
 use std::cmp;
-use std::fs::File;
 use std::path::Path;
 use std::borrow::Cow;
 use rfsapi::RawFileData;
 use url::percent_encoding;
 use iron::headers::UserAgent;
+use std::fs::{FileType, File};
 use std::collections::HashMap;
 use time::{self, Duration, Tm};
 use iron::{mime, Headers, Url};
@@ -18,6 +19,7 @@ use std::io::{BufReader, BufRead};
 use base64::display::Base64Display;
 use mime_guess::{guess_mime_type_opt, get_mime_type_str};
 
+pub use self::os::*;
 pub use self::content_encoding::*;
 
 
@@ -115,7 +117,10 @@ pub fn uppercase_first(s: &str) -> String {
 /// assert!(!file_binary("Cargo.toml"));
 /// ```
 pub fn file_binary<P: AsRef<Path>>(path: P) -> bool {
-    File::open(path).and_then(|f| BufReader::new(f).read_line(&mut String::new())).is_err()
+    let path = path.as_ref();
+    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)
 }
 
 /// Fill out an HTML template.
@@ -209,6 +214,11 @@ pub fn is_symlink<P: AsRef<Path>>(p: P) -> bool {
     p.as_ref().read_link().is_ok()
 }
 
+/// Check if a path refers to a file in a way that includes Unix devices and Windows symlinks.
+pub fn is_actually_file(tp: &FileType) -> bool {
+    tp.is_file() || is_device(tp)
+}
+
 /// Construct string representing a human-readable size.
 ///
 /// Stolen, adapted and inlined from [fielsize.js](http://filesizejs.com).
diff --git a/src/util/os/mod.rs b/src/util/os/mod.rs
new file mode 100644
index 0000000..709275a
--- /dev/null
+++ b/src/util/os/mod.rs
@@ -0,0 +1,9 @@
+#[cfg(target_os = "windows")]
+mod windows;
+#[cfg(not(target_os = "windows"))]
+mod non_windows;
+
+#[cfg(target_os = "windows")]
+pub use self::windows::{file_length, is_device};
+#[cfg(not(target_os = "windows"))]
+pub use self::non_windows::{file_length, is_device};
diff --git a/src/util/os/non_windows.rs b/src/util/os/non_windows.rs
new file mode 100644
index 0000000..1d635ae
--- /dev/null
+++ b/src/util/os/non_windows.rs
@@ -0,0 +1,35 @@
+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;
+use std::path::Path;
+
+
+include!(concat!(env!("OUT_DIR"), "/ioctl-data/ioctl.rs"));
+
+
+/// 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
+pub fn file_length<P: AsRef<Path>>(meta: &Metadata, path: &P) -> 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 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;
+            unsafe { close(dev_file) };
+
+            if ok {
+                return block_count as u64 * 512;
+            }
+        }
+    }
+
+    meta.len()
+}
diff --git a/src/util/os/windows.rs b/src/util/os/windows.rs
new file mode 100644
index 0000000..3d2b6e8
--- /dev/null
+++ b/src/util/os/windows.rs
@@ -0,0 +1,13 @@
+use std::fs::{FileType, Metadata};
+use std::path::Path;
+
+
+/// OS-specific check for fileness
+pub fn is_device(_: &FileType) -> bool {
+    false
+}
+
+/// Check file length responsibly
+pub fn file_length<P: AsRef<Path>>(meta: &Metadata, _: &P) -> u64 {
+    meta.len()
+}


Test patch: