├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── RELEASE_PROCEDURES.md ├── build.rs ├── resources └── test_file ├── rustfmt.toml ├── src ├── filesystem.rs ├── lib.rs └── vfs.rs ├── tests └── skeptic.rs └── watch.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | #Added by cargo 14 | # 15 | #already existing elements are commented out 16 | 17 | /target 18 | #**/*.rs.bk 19 | #Cargo.lock 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | language: rust 3 | rust: 4 | - stable 5 | - beta 6 | - nightly 7 | - 1.33.0 8 | os: 9 | - linux 10 | - osx 11 | - windows 12 | matrix: 13 | allow_failures: 14 | - rust: nightly 15 | cache: cargo 16 | 17 | script: 18 | - cargo test; 19 | - cargo test --doc; 20 | - cargo test --test skeptic; 21 | 22 | after_success: | 23 | if [[ "${TRAVIS_OS_NAME}" == "linux" && "${TRAVIS_RUST_VERSION}" == stable ]]; then 24 | RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin 25 | # Uncomment the following line for coveralls.io 26 | # cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID 27 | 28 | # Uncomment the following two lines create and upload a report for codecov.io 29 | cargo tarpaulin --out Xml 30 | bash <(curl -s https://codecov.io/bash) 31 | fi 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 2 | 3 | ## Added 4 | 5 | 6 | ## Changed 7 | 8 | 9 | ## Deprecated 10 | 11 | ## Removed 12 | 13 | 14 | ## Fixed 15 | 16 | 17 | ## Broken 18 | 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | See the code of conduct at https://github.com/ggez/ggez 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See the contribution guide at https://github.com/ggez/ggez 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gvfs" 3 | description = "A Rust crate providing a configurable virtual file system for game assets, a la PhysFS" 4 | version = "0.1.2" 5 | homepage = "http://ggez.rs" 6 | repository = "https://github.com/ggez/gvfs" 7 | documentation = "https://docs.rs/gvfs" 8 | keywords = ["filesystem", "game", "webassembly", "zip"] 9 | authors = [ 10 | "Simon Heath ", 11 | ] 12 | 13 | edition = "2018" 14 | license = "MIT / Apache-2.0" 15 | readme = "README.md" 16 | categories = ["filesystem", "game-engines"] 17 | build = "build.rs" 18 | 19 | [badges] 20 | maintenance = { status = "experimental" } 21 | 22 | [lib] 23 | name = "gvfs" 24 | path = "src/lib.rs" 25 | 26 | [features] 27 | 28 | [dependencies] 29 | directories = "2" 30 | zip = { version = "0.5", default-features = false } 31 | log = "0.4" 32 | 33 | [dev-dependencies] 34 | skeptic = "0.13" 35 | 36 | [build-dependencies] 37 | skeptic = "0.13" 38 | 39 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019, The ggez Developers 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 The ggez Developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What Is This? 2 | 3 | [![Build Status](https://travis-ci.org/ggez/gvfs.svg?branch=master)](https://travis-ci.org/ggez/gvfs) 4 | [![Docs Status](https://docs.rs/gvfs/badge.svg)](https://docs.rs/gvfs) 5 | [![License](https://img.shields.io/crates/l/gvfs.svg)](http://crates.io/cratee/gvfs) 6 | [![Crates.io](https://img.shields.io/crates/v/gvfs.svg)](https://crates.io/crates/gvfs) 7 | [![Crates.io](https://img.shields.io/crates/d/gvfs.svg)](https://crates.io/crates/gvfs) 8 | 9 | 10 | A Rust crate providing a configurable virtual file system for game assets, a la PhysFS 11 | 12 | 13 | ## Features 14 | 15 | ## Supported platforms 16 | 17 | ## Usage 18 | 19 | ## Getting started 20 | 21 | ## Help! 22 | -------------------------------------------------------------------------------- /RELEASE_PROCEDURES.md: -------------------------------------------------------------------------------- 1 | See the relase procedures at https://github.com/ggez/ggez 2 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use skeptic; 2 | use std::path::PathBuf; 3 | 4 | fn main() { 5 | let mdbook_files: Vec = vec!["README.md".into()]; 6 | skeptic::generate_doc_tests(&mdbook_files); 7 | } 8 | -------------------------------------------------------------------------------- /resources/test_file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/gvfs/8b651f862a9768f70d0348d7ece64db34e3ad676/resources/test_file -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/gvfs/8b651f862a9768f70d0348d7ece64db34e3ad676/rustfmt.toml -------------------------------------------------------------------------------- /src/filesystem.rs: -------------------------------------------------------------------------------- 1 | //! A cross-platform interface to the filesystem. 2 | //! 3 | //! This module provides access to files in specific places: 4 | //! 5 | //! * The `resources/` subdirectory in the same directory as the 6 | //! program executable, if any, 7 | //! * The `resources.zip` file in the same 8 | //! directory as the program executable, if any, 9 | //! * The root folder of the game's "save" directory which is in a 10 | //! platform-dependent location, 11 | //! such as `~/.local/share//` on Linux. The `gameid` 12 | //! is the the string passed to 13 | //! [`ContextBuilder::new()`](../struct.ContextBuilder.html#method.new). 14 | //! Some platforms such as Windows also incorporate the `author` string into 15 | //! the path. 16 | //! 17 | //! These locations will be searched for files in the order listed, and the first file 18 | //! found used. That allows game assets to be easily distributed as an archive 19 | //! file, but locally overridden for testing or modding simply by putting 20 | //! altered copies of them in the game's `resources/` directory. It 21 | //! is loosely based off of the `PhysicsFS` library. 22 | //! 23 | //! See the source of the [`files` example](https://github.com/ggez/ggez/blob/master/examples/files.rs) for more details. 24 | //! 25 | //! Note that the file lookups WILL follow symlinks! This module's 26 | //! directory isolation is intended for convenience, not security, so 27 | //! don't assume it will be secure. 28 | 29 | use std::env; 30 | use std::fmt; 31 | use std::io; 32 | use std::path; 33 | 34 | use directories::ProjectDirs; 35 | use log::*; 36 | 37 | use crate::vfs::{self, VFS}; 38 | use crate::{Error, Result}; 39 | 40 | /// A structure that contains the filesystem state and cache. 41 | #[derive(Debug)] 42 | pub struct Filesystem { 43 | vfs: vfs::OverlayFS, 44 | resources_path: path::PathBuf, 45 | zip_path: path::PathBuf, 46 | user_config_path: path::PathBuf, 47 | user_data_path: path::PathBuf, 48 | } 49 | 50 | /// Represents a file, either in the filesystem, or in the resources zip file, 51 | /// or whatever. 52 | pub enum File { 53 | /// A wrapper for a VFile trait object. 54 | VfsFile(Box), 55 | } 56 | 57 | impl fmt::Debug for File { 58 | // Make this more useful? 59 | // But we can't seem to get a filename out of a file, 60 | // soooooo. 61 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 62 | match *self { 63 | File::VfsFile(ref _file) => write!(f, "VfsFile"), 64 | } 65 | } 66 | } 67 | 68 | impl io::Read for File { 69 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 70 | match *self { 71 | File::VfsFile(ref mut f) => f.read(buf), 72 | } 73 | } 74 | } 75 | 76 | impl io::Write for File { 77 | fn write(&mut self, buf: &[u8]) -> io::Result { 78 | match *self { 79 | File::VfsFile(ref mut f) => f.write(buf), 80 | } 81 | } 82 | 83 | fn flush(&mut self) -> io::Result<()> { 84 | match *self { 85 | File::VfsFile(ref mut f) => f.flush(), 86 | } 87 | } 88 | } 89 | 90 | impl Filesystem { 91 | /// Create a new `Filesystem` instance, using the given `id` and (on 92 | /// some platforms) the `author` as a portion of the user 93 | /// directory path. 94 | pub fn new(id: &str, author: &str) -> Result { 95 | let mut root_path = env::current_exe()?; 96 | 97 | // Ditch the filename (if any) 98 | if root_path.file_name().is_some() { 99 | let _ = root_path.pop(); 100 | } 101 | 102 | // Set up VFS to merge resource path, root path, and zip path. 103 | let mut overlay = vfs::OverlayFS::new(); 104 | 105 | let mut resources_path; 106 | let mut resources_zip_path; 107 | let user_data_path; 108 | let user_config_path; 109 | 110 | let project_dirs = match ProjectDirs::from("", author, id) { 111 | Some(dirs) => dirs, 112 | None => { 113 | return Err(Error::VfsError(String::from( 114 | "No valid home directory path could be retrieved.", 115 | ))); 116 | } 117 | }; 118 | 119 | // /resources/ 120 | { 121 | resources_path = root_path.clone(); 122 | resources_path.push("resources"); 123 | trace!("Resources path: {:?}", resources_path); 124 | let physfs = vfs::PhysicalFS::new(&resources_path, true); 125 | overlay.push(Box::new(physfs)); 126 | } 127 | 128 | // /resources.zip 129 | { 130 | resources_zip_path = root_path.clone(); 131 | resources_zip_path.push("resources.zip"); 132 | if resources_zip_path.exists() { 133 | trace!("Resources zip file: {:?}", resources_zip_path); 134 | let zipfs = vfs::ZipFS::new(&resources_zip_path)?; 135 | overlay.push(Box::new(zipfs)); 136 | } else { 137 | trace!("No resources zip file found"); 138 | } 139 | } 140 | 141 | // Per-user data dir, 142 | // ~/.local/share/whatever/ 143 | { 144 | user_data_path = project_dirs.data_local_dir(); 145 | trace!("User-local data path: {:?}", user_data_path); 146 | let physfs = vfs::PhysicalFS::new(&user_data_path, true); 147 | overlay.push(Box::new(physfs)); 148 | } 149 | 150 | // Writeable local dir, ~/.config/whatever/ 151 | // Save game dir is read-write 152 | { 153 | user_config_path = project_dirs.config_dir(); 154 | trace!("User-local configuration path: {:?}", user_config_path); 155 | let physfs = vfs::PhysicalFS::new(&user_config_path, false); 156 | overlay.push(Box::new(physfs)); 157 | } 158 | 159 | let fs = Filesystem { 160 | vfs: overlay, 161 | resources_path, 162 | zip_path: resources_zip_path, 163 | user_config_path: user_config_path.to_path_buf(), 164 | user_data_path: user_data_path.to_path_buf(), 165 | }; 166 | 167 | Ok(fs) 168 | } 169 | 170 | /// Opens the given `path` and returns the resulting `File` 171 | /// in read-only mode. 172 | pub fn open>(&mut self, path: P) -> Result { 173 | self.vfs.open(path.as_ref()).map(|f| File::VfsFile(f)) 174 | } 175 | 176 | /// Opens a file in the user directory with the given 177 | /// [`filesystem::OpenOptions`](struct.OpenOptions.html). 178 | /// Note that even if you open a file read-write, it can only 179 | /// write to files in the "user" directory. 180 | pub fn open_options>( 181 | &mut self, 182 | path: P, 183 | options: vfs::OpenOptions, 184 | ) -> Result { 185 | self.vfs 186 | .open_options(path.as_ref(), options) 187 | .map(|f| File::VfsFile(f)) 188 | .map_err(|e| { 189 | Error::VfsError(format!( 190 | "Tried to open {:?} but got error: {:?}", 191 | path.as_ref(), 192 | e 193 | )) 194 | }) 195 | } 196 | 197 | /// Creates a new file in the user directory and opens it 198 | /// to be written to, truncating it if it already exists. 199 | pub fn create>(&mut self, path: P) -> Result { 200 | self.vfs.create(path.as_ref()).map(|f| File::VfsFile(f)) 201 | } 202 | 203 | /// Create an empty directory in the user dir 204 | /// with the given name. Any parents to that directory 205 | /// that do not exist will be created. 206 | pub fn create_dir>(&mut self, path: P) -> Result<()> { 207 | self.vfs.mkdir(path.as_ref()) 208 | } 209 | 210 | /// Deletes the specified file in the user dir. 211 | pub fn delete>(&mut self, path: P) -> Result<()> { 212 | self.vfs.rm(path.as_ref()) 213 | } 214 | 215 | /// Deletes the specified directory in the user dir, 216 | /// and all its contents! 217 | pub fn delete_dir>(&mut self, path: P) -> Result<()> { 218 | self.vfs.rmrf(path.as_ref()) 219 | } 220 | 221 | /// Check whether a file or directory exists. 222 | pub fn exists>(&self, path: P) -> bool { 223 | self.vfs.exists(path.as_ref()) 224 | } 225 | 226 | /// Check whether a path points at a file. 227 | pub fn is_file>(&self, path: P) -> bool { 228 | self.vfs 229 | .metadata(path.as_ref()) 230 | .map(|m| m.is_file()) 231 | .unwrap_or(false) 232 | } 233 | 234 | /// Check whether a path points at a directory. 235 | pub fn is_dir>(&self, path: P) -> bool { 236 | self.vfs 237 | .metadata(path.as_ref()) 238 | .map(|m| m.is_dir()) 239 | .unwrap_or(false) 240 | } 241 | 242 | /// Returns a list of all files and directories in the resource directory, 243 | /// in no particular order. 244 | /// 245 | /// Lists the base directory if an empty path is given. 246 | pub fn read_dir>( 247 | &mut self, 248 | path: P, 249 | ) -> Result>> { 250 | let itr = self.vfs.read_dir(path.as_ref())?.map(|fname| { 251 | fname.expect("Could not read file in read_dir()? Should never happen, I hope!") 252 | }); 253 | Ok(Box::new(itr)) 254 | } 255 | 256 | fn write_to_string(&mut self) -> String { 257 | use std::fmt::Write; 258 | let mut s = String::new(); 259 | for vfs in self.vfs.roots() { 260 | write!(s, "Source {:?}", vfs).expect("Could not write to string; should never happen?"); 261 | match vfs.read_dir(path::Path::new("/")) { 262 | Ok(files) => { 263 | for itm in files { 264 | write!(s, " {:?}", itm) 265 | .expect("Could not write to string; should never happen?"); 266 | } 267 | } 268 | Err(e) => write!(s, " Could not read source: {:?}", e) 269 | .expect("Could not write to string; should never happen?"), 270 | } 271 | } 272 | s 273 | } 274 | 275 | /// Prints the contents of all data directories 276 | /// to standard output. Useful for debugging. 277 | pub fn print_all(&mut self) { 278 | println!("{}", self.write_to_string()); 279 | } 280 | 281 | /// Outputs the contents of all data directories, 282 | /// using the "info" log level of the [`log`](https://docs.rs/log/) crate. 283 | /// Useful for debugging. 284 | pub fn log_all(&mut self) { 285 | info!("{}", self.write_to_string()); 286 | } 287 | 288 | /// Adds the given (absolute) path to the list of directories 289 | /// it will search to look for resources. 290 | /// 291 | /// You probably shouldn't use this in the general case, since it is 292 | /// harder than it looks to make it bulletproof across platforms. 293 | /// But it can be very nice for debugging and dev purposes, such as 294 | /// by pushing `$CARGO_MANIFEST_DIR/resources` to it 295 | pub fn mount(&mut self, path: &path::Path, readonly: bool) { 296 | let physfs = vfs::PhysicalFS::new(path, readonly); 297 | trace!("Mounting new path: {:?}", physfs); 298 | self.vfs.push(Box::new(physfs)); 299 | } 300 | 301 | /// Adds any object that implements Read + Seek as a zip file. 302 | /// 303 | /// Note: This is not intended for system files for the same reasons as 304 | /// for `.mount()`. Rather, it can be used to read zip files from sources 305 | /// such as `std::io::Cursor::new(includes_bytes!(...))` in order to embed 306 | /// resources into the game's executable. 307 | pub fn add_zip_file(&mut self, reader: R) -> Result<()> { 308 | let zipfs = vfs::ZipFS::from_read(reader)?; 309 | trace!("Adding zip file from reader"); 310 | self.vfs.push(Box::new(zipfs)); 311 | Ok(()) 312 | } 313 | } 314 | 315 | #[cfg(test)] 316 | mod tests { 317 | use crate::filesystem::*; 318 | use crate::*; 319 | use std::io::{Read, Write}; 320 | use std::path; 321 | 322 | fn dummy_fs_for_tests() -> Filesystem { 323 | let mut path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 324 | path.push("resources"); 325 | let physfs = vfs::PhysicalFS::new(&path, false); 326 | let mut ofs = vfs::OverlayFS::new(); 327 | ofs.push(Box::new(physfs)); 328 | Filesystem { 329 | vfs: ofs, 330 | 331 | resources_path: "".into(), 332 | zip_path: "".into(), 333 | user_config_path: "".into(), 334 | user_data_path: "".into(), 335 | } 336 | } 337 | 338 | #[test] 339 | fn headless_test_file_exists() { 340 | let f = dummy_fs_for_tests(); 341 | 342 | let tile_file = path::Path::new("/test_file"); 343 | assert!(f.exists(tile_file)); 344 | assert!(f.is_file(tile_file)); 345 | 346 | let tile_file = path::Path::new("/oglebog.png"); 347 | assert!(!f.exists(tile_file)); 348 | assert!(!f.is_file(tile_file)); 349 | assert!(!f.is_dir(tile_file)); 350 | } 351 | 352 | #[test] 353 | fn headless_test_read_dir() { 354 | let mut f = dummy_fs_for_tests(); 355 | 356 | let dir_contents_size = f.read_dir("/").unwrap().count(); 357 | assert!(dir_contents_size > 0); 358 | } 359 | 360 | #[test] 361 | fn headless_test_create_delete_file() { 362 | let mut fs = dummy_fs_for_tests(); 363 | let test_file = path::Path::new("/testfile.txt"); 364 | let bytes = "test".as_bytes(); 365 | 366 | { 367 | let mut file = fs.create(test_file).unwrap(); 368 | let _ = file.write(bytes).unwrap(); 369 | } 370 | { 371 | let mut buffer = Vec::new(); 372 | let mut file = fs.open(test_file).unwrap(); 373 | let _ = file.read_to_end(&mut buffer).unwrap(); 374 | assert_eq!(bytes, buffer.as_slice()); 375 | } 376 | 377 | fs.delete(test_file).unwrap(); 378 | } 379 | 380 | #[test] 381 | fn headless_test_file_not_found() { 382 | let mut fs = dummy_fs_for_tests(); 383 | { 384 | let rel_file = "testfile.txt"; 385 | match fs.open(rel_file) { 386 | Err(Error::ResourceNotFound(_, _)) => (), 387 | Err(e) => panic!("Invalid error for opening file with relative path: {:?}", e), 388 | Ok(f) => panic!("Should have gotten an error but instead got {:?}!", f), 389 | } 390 | } 391 | 392 | { 393 | // This absolute path should work on Windows too since we 394 | // completely remove filesystem roots. 395 | match fs.open("/ooglebooglebarg.txt") { 396 | Err(Error::ResourceNotFound(_, _)) => (), 397 | Err(e) => panic!("Invalid error for opening nonexistent file: {}", e), 398 | Ok(f) => panic!("Should have gotten an error but instead got {:?}", f), 399 | } 400 | } 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! TODO: Crate docs 2 | 3 | #![forbid(missing_docs)] 4 | #![forbid(missing_debug_implementations)] 5 | #![forbid(unused_results)] 6 | #![forbid(unsafe_code)] 7 | #![warn(bare_trait_objects)] 8 | #![warn(missing_copy_implementations)] 9 | 10 | use std::fmt; 11 | use std::sync::Arc; 12 | 13 | pub mod filesystem; 14 | pub mod vfs; 15 | 16 | /// Error type 17 | /// 18 | /// TODO: This all needs to be consistent-ified. 19 | /// 20 | /// error types: invalid vfs type (zip file corrupt, etc), 21 | /// read/write error (IOError), not found, maybe something else... 22 | #[derive(Debug, Clone)] 23 | pub enum Error { 24 | /// TODO 25 | VfsError(String), 26 | /// TODO 27 | ResourceNotFound(String, Vec<(std::path::PathBuf, Error)>), 28 | /// TODO 29 | ZipError(String), 30 | /// TODO 31 | IOError(Arc), 32 | } 33 | 34 | /// Shortcut result type 35 | pub type Result = std::result::Result; 36 | 37 | impl From for Error { 38 | fn from(e: zip::result::ZipError) -> Error { 39 | let errstr = format!("Zip error: {}", e); 40 | 41 | Error::VfsError(errstr) 42 | } 43 | } 44 | 45 | impl From for Error { 46 | fn from(e: std::io::Error) -> Error { 47 | Error::IOError(Arc::new(e)) 48 | } 49 | } 50 | 51 | // TODO 52 | impl fmt::Display for Error { 53 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 54 | match *self { 55 | _ => write!(f, "Error {:?}", self), 56 | } 57 | } 58 | } 59 | 60 | // TODO 61 | impl std::error::Error for Error { 62 | fn cause(&self) -> Option<&dyn std::error::Error> { 63 | None 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | #[test] 70 | fn it_works() { 71 | assert_eq!(2 + 2, 4); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/vfs.rs: -------------------------------------------------------------------------------- 1 | //! A virtual file system layer that lets us define multiple 2 | //! "file systems" with various backing stores, then merge them 3 | //! together. 4 | //! 5 | //! Basically a re-implementation of the C library `PhysFS`. The 6 | //! `vfs` crate does something similar but has a couple design 7 | //! decisions that make it kind of incompatible with this use case: 8 | //! the relevant trait for it has generic methods so we can't use it 9 | //! as a trait object, and its path abstraction is not the most 10 | //! convenient. 11 | 12 | use std::cell::RefCell; 13 | use std::fmt::{self, Debug}; 14 | use std::fs; 15 | use std::io::{self, Read, Seek, Write}; 16 | use std::path::{self, Path, PathBuf}; 17 | 18 | use zip; 19 | 20 | use crate::{Error, Result}; 21 | 22 | /// What it says on the tin 23 | fn convenient_path_to_str(path: &path::Path) -> Result<&str> { 24 | path.to_str().ok_or_else(|| { 25 | let errmessage = format!("Invalid path format for resource: {:?}", path); 26 | Error::VfsError(errmessage) 27 | }) 28 | } 29 | 30 | /// Our basic trait for files. All different types of filesystem 31 | /// must provide a thing that implements this trait. 32 | pub trait VFile: Read + Write + Seek + Debug {} 33 | 34 | impl VFile for T where T: Read + Write + Seek + Debug {} 35 | 36 | /// Options for opening files 37 | /// 38 | /// We need our own version of this structure because the one in 39 | /// `std` annoyingly doesn't let you read the read/write/create/etc 40 | /// state out of it. 41 | #[must_use] 42 | #[derive(Debug, Default, Copy, Clone, PartialEq)] 43 | pub struct OpenOptions { 44 | read: bool, 45 | write: bool, 46 | create: bool, 47 | append: bool, 48 | truncate: bool, 49 | } 50 | 51 | impl OpenOptions { 52 | /// Create a new instance with defaults. 53 | pub fn new() -> OpenOptions { 54 | Default::default() 55 | } 56 | 57 | /// Open for reading 58 | pub fn read(mut self, read: bool) -> OpenOptions { 59 | self.read = read; 60 | self 61 | } 62 | 63 | /// Open for writing 64 | pub fn write(mut self, write: bool) -> OpenOptions { 65 | self.write = write; 66 | self 67 | } 68 | 69 | /// Create the file if it does not exist yet 70 | pub fn create(mut self, create: bool) -> OpenOptions { 71 | self.create = create; 72 | self 73 | } 74 | 75 | /// Append at the end of the file 76 | pub fn append(mut self, append: bool) -> OpenOptions { 77 | self.append = append; 78 | self 79 | } 80 | 81 | /// Truncate the file to 0 bytes after opening 82 | pub fn truncate(mut self, truncate: bool) -> OpenOptions { 83 | self.truncate = truncate; 84 | self 85 | } 86 | 87 | fn to_fs_openoptions(self) -> fs::OpenOptions { 88 | let mut opt = fs::OpenOptions::new(); 89 | let _ = opt 90 | .read(self.read) 91 | .write(self.write) 92 | .create(self.create) 93 | .append(self.append) 94 | .truncate(self.truncate) 95 | .create(self.create); 96 | opt 97 | } 98 | } 99 | 100 | /// A trait for a virtual file system, such as a zip file or a point 101 | /// in the real file system. 102 | pub trait VFS: Debug { 103 | /// Open the file at this path with the given options 104 | fn open_options(&self, path: &Path, open_options: OpenOptions) -> Result>; 105 | /// Open the file at this path for reading 106 | fn open(&self, path: &Path) -> Result> { 107 | self.open_options(path, OpenOptions::new().read(true)) 108 | } 109 | /// Open the file at this path for writing, truncating it if it exists already 110 | fn create(&self, path: &Path) -> Result> { 111 | self.open_options( 112 | path, 113 | OpenOptions::new().write(true).create(true).truncate(true), 114 | ) 115 | } 116 | /// Open the file at this path for appending, creating it if necessary 117 | fn append(&self, path: &Path) -> Result> { 118 | self.open_options( 119 | path, 120 | OpenOptions::new().write(true).create(true).append(true), 121 | ) 122 | } 123 | /// Create a directory at the location by this path 124 | fn mkdir(&self, path: &Path) -> Result; 125 | 126 | /// Remove a file or an empty directory. 127 | fn rm(&self, path: &Path) -> Result; 128 | 129 | /// Remove a file or directory and all its contents 130 | fn rmrf(&self, path: &Path) -> Result; 131 | 132 | /// Check if the file exists 133 | fn exists(&self, path: &Path) -> bool; 134 | 135 | /// Get the file's metadata 136 | fn metadata(&self, path: &Path) -> Result>; 137 | 138 | /// Retrieve all file and directory entries in the given directory. 139 | fn read_dir(&self, path: &Path) -> Result>>>; 140 | 141 | /// Retrieve the actual location of the VFS root, if available. 142 | fn to_path_buf(&self) -> Option; 143 | } 144 | 145 | /// The metadata we can read from a file. 146 | pub trait VMetadata { 147 | /// Returns whether or not it is a directory. 148 | /// Note that zip files don't actually have directories, awkwardly, 149 | /// just files with very long names. 150 | fn is_dir(&self) -> bool; 151 | /// Returns whether or not it is a file. 152 | fn is_file(&self) -> bool; 153 | /// Returns the length of the thing. If it is a directory, 154 | /// the result of this is undefined/platform dependent. 155 | fn len(&self) -> u64; 156 | } 157 | 158 | /// A VFS that points to a directory and uses it as the root of its 159 | /// file hierarchy. 160 | /// 161 | /// It IS allowed to have symlinks in it! They're surprisingly 162 | /// difficult to get rid of. 163 | #[derive(Clone)] 164 | pub struct PhysicalFS { 165 | root: PathBuf, 166 | readonly: bool, 167 | } 168 | 169 | /// Metadata for a physical file. 170 | #[derive(Debug, Clone)] 171 | pub struct PhysicalMetadata(fs::Metadata); 172 | 173 | impl VMetadata for PhysicalMetadata { 174 | fn is_dir(&self) -> bool { 175 | self.0.is_dir() 176 | } 177 | fn is_file(&self) -> bool { 178 | self.0.is_file() 179 | } 180 | fn len(&self) -> u64 { 181 | self.0.len() 182 | } 183 | } 184 | 185 | /// This takes an absolute path and returns either a sanitized relative 186 | /// version of it, or None if there's something bad in it. 187 | /// 188 | /// What we want is an absolute path with no `..`'s in it, so, something 189 | /// like "/foo" or "/foo/bar.txt". This means a path with components 190 | /// starting with a `RootDir`, and zero or more `Normal` components. 191 | /// 192 | /// We gotta return a new path because there's apparently no real good way 193 | /// to turn an absolute path into a relative path with the same 194 | /// components (other than the first), and pushing an absolute `Path` 195 | /// onto a `PathBuf` just completely nukes its existing contents. 196 | fn sanitize_path(path: &path::Path) -> Option { 197 | let mut c = path.components(); 198 | match c.next() { 199 | Some(path::Component::RootDir) => (), 200 | _ => return None, 201 | } 202 | 203 | fn is_normal_component(comp: path::Component) -> Option<&str> { 204 | match comp { 205 | path::Component::Normal(s) => s.to_str(), 206 | _ => None, 207 | } 208 | } 209 | 210 | // This could be done more cleverly but meh 211 | let mut accm = PathBuf::new(); 212 | for component in c { 213 | if let Some(s) = is_normal_component(component) { 214 | accm.push(s) 215 | } else { 216 | return None; 217 | } 218 | } 219 | Some(accm) 220 | } 221 | 222 | impl PhysicalFS { 223 | /// Create new PhysicalFS 224 | pub fn new(root: &Path, readonly: bool) -> Self { 225 | PhysicalFS { 226 | root: root.into(), 227 | readonly, 228 | } 229 | } 230 | 231 | /// Takes a given absolute `&Path` and returns 232 | /// a new PathBuf containing the canonical 233 | /// absolute path you get when appending it 234 | /// to this filesystem's root. 235 | /// 236 | /// So if this FS's root is `/home/icefox/foo` then 237 | /// calling `fs.to_absolute("/bar")` should return 238 | /// `/home/icefox/foo/bar` 239 | fn to_absolute(&self, p: &Path) -> Result { 240 | if let Some(safe_path) = sanitize_path(p) { 241 | let mut root_path = self.root.clone(); 242 | root_path.push(safe_path); 243 | Ok(root_path) 244 | } else { 245 | let msg = format!( 246 | "Path {:?} is not valid: must be an absolute path with no \ 247 | references to parent directories", 248 | p 249 | ); 250 | Err(Error::VfsError(msg)) 251 | } 252 | } 253 | 254 | /// Creates the PhysicalFS's root directory if necessary. 255 | /// Idempotent. 256 | /// 257 | /// This way we can avoid creating the directory 258 | /// until it's actually used, though it IS a tiny bit of a 259 | /// performance malus. 260 | fn create_root(&self) -> Result { 261 | if !self.root.exists() { 262 | fs::create_dir_all(&self.root).map_err(Error::from) 263 | } else { 264 | Ok(()) 265 | } 266 | } 267 | } 268 | 269 | impl Debug for PhysicalFS { 270 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 271 | write!(f, "", self.root.display()) 272 | } 273 | } 274 | 275 | impl VFS for PhysicalFS { 276 | /// Open the file at this path with the given options 277 | fn open_options(&self, path: &Path, open_options: OpenOptions) -> Result> { 278 | if self.readonly 279 | && (open_options.write 280 | || open_options.create 281 | || open_options.append 282 | || open_options.truncate) 283 | { 284 | let msg = format!( 285 | "Cannot alter file {:?} in root {:?}, filesystem read-only", 286 | path, self 287 | ); 288 | return Err(Error::VfsError(msg)); 289 | } 290 | self.create_root()?; 291 | let p = self.to_absolute(path)?; 292 | open_options 293 | .to_fs_openoptions() 294 | .open(p) 295 | .map(|x| Box::new(x) as Box) 296 | .map_err(Error::from) 297 | } 298 | 299 | /// Create a directory at the location by this path 300 | fn mkdir(&self, path: &Path) -> Result { 301 | if self.readonly { 302 | return Err(Error::VfsError( 303 | "Tried to make directory {} but FS is \ 304 | read-only" 305 | .to_string(), 306 | )); 307 | } 308 | self.create_root()?; 309 | let p = self.to_absolute(path)?; 310 | //println!("Creating {:?}", p); 311 | fs::DirBuilder::new() 312 | .recursive(true) 313 | .create(p) 314 | .map_err(Error::from) 315 | } 316 | 317 | /// Remove a file 318 | fn rm(&self, path: &Path) -> Result { 319 | if self.readonly { 320 | return Err(Error::VfsError( 321 | "Tried to remove file {} but FS is read-only".to_string(), 322 | )); 323 | } 324 | 325 | self.create_root()?; 326 | let p = self.to_absolute(path)?; 327 | if p.is_dir() { 328 | fs::remove_dir(p).map_err(Error::from) 329 | } else { 330 | fs::remove_file(p).map_err(Error::from) 331 | } 332 | } 333 | 334 | /// Remove a file or directory and all its contents 335 | fn rmrf(&self, path: &Path) -> Result { 336 | if self.readonly { 337 | return Err(Error::VfsError( 338 | "Tried to remove file/dir {} but FS is \ 339 | read-only" 340 | .to_string(), 341 | )); 342 | } 343 | 344 | self.create_root()?; 345 | let p = self.to_absolute(path)?; 346 | if p.is_dir() { 347 | fs::remove_dir_all(p).map_err(Error::from) 348 | } else { 349 | fs::remove_file(p).map_err(Error::from) 350 | } 351 | } 352 | 353 | /// Check if the file exists 354 | fn exists(&self, path: &Path) -> bool { 355 | match self.to_absolute(path) { 356 | Ok(p) => p.exists(), 357 | _ => false, 358 | } 359 | } 360 | 361 | /// Get the file's metadata 362 | fn metadata(&self, path: &Path) -> Result> { 363 | self.create_root()?; 364 | let p = self.to_absolute(path)?; 365 | p.metadata() 366 | .map(|m| Box::new(PhysicalMetadata(m)) as Box) 367 | .map_err(Error::from) 368 | } 369 | 370 | /// Retrieve the path entries in this path 371 | fn read_dir(&self, path: &Path) -> Result>>> { 372 | self.create_root()?; 373 | let p = self.to_absolute(path)?; 374 | // This is inconvenient because path() returns the full absolute 375 | // path of the bloody file, which is NOT what we want! 376 | // But if we use file_name() to just get the name then it is ALSO not what we want! 377 | // what we WANT is the full absolute file path, *relative to the resources dir*. 378 | // So that we can do read_dir("/foobar/"), and for each file, open it and query 379 | // it and such by name. 380 | // So we build the paths ourself. 381 | let direntry_to_path = |entry: &fs::DirEntry| -> Result { 382 | let fname = entry 383 | .file_name() 384 | .into_string() 385 | .expect("Non-unicode char in file path? Should never happen, I hope!"); 386 | let mut pathbuf = PathBuf::from(path); 387 | pathbuf.push(fname); 388 | Ok(pathbuf) 389 | }; 390 | let itr = fs::read_dir(p)? 391 | .map(|entry| direntry_to_path(&entry?)) 392 | .collect::>() 393 | .into_iter(); 394 | Ok(Box::new(itr)) 395 | } 396 | 397 | /// Retrieve the actual location of the VFS root, if available. 398 | fn to_path_buf(&self) -> Option { 399 | Some(self.root.clone()) 400 | } 401 | } 402 | 403 | /// A structure that joins several VFS's together in order. 404 | /// 405 | /// So if a file isn't found in one FS it will search through them 406 | /// looking for it and return the 407 | #[derive(Debug)] 408 | pub struct OverlayFS { 409 | roots: Vec>, 410 | } 411 | 412 | impl OverlayFS { 413 | /// New OverlayFS containing zero filesystems. 414 | pub fn new() -> Self { 415 | Self { roots: Vec::new() } 416 | } 417 | 418 | /// Adds a new VFS to the end of the list. 419 | pub fn push(&mut self, fs: Box) { 420 | self.roots.push(fs); 421 | } 422 | 423 | /// Get a reference to the inner file systems, 424 | /// in search order. 425 | pub fn roots(&self) -> &[Box] { 426 | &self.roots 427 | } 428 | } 429 | 430 | impl VFS for OverlayFS { 431 | /// Open the file at this path with the given options 432 | fn open_options(&self, path: &Path, open_options: OpenOptions) -> Result> { 433 | let mut tried: Vec<(PathBuf, Error)> = vec![]; 434 | 435 | for vfs in &self.roots { 436 | match vfs.open_options(path, open_options) { 437 | Err(e) => { 438 | if let Some(vfs_path) = vfs.to_path_buf() { 439 | tried.push((vfs_path, e)); 440 | } else { 441 | tried.push((PathBuf::from(""), e)); 442 | } 443 | } 444 | f => return f, 445 | } 446 | } 447 | let errmessage = String::from(convenient_path_to_str(path)?); 448 | Err(Error::ResourceNotFound(errmessage, tried)) 449 | } 450 | 451 | /// Create a directory at the location by this path 452 | fn mkdir(&self, path: &Path) -> Result { 453 | for vfs in &self.roots { 454 | match vfs.mkdir(path) { 455 | Err(_) => (), 456 | f => return f, 457 | } 458 | } 459 | Err(Error::VfsError(format!( 460 | "Could not find anywhere writeable to make dir {:?}", 461 | path 462 | ))) 463 | } 464 | 465 | /// Remove a file 466 | fn rm(&self, path: &Path) -> Result { 467 | for vfs in &self.roots { 468 | match vfs.rm(path) { 469 | Err(_) => (), 470 | f => return f, 471 | } 472 | } 473 | Err(Error::VfsError(format!("Could not remove file {:?}", path))) 474 | } 475 | 476 | /// Remove a file or directory and all its contents 477 | fn rmrf(&self, path: &Path) -> Result { 478 | for vfs in &self.roots { 479 | match vfs.rmrf(path) { 480 | Err(_) => (), 481 | f => return f, 482 | } 483 | } 484 | Err(Error::VfsError(format!( 485 | "Could not remove file/dir {:?}", 486 | path 487 | ))) 488 | } 489 | 490 | /// Check if the file exists 491 | fn exists(&self, path: &Path) -> bool { 492 | for vfs in &self.roots { 493 | if vfs.exists(path) { 494 | return true; 495 | } 496 | } 497 | 498 | false 499 | } 500 | 501 | /// Get the file's metadata 502 | fn metadata(&self, path: &Path) -> Result> { 503 | for vfs in &self.roots { 504 | match vfs.metadata(path) { 505 | Err(_) => (), 506 | f => return f, 507 | } 508 | } 509 | Err(Error::VfsError(format!( 510 | "Could not get metadata for file/dir {:?}", 511 | path 512 | ))) 513 | } 514 | 515 | /// Retrieve the path entries in this path 516 | fn read_dir(&self, path: &Path) -> Result>>> { 517 | // This is tricky 'cause we have to actually merge iterators together... 518 | // Doing it the simple and stupid way works though. 519 | let mut v = Vec::new(); 520 | for fs in &self.roots { 521 | if let Ok(rddir) = fs.read_dir(path) { 522 | v.extend(rddir) 523 | } 524 | } 525 | Ok(Box::new(v.into_iter())) 526 | } 527 | 528 | /// Retrieve the actual location of the VFS root, if available. 529 | fn to_path_buf(&self) -> Option { 530 | None 531 | } 532 | } 533 | 534 | trait ZipArchiveAccess { 535 | fn by_name<'a>(&'a mut self, name: &str) -> zip::result::ZipResult>; 536 | fn by_index<'a>( 537 | &'a mut self, 538 | file_number: usize, 539 | ) -> zip::result::ZipResult>; 540 | fn len(&self) -> usize; 541 | } 542 | 543 | impl ZipArchiveAccess for zip::ZipArchive { 544 | fn by_name(&mut self, name: &str) -> zip::result::ZipResult { 545 | self.by_name(name) 546 | } 547 | 548 | fn by_index(&mut self, file_number: usize) -> zip::result::ZipResult { 549 | self.by_index(file_number) 550 | } 551 | 552 | fn len(&self) -> usize { 553 | self.len() 554 | } 555 | } 556 | 557 | impl Debug for dyn ZipArchiveAccess { 558 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 559 | // Hide the contents; for an io::Cursor, this would print what is 560 | // likely to be megabytes of data. 561 | write!(f, "") 562 | } 563 | } 564 | 565 | /// A filesystem backed by a zip file. 566 | #[derive(Debug)] 567 | pub struct ZipFS { 568 | // It's... a bit jankity. 569 | // Zip files aren't really designed to be virtual filesystems, 570 | // and the structure of the `zip` crate doesn't help. See the various 571 | // issues that have been filed on it by icefoxen. 572 | // 573 | // ALSO THE SEMANTICS OF ZIPARCHIVE AND HAVING ZIPFILES BORROW IT IS 574 | // HORRIFICALLY BROKEN BY DESIGN SO WE'RE JUST GONNA REFCELL IT AND COPY 575 | // ALL CONTENTS OUT OF IT AAAAA. 576 | source: Option, 577 | archive: RefCell>, 578 | // We keep an index of what files are in the zip file 579 | // because trying to read it lazily is a pain in the butt. 580 | index: Vec, 581 | } 582 | 583 | impl ZipFS { 584 | /// Make new VFS from a zip file 585 | pub fn new(filename: &Path) -> Result { 586 | let f = fs::File::open(filename)?; 587 | let archive = Box::new(zip::ZipArchive::new(f)?); 588 | ZipFS::from_boxed_archive(archive, Some(filename.into())) 589 | } 590 | 591 | /// Creates a `ZipFS` from any `Read+Seek` object, most useful with an 592 | /// in-memory `std::io::Cursor`. 593 | pub fn from_read(reader: R) -> Result 594 | where 595 | R: Read + Seek + 'static, 596 | { 597 | let archive = Box::new(zip::ZipArchive::new(reader)?); 598 | ZipFS::from_boxed_archive(archive, None) 599 | } 600 | 601 | fn from_boxed_archive( 602 | mut archive: Box, 603 | source: Option, 604 | ) -> Result { 605 | let items = (0..archive.len()).map(|i| { 606 | archive 607 | .by_index(i) 608 | .map_err(Error::from) 609 | .map(|item| item.name().to_string()) 610 | }); 611 | 612 | // Propagate first bad result... 613 | let idx = items.collect::>>()?; 614 | 615 | // Or ignore bad results entirely... 616 | /* 617 | let idx = items 618 | .filter_map(Result::ok) 619 | .collect::>(); 620 | */ 621 | 622 | Ok(Self { 623 | source, 624 | archive: RefCell::new(archive), 625 | index: idx, 626 | }) 627 | } 628 | } 629 | 630 | /// A wrapper to contain a zipfile so we can implement 631 | /// (janky) Seek on it and such. 632 | /// 633 | /// We're going to do it the *really* janky way and just read 634 | /// the whole `ZipFile` into a buffer, which is kind of awful but means 635 | /// we don't have to deal with lifetimes, self-borrowing structs, 636 | /// rental, re-implementing Seek on compressed data, making multiple zip 637 | /// zip file objects share a single file handle, or any of that 638 | /// other nonsense. 639 | #[derive(Clone)] 640 | pub struct ZipFileWrapper { 641 | buffer: io::Cursor>, 642 | } 643 | 644 | impl ZipFileWrapper { 645 | fn new(z: &mut zip::read::ZipFile) -> Result { 646 | let mut b = Vec::new(); 647 | let _ = z.read_to_end(&mut b)?; 648 | Ok(Self { 649 | buffer: io::Cursor::new(b), 650 | }) 651 | } 652 | } 653 | 654 | impl io::Read for ZipFileWrapper { 655 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 656 | self.buffer.read(buf) 657 | } 658 | } 659 | 660 | impl io::Write for ZipFileWrapper { 661 | fn write(&mut self, _buf: &[u8]) -> io::Result { 662 | panic!("Cannot write to a zip file!") 663 | } 664 | 665 | fn flush(&mut self) -> io::Result<()> { 666 | Ok(()) 667 | } 668 | } 669 | 670 | impl io::Seek for ZipFileWrapper { 671 | fn seek(&mut self, pos: io::SeekFrom) -> io::Result { 672 | self.buffer.seek(pos) 673 | } 674 | } 675 | 676 | impl Debug for ZipFileWrapper { 677 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 678 | write!(f, "") 679 | } 680 | } 681 | 682 | #[derive(Debug, Copy, Clone, PartialEq)] 683 | struct ZipMetadata { 684 | len: u64, 685 | is_dir: bool, 686 | is_file: bool, 687 | } 688 | 689 | impl ZipMetadata { 690 | /// Returns a ZipMetadata, or None if the file does not exist or such. 691 | /// This is not QUITE correct; since zip archives don't actually have 692 | /// directories (just long filenames), we can't get a directory's metadata 693 | /// this way without basically just faking it. 694 | /// 695 | /// This does make listing a directory rather screwy. 696 | fn new(name: &str, archive: &mut dyn ZipArchiveAccess) -> Option { 697 | match archive.by_name(name) { 698 | Err(_) => None, 699 | Ok(zipfile) => { 700 | let len = zipfile.size(); 701 | Some(ZipMetadata { 702 | len, 703 | is_file: true, 704 | is_dir: false, // mu 705 | }) 706 | } 707 | } 708 | } 709 | } 710 | 711 | impl VMetadata for ZipMetadata { 712 | fn is_dir(&self) -> bool { 713 | self.is_dir 714 | } 715 | fn is_file(&self) -> bool { 716 | self.is_file 717 | } 718 | fn len(&self) -> u64 { 719 | self.len 720 | } 721 | } 722 | 723 | impl VFS for ZipFS { 724 | fn open_options(&self, path: &Path, open_options: OpenOptions) -> Result> { 725 | // Zip is readonly 726 | let path = convenient_path_to_str(path)?; 727 | if open_options.write || open_options.create || open_options.append || open_options.truncate 728 | { 729 | let msg = format!( 730 | "Cannot alter file {:?} in zipfile {:?}, filesystem read-only", 731 | path, self 732 | ); 733 | return Err(Error::VfsError(msg)); 734 | } 735 | let mut stupid_archive_borrow = self.archive 736 | .try_borrow_mut() 737 | .expect("Couldn't borrow ZipArchive in ZipFS::open_options(); should never happen! Report a bug at https://github.com/ggez/gvfs/"); 738 | let mut f = stupid_archive_borrow.by_name(path)?; 739 | let zipfile = ZipFileWrapper::new(&mut f)?; 740 | Ok(Box::new(zipfile) as Box) 741 | } 742 | 743 | fn mkdir(&self, path: &Path) -> Result { 744 | let msg = format!( 745 | "Cannot mkdir {:?} in zipfile {:?}, filesystem read-only", 746 | path, self 747 | ); 748 | Err(Error::VfsError(msg)) 749 | } 750 | 751 | fn rm(&self, path: &Path) -> Result { 752 | let msg = format!( 753 | "Cannot rm {:?} in zipfile {:?}, filesystem read-only", 754 | path, self 755 | ); 756 | Err(Error::VfsError(msg)) 757 | } 758 | 759 | fn rmrf(&self, path: &Path) -> Result { 760 | let msg = format!( 761 | "Cannot rmrf {:?} in zipfile {:?}, filesystem read-only", 762 | path, self 763 | ); 764 | Err(Error::VfsError(msg)) 765 | } 766 | 767 | fn exists(&self, path: &Path) -> bool { 768 | let mut stupid_archive_borrow = self.archive 769 | .try_borrow_mut() 770 | .expect("Couldn't borrow ZipArchive in ZipFS::exists(); should never happen! Report a bug at https://github.com/ggez/gvfs/"); 771 | if let Ok(path) = convenient_path_to_str(path) { 772 | stupid_archive_borrow.by_name(path).is_ok() 773 | } else { 774 | false 775 | } 776 | } 777 | 778 | fn metadata(&self, path: &Path) -> Result> { 779 | let path = convenient_path_to_str(path)?; 780 | let mut stupid_archive_borrow = self.archive 781 | .try_borrow_mut() 782 | .expect("Couldn't borrow ZipArchive in ZipFS::metadata(); should never happen! Report a bug at https://github.com/ggez/gvfs/"); 783 | match ZipMetadata::new(path, &mut **stupid_archive_borrow) { 784 | None => Err(Error::VfsError(format!( 785 | "Metadata not found in zip file for {}", 786 | path 787 | ))), 788 | Some(md) => Ok(Box::new(md) as Box), 789 | } 790 | } 791 | 792 | /// Zip files don't have real directories, so we (incorrectly) hack it by 793 | /// just looking for a path prefix for now. 794 | fn read_dir(&self, path: &Path) -> Result>>> { 795 | let path = convenient_path_to_str(path)?; 796 | let itr = self 797 | .index 798 | .iter() 799 | .filter(|s| s.starts_with(path)) 800 | .map(|s| Ok(PathBuf::from(s))) 801 | .collect::>(); 802 | Ok(Box::new(itr.into_iter())) 803 | } 804 | 805 | fn to_path_buf(&self) -> Option { 806 | self.source.clone() 807 | } 808 | } 809 | 810 | #[cfg(test)] 811 | mod tests { 812 | use super::*; 813 | use std::io::{self, BufRead}; 814 | 815 | #[test] 816 | fn headless_test_path_filtering() { 817 | // Valid paths 818 | let p = path::Path::new("/foo"); 819 | assert!(sanitize_path(p).is_some()); 820 | 821 | let p = path::Path::new("/foo/"); 822 | assert!(sanitize_path(p).is_some()); 823 | 824 | let p = path::Path::new("/foo/bar.txt"); 825 | assert!(sanitize_path(p).is_some()); 826 | 827 | let p = path::Path::new("/"); 828 | assert!(sanitize_path(p).is_some()); 829 | 830 | // Invalid paths 831 | let p = path::Path::new("../foo"); 832 | assert!(sanitize_path(p).is_none()); 833 | 834 | let p = path::Path::new("foo"); 835 | assert!(sanitize_path(p).is_none()); 836 | 837 | let p = path::Path::new("/foo/../../"); 838 | assert!(sanitize_path(p).is_none()); 839 | 840 | let p = path::Path::new("/foo/../bop"); 841 | assert!(sanitize_path(p).is_none()); 842 | 843 | let p = path::Path::new("/../bar"); 844 | assert!(sanitize_path(p).is_none()); 845 | 846 | let p = path::Path::new(""); 847 | assert!(sanitize_path(p).is_none()); 848 | } 849 | 850 | #[test] 851 | fn headless_test_read() { 852 | let cargo_path = Path::new(env!("CARGO_MANIFEST_DIR")); 853 | let fs = PhysicalFS::new(cargo_path, true); 854 | let f = fs.open(Path::new("/Cargo.toml")).unwrap(); 855 | let mut bf = io::BufReader::new(f); 856 | let mut s = String::new(); 857 | let _ = bf.read_line(&mut s).unwrap(); 858 | // Trim whitespace from string 'cause it will 859 | // potentially be different on Windows and Unix. 860 | let trimmed_string = s.trim(); 861 | assert_eq!(trimmed_string, "[package]"); 862 | } 863 | 864 | #[test] 865 | fn headless_test_read_overlay() { 866 | let cargo_path = Path::new(env!("CARGO_MANIFEST_DIR")); 867 | let fs1 = PhysicalFS::new(cargo_path, true); 868 | let mut f2path = PathBuf::from(cargo_path); 869 | f2path.push("src"); 870 | let fs2 = PhysicalFS::new(&f2path, true); 871 | let mut ofs = OverlayFS::new(); 872 | ofs.push(Box::new(fs1)); 873 | ofs.push(Box::new(fs2)); 874 | 875 | assert!(ofs.exists(Path::new("/Cargo.toml"))); 876 | assert!(ofs.exists(Path::new("/lib.rs"))); 877 | assert!(!ofs.exists(Path::new("/foobaz.rs"))); 878 | } 879 | 880 | #[test] 881 | fn headless_test_physical_all() { 882 | let cargo_path = Path::new(env!("CARGO_MANIFEST_DIR")); 883 | let fs = PhysicalFS::new(cargo_path, false); 884 | let testdir = Path::new("/testdir"); 885 | let f1 = Path::new("/testdir/file1.txt"); 886 | 887 | // Delete testdir if it is still lying around 888 | if fs.exists(testdir) { 889 | fs.rmrf(testdir).unwrap(); 890 | } 891 | assert!(!fs.exists(testdir)); 892 | 893 | // Create and delete test dir 894 | fs.mkdir(testdir).unwrap(); 895 | assert!(fs.exists(testdir)); 896 | fs.rm(testdir).unwrap(); 897 | assert!(!fs.exists(testdir)); 898 | 899 | let test_string = "Foo!"; 900 | fs.mkdir(testdir).unwrap(); 901 | { 902 | let mut f = fs.append(f1).unwrap(); 903 | let _ = f.write(test_string.as_bytes()).unwrap(); 904 | } 905 | { 906 | let mut buf = Vec::new(); 907 | let mut f = fs.open(f1).unwrap(); 908 | let _ = f.read_to_end(&mut buf).unwrap(); 909 | assert_eq!(&buf[..], test_string.as_bytes()); 910 | } 911 | 912 | { 913 | // Test metadata() 914 | let m = fs.metadata(f1).unwrap(); 915 | assert!(m.is_file()); 916 | assert!(!m.is_dir()); 917 | assert_eq!(m.len(), 4); 918 | 919 | let m = fs.metadata(testdir).unwrap(); 920 | assert!(!m.is_file()); 921 | assert!(m.is_dir()); 922 | // Not exactly sure what the "length" of a directory is, buuuuuut... 923 | // It appears to vary based on the platform in fact. 924 | // On my desktop, it's 18. 925 | // On Travis's VM, it's 4096. 926 | // On Appveyor's VM, it's 0. 927 | // So, it's meaningless. 928 | //assert_eq!(m.len(), 18); 929 | } 930 | 931 | { 932 | // Test read_dir() 933 | let r = fs.read_dir(testdir).unwrap(); 934 | assert_eq!(r.count(), 1); 935 | let r = fs.read_dir(testdir).unwrap(); 936 | for f in r { 937 | let fname = f.unwrap(); 938 | assert!(fs.exists(&fname)); 939 | } 940 | } 941 | 942 | { 943 | assert!(fs.exists(f1)); 944 | fs.rm(f1).unwrap(); 945 | assert!(!fs.exists(f1)); 946 | } 947 | 948 | fs.rmrf(testdir).unwrap(); 949 | assert!(!fs.exists(testdir)); 950 | } 951 | 952 | fn make_zip_fs() -> Box { 953 | let mut finished_zip_bytes: io::Cursor<_> = { 954 | let zip_bytes = io::Cursor::new(vec![]); 955 | let mut zip_archive = zip::ZipWriter::new(zip_bytes); 956 | 957 | zip_archive 958 | .start_file("fake_file_name.txt", zip::write::FileOptions::default()) 959 | .unwrap(); 960 | let _bytes = zip_archive.write(b"Zip contents!").unwrap(); 961 | zip_archive.add_directory("fake_dir", zip::write::FileOptions::default()) 962 | .unwrap(); 963 | zip_archive 964 | .start_file("fake_dir/file.txt", zip::write::FileOptions::default()) 965 | .unwrap(); 966 | let _bytes = zip_archive.write(b"Zip contents!").unwrap(); 967 | 968 | zip_archive.finish().unwrap() 969 | }; 970 | 971 | let _bytes = finished_zip_bytes.seek(io::SeekFrom::Start(0)).unwrap(); 972 | let zfs = ZipFS::from_read(finished_zip_bytes).unwrap(); 973 | Box::new(zfs) 974 | } 975 | 976 | #[test] 977 | fn test_zip_files() { 978 | let zfs = make_zip_fs(); 979 | 980 | assert!(zfs.exists(Path::new("fake_file_name.txt".into()))); 981 | 982 | let mut contents = String::new(); 983 | let _bytes = zfs 984 | .open(Path::new("fake_file_name.txt")) 985 | .unwrap() 986 | .read_to_string(&mut contents) 987 | .unwrap(); 988 | assert_eq!(contents, "Zip contents!"); 989 | } 990 | 991 | #[test] 992 | fn headless_test_zip_all() { 993 | let fs = make_zip_fs(); 994 | let testdir = Path::new("/testdir"); 995 | let testfile = Path::new("/file1.txt"); 996 | // TODO: Fix absolute vs. relative paths for zip files... 997 | let existing_file = Path::new("fake_file_name.txt"); 998 | let existing_dir = Path::new("fake_dir"); 999 | 1000 | assert!(!fs.exists(testfile)); 1001 | assert!(!fs.exists(testdir)); 1002 | assert!(fs.exists(existing_file)); 1003 | // TODO: This fails, why? 1004 | //assert!(fs.exists(existing_dir)); 1005 | 1006 | 1007 | // Create and delete test dir -- which always fails 1008 | assert!(fs.mkdir(testdir).is_err()); 1009 | assert!(!fs.exists(testdir)); 1010 | assert!(fs.rm(testdir).is_err()); 1011 | 1012 | // Reading an existing file succeeds. 1013 | let _ = fs.open(existing_file).unwrap(); 1014 | // Writing to a new fails 1015 | assert!(fs.create(testfile).is_err()); 1016 | // Appending a file fails 1017 | assert!(fs.append(testfile).is_err()); 1018 | 1019 | { 1020 | // Test metadata() 1021 | let m = fs.metadata(existing_file).unwrap(); 1022 | assert!(m.is_file()); 1023 | assert!(!m.is_dir()); 1024 | assert_eq!(m.len(), 13); 1025 | 1026 | // TODO: Fix 1027 | /* 1028 | let m = fs.metadata(existing_dir).unwrap(); 1029 | assert!(!m.is_file()); 1030 | assert!(m.is_dir()); 1031 | */ 1032 | 1033 | assert!(fs.metadata(testfile).is_err()); 1034 | } 1035 | 1036 | { 1037 | // TODO: Test read_dir() 1038 | /* 1039 | let r = fs.read_dir(existing_dir).unwrap(); 1040 | assert_eq!(r.count(), 1); 1041 | let r = fs.read_dir(testdir).unwrap(); 1042 | for f in r { 1043 | let fname = f.unwrap(); 1044 | assert!(fs.exists(&fname)); 1045 | } 1046 | */ 1047 | } 1048 | 1049 | assert!(fs.rmrf(testdir).is_err()); 1050 | assert!(fs.rmrf(existing_dir).is_err()); 1051 | 1052 | } 1053 | 1054 | // BUGGO: TODO: Make sure all functions are tested for OverlayFS and ZipFS!! 1055 | } 1056 | -------------------------------------------------------------------------------- /tests/skeptic.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/skeptic-tests.rs")); 2 | -------------------------------------------------------------------------------- /watch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cargo watch --delay 1.5 -c -x fmt -x check -x build 3 | --------------------------------------------------------------------------------