[
  {
    "path": ".gitignore",
    "content": "/target\n/temp_disks\n.vscode/launch.json\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"rust-analyzer.runnables.extraEnv\": {\n        \"RUST_LOG\": \"info,fluster_fs=debug\" // debug level logs in tests\n    },\n    \"rust-analyzer.runnables.cargo.args\": [\n        \"--no-fail-fast\",\n        \"--\",\n        \"--nocapture\"\n    ],\n    \"cSpell.words\": [\n        \"uncatagorized\"\n    ]\n}"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"fluster_fs\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nbitflags = \"2.9.1\"\nclap = { version = \"4.5.41\", features = [\"derive\"] }\ncrc32c = \"0.6.8\"\nctrlc = \"3.4.7\"\nenum_dispatch = \"0.3.13\"\nenv_logger = \"0.11.8\"\nfuse_mt = \"0.6.1\"\nlazy_static = \"1.5.0\"\nlibc = \"0.2.174\"\nlog = \"0.4.27\"\nlog-panics = { version = \"2.1.0\", features = [\"with-backtrace\"] }\nonce_cell = \"1.21.3\"\noneshot = \"0.1.11\"\nrand = \"0.9.1\"\nratatui = \"0.29.0\"\nrprompt = \"2.2.0\"\ntempfile = \"3.20.0\"\ntest-log = \"0.2.18\"\nthiserror = \"2.0.12\"\ntui-logger = \"0.17.3\"\ntui-textarea = \"0.7.0\"\n\n# Floppy builds should be small enough to fit on a single floppy.\n# Because thats funny.\n[profile.floppy]\ninherits = \"release\"\nstrip = true  # Automatically strip symbols from the binary.\nopt-level = \"z\"  # Optimize for size.\nlto = true\ncodegen-units = 1\n# panic = \"abort\" # We will NOT use abort. Abort is gross."
  },
  {
    "path": "LICENSE.txt",
    "content": "Attribution 4.0 International\n\n=======================================================================\n\nCreative Commons Corporation (\"Creative Commons\") is not a law firm and\ndoes not provide legal services or legal advice. Distribution of\nCreative Commons public licenses does not create a lawyer-client or\nother relationship. Creative Commons makes its licenses and related\ninformation available on an \"as-is\" basis. Creative Commons gives no\nwarranties regarding its licenses, any material licensed under their\nterms and conditions, or any related information. Creative Commons\ndisclaims all liability for damages resulting from their use to the\nfullest extent possible.\n\nUsing Creative Commons Public Licenses\n\nCreative Commons public licenses provide a standard set of terms and\nconditions that creators and other rights holders may use to share\noriginal works of authorship and other material subject to copyright\nand certain other rights specified in the public license below. The\nfollowing considerations are for informational purposes only, are not\nexhaustive, and do not form part of our licenses.\n\n     Considerations for licensors: Our public licenses are\n     intended for use by those authorized to give the public\n     permission to use material in ways otherwise restricted by\n     copyright and certain other rights. Our licenses are\n     irrevocable. Licensors should read and understand the terms\n     and conditions of the license they choose before applying it.\n     Licensors should also secure all rights necessary before\n     applying our licenses so that the public can reuse the\n     material as expected. Licensors should clearly mark any\n     material not subject to the license. This includes other CC-\n     licensed material, or material used under an exception or\n     limitation to copyright. More considerations for licensors:\n    wiki.creativecommons.org/Considerations_for_licensors\n\n     Considerations for the public: By using one of our public\n     licenses, a licensor grants the public permission to use the\n     licensed material under specified terms and conditions. If\n     the licensor's permission is not necessary for any reason--for\n     example, because of any applicable exception or limitation to\n     copyright--then that use is not regulated by the license. Our\n     licenses grant only permissions under copyright and certain\n     other rights that a licensor has authority to grant. Use of\n     the licensed material may still be restricted for other\n     reasons, including because others have copyright or other\n     rights in the material. A licensor may make special requests,\n     such as asking that all changes be marked or described.\n     Although not required by our licenses, you are encouraged to\n     respect those requests where reasonable. More considerations\n     for the public:\n    wiki.creativecommons.org/Considerations_for_licensees\n\n=======================================================================\n\nCreative Commons Attribution 4.0 International Public License\n\nBy exercising the Licensed Rights (defined below), You accept and agree\nto be bound by the terms and conditions of this Creative Commons\nAttribution 4.0 International Public License (\"Public License\"). To the\nextent this Public License may be interpreted as a contract, You are\ngranted the Licensed Rights in consideration of Your acceptance of\nthese terms and conditions, and the Licensor grants You such rights in\nconsideration of benefits the Licensor receives from making the\nLicensed Material available under these terms and conditions.\n\n\nSection 1 -- Definitions.\n\n  a. Adapted Material means material subject to Copyright and Similar\n     Rights that is derived from or based upon the Licensed Material\n     and in which the Licensed Material is translated, altered,\n     arranged, transformed, or otherwise modified in a manner requiring\n     permission under the Copyright and Similar Rights held by the\n     Licensor. For purposes of this Public License, where the Licensed\n     Material is a musical work, performance, or sound recording,\n     Adapted Material is always produced where the Licensed Material is\n     synched in timed relation with a moving image.\n\n  b. Adapter's License means the license You apply to Your Copyright\n     and Similar Rights in Your contributions to Adapted Material in\n     accordance with the terms and conditions of this Public License.\n\n  c. Copyright and Similar Rights means copyright and/or similar rights\n     closely related to copyright including, without limitation,\n     performance, broadcast, sound recording, and Sui Generis Database\n     Rights, without regard to how the rights are labeled or\n     categorized. For purposes of this Public License, the rights\n     specified in Section 2(b)(1)-(2) are not Copyright and Similar\n     Rights.\n\n  d. Effective Technological Measures means those measures that, in the\n     absence of proper authority, may not be circumvented under laws\n     fulfilling obligations under Article 11 of the WIPO Copyright\n     Treaty adopted on December 20, 1996, and/or similar international\n     agreements.\n\n  e. Exceptions and Limitations means fair use, fair dealing, and/or\n     any other exception or limitation to Copyright and Similar Rights\n     that applies to Your use of the Licensed Material.\n\n  f. Licensed Material means the artistic or literary work, database,\n     or other material to which the Licensor applied this Public\n     License.\n\n  g. Licensed Rights means the rights granted to You subject to the\n     terms and conditions of this Public License, which are limited to\n     all Copyright and Similar Rights that apply to Your use of the\n     Licensed Material and that the Licensor has authority to license.\n\n  h. Licensor means the individual(s) or entity(ies) granting rights\n     under this Public License.\n\n  i. Share means to provide material to the public by any means or\n     process that requires permission under the Licensed Rights, such\n     as reproduction, public display, public performance, distribution,\n     dissemination, communication, or importation, and to make material\n     available to the public including in ways that members of the\n     public may access the material from a place and at a time\n     individually chosen by them.\n\n  j. Sui Generis Database Rights means rights other than copyright\n     resulting from Directive 96/9/EC of the European Parliament and of\n     the Council of 11 March 1996 on the legal protection of databases,\n     as amended and/or succeeded, as well as other essentially\n     equivalent rights anywhere in the world.\n\n  k. You means the individual or entity exercising the Licensed Rights\n     under this Public License. Your has a corresponding meaning.\n\n\nSection 2 -- Scope.\n\n  a. License grant.\n\n       1. Subject to the terms and conditions of this Public License,\n          the Licensor hereby grants You a worldwide, royalty-free,\n          non-sublicensable, non-exclusive, irrevocable license to\n          exercise the Licensed Rights in the Licensed Material to:\n\n            a. reproduce and Share the Licensed Material, in whole or\n               in part; and\n\n            b. produce, reproduce, and Share Adapted Material.\n\n       2. Exceptions and Limitations. For the avoidance of doubt, where\n          Exceptions and Limitations apply to Your use, this Public\n          License does not apply, and You do not need to comply with\n          its terms and conditions.\n\n       3. Term. The term of this Public License is specified in Section\n          6(a).\n\n       4. Media and formats; technical modifications allowed. The\n          Licensor authorizes You to exercise the Licensed Rights in\n          all media and formats whether now known or hereafter created,\n          and to make technical modifications necessary to do so. The\n          Licensor waives and/or agrees not to assert any right or\n          authority to forbid You from making technical modifications\n          necessary to exercise the Licensed Rights, including\n          technical modifications necessary to circumvent Effective\n          Technological Measures. For purposes of this Public License,\n          simply making modifications authorized by this Section 2(a)\n          (4) never produces Adapted Material.\n\n       5. Downstream recipients.\n\n            a. Offer from the Licensor -- Licensed Material. Every\n               recipient of the Licensed Material automatically\n               receives an offer from the Licensor to exercise the\n               Licensed Rights under the terms and conditions of this\n               Public License.\n\n            b. No downstream restrictions. You may not offer or impose\n               any additional or different terms or conditions on, or\n               apply any Effective Technological Measures to, the\n               Licensed Material if doing so restricts exercise of the\n               Licensed Rights by any recipient of the Licensed\n               Material.\n\n       6. No endorsement. Nothing in this Public License constitutes or\n          may be construed as permission to assert or imply that You\n          are, or that Your use of the Licensed Material is, connected\n          with, or sponsored, endorsed, or granted official status by,\n          the Licensor or others designated to receive attribution as\n          provided in Section 3(a)(1)(A)(i).\n\n  b. Other rights.\n\n       1. Moral rights, such as the right of integrity, are not\n          licensed under this Public License, nor are publicity,\n          privacy, and/or other similar personality rights; however, to\n          the extent possible, the Licensor waives and/or agrees not to\n          assert any such rights held by the Licensor to the limited\n          extent necessary to allow You to exercise the Licensed\n          Rights, but not otherwise.\n\n       2. Patent and trademark rights are not licensed under this\n          Public License.\n\n       3. To the extent possible, the Licensor waives any right to\n          collect royalties from You for the exercise of the Licensed\n          Rights, whether directly or through a collecting society\n          under any voluntary or waivable statutory or compulsory\n          licensing scheme. In all other cases the Licensor expressly\n          reserves any right to collect such royalties.\n\n\nSection 3 -- License Conditions.\n\nYour exercise of the Licensed Rights is expressly made subject to the\nfollowing conditions.\n\n  a. Attribution.\n\n       1. If You Share the Licensed Material (including in modified\n          form), You must:\n\n            a. retain the following if it is supplied by the Licensor\n               with the Licensed Material:\n\n                 i. identification of the creator(s) of the Licensed\n                    Material and any others designated to receive\n                    attribution, in any reasonable manner requested by\n                    the Licensor (including by pseudonym if\n                    designated);\n\n                ii. a copyright notice;\n\n               iii. a notice that refers to this Public License;\n\n                iv. a notice that refers to the disclaimer of\n                    warranties;\n\n                 v. a URI or hyperlink to the Licensed Material to the\n                    extent reasonably practicable;\n\n            b. indicate if You modified the Licensed Material and\n               retain an indication of any previous modifications; and\n\n            c. indicate the Licensed Material is licensed under this\n               Public License, and include the text of, or the URI or\n               hyperlink to, this Public License.\n\n       2. You may satisfy the conditions in Section 3(a)(1) in any\n          reasonable manner based on the medium, means, and context in\n          which You Share the Licensed Material. For example, it may be\n          reasonable to satisfy the conditions by providing a URI or\n          hyperlink to a resource that includes the required\n          information.\n\n       3. If requested by the Licensor, You must remove any of the\n          information required by Section 3(a)(1)(A) to the extent\n          reasonably practicable.\n\n       4. If You Share Adapted Material You produce, the Adapter's\n          License You apply must not prevent recipients of the Adapted\n          Material from complying with this Public License.\n\n\nSection 4 -- Sui Generis Database Rights.\n\nWhere the Licensed Rights include Sui Generis Database Rights that\napply to Your use of the Licensed Material:\n\n  a. for the avoidance of doubt, Section 2(a)(1) grants You the right\n     to extract, reuse, reproduce, and Share all or a substantial\n     portion of the contents of the database;\n\n  b. if You include all or a substantial portion of the database\n     contents in a database in which You have Sui Generis Database\n     Rights, then the database in which You have Sui Generis Database\n     Rights (but not its individual contents) is Adapted Material; and\n\n  c. You must comply with the conditions in Section 3(a) if You Share\n     all or a substantial portion of the contents of the database.\n\nFor the avoidance of doubt, this Section 4 supplements and does not\nreplace Your obligations under this Public License where the Licensed\nRights include other Copyright and Similar Rights.\n\n\nSection 5 -- Disclaimer of Warranties and Limitation of Liability.\n\n  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE\n     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS\n     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF\n     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,\n     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,\n     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR\n     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,\n     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT\n     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT\n     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.\n\n  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE\n     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,\n     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,\n     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,\n     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR\n     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN\n     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR\n     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR\n     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.\n\n  c. The disclaimer of warranties and limitation of liability provided\n     above shall be interpreted in a manner that, to the extent\n     possible, most closely approximates an absolute disclaimer and\n     waiver of all liability.\n\n\nSection 6 -- Term and Termination.\n\n  a. This Public License applies for the term of the Copyright and\n     Similar Rights licensed here. However, if You fail to comply with\n     this Public License, then Your rights under this Public License\n     terminate automatically.\n\n  b. Where Your right to use the Licensed Material has terminated under\n     Section 6(a), it reinstates:\n\n       1. automatically as of the date the violation is cured, provided\n          it is cured within 30 days of Your discovery of the\n          violation; or\n\n       2. upon express reinstatement by the Licensor.\n\n     For the avoidance of doubt, this Section 6(b) does not affect any\n     right the Licensor may have to seek remedies for Your violations\n     of this Public License.\n\n  c. For the avoidance of doubt, the Licensor may also offer the\n     Licensed Material under separate terms or conditions or stop\n     distributing the Licensed Material at any time; however, doing so\n     will not terminate this Public License.\n\n  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public\n     License.\n\n\nSection 7 -- Other Terms and Conditions.\n\n  a. The Licensor shall not be bound by any additional or different\n     terms or conditions communicated by You unless expressly agreed.\n\n  b. Any arrangements, understandings, or agreements regarding the\n     Licensed Material not stated herein are separate from and\n     independent of the terms and conditions of this Public License.\n\n\nSection 8 -- Interpretation.\n\n  a. For the avoidance of doubt, this Public License does not, and\n     shall not be interpreted to, reduce, limit, restrict, or impose\n     conditions on any use of the Licensed Material that could lawfully\n     be made without permission under this Public License.\n\n  b. To the extent possible, if any provision of this Public License is\n     deemed unenforceable, it shall be automatically reformed to the\n     minimum extent necessary to make it enforceable. If the provision\n     cannot be reformed, it shall be severed from this Public License\n     without affecting the enforceability of the remaining terms and\n     conditions.\n\n  c. No term or condition of this Public License will be waived and no\n     failure to comply consented to unless expressly agreed to by the\n     Licensor.\n\n  d. Nothing in this Public License constitutes or may be interpreted\n     as a limitation upon, or waiver of, any privileges and immunities\n     that apply to the Licensor or You, including from the legal\n     processes of any jurisdiction or authority.\n\n\n=======================================================================\n\nCreative Commons is not a party to its public\nlicenses. Notwithstanding, Creative Commons may elect to apply one of\nits public licenses to material it publishes and in those instances\nwill be considered the “Licensor.” The text of the Creative Commons\npublic licenses is dedicated to the public domain under the CC0 Public\nDomain Dedication. Except for the limited purpose of indicating that\nmaterial is shared under a Creative Commons public license or as\notherwise permitted by the Creative Commons policies published at\ncreativecommons.org/policies, Creative Commons does not authorize the\nuse of the trademark \"Creative Commons\" or any other trademark or logo\nof Creative Commons without its prior written consent including,\nwithout limitation, in connection with any unauthorized modifications\nto any of its public licenses or any other arrangements,\nunderstandings, or agreements concerning use of licensed material. For\nthe avoidance of doubt, this paragraph does not form part of the\npublic licenses.\n\nCreative Commons may be contacted at creativecommons.org.\n"
  },
  {
    "path": "build.rs",
    "content": "fn main() {\n    #[cfg(target_os = \"windows\")]\n    panic!(\n        \"The `fuser` crate cannot be built on windows. You must build and use fluster_fs through WSL.\"\n    );\n}\n"
  },
  {
    "path": "readme.md",
    "content": "  \n<h1 align=\"center\">\n  <br>\n  <img src=\"https://github.com/DocJade/fluster_rs/blob/master/img/flustered.png?raw=true\" alt=\"Fluster\" width=\"256\">\n  <br>\n  Fluster\n  <br>\n</h1>\n\n<h3 align=\"center\">A futuristic filesystem that's stuck in the past.</h4>\n\n<p align=\"center\">\n  <img alt=\"Blazingly fast!\" src=\"https://img.shields.io/badge/Blazingly_fast!-000000?logo=rust&logoColor=white\">\n  <a href=\"https://kofi.docjade.com/\">\n    <img alt=\"Support me on Ko-fi!\" src=\"https://img.shields.io/badge/Support%20me%20on%20Ko--fi!-FF5E5B?logo=ko-fi&logoColor=white\">\n  </a>\n  <a href=\"https://en.wikipedia.org/wiki/Gluten\">\n\t<img alt=\"Gluten free!\" src=\"https://img.shields.io/badge/Gluten_free!-blue\">\n</a>\n</p>\n\n<p align=\"center\">\n  <a href=\"#features\">Features</a> •\n  <a href=\"#how-to-use\">How To Use</a> •\n  <a href=\"#credits\">Credits</a> •\n  <a href=\"#license\">License</a>\n</p>\n\n\n<p align=\"center\">\n\t<img src=\"https://github.com/DocJade/fluster_rs/blob/master/img/WindowsTerminal_7J9iESbHvl.png?raw=true\"alt=\"Fluster playing \\\"Bad Apple\\\"\" width=\"720\">\n</p>\n\n## Features\n\n*  Multi-disk\n\t* Spans a single filesystem across as many floppy disks as are required to store the data, treating them as a single pool.\n* Disk failure detection\n\t* Automatically detects and troubleshoot drive and disk issues.\n* Automatic backups\n\t* Floppy disks are unreliable, so every block operation is backed up to `/var/fluster` in case disk recovery is required.\n* Tiered caching\n\t* Triple tiered, in-memory cache to minimize disk swapping, while only using 2 floppy disks worth of memory.\n* Error checking\n\t* Every 512 byte block has a 4 byte CRC to detect corruption or bad reads, and disk operations will automatically retry if the CRC fails.\n* FUSE based\n\t* Built on [FUSE](https://github.com/libfuse/libfuse), which makes Fluster! mountable on any UNIX or UNIX-like system that supports FUSE.\n\n \n## How To Use\n\nTo clone and run Fluster!, you'll need [Rust](www.rust-lang.org), a FUSE implementation, a floppy drive, and at least two floppy disks.\n\n### Prerequisites\n#### For Linux & macOS\n\n- **Install:**\n\t- **Rust:** Follow the official installation [guide](https://www.rust-lang.org/).\n\t- **FUSE:** On most Linux distributions, libfuse is available through your package manager (e.g., sudo apt-get install libfuse-dev).\n\t\t- On macOS, you may need [macFUSE](https://macfuse.github.io/), although I have not tested Fluster! on MacOS at all, since you should use a real operating system.\n\n#### For Windows Users\n- If you're running Fluster! on Windows, please [read this guide](https://github.com/DocJade/fluster_rs/blob/master/windows.md).\n\n### Building and running\n\n#### Build Fluster!:\n```bash\n# Clone the repository\ngit clone https://github.com/DocJade/fluster_rs\n\n# Go into the repository\ncd fluster_fs\n\n# Build with the recommended 'floppy' profile\ncargo build --profile floppy\n```\n\n#### Run Fluster!:\n```bash\n# Example usage:\n# Create a directory to mount the filesystem\nmkdir ~/fluster_mount_point\n# Run the app (requires root privileges for mounting)\nsudo ./target/floppy/fluster_fs --block-device-path \"/dev/sdX\" --mount-point \"~/fluster_mount_point\"\n```\n- Replace /dev/sdX with the actual path to your floppy drive.\n\n#### Unmounting Fluster!:\n```bash\nfusermount -u ~/fluster_mount_point\n```\n- Do note that unmounting Fluster! does not immediately shut down fluster, you will still need to swap disks to flush the cache to disk.\n## Credits\n\n- [DocJade](https://docjade.com/) (That's me!)\n- [Rust](https://www.rust-lang.org/) ([Not the bad Rust](https://rust.facepunch.com/))\n- [The Rippingtons](https://www.rippingtons.com/), who kept me from going insane while writing this.\n- [Femtanyl](https://femtanyl.bandcamp.com/), who helped me go insane while writing this.\n\n## Notes:\nOriginally I planned to keep an up-to-date implementation spec of Fluster! in `./filesystem_design`, but this slowly became more and more out of date, as is now very un-representative of the final product. Some information in there such as Inode blocks and how they are constructed should be mostly up to date. Dense disks aren't real, and they cannot hurt you.\n\nIf you're wondering \"Where is the EXE so I can just run it myself!\" Please understand that there isn't one, and for good reason. If you don't know how to build and run this project yourself, chances are you would use it improperly, and possibly wipe your C: drive.\n\n## See Fluster! in action\n\n[YouTube link](https://www.youtube.com/watch?v=cTPBGZcTRqo)\n\n## You may also like...\n\n- Pornography, search \"boobs\" on google for more info.\n\n## License\n\n<a href=\"https://github.com/DocJade/fluster_rs\">Fluster!</a> © 2025 by <a href=\"https://docjade.com/\">DocJade</a> is licensed under <a href=\"https://creativecommons.org/licenses/by/4.0/\">Creative Commons Attribution 4.0 International</a>\n\n---\n\n> [I should just come up with a cool name and the rest will like as itself.](https://www.youtube.com/watch?v=dmzk5y_mrlg)\n"
  },
  {
    "path": "src/error_types/block.rs",
    "content": "// Blocks usually return similar types of errors.\nuse thiserror::Error;\n\n#[derive(Debug, Error, PartialEq)]\n/// Errors related to block manipulation. Not disk level modification, but our custom block types.\npub enum BlockManipulationError {\n    #[error(\"Adding content to this block failed, due to the block not having enough capacity for the new content.\")]\n    OutOfRoom,\n    #[error(\"This method can only be called on the final block in a chain of this type of block.\")]\n    NotFinalBlockInChain,\n    #[error(\"The arguments given for this operation are out of bounds, or otherwise not supported.\")]\n    Impossible,\n    #[error(\"The data that was attempted to be retrieved from this block did not exist.\")]\n    NotPresent\n}"
  },
  {
    "path": "src/error_types/conversions.rs",
    "content": "// Conversions between all of the lower types.\n\n//\n// Imports\n//\n\nuse std::io::ErrorKind;\nuse std::time::Duration;\nuse log::debug;\nuse log::error;\n\nuse log::warn;\nuse thiserror::Error;\nuse crate::error_types::critical::CriticalError;\nuse crate::error_types::drive::DriveError;\nuse crate::error_types::drive::DriveIOError;\nuse crate::error_types::drive::InvalidDriveReason;\nuse crate::error_types::drive::WrappedIOError;\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\nuse crate::tui::notify::NotifyTui;\nuse crate::tui::tasks::TaskType;\n\n\n\n// Not error type can just be converted upwards willy-nilly, that led to the old\n// and horrible FloppyDiskError type which everything ended up returning. Not good.\n\n// We do not allow string errors. This is RUST damn it, not python!\n\n// We also have a custom conversion error type, so lower level callers can get more info\n// about what they need to do to be able to perform the cast to a higher error type.\n\n#[derive(Debug, Clone, Copy, Error, PartialEq)]\n/// Errors related to IO on the inserted floppy disk.\npub enum CannotConvertError {\n    #[error(\"You must retry this operation. If retrying repeatedly fails, throw a Critical error.\")]\n    MustRetry,\n}\n\n//\n// Drive errors\n//\n\n\nimpl TryFrom<DriveIOError> for DriveError {\n    type Error = CannotConvertError;\n\n    fn try_from(value: DriveIOError) -> Result<Self, Self::Error> {\n        match value {\n            DriveIOError::Retry => {\n                // Operation must be retried, cant cast that upwards.\n                Err(CannotConvertError::MustRetry)\n            },\n        }\n    }\n}\n\n//\n// std::io:Error wrapping\n//\n\nimpl WrappedIOError {\n    pub(crate) fn wrap(io_error: std::io::Error, error_origin: DiskPointer) -> Self {\n        WrappedIOError {\n            io_error,\n            error_origin,\n        }\n    }\n}\n\n//\n// WrappedIOError to DriveIOError\n//\n\nimpl TryFrom<WrappedIOError> for DriveIOError {\n    type Error = CannotConvertError;\n\n    fn try_from(value: WrappedIOError) -> Result<Self, Self::Error> {\n\n        // Sleep for a tad just in case we're doing a retry\n        std::thread::sleep(Duration::from_secs(1));\n\n        // Log where we were trying to do IO at when the error occurred.\n        debug!(\"IO error occured while trying to access disk {} block {}\", value.error_origin.disk, value.error_origin.block);\n        debug!(\"Error type: {:#?}\", value.io_error);\n\n        match value.io_error.kind() {\n            ErrorKind::NotFound => {\n                // The floppy drive path is not there.\n                CriticalError::DriveInaccessible(InvalidDriveReason::NotFound).handle();\n                // If handling worked, can retry.\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::PermissionDenied => {\n                // Dont have permission to perform IO on the drive.\n                // Nothing we can do.\n                CriticalError::DriveInaccessible(InvalidDriveReason::PermissionDenied).handle();\n                // If handling worked, can retry.\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::ConnectionRefused |\n            ErrorKind::ConnectionReset |\n            ErrorKind::HostUnreachable |\n            ErrorKind::NetworkUnreachable |\n            ErrorKind::ConnectionAborted |\n            ErrorKind::NotConnected |\n            ErrorKind::AddrInUse |\n            ErrorKind::AddrNotAvailable  |\n            ErrorKind::NetworkDown |\n            ErrorKind::StaleNetworkFileHandle => {\n                // Okay you should not be using fluster over the network dawg.\n                // 100% your fault\n                CriticalError::DriveInaccessible(InvalidDriveReason::Networking).handle();\n                // We cant recover from that\n                unreachable!(\"Networked floppy drive??? Really??? gtfo\");\n            },\n            ErrorKind::BrokenPipe => {\n                // What\n                // I doubt you could even make fluster start with pipes.\n                unreachable!(\"Broken pipe with fluster, why are you using pipes in the first place???\");\n            },\n            ErrorKind::AlreadyExists => {\n                // Fluster does not create files during IO operations, only in backups.\n                // Therefore this should not happen.\n                // Especially since we always open the backups if they already exist.\n                unreachable!(\"Fluster tried to create a file that already existed somehow. This should be impossible.\");\n            },\n            ErrorKind::WouldBlock => {\n                // Fluster does not ask for blocking IO.\n                // In theory this can just be retried.\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::NotADirectory => {\n                // This should never happen, since we always try to write to a file, not a directory.\n                unreachable!(\"Fluster does not open directories, this is impossible.\");\n            },\n            ErrorKind::IsADirectory => {\n                // User has passed in a directory for the floppy disk drive instead of a file for it.\n                CriticalError::DriveInaccessible(InvalidDriveReason::NotAFile).handle();\n                // We cant recover from that, but pretend we can\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::DirectoryNotEmpty => {\n                // Fluster does not try to delete directories.\n                unreachable!(\"Fluster does not delete directories, this should be impossible.\");\n            },\n            ErrorKind::ReadOnlyFilesystem => {\n                // Cant use fluster on read-only floppy for obvious reasons.\n                CriticalError::DriveInaccessible(InvalidDriveReason::ReadOnly).handle();\n                // If it was just the write-protect notch, we can recover.\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::InvalidInput => {\n                // The paramaters given for the IO action were bad, chances are, retrying this wont\n                // do anything. We're cooked.\n                // But hopefully this shouldn't happen because I'm epic sauce :D\n                unreachable!(\"Invalid input parameters into IO action.\")\n            },\n            ErrorKind::InvalidData => {\n                // See above, blah blah blah epic sauce\n                unreachable!(\"Data not valid for the operation showed up in IO action.\")\n            },\n            ErrorKind::TimedOut => {\n                // The IO took too long, we should be able to try again.\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::WriteZero => {\n                // Writing a complete bytestream failed.\n                // Maybe the operation was canceled and needs to be retried?\n                // Not sure if the floppy drive requires minimum write sizes, but 512 aught to be enough.\n\n                // We dont cast this up, we make the caller retry the write.\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::StorageFull => {\n                // Fluster does not use a filesystem when doing writes to the disk.\n                // Maybe this could happen when attempting to write past the end of the disk?\n                // But we have bounds checking for that.\n                warn!(\"Floppy drive claims to be full, we dont care.\");\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::NotSeekable => {\n                // We must be able to seek files to read and write from them, this is a\n                // configuration issue.\n                CriticalError::DriveInaccessible(InvalidDriveReason::NotSeekable).handle();\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::QuotaExceeded => {\n                // Not sure what other quotas other than size are possible, the man page\n                // quota(1) doesn't specify any other quota types.\n                // Plus, this shouldn't happen for raw IO, right?\n                unreachable!(\"Floppy drives shouldn't have a quota.\");\n            },\n            ErrorKind::FileTooLarge => {\n                // Fluster does not use an underlying filesystem.\n                // Very funny since the biggest files we deal with are in the low MBs\n                unreachable!(\"Somehow a write was too large, even though we dont use a filesystem directly.\");\n            },\n            ErrorKind::ResourceBusy => {\n                // Disk is busy, we can retry though.\n                // Force caller to retry.\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::ExecutableFileBusy => {\n                // If you're somehow running the floppy drive as an executable,\n                // you have bigger issues.\n                unreachable!(\"How are you running the floppy drive as an executable?\");\n            },\n            ErrorKind::Deadlock => {\n                // File locking deadlock, not much we can do here except try again.\n                // Force caller to retry\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::CrossesDevices => {\n                // Fluster does not do renames on the floppy disk path.\n                unreachable!(\"Fluster does not support rename the file paths, this should never happen.\");\n            },\n            ErrorKind::TooManyLinks => {\n                // We do not create links.\n                unreachable!(\"Fluster does not support links, no idea how we got here.\");\n            },\n            ErrorKind::InvalidFilename => {\n                // The path to the disk is invalid somehow.\n                CriticalError::DriveInaccessible(InvalidDriveReason::InvalidPath).handle();\n                // We cant recover from that, but in case we can, just try again.\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::ArgumentListTooLong => {\n                // Fluster does not call programs\n                unreachable!(\"Fluster wasn't able to call an external program. Wait, we don't do that? Huh?\");\n            },\n            ErrorKind::Interrupted => {\n                // \"Interrupted operations can typically be retried.\"\n                // Force caller to retry\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::Unsupported => {\n                // Whatever operation we're trying to do, its not possible.\n                // Not really much we can do here either.\n                CriticalError::DriveInaccessible(InvalidDriveReason::UnsupportedOS).handle();\n                // We cant recover from that, so this will never be returned.\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::UnexpectedEof => {\n                // This would happen if we read past the end of the floppy disk,\n                // which should be protected by guard conditions.\n                // Maybe someone's trying to run fluster with 8\" disks?\n                // We'll just retry the operation, since this should be guarded anyways.\n                // Force caller to retry\n                Err(CannotConvertError::MustRetry)\n            },\n            ErrorKind::OutOfMemory => {\n                // Bro what\n                // Nothing we can really do.\n                panic!(\"Please visit https://downloadmoreram.com/ then re-run Fluster.\");\n            },\n            ErrorKind::Other => {\n                // \"This ErrorKind is not used by the standard library.\"\n                // This is impossible to reach.\n                unreachable!(\"Somehow got an `other` error kind, this is impossible as far as i can tell.\");\n            },\n            _ => {\n                // This error is newer than the rust version fluster was originally written for.\n                // GLHF!\n                \n                // Is the floppy drive empty?\n                // code: 123,\n                // message: \"No medium found\",\n                if let Some(raw) = value.io_error.raw_os_error() && raw == 123_i32 {\n                    // No disk is in the drive.\n                    // This can happen even if there is a disk in the drive, so we keep\n                    // trying.\n                    debug!(\"Is no disk inserted?\");\n                    // Just keep retrying, if there is an issue with the floppy drive, we need to\n                    // eventually end up in the panic handler.\n\n                    // Show user that we're waiting for the drive to spin up\n                    // We wait 5 seconds. That's usually fast enough.\n                    let handle = NotifyTui::start_task(TaskType::WaitingForDriveSpinUp, 5*5);\n                    for _ in 0..5*5 {\n                        NotifyTui::complete_task_step(&handle);\n                        std::thread::sleep(Duration::from_millis(100));\n                    }\n                    NotifyTui::finish_task(handle);\n                    return Err(CannotConvertError::MustRetry)\n                }\n\n                // Well, we'll just pretend we can retry any unknown error...\n                warn!(\"UNKNOWN ERROR KIND:\");\n                warn!(\"{value:#?}\");\n                warn!(\"Ignoring, pretending we can retry...\");\n                Ok(DriveIOError::Retry)\n            },\n        }\n    }\n}"
  },
  {
    "path": "src/error_types/critical.rs",
    "content": "// Critical errors are errors that we cannot recover from without some sort of higher intervention.\n// Returning this error type means you've done all you possibly can, and need saving at a higher level, or\n// we are in a unrecoverable state.\n\nuse std::{\n    fs::OpenOptions,\n    os::unix::fs::FileExt,\n    path::PathBuf,\n    process::exit\n};\n\nuse thiserror::Error;\nuse log::error;\n\nuse crate::{\n    error_types::drive::InvalidDriveReason,\n    filesystem::{\n        disk_backup::restore::restore_disk,\n        filesystem_struct::FLOPPY_PATH\n    },\n    tui::prompts::TuiPrompt\n};\n\n#[derive(Debug, Clone, Copy, Error, PartialEq)]\n/// Use this error type if an error happens that you are unable to\n/// recover from without intervention.\n/// \n/// Creating critical errors is a last resort. Whatever error that was causing\n/// your failure must be passed in.\npub enum CriticalError {\n    #[error(\"The floppy drive is inaccessible for some reason.\")]\n    DriveInaccessible(InvalidDriveReason),\n    #[error(\"We've retried an operation too many times. Something must be wrong.\")]\n    OutOfRetries(RetryCapError) // Keep track of where we ran out of retries.\n}\n\n#[derive(Debug, Clone, Copy, PartialEq)]\n/// When you run out of retries on an operation, its useful to know what kind of issue was occurring.\npub enum RetryCapError {\n    /// Opening the disk is repeatedly failing\n    OpenDisk,\n    /// Attempting to write a block is repeatedly failing.\n    WriteBlock,\n    /// Attempting to read a block is repeatedly failing.\n    ReadBlock,\n}\n\n//\n// =========\n// Attempt to recover\n// =========\n//\n\nimpl CriticalError {\n    /// Try to recover from a critical error.\n    /// \n    /// Returns nothing, since if recovery fails, fluster has shut down.\n    /// If this function completes successfully, you can re-attempt the operation that resulted in the critical error.\n    /// This should only be called once per operation, if you are consistently calling attempt_recovery, there is a deeper\n    /// issue that you must address.\n    pub(crate) fn handle(self) {\n        go_handle_critical(self)\n    }\n}\n\n\nfn go_handle_critical(error: CriticalError) {\n\n    // Critical recovery is not allowed in tests.\n    if cfg!(test) {\n        panic!(\"Tried to recover from a critical error! {error:#?}\");\n    }\n\n    let mitgated = match error {\n        CriticalError::DriveInaccessible(invalid_drive_reason) => handle_drive_inaccessible(invalid_drive_reason),\n        CriticalError::OutOfRetries(reason) => handle_out_of_retries(reason),\n    };\n\n\n    // If that worked, the caller that caused this critical to be thrown should be able to\n    // complete whatever operation they need.\n    if mitgated {\n        return\n    }\n\n    // None of that worked. We must give up.\n    // .o7\n    println!(\"Critical error recovery has failed.\");\n    println!(\"{error:#?}\");\n    // Disk/drive are in unknown state, but by god we still must try to flush via panicking.\n    panic!(\"Fluster! has encountered an unrecoverable error, and must shut down.\\nGoodbye.\");\n}\n\n\n\n//\n// Sub-type handlers\n//\n\n\n// Returns true if mitigation succeeded.\nfn handle_drive_inaccessible(reason: InvalidDriveReason) -> bool {\n    match reason {\n        InvalidDriveReason::NotAFile => {\n            // A non-file cannot be used as a floppy disk\n            inform_improper_floppy_drive()\n        },\n        InvalidDriveReason::PermissionDenied => {\n            // Need to be able to do IO obviously.\n            inform_improper_floppy_drive()\n        },\n        InvalidDriveReason::Networking => {\n            // Cant use network drives\n            inform_improper_floppy_drive()\n        },\n        InvalidDriveReason::ReadOnly => {\n            // Did they leave the write protect notch on?\n            inform_improper_floppy_drive()\n        },\n        InvalidDriveReason::NotSeekable => {\n            // Floppy drives must be seekable.\n            inform_improper_floppy_drive()\n        },\n        InvalidDriveReason::InvalidPath => {\n            // Need to be able to get to the drive\n            inform_improper_floppy_drive()\n        },\n        InvalidDriveReason::UnsupportedOS => {\n            // Homebrew OS maybe? We don't use that many\n            // file operations, certainly not many unusual ones, thus\n            // this shouldn't happen on normal platforms.\n            error!(\"Simple file-based IO is marked as unsupported by your operating system.\");\n            error!(\"I'm assuming you're using a non-standard Rust build target / OS destination.\");\n            error!(\"Obviously I cannot support that. If you really want to use Fluster (why?), you'll have to\");\n            error!(\"update Fluster to make it compatible with your system/setup. Good luck!\");\n            // We shouldn't've even finished a single write yet, there shouldn't be anything to flush.\n            exit(-1); \n        },\n        InvalidDriveReason::NotFound => {\n            // Maybe the drive is tweaking?\n            // Ask the user if they wanna do troubleshooting.\n            loop {\n                let response = TuiPrompt::prompt_input(\n                    \"Floppy drive error.\".to_string(),\n                    \"The floppy drive was not found, would you like to retry, or start troubleshooting?\\n\n                    (R)etry / (T)roubleshoot\".to_string(),\n                    true\n                );\n                if response.starts_with('r') {\n                    // User just wants to the retry.\n                    // retrun true, since we've \"done all we can\"\n                    return true;\n                } else if response.starts_with('t') {\n                    // Since the drive is not found, we will first\n                    return troubleshooter();\n                }\n            }\n            \n        },\n    }\n}\n\n/// Returns true if mitigation succeeded.\n/// \n/// yes this is the same as the other handler, but whatever\nfn handle_out_of_retries(reason:RetryCapError) -> bool {\n    match reason {\n        RetryCapError::OpenDisk => {\n            // Run the troubleshooter\n            troubleshooter()\n        },\n        RetryCapError::WriteBlock => troubleshooter(),\n        RetryCapError::ReadBlock => troubleshooter(),\n    }\n}\n\n\n//\n// User guided troubleshooting\n//\n\n/// Returns true if we were able to pinpoint the issue and resolve it.\nfn troubleshooter() -> bool {\n    // Inform the user that the troubleshooter is running.\n    println!(\"Fluster is troubleshooting, please wait...\");\n\n    // Do the easiest things first, preferably ones that do not involve interaction.\n\n    // Run the disk checker.\n\n    // If that passes, we now know:\n    // - Every block on the disk is readable\n    // - Every block on the disk is writable.\n    // - The drive is connected properly and is working.\n    \n    // If all of that is working, troubleshooting is done, since we did not find any issues.\n    // But this is suspicious. Why did the troubleshooter get called when everything is working?\n    if check_disk() {\n        TuiPrompt::prompt_enter(\n            \"Strange...\".to_string(),\n            \"Troubleshooter unexpectedly found nothing wrong.\\n\n            Suggestion: You should cancel all file operations and unmount Fluster to flush everything\n            to disk, just in case.\\n\n            If you are already in the process of unmounting, good luck!\".to_string(),\n            true\n        );\n        return true;\n    }\n    \n    // Something is wrong with the disk or the drive. We will now walk through the\n    // fastest and easiest options first.\n    \n    // Ask the user to re-seat the disk.\n    TuiPrompt::prompt_enter(\n        \"Troubleshooting: Re-seat floppy.\".to_string(),\n        \"Please eject the floppy disk, then re-insert it.\\n\n        If the disk is currently spinning, please wait a moment to see if it will stop spinning before\n        performing the ejection. If the disk continues to spin regardless, proceed with ejection.\".to_string(),\n        true\n    );\n    \n    // Maybe re-seating was all we needed?\n    if check_disk() {\n        // Neat.\n        troubleshooter_finished();\n        return true\n    }\n\n    // Now we know for sure that either the disk is dead, or the drive is not working.\n\n    // Let's try another disk, that'll let us narrow it down if the disk was bad.\n    TuiPrompt::prompt_enter(\n        \"Troubleshooting: Different disk.\".to_string(),\n        \"Please swap disks to any known good disk. Remember which disk was removed.\".to_string(),\n        true\n    );\n\n    // Run the check then have the user put the possibly bad disk back in for continuity.\n    let disk_bad = check_disk();\n\n    TuiPrompt::prompt_enter(\n        \"Troubleshooting: Return disk.\".to_string(),\n        \"Please swap back to the disk you previously removed.\".to_string(),\n        true\n    );\n\n    // Now, if the known good disk passed the disk check, we know that it's the drive that is having issues.\n    // Otherwise, the disk is bad.\n\n    if disk_bad {\n        // Bummer, we need to replace this disk.\n        do_disk_restore();\n\n        // Disk has been restored.\n        return true;\n    }\n\n    // Disk wasn't bad, so the drive must be the issue.\n\n    // Try un-plugging and plugging it back in lmao.\n\n    loop {\n        do_remount();\n\n        if check_disk() {\n            // Remounting fixed it.\n            troubleshooter_finished();\n            return true;\n        };\n\n        // That failed. Retry?\n        let prompted = TuiPrompt::prompt_input(\n            \"Troubleshooting: Try again?\".to_string(),\n            \"Check disk is still failing.\\n\n            This is our last troubleshooting step before completely giving up.\\n\n            You can also forcibly restore a disk if needed.\\n\n            Would you like to try re-mounting again, or throw in the towel?\\n\n            (Y)es/(D)isk restore/(G)ive up\".to_string(),\n            true\n        );\n\n        if prompted.to_ascii_lowercase().contains('g') {\n            // user gives up.\n            break\n        } else if prompted.to_ascii_lowercase().contains('d') {\n            do_disk_restore();\n        }\n    }\n\n    // Remounting did not work, and the user has given up.\n    TuiPrompt::prompt_enter(\n        \"Troubleshooting failed. :(\".to_string(),\n        \"Troubleshooting has failed. No fix that was attempted worked.\\n\n        All of the disks are backed up to the backup directory. No data should be lost, although there might be partially written data.\\n\n        Worst comes to worst, you can re-image all of your disks from backups.\\n\n        Before restoring those disks though, make sure to back-up the backups, since they might be slightly corrupt.\".to_string(),\n        false\n    );\n    false\n}\n\n\n//\n// Troubleshooting actions\n//\n\n/// Read every block on the disk to determine if the disk is bad.\n/// \n/// This may take a while.\n/// \n/// Returns true if every block was read and written correctly.\nfn check_disk() -> bool {\n    println!(\"Checking if the disk and drive are working...\");\n    // Just loop over all of the blocks and try reading them.\n    // We need to do it manually ourselves since we dont want\n    // to throw another critical error while handling another one.\n\n    // Open the disk currently in the drive.\n    // If we cannot lock, we obviously cant use the drive. Thus returns false.\n    let disk_path = if let Ok(guard) = FLOPPY_PATH.try_lock() {\n        guard.clone()\n    } else {\n        // The lock is poisoned, which means we died somewhere else.\n        // So since we're already in the troubleshooter, we'll just clear the lock :clueless: then\n        // return false.\n        FLOPPY_PATH.clear_poison();\n        return false\n    };\n    \n    // Read the entire thing in one go\n    println!(\"Open floppy drive...\");\n    let disk_file = match OpenOptions::new().read(true).write(true).open(&disk_path) {\n        Ok(ok) => ok,\n        Err(error) => {\n            // There is something wrong with reading in the drive, which would imply that\n            // the drive is inaccessible or something. We cannot resolve here.\n            println!(\"Failed to open drive.\");\n            println!(\"{error:#?}\");\n            return false;\n        },\n    };\n    println!(\"Ok.\");\n    \n    // Now read in the entire disk.\n    println!(\"Reading entire disk...\");\n    let mut whole_disk: Vec<u8> = vec![0; 512*2880];\n    let _ = disk_file.sync_all();\n    let read_result = disk_file.read_exact_at(&mut whole_disk, 0);\n    let _ = disk_file.sync_all();\n    \n    // If that failed at all, checking the disk is bad either due to the drive, or the disk.\n    if let Err(error) =  read_result {\n        // Read failed. Something is up.\n        println!(\"Fail.\");\n        println!(\"{error:#?}\");\n        return false;\n    };\n    println!(\"Ok.\");\n    \n    // Now we write the entire disk back again to see if every block accepts writes.\n    println!(\"Writing entire disk...\");\n    let _ = disk_file.sync_all();\n    let write_result = disk_file.write_all_at(&whole_disk, 0);\n    let _ = disk_file.sync_all();\n    \n    // Did the write work?\n    if let Err(error) = write_result {\n        // nope\n        println!(\"Fail.\");\n        println!(\"{error:#?}\");\n        return false;\n    };\n    println!(\"Ok.\");\n    println!(\"Disk and drive appear to be working correctly.\");\n    true\n}\n\n\n/// Some actions might change the path to the floppy disk drive, we need to let the user update that\n/// if they need.\nfn update_drive_path() {\n    // what's the new path\n    let new_path: std::path::PathBuf;\n    loop {\n        let possible = TuiPrompt::prompt_input(\n            \"Troubleshooting: Path change.\".to_string(),\n            \"If the path to the floppy drive has changed due to the re-mount, please\n            enter the new path. Otherwise hit enter.\".to_string(),\n            false\n        );\n        // If nothing was entered, the path has not changed\n        if possible.is_empty() {\n            // no change.\n            return\n        }\n        let could_be = PathBuf::from(possible);\n        let maybe = match could_be.canonicalize() {\n            Ok(ok) => ok,\n            Err(err) => {\n                // what\n                TuiPrompt::prompt_enter(\n                    \"Invalid path.\".to_string(),\n                    format!(\"Unable to canonicalize path. Please provide a valid path.\\n\\n{err:#?}\"),\n                    false\n                );\n                continue;\n            },\n        };\n        if std::fs::exists(&maybe).unwrap_or(false) {\n            // Good.\n            new_path = maybe;\n            break\n        } else {\n            TuiPrompt::prompt_enter(\n                \"Invalid path.\".to_string(),\n                \"Unable to either open path, or confirm it exists. Please provide a valid path.\".to_string(),\n                false\n            );\n            continue;\n        }\n    }\n\n    // Set that new path\n    if let Ok(mut something) = FLOPPY_PATH.try_lock() {\n        *something = new_path;\n    } else {\n        // The lock is poisoned, we'll just uhhh... ignore that.\n        // The woes of a non-transactional filesystem.\n        // TODO: kill PastJade for not thinking that far ahead.\n        FLOPPY_PATH.clear_poison();\n        if let Ok(mut something_the_sequel) = FLOPPY_PATH.try_lock() {\n            *something_the_sequel = new_path;\n        } else {\n            // ????\n            // Already VERY cooked, might as well panic for fun!\n            panic!(\"Tried to clear poison on the floppy path but that didn't work! Giving up.\");\n        }\n    }\n}\n\n\n\n\n//\n// User actions\n//\n\n\n\n/// Ask the user to remount the floppy drive.\nfn do_remount() {\n    TuiPrompt::prompt_enter(\n        \"Troubleshooting: Remount drive.\".to_string(),\n        \"Please re-mount the floppy drive.\\n\n        You can find more information about remounting in the README.\\n\n        Press enter after you have finished re-mounting the drive.\".to_string(),\n        false\n    );\n    // This might have changed the path to the floppy drive.\n    update_drive_path();\n}\n\n/// Inform the user that the disk needs to be re-created.\n/// \n/// Make sure to put the bad disk back in the drive beforehand so the user\n/// knows what disk to discard.\nfn do_disk_restore() {\n    TuiPrompt::prompt_enter(\n        \"Troubleshooting: Bad disk.\".to_string(),\n        \"The troubleshooter has determined that the disk currently within the drive is bad.\\n\n        This disk will need to be re-created.\".to_string(),\n        false\n    );\n\n    // Now start the restore.\n    let mut failure = false;\n    loop {\n        if failure {\n            // We tried restoring the disk, but the restore failed.\n            // Tell the user and ask if they want to attempt to restore to\n            // the same disk again, or have them put in a new disk.\n            TuiPrompt::prompt_enter(\n                \"Restoration: Failure.\".to_string(),\n                \"Restoring disk has failed. Restoring can be retried though.\\n\n                If you would like to attempt restoring to the same disk that you inserted previously,\n                leave it in the drive, and ignore the message about swapping disks.\\n\n                You can re-try as many times as you would like, but if the new disk continues to fail, you\n                should try using another disk to restore onto.\\n\n                If you just cannot seem to restore to a new disk, idk man you're cooked lmao good luck bozo.\".to_string(),\n                false\n            );\n        }\n        \n        // Pull out the bad one, disk restore needs an empty drive.\n        // We need to know what disk it was.\n        let disk_number: u16;\n        loop {\n            let to_convert = TuiPrompt::prompt_input(\n                \"Restoration: New disk.\".to_string(),\n                \"Please remove the bad disk currently inserted in the drive, then\n                enter it's disk number.\".to_string(),\n                false\n            );\n            if let Ok(number) = to_convert.parse::<u16>() {\n                disk_number = number;\n                break;\n            }\n            TuiPrompt::prompt_enter(\n                \"Restoration: Bad number.\".to_string(),\n                \"Parsing error, please try again. Only enter the number of the disk.\".to_string(),\n                false\n            );\n        }\n\n        // Now restore that disk.\n        if restore_disk(disk_number) {\n            // restore worked!\n            return\n        }\n        // Restoration failed, it can be retried.\n        failure = true;\n    }\n}\n\n//\n// User information\n//\n\n// fn inform_improper_mount_point() -> ! {\n//     TuiPrompt::prompt_enter(\n//         \"Bad mount point.\".to_string(),\n//         \"The point where you have tried to mount fluster is invalid for some reason.\\n\n//         Please re-confirm that the mount point is valid, then re-run fluster. Good luck!\".to_string(),\n//         true\n//     );\n//     exit(-1) // Exiting, no floppy drive to flush with.\n// }\n\nfn inform_improper_floppy_drive() -> bool {\n    // We cannot use this floppy drive.\n\n    // First check if the user has inserted a write-protected disk\n    loop {\n        let prompted: String = TuiPrompt::prompt_input(\n            \"Troubleshooting: Write protected.\".to_string(),\n            \"Please remove the floppy disk from the drive, and confirm that it is not set to read-only.\\n\\n\n            Was the disk set to read-only? \\\"yes\\\"/\\\"no\\\"\".to_string(),\n            false\n        );\n        if prompted.contains('y') {\n            // Whoops!\n            TuiPrompt::prompt_enter(\n                \"Troubleshooting finished.\".to_string(),\n                \"Cool! That means we do not need to shut down.\n                Please set the disk to read/write, and insert it back into the drive.\\n\n                Please make sure you do not insert write protected disks in the future, or the troubleshooter will start again.\".to_string(),\n                false\n            );\n            return true;\n        } else if prompted.contains('n') {\n            // Well crap.\n            break\n        };\n    }\n\n    // Disk was not write protected. Drive is bad.\n    TuiPrompt::prompt_enter(\n        \"Troubleshooting failed.\".to_string(),\n        \"Fluster is unable to access the floppy disk from your floppy drive.\\n\n        Please make sure that the path you provided for the drive is:\\n\n        - Valid\\n\n        - A file, and not a directory\\n\n        - Accessible by your current user\\n\n        - Is not over the network\\n\n        - Is not mounted as read-only\\n\\n\n        Fluster will now exit, since operating without a drive is not possible.\".to_string(),\n        true\n    );\n    exit(-1); // Exiting, since we cant even flush the cache due to no floppy drive.\n}\n\n// Helper just do dedupe\nfn troubleshooter_finished() {\n    TuiPrompt::prompt_enter(\n        \"Troubleshooting succeeded!\".to_string(),\n        \"Troubleshooting finished successfully.\".to_string(),\n        false\n    );\n}"
  },
  {
    "path": "src/error_types/drive.rs",
    "content": "// Error types pertaining to the floppy drive itself.\n// We do not allow string errors. This is RUST damn it, not python!\nuse thiserror::Error;\n\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\n\n#[derive(Debug, Error, PartialEq)]\n/// Super-error about the floppy drive itself.\n/// \n/// We are unable to handle read errors at this level. All IO related errors\n/// are within the DriveIOError type.\npub enum DriveError {\n    #[error(\"No disk is currently inserted.\")]\n    DriveEmpty,\n    #[error(\"The operation failed for non-critical reasons, but no corruption occurred, and the operation can be retried with the same arguments.\")]\n    Retry,\n    #[error(\"An operation on this disk is taking too long..\")]\n    TakingTooLong\n}\n\n#[derive(Debug, Clone, Copy, Error, PartialEq)]\n/// Errors related to IO on the inserted floppy disk.\n/// \n/// This is only used at the lowest of levels on actual IO operations.\npub enum DriveIOError {\n    #[error(\"The operation failed for non-critical reasons, but no corruption occurred, and the operation can be retried with the same arguments.\")]\n    Retry,\n}\n\n#[derive(Debug)]\npub struct WrappedIOError {\n    /// The io error that you are trying to handle\n    pub(super) io_error: std::io::Error,\n    /// The DiskPointer to where this issue occurred.\n    pub(super) error_origin: DiskPointer\n}\n\n\n\n#[derive(Debug, PartialEq, Clone, Copy)]\n/// Reasons why we cannot use the provided floppy disk path\npub enum InvalidDriveReason {\n    /// Pointed at a folder instead of a file.\n    NotAFile,\n    /// We dont have permission to access the path provided\n    PermissionDenied,\n    /// We do not support using fluster over the network.\n    Networking,\n    /// Disk must be read and write.\n    ReadOnly,\n    /// File that refers to the floppy drive is not seekable.\n    NotSeekable,\n    /// The path is invalid in some way.\n    InvalidPath,\n    /// The filesystem (or operating system) that you're running fluster on\n    /// does not support basic disk IO.\n    UnsupportedOS,\n    /// Generic \"not found\"\n    NotFound\n}"
  },
  {
    "path": "src/error_types/filesystem.rs",
    "content": "use libc::c_int;\nuse log::error;\n\nuse crate::error_types::drive::DriveError;\n\n//\n//\n// ======\n// C Error values\n// ======\n//\n//\n\n// Errors gleamed from\n// https://man7.org/linux/man-pages/man3/errno.3.html\n// https://man7.org/linux/man-pages/man2/openat.2.html\n\n/// Bro thinks he's Shakespeare.\npub(in super::super) const FILE_NAME_TOO_LONG: c_int = libc::ENAMETOOLONG;\n/// Tried to modify a non-empty directory in a way that required it to be empty.\npub(in super::super) const DIRECTORY_NOT_EMPTY: c_int = libc::ENOTEMPTY;\n/// This seat's taken.\npub(in super::super) const ITEM_ALREADY_EXISTS: c_int = libc::EEXIST;\n/// Tried to do directory stuff to a file.\npub(in super::super) const NOT_A_DIRECTORY: c_int = libc::ENOTDIR;\n/// Ad hominem\npub(in super::super) const INVALID_ARGUMENT: c_int = libc::EINVAL;\n/// Tried to do things to a directory that it does not support.\npub(in super::super) const IS_A_DIRECTORY: c_int = libc::EISDIR;\n// /// Function not implemented.\n// pub(in super::super) const UNIMPLEMENTED: c_int = libc::ENOSYS;\n/// This operation is not supported in this filesystem.\n// pub(in super::super) const UNSUPPORTED: c_int = libc::ENOTSUP; \n/// Access denied / files does not exist.\npub(in super::super) const NO_SUCH_ITEM: c_int = libc::ENOENT;\n/// Tried to seek to an invalid file position.\n// pub(in super::super) const INVALID_SEEK: c_int = libc::ESPIPE;\n/// Tried to use a filehandle that is stale. New one is required.\npub(in super::super) const STALE_HANDLE: c_int = libc::ESTALE;\n// Generic IO error. The dreaded OS(5) Input/Output error.\npub(in super::super) const GENERIC_FAILURE: c_int = libc::EIO;\n/// You are insane.\n// pub(in super::super) const FILE_TOO_BIG: c_int = libc::EFBIG;\n/// Operation was interrupted for some reason, but can be retried.\npub(in super::super) const TRY_AGAIN: c_int = libc::ERESTART;\n/// Device / filesystem is busy, try again later.\n/// \n/// Should never happen in fluster due to being single threaded.\npub(in super::super) const BUSY: c_int = libc::EBUSY;\n\nimpl From<DriveError> for c_int {\n    fn from(value: DriveError) -> Self {\n        match value {\n            DriveError::DriveEmpty => {\n                // The drive empty error should never get this high\n                error!(\"Drive empty error should never make it to the filesystem level!\");\n                error!(\"Telling file system that we are busy...\");\n                BUSY\n            },\n            DriveError::Retry => TRY_AGAIN,\n            DriveError::TakingTooLong => BUSY,\n        }\n    }\n}"
  },
  {
    "path": "src/error_types/header.rs",
    "content": "// Errors for header conversions.\nuse thiserror::Error;\n\n#[derive(Debug, Error, PartialEq)]\n/// Errors related to block manipulation. Not disk level modification, but our custom block types.\npub enum HeaderError {\n    #[error(\"This is not a header of the requested type.\")]\n    Invalid,\n    #[error(\"The block that was requested to turn into a header is completely blank.\")]\n    Blank,\n}"
  },
  {
    "path": "src/error_types/mod.rs",
    "content": "pub(crate) mod drive;\npub(crate) mod conversions;\npub(crate) mod block;\npub(crate) mod filesystem;\npub(crate) mod critical;\npub(crate) mod header;"
  },
  {
    "path": "src/filesystem/disk_backup/mod.rs",
    "content": "pub mod restore;\npub mod update;"
  },
  {
    "path": "src/filesystem/disk_backup/restore.rs",
    "content": "// Restore a disk from a backup.\n\nuse std::{fs::File, io::{\n    Read,\n    Seek\n}, os::unix::fs::FileExt};\n\nuse log::{debug, error, warn};\n\nuse crate::{filesystem::filesystem_struct::FLOPPY_PATH, tui::{notify::NotifyTui, prompts::TuiPrompt, tasks::TaskType}};\n\n/// Returns true if the entire disk was re-created successfully.\n/// \n/// Assumes the drive is empty when called.\npub fn restore_disk(number: u16) -> bool {\n    debug!(\"Beginning restore of disk `{number}`.\");\n    \n    // Get a new blank disk.\n    TuiPrompt::prompt_enter(\n        \"Insert blank disk.\".to_string(),\n        format!(\"Please insert a brand new, blank disk that will become the new disk {number}, then press enter.\\n\n        WARNING: Disk will NOT be checked for blankness, this WILL destroy data if a non-blank disk is inserted!\"),\n        false\n    );\n    \n    let handle = NotifyTui::start_task(TaskType::RestoreDisk, 6);\n    // Find the disk in the backup folder\n    // If it't not in there, you're cooked.\n    // Try opening the backup file at most 5 times.\n\n    let mut tries = 1_u8;\n    let mut backed_up: std::fs::File;\n    debug!(\"Opening backup file...\");\n    loop {\n        match std::fs::OpenOptions::new()\n        .read(true)\n        .open(format!(\"/var/fluster/disk_{number}.fluster_backup\")) {\n            Ok(ok) => {\n                backed_up = ok;\n                break;\n            },\n            Err(_) => {\n                if tries == 5 {\n                    // ruh roh\n                    warn!(\"Fail. Out of retries.\");\n                    return false;\n                } else {\n                    warn!(\"Fail, trying again...\");\n                    tries += 1;\n                    continue;\n                }\n            },\n        };\n    };\n\n    NotifyTui::complete_task_step(&handle);\n    \n    // Now read in the entire floppy backup.\n    // Again, at most 5 tries.\n    \n    let mut bytes: Vec<u8> = Vec::with_capacity(2880*512);\n    let mut tries = 1_u8;\n    debug!(\"Reading backup data...\");\n    loop {\n        if backed_up.rewind().is_ok() && backed_up.read_to_end(&mut bytes).is_ok() {\n            // All good.\n            break\n        }\n        if tries == 5 {\n            // cooked.\n            error!(\"Fail. Out of retries.\");\n            return false;\n        }\n        debug!(\"Fail, trying again...\");\n        bytes.clear();\n        tries += 1;\n        continue;\n    };\n\n    NotifyTui::complete_task_step(&handle);\n\n    // Copy the entire contents of that backup to the new disk.\n\n    // We'll just dump the entire file to the block device without using our floppy handler,\n    // since we cant really trust my logic hehe\n\n    // Get the path to the floppy drive block device.\n    // We'll pre-clear poison, just in case.\n    FLOPPY_PATH.clear_poison();\n    let block_path = if let Ok(path) = FLOPPY_PATH.lock()  {\n        path.clone()\n    } else {\n        // well... cooked\n        error!(\"Floppy path is poisoned!\");\n        return false\n    };\n\n    NotifyTui::complete_task_step(&handle);\n\n    // Open the block device as a file\n    let block_file: File = if let Ok(opened) = File::options().read(true).write(true).open(block_path) {\n        opened\n    } else {\n        // Well, we couldn't open the floppy path. Return false.\n        return false;\n    };\n\n    NotifyTui::complete_task_step(&handle);\n    \n    // Now write all that in\n    let mut write_worked = false;\n    for _ in 0..5 {\n        let result = block_file.write_all_at(&bytes, 0);\n        match result {\n            Ok(_) => {\n                // write finished!\n                write_worked = true;\n                break\n            },\n            Err(err) => {\n                // That didn't work!\n                error!(\"Writing to the drive failed!\");\n                error!(\"{err:#?}\");\n                continue;\n            },\n        }\n    }\n\n    NotifyTui::complete_task_step(&handle);\n\n    // Now sync the disk to pause until the data all actually hits the disk.\n    // Not a big issue if this doesnt work, we're still gonna wait for the disk to spin down\n    // before swapping disks.\n    let _ = block_file.sync_all();\n    NotifyTui::complete_task_step(&handle);\n\n    // Did that work?\n    if write_worked {\n        // Disk written!\n        NotifyTui::finish_task(handle);\n        debug!(\"Disk restored!\");\n        true\n    } else {\n        // Well shoot.\n        NotifyTui::cancel_task(handle);\n        error!(\"Disk restore failed!\");\n        false\n    }\n}"
  },
  {
    "path": "src/filesystem/disk_backup/update.rs",
    "content": "// Update the backup disk with new contents.\n\n// The path that the disk backups go in /var/fluster\n\n// Porting fluster? Then you've gotta update this for sure.\n\nuse std::os::unix::fs::FileExt;\n\nuse log::error;\n\nuse crate::{filesystem::filesystem_struct::WRITE_BACKUPS, pool::disk::generic::{block::block_structs::RawBlock, generic_structs::pointer_struct::DiskPointer}};\n\npub(crate) fn update_backup(block: &RawBlock) {\n    // Ignore backups if needed.\n    if let Some(ian_the_bool) = WRITE_BACKUPS.get() {\n        if !ian_the_bool {\n            // Skip, backups are disabled.\n            return\n        }\n    } else {\n        // The backups flag hasn't been set up, which should be impossible, but we'll just return\n        return\n    }\n\n    // Make the backup folder if it does not exist yet\n    if std::fs::create_dir_all(\"/var/fluster\").is_err() {\n        // Unable to create the folders and such.\n        error!(\"Fluster needs to be able to create/use /var/fluster for disk backups.\");\n        error!(\"We cannot continue without backups. Shutting down. If you are unable to use\");\n        error!(\"backups, set the flag.\");\n        panic!(\"Unable to update backups!\"); // we panic here, since we still want to flush the disks.\n    }\n\n    // Open or create the backup file for the disk\n    let disk_path: String = format!(\"/var/fluster/disk_{}.fluster_backup\", block.block_origin.disk);\n    let backup_file = if let Ok(file) = std::fs::OpenOptions::new().create(true).truncate(false).write(true).open(disk_path) {\n        file\n    } else {\n        // Cannot open the backup file, we're cooked.\n        error!(\"Fluster was unable to create or open one of its backup files, if you see this spamming your logs, its probably chronic.\");\n        error!(\"Fix your permissions, or backups wont work!\");\n        // pretend we did the backup, crashing is worse.\n        return\n    };\n\n    // Write in that block. We will try twice at maximum.\n    for _ in 0..2 {\n        if backup_file.write_all_at(&block.data, block.block_origin.block as u64 * 512).is_err() {\n            // That did not work. Crap.\n            // We'll try again.\n        } else {\n            // That worked!\n            return\n        }\n    }\n\n    // We couldn't update the file.\n    error!(\"Fluster failed to write to a backup for one of the disks, if you see this spamming your logs, its probably chronic.\");\n    error!(\"You should investigate!\");\n\n}\n\npub(crate) fn large_update_backup(start: DiskPointer, data: &[u8]) {\n    // Ignore backups if needed.\n    if let Some(ian_the_bool) = WRITE_BACKUPS.get() {\n        if !ian_the_bool {\n            // Skip, backups are disabled.\n            return\n        }\n    } else {\n        // The backups flag hasn't been set up, which should be impossible, but we'll just return\n        return\n    }\n\n    // Open or create the backup file for the disk\n    let disk_path: String = format!(\"/var/fluster/disk_{}.fluster_backup\", start.disk);\n    let backup_file = if let Ok(file) = std::fs::OpenOptions::new().create(true).truncate(false).write(true).open(disk_path) {\n        file\n    } else {\n        // Cannot open the backup file, we're cooked.\n        error!(\"Fluster was unable to create or open one of its backup files, if you see this spamming your logs, its probably chronic.\");\n        error!(\"Fix your permissions, or backups wont work!\");\n        // pretend we did the backup, crashing is worse.\n        return\n    };\n\n    // Write in that block. We will try twice at maximum.\n    for _ in 0..2 {\n        if backup_file.write_all_at(data, start.block as u64 * 512).is_err() {\n            // That did not work. Crap.\n            // We'll try again.\n        } else {\n            // That worked!\n            return\n        }\n    }\n\n    // We couldn't update the file.\n    error!(\"Fluster failed to write to a backup for one of the disks, if you see this spamming your logs, its probably chronic.\");\n    error!(\"You should investigate!\");\n}"
  },
  {
    "path": "src/filesystem/file_attributes/conversion.rs",
    "content": "use fuse_mt::{FileAttr, FileType};\nuse libc::c_int;\nuse log::debug;\nuse std::time::SystemTime;\n\nuse crate::{\n    error_types::drive::DriveError, filesystem::file_handle::file_handle_struct::FileHandle, pool::disk::standard_disk::block::directory::directory_struct::{\n                DirectoryItem, DirectoryItemFlags\n            }, tui::{notify::NotifyTui, tasks::TaskType}\n};\n\n\n\n// Take in a file handle and spit out its attributes.\nimpl TryFrom<FileHandle> for FileAttr {\n    type Error = c_int;\n    \n    fn try_from(value: FileHandle) -> Result<Self, Self::Error> {\n        debug!(\"Retrieving file metadata from handle...\");\n        // Get the directory item\n        let item: DirectoryItem = value.get_directory_item()?;\n        Ok(go_get_metadata(item)?)\n    }\n}\n\n// You can also call this on DirectoryItem\nimpl TryFrom<DirectoryItem> for FileAttr {\n    type Error = DriveError;\n\n    fn try_from(value: DirectoryItem) -> Result<Self, Self::Error> {\n        go_get_metadata(value)\n    }\n}\n\nfn go_get_metadata(item: DirectoryItem) -> Result<FileAttr, DriveError> {\n    debug!(\"Extracting metadata from item `{}`...\", item.name);\n    let handle = NotifyTui::start_task(TaskType::GetMetadata(item.name.clone()), 3);\n    // Now for ease of implementation, we (very stupidly) ignore all file access permissions,\n    // owner information, and group owner information.\n    \n    // Root owns all files (user id 0)\n    // Owner is in the superuser group (group id 0)\n    // All permission bits are set (very scary!) go execute a jpeg, i dont even care anymore.\n    \n    // Due to this, we also do not check any permissions on reads or writes! :D\n    \n    \n    \n    // How big is it\n    debug!(\"Getting size...\");\n    let size: u64 = item.get_size()?;\n    NotifyTui::complete_task_step(&handle);\n    \n    // extract the times\n    debug!(\"Created at...\");\n    let creation_time: SystemTime = item.get_created_time()?.into();\n    debug!(\"Modified at...\");\n    let modified_time: SystemTime = item.get_modified_time()?.into();\n    NotifyTui::complete_task_step(&handle);\n    \n    // \"What kind of item is this?\"\n    // https://www.tiktok.com/@ki2myyysc6/video/7524954406438161694\n    let file_kind: FileType = if item.flags.contains(DirectoryItemFlags::IsDirectory) {\n        // \"This is a directory, used for holding items in a filesystem, such as files or other directories.\"\n        debug!(\"Is a directory...\");\n        FileType::Directory\n    } else {\n        // \"This is a file, used to store arbitrary data, it is very useful!\"\n        debug!(\"Is a file...\");\n        FileType::RegularFile\n    };\n    NotifyTui::complete_task_step(&handle);\n    \n    debug!(\"Metadata done.\");\n    NotifyTui::finish_task(handle);\n    \n    // Put it all together\n    Ok(FileAttr {\n        // Size of item in bytes.\n        size,\n        // Bytes div_ceil 512\n        blocks: size.div_ceil(512),\n        // We dont support access times.\n        atime: SystemTime::UNIX_EPOCH,\n        // modification time\n        mtime: modified_time,\n        // metadata change, not supported\n        ctime: SystemTime::UNIX_EPOCH,\n        // creation time\n        crtime: creation_time,\n        // file type\n        kind: file_kind,\n        // File permissions, not supported\n        perm: 0o777, // All permission bits\n        // links not supported\n        nlink: 2, // This has to be set to 2 or things get angry, idk.\n        // owner id, always root\n        uid: 0,\n        // owner group, always root\n        gid: 0,\n        // special id, not supported\n        rdev: 0,\n        // macos flags, who gaf? not me. use a real operating system /bait\n        flags: 0,\n    })\n}"
  },
  {
    "path": "src/filesystem/file_attributes/mod.rs",
    "content": "pub mod conversion;"
  },
  {
    "path": "src/filesystem/file_handle/file_handle_methods.rs",
    "content": "// Make the handle do things.\n\nuse std::{collections::HashMap, sync::{Arc, Mutex}};\n\nuse lazy_static::lazy_static;\nuse libc::c_int;\nuse log::{debug, error};\n\n//\n// Global info about open files\n//\n\nstruct LoveHandles {\n    /// Hashmap of the currently allocated handles\n    allocated: HashMap<u64, FileHandle>,\n    /// Highest allocated number (is kept up to date internally)\n    highest: u64,\n    /// Recently freed handles (ie open space in the hashmap)\n    free: Vec<u64>\n}\n\nimpl LoveHandles {\n    /// Make a new one, should only be called once.\n    fn new() -> Self {\n        // Empty\n        LoveHandles {\n            allocated: HashMap::new(),\n            highest: 0,\n            free: Vec::new(),\n        }\n    }\n\n    /// Make a new handle\n    fn make_handle(&mut self, item: FileHandle) -> u64 {\n        // Get a number\n        let num = self.next_free();\n\n        // Put it in the hashmap.\n        // We also assert that we have not already used this number.\n        assert!(self.allocated.insert(num, item).is_none(), \"We already used this handle number, even though we thought it was free!\");\n\n        // All done.\n        num\n    }\n\n    /// Get the handle back\n    fn read_handle(&self, number: u64) -> FileHandle {\n        // Handles are not read after freeing, doing so is undefined behavior.\n        if let Some(handle) = self.allocated.get(&number) {\n            // Cool, it's there.\n            handle.clone()\n        } else {\n            // We are cooked.\n            error!(\"Tried to read a handle that was not allocated!\");\n            panic!(\"Use after free on handle.\");\n        }\n    }\n\n    /// Get the next free handle (internal abstraction)\n    fn next_free(&mut self) -> u64 {\n        // Prefer vec items\n        if self.free.is_empty() {\n            // Time for a new number then.\n            let give = self.highest;\n            self.highest += 1;\n            return give;\n        }\n\n        // There is a vec item.\n        self.free.pop().expect(\"Guarded.\")\n    }\n\n    /// You need to let go...\n    fn release_handle(&mut self, number: u64) {\n        // Handles are only ever freed once. Freeing an empty handle is undefined behavior, thus we\n        // cant do anything but give up.\n        if self.allocated.remove(&number).is_none() {\n            // Bad!\n            error!(\"Tried to free a handle that was not allocated!\");\n            panic!(\"Double free on handle.\");\n        };\n\n        // Is this number right below the current highest?\n        if number == self.highest - 1 {\n            // Yep! Reduce highest.\n            self.highest -= 1;\n        }\n    }\n}\n\n\n\nlazy_static! {\n    static ref LOANED_HANDLES: Arc<Mutex<LoveHandles>> = Arc::new(Mutex::new(LoveHandles::new()));\n}\n\n\n\n\n\n//\n// The actual handles\n//\n\nuse crate::{\n    error_types::filesystem::*,\n    filesystem::file_handle::file_handle_struct::FileHandle,\n    pool::disk::{\n        standard_disk::block::{\n            directory::directory_struct::{\n                DirectoryBlock,\n                DirectoryItem\n            },\n            io::directory::types::NamedItem\n        }\n    }\n};\n\nimpl FileHandle {\n    /// The name of the file/folder, if it exists.\n    /// This will return None on the root.\n    pub fn name(&self) -> &str {\n        // Get the name, if it exists.\n        if let Some(name) = self.path.file_name() {\n            name.to_str().expect(\"Should be valid UTF8\")\n        } else {\n            // No name, this must be the root.\n            \"\"\n        }\n    }\n\n    /// Allocate the file handle for tracking.\n    /// \n    /// Will block.\n    /// \n    /// Does not create a new ItemHandle, only stores it.\n    pub fn allocate(self) -> u64 {\n        // This is blocking.\n        let read_handles = &mut LOANED_HANDLES.lock().expect(\"Other mutex holders should not panic.\");\n        // Add it\n        read_handles.make_handle(self)\n    }\n\n    /// Get contents of handle.\n    /// \n    /// Will block.\n    pub fn read(handle: u64) -> Self {\n        // This is blocking\n        let read_handles = LOANED_HANDLES.lock().expect(\"Other mutex holders should not panic.\");\n        read_handles.read_handle(handle)\n    }\n\n    /// Release a handle.\n    /// \n    /// Will block.\n    pub fn drop_handle(handle: u64) {\n        // This is blocking\n        let read_handles = &mut LOANED_HANDLES.lock().expect(\"Other mutex holders should not panic.\");\n        read_handles.release_handle(handle);\n    }\n\n    /// Check if this handle is a file or a directory by attempting to read it from disk, otherwise\n    /// deducing the type from it's path string.\n    pub fn is_file(&self) -> Result<Option<bool>, c_int> {\n        // Annoyingly, rust's PathBuf type doesn't have a way to test if itself is a directory\n        // without reading from disk, which makes it completely useless for deducing if the passed argument\n        // is a file or folder. Very very annoying.\n        //\n        // You can't just check for file extensions, since files do not _need_ an extension...\n        //\n        // The approach i'll take is to see if the path ends with a delimiter. good luck lmao\n\n        // But before we fallback to the path based deduction, we can attempt to load the file from disk if it exists.\n        // If it does, we have our sure answer about what type it is, otherwise we will use the crappy string logic.\n\n        // I don't particularly enjoy needing to access the disk here, but chances are if you're trying to find what type\n        // something is, you'll be modifying it soon anyways.\n        \n        let name: String = self.name().to_string();\n        debug!(\"Attempting to deduce if `{}` is a file or directory...\", self.path.display());\n        \n        // Does the parent exist?\n        debug!(\"Checking if it already exists...\");\n        if let Some(parent) = DirectoryBlock::try_find_directory(self.path.parent())? {\n            // Parent does exist, is this item there in either form?\n            let file: NamedItem = NamedItem::File(name.clone());\n            let directory: NamedItem = NamedItem::Directory(name.clone());\n            let maybe_file = parent.find_item(&file)?;\n            if maybe_file.is_some() {\n                // It was a file\n                debug!(\"Yes, and it's a file.\");\n                return Ok(Some(true));\n            }\n            let maybe_directory = parent.find_item(&directory)?;\n            if maybe_directory.is_some() {\n                // It was a directory.\n                debug!(\"Yes, and it's a directory.\");\n                return Ok(Some(false));\n            }\n        }\n        debug!(\"Item did not exist!\");\n\n        // Rather than guess, we'll just return that the file did not exist, which should not the be the case for\n        // file handles, but maybe the caller just had a stale handle, or is spoofing a new file?\n        Ok(None)\n    }\n\n    \n    /// Loads in and returns the directory item if it exists.\n    pub fn get_directory_item(&self) -> Result<DirectoryItem, c_int> {\n        // Open the containing folder\n        let block = match DirectoryBlock::try_find_directory(self.path.parent())? {\n            Some(ok) => ok,\n            None => {\n                // Containing block did not exist.\n                return Err(NO_SUCH_ITEM);\n            },\n        };\n\n        let named_item = if let Some(bool) = self.get_named_item()? {\n            // There was an item with this name.\n            bool\n        } else {\n            // We are trying to deduce the name of an item that does not exist.\n            return Err(NO_SUCH_ITEM);\n        };\n\n        // Find the item\n        if let Some(exists) = block.find_item(&named_item)? {\n            // File existed.\n            Ok(exists)\n        } else {\n            // No such item.\n            Err(NO_SUCH_ITEM)\n        }\n    }\n\n    /// Get a named item from this handle.\n    pub(crate) fn get_named_item(&self) -> Result<Option<NamedItem>, c_int> {\n        // Get a name\n        let name: String = self.name().to_string();\n        let file_check = self.is_file()?;\n        \n        // If this is none, the caller is trying to extract a named item from\n        // an invalid handle or a spoofed file. We return None, since the item did not\n        // exist in either form.\n\n        if let Some(is_file) = file_check {\n            // An item was there.\n            // Deduce the type\n            if is_file {\n                // yeah its a file\n                return Ok(Some(NamedItem::File(name)));\n            } else {\n                // dir\n                return Ok(Some(NamedItem::Directory(name)));\n            }\n        }\n\n        // There was no item.\n        Ok(None)\n\n        \n    }\n}"
  },
  {
    "path": "src/filesystem/file_handle/file_handle_struct.rs",
    "content": "//\n//\n// ======\n// Handle type\n// ======\n//\n//\n\n\n// We are in charge of our own file handle management. Fun! (lie)\n// So we need a way to hand out and retrieve them.\n\n\n\n/// Handle for any type of item (file or directory).\n#[derive(Debug, Clone)]\npub(crate) struct FileHandle {\n    /// The path of this file/folder.\n    pub path: Box<std::path::Path>, // Non-static size, thus boxed.\n}"
  },
  {
    "path": "src/filesystem/file_handle/mod.rs",
    "content": "pub(super) mod file_handle_methods;\npub(super) mod file_handle_struct;"
  },
  {
    "path": "src/filesystem/filesystem_struct.rs",
    "content": "// This is where the fun begins\n\n// Imports\n\nuse crate::pool::pool_actions::pool_struct::Pool;\nuse std::{\n    path::PathBuf,\n    sync::{Arc, Mutex, OnceLock},\n};\n// Structs, Enums, Flags\n\npub struct FlusterFS {\n    #[allow(dead_code)] // it's lying.\n    pub(crate) pool: Arc<Mutex<Pool>>,\n}\n\nuse lazy_static::lazy_static;\n\n// Global varibles\n// We need to access the path quite deep down into the disk functions, passing it all the way down there would be silly.\n// Same with the virtual disk flag.\nlazy_static! {\n    /// Use virtual disks instead of actually mounting the provided floppy drive path.\n    pub(crate) static ref USE_VIRTUAL_DISKS: Mutex<Option<PathBuf>> = Mutex::new(None);\n    /// The full path to the floppy drive.\n    pub(crate) static ref FLOPPY_PATH: Mutex<PathBuf> = Mutex::new(PathBuf::new());\n}\n\n// Backups cannot be disabled at runtime, so we use a once lock for them\n/// Enable and disable backing up disks to /var/fluster\npub(crate) static WRITE_BACKUPS: OnceLock<bool> = OnceLock::new();\n// TUI cannot be disabled mid run.\npub(crate) static USE_TUI: OnceLock<bool> = OnceLock::new();\n\n/// Options availble at time of pool creation / filesystem load\npub struct FilesystemOptions {\n    /// Use virtual disks in a temp folder instead of accessing the floppy drive.\n    /// This option is used for testing.\n    #[allow(dead_code)] // it's lying.\n    pub(super) use_virtual_disks: Option<PathBuf>,\n    /// The location of the floppy drive block device\n    #[allow(dead_code)] // it's lying.\n    pub(super) floppy_drive: PathBuf,\n    /// Enable backing up disks to /var/fluster\n    #[allow(dead_code)] // it's lying.\n    pub(super) enable_backup: bool,\n    /// Enable the TUI\n    #[allow(dead_code)] // it's lying.\n    pub(super) enable_tui: bool\n}\n"
  },
  {
    "path": "src/filesystem/fuse_filesystem_methods.rs",
    "content": "// The actual FUSE filesystem layer.\n\n//\n//\n// ======\n// Imports\n// ======\n//\n//\n\nuse std::{ffi::OsStr, path::Path, time::Duration};\n\nuse fuse_mt::{DirectoryEntry, FileAttr, FileType, FilesystemMT, Statfs};\nuse log::{debug, error, info, warn};\nuse rand::Rng;\n\nuse crate::{\n    filesystem::{\n        filesystem_struct::FlusterFS,\n        item_flag::flag_struct::ItemFlag\n    },\n    pool::{disk::{\n        generic::io::cache::cache_io::CachedBlockIO,\n        standard_disk::block::{\n            directory::directory_struct::{\n                DirectoryBlock, DirectoryItem, DirectoryItemFlags\n            },\n            io::directory::types::NamedItem\n        }\n    }, pool_actions::pool_struct::{Pool, GLOBAL_POOL}}, tui::{notify::NotifyTui, prompts::TuiPrompt, tasks::TaskType}\n};\n\nuse super::file_handle::file_handle_struct::FileHandle;\nuse crate::error_types::filesystem::*;\n\nuse fuse_mt::CreatedEntry;\n\n\n\n\n//\n//\n// ======\n// Constants\n// ======\n//\n//\n\n// You should probably be able to set your own custom TTL on mount, but\n// guess what? You should also probably be able to chown files. but that ain't happening either.\n// Hard coded to one year, see issue #51\nconst HANDLE_TIME_TO_LIVE: Duration = Duration::from_secs(365*24*60*60);\n\n\n\n//\n//\n// ======\n// The actual fuse layer\n// ======\n//\n// There's a lot of stuff in here we technically dont need. And I'm going to assume the information on this page is correct\n// https://www.cs.hmc.edu/~geoff/classes/hmc.cs135.201001/homework/fuse/fuse_doc.html\n// I have archived this page on internet archive.\n// Thanks Geoff! I hope your life is going well, 16 years later.\n//\n// In theory, some calls like truncate() should be handled by the os before other operations here. We will still check for those flags, just in case.\n//\n// Also, in theory again, we should NEVER modify the flags before returning them. Linux VFS depends on this (in theory)\n\nimpl FilesystemMT for FlusterFS {\n    // The most British function in Fluster\n    fn init(&self, _req: fuse_mt::RequestInfo) -> fuse_mt::ResultEmpty {\n        // To speed up reads / reduce swaps, we will read the entire directory tree structure into memory\n        // on startup.\n\n        // So we will list the root directory.\n        // None means we get the root\n        let root = DirectoryBlock::try_find_directory(None).expect(\"There should be a root directory before init\").expect(\"ditto\");\n\n        // Keep listing each subdirectory until we're done, and if its not a directory, get the size of the file to\n        // load in its info as well.\n\n        let mut all_items: Vec<DirectoryItem> = root.list().expect(\"should be there\");\n\n        while let Some(item) = all_items.pop() {\n            if item.flags.contains(DirectoryItemFlags::IsDirectory) {\n                let block = item.get_directory_block().expect(\"If this doesn't work we're cooked anyways\");\n                all_items.extend(block.list().expect(\"Ditto\").into_iter());\n            } else {\n                // This is a file, just get the size\n                // We do this twice to make sure it get promoted.\n                let _ = item.get_size().expect(\"cooked\");\n                let _ = item.get_size().expect(\"cooked\");\n            }\n        }\n\n        Ok(())\n    }\n\n    // Called when filesystem is unmounted. Should flush all data to disk.\n    fn destroy(&self) {\n        // Inform user the filesystem is shutting down\n        TuiPrompt::prompt_enter(\"Fluster! is shutting down.\".to_string(),\n            \"Cache will now be flushed to disk\".to_string(),\n            true\n        );\n        info!(\"Shutting down filesystem...\");\n        // Flush all of the tiers of cache.\n        info!(\"Flushing cache...\");\n        // We dont retry flushing the cache, since if the flushing fails, that means it went all the way through\n        // the troubleshooter and everything. If we retry here, chances are the cache has already dropped some blocks,\n        // so we wouldn't even be able to properly finish it at this point.\n        CachedBlockIO::flush().expect(\"I sure hope cache flushing works!\");\n        // Now flush pool information\n        info!(\"Flushing pool info...\");\n        // Same story here.\n        Pool::flush().expect(\"I sure hope pool flushing works!\");\n        info!(\"Goodbye! .o/\");\n    }\n\n    // Get file attributes of an item.\n    fn getattr(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        path: &std::path::Path,\n        fh: Option<u64>,\n    ) -> fuse_mt::ResultEntry {\n        debug!(\"Getting attributes of `{}`...\", path.display());\n\n        // I already wrote a method for this yay\n        // but that assumes we have a handle.\n        if let Some(handle) = fh {\n            debug!(\"Handle was provided, getting and returning attributes.\");\n            // Handle exists, easy path.\n            return Ok(\n                (\n                    HANDLE_TIME_TO_LIVE,\n                    FileHandle::read(handle).try_into()?\n                )\n            )\n        }\n\n        // No handle, dang.\n        \n        // Go find that sucker\n\n        // Making a temporary handle (doesn't need to be allocated) lets us call some easier methods.\n        // We cant just use the TryInto FileAttr since we dont know for sure if the item exists yet.\n        let temp_handle: FileHandle = FileHandle {\n            path: path.into(),\n        };\n\n        // Go get the item.\n        // This will automatically throw the correct error if the file/folder did not exist.\n        let found_item = temp_handle.get_directory_item()?;\n\n        // Get the attributes\n        debug!(\"Getting attributes of item...\");\n        let found_attributes: FileAttr = found_item.try_into()?;\n        debug!(\"Done! Returning.\");\n\n        Ok(\n            (\n                HANDLE_TIME_TO_LIVE,\n                found_attributes\n            )\n        )\n    }\n\n    // We dont support file permissions.\n    \n    // fn chmod(\n    //     &self,\n    //     _req: fuse_mt::RequestInfo,\n    //     _path: &std::path::Path,\n    //     _fh: Option<u64>,\n    //     _mode: u32,\n    // ) -> fuse_mt::ResultEmpty {\n    //     Err(libc::ENOSYS)\n    // }\n\n    // We dont support file permissions.\n    // fn chown(\n    //     &self,\n    //     _req: fuse_mt::RequestInfo,\n    //     _path: &std::path::Path,\n    //     _fh: Option<u64>,\n    //     _uid: Option<u32>,\n    //     _gid: Option<u32>,\n    // ) -> fuse_mt::ResultEmpty {\n    //     Err(libc::ENOSYS)\n    // }\n\n    // File truncation is supported.\n    // Does not always truncate file to 0 bytes long.\n    fn truncate(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        path: &std::path::Path,\n        fh: Option<u64>,\n        size: u64,\n    ) -> fuse_mt::ResultEmpty {\n        debug!(\"Truncating `{}` to be `{}` bytes long...\", path.display(), size);\n        let task_handle = NotifyTui::start_task(\n            TaskType::FilesystemTruncateFile(\n                path.file_name().unwrap_or(OsStr::new(\"?\")).display().to_string()\n            ),\n            2\n        );\n        // Get a file handle\n        let handle: FileHandle = if let Some(exists) = fh {\n            debug!(\"File handle was passed in, using that...\");\n            // Got a handle from the call, no fancy work.\n            // Read it in\n            FileHandle::read(exists)\n        } else {\n            debug!(\"No handle provided, spoofing...\");\n            // Temp handle that we will not allocate.\n            FileHandle {\n                path: path.into(),\n            }\n        };\n\n        debug!(\"Handle obtained.\");\n\n        // Go load the file to truncate.\n        // Will return properly if item does not exist.\n        let found_item = handle.get_directory_item()?;\n        NotifyTui::complete_task_step(&task_handle);\n        \n        // Now with the directory item, we can run the truncation.\n        debug!(\"Starting truncation...\");\n        found_item.truncate(size)?;\n        NotifyTui::complete_task_step(&task_handle);\n        debug!(\"Truncation finished.\");\n        NotifyTui::finish_task(task_handle);\n        // All done.\n        Ok(())\n    }\n\n    // We do not support manually updating timestamps.\n    // fn utimens(\n    //     &self,\n    //     _req: fuse_mt::RequestInfo,\n    //     _path: &std::path::Path,\n    //     _fh: Option<u64>,\n    //     _atime: Option<std::time::SystemTime>,\n    //     _mtime: Option<std::time::SystemTime>,\n    // ) -> fuse_mt::ResultEmpty {\n    //     Err(libc::ENOSYS)\n    // }\n\n    // We do not support manually updating timestamps.\n    // fn utimens_macos(\n    //     &self,\n    //     _req: fuse_mt::RequestInfo,\n    //     _path: &std::path::Path,\n    //     _fh: Option<u64>,\n    //     _crtime: Option<std::time::SystemTime>,\n    //     _chgtime: Option<std::time::SystemTime>,\n    //     _bkuptime: Option<std::time::SystemTime>,\n    //     _flags: Option<u32>,\n    // ) -> fuse_mt::ResultEmpty {\n    //     Err(libc::ENOSYS)\n    // }\n\n    // We do not support symbolic links.\n    // fn readlink(&self, _req: fuse_mt::RequestInfo, _path: &std::path::Path) -> fuse_mt::ResultData {\n    //     Err(libc::ENOSYS)\n    // }\n\n    // \"This function is rarely needed, since it's uncommon to make these objects inside special-purpose filesystems.\"\n    // This is for fancy things like block objects i believe, we do not support this.\n    // fn mknod(\n    //     &self,\n    //     _req: fuse_mt::RequestInfo,\n    //     _parent: &std::path::Path,\n    //     _name: &std::ffi::OsStr,\n    //     _mode: u32,\n    //     _rdev: u32,\n    // ) -> fuse_mt::ResultEntry {\n    //     Err(libc::ENOSYS)\n    // }\n\n    // Create a new directory if it does not already exist.\n    // Returns file attributes about the new directory\n    fn mkdir(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        parent: &std::path::Path,\n        name: &std::ffi::OsStr,\n        _mode: u32, // Permission bit related. Do not need.\n    ) -> fuse_mt::ResultEntry {\n        debug!(\"Creating new directory in `{}` named `{}`.\", parent.display(), name.display());\n        let handle = NotifyTui::start_task(TaskType::FilesystemMakeDirectory(name.display().to_string()), 3);\n        // Make sure the name isn't too long\n        if name.len() > 255 {\n            debug!(\"Name is too long.\");\n            return Err(FILE_NAME_TOO_LONG);\n        }\n\n        // the new directory\n        let new_dir: DirectoryItem;\n        let the_name: String = name.to_str().expect(\"Should be valid utf8\").to_string();\n\n        // Open parent\n        if let Some(mut parent) = DirectoryBlock::try_find_directory(Some(parent))? {\n            NotifyTui::complete_task_step(&handle);\n            debug!(\"Checking if directory exists...\");\n            if parent.find_item(&NamedItem::Directory(the_name.clone()))?.is_some() {\n                // Directory already exists.\n                debug!(\"Directory already exists.\");\n                NotifyTui::cancel_task(handle);\n                return Err(ITEM_ALREADY_EXISTS)\n            }\n            \n            // Make the directory\n            debug!(\"It did not, creating directory...\");\n            new_dir = parent.make_directory(the_name)?;\n            NotifyTui::complete_task_step(&handle);\n            debug!(\"Directory created.\");\n        } else {\n            // No such parent\n            debug!(\"Parent did not exist.\");\n            NotifyTui::cancel_task(handle);\n            return Err(NO_SUCH_ITEM);\n        }\n        \n        // Now we need attribute information about it.\n        debug!(\"Getting attribute info...\");\n        let attributes: FileAttr = new_dir.try_into()?;\n        NotifyTui::complete_task_step(&handle);\n        debug!(\"Done.\");\n        \n        // All done!\n        debug!(\"Directory created successfully.\");\n        NotifyTui::finish_task(handle);\n        Ok(\n            (\n                HANDLE_TIME_TO_LIVE,\n                attributes\n            )\n        )\n    }\n\n    // Deletes a file.\n    fn unlink(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        parent: &std::path::Path,\n        name: &std::ffi::OsStr,\n    ) -> fuse_mt::ResultEmpty {\n        debug!(\"Deleting file `{}` from directory `{}`...\", name.display(), parent.display());\n\n        let handle = NotifyTui::start_task(TaskType::FilesystemDeleteFile(name.display().to_string()), 3);\n\n        // Make a fake handle to lookup the file we are looking for\n        let temp_handle: FileHandle = FileHandle {\n            path: parent.join(name).into(),\n        };\n\n        // This will return properly if the item did not exist.\n        let file = temp_handle.get_directory_item()?;\n        NotifyTui::complete_task_step(&handle);\n        \n        // Make sure it's a file\n        if file.flags.contains(DirectoryItemFlags::IsDirectory) {\n            // Cannot unlink directories.\n            debug!(\"A directory was provided, not a file.\");\n            NotifyTui::cancel_task(handle);\n            return Err(NOT_A_DIRECTORY);\n        };\n        \n        // Now we need the parent directory block to perform the removal\n        debug!(\"Looking for file...\");\n        if let Some(mut parent_dir) = DirectoryBlock::try_find_directory(Some(parent))? {\n            NotifyTui::complete_task_step(&handle);\n            // Delete the file\n            if parent_dir.delete_file(file.into())?.is_some() {\n                NotifyTui::complete_task_step(&handle);\n                // All done.\n                debug!(\"File deleted.\");\n                NotifyTui::finish_task(handle);\n                Ok(())\n            } else {\n                // Weird, we checked that the directory was there, but when we went to delete it, it wasnt???\n                warn!(\"We found the directory to delete, but when we tried to delete it, it was missing.\");\n                // this should not happen lmao, but whatever.\n                NotifyTui::cancel_task(handle);\n                Err(NO_SUCH_ITEM)\n            }\n        } else {\n            // Should be impossible to get here, since we were already able to get the item from the parent\n            // a few lines ago.\n            // But we'll still gracefully handle it just in case.\n            debug!(\"Parent folder does not exist.\");\n            NotifyTui::cancel_task(handle);\n            Err(NO_SUCH_ITEM)\n        }\n    }\n\n    // Deletes a directory.\n    // Should fail if the directory is not empty.\n    fn rmdir(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        parent: &std::path::Path,\n        name: &std::ffi::OsStr,\n    ) -> fuse_mt::ResultEmpty {\n        debug!(\"Attempting to remove directory `{}` from `{}`...\", name.display(), parent.display());\n\n        let handle = NotifyTui::start_task(TaskType::FilesystemRemoveDirectory(name.display().to_string()), 4);\n\n        let string_name: String = name.to_str().expect(\"Should be valid utf8\").to_string();\n\n        // Open the parent directory\n        if let Some(parent_dir) = DirectoryBlock::try_find_directory(Some(parent))? {\n            NotifyTui::complete_task_step(&handle);\n            // Parent exists, get the child\n            if let Some(child_dir) = parent_dir.find_item(&NamedItem::Directory(string_name))? {\n                NotifyTui::complete_task_step(&handle);\n                // Directory exists.\n                \n                // Make sure this is actually a directory\n                if !child_dir.flags.contains(DirectoryItemFlags::IsDirectory) {\n                    // Not a dir\n                    debug!(\"Provided item is not a directory.\");\n                    NotifyTui::cancel_task(handle);\n                    return Err(NOT_A_DIRECTORY);\n                }\n                \n                // Get the block\n                let block_to_delete = child_dir.get_directory_block()?;\n                NotifyTui::complete_task_step(&handle);\n                \n                // Make sure it's empty\n                if !block_to_delete.is_empty()? {\n                    // Nope.\n                    debug!(\"Directory is not empty, cannot delete.\");\n                    NotifyTui::cancel_task(handle);\n                    return Err(DIRECTORY_NOT_EMPTY);\n                }\n                \n                // Run the deletion.\n                debug!(\"Deleting directory...\");\n                block_to_delete.delete_self(child_dir)?;\n                NotifyTui::complete_task_step(&handle);\n                NotifyTui::finish_task(handle);\n                debug!(\"Done.\");\n                Ok(())\n                \n            } else {\n                // child directory did not exist.\n                debug!(\"The directory we wanted to delete does not exist.\");\n                NotifyTui::cancel_task(handle);\n                Err(NO_SUCH_ITEM)\n            }\n        } else {\n            // parent dir went to get milk\n            debug!(\"Parent directory does not exist.\");\n            NotifyTui::cancel_task(handle);\n            Err(NO_SUCH_ITEM)\n        }\n    }\n\n    // We do not support symlinks.\n    // fn symlink(\n    //     &self,\n    //     _req: fuse_mt::RequestInfo,\n    //     _parent: &std::path::Path,\n    //     _name: &std::ffi::OsStr,\n    //     _target: &std::path::Path,\n    // ) -> fuse_mt::ResultEntry {\n    //     Err(libc::ENOSYS)\n    // }\n\n    // Renames / moves item.\n    // Complicated error logic due to https://man7.org/linux/man-pages/man2/rename.2.html\n    fn rename(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        parent: &std::path::Path,\n        name: &std::ffi::OsStr,\n        newparent: &std::path::Path,\n        newname: &std::ffi::OsStr,\n    ) -> fuse_mt::ResultEmpty {\n        debug!(\"Renaming a item from `{}` to `{}`,\", name.display(), newname.display());\n        debug!(\"and moving from `{}` to `{}`.\", parent.display(), newparent.display());\n\n        // According to the man pages, we should get some flags here. but we dont.\n        // I assume things like RENAME_NOREPLACE are being handled for us then.\n\n        // Any case:\n        // EISDIR newpath is an existing directory, but oldpath is not a directory.\n        // EINVAL The new pathname contained a path prefix of the old, or, more generally, an attempt was made to make a directory a\n        //  subdirectory of itself. (recursion moment)\n        // ENOENT The link named by oldpath does not exist; or, a directory component in newpath does not exist; or, oldpath or newpath\n        //  is an empty string. (TLDR this is a catch all)\n\n        // If we are moving a directory:\n        // The new path must not exist, or be empty.\n        // ENOTEMPTY or EEXIST newpath is a nonempty directory\n        // ENOTDIR A component used as a directory in oldpath or newpath is not, in fact, a directory.  Or, oldpath is a directory, and\n        //  newpath exists but is not a directory. (Cant move non directories into directories, cant move directory into non directory.)\n\n        // \"If newpath exists but the operation fails for some reason,\n        //  rename() guarantees to leave an instance of newpath in place.\"\n        // why word it like this lmao\n        // if the destination already exists, but the move fails, keep what was already at the destination.\n\n        // Also in theory, we should be checking if anyone is reading this item and if they are, return busy.\n        // but there isnt any infra for that yet, and with the one year timeouts, you would have to wait a while.\n        // fun. we will ignore it until something explodes.\n\n\n\n        // This is gonna be REALLY complicated. lol.\n        \n        \n\n        // Make sure the new name isn't too long\n        if newname.len() > 255 {\n            // too long\n            warn!(\"New item name was too long\");\n            return Err(FILE_NAME_TOO_LONG);\n        }\n\n\n        // Now, the \"easy\" cases.\n        // Where we're coming from (including name of file/folder)\n        let source_full_temp_handle: FileHandle = FileHandle {\n            path: parent.join(name).into(),\n        };\n\n        // Where we want to go\n        // Where we're coming from (including name of file/folder)\n        let destination_full_temp_handle: FileHandle = FileHandle {\n            path: newparent.join(newname).into(),\n        };\n\n        // If they are the same, we dont need to do anything at all.\n        if source_full_temp_handle.path == destination_full_temp_handle.path {\n            // why...\n            debug!(\"Source and destination are the same, skipping.\");\n            return Ok(());\n        }\n\n        // Make sure the two are the same underlying type\n        let source_is_file = match source_full_temp_handle.is_file()? {\n            Some(bool) => bool,\n            None => {\n                // The source item does not exist.\n                return Err(NO_SUCH_ITEM);\n            },\n        };\n\n        let destination_is_file = match destination_full_temp_handle.is_file()? {\n            Some(bool) => bool,\n            None => {\n                // The destination item does not exist, which means we're not going to replace a pre-existing item, thus we\n                // can just copy what the other file type is, since we will be creating this later.\n                source_is_file\n            },\n        };\n\n\n        debug!(\"Making sure the two are of the same type...\");\n        if source_is_file == destination_is_file {\n            debug!(\n                \"Types are the same, both are {}.\",\n                if source_is_file {\n                    \"files\"\n                } else {\n                    \"directories\"\n                }\n            )\n            // They are both the same.\n        } else {\n            // Types are different, we cannot do that\n            warn!(\"Types are different, we cannot perform this rename/move.\");\n            return Err(NOT_A_DIRECTORY);\n        }\n\n        // Now that we know the two types are the same,\n        // Grab the parents and the item we are attempting to move depending on type.\n\n        // For both types of rename/move operations, we must have:\n        // - The parent folder of the source, and destination\n        // - The item we are renaming\n        // But we do not need the destination item to exist.\n        // Any logic around that is handled differently depending on if this was a file or not.\n\n        // I wrote directory handling first, then came back for file movement. Standing on the shoulders of myself, i know that\n        // directories always return a Err(NO_SUCH_ITEM) if either of the parents do not exist. This is also true of files, so we\n        // can perform that check out here.\n        \n        debug!(\"Trying to obtain the parents of the source and destination, and the directory item for the item we are trying to move.\");\n\n        let source_item_name: String = name.to_str().expect(\"Should be valid utf8\").to_string();\n        let destination_item_name: String = newname.to_str().expect(\"Should be valid utf8\").to_string();\n\n        debug!(\"Checking if parents existed...\");\n        // Try to get the source parent directory, then try to get the item refering to the dir we are moving.\n        let mut source_parent_dir: DirectoryBlock = if let Some(exist) = DirectoryBlock::try_find_directory(Some(parent))? {\n            // Good.\n            debug!(\"Source parent exists.\");\n            exist\n        } else {\n            // missing\n            warn!(\"Source parent did not exist. Cannot continue.\");\n            return Err(NO_SUCH_ITEM);\n        };\n\n        let mut destination_parent_dir: DirectoryBlock = if let Some(exist) = DirectoryBlock::try_find_directory(Some(newparent))? {\n            // Good.\n            debug!(\"Destination parent exists.\");\n            exist\n        } else {\n            // missing\n            warn!(\"Destination parent did not exist. Cannot continue.\");\n            return Err(NO_SUCH_ITEM);\n        };\n\n        // Item logic must be handled lower down, but we can at least abstract the calls out at this point to work with options later.\n\n        // We know the kind here so we can abstract this away as well.\n        let maybe_source_directory_item: Option<DirectoryItem>;\n        let maybe_destination_directory_item: Option<DirectoryItem>;\n        if source_is_file {\n            // both files.\n            maybe_source_directory_item = source_parent_dir.find_item(&NamedItem::File(source_item_name.clone()))?;\n            maybe_destination_directory_item = destination_parent_dir.find_item(&NamedItem::File(destination_item_name.clone()))?;\n        } else {\n            // both directories.\n            maybe_source_directory_item = source_parent_dir.find_item(&NamedItem::Directory(source_item_name.clone()))?;\n            maybe_destination_directory_item = destination_parent_dir.find_item(&NamedItem::Directory(destination_item_name.clone()))?;\n        };\n\n        // The following complicated move logic requires that the two parent directories be different. If the source and \n        // destination directories are the same, we can just rename the inode, skipping all of the fancier operations.\n\n        if parent == newparent {\n            // Sweet!\n            // We dont even need the destination info\n            drop(maybe_destination_directory_item);\n            drop(destination_full_temp_handle);\n            drop(destination_parent_dir);\n\n            // Source must exist.\n            let source = match maybe_source_directory_item {\n                Some(ok) => ok,\n                None => {\n                    // Can't rename nothing.\n                    return Err(NO_SUCH_ITEM);\n                },\n            };\n\n            // Type does not matter, we can just update the name in the directory, since inodes do not hold that info.\n            // Explicitly check the error, silently returning when this fails is bad.\n            let rename_result = match source_parent_dir.try_rename_item(&source.into(), destination_item_name) {\n                Ok(ok) => ok,\n                Err(err) => {\n                    // Renaming failed for a lower level issue!\n                    warn!(\"Item rename failed! Why?\");\n                    warn!(\"`{err:#?}`\");\n                    // Bail out\n                    return Err(err.into());\n                },\n            };\n\n\n            if rename_result {\n                // rename worked.\n                debug!(\"Item renamed successfully.\");\n                return Ok(())\n            } else {\n                // Somehow the item we wanted to rename disapeared, we'll return a generic error.\n                // Since panicing at this high of a level would be stupid. Even though this branch is\n                // unlikely.\n                warn!(\"Item to rename disappeared!\");\n                return Err(GENERIC_FAILURE);\n            }\n        }\n\n        // This rename moves the item between directories.\n\n\n        // we branch depending on if it was a file or directory, handling is slightly different\n        if source_is_file {\n            //\n            // File movement.\n            //\n            debug!(\"Starting file movement...\");\n            // If the new item exists, it will be replaced. (see rename(2) manpage)\n\n            // The source item must exist.\n            debug!(\"Checking that source item exists...\");\n            let source_item = if let Some(existed) = maybe_source_directory_item {\n                // good\n                debug!(\"Yes it does.\");\n                existed\n            } else {\n                // cant move nothing.\n                warn!(\"Source item did not exist. Cannot perform rename/move.\");\n                return Err(NO_SUCH_ITEM);\n            };\n\n            debug!(\"Checking if destination file already existed...\");\n            // Check if the destination file already exists, that will change our behavior on failure.\n            if maybe_destination_directory_item.is_some() {\n                // Destination item exists, we will be overwriting this, but we will hold onto it just in case.\n                // In theory we should try to put this back if the rename fails.\n\n                // Since the item exists already, we will extract it. To delete the old file we need to have the file in the block. Which\n                // complicates things a bit. So we pull it out, and when we are ready to delete it, we rename it, put it back in, and delete it.\n                let extracted_old: DirectoryItem = match destination_parent_dir.find_and_extract_item(&NamedItem::File(destination_item_name.clone())) {\n                    Ok(ok) => match ok {\n                        Some(ok) => ok,\n                        None => {\n                            // We tried to extract it, but it was no longer there?\n                            // No data has been modified (by us here at least), entire operation can be retried.\n                            warn!(\"Destination item was no longer there when extraction was attempted. Non-fatal, but weird.\");\n                            return Err(NO_SUCH_ITEM)\n                        },\n                    },\n                    Err(error) => {\n                        // The extraction failed at _some_ point, unknown where. Chances are the destination item is now gone. But we have failed.\n                        warn!(\"Extracting the old item failed for a low level reason, we have to bail.\");\n                        return Err(error.into())\n                    },\n                };\n\n                // Now we have that copy for later, we can stick a copy of the source item into the destination\n                debug!(\"Inserting copy of the source file into the destination directory...\");\n                // We must give it the new name first:\n                let mut renamed_source_item = source_item;\n                renamed_source_item.name = destination_item_name.clone();\n                renamed_source_item.name_length = destination_item_name.len() as u8; // Already checked name length.\n                match destination_parent_dir.add_item(&renamed_source_item) {\n                    Ok(_) => (),\n                    Err(error) => {\n                        // Failed to add item to destination\n                        warn!(\"Failed to put copy into the destination for low level reason.\");\n                        // Try to put back the file that was here before.\n                        warn!(\"Attempting to restore previous item if possible...\");\n                        // Chances are if the previous one failed, this will to, but whatever.\n                        if destination_parent_dir.add_item(&extracted_old).is_ok() {\n                            warn!(\"Previous file restored.\");\n                        } else {\n                            // oof\n                            warn!(\"Failed to restore previous file. File has been permanently lost.\");\n                        }\n                        // As good as it'll get. Return the error that caused us to fail.\n                        return Err(error.into())\n                    },\n                }\n                debug!(\"Done.\");\n                debug!(\"Removing old item from origin directory...\");\n\n                // Now go to the parent of the source and tell em to slime tf outta the old item\n\n                match source_parent_dir.find_and_extract_item(&NamedItem::File(source_item_name.clone())) {\n                    Ok(ok) => {\n                        match ok {\n                            Some(_found) => {\n                                // Removal worked\n                                debug!(\"Source item removed.\")\n                            },\n                            None => {\n                                // Item we tried to remove was not there.\n                                // Weird, but if we got this far, it must have at least made it into the destination.\n                                warn!(\"The source item we tried to remove was not there. Which is fine, since that was the goal anyways.\");\n                            },\n                        }\n                    },\n                    Err(err) => {\n                        // We have now failed to remove the previous item, but the new one is in place. This is good enough.\n                        // If the item is still in that other block, it is now a duplicate reference of the underlying file.\n                        // Not great... but not much I can do here. No transactions means no safe actions!\n                        warn!(\"Failed to remove previous item, it may still be there. Good enough.\");\n                        // The move has now technically finished, even though we have errored at this point, so guess what?\n                        debug!(\"Removal failed due to: {err:#?}\");\n                        return Ok(());\n                    },\n                };\n\n                // Now that the file is no longer where it used to live, we can delete the item that used to live in the destination that\n                // we overwrote.\n\n                // If this fails, the item is no longer in the file, but will still occupy blocks on disk, which isn't great, but at least\n                // you cant get to them.\n\n                // Rename the item so we dont collide with the newly moved in file\n                let mut renamed: DirectoryItem = extracted_old;\n                renamed.name.push_str(\".fluster_old\");\n                // Is this too long now?\n                if renamed.name.len() > 255 {\n                    // Shoot, go with a random name instead.\n                    let mut random: rand::prelude::ThreadRng = rand::rng();\n                    let random_name: u128 = random.random();\n                    let mut random_name_string = random_name.to_string();\n                    random_name_string.truncate(128);\n                    renamed.name = random_name_string;\n                    renamed.name_length = renamed.name.len() as u8; // will fit\n                }\n\n                // Hold onto the new name for later\n                let deletion_name: String = renamed.name.clone();\n\n                // Put the renamed item in there.\n                debug!(\"Re-inserting old file with a new name to delete it...\");\n                match destination_parent_dir.add_item(&renamed) {\n                    Ok(_) => {\n                        // Adding worked.\n                        debug!(\"Inserted.\")\n                    },\n                    Err(err) => {\n                        // Damn. We will just leak the blocks this took up.\n                        warn!(\"Insertion failed.\");\n                        warn!(\"Just to keep going, we will leak the blocks that the old file references.\");\n                        warn!(\"Rename \\\"finished\\\".\");\n                        debug!(\"Failed due to: {err:#?}\");\n                        // Rename still worked overall.\n                        return Ok(());\n                    },\n                }\n                // Now kill it\n                debug!(\"Deleting the old file...\");\n                match destination_parent_dir.delete_file(NamedItem::File(deletion_name)) {\n                    Ok(ok) => match ok {\n                        Some(_) => {\n                            // File was deleted.\n                            debug!(\"Old file deleted.\")\n                        },\n                        None => {\n                            // The file did not exist?!?\n                            warn!(\"Somehow, we added the file, and when we went to delete it, it no longer existed.\");\n                            warn!(\"It probably leaked, but there is nothing we can do.\");\n                            // Good enough!\n                            warn!(\"Good enough. Rename finished.\");\n                            return Ok(());\n                        },\n                    },\n                    Err(err) => {\n                        // Yet another leak scenario.\n                        warn!(\"Deletion failed.\");\n                        warn!(\"Just to keep going, we will leak the blocks that the old file references.\");\n                        warn!(\"Rename \\\"finished\\\".\");\n                        debug!(\"Failed due to: {err:#?}\");\n                        // Rename still worked overall.\n                        return Ok(());\n                    },\n                }\n\n                // File has been deleted. Cleanup is now finished.\n                // Done moving file, which replaced a pre-existing file.\n            } else {\n                // We are not trying to overwrite a pre-existing file, this makes our lives easier.\n                debug!(\"Destination item does not exist yet. We will create it.\");\n\n                // We must give it the new name first:\n                let mut renamed_source_item = source_item;\n                renamed_source_item.name = destination_item_name.clone();\n                renamed_source_item.name_length = destination_item_name.len() as u8; // Already checked name length.\n\n                // Put the file into the destination\n                debug!(\"Adding source file to destination directory...\");\n                match destination_parent_dir.add_item(&renamed_source_item) {\n                    Ok(_) => {\n                        // That worked\n                    },\n                    Err(err) => {\n                        // Failed to add to destination\n                        // Drive level issue.\n                        warn!(\"Failed at a level lower than us. Unknown state.\");\n                        debug!(\"Failed due to: {err:#?}\");\n                        return Err(err.into())\n                    },\n                }\n                debug!(\"File added.\");\n\n                // Now we need to remove the old item.\n                match source_parent_dir.delete_file(NamedItem::File(source_item_name)) {\n                    Ok(ok) => {\n                        // if ok is none, the item disappeared, which should not happen.\n                        // Yes its weird that the item dissapeared, but its better to let the caller\n                        // re-list the directory and try the rename again if needed. Otherwise we would\n                        // just crash, which is, sub-par.\n\n                        if ok.is_none() {\n                            warn!(\"The item we were renaming disappeared!\");\n                            return Err(GENERIC_FAILURE)\n                        }\n                    },\n                    Err(err) => {\n                        // The file made it to the destination, but removing the original failed.\n                        // The old item may still be there, or it leaked blocks due to failed cleanup.\n                        warn!(\"Failed to delete source item, it may still be there.\");\n                        warn!(\"Blocks were probably leaked.\");\n                        warn!(\"Non-critical deletion failure: {err:#?}\")\n                        // Good enough.\n                    },\n                }\n            }\n            // All done.\n            debug!(\"File moved successfully.\");\n            Ok(())\n        } else {\n            //\n            // Directory movement.\n            //\n            debug!(\"Starting directory movement...\");\n            \n            // Make sure we aren't trying to make a self referential folder\n            debug!(\"Checking for recursion...\");\n            if destination_full_temp_handle.path.starts_with(&source_full_temp_handle.path) {\n                // Destination contains the source, therefore this is self referential.\n                warn!(\"Cannot move directory inside of itself.\");\n                return Err(INVALID_ARGUMENT);\n            }\n            debug!(\"No recursion.\");\n\n            \n            // To fulfill the requirement of the destination directory being empty, we will check if the directory exists in the\n            // parent. If it does, make sure its empty, if it is, then we will update the DirectoryItem to point at the block for\n            // the start of the DirectoryBlocks of the source directory.\n            // If the directory does not exist, we will create it and still do the same pointer swap.\n            // if the directory is not empty, we have to cancel the move.\n\n            // Do both directories exist?\n            debug!(\"Checking if the parents contain directory item we want to move...\");\n            // source\n            let source_directory_item: DirectoryItem = if let Some(item) = maybe_source_directory_item {\n                // Source exists\n                item\n            } else {\n                // Item was not there. Cant copy nothing.\n                debug!(\"Source directory missing, cannot rename/move folder.\");\n                return Err(NO_SUCH_ITEM);\n            };\n            debug!(\"Source good.\");\n\n            // Check that the destination folder exists and is empty.\n            if let Some(item) = maybe_destination_directory_item {\n                // Destination has to be empty\n                debug!(\"Destination already existed, making sure its empty...\");\n                if item.clone().get_directory_block()?.is_empty()? {\n                    // All good, we will delete the directory since we are going to replace it.\n                    debug!(\"It's empty, it will be deleted soon\");\n                } else {\n                    // Directory was not empty, we cannot continue\n                    warn!(\"Destination directory was not empty, cannot rename/move folder.\");\n                    return Err(DIRECTORY_NOT_EMPTY);\n                }\n            } else {\n                // no destination, we will make it\n                debug!(\"Destination did not exist, creating...\");\n                // Annoying clone.\n                let _ = destination_parent_dir.make_directory(destination_item_name.clone())?;\n                // yes we are creating it to just remove it again, but we are supposed to (in theory if i remember correctly idk its 1am) leave\n                // a folder at the destination even if the move fails.\n            };\n            debug!(\"Destination good.\");\n\n\n            // Now for the fun part, we can extract the DirectoryItem from the first directory, and swap it into\n            // the second one, thus the new folder points at the contents without moving the underlying files in the folder.\n\n            debug!(\"Swapping DirectoryItems...\");\n\n            // Remove the destination folder\n            // \"Inshallah he will be grounded into a fine dust\"\n\n            // We have to tread lightly at this point. If the swap fails, we would lose data.\n            // 🤓 erm actually the data would still be there, just not referenced- SHUT UP\n\n            // Extract, and delete. Extraction cleans up anything this used to point to for us.\n            debug!(\"Extracting destination...\");\n            let _extracted_dest = match destination_parent_dir.find_and_extract_item(&NamedItem::Directory(destination_item_name.clone())) {\n                Ok(ok) => {\n                    // Directory had to've been there, right?\n                    if let Some(worked) = ok {\n                        // Found and extracted the item, we will hold onto it incase we need to recreate it.\n                        worked\n                    } else {\n                        // What\n                        warn!(\"Tried to delete the destination directory to prepare for swap, but it was no longer there!\");\n                        // This should be impossible.\n                        // But just in case, er, say the operation failed.\n                        return Err(GENERIC_FAILURE);\n                    }\n                    // We will hold onto it just in case, even though it's empty.\n                },\n                Err(err) => {\n\n                    // This should fail tests.\n                    if cfg!(test) {\n                        panic!(\"{err:#?}\");\n                    }\n\n                    // Drive level issue.\n                    warn!(\"Failed at a level lower than us. Unknown state.\");\n                    return Err(err.into())\n                },\n            };\n            debug!(\"Destination extracted\");\n\n            // Now get a COPY of the source, we wont remove the source until we know for sure we have properly moved the folder.\n            // Wait, we already have it, in `source_directory_item`, duh\n            \n            // Attempt to add the directory to the new parent\n            debug!(\"Inserting copy of source directory...\");\n\n            // Make sure we rename dat mf fr\n            let mut renamed_source_dir_item = source_directory_item;\n            renamed_source_dir_item.name = destination_item_name.clone();\n            renamed_source_dir_item.name_length = destination_item_name.len() as u8; // Length checked above\n\n            match destination_parent_dir.add_item(&renamed_source_dir_item) {\n                Ok(_) => {\n                    // that worked\n                    // No need to do anything\n                },\n                Err(err) => {\n                    // Drive level issue.\n                    warn!(\"Failed at a level lower than us. Unknown state.\");\n                    // Attempt to uphold POSIX standard (like hell the rest of fluster is compliant) by\n                    // at least attempting to put the original directory back again.\n                    // We dont actually need to though, since it hasn't been extracted yet.\n\n                    // This should fail tests.\n                    if cfg!(test) {\n                        panic!(\"{err:#?}\");\n                    }\n\n                    return Err(err.into())\n                },\n            }\n            debug!(\"Insertion succeeded.\");\n            \n            // Now that the data has been safely pointed at from the new location, we will remove the old reference to it.\n            debug!(\"Removing old source...\");\n            let ashes = match source_parent_dir.find_and_extract_item(&NamedItem::Directory(source_item_name.clone())) {\n                Ok(ash) => ash,\n                Err(err) => {\n                    // Drive level issue.\n                    warn!(\"Failed at a level lower than us. Unknown state.\");\n                    // So now we have properly moved the source into the destination, but the source might still be there.\n                    // This wouldn't be too bad, if not for the fact that now both DirectoryBlocks contain the same DirectoryItem which\n                    // points to the same directory. Thus they have become hard-linked. Not great.\n                    // Good luck lmao.\n                    warn!(\"There isn't really anything we can do at this point, a hard link has been created due to this.\");\n                    warn!(\"We consider this good enough. Done moving directory.\");\n                    // shoulda thought ahead and made fluster more transactional, oh well.\n                    // We have to ignore the error, but might as well print it.\n                    debug!(\"{err:#?}\");\n\n                    // This should fail tests.\n                    if cfg!(test) {\n                        panic!(\"{err:#?}\");\n                    }\n\n                    return Ok(());\n                },\n            };\n\n\n            if let Some(_to_ashes) = ashes {\n                // It was there, and removed.\n                // We're all done.\n                debug!(\"Done.\")\n            } else {\n                // ????? DIRECTORY IS NOT THERE (GONE) (STOLEM)\n                warn!(\"Somehow, we copied the source directory across correctly, but now the source is missing so we cant remove it.\");\n                // I mean like, this still worked, no?\n                // goals:\n                // put source in destination: check\n                // source is no longer there: check\n                // sooooooo\n                // GOOD ENOUGH!\n                warn!(\"...but that's close enough.\");\n            };\n\n            // All done!\n            debug!(\"Rename finished, directories renamed/moved.\");\n            Ok(())\n        }\n        // this comment is unreachable, all cases are covered.\n    }\n\n    // We do not support hard links.\n    // fn link(\n    //     &self,\n    //     _req: fuse_mt::RequestInfo,\n    //     _path: &std::path::Path,\n    //     _newparent: &std::path::Path,\n    //     _newname: &std::ffi::OsStr,\n    // ) -> fuse_mt::ResultEntry {\n    //     Err(libc::ENOSYS)\n    // }\n\n    // Open a file and get a handle that will be used to access it.\n    // Does not create files.\n    fn open(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        path: &std::path::Path,\n        flags: u32,\n    ) -> fuse_mt::ResultOpen {\n        debug!(\"Opening item at path `{}`...\", path.display());\n        let task_handle = NotifyTui::start_task(TaskType::FilesystemOpenFile(\n            path.file_name().unwrap_or(OsStr::new(\"?\")).display().to_string()\n        ), 4);\n        // Deduce the open permissions.\n        debug!(\"Deducing flags...\");\n        let converted_flag: ItemFlag = ItemFlag::deduce_flag(flags)?;\n        debug!(\"Ok.\");\n\n        // We require at least one of the read/write flags.\n        // ...or, more correctly: we would require them if we used them.\n        // We dont. Everything is read/write.\n\n        // We ignore any flags that are not valid for this method, such as\n        // truncation or creation flags.\n\n        // open() always returns a brand new file handle, regardless if that file was\n        // already open somewhere else.\n        let mut handle: FileHandle = FileHandle {\n            path: path.into(),\n        };\n\n        // We do not allocate the file handle until we are sure we will use it.\n\n        // Make sure the name of the file is not too long.\n        if handle.name().len() > 255 {\n            warn!(\"File name is too long.\");\n            // File name was too long.\n            NotifyTui::cancel_task(task_handle);\n            return Err(FILE_NAME_TOO_LONG)\n        }\n        \n        // If this is the dot directory, we need to go up a level to read ourselves.\n        if handle.name() == \".\" {\n            // Go up a path.\n            // If this returns none, all is well\n            handle.path = handle.path.parent().unwrap_or(Path::new(\"\")).into();\n        }\n        \n        // Load in info about where the file should be.\n        // This will bail if a low level floppy issue happens.\n        debug!(\"Attempting to load in the parent directory...\");\n        let containing_dir_block: DirectoryBlock = match DirectoryBlock::try_find_directory(handle.path.parent())? {\n            Some(ok) => ok,\n            None => {\n                // Cannot load files from directories that do not exist.\n                warn!(\"Directory that the item was supposed to be contained within does not exist.\");\n                NotifyTui::cancel_task(task_handle);\n                return Err(NO_SUCH_ITEM)\n            },\n        };\n        debug!(\"Directory loaded.\");\n        NotifyTui::complete_task_step(&task_handle);\n        \n        // At this point. We need to know if we are looking for a directory or a file.\n        debug!(\"Deducing request item type...\");\n        let extracted_name = handle.name();\n        \n        let item_to_find: NamedItem = match handle.get_named_item()? {\n            Some(item) => item,\n            None => {\n                // No such item exists.\n                NotifyTui::cancel_task(task_handle);\n                return Err(NO_SUCH_ITEM);\n            },\n        };\n        \n        if item_to_find.is_file() {\n            debug!(\"Looking for a file...\");\n        } else {\n            debug!(\"Looking for a directory...\");\n        }\n        debug!(\"Named `{extracted_name}`.\");\n        \n        // Hold onto the item until we need it\n        let found_item: DirectoryItem;\n        \n        // Now load in the directory item.\n        debug!(\"Attempting to find the item...\");\n        if let Some(exists) = containing_dir_block.find_item(&item_to_find)? {\n            debug!(\"Item exists.\");\n            found_item = exists;\n        } else {\n            // No item\n            debug!(\"Item does not exist.\");\n            NotifyTui::cancel_task(task_handle);\n            return Err(NO_SUCH_ITEM);\n        }\n        NotifyTui::complete_task_step(&task_handle);\n        \n        // We have now loaded in the directory item, or bailed out if needed.\n        \n        // Assert that this is a directory if required.\n        // In theory we could check this earlier, but it's good to ensure that the underlying\n        // item agrees.\n        if converted_flag.contains(ItemFlag::ASSERT_DIRECTORY) {\n            debug!(\"Caller wants to ensure they are opening a directory.\");\n            if !found_item.flags.contains(DirectoryItemFlags::IsDirectory) {\n                debug!(\"This is not a directory.\");\n                NotifyTui::cancel_task(task_handle);\n                return Err(NOT_A_DIRECTORY)\n            }\n            debug!(\"This is a directory.\");\n        }\n        \n        // We are done creating/loading the file, its time to get a handle.\n        debug!(\"Getting a handle on things...\");\n        let new_handle: u64 = handle.allocate();\n\n        NotifyTui::complete_task_step(&task_handle);\n        \n        // Done!\n        debug!(\"Opening finished.\");\n        Ok((new_handle, converted_flag.into()))\n    }\n\n    // Read file data from a file handle.\n    // \"Note that it is not an error for this call to request to read past the end of the file,\n    //   and you should only return data up to the end of the file\n    //   (i.e. the number of bytes returned will be fewer than requested; possibly even zero).\n    //   Do not extend the file in this case.\"\n    //\n    // Uses callbacks, wacky, not sure how that works.\n    fn read(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        path: &std::path::Path,\n        fh: u64,\n        offset: u64,\n        size: u32,\n        callback: impl FnOnce(fuse_mt::ResultSlice<'_>) -> fuse_mt::CallbackResult,\n    ) -> fuse_mt::CallbackResult {\n        debug!(\"Reading `{}` bytes from file `{}`\", size, path.display());\n\n        let task_handle = NotifyTui::start_task(\n            TaskType::FilesystemReadFile(\n                path.file_name()\n                .unwrap_or(OsStr::new(\"?\")).display().to_string()\n            ),\n            3\n        );\n\n        // Open the file handle\n        let got_handle = FileHandle::read(fh);\n\n        // Still not sure if we need to check this, but whatever.\n        if got_handle.path != path.into() {\n            // They aren't the same? not sure what to do with that\n            error!(\"readdir() tried to read a path, but provided a handle to a different path.\");\n            error!(\"fh: `{}` | path: `{}`\", got_handle.path.display(), path.display());\n            error!(\"Not sure what to do here, giving up.\");\n            NotifyTui::cancel_task(task_handle);\n            return callback(Err(GENERIC_FAILURE));\n        }\n        \n        // Try finding the directory item\n        let file = match got_handle.get_directory_item() {\n            Ok(ok) => ok,\n            Err(err) => {\n                // Getting the item failed, maybe it wasn't there.\n                NotifyTui::cancel_task(task_handle);\n                return callback(Err(err))\n            },\n        };\n        NotifyTui::complete_task_step(&task_handle);\n        \n        // Make sure that it's a file.\n        if file.flags.contains(DirectoryItemFlags::IsDirectory) {\n            // Can't read a directory!\n            warn!(\"Tried to read a directory as a file. Ignoring...\");\n            return callback(Err(IS_A_DIRECTORY));\n        }\n        \n        // Found a file!\n        // We need to bound our read by the size of the file, since the read() filesystem call can\n        // try to read past the end.\n        let file_size = match file.get_size() {\n            Ok(ok) => ok,\n            Err(error) => {\n                // Lower level error\n                NotifyTui::cancel_task(task_handle);\n                warn!(\"Failed to get size of file! Giving up...\");\n                return callback(Err(error.into()))\n            },\n        };\n\n        NotifyTui::complete_task_step(&task_handle);\n        \n        // Subtract the offset to idk man why am i explaining this im sure you understand.\n        // Reads are limited to 4GB long, which should be way above our max read size anyways.\n\n        // If a read bigger than that comes in, we'll ignore it.\n        let checking_size = std::cmp::min(size as u64, file_size - offset);\n        let bounded_read_length = if checking_size > u32::MAX.into() {\n            // Cant do that.\n\n            // Also if you're here, that means the file you're trying to read is actually >4GB.\n            // You are insane.\n\n            warn!(\"Tried to read more than 4GB at once!\");\n            NotifyTui::cancel_task(task_handle);\n            return callback(Err(INVALID_ARGUMENT));\n        } else {\n            // Size checked, this cast is safe.\n            checking_size as u32\n        };\n\n        if bounded_read_length != size {\n            // size did change.\n            debug!(\"Read was too large, truncated to `{bounded_read_length}` bytes.\");\n        }\n\n        // Do the read.\n        // This vec might be HUGE, this is why we need to limit the read size on the filesystem.\n        debug!(\"Starting read...\");\n        let read_buffer: Vec<u8> = match file.read_file(offset, bounded_read_length) {\n            Ok(ok) => ok,\n            Err(error) => {\n                // Lower level error\n                warn!(\"Failed while reading the file! Giving up...\");\n                NotifyTui::cancel_task(task_handle);\n                return callback(Err(error.into()))\n            },\n        };\n        NotifyTui::complete_task_step(&task_handle);\n        debug!(\"Read finished.\");\n        NotifyTui::finish_task(task_handle);\n\n        // All done!\n        callback(Ok(&read_buffer))\n    }\n\n    // Write data to a file using a file handle.\n    fn write(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        path: &std::path::Path,\n        fh: u64,\n        offset: u64,\n        data: Vec<u8>,\n        _flags: u32, // hehe\n    ) -> fuse_mt::ResultWrite {\n        debug!(\"Writing `{}` bytes to file `{}`...\", data.len(), path.display());\n        let task_handle = NotifyTui::start_task(\n            TaskType::FilesystemWriteFile(\n                path.file_name()\n                .unwrap_or(OsStr::new(\"?\")).display().to_string()\n            ),\n            2\n        );\n\n        // Open the file handle\n        let got_handle = FileHandle::read(fh);\n\n        // Still not sure if we need to check this, but whatever.\n        if got_handle.path != path.into() {\n            // They aren't the same? not sure what to do with that\n            error!(\"readdir() tried to read a path, but provided a handle to a different path.\");\n            error!(\"fh: `{}` | path: `{}`\", got_handle.path.display(), path.display());\n            error!(\"Not sure what to do here, giving up.\");\n            NotifyTui::cancel_task(task_handle);\n            return Err(GENERIC_FAILURE);\n        }\n\n        // Try finding the directory item\n        let file = got_handle.get_directory_item()?;\n        NotifyTui::complete_task_step(&task_handle);\n        \n        // man page:\n        // If count is zero and fd refers to a regular file, then write() may\n        // return a failure status if one of the errors below is detected.\n        // If no errors are detected, or error detection is not performed, 0\n        // is returned without causing any other effect.  If count is zero\n        // and fd refers to a file other than a regular file, the results are\n        // not specified.\n        //\n        // So if we want to write zero bytes, do nothing\n        if data.is_empty() {\n            // uh ok then\n            debug!(\"Caller wanted to write 0 bytes. Skipping write.\");\n            NotifyTui::cancel_task(task_handle);\n            return Ok(0);\n        }\n\n\n\n\n        // Now write to the file!\n        debug!(\"Starting write...\");\n        let bytes_written = file.write_file(&data, offset)?;\n        NotifyTui::complete_task_step(&task_handle);\n        debug!(\"Write completed.\");\n\n        // According to spec, we just tell the caller how many bytes we wrote.\n        // If we didn't write everything, its on them to keep going.\n        // https://man7.org/linux/man-pages/man2/write.2.html\n\n        // Return the number of bytes written.\n        NotifyTui::finish_task(task_handle);\n        Ok(bytes_written)\n    }\n\n    // Flushing does not do anything, since we manually handle our caching.\n    fn flush(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        _path: &std::path::Path,\n        _fh: u64,\n        _lock_owner: u64,\n    ) -> fuse_mt::ResultEmpty {\n\n        // We dont want the OS to be able to flush the cache to disk, this could happen randomly for no reason.\n        // We are responsible for tracking how stale the cache is.\n\n        // The only time we will flush the cache from this level is on shutdown.\n        \n        Ok(())\n    }\n\n    // Releasing a file handle.\n    fn release(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        _path: &std::path::Path,\n        fh: u64,\n        _flags: u32,\n        _lock_owner: u64,\n        _flush: bool,\n    ) -> fuse_mt::ResultEmpty {\n        FileHandle::drop_handle(fh);\n        Ok(())\n    }\n\n    // See flush()\n    fn fsync(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        _path: &std::path::Path,\n        _fh: u64,\n        _datasync: bool,\n    ) -> fuse_mt::ResultEmpty {\n        Ok(())\n    }\n\n    // Open a directory and get a handle to it\n    fn opendir(\n        &self,\n        req: fuse_mt::RequestInfo,\n        path: &std::path::Path,\n        flags: u32,\n    ) -> fuse_mt::ResultOpen {\n\n        // This just gets pushed over to open(), since\n        // we already handle directories over there.\n        //\n        // Should we handle files and directories both in open? maybe not.\n        self.open(req, path, flags)\n    }\n\n    // List the contents of a directory.\n    // \"Return one or more directory entries (struct dirent) to the caller.\"\n    // \"This is one of the most complex FUSE functions.\" Oof.\n    // \"The readdir function is somewhat like read, in that it starts at a\n    //  given offset and returns results in a caller-supplied buffer.\"\n    // \"However, the offset not a byte offset\" What the hell\n    // \"...and the results are a series of struct dirents rather than being uninterpreted bytes\" those are just words Geoffery\n    //\n    // Luckily we are working at a level way above that!\n    fn readdir(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        path: &std::path::Path,\n        fh: u64,\n    ) -> fuse_mt::ResultReaddir {\n        debug!(\"Getting contents of directory `{}`...\", path.display());\n\n        let task_handle = NotifyTui::start_task(TaskType::FilesystemReadDirectory(\n            path.file_name().unwrap_or(OsStr::new(\"?\")).display().to_string()\n            ), \n            4\n        );\n\n        // Make sure the file handle and the incoming path are the same. I assume they should be, but\n        // cant hurt to check.\n        let got_handle = FileHandle::read(fh);\n        \n        if got_handle.path != path.into() {\n            // They aren't the same? not sure what to do with that\n            error!(\"readdir() tried to read a path, but provided a handle to a different path.\");\n            error!(\"fh: `{}` | path: `{}`\", got_handle.path.display(), path.display());\n            error!(\"Not sure what to do here, giving up.\");\n            NotifyTui::cancel_task(task_handle);\n            return Err(GENERIC_FAILURE);\n        }\n        \n        // Since we have a handle, getting the directory is easy.\n        debug!(\"Getting the directory item from handle...\");\n        let dir_item: DirectoryItem = if let Ok(exists) = got_handle.get_directory_item() {\n            // good\n            exists\n        } else {\n            // Tried to read in a directory item that did not exist, yet we have a handle to it?\n            // Guess the handle must be stale?\n            \n            // Yes, get_directory_item() returns its own error, but we should get rid of the invalid handle.\n\n            warn!(\"Tried to read in a directory item from a handle, but the item was not there. Returning stale.\");\n            NotifyTui::cancel_task(task_handle);\n            return Err(STALE_HANDLE)\n        };\n\n        NotifyTui::complete_task_step(&task_handle);\n        \n        // Double check that this is a file.\n        if !dir_item.flags.contains(DirectoryItemFlags::IsDirectory) {\n            // No.\n            warn!(\"Tried to call readdir on a file!\");\n            NotifyTui::cancel_task(task_handle);\n            return Err(NOT_A_DIRECTORY);\n        }\n        \n        debug!(\"Getting directory block...\");\n        let dir_block = dir_item.get_directory_block()?;\n        NotifyTui::complete_task_step(&task_handle);\n        \n        // List the files off\n        debug!(\"Listing items...\");\n        let items = dir_block.list()?;\n        NotifyTui::complete_task_step(&task_handle);\n        \n        // Now pull out the names and types\n        let mut listed_items: Vec<DirectoryEntry> = items.iter().map(|item| {\n            let kind = if item.flags.contains(DirectoryItemFlags::IsDirectory) {\n                FileType::Directory\n            } else {\n                FileType::RegularFile\n            };\n            \n            DirectoryEntry {\n                name: item.name.clone().into(),\n                kind,\n            }\n        }).collect();\n        NotifyTui::complete_task_step(&task_handle);\n        \n        // Now add the unix `.` item.\n        listed_items.push(\n            DirectoryEntry {\n                name: std::ffi::OsStr::new(\".\").into(),\n                kind: FileType::Directory,\n            }\n        );\n\n        NotifyTui::finish_task(task_handle);\n        \n        // All done!\n        debug!(\"Done. Directory contained `{}` items.\", listed_items.len());\n        Ok(listed_items)\n    }\n\n    // See release()\n    fn releasedir(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        _path: &std::path::Path,\n        fh: u64,\n        _flags: u32,\n    ) -> fuse_mt::ResultEmpty {\n        FileHandle::drop_handle(fh);\n        Ok(())\n    }\n\n    // See flush()\n    fn fsyncdir(\n        &self,\n        _req: fuse_mt::RequestInfo,\n        _path: &std::path::Path,\n        _fh: u64,\n        _datasync: bool,\n    ) -> fuse_mt::ResultEmpty {\n        Ok(())\n    }\n\n    // Get file system statistics.\n    // Seemingly contains information about the file system, like optimal block size and max file name length\n    fn statfs(&self, _req: fuse_mt::RequestInfo, _path: &std::path::Path) -> fuse_mt::ResultStatfs {\n        // This does not appear to be required, but we can implement it anyways.\n\n        // Get the pool header, we need it for disk counts and such.\n        // If this doesnt work, just tell the caller to try again later.\n\n        let pool = if let Ok(pool_inner) = GLOBAL_POOL.get().expect(\"Global pool should be created at this stage!\").try_lock() {\n            pool_inner.header\n        } else {\n            // Lock failed.\n            debug!(\"Tried to get stats about the pool, but locking the pool failed. Skipping...\");\n            return Err(TRY_AGAIN)\n        };\n\n        // Number of blocks on the device is just the highest disk*2880\n        let blocks: u64 = pool.highest_known_disk as u64 * 2880;\n\n        // We know how many blocks are free.\n        // Not sure how Linux reacts to disks that grow on the fly, but\n        // it seems like a likely enough use-case that this should be fine...\n        let bfree: u64 = pool.pool_standard_blocks_free.into();\n\n        // The number of available blocks is the same as the number of free blocks.\n        let bavail = bfree;\n\n        // Knowing how many files exist is a process that would take a VERY long time to figure out\n        // typically. This seems like a bad idea to actually look up on the fly.\n        // Just say there's a few.\n        let files: u64 = 985;\n\n        // We don't have a limit to file inodes, so we always have a lot free.\n        let ffree: u64 = 12345;\n\n        // Blocks are 512 bytes, technically, but innards of those blocks are limited...\n        // Not that huge of a deal for transfers to be aligned, so we'll just say 512 still.\n        let bsize: u32 = 512;\n\n        // Max filename length is 255 characters.\n        let namelen: u32 = 255;\n\n        // Fragments are the min size that can be allocated to a file, which in our case is roughly a block.\n        let frsize: u32 = 512;\n\n        let stat: Statfs = Statfs {\n            blocks,\n            bfree,\n            bavail,\n            files,\n            ffree,\n            bsize,\n            namelen,\n            frsize,\n        };\n\n        Ok(stat)\n    }\n\n    // Extended attributes are not supported.\n    // fn setxattr(\n    //     &self,\n    //     _req: fuse_mt::RequestInfo,\n    //     _path: &std::path::Path,\n    //     _name: &std::ffi::OsStr,\n    //     _value: &[u8],\n    //     _flags: u32,\n    //     _position: u32,\n    // ) -> fuse_mt::ResultEmpty {\n    //     Err(libc::ENOSYS)\n    // }\n\n    // Extended attributes are not supported.\n    // fn getxattr(\n    //     &self,\n    //     _req: fuse_mt::RequestInfo,\n    //     _path: &std::path::Path,\n    //     _name: &std::ffi::OsStr,\n    //     _size: u32,\n    // ) -> fuse_mt::ResultXattr {\n    //     Err(libc::ENOSYS)\n    // }\n\n    // Extended attributes are not supported.\n    // fn listxattr(\n    //     &self,\n    //     _req: fuse_mt::RequestInfo,\n    //     _path: &std::path::Path,\n    //     _size: u32,\n    // ) -> fuse_mt::ResultXattr {\n    //     Err(libc::ENOSYS)\n    // }\n\n    // Extended attributes are not supported.\n    // fn removexattr(\n    //     &self,\n    //     _req: fuse_mt::RequestInfo,\n    //     _path: &std::path::Path,\n    //     _name: &std::ffi::OsStr,\n    // ) -> fuse_mt::ResultEmpty {\n    //     Err(libc::ENOSYS)\n    // }\n\n    // \"This call is not required but is highly recommended.\" Okay then we wont do it muhahaha\n    // fn access(\n    //     &self,\n    //     _req: fuse_mt::RequestInfo,\n    //     _path: &std::path::Path,\n    //     _mask: u32,\n    // ) -> fuse_mt::ResultEmpty {\n    //     Err(libc::ENOSYS)\n    // }\n\n    // Creates and opens a new file, returns a file handle.\n    fn create(\n        &self,\n        req: fuse_mt::RequestInfo,\n        parent: &std::path::Path,\n        name: &std::ffi::OsStr,\n        _mode: u32,\n        flags: u32,\n    ) -> fuse_mt::ResultCreate {\n        debug!(\"Creating new file named `{}` in `{}`...\", name.display(), parent.display());\n\n        let task_handle = NotifyTui::start_task(TaskType::FilesystemCreateFile(name.display().to_string()), 5);\n\n        // Extract the flags\n        // Will bail if needed.\n        let deduced_flags: ItemFlag = ItemFlag::deduce_flag(flags)?;\n\n        // Is the name too long?\n        if name.len() > 255 {\n            debug!(\"File name is too long. Bailing.\");\n            NotifyTui::cancel_task(task_handle);\n            return Err(FILE_NAME_TOO_LONG)\n        }\n        \n        // Try and load in the parent directory\n        // This will bail if a low level floppy issue happens.\n        debug!(\"Attempting to load in the parent directory...\");\n        let mut containing_dir_block: DirectoryBlock = match DirectoryBlock::try_find_directory(Some(parent))? {\n            Some(ok) => ok,\n            None => {\n                // Nope, no parent.\n                warn!(\"Cannot create files in directories that do not exist.\");\n                NotifyTui::cancel_task(task_handle);\n                return Err(NO_SUCH_ITEM)\n            },\n        };\n        debug!(\"Directory loaded.\");\n        NotifyTui::complete_task_step(&task_handle);\n        \n        // Make sure the file does not already exist.\n        debug!(\"Checking if file already exists...\");\n        let converted_name: String = name.to_str().expect(\"Should be valid UTF8.\").to_string();\n        // Will bail if needed.\n        if let Some(exists) = containing_dir_block.find_item(&NamedItem::File(converted_name.clone()))? {\n            NotifyTui::complete_task_step(&task_handle);\n            debug!(\"File already exists.\");\n            // But do we care?\n            if deduced_flags.contains(ItemFlag::CREATE_EXCLUSIVE) {\n                // Yes we do, this is a failure.\n                debug!(\"Caller wanted to create this file, not open it. Bailing.\");\n                NotifyTui::cancel_task(task_handle);\n                return Err(ITEM_ALREADY_EXISTS)\n            }\n            \n            // Since the file already exists we can skip the creation process.\n            // just load it in as usual.\n            \n            // Full item path\n            let constructed_path: &Path = &parent.join(name);\n            \n            // Dont care about the returned flags, they wont change anyways.\n            let (file_handle, _): (u64, u32) = self.open(req, constructed_path, flags)?;\n            NotifyTui::complete_task_step(&task_handle);\n            \n            // Get the innards of the handle\n            let handle_inner: FileHandle = FileHandle::read(file_handle);\n            NotifyTui::complete_task_step(&task_handle);\n\n            // Truncate if needed (open(2) syscall)\n            // Must be a file\n            if deduced_flags.contains(ItemFlag::TRUNCATE) && !exists.flags.contains(DirectoryItemFlags::IsDirectory) {\n                self.truncate(req, constructed_path, Some(file_handle), 0)?; // Truncate to 0\n            }\n            \n            // Get the metadata from that\n            debug!(\"Getting file attributes...\");\n            let facebook_data: FileAttr = handle_inner.try_into()?;\n            NotifyTui::complete_task_step(&task_handle);\n            \n            // Put it all together\n            // No idea what the TTL should be set to. I'm assuming that's how long the handles last?\n            // I will never drop handles on my side, the OS has to drop em.\n            debug!(\"Done reading in file, returning.\");\n            NotifyTui::finish_task(task_handle);\n            return Ok(CreatedEntry {\n                ttl: HANDLE_TIME_TO_LIVE,\n                attr: facebook_data,\n                fh: file_handle,\n                flags,  // We use the same flags we came in with. Not the one from the loaded file.\n                        // Is that a bad idea? No idea. Seems to work just fine.\n            })\n        }\n\n        NotifyTui::complete_task_step(&task_handle);\n        \n        // File did not exist, actually creating it...\n        debug!(\"Creating file...\");\n        let resulting_item: DirectoryItem = containing_dir_block.new_file(converted_name)?;\n        NotifyTui::complete_task_step(&task_handle);\n        debug!(\"Created file.\");\n\n        // Full item path\n        let constructed_path: &Path = &parent.join(name);\n\n        // Construct and return the handle to the new file\n        let new_handle: FileHandle = FileHandle {\n            path: constructed_path.into(),\n        };\n\n        // We can get attributes directly from the directory item we just made\n        let attributes: FileAttr = resulting_item.try_into()?;\n        NotifyTui::complete_task_step(&task_handle);\n\n        // Allocate the handle for it\n        let handle_num: u64 = new_handle.allocate();\n        NotifyTui::complete_task_step(&task_handle);\n\n        // Assemble it, and we're done!\n        NotifyTui::finish_task(task_handle);\n        debug!(\"Done creating file.\");\n        Ok(CreatedEntry {\n            ttl: HANDLE_TIME_TO_LIVE,\n            attr: attributes,\n            fh: handle_num,\n            flags,\n        })\n    }\n}\n"
  },
  {
    "path": "src/filesystem/internal_filesystem_methods.rs",
    "content": "// For stuff like initialization and options.\n\n//\n//\n// ======\n// Imports\n// ======\n//\n//\n\nuse std::path::PathBuf;\n\nuse log::debug;\n\nuse crate::filesystem::filesystem_struct::USE_TUI;\nuse crate::filesystem::filesystem_struct::WRITE_BACKUPS;\nuse crate::pool::pool_actions::pool_struct::Pool;\nuse crate::filesystem::filesystem_struct::FilesystemOptions;\nuse crate::filesystem::filesystem_struct::FlusterFS;\nuse crate::filesystem::filesystem_struct::FLOPPY_PATH;\nuse crate::filesystem::filesystem_struct::USE_VIRTUAL_DISKS;\n\n\n//\n//\n// ======\n// Implementations\n// ======\n//\n//\n\n\n// Filesystem option setup. Does not start filesystem.\nimpl FilesystemOptions {\n    /// Initializes options for the filesystem, also configures the virtual disks if needed.\n    pub fn new(use_virtual_disks: Option<PathBuf>, floppy_drive: PathBuf, backup: Option<bool>, enable_tui: bool) -> Self {\n        debug!(\"Configuring file system options...\");\n        // Set the globals\n        // set the floppy disk path\n        debug!(\"Setting the floppy path...\");\n        debug!(\"Locking FLOPPY_PATH...\");\n        // There's no way anyone else has a lock on this or its poisoned at this point.\n        *FLOPPY_PATH\n            .try_lock()\n            .expect(\"Fluster! Is single threaded.\") = floppy_drive.clone();\n        debug!(\"Done.\");\n\n        // Set the virtual disk flag if needed\n        if let Some(path) = use_virtual_disks.clone() {\n            debug!(\"Setting up virtual disks...\");\n            // Sanity checks\n            // Make sure this is a directory, and that the directory already exists\n            if !path.is_dir() || !path.exists() {\n                // Why must you do this\n                panic!(\"Virtual disk argument must be a valid path to a pre-existing directory.\");\n            }\n\n            debug!(\"Locking USE_VIRTUAL_DISKS...\");\n            *USE_VIRTUAL_DISKS\n                .try_lock()\n                .expect(\"Fluster! Is single threaded.\") = Some(path.to_path_buf());\n            debug!(\"Done.\");\n        };\n\n        // Disable backups if needed.\n        // Backups default to being enabled.\n        let enable_backup = backup.unwrap_or(true);\n        debug!(\"Setting WRITE_BACKUPS...\");\n        WRITE_BACKUPS.set(enable_backup).expect(\"This should only ever be called once.\");\n        debug!(\"Done.\");\n\n        // Disable tui\n        // TUI is enabled by default\n        debug!(\"Setting USE_TUI...\");\n        USE_TUI.set(enable_tui).expect(\"This should only ever be called once.\");\n        debug!(\"Done.\");\n\n\n        debug!(\"Done configuring.\");\n        Self {\n            use_virtual_disks,\n            floppy_drive,\n            enable_backup,\n            enable_tui,\n        }\n    }\n}\n\n// Starting the filesystem.\nimpl FlusterFS {\n    /// Create new filesystem handle, this will kick off the whole process of loading in information about the pool.\n    /// Takes in options to configure the new pool.\n    pub fn start(options: &FilesystemOptions) -> Self {\n        debug!(\"Starting file system...\");\n        // Right now we dont use the options for anything, but they do initialize the globals we need, so we still need to pass it in.\n        #[allow(dead_code)]\n        #[allow(unused_variables)]\n        let unused = options;\n        let fs = FlusterFS { pool: Pool::load() };\n        debug!(\"Done starting filesystem.\");\n        fs\n    }\n}"
  },
  {
    "path": "src/filesystem/item_flag/flag_struct.rs",
    "content": "use bitflags::bitflags;\nuse libc::c_int;\nuse log::warn;\n\n//\n//\n// ======\n// Flag type\n// ======\n//\n//\n\n// Flags are handled with bare u32 integers,\n// hence we have a bitflag type to make dealing with them easier.\n\n// Open documentation:\n// https://man7.org/linux/man-pages/man2/openat.2.html\n// The flags are in libc::\n\n// When it says \"Has no effect\", I mean on the fluster side. Fluster just does not care\n// about this flag being set or unset.\n\n// I'm pretty sure that the read/write flags do not overlap. If they do I will split this into multiple types.\n\n// All of the c flags are i32 for reasons unknown to me, so we have to cast all of them lol\n// Not sure why the fuse_mt crate uses u32...\n\nbitflags! {\n    /// Flags that items have.\n    #[derive(Debug, PartialEq, Eq, Clone, Copy)]\n    pub(crate) struct ItemFlag: u32 {\n        /// The file is opened in append mode.\n        /// Before each write, the file offset is positioned at the end of the file.\n        /// The modification of the file offset and write is done as one atomic step.\n        const APPEND = libc::O_APPEND as u32;\n        \n        /// Async, fluster does not support this. Thus we will not\n        /// add this bit to the flags.\n        /// \n        /// Has no effect.\n        const O_ASYNC = libc::O_ASYNC as u32;\n\n        /// Has to do with closing when executing, ignoring, good luck.\n        /// \n        /// Has no effect\n        const O_CLOEXEC = libc::O_CLOEXEC as u32;\n\n        /// If the path does not exist, create it as a regular file.\n        const CREATE = libc::O_CREAT as u32;\n\n        /// Has to do with direct IO. We don't really care, since we have no special\n        /// handling for this kinda thing.\n        /// \n        /// Has no effect.\n        const O_DIRECT = libc::O_DIRECT as u32;\n\n        /// Fail the open if the path is not a directory.\n        const ASSERT_DIRECTORY = libc::O_DIRECTORY as u32;\n\n        /// Has to do with data syncing. We do not care.\n        /// \n        /// Has no effect\n        const O_DSYNC = libc::O_DSYNC as u32;\n\n        /// Ensure that call creates the file. if this is set and O_CREAT is also set, we're\n        /// supposed to turn a EEXIST on open if path already exists.\n        /// \n        /// O_EXCL is undefined if used without O_CREAT (unless pointing at block devices which fluster is not.)\n        const CREATE_EXCLUSIVE = libc::O_EXCL as u32;\n        \n        /// Deals with filesizes with offsets that can be greated than off_t (I think that's 32 bit)\n        /// \n        /// If you need files that big, fluster is not the tool for you.\n        /// Has no effect.\n        const O_LARGEFILE = libc::O_LARGEFILE as u32;\n        \n        /// Do not update file access time.\n        /// \n        /// Cool, we don't support that anyways.\n        /// \n        /// Has no effect.\n        const O_NOATIME = libc::O_NOATIME as u32;\n\n        /// If path is a terminal device, do not control it or whatever.\n        /// \n        /// Fluster! does not have terminal devices.\n        /// Has no effect.\n        const O_NOCTTY = libc::O_NOCTTY as u32;\n\n        /// Symbolic link related. We do not support links.\n        // const O_NOFOLLOW = libc::O_NOFOLLOW;\n        \n        /// Open in non-blocking mode.\n        /// Fluster is single threaded. EVERYTHING blocks dawg.\n        /// \n        /// Has no effect.\n        const O_NONBLOCK = libc::O_NONBLOCK as u32;\n        const O_NDELAY = libc::O_NDELAY as u32; // Alternate name for same flag\n\n        /// Dont follow symlinks.\n        /// \n        /// Fluster does not have symlinks.\n        /// Has no effect.\n        const O_NOFOLLOW = libc::O_NOFOLLOW as u32; // Alternate name for same flag\n        \n        /// Gets file descriptor for this path but not the actual file.\n        /// \n        /// Guess what buddy? you'll just get the whole file regardless.\n        /// \n        /// Has no effect.\n        const O_PATH = libc::O_PATH as u32;\n        \n        /// Do synchronized file I/O.\n        /// \n        /// This is supposed to force sync to disk, but we are silly and don't care :)\n        /// \n        /// Has no effect.\n        const O_SYNC = libc::O_SYNC as u32;\n        \n        /// Creates unnamed tempfiles.\n        /// \n        /// We do not support this.\n        /// Has no effect.\n        const O_TMPFILE  = libc::O_TMPFILE as u32;\n        \n        /// If the file already exists, truncate it.\n        /// \n        /// There is already a truncate method on the filesystem, but this may get called elsewhere\n        /// so we still need to care elsewhere.\n        const TRUNCATE = libc::O_TRUNC as u32;\n\n\n    }\n}\n\n/// Convert a flag to a u32 for use in returning.\nimpl From<ItemFlag> for u32 {\n    fn from(value: ItemFlag) -> Self {\n        value.bits()\n    }\n}\n\n/// Tried to convert a u32 into a valid flag, returns an `Unsupported` error if a non-existent flag is set.\nimpl ItemFlag {\n    pub fn deduce_flag(value: u32) -> Result<Self, c_int> {\n        // All bits must be used. We need to know what they all are.\n        if let Some(valid) = ItemFlag::from_bits(value) {\n            // All good.\n            Ok(valid)\n        } else {\n            // Has invalid bits set.\n            // We will print some information to deduce the unused bits.\n            warn!(\"Incoming flag bits had unused bits set. This operation is unimplemented.\");\n            warn!(\"Listing known and unknown flags:\");\n\n            warn!(\"Known:\");\n            let known = ItemFlag::from_bits_truncate(value);\n            for name in known.iter_names() {\n                warn!(\"`{}` with value `{}` (binary: `{:0>32b}`)\", name.0, name.1.bits(), name.1.bits())\n            }\n\n            warn!(\"Unknown:\");\n            let unknown_bits = value & !ItemFlag::all().bits();\n            warn!(\"{unknown_bits:0>32b}\");\n            // now print out the values of those bits\n            warn!(\"Values for those bits:\");\n            for i in 0..32 {\n                // shift over to mask out the bit\n                let mask: u32 = 1 << i as u32;\n                // if the bit is set, print the value\n                if mask & unknown_bits != 0 {\n                    warn!(\"{mask} (hex {mask:X})\")\n                }\n            }\n\n            // I've spent several hours trying to track down what 0x8000 is supposed to be a flag for, no luck.\n            // I'm just going to assume its some internal flag at this point. We will ignore all flag bits that we dont know.\n\n            warn!(\"Continuing anyways, but ignoring those bits may have side effects.\");\n            Ok(known)\n        }\n    }\n}"
  },
  {
    "path": "src/filesystem/item_flag/mod.rs",
    "content": "pub(super) mod flag_struct;"
  },
  {
    "path": "src/filesystem/mod.rs",
    "content": "mod fuse_filesystem_methods;\nmod internal_filesystem_methods;\npub mod filesystem_struct;\nmod file_handle;\nmod item_flag;\nmod file_attributes;\npub mod disk_backup;"
  },
  {
    "path": "src/filesystem_design/allocation_spec.md",
    "content": "# Find and allocate blocks\n\nWe take in a number (u16) of blocks that the caller wishes to reserve.\nWe return a `Result<Vec<DiskPointer>, AllocationError>`\n\n`AllocationError` contains types for block situations such as `NotEnoughSpace`, which requires the caller to add more disks to the pool.\n\n### Note:\nThis operation does not flag blocks as used, this section is read only.\nThe caller is responsible for updating the allocation tables in the headers of disks they write to.\n\nFinding the next block follows these steps:\n\n## Scanning:\nBefore we can write any data, we need to ensure we have all the room for it.\nThis is the discovery phase, no data is written.\n\nTerminology:\n- `Start Disk`\n- - The disk where the allocation of blocks begins. (Allocations of blocks always go upwards away from the pool disk).\n- `Allocation length`\n- - The number of blocks we wish to allocate.\n- `Note`\n- - Copy this value into memory off of the disk for later use.\n\n### Process:\n- Insert the pool disk, `Note`: `highest_known_disk`, `pool_blocks_free`, and `disk_with_next_free_block`.\n- - If `disk_with_next_free_block` == u16::MAX:\n- - - There are no more free blocks. We need another disk. Return `NotEnoughSpace`\n- - If `pool_blocks_free` < `Allocation length`:\n- - - There aren't enough free blocks. We need another disk. Return `NotEnoughSpace`\n- Insert `pool_blocks_free`, hereafter referred to as the `Start disk`\n- Goto `Find Blocks`\n- If not enough space is found:\n- - There aren't enough free blocks in the entire pool.\n- - This should have been caught by `pool_blocks_free`.\n- - An assertion will go here. We should never hit this branch.\n- Update `pool_blocks_free` with how many blocks were allocated\n- Update `disk_with_next_free_block`:\n- - `Note`: Disk and Block numbers of the final allocated block\n- - If the block number is the final block on the disk:\n- - - Set `disk_with_next_free_block` to u16::MAX.\n- - - Otherwise, set `disk_with_next_free_block` to the Disk of the final allocated block.\n\n\n\n## Find Blocks\nIncoming arguments:\n- `Start Disk`\n- - Disk number to start our search from.\n- `Allocation length`\n- - The number of blocks we wish to allocate.\n- `End Disk`\n- - The disk number of the final disk in the pool.\n\nReturns:\n- `Vec<DiskPointer>`\n\nTerminology:\n- Variable `Index`\n- - The current disk we are examining. (Lies within range `Start Disk`..=`End Disk`)\n- - Starts at `Start Disk`\n- Variable `Free blocks seen`\n- - Keeps track of how many free blocks we have seen across all disks up to this point.\n- Variable `Block pointers`\n- - A Vec of `<DiskPointer>`s to each free block we are considering.\n- Variable `Blocks remaining`\n- - A count of how many more blocks we need to allocate\n\n### Note:\nThis section does not flag blocks as used, this section is read only.\n\n### Process:\n- Insert disk `Index`\n- Count number of blocks free in allocation table.\n- If the number of blocks free is >= `Blocks remaining`\n- - Copy as many disk pointers into `Block pointers` as there are `Blocks remaining`, and return the pointers.\n- Copy pointers to all of the free blocks into `Block pointers`, decrement `Blocks remaining` accordingly.\n- If `Index` >= `End Disk`\n- - There were not enough free blocks.\n- - This should not be possible. Caller must guarantee that there is enough free space.\n- - Assertion goes here.\n- Increment `Index`\n- Loop"
  },
  {
    "path": "src/filesystem_design/dense_disk.md",
    "content": "# Dense disk\n\nSometimes, you've got a really big file. And I mean REALLY big.\n\nIf a file is over the size of a full floppy, find out how many full floppies it can span and reserve those\nthe rest of the file will go in data blocks as usual\n\n# Disk header format\n\nThe disk header lives on block 0 of every disk.\n\nHeader format:\n\n| offset | length | Field                                                 |\n| ------ | ------ | ----------------------------------------------------- |\n| 0      | 8      | Magic number for identifying a fluster drive.Fluster! |\n| 8      | 1      | Bitflags                                              |\n| 9      | 2      | Disk number                                           |\n| -      | -      | Reserved                                              |\n| 148    | 360    | Block usage bitplane                                  |\n| 509    | 4      | CRC                                                   |\n\nBitflags:\n\n| bit | flag                                           |\n| --- | ---------------------------------------------- |\n| 0   | Reserved                                       |\n| 1   | Reserved                                       |\n| 2   | Reserved                                       |\n| 3   | Reserved                                       |\n| 4   | Reserved                                       |\n| 5   | Reserved                                       |\n| 7   | Reserved for Standard disks. Must never be set.|\n| 6   | Marker bit, Must always be set.                |\n| 8   | Reserved for Pool headers. Must never be set.  |"
  },
  {
    "path": "src/filesystem_design/design_choices.md",
    "content": "# Reasons for certain things\n\n# Why 4 byte CRCs?\nAfter 20MB of read-write with random head seeking, I only got 1 failed byte.\nA 4 byte crc on our 512 byte block gives us a hamming distance of 6, which is probably even overkill unless\nthe floppy drive is actively being shaken by a pit bull who mistook it for a toddler.\n\n# Why little endian?\nStack exchange said it was cool.\n\n# What order are the bitflags in the documentation?\nflag 0 is the least significant bit\n\n# Why are some reads bigger than they need to be?\nI was having an issue reading just 8 bytes into a buffer.\nTurns out Windows wont let you read directly from a floppy disk into a buffer smaller than 512 bytes.\nThis took an annoyingly long time to figure out.\n\n# Why do file extent blocks have a `bytes_free` field even though they arent dynamically allocated?\nEase of use.\n\n# A lot of stuff seems wasteful cpu wise...\nThink of it this way, 99% of the time we will be waiting for data from disk, so it evens out!\n\n# Why is an entire disk dedicated to information about the pool?\n\nChances are, if you are using this filesystem, you are storing many files across many floppies.\n\nFinding a file is a slow and tedious process. We have to start from the first disk and search, possibly swapping between many disks before finding the file we are seeking. Most of this overhead comes from looking up the location of the file inode, not loading the file itself.\n\nDedicating an entire disk to pool information lets us keep a cache of file locations, skipping the entire search process.\nThis will result in fewer disk swaps, and a massive speedup in search time.\n\n# Why is the project laid out like that?\nOriginally, I didn't want to accidentally give access to private functions used for subsystems, but I ended up repeatedly dividing everything up until I was left with Pool::Disk::(Some disk type) then each disk implements its own innards, or uses generic functions from Pool::Disk.\n\nOrganizationally, I feel like it makes sense, but the amount of nesting is pretty wild.\nThis is my first time trying to keep a project organized in a sensible way, so... lol.\n\n# Why are there so many comments?\nI've had too many hobby projects in the pass where I've thought to myself, \"The code is self documenting\". Sure, that might be the case, but the amount of mental effort it takes to understand what's going on 3 days after I wrote something is a LOT higher than if I just left some comments.\n\nI'd prefer too many comments over a headache trying to reverse-engineer what I was thinking previously.\n\nAlso it lowers the bar of entry for the casual viewer :D\n\n# Why do some of the higher level abstractions use so many generic traits?\nWhile writing this project, I learned more and more about traits, so I started using them because they're cool!"
  },
  {
    "path": "src/filesystem_design/disk_header.md",
    "content": "# Block layout\n\n512 bytes in size\nA floppy disk can hold 2880 blocks of this size.\n\n# Header update situations:\n\nNew disk is created:\n\n- Highest known disk has to be updated on the root disk (disk 0)\n\n# Disk header format\n\nThe disk header lives on block 0 of every disk.\n\nHeader format:\n\n| offset | length | Field                                                 |\n| ------ | ------ | ----------------------------------------------------- |\n| 0      | 8      | Magic number for identifying a fluster drive.Fluster! |\n| 8      | 1      | Bitflags                                              |\n| 9      | 2      | Disk number (u16)                                     |\n| -      | -      | Reserved                                              |\n| 148    | 360    | Block usage bitplane                                  |\n| 509    | 4      | CRC                                                   |\n\nBitflags:\n\n| bit | flag                                          |\n| --- | --------------------------------------------- |\n| 0   | Reserved                                      |\n| 1   | Reserved                                      |\n| 2   | Reserved                                      |\n| 3   | Reserved                                      |\n| 4   | Reserved                                      |\n| 5   | Reserved                                      |\n| 6   | Marker bit, Must always be set.               |\n| 7   | Reserved for Dense disks.  Must never be set. |\n| 8   | Reserved for Pool headers. Must never be set. |\n\n8 bytes:\n1 byte: bitflags\n2 bytes: Disk number\n138 bytes: Reserved\n360 bytes: Block usage bitplane\n\nFinal 4 byte: crc\n"
  },
  {
    "path": "src/filesystem_design/disk_layout.md",
    "content": "# Disk Layout\nBlock 0: Disk header\n// Only required on the origin disk\nBlock 1: Inode block\nBlock 2: Directory block\n\nUnless its a dense disk,\ndense disks only have the header.\n\nRemaining blocks: any inode, directory, or data.\n\n\n# Block types\nHeader (See `disk_header`)\nInode\nDirectory Data\nFile Extents\nData\n\n# Data block\n1 byte: bitflags\n    0: Reserved for future use\n    1: Reserved for future use\n    2: Reserved for future use\n    3: Reserved for future use\n    4: Reserved for future use\n    5: Reserved for future use\n    6: Reserved for future use\n    7: Reserved for future use\n\nremaining bytes: raw data\n\nfinal 4 bytes: CRC\n\n# Directory block\n\nItems on the directory block don't need to be in any\nspecific order, we do not index directly into these\nblocks.\n\n1 byte: bitflags\n    0: This is the last directory block on the disk.\n    1: Reserved for future use\n    2: Reserved for future use\n    3: Reserved for future use\n    4: Reserved for future use\n    5: Reserved for future use\n    6: Reserved for future use\n    7: Reserved for future use\n2 bytes: number of free bytes\n4 bytes: next directory block (disk pointer, we have no idea where the next directory could be.)\n    - If u16:MAX then this is the end of the directory chain\n\nremaining bytes: directory data\n\nfinal 4 bytes: CRC\n\n# File Extents block\n1 byte: bitflags\n    0: Reserved for future use\n    1: Reserved for future use\n    2: Reserved for future use\n    3: Reserved for future use\n    4: Reserved for future use\n    5: Reserved for future use\n    6: Reserved for future use\n    7: Reserved for future use\n2 bytes: number of free bytes\n4 bytes: Next block\n    - 2 Bytes: Disk number\n    - 2 Bytes: Block on disk\n    - if all 4 bytes are full 1's, this is the final block\n\nremaining bytes: extent data\n\nfinal 4 bytes: CRC\n\n# Inode block\n1 byte: bitflags\n    0: This is the last inode block on the disk.\n    1: Reserved for future use\n    2: Reserved for future use\n    3: Reserved for future use\n    4: Reserved for future use\n    5: Reserved for future use\n    6: Reserved for future use\n    7: Reserved for future use\n2 bytes: number of free bytes\n4 bytes: next directory block (disk pointer, we have no idea where the next directory could be.)\n    - If u16:MAX then this is the end of the directory chain\n\nremaining bytes: inode data\n\nfinal 4 bytes: CRC\n\nIf you are on the final inode disk and realize you need to make another inode block, you update the\nbitflag and reserve another block.\nIf you are out of blocks on that disk, go to the next disk if bit 1 is set.\nIf bit 1 is not set, then you can simply go to the next disk indicated. Otherwise you must find a _NEW_ disk\nto put the next inode block on and update flags accordingly. (New disk inodes must be in the default position)\n\nOn disk 0, the first inode in the block MUST be a directory referencing `/` aka the root."
  },
  {
    "path": "src/filesystem_design/inode_format.md",
    "content": "# Example Traversal\n\nLets find `/foo/bar.txt`\n\n### Start at Root Inode\nThe location of the root inode is fixed (Disk 0, Block 1, Slot 0).\nRead the Inode at this address. It's a Directory type.\n\n### Find Root's Directory Data\nFrom the root Inode, get the pointer to its Directory block.\n\n### Scan Root's Directory Data for `foo`\nRead that DirectoryDataBlock.\nSearch its children map for the key `foo`.\nIf found, you get the InodeAddress for `foo`.\nIf not found and there's a next_block pointer, follow the chain and repeat the search.\n\n### Find `foo`'s Directory Data\nRead the Inode at `foo`'s address. It's also a Directory type.\nFrom this Inode, get the pointer to its Directory block.\n\n### Scan `foo`'s Directory Data for `bar.txt`\nRead the DirectoryDataBlock for foo.\nSearch its children map for the key `bar.txt`.\nIf found, you get the InodeAddress for `bar.txt`.\n\n### Find `bar.txt`'s Extents\nRead the Inode at `bar.txt`'s address. It's a File type.\nFrom this Inode, get the pointer to its first_extent_block.\n\n### Read File Data\nRead that FileExtentBlock to get the list of FileExtents,\nwhich finally tell you which blocks on which disks hold the actual file data.\nExtents in this file are in order.\n\n\n# Inode format\n1 byte: bitflags\n    - 0: File type (0 directory, 1 file)\n    - 1: Reserved for future use\n    - 2: Reserved for future use\n    - 3: Reserved for future use\n    - 4: Reserved for future use\n    - 5: Reserved for future use\n    - 6: Reserved for future use\n    - 7: Marker bit (Always set)\n4-12 bytes: Inode data\n    * File:\n        - 8 bytes for size\n        - 4 bytes for pointer to the File Extents block\n            - 2 Bytes: Disk number\n            - 2 Bytes: Block on disk\n    * Directory:\n        - 4 bytes for pointer to Directory Data block\n            - 2 Bytes: Disk number\n            - 2 Bytes: Block on disk\n12 bytes: Created timestamp\n    - 8 bytes: Seconds since epoch\n    - 4 bytes: nanosecond offset\n12 bytes: Modified timestamp\n    - 8 bytes: Seconds since epoch\n    - 4 bytes: nanosecond offset\n\n\n# Inode block\nsee `disk_layout`\n\n# Directory block\nsee `disk_layout`\n\n\n# Directory item format\n1 byte: bitflags\n    0: Inode is on this disk\n    1: Reserved for future use\n    2: Reserved for future use\n    3: Reserved for future use\n    4: Reserved for future use\n    5: Reserved for future use\n    6: Reserved for future use\n    7: Marker bit (Always set)\n1 byte: length of item name\n? bytes: item name\n3-5 bytes: inode location\n    - 2 Bytes: Disk number (Not included if flag set)\n    - 2 Bytes: Block on disk\n    - 1 Byte: Index into inode block\n\n\n\n# File Extents block\nsee `disk_layout`\n\n# Extent format\nbitflag considerations:\n- You cannot have a local dense-disk.\n\n1 byte: bitflags\n    0: This extent is a dense-disk\n    1: The block is on this disk\n    2: Reserved for future use\n    3: Reserved for future use\n    4: Reserved for future use\n    5: Reserved for future use\n    6: Reserved for future use\n    7: Marker bit (Always set)\n2-5 Bytes: extent information\n    - 2 Bytes: Disk number (Not included if block is local)\n    - 2 Bytes: Start block (Not included if dense)\n    - 1 Byte: Length (Not included if dense)\n"
  },
  {
    "path": "src/filesystem_design/pool_header.md",
    "content": "# Pool header\n\nThe root disk only holds information about the pool. Blocks cannot be stored to this disk.\n\n| Offset | Length | Field                                                                                          |\n| ------ | ------ | ---------------------------------------------------------------------------------------------- |\n| 0      | 8      | Magic number for idenifying a fluster drive `Fluster!`                                         |\n| 8      | 1      | Bitflags                                                                                       |\n| 9      | 2      | Highest known disk number.                                                                     |\n| 11     | 2      | Disk with the next free block in the pool.<br />Set to u16::MAX if the final disk has no room. |\n| 13     | 4      | Number of blocks free across all disks in the pool.                                            |\n| -      | -      | Reserved                                                                                       |\n| 148    | 360    | Block usage bitplane                                                                           |\n| 509    | 4      | Block CRC                                                                                      |\n\nBitflags:\n\n| bit | flag                                      |\n| --- | ----------------------------------------- |\n| 0   | Reserved                                  |\n| 1   | Reserved                                  |\n| 2   | Reserved                                  |\n| 3   | Reserved                                  |\n| 4   | Reserved                                  |\n| 5   | Reserved                                  |\n| 6   | Reserved                                  |\n| 7   | Reserved                                  |\n| 8   | Marks this as a pool header. Must be set. |\n"
  },
  {
    "path": "src/filesystem_design/pool_layout.md",
    "content": "# The pool\n\nThe pool will be our highest level of abstraction on top of the disks, every action against the underlying disks should be done through the pool."
  },
  {
    "path": "src/filesystem_design/possible_speed_improvments.md",
    "content": "# Speed improvement ideas\n\n# Disk pre-seek\nIf we know we are about to change disks, is it possible to pre-align the head of the drive\nto the next block we will read on the next disk while the user swaps?\n\n# In-memory inode cache\nThis might be practically mandatory to get any usability out of the file system unless\nwe want to be swapping tons of disks for every read operation.\n\nYou should be able to enable or disable this on the fly if you want."
  },
  {
    "path": "src/helpers/hex_view.rs",
    "content": "// Take in a vec of bytes and return a hex view of it\n\npub fn hex_view(bytes: Vec<u8>) -> String {\n    let mut offset = 0;\n    let bytes_length = bytes.len();\n\n    let mut screen_string = String::new();\n\n    // push the header\n    screen_string.push_str(\" Offset(h)  00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\\n\");\n\n    while offset < bytes_length {\n        // make the line\n        let mut line = String::new();\n        // first goes the offset, padded so its 10 characters long\n        line.push_str(&format!(\"{offset:0>10X}  \"));\n        // now for all the numbers\n        for i in 0..16 {\n            // skip if we are outside of range\n            if offset + i >= bytes_length {\n                line.push_str(\"  \");\n            } else {\n                let byte = bytes[offset + i];\n                let byte_component = format!(\"{byte:02X} \");\n                line.push_str(&byte_component);\n            }\n        }\n\n        // now for the text version\n        line.push(' ');\n        for i in 0..16 {\n            let mut character: char;\n            if offset + i >= bytes_length {\n                character = ' ';\n            } else {\n                // convert\n                let byte = bytes[offset + i];\n                character = char::from_u32(byte as u32).unwrap_or('?');\n                // unless:\n                if !character.is_ascii() || character.is_ascii_control() {\n                    character = '.';\n                }\n            }\n\n            line.push(character);\n        }\n\n        // line is done. Add it to the screen\n        screen_string.push_str(&line);\n        screen_string.push('\\n');\n\n        // Now increment the offset\n        offset += 16;\n    }\n\n    // done!\n    screen_string\n}\n"
  },
  {
    "path": "src/helpers/mod.rs",
    "content": "pub mod hex_view;\n"
  },
  {
    "path": "src/lib.rs",
    "content": "// The library/filesystem cannot use unwraps.\n#![deny(clippy::unwrap_used)]\n\n// Asserts need to have a reason.\n#![deny(clippy::missing_assert_message)]\n\n// Gotta use all the results.\n#![deny(unused_results)]\n// I need to force some methods to only be used in special places.\n// Doing the publicity for it would be a pain, so we just piggyback on\n// depreciated\n#![deny(deprecated)]\n\n// Only use the filesystem in main.rs\n// We only support 64 bit systems. since we expect usize to be that size.\n#[cfg(target_pointer_width = \"64\")]\npub mod filesystem;\n\n// Within the crate, we can use:\nmod helpers;\nmod error_types;\nmod pool;\npub mod tui;"
  },
  {
    "path": "src/main.rs",
    "content": "use std::{\n    ffi::OsStr,\n    path::PathBuf,\n    sync::{\n        atomic::{\n            AtomicBool,\n            Ordering\n        },\n        Arc\n    },\n    thread,\n    time::Duration\n};\n\nuse clap::Parser;\nuse fluster_fs::{\n    filesystem::filesystem_struct::{\n        FilesystemOptions,\n        FlusterFS\n    },\n    tui::notify::TUI_MANAGER\n};\n\n// Logging\nuse env_logger::Env;\nuse ratatui::{\n    crossterm::{\n        execute,\n        terminal::{\n            disable_raw_mode,\n            enable_raw_mode,\n            EnterAlternateScreen,\n            LeaveAlternateScreen\n        }\n    },\n    prelude::CrosstermBackend,\n    Terminal\n};\n\nuse tui_logger::TuiLoggerFile;\n\n#[derive(Parser)]\nstruct Cli {\n    /// Path to the floppy block device.\n    #[arg(long)]\n    block_device_path: String,\n    /// The mount point to mount the Fluster pool.\n    #[arg(long)]\n    mount_point: String,\n    /// Run with virtual floppy disks for testing. Path to put tempfiles in.\n    #[arg(long)]\n    use_virtual_disks: Option<String>,\n    /// Make backups of disks in /var/fluster. Disabling this VERY unsafe, you should\n    /// leave this on unless you are doing testing or don't care that much about your data.\n    #[arg(long)]\n    enable_disk_backup: Option<bool>,\n    /// Disable the TUI interface.\n    #[arg(long)]\n    disable_tui: Option<bool>,\n}\n\nfn main() {    \n    // Get cli arguments\n    let cli = Cli::parse();\n\n    // get the mount point\n    let mount_point = PathBuf::from(cli.mount_point);\n\n    // Start the logger\n    // If we are using the tui, we need to use the TUI logger instead of env.\n    if !cli.disable_tui.unwrap_or(false) {\n        // use the tui\n        tui_logger::init_logger(log::LevelFilter::Debug).unwrap();\n        let log_level = std::env::var(\"RUST_LOG\")\n            .map(|s| match s.to_lowercase().as_str() {\n                \"error\" => log::LevelFilter::Error,\n                \"warn\" => log::LevelFilter::Warn,\n                \"info\" => log::LevelFilter::Info,\n                \"debug\" => log::LevelFilter::Debug,\n                \"trace\" => log::LevelFilter::Trace,\n                _ => log::LevelFilter::Debug,\n            })\n            .unwrap_or(log::LevelFilter::Debug);\n        tui_logger::set_default_level(log_level);\n        // Also write out to a file.\n        tui_logger::set_log_file(TuiLoggerFile::new(\n            &mount_point.parent()\n            .expect(\"You really shouldn't be mounting fluster as `/` lmao.\")\n            .join(\"flusterlog.txt\")\n            .display()\n            .to_string()\n        ));\n    } else {\n        // normal logger\n        env_logger::Builder::from_env(Env::default().default_filter_or(\"debug\")).init();\n    }\n\n    // Log panics\n    log_panics::Config::default().backtrace_mode(log_panics::BacktraceMode::Resolved).install_panic_hook();\n\n    // Set up Ctrl+C handler\n    ctrlc::set_handler(move || {\n        println!(\"Fluster cannot be closed with ctrl+c. You need to unmount the filesystem with `fusermount -u (path)`.\");\n        println!(\"Busy? Close everything that may be looking at the filesystem.\");\n        println!(\"Still busy? Too bad, wait it out. Or suffer data loss. Your choice.\");\n        println!(\"Ignoring...\");\n    })\n    .unwrap();\n\n    // Check if the mount point is valid\n    std::fs::create_dir_all(&mount_point).unwrap();\n\n    // Assemble the options\n    let use_virtual_disks: Option<PathBuf> = cli.use_virtual_disks.map(PathBuf::from);\n    let backup: Option<bool> = cli.enable_disk_backup;\n    let enable_tui = !cli.disable_tui.unwrap_or(false);\n\n    let options: FilesystemOptions =\n        FilesystemOptions::new(use_virtual_disks, cli.block_device_path.into(), backup, enable_tui);\n\n\n    // Now before starting the filesystem, we need to start the TUI if needed.\n\n    // We also need to be able to tell the TUI to shut down when we unmount.\n    let shutdown_tui = Arc::new(AtomicBool::new(false));\n\n\n    let tui_thread_handle = if enable_tui {\n        let signal = Arc::clone(&shutdown_tui);\n        // Spawn a thread that handles the TUI\n        Some(thread::spawn(move || {\n            // Set up the terminal window\n            let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout())).unwrap();\n            // Swap to another screen to preserve old terminal content\n            execute!(terminal.backend_mut(), EnterAlternateScreen).unwrap();\n            // Raw mode for tui stuff\n            enable_raw_mode().unwrap();\n\n            // Wait a bit for Fluster! to start up and initialize things\n            thread::sleep(Duration::from_millis(100));\n\n            // Rendering loop, we do terminal cleanup when told to.\n            while !signal.load(Ordering::Relaxed) {\n                // Try and lock the TUI, if we cant, we'll just skip this frame.\n                if let Ok(mut tui) = TUI_MANAGER.try_lock() {\n                    // Draw it!\n                    // We ignore if the drawing fails, we'll just try it again.\n                    let _ = terminal.draw(|frame| tui.draw(frame));\n                }\n                // Now wait before drawing the next frame.\n                thread::sleep(Duration::from_millis(16)); // just above 60fps\n            }\n\n            // We've broken from the loop, time to shut down the TUI.\n            execute!(terminal.backend_mut(), LeaveAlternateScreen).unwrap();\n            disable_raw_mode().unwrap();\n        }))\n    } else {\n        None\n    };\n\n    let filesystem: FlusterFS = FlusterFS::start(&options);\n\n    // Now for the fuse mount options\n    let fuse_options = [\n        OsStr::new(\"-onodev\"), // Disable dev devices\n        OsStr::new(\"-onoatime\"), // No access times\n        OsStr::new(\"-onosuid\"), // Ignore file/folder permissions (lol)\n        OsStr::new(\"-orw\"), // Read/Write\n        OsStr::new(\"-oexec\"), // Files are executable\n        OsStr::new(\"-osync\"), // No async.\n        OsStr::new(\"-odirsync\"), // No async\n        OsStr::new(\"-oallow_other\"), // Allow other users to open the mount point (ie windows outisde of WSL)\n        OsStr::new(\"-ofsname=fluster\"), // Set the name of the fuse mount\n    ];\n\n    // Mount it\n\n    // Internal fuse_mt startup stuff i think, no comments on the function implementation.\n    // takes in the filesystem, and the number of threads the filesystem will use\n    // Fluster! Is single threaded, so we actually set it to 0 threads. Weirdly.\n    let mt_thing = fuse_mt::FuseMT::new(filesystem, 0);\n\n\n    match fuse_mt::mount(mt_thing, &mount_point, &fuse_options) {\n        Ok(_) => {\n            // Filesystem was unmounted successfully.\n            println!(\"Fluster! has been unmounted.\");\n        },\n        Err(err) => {\n            // rhut row\n            println!(\"Fluster is dead and you killed them. {err:#?}\");\n        },\n    }\n\n    // Clean up the TUI.\n    if let Some(handle) = tui_thread_handle {\n        shutdown_tui.store(true, Ordering::Relaxed);\n        handle.join().expect(\"TUI rendering thread failed to join on shutdown! But Fluster otherwise shut down successfully.\");\n    }\n\n}\n"
  },
  {
    "path": "src/pool/disk/blank_disk/blank_disk_methods.rs",
    "content": "// Yep.\n\nuse std::fs::File;\n\nuse log::error;\n\nuse crate::{\n    error_types::drive::DriveError,\n        pool::disk::{\n        blank_disk::blank_disk_struct::BlankDisk,\n        generic::{\n            block::{\n                allocate::block_allocation::BlockAllocation,\n                block_structs::RawBlock\n            },\n            disk_trait::GenericDiskMethods,\n            generic_structs::pointer_struct::DiskPointer,\n            io::write::write_block_direct\n        }\n    }\n};\n\nimpl GenericDiskMethods for BlankDisk {\n    #[doc = \" Read a block\"]\n    #[doc = \" Cannot bypass CRC.\"]\n    fn unchecked_read_block(&self, _block_number: u16) -> Result<RawBlock, DriveError> {\n        // We should NEVER read a block from a blank disk, why would we do that?\n        unreachable!(\"Attempted to read a block from a blank disk! Not allowed! You need to turn it into another type first!\")\n    }\n\n    #[doc = \" Write a block\"]\n    fn unchecked_write_block(&mut self, block: &RawBlock) -> Result<(), DriveError> {\n        // This is the first call, we have not recursed.\n        write_block_direct(&self.disk_file, block, false)\n    }\n\n    #[doc = \" Get the inner file used for IO operations\"]\n    fn disk_file(self) -> File {\n        self.disk_file\n    }\n\n    #[doc = \" Get the number of the floppy disk.\"]\n    fn get_disk_number(&self) -> u16 {\n        // Why are we getting the disk number of a blank floppy?\n        error!(\"Attempted to get the disk number of a blank disk! Not allowed!\");\n        // We will ignore the action and return a nonsensical number, this prevents fluster\n        // from crashing if you have a disk blank disk in the drive after finishing troubleshooting.\n        u16::MAX\n    }\n\n    #[doc = \" Set the number of this disk.\"]\n    fn set_disk_number(&mut self, _disk_number: u16) -> () {\n        // You cannot set the number of a blank disk.\n        // Trying to set the disk number is doomed to fail, because at this point it thinks its an initialized disk, which it is not.\n        unreachable!(\"Attempted to set the disk number of a blank disk! Not allowed!\")\n    }\n\n    #[doc = \" Get the inner file used for write operations\"]\n    fn disk_file_mut(&mut self) -> &mut File {\n        &mut self.disk_file\n    }\n\n    #[doc = \" Sync all in-memory information to disk\"]\n    fn flush(&mut self) -> Result<(), DriveError> {\n        // There is no in-memory information for this disk.\n        // So we can safely ignore this.\n        Ok(())\n    }\n    \n    #[doc = \" Write chunked data, starting at a block.\"]\n    fn unchecked_write_large(&mut self, data:Vec<u8>, start_block:DiskPointer) -> Result<(), DriveError> {\n        crate::pool::disk::generic::io::write::write_large_direct(&self.disk_file, &data, start_block)\n    }\n    \n    #[doc = \" Read multiple blocks\"]\n    #[doc = \" Does not check CRC!\"]\n    fn unchecked_read_multiple_blocks(&self, _block_number: u16, _num_block_to_read: u16) -> Result<Vec<RawBlock>,DriveError> {\n        unreachable!(\"Attempted to read a block from a blank disk! Not allowed! You need to turn it into another type first!\")\n    }\n}\n\n// Occasionally we need a new blank disk\nimpl BlankDisk {\n    pub fn new(file: File) -> Self {\n        Self { disk_file: file }\n    }\n}\n\nimpl BlockAllocation for BlankDisk {\n    #[doc = \" Get the block allocation table\"]\n    fn get_allocation_table(&self) ->  &[u8] {\n        unreachable!(\"Block allocation is not supported on blank disks.\")\n    }\n    \n    #[doc = \" Update and flush the allocation table to disk.\"]\n    fn set_allocation_table(&mut self, _new_table: &[u8]) -> Result<(), DriveError> {\n        unreachable!(\"Block allocation is not supported on blank disks.\")\n        \n    }\n}"
  },
  {
    "path": "src/pool/disk/blank_disk/blank_disk_struct.rs",
    "content": "// Need for type constraints\n\n#[derive(Debug)]\npub struct BlankDisk {\n    /// Every disk type needs a file!\n    pub(in super::super) disk_file: std::fs::File,\n}\n"
  },
  {
    "path": "src/pool/disk/blank_disk/mod.rs",
    "content": "pub mod blank_disk_methods;\npub mod blank_disk_struct;\n"
  },
  {
    "path": "src/pool/disk/drive_methods.rs",
    "content": "// Methods that are generic across all types of disk.\n\n// Using the floppy drive interface should work like this:\n// Request a disk, get back a DiskType that matches the number provided.\n\n// Imports\n\nuse log::error;\nuse log::trace;\nuse log::warn;\n\nuse crate::error_types::conversions::CannotConvertError;\nuse crate::error_types::critical::CriticalError;\nuse crate::error_types::critical::RetryCapError;\nuse crate::error_types::drive::DriveError;\nuse crate::error_types::drive::DriveIOError;\nuse crate::error_types::drive::WrappedIOError;\nuse crate::helpers::hex_view::hex_view;\nuse crate::pool::disk::blank_disk::blank_disk_struct::BlankDisk;\nuse crate::pool::disk::drive_struct::DiskBootstrap;\nuse crate::pool::disk::generic::block::block_structs::RawBlock;\nuse crate::pool::disk::generic::disk_trait::GenericDiskMethods;\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\n// The cache is NOT allowed in here at all, since any writes happen through the cache regardless.\n// Thus if we are loading in a disk, this is a real swap.\n// use crate::pool::disk::generic::io::cache::cache_io::CachedBlockIO;\nuse crate::pool::disk::generic::io::read::read_block_direct;\n\nuse crate::pool::disk::generic::io::wipe::destroy_disk;\nuse crate::pool::disk::standard_disk::standard_disk_struct::StandardDisk;\n\nuse crate::pool::disk::pool_disk::pool_disk_struct::PoolDisk;\n\nuse crate::filesystem::filesystem_struct::FLOPPY_PATH;\nuse crate::filesystem::filesystem_struct::USE_VIRTUAL_DISKS;\nuse crate::pool::disk::unknown_disk::unknown_disk_struct::UnknownDisk;\nuse crate::tui::notify::NotifyTui;\nuse crate::tui::prompts::TuiPrompt;\n\nuse super::drive_struct::DiskType;\nuse super::drive_struct::FloppyDrive;\n\nuse std::fs::File;\nuse std::fs::OpenOptions;\nuse std::sync::atomic::AtomicU16;\nuse std::sync::atomic::Ordering;\n\n// Disk tracking global.\n\n// To better count disk swaps, we need to know what the most recently opened disk was\nstatic CURRENT_DISK_IN_DRIVE: AtomicU16 = AtomicU16::new(u16::MAX);\n\n// Implementations\n\n/// Various operations on the underlying Disk.\n/// This is meant to be high level, just enough to get to the disk type below.\nimpl FloppyDrive {\n    /// Open the disk currently in the drive, regardless of disk type.\n    /// This should only be used when initializing the pool. Use open() instead.\n    pub fn open_direct(disk_number: u16) -> Result<DiskType, DriveError> {\n        // This function does not create disks.\n        open_and_deduce_disk(disk_number, false)\n    }\n\n    /// Opens a specific disk, or waits until the user inserts that disk.\n    #[deprecated =\"You should be using the cache! Unless you are using this in the cache.\"]\n    pub fn open(disk_number: u16) -> Result<DiskType, DriveError> {\n        prompt_for_disk(disk_number)\n    }\n\n    /// Prompts the user for a blank floppy disk.\n    pub fn get_blank_disk(disk_number: u16) -> Result<BlankDisk, DriveError> {\n        prompt_for_blank_disk(disk_number)\n    }\n\n    /// Find out what disk is currently in the drive.\n    pub fn currently_inserted_disk_number() -> u16 {\n        CURRENT_DISK_IN_DRIVE.load(Ordering::Relaxed)\n    }\n}\n\n// Functions for implementations\n\nfn open_and_deduce_disk(disk_number: u16, new_disk: bool) -> Result<DiskType, DriveError> {\n    trace!(\"Opening and deducing disk disk {disk_number}...\");\n    trace!(\"Is it a new disk? : {new_disk}\");\n    // First, we need the file to read from\n    let disk_file: File = get_floppy_drive_file(disk_number, new_disk)?;\n\n    // Now we must get the 0th block\n    // We need to read a block before we have an actual disk, so we need\n    // to call this function directly as a workaround.\n\n    // This also must be called directly, since we cannot use the cache here.\n    // The cache expects to not be accessed while doing flushes, which requires all\n    // calls that load information about disks to not access the cache.\n\n    // We must ignore the CRC here, since we know nothing about the disk.\n    trace!(\"Reading in the header at block 0...\");\n    let header_block: RawBlock = read_block_direct(&disk_file, disk_number, 0, true, false)?;\n\n    // Now we check for the magic\n    trace!(\"Checking for magic...\");\n    if !check_for_magic(&header_block.data) {\n        trace!(\"No magic, checking if its blank...\");\n        // The magic is missing, check if the block is empty\n        if header_block.data.iter().all(|byte| *byte == 0) {\n            // Block is completely blank.\n            trace!(\"Disk is blank, returning.\");\n            return Ok(DiskType::Blank(BlankDisk::new(disk_file)));\n        }\n        // Otherwise, we dont know what kind of disk this is.\n        // Its probably not a fluster disk.\n        trace!(\"Disk was not blank, returning unknown disk...\");\n        return Ok(DiskType::Unknown(UnknownDisk::new(disk_file)));\n    }\n\n    // Magic exists, time to figure out what kind of disk this is.\n    trace!(\"Disk has magic, deducing type...\");\n    // Bitflags will tell us.\n\n    // Pool disk.\n    // The header reads should check the CRC of the block.\n    if header_block.data[8] & 0b10000000 != 0 {\n        trace!(\"Head is for a pool disk, returning.\");\n        return Ok(DiskType::Pool(PoolDisk::from_header(\n            header_block,\n            disk_file,\n        )));\n    }\n\n    // Standard disk.\n    if header_block.data[8] & 0b00100000 != 0 {\n        trace!(\"Head is for a standard disk, returning.\");\n        return Ok(DiskType::Standard(StandardDisk::from_header(\n            header_block,\n            disk_file,\n        )));\n    }\n\n    // it should be impossible to get here\n    error!(\"Hexdump:\\n{}\", hex_view(header_block.data.to_vec()));\n    error!(\"We cannot continue with an un-deducible disk!\");\n    panic!(\"Header of disk did not match any known disk type!\");\n}\n\n/// Get the path of the floppy drive\nfn get_floppy_drive_file(disk_number: u16, new_disk: bool) -> Result<File, DriveError> {\n    // If we are running with virtual disks enabled, we are going to use a temp folder instead of the actual disk to speed up\n    // development, waiting for disk seeks is slow and loud lol.\n\n    if let Ok(maybe_path) = USE_VIRTUAL_DISKS.try_lock() {\n        if let Some(virtual_disk_path) = maybe_path.clone() {\n            // Virtual disks are enabled.\n            trace!(\"Attempting to access virtual disk {disk_number}...\");\n            trace!(\"Are we creating this disk? : {new_disk}\");\n            // Get the tempfile.\n            // These files do not delete themselves.\n\n            // if disk 0 is missing, we need to make it,\n            // because the pool cannot create disk 0 without first loading itself... from disk 0.\n            // This is for virtual disks, so if this fails its on the user.\n\n            // If using virtual disks fails, we immediately bail.\n\n\n            if OpenOptions::new()\n                .read(true)\n                .write(true)\n                .create(true)\n                .truncate(false)\n                .open(virtual_disk_path.join(\"disk0.fsr\")).is_err() {\n                // No good.\n                panic!(\"You are in-charge of making virtual disks work.\");\n            };\n\n            // If the tempfile does not exist, that means `create` was never called, which is an issue.\n            // This will create the disk if the correct argument is passed.\n\n            trace!(\"Opening the temp disk with read/write privileges...\");\n            let temp_disk_file = if let Ok(file) = OpenOptions::new()\n                .read(true)\n                .write(true)\n                .create(new_disk) // We will panic if the disk does not exist, unless told to create it.\n                .truncate(false)\n                .open(virtual_disk_path.join(format!(\"disk{disk_number}.fsr\"))) {\n                file\n            } else {\n                // Failed.\n                panic!(\"Disks should be created before read.\");\n            };\n\n            // Make sure the file is one floppy big, should have no effect on pre-existing files, since\n            // they will already be this size.\n            trace!(\"Attempting to resize the temporary file to floppy size...\");\n\n            // This is for virtual disks, so if this fails its on the user.\n            if temp_disk_file.set_len(512 * 2880).is_err() {\n                panic!(\"If you're using virtual disks, you should be able to resize the virtual disks.\");\n            }\n\n            trace!(\"Returning virtual disk.\");\n            return Ok(temp_disk_file);\n        }\n    }\n\n    // Get the global path to the floppy disk drive\n    let disk_path = if let Ok(path) = FLOPPY_PATH.try_lock() {\n        path.clone()\n    } else {\n        // Poison? In MY drive method?\n        // _It's more common than you think!_\n        \n        // I REALLY hope we're shutting down at this point.\n        // We need the disk path to be able to flush contents to disk upon panic, so we have to clean this up.\n        error!(\"FLOPPY_PATH is poisoned! Clearing, but you REALLY need to shut down immediately.\");\n        FLOPPY_PATH.clear_poison();\n        if let Ok(round_two) = FLOPPY_PATH.try_lock() {\n            round_two.clone()\n        } else {\n            // Err...\n            panic!(\"Failed to clear poison!\");\n        }\n    };\n\n    // Open the disk, or return an error from it.\n\n    // Before that though, make sure the block device we are trying to\n    // access is actually mounted\n    if !std::fs::exists(&disk_path).unwrap_or(false) {\n        // Bound to fail.\n        return Err(DriveError::DriveEmpty)\n    }\n\n\n\n    // We will try 10 times.\n    // If we fail to open the floppy drive file, there's a bigger issue than this function can deal with.\n\n    for _ in 0..10 {\n        // Open the file.\n        let open_attempt = OpenOptions::new().read(true).write(true).open(&disk_path);\n        \n\n        // If it opened, return, otherwise we need to handle the IO error.\n        let io_error = match open_attempt {\n            Ok(ok) => return Ok(ok),\n            Err(err) => err,\n        };\n\n        // That did not work, see if we can cast up the error\n\n        let pointer = DiskPointer {\n            disk: disk_number,\n            block: 0,\n        };\n\n        // Try converting that up to a DriveError\n        let wrapped: WrappedIOError = WrappedIOError::wrap(io_error, pointer);\n        let drive_io_error: Result<DriveIOError, CannotConvertError> = DriveIOError::try_from(wrapped);\n\n        // Did that work?\n        let converted = match drive_io_error {\n            Ok(ok) => ok,\n            Err(err) => {\n                // Looks like we need to handle this ourselves.\n                match err {\n                    CannotConvertError::MustRetry => {\n                        // Look's like we're trying again!\n                        continue;\n                    },\n                }\n            },\n        };\n\n        // The conversion worked, can we get it up to a DriveError?\n        let drive_error: Result<DriveError, CannotConvertError> = DriveError::try_from(converted);\n\n        // Did that also work? If so, throw it!\n        match drive_error {\n            Ok(ok) => return Err(ok), // lol\n            Err(err) => {\n                match err {\n                    CannotConvertError::MustRetry => {\n                        continue;\n                    },\n                }\n            },\n        };\n    };\n\n    drop(disk_path);\n\n    // We've failed 10 times. Nothing we can do.\n    // We can probably recover for this assuming the critical handler can either rebuild the disk\n    // or somehow make it writable again\n    CriticalError::OutOfRetries(RetryCapError::OpenDisk).handle();\n    // If that works, recurse, we should be able to get the file now.\n    get_floppy_drive_file(disk_number, new_disk)\n}\n\n/// Look for the magic \"Fluster!\" string.\npub fn check_for_magic(block_bytes: &[u8]) -> bool {\n    // is the \"Fluster!\" magic present?\n    block_bytes[0..8] == *\"Fluster!\".as_bytes()\n}\n\n/// Prompt user to insert the disk we want.\n/// If the disk is already in the drive, no prompt will happen.\n/// Will error out for non-wrong disk related issues.\n/// This function does not disable the CRC check, you must use open() if you are ignoring CRC.\nfn prompt_for_disk(disk_number: u16) -> Result<DiskType, DriveError> {\n    trace!(\"Prompting for disk {disk_number}...\");\n    let mut is_user_an_idiot: bool = false; // Did the user put in the wrong disk when asked?\n    let mut disk: DiskType;\n\n    loop {\n        // Try opening the current disk.\n        // We do not create disks here.\n        disk = open_and_deduce_disk(disk_number, false)?;\n\n        // Obviously, a blank disk cannot be the right disk, since it has\n        // no disk number.\n        if let DiskType::Blank(_) = disk {\n            // what\n            TuiPrompt::prompt_enter(\n                \"Wrong disk\".to_string(),\n                \"This disk is blank. Try again.\".to_string(),\n                true\n            );\n            continue;\n        }\n\n        // Is this the correct disk?\n        let new_disk_number = disk.get_disk_number();\n\n        // Update the current disk if needed\n        let previous_disk = CURRENT_DISK_IN_DRIVE.load(Ordering::Relaxed);\n\n        if new_disk_number != previous_disk {\n            // We have swapped disks.\n\n            // Inform the TUI. It's in charge of tracking swaps.\n            NotifyTui::disk_swapped(new_disk_number);\n\n            CURRENT_DISK_IN_DRIVE.store(new_disk_number, Ordering::Relaxed);\n        }\n\n        // Check if this is the right disk number\n        if disk_number == new_disk_number {\n            // Thats the right disk!\n            trace!(\"Got the correct disk.\");\n            return Ok(disk);\n        }\n\n        warn!(\"Wrong disk received. Got disk {}\", disk.get_disk_number());\n\n\n        // This was not the right disk.\n        // We should ALWAYS get the correct disk when testing.\n        #[cfg(test)]\n        if cfg!(test) {\n            error!(\"Got an invalid disk during a test!\");\n            panic!(\"Test received an invalid disk!\");\n        }\n\n        // Prompt user to swap disks.\n        // But we don't prompt if the read failed, since we want to silently retry it.\n        if is_user_an_idiot {\n            println!(\"Wrong disk. Try again.\");\n        } else {\n            is_user_an_idiot = true;\n        }\n\n        // If this prompt fails, either there's an issue with the disk, or the user didn't respond in time\n        let result = TuiPrompt::prompt_wait_for_disk_swap(\n            \"Please swap disks.\".to_string(),\n            format!(\"Please remove disk {previous_disk}, and insert disk {disk_number}\"),\n            true\n        );\n\n        match result {\n            Ok(ok) => ok,\n            Err(_) => {\n                // We'll launch the troubleshooter regardless of error type,\n                // since we really need swapping disks to work.\n                CriticalError::OutOfRetries(RetryCapError::OpenDisk).handle();\n                // Then we just try the swap again.\n            },\n        }\n    }\n}\n\n// get a blank disk\nfn prompt_for_blank_disk(disk_number: u16) -> Result<BlankDisk, DriveError> {\n    // Pester user for a blank disk\n    let mut try_again: bool = false;\n\n    // If we are on virtual disks, skip the initial prompt\n    let use_virtual: bool = if let Ok(locked) = USE_VIRTUAL_DISKS.try_lock() {\n        locked.is_some()\n    } else {\n        // Poisoned. We should not be adding new disks after being poisoned. We should be shutting down.\n        // Just give up, if we're trying to do that, chances are we just cannot shut down.\n        panic!(\"Attempted to get a new, blank disk for a poisoned pool! Not allowed!\");\n    };\n\n    if !use_virtual {\n        TuiPrompt::prompt_wait_for_disk_swap(\n            \"New disk.\".to_string(),\n            format!(\"Creating a new disk, please insert a blank disk that will become disk {disk_number}.\"),\n            true\n        )?;\n    }\n\n    loop {\n        if try_again {\n            let action = TuiPrompt::prompt_input(\n                \"Disk is not blank.\".to_string(),\n                \"That disk is not blank. Please insert a blank disk, then hit enter. Or type \\\"wipe\\\" to forcibly wipe this disk.\".to_string(),\n                false\n            );\n\n            if action.contains(\"wipe\") {\n                // go wipe that disk\n                let mut wipe_me = open_and_deduce_disk(disk_number, false)?;\n                destroy_disk(wipe_me.disk_file_mut())?;\n                drop(wipe_me);\n            }\n\n        }\n        // we are making a new disk, so we must specify as such.\n        let mut disk = open_and_deduce_disk(disk_number, true)?;\n        match disk {\n            // if its blank, all done\n            DiskType::Blank(blank_disk) => return Ok(blank_disk),\n            _ => {\n                // But if the disk is not blank, \n                display_info_and_ask_wipe(&mut disk)?;\n                // try again\n                try_again = true;\n                continue;\n            }\n        }\n    }\n}\n\n/// Takes in a non-blank disk and displays info about it, then asks the user if they would like to wipe the disk.\n/// Wipes the disk if the user asks, returns nothing.\n/// Will also return nothing if the user does not wipe the disk.\npub fn display_info_and_ask_wipe(disk: &mut DiskType) -> Result<(), DriveError> {\n    // This isn't a very friendly interface, but it'll do for now.\n\n    // Display the disk type\n    let answer = TuiPrompt::prompt_input(\n        \"Disk is not blank.\".to_string(),\n        format!(\"The disk inserted is not blank. It is of type `{disk:?}`.\\nWould you like to wipe this disk?\\n\\\"yes\\\"/\\\"no\\\":\"),\n        false\n    ).to_ascii_lowercase().contains(\"yes\");\n\n    if answer {\n        // Wipe time!\n        // If this fails, inform user.\n        if destroy_disk(disk.disk_file_mut()).is_err() {\n            TuiPrompt::prompt_enter(\n                \"Wipe failed!\".to_string(),\n                \"Failed to wipe that disk! It's probably bad.\".to_string(),\n                true\n            );\n        }\n    } else {\n        // No wipe.\n        TuiPrompt::prompt_enter(\n            \"Wipe canceled.\".to_string(),\n            \"Okay, this disk will not be wiped.\".to_string(),\n            false\n        );\n    }\n    Ok(())\n}"
  },
  {
    "path": "src/pool/disk/drive_struct.rs",
    "content": "// I think I slipped a disk.\n\n// Imports\n\nuse crate::{error_types::drive::DriveError, pool::disk::{\n    blank_disk::blank_disk_struct::BlankDisk, unknown_disk::unknown_disk_struct::UnknownDisk,\n}};\nuse std::fs::File;\n\nuse enum_dispatch::enum_dispatch;\n\nuse crate::pool::disk::{\n    generic::block::block_structs::RawBlock,\n    pool_disk::pool_disk_struct::PoolDisk,\n    standard_disk::standard_disk_struct::StandardDisk,\n};\n\n// Structs, Enums, Flags\n\n/// The floppy drive\n/// The FloppyDrive type doesn't contain anything itself, its just an interface for\n/// retrieving the various types of disk.\npub struct FloppyDrive {\n    // Nothing! This type is just for methods.\n}\n\n/// The different types of disks contained within a pool.\n/// This contains disk info.\n#[enum_dispatch]\n#[derive(Debug)]\npub enum DiskType {\n    Pool(PoolDisk),\n    Standard(StandardDisk),\n    Unknown(UnknownDisk),\n    Blank(BlankDisk),\n}\n\n// /// We also have another type that does not contain the disk info.\n// #[derive(Debug, Clone, Copy, PartialEq, Eq)]\n// pub enum JustDiskType {\n//     Pool,\n//     Standard,\n//     Unknown,\n//     Blank,\n// }\n\n// Let us match the two\n// impl PartialEq<JustDiskType> for DiskType {\n//     fn eq(&self, other: &JustDiskType) -> bool {\n//         match self {\n//             DiskType::Pool(_) => matches!(other, JustDiskType::Pool),\n//             DiskType::Standard(_) => matches!(other, JustDiskType::Standard),\n//             DiskType::Unknown(_) => matches!(other, JustDiskType::Unknown),\n//             DiskType::Blank(_) => matches!(other, JustDiskType::Blank),\n//         }\n//     }\n// }\n\n/// All disk types need to be able to create themselves from a raw block.\n/// Or, be able to create themselves from a blank disk.\n/// We also need to create fake disks to allow creating disks (confusing eh?)\npub trait DiskBootstrap {\n    /// Create brand new disk.\n    /// This takes in a blank floppy disk, and does all the needed setup on the disk,\n    /// such as writing the header, and other block setup.\n    fn bootstrap(file: File, disk_number: u16) -> Result<Self, DriveError>\n    where\n        Self: std::marker::Sized;\n    /// Create self from incoming header block and file.\n    fn from_header(block: RawBlock, file: File) -> Self;\n}\n"
  },
  {
    "path": "src/pool/disk/generic/block/allocate/block_allocation.rs",
    "content": "// Find, reserve, or even free blocks!\n\n// We do not allow these operations to be misused, if invalid state is provided, we panic.\n// We will not:\n// free bytes that are already free\n// allocate bytes that are already allocated\n// allocate past the end of the table\n\nuse std::cmp::min;\n\nuse enum_dispatch::enum_dispatch;\nuse crate::{\n    error_types::drive::DriveError,\n    pool::disk::drive_struct::DiskType\n};\nuse crate::pool::disk::pool_disk::pool_disk_struct::PoolDisk;\nuse crate::pool::disk::standard_disk::standard_disk_struct::StandardDisk;\nuse crate::pool::disk::unknown_disk::unknown_disk_struct::UnknownDisk;\nuse crate::pool::disk::blank_disk::blank_disk_struct::BlankDisk;\nuse log::{\n    debug,\n    trace\n};\n\n// To be able to allocate blocks, we need a couple things\n#[enum_dispatch(DiskType)]\npub trait BlockAllocation {\n    /// Get the block allocation table\n    fn get_allocation_table(&self) -> &[u8];\n\n    /// Update and flush the allocation table to disk.\n    fn set_allocation_table(&mut self, new_table: &[u8]) -> Result<(), DriveError>;\n\n    /// Attempts to find free blocks on the disk.\n    /// Returns indexes for the found blocks, or returns the number of blocks free if there is not enough space.\n    fn find_free_blocks(&self, blocks: u16) -> Result<Vec<u16>, u16> {\n        go_find_free_blocks(self, blocks)\n    }\n\n    /// Allocates the requested blocks.\n    /// Will panic if fed invalid data.\n    fn allocate_blocks(&mut self, blocks: &Vec<u16>) -> Result<u16, DriveError> {\n        go_allocate_or_free_blocks(self, blocks, true)\n    }\n\n    /// Frees the requested blocks.\n    /// Will panic if fed invalid data.\n    fn free_blocks(&mut self, blocks: &Vec<u16>) -> Result<u16, DriveError> {\n        go_allocate_or_free_blocks(self, blocks, false)\n    }\n\n    /// Check if a specific block is allocated\n    fn is_block_allocated(&self, block_number: u16) -> bool {\n        go_check_block_allocated(self, block_number)\n    }\n}\n\nfn go_find_free_blocks<T: BlockAllocation + ?Sized>(\n    caller: &T,\n    blocks_requested: u16,\n) -> Result<Vec<u16>, u16> {\n    // The allocation table is a stream of bits, the first bit is the 0th block.\n\n    // Vector of free block locations.\n    // Pre-allocated, expecting that we get all of blocks. But we obviously could not find\n    // more than 2880 free blocks per disk.\n    let mut free: Vec<u16> = Vec::with_capacity(min(2880_u16, blocks_requested) as usize);\n\n    // Now loop through the table looking for free slots.\n    for (byte_index, byte) in caller.get_allocation_table().iter().enumerate() {\n        // loop over the bits\n        for sub_bit in 0..8 {\n            // check if the furthest left bit is free.\n            // we shift over to the bit we want, then we AND it to check if the highest bit is set.\n            // Since we know the bit on one side of the AND is always set, the result will be 0 if the bit is unset.\n            // Thus, the result of the if statement will be `0` if the block is free.\n            // Could this be done cleaner? Maybe, I'm not very experienced with bitwise operations.\n            if (byte << sub_bit) & 0b10000000 == 0 {\n                // bit isn't set, the block is free!\n                free.push((byte_index as u16 * 8) + sub_bit);\n\n                // Do we have enough blocks now?\n                if free.len() == blocks_requested.into() {\n                    // Yep!\n                    return Ok(free);\n                }\n            }\n        }\n    }\n    // We've ran out of bytes. We must not have enough free room.\n    Err(free.len() as u16)\n}\n\n/// allocate false frees the provided bytes.\nfn go_allocate_or_free_blocks<T: BlockAllocation + ?Sized>(\n    caller: &mut T,\n    blocks: &Vec<u16>,\n    allocate: bool,\n) -> Result<u16, DriveError> {\n    debug!(\n        \"Attempting to {} {} blocks on the current disk...\",\n        if allocate { \"Allocate\" } else { \"free\" },\n        blocks.len()\n    );\n\n    // If the user provides a vec with a duplicate item, we will panic from double free / double allocate\n    // Vec ordering does not matter, as we calculate the offset from each item\n    // The user must allocate/free at least one block, and that block cannot be past the end of the table.\n    assert!(*blocks.last().expect(\"Should allocate at least 1 block.\") < 2880, \"Tried to free a block past the end of the disk!\");\n\n    // Table to edit\n    // 2880 blocks / 8 blocks per bit = 360\n    let mut new_allocation_table: [u8; 360] = [0u8; 360];\n    new_allocation_table.copy_from_slice(caller.get_allocation_table());\n\n    trace!(\"Updating blocks...\");\n    for block in blocks {\n        // Get the bit\n        // Integer division rounds towards zero, so this is fine.\n        let byte: usize = (block / 8) as usize;\n        let test_bit: u8 = 0b00000001 << (7 - (block % 8));\n        // check the bit\n        if new_allocation_table[byte] & test_bit == 0 {\n            // block is free.\n            if allocate {\n                // Good! Send it back\n                new_allocation_table[byte] |= test_bit;\n                continue;\n            } else {\n                // We are trying to free a freed block\n                panic!(\"Cannot free block that is already free!\")\n            }\n        } else {\n            // Block is not free\n            if allocate {\n                // Trying to allocate used block.\n                panic!(\"Cannot allocate block that is already allocated!\")\n            } else {\n                // Good! Free the block\n                new_allocation_table[byte] ^= test_bit;\n                continue;\n            }\n        }\n    }\n    trace!(\"Done updating blocks.\");\n\n    // All operations are done, write back the new table\n    trace!(\"Writing back new allocation table...\");\n    caller.set_allocation_table(&new_allocation_table)?;\n    debug!(\"Done.\");\n    Ok(blocks.len() as u16)\n}\n\nfn go_check_block_allocated<T: BlockAllocation + ?Sized>(caller: &T, block_number: u16) -> bool {\n    assert!(block_number < 2880, \"Tried to free block {block_number}. That is past the end of the disk.\");\n    // Integer division rounds towards zero, so this is fine.\n    let byte: usize = (block_number / 8) as usize;\n    let test_bit: u8 = 0b00000001 << (7 - (block_number % 8));\n    // check the bit\n    caller.get_allocation_table()[byte] & test_bit != 0\n}\n"
  },
  {
    "path": "src/pool/disk/generic/block/allocate/mod.rs",
    "content": "pub mod block_allocation;\n#[cfg(test)]\nmod tests;\n"
  },
  {
    "path": "src/pool/disk/generic/block/allocate/tests.rs",
    "content": "// I allocate development time to testing.\n// Unwrapping is okay here, since we want unexpected outcomes to fail tests.\n#![allow(clippy::unwrap_used)]\nuse test_log::test; // We want to see logs while testing.\n\nuse rand::{\n    Rng,\n    rngs::ThreadRng\n};\n\nuse crate::error_types::drive::DriveError;\n\nuse super::block_allocation::BlockAllocation;\n\n#[test]\n/// Allocate a single block from an empty table, make sure the allocated block is in the right spot.\nfn allocate_and_free_one_block() {\n    let mut table = TestTable::new();\n\n    let open_block = table\n        .find_free_blocks(1)\n        .expect(\"Should have > 1 block free.\");\n\n    assert_eq!(open_block.len(), 1); // We only asked for 1 block\n    assert_eq!(*open_block.first().expect(\"Guarded\"), 0_u16); // the first block should be free\n\n    let blocks_allocated = table.allocate_blocks(&open_block).unwrap();\n\n    assert_eq!(blocks_allocated, 1); // Should have allocated 1 block\n    assert_eq!(table.block_usage_map[0], 0b10000000); // First block got set.\n\n    let blocks_freed = table.free_blocks(&[0_u16].to_vec()).unwrap(); // free the first block\n\n    assert_eq!(blocks_freed, 1); // Should have freed the block\n    assert_eq!(table.block_usage_map[0], 0b00000000); // First block got freed\n}\n\n#[test]\n/// Attempt to allocate more blocks than there are on a disk\n/// This is a valid use-case, mass allocations like this will be used for\n/// putting as much data as we can fit onto a disk.\nfn oversized_allocation() {\n    let table = TestTable::new();\n    let open_block = table\n        .find_free_blocks(5000)\n        .expect_err(\"There shouldn't be enough room.\");\n    assert_eq!(open_block, 2880);\n}\n\n/// Fill a table with free gaps in it\n#[test]\nfn saturate_table() {\n    for _ in 0..1000 {\n        let mut random: ThreadRng = rand::rng();\n        let mut table = TestTable::new();\n        // Fill with random bytes\n        let mut random_table = [0u8; 360];\n        for byte in random_table.iter_mut() {\n            let new_byte: u8 = random.random();\n            *byte = new_byte;\n        }\n\n        table.block_usage_map = random_table;\n\n        // Make sure that the table knows how many block are actually allocated already.\n        let blocks_pre_set: u32 = random_table.iter().map(|byte| byte.count_ones()).sum();\n\n        // Now fill up the table\n        let free_blocks = table\n            .find_free_blocks(5000)\n            .expect_err(\"There shouldn't be enough room.\");\n\n        // make sure the table is reporting the correct amount of free blocks.\n        assert_eq!(2880 - free_blocks as u32, blocks_pre_set);\n\n        let blocks_to_allocate = table\n            .find_free_blocks(free_blocks)\n            .expect(\"Self reported max capacity.\");\n        let blocks_allocated: u16 = table.allocate_blocks(&blocks_to_allocate).unwrap();\n\n        assert_eq!(blocks_allocated, free_blocks);\n        // Is it actually full tho?\n        let num_unset_bits: u32 = table\n            .block_usage_map\n            .iter()\n            .map(|byte| byte.count_zeros())\n            .sum();\n        assert_eq!(num_unset_bits, 0);\n    }\n}\n\n/// Allocate random blocks and make sure they got marked\n#[test]\nfn marking() {\n    for _ in 0..1000 {\n        let mut random: ThreadRng = rand::rng();\n        let mut table = TestTable::new();\n\n        // the table is empty, so we should be able to reserve any block we want.\n        let random_block: u16 = random.random_range(0..2880);\n        let allocated_count = table.allocate_blocks(&[random_block].to_vec()).unwrap();\n        assert_eq!(allocated_count, 1);\n        // Check that it got set\n        let is_allocated = table.is_block_allocated(random_block);\n        assert!(is_allocated)\n    }\n}\n\n// We need a struct that implements the allocation methods for testing\n\nstruct TestTable {\n    pub block_usage_map: [u8; 360],\n}\n\nimpl TestTable {\n    fn new() -> Self {\n        Self {\n            block_usage_map: [0u8; 360],\n        }\n    }\n}\n\nimpl BlockAllocation for TestTable {\n    fn get_allocation_table(&self) -> &[u8] {\n        &self.block_usage_map\n    }\n\n    fn set_allocation_table(&mut self, new_table: &[u8]) -> Result<(), DriveError> {\n        self.block_usage_map = new_table\n            .try_into()\n            .expect(\"New table should be the same size as old table.\");\n        // We dont need to flush, since this table is all in memory for testing\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/pool/disk/generic/block/block_structs.rs",
    "content": "// Structs that can be deduced from a block\n\n// Imports\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\n\n// Structs, Enums, Flags\n\n/// A raw data block\n/// This should only be used internally, interfacing into this should\n/// be abstracted away into other types (For example DiskHeader)\n#[derive(Debug)]\npub struct RawBlock {\n    /// Which block on the disk this is\n    pub block_origin: DiskPointer,\n    /// The block in its entirety.\n    pub data: [u8; 512],\n}"
  },
  {
    "path": "src/pool/disk/generic/block/crc.rs",
    "content": "// CRC check\n\n// Check whether the CRC matches the block or not\n// returns true if crc matches the block correctly.\npub fn check_crc(block: [u8; 512]) -> bool {\n    let existing: [u8; 4] = block[508..512].try_into().expect(\"4 = 4\");\n    let computed: [u8; 4] = compute_crc(&block[0..508]);\n    existing == computed\n}\n\n// Takes in the data and calculates the CRC\npub fn compute_crc(bytes: &[u8]) -> [u8; 4] {\n    let checksum: u32 = crc32c::crc32c(bytes);\n    checksum.to_le_bytes()\n}\n\n/// Every block will always have a CRC in its last 4 bytes, regardless of block type.\npub(crate) fn add_crc_to_block(block: &mut [u8; 512]) {\n    let crc = compute_crc(&block[0..508]);\n    block[508..].copy_from_slice(&crc);\n}\n\n// Correcting detected errors could in theory be done with trying to flip every bit, but realistically,\n// we're better off just re-reading it, or restoring it from backup."
  },
  {
    "path": "src/pool/disk/generic/block/mod.rs",
    "content": "pub mod allocate;\npub mod block_structs;\npub mod crc;"
  },
  {
    "path": "src/pool/disk/generic/disk_trait.rs",
    "content": "// All types of disk MUST implement this.\n// Enforced by traits.\n\nuse std::fs::File;\n\nuse enum_dispatch::enum_dispatch;\n\nuse crate::{\n    error_types::drive::DriveError,\n    pool::disk::{\n        drive_struct::DiskType,\n        generic::{\n            block::block_structs::RawBlock,\n            generic_structs::pointer_struct::DiskPointer\n        },\n    }\n};\n\n// Generic disks must also have disk numbers, and be able to retrieve their inner File.\n#[enum_dispatch(DiskType)] // Force every disk type to implement these methods.\npub trait GenericDiskMethods {\n    /// Read a block\n    /// Cannot bypass CRC.\n    fn unchecked_read_block(&self, block_number: u16) -> Result<RawBlock, DriveError>;\n\n    /// Read multiple blocks\n    /// Does not check CRC!\n    fn unchecked_read_multiple_blocks(&self, block_number: u16, num_block_to_read: u16) -> Result<Vec<RawBlock>, DriveError>;\n\n    /// Write a block.\n    fn unchecked_write_block(&mut self, block: &RawBlock) -> Result<(), DriveError>;\n\n    /// Write chunked data, starting at a block.\n    fn unchecked_write_large(&mut self, data: Vec<u8>, start_block: DiskPointer) -> Result<(), DriveError>;\n\n    /// Get the inner file.\n    fn disk_file(self) -> File;\n\n    /// Get the inner file for write operations.\n    fn disk_file_mut(&mut self) -> &mut File;\n\n    /// Get the number of the floppy disk.\n    fn get_disk_number(&self) -> u16;\n\n    /// Set the number of this disk.\n    fn set_disk_number(&mut self, disk_number: u16);\n\n    /// Sync all in-memory information to disk\n    /// Headers and such.\n    fn flush(&mut self) -> Result<(), DriveError>;\n}\n"
  },
  {
    "path": "src/pool/disk/generic/generic_structs/find_space.rs",
    "content": "// A cool function that finds free space in a slice of bytes\n\n// Trait constraint that all input types must meet\npub trait BytePingPong {\n    /// Converts a type to its byte representation\n    fn to_bytes(&self) -> Vec<u8>;\n    /// Converts bytes into itself, and\n    /// will discard extra trailing bytes.\n    fn from_bytes(bytes: &[u8]) -> Self;\n}\n\n/// Find a contiguous space of x bytes in the input slice.\n/// Returns an index into the input data where the next `requested_space` bytes are empty.\n/// We assume the caller already checked if there is enough room, so if we do not find\n/// a space big enough, we will return None.\npub fn find_free_space<T: BytePingPong>(data: &[u8], requested_space: usize) -> Option<usize> {\n    // Assumptions:\n    // All incoming types will have bitflags\n    // - All incoming bitflags will have a marker bit in position 7\n    // Free space will be all 0's\n\n    // We will:\n    // - Look at a byte and check for the marker bit\n    // - - If there is no bit, this must be the start of unused space\n    // - - If there is a bit, get the length of this item, and jump ahead that far, start over\n    // - Check if the next `requested_space` bytes are empty:\n    // - - Yes? Return the current index\n    // - - No? Find which byte had data, and set the index to that byte. Start over.\n\n    // How far we are indexed into the data\n    let mut index: usize = 0;\n    let data_length = data.len();\n\n    // Sanity check, are we requesting more bytes than there is room possibly for bytes?\n    assert!(\n        requested_space <= data_length,\n        \"We cant find `{requested_space}` bytes free space in a slice of `{data_length}` size..\"\n    );\n\n    // We wont search bytes that are too far into the block to have enough space after them\n    // for the incoming data.\n    while index <= data_length - requested_space {\n        // Check for the marker bit\n        if data[index] & 0b10000000 != 0 {\n            // The bit is set. We need to seek forwards.\n\n            // To find out how far we need to seek, we will convert the bytes starting at offset\n            // to type <T>, then convert that type back to bytes again, and get the length of that\n            // This might be tad wasteful, but it is simple lol.\n\n            // Don't like it? Make a pull request! :D\n\n            index += T::from_bytes(&data[index..]).to_bytes().len();\n            continue;\n        }\n\n        // The bit is not set. Check if there's room\n        let enough_space: bool = data[index..index + requested_space]\n            .iter()\n            .all(|&byte| byte == 0);\n\n        if enough_space {\n            // Found space!\n            return Some(index);\n        }\n\n        // There was a byte in the way, find which byte caused it\n        let non_empty_byte_offset: usize = data[index..index + requested_space]\n            .iter()\n            .position(|&byte| byte != 0)\n            .expect(\"There has to be a byte in the way.\");\n\n        // Move that far forward, then try again.\n        // The index we are already on MUST be either zero, or the start of a <T>\n        // Since we already know we arent at the start of <T>, we will always jump at least\n        // one byte forwards.\n        index += non_empty_byte_offset;\n        continue;\n    }\n\n    // If we made it out of the while loop, that must mean there is not an open space.\n    None\n}\n"
  },
  {
    "path": "src/pool/disk/generic/generic_structs/mod.rs",
    "content": "pub mod find_space;\npub mod pointer_struct;\n"
  },
  {
    "path": "src/pool/disk/generic/generic_structs/pointer_struct.rs",
    "content": "/// Points to a specific block on a disk\n\n#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]\npub(crate) struct DiskPointer {\n    pub(crate) disk: u16,\n    pub(crate) block: u16,\n}\n\nimpl DiskPointer {\n    pub(crate) fn to_bytes(self) -> [u8; 4] {\n        let mut buffer: [u8; 4] = [0u8; 4];\n        buffer[..2].copy_from_slice(&self.disk.to_le_bytes());\n        buffer[2..].copy_from_slice(&self.block.to_le_bytes());\n        buffer\n    }\n    pub(crate) fn from_bytes(bytes: [u8; 4]) -> Self {\n        Self {\n            disk: u16::from_le_bytes(bytes[..2].try_into().expect(\"2 = 2\")),\n            block: u16::from_le_bytes(bytes[2..].try_into().expect(\"2 = 2\")),\n        }\n    }\n    // Random pointers for testing\n    #[cfg(test)]\n    pub(crate) fn get_random() -> Self {\n        use rand::Rng;\n        let mut random = rand::rng();\n        Self {\n            disk: random.random(),\n            block: random.random(),\n        }\n    }\n    /// Creates a new disk pointer with no destination.\n    pub(crate) fn new_final_pointer() -> Self {\n        Self {\n            disk: u16::MAX,\n            block: u16::MAX,\n        }\n    }\n    /// Check if this pointer doesn't go anywhere\n    pub(crate) fn no_destination(&self) -> bool {\n        self.disk == u16::MAX || self.block == u16::MAX\n    }\n}"
  },
  {
    "path": "src/pool/disk/generic/io/cache/cache_implementation.rs",
    "content": "// Non-public cache construction\n\n// Some details about the cache:\n// The lowest tier, 0, is completely emptied when it's full. Since we\n//  assume that the data within there is of very low quality. If it was\n//  worth keeping around, it would have been promoted already\n// Tier 1 pushes it's best cached item to tier 2 when it's full.\n// Tier 2 discards its least valuable cache item when it's full.\n// Within tiers, items are promoted to a higher position whenever a read\n//  successfully hits them. The only exception to this is tier 0, where\n//  successful reads promote an item up to tier 1.\n\n// When a new item is added to a tier, it starts in the highest position, as it\n//  is the most fresh. It is expected that if this item is weaker than pre-existing\n//  items, that the newly added top item will quickly slide down in rank.\n\n// The lower cache tiers are inherently more volatile, so they need to be\n//  larger to support more opportunities for items to promote before being\n//  thrashed out of the cache. Thus we will split the cache into:\n// 0: 1/2   of total allowed cache size\n// 1: 1/4th of total allowed cache size\n// 2: 1/4th of total allowed cache size\n// It may seem weird to make the highest tier the same size as the one below it,\n//  but items that reach this tier are now such a high quality that they would be\n//  very quickly replaced if they became stale, since the constant read hits that are\n//  expected of these items would move stale items to the lowest positions very quickly.\n\n// Promotion within tiers always moves the item from whatever index it's currently at, to\n//  the very top of the tier. This should ensure that the hottest items stay close to the\n//  top, previously I used bubble sort, which could lead to slightly less used items to\n//  not promote away from the bottom of the queue fast enough.\n\nuse std::{\n    collections::{\n        HashMap,\n        VecDeque\n    },\n    sync::Mutex\n};\n\nuse lazy_static::lazy_static;\nuse log::debug;\n\nuse crate::{\n    error_types::drive::DriveError,\n    pool::disk::{\n        drive_struct::{\n            DiskType,\n            FloppyDrive,\n        },\n        generic::{\n            block::block_structs::RawBlock,\n            disk_trait::GenericDiskMethods,\n            generic_structs::pointer_struct::DiskPointer,\n            io::{\n                cache::{\n                    cache_io::CachedBlockIO,\n                    statistics::BlockCacheStatistics\n                }\n            }\n        },\n        standard_disk::standard_disk_struct::StandardDisk\n    }, tui::{notify::NotifyTui, tasks::TaskType}\n};\n\n//\n// =========\n// GLOBAL? LOCAL? IDK\n// =========\n//\n\n// The maximum amount of blocks all caches can store\n#[cfg(test)] // Small cache on test is faster.\nconst CACHE_SIZE: usize = 2880 * 2;\n#[cfg(not(test))]\nconst CACHE_SIZE: usize = 2880 * 16;\n\n// The actual cached data\nlazy_static! {\n    static ref CASHEW: Mutex<BlockCache> = Mutex::new(BlockCache::new());\n}\n\n//\n// =========\n// STRUCTS\n// =========\n//\n\n/// The wrapper around all the cache tiers\n/// Only avalible within the cache folder,\n/// all public interfaces are built on top of CachedBlockIO.\npub(super) struct BlockCache {\n    // The different levels of cache.\n    // All of the internals are private.\n\n    /// Highest quality, items in this level came from the highest spot from the tier below when\n    /// it was completely full. IE filled with the best of level_1.\n    tier_2: TieredCache,\n    /// Might be useful, promoted from level 0 after being read at least once.\n    tier_1: TieredCache,\n    /// Unproven items, might as well be garbage.\n    tier_0: TieredCache,\n}\n\n/// The actual caches\n#[derive(Clone)]\nstruct TieredCache {\n    /// How big this cache is.\n    size: usize,\n    /// The items currently in the cache, hashmap pair\n    items_map: HashMap<DiskPointer, CachedBlock>,\n    /// Keep track of the order of items in the cache\n    order: VecDeque<DiskPointer>\n}\n\n/// The cached blocks\n/// Available in the cache folder to provide conversion methods.\n#[derive(Debug, Clone)]\npub(super) struct CachedBlock {\n    /// Where this block came from.\n    block_origin: DiskPointer,\n    /// The content of the block.\n    data: Vec<u8>,\n    /// Whether or not this block needs to be flushed.\n    /// \n    /// Blocks that are read but never written do not need to be flushed.\n    pub(super) requires_flush: bool\n}\n\n//\n// =========\n// Implementations\n// =========\n//\n\n// The entire cache\n// These functions are public to the cache folder, since we need these for read/write\nimpl BlockCache {\n    /// Create a new empty cache\n    fn new() -> Self {\n        // Get the max size of the cache\n        let size: usize = CACHE_SIZE;\n        // Need the 3 tiers\n        // Division rounds down, so this is fine.\n        let tier_0: TieredCache = TieredCache::new(size/2);\n        let tier_1: TieredCache = TieredCache::new(size/4);\n        let tier_2: TieredCache = TieredCache::new(size/4);\n        // All done\n        Self {\n            tier_0,\n            tier_1,\n            tier_2,\n        }\n    }\n\n    /// Retrieves an item from the cache if it exists.\n    /// \n    /// Updates the underlying caches to promote the read item.\n    pub(super) fn try_find(pointer: DiskPointer) -> Option<CachedBlock> {\n        go_try_find_cache(pointer, false)\n    }\n\n    /// Retrieves an item from the cache if it exists, but does not promote the item.\n    pub(super) fn try_find_silent(pointer: DiskPointer) -> Option<CachedBlock> {\n        go_try_find_cache(pointer, true)\n    }\n\n    /// Add an item to the cache, or update it if the item is already present.\n    /// \n    /// If the item is new, it will be placed in the lowest tier in the cache.\n    /// \n    /// Make sure you properly set wether the block needs flushing or not.\n    pub(super) fn add_or_update_item(item: CachedBlock) -> Result<(), DriveError> {\n        go_add_or_update_item_cache(item)\n    }\n\n    /// Get the hit-rate of the cache\n    pub(super) fn get_hit_rate() -> f64 {\n        BlockCacheStatistics::get_hit_rate()\n    }\n\n    /// Get the pressure of tier 0.\n    /// \n    /// Must drop cache before calling.\n    pub(super) fn get_pressure() -> f64 {\n        go_get_cache_pressure()\n    }\n\n    // Promotes a tier 0 cache item upwards.\n    fn promote_item(&mut self, item: CachedBlock) {\n        go_promote_item_cache(self, item)\n    }\n\n    /// Removes an item from the cache if it exists.\n    /// \n    /// You must flush this item to disk yourself (if needed), or you will lose data!\n    /// \n    /// Returns nothing.\n    pub(super) fn remove_item(pointer: &DiskPointer) {\n        go_remove_item_cache(pointer)\n    }\n\n    // /// Reserve a block on a disk, skipping the disk if possible.\n    // /// \n    // /// Panics if block was already allocated.\n    // pub(super) fn cached_block_allocation(raw_block: &RawBlock) -> Result<(), DriveError> {\n    //     let mut cache_disk: CachedAllocationDisk = CachedAllocationDisk::open(raw_block.block_origin.disk)?;\n    //     let _ = cache_disk.allocate_blocks(&vec![raw_block.block_origin.block])?;\n    //     // Shouldn't even need to check if it allocated one block, no way it could allocate more.\n    //     Ok(())\n    // }\n    \n    /// Flushes all information in a tier to disk.\n    /// \n    /// Caller must drop all references to cache before calling this.\n    pub(super) fn flush(tier_number: usize) -> Result<(), DriveError> {\n        go_flush_tier(tier_number)\n    }\n\n    /// Drops items from this cache tier that have not been updated, and thus don't need to be written to disk.\n    /// \n    /// You should really only call this on tier 0, since items in the higher tiers are usually very read heavy, thus\n    /// are usually not updated. Cleaning up those higher tiers would almost certainly discard valuable blocks.\n    /// \n    /// Caller must drop all references to cache before calling this.\n    /// \n    /// Returns how many blocks were discarded, or None if the tier was already empty.\n    pub(super) fn cleanup_tier(tier_number: usize) -> Option<u64> {\n        go_cleanup_tier(tier_number)\n    }\n\n    /// Flushes any low-importance pending writes on a selected disk.\n    /// \n    /// This should be called when you know you are about to swap disks, since\n    /// otherwise you might swap disks for a read, then immediately need to swap back\n    /// again because the cache filled up.\n    /// \n    /// Returns how many blocks were freed from the cache.\n    /// \n    /// Caller must drop all references to the cache before calling this.\n    pub(super) fn flush_a_disk(disk_number: u16) -> Result<u64, DriveError> {\n        go_flush_disk_from_cache(disk_number)\n    }\n\n    /// Find what disk is the most common in the lowest cache tier.\n    /// \n    /// Returns the disk with the most blocks on it (or picks the first one if it is a tie) and how many\n    /// blocks are from that disk. (disk, blocks)\n    /// \n    /// Panics if the tier is empty.\n    /// \n    /// You should clean-up the cache before calling this, as to get a count of only\n    /// dirty blocks.\n    /// \n    /// Caller must drop all references to the cache before calling this.\n    pub(super) fn most_common_disk() -> (u16, u16) {\n        go_find_most_common_disk()\n    }\n\n    /// Find out how much free space is in a tier\n    /// \n    /// Returns number of empty spaces in the tier\n    pub(super) fn get_tier_space(tier_number: usize) -> usize {\n        go_get_tier_free_space(tier_number)\n    }\n}\n\n// Cache tiers\nimpl TieredCache {\n    /// Create a new, empty tier of a set size\n    fn new(size: usize) -> Self {\n        go_make_new_tier(size)\n    }\n    /// Check if an item is in this tier.\n    /// \n    /// Adds a hit to the tier statistics if found, otherwise\n    /// leaves the statistics alone.\n    /// \n    /// Returns the index of the item if it exists.\n    /// \n    /// Does not update tier order.\n    fn find_item(&self, pointer: &DiskPointer) -> Option<usize> {\n        go_find_tier_item(self, pointer)\n    }\n    /// Retrieves an item from this tier at the given index.\n    /// \n    /// Will promote the item within this tier if not silent.\n    /// \n    /// Updates tier order.\n    /// \n    /// Returns None if there is no item at the index.\n    fn get_item(&mut self, index: usize, silent: bool) -> Option<&CachedBlock> {\n        go_get_tier_item(self, index, silent)\n    }\n    /// Extracts an item at an index, removing it from the tier.\n    /// \n    /// Returns None if there is no item at the index.\n    fn extract_item(&mut self, index: usize) -> Option<CachedBlock> {\n        go_extract_tier_item(self, index)\n    }\n    /// Adds an item to this tier. Will be the new highest item in the tier.\n    /// \n    /// Will panic if tier is already full.\n    fn add_item(&mut self, item: CachedBlock) {\n        go_add_tier_item(self, item)\n    }\n    /// Updates / replaces an item at a given index.\n    /// \n    /// Updates order.\n    /// \n    /// Will panic if index is empty / out of bounds.\n    fn update_item(&mut self, index: usize, new_item: CachedBlock) {\n        go_update_tier_item(self, index, new_item)\n    }\n    /// Pops the best item of the tier.\n    /// \n    /// Returns None if the tier is empty\n    fn get_best(&mut self) -> Option<CachedBlock> {\n        go_get_tier_best(self)\n    }\n    /// Pops the worst item of the tier.\n    /// \n    /// Returns None if the tier is empty\n    fn get_worst(&mut self) -> Option<CachedBlock> {\n        go_get_tier_worst(self)\n    }\n    /// Check if this tier is full\n    fn is_full(&self) -> bool {\n        go_check_tier_full(self)\n    }\n}\n\n// Nice to haves for the CachedBlocks\nimpl CachedBlock {\n    /// Turn a CachedBlock into a RawBlock\n    pub(super) fn into_raw(self) -> RawBlock {\n        RawBlock {\n            block_origin: self.block_origin,\n            data: self.data.try_into().expect(\"Should be 512 bytes.\"),\n        }\n    }\n    /// Turn a RawBlock into a CachedBlock\n    /// \n    /// Expects the raw block to already have a disk set.\n    pub(super) fn from_raw(block: &RawBlock, requires_flush: bool) -> Self {\n        Self {\n            block_origin: block.block_origin,\n            data: block.data.to_vec(),\n            requires_flush\n        }\n    }\n}\n\n//\n// =========\n// BlockCache Functions\n// =========\n//\n\nfn go_try_find_cache(pointer: DiskPointer, silent: bool) -> Option<CachedBlock> {\n\n    // Make sure this is a valid disk pointer, otherwise something is horribly wrong.\n    assert!(!pointer.no_destination(), \"Tried to find the no_destination pointer in the block cache!\");\n\n    // To prevent callers from having to lock the global themselves, we will grab it here ourselves\n    // and pass it downwards into any functions that require it.\n    let cache = &mut CASHEW.try_lock().expect(\"Single threaded.\");\n\n    // Try from highest to lowest\n    // Tier 2\n    if let Some(found) = cache.tier_2.find_item(&pointer) {\n        // In the highest rank!\n        BlockCacheStatistics::record_hit();\n        // Grab it, which will also update the order.\n        return cache.tier_2.get_item(found, silent).cloned()\n    }\n\n    // Tier 1\n    if let Some(found) = cache.tier_1.find_item(&pointer) {\n        // Somewhat common it seems.\n        BlockCacheStatistics::record_hit();\n        // Grab it, which will also update the order.\n        return cache.tier_1.get_item(found, silent).cloned()\n    }\n\n    // Tier 0\n    if let Some(found) = cache.tier_0.find_item(&pointer) {\n        // Scraping the barrel, but at least it was there!\n        BlockCacheStatistics::record_hit();\n        // Since this is the lowest tier, we need to immediately promote this if needed.\n        if !silent {\n            let item = cache.tier_0.extract_item(found).expect(\"Just checked.\");\n            cache.promote_item(item.clone());\n            return Some(item);\n        } else {\n            // Dont need to promote.\n            let read = cache.tier_0.items_map.get(&pointer).expect(\"Already checked\");\n            return Some(read.clone());\n        }\n    }\n\n    // It wasn't in the cache. Record the miss if needed.\n    if !silent {\n        BlockCacheStatistics::record_miss();\n    }\n\n    // All done.\n    None\n}\n\nfn go_promote_item_cache(cache: &mut BlockCache, t0_item: CachedBlock) {\n    // This is where the magic happens.\n\n    // Since tiers only change size or have new items added to them when tier 0 has a good read,\n    // we only have to implement a cache-wide promotion scheme for tier 0.\n\n    // See if there is room in tier 1\n    if !cache.tier_1.is_full() {\n        // There was room.\n        cache.tier_1.add_item(t0_item);\n        return\n    }\n\n    // There was not room, we need to move an item upwards.\n    let t1_best: CachedBlock = cache.tier_1.get_best().expect(\"How are we empty and full?\");\n\n    if !cache.tier_2.is_full() {\n        // not full, directly add it.\n        cache.tier_2.add_item(t1_best);\n    } else {\n        // The best cache is full.\n        // We will have to move the worst tier 2 item to tier 0. If we discarded it\n        // outright, the block it contains would never get flushed to disk.\n        let worst_of_2 = cache.tier_2.get_worst().expect(\"How are we empty and full?\");\n\n        // Since we popped an item from t0 to call this function, it must now have at least\n        // one slot open, so we can add to it.\n        cache.tier_0.add_item(worst_of_2);\n\n\n        // Now put that tier 1 item in tier 2 to make room for the new tier 1 item from tier 0.\n        // Confused yet?\n        cache.tier_2.add_item(t1_best);\n    }\n\n    // Now that tier 1 has had room made, add the t0 to t1\n    cache.tier_1.add_item(t0_item);\n\n    // All done!\n}\n\nfn go_add_or_update_item_cache(block: CachedBlock) -> Result<(), DriveError> {\n\n    // Make sure the block has a valid location\n    assert!(!block.block_origin.no_destination(), \"Attempted to add a block to the cache with a location of no_destination !\");\n\n    // We don't update the cache statistics in here, since a hit while updating makes no sense.\n\n    // To prevent callers from having to lock the global themselves, we will grab it here ourselves\n    // and pass it downwards into any functions that require it.\n    let mut cache = CASHEW.try_lock().expect(\"Single threaded.\");\n\n    // Since we search for the item in every tier before adding, this prevents duplicates.\n\n    // Top to bottom.\n\n    if let Some(index) = cache.tier_2.find_item(&block.block_origin) {\n        // Fancy block!\n        cache.tier_2.update_item(index, block);\n        return Ok(())\n    }\n\n    if let Some(index) = cache.tier_1.find_item(&block.block_origin) {\n        // Useful!\n        cache.tier_1.update_item(index, block);\n        return Ok(())\n    }\n\n    // Annoyingly, we still have to update the garbage, since reading presumes that stuff in tier 0 is up to date.\n\n    if let Some(index) = cache.tier_0.find_item(&block.block_origin) {\n        // Polished garbage.\n        cache.tier_0.update_item(index, block);\n        return Ok(())\n    }\n\n    // It wasn't in any of the tiers, so we will add it to tier 0.\n    \n    // Make sure we have room first\n    // Hold onto the size of the tier\n    let tier_0_size = cache.tier_0.size;\n    if cache.tier_0.is_full() {\n        debug!(\"Tried adding new block to cache, but cache is full. Cleaning up tier 0...\");\n        // We don't have room, so we need to flush out tier 0 of the cache.\n        // But first we can try dropping items that do not require flushing\n        drop(cache);\n        if BlockCache::cleanup_tier(0).is_none() {\n            // Nothing to cleanup, need to write data. Try the current disk first.\n            debug!(\"Cleanup wasn't enough, flushing current disk...\");\n            // We want to flush at least a quarter of the cache teir, otherwise we start thrashing\n            // the cache, wasting time.\n            let blocks_required = tier_0_size as u64 / 4;\n            let blocks_freed = BlockCache::flush_a_disk(FloppyDrive::currently_inserted_disk_number())?;\n            if blocks_freed < blocks_required {\n                // Didn't flush enough from the first disk, pick the best disk and flush that next.\n                let (most_common_disk, blocks_for_common) = BlockCache::most_common_disk();\n\n                // Would that free enough space?\n                if blocks_for_common as u64 + blocks_freed < blocks_required {\n                    // That still wouldn't be enough. Do a full flush.\n                    BlockCache::flush(0)?;\n                } else {\n                    // That will make enough room, flush that common disk.\n                    let _ = BlockCache::flush_a_disk(most_common_disk)?;\n                }\n            }\n        }\n\n\n        let cache: &mut std::sync::MutexGuard<'_, BlockCache> = &mut CASHEW.try_lock().expect(\"Single threaded.\");\n        cache.tier_0.add_item(block);\n\n        \n        return Ok(());\n    }\n    \n    // Put it in\n    cache.tier_0.add_item(block);\n    drop(cache);\n    \n    // Update the hit rate\n    NotifyTui::set_cache_hit_rate(BlockCache::get_hit_rate());\n    // Update the cache pressure\n    NotifyTui::set_cache_pressure(BlockCache::get_pressure());\n    Ok(())\n}\n\nfn go_remove_item_cache(pointer: &DiskPointer) {\n    // If we just find and extract on every tier, that works\n    // Slow? Maybe...\n    // To prevent callers from having to lock the global themselves, we will grab it here ourselves\n    // and pass it downwards into any functions that require it.\n    let cache = &mut CASHEW.try_lock().expect(\"Single threaded.\");\n\n    // Since we are clearing just one item, not a whole disk, we only need to check each tier once, since there\n    // cant be any duplicates, and we can return as soon as we see a matching item.\n\n    if let Some(index) = cache.tier_2.find_item(pointer) {\n        // We discard the removed item. We assume the caller already\n        // grabbed their own copy if they needed it.\n        let _ = cache.tier_2.extract_item(index);\n        return\n    }\n\n    if let Some(index) = cache.tier_1.find_item(pointer) {\n        let _ = cache.tier_1.extract_item(index);\n        return\n    }\n\n    if let Some(index) = cache.tier_0.find_item(pointer) {\n        let _ = cache.tier_0.extract_item(index);\n    }\n\n}\n\n//\n// =========\n// TieredCache Functions\n// =========\n//\n\n\nfn go_make_new_tier(size: usize) -> TieredCache {\n    // New tiers are obviously empty.\n    let mut new_hashmap: HashMap<DiskPointer, CachedBlock> = HashMap::with_capacity(size);\n    new_hashmap.shrink_to(size);\n    let mut new_order: VecDeque<DiskPointer> = VecDeque::with_capacity(size);\n    new_order.shrink_to(size);\n    TieredCache {\n        size,\n        items_map: new_hashmap,\n        order: new_order\n    }\n}\n\nfn go_find_tier_item(tier: &TieredCache, pointer: &DiskPointer) -> Option<usize> {\n    // Does not update order\n    // Just see if it exists.\n\n    // Skip if the tier is empty\n    if tier.order.is_empty() {\n        return None;\n    }\n\n    // We check the order, because we care about index here, not the actual block.\n    tier.order.iter().position(|x| x == pointer)\n}\n\nfn go_get_tier_item(tier: &mut TieredCache, index: usize, silent: bool) -> Option<&CachedBlock> {\n    // Updates order if non-silent\n    if !silent {\n        // Find what item the index refers to\n        let wanted_block_pointer: DiskPointer = tier.order.remove(index)?;\n\n        // Now get that item\n        let the_block = tier.items_map.get(&wanted_block_pointer)?;\n\n        // Now move the item to the front of the tier\n        tier.order.push_front(wanted_block_pointer);\n\n        Some(the_block)\n    } else {\n        // Silent operation, we just need to read it.\n        let wanted_pointer = tier.order.get(index)?;\n        let wanted_block = tier.items_map.get(wanted_pointer)?;\n        Some(wanted_block)\n    }\n}\n\nfn go_extract_tier_item(tier: &mut TieredCache, index: usize) -> Option<CachedBlock> {\n    // Pops an item from any index, preserves order of other items\n\n    // Find the item\n    let wanted_block_pointer: DiskPointer = tier.order.remove(index)?;\n\n    // Go get it\n    tier.items_map.remove(&wanted_block_pointer)\n}\n\nfn go_add_tier_item(tier: &mut TieredCache, item: CachedBlock) {\n    // New tier items go at the front, since they are the freshest.\n    assert!(!tier.is_full(), \"Tried to add an item to a tier that is already full!\");\n\n    // Put the pointer into the ordering\n    tier.order.push_front(item.block_origin);\n\n    // Add to the hashmap\n    let already_existed = tier.items_map.insert(item.block_origin, item);\n\n    // Make sure that did not already exist\n    assert!(already_existed.is_none(), \"Item added to the tier was a duplicate!\");\n}\n\nfn go_update_tier_item(tier: &mut TieredCache, index: usize, new_item: CachedBlock) {\n    // Replace the item, IE the contents of the block have changed.\n\n    // If the contents have changed, the new item MUST have the flush bool set.\n    assert!(new_item.requires_flush, \"Incoming update item for tier did not have the flush bit set!\");\n\n    // Updating is an access after all... so we will promote it.\n\n    // Update the order\n    let to_move = tier.order.remove(index).expect(\"Provided index into the tier should be valid.\");\n    tier.order.push_front(to_move);\n\n    // Now replace the item in the hashmap at the index.\n    let replaced = tier.items_map.insert(to_move, new_item);\n    \n    // Make sure we actually replaced it. Not adding here!\n    assert!(replaced.is_some(), \"Tier item we were trying to update wasn't there!\");\n}\n\nfn go_get_tier_best(tier: &mut TieredCache) -> Option<CachedBlock> {\n    // Best is at the front\n\n    // Get the pointer\n    let front_pointer = tier.order.pop_front()?;\n\n    // Get the block\n    // This will return an option, its the callers fault if this item does not exist.\n    tier.items_map.remove(&front_pointer)\n}\n\nfn go_get_tier_worst(tier: &mut TieredCache) -> Option<CachedBlock> {\n    // The worst item is at the end of the vec\n    \n    // Get the pointer\n    let front_pointer = tier.order.pop_back()?;\n\n    // Get the block\n    // This will return an option, its the callers fault if this item does not exist.\n    tier.items_map.remove(&front_pointer)\n}\n\nfn go_flush_tier(tier_number: usize) -> Result<(), DriveError> {\n    debug!(\"Flushing tier {tier_number} of the cache...\");\n    let handle = NotifyTui::start_task(TaskType::FlushTier, 2);\n    // We will be flushing all data from this tier of the cache to disk.\n    // This can be used on any tier, but will usually be called on tier 0.\n\n    // Run tier cleanup first to remove anything that doesn't need to be written.\n    // Don't care how many blocks are cleaned up.\n    let _ = go_cleanup_tier(tier_number);\n    NotifyTui::complete_task_step(&handle);\n    \n    // We will extract all of the cache items at once, leaving the tier empty.\n    let items_map_to_flush: HashMap<DiskPointer, CachedBlock>;\n    let items_order_to_flush: VecDeque<DiskPointer>;\n    // We only get the order just to discard it.\n    \n    // Keep the cache locked within just this area.\n    {\n        // Get the block cache\n        let mut cache = CASHEW.try_lock().expect(\"Single threaded.\");\n        \n        // find the tier we need to flush\n        let tier_to_flush: &mut TieredCache = match tier_number {\n            0 => &mut cache.tier_0,\n            1 => &mut cache.tier_1,\n            2 => &mut cache.tier_2,\n            _ => panic!(\"Tried to access a non-existent cache tier!\"),\n        };\n        \n        // If the tier is empty, there's nothing to do.\n        if tier_to_flush.order.is_empty() {\n            return Ok(());\n        }\n        \n        // Move all items from the tier into our local variable,\n        // leaving the cache's tier empty.\n        \n        // In theory, if the flush fails, we would now lose data...\n        // just dont fail lol, good luck\n        \n        items_map_to_flush = std::mem::take(&mut tier_to_flush.items_map);\n        items_order_to_flush = std::mem::take(&mut tier_to_flush.order);\n    }\n    \n    let _ = items_order_to_flush;\n    \n    // Cache is now unlocked\n    NotifyTui::complete_task_step(&handle);\n    \n    // first we grab all of the items and sort them by disk, low to high, and also sort the blocks\n    // within those disks to be in order. Since if the blocks are in order, the head doesn't have to move around\n    // the disk as much.\n    \n    // Get the items from the hashmap\n    let mut items: Vec<CachedBlock> = items_map_to_flush.into_values().collect();\n\n    // Before sorting, we can toss any blocks that do not have flush set, since\n    // they were never updated and thus don't need to be written back to disk.\n    items.retain(|block| block.requires_flush);\n\n    // If we ended up with no items, that means the tier was completely filled with items\n    // that did not need to be flushed, and we can exit early.\n    if items.is_empty() {\n        // Cool\n        return Ok(());\n    }\n\n    // There are still items in here, we have work to do.\n\n    // Sort the blocks we will actually be writing to put the same disks in order, then by block order.\n    items.sort_unstable_by_key(|item| (item.block_origin.disk, item.block_origin.block));\n    \n    // Now to reduce head movement even further, we don't want to check the allocation table\n    // while making our writes. Since that would require seeking to block 0 after each write.\n    \n    // You might be thinking, \"Why can't we use the cache for the allocation tables?\", darn good idea,\n    // but we cannot access the cache from down here, since that would require locking the entire cache\n    // a second time. Also we might be out of room in the cache for the read required to get the table,\n    // which would cause us to flush the tier again, which we are already doing. Bad news.\n    \n    // But there are some assumptions we can make about the items we are flushing:\n    // - We assume the items within the cache are valid. (A given, but can't hurt to mention)\n    // - If an item is contained within a cache tier, the block it came from must\n    //    be allocated, and moreover, unchanged since the last time we flushed to it.\n    // - We currently have full control over the floppy disk. Since all high-level\n    //    IO happens on the cache itself, we can swap disks and even finish on a\n    //    completely different disk without worrying about other callers.\n    // - - Furthermore, since we have full control over the disk, the allocation tables\n    //      cannot be changing.\n    // - When an item is removed from the cache manually, it must have been flushed to disk.\n    // - Invalidated items on cache levels higher than 0 will put their invalidated item into\n    //    tier zero, thus they will be flushed to disk when it is cleared.\n    \n    // Basically, we don't have to care about the allocation table AT ALL down here. If\n    // we have a block, we know it is allocated. When a block is freed, it must be removed\n    // from the cache entirely.\n    \n    // Therefore, we can make all of our writes in one pass per disk, and never have to look at\n    // the allocation table at all!\n    \n    // To properly allow lazy-loading disks into the drive, we allow the disk loading routine to use cached blocks\n    // if they exist.\n    \n    // The problem is, this causes the disk check to always return true if the header is in the cache, meaning\n    // in theory, an incorrect disk can be in the drive.\n    \n    // To solve this, down here we must grab the header from the cache if it is there, then \n    // we hold onto that, load the disk (which now has to do a proper block read to check if its the right disk), then\n    // update the disk if its the correct one.\n\n    // This is the only place that actual disk writes ever happen in normal operation outside of disk initialization.\n    \n    // Open the first disk to write to\n    \n    \n    // Now we can chunk together the blocks into larger continuous writes for speed.\n    // First chunk by disk\n    let chunked_by_disk: Vec<Vec<CachedBlock>> = items\n        .chunk_by(|a, b| b.block_origin.disk == a.block_origin.disk)\n        .map(|block| block.to_vec()).collect();\n    \n    NotifyTui::add_steps_to_task(&handle, chunked_by_disk.len() as u64);\n    \n    // Now we can loop over the disks\n    for disk_chunk in chunked_by_disk {\n        // open the disk\n        let mut current_disk: StandardDisk = disk_load_header_invalidation(disk_chunk[0].block_origin.disk)?;\n        \n        // Now chunk together the blocks.\n        // Comparison adds instead of subtracts to prevent overflow.\n        let chunked_by_block: Vec<Vec<CachedBlock>> = disk_chunk\n        .chunk_by(|a, b| b.block_origin.block == a.block_origin.block + 1)\n        .map(|block| block.to_vec()).collect();\n    \n    \n        NotifyTui::add_steps_to_task(&handle, chunked_by_block.len() as u64);\n        // Now loop over those.\n        for block_chunk in chunked_by_block {\n            // If this chunk only has one item in it, do a normal write.\n            if block_chunk.len() == 1 {\n                // Unchecked due to cached headers.\n                current_disk.unchecked_write_block(&block_chunk[0].clone().into_raw())?;\n                NotifyTui::complete_task_step(&handle);\n                continue;\n            }\n            \n            // There are multiple blocks in a row to update, we need to stitch their bytes together.\n            let bytes_to_write: Vec<u8> = block_chunk.iter().flat_map(|block| block.data.clone()).collect();\n            \n            // Now do the large write.\n            // Unchecked since the headers for the disk may still be in the cache.\n            current_disk.unchecked_write_large(bytes_to_write, block_chunk[0].block_origin)?;\n            NotifyTui::complete_task_step(&handle);\n        }\n        NotifyTui::complete_task_step(&handle);\n    }\n    \n    // All done, don't need to do any cleanup for previously stated reasons\n    debug!(\"Done flushing tier {tier_number} of the cache.\");\n\n    // Let the TUI know\n    NotifyTui::cache_flushed();\n    NotifyTui::finish_task(handle);\n    \n    Ok(())\n}\n\n// Returns an option on if any blocks were freed, and how many.\nfn go_cleanup_tier(tier_number: usize) -> Option<u64> {\n    // Discard all items in this tier that don't need to be written back to disk.\n    debug!(\"Cleaning up tier {tier_number} of the cache...\");\n\n    // Usually I would scope the cache, but we'll be doing these operations without touching the disk.\n\n    // Get the block cache\n    let mut cache = CASHEW.try_lock().expect(\"Single threaded.\");\n    \n    // find the tier we need to flush\n    let tier_to_flush: &mut TieredCache = match tier_number {\n        0 => &mut cache.tier_0,\n        1 => &mut cache.tier_1,\n        2 => &mut cache.tier_2,\n        _ => panic!(\"Tried to access a non-existent cache tier!\"),\n    };\n    \n    // If the tier is empty, there's nothing to do.\n    if tier_to_flush.order.is_empty() {\n        return None;\n    }\n\n    // Now go through all the tier items and check if we can discard them.\n\n    let mut blocks_discarded: u64 = 0;\n    \n    let blocks_to_cleanup_map = &mut tier_to_flush.items_map;\n    let blocks_to_cleanup_order = &mut tier_to_flush.order;\n\n    // To be clever, we can use retain, and only retain the items that do need to be written, otherwise discarding\n    // the blocks we dont need as we come across them.\n    blocks_to_cleanup_order.retain(|pointer| {\n        // Get the block from the hashmap\n        let block = blocks_to_cleanup_map.get(pointer).expect(\"If there's a key in, there should be a block.\");\n        if block.requires_flush {\n            // This needs to be flushed, so we return true to hold onto this block.\n            return true; // Weird that return works in here, never seen that before.\n        }\n        // Block does not need to be flushed! Discard it.\n        let _ = blocks_to_cleanup_map.remove(pointer);\n\n        // Increment the discard count\n        blocks_discarded += 1;\n\n        // Return false to discard this pointer from the order vec\n        false\n    });\n\n    // Unneeded blocks have now been discarded.\n    \n    // If we weren't able to free anything, we still need to return None here.\n    if blocks_discarded == 0 {\n        debug!(\"All blocks in tier require flushing to disk.\");\n        return None;\n    }\n    \n    debug!(\"Dropped {blocks_discarded} un-needed blocks from the tier.\");\n\n    // Now is a good time to update the hit rate of the TUI, since the hit rate must have decreased\n    NotifyTui::set_cache_hit_rate(BlockCache::get_hit_rate());\n\n    Some(blocks_discarded)\n}\n\n/// Flush all blocks in tier 0 that correspond to a certain disk.\n/// \n/// This should be called before disk swaps to prevent needing to immediately swap back to\n/// flush the cache.\nfn go_flush_disk_from_cache(disk_number: u16) -> Result<u64, DriveError> {\n    // Pull out the tier items we need.\n    let handle = NotifyTui::start_task(TaskType::FlushCurrentDisk, 1);\n    debug!(\"Flushing cached content of disk {disk_number}...\");\n    \n    // Get the block cache\n    let mut cache = CASHEW.try_lock().expect(\"Single threaded.\");\n    \n    // get tier 0\n    let tier_0: &mut TieredCache = &mut cache.tier_0;\n    \n    // If the tier is already empty, there's nothing to do.\n    if tier_0.order.is_empty() {\n        NotifyTui::cancel_task(handle);\n        return Ok(0);\n    }\n    \n    // Now work our way through the cache, grabbing anything related to the current disk.\n    // Extract it if it refers to the correct disk,\n    // Ignore the block if it does not require flushing.\n    // - We discard it ourselves here since those reads might still be useful, so cleaning up here\n    //   Might be too early.\n    let clone_to_flush: HashMap<DiskPointer, CachedBlock> = tier_0.items_map.clone()\n        .extract_if(|pointer, block| pointer.disk == disk_number && block.requires_flush)\n        .collect();\n\n    // Split that into pointers and blocks.\n    // We take a clone of them, since we wanna actually delete them later, in case the flush fails.\n\n    let cloned_pointers_to_discard: Vec<DiskPointer> = clone_to_flush.clone().keys().cloned().collect();\n    let mut cloned_blocks_to_flush: Vec<CachedBlock> = clone_to_flush.clone().into_values().collect();\n\n    // We're done working with the cache.\n    let _ = tier_0;\n    drop(cache);\n\n    // Exit early if we dont have anything\n    if cloned_blocks_to_flush.is_empty() {\n        debug!(\"Nothing to flush from this disk.\");\n        return Ok(0);\n    }\n\n    // Debug how many blocks we're about to flush\n    debug!(\"Writing {} blocks to disk...\", cloned_blocks_to_flush.len());\n\n    // Sort the blocks\n    cloned_blocks_to_flush.sort_unstable_by_key(|block| block.block_origin.block);\n    \n    // Chunk the blocks for faster writes\n    let chunked_blocks: Vec<Vec<CachedBlock>> = cloned_blocks_to_flush\n        .chunk_by(|a, b| b.block_origin.block == a.block_origin.block + 1)\n        .map(|block| block.to_vec()).collect();\n\n    // open the disk we're writing to\n    let mut disk: StandardDisk = disk_load_header_invalidation(disk_number)?;\n\n    // Now loop over those.\n\n    NotifyTui::add_steps_to_task(&handle, chunked_blocks.len() as u64);\n    NotifyTui::complete_task_step(&handle);\n    for block_chunk in chunked_blocks {\n        // If this chunk only has one item in it, do a normal write.\n        if block_chunk.len() == 1 {\n            disk.unchecked_write_block(&block_chunk[0].clone().into_raw())?;\n            NotifyTui::complete_task_step(&handle);\n            continue;\n        }\n        \n        // There are multiple blocks in a row to update, we need to stitch their bytes together.\n        let bytes_to_write: Vec<u8> = block_chunk.iter().flat_map(|block| block.data.clone()).collect();\n        \n        // Now do the large write.\n        // Unchecked since the headers for the disk may still be in the cache.\n        disk.unchecked_write_large(bytes_to_write, block_chunk[0].block_origin)?;\n        NotifyTui::complete_task_step(&handle);\n    }\n    debug!(\"Flushing disk from cache complete.\");\n    NotifyTui::finish_task(handle);\n\n    // Now that the writes are done, actually remove the blocks from the cache. If we removed them earlier\n    // and any of these operations failed, we would lose data.\n\n    // Get the cache/tier back\n    let mut cache = CASHEW.try_lock().expect(\"Single threaded.\");\n    let tier_0: &mut TieredCache = &mut cache.tier_0;\n\n    // Toss the blocks\n    let _: HashMap<DiskPointer, CachedBlock> = tier_0.items_map\n        .extract_if(|pointer, block| pointer.disk == disk_number && block.requires_flush)\n        .collect();\n\n    // Then discard all of the sorting information about those blocks\n    tier_0.order.retain(|order| !cloned_pointers_to_discard.contains(order));\n\n    // Done with cache.\n    let _ = tier_0;\n    drop(cache);\n\n    // Update the hit rate of the cache, might as well.\n    NotifyTui::set_cache_hit_rate(BlockCache::get_hit_rate());\n\n    // All done.\n    Ok(cloned_blocks_to_flush.len() as u64)\n}\n\nfn go_check_tier_full(tier: &TieredCache) -> bool {\n    tier.order.len() == tier.size\n}\n\nfn go_find_most_common_disk() -> (u16, u16) {\n    // Hash map to make counting the disks easier, since there can be holes\n    let mut disks: HashMap<u16, u16> = HashMap::new();\n\n    // Get the block cache\n    let cache = CASHEW.try_lock().expect(\"Single threaded.\");\n    \n    // get tier 0\n    let tier_0: &TieredCache = &cache.tier_0;\n\n    // Tally up the blocks\n    for i in &tier_0.order {\n        if let Some(block_count) = disks.get_mut(&i.disk) {\n            // Increment\n            *block_count += 1;\n        } else {\n            // disk is not in hashmap yet\n            let _ = disks.insert(i.disk, 1);\n        }\n    }\n\n    // Now get the best disk\n    disks.drain().max_by_key(|pair| pair.1).expect(\"Should only be called on non-empty tiers.\")\n}\n\nfn go_get_cache_pressure() -> f64 {\n    // Get the block cache\n    let cache = CASHEW.try_lock().expect(\"Single threaded.\");\n    cache.tier_0.order.len() as f64 / cache.tier_0.size as f64\n}\n\nfn go_get_tier_free_space(tier_number: usize) -> usize {\n    // Open that tier\n    let cache = CASHEW.try_lock().expect(\"Single threaded.\");\n    let tier_to_check: &TieredCache = match tier_number {\n        0 => &cache.tier_0,\n        1 => &cache.tier_1,\n        2 => &cache.tier_2,\n        _ => panic!(\"Tried to access a non-existent cache tier!\"),\n    };\n\n    tier_to_check.size - tier_to_check.items_map.len()\n}\n\n/// Function for handling the possibility of cached disk headers.\n/// This can only be used in the cache.\n/// \n/// This should be used in place of direct disk opening to ensure headers are up to date.\npub(in super::super::cache) fn disk_load_header_invalidation(disk_number: u16) -> Result<StandardDisk, DriveError> {\n    // Try to find the header for this disk in the cache\n\n    let header_pointer: DiskPointer = DiskPointer {\n        disk: disk_number,\n        block: 0,\n    };\n\n    // If the header is already cached, and is not dirty, we don't need to update the underlying disk.\n\n    if let Some(is_dirty) = CachedBlockIO::status_of_cached_block(header_pointer) {\n        if is_dirty {\n            // Header needs to be written to the disk real quick\n            // Grab the header from the cache.\n            let header_block = CachedBlockIO::read_block(header_pointer)?;\n            // Remove it\n            CachedBlockIO::remove_block(&header_pointer);\n\n            // Now write that to the disk\n#           [allow(deprecated)] // This is being used for the cache.\n            let mut disk: StandardDisk = match FloppyDrive::open(disk_number)? {\n                DiskType::Standard(standard_disk) => DiskType::Standard(standard_disk),\n                _ => unreachable!(\"Cache cannot be used for pool disks.\"),\n            }.try_into().expect(\"Must be standard.\");\n\n            disk.unchecked_write_block(&header_block)?;\n\n            // Disk is now out of date, we will toss it, then it will be opened again below.\n            drop(disk);\n        }\n    } \n\n    // Header is not cached, or is not dirty. Or we have now written the updated header back to disk.\n    #[allow(deprecated)] // This is being used for the cache.\n    let outgoing = match FloppyDrive::open(disk_number)? {\n        DiskType::Standard(standard_disk) => DiskType::Standard(standard_disk),\n        _ => unreachable!(\"Cache cannot be used for pool disks.\"),\n    };\n    Ok(outgoing.try_into().expect(\"Must be standard\"))\n}"
  },
  {
    "path": "src/pool/disk/generic/io/cache/cache_io.rs",
    "content": "// External interaction with the block cache\n\nuse crate::{\n    error_types::drive::DriveError,\n    pool::disk::{\n        drive_struct::FloppyDrive,\n        generic::{\n            block::{\n                allocate::block_allocation::BlockAllocation,\n                block_structs::RawBlock\n            },\n            disk_trait::GenericDiskMethods,\n            generic_structs::pointer_struct::DiskPointer,\n            io::cache::{\n                cache_implementation::{\n                    BlockCache,\n                    CachedBlock\n                },\n            cached_allocation::CachedAllocationDisk\n        }\n    }, standard_disk::standard_disk_struct::StandardDisk\n}, tui::notify::NotifyTui};\n\n//\n// =========\n// Structs\n// =========\n//\n\n\n/// Struct for implementing cache methods on.\n/// Holds no information, this is just for calling.\npub struct CachedBlockIO {\n   // m tea\n}\n\n// Cache methods\nimpl CachedBlockIO {\n    // /// Sometimes you need to forcibly write a disk during initialization procedures, so we need a bypass.\n    // /// \n    // /// This will ensure the correct disk is in the drive, and the header is properly up to date before\n    // /// writing anything.\n    // /// \n    // /// !! == DANGER == !!\n    // /// \n    // /// This function should ONLY be used when initializing disks, since this does not properly update the cache.\n    // /// The information written with this function will not be written to cache, nor will the information about this\n    // /// disk be flushed from the cache.\n    // /// \n    // /// This function also does not update the allocation table.\n    // /// \n    // /// You better know what you're doing.\n    // /// \n    // /// !! == DANGER == !!\n    // pub fn forcibly_write_a_block(raw_block: &RawBlock) -> Result<(), DriveError> {\n    //     go_force_write_block(raw_block)\n    // }\n\n    // /// Attempts to read a block from the cache, does not load from disk if not present.\n    // /// \n    // /// Returns the block if present, or None if absent.\n    // pub fn try_read(block_origin: DiskPointer) -> Option<RawBlock> {\n    //     if let Some(cached) = BlockCache::try_find(block_origin) {\n    //         // Was there!\n    //         // Tell the TUI\n    //         NotifyTui::read_cached();\n    //         return Some(cached.into_raw())\n    //     }\n    //     // Missing.\n    //     None\n    // }\n\n    \n    /// Check if a block is in the cache, and if it is dirty or not.\n    /// \n    /// Returns Some(true) if the block is dirty, false if clean, or None if the block is absent.\n    pub fn status_of_cached_block(block_origin: DiskPointer) -> Option<bool> {\n        if let Some(cached) = BlockCache::try_find(block_origin) {\n            return Some(cached.requires_flush)\n        }\n        // Missing.\n        None\n    }\n\n    /// Reads in a block from disk, attempts to read it from the cache first.\n    /// \n    /// Block must already be allocated on origin disk.\n    /// \n    /// Only works on standard disks.\n    pub fn read_block(block_origin: DiskPointer) -> Result<RawBlock, DriveError> {\n        go_read_cached_block(block_origin)\n    }\n\n    // /// Writes a block to disk. Adds newly written block to cache.\n    // /// \n    // /// Block must not be allocated on destination disk, will allocate on write.\n    // /// \n    // /// Only works on standard disks.\n    // pub fn write_block(raw_block: &RawBlock) -> Result<(), DriveError> {\n    //     go_write_cached_block(raw_block)\n    // }\n\n    /// Updates pre-existing block on disk, updates cache.\n    /// \n    /// Block must be already allocated on the destination disk.\n    /// \n    /// Only works on standard disks.\n    pub fn update_block(raw_block: &RawBlock) -> Result<(), DriveError> {\n        go_update_cached_block(raw_block)\n    }\n\n    /// Sometimes you just need to remove a block from the cache, not even set it to zeros.\n    /// \n    /// You MUST flush the block you are passing in before calling this function (if needed), or you WILL lose data!\n    pub fn remove_block(block_origin: &DiskPointer) {\n        BlockCache::remove_item(block_origin)\n    }\n\n    /// Flush the entire cache to disk.\n    pub fn flush() -> Result<(), DriveError> {\n        // There are currently 3 tiers of cache.\n        // ! If that changes, this must be updated !\n        // ! or there will be unflushed data still !\n        BlockCache::flush(0)?;\n        BlockCache::flush(1)?;\n        BlockCache::flush(2)\n    }\n}\n\n//\n// =========\n// CachedBlockIO functions\n// =========\n//\n\n\n// This function also updates the block order after the read.\nfn go_read_cached_block(block_location: DiskPointer) -> Result<RawBlock, DriveError> {\n    // Grab the block from the cache if it exists.\n\n    // Block must be allocated.\n    // Unless it is a header, which are always allocated.\n    // If we check for header allocation, we would try to open the header for the allocation check, to check if the header is allocated,\n    // which would recurse and overflow the stack.\n    if block_location.block != 0 {\n        // This isn't a header.\n        let is_allocated = CachedAllocationDisk::open(block_location.disk)?.is_block_allocated(block_location.block);\n        assert!(is_allocated, \"Tried to use the cache to read a block that was not allocated!\");\n    }\n    \n    let disk_in_drive = FloppyDrive::currently_inserted_disk_number();\n    \n    if let Some(found_block) = BlockCache::try_find(block_location) {\n        // It was in the cache! Return the block...\n\n        // Notify the TUI\n        NotifyTui::read_cached();\n\n        // If we would've swapped disks, also increment that\n        if disk_in_drive != block_location.disk {\n            NotifyTui::swap_saved();\n        }\n\n        return Ok(found_block.into_raw());\n    }\n\n    \n    // The block was not in the cache, we need to go get it old-school style.\n    // If we are about to swap disks, we will flush tier 0 of the disk.\n    if disk_in_drive != block_location.disk {\n        // About to swap, do the flush.\n        // Dont care how many blocks this flushes.\n        let _ = BlockCache::flush_a_disk(disk_in_drive)?;\n    };\n\n    // Now that the cache was flushed (if needed), do the read.\n    let disk: StandardDisk = super::cache_implementation::disk_load_header_invalidation(block_location.disk)?;\n\n    // We prefer to read at least 96 blocks, if the extra blocks dont fit, we just discard them.\n    // If that fails somehow, we will try just a standard single block read as a fallback.\n    // We also check to make sure we got something back, otherwise we have to fall back to the other read style.\n\n    // But if we don't have room for 96 blocks, we will read as many as we can fit.\n    let tier_free_space = BlockCache::get_tier_space(0);\n    let to_read = std::cmp::min(tier_free_space, 96);\n\n    if let Ok(blocks) = &disk.unchecked_read_multiple_blocks(block_location.block, to_read as u16) && !blocks.is_empty() {\n        for block in blocks {\n            // If the block is already in the cache, we skip adding it, since\n            // it may have been updated already.\n            // Silent, or we would be randomly promoting blocks.\n            if BlockCache::try_find_silent(block.block_origin).is_some() {\n                // Skip\n                continue;\n            }\n\n            // Add it to the cache, since the block doesn't exist yet.\n            BlockCache::add_or_update_item(CachedBlock::from_raw(block, false))?;\n        }\n        // We have to cast back and forth to clone it. Lol.\n        let silly: RawBlock = CachedBlock::from_raw(&blocks[0], false).into_raw();\n        return Ok(silly)\n    }\n\n    // Need to do a singular read.\n    // Already checked if it was allocated.\n    let read_block = disk.unchecked_read_block(block_location.block)?;\n    \n    // Add it to the cache.\n    // This is a block read from disk, so we do not set the flush flag.\n    BlockCache::add_or_update_item(CachedBlock::from_raw(&read_block, false))?;\n\n    // Return the block.\n    Ok(read_block)\n}\n\n// fn go_write_cached_block(raw_block: &RawBlock) -> Result<(), DriveError> {\n//     // Write a block to the disk, also updating the cache with the block (or adding it if it does not yet exist.)\n// \n//     // The cache expects the block's destination to be allocated already, so we will allocate it here.\n//     // We want to use the cache for this allocation if at all possible.\n//     BlockCache::cached_block_allocation(raw_block)?;\n// \n//     // Update the cache with the updated block.\n//     // This is a write, so this will need to be flushed.\n//     BlockCache::add_or_update_item(CachedBlock::from_raw(raw_block, true))?;\n// \n//     // We don't need to write, since the cache will do it for us.\n// \n//     // Notify the TUI\n//     NotifyTui::write_cached();\n// \n//     Ok(())\n// }\n\nfn go_update_cached_block(raw_block: &RawBlock) -> Result<(), DriveError> {\n    // Update like windows, but better idk this joke sucks lmao\n\n    // We have to skip the allocation check if we are attempting to update the header, otherwise\n    // this will recuse and overflow the stack\n\n    if raw_block.block_origin.block != 0 {\n        // This is not a header.\n        // Make sure block is currently allocated.\n        let is_allocated = CachedAllocationDisk::open(raw_block.block_origin.disk)?.is_block_allocated(raw_block.block_origin.block);\n        assert!(is_allocated, \"Tried to use the cache to update a block that was not allocated!\");\n    }\n\n    // Update the cache with the updated block.\n    // This is an update, so it must be flushed, since the block has changed.\n    BlockCache::add_or_update_item(CachedBlock::from_raw(raw_block, true))?;\n\n    // Notify the TUI\n    NotifyTui::write_cached();\n\n    // We don't need to write, since the cache will do it for us on flush.\n    Ok(())\n}\n\n// /// Forcibly writes a block to disk immediately, bypasses the cache.\n// fn go_force_write_block(raw_block: &RawBlock) -> Result<(), DriveError> {\n//     // Load in the disk to write to, ensuring that the header is up to date.\n//     let mut disk: StandardDisk = super::cache_implementation::disk_load_header_invalidation(raw_block.block_origin.disk)?;\n//     disk.unchecked_write_block(raw_block)\n// }"
  },
  {
    "path": "src/pool/disk/generic/io/cache/cached_allocation.rs",
    "content": "// Sidestep the disk if possible when marking a block as allocated.\n\nuse log::error;\n\nuse crate::{\n    error_types::drive::DriveError,\n    pool::disk::{\n        generic::{\n            block::{\n                allocate::block_allocation::BlockAllocation,\n                block_structs::RawBlock\n            },\n            generic_structs::pointer_struct::DiskPointer,\n            io::cache::cache_io::CachedBlockIO\n        },\n        standard_disk::block::header::header_struct::StandardDiskHeader\n    }\n};\n\n// To not require a rewrite of pool block allocation logic, we will make fake disks for it to use.\npub(crate) struct CachedAllocationDisk {\n    /// The header of the disk we are imitating\n    imitated_header: StandardDiskHeader,\n    /// If anything has changed about the disk while we were using it, it\n    /// needs to be updated in the cache. Otherwise, we would always update\n    /// it even for simple non-mutating checks against the header.\n    was_updated: bool,\n}\n\nimpl CachedAllocationDisk {\n    /// Attempt to create a new cached disk for allocation.\n    /// \n    /// This only works if the header for this disk is currently in the cache.\n    /// \n    /// To flush the new allocation table to the cache, this needs to be dropped.\n    /// Thus, if you allocate then immediately write, you need to drop this before the write.\n    pub(crate) fn open(disk_number: u16) -> Result<Self, DriveError> {\n        // Go get the header for this disk. Usually this is cached, but\n        // will fall through if needed.\n        let header_pointer: DiskPointer = DiskPointer {\n            disk: disk_number,\n            block: 0,\n        };\n\n        let read: RawBlock = CachedBlockIO::read_block(header_pointer)?;\n        let imitated_header: StandardDiskHeader = StandardDiskHeader::from_block(&read);\n        Ok(\n            Self {\n            imitated_header,\n            was_updated: false, // Haven't done anything yet.\n            }\n        )\n    }\n}\n\n// We need to support all of the allocation methods that disks normally use.\n\nimpl BlockAllocation for CachedAllocationDisk {\n    #[doc = \" Get the block allocation table\"]\n    fn get_allocation_table(&self) -> &[u8] {\n        &self.imitated_header.block_usage_map\n    }\n\n    #[doc = \" Update and flush the allocation table to disk.\"]\n    fn set_allocation_table(&mut self,new_table: &[u8]) -> Result<(), DriveError> {\n        self.imitated_header.block_usage_map = new_table\n            .try_into()\n            .expect(\"Incoming table size should be the same as outgoing.\");\n\n        // Since the allocation table has changed, we need to now update the cached block\n        // for this disk later.\n        self.was_updated = true;\n        Ok(())\n    }\n}\n\n// When these fake disks are dropped, their updated (if updated) blocks need to go into the cache\nimpl Drop for CachedAllocationDisk {\n    fn drop(&mut self) {\n\n        // If we didn't update the allocation table, we don't need to\n        // do anything, since the cached header is still up to date.\n        if !self.was_updated {\n            return;\n        }\n\n\n        // Put our fake header in the cache.\n        let updated = self.imitated_header.to_block();\n        // If this fails we are major cooked, we will try 10 times.\n        for i in (1..=10).rev() {\n            let result = CachedBlockIO::update_block(&updated);\n            if let Err(bad) = result {\n                // UH OH\n                error!(\"Attempting to flush a CachedAllocationDisk is failing!\");\n                error!(\"{bad:#?}\");\n                // If we are out of attempts, we must die.\n                let remaining_attempts = i - 1;\n                if remaining_attempts == 0 {\n                    // Cooked.\n                    error!(\"Well.. Shit!\");\n                    error!(\"Filesystem is in an unrecoverable state!\");\n                    error!(\"Giving up.\");\n                    panic!(\"Failed to flush CachedAllocationDisk!\") // bye bye!\n                }\n                error!(\"{remaining_attempts} attempts remaining!\")\n            } else {\n                // Worked! All done.\n                break\n            }\n        }\n        // Block has been put in the cache.\n    }\n}"
  },
  {
    "path": "src/pool/disk/generic/io/cache/mod.rs",
    "content": "mod cache_implementation;\npub(crate) mod cache_io;\nmod statistics;\npub(crate) mod cached_allocation;"
  },
  {
    "path": "src/pool/disk/generic/io/cache/statistics.rs",
    "content": "// Statistics about the cache\n\nuse std::{collections::VecDeque, sync::Mutex};\n\nuse lazy_static::lazy_static;\n\n// Holds the cache\nconst HIT_MEMORY: usize = 10_000; // How many of the last reads we keep track of to calculate hit rate.\nlazy_static! {\n    // Where the stats are stored\n    static ref CACHE_STATISTICS: Mutex<BlockCacheStatistics> = Mutex::new(BlockCacheStatistics::new());\n}\n\n//\n// =========\n// Structs\n// =========\n//\n\n/// Statistic information about the cache\npub(super) struct BlockCacheStatistics {\n    /// Stats for calculating cache hit rates\n    hits_and_misses: VecDeque<bool>,\n    // How many disk swaps we've prevented\n    // swaps_saved: u64\n}\n\n//\n// =========\n// BlockCacheStatistics functions\n// =========\n//\n\n// The hit-rate and recoding is public, since its the cache_io that updates and reads these.\nimpl BlockCacheStatistics {\n    /// New stats yay\n    fn new() -> Self {\n        Self {\n            hits_and_misses: VecDeque::with_capacity(HIT_MEMORY),\n            // swaps_saved: 0,\n        }\n    }\n    pub(super) fn get_hit_rate() -> f64 {\n        // Get ourselves\n        let stats = CACHE_STATISTICS.lock().expect(\"Single threaded\");\n        if stats.hits_and_misses.is_empty() {\n            return 0.0\n        }\n        // rate is hits / total requests\n        let hits = stats.hits_and_misses.iter().filter(|&&hit| hit).count();\n        hits as f64 / stats.hits_and_misses.len() as f64\n    }\n    /// Record a cache hit.\n    /// \n    /// Two functions to avoid confusion.\n    pub(super) fn record_hit() {\n        // Get ourselves\n        let stats = &mut CACHE_STATISTICS.lock().expect(\"Single threaded\");\n\n        // Need to pop the oldest hit if we're out of room.\n        if stats.hits_and_misses.len() >= HIT_MEMORY {\n            let _ = stats.hits_and_misses.pop_back();\n        }\n        stats.hits_and_misses.push_front(true);\n    }\n\n    /// Record a cache hit.\n    /// \n    /// Two functions to avoid confusion.\n    pub(super) fn record_miss() {\n        // Get ourselves\n        let stats = &mut CACHE_STATISTICS.lock().expect(\"Single threaded\");\n\n        // Need to pop the oldest hit if we're out of room.\n        if stats.hits_and_misses.len() >= HIT_MEMORY {\n            let _ = stats.hits_and_misses.pop_back();\n        }\n        stats.hits_and_misses.push_front(false);\n    }\n}"
  },
  {
    "path": "src/pool/disk/generic/io/checked_io.rs",
    "content": "// IO operations that ensure allocations are properly set.\n// We panic in here if we try to read/write in an invalid way, since that indicates a logic error elsewhere.\n\nuse log::trace;\n\nuse crate::{\n    error_types::drive::DriveError,\n    pool::{\n        disk::generic::{\n            block::{\n                allocate::block_allocation::BlockAllocation,\n                block_structs::RawBlock,\n            },\n            disk_trait::GenericDiskMethods,\n            generic_structs::pointer_struct::DiskPointer,\n        },\n        pool_actions::pool_struct::GLOBAL_POOL,\n    }\n};\n\n// A fancy new trait thats built out of other traits!\n// Automatically add it to all types that implement the subtypes we need.\nimpl<T: BlockAllocation + GenericDiskMethods> CheckedIO for T {}\npub(super) trait CheckedIO: BlockAllocation + GenericDiskMethods {\n    /// Read a block from the disk, ensuring it has already been allocated, as to not read junk.\n    /// Panics if block was not allocated.\n    /// \n    /// This should ONLY be used in the cache implementation. If you are dealing with disks directly, you are\n    /// doing it wrong.\n    fn checked_read(&self, block_number: u16) -> Result<RawBlock, DriveError> {\n        trace!(\"Performing checked read on block {block_number}...\",);\n        // Block must be allocated\n        assert!(self.is_block_allocated(block_number), \"Tried to read an unallocated block!\");\n        // This unchecked read is safe, because we've now checked it.\n        let result = self.unchecked_read_block(block_number)?;\n        trace!(\"Block read successfully.\");\n        Ok(result)\n    }\n\n    /// Write a block to the disk, ensuring it has not already been allocated, as to not overwrite data.\n    ///\n    /// Sets the block as used after writing.\n    ///\n    /// Panics if block was not free.\n    fn checked_write(&mut self, block: &RawBlock) -> Result<(), DriveError> {\n        trace!(\"Performing checked write on block {}...\", block.block_origin.block);\n        // Make sure block is free\n        assert!(!self.is_block_allocated(block.block_origin.block), \"Tried to write to an non-free block!\");\n        trace!(\"Block was not already allocated, writing...\");\n        self.unchecked_write_block(block)?;\n        // Now mark the block as allocated.\n        trace!(\"Marking block as allocated...\");\n        let blocks_allocated = self.allocate_blocks(&[block.block_origin.block].to_vec())?;\n        // Make sure it was actually allocated.\n        assert_eq!(blocks_allocated, 1, \"Failed to mark the block as allocated after write!\");\n        // Now decrement the pool header\n        trace!(\"Updating the pool's free block count...\");\n        trace!(\"Locking GLOBAL_POOL...\");\n\n        // If the pool is shutting down, this may be poisoned. If it is, we just have to ignore it since\n        // we dont want to panic during a panic-caused shutdown.\n\n        if let Ok(mut update) = GLOBAL_POOL.get().expect(\"Pool must exist for CheckedIO to be performed.\").try_lock() {\n            update.header.pool_standard_blocks_free -= 1;\n        };\n\n        trace!(\"Block written successfully.\");\n        Ok(())\n    }\n\n    /// Updates an underlying block with new information.\n    /// This overwrites the data in the block. (Obviously)\n    /// Panics if block was not previously allocated.\n    fn checked_update(&mut self, block: &RawBlock) -> Result<(), DriveError> {\n        trace!(\n            \"Performing checked update on block {}...\",\n            block.block_origin.block\n        );\n        // Make sure block is allocated already\n        assert!(self.is_block_allocated(block.block_origin.block), \"Tried to update an unallocated block.\");\n        self.unchecked_write_block(block)?;\n        trace!(\"Block updated successfully.\");\n        Ok(())\n    }\n\n    /// Updates several blocks starting at start_block with data. Blocks must already be allocated.\n    /// This overwrites the data in the block. (Obviously)\n    /// Panics if any of the blocks were not previously allocated.\n    fn checked_large_update(&mut self, data: Vec<u8>, start_block: DiskPointer) -> Result<(), DriveError> {\n        trace!(\n            \"Performing checked large update starting on block {}...\",\n            start_block.block\n        );\n        // Make sure all of the blocks this refers to are already allocated.\n        for block in start_block.block..start_block.block + data.len().div_ceil(512) as u16 {\n            assert!(self.is_block_allocated(block), \"One of the blocks in a large write was not allocated!\");\n        }\n        self.unchecked_write_large(data, start_block)?;\n        trace!(\"Blocks updated successfully.\");\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/pool/disk/generic/io/mod.rs",
    "content": "// pub mod checked_io;\npub mod read;\npub mod wipe;\npub mod write;\npub mod cache;"
  },
  {
    "path": "src/pool/disk/generic/io/read.rs",
    "content": "// Reading!\n\n// Safety\n#![deny(clippy::unwrap_used)]\n#![deny(clippy::expect_used)]\n\n// Imports\n\nuse log::{\n    error,\n    warn\n};\n\nuse crate::{\n    error_types::{\n        conversions::CannotConvertError, critical::{\n            CriticalError,\n            RetryCapError\n        },\n        drive::{\n            DriveError,\n            DriveIOError,\n            WrappedIOError\n        }\n    },\n    pool::disk::{drive_struct::FloppyDrive, generic::generic_structs::pointer_struct::DiskPointer},\n    tui::{\n        notify::NotifyTui,\n        tasks::TaskType\n    }\n};\n\nuse super::super::block::block_structs::RawBlock;\nuse super::super::block::crc::check_crc;\nuse std::{\n    fs::File,\n    os::unix::fs::FileExt\n};\n\n// Implementations\n\n// DONT USE THE CACHE DOWN HERE!\n// We rely on this call to _actually_ read from the disk, not just parrot back what's in the cache.\n// The cache calls this when an item isn't found. Checking again down here is pointless. If it was\n// in the cache, we wouldn't be here.\n\n\n/// Read a block on the currently inserted disk in the floppy drive\n/// ONLY FOR LOWER LEVEL USE, USE CHECKED_READ()!\npub(crate) fn read_block_direct(\n    disk_file: &File,\n    originating_disk: u16,\n    block_index: u16,\n    ignore_crc: bool,\n    has_recursed: bool,\n) -> Result<RawBlock, DriveError> {\n    let handle = NotifyTui::start_task(TaskType::DiskReadBlock, 1);\n    // Bounds checking\n    if block_index >= 2880 {\n        // This block is impossible to access.\n        panic!(\"Impossible read offset `{block_index}`!\")\n    }\n\n    let pointer: DiskPointer = DiskPointer {\n        disk: originating_disk,\n        block: block_index,\n    };\n\n    // allocate space for the block\n    let mut read_buffer: [u8; 512] = [0u8; 512];\n\n    // Calculate the offset into the disk\n    let read_offset: u64 = block_index as u64 * 512;\n\n    // Enter a loop to retry reading the block 10 times at most.\n    // If we try 3 times without success, we are cooked.\n\n    for _ in 0..3 {\n\n        // Seek to the requested block and read 512 bytes from it\n        let read_result = disk_file.read_exact_at(&mut read_buffer, read_offset);\n        if let Err(error) = read_result {\n            // That did not work.\n            \n            // Try converting it into a DriveIOError\n            let wrapped: WrappedIOError = WrappedIOError::wrap(error, pointer);\n            let converted: Result<DriveIOError, CannotConvertError> = wrapped.try_into();\n            if let Ok(bail) = converted {\n                // We don't need to / can't handle this error, up we go.\n                // But we might still need to retry this\n                if let Ok(actually_bail) = DriveError::try_from(bail) {\n                    // Something is up that we cant handle here.\n                    // We don't bail on missing disks though, sometimes the drive is just being\n                    // a bit silly and needs a few tries to realize the disk is in there.\n                    if actually_bail == DriveError::DriveEmpty {\n                        // Try again.\n                        continue;\n                    }\n\n                    return Err(actually_bail)\n                }\n            }\n            // We must handle the error. Down here that just means trying the write again.\n            continue;\n        }\n\n        // Read worked.\n\n        // Check the CRC, unless the user disabled it on this call.\n        // CRC checks should only be disabled when absolutely needed, such as\n        // when reading in unknown blocks from unknown disks to check headers.\n        if !ignore_crc && !check_crc(read_buffer) {\n            // CRC check failed, we have to try again.\n            warn!(\"CRC check failed, retrying...\");\n            continue;\n        }\n\n        // Read successful.\n        NotifyTui::complete_task_step(&handle);\n        // send it.\n        let block_origin: DiskPointer = DiskPointer {\n            disk: originating_disk,\n            block: block_index,\n        };\n\n        // Inform TUI\n        NotifyTui::finish_task(handle);\n        NotifyTui::block_read(1);\n\n        return Ok(RawBlock {\n            block_origin,\n            data: read_buffer,\n        });\n    }\n\n    // If we've recursed, critical cleanup has failed.\n    if has_recursed {\n        return Err(DriveError::Retry)\n    }\n\n    // We've made it out of the loop without a good read. We are doomed.\n    error!(\"Read failure, requires assistance.\");\n\n    // Do the error cleanup\n    CriticalError::OutOfRetries(RetryCapError::ReadBlock).handle();\n\n    // Re-grab the file, since the drive path may have changed after disk cleanup\n    // After recovery, the path to the disk may have changed. This is a little naughty, but\n    // we'll re-grab the disk file.\n    let re_open = FloppyDrive::open_direct(originating_disk)?;\n\n    // The type doesn't matter, as long as we get the disk file out.\n    // If its blank or unknown, we can still write to it, reading would just be bad.\n    let new_file = match re_open {\n        crate::pool::disk::drive_struct::DiskType::Pool(pool_disk) => {\n            pool_disk.disk_file\n        },\n        crate::pool::disk::drive_struct::DiskType::Standard(standard_disk) => {\n            standard_disk.disk_file\n        },\n        crate::pool::disk::drive_struct::DiskType::Unknown(unknown_disk) => {\n            unknown_disk.disk_file\n        },\n        crate::pool::disk::drive_struct::DiskType::Blank(blank_disk) => {\n            blank_disk.disk_file\n        },\n    };\n\n    read_block_direct(&new_file, originating_disk, block_index, ignore_crc, true)\n}\n\n\n/// Automatically truncate reads if it would go off of the end of the disk.\n/// \n/// Returns a Vec of RawBlock. May not be the full length of requested blocks.\npub(crate) fn read_multiple_blocks_direct(\n    disk_file: &File,\n    originating_disk: u16,\n    block_index: u16,\n    num_to_read: u16,\n    has_recursed: bool,\n) -> Result<Vec<RawBlock>, DriveError> {\n    // Bounds checking\n    if block_index >= 2880 {\n        // This block is impossible to access.\n        panic!(\"Impossible read offset `{block_index}`!\")\n    }\n\n    // Figure out how many blocks we can read\n    let checked_num_to_read = std::cmp::min(num_to_read, 2880 - block_index);\n\n    // Start the read task.\n    let handle = NotifyTui::start_task(TaskType::DiskReadBlock, 1);\n    \n    // The start point of the read\n    let pointer: DiskPointer = DiskPointer {\n        disk: originating_disk,\n        block: block_index,\n    };\n\n    // allocate space for the blocks we want to read\n    let mut read_buffer: Vec<u8> = vec![0; checked_num_to_read as usize * 512];\n\n    // Calculate the offset into the disk\n    let read_offset: u64 = block_index as u64 * 512;\n\n    // Try to read the entire chunk in.\n    // If we try 3 times without success, we are cooked.\n\n    for _ in 0..3 {\n\n        // Seek to the requested block and read as many bytes as we need.\n        let read_result = disk_file.read_exact_at(&mut read_buffer, read_offset);\n        if let Err(error) = read_result {\n            // That did not work.\n            \n            // Try converting it into a DriveIOError\n            let wrapped: WrappedIOError = WrappedIOError::wrap(error, pointer);\n            let converted: Result<DriveIOError, CannotConvertError> = wrapped.try_into();\n            if let Ok(bail) = converted {\n                // We don't need to / can't handle this error, up we go.\n                // But we might still need to retry this\n                if let Ok(actually_bail) = DriveError::try_from(bail) {\n                    // Something is up that we cant handle here.\n                    // We don't bail on missing disks though, sometimes the drive is just being\n                    // a bit silly and needs a few tries to realize the disk is in there.\n                    if actually_bail == DriveError::DriveEmpty {\n                        // Try again.\n                        continue;\n                    }\n\n                    return Err(actually_bail)\n                }\n            }\n            // We must handle the error. Down here that just means trying the write again.\n            continue;\n        }\n\n        // Read worked.\n\n        // Read successful.\n        NotifyTui::complete_task_step(&handle);\n\n        // Split it back out into blocks\n        let mut output_blocks: Vec<RawBlock> = Vec::with_capacity(checked_num_to_read.into());\n\n        let block_chunks = read_buffer.chunks_exact(512);\n        for (index, block) in block_chunks.enumerate() {\n\n            // Cast the block slice into a known size, this should always work\n            let data = match block.try_into() {\n                Ok(ok) => ok,\n                Err(_) => unreachable!(\"How was the chunk size of 512 not 512 bytes?\"),\n            };\n\n            output_blocks.push(\n                RawBlock {\n                    block_origin: DiskPointer {\n                        disk: originating_disk,\n                        block: block_index + index as u16\n                    },\n                    data\n                }\n            );\n        }\n\n        // Inform TUI\n        NotifyTui::finish_task(handle);\n        NotifyTui::block_read(checked_num_to_read);\n\n        return Ok(output_blocks);\n    }\n\n    // If we've recursed, critical cleanup has failed.\n    if has_recursed {\n        return Err(DriveError::Retry)\n    }\n\n    // We've made it out of the loop without a good read. We are doomed.\n    error!(\"Read failure, requires assistance.\");\n\n    // Do the error cleanup\n    CriticalError::OutOfRetries(RetryCapError::ReadBlock).handle();\n\n    // Re-grab the file, since the drive path may have changed after disk cleanup\n    // After recovery, the path to the disk may have changed. This is a little naughty, but\n    // we'll re-grab the disk file.\n    let re_open = FloppyDrive::open_direct(originating_disk)?;\n\n    // The type doesn't matter, as long as we get the disk file out.\n    // If its blank or unknown, we can still write to it, reading would just be bad.\n    let new_file = match re_open {\n        crate::pool::disk::drive_struct::DiskType::Pool(pool_disk) => {\n            pool_disk.disk_file\n        },\n        crate::pool::disk::drive_struct::DiskType::Standard(standard_disk) => {\n            standard_disk.disk_file\n        },\n        crate::pool::disk::drive_struct::DiskType::Unknown(unknown_disk) => {\n            unknown_disk.disk_file\n        },\n        crate::pool::disk::drive_struct::DiskType::Blank(blank_disk) => {\n            blank_disk.disk_file\n        },\n    };\n\n    // recurse.\n\n    read_multiple_blocks_direct(&new_file, originating_disk, block_index, num_to_read, true)\n}"
  },
  {
    "path": "src/pool/disk/generic/io/wipe.rs",
    "content": "// Squeaky clean!\n\nuse std::{fs::File, time::Duration};\n\nuse log::debug;\n\nuse crate::{error_types::drive::DriveError, pool::disk::generic::generic_structs::pointer_struct::DiskPointer, tui::{notify::NotifyTui, tasks::TaskType}};\n\n/// Wipes ALL data on ALL blocks on the disk.\npub(crate) fn destroy_disk(disk: &mut File) -> Result<(), DriveError> {\n    // Bye bye!\n    let chunk_size: usize = 64;\n    debug!(\"Wiping currently inserted disk...\");\n    let ten_blank_blocks: Vec<u8> = vec![0; 512 * chunk_size];\n\n    // Make a new task to track disk wiping progress.\n    let task_handle = NotifyTui::start_task(TaskType::WipeDisk, (2880/chunk_size) as u64);\n    \n    // Write in large chunks for speed.\n    for i in 0..2880/chunk_size {\n        let pointer: DiskPointer = DiskPointer {\n            disk: 42069_u16,\n            block: (i * chunk_size) as u16,\n        };\n        \n        // We will keep track of how long this is taking, since if a single chunk of blocks\n        // takes weirdly long, chances are the disk is bad.\n        let now = std::time::Instant::now();\n\n        super::write::write_large_direct(disk, &ten_blank_blocks, pointer)?;\n\n        if now.elapsed() > Duration::from_secs(10) {\n            // Took too long, this disk is no good.\n            NotifyTui::cancel_task(task_handle);\n            return Err(DriveError::TakingTooLong)\n        }\n        \n        let percent = (((i + 1) * chunk_size) as f32 / 2880_f32) * 100.0;\n        debug!(\"{percent:.1}%...\");\n        NotifyTui::complete_task_step(&task_handle);\n    }\n    debug!(\"Wipe complete.\");\n    NotifyTui::finish_task(task_handle);\n    \n    Ok(())\n}"
  },
  {
    "path": "src/pool/disk/generic/io/write.rs",
    "content": "// Writing!\n\n// Safety\n#![deny(clippy::unwrap_used)]\n#![deny(clippy::expect_used)]\n\n// Imports\n\nuse log::{\n    trace,\n    error\n};\n\nuse crate::error_types::conversions::CannotConvertError;\nuse crate::error_types::critical::{CriticalError, RetryCapError};\nuse crate::error_types::drive::{DriveError, DriveIOError, WrappedIOError};\nuse crate::filesystem::filesystem_struct::WRITE_BACKUPS;\nuse crate::pool::disk::drive_struct::FloppyDrive;\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\nuse crate::tui::notify::NotifyTui;\nuse crate::tui::tasks::TaskType;\n\nuse super::super::block::block_structs::RawBlock;\nuse std::ops::Rem;\nuse std::{\n    fs::File,\n    os::unix::fs::FileExt\n};\n\n// Implementations\n\n/// Write a block to the currently inserted disk in the floppy drive\n/// ONLY FOR LOWER LEVEL USE, USE CHECKED_WRITE()!\npub(crate) fn write_block_direct(disk_file: &File, block: &RawBlock, has_recursed: bool) -> Result<(), DriveError> {\n    let handle = NotifyTui::start_task(TaskType::DiskWriteBlock, 1);\n    trace!(\n        \"Directly writing block {} to currently inserted disk...\",\n        block.block_origin.block\n    );\n    // Bounds checking\n    if block.block_origin.block >= 2880 {\n        // This block is impossible to access.\n        panic!(\"Impossible write offset `{}`!\",  block.block_origin.block)\n    }\n\n    // Update the disk backup\n    crate::filesystem::disk_backup::update::update_backup(block);\n\n    let pointer: DiskPointer = block.block_origin;\n\n    // Calculate the offset into the disk\n    let write_offset: u64 = block.block_origin.block as u64 * 512;\n\n    for _ in 0..3 {\n        // Write the data.\n        let write_result = disk_file.write_all_at(&block.data, write_offset);\n\n        if let Err(error) = write_result {\n            // That did not work.\n            \n            // Try converting it into a DriveIOError\n            let wrapped: WrappedIOError = WrappedIOError::wrap(error, pointer);\n            let converted: Result<DriveIOError, CannotConvertError> = wrapped.try_into();\n            if let Ok(bail) = converted {\n                // We don't need to / can't handle this error, up we go.\n                // But we might still need to retry this\n                if let Ok(actually_bail) = DriveError::try_from(bail) {\n                    // Something is up that we cant handle here.\n                    // We don't bail on missing disks though, sometimes the drive is just being\n                    // a bit silly and needs a few tries to realize the disk is in there.\n                    if actually_bail == DriveError::DriveEmpty {\n                        // Try again.\n                        continue;\n                    }\n                }\n            }\n            // We must handle the error. Down here that just means trying the write again.\n            continue;\n        }\n\n        // Writing worked! all done.\n        NotifyTui::complete_task_step(&handle);\n        trace!(\"Block written successfully.\");\n\n        // Attempt to sync the write, we only do this if backups are turned on, since we dont\n        // wanna slow down tests.\n        if let Some(enabled) = WRITE_BACKUPS.get() {\n            if *enabled {\n                // if this fails, oh well.\n                let _ = disk_file.sync_all();\n            }\n        }\n\n        // Notify the TUI\n        NotifyTui::block_written(1);\n        NotifyTui::finish_task(handle);\n\n        return Ok(());\n    };\n\n    // We've made it outside of the loop. The error is unrecoverable.\n    \n    // The recursion failed, so the previous error handling failed. We will bail.\n    if has_recursed {\n        // Rough.\n        return Err(DriveError::Retry);\n    }\n    error!(\"Write failure, requires assistance.\");\n\n    // Do the error cleanup, if that works, the disk should be working now, and we can recurse, since we\n    // should now be able to complete the operation successfully.\n    CriticalError::OutOfRetries(RetryCapError::WriteBlock).handle();\n\n\n    // After recovery, the path to the disk may have changed. This is a little naughty, but\n    // we'll re-grab the disk file.\n    let re_open = FloppyDrive::open_direct(block.block_origin.disk)?;\n\n    // The type doesn't matter, as long as we get the disk file out.\n    // If its blank or unknown, we can still write to it, reading would just be bad.\n    let new_file = match re_open {\n        crate::pool::disk::drive_struct::DiskType::Pool(pool_disk) => {\n            pool_disk.disk_file\n        },\n        crate::pool::disk::drive_struct::DiskType::Standard(standard_disk) => {\n            standard_disk.disk_file\n        },\n        crate::pool::disk::drive_struct::DiskType::Unknown(unknown_disk) => {\n            unknown_disk.disk_file\n        },\n        crate::pool::disk::drive_struct::DiskType::Blank(blank_disk) => {\n            blank_disk.disk_file\n        },\n    };\n\n    // Now recurse.\n\n    write_block_direct(&new_file, block, true)\n}\n\n/// Write a vec of bytes starting at offset to the currently inserted disk in the floppy drive.\n/// ONLY FOR LOWER LEVEL USE, USE CHECKED_WRITE()!\npub(crate) fn write_large_direct(disk_file: &File, data: &[u8], start_block: DiskPointer) -> Result<(), DriveError> {\n    let handle = NotifyTui::start_task(TaskType::DiskWriteLarge, 1);\n    // Bounds checking\n    if start_block.block >= 2880 {\n        // This block is impossible to access.\n        panic!(\"Impossible write offset `{}`!\",  start_block.block)\n    }\n\n    let pointer: DiskPointer = start_block;\n\n    // Must write full blocks (512 byte chunks)\n    assert!(data.len().rem(512) == 0, \"Large writes must be a multiple of 512!\");\n\n    // Make sure we don't run off the end of the disk\n    assert!(start_block.block + ((data.len().div_ceil(512) - 1) as u16) < 2880_u16, \"Write would go off the end of the disk!\");\n\n    trace!(\n        \"Directly writing {} blocks worth of bytes starting at block {} to currently inserted disk...\",\n        data.len().div_ceil(512), start_block.block\n    );\n\n    // Update the disk backup\n    crate::filesystem::disk_backup::update::large_update_backup(start_block, data);\n\n    // Calculate the offset into the disk\n    let write_offset: u64 = start_block.block as u64 * 512;\n\n    // Pre-sync the disk just in case its already writing.\n    if let Some(enabled) = WRITE_BACKUPS.get() {\n        if *enabled {\n            // if this fails, oh well.\n            let _ = disk_file.sync_all();\n        }\n    }\n\n\n\n    // Now enter a loop so we can attempt the write at most 10 times, in case it fails.\n    for _ in 0..3 {\n        // Write the data.\n        let write_result = disk_file.write_all_at(data, write_offset);\n\n        if let Err(error) = write_result {\n            // That did not work.\n            \n             // Try converting it into a DriveIOError\n            let wrapped: WrappedIOError = WrappedIOError::wrap(error, pointer);\n            let converted: Result<DriveIOError, CannotConvertError> = wrapped.try_into();\n            if let Ok(bail) = converted {\n                // We don't need to / can't handle this error, up we go.\n                // But we might still need to retry this\n                if let Ok(actually_bail) = DriveError::try_from(bail) {\n                    // Something is up that we cant handle here.\n                    // We don't bail on missing disks though, sometimes the drive is just being\n                    // a bit silly and needs a few tries to realize the disk is in there.\n                    if actually_bail == DriveError::DriveEmpty {\n                        // Try again.\n                        continue;\n                    }\n                }\n            }\n            // We must handle the error. Down here that just means trying the write again.\n            continue;\n        }\n\n        // Writing worked! all done.\n        trace!(\"Several blocks written successfully.\");\n        NotifyTui::complete_task_step(&handle);\n\n        // Attempt to sync the write, we only do this if backups are turned on, since we dont\n        // wanna slow down tests.\n        if let Some(enabled) = WRITE_BACKUPS.get() {\n            if *enabled {\n                // if this fails, oh well.\n                let _ = disk_file.sync_all();\n            }\n        }\n\n        // Notify the TUI\n        NotifyTui::finish_task(handle);\n        NotifyTui::block_written((data.len()/512) as u16);\n\n        return Ok(());\n    };\n\n    // We've made it outside of the loop. The error is unrecoverable.\n    error!(\"Write failure, requires assistance.\");\n\n    // Do the error cleanup, if that works, the disk should be working now, we should now be able to write\n    // to the disk, but instead of recursing, we call the fallback to try to be a bit safer with the failure, and\n    // to prevent infinite recursion.\n    CriticalError::OutOfRetries(RetryCapError::WriteBlock).handle();\n    large_write_fallback(disk_file, data, start_block)\n}\n\n/// If large writes are continually failing, maybe we'll have better luck with singular writes.\nfn large_write_fallback(disk_file: &File, data: &[u8], start_block: DiskPointer) -> Result<(), DriveError> {\n    // Extract the vec of data into blocks.\n\n    // Pointer that'll be incremented as we're creating blocks\n    let mut new_pointer = start_block;\n\n    // Chunk the data into block sized chunks\n    let chunked = data.chunks(512);\n\n    // Now this isn't a great idea, but this is the fallback anyways.\n    // If any of the chunks (only the last one could have this happen) is\n    // less than 512 bytes in size, we'll pad it to 512 with zeros, which yeah, stuff\n    // might absolutely explode, but at least we can attempt to keep going lmao\n\n    // Loop over the chunks and construct the blocks\n    let blocks: Vec<RawBlock> = chunked.into_iter().map(|chunk|\n        {\n            // Pad the slice if needed\n            let mut padded: Vec<u8> = Vec::with_capacity(512);\n            padded.extend(chunk);\n            let difference = 512 - padded.len();\n            if difference != 0 {\n                // Add zeros for padding\n                let padding: Vec<u8> = vec![0; difference];\n                padded.extend(padding);\n            }\n\n            // Now turn that into a slice\n            let sliced: [u8; 512] = if let Ok(got) = padded[0..512].try_into() {\n                got\n            } else {\n                // 512 is not 512 today apparently.\n                unreachable!(\"512 is 512\")\n            };\n\n            // Make a block\n            let blocked: RawBlock = RawBlock {\n                block_origin: new_pointer,\n                data: sliced,\n            };\n\n            // Increment the block pointer\n            new_pointer.block += 1;\n\n            // Out goes this block\n            blocked\n        }\n    ).collect();\n\n    // Now that we have all of the blocks, write them to the disk\n    for block in blocks {\n        // This is the first call, we have not recursed.\n        write_block_direct(disk_file, &block, false)?;\n    }\n\n    Ok(())\n}"
  },
  {
    "path": "src/pool/disk/generic/mod.rs",
    "content": "pub mod block;\npub mod disk_trait;\npub mod generic_structs;\npub mod io;"
  },
  {
    "path": "src/pool/disk/mod.rs",
    "content": "pub mod blank_disk;\nmod drive_methods;\npub mod drive_struct;\npub mod generic;\npub mod pool_disk;\npub mod standard_disk;\npub mod unknown_disk;\n"
  },
  {
    "path": "src/pool/disk/pool_disk/block/header/header_methods.rs",
    "content": "// So no head?\n\n// Imports\n\nuse std::process::exit;\n\nuse log::debug;\nuse log::warn;\n\nuse crate::error_types::drive::DriveError;\nuse crate::error_types::header::HeaderError;\nuse crate::filesystem::disk_backup::restore::restore_disk;\nuse crate::filesystem::filesystem_struct::USE_VIRTUAL_DISKS;\nuse crate::pool::disk::blank_disk::blank_disk_struct::BlankDisk;\nuse crate::pool::disk::drive_methods::check_for_magic;\nuse crate::pool::disk::drive_methods::display_info_and_ask_wipe;\nuse crate::pool::disk::drive_struct::DiskType;\nuse crate::pool::disk::drive_struct::FloppyDrive;\nuse crate::pool::disk::generic::block::block_structs::RawBlock;\nuse crate::pool::disk::generic::block::crc::add_crc_to_block;\nuse crate::pool::disk::generic::disk_trait::GenericDiskMethods;\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\nuse crate::pool::disk::generic::io::wipe::destroy_disk;\nuse crate::pool::disk::pool_disk::block::header::header_struct::PoolHeaderFlags;\nuse crate::pool::disk::pool_disk::pool_disk_struct::PoolDisk;\nuse crate::tui::prompts::TuiPrompt;\nuse super::header_struct::PoolDiskHeader;\n\n// Implementations\n\nimpl PoolDiskHeader {\n    /// Reterive the header from the pool disk\n    pub fn read() -> Result<Self, DriveError> {\n        read_pool_header_from_disk()\n    }\n    /// Overwrite the current header stored on the pool disk.\n    pub fn write(&self) -> Result<(), DriveError> {\n        write_pool_header_to_disk(self)\n    }\n    /// Convert the pool header into a RawBlock\n    pub fn to_block(self) -> RawBlock {\n        pool_header_to_raw_block(self)\n    }\n    /// Try and convert a raw block into a pool header\n    pub fn from_block(block: &RawBlock) -> Result<PoolDiskHeader, HeaderError> {\n        pool_header_from_raw_block(block)\n    }\n}\n\n/// This function bypasses the usual disk types.\n/// I dont want to rewrite this right now. It'll do.\nfn read_pool_header_from_disk() -> Result<PoolDiskHeader, DriveError> {\n    // Get the header block from the pool disk (disk 0)\n    // If the header is missing, and there is no fluster magic, ask if we are creating a new pool.\n\n    // This function will return an error if reading in the pool is determined to be impossible due to\n    // factors outside of our control.\n\n    // Get block 0 of disk 0\n\n    // We will contain all of our logic within a loop, so if the user inserts the incorrect disk we can ask for another, etc\n    // This is messy. Sorry.\n\n    loop {\n        // if we are running with virtual disks, we skip the prompt.\n        if !USE_VIRTUAL_DISKS\n            .try_lock()\n            .expect(\"Fluster is single threaded.\")\n            .is_some()\n        {\n            // Not using virtual disks, prompt the user...\n            let result = TuiPrompt::prompt_input(\n                \"Insert pool disk.\".to_string(),\n                \"Please insert the pool root disk (Disk 0), then press enter. Or type \\\"wipe\\\" or \\\"restore\\\".\".to_string(),\n                false\n            );\n            \n            // This is the only chance the user gets to enter disk wiping mode.\n            // Why are we doing this in pool/header_methods ? idk.\n\n            if result.contains(\"wipe\") {\n                disk_wiper_mode()\n            }\n            // Or restore\n            if result.contains(\"restore\") {\n                disk_restore_mode()\n            }\n        }\n\n        // User wants to open this disk for the pool.\n\n        // Attempt to extract the header\n        let some_disk = FloppyDrive::open_direct(0)?;\n\n        // We've now read in either the PoolDisk, or some other type of disk.\n        // Find out what it is.\n\n        match some_disk {\n            crate::pool::disk::drive_struct::DiskType::Pool(pool_disk) => {\n                // This is what we want!\n                return Ok(pool_disk.header);\n            }\n            crate::pool::disk::drive_struct::DiskType::Standard(standard_disk) => {\n                // For any disk type other than Blank, we will ask if user wants to wipe it.\n                display_info_and_ask_wipe(&mut DiskType::Standard(standard_disk))?;\n                // Start the loop over, if they wiped the disk, the outcome will change.\n                continue;\n            }\n            crate::pool::disk::drive_struct::DiskType::Unknown(file) => {\n                display_info_and_ask_wipe(&mut DiskType::Unknown(file))?;\n                continue;\n            }\n            crate::pool::disk::drive_struct::DiskType::Blank(disk) => {\n                // The disk is blank, we will ask if the user wants to create a new pool.\n                prompt_for_new_pool(disk)?;\n                // The user either created a new pool, or didnt, so we just continue and run through this again.\n                continue;\n            }\n        }\n    }\n}\n\n/// Ask the user if they want to create a new pool with the currently inserted disk.\n/// If so, we blank out the disk\nfn prompt_for_new_pool(disk: BlankDisk) -> Result<(), DriveError> {\n    // if we are running with virtual disks, we skip the prompt.\n    debug!(\"Locking USE_VIRTUAL_DISKS...\");\n    if USE_VIRTUAL_DISKS\n        .try_lock()\n        .expect(\"Fluster is single threaded.\")\n        .is_some()\n    {\n        debug!(\"We are running with virtual disks, skipping the new pool prompt.\");\n        // Using virtual disks, we are going to create the pool immediately.\n        return create_new_pool_disk(disk);\n    } else {\n        // If we are running a test, we should never be asking for user input, thus we should always\n        // be using virtual disks.\n        assert!(!cfg!(test), \"Asked for user input during a test!\");\n    }\n\n    // Ask the user if they want to create a new pool starting on this disk (hereafer disk 0 / root disk)\n    let reply = TuiPrompt::prompt_input(\n        \"Create new pool?\".to_string(),\n        \"This disk is blank.\\nDo you wish to create a new pool?\\n\\\"yes\\\"/\\\"no\\\"\".to_string(),\n        false\n    );\n\n    if !reply.to_lowercase().contains(\"yes\") {\n        // They dont wanna make a new one.\n        return Ok(());\n    }\n\n    // User said yes. Make the disk.\n    create_new_pool_disk(disk)\n}\n\nfn pool_header_from_raw_block(block: &RawBlock) -> Result<PoolDiskHeader, HeaderError> {\n    // As usual, check for the magic\n    if !check_for_magic(&block.data) {\n        // There is no magic, thus this cannot be a header\n\n        // The disk may be blank though.\n        if block.data.iter().all(|byte| *byte == 0) {\n            // Disk is blank\n            return Err(HeaderError::Blank);\n        }\n\n        // Something else is wrong.\n        return Err(HeaderError::Invalid);\n    }\n\n    // Easier alignment\n    let mut offset: usize = 8;\n\n    // Pool headers always have bit 7 set in the flags, other headers are forbidden from writing this bit.\n    let flags: PoolHeaderFlags = match PoolHeaderFlags::from_bits(block.data[offset]) {\n        Some(ok) => ok,\n        None => {\n            // extra bits in the flags were set, either this isn't a pool header, or it is corrupted in some way.\n            return Err(HeaderError::Invalid);\n        }\n    };\n\n    // Make sure the pool header bit is indeed set.\n    if !flags.contains(PoolHeaderFlags::RequiredHeaderBit) {\n        // The header must have missed the joke, since it didn't quite get the bit.\n        // Not a pool header.\n        return Err(HeaderError::Invalid);\n    }\n\n    offset += 1;\n\n    // Now we can actually start extracting the header.\n\n    // Highest disk\n    let highest_known_disk: u16 =\n        u16::from_le_bytes(block.data[offset..offset + 2].try_into().expect(\"2 bytes = 2 bytes\"));\n\n    offset += 2;\n\n    // Disk with next free block\n    let disk_with_next_free_block: u16 =\n        u16::from_le_bytes(block.data[offset..offset + 2].try_into().expect(\"2 bytes = 2 bytes\"));\n\n    offset += 2;\n\n    // Blocks free in pool\n    let pool_standard_blocks_free: u32 =\n        u32::from_le_bytes(block.data[offset..offset + 4].try_into().expect(\"2 bytes = 2 bytes\"));\n\n    // Block allocation map\n    // Stop using the offset since this is always at the end.\n    let block_usage_map: [u8; 360] = block.data[148..148 + 360].try_into().expect(\"2 bytes = 2 bytes\");\n\n    // The latest inode write is not persisted between launches, so we point at the root inode.\n    let latest_inode_write: DiskPointer = DiskPointer { disk: 1, block: 1 };\n\n    Ok(PoolDiskHeader {\n        flags,\n        highest_known_disk,\n        disk_with_next_free_block,\n        pool_standard_blocks_free,\n        latest_inode_write, // This is not persisted between launches.\n        block_usage_map,\n    })\n}\n\nfn pool_header_to_raw_block(header: PoolDiskHeader) -> RawBlock {\n    // Deconstruct / discombobulate\n    #[deny(unused_variables)] // You need to write ALL of them.\n    let PoolDiskHeader {\n        flags,\n        highest_known_disk,\n        disk_with_next_free_block,\n        pool_standard_blocks_free,\n        latest_inode_write,\n        block_usage_map,\n    } = header;\n\n    // Create buffer for the header\n    let mut buffer: [u8; 512] = [0u8; 512];\n\n    // The magic\n    buffer[0..8].copy_from_slice(\"Fluster!\".as_bytes());\n\n    // offset for easier alignment\n    let mut offset: usize = 8;\n\n    // Flags\n    buffer[offset] = flags.bits();\n    offset += 1;\n\n    // Highest known disk\n    buffer[offset..offset + 2].copy_from_slice(&highest_known_disk.to_le_bytes());\n    offset += 2;\n\n    // Disk with next free block\n    buffer[offset..offset + 2].copy_from_slice(&disk_with_next_free_block.to_le_bytes());\n    offset += 2;\n\n    // Free blocks\n    buffer[offset..offset + 4].copy_from_slice(&pool_standard_blocks_free.to_le_bytes());\n\n    // We do not save the inode write disk information.\n    let _ = latest_inode_write;\n\n    // Block usage map\n    // Doesn't use offset, static location.\n    buffer[148..148 + 360].copy_from_slice(&block_usage_map);\n\n    // Add the CRC\n    add_crc_to_block(&mut buffer);\n\n    // This needs to always go at block 0\n    let block_origin: DiskPointer = DiskPointer {\n        disk: 0, // Pool disk is always disk 0\n        block: 0, // Header is always at block 0\n    };\n\n    RawBlock {\n        block_origin,\n        data: buffer,\n    }\n}\n\nfn create_new_pool_disk(mut disk: BlankDisk) -> Result<(), DriveError> {\n    // Time for a brand new pool!\n    debug!(\"A new pool disk was created.\");\n    // We will create a brand new header, and write that header to the disk.\n    let new_header = new_pool_header();\n\n    // Now we need to write that\n    let writeable_block: RawBlock = new_header.to_block();\n\n    // Write it to the disk!\n    // This is unchecked because the disk header does not exist yet, we cannot allocate space\n    // for the header without the header.\n\n    // We dont use cached IO here, since pool disks cannot have any caching on them.\n    \n    disk.unchecked_write_block(&writeable_block)?;\n\n    // Done!\n    Ok(())\n}\n\n// Brand new pool header\nfn new_pool_header() -> PoolDiskHeader {\n    // Default pool header\n\n    // Flags\n    let mut flags: PoolHeaderFlags = PoolHeaderFlags::empty();\n    // Needs the required bit\n    flags.insert(PoolHeaderFlags::RequiredHeaderBit);\n\n    // The highest known disk for a brand new pool is the root disk itself, zero.\n    let highest_known_disk: u16 = 0;\n\n    // The disk with the next free block.\n    // Starts at one to skip the pool disk.\n    let disk_with_next_free_block: u16 = 1;\n\n    // How many pool blocks are free? None! We only have the root disk!\n    let pool_standard_blocks_free: u32 = 0;\n\n    // What blocks are free on the pool disk? Not the first one!\n    let mut block_usage_map: [u8; 360] = [0u8; 360];\n    block_usage_map[0] = 0b10000000;\n\n    // Everything is empty, so the latest write is just gonna be the root inode.\n    let latest_inode_write: DiskPointer = DiskPointer { disk: 1, block: 1 };\n\n    PoolDiskHeader {\n        flags,\n        highest_known_disk,\n        disk_with_next_free_block,\n        pool_standard_blocks_free,\n        latest_inode_write, // This is not persisted on disk.\n        block_usage_map,\n    }\n}\n\n/// Put that pool away\nfn write_pool_header_to_disk(header: &PoolDiskHeader) -> Result<(), DriveError> {\n    // Make a block\n    let header_block = header.to_block();\n\n    // Get the pool disk\n    #[allow(deprecated)] // Pool disks cannot use the cache.\n    let mut disk: PoolDisk = match FloppyDrive::open(0)? {\n        DiskType::Pool(pool_disk) => pool_disk,\n        _ => unreachable!(\"Disk 0 should NEVER be assigned to a non-pool disk!\"),\n    };\n\n    // Replace the header\n    disk.header = *header;\n    \n    // Write it.\n    // We cant use the usual disk.flush() since that usually uses cache methods. Pool disk blocks are not cached.\n    disk.unchecked_write_block(&header_block)?;\n\n    // All done.\n    Ok(())\n}\n\n/// Disk wiper mode\nfn disk_wiper_mode() -> ! {\n    // Time to wipe some disks!\n    println!(\"Welcome to disk wiper mode!\");\n    loop {\n\n        let are_we_done_yet = TuiPrompt::prompt_input(\n            \"Wipe finished.\".to_string(),\n            \"Please insert the next disk you would like to wipe, then hit enter. Or type \\\"exit\\\".\".to_string(),\n            true\n        ).contains(\"exit\");\n\n        if are_we_done_yet {\n            // User is bored.\n            break;\n        }\n        // Wiping another disk.\n        println!(\"Wiping the inserted disk, please wait...\");\n        \n        // Get the disk from the drive. Disk numbers do not matter here.\n        let mut disk = match FloppyDrive::open_direct(0) {\n            Ok(ok) => ok,\n            Err(err) => {\n                // Uh oh\n\n                // If there just isn't a disk in the drive, we can continue\n                if err == DriveError::DriveEmpty {\n                    println!(\"Cant wipe nothing bozo. Put a disk in!\");\n                    continue;\n                }\n\n                println!(\"Opening the disk failed. Here's why:\");\n                println!(\"{err:#?}\");\n                break; // cannot go further if the drive is angry.\n            },\n        };\n\n        // Wipe the disk\n        match destroy_disk(disk.disk_file_mut()) {\n            Ok(_) => {},\n            Err(err) => {\n                // Uh oh\n                println!(\"Wiping the disk failed. Here's why:\");\n                println!(\"{err:#?}\");\n                match err {\n                    DriveError::DriveEmpty => {},\n                    DriveError::Retry => {},\n                    DriveError::TakingTooLong => {\n                        println!(\"That disk is responding VERY slowly to writes, its probably bad.\")\n                    },\n                }\n                break;\n            },\n        }\n\n        println!(\"Disk wiped.\");\n    }\n\n    // Done wiping disks, user must restart Fluster.\n    println!(\"Exiting disk wiping mode. You must restart fluster.\");\n    exit(0);\n}\n\n\n/// Disk restoration mode\nfn disk_restore_mode() -> ! {\n    loop {\n        let prompt_response = TuiPrompt::prompt_input(\n            \"Disk restore mode\".to_string(),\n            \"Please insert a disk to overwrite, then enter the number of the disk you would like to restore. Or type \\\"exit\\\".\".to_string(),\n            true\n        );\n\n        if prompt_response.contains(\"exit\") {\n            // All done.\n            break\n        }\n\n        // now what number do we want\n        let disk_to_restore: u16 = match prompt_response.parse() {\n            Ok(ok) => ok,\n            Err(err) => {\n                warn!(\"Failed to parse number!\");\n                warn!(\"{err:#?}\");\n                TuiPrompt::prompt_enter(\n                    \"bruh\".to_string(),\n                    \"Thats not a real disk number, try again.\".to_string(),\n                    false\n                );\n                continue;\n            },\n        };\n\n        // Call the restor-er-inator\n        let worked = restore_disk(disk_to_restore);\n\n        if worked {\n            // Do nothing, since it worked. Just dump em back to the beginning again\n            debug!(\"Restoration worked!\");\n            continue;\n        }\n\n        // Didn't work\n        TuiPrompt::prompt_enter(\n            \"no workie\".to_string(),\n            \"That restore did not work, you should try another disk.\".to_string(),\n            false\n        );\n        continue;\n    }\n    \n    // Done restoring disks, user must restart Fluster.\n    println!(\"Exiting disk restore mode. You must restart fluster.\");\n    exit(0);\n}"
  },
  {
    "path": "src/pool/disk/pool_disk/block/header/header_struct.rs",
    "content": "// Header for the pool disk\n\n// Imports\nuse bitflags::bitflags;\n\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\n\n// Structs, Enums, Flags\n\n/// The header of the pool disk\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\npub struct PoolDiskHeader {\n    /// Flags about the pool.\n    pub flags: PoolHeaderFlags,\n    /// The highest disk number that we have created\n    pub highest_known_disk: u16,\n    /// The next disk with a free block on it, or u16::MAX\n    pub disk_with_next_free_block: u16,\n    /// The number of free standard blocks across all disks\n    pub pool_standard_blocks_free: u32,\n    /// The disk with the most recent inode write.\n    /// Used for speeding up inode additions.\n    pub latest_inode_write: DiskPointer,\n    /// Map of used blocks on this disk\n    pub block_usage_map: [u8; 360],\n}\n\nbitflags! {\n    #[derive(Debug, PartialEq, Eq, Clone, Copy)]\n    pub struct PoolHeaderFlags: u8 {\n        // All Pool headers MUST have this bit set.\n        const RequiredHeaderBit = 0b10000000;\n    }\n}"
  },
  {
    "path": "src/pool/disk/pool_disk/block/header/mod.rs",
    "content": "mod header_methods;\npub mod header_struct;\n#[cfg(test)]\nmod tests;\n"
  },
  {
    "path": "src/pool/disk/pool_disk/block/header/tests.rs",
    "content": "// Head in the pool? Preposterous!\n// Unwrapping is okay here, since we want unexpected outcomes to fail tests.\n#![allow(clippy::unwrap_used)]\n\n// Imports\nuse rand::Rng;\nuse rand::rngs::ThreadRng;\n\nuse crate::pool::disk::generic::block::block_structs::RawBlock;\nuse crate::pool::disk::pool_disk::block::header::header_struct::PoolDiskHeader;\nuse crate::pool::disk::pool_disk::block::header::header_struct::PoolHeaderFlags;\n\nuse test_log::test; // We want to see logs while testing.\n\n// Tests\n\n// Ensure we can encode and decode a block\n#[test]\nfn block_ping_pong() {\n    for _ in 0..1000 {\n        let new_block = PoolDiskHeader::random();\n        // Wizard, CAST!\n        let raw_block: RawBlock = new_block.to_block();\n        // Again!\n        let banach_tarski: PoolDiskHeader = PoolDiskHeader::from_block(&raw_block).unwrap();\n\n        assert_eq!(new_block, banach_tarski)\n    }\n}\n\n#[cfg(test)]\nimpl PoolDiskHeader {\n    fn random() -> Self {\n        use crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\n\n        let mut random: ThreadRng = rand::rng();\n        // latest inode write isnt persisted to the pool on deserialization so we dont care here either\n        let latest_inode_write: DiskPointer = DiskPointer { disk: 1, block: 1 };\n\n        Self {\n            flags: PoolHeaderFlags::random(),\n            highest_known_disk: random.random(),\n            disk_with_next_free_block: random.random(),\n            pool_standard_blocks_free: random.random(),\n            block_usage_map: random_allocations(),\n            latest_inode_write, // This does not get saved to disk.\n        }\n    }\n}\n\n#[cfg(test)]\nimpl PoolHeaderFlags {\n    fn random() -> Self {\n        // Currently we only have the marker bit.\n        PoolHeaderFlags::RequiredHeaderBit\n    }\n}\n\nfn random_allocations() -> [u8; 360] {\n    let mut random: ThreadRng = rand::rng();\n    let mut buffer = [0u8; 360];\n    for byte in buffer.iter_mut() {\n        *byte = random.random()\n    }\n    buffer\n}\n"
  },
  {
    "path": "src/pool/disk/pool_disk/block/mod.rs",
    "content": "pub mod header;\n"
  },
  {
    "path": "src/pool/disk/pool_disk/mod.rs",
    "content": "pub mod block;\npub mod pool_disk_methods;\npub mod pool_disk_struct;\n"
  },
  {
    "path": "src/pool/disk/pool_disk/pool_disk_methods.rs",
    "content": "// Pool disk\n\n//Imports\n\nuse std::fs::File;\n\nuse log::error;\n\nuse crate::{\n    error_types::drive::DriveError,\n    pool::disk::{\n        drive_struct::DiskBootstrap,\n        generic::{\n            block::{\n                allocate::block_allocation::BlockAllocation,\n                block_structs::RawBlock,\n                crc::check_crc,\n            }, disk_trait::GenericDiskMethods, generic_structs::pointer_struct::DiskPointer, io::{read::read_block_direct, write::write_block_direct}\n        },\n        pool_disk::block::header::header_struct::PoolDiskHeader,\n    }\n};\n\nuse super::pool_disk_struct::PoolDisk;\n\n// Implementations\n\nimpl PoolDisk {\n    // there are no pool disk methods\n}\n\n// Bootstrapping\nimpl DiskBootstrap for PoolDisk {\n    fn bootstrap(_file: File, _disk_number: u16) -> Result<Self, DriveError> {\n        // Annoyingly, we do bootstrapping of the pool disk from elsewhere, so this has to be here just\n        // to fill criteria for DiskBootstrap\n        unreachable!(\"Pool disks are not bootstrapped.\")\n    }\n\n    /// This method will panic if fed an invalid block.\n    fn from_header(block: RawBlock, file: File) -> Self {\n        // Immediately check the CRC of the incoming block, we don't know what state it's in\n        if !check_crc(block.data) {\n            // CRC failed!\n            // If the CRC on the pool header has failed, we're cooked.\n            error!(\"Someday we should be able to recover from crc checks... that is not today.\");\n            panic!(\"Bad CRC on pool header!\");\n        };\n        // CRC is good, construct the disk...\n        // Expect is fine, dying this early isnt a big deal.\n        // Plus we should be only called after already verifying that the CRC is good, and that the block is not empty.\n        let header = PoolDiskHeader::from_block(&block).expect(\"Pool header block needs to be good at this point.\");\n        Self {\n            number: 0, // The pool disk is always disk 0\n            header,\n            disk_file: file,\n        }\n    }\n}\n\n// Block allocator\n// This disk has block level allocations\nimpl BlockAllocation for PoolDisk {\n    fn get_allocation_table(&self) -> &[u8] {\n        &self.header.block_usage_map\n    }\n\n    fn set_allocation_table(&mut self, new_table: &[u8]) -> Result<(), DriveError> {\n        self.header.block_usage_map = new_table\n            .try_into()\n            .expect(\"Incoming table size should be the same as outgoing.\");\n        self.flush()\n    }\n}\n\n// Generic\nimpl GenericDiskMethods for PoolDisk {\n    #[doc = \" Read a block\"]\n    #[doc = \" Cannot bypass CRC.\"]\n    fn unchecked_read_block(&self, block_number: u16) -> Result<RawBlock, DriveError> {\n        // This is the first call, we have not recursed.\n        read_block_direct(&self.disk_file, self.number, block_number, false, false)\n    }\n\n    #[doc = \" Write a block\"]\n    fn unchecked_write_block(&mut self, block: &RawBlock) -> Result<(), DriveError> {\n        // This is the first call, we have not recursed.\n        write_block_direct(&self.disk_file, block, false)\n    }\n\n    #[doc = \" Get the inner file used for IO operations\"]\n    fn disk_file(self) -> File {\n        self.disk_file\n    }\n\n    #[doc = \" Get the number of the floppy disk.\"]\n    fn get_disk_number(&self) -> u16 {\n        self.number\n    }\n\n    #[doc = \" Set the number of this disk.\"]\n    fn set_disk_number(&mut self, disk_number: u16) {\n        self.number = disk_number\n    }\n\n    #[doc = \" Get the inner file used for write operations\"]\n    fn disk_file_mut(&mut self) -> &mut File {\n        &mut self.disk_file\n    }\n\n    #[doc = \" Sync all in-memory information to disk\"]\n    fn flush(&mut self) -> Result<(), DriveError> {\n        error!(\"You cannot call flush on a pool disk header.\");\n        error!(\"This must be handled manually via a disk unchecked write.\");\n        panic!(\"Tried to flush a pool header with .flush() !\");\n    }\n    \n    #[doc = \" Write chunked data, starting at a block.\"]\n    fn unchecked_write_large(&mut self, _data:Vec<u8>, _start_block: DiskPointer) -> Result<(), DriveError> {\n        // We do not allow large writes to the pool disk.\n        // Man the pool disk really ended up useless didn't it?\n        panic!(\"No large writes on pool disks.\");\n    }\n    \n    #[doc = \" Read multiple blocks\"]\n    #[doc = \" Does not check CRC!\"]\n    fn unchecked_read_multiple_blocks(&self, _block_number: u16, _num_block_to_read: u16) -> Result<Vec<RawBlock>, DriveError> {\n        panic!(\"No large reads on pool disks.\");\n    }\n}\n"
  },
  {
    "path": "src/pool/disk/pool_disk/pool_disk_struct.rs",
    "content": "// poooool\n\n// Imports\n\nuse crate::pool::disk::pool_disk::block::header::header_struct::PoolDiskHeader;\n\n// Structs, Enums, Flags\n#[derive(Debug)]\npub struct PoolDisk {\n    /// Disk number\n    pub number: u16,\n    /// The disk's header\n    pub header: PoolDiskHeader,\n    // The disk's file\n    pub(in super::super) disk_file: std::fs::File,\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/directory/directory_methods.rs",
    "content": "// Directory? Is that come kind of surgery?\n\n// imports\n\n// Implementations\n\nuse log::debug;\n\nuse crate::{error_types::{block::BlockManipulationError, drive::DriveError}, pool::disk::{\n    generic::{\n        block::{\n            block_structs::RawBlock,\n            crc::add_crc_to_block\n        },\n        generic_structs::pointer_struct::DiskPointer,\n        io::cache::cache_io::CachedBlockIO,\n    },\n    standard_disk::block::{\n        directory::directory_struct::{\n            DirectoryBlock, DirectoryBlockFlags, DirectoryItem, DirectoryItemFlags\n        },\n        inode::inode_struct::{\n            Inode,\n            InodeDirectory,\n            InodeLocation,\n            InodeTimestamp\n        },\n    }\n}, tui::{notify::NotifyTui, tasks::TaskType}};\n\n// We can convert from a raw block to a directory bock, but not the other way around.\nimpl From<RawBlock> for DirectoryBlock {\n    fn from(block: RawBlock) -> Self {\n        Self::from_block(&block)\n    }\n}\n\nimpl DirectoryBlock {\n    /// This assumes that you are writing this block back to the same\n    /// location you got it from. If that is not the case, you need to swap out\n    /// the origin BEFORE using this method.\n    pub fn to_block(&self) -> RawBlock {\n        directory_block_to_bytes(self)\n    }\n    pub fn from_block(block: &RawBlock) -> Self {\n        directory_block_from_bytes(block)\n    }\n\n    /// Try to add an DirectoryItem to this block.\n    ///\n    /// Returns nothing.\n    pub fn try_add_item(&mut self, item: &DirectoryItem) -> Result<(), BlockManipulationError> {\n        directory_block_try_add_item(self, item)\n    }\n\n    /// Try to remove a item from a directory.\n    /// The item on the directory must match the item provided exactly.\n    ///\n    /// Returns nothing.\n    pub(in super::super) fn try_remove_item(\n        &mut self,\n        item: &DirectoryItem,\n    ) -> Result<(), BlockManipulationError> {\n        directory_block_try_remove_item(self, item)\n    }\n\n    /// Create a new directory block.\n    /// \n    /// Requires the location/destination of this block.\n    ///\n    /// New directory blocks are the new final block on the disk.\n    /// New directory blocks do not point to the next block (as none exists).\n    /// Caller is responsible with updating previous block to point to this new block if needed.\n    pub fn new(origin: DiskPointer) -> Self {\n        new_directory_block(origin)\n    }\n\n    /// Get the items located within this block.\n    /// This function is just to obscure the items by default, so higher up callers\n    /// use higher abstractions\n    pub fn get_items(&self) -> Vec<DirectoryItem> {\n        self.directory_items.clone()\n    }\n\n    /// Check if this block is empty\n    pub fn is_empty(&self) -> Result<bool, DriveError> {\n        Ok(self.list()?.len() == 0)\n    }\n}\n\n// funtions for those impls\n\nfn directory_block_try_remove_item(\n    block: &mut DirectoryBlock,\n    incoming_item: &DirectoryItem,\n) -> Result<(), BlockManipulationError> {\n    // Attempt to remove an item\n\n    // attempt the removal\n    if let Some(index) = block\n        .directory_items\n        .iter()\n        .position(|item| item == incoming_item)\n    {\n        // Item exists.\n        // update the free bytes counter\n        block.bytes_free += incoming_item.to_bytes(block.block_origin.disk).len() as u16;\n\n        // We can use swap_remove here since the ordering of items does not matter.\n        let _ = block.directory_items.swap_remove(index);\n        Ok(())\n    } else {\n        Err(BlockManipulationError::NotPresent)\n    }\n}\n\nfn directory_block_try_add_item(\n    block: &mut DirectoryBlock,\n    item: &DirectoryItem,\n) -> Result<(), BlockManipulationError> {\n    // Attempt to add a new item to the directory.\n\n    // check if we have room\n    let new_item_bytes: Vec<u8> = item.to_bytes(block.block_origin.disk);\n    let new_item_length: usize = new_item_bytes.len();\n\n    if new_item_length > block.bytes_free.into() {\n        // We don't have room for this inode. The caller will have to use another block.\n        return Err(BlockManipulationError::OutOfRoom);\n    }\n\n    // luckily since directory blocks dont require any ordering, we can just append it to the vec and update\n    // the amount of free space remaining, since writing the actual data will just happen at the deserialization stage.\n\n    block.directory_items.push(item.clone());\n\n    // Update free space\n    // This cast is fine, item lengths could never hit > 2^16\n    block.bytes_free -= new_item_length as u16;\n\n    // Done!\n    Ok(())\n}\n\nfn new_directory_block(origin: DiskPointer) -> DirectoryBlock {\n    // New block!\n\n    // Flags\n    // New blocks are assumed to be the last in the chain.\n    let flags: DirectoryBlockFlags = DirectoryBlockFlags::empty(); // Currently unused.\n\n    // Bytes free\n    // An empty block has 501 bytes free.\n    let bytes_free: u16 = 501;\n\n    // Next block\n    // New blocks assume we are the final block in the chain.\n    let next_block: DiskPointer = DiskPointer::new_final_pointer();\n\n    // Items\n    // New blocks have no items. duh.\n    // If this is the root disk, the caller needs to add the root directory.\n    // Not pre-allocated, since we have no idea what's going in here yet.\n    let directory_items: Vec<DirectoryItem> = Vec::new();\n\n    // All done.\n    DirectoryBlock {\n        flags,\n        bytes_free,\n        next_block,\n        block_origin: origin,\n        directory_items,\n    }\n}\n\n/// We assume this is being written to the same place as it originated.\nfn directory_block_to_bytes(block: &DirectoryBlock) -> RawBlock {\n    // Deconstruct the bock\n    let DirectoryBlock {\n        flags,\n        bytes_free,\n        next_block,\n        #[allow(unused_variables)] // The items are extracted in a different way\n        directory_items,\n        block_origin,\n    } = block;\n\n    let mut buffer: [u8; 512] = [0u8; 512];\n\n    // flags\n    buffer[0] = flags.bits();\n\n    // free bytes\n    buffer[1..1 + 2].copy_from_slice(&bytes_free.to_le_bytes());\n\n    // next block\n    buffer[3..3 + 4].copy_from_slice(&next_block.to_bytes());\n\n    // Directory items\n    buffer[7..7 + 501].copy_from_slice(&block.item_bytes_from_vec(block_origin.disk));\n\n    // add the CRC\n    add_crc_to_block(&mut buffer);\n\n    // All done!\n    // This block is going to be written, thus does not need disk information.\n    RawBlock {\n        block_origin: *block_origin,\n        data: buffer,\n    }\n}\n\nfn directory_block_from_bytes(block: &RawBlock) -> DirectoryBlock {\n    // Flags\n    let flags: DirectoryBlockFlags = DirectoryBlockFlags::from_bits_retain(block.data[0]);\n\n    // Free bytes, come and get 'em\n    let bytes_free: u16 = u16::from_le_bytes(block.data[1..1 + 2].try_into().expect(\"2 = 2\"));\n\n    // Next block\n    let next_block: DiskPointer =\n        DiskPointer::from_bytes(block.data[3..3 + 4].try_into().expect(\"2 = 2\"));\n\n    // The directory items\n    let directory_items: Vec<DirectoryItem> =\n        DirectoryBlock::item_vec_from_bytes(&block.data[7..7 + 501], block.block_origin.disk);\n\n    let block_origin = block.block_origin;\n\n    // All done\n    DirectoryBlock {\n        flags,\n        bytes_free,\n        next_block,\n        block_origin,\n        directory_items,\n    }\n}\n\n// Conversions for the Vec of items\nimpl DirectoryBlock {\n    fn item_bytes_from_vec(&self, destination_disk: u16) -> [u8; 501] {\n        let mut index: usize = 0;\n        let mut buffer: [u8; 501] = [0u8; 501];\n\n        // Iterate over the items\n        for i in &self.directory_items {\n            // Cast item to bytes\n            for byte in i.to_bytes(destination_disk) {\n                // Put bytes in the buffer.\n                buffer[index] = byte;\n                index += 1;\n            }\n        }\n        buffer\n    }\n\n    fn item_vec_from_bytes(bytes: &[u8], origin_disk: u16) -> Vec<DirectoryItem> {\n        let mut items: Vec<DirectoryItem> = Vec::with_capacity(83); // Theoretical limit\n        let mut index: usize = 0;\n        loop {\n            // Are we out of bytes?\n            if index >= bytes.len() {\n                break;\n            }\n\n            // Get the flags\n            let flags: DirectoryItemFlags = DirectoryItemFlags::from_bits(bytes[index])\n                .expect(\"Flags should only have used bits set.\");\n\n            // Check for marker bit\n            if !flags.contains(DirectoryItemFlags::MarkerBit) {\n                // No more items.\n                break;\n            }\n\n            // Do the conversion\n            let (item_size, item) = DirectoryItem::from_bytes(&bytes[index..], origin_disk);\n\n            // increment index\n            index += item_size as usize;\n\n            // Done with this one\n            items.push(item)\n        }\n\n        // All done\n        items\n    }\n}\n\n// Conversions for the Vec of items\nimpl DirectoryItem {\n    /// Turn an item into bytes. Requires the destination disk.\n    pub(super) fn to_bytes(&self, destination_disk: u16) -> Vec<u8> {\n        let mut vec: Vec<u8> = Vec::with_capacity(262); // Theoretical limit\n        // Flags\n        vec.push(self.flags.bits());\n\n        // Item name length\n        vec.push(self.name_length);\n\n        // The name of the item\n        vec.extend(self.name.as_bytes());\n\n        // location of the inode\n        vec.extend(self.location.as_bytes(destination_disk));\n\n        // All done\n        vec\n    }\n\n    // Returns self, and how many bytes it took to construct this.\n    pub(super) fn from_bytes(bytes: &[u8], origin_disk: u16) -> (u8, Self) {\n        let mut index: usize = 0;\n        // Flags\n        let flags: DirectoryItemFlags =\n            DirectoryItemFlags::from_bits(bytes[index]).expect(\"Flags should only have used bits set.\");\n        index += 1;\n\n        // Make sure the flag is set\n        assert!(flags.contains(DirectoryItemFlags::MarkerBit), \"Directory items must have the marker bit set.\");\n\n        // Item name length\n        let name_length: u8 = bytes[index];\n        index += 1;\n\n        // Item name\n        let name: String = String::from_utf8(bytes[index..index + name_length as usize].to_vec())\n            .expect(\"File names should be valid UTF-8\");\n        index += name_length as usize;\n        \n        // Inode location\n        let (location_size, location) = InodeLocation::from_bytes(&bytes[index..], origin_disk);\n        index += location_size as usize;\n\n        let done = Self {\n            flags,\n            name_length,\n            name,\n            location,\n        };\n\n        (index as u8, done)\n    }\n\n    /// Get the size of the item. Regardless of type.\n    pub(crate) fn get_size(&self) -> Result<u64, DriveError> {\n        let handle = NotifyTui::start_task(TaskType::GetSize, 1);\n        debug!(\"Getting size of `{}`...\", self.name);\n        // Grab the inode to work with\n        let inode: Inode = self.get_inode()?;\n\n        // If this is a file, it's easy\n        if let Some(file) = inode.extract_file() {\n            debug!(\"Item is a file, getting size directly...\");\n            NotifyTui::complete_task_step(&handle);\n            NotifyTui::finish_task(handle);\n            return Ok(file.get_size())\n        }\n        \n        // More work to do\n        NotifyTui::add_steps_to_task(&handle, 2);\n        \n        // Otherwise, this must be a directory, so we need the directory block\n        debug!(\"Item is a directory...\");\n        let inode_directory: InodeDirectory = inode.extract_directory().expect(\"Not a file, so its a directory.\");\n        NotifyTui::complete_task_step(&handle);\n        \n        // Load the block\n        debug!(\"Getting origin block...\");\n        let raw_block: RawBlock = CachedBlockIO::read_block(inode_directory.pointer)?;\n        \n        let directory: DirectoryBlock = DirectoryBlock::from_block(&raw_block);\n        NotifyTui::complete_task_step(&handle);\n        \n        // Now we can call the size method.\n        debug!(\"Calling `get_size` on loaded DirectoryBlock...\");\n        let found_size = directory.get_size()?;\n        NotifyTui::complete_task_step(&handle);\n        NotifyTui::finish_task(handle);\n        Ok(found_size)\n\n    }\n\n    /// Get when the inode / item was created.\n    pub(crate) fn get_created_time(&self) -> Result<InodeTimestamp, DriveError> {\n        // get the inode\n        let inode = self.get_inode()?;\n        Ok(inode.created)\n    }\n\n    /// Get when the inode / item was modified.\n    pub(crate) fn get_modified_time(&self) -> Result<InodeTimestamp, DriveError> {\n        // get the inode\n        let inode = self.get_inode()?;\n        Ok(inode.modified)\n    }\n\n    // /// All item types point to a block that holds their information.\n    // /// You can see what block they point to, but you REALLY should not be doing reads like this.\n    // fn get_items_pointer(&self) -> Result<DiskPointer, DriveError> {\n    //     Ok(self.get_inode()?.get_pointer())\n    // }\n\n    /// Turn a directory type DirectoryItem into a DirectoryBlock.\n    /// \n    /// Panics if fed a file.\n    pub(crate) fn get_directory_block(&self) -> Result<DirectoryBlock, DriveError> {\n        // Grab the inode to work with\n        let inode: Inode = self.get_inode()?;\n\n        if let Some(dir) = inode.extract_directory() {\n            // Get the directory block\n            let raw_block: RawBlock = CachedBlockIO::read_block(dir.pointer)?;\n            Ok(DirectoryBlock::from_block(&raw_block))\n        } else {\n            // This was not a file.\n            panic!(\"Attempted to turn a DirectoryItem of File type into a DirectoryBlock!\")\n        }\n    }\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/directory/directory_struct.rs",
    "content": "// Directory struct!\n\n// Imports\n\nuse bitflags::bitflags;\n\nuse crate::pool::disk::{\n    generic::generic_structs::pointer_struct::DiskPointer,\n    standard_disk::block::inode::inode_struct::InodeLocation,\n};\n\n// Structs / Enums / Flags\n\n#[derive(Debug, PartialEq, Eq, Clone)]\npub struct DirectoryItem {\n    pub flags: DirectoryItemFlags,\n    pub name_length: u8,\n    pub name: String,\n    pub location: InodeLocation,\n}\n\n// This type is not clone, since you could end up with a block that is out of sync due to\n// changes made on a copy/clone of it.\n#[derive(Debug, PartialEq, Eq)]\npub(crate) struct DirectoryBlock {\n    pub(super) flags: DirectoryBlockFlags,\n    pub(super) bytes_free: u16,\n    // Points to the next directory block.\n    // Directories are separate from each other, you cannot get from one directory to another by just following\n    // the next block pointer. This pointer represents a _continuation_ of the current directory.\n    pub next_block: DiskPointer,\n    // At runtime its useful to know where this block came from.\n    // This doesn't need to get written to disk.\n    pub block_origin: DiskPointer, // This MUST be set. it cannot point nowhere.\n    pub(crate) directory_items: Vec<DirectoryItem>,\n}\n\nbitflags! {\n    #[derive(Debug, PartialEq, Eq, Clone, Copy)]\n    pub struct DirectoryItemFlags: u8 {\n        const IsDirectory = 0b00000010; // Set if directory\n        const MarkerBit = 0b10000000;\n    }\n}\n\nbitflags! {\n    #[derive(Debug, PartialEq, Eq, Clone, Copy)]\n    pub struct DirectoryBlockFlags: u8 {\n        // Currently unused.\n    }\n}"
  },
  {
    "path": "src/pool/disk/standard_disk/block/directory/mod.rs",
    "content": "mod directory_methods;\npub(crate) mod directory_struct;\n#[cfg(test)]\nmod tests;\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/directory/tests.rs",
    "content": "// Directory tests\n// Unwrapping is okay here, since we want unexpected outcomes to fail tests.\n#![allow(clippy::unwrap_used)]\n\n// Imports\n\nuse rand::{self, Rng};\n\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\nuse crate::pool::disk::standard_disk::block::directory::directory_struct::DirectoryBlock;\nuse crate::pool::disk::standard_disk::block::directory::directory_struct::DirectoryItemFlags;\nuse crate::pool::disk::standard_disk::block::directory::directory_struct::DirectoryItem;\nuse crate::pool::disk::standard_disk::block::inode::inode_struct::InodeLocation;\n\nuse test_log::test; // We want to see logs while testing.\n\n// Tests\n\n#[test]\nfn blank_directory_block_serialization() {\n    // We need a origin for the block, even if nonsensical.\n    let block_origin = DiskPointer {\n        disk: 420,\n        block: 69,\n    };\n    let test_block: DirectoryBlock = DirectoryBlock::new(block_origin);\n    let serialized = test_block.to_block();\n    let deserialized = DirectoryBlock::from_block(&serialized);\n    assert_eq!(test_block, deserialized)\n}\n\n#[test]\nfn directory_item_serialization() {\n    for _ in 0..1000 {\n        // Needs a fake disk where this block was read from.\n        let fake_disk: u16 = 21; // you stupid\n        let test_item = DirectoryItem::get_random();\n        let serialized = test_item.to_bytes(fake_disk);\n        let (_, deserialized) = DirectoryItem::from_bytes(&serialized, fake_disk);\n        assert_eq!(test_item, deserialized)\n    }\n}\n\n// Should be covered by other tests, would need to be rewritten because serializing random data is no longer valid.\n// #[test]\n// fn filled_directory_block_serialization() {\n//     for _ in 0..1000 {\n//         // We need a origin for the block, even if nonsensical.\n//         let block_origin = DiskPointer {\n//             disk: 420,\n//             block: 69,\n//         };\n//         let mut test_block: DirectoryBlock = DirectoryBlock::new(block_origin);\n//         // Fill with random inodes until we run out of room.\n//         loop {\n//             match test_block.try_add_item(&DirectoryItem::get_random()) {\n//                 Ok(_) => continue,\n//                 Err(err) => match err {\n//                     DirectoryBlockError::NotEnoughSpace => {\n//                         // Done filling it up\n//                         break;\n//                     }\n//                     _ => panic!(\"Got an error while adding item!\"),\n//                 },\n//             }\n//         }\n// \n//         // Check serialization\n//         let serialized = test_block.to_block();\n//         let deserialized = DirectoryBlock::from_block(&serialized);\n//         assert_eq!(test_block, deserialized)\n//     }\n// }\n\n#[test]\nfn add_and_remove_to_directory_block() {\n    for _ in 0..1000 {\n        // We need a origin for the block, even if nonsensical.\n        let block_origin = DiskPointer {\n            disk: 420,\n            block: 69,\n        };\n        let mut test_block: DirectoryBlock = DirectoryBlock::new(block_origin);\n        // Fill with random inodes until we run out of room.\n        let random_item: DirectoryItem = DirectoryItem::get_random();\n        test_block.try_add_item(&random_item.clone()).unwrap();\n        // Make sure that went in\n        assert!(!test_block.directory_items.is_empty());\n        test_block.try_remove_item(&random_item).unwrap();\n        // Make sure it was removed\n        assert!(test_block.directory_items.is_empty());\n    }\n}\n\n#[test]\nfn adding_and_removing_updates_size() {\n    for _ in 0..1000 {\n        // We need a origin for the block, even if nonsensical.\n        let block_origin = DiskPointer {\n            disk: 420,\n            block: 69,\n        };\n        let mut test_block: DirectoryBlock = DirectoryBlock::new(block_origin);\n        let random_item: DirectoryItem = DirectoryItem::get_random();\n        let new_free = test_block.bytes_free;\n\n        test_block.try_add_item(&random_item).unwrap();\n        let added_free = test_block.bytes_free;\n\n        test_block.try_remove_item(&random_item).unwrap();\n        let removed_free = test_block.bytes_free;\n\n        // Added should have less space\n        assert!(added_free < new_free);\n        // removed should have more space\n        assert!(added_free < removed_free);\n        // The block should be empty again\n        assert!(new_free == removed_free);\n    }\n}\n\n// Impl for going gorilla mode, absolutely ape shit, etc\n\n#[cfg(test)]\nimpl DirectoryItemFlags {\n    fn new() -> Self {\n        // We always need the marker bit set\n        DirectoryItemFlags::MarkerBit\n    }\n}\n\n#[cfg(test)]\nimpl DirectoryItem {\n    fn get_random() -> Self {\n        let name: String = get_random_name();\n        let name_length: u8 = name.len().try_into().unwrap();\n        assert_eq!(name_length as usize, name.len());\n        let location = InodeLocation::get_random();\n        let flags = DirectoryItemFlags::new();\n        DirectoryItem {\n            flags,\n            name_length,\n            name,\n            location,\n        }\n    }\n}\n\n#[cfg(test)]\nfn get_random_name() -> String {\n    // make a random string of at most 255 characters, and at least 1 character\n    use rand::distr::{Alphanumeric, SampleString};\n    use std::cmp::max;\n\n    let mut random = rand::rng();\n    let random_length: u8 = max(random.random(), 1); // at least one character\n\n    // make the string\n    Alphanumeric.sample_string(&mut random, random_length as usize)\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/file_extents/file_extents_methods.rs",
    "content": "// Method acting, for extents.\n\n// Consts\n// This may change if I decide to get rid of the flags on data blocks, so here's a const.\npub(crate) const DATA_BLOCK_OVERHEAD: u64 = 5; // 1 flag, 4 checksum.\n\n// Imports\n\n\nuse crate::{error_types::block::BlockManipulationError, pool::disk::{\n    generic::{\n        block::{\n            block_structs::RawBlock,\n            crc::add_crc_to_block\n        },\n        generic_structs::pointer_struct::DiskPointer\n    },\n    standard_disk::block::file_extents::file_extents_struct::{\n        ExtentFlags,\n        FileExtent,\n        FileExtentBlock,\n        FileExtentBlockFlags,\n    },\n}};\n\n// Implementations\n\n// Impl the conversion from RawBlock\nimpl From<RawBlock> for FileExtentBlock {\n    fn from(value: RawBlock) -> FileExtentBlock {\n        from_bytes(&value)\n    }\n}\n\n// impl the extent vec to byte conversion\nimpl FileExtentBlock {\n    pub(super) fn extents_to_bytes(&self, destination_disk_number: u16) -> Vec<u8> {\n        extents_to_bytes(&self.extents, destination_disk_number)\n    }\n\n    pub(crate) fn from_block(block: &RawBlock) -> Self {\n        from_bytes(block)\n    }\n\n    /// Byte me!\n    /// \n    /// This assumes you will be writing this block back to where you got it from. If this\n    /// is not the case, you need to update the block origin before calling.\n    pub(crate) fn to_block(&self) -> RawBlock {\n        to_block(self)\n    }\n\n    /// Attempts to add a file extent to this block.\n    /// \n    /// Does not write new block to disk. Caller must write it.\n    ///\n    /// Returns nothing\n    pub(crate) fn add_extent(&mut self, extent: FileExtent) -> Result<(), BlockManipulationError> {\n        extent_block_add_extent(self, extent)\n    }\n\n    /// Create a new extent block.\n    /// \n    /// Requires a destination for the block.\n    ///\n    /// New Extent blocks are the new final block on the disk.\n    /// New Extent blocks do not point to the next block (as none exists).\n    /// Caller is responsible with updating previous block to point to this new block.\n    pub(crate) fn new(block_origin: DiskPointer) -> Self {\n        FileExtentBlock {\n            flags: FileExtentBlockFlags::default(),\n            bytes_free: 501, // new blocks have 501 free bytes\n            next_block: DiskPointer::new_final_pointer(),\n            extents: Vec::new(), // Not pre-allocated, no idea how much will end up in here.\n            block_origin,\n        }\n    }\n\n    /// Retrieves all extents within this _block_. NOT THE ENTIRE FILE.\n    /// \n    /// If you want all of the extents that a file contains, you should be calling\n    /// methods on the InodeFile itself.\n    /// \n    /// Returned extents may not contain the disk component of their pointers.\n    pub(crate) fn get_extents(&self) -> Vec<FileExtent> {\n        // Just a layer of abstraction to prevent direct access.\n        self.extents.clone()\n    }\n\n    // /// Helper function that calculates how many blocks an input amount of data will require.\n    // /// Does not take into account the sizes of FileExtent blocks or such, just the DataBlock size.\n    // /// We are assuming you aren't going to write more than 32MB at a time.\n    // pub fn size_to_blocks(size_in_bytes: u64) -> u16 {\n    //     // This calculation never changes, since the overhead of block is always the same.\n    //     // A block holds 512 bytes, but we reserve 1 bytes for the flags (Currently unused),\n    //     // and 4 more bytes for the checksum.\n    // \n    //     // We will always need to round up on this division.\n    //     let mut blocks: u64;\n    //     blocks = size_in_bytes / (512 - DATA_BLOCK_OVERHEAD);\n    //     // If there is a remainder, we also need to add an additional block.\n    //     if size_in_bytes % (512 - DATA_BLOCK_OVERHEAD) != 0 {\n    //         // One more.\n    //         blocks += 1;\n    //     }\n    //     // This truncates the value.\n    //     // if you are somehow about to write a buffer of >22 floppy disks in one go, you have bigger issues.\n    //     blocks as u16\n    // }\n\n    /// Forcibly replace all extents in a FileExtentBlock.\n    /// \n    /// This will also canonicalize the incoming extents. IE, if the disk in the extent matches the\n    /// disk this block comes from, we will remove the disk and update flags.\n    /// \n    /// You must ensure that the provided extents will fit. Otherwise this will panic.\n    /// If you aren't sure that the new items will fit,\n    /// you should NOT be calling this method.\n    /// \n    /// This can only be called on the last extent in the chain.\n    /// \n    /// Will automatically recalculate size.\n    pub(in super::super::super::block) fn force_replace_all_extents(&mut self, new_extents: Vec<FileExtent>) {\n        // Since outside callers cannot manually drain the extents from a block, this lets us make sure\n        // that if you NEED to update extents, you can do that safely, and recalculate the size automatically.\n\n        // Pull the extents in so we can modify them as needed.\n        let mut to_add = new_extents;\n\n        // Where are we?\n        let our_disk = self.block_origin.disk;\n        \n        // Empty ourselves\n        self.extents = Vec::with_capacity(to_add.len());\n        \n        // Yes this is a silly way to see what the default capacity of an extent block is, but im sure\n        // the compiler will just optimize all of it away.\n        let default_free = FileExtentBlock::new(DiskPointer::new_final_pointer()).bytes_free;\n\n        self.bytes_free = default_free;\n        \n        // Now add the new extents, fixing the disk numbers as needed.\n        for new in &mut to_add {\n            // if the disk is the same as the block origin, we will set the local flag and such.\n            if new.start_block.disk == our_disk {\n                // Disk matched, Give the extent the local flag.\n                // We don't need to update the disk number, since that'll toss itself on write.\n                new.flags.insert(ExtentFlags::LocalExtent);\n            } else {\n                // Remove the local flag, just in case.\n                new.flags.remove(ExtentFlags::LocalExtent);\n            }\n\n            // Add it\n            self.add_extent(*new).expect(\"Should be last extent, and new items shouldn't be too big.\")\n        }\n        // All done.\n    }\n}\n\n//\n// Functions\n//\n\n/// Add an extent to a block. Returns false if extent could not fit.\nfn extent_block_add_extent(\n    block: &mut FileExtentBlock,\n    extent: FileExtent,\n) -> Result<(), BlockManipulationError> {\n    // Try and add an extent to the block\n\n    // Yes this causes a lot of extent.to_bytes() calls, but\n    // we need to be able to toss the disk number.\n    // Its fast enough.\n\n    // What block this is going into\n    let destination_disk_number: u16 = block.block_origin.disk;\n\n    // Since new blocks always have to go at the end of the inode chain, if there\n    // is a block after this, the block needs to immediately fail.\n    if !block.next_block.no_destination() {\n        // Keep goin dawg, not this block.\n        return Err(BlockManipulationError::NotFinalBlockInChain)\n    }\n\n    // figure out how big the extent is.\n    // This always less than 2^16 bytes, truncation is fine.\n    let extent_size: u16 = extent.to_bytes(destination_disk_number).len() as u16;\n\n    // will it fit?\n    if extent_size > block.bytes_free {\n        // Nope!\n        return Err(BlockManipulationError::OutOfRoom);\n    }\n\n    // It'll fit! Add it to the Vec.\n    block.extents.push(extent);\n\n    // we are using that space now.\n    block.bytes_free -= extent_size;\n\n    Ok(())\n}\n\nfn from_bytes(block: &RawBlock) -> FileExtentBlock {\n    // flags\n    let flags: FileExtentBlockFlags = FileExtentBlockFlags::from_bits_retain(block.data[0]);\n    \n    // What block this came from\n    let origin_disk = block.block_origin.disk;\n\n    // bytes free\n    let bytes_free: u16 = u16::from_le_bytes(block.data[1..1 + 2].try_into().expect(\"2 = 2\"));\n\n    // Next block\n    let next_block: DiskPointer =\n        DiskPointer::from_bytes(block.data[3..3 + 4].try_into().expect(\"4 = 4\"));\n\n    // Extract the extents in this block\n    let extent_data = &block.data[7..7 + 501];\n    let extents: Vec<FileExtent> = bytes_to_extents(extent_data, origin_disk);\n\n    FileExtentBlock {\n        flags,\n        bytes_free,\n        next_block,\n        extents,\n        block_origin: block.block_origin,\n    }\n}\n\nfn to_block(extent_block: &FileExtentBlock) -> RawBlock {\n    let FileExtentBlock {\n        flags,\n        next_block,\n        bytes_free,\n        #[allow(unused_variables)] // The extents are extracted in a different way\n        extents,\n        block_origin: origin // We assume the block will be written back to the same spot it came from.\n    } = extent_block;\n\n    let mut buffer: [u8; 512] = [0u8; 512];\n    let mut index: usize = 0;\n\n    // bitflags\n    buffer[index] = flags.bits();\n    index += 1;\n\n    // free bytes\n    buffer[index..index + 2].copy_from_slice(&bytes_free.to_le_bytes());\n    index += 2;\n\n    // Next block\n    buffer[index..index + 4].copy_from_slice(&next_block.to_bytes());\n    index += 4;\n\n    // Extents\n    buffer[index..index + 501].copy_from_slice(&extent_block.extents_to_bytes(origin.disk));\n\n    // add the CRC\n    add_crc_to_block(&mut buffer);\n\n    let finished_block: RawBlock = RawBlock {\n        block_origin: extent_block.block_origin,\n        data: buffer,\n    };\n\n    finished_block\n}\n\n// Convert the extents to a properly sized array of bytes\nfn extents_to_bytes(extents: &[FileExtent], destination_disk_number: u16) -> Vec<u8> {\n    // I couldn't think of a nicer way to do this conversion\n    let mut index: usize = 0;\n    let mut buffer: [u8; 501] = [0u8; 501];\n\n    for i in extents {\n        for byte in i.to_bytes(destination_disk_number) {\n            buffer[index] = byte;\n            index += 1;\n        }\n    }\n    buffer.to_vec()\n}\n\n// Takes in bytes and makes extents, automatically determines when to stop.\nfn bytes_to_extents(bytes: &[u8], origin_disk_number: u16) -> Vec<FileExtent> {\n    let mut offset: usize = 0;\n\n    // As stated in `to_bytes` file extents are at most 6 bytes, so we will pre-allocate\n    // room for a totally full extent block, which right now is 501 bytes.\n    let mut extent_vec: Vec<FileExtent> = Vec::with_capacity(501_usize.div_ceil(6));\n\n    loop {\n        // make sure we dont go off the deep end\n        if offset >= bytes.len() {\n            // cant be more.\n            break;\n        }\n        // check for the marker\n        let flag = ExtentFlags::from_bits_retain(bytes[offset]);\n        if !flag.contains(ExtentFlags::MarkerBit) {\n            // no more extents to read.\n            break;\n        }\n\n        // read in an extent\n        let (bytes_used, new_extent) = FileExtent::from_bytes(&bytes[offset..], origin_disk_number);\n        extent_vec.push(new_extent);\n        // increment offset\n        offset += bytes_used as usize;\n    }\n\n    // Done!\n    extent_vec\n}\n\n// Welcome to subtype impl hell\n\nimpl FileExtent {\n    /// Must provide what disk the FileExtentBlock that contains this FileExtent will end up on.\n    /// \n    /// Ignores incoming Local flag, will update flags automatically.\n    pub(super) fn to_bytes(mut self, destination_disk_number: u16) -> Vec<u8> {\n        let mut vec: Vec<u8> = Vec::with_capacity(6); // At most 6 bytes.\n\n        // If the disk number is the same, we set the local flag.\n        if self.start_block.disk == destination_disk_number {\n            self.flags.insert(ExtentFlags::LocalExtent);\n        } else {\n            // Otherwise we wont. duh\n            self.flags.remove(ExtentFlags::LocalExtent);\n        }\n\n        // flags\n        vec.push(self.flags.bits());\n\n        if !self.flags.contains(ExtentFlags::LocalExtent) {\n            // Disk number\n            vec.extend_from_slice(\n                &self\n                    .start_block.disk\n                    .to_le_bytes(),\n            );\n        }\n\n        // Start block\n        vec.extend_from_slice(\n            &self\n                .start_block\n                .block\n                .to_le_bytes(),\n        );\n        // Length\n        vec.push(\n            self.length\n        );\n        \n\n        vec\n    }\n    /// You can feed feed this too many bytes, but as long as the flag is in the right spot, it will work correctly.\n    /// \n    /// Also returns how many bytes the read extent was made of.\n    pub(super) fn from_bytes(bytes: &[u8], origin_disk_number: u16) -> (u8, FileExtent) {\n        let mut offset: usize = 0; // Extents are always <=6 bytes, so we cast this later\n\n        let flags: ExtentFlags =\n            ExtentFlags::from_bits(bytes[0]).expect(\"Unused bits should not be set.\");\n        \n        offset += 1;\n\n        let disk_number: u16;\n\n        // Disk number\n        if flags.contains(ExtentFlags::LocalExtent) {\n            // Use the provided disk number.\n            disk_number = origin_disk_number;\n        } else {\n            disk_number = u16::from_le_bytes(bytes[offset..offset + 2].try_into().expect(\"2 = 2 \"),);\n            offset += 2;\n        }\n        \n        // Start block\n        let start_block: u16 = u16::from_le_bytes(bytes[offset..offset + 2].try_into().expect(\"2 = 2 \"));\n        offset += 2;\n\n        // Length\n        let length: u8 = bytes[offset];\n\n        // Final offset increment, since we are also using this to track size.\n        offset += 1;\n\n        // Construct a pointer for the start block\n        let start_block: DiskPointer = DiskPointer {\n            disk: disk_number,\n            block: start_block,\n        };\n\n        // Return the number of bytes this was constructed from, and the extent\n        let the_extent_of_it = FileExtent {\n            flags,\n            start_block,\n            length,\n        };\n\n        (offset as u8, the_extent_of_it)\n    }\n\n    /// Helper function that extracts all of the blocks that this extent refers to.\n    /// \n    /// Only gets info about this specific extent, does no traversal.\n    /// \n    /// Needs to know what disk this FileExtent came from.\n    pub(crate) fn get_pointers(&self) -> Vec<DiskPointer> {\n        // Each block that the extent references\n        let mut pointers: Vec<DiskPointer> = Vec::with_capacity(self.length.into());\n        for n in 0..self.length {\n            pointers.push(DiskPointer {\n                disk: self.start_block.disk,\n                block: self.start_block.block + n as u16\n            });\n        };\n        pointers\n    }\n\n    /// Make a new file extent\n    pub(crate) fn new(start_block: DiskPointer, length: u8) -> Self {\n        Self {\n            // These flags will be calculated on write.\n            flags: ExtentFlags::MarkerBit,\n            start_block,\n            length,\n        }\n    }\n}\n\n// Default bitflags\nimpl FileExtentBlockFlags {\n    pub fn default() -> Self {\n        // We aren't using any bits right now.\n        FileExtentBlockFlags::empty()\n    }\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/file_extents/file_extents_struct.rs",
    "content": "// File extents\n\n// Imports\n\nuse bitflags::bitflags;\n\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\n\n// Structs, Enums, Flags\n\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\npub struct FileExtent {\n    /// Callers should never have to care about the flags.\n    pub(super) flags: ExtentFlags,\n    /// Points to the first block of the extent. Inclusive.\n    pub(crate) start_block: DiskPointer,\n    /// How many blocks in a row starting from the start block\n    /// are data blocks for this file.\n    /// \n    /// Never traverses disks.\n    pub(crate) length: u8,\n}\n\nbitflags! {\n    #[derive(Debug, PartialEq, Eq, Clone, Copy)]\n    pub struct ExtentFlags: u8 {\n        // While the returned extents will always have their disk number set, at a lower level\n        // we save bytes by tossing the disk bytes if the extent is local. The disk number is\n        // then reconstructed on read.\n        const LocalExtent = 0b00000001;\n        const MarkerBit = 0b10000000;\n    }\n}\n\n// Extents block\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct FileExtentBlock {\n    pub(super) flags: FileExtentBlockFlags,\n    pub(super) bytes_free: u16,\n    pub(crate) next_block: DiskPointer,\n    // At runtime its useful to know where this block came from.\n    // This doesn't need to get written to disk.\n    pub block_origin: DiskPointer, // This MUST be set. it cannot point nowhere.\n    pub(super) extents: Vec<FileExtent>,\n}\n\nbitflags! {\n    #[derive(Debug, PartialEq, Eq, Clone, Copy)]\n    pub struct FileExtentBlockFlags: u8 {\n        // Currently unused.\n    }\n}"
  },
  {
    "path": "src/pool/disk/standard_disk/block/file_extents/mod.rs",
    "content": "pub mod file_extents_methods;\npub mod file_extents_struct;\n#[cfg(test)]\nmod tests;\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/file_extents/tests.rs",
    "content": "// Tests are cool.\n\n// Imports\n\nuse crate::error_types::block::BlockManipulationError;\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\n\nuse super::file_extents_struct::ExtentFlags;\nuse super::file_extents_struct::FileExtent;\nuse super::file_extents_struct::FileExtentBlock;\nuse rand::Rng;\nuse rand::rngs::ThreadRng;\n\nuse test_log::test; // We want to see logs while testing.\n\n// Tests\n\n#[test]\nfn random_extents_serialization() {\n    // Make some random extents and de/serialize them\n    for _ in 0..1000 {\n        // Need a test disk number for the serialization to happen on\n        let origin_disk: u16 = 42;\n        let test_extent = FileExtent::random();\n        let serialized = test_extent.to_bytes(origin_disk);\n        let (de_len, deserialized) = FileExtent::from_bytes(&serialized, origin_disk);\n        let re_serialized = deserialized.to_bytes(origin_disk);\n        let (re_de_len, re_deserialized) = FileExtent::from_bytes(&re_serialized, origin_disk);\n        assert_eq!(deserialized, re_deserialized);\n        assert_eq!(de_len, re_de_len);\n    }\n}\n\n#[test]\nfn empty_extent_block_serialization() {\n    let block_origin = DiskPointer {\n        disk: 420,\n        block: 69,\n    };\n    let test_block = FileExtentBlock::new(block_origin);\n    let serialized = test_block.to_block();\n    let deserialized = FileExtentBlock::from_block(&serialized);\n    assert_eq!(test_block, deserialized);\n}\n\n#[test]\nfn full_extent_block() {\n    let block_origin = DiskPointer {\n        disk: 420,\n        block: 69,\n    };\n    let mut test_block = FileExtentBlock::new(block_origin);\n    let mut extents: Vec<FileExtent> = Vec::new();\n    loop {\n        let new_extent: FileExtent = FileExtent::random();\n        match test_block.add_extent(new_extent) {\n            Ok(_) => {\n                // keep track of the extents we put in\n                extents.push(new_extent);\n                // keep going\n            }\n            Err(err) => match err {\n                BlockManipulationError::OutOfRoom => break, // full\n                _ => panic!(\"This only happens on one block, how is this not the final block?\")\n            },\n        }\n    }\n    // Make sure all of the extents stored correctly\n    let retrieved_extents: Vec<FileExtent> = test_block.get_extents();\n    assert!(extents.iter().all(|item| retrieved_extents.contains(item)));\n}\n\n#[test]\nfn random_block_serialization() {\n    for _ in 0..1000 {\n        // We need a origin for the block, even if nonsensical.\n        let block_origin = DiskPointer {\n            disk: 420,\n            block: 69,\n        };\n        let test_block = FileExtentBlock::get_random(block_origin);\n        let serialized = test_block.to_block();\n        let deserialized = FileExtentBlock::from_block(&serialized);\n        assert_eq!(test_block, deserialized)\n    }\n}\n\n// Helper functions\n\n#[cfg(test)]\nimpl FileExtentBlock {\n    fn get_random(block_origin: DiskPointer) -> Self {\n        let mut test_block = FileExtentBlock::new(block_origin);\n        let mut random: ThreadRng = rand::rng();\n        // Fill with a random amount of items.\n        loop {\n            // consider stopping early\n            if random.random_bool(0.50) {\n                break;\n            }\n            let new_extent: FileExtent = FileExtent::random();\n            match test_block.add_extent(new_extent) {\n                Ok(_) => {}\n                Err(err) => match err {\n                    BlockManipulationError::OutOfRoom => break, // full\n                    _ => panic!(\"This only happens on one block, how is this not the final block?\")\n                },\n            }\n        }\n        test_block\n    }\n}\n\n#[cfg(test)]\nimpl FileExtent {\n    fn random() -> Self {\n        let mut random: ThreadRng = rand::rng();\n        // Flags do not matter, they are auto deduced.\n        let flags = ExtentFlags::new();\n        let length: u8 = random.random();\n        let start_block: DiskPointer = DiskPointer::get_random();\n\n        // All done.\n        FileExtent {\n            flags,\n            start_block,\n            length,\n        }\n    }\n}\n\n#[cfg(test)]\nimpl ExtentFlags {\n    fn new() -> Self {\n        // always need the marker bit.\n        let mut flag = ExtentFlags::empty();\n        flag.insert(ExtentFlags::MarkerBit);\n        flag\n    }\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/header/header_methods.rs",
    "content": "// Imports\n\nuse crate::pool::disk::{\n    generic::{block::{block_structs::RawBlock, crc::add_crc_to_block}, generic_structs::pointer_struct::DiskPointer},\n    standard_disk::block::header::header_struct::{StandardDiskHeader, StandardHeaderFlags},\n};\n\n// Implementations\n\nimpl StandardDiskHeader {\n    pub fn from_block(raw_block: &RawBlock) -> StandardDiskHeader {\n        extract_header(raw_block)\n    }\n    pub fn to_block(&self) -> RawBlock {\n        to_disk_block(self)\n    }\n}\n\n// Impl the conversion from a RawBlock to a DiskHeader\nimpl From<RawBlock> for StandardDiskHeader {\n    fn from(value: RawBlock) -> Self {\n        extract_header(&value)\n    }\n}\n\n// Functions\n\n/// Extract header info from a disk\nfn extract_header(raw_block: &RawBlock) -> StandardDiskHeader {\n    // Time to pull apart the header!\n\n    // Bit flags\n    let flags: StandardHeaderFlags = StandardHeaderFlags::from_bits_retain(raw_block.data[8]);\n\n    // The disk number\n    let disk_number: u16 =\n        u16::from_le_bytes(raw_block.data[9..9 + 2].try_into().expect(\"Impossible\"));\n\n    // block usage bitplane\n    let block_usage_map: [u8; 360] = raw_block.data[148..148 + 360]\n        .try_into()\n        .expect(\"Impossible.\");\n\n    StandardDiskHeader {\n        flags,\n        disk_number,\n        block_usage_map,\n    }\n}\n\n/// Converts the header type into its equivalent 512 byte block\nfn to_disk_block(header: &StandardDiskHeader) -> RawBlock {\n    // Now, this might seem stupid to reconstruct the struct immediately, but\n    // doing this ensures that if the struct is updated, we have to look at this function\n    // as well.\n\n    let StandardDiskHeader {\n        flags,\n        disk_number,\n        block_usage_map,\n    } = header;\n\n    // Create the buffer for the header\n    let mut buffer: [u8; 512] = [0u8; 512];\n\n    // Magic numbers!\n    buffer[0..8].copy_from_slice(\"Fluster!\".as_bytes());\n\n    // Now the flags\n    buffer[8] = flags.bits();\n\n    // The disk number\n    buffer[9..9 + 2].copy_from_slice(&disk_number.to_le_bytes());\n\n    // The block map\n    buffer[148..148 + 360].copy_from_slice(block_usage_map);\n\n    // Now CRC it\n    add_crc_to_block(&mut buffer);\n\n    // Make the RawBlock\n    // Disk headers are always block 0.\n    let block_origin = DiskPointer {\n        disk: header.disk_number,\n        block: 0,\n    };\n    \n    let finished_block: RawBlock = RawBlock {\n        block_origin,\n        data: buffer,\n    };\n\n    // All done!\n    finished_block\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/header/header_struct.rs",
    "content": "// Imports\nuse bitflags::bitflags;\n\n// Structs, Enums, Flags\n\n/// The header of a disk\n#[derive(Debug, PartialEq, Eq, Clone)]\npub struct StandardDiskHeader {\n    pub flags: StandardHeaderFlags,\n    pub disk_number: u16,\n    pub block_usage_map: [u8; 360], // not to be indexed directly, use a method to check.\n}\n\nbitflags! {\n    #[derive(Debug, PartialEq, Eq, Clone, Copy)]\n    pub struct StandardHeaderFlags: u8 {\n        const Marker = 0b00100000; // Must be set.\n        // 0b01000000; // Reserved for dense disk\n        // 0b10000000; // Reserved for pool disk\n    }\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/header/mod.rs",
    "content": "mod header_methods;\npub mod header_struct;\n#[cfg(test)]\nmod tests;\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/header/tests.rs",
    "content": "// You need to test head? You can try on me, I guess...\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/inode/inode_methods.rs",
    "content": "// Inode a block, then he moved away.\n\n// Imports\n\n// Implementations\n\nuse std::time::Duration;\nuse std::time::SystemTime;\nuse std::time::UNIX_EPOCH;\n\nuse super::inode_struct::Inode;\nuse super::inode_struct::InodeBlock;\nuse super::inode_struct::InodeBlockFlags;\nuse super::inode_struct::InodeFlags;\nuse crate::error_types::block::BlockManipulationError;\nuse crate::error_types::drive::DriveError;\nuse crate::pool::disk::generic::block::crc::add_crc_to_block;\nuse crate::pool::disk::generic::generic_structs::find_space::find_free_space;\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\nuse crate::pool::disk::generic::io::cache::cache_io::CachedBlockIO;\nuse crate::pool::disk::generic::{\n    block::block_structs::RawBlock, generic_structs::find_space::BytePingPong,\n};\nuse crate::pool::disk::standard_disk::block::inode::inode_struct::InodeLocation;\nuse crate::pool::disk::standard_disk::block::inode::inode_struct::InodeOffsetPacking;\nuse crate::pool::disk::standard_disk::block::inode::inode_struct::PackedInodeLocationFlags;\nuse crate::pool::disk::standard_disk::block::inode::inode_struct::{\n    InodeDirectory, InodeFile, InodeTimestamp,\n};\n\nimpl From<RawBlock> for InodeBlock {\n    fn from(value: RawBlock) -> Self {\n        from_raw_block(&value)\n    }\n}\n\n// Add ability for inodes to have space searched for them\nimpl BytePingPong for Inode {\n    fn to_bytes(&self) -> Vec<u8> {\n        self.as_bytes()\n    }\n\n    fn from_bytes(bytes: &[u8]) -> Self {\n        Self::from_bytes(bytes)\n    }\n}\n\nimpl InodeBlock {\n    pub fn to_block(&self) -> RawBlock {\n        to_raw_bytes(self)\n    }\n    pub fn from_block(block: &RawBlock) -> Self {\n        from_raw_block(block)\n    }\n    /// Create a new inode block\n    ///\n    /// New Inode blocks are the new final block on the disk.\n    /// New Inode blocks do not point to the next block (as none exists).\n    /// Caller is responsible with updating previous block to point to this new block.\n    pub fn new(block_origin: DiskPointer) -> Self {\n        new_inode_block(block_origin)\n    }\n    /// Try to add an Inode to this block.\n    /// Updates the byte usage counter.\n    ///\n    /// This does NOT automatically flush information to the disk.\n    ///\n    /// Returns the offset of the added inode\n    pub fn try_add_inode(&mut self, inode: Inode) -> Result<u16, BlockManipulationError> {\n        inode_block_try_add_inode(self, inode)\n    }\n    /// Removes inodes based off of the offset into the block.\n    /// Updates the byte usage counter.\n    /// This does not remove the data the inode points to. The caller is responsible for propagation.\n    /// \n    /// Does not flush to disk.\n    ///\n    /// Returns nothing.\n    pub fn try_remove_inode(&mut self, inode_offset: u16) -> Result<(), BlockManipulationError> {\n        inode_block_try_remove_inode(self, inode_offset)\n    }\n    /// Try and read an inode from the block.\n    ///\n    /// Returns Inode.\n    pub fn try_read_inode(&self, inode_offset: u16) -> Result<Inode, BlockManipulationError> {\n        inode_block_try_read_inode(self, inode_offset)\n    }\n    /// Set a new destination on a block.\n    ///\n    /// Does not flush the new destination to disk, only updates it.\n    ///\n    /// May swap disks. Will not return to original disk.\n    pub fn new_destination(&mut self, pointer: DiskPointer) {\n        // dont feel like splitting this into a function rn, sue me.\n        self.next_inode_block = pointer;\n    }\n    /// Underlying inode information (File size for example) may change, so we need to be able to\n    /// update our insides.\n    /// \n    /// Updates internal data, flushes to disk.\n    /// \n    /// Incoming inode data must be the same size as the pre-existing inode.\n    /// Will panic if incoming inode is of the wrong size.\n    /// Will panic if there is not an inode at the location you are trying to overwrite.\n    /// \n    /// May swap disks, does not return to caller disk. Ends up wherever the inode block originally came from.\n    /// \n    /// Returns nothing,\n    pub fn update_inode(&mut self, inode_offset: u16, updated_inode: Inode,) -> Result<(), DriveError> {\n        // get the item at the current offset\n        let old = self.try_read_inode(inode_offset).expect(\"Caller should provide valid offset\");\n\n        // Find out how big that is\n        let old_size = old.as_bytes().len();\n\n        // Make sure the new one is the right size\n        let new_size = updated_inode.as_bytes().len();\n\n        assert_eq!(old_size, new_size, \"To update an inode, the new inode must be the same size as the old one.\");\n\n        // Now that we know we can safely perform this operation, we will directly edit ourselves.\n        self.inodes_data[inode_offset as usize..inode_offset as usize + old_size].copy_from_slice(&updated_inode.as_bytes());\n\n        // Now we need to flush these updates to disk\n        let raw = self.to_block();\n        CachedBlockIO::update_block(&raw)?;\n\n        // All done!\n        Ok(())\n    }\n}\n\n//\n// Functions\n//\n\nfn inode_block_try_read_inode(block: &InodeBlock, offset: u16) -> Result<Inode, BlockManipulationError> {\n    // Attempt to read in the inode at this location\n    // extract function at bottom of file\n\n    // Bounds checking\n    if offset as usize > block.inodes_data.len() {\n        // We cannot read past the end of the end of the data!\n        return Err(BlockManipulationError::Impossible);\n    }\n    // get a slice with that inode and deserialize it\n    Ok(Inode::from_bytes(&block.inodes_data[offset as usize..]))\n}\n\nfn inode_block_try_remove_inode(\n    block: &mut InodeBlock,\n    inode_offset: u16,\n) -> Result<(), BlockManipulationError> {\n    // Attempt to remove an inode from the block\n\n    // Assumption:\n    // Caller gave us a valid offset.\n    // There isn't a great way to check this besides scanning through the entire block to find all of the\n    // inodes, but we can at least check the marker bit.\n    // Additionally, if there are extra unused bits set in the flags, this is almost certainly an invalid offset.\n    let flags = match InodeFlags::from_bits(block.inodes_data[inode_offset as usize]) {\n        Some(ok) => ok,\n        None => {\n            // Unused bits are set. This cannot be the start of an inode.\n            return Err(BlockManipulationError::Impossible);\n        }\n    };\n\n    if !flags.contains(InodeFlags::MarkerBit) {\n        // Missing flag.\n        // This cannot be the beginning of an inode.\n        return Err(BlockManipulationError::Impossible);\n    };\n\n    // Assumption: There is a valid inode at the provided offset\n    // Yes the cast back and forth is silly, but at least its easy.\n    let inode_to_remove_length: usize =\n        Inode::from_bytes(&block.inodes_data[inode_offset as usize..])\n            .as_bytes()\n            .len();\n\n    // Blank out those bytes\n    // This range is inclusive because we are removing the last byte of the item as well, not just up to the last byte.\n    block.inodes_data[inode_offset as usize..inode_offset as usize + inode_to_remove_length]\n        .iter_mut()\n        .for_each(|byte| *byte = 0);\n\n    // sanity check, bytes are now empty\n    #[cfg(test)]\n    {\n        for i in 0..inode_to_remove_length {\n            assert_eq!(block.inodes_data[inode_offset as usize + i], 0, \"Zeroed out an Inode, but it didn't end up blank!\")\n        }\n    }\n\n    // update how many bytes are free\n    block.bytes_free += inode_to_remove_length as u16;\n\n    // Done\n    Ok(())\n}\n\nfn inode_block_try_add_inode(\n    inode_block: &mut InodeBlock,\n    new_inode: Inode,\n) -> Result<u16, BlockManipulationError> {\n    // Attempt to add an inode to the block.\n\n    // Check if we have room for the new inode.\n    let new_inode_bytes: Vec<u8> = new_inode.as_bytes();\n    let new_inode_length: usize = new_inode_bytes.len();\n\n    if new_inode_length > inode_block.bytes_free.into() {\n        // We don't have room for this inode. The caller will have to use another block.\n        return Err(BlockManipulationError::OutOfRoom);\n    }\n\n    // find a spot to put our new Inode\n    let offset = match find_free_space::<Inode>(&inode_block.inodes_data, new_inode_length) {\n        Some(ok) => ok,\n        None => {\n            // couldn't find enough space, block must be fragmented.\n            // Defrag is hard with only pointers that go in one direction... So no defrag.\n            return Err(BlockManipulationError::OutOfRoom);\n        }\n    };\n\n    // Put in the Inode\n    inode_block.inodes_data[offset..offset + new_inode_length].copy_from_slice(&new_inode_bytes);\n\n    // Subtract the new space we've taken up\n    // Cast from usize to u16 should be fine in all cases,\n    // how would an inode be more than 2^16 bytes? lol.\n    inode_block.bytes_free -= new_inode_length as u16;\n\n    // Return that offset, we're done.\n    // Cast: max of 501 is < u16. Safe.\n    Ok(offset as u16)\n}\n\nfn new_inode_block(block_origin: DiskPointer) -> InodeBlock {\n    // Create the flags\n    // No default flags are required.\n    let flags: InodeBlockFlags = InodeBlockFlags::empty();\n\n    // An inode block with no content has 501 bytes free.\n    let bytes_free: u16 = 501;\n\n    // Since this is the final block on the disk, and we obviously cant\n    // point to the next disk, since we dont know if it even exists.\n    // Thus, this is the end of the Inode chain.\n    let next_inode_block: DiskPointer = DiskPointer::new_final_pointer();\n\n    // A new inode block has no inodes in it.\n    // Special care must be taken by the caller to\n    // ensure to put the root inode into the root disk.\n    let inodes_data: [u8; 501] = [0u8; 501];\n\n    // all done\n    InodeBlock {\n        flags,\n        bytes_free,\n        next_inode_block,\n        inodes_data,\n        block_origin,\n    }\n}\n\nfn from_raw_block(block: &RawBlock) -> InodeBlock {\n    // Flags\n    let flags: InodeBlockFlags = InodeBlockFlags::from_bits_retain(block.data[0]);\n\n    // Bytes free\n    let bytes_free: u16 = u16::from_le_bytes(block.data[1..1 + 2].try_into().expect(\"2 into 2\"));\n\n    // Next inode block\n    let next_inode_block: DiskPointer =\n        DiskPointer::from_bytes(block.data[3..3 + 4].try_into().expect(\"4 into 4\"));\n\n    // Inodes\n    let inodes_data: [u8; 501] = block.data[7..7 + 501].try_into().expect(\"501 into 501\");\n\n    // From dust we came\n    let block_origin: DiskPointer = block.block_origin;\n\n    // All done\n    InodeBlock {\n        flags,\n        bytes_free,\n        next_inode_block,\n        inodes_data,\n        block_origin,\n    }\n}\n\nfn to_raw_bytes(block: &InodeBlock) -> RawBlock {\n    let InodeBlock {\n        flags,\n        bytes_free,\n        next_inode_block,\n        inodes_data,\n        block_origin, // And to dust we shall return.\n    } = block;\n\n    let mut buffer: [u8; 512] = [0u8; 512];\n\n    // Flags\n    buffer[0] = flags.bits();\n\n    // Bytes free\n    buffer[1..1 + 2].copy_from_slice(&bytes_free.to_le_bytes());\n\n    // next inode block\n    buffer[3..3 + 4].copy_from_slice(&next_inode_block.to_bytes());\n\n    // inodes\n    buffer[7..7 + 501].copy_from_slice(inodes_data);\n\n    // crc\n    add_crc_to_block(&mut buffer);\n\n    // Make the block\n    let final_block: RawBlock = RawBlock {\n        block_origin: *block_origin,\n        data: buffer,\n    };\n\n    final_block\n}\n\n//\n// impl for subtypes\n//\n\nimpl Inode {\n    pub(super) fn as_bytes(&self) -> Vec<u8> {\n        let mut vec: Vec<u8> = Vec::with_capacity(37); // max size of an inode\n\n        // flags\n        vec.push(self.flags.bits());\n\n        // Inode data\n        // There should never be both a file and a directory in an inode.\n        if let Some(directory) = self.directory {\n            vec.extend(directory.as_bytes());\n        }\n\n        if let Some(file) = self.file {\n            vec.extend(file.as_bytes());\n        }\n\n        // Timestamps\n        // Created\n        vec.extend(self.created.to_bytes());\n\n        // Modified\n        vec.extend(self.modified.to_bytes());\n\n        // All done.\n        vec\n    }\n\n    /// Will only read the first inode in provided slice.\n    /// No validation is done to check if this is a valid inode!\n    /// Caller MUST ensure this is a valid slice that contains an inode starting\n    /// at bit zero, otherwise no guarantees can be made about the returned inode.\n    pub(super) fn from_bytes(bytes: &[u8]) -> Self {\n        let mut timestamp_offset: usize = 0;\n\n        // Flags\n        let flags: InodeFlags =\n            InodeFlags::from_bits(bytes[0]).expect(\"Flags should only have used bits set.\");\n        timestamp_offset += 1;\n\n        // We must have the marker bit.\n        assert!(flags.contains(InodeFlags::MarkerBit), \"Inodes must contain the marker bit.\");\n\n        // File or directory\n        let file: Option<InodeFile> = if flags.contains(InodeFlags::FileType) {\n            timestamp_offset += 12;\n            Some(InodeFile::from_bytes(\n                bytes[1..1 + 12].try_into().expect(\"12 = 12\"),\n            ))\n        } else {\n            None\n        };\n\n        let directory: Option<InodeDirectory> = if !flags.contains(InodeFlags::FileType) {\n            timestamp_offset += 4;\n            Some(InodeDirectory::from_bytes(\n                bytes[1..1 + 4].try_into().expect(\"4 = 4\"),\n            ))\n        } else {\n            None\n        };\n\n        // Timestamps\n\n        // Created\n        let created: InodeTimestamp = InodeTimestamp::from_bytes(\n            bytes[timestamp_offset..timestamp_offset + 12]\n                .try_into()\n                .expect(\"12 = 12\"),\n        );\n\n        // Created timestamp is 12 bytes.\n        timestamp_offset += 12;\n\n        // Modified\n        let modified: InodeTimestamp = InodeTimestamp::from_bytes(\n            bytes[timestamp_offset..timestamp_offset + 12]\n                .try_into()\n                .expect(\"12 = 12\"),\n        );\n\n        // Done.\n        Self {\n            flags,\n            file,\n            directory,\n            created,\n            modified,\n        }\n    }\n}\n\nimpl InodeFile {\n    fn as_bytes(&self) -> [u8; 12] {\n        let mut buffer: [u8; 12] = [0u8; 12];\n        buffer[..8].copy_from_slice(&self.size.to_le_bytes());\n        buffer[8..].copy_from_slice(&self.pointer.to_bytes());\n        buffer\n    }\n    fn from_bytes(bytes: [u8; 12]) -> Self {\n        Self {\n            size: u64::from_le_bytes(bytes[..8].try_into().expect(\"8 = 8\")),\n            pointer: DiskPointer::from_bytes(bytes[8..].try_into().expect(\"4 = 4\")),\n        }\n    }\n}\n\nimpl InodeDirectory {\n    fn as_bytes(&self) -> [u8; 4] {\n        self.pointer.to_bytes()\n    }\n    fn from_bytes(bytes: [u8; 4]) -> Self {\n        Self {\n            pointer: DiskPointer::from_bytes(bytes),\n        }\n    }\n    // Converts a disk pointer into a directory\n    pub fn from_disk_pointer(pointer: DiskPointer) -> Self {\n        Self { pointer }\n    }\n}\n\nimpl InodeTimestamp {\n    pub(super) fn to_bytes(self) -> [u8; 12] {\n        let mut buffer: [u8; 12] = [0u8; 12];\n        buffer[..8].copy_from_slice(&self.seconds.to_le_bytes());\n        buffer[8..].copy_from_slice(&self.nanos.to_le_bytes());\n        buffer\n    }\n    pub(super) fn from_bytes(bytes: [u8; 12]) -> Self {\n        Self {\n            seconds: u64::from_le_bytes(bytes[..8].try_into().expect(\"8 = 8\")),\n            nanos: u32::from_le_bytes(bytes[8..].try_into().expect(\"4 = 4\")),\n        }\n    }\n    // Create a timestamp that refers to the current moment in time.\n    pub fn now() -> Self {\n        // Get the time\n        let now = SystemTime::now();\n        let duration_since_epoch = now\n            .duration_since(UNIX_EPOCH)\n            .expect(\"You shouldn't be using fluster in the 1960s. If you are, email me.\");\n        Self {\n            seconds: duration_since_epoch.as_secs(),\n            nanos: duration_since_epoch.subsec_nanos(),\n        }\n    }\n}\n\n// impl InodeFlags {\n//     pub fn new() -> Self {\n//         // We need the marker bit.\n//         InodeFlags::MarkerBit\n//     }\n// }\n\nimpl InodeLocation {\n    pub fn as_bytes(&self, destination_disk: u16) -> Vec<u8> {\n        let mut vec: Vec<u8> = Vec::with_capacity(6); // Max size of this type\n\n        // We can ignore the offset contained within self, since we dont write that value, we\n        // write it with packed instead.\n\n        // Calculate the flags\n        let (mut flags, offset) = self.packed.extract();\n\n        // Can we make this a diskless location?\n        if destination_disk == self.pointer.disk {\n            // Yes\n            flags.insert(PackedInodeLocationFlags::RequiresDisk);\n        } else {\n            // No\n            flags.remove(PackedInodeLocationFlags::RequiresDisk);\n        }\n\n        // Pack that back down to update the flags.\n        let packed = InodeOffsetPacking::new(flags, offset);\n\n        // The packed offset stuffs\n        vec.extend_from_slice(&packed.inner.to_le_bytes());\n\n        // Disk number, if required.\n        if !flags.contains(PackedInodeLocationFlags::RequiresDisk) {\n            vec.extend_from_slice(&self.pointer.disk.to_le_bytes());\n        }\n\n        // Block on disk\n        vec.extend_from_slice(&self.pointer.block.to_le_bytes());\n\n        vec\n    }\n    /// Turn the location into bytes. Takes in as many bytes as needed, and no more.\n    /// \n    /// Returns how many bytes it took to create this.\n    /// \n    /// Assumes its fed valid bytes, will panic if not.\n    /// \n    /// Must pass in the disk this came from.\n    pub fn from_bytes(bytes: &[u8], origin_disk: u16) -> (u8, Self) {\n        let mut index: usize = 0;\n\n        // Extract the flags\n        let packed_number = u16::from_le_bytes(bytes[index..index + 2].try_into().expect(\"2 = 2\"));\n        let packed = InodeOffsetPacking::from_u16(packed_number);\n        let (flags, offset) = packed.extract();\n\n        index += 2;\n\n        // Make sure this is a valid InodeLocation\n        assert!(flags.contains(PackedInodeLocationFlags::MarkerBit), \"Inodes must contain the flag bit.\");\n\n        \n        // Disk number\n        // Only need to grab if flag is set\n        let disk: u16 = if flags.contains(PackedInodeLocationFlags::RequiresDisk) {\n            origin_disk\n        } else {\n            // Go get it\n            let tmp = u16::from_le_bytes(bytes[index..index + 2].try_into().expect(\"2 = 2\"));\n            index += 2;\n            tmp\n        };\n        \n        // The block\n        let block: u16 = u16::from_le_bytes(bytes[index..index + 2].try_into().expect(\"2 = 2\"));\n        index += 2;\n\n        // Make the pointer\n        let pointer: DiskPointer = DiskPointer {\n            disk,\n            block,\n        };\n\n        // Re-assemble packed, we dont use any flags besides the marker bit outside of here.\n        let fit_girl_repacked = InodeOffsetPacking::new(PackedInodeLocationFlags::MarkerBit, offset);\n\n        // All done.\n        let done = InodeLocation {\n            packed: fit_girl_repacked,\n            pointer,\n            offset\n        };\n\n        (index as u8, done)\n    }\n\n    /// Make a new one!\n    pub(crate) fn new(pointer: DiskPointer, offset: u16) -> Self {\n        // In theory the flags are not read, they are calculated on write.\n        // Just set the marker bit.\n        Self {\n            packed: InodeOffsetPacking::new(PackedInodeLocationFlags::MarkerBit, offset),\n            pointer,\n            offset,\n        }\n    }\n}\n\nimpl InodeBlock {\n    /// Find the next block in the inode chain, if it exists.\n    pub fn next_block(&self) -> Option<DiskPointer> {\n        // First check if we have anywhere to go\n        if self.next_inode_block.no_destination() {\n            // Nowhere to go.\n            None\n        } else {\n            Some(self.next_inode_block)\n        }\n    }\n}\n\n\nimpl InodeFile {\n    /// New empty inode files\n    /// Pointer points to the first extent block.\n    pub fn new(disk_pointer: DiskPointer) -> Self {\n        Self {\n            size: 0, // New files are empty\n            pointer: disk_pointer,\n        }\n    }\n    /// Get size of a file\n    pub fn get_size(&self) -> u64 {\n        self.size\n    }\n    /// Set the size of the file\n    /// \n    /// Does not flush change to disk.\n    pub fn set_size(&mut self, size: u64) {\n        self.size = size;\n    }\n}\n\n\n// Extract an inode into its inner type\nimpl Inode {\n    pub fn extract_file(&self) -> Option<InodeFile> {\n        self.file\n    }\n    pub fn extract_directory(&self) -> Option<InodeDirectory> {\n        self.directory\n    }\n    // /// All inodes point somewhere.\n    // pub fn get_pointer(&self) -> DiskPointer {\n    //     if let Some(dir) = self.extract_directory() {\n    //         dir.pointer\n    //     } else {\n    //         self.extract_file().expect(\"Not a directory, so it must be a file.\").pointer\n    //     }\n    // }\n}\n\n// convert an inode timestamp into SystemTime\nimpl From<InodeTimestamp> for SystemTime {\n    fn from(value: InodeTimestamp) -> Self {\n        // All of our measurements are relative to the Unix Epoch\n        // https://xkcd.com/376/\n\n        let epoch: SystemTime = SystemTime::UNIX_EPOCH;\n        \n        // Get the offset\n        let duration: Duration = Duration::new(value.seconds, value.nanos);\n        \n        // 88MPH\n        epoch.checked_add(duration).expect(\"This will break in 2038. Until then, hi dad!\")\n    }\n}\n\n\n// Deconstruct packed InodeLocation information to get the flags and inode offset\nimpl InodeOffsetPacking {\n    /// Extract the flags and an offset.\n    pub(super) fn extract(&self) -> (PackedInodeLocationFlags, u16) {\n        // First we get the flags, the flags expect 8 bits, so we nab those.\n        // Shift right 8 times to bring the flags in line.\n        let flag_bits: u8 = (self.inner >> 8) as u8;\n\n        // This can leave a trailing bit at the end, but truncating will remove it.\n        let flags: PackedInodeLocationFlags = PackedInodeLocationFlags::from_bits_truncate(flag_bits);\n\n        // Now we need the number, which we can get by laying the flags back on top of the inner value.\n        // get the flag bits, invert them, shift them back over, then mask em out\n        // The only remaining bits must be the offset value.\n        let truncated_flag_bits: u16 = !((flags.bits() as u16) << 8);\n        let offset: u16 = self.inner & truncated_flag_bits;\n\n        // All done.\n        (flags, offset)\n    }\n\n    /// Make a new one\n    /// This can only be used down here, you should be using InodeLocation::new().\n    fn new(flags: PackedInodeLocationFlags, offset: u16) -> Self {\n        // See extract() for more docs about what is goin on here.\n        let inner: u16 = offset | ((flags.bits() as u16) << 8);\n        Self {\n            inner,\n        }\n    }\n\n    /// From a u16.\n    /// \n    /// Yes this is silly.\n    pub(super) fn from_u16(incoming: u16) -> Self {\n        Self { inner: incoming }\n    }\n}"
  },
  {
    "path": "src/pool/disk/standard_disk/block/inode/inode_struct.rs",
    "content": "// Inode layout\n\n// Imports\n\nuse bitflags::bitflags;\n\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\n\n// Structs, Enums, Flags\n\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\npub(crate) struct Inode {\n    pub flags: InodeFlags,\n    pub file: Option<InodeFile>,\n    pub directory: Option<InodeDirectory>,\n    pub created: InodeTimestamp,\n    pub modified: InodeTimestamp,\n}\n\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\npub struct InodeFile {\n    /// The size of the pointed to file in bytes.\n    pub(super) size: u64,\n    /// Points to the first extent block in the chain for this file.\n    pub(crate) pointer: DiskPointer,\n}\n\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\npub(crate) struct InodeDirectory {\n    /// Points to a DirectoryBlock.\n    pub(crate) pointer: DiskPointer,\n}\n\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\n/// Relative to Unix Epoch\npub struct InodeTimestamp {\n    pub(crate) seconds: u64,\n    pub(crate) nanos: u32,\n}\n\n// Points to a specific inode globally\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\npub struct InodeLocation {\n    /// The InodeBlock that this offset goes into can only hold 501 bytes, thus\n    /// many of the bits in the u16 are unused. (only 9 out of 16 bits are used).\n    /// Thus the upper 7 bits are free for other uses. We can pack flags in here too.\n    /// When reconstructing disk pointers, we need to know if this is local or not, so\n    /// we will use the second highest bit on the offset to denote if we need a disk number to\n    /// reconstruct the pointer.\n    /// \n    /// We will use the highest bit as a marker bit for reading.\n    /// \n    /// For ordering sake, it'll also go at the front of the type.\n    /// \n    /// Must also be private, since you cannot get the usual offset without a method call now.\n    pub(super) packed: InodeOffsetPacking,\n    /// Disk component automatically gets tossed and added on write/read.\n    pub(crate) pointer: DiskPointer,\n    /// This offset is extracted from packed during read\n    pub(crate) offset: u16,\n}\n\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\npub(super) struct InodeOffsetPacking {\n    pub(super) inner: u16, // Combination of flags and the offset.\n}\n\nbitflags! {\n    #[derive(Debug, PartialEq, Eq, Clone, Copy)]\n    pub struct InodeFlags: u8 {\n        const FileType = 0b00000001; // Set if this is a file\n        const MarkerBit = 0b10000000; // Always set\n    }\n}\n\nbitflags! {\n    #[derive(Debug, PartialEq, Eq, Clone, Copy)]\n    pub(super) struct PackedInodeLocationFlags: u8 {\n        const MarkerBit = 0b10000000; // Always set\n        const RequiresDisk = 0b01000000; // Does this InodeLocation require a disk to be reconstructed?\n        // 5 unused bits.\n    }\n}\n\n// The block\n\n#[derive(Debug, PartialEq, Eq, Clone)]\npub struct InodeBlock {\n    pub(super) flags: InodeBlockFlags,\n    // Manipulating Inodes must be done through methods on the struct\n    pub(super) bytes_free: u16,\n    pub(super) next_inode_block: DiskPointer,\n    // At runtime its useful to know where this block came from.\n    // This doesn't need to get written to disk.\n    pub block_origin: DiskPointer, // This MUST be set. it cannot point nowhere.\n    pub(super) inodes_data: [u8; 501],\n}\n\nbitflags! {\n    #[derive(Debug, PartialEq, Eq, Clone, Copy)]\n    pub struct InodeBlockFlags: u8 {\n        // Currently unused.\n    }\n}"
  },
  {
    "path": "src/pool/disk/standard_disk/block/inode/mod.rs",
    "content": "mod inode_methods;\npub mod inode_struct;\n#[cfg(test)]\nmod tests;\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/inode/tests.rs",
    "content": "// inode the tests.\n// Unwrapping is okay here, since we want unexpected outcomes to fail tests.\n#![allow(clippy::unwrap_used)]\n\n// Imports\n\n// Tests\n\nuse crate::error_types::block::BlockManipulationError;\nuse crate::pool::disk::standard_disk::block::inode::inode_struct::Inode;\nuse crate::pool::disk::standard_disk::block::inode::inode_struct::InodeFile;\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\nuse crate::pool::disk::standard_disk::block::inode::inode_struct::InodeBlock;\nuse crate::pool::disk::standard_disk::block::inode::inode_struct::InodeFlags;\nuse crate::pool::disk::standard_disk::block::inode::inode_struct::InodeLocation;\nuse crate::pool::disk::standard_disk::block::inode::inode_struct::InodeDirectory;\nuse crate::pool::disk::standard_disk::block::inode::inode_struct::InodeTimestamp;\nuse rand::Rng;\n\nuse test_log::test; // We want to see logs while testing.\n#[test]\nfn blank_inode_block_serialization() {\n    // Just like the directory blocks, we must spoof the disk read.\n    let block_origin = DiskPointer {\n        disk: 420,\n        block: 69,\n    };\n    let test_block: InodeBlock = InodeBlock::new(block_origin);\n    let serialized = test_block.to_block();\n    let deserialized = InodeBlock::from_block(&serialized);\n    assert_eq!(test_block, deserialized)\n}\n\n#[test]\nfn fill_inode_block() {\n    let block_origin = DiskPointer {\n        disk: 420,\n        block: 69,\n    };\n    let mut test_block: InodeBlock = InodeBlock::new(block_origin);\n    let mut added_inodes: Vec<Inode> = Vec::new();\n    let mut inode_offsets: Vec<u16> = Vec::new();\n    loop {\n        let inode: Inode = Inode::get_random();\n        let add_result = test_block.try_add_inode(inode);\n        if add_result.is_err() {\n            // It must be full, its impossible to fragment without removing items.\n            assert_eq!(add_result.err().unwrap(), BlockManipulationError::OutOfRoom);\n            break;\n        }\n        // Keep track of all added inodes so we can validate them.\n        added_inodes.push(inode);\n        inode_offsets.push(add_result.unwrap());\n    }\n    // Ensure all the inodes are present and valid.\n    for i in 0..added_inodes.len() {\n        // read it\n        let read_inode: Inode = test_block.try_read_inode(inode_offsets[i]).unwrap();\n        // Make sure they're the same.\n        assert_eq!(added_inodes[i], read_inode);\n    }\n}\n\n#[test]\nfn filled_inode_block_serialization() {\n    for _ in 0..1000 {\n        let block_origin = DiskPointer {\n            disk: 420,\n            block: 69,\n        };\n        let mut test_block: InodeBlock = InodeBlock::new(block_origin);\n        // Fill with random inodes until we run out of room.\n        loop {\n            let add_result = test_block.try_add_inode(Inode::get_random());\n            if add_result.is_err() {\n                // It must be full, its impossible to fragment without removing items.\n                assert_eq!(add_result.err().unwrap(), BlockManipulationError::OutOfRoom);\n                break;\n            }\n        }\n\n        // Check serialization\n        let serialized = test_block.to_block();\n        let deserialized = InodeBlock::from_block(&serialized);\n        assert_eq!(test_block, deserialized)\n    }\n}\n\n#[test]\nfn add_and_read_inode() {\n    for _ in 0..1000 {\n        let block_origin = DiskPointer {\n            disk: 420,\n            block: 69,\n        };\n        let mut test_block: InodeBlock = InodeBlock::new(block_origin);\n        let inode: Inode = Inode::get_random();\n        let offset = test_block.try_add_inode(inode).unwrap();\n        let read_inode = test_block.try_read_inode(offset).unwrap();\n        assert_eq!(inode, read_inode);\n    }\n}\n\n#[test]\n// Make sure the offsets are working correctly\nfn inode_location_consistency() {\n    for _ in 0..1000 {\n        let new: InodeLocation = InodeLocation::get_random();\n        let disk_number: u16 = new.pointer.disk;\n        let frosted_flaked = new.as_bytes(disk_number);\n        let (_, we_have_the_technology) = InodeLocation::from_bytes(&frosted_flaked, disk_number);\n        assert_eq!(new, we_have_the_technology);\n    }\n}\n\n#[test]\n// Ensure inodes are the correct size for their subtype\nfn inode_correct_sizes() {\n    for _ in 0..1000 {\n        let test_inode: Inode = Inode::get_random();\n        if test_inode.file.is_some() {\n            // A file inode should be 37 bytes long\n            assert_eq!(test_inode.as_bytes().len(), 37)\n        } else {\n            // A directory inode should be 29 bytes long\n            assert_eq!(test_inode.as_bytes().len(), 29)\n        }\n    }\n}\n\n#[test]\n// Inodes should be the same size when re/deserializing them\nfn inode_consistent_serialization() {\n    for _ in 0..1000 {\n        let inode: Inode = Inode::get_random();\n        let serial = inode.as_bytes();\n        let deserial = Inode::from_bytes(&serial);\n        let re_serial = deserial.as_bytes();\n        let re_deserial = Inode::from_bytes(&re_serial);\n\n        // Original Inode survived\n        assert_eq!(inode, re_deserial);\n\n        // Intermediate did not change\n        assert_eq!(deserial, re_deserial);\n\n        // byte versions are the same\n        assert_eq!(serial, re_serial);\n    }\n}\n\n#[test]\nfn timestamp_consistent_serialization() {\n    for _ in 0..1000 {\n        let inode: InodeTimestamp = InodeTimestamp::get_random();\n        let serial = inode.to_bytes();\n        let deserial = InodeTimestamp::from_bytes(serial);\n        let re_serial = deserial.to_bytes();\n        let re_deserial = InodeTimestamp::from_bytes(re_serial);\n\n        // Original InodeTimestamp survived\n        assert_eq!(inode, re_deserial);\n\n        // Intermediate did not change\n        assert_eq!(deserial, re_deserial);\n\n        // byte versions are the same\n        assert_eq!(serial, re_serial);\n    }\n}\n\n// Impl to make randoms\n\n#[cfg(test)]\nimpl Inode {\n    pub(crate) fn get_random() -> Self {\n        use rand::random_bool;\n        if random_bool(0.5) {\n            // A file\n            let mut flags = InodeFlags::MarkerBit;\n            flags.insert(InodeFlags::FileType);\n\n            Inode {\n                flags,\n                file: Some(InodeFile::get_random()),\n                directory: None,\n                created: InodeTimestamp::get_random(),\n                modified: InodeTimestamp::get_random(),\n            }\n        } else {\n            // A directory\n            Inode {\n                flags: InodeFlags::MarkerBit,\n                file: None,\n                directory: Some(InodeDirectory::get_random()),\n                created: InodeTimestamp::get_random(),\n                modified: InodeTimestamp::get_random(),\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nimpl InodeFile {\n    pub(crate) fn get_random() -> Self {\n        let mut random = rand::rng();\n        InodeFile {\n            size: random.random(),\n            pointer: DiskPointer::get_random(),\n        }\n    }\n}\n\n#[cfg(test)]\nimpl InodeTimestamp {\n    pub(crate) fn get_random() -> Self {\n        let mut random = rand::rng();\n        InodeTimestamp {\n            seconds: random.random(),\n            nanos: random.random(),\n        }\n    }\n}\n\n#[cfg(test)]\nimpl InodeLocation {\n    #[cfg(test)]\n    pub(crate) fn get_random() -> Self {\n        let mut random = rand::rng();\n        let pointer: DiskPointer = DiskPointer {\n            disk: random.random(),\n            block: random.random(),\n        };\n\n        // random offset\n        // Not testing the entire range but whatever\n        let offset: u16 = random.random_range(0..250);\n\n        InodeLocation::new(pointer, offset)\n    }\n}\n\n#[cfg(test)]\nimpl InodeDirectory {\n    pub(crate) fn get_random() -> Self {\n        InodeDirectory {\n            pointer: DiskPointer::get_random(),\n        }\n    }\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/directory/mod.rs",
    "content": "pub mod movement;\npub mod read;\n#[cfg(test)]\npub mod tests;\npub(crate) mod types;\npub mod write;\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/directory/movement.rs",
    "content": "// Helpers to move between directories\n\nuse std::path::{\n    Component,\n    Path\n};\n\nuse log::debug;\n\nuse crate::{error_types::drive::DriveError,\n    pool::{\n        disk::{\n            generic::{\n                generic_structs::pointer_struct::DiskPointer,\n                io::cache::cache_io::CachedBlockIO\n            },\n            standard_disk::block::{\n                directory::directory_struct::DirectoryBlock,\n                inode::inode_struct::InodeBlock,\n                io::directory::types::NamedItem,\n            },\n        },\n        pool_actions::pool_struct::Pool\n    },\n    tui::{\n        notify::NotifyTui,\n        tasks::TaskType\n    }\n};\n\nimpl DirectoryBlock {\n    /// Attempts to open a directory in the current directory block.\n    /// This will check if the directory already exists, if it doesn't,\n    /// Ok(None) will be returned, because there was no directory to open.\n    ///\n    /// May swap disks, will end up on whatever disk the new directory is located on, unless\n    /// you specify a return location.\n    ///\n    /// If there is no new directory, this will end up wherever the end of the input directory was, unless\n    /// you set the return disk.\n    pub fn change_directory(\n        self,\n        directory_name: String,\n    ) -> Result<Option<DirectoryBlock>, DriveError> {\n        let handle = NotifyTui::start_task(TaskType::ChangingDirectory(directory_name.clone()), 3);\n        debug!(\"Attempting to CD to `{directory_name}`\");\n        // Get all items in this directory\n\n        let found_dir = self.find_item(&NamedItem::Directory(directory_name))?;\n        NotifyTui::complete_task_step(&handle);\n\n        // Return if the dir did not exist, or keep goin\n        let wanted = match found_dir {\n            Some(found) => found,\n            None => {\n                // The directory did not exist.\n                NotifyTui::complete_multiple_task_steps(&handle, 2);\n                NotifyTui::finish_task(handle);\n                debug!(\"Directory did not exist.\");\n                return Ok(None)\n            },\n        };\n        \n        // Directory exists, time to open that bad boy\n        // Extract the location\n        let final_destination = &wanted.location;\n        debug!(\n            \"Directory claims to live at: disk {} block {} offset {}\",\n            final_destination.pointer.disk,\n            final_destination.pointer.block,\n            final_destination.offset\n        );\n        // Since we got these items from self.list, all of these inode locations MUST have a disk destination\n        // already set for us. So we dont have to check.\n        \n        // Load!\n        // Now this doesn't point to the next directory block, it points to the next _Inode_ block\n        // that points to it.\n        let pointer: DiskPointer = final_destination.pointer;\n        \n        let inode_block = InodeBlock::from_block(&CachedBlockIO::read_block(pointer)?);\n        NotifyTui::complete_task_step(&handle);\n        \n        // Now read in the inode\n        let inode = inode_block\n        .try_read_inode(final_destination.offset)\n        .expect(\"Directories in a DirectoryBlock should point to a valid inode!\");\n        \n\n        // Where is the block?\n        let actual_next_block = inode\n            .directory\n            .expect(\"Should point to a directory inode, not a file.\")\n            .pointer;\n        // Just in case...\n        assert!(!actual_next_block.no_destination(), \"Tried to open a new directory block, but pointer had no destination\");\n\n        // Go go go!\n        let new_dir_block: DirectoryBlock =\n            DirectoryBlock::from_block(&CachedBlockIO::read_block(actual_next_block)?);\n        NotifyTui::complete_task_step(&handle);\n\n        // All done! Enjoy the new block.\n        Ok(Some(new_dir_block))\n    }\n\n    /// Attempts to open any directory in the pool.\n    /// \n    /// Will automatically grab the root directory.\n    pub(crate) fn try_find_directory(maybe_path: Option<&Path>) -> Result<Option<DirectoryBlock>, DriveError> {\n        debug!(\"Attempting to find and open a directory...\");\n        // Pretty simple loop, bail if the directory does not exist at any level.\n        let mut current_directory: DirectoryBlock;\n        // Load in the root directory\n        current_directory = Pool::get_root_directory()?;\n\n        // If no path was supplied, this is the root directory.\n        let path = match maybe_path {\n            Some(ok) => ok,\n            None => {\n                // This must be root.\n                debug!(\"No path was provided to find, it is assumed the caller wants the root directory.\");\n                return Ok(Some(current_directory))\n            },\n        };\n\n        debug!(\"Looking for `{}`...\", path.display());\n\n        // Easy way out, if the incoming path is empty, that means its the root directory itself.\n        if path.iter().count() == 0 {\n            // There are no paths above the root, we are trying to load the root.\n            return Ok(Some(current_directory))\n        }\n\n        // Split the path into folder names\n        for folder in path.components() {\n            // Is this the start?\n            if folder == Component::RootDir {\n                // Skip\n                continue;\n            }\n            // Try to move into the folder\n            if let Some(new_dir) = current_directory.change_directory(folder.as_os_str().to_str().expect(\"Should be valid utf8\").to_string())? {\n                // Directory exists. Move in.\n                current_directory = new_dir;\n                continue;\n            } else {\n                // No such directory\n                return Ok(None)\n            }\n        }\n\n        // Now that we're out of the for loop, we must be in the correct directory.\n        Ok(Some(current_directory))\n\n    }\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/directory/read.rs",
    "content": "// Higher level abstractions for reading directories.\n\nuse log::{debug, error, warn};\n\nuse crate::{error_types::drive::DriveError, pool::{\n    disk::{\n        generic::{\n            block::block_structs::RawBlock,\n            generic_structs::pointer_struct::DiskPointer,\n            io::cache::cache_io::CachedBlockIO\n        },\n        standard_disk::block::{\n            directory::directory_struct::{\n                DirectoryBlock, DirectoryItem, DirectoryItemFlags\n            },\n            io::directory::types::NamedItem,\n        }\n    },\n    pool_actions::pool_struct::Pool\n}, tui::{notify::NotifyTui, tasks::TaskType}};\n\nimpl DirectoryBlock {\n    /// Check if this directory contains an item with the provided name and type.\n    /// This checks the entire directory, not just the current block.\n    /// \n    /// Returns Option<DirectoryItem> if it exists.\n    ///\n    /// May swap disks.\n    pub fn find_item(\n        &self,\n        item_to_find: &NamedItem,\n    ) -> Result<Option<DirectoryItem>, DriveError> {\n        let extracted_debug = item_to_find.debug_strings();\n        debug!(\n            \"Checking if a directory contains the {} `{}`...\",\n            extracted_debug.0, extracted_debug.1\n        );\n\n        // Special case if we are trying to find the root directory.\n        if self.is_root() {\n            // This is the root directory, are we trying to find a nameless directory?\n            if *item_to_find == NamedItem::Directory(\"\".to_string()) {\n                // This was a lookup on the root directory.\n                debug!(\"Caller was looking for the root directory item, skipping lookup...\");\n                return Ok(Some(Pool::get_root_directory_item()));\n            }\n        }\n        \n        // No need to have a task if its the root dir, since that's nearly instant.\n        let handle = NotifyTui::start_task(TaskType::FindItemInDirectory(extracted_debug.1.to_string()), 2);\n        \n        // Get items\n        let items: Vec<DirectoryItem> = self.list()?;\n        NotifyTui::complete_task_step(&handle);\n        \n        // Look for the requested item in the new vec, the index into this vec will be the same\n        // as the index into the og items vec\n        if let Some(item) = item_to_find.find_in(&items) {\n            // It's in there!\n            NotifyTui::complete_task_step(&handle);\n            NotifyTui::finish_task(handle);\n            debug!(\"Yes it did.\");\n            Ok(Some(item))\n        } else {\n            // The item wasn't in there.\n            NotifyTui::complete_task_step(&handle);\n            NotifyTui::finish_task(handle);\n            debug!(\"No it didn't.\");\n            Ok(None)\n        }\n    }\n    /// Returns an Vec of all items in this directory ordered alphabetically descending.\n    ///\n    /// Returned DirectoryItem(s) will have their InodeLocation's disk set.\n    ///\n    /// May swap disks.\n    pub fn list(&self) -> Result<Vec<DirectoryItem>, DriveError> {\n        go_list_directory(self)\n    }\n\n    /// Get the size of a directory by totalling all of the items contained within it.\n    /// \n    /// Does not recurse into sub-directories. (Seems to be standard behavior in ls -l)\n    /// \n    /// Returns the size in bytes.\n    pub fn get_size(&self) -> Result<u64, DriveError> {\n        debug!(\"Getting size of a directory...\");\n        // get all the items\n        debug!(\"Listing items...\");\n        let items = self.list()?;\n        \n        debug!(\"Totaling up item sizes...\");\n        let mut total_size: u64 = 0;\n        for item in items {\n            // Ignore if this is a directory.\n            // We don't recurse into the next directory, we only get the size of the items\n            // directly contained within this directory.\n            if item.flags.contains(DirectoryItemFlags::IsDirectory) {\n                continue;\n            }\n            // Get the size of this file\n            let inode = item.get_inode()?;\n            let file = inode.extract_file().expect(\"The inode the directory item points at should be a file.\");\n            total_size += file.get_size()\n        }\n\n        // All done\n        debug!(\"Size obtained. `{total_size}` bytes.\");\n        Ok(total_size)\n    }\n\n    /// Check if this DirectoryBlock is the head of the root directory.\n    /// \n    /// This will return false on any other block than the head block.\n    fn is_root(&self) -> bool {\n        // Lives in a static place.\n        static ROOT_BLOCK_LOCATION: DiskPointer = DiskPointer {\n            disk: 1,\n            block: 2,\n        };\n\n        self.block_origin == ROOT_BLOCK_LOCATION\n    }\n\n    /// Extracts an item from a directory block, blanking out the space it used to occupy.\n    /// \n    /// This looks for the item in the entire directory, not just the block this was called on.\n    /// Due to this, we assume this is being called on the head of the DirectoryBlock chain.\n    /// \n    /// Automatically flushes changes to disk if required.\n    /// \n    /// If you just want to get the item for reading or minor modifications, use find_item()\n    /// \n    /// Updates the passed in directory block.\n    /// \n    /// Returns nothing if the item did not exist.\n    pub(crate) fn find_and_extract_item(&mut self, item_to_find: &NamedItem) -> Result<Option<DirectoryItem>, DriveError> {\n\n        // Go find the item.\n\n        // Nice struct to make dealing with this a bit nicer\n        struct ItemFound {\n            /// The item\n            item: DirectoryItem,\n            /// This is set if the removal of that item caused the block to be fully emptied.\n            /// \n            /// Thus, if this is set, this block needs to be deallocated, and have the block before it\n            /// set to point to this new pointer, which points to the block _after_ the block that the item was found in.\n            /// \n            /// Slightly confusing.\n            empty_thus_new_pointer: Option<DiskPointer>,\n            /// Which block in the list we were contained within, indexed from front to back.\n            origin_index: usize,\n\n        }\n\n        // Get the blocks\n        let mut blocks: Vec<DirectoryBlock> = get_blocks(self.block_origin)?;\n\n        // Find the item, and deduce what block it's in.\n        // Index, the item, maybe pointer to the next block\n        let mut find: Option<ItemFound> = None;\n        for (index, block) in blocks.iter_mut().enumerate() {\n            // Is it in here?\n            if let Some(found) = block.block_extract_item(item_to_find)? {\n                // Cool!\n                find = Some(ItemFound {\n                    item: found.0,\n                    empty_thus_new_pointer: found.1,\n                    origin_index: index,\n                });\n                break\n            }\n\n        };\n\n        // Did we find the item?\n        let found = match find {\n            Some(ok) => ok,\n            None => {\n                // Item did not exist.\n                return Ok(None);\n            },\n        };\n\n        // If we didn't get a pointer, there is no required cleanup, since no blocks were emptied.\n        let new_pointer = match found.empty_thus_new_pointer {\n            Some(ok) => ok,\n            None => {\n                // No cleanup required!\n                return Ok(Some(found.item));\n            },\n        };\n\n        // We got a pointer, thus a block was emptied.\n\n        // If the block that was emptied was the first one in the list, don't need to do anything.\n        // Sure, we could shuffle the head forwards, but adding things to directories searches front to back anyways, so\n        // switching the pointers around would be needlessly complicated.\n        if found.origin_index == 0 {\n            // Cool!\n            return Ok(Some(found.item));\n        }\n\n        // We have emptied a block in the middle of the chain. We need to update the pointer behind us to point\n        // past us.\n\n        // This operation is independent to the block in front of us, so no update is required there.\n\n        // If this was the last block in the chain, this will just point to the no_destination pointer, which just marks the\n        // new end of the chain.\n\n        let previous_block = &mut blocks[found.origin_index - 1];\n\n        // Now update that block with the new pointer\n        previous_block.next_block = new_pointer;\n\n        // Update it.\n        let raw_ed = previous_block.to_block();\n        CachedBlockIO::update_block(&raw_ed)?;\n\n        // Now we will free that block that was emptied.\n\n        // Now delete the block that we emptied by freeing it.\n        let release_me = blocks[found.origin_index].block_origin;\n        let freed = Pool::free_pool_block_from_disk(&[release_me])?;\n        // this should ALWAYS be 1\n        assert_eq!(freed, 1, \"We should always free one block when removing an empty directory block in a chain.\");\n        \n        // All done!\n        // Update the incoming block head, in case we changed it.\n        // Since we need to own this, we'll just pull it out of the vec.\n        // The updated block order does not matter, since we're immediately dropping this afterwards.\n        *self = blocks.swap_remove(0);\n        Ok(Some(found.item))\n    }\n\n    /// Extract an item from this directory block, if it exists.\n    /// \n    /// Will flush self to disk if block is updated.\n    /// \n    /// If the block is now empty, will also return Some() pointer it's next block, regardless\n    /// if that block exists or not (will return a final pointer on the last block).\n    /// \n    /// Not a public function, use `find_and_extract_item`.\n    fn block_extract_item(&mut self, item_to_find: &NamedItem) -> Result<Option<(DirectoryItem, Option<DiskPointer>)>, DriveError> {\n        // Do we have the requested item?\n        if let Some(found) = item_to_find.find_in(&self.directory_items) {\n            // Found the item!\n            // Remove it from ourselves.\n            self.try_remove_item(&found).expect(\"Guard, we already know its in there.\");\n            // Now flush ourselves to disk\n            let raw_block = self.to_block();\n            CachedBlockIO::update_block(&raw_block)?;\n\n            // If we are now empty, also return a pointer to the next block\n            let maybe_pointer: Option<DiskPointer> = if self.get_items().is_empty() {\n                // Yep\n                Some(self.next_block)\n            } else {\n                None\n            };\n\n            // Now return the item, and the possible pointer to the next block\n            return Ok(Some((found, maybe_pointer)))\n        }\n\n        // Not in here.\n        Ok(None)\n    }\n\n    /// Rename an item in place.\n    /// \n    /// Searches entire directory for the item.\n    /// \n    /// Assumes that the passed in directory block is the head.\n    /// \n    /// Returns true if the item existed and was renamed.\n    /// \n    /// Flushes change to disk.\n    pub(crate) fn try_rename_item(&mut self, to_rename: &NamedItem, new_name: String) -> Result<bool, DriveError> {\n\n        // Since the size of the item might change (name length change) we cant just update the name directly, we have to\n        // extract the item and re-add it.\n\n        // This may move the item across disks, thus if its set to local, we must add the disk number.\n        // If the disk number is no longer required after its written down, `add_item` will make it local again,\n\n        // We also take in the directory item instead of the named item, since you shouldn't be holding onto it after this.\n\n        // Make sure the name is valid.\n        assert!(new_name.len() <= 255, \"Name is too long.\");\n\n        // Get the item\n        if let Some(mut exists) = self.find_and_extract_item(to_rename)? {\n            // Copy it, just in case...\n            let copy = exists.clone();\n            // Now rename it and put it back\n            exists.name_length = new_name.len() as u8;\n            exists.name = new_name;\n            // If this doesn't work, the item is now gone forever lol, thus\n            // we will check the result of this operation and try to put the item back if we can.\n            let add_result = self.add_item(&exists);\n            if add_result.is_ok() {\n                // All good.\n                Ok(true)\n            } else {\n                // Addition failed!\n                warn!(\"Adding item during rename failed.\");\n                warn!(\"Attempting to restore non-renamed item...\");\n                if self.add_item(&copy).is_ok() {\n                    // That worked\n                    warn!(\"Old item restored.\")\n                } else {\n                    error!(\"Failed to restore old item during rename failure! Item has been lost!\");\n                    // Well shit. Not much we can do.\n                    println!(\"Fluster has just lost your file/folder named `{}`, sorry!\", copy.name);\n                    // we have to give up.\n                    panic!(\"File lost during rename.\");\n                }\n                // We need to fail tests even if the item was restored.\n                if cfg!(test) {\n                    panic!(\"Rename failure. Addition failed.\")\n                }\n                // Now we are... fine? The item is still there, it just \n                // wasn't renamed.\n                Err(DriveError::Retry)\n            }\n        } else {\n            // No such item.\n            Ok(false)\n        }\n    }\n}\n\n// Functions\n\nfn go_list_directory(\n    block: &DirectoryBlock,\n) -> Result<Vec<DirectoryItem>, DriveError> {\n    let handle = NotifyTui::start_task(TaskType::ListingDirectory, 2);\n    debug!(\"Listing a directory...\");\n    // We need to iterate over the entire directory and get every single item.\n    // We assume we are handed the first directory in the chain.\n    \n    // Get the blocks\n    debug!(\"Getting blocks...\");\n    let blocks = get_blocks(block.block_origin)?;\n    debug!(\"This directory is made of {} blocks.\", blocks.len());\n    NotifyTui::complete_task_step(&handle);\n    \n    // Get the items out of them\n    debug!(\"Getting items...\");\n    let mut items_found: Vec<DirectoryItem> = blocks.into_iter().flat_map(move |block| {\n        block.get_items()\n    }).collect();\n    NotifyTui::complete_task_step(&handle);\n    \n    \n    // Sort all of the items by name, not sure what internal order it is, but it will be\n    // sorted by whatever comparison function String uses.\n    debug!(\"Sorting...\");\n    items_found.sort_by_key(|item| item.name.to_lowercase());\n    NotifyTui::finish_task(handle);\n    \n    debug!(\"Directory listing finished.\");\n    Ok(items_found)\n}\n\n\n/// Starting on the head block of a DirectoryBlock, return every block in the chain, in order.\n/// \n/// Does not take in a directory block, since we would need to consume it.\n/// \n/// Includes the head block.\nfn get_blocks(start_block_location: DiskPointer) -> Result<Vec<DirectoryBlock>, DriveError> {\n    // Needing to consume the incoming block would be stinky. But since cloning is not allowed, and we\n    // need to return the head block, we have to go get it ourselves.\n\n    // This must be a valid block\n    assert!(!start_block_location.no_destination(), \"Provided head directory block does not exist!\");\n    \n    let raw_read: RawBlock = CachedBlockIO::read_block(start_block_location)?;\n    let start_block: DirectoryBlock = DirectoryBlock::from_block(&raw_read);\n\n    // We assume we are handed the first directory in the chain.\n    // Cannot pre-allocate the vec, since we dont know how many blocks there will be.\n    let mut blocks: Vec<DirectoryBlock> = Vec::new();\n    let mut current_dir_block: DirectoryBlock = start_block;\n\n    // Big 'ol loop, we will break when we hit the end of the directory chain.\n    loop {\n        // Remember where the next block is\n        let next_block: DiskPointer = current_dir_block.next_block;\n        // Add the current block to the Vec\n        blocks.push(current_dir_block);\n\n        // I want to get off Mr. Bone's wild ride\n        if next_block.no_destination() {\n            // We're done!\n            break;\n        }\n        \n        // Load in the next block.\n        let next_block_reader = CachedBlockIO::read_block(next_block)?;\n        current_dir_block = DirectoryBlock::from_block(&next_block_reader);\n\n        // Onwards!\n        continue;\n    }\n    Ok(blocks)\n}"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/directory/tests.rs",
    "content": "// Files, direct to thee.\n// Unwrapping is okay here, since we want unexpected outcomes to fail tests.\n#![allow(clippy::unwrap_used)]\n\nuse std::path::PathBuf;\n\nuse log::debug;\nuse rand::{rngs::ThreadRng, seq::{IndexedRandom, SliceRandom}, Rng};\nuse tempfile::{TempDir, tempdir};\n\nuse crate::{\n    filesystem::filesystem_struct::{FilesystemOptions, FlusterFS},\n    pool::{\n        disk::standard_disk::block::{directory::directory_struct::DirectoryItem, io::directory::types::NamedItem},\n        pool_actions::pool_struct::Pool,\n    },\n};\n\nuse test_log::test; // We want to see logs while testing.\n\n// Since these tests touch global state, they need to be forked, otherwise they will collide.\n\n#[test]\nfn add_directory() {\n    // Use the filesystem starter to get everything in the right spots\n    let _fs = get_filesystem();\n    // Now try adding a directory to the pool\n    let mut block = Pool::get_root_directory().unwrap();\n    let _ = block.make_directory(\"test\".to_string()).unwrap();\n    // We dont even check if its there, we just want to know if writing it failed.\n}\n\n#[test]\n// Make sure creating a file only makes one entry.\nfn creating_only_makes_one_directory() {\n    // Use the filesystem starter to get everything in the right spots\n    let _fs = get_filesystem();\n    // Now try adding a directory to the pool\n    let mut block = Pool::get_root_directory().unwrap();\n    let result = block.make_directory(\"test\".to_string()).unwrap();\n\n    let listed = block.list().unwrap();\n\n    // There should only be one directory.\n    assert_eq!(listed.len(), 1);\n    \n    // The returned item should be the same\n    assert_eq!(listed[0], result);\n}\n\n#[test]\nfn add_and_delete_directory() {\n    let _fs = get_filesystem();\n    let mut block = Pool::get_root_directory().unwrap();\n    let _ = block.make_directory(\"test\".to_string()).unwrap();\n    \n    // Now delete that directory\n\n    // Extract it\n    let test_dir_item = block.find_and_extract_item(&NamedItem::Directory(\"test\".to_string())).unwrap().unwrap();\n\n    // Call delete on it\n    test_dir_item.get_directory_block().unwrap().delete_self(test_dir_item).unwrap();\n\n    // Directory should now be empty\n    assert!(block.list().unwrap().is_empty());\n}\n\n#[test]\n// Make sure directories shrink when items are removed.\nfn deletion_shrinks() {\n    let _fs = get_filesystem();\n    let mut block = Pool::get_root_directory().unwrap();\n    \n    // make a bunch of directories in here with large names to quickly expand the block\n    for i in 0..200 {\n        let _ = block.make_directory(format!(\"test_this_is_a_long_name_to_use_more_space_lol_{i}\")).unwrap();\n    }\n    \n    // Remove all of them.\n    for i in 0..200 {\n        let delete_me = block.find_and_extract_item(&NamedItem::Directory(format!(\"test_this_is_a_long_name_to_use_more_space_lol_{i}\"))).unwrap().unwrap();\n        delete_me.get_directory_block().unwrap().delete_self(delete_me).unwrap()\n    }\n    \n    // Now make sure the empty block is only 1 block large.\n    assert!(block.next_block.no_destination());\n\n    // Should also contain nothing\n    assert!(block.list().unwrap().is_empty());\n}\n\n#[test]\n// Try renaming some items\nfn rename_items() {\n    let _fs = get_filesystem();\n    let mut block = Pool::get_root_directory().unwrap();\n    \n    // A lot of directories\n    let mut directories: Vec<DirectoryItem> = Vec::new();\n    for i in 0..100 {\n        directories.push(block.make_directory(format!(\"dir_{i}\")).unwrap());\n    }\n\n    // A lot of files\n    let mut files: Vec<DirectoryItem> = Vec::new();\n    for i in 0..100 {\n        files.push(block.new_file(format!(\"file_{i}.txt\")).unwrap());\n    }\n\n    // Shuffle for fun\n    let mut all_items: Vec<DirectoryItem> = Vec::new();\n    all_items.extend(directories);\n    all_items.extend(files);\n\n    let mut random: ThreadRng = rand::rng();\n\n    all_items.shuffle(&mut random);\n\n    // How many do we have\n    let number_made: usize = all_items.len();\n\n\n\n    // Go rename all of them\n    for item in all_items {\n        // need a new name... hmmmmm\n        let new_name: String = format!(\"new_{}\", item.name);\n        let renamed = block.try_rename_item(&item.into(), new_name).unwrap();\n        assert!(renamed)\n    }\n\n    // Make sure the directory still contains the correct number of items. (ie we didn't duplicate anything.)\n    let list = block.list().unwrap();\n    assert_eq!(number_made, list.len());\n}\n\n#[test]\nfn add_directory_and_list() {\n    // Use the filesystem starter to get everything in the right spots\n    let _fs = get_filesystem();\n    // Now try adding a directory to the pool\n    let mut block = Pool::get_root_directory().unwrap();\n    let _ = block.make_directory(\"test\".to_string(),).unwrap();\n\n    // try to find it again\n    let new_block = Pool::get_root_directory().unwrap();\n    assert!(\n        new_block\n            .find_item(&NamedItem::Directory(\"test\".to_string()),)\n            .unwrap()\n            .is_some()\n    );\n}\n\n#[test]\nfn nested_directory_hell() {\n    // Use the filesystem starter to get everything in the right spots\n    let _fs = get_filesystem();\n    let mut random: ThreadRng = rand::rng();\n    let mut name_number: usize = 0;\n\n    // Create random directories at random places.\n    for _ in 0..10_000 {\n        // Load in the root\n        let mut where_are_we = Pool::get_root_directory().unwrap();\n        // We will open random directories a few times, if they exist.\n        loop {\n            // List the current directory\n            let square_holes = where_are_we.list().unwrap();\n            // If there is no directories at this level, we're done.\n            if square_holes.is_empty() {\n                break;\n            }\n            // Random chance to not go any deeper.\n            if random.random_bool(0.2) {\n                // Incentivize deep nesting.\n                // not going any further.\n                break;\n            }\n            // Random chance to go back to the root for more chaos.\n            if random.random_bool(0.05) {\n                // Back to root!\n                where_are_we = Pool::get_root_directory().unwrap();\n                continue;\n            }\n            // Looks like we're entering a new directory.\n            let destination = square_holes\n                .choose(&mut random)\n                .expect(\"Already checked if it was empty.\")\n                .name\n                .clone();\n            // Go forth!\n            where_are_we = where_are_we\n                .change_directory(destination)\n                .unwrap()\n                .unwrap();\n            continue;\n        }\n        // Now that we've picked a directory, lets make a new one in here.\n        // To make sure we dont end up with duplicate directory names, we just use a counter.\n        let _ = where_are_we\n            .make_directory(name_number.to_string())\n            .unwrap();\n        name_number += 1;\n    }\n}\n\n#[test]\n/// Ensure that directories eventually start reporting other disks besides disk 1 by\n/// writing way too many directories.\nfn directories_switch_disks() -> Result<(), ()> {\n    // Use the filesystem starter to get everything in the right spots\n    let _fs = get_filesystem();\n    for i in 0..3000 {\n        // There's only 2880 blocks on the first disk, assuming no overhead.\n        let mut root_dir = Pool::get_root_directory().unwrap();\n        let _ = root_dir.make_directory(i.to_string()).unwrap();\n    }\n    // Now make sure we actually have directories that claim to live on another disk\n    let root_dir_done = Pool::get_root_directory().unwrap();\n    for dir in root_dir_done.list().unwrap() {\n        if dir.location.pointer.disk != 1 {\n            // Made it to another disk.\n            debug!(\n                \"Made it onto another disk! Disk: {}\",\n                dir.location.pointer.disk\n            );\n            return Ok(());\n        }\n    }\n    // They were all disk 1!\n    panic!(\"All directories are on disk 1!\");\n}\n\n// We need a filesystem to run directory tests on.\npub fn get_filesystem() -> FlusterFS {\n    let temp_dir = get_new_temp_dir();\n    let floppy_drive: PathBuf = PathBuf::new(); // This is never read since we are using temporary disks.\n    let fs_options = FilesystemOptions::new(Some(temp_dir.path().to_path_buf()), floppy_drive, Some(false), false);\n    FlusterFS::start(&fs_options)\n    // We don't actually have to mount it for non-integration testing.\n}\n\n// Temporary directories for virtual disks\npub fn get_new_temp_dir() -> TempDir {\n    let mut dir = tempdir().unwrap();\n    dir.disable_cleanup(true);\n    debug!(\n        \"Created a temp directory at {}, it will not be deleted on exit.\",\n        dir.path().to_string_lossy()\n    );\n    dir\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/directory/types.rs",
    "content": "// Helper types.\n\nuse crate::pool::disk::standard_disk::block::directory::directory_struct::{\n    DirectoryItemFlags, DirectoryItem,\n};\n\n// Need a way to search for either a file or a directory\n#[derive(Ord, PartialEq, Eq, PartialOrd)]\npub(crate) enum NamedItem {\n    File(String),\n    Directory(String),\n}\n/// Specific types for named items.\nimpl NamedItem {\n    /// Extracts the type's name, and the name of that type. (ie \"file\", \"test.txt\")\n    pub fn debug_strings(&self) -> (&'static str, &String) {\n        match self {\n            NamedItem::File(name) => (\"file\", name),\n            NamedItem::Directory(name) => (\"directory\", name),\n        }\n    }\n    /// Search a Vec<DirectoryItem> for a NamedItem\n    /// Returns the item if it exists.\n    pub fn find_in(&self, to_search: &[DirectoryItem]) -> Option<DirectoryItem> {\n        // Searching with this function only does the minimum amount of clones\n        // to deduce if the item is present or not, instead of needing to clone the\n        // entire Vec to construct the new type.\n        let item_found: Option<&DirectoryItem> = to_search.iter().find(|item: &&DirectoryItem| {\n            let convert: NamedItem = NamedItem::from((*item).clone());\n            convert == *self\n        });\n        item_found.cloned()\n    }\n    /// Helper function to figure out if this is a file\n    pub fn is_file(&self) -> bool {\n        matches!(self, NamedItem::File(_))\n    }\n    // /// Helper function to figure out if this is a directory\n    // pub fn is_directory(&self) -> bool {\n    //     matches!(self, NamedItem::Directory(_))\n    // }\n}\n\n/// Helper to turn DirectoryItem(s) into NamedItem(s)\nimpl From<DirectoryItem> for NamedItem {\n    fn from(value: DirectoryItem) -> Self {\n        if value.flags.contains(DirectoryItemFlags::IsDirectory) {\n            NamedItem::Directory(value.name)\n        } else {\n            NamedItem::File(value.name)\n        }\n    }\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/directory/write.rs",
    "content": "// Write a new directory into a directory block\n\nuse log::{debug, error};\n\nuse crate::{error_types::drive::DriveError, pool::{\n    disk::{\n        generic::{\n            block::block_structs::RawBlock,\n            generic_structs::pointer_struct::DiskPointer,\n            io::cache::cache_io::CachedBlockIO,\n        },\n        standard_disk::block::{\n                directory::directory_struct::{\n                    DirectoryBlock, DirectoryItem, DirectoryItemFlags\n                },\n                inode::inode_struct::{\n                    Inode,\n                    InodeBlock,\n                    InodeDirectory,\n                    InodeFlags,\n                    InodeTimestamp\n                },\n                io::directory::types::NamedItem,\n            },\n    },\n    pool_actions::pool_struct::Pool,\n}, tui::{notify::NotifyTui, tasks::TaskType}};\n\nimpl DirectoryBlock {\n    /// Add a new item to this block, extending this block if needed.\n    /// Updated blocks are written to disk.\n    ///\n    /// Updates the block that was passed in, since the contents of the block may have changed.\n    ///\n    /// Returns nothing.\n    pub fn add_item(\n        &mut self,\n        item: &DirectoryItem,\n    ) -> Result<(), DriveError> {\n        go_add_item(self, item)\n    }\n    /// Creates a new directory block, and adds its location to the input block.\n    /// Blocks are created and updated as needed.\n    ///\n    /// Updates the directory block that was passed in.\n    ///\n    /// The name of the new directory must be less than 256 characters long.\n    /// Attempting to recreate an already existing directory will panic.\n    ///\n    /// Returns the created directory as a DirectoryItem.\n    pub fn make_directory(\n        &mut self,\n        name: String,\n    ) -> Result<DirectoryItem, DriveError> {\n        go_make_directory(self, name)\n    }\n\n    /// Remove a the given directory. Removes all blocks that contained information about this directory, and updates\n    /// all other blocks to remove references to this directory.\n    /// \n    /// You must also pass in the DirectoryItem that refers to this directory. You should extract it from the parent\n    /// directory.\n    /// \n    /// The directory block must be empty of all items.\n    /// \n    /// Consumes the incoming block, since it will no longer exist.\n    /// \n    /// May swap disks.\n    /// \n    /// Returns nothing on success.\n    pub fn delete_self(self, self_item: DirectoryItem) -> Result<(), DriveError> {\n        // In theory, as long as the caller used an extracted directory item to call\n        // this method, even if this call fails, all references to it will now be gone on\n        // a directory level. So even if the inode or the block wasn't freed, its still\n        // \"deleted\", but just leaked its blocks. Which is unfortunate, but fine.\n\n\n        // Make sure the directory is empty.\n        // Caller must check.\n        if !self.list()?.is_empty() {\n            panic!(\"Cannot delete an non-empty directory!\");\n        }\n\n        // Directories should shrink when items are removed. An empty\n        // directory should only be 1 block in size.\n        // Thus, we only have to deallocate ourselves.\n        \n        // Remove our inode.\n        // We need to find it manually, since we will be updating the\n        // inode block.\n        let read: RawBlock = CachedBlockIO::read_block(self_item.location.pointer)?;\n        let mut inode_block: InodeBlock = InodeBlock::from_block(&read);\n\n        if let Err(error) = inode_block.try_remove_inode(self_item.location.offset) {\n            // Not good. Something was wrong with the inode pointer.\n            // This is a very very very bad thing.\n            // The inode blocks may be corrupted.\n            // We cannot recover.\n            panic!(\"Tried to remove an invalid inode. Unrecoverable. {error:#?}\")\n        }\n\n        // Write back the updated inode block\n        CachedBlockIO::update_block(&inode_block.to_block())?;\n\n        // Now we can free the block that the directory occupied.\n        let _ = Pool::free_pool_block_from_disk(&[self.block_origin])?;\n\n        // All done, directory deleted.\n        drop(self); // So long\n        drop(self_item); // Space Cowboy\n        Ok(())\n    }\n}\n\nfn go_make_directory(\n    directory: &mut DirectoryBlock,\n    name: String,\n) -> Result<DirectoryItem, DriveError> {\n    debug!(\"Attempting to create a new directory with name `{name}`...\");\n    // Check to make sure this block does not already contain the directory we are trying to add.\n    // We dont care if listing the directory puts us somewhere else, because we're immediately going to\n    // go get a new directory block, which would possibly just swap disks again, and our final update\n    // to the original directory block has its origin already specified with block_origin.\n    debug!(\"Checking if a directory with that name already exists...\");\n    if directory\n    .find_item(&NamedItem::Directory(name.clone()))?\n    .is_some()\n    {\n        // We are attempting to create a duplicate item.\n        error!(\"ATTEMPTED TO CREATE A DUPLICATE DIRECTORY! PANICKING!\");\n        panic!(\"Attempted to create duplicate directory!\")\n    }\n    \n    debug!(\"Name is free.\");\n\n    // And make sure the name isn't too long.\n    assert!(name.len() < 256, \"Can't make a directory with this name, it's too long.\");\n\n    // Reserve a spot for the new directory\n    debug!(\"Getting a new directory block...\");\n    let new_directory_location = go_make_new_directory_block()?;\n\n    // Now that we've made the directory, we need an inode that points to it.\n\n    // Since this is a brand new directory, this inode will have a creation and modified time of right now\n    let now = InodeTimestamp::now();\n\n    let inode: Inode = Inode {\n        flags: InodeFlags::MarkerBit, // No file bit, since this is a directory\n        file: None,\n        directory: Some(InodeDirectory::from_disk_pointer(new_directory_location)),\n        created: now,\n        modified: now,\n    };\n\n    // Go put it somewhere.\n    debug!(\"Adding the inode for the new directory...\");\n    let inode_result = Pool::fast_add_inode(inode)?;\n\n    // Now we add this newly created directory to the calling directory.\n    let mut flags: DirectoryItemFlags = DirectoryItemFlags::MarkerBit;\n    // We also must mark it as a directory, not a normal file.\n    flags.insert(DirectoryItemFlags::IsDirectory);\n\n    // Put it all together\n    let final_directory_item = DirectoryItem {\n        flags,\n        name_length: name.len() as u8,\n        name,\n        location: inode_result,\n    };\n\n    // Put it into the caller directory!\n    // We dont need to pass in a return disk, since we will return ourselves next if needed.\n    debug!(\"Adding the new directory to the caller...\");\n    directory.add_item(&final_directory_item)?;\n\n    // All done!\n    debug!(\"Done creating directory.\");\n    Ok(final_directory_item)\n}\n\n/// Allocates space for and writes a new directory block.\n///\n/// Returns where the new block is.\n///\n/// May swap disks, does not return to original disk.\nfn go_make_new_directory_block() -> Result<DiskPointer, DriveError> {\n    // Ask the pool for a new block\n    // No crc, will overwrite.\n    let new_directory_location = Pool::find_and_allocate_pool_blocks(1, false)?[0];\n\n    // Open the new block and write that bastard\n    let new_directory_block: RawBlock = DirectoryBlock::new(new_directory_location).to_block();\n\n    CachedBlockIO::update_block(&new_directory_block)?;\n\n    // All done!\n    Ok(new_directory_location)\n}\n\n// Add an item to a directory\nfn go_add_item(\n    directory: &mut DirectoryBlock,\n    item: &DirectoryItem,\n) -> Result<(), DriveError> {\n    let handle = NotifyTui::start_task(TaskType::CreateDirectoryItem, 2);\n    debug!(\"Adding new item to directory...\");\n\n    // Added items must have their flag set.\n    assert!(item.flags.contains(DirectoryItemFlags::MarkerBit), \"New directory items must have the marker bit set.\");\n\n    // Added items must have a valid location\n    assert!(!item.location.pointer.no_destination(), \"New directory items must have a proper location.\");\n\n    // Persistent vars\n    // We may load in other blocks, so these may change\n    let mut new_block_origin: DiskPointer;\n    let mut current_directory: &mut DirectoryBlock = directory;\n    // If we swap disks, we need to update the item to not be on the local disk anymore.\n    // We clone here so higher up we can keep directory items that are added to directories instead of consuming them on write.\n    let item_to_add: DirectoryItem = item.clone();\n\n    // Need to hold this out here or the borrow will be dropped.\n    let mut next_directory: DirectoryBlock;\n\n    // Now for the loop\n    loop {\n        // Try adding the item to the current block\n        if current_directory.try_add_item(&item_to_add).is_ok() {\n            // Cool! We found a spot!\n            break;\n        }\n        // There was not enough room in that block, we need to find the next one.\n        new_block_origin = go_find_next_or_extend_block(current_directory)?;\n\n        // Load the new directory\n        let read_block: RawBlock = CachedBlockIO::read_block(new_block_origin)?;\n        next_directory = DirectoryBlock::from_block(&read_block);\n        current_directory = &mut next_directory;\n\n        // Time to try again!\n        continue;\n    }\n\n    NotifyTui::complete_task_step(&handle);\n    \n    // Now that the loop has ended, we need to write the block that we just updated.\n    // We assume the block has already been reserved, we are simply updating it.\n    let to_write: RawBlock = current_directory.to_block();\n    CachedBlockIO::update_block(&to_write)?;\n    NotifyTui::complete_task_step(&handle);\n    NotifyTui::finish_task(handle);\n\n    debug!(\"Item added.\");\n    // Done!\n    Ok(())\n}\n\n/// Finds the next section of this directory, or extends it if there is none.\n/// \n/// Needs a mutable reference, since the pointer may change.\n///\n/// May swap disks, will return to original disk.\nfn go_find_next_or_extend_block(\n    directory: &mut DirectoryBlock,\n) -> Result<DiskPointer, DriveError> {\n    let mut block_to_load: DiskPointer = directory.next_block;\n\n    // Make sure we actually have somewhere to go.\n    if !directory.next_block.no_destination() {\n        // Already have another block to go to.\n        return Ok(block_to_load);\n    }\n\n    // Looks like we need a new block\n    // Get the block in question.\n    block_to_load = go_make_new_directory_block()?;\n\n    // Now we must update the previous block to point to this new one.\n    directory.next_block = block_to_load;\n\n    // Write back the updated destination\n    let raw_block: RawBlock = directory.to_block();\n    CachedBlockIO::update_block(&raw_block)?;\n\n    // All done.\n    Ok(block_to_load)\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/file/mod.rs",
    "content": "pub mod write;\npub mod read;\npub mod movement;\n#[cfg(test)]\nmod tests;"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/file/movement.rs",
    "content": "// We need to go to seek points and such.\n\nuse log::debug;\n\nuse crate::{error_types::drive::DriveError, pool::disk::{\n    generic::{\n        block::block_structs::RawBlock,\n        generic_structs::pointer_struct::DiskPointer,\n        io::cache::cache_io::CachedBlockIO\n    },\n    standard_disk::block::{\n            directory::directory_struct::DirectoryItem,\n            file_extents::file_extents_methods::DATA_BLOCK_OVERHEAD,\n            inode::inode_struct::{\n                Inode,\n                InodeBlock,\n                InodeFile\n            }\n        }\n}};\n\nimpl InodeFile {\n    /// Find where a seek lands.\n    /// Returns (index, offset), index is the index into the input blocks array,\n    /// offset is the offset within that block, skipping the flag byte already.\n    pub(super) fn byte_finder(byte_offset: u64) -> (usize, u16) {\n        // Assumptions:\n        // We aren't attempting to find a byte offset that is outside of the file.\n\n        let block_capacity = 512 - DATA_BLOCK_OVERHEAD;\n\n        // We can divide the incoming offset by the block capacity to figure out which block it's in.\n        // This gives the index into the `blocks` slice directly.\n        let block_index = (byte_offset / block_capacity) as usize;\n\n        // Now within that block we can find which byte it is by taking the modulo.\n        // But we do need to move forwards one byte into the block to skip the flag.\n        let offset_in_block = (byte_offset % block_capacity) as u16 + 1;\n\n        // All done!\n        (block_index, offset_in_block)\n    }\n}\n\nimpl DirectoryItem {\n    /// Retrieve the inode that refers to this block.\n    pub(crate) fn get_inode(&self) -> Result<Inode, DriveError> {\n        debug!(\"Extracting inode from DirectoryItem...\");\n        // read in that inode block\n        let pointer: DiskPointer = self.location.pointer;\n        \n        debug!(\"Reading in InodeBlock at (disk {} block {})...\", pointer.disk, pointer.block);\n        let raw_block: RawBlock = CachedBlockIO::read_block(pointer)?;\n        let block: InodeBlock = InodeBlock::from_block(&raw_block);\n        \n        // return the inode\n        let inode_good = block.try_read_inode(self.location.offset).expect(\"Invalid inode offset provided!\");\n        debug!(\"Inode found.\");\n        Ok(inode_good)\n    }\n}"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/file/read.rs",
    "content": "// Reading a block is way easier than writing it.\n// Must use cached IO, does not touch disk directly.\n\n\nuse log::{\n    debug,\n    trace\n};\n\nuse crate::{error_types::drive::DriveError, pool::disk::{\n    generic::{\n        block::block_structs::RawBlock,\n        generic_structs::pointer_struct::DiskPointer,\n        io::cache::cache_io::CachedBlockIO\n    },\n    standard_disk::block::{\n            directory::directory_struct::{\n                DirectoryItem, DirectoryItemFlags\n            },\n            file_extents::{\n                file_extents_methods::DATA_BLOCK_OVERHEAD,\n                file_extents_struct::{\n                    FileExtent,\n                    FileExtentBlock\n                }\n            },\n            inode::inode_struct::{\n                InodeBlock,\n                InodeFile\n            }\n        }\n}, tui::{notify::NotifyTui, tasks::TaskType}};\n\nimpl InodeFile {\n    // Local functions\n    /// Extract all of the extents and spit out a list of all of the blocks.\n    pub(super) fn as_pointers(&self) -> Result<Vec<DiskPointer>, DriveError> {\n        go_to_pointers(self)\n    }\n    /// Extract all of the extents.\n    pub(super) fn as_extents(&self) -> Result<Vec<FileExtent>, DriveError> {\n        let root = self.get_root_block()?;\n        go_to_extents(&root)\n    }\n    /// Goes and gets the FileExtentBlock this refers to.\n    fn get_root_block(&self) -> Result<FileExtentBlock, DriveError> {\n        go_get_root_block(self)\n    }\n    /// Read a file\n    fn read(&self, seek_point: u64, size: u32) -> Result<Vec<u8>, DriveError> {\n        go_read_file(self, seek_point, size)\n    }\n}\n\n// We dont want to call read/write on the inodes, we should do it up here so we\n// we can automatically update the information on the file, and the directory if needed.\nimpl DirectoryItem {\n    /// Read a file.\n    ///\n    /// Assumptions:\n    /// - This is an FILE, not a DIRECTORY.\n    /// - The location of this directory item has it's disk set.\n    /// - The inode that the item points at does exist, and is valid.\n    /// \n    /// Reads in a file at a starting offset, and returns `x` bytes after that offset.\n    /// \n    /// Optionally returns to a specified disk.\n    pub fn read_file(&self, seek_point: u64, size: u32) -> Result<Vec<u8>, DriveError> {\n        // Is this a file?\n        if self.flags.contains(DirectoryItemFlags::IsDirectory) {\n            // Uh, no it isn't why did you give me a dir?\n            panic!(\"Tried to read a directory as a file!\");\n        }\n\n        // Extract out the file\n        let location = &self.location;\n\n        // Get the inode block\n        let pointer: DiskPointer = location.pointer;\n\n        let raw_block: RawBlock = CachedBlockIO::read_block(pointer)?;\n        let inode_block: InodeBlock = InodeBlock::from_block(&raw_block);\n\n        // Get the actual file\n        let inode_file = inode_block.try_read_inode(location.offset).expect(\"Already checked if it was a file.\");\n        let file = inode_file.extract_file().expect(\"File flag means a file inode should exist.\");\n\n        // Now we can read in the file\n        let read_bytes = file.read(seek_point, size,)?;\n\n        // Now we have the bytes. If we were writing, we would have to flush info about the file to disk, but we don't\n        // need to for a read. We are all done\n\n        Ok(read_bytes)\n    }\n}\n\n\n\nfn go_to_pointers(location: &InodeFile) -> Result<Vec<DiskPointer>, DriveError> {\n    // get extents\n    let extents = location.as_extents()?;\n    // Extract all the blocks.\n    // Pre-allocating this vec isn't really possible, but we at least know that\n    // every extent will contain at least one block.\n    let mut blocks: Vec<DiskPointer> = Vec::with_capacity(extents.len());\n\n    // For each extent\n    for e in extents {\n        // each block that the extent references\n        for n in 0..e.length {\n            blocks.push(DiskPointer {\n                disk: e.start_block.disk,\n                block: e.start_block.block + n as u16\n            });\n        }\n    }\n\n    Ok(blocks)\n}\n\n// Functions\n\nfn go_to_extents(\n    block: &FileExtentBlock,\n) -> Result<Vec<FileExtent>, DriveError> {\n    // Totally didn't just lift the directory logic and tweak it, no sir.\n    debug!(\"Extracting extents for a file...\");\n    // We need to iterate over the entire ExtentBlock chain and get every single item.\n    // We assume we are handed the first ExtentBlock in the chain.\n    // Cannot pre-allocate here, since we have no idea how many extents there will be.\n    let mut extents_found: Vec<FileExtent> = Vec::new();\n    let mut current_dir_block: FileExtentBlock = block.clone();\n\n    // Big 'ol loop, we will break when we hit the end of the directory chain.\n    loop {\n        // Add all of the contents of the current directory to the total.\n        let new_items = current_dir_block.get_extents();\n        extents_found.extend_from_slice(&new_items);\n\n        // I want to get off Mr. Bone's wild ride\n        if current_dir_block.next_block.no_destination() {\n            // We're done!\n            trace!(\"Done getting FileExtent(s).\");\n            break;\n        }\n\n        trace!(\"Need to continue on the next block.\");\n        // Time to load in the next block.\n        let next_block = current_dir_block.next_block;\n        let raw_block: RawBlock = CachedBlockIO::read_block(next_block)?;\n        current_dir_block = FileExtentBlock::from_block(&raw_block);\n\n        // Onwards!\n        continue;\n    }\n\n    debug!(\"Extents retrieved.\");\n    Ok(extents_found)\n}\n\n\nfn go_get_root_block(file: &InodeFile) -> Result<FileExtentBlock, DriveError> {\n    // Make sure this actually goes somewhere\n    assert!(!file.pointer.no_destination(), \"Pointer with no destination!\");\n    let raw_block: RawBlock = CachedBlockIO::read_block(file.pointer)?;\n    let block = FileExtentBlock::from_block(&raw_block);\n    Ok(block)\n}\n\n\n\nfn go_read_file(file: &InodeFile, seek_point: u64, size: u32) -> Result<Vec<u8>, DriveError> {\n    let handle = NotifyTui::start_task(TaskType::FileReadBytes, size.into());\n    // Make sure the file is big enough\n    assert!(file.get_size()>= seek_point + size as u64, \"Not enough bytes in this file to satisfy the read!\");\n\n    // Find the start point\n    let (block_index, mut byte_index) = InodeFile::byte_finder( seek_point);\n\n    // The byte_finder already skips the flag, so it ends up adding one, we need to subtract that.\n    // This is a bandaid fix. this logic is ugly.\n    // Not gonna refactor it tho, hehe.\n    byte_index -= 1;\n\n    let blocks = file.as_pointers()?;\n    let mut bytes_remaining: u32 = size;\n    let mut current_block: usize = block_index;\n\n    // Since we will be writing into this vec, we need to pre-fill it with zeros to allow for indexing.\n    // Doing it like this also avoids needing to grow the vec with additional data.\n    let mut collected_bytes: Vec<u8> = vec![0_u8; size as usize];\n\n    // We dont need to deal with the disk at all at this level, we will use\n    // the cache for all IO\n\n    loop {\n        // Are we done reading?\n        if bytes_remaining == 0 {\n            // All done!\n            break\n        }\n\n        // Get where the next bytes need to go\n        let append_point = (size - bytes_remaining) as usize;\n\n        // Read into the buffer\n        let bytes_read = read_bytes_from_block(&mut collected_bytes, append_point, blocks[current_block], byte_index, bytes_remaining)?;\n        \n        // After the first read, we are now aligned to the start of blocks\n        byte_index = 0;\n\n        // Update how many bytes we've read\n        bytes_remaining -= bytes_read as u32;\n        NotifyTui::complete_multiple_task_steps(&handle, bytes_read.into());\n\n        // Keep going!\n        current_block += 1;\n        continue;\n    }\n\n    NotifyTui::finish_task(handle);\n\n    // All done!\n    Ok(collected_bytes)\n}\n\n\n\n\n\n/// Read as many bytes as we can from this block.\n/// \n/// Buffer must have enough room for our write. MUST pre-allocate it.\n/// \n/// buffer_offset is how far into the provided buffer to append the newly read bytes.\n/// \n/// Places read bytes into the provided buffer.\n/// \n/// Returns number of bytes read.\nfn read_bytes_from_block(buffer: &mut [u8], buffer_offset: usize, block: DiskPointer, internal_block_offset: u16, bytes_to_read: u32) -> Result<u16, DriveError> {\n\n    // How much data a block can hold\n    let data_capacity = 512 - DATA_BLOCK_OVERHEAD as usize;\n    let offset = internal_block_offset as usize;\n\n    // Check for impossible offsets\n    assert!(offset < data_capacity, \"Tried to read outside of the capacity of a block.\");\n    \n    // Calculate bytes to write based on REMAINING space.\n    let remaining_space = data_capacity - offset;\n    let bytes_to_read = std::cmp::min(bytes_to_read, remaining_space as u32);\n    \n    // We also don't support 0 byte reads\n    // Since that would be a failure mode of the caller, in theory could be\n    // stuck in an infinite loop type shi.\n    // Why panic? It won't if you fix the caller! :D\n    assert_ne!(bytes_to_read, 0, \"Tried to read 0 bytes from a block!\");\n\n\n    // load the block\n    let block_copy: RawBlock = CachedBlockIO::read_block(block)?;\n    \n    // Read that sucker\n    // Skip the first byte with the flag\n    let start = offset + 1;\n    let end = start + bytes_to_read as usize;\n    let amount_read: usize = end - start;\n\n    // Put the bytes into the buffer that was passed in.\n\n    // Create slices for the buffer and the data, zero cost abstraction i think, just makes\n    // code prettier.\n\n    let destination_slice = &mut buffer[buffer_offset..buffer_offset + amount_read];\n    let block_data = &block_copy.data[start..end];\n\n    destination_slice.copy_from_slice(block_data);\n\n    // Return the bytes we read.\n    // No way to read more than 512 bytes, so u16 is fine\n    Ok(amount_read as u16)\n}"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/file/tests.rs",
    "content": "// Files, direct to thee.\n// Unwrapping is okay here, since we want unexpected outcomes to fail tests.\n#![allow(clippy::unwrap_used)]\nuse rand::{rngs::ThreadRng, Rng, RngCore};\nuse test_log::test;\n\nuse crate::pool::{disk::{standard_disk::block::{directory::directory_struct::DirectoryItem, io::directory::{tests::get_filesystem, types::NamedItem}}}, pool_actions::pool_struct::Pool}; // We want to see logs while testing.\n\n/// Can we make a new file?\n#[test]\nfn create_blank() {\n    // Make a blank file\n    let _fs = get_filesystem();\n    let mut root_block = Pool::get_root_directory().unwrap();\n    let new_item = root_block.new_file(\"test123.txt\".to_string()).unwrap();\n\n    let new_file = new_item.get_inode().unwrap().extract_file().unwrap();\n\n    assert_eq!(new_file.get_size(), 0); // Brand new files should be empty.\n}\n\n/// Can we make a new small file?\n#[test]\nfn write_small_file() {\n    // Make a blank file\n    let fs = get_filesystem();\n    let mut root_block = Pool::get_root_directory().unwrap();\n    let new_item = root_block.new_file(\"test123.txt\".to_string()).unwrap();\n\n    // Bytes to write\n    let mut random: ThreadRng = rand::rng();\n    let mut bytes: [u8; 512] = [0_u8; 512];\n\n    random.fill_bytes(&mut bytes);\n\n    // Write to that file\n    // We will write from the start.\n    let seek_point: u64 = 0;\n    let bytes_written = new_item.write_file(&bytes, seek_point).unwrap();\n\n    // Make sure we actually wrote all the bytes\n    assert_eq!(bytes_written, bytes.len() as u32);\n    //  This should fit on one disk.\n    assert_eq!(fs.pool.lock().expect(\"testing\").header.highest_known_disk, 1);\n}\n\n/// Can we make a new small file?\n#[test]\nfn write_big_file() {\n    // Make a blank file\n    let fs = get_filesystem();\n    let mut root_block = Pool::get_root_directory().unwrap();\n    let new_file = root_block.new_file(\"test123.txt\".to_string()).unwrap();\n\n    // lol, how about 4 MB\n    // This is 4x what fluster will normally handle\n    const FOUR_MEG: usize = 4 * 1024 * 1024;\n    let mut random: ThreadRng = rand::rng();\n    let mut bytes: Vec<u8> = Vec::with_capacity(FOUR_MEG);\n    bytes.resize_with(FOUR_MEG, || 0);\n\n    random.fill_bytes(&mut bytes);\n\n    // Write to that file\n    // We will write from the start.\n    let seek_point: u64 = 0;\n    let bytes_written = new_file.write_file(&bytes, seek_point).unwrap();\n\n    // Make sure we actually wrote all the bytes\n    assert_eq!(bytes_written, bytes.len() as u32);\n    // Make sure this actually used multiple disks\n    assert!(fs.pool.lock().expect(\"testing\").header.highest_known_disk > 1);\n}\n\n/// Make a lot of empty random files.\n#[test]\nfn make_lots_of_files() {\n    // Make a blank file\n    let _fs = get_filesystem();\n    for (current_filename_number, _) in (0..1000).enumerate() {\n        let mut root_block = Pool::get_root_directory().unwrap();\n        let new_name: String = format!(\"{current_filename_number}.txt\");\n        let _new_file = root_block.new_file(new_name).unwrap();\n        // we wont write anything.\n    }\n}\n\n/// Make a lot of filled random size files.\n#[test]\n#[ignore = \"Very slow.\"]\nfn make_lots_of_filled_files() {\n    let _fs = get_filesystem();\n    let mut random: ThreadRng = rand::rng();\n    for (current_filename_number, _) in (0..1000).enumerate() {\n        let mut root_block = Pool::get_root_directory().unwrap();\n        let new_name: String = format!(\"{current_filename_number}.txt\");\n        let new_file = root_block.new_file(new_name).unwrap();\n        \n\n        // Random size between 1 byte and 1 MB\n        let file_size: usize = random.random_range(1..1024*1024);\n        let mut bytes: Vec<u8> = Vec::with_capacity(file_size);\n        bytes.resize_with(file_size, || 0);\n\n        random.fill_bytes(&mut bytes);\n\n        // Write to that file\n        let bytes_written = new_file.write_file(&bytes, 0).unwrap();\n\n        // Make sure we actually wrote all the bytes\n        assert_eq!(bytes_written, bytes.len() as u32);\n    }\n}\n\n\n/// Can we properly read and write a test file?\n#[test]\nfn write_and_read_small() {\n    // Make a blank file\n    let _fs = get_filesystem();\n    let mut root_block = Pool::get_root_directory().unwrap();\n    let new_file = root_block.new_file(\"test123.txt\".to_string()).unwrap();\n\n    // Bytes to write\n    let mut random: ThreadRng = rand::rng();\n    let mut bytes: [u8; 512] = [0_u8; 512];\n\n    random.fill_bytes(&mut bytes);\n\n    // Write to that file\n    // We will write from the start.\n    let seek_point: u64 = 0;\n    let bytes_written = new_file.write_file(&bytes, seek_point).unwrap();\n\n    // Make sure we actually wrote all the bytes\n    assert_eq!(bytes_written, bytes.len() as u32);\n\n    // Read back in that file\n    // We will find the file by its file name, to ensure disk access works correctly.\n\n    let root_block = Pool::get_root_directory().unwrap();\n    // Go fetch\n    let named: NamedItem = NamedItem::File(\"test123.txt\".to_string());\n    let read_me = root_block.find_item(&named).unwrap().expect(\"We just made it\");\n\n    // Read the contained data\n    let read_data = read_me.read_file(0, 512).unwrap();\n\n    // Does it match?\n    assert_eq!(read_data.len(), bytes.len());\n    assert_eq!(read_data, bytes);\n}\n\n/// Can we properly read and write a test file?\n#[test]\nfn write_and_read_large() {\n    // Make a blank file\n    let _fs = get_filesystem();\n    let mut root_block = Pool::get_root_directory().unwrap();\n    let new_file = root_block.new_file(\"test123.txt\".to_string()).unwrap();\n\n    // Bytes to write\n    const FOUR_MEG: usize = 4 * 1024 * 1024;\n    let mut random: ThreadRng = rand::rng();\n    let mut bytes: Vec<u8> = Vec::with_capacity(FOUR_MEG);\n    bytes.resize_with(FOUR_MEG, || 0);\n\n    random.fill_bytes(&mut bytes);\n\n    // Write to that file\n    // We will write from the start.\n    let seek_point: u64 = 0;\n    let bytes_written = new_file.write_file(&bytes, seek_point).unwrap();\n\n    // Make sure we actually wrote all the bytes\n    assert_eq!(bytes_written, bytes.len() as u32);\n\n    // Read back in that file\n    // We will find the file by its file name, to ensure disk access works correctly.\n\n    let root_block = Pool::get_root_directory().unwrap();\n    // Go fetch\n    let named: NamedItem = NamedItem::File(\"test123.txt\".to_string());\n    let read_me = root_block.find_item(&named).unwrap().expect(\"We just made it\");\n\n    // Read the contained data\n    let read_data = read_me.read_file(0, FOUR_MEG.try_into().unwrap()).unwrap();\n\n    // Does it match?\n    assert_eq!(read_data.len(), bytes.len());\n    assert_eq!(read_data, bytes);\n}\n\n\n/// Read and write a lot of random files\n#[test]\n#[ignore = \"Very slow.\"]\nfn read_and_write_random_files() {\n    let _ = get_filesystem();\n    let mut random: ThreadRng = rand::rng();\n    let mut random_files: Vec<Vec<u8>> = Vec::new();\n    const TEST_LENGTH: usize = 1000;\n    const MAX_FILE_SIZE: usize = 1024 * 1024; // Currently one meg\n    for (current_filename_number, _) in (0..TEST_LENGTH).enumerate() {\n        let mut root_block = Pool::get_root_directory().unwrap();\n        let new_name: String = format!(\"{current_filename_number}.txt\");\n        let new_file = root_block.new_file(new_name).unwrap();\n        \n        \n        // Random size between 1 byte and 1 MB\n        let file_size: usize = random.random_range(1..MAX_FILE_SIZE);\n        let mut bytes: Vec<u8> = Vec::with_capacity(file_size);\n        bytes.resize_with(file_size, || 0);\n        \n        random.fill_bytes(&mut bytes);\n        \n        // Keep track of this file\n        random_files.push(bytes.clone());\n        \n        // Write to that file\n        let bytes_written = new_file.write_file(&bytes, 0).unwrap();\n        \n        // Make sure we actually wrote all the bytes\n        assert_eq!(bytes_written, bytes.len() as u32);\n    }\n\n    // Now we need to read all of the files back out\n    let root_block = Pool::get_root_directory().unwrap();\n    for (current_file, _) in (0..TEST_LENGTH).enumerate() {\n        let named: NamedItem = NamedItem::File(format!(\"{current_file}.txt\"));\n        let found: DirectoryItem = root_block.find_item(&named).unwrap().unwrap();\n\n        // read it\n        let file_size: u64 = found.get_inode().unwrap().extract_file().unwrap().get_size();\n        let read = found.read_file(0, file_size.try_into().unwrap()).unwrap();\n\n        // Compare\n        check_byte_vec_equality(&read, &random_files[current_file]);\n    }\n}\n\n\n\n/// Test helper for tracking down file corruption.\n/// \n/// Returns nothing, panics if bytes are not the same.\nfn check_byte_vec_equality(a: &[u8], b: &[u8]) {\n    // Make sure they are the same length\n    assert_eq!(a.len(), b.len());\n\n    // Check the bytes\n    // The following line is the current Guinness World Records\n    // holder for \"Worlds most unreadable iterator chain\"\n    for (index, (byte_a, byte_b)) in a.iter().zip(b.iter()).enumerate() {\n        assert_eq!(byte_a, byte_b, \"Byte mismatch at index `{index}`! a: `{byte_a}`, b: `{byte_b}`\");\n        // We cant use enumerate here (i think?) so manual index tracking.\n    }\n}"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/file/write.rs",
    "content": "// Writing files.\n\n// We will take in InodeFile(s) instead of Extent related types, since we need info about how big files are so they are easier to extend.\n// Creating files is handles on the directory side, since new files just have a name and location.\n\nuse std::{\n    cmp::max,\n    ops::{\n        Div,\n        Rem\n    }\n};\n\nuse log::{debug, warn};\nuse log::error;\n\nuse crate::{error_types::drive::DriveError, pool::{\n    disk::{\n        generic::{\n            block::{\n                block_structs::RawBlock,\n                crc::add_crc_to_block\n            },\n            generic_structs::pointer_struct::DiskPointer,\n            io::cache::cache_io::CachedBlockIO\n        },\n        standard_disk::block::{\n                directory::directory_struct::{\n                    DirectoryBlock, DirectoryItem, DirectoryItemFlags\n                },\n                file_extents::{\n                    file_extents_methods::DATA_BLOCK_OVERHEAD,\n                    file_extents_struct::{\n                        FileExtent,\n                        FileExtentBlock\n                    }\n                },\n                inode::inode_struct::{\n                    Inode,\n                    InodeBlock,\n                    InodeFile,\n                    InodeFlags,\n                    InodeTimestamp\n                },\n                io::directory::types::NamedItem\n            }\n    },\n    pool_actions::pool_struct::Pool\n}, tui::{notify::NotifyTui, tasks::TaskType}};\n\nimpl InodeFile {\n    /// Update the contents of a file starting at the provided seek point.\n    /// Will automatically grow file if needed.\n    /// \n    /// !! This does not flush information to disk! You must\n    /// write back the new size of the file to disk! !!\n    /// \n    /// Optionally returns to a provided disk when done.\n    /// \n    /// Returns number of bytes written, but also updates the incoming file's size automatically\n    fn write(&mut self, bytes: &[u8], seek_point: u64) -> Result<u32, DriveError> {\n       go_write(self, bytes, seek_point)\n    }\n}\n\nimpl DirectoryBlock {\n    /// Create a new empty file in this directory with a name.\n    /// \n    /// Adds the file to the directory, flushes it to disk.\n    /// \n    /// Requires a mutable borrow, since this may update the block.\n    /// \n    /// Returns the created file's directory item. Will contain disk info.\n    /// \n    /// Should include extension iirc?\n    pub fn new_file(&mut self, name: String) -> Result<DirectoryItem, DriveError> {\n        go_make_new_file(self, name)\n    }\n\n    /// Deletes an file by deallocating every block the file used to take up, and removing it\n    /// from the directory.\n    /// \n    /// If you are looking to truncate a file, you need to call truncate() on the actual directory item.\n    /// \n    /// Returns `None` if the file did not exist.\n    ///\n    /// Panics if fed a directory. Use remove_directory() !\n    pub fn delete_file(&mut self, file: NamedItem) -> Result<Option<()>, DriveError> {\n        // We only handle files here\n        if !file.is_file() {\n            // Why\n            panic!(\"Cannot delete_file a non-file!\");\n        }\n        \n        // Extract the item\n        let extracted_item: DirectoryItem;\n        if let Some(exists) = self.find_and_extract_item(&file)? {\n            // Item was there\n            extracted_item = exists\n        } else {\n            // Tried to delete a file that does not exist in this directory.\n            return Ok(None)\n        };\n\n        // Delete it.\n        truncate_or_delete_file(&extracted_item, true, None)?;\n\n        // Since the extraction function already handles pulling out the item from the directory blocks, we are done.\n        Ok(Some(()))\n    }\n}\n\n// We dont want to call read/write on the inodes, we should do it up here so we\n// we can automatically update the information on the file, and the directory if needed.\nimpl DirectoryItem {\n    /// Write data to a file.\n    ///\n    /// Assumptions:\n    /// - This is an FILE, not a DIRECTORY.\n    /// - The location of this directory item has it's disk set.\n    /// - The inode that the item points at does exist, and is valid.\n    /// \n    /// Write to a file at a starting offset, and sets `x` bytes after that offset.\n    /// \n    /// Does not consume the directory item, since the data lower down was updated, not the\n    /// directory item itself.\n    /// \n    /// Returns how many bytes were written.\n    /// \n    /// Optionally returns to a specified disk.\n    pub fn write_file(&self, bytes: &[u8], seek_point: u64) -> Result<u32, DriveError> {\n        // We only handle files here\n        if self.flags.contains(DirectoryItemFlags::IsDirectory) {\n            // Why\n            panic!(\"Cannot delete_file a non-file!\");\n        }\n\n        // Extract out the file\n        let location = &self.location;\n\n        // Get the inode block\n        let the_pointer_in_question: DiskPointer = location.pointer;\n\n        let read: RawBlock = CachedBlockIO::read_block(the_pointer_in_question)?;\n        let mut inode_block: InodeBlock = InodeBlock::from_block(&read);\n\n        // Get the actual file.\n        // The inode MUST exist.\n        // A smarter filesystem would return the fact that the inode is missing and either try to rebuild it or\n        // have the caller discard the item. But er, I dont have time.\n        \n        let inode_with_file = if let Ok(inode) = inode_block.try_read_inode(location.offset) {\n            inode\n        } else {\n            // No inode...\n            // Maybe some day, propagate that...\n            // If this fails in the video, I'll write it lmao.\n            panic!(\"No inode exists for this DirectoryItem's file. We cannot read it.\");\n        };\n\n        // We already checked that this is a file.\n        let mut file = if let Some(the_file) = inode_with_file.extract_file() {\n            the_file\n        } else {\n            // ???\n            panic!(\"File is a file, but not a file. Nice.\");\n        };\n\n        // Write to the file\n        // This automatically updates the underlying file with the new size.\n        let num_bytes_written = file.write(bytes, seek_point)?;\n\n        // Now that the bytes are written, the size of the file may have changed, so we need to flush this new information to disk.\n\n        // Reconstruct the inode\n        let mut updated_inode: Inode = inode_with_file;\n\n        // Replace the inner file with the new updated one\n        updated_inode.file = Some(file);\n\n        // We also need to update the modify timestamp.\n        updated_inode.modified = InodeTimestamp::now();\n\n        // Now update the inode in the block. This also flushes to disk for us.\n        inode_block.update_inode(location.offset, updated_inode)?;\n\n        // All done. Return the number of bytes we wrote.\n        Ok(num_bytes_written)\n    }\n\n    /// Truncates a file to a specified byte length.\n    /// \n    /// No action needs to be taken after this method.\n    /// \n    /// Panics if fed a directory.\n    pub fn truncate(&self, new_size: u64) -> Result<(), DriveError> {\n        truncate_or_delete_file(self, false, Some(new_size))\n    }\n}\n\nfn go_write(inode_file: &mut InodeFile, bytes: &[u8], seek_point: u64) -> Result<u32, DriveError> {\n    let handle = NotifyTui::start_task(TaskType::FileWriteBytes, bytes.len() as u64);\n    // Decompose the file into its pointers\n    // No return location, we don't care where this puts us.\n    let mut blocks = inode_file.as_pointers()?;\n\n    // get the seek point\n    let (block_index, mut byte_index) = InodeFile::byte_finder( seek_point);\n\n    // The byte_finder already skips the flag, so it ends up adding one, we need to subtract that.\n    byte_index -= 1;\n\n    // Make sure we actually have a block at that offset. We cannot start writing from unallocated space.\n    if block_index > blocks.len() {\n        // Being told to write to a point we do not have.\n        panic!(\"Attempted to write to unallocated space!\");\n    }\n    \n    // Now we can calculate where the final byte of this write will end up.\n    // Minus 1, since we are writing to the byte we start the seek from\n    // IE: if we write 1 byte from out offset, we don't actually move forwards into the next byte.\n    let (mut final_block_index, _) = InodeFile::byte_finder(seek_point + bytes.len() as u64 - 1);\n\n    // If final block index is 0, we still need at least 1 block, since block 0 is the first block.\n    // Thus we must always add 1.\n    final_block_index += 1;\n\n    // Special case, if we have 0 blocks, everything up till now works fine, but we always need at least 1 block to write into.\n    // To make my life easier, we will do it here instead of relying on the caller.\n\n    // Now, if our final block index is larger than how many blocks we currently have, that means we need to pre-allocate the new room.\n    if final_block_index > blocks.len() || blocks.is_empty() {\n        // We need to get more room.\n        // Special case will always allocate at least one block.\n        let needed_blocks = max(final_block_index - blocks.len(), 1);\n\n        // This should always be <= the max write size set in \n        // filesystem_methods.rs. Which should ALWAYS be quite far away\n        // from u16::MAX, but we check anyways.\n\n        // We hard cap it to u16 block though, just in case.\n        if needed_blocks > u16::MAX.into() {\n            // Crazy.\n            panic!(\"Tried to write 2^16 blocks worth of data in one go! Not allowed!\");\n        }\n\n        // Add that many more blocks to this file.\n        // Since we know its already less than u16::MAX this cast is fine.\n        let new_pointers = expand_file(*inode_file, needed_blocks as u16)?;\n\n        // The new pointers are already in order for us, and we will add them onto the end of the\n        // pointers we grabbed earlier from the file.\n        blocks.extend(new_pointers.iter());\n    }\n\n    // Now we know we have enough space for this write, let's get started.\n\n    let mut bytes_written: usize = 0;\n    // Since the write can start un-aligned, we need to use an offset until its aligned again.\n    let mut byte_write_index: u16 = byte_index;\n\n    // Now we will loop through the blocks starting at the current index\n    for block in &blocks[block_index..] {\n        // are we out of bytes to write?\n        if bytes_written == bytes.len() {\n            // All done!\n            break\n        }\n        // Update the block\n        let written = update_block(*block, &bytes[bytes_written..], byte_write_index)?;\n        // After the first write, the offset should be fixed now, since we've either written all of our bytes, in\n        // which case we would be done, or we ran out of room in the block, thus the next block's offset would be 0.\n        byte_write_index = 0;\n        // Update how many bytes we've written\n        bytes_written += written;\n        NotifyTui::complete_multiple_task_steps(&handle, written as u64);\n        // Keep going!\n        continue;\n    }\n\n    // Done writing bytes!\n    // Update the file size, only if we wrote past the end.\n    let write_end = seek_point + bytes_written as u64;\n    if write_end > inode_file.get_size() {\n        inode_file.set_size(write_end);\n    }\n\n    NotifyTui::finish_task(handle);\n\n    // Return how many bytes we wrote!\n    Ok(bytes_written as u32)\n}\n\n/// Updates a block with new content, overwriting previous content at an offset.\n/// \n/// You can feed in as many bytes as you like, but it will only write as many as it can.\n/// \n/// Offset is the first data byte, not the first byte of the block!\n/// \n/// Returns number of bytes written.\nfn update_block(block: DiskPointer, bytes: &[u8], offset: u16) -> Result<usize, DriveError> {\n\n    // How much data a block can hold\n    let data_capacity = 512 - DATA_BLOCK_OVERHEAD as usize;\n    let offset = offset as usize;\n\n    // Check for impossible offsets\n    if offset >= data_capacity {\n        panic!(\"Tried to write outside of the capacity of a block.\");\n    }\n    \n    // Calculate bytes to write based on REMAINING space.\n    let remaining_space = data_capacity - offset;\n    let bytes_to_write = std::cmp::min(bytes.len(), remaining_space);\n    \n    // We also don't support 0 byte writes.\n    // Since that would be a failure mode of the caller, in theory could be\n    // stuck in an infinite loop type shi.\n    // Why exit? It won't if you fix the caller! :D\n    if bytes_to_write == 0 {\n        panic!(\"Tried to write 0 bytes to a block!\");\n    }\n\n    // Now, if we are about to completely fill a block (ie every byte in the block will change)\n    // we dont need to actually \"update\" the block, we can just replace it entirely.\n\n    let mut block_copy = if bytes_to_write == data_capacity && offset == 0 {\n        // Full block replacement, make fake block.\n\n        // The flag byte never ended up being used, so we dont have to worry about it.\n        RawBlock {\n            block_origin: block,\n            data: [0u8; 512], // Start with a blank slate\n        }\n    } else {\n        // Partial update, still need to read in the old block.\n        CachedBlockIO::read_block(block)?\n    };\n    \n    // Modify that sucker\n    // Skip the first byte with the flag\n    let start = offset + 1;\n    let end = start + bytes_to_write;\n\n    block_copy.data[start..end].copy_from_slice(&bytes[..bytes_to_write]);\n\n    // Update the crc\n    add_crc_to_block(&mut block_copy.data);\n\n    // Write that sucker\n    CachedBlockIO::update_block(&block_copy)?;\n\n    // Return the number of bytes we wrote.\n    Ok(bytes_to_write)\n}\n\n\n/// Expands a file by adding `x` new blocks to the extents. Returns disk pointers for the new extents.\n/// Updates underlying ExtentBlock(s) for this file.\n/// \n/// May swap disks, does not return to any start disk.\nfn expand_file(inode_file: InodeFile, blocks: u16) -> Result<Vec<DiskPointer>, DriveError> {\n    debug!(\"Expanding a file by {blocks} blocks...\");\n    // Go grabby some new blocks.\n    // These will be already reserved for us.\n    // We also need to write the CRC for later.\n    let reserved_blocks = Pool::find_and_allocate_pool_blocks(blocks, true)?;\n\n    // Make some extents from that\n    let new_extents = pointers_into_extents(&reserved_blocks);\n\n    // Add the extents.\n    expanding_add_extents(inode_file, &new_extents)?;\n\n    // Return the pointers to those new extents.\n    Ok(reserved_blocks)\n}\n\n/// Expands an ExtentBlockBlock.\n/// Always extends by one block.\n/// \n/// Will swap disks to the location of the new block. Will not return to the disk the caller started on.\n/// \n/// Sets the new destination in incoming block.\nfn expand_extent_block(block: &mut FileExtentBlock) -> Result<(), DriveError> {\n    // Get a new block from the pool.\n    // No need for crc, we will immediately write over it.\n    let new_block_location = Pool::find_and_allocate_pool_blocks(1, false)?[0];\n\n    // Put the a block there\n    let new_block: RawBlock = FileExtentBlock::new(new_block_location).to_block();\n\n    // Write, since we looked for a free block, didn't reserve it yet.\n    CachedBlockIO::update_block(&new_block)?;\n\n    // Now update the block we came in here with\n    block.next_block = new_block_location;\n\n    // Updated! All done.\n    Ok(())\n}\n\n/// Will automatically deduce runs of blocks from a slice of disk pointers, then add those extents to the block,\n/// expanding the block if needed.\n/// \n/// We assume the incoming pointers are already sorted by disk, block. (1, 1), (1, 2), (2, 1) etc.\n/// \n/// Does not check if blocks are already allocated, caller _MUST_ provide marked blocks.\nfn expanding_add_extents(file: InodeFile, extents: &[FileExtent]) -> Result<(), DriveError> {\n    // We will reverse the extents vec so we can pop them off the back\n    // for easier adding, avoiding an index.\n    let mut new_extents: Vec<FileExtent> = extents.to_vec();\n    new_extents.reverse();\n\n    // Go get the extent block to add to.\n    // We need the final one in the chain.\n    let mut current_extent_block: FileExtentBlock;\n    \n    // Read in the initial block\n    let raw_read: RawBlock = CachedBlockIO::read_block(file.pointer)?;\n    current_extent_block = FileExtentBlock::from_block(&raw_read);\n\n    loop {\n        // Is this the final block?\n        if !current_extent_block.next_block.no_destination() {\n            // No it isn't. We need to load the next block.\n            // Get the block.\n            let reader_mc_deeder: RawBlock = CachedBlockIO::read_block(current_extent_block.next_block)?;\n            current_extent_block = FileExtentBlock::from_block(&reader_mc_deeder);\n            // Try again.\n            continue;\n        }\n\n        // This is the final block.\n        // Try adding extents\n        while let Some(last) = new_extents.last() {\n            // Try adding a new extent.\n            let added_result = current_extent_block.add_extent(*last);\n\n            // if that worked, that means we added the extent successfully.\n            if added_result.is_ok() {\n                // Good! Keep going\n                // Pop off the extent since we're done with it.\n                let _ = new_extents.pop();\n                continue;\n            }\n            // Otherwise we either ran out of room, or this isn't the last block in the chain.\n            // We already checked the latter, so...\n            break\n        }\n        \n        // There are two cases that cause us to break out of that loop.\n        if new_extents.is_empty() {\n            // We ran out of file extents to add. we are done.\n            // Flush it\n            flush_to_disk(&current_extent_block)?;\n            // bye\n            break\n        }\n\n        // We must've ran out of room.\n        // Expand the block please.\n        expand_extent_block(&mut current_extent_block)?;\n        \n        // Now we must write that extended block to disk.\n        flush_to_disk(&current_extent_block)?;\n\n        // The block has a new destination now, and has been flushed to disk. There is nothing\n        // else left for us to do before just looping again, since the next loop\n        // will see the new destination.\n        continue;\n    }\n\n    // All done adding extents! We've already flushed the blocks as well.\n    // There is nothing left to do.\n    Ok(())\n}\n\n/// Automatically groups incoming pointers into a new vec of file extents.\n/// assumes all of the incoming pointers are already sorted.\nfn pointers_into_extents(pointers: &[DiskPointer]) -> Vec<FileExtent> {\n    // I feel like there is 100% a better way to do this, but i dont know it. so too bad!\n\n    // Cant pre-allocate room in the vec, since we dont know how many extents we'll be creating,\n    // and estimating it is hard.\n    let mut new_extents: Vec<FileExtent> = Vec::new();\n    \n    // Loop over the pointers and create extents.\n    for pointer in pointers {\n        // Check if we need to make a new extent.\n        // We start at 0 since we increment at the end of the loop.\n        let new: FileExtent = FileExtent::new(*pointer, 0);\n        // We need a new one if:\n        // - There are no extents\n        // - The disk number is different\n        // - The length is maxed out\n        // - The next block is not contiguous. (ie last block was 1, new block != 2)\n\n        // yes this is ugly, at least it doesnt have to check for local disks anymore\n        if let Some(extent) = new_extents.last() {\n            if extent.start_block.disk != pointer.disk || // Is the disk number different?\n            extent.length == u8::MAX || // Is this extent out of room?\n            extent.start_block.block + extent.length as u16 != pointer.block // Non contiguous?\n            {\n                // Need a new one.\n                new_extents.push(new);\n            }\n            // All checks pass, fall out.\n        } else {\n            // There isn't any extents yet, make the first one\n            new_extents.push(new);\n        }\n        \n        // This pointer extends the previous (or new) extent block, add one to the length.\n        match new_extents.last_mut() {\n            Some(last) => {\n                last.length += 1;\n            },\n            None => {\n                // I guess we never got any pointers.\n                // Do nothing.\n            },\n        }\n    }\n\n    // All done!\n    new_extents\n}\n\n/// Create a new file.\nfn go_make_new_file(directory_block: &mut DirectoryBlock, name: String) -> Result<DirectoryItem, DriveError> {\n    // Directory blocks already have a method to add a new item to them, so we just need\n    // to create that item to add.\n\n    // New files must have a filename that is <= u8::MAX.\n    // Caller is in charge of checking this before giving it to us.\n    if name.len() > u8::MAX.into() {\n        panic!(\"File name was too long!\");\n    }\n\n    // Timestamp for file creation\n    let right_now: InodeTimestamp = InodeTimestamp::now();\n    \n    // No need for CRC, we will be writing over it.\n    let in_progress = Pool::find_and_allocate_pool_blocks(1, false)?;\n    let reserved_block: DiskPointer = in_progress[0];\n    \n    // Now that we have the new block we need a FileExtentBlock to write into it.\n    let new_block: FileExtentBlock = FileExtentBlock::new(reserved_block);\n\n    // No need to set the marker bit since this is a file ofc.\n\n    // Now let's write that new block\n    let raw: RawBlock = new_block.to_block();\n    // Block is not marked as reserved, so this is a write.\n    CachedBlockIO::update_block(&raw)?;\n\n    // Construct the file that we'll be returning.\n    let finished_new_file: InodeFile = InodeFile::new(reserved_block);\n\n    // Now that the block has been written, put that sucker into the directory\n    \n    // We do need an inode location tho, so get one\n    let new_inode: Inode = Inode {\n        flags: {\n            // We need to set the marker bit and the inode type (file)\n            let mut inner = InodeFlags::MarkerBit;\n            inner.insert(InodeFlags::FileType);\n            inner\n        },\n        file: Some(finished_new_file),\n        directory: None,\n        created: right_now,\n        modified: right_now,\n    };\n\n    let new_inode_location = Pool::fast_add_inode(new_inode)?;\n\n    // Wrap it all up in a little bow to put into the directory\n\n    // Now we need to set up the flags for the new directory item\n    // Flag\n    let flags: DirectoryItemFlags = DirectoryItemFlags::MarkerBit;\n    // Thats it. Lol.\n\n    // Construct the new file\n    let new_file: DirectoryItem = DirectoryItem {\n        flags,\n        name_length: name.len() as u8,\n        name,\n        location: new_inode_location,\n    };\n\n    directory_block.add_item(&new_file)?;\n    // If we're here, that worked. We are all done adding the item to the directory.\n\n    // All done.\n    // Dont need to swap disks, already did that on the item add.\n    Ok(new_file)\n}\n\n// One hell of a function.\n/// Will only truncate if delete is false.\nfn truncate_or_delete_file(item: &DirectoryItem, delete: bool, new_size: Option<u64>) -> Result<(), DriveError> {\n    // Is this a file?\n    if item.flags.contains(DirectoryItemFlags::IsDirectory) {\n        // Uh, no it isn't why did you give me a dir?\n        panic!(\"Tried to truncate or delete a directory as if it was a file!\");\n    }\n\n    // Load the size of the directory item\n    let file_size: u64 = item.get_size()?;\n\n    // If we aren't deleting, and the size is the same as the current size, we can skip truncation.\n    if let Some(the_new_size) = new_size {\n        // Make sure delete flag is not set.\n        // In theory, None means delete is set.\n        if !delete && the_new_size == file_size {\n            // Skip\n            return Ok(());\n        }\n    }\n\n    // Extract out the file\n    let file_inode_location = &item.location;\n\n    // Get the inode block\n    let the_pointer_in_question: DiskPointer = file_inode_location.pointer;\n\n    let read: RawBlock = CachedBlockIO::read_block(the_pointer_in_question)?;\n    let mut inode_block: InodeBlock = InodeBlock::from_block(&read);\n\n    // Get the actual file\n    let mut inode_with_file: Inode = if let Ok(inode) = inode_block.try_read_inode(file_inode_location.offset) {\n        inode\n    } else {\n        // The inode for this file does not exist.\n        panic!(\"Cannot truncate or delete files that do not have an inode!\");\n    };\n    // We already checked that this is a file.\n    let mut file: InodeFile = if let Some(the_file) = inode_with_file.extract_file() {\n        the_file\n    } else {\n        // ?\n        panic!(\"Flag for file set, but no file.\");\n    };\n\n    // If the truncation is just growing the file, we can do this directly on the file itself without messing with the extents.\n\n    // Truncation can also grow files, check if the truncation is larger than the current size.\n    // Growing cannot happen at the same time as deletion.\n    if let Some(extracted_new_size) = new_size && file_size < extracted_new_size && !delete {\n        // We are just growing.\n        // Growing is easy, we just write zeros to make it the new size.\n\n        // The difference\n        let grow_size: usize = (extracted_new_size - file_size) as usize;\n        \n        // We need to do this in a loop, since would be consuming as much ram as the write is big, which isn't great.\n        // So we will do it in 1MB chunks, but this may change in the future if its too slow.\n        const CHUNK_SIZE: usize = 1024*1024;\n        let zero_chunk: Vec<u8> = vec![0; CHUNK_SIZE];\n\n        // Write to the end of the file with the zeros.\n        let mut seek_point = file_size;\n        for _ in 0..grow_size.div(CHUNK_SIZE) {\n            // Yeah... Keep eating...\n            let _ = item.write_file(&zero_chunk, seek_point)?;\n            seek_point += CHUNK_SIZE as u64;\n        }\n\n        // Final write if there are any remaining bytes.\n        let remainder = grow_size.rem(CHUNK_SIZE);\n        if remainder != 0 {\n            let final_zeros: Vec<u8> = vec![0; remainder];\n            let _ = item.write_file(&final_zeros, seek_point)?;\n        }\n        \n        // Make sure the new size is correct\n        let gotten_size = item.get_size()?;\n        if extracted_new_size != gotten_size {\n            // Truncation did not work properly.\n            error!(\"Truncated to the wrong size! Expected {extracted_new_size} got {gotten_size} !\")\n        };\n        \n        // All done.\n        return Ok(());\n    }\n\n    // If we are here, we must be shrinking or deleting.\n\n    // If we are deleting, we can skip the more complicated extent logic.\n    if delete {\n        // We are deleting all of the blocks, so just get all of them.\n        let mut used_blocks = file.as_pointers()?;\n\n        // We also need to free the extent blocks themselves, not just where they point.\n        let mut extent_block_pointer = file.pointer;\n\n        while !extent_block_pointer.no_destination() {\n            used_blocks.push(extent_block_pointer);\n            // This work has already been done on `to_pointers`, maybe there should be another\n            // method that returns the pointers, and the pointers to the extent blocks at the same time.\n            // Not gonna write that tho, this is fine.\n            let read = CachedBlockIO::read_block(extent_block_pointer)?;\n            let extent_block: FileExtentBlock = FileExtentBlock::from_block(&read);\n            extent_block_pointer = extent_block.next_block;\n        }\n\n        // We dont have to worry about updating the underlying block, since the deletion call\n        // will discard the item automagically.\n\n        // Sort the blocks to reduce swap\n        used_blocks.sort_unstable_by_key(|block| (block.disk, block.block));\n\n        // Chunk by disk.\n        let chunked = used_blocks.chunk_by(|a, b| a.disk == b.disk);\n\n        // Delete all the blocks by freeing all of them.\n        for chunk in chunked {\n            let _ = Pool::free_pool_block_from_disk(chunk)?;\n        }\n\n        // All done!\n        return Ok(());\n    }\n\n    // Since we didn't delete, we must be shrinking, since we already checked for growing\n    // This should be guarded.\n    let new_size = new_size.expect(\"Cannot truncate a file without a size to truncate to.\");\n\n\n    // To truncate, several things need to happen:\n    // - We need to update the data that is contained within the final data block to write in zeros\n    // -  past the new ending size.\n    // - We need to remove all of the data blocks that are no longer used.\n    // - We need to remove all of the file extent blocks that are no longer used.\n    // - We need to update the pointer on the new final extent block (if needed)\n    // - - This will never point at a new block, since its the new final, and we cannot\n    // - -  remove extents from the middle of a file.\n    // - We need to remove all extents past this one in the new final file extent block.\n    // - Update the file size.\n\n    // I considered extracting out all of the extents to consolidate them again, but there would be\n    // no space savings from doing that, since extents are always added at the end of the chain, and must be in order.\n    // Thus, if an extent could have fit in the previous block, it would have already been there. You cannot truncate in\n    // the middle of the file.\n    // Thus, removing extents does not create gaps, and is already as efficient as possible (in Fluster's implementation at least, lol)\n\n    // Steps:\n    // Deduce what data block is the new final block.\n    // - The FileExtentBlock that contained that block is the new final FileExtentBlock in the chain.\n    // - The Extent that the data block was contained within is now the final Extent within the FileExtentBlock\n    // Update the new final data block.\n    // - Write zeros to the end of the block after the new file end point.\n    // - Do not write it yet, it must be written after the ExtentBlock has been updated, otherwise we\n    // -  would be changing file data without changing the file ending, corrupting the underlying file.\n    // Remove all extents past this one in the containing extent block.\n    // - Just pop off all ones after this one, and hold onto the pointers for them\n    // -  so we can free those blocks later.\n    // Update the pointer on the new final ExtentBlock\n    // - Hold onto where it used to go, we need it for cleanup later\n    // - Point to nowhere.\n    // All at once:\n    // - Flush the updated final extent to disk, then the updated data block.\n    // - - Make sure to set the extents to be local if needed, since the disk gets set during the read process.\n    // - - After this point, the file has been properly truncated from the filesystem perspective.\n    // - - Even if the rest of the deletion fails after this point, we at least wont be pointing at the data blocks\n    // - -  that were supposed to be freed. We'll have leaked them though, which stinks, but whatever.\n    // - Update the file size\n    // - Update the file modify time\n    // Collect all of the DiskPointers to the remaining ExtentBlocks.\n    // - Cant immediately remove them, we still need the DiskPointers to the data blocks\n    // Collect all of the DiskPointers to the data blocks within the extents in the remaining extent blocks.\n    // - Loop over the collected disk pointers in the previous step, open the FileExtentBlock, extract the\n    // - DiskPointers from all of the contained Extents\n    // Free all of the blocks we've collected\n    \n\n    // Is this a lot of documentation? yes.\n    // Is this the third time i've rewritten this function from near-scratch today? Also yes.\n\n\n\n    // == Deduce what data block is the new final block. ==\n\n    // We need to find the block offset.\n    let (new_final_block_index, new_final_block_byte_index) = InodeFile::byte_finder(new_size);\n\n    // Now we can loop through the FileExtentBlocks, tracking how many data blocks we've seen so far\n    // Get the first extent block\n    let read: RawBlock = CachedBlockIO::read_block(file.pointer)?;\n    let first_extent_block = FileExtentBlock::from_block(&read);\n\n    // Go find the block, also keep track of what extent caused us to be full, since\n    // that'll be our new final extent.\n    let mut new_final_extent_block: FileExtentBlock = first_extent_block;\n    // Dummy values that will be overwritten.\n    let mut pointer_to_new_final_data_block: DiskPointer = DiskPointer::new_final_pointer();\n    let mut offset_in_final_extent: usize = 0;\n    let mut final_extent_index: usize = 0;\n\n    let mut blocks_seen: usize = 0;\n    let mut done: bool = false;\n\n    // At this point, we know the truncation HAS to be less than the current size of the file, thus\n    // we do not need to check if we run out of blocks, since they must be there.\n    // If they aren't there, its a more fundamental issue, not our problem.\n    loop {\n        // Get the extents from the ExtentBlock\n        let extents = new_final_extent_block.get_extents();\n\n        // Now search through those extents, incrementing how many\n        // blocks we've seen and keeping track of what extent this is\n        for (index, extent) in extents.iter().enumerate() {\n            // How many blocks are in here?\n            let pointers: Vec<DiskPointer> = extent.get_pointers();\n            // Add all of those pointers to the count\n            blocks_seen += pointers.len();\n\n            // Have we seen enough blocks?\n            if blocks_seen >= new_final_block_index {\n                // The last extent we opened is the final one!\n\n                // We need to deduce which block it was, since we want to know the index of it.\n                // Sure, we could have looped over the pointers instead of adding the length and just captured the disk pointer\n                // by itself, but we still need to know the index as well, so we can remove the items after it later.\n                // \"erm what about iter().enumerate()\" sybau ts pmo...\n\n                // How many blocks we found - the number of blocks we wanted = how many extra blocks we read.\n                // therefore, pointers.len() - extra = index into pointers where the block is\n\n                // The number of blocks a extent can hold is at most 256, which fits into an i16 when negative.\n                let offset = pointers.len() - (blocks_seen - new_final_block_index);\n                offset_in_final_extent = offset;\n\n                // Now we can get the pointer to the final data block\n                pointer_to_new_final_data_block = pointers[offset];\n\n                final_extent_index = index;\n                done = true;\n                break\n            }\n        }\n\n        // Done?\n        if done {\n            break\n        }\n        \n        // Need to keep going, get the next block.\n        // Dont need to check if this is a final pointer, since we would crash if it was, and it shouldn't be.\n        let read: RawBlock = CachedBlockIO::read_block(new_final_extent_block.next_block)?;\n        let next = FileExtentBlock::from_block(&read);\n        new_final_extent_block = next;\n    }\n\n    // == Update the new final data block. ==\n    // == - Write zeros to the end of the block after the new file end point. ==\n    // == - Do not write it yet, it must be written after the ExtentBlock has been updated, otherwise we ==\n    // == -  would be changing file data without changing the file ending, corrupting the underlying file. ==\n\n    // easy\n    \n    // Find the index into the block where everything past it will be blanked out...\n    // jk, we already know that hehe, its in new_final_block_byte_index\n\n    // Now load in the old block so we can update it\n    let mut updated_final_data_block: RawBlock = CachedBlockIO::read_block(pointer_to_new_final_data_block)?;\n\n    // Now blank it out.\n    // Currently, the last 4 bytes of the block are the checksum. but since we're going to be updating the block anyways, we can write\n    // over it, since we'll have to re-checksum it anyways.\n    updated_final_data_block.data[new_final_block_byte_index as usize..].fill(0_u8);\n\n    // Put the checksum back on\n    add_crc_to_block(&mut updated_final_data_block.data);\n\n    // Dont write the block yet, we'll hold onto it until _after_ we do the FileExtentBlock update.\n\n\n\n    // == Remove all extents past this one in the containing extent block. ==\n    // == - Just pop off all ones after this one, and hold onto the pointers for them ==\n    // == -  so we can free those blocks later. ==\n\n    // Start blocks that we need to free.\n    // Cannot pre-allocate, since there's no way to know how many blocks we'll be freeing\n    // at this point. Guesstimations could be made, but oh well.\n    let mut blocks_to_free: Vec<DiskPointer> = Vec::new();\n\n    // We also need to grab the extra disk pointers from the extent that holds the new\n    // final data block, if there are any.\n\n    // Update the extents for the final block.\n    let mut updated_extents = new_final_extent_block.get_extents();\n\n    for (index, extent) in updated_extents.iter_mut().enumerate() {\n        // skip if this is before the final extent\n        if index < final_extent_index {\n            // skip\n            continue;\n        }\n        // is this the final extent\n        if index == final_extent_index {\n            // Now we need to remove the extra disk pointers if there are any.\n            let pointers = extent.get_pointers();\n            // Split the vec to remove anything after the final data block pointer.\n            // Splitting keeps `start..split`, but we need `start..=split` so we will need to\n            // increment the index.\n            let split_point: usize = offset_in_final_extent + 1;\n\n            // Splitting will panic if this is past the end of the array. Which would be the case\n            // if we found exactly as many blocks as we needed in the final extent.\n            if split_point > pointers.len() {\n                // We dont need to update this extent.\n                continue;\n            }\n\n            // Do the splits, keeping the second section so we can get the pointers from it\n            let to_free = pointers.split_at(split_point).1;\n\n            // Now to update the extent, we just need to remove all items past the split point.\n            // since extents are encoded as a start + length, we can just subtract the number of extra items.\n            extent.length -= to_free.len() as u8;\n\n            // Now add the extra pointers to the trash pile\n            blocks_to_free.extend(to_free);\n            \n            // All done\n            continue;\n        }\n        // This is after the last extent we care about, trash everything.\n        let pointers = extent.get_pointers();\n        blocks_to_free.extend(pointers);\n    }\n\n    // Now that we've collected all the extents we care about, and trashed everything else, we can drop any extra\n    // extents from the block if they exist.\n    // truncate is `..end` not `..=end` so we add one.\n    let truncate_point: usize = final_extent_index + 1;\n    updated_extents.truncate(truncate_point);\n\n    // Now we only have the extents we care about in `updated_extents`.\n\n    // == Update the pointer on the new final ExtentBlock ==\n    // == - Hold onto where it used to go, we need it for cleanup later ==\n    // == - Point to nowhere. ==\n    \n    // We have to point to nowhere before we can put in the new extents.\n    // We will also hold onto it\n    let unreferenced_extent_block_chain_start: DiskPointer = new_final_extent_block.next_block;\n    new_final_extent_block.next_block = DiskPointer::new_final_pointer();\n\n    // == All at once: ==\n    // == - Flush the updated final extent to disk, then the updated data block. ==\n    // == - - Make sure to set the extents to be local if needed, since the disk gets set during the read process. ==\n    // == - - After this point, the file has been properly truncated from the filesystem perspective. ==\n    // == - - Even if the rest of the deletion fails after this point, we at least wont be pointing at the data blocks ==\n    // == - -  that were supposed to be freed. We'll have leaked them though, which stinks, but whatever. ==\n    // == - Update the file size ==\n    // == - Update the file modify time ==\n\n    // Add the extents\n    // This does not flush to disk.\n    new_final_extent_block.force_replace_all_extents(updated_extents);\n\n    // Now for the scary part.\n    // This write must complete, followed by the update to the data block, otherwise data\n    // will corrupt.\n\n    let finished_extent_block: RawBlock = new_final_extent_block.to_block();\n\n    debug!(\"Writing updated file extent block after size decrease...\");\n    debug!(\"If this fails, data corruption WILL occur..\");\n    \n    // Oh boy.\n    // We will do all 3 steps at once, even if the two writes fail, as long as the file size change works, the file\n    // will _possibly_ be in a usable state, since bounds checks are done to make sure we dont read past the end.\n    // ...Until something tries to extend the file, new blocks will pointlessly be added, and writing may skip over blocks,\n    // resulting in the next read containing old data from pre-truncation.\n\n    let extent_block_result = CachedBlockIO::update_block(&finished_extent_block);\n    let data_block_result = CachedBlockIO::update_block(&updated_final_data_block);\n    \n    // Update the file\n    file.set_size(new_size);\n    inode_with_file.file = Some(file);\n    // might as well set the time here too\n    inode_with_file.modified = InodeTimestamp::now();\n    // Write it back to the block it came from\n    let inode_update_result = inode_block.update_inode(file_inode_location.offset, inode_with_file);\n    \n\n    // Now, did that all work?\n    let all_worked: bool = extent_block_result.is_ok() && data_block_result.is_ok() && inode_update_result.is_ok();\n    let all_failed: bool = extent_block_result.is_err() && data_block_result.is_err() && inode_update_result.is_err();\n    if !all_worked && !all_failed { // at least one fail, but not all of them.\n        // DAMNIT!\n\n        // \"why are you printing so much out here\"\n        // Well if the filesystem fails during my factorio run, this would\n        // add extra drama hahaha... god i hope it doesn't fail...\n\n        error!(\"TRUNCATION FAILURE!\");\n        error!(\"Listing what failed:\");\n        error!(\"==-==-==-==-==-==-==-==\");\n        if extent_block_result.is_err() {\n            error!(\"- Extent block write.\");\n        }\n        if data_block_result.is_err() {\n            error!(\"- Data block write.\");\n        }\n        if inode_update_result.is_err() {\n            error!(\"- Inode update.\");\n        }\n        error!(\"==-==-==-==-==-==-==-==\");\n        error!(\"We have to keep going. But this file may now be VERY unstable.\");\n        error!(\"Any further calls on this file will almost certainly do unexpected things.\");\n\n        // We have now leaked all of the blocks that we were intending to free.\n        error!(\"At least `{}` blocks have now been leaked.\", blocks_to_free.len());\n        error!(\"That does not include:\");\n        error!(\"FileExtent blocks past the new final extent block in the extent chain.\");\n        error!(\"Data storing blocks that those FileExtent blocks pointed to.\");\n        // We can at least estimate it.\n        // yes i know that %512 does not account for the flags and such in the data blocks,\n        // but we arent counting the extent block overhead either so\n        let leak_estimate: usize = (file_size - new_size).div_ceil(512) as usize;\n        error!(\"Rough estimate: `{leak_estimate}` additional blocks leaked.\");\n        error!(\"Godspeed.\");\n\n        // If we are testing, this should panic as well.\n        if cfg!(test) {\n            panic!(\"Truncation fail.\");\n        }\n\n        // No error, since we need to just ignore the error if we dont wanna completely give up\n        return Ok(());\n        \n        // Too late to turn back, unless all 3 failed? but what are the odds?\n    } else if all_failed {\n        // Woah.\n        // All three operations failed, which, unusually, is a good thing in this case.\n        warn!(\"TRUNCATION FAILURE!\");\n        warn!(\"All of the operations failed, which means nothing was changed.\");\n        warn!(\"Scary, but we are actually fine in this case.\");\n        warn!(\"We can continue like nothing happened, because nothing did.\");\n\n        // This should also fail tests.\n        if cfg!(test) {\n            panic!(\"Truncation fail: No update.\");\n        }    \n\n        // If the disk is busy, we retry, so...\n        return Err(DriveError::Retry);\n    } else {\n        // Everything worked!\n        debug!(\"Content update finished successfully. Phew.\")\n    }\n\n    // Cool! Now we can go free blocks we don't need anymore.\n\n    // Failure after this point will leak blocks. so we keep it locked up to report a leak.\n\n    debug!(\"Running truncate cleanup...\");\n    let cleanup_result = truncate_cleanup(blocks_to_free, unreferenced_extent_block_chain_start);\n\n    // Did that all sail smoothly\n    match cleanup_result {\n        Ok(free_count) => {\n            // All good! All garbage has been freed!\n            debug!(\"Truncate cleanup finished successfully.\");\n            debug!(\"Truncation freed {free_count} blocks.\");\n            Ok(())\n        },\n        Err(err) => {\n            // Oh well.\n            error!(\"Truncate cleanup did not finish!\");\n            error!(\"We have leaked blocks!\");\n            error!(\"Unknown how many leaked. But we can still safely continue.\");\n            error!(\"Failue: {err:#?}\");\n            // panic in tests.\n            if cfg!(test) {\n                panic!(\"Truncation cleanup fail.\");\n            }\n            // We can keep going without caring, too bad about them wasted blocks tho, eh?\n            Ok(())\n        }\n    }\n}\n\n/// Returns how many blocks were freed.\nfn truncate_cleanup(pre_collected: Vec<DiskPointer>, next_extent_block: DiskPointer) -> Result<usize, DriveError> {\n    // The rest of the cleanup happens in this function so we can easily check if any of it fails.\n    debug!(\"Starting truncation cleanup, started with {} pre-collected blocks...\", pre_collected.len());\n    \n    // Dump those pointers into a new local pile\n    let mut to_free: Vec<DiskPointer> = pre_collected;\n\n    // == Collect all of the DiskPointers to the remaining ExtentBlocks. ==\n    // == - Cant immediately remove them, we still need the DiskPointers to the data blocks ==\n\n    // Make sure the pointer goes somewhere, if it doesnt, we can skip this step.\n    // Shadow it so we can update it for the loop\n    let mut next_extent_block = next_extent_block;\n\n    // Also collect the extents, gotta remember where the're from too\n    // Cant pre-allocate this, no idea how many extent blocks there will be.\n    let mut extents: Vec<(u16, FileExtent)> = Vec::new();\n\n    debug!(\"Collecting blocks referred to by extents...\");\n    while !next_extent_block.no_destination() {\n        // Open the extent\n        let raw: RawBlock = CachedBlockIO::read_block(next_extent_block)?;\n        let read: FileExtentBlock = FileExtentBlock::from_block(&raw);\n\n        // get the extents\n        let tents: Vec<(u16, FileExtent)> = read.get_extents().into_iter().map(|i|{\n            (next_extent_block.disk, i)\n        }).collect();\n\n        extents.extend(tents);\n\n        // Add this extent block to the pile\n        to_free.push(next_extent_block);\n\n        // set the next block\n        next_extent_block = read.next_block;\n    }\n\n    debug!(\"Done.\");\n    \n    // == Collect all of the DiskPointers to the data blocks within the extents in the remaining extent blocks. ==\n    // == - Loop over the collected disk pointers in the previous step, open the FileExtentBlock, extract the ==\n    // == - DiskPointers from all of the contained Extents ==\n    \n    // To save reads i grabbed the extents as i read the blocks, we just need the pointers\n    debug!(\"Extracting pointers...\");\n    for (_, tent) in extents { // I used to be a tent, but I got too old and cant pitch them anymore.\n        let pointers = tent.get_pointers();\n        to_free.extend(pointers);\n    }\n    debug!(\"Done.\");\n    \n    // == Free all of the blocks we've collected ==\n\n\n    // Sort the blocks to reduce the amount of head seeking. This also groups together the disks.\n    to_free.sort_unstable_by_key(|block| (block.disk, block.block));\n\n    // Make sure there are no duplicates.\n    // Yes we shouldn't be getting them in the first place, but if we somehow do, this will\n    // crash due to double free.\n    let pre_dedup = to_free.len();\n    to_free.dedup();\n    let post_dedup = to_free.len();\n    if pre_dedup != post_dedup {\n        // Sizes are different, cooked.\n        panic!(\"There was duplicate blocks during truncation cleanup. Cannot continue.\");\n    }\n\n    // Hold onto how many blocks we're freeing for returning.\n    let amount_freed = to_free.len();\n\n    // Split into sections based on when the disk changes.\n    let chunked = to_free.chunk_by(|a, b| a.disk == b.disk);\n\n    // Now go free all of those blocks.\n    // This will zero out the blocks, and remove them from the cache for us.\n    debug!(\"Freeing blocks...\");\n    for chunk in chunked {\n        let _ = Pool::free_pool_block_from_disk(chunk)?;\n    }\n    debug!(\"Done.\");\n    debug!(\"All done cleaning up truncation.\");\n\n    // All done! Return how many blocks we freed.\n    Ok(amount_freed)\n}\n\n/// Just flushes the current FileExtentBlock to disk, nice helper function\nfn flush_to_disk(block: &FileExtentBlock) -> Result<(), DriveError> {\n    // Raw it\n    let raw = block.to_block();\n    // Write it.\n    CachedBlockIO::update_block(&raw)?;\n    Ok(())\n}"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/inode/mod.rs",
    "content": "pub mod read;\n#[cfg(test)]\nmod tests;\npub mod write;\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/inode/read.rs",
    "content": "// Functions from other files are used for reading inodes at this level.\n// If you need to touch an inode, you're probably doing some lower level stuff, so\n// you need to use lower level tools.\n//\n// \n// bonus joke:\n// what did the blank floppy say to the drive?\n//\n//"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/inode/tests.rs",
    "content": "// Inode tests.\n// Unwrapping is okay here, since we want unexpected outcomes to fail tests.\n#![allow(clippy::unwrap_used)]\n\nuse std::path::PathBuf;\n\nuse log::debug;\nuse tempfile::{TempDir, tempdir};\n\nuse crate::{\n    filesystem::filesystem_struct::{FilesystemOptions, FlusterFS},\n    pool::{\n        disk::standard_disk::block::inode::inode_struct::Inode, pool_actions::pool_struct::Pool,\n    },\n};\n\nuse test_log::test; // We want to see logs while testing.\n\n// In the inode test file, we already create some functions that we can reuse in here.\n\n// Since these tests touch global state, they need to be isolated... somehow\n\n#[test]\nfn add_inode() {\n    // Use the filesystem starter to get everything in the right spots\n    let _fs = get_filesystem();\n    // Now try adding a directory to the pool\n    let _ = Pool::add_inode(Inode::get_random()).unwrap();\n}\n\n#[test]\nfn add_many_inode() {\n    let _fs = get_filesystem();\n    for _ in 0..1000 {\n        let _ = Pool::add_inode(Inode::get_random()).unwrap();\n    }\n}\n\n// We need a filesystem to run directory tests on.\nfn get_filesystem() -> FlusterFS {\n    let temp_dir = get_new_temp_dir();\n    let floppy_drive: PathBuf = PathBuf::new(); // This is never read since we are using temporary disks.\n    let fs_options = FilesystemOptions::new(Some(temp_dir.path().to_path_buf()), floppy_drive, Some(false), false);\n    FlusterFS::start(&fs_options)\n}\n\n//\n// Helper functions\n//\n\n// Temporary directories for virtual disks\nfn get_new_temp_dir() -> TempDir {\n    let mut dir = tempdir().unwrap();\n    dir.disable_cleanup(true);\n    debug!(\n        \"Created a temp directory at {}, it will not be deleted on exit.\",\n        dir.path().to_string_lossy()\n    );\n    dir\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/inode/write.rs",
    "content": "// Inode this was going somewhere...\n\n// Imports\n\n// Implementations\n\n// Functions\n\nuse log::trace;\n\nuse crate::{\n    error_types::{\n        block::BlockManipulationError,\n        drive::DriveError\n    },\n    pool::{\n        disk::{\n            generic::{\n                block::block_structs::RawBlock,\n                generic_structs::pointer_struct::DiskPointer,\n                io::cache::cache_io::CachedBlockIO,\n            },\n            standard_disk::block::inode::inode_struct::{\n                    Inode,\n                    InodeBlock,\n                    InodeLocation\n                },\n        },\n        pool_actions::pool_struct::{\n            Pool,\n            GLOBAL_POOL\n        },\n    }\n};\n\n\n// The pool MUST exist for inodes to be created.\nmacro_rules! get_pool {\n    () => {\n        if let Ok(innards) = GLOBAL_POOL.get().expect(\"There has to be a global pool at this point.\").try_lock() {\n            innards\n        } else {\n            // Cannot do inode stuff with dying pool, dying pools need to just shut down immediately.\n            panic!(\"A poisoned pool cannot have inode operations performed against it!\");\n        }\n    };\n}\n\n\n\n\n// For the pool implementations, we do not use Self, as we might try to double mut it if the inode\n// addition routine adds a new disk.\nimpl Pool {\n    /// Add an inode to the never ending inode chain.\n    ///\n    /// This method will look at the pool, and attempt adding an inode\n    /// to the lowest disk with the most recent successful inode write.\n    /// (Ignoring manual writes done outside of this method.)\n    ///\n    /// This function will traverse to the next blocks in the chain,\n    /// and create new disks if needed.\n    ///\n    /// Returns where the inode ended up.\n    pub fn fast_add_inode(inode: Inode) -> Result<InodeLocation, DriveError> {\n        trace!(\"Fast adding inode...\");\n        // Get the pool's latest inode disk\n        let start_pointer: DiskPointer = get_pool!().header.latest_inode_write;\n\n        // load in that block\n        let da_reader: RawBlock = CachedBlockIO::read_block(start_pointer)?;\n        let start_block: InodeBlock = InodeBlock::from_block(&da_reader);\n\n        let result = go_add_inode(inode, start_block)?;\n        // Where that ended up needs to be known\n        let success_write_pointer: DiskPointer = result.pointer;\n\n        // Update the pool with new successful write.\n        {\n            let mut pool = get_pool!();\n            pool.header.latest_inode_write = success_write_pointer;\n        }\n\n        // all done\n        Ok(result)\n    }\n    /// Add an inode to the never ending inode chain.\n    ///\n    /// This method adds an inode to the pool, starting from\n    /// the origin inode block on the origin disk.\n    /// This may take a long time, but will ensure that the first\n    /// available spot within the entire inode pool is used.\n    ///\n    /// This function will traverse to the next blocks in the chain,\n    /// and create new disks if needed.\n    ///\n    /// Returns where the inode ended up.\n    pub fn add_inode(inode: Inode) -> Result<InodeLocation, DriveError> {\n        trace!(\"Adding inode, starting from disk 1...\");\n        // Start from the origin.\n        let start_pointer: DiskPointer = DiskPointer { disk: 1, block: 1 };\n\n        let da_reader: RawBlock = CachedBlockIO::read_block(start_pointer)?;\n\n        let start_block: InodeBlock = InodeBlock::from_block(&da_reader);\n\n        let result = go_add_inode(inode, start_block)?;\n        // Where that ended up needs to be known\n        let success_write_pointer: DiskPointer = result.pointer;\n\n        // Update the pool with new successful write.\n        {\n            let mut pool = get_pool!();\n            pool.header.latest_inode_write = success_write_pointer;\n        }\n\n        // all done\n        Ok(result)\n    }\n}\n\nfn go_add_inode(inode: Inode, start_block: InodeBlock) -> Result<InodeLocation, DriveError> {\n    // We will start from the provided block.\n\n    \n    // For when we switch disks (abstracted away but hehe!)\n    let mut current_block_number: u16 = start_block.block_origin.block;\n    let mut current_disk: u16 = start_block.block_origin.disk;\n    let mut current_block: InodeBlock = start_block;\n    // For when we eventually find a spot.\n    let inode_offset: u16;\n\n    // Now we loop, looking for room.\n    loop {\n        // Does the current block have room?\n        match current_block.try_add_inode(inode) {\n            Ok(ok) => {\n                // We've got a spot!\n                inode_offset = ok;\n                break;\n            }\n            Err(error) => match error {\n                BlockManipulationError::OutOfRoom => {\n                    // Not enough room on this block, go fish.\n                }\n                BlockManipulationError::Impossible => {\n                    // Impossible offsets should never happen.\n                    // Cant keep going with logic errors.\n                    panic!(\"Impossible inode offset. {inode:#?}\");\n                },\n                BlockManipulationError::NotFinalBlockInChain | BlockManipulationError::NotPresent => {\n                    // Not possible here, inodes dont care about being the final block,\n                    // and adding doesnt check if an item is present.\n                    panic!(\"Apparently inodes DO care!\");\n                },\n            },\n        }\n        // There wasn't enough room, proceed to the next block.\n        let pointer_to_next_block: DiskPointer = get_next_block(current_block.clone())?;\n        // Go there\n        // We always re-open the disk to get the freshest allocation table.\n        current_disk = pointer_to_next_block.disk;\n\n        let reader: RawBlock = CachedBlockIO::read_block(pointer_to_next_block)?;\n        current_block = InodeBlock::from_block(&reader);\n        current_block_number = pointer_to_next_block.block;\n        // start over!\n        continue;\n    }\n\n    // The inode has now been added to the block, we must write this to disk before continuing.\n    let block_to_write: RawBlock = current_block.to_block();\n    // We are updating, because how would we be writing back to a block that was not allocated when we read it?\n    CachedBlockIO::update_block(&block_to_write)?;\n\n    // All done! Now we can return where that inode eventually ended up\n    let pointer: DiskPointer = DiskPointer {\n        disk: current_disk,\n        block: current_block_number,\n    };\n\n    let new_location: InodeLocation = InodeLocation::new(pointer, inode_offset);\n\n    Ok(new_location)\n}\n\nfn get_next_block(current_block: InodeBlock) -> Result<DiskPointer, DriveError> {\n    // Extract the pointer\n    if let Some(ok) = current_block.next_block() {\n        // Sweet, it exists already.\n        return Ok(ok);\n    }\n\n    // Time to make a new inode block\n    // Why is there a second function for that? idk i felt like it\n    // This writes the new block.\n    let new_block_location: DiskPointer = make_new_inode_block()?;\n\n    // Now we just need to update the block we were called on.\n    let mut the_cooler_inode = current_block;\n    the_cooler_inode.new_destination(new_block_location);\n    let please_let_me_hit = the_cooler_inode.to_block();\n\n    // Update that block G\n    CachedBlockIO::update_block(&please_let_me_hit)?;\n\n    // return the pointer to the next block.\n    Ok(new_block_location)\n}\n\n/// We need a new inode block, we will reach upwards and get a new block made for us.\nfn make_new_inode_block() -> Result<DiskPointer, DriveError> {\n    // Ask the pool for a new block pwease\n    // No need for crc since we'll be overwriting it immediately.\n    let ask_nicely = Pool::find_and_allocate_pool_blocks(1, false)?;\n    // And you shall receive.\n    let new_block_location = ask_nicely[0];\n\n    // New block to throw there\n    let new_block: InodeBlock = InodeBlock::new(new_block_location);\n    let but_raw: RawBlock = new_block.to_block();\n\n    // Write it Ralph!\n    // I'm gonna write it!\n    // New block, so standard write.\n    CachedBlockIO::update_block(&but_raw)?;\n\n    // Now throw it back, I mean the pointer\n    Ok(new_block_location)\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/io/mod.rs",
    "content": "pub(crate) mod directory;\nmod file;\nmod inode;\n"
  },
  {
    "path": "src/pool/disk/standard_disk/block/mod.rs",
    "content": "pub mod directory;\nmod file_extents;\npub mod header;\npub mod inode;\npub mod io;\n"
  },
  {
    "path": "src/pool/disk/standard_disk/mod.rs",
    "content": "pub mod block;\nmod standard_disk_methods;\npub mod standard_disk_struct;\n"
  },
  {
    "path": "src/pool/disk/standard_disk/standard_disk_methods.rs",
    "content": "// Imports\nuse std::fs::File;\n\nuse log::debug;\n\nuse crate::{\n    error_types::drive::DriveError,\n    pool::{\n        disk::{\n            drive_struct::{\n                DiskBootstrap,\n                DiskType,\n                FloppyDrive\n            },\n            generic::{\n                block::{\n                    allocate::block_allocation::BlockAllocation,\n                    block_structs::RawBlock,\n                },\n                disk_trait::GenericDiskMethods,\n                generic_structs::pointer_struct::DiskPointer,\n                io::{\n                    cache::cache_io::CachedBlockIO,\n                    read::{\n                        read_block_direct,\n                        read_multiple_blocks_direct\n                    },\n                    write::{\n                        write_block_direct,\n                        write_large_direct\n                    }\n                },\n            },\n            standard_disk::{\n                block::{\n                    directory::directory_struct::DirectoryBlock,\n                    header::header_struct::{\n                        StandardDiskHeader,\n                        StandardHeaderFlags\n                    },\n                    inode::inode_struct::{\n                        Inode,\n                        InodeBlock,\n                        InodeDirectory,\n                        InodeFlags,\n                        InodeTimestamp,\n                    },\n                },\n                standard_disk_struct::StandardDisk,\n            },\n        },\n        pool_actions::pool_struct::{\n            Pool,\n            GLOBAL_POOL\n        },\n    }\n};\n\n// Implementations\n\n// !! Only numbered options should be public! !!\n\nimpl DiskBootstrap for StandardDisk {\n    fn bootstrap(file: File, disk_number: u16) -> Result<StandardDisk, DriveError> {\n        debug!(\"Boostrapping a standard disk...\");\n\n        // Update how many blocks are free in the pool\n        // New standard disks have only the header allocated.\n        // 2880 - 1 = 2879\n        // But, the disk setup process will automatically decrement this count for us.\n        {\n            let arc = GLOBAL_POOL.get().expect(\"Shouldn't be allocating disks without a pool\");\n            let mut pool = if let Ok(innards) = arc.try_lock() {\n                innards\n            } else {\n                // Poisoned! We can't make new disks while poisoned.\n                panic!(\"Attempted to bootstrap a standard disk while pool is poisoned!\");\n            };\n\n            pool.header.pool_standard_blocks_free += 2880;\n        }\n\n        // Make the disk\n        debug!(\"Running create...\");\n        let mut disk = create(file, disk_number)?;\n        // Now that we have a disk, we can use the safe IO.\n\n        // if this is disk 1 then we need to add:\n        // Inode block\n        // Directory block\n        // the root directory to that block\n        if disk_number != 1 {\n            // Dont need to do anything.\n            debug!(\"Done bootstrapping standard disk.\");\n            return Ok(disk);\n        }\n        debug!(\"This is the origin standard disk, doing a bit more...\");\n\n        // the new origin disk needs to have blocks 1 and 2 allocated as well for the first inode/directory blocks.\n        let _ = disk.allocate_blocks(&vec![1,2])?;\n        // Ignoring resulting value, since it will always be 2.\n        // Which means we also need to update the pool block count again.\n\n\n        // Write the inode block\n        debug!(\"Writing inode block...\");\n        let inode_block_origin: DiskPointer = DiskPointer {\n            disk: disk_number,\n            block: 1,\n        };\n        let inode_block = InodeBlock::new(inode_block_origin);\n        let inode_writer = inode_block.to_block();\n        disk.unchecked_write_block(&inode_writer)?;\n        \n        // Create the directory block\n        \n        // Write that to the disk. It goes in block 2.\n        debug!(\"Writing root directory block...\");\n        let directory_block_origin: DiskPointer = DiskPointer {\n            disk: disk_number,\n            block: 2,\n        };\n        let directory_block: DirectoryBlock = DirectoryBlock::new(directory_block_origin);\n        let the_directory_block: RawBlock = directory_block.to_block();\n        disk.unchecked_write_block(&the_directory_block)?;\n        \n        // Now we need to manually add the inode that points to it. Because the inode at the 0 index\n        // of block 1 is the inode that points to the root directory\n        \n        // Add the root inode\n        debug!(\"Writing root directory inode...\");\n        let pointer_to_dat_mf: DiskPointer = DiskPointer {\n            disk: 1,\n            block: 2, // The root directory is at block 2.\n        };\n\n        let root_directory_inode = InodeDirectory::from_disk_pointer(pointer_to_dat_mf);\n        let right_now = InodeTimestamp::now();\n        let the_actual_inode: Inode = Inode {\n            flags: InodeFlags::MarkerBit, // Not a file, so only the marker.\n            file: None,\n            directory: Some(root_directory_inode),\n            created: right_now,\n            modified: right_now,\n        };\n\n        // Adding inodes automatically makes more room if needed, and since this is disk 1, this is actually\n        // the first inode we EVER create, so not having room is preposterous!\n        let inode_result = Pool::add_inode(the_actual_inode).expect(\"Brand new disk is full????\");\n\n        // Make sure that actually ended up at the right spot.\n        if inode_result.pointer != inode_block_origin || inode_result.offset != 0 {\n            // Making the inode block is cooked. Too much depends on this being in the correct spot.\n            panic!(\"Inode ended up at the wrong spot during disk creation! Cannot continue!\");\n        }\n        \n        // All done!\n        debug!(\"Done bootstrapping standard disk.\");\n        // Since we wrote information to it, we need to read in that disk again before returning it\n        #[allow(deprecated)] // We do not use the cache while bootstrapping.\n        let finished_disk: StandardDisk = match FloppyDrive::open(disk_number)? {\n            DiskType::Standard(standard_disk) => standard_disk,\n            _ => {\n                // User managed to swap out disk 1 at the exact moment we finished putting it together. Shame on them.\n                panic!(\"Disk 1 was replaced in the drive during bootstrapping re-load.\");\n            },\n        };\n        Ok(finished_disk)\n    }\n\n    fn from_header(block: RawBlock, file: File) -> Self {\n        // load in the header.\n        // We assume the caller has passed in the freshest version of the header.\n        let header: StandardDiskHeader =\n            StandardDiskHeader::from_block(&block);\n            \n        StandardDisk {\n            number: header.disk_number,\n            disk_file: file,\n            header,\n        }\n\n    }\n}\n\n// This disk has block level allocations\nimpl BlockAllocation for StandardDisk {\n    fn get_allocation_table(&self) -> &[u8] {\n        &self.header.block_usage_map\n    }\n\n    fn set_allocation_table(&mut self, new_table: &[u8]) -> Result<(), DriveError> {\n        // Gotta use type annotations here, inline methods on the result dont seem to work on\n        // declaration/assignments.\n        let cast = if let Ok(casted) = TryInto::<[u8; 360]>::try_into(new_table) {\n            casted\n        } else {\n            // Incoming bytes were the wrong length.\n            panic!(\"Allocation table length is wrong!\");\n        };\n\n        self.header.block_usage_map = cast;\n        self.flush()\n    }\n}\n\n/// Ocasionally, we need to create fake headers during disk loading.\nimpl StandardDiskHeader {\n    fn spoof() -> Self {\n        Self {\n            flags: StandardHeaderFlags::from_bits_retain(0b00100000), // Gotta set that marker bit.\n            disk_number: u16::MAX,\n            block_usage_map: [1u8; 360],\n        }\n    }\n}\n\n//\n// Public functions\n//\n\n/// Initializes a disk by writing header data.\n/// Returns the newly created disk.\n///\n/// This will only work on a disk that is blank / header-less.\n/// This will create a disk of any disk number, it is up to the caller to ensure that\n/// duplicate disks are not created, and to track the creation of this new disk.\nfn create(file: File, disk_number: u16) -> Result<StandardDisk, DriveError> {\n    debug!(\"Creating new standard disk {disk_number}\");\n    debug!(\"Creating spoofed disk...\");\n    // Spoof the header, since we're about to give it a new one.\n    let mut disk: StandardDisk = StandardDisk {\n        number: disk_number,\n        header: StandardDiskHeader::spoof(),\n        disk_file: file,\n    };\n\n    // Now give it some head    er\n    // This function checks if the disk is blank for us.\n    debug!(\"Initializing the disk from spoof...\");\n    initialize_numbered(&mut disk, disk_number)?;\n\n    // done\n    debug!(\"Done creating disk.\");\n    Ok(disk)\n}\n\n//\n// Private functions\n//\n\n/// Initialize a normal disk for usage. (NOT DATA, NOT POOL)\n/// Expects a disk without a header.\n/// Will wipe the rest of the disk,\n///\n/// Errors if provided with a disk that has a header.\nfn initialize_numbered(disk: &mut StandardDisk, disk_number: u16) -> Result<(), DriveError> {\n    debug!(\"Initializing a new standard disk...\");\n    // A new, fresh disk!\n\n    // Time to write in all of the header data.\n    // Construct the new header\n\n    // New disks have no flags set, besides the required marker bit\n    let mut flags: StandardHeaderFlags = StandardHeaderFlags::empty();\n    flags.insert(StandardHeaderFlags::Marker);\n\n    // New disks do have a few pre-allocated blocks, namely the header and the first inode block\n    // But they will be allocated during the creation process.\n    let mut block_usage_map: [u8; 360] = [0u8; 360];\n    // We must mark the first block used, because we cant run the allocated without being able to update\n    // the header block (which needs to be allocated for the update)\n    block_usage_map[0] = 0b10000000;\n\n    let header = StandardDiskHeader {\n        flags,\n        disk_number,\n        block_usage_map,\n    };\n\n    // Now serialize that, and write it\n    let header_block = &header.to_block();\n\n    // Update the header on the provided disk, since it's currently spoofed.\n    disk.header = header;\n\n    // Since this is a brand new disk without proper header information finalized, we have to do a direct write here\n\n    debug!(\"Writing header...\");\n    disk.unchecked_write_block(header_block)?;\n    debug!(\"Header written.\");\n\n    // All done!\n    debug!(\"Done initializing.\");\n    Ok(())\n}\n\n// Generic disk operations\nimpl GenericDiskMethods for StandardDisk {\n    #[doc = \" Read a block\"]\n    #[doc = \" Cannot bypass CRC.\"]\n    fn unchecked_read_block(&self, block_number: u16) -> Result<RawBlock, DriveError> {\n        // This is the first call, we have not recursed.\n        read_block_direct(&self.disk_file, self.number, block_number, false, false)\n    }\n\n    #[doc = \" Write a block\"]\n    fn unchecked_write_block(&mut self, block: &RawBlock) -> Result<(), DriveError> {\n        // This is the first call, we have not recursed.\n        write_block_direct(&self.disk_file, block, false)\n    }\n\n    #[doc = \" Write chunked data, starting at a block.\"]\n    fn unchecked_write_large(&mut self, data:Vec<u8>, start_block:DiskPointer) -> Result<(), DriveError> {\n        write_large_direct(&self.disk_file, &data, start_block)\n    }\n\n    #[doc = \" Get the inner file used for IO operations\"]\n    fn disk_file(self) -> File {\n        self.disk_file\n    }\n\n    #[doc = \" Get the number of the floppy disk.\"]\n    fn get_disk_number(&self) -> u16 {\n        self.number\n    }\n\n    #[doc = \" Set the number of this disk.\"]\n    fn set_disk_number(&mut self, disk_number: u16) {\n        self.number = disk_number\n    }\n\n    #[doc = \" Get the inner file used for write operations\"]\n    fn disk_file_mut(&mut self) -> &mut File {\n        &mut self.disk_file\n    }\n\n    #[doc = \" Sync all in-memory information to disk\"]\n    #[doc = \" Headers and such.\"]\n    fn flush(&mut self) -> Result<(), DriveError> {\n        // Not really to disk, but if nobody is looking...\n        CachedBlockIO::update_block(&self.header.to_block())\n    }\n    \n    #[doc = \" Read multiple blocks\"]\n    #[doc = \" Does not check CRC!\"]\n    fn unchecked_read_multiple_blocks(&self, block_number: u16, num_block_to_read: u16) -> Result<Vec<RawBlock>, DriveError> {\n        // This is the first call, we have not recursed.\n        read_multiple_blocks_direct(&self.disk_file, self.number, block_number, num_block_to_read, false)\n    }\n\n}\n"
  },
  {
    "path": "src/pool/disk/standard_disk/standard_disk_struct.rs",
    "content": "// Information about a standard disk\n\n// Imports\n\nuse super::block::header::header_struct::StandardDiskHeader;\n\n// Structs, Enums, Flags\n#[derive(Debug)]\npub struct StandardDisk {\n    /// Which disk is this?\n    pub number: u16,\n    /// The disk header\n    pub header: StandardDiskHeader,\n    /// The file that refers to this disk\n    pub(in super::super) disk_file: std::fs::File,\n}\n"
  },
  {
    "path": "src/pool/disk/unknown_disk/mod.rs",
    "content": "pub mod unknown_disk_methods;\npub mod unknown_disk_struct;\n"
  },
  {
    "path": "src/pool/disk/unknown_disk/unknown_disk_methods.rs",
    "content": "use std::fs::File;\n\nuse crate::{\n    error_types::drive::DriveError,\n    pool::disk::{\n        generic::{\n            block::{\n                allocate::block_allocation::BlockAllocation,\n                block_structs::RawBlock\n            },\n            disk_trait::GenericDiskMethods,\n            generic_structs::pointer_struct::DiskPointer,\n            io::write::write_block_direct\n        },\n        unknown_disk::unknown_disk_struct::UnknownDisk\n    }\n};\n\nimpl GenericDiskMethods for UnknownDisk {\n    #[doc = \" Read a block\"]\n    #[doc = \" Cannot bypass CRC.\"]\n    fn unchecked_read_block(&self, _block_number: u16) -> Result<RawBlock, DriveError> {\n        // We cant read from generic disks.\n        panic!(\"Attempted to read blocks from a disk we know nothing about, we cannot do that.\");\n    }\n    \n    #[doc = \" Write a block\"]\n    fn unchecked_write_block(&mut self, block: &RawBlock) -> Result<(), DriveError> {\n        // This is the first call, we have not recursed.\n        write_block_direct(&self.disk_file, block, false)\n    }\n    \n    #[doc = \" Get the inner file used for IO operations\"]\n    fn disk_file(self) -> File {\n        self.disk_file\n    }\n    \n    #[doc = \" Get the number of the floppy disk.\"]\n    fn get_disk_number(&self) -> u16 {\n        // Unknown disks have no number.\n        panic!(\"Attempted to get the disk number of a disk we know nothing about! Not allowed!\");\n    }\n    \n    #[doc = \" Set the number of this disk.\"]\n    fn set_disk_number(&mut self, _disk_number: u16) {\n        // You cannot set the disk number of an unknown disk.\n        panic!(\"Attempted to set the disk number of a disk we know nothing about! Not allowed!\");\n\n    }\n\n    #[doc = \" Get the inner file used for write operations\"]\n    fn disk_file_mut(&mut self) -> &mut File {\n        &mut self.disk_file\n    }\n\n    #[doc = \" Sync all in-memory information to disk\"]\n    fn flush(&mut self) -> Result<(), DriveError> {\n        // There is no in-memory information for this disk.\n        // So just don't do anything.\n        Ok(())\n    }\n    \n    #[doc = \" Write chunked data, starting at a block.\"]\n    fn unchecked_write_large(&mut self, _data: Vec<u8>, _start_block: DiskPointer) -> Result<(), DriveError> {\n        panic!(\"Cannot do large writes to unknown disks!\");\n    }\n    \n    #[doc = \" Read multiple blocks\"]\n    #[doc = \" Does not check CRC!\"]\n    fn unchecked_read_multiple_blocks(&self, _block_number: u16, _num_block_to_read: u16) -> Result<Vec<RawBlock>,DriveError> {\n        panic!(\"Cannot do large reads from unknown disks!\");\n    }\n}\n\nimpl UnknownDisk {\n    pub fn new(file: File) -> Self {\n        Self { disk_file: file }\n    }\n}\n\nimpl BlockAllocation for UnknownDisk {\n    #[doc = \" Get the block allocation table\"]\n    fn get_allocation_table(&self) ->  &[u8] {\n        panic!(\"Unknown disks do not support allocations.\");\n\n    }\n    \n    #[doc = \" Update and flush the allocation table to disk.\"]\n    fn set_allocation_table(&mut self, _new_table: &[u8]) -> Result<(), DriveError> {\n        panic!(\"Unknown disks do not support allocations.\");\n\n    }\n}"
  },
  {
    "path": "src/pool/disk/unknown_disk/unknown_disk_struct.rs",
    "content": "// Sometimes we know nothing about a disk, but we still need a type for it\n// so we can satisfy type constraints on DiskType\n\n#[derive(Debug)]\npub struct UnknownDisk {\n    /// Every disk needs a file\n    pub(in super::super) disk_file: std::fs::File,\n}\n"
  },
  {
    "path": "src/pool/io/allocate.rs",
    "content": "// Pool level block allocations\n\n\nuse log::{debug, error};\n\nuse crate::{\n    error_types::drive::DriveError, pool::{\n        disk::{\n            generic::{\n                block::{\n                    allocate::block_allocation::BlockAllocation,\n                    block_structs::RawBlock,\n                    crc::add_crc_to_block\n                },\n                generic_structs::pointer_struct::DiskPointer,\n                io::cache::{\n                    cache_io::CachedBlockIO,\n                    cached_allocation::CachedAllocationDisk\n                }\n            },\n            standard_disk::standard_disk_struct::StandardDisk,\n        },\n        pool_actions::pool_struct::{\n            Pool,\n            GLOBAL_POOL\n        },\n    },\n    tui::{\n        notify::NotifyTui,\n        tasks::TaskType\n    }\n};\n\n\n// We need to be able to get at the innards of the Global Pool quite often.\n// If they dont exist at this stage, we're cooked regardless and must exit.\nmacro_rules! get_pool {\n    () => {\n        if let Ok(innards) = GLOBAL_POOL.get().expect(\"Global pool should be created at this stage!\").try_lock() {\n            innards\n        } else {\n            // Somebody peed in the pool.\n            // Usually we would try to do something about this, but the pool should be shutting down if\n            // poisoning has occurred. Which in that case, we should NOT be allocating new blocks!\n            // No recovery will be attempted.\n            panic!(\"A poisoned pool cannot be allocated against!\");\n        }\n    };\n}\n\n\n\nimpl Pool {\n    /// Finds blocks across the entire pool.\n    /// \n    /// Can only allocate a maximum of 32MB worth of blocks in one go.\n    /// If you are asking for that many, something is 100% wrong.\n    /// Writes should be limited to 1MB so this should never happen.\n    /// \n    /// If this fails, ur kinda cooked ngl, since now a bunch of random blocks have been\n    /// allocated for no reason.\n    /// \n    /// Searches standard disks, yada yada.\n    /// \n    /// This will mark the blocks as allocated.\n    /// \n    /// You can optionally also set the CRC on the new empty blocks, which is useful for\n    /// new file blocks.\n    /// \n    /// Will add new disks if needed.\n    /// \n    /// May swap disks, will not return to where it started.\n    /// \n    /// Returns disk pointers for the newly reserved blocks, or a disk error.\n    pub fn find_and_allocate_pool_blocks(blocks: u16, add_crc: bool) -> Result<Vec<DiskPointer>, DriveError> {\n        // This is just an abstraction to force a different function name, even though\n        // the function it calls is the same as find_free_pool_blocks()\n        go_find_free_pool_blocks(blocks, add_crc)\n    }\n\n    /// Frees a block from a disk in the pool.\n    /// \n    /// It's not required, but you should sort the order of the blocks to\n    /// reduce drive seeking.\n    /// \n    /// All blocks must come from same disk.\n    /// \n    /// Returns how many blocks were freed.\n    /// \n    /// Will destroy any data currently in that block.\n    pub fn free_pool_block_from_disk(blocks: &[DiskPointer]) -> Result<u16, DriveError> {\n        go_deallocate_pool_block(blocks)\n    }\n}\n\nfn go_find_free_pool_blocks(blocks: u16, add_crc: bool) -> Result<Vec<DiskPointer>, DriveError> {\n    let handle = NotifyTui::start_task(TaskType::PoolAllocateBlocks(blocks), blocks.into());\n    debug!(\"Attempting to allocate {blocks} blocks across the pool...\");\n\n    // Get where we're most likely to find the next free block, and hold onto the highest\n    // known disk as well, in-case we need to grow the pool.\n    // Scoped to drop the mutex.\n    let (probable_disk, highest_disk) = {\n        let header = get_pool!().header;\n        (header.disk_with_next_free_block, header.highest_known_disk)\n    };\n\n    // First we open up the disk with the most recent successful pool allocation\n    let mut disk_to_check = probable_disk;\n    let mut new_highest_disk = highest_disk; // We may create new disks during the process.\n\n    // We will assume that we will be getting as many blocks as we need, so we can pre-allocate the\n    // vec.\n    let mut free_blocks: Vec<DiskPointer> = Vec::with_capacity(blocks.into());\n\n    // Make sure the highest disks and disk to check are valid.\n    // Getting here is a bad sign, allowing this continue is a worse one.\n    // We will just give up if such an event transpires.\n    if disk_to_check == u16::MAX || new_highest_disk < 1 {\n        // Either we're trying to allocate on an invalid disk, or the pool\n        // doesnt have enough disks?\n        error!(\"Tried to allocate on u16::MAX or we didn't have enough disks!\");\n        error!(\"We cannot continue!\");\n        panic!(\"Impossible pool-level allocation!\");\n    }\n\n    // Now we loop until we find enough free blocks.\n    loop {\n        // Since we use a mix of real and fake disks in here, we need to have a type that we can use for our allocation\n        // methods. So we will box it up. Yes this is kinda evil.\n        let mut disk: Box<dyn BlockAllocation>;\n        // Check if the disk we are about to load is out of range\n        if disk_to_check > new_highest_disk {\n            debug!(\"Ran out of room, creating new disk...\");\n            // We need to make this disk before trying to allocate blocks on it.\n            let new_disk: StandardDisk = Pool::new_disk::<StandardDisk>()?;\n            disk = Box::new(new_disk);\n            // increment the highest known disk\n            new_highest_disk += 1;\n        } else {\n            // We are loading a pre-existing disk, hit up the cache for it.\n            let new_disk: CachedAllocationDisk = CachedAllocationDisk::open(disk_to_check)?;\n            disk = Box::new(new_disk);\n        };\n\n        // Check if this disk has enough room.\n        // we will grab all we can.\n        match disk.find_free_blocks(blocks - free_blocks.len() as u16) {\n            Ok(ok) => {\n                debug!(\"Found the last {} blocks we needed on disk {}!\", ok.len(), disk_to_check);\n                // We were able to allocate all of the blocks we asked for!\n                // We're done!\n                free_blocks.append(&mut block_indexes_to_pointers(&ok, disk_to_check));\n\n                // Allocate those blocks.\n                let _ = disk.allocate_blocks(&ok)?;\n\n                // Now we drop the disk to make it flush the new allocation table.\n                drop(disk);\n\n                // We also need to update the global pool to say these were marked as used, otherwise we would never know.\n                // Trust me I found out the hard way.\n                debug!(\"Updating the pool's free block count...\");\n                {\n                    let mut header = get_pool!().header;\n                    header.pool_standard_blocks_free -= ok.len() as u32;\n                }\n\n                // Add crc to blocks if requested.\n                if add_crc {\n                    debug!(\"CRC requested, adding...\");\n                    write_empty_crc(&ok, disk_to_check)?;\n                }\n\n                NotifyTui::complete_multiple_task_steps(&handle, ok.len() as u64);\n\n                break;\n            }\n            Err(amount) => {\n                // There wasn't enough blocks free on this disk, but we can allocate at least `amount`\n                debug!(\"Still need more blocks, disk {disk_to_check} only had {amount} blocks free.\");\n                // Bail early if there's zero blocks\n                if amount == 0 {\n                    disk_to_check += 1;\n                    continue;\n                }\n\n                let blockie_doos = disk.find_free_blocks(amount).expect(\"Disk should not change its mind about how many blocks it has free\");\n\n\n                free_blocks.append(&mut block_indexes_to_pointers(&blockie_doos, disk_to_check));\n\n                // Allocate those blocks if needed.\n                \n                let _ = disk.allocate_blocks(&blockie_doos)?;\n                // Now we drop the disk to make it flush the new allocation table.\n                drop(disk);\n\n                // We also need to update the global pool to say these were marked as used, otherwise we would never know.\n                // Trust me I found out the hard way.\n                debug!(\"Updating the pool's free block count...\");\n                {\n                    let mut header = get_pool!().header;\n                    header.pool_standard_blocks_free -= blockie_doos.len() as u32;\n                }\n                \n                // Add crc to blocks if requested\n                if add_crc {\n                    debug!(\"CRC requested, adding...\");\n                    write_empty_crc(&blockie_doos, disk_to_check)?;\n                }\n\n                NotifyTui::complete_multiple_task_steps(&handle, blockie_doos.len() as u64);\n\n                // Waiter! Waiter! More disks please!\n                disk_to_check += 1;\n                continue;\n            }\n        }\n    }\n\n    // Now that we have allocated, the most probable disk is the last disk we got blocks from.\n    {\n        let header = &mut get_pool!().header;\n        header.disk_with_next_free_block = disk_to_check;\n    }\n\n    // We will sort the resulting vector to make to group the disks together, this will\n    // reduce swapping.\n\n    free_blocks.sort_unstable_by_key(|pointer| (pointer.disk, pointer.block));\n\n    debug!(\"Allocation complete.\");\n    NotifyTui::finish_task(handle);\n    Ok(free_blocks)\n}\n\n// helper\nfn block_indexes_to_pointers(blocks: &Vec<u16>, disk: u16) -> Vec<DiskPointer> {\n    // We will have as many pointers as we got blocks in.\n    let mut result: Vec<DiskPointer> = Vec::with_capacity(blocks.len());\n    for block in blocks {\n        result.push(DiskPointer { disk, block: *block });\n    }\n    result\n}\n\n/// Sometimes we need a new block that is empty, but we still need to have the crc set.\n/// Assumes block are already marked.\n/// \n/// This method only works on standard disks\nfn write_empty_crc(blocks: &[u16], disk: u16) -> Result<(), DriveError> {\n    debug!(\"Adding crc to {} blocks on disk {disk}...\", blocks.len());\n    let handle = NotifyTui::start_task(TaskType::WriteCRC, blocks.len() as u64);\n    // These new blocks do not have their CRC set, we need to just write empty blocks to them to set the crc.\n    let mut empty_data: [u8; 512] = [0_u8; 512];\n    // CRC that sucker\n    add_crc_to_block(&mut empty_data);\n\n    // Make block to write, must update inside of loop.\n    for block in blocks {\n        let block_origin: DiskPointer = DiskPointer {\n            disk,\n            block: *block,\n        };\n        let empty_raw_block: RawBlock = RawBlock {\n            block_origin,\n            data: empty_data,\n        };\n\n        CachedBlockIO::update_block(&empty_raw_block)?;\n        NotifyTui::complete_task_step(&handle);\n    }\n\n    NotifyTui::finish_task(handle);\n\n    // All of the blocks now have a empty block with a crc on it.\n    Ok(())\n}\n\nfn go_deallocate_pool_block(blocks: &[DiskPointer]) -> Result<u16, DriveError> {\n    debug!(\"Deallocating some blocks from the pool...\");\n    let handle = NotifyTui::start_task(TaskType::DiskDeallocateBlocks(blocks.len() as u16), blocks.len() as u64);\n    // We assume the blocks are pre-sorted to reduce disk seeking, but even\n    // if they aren't, the cache will re-sort them on write.\n\n    // We have to be fed at least one block.\n    if blocks.is_empty() {\n        // Freeing 0 blocks does nothing, so...\n        return Ok(0)\n    }\n\n    // All of the disk numbers must be the same, otherwise this is a mallformed free call.\n    let starter = blocks[0]; // Already checked if the array is empty.\n    if !blocks.iter().all(|pointer| pointer.disk == starter.disk) {\n        // At least one of the blocks is for a different disk, we can't do that.\n        panic!(\"Pool deallocation attempted to free blocks from multiple different disks at once! Not allowed!\");\n    }\n\n    let mut extracted_blocks: Vec<u16> = Vec::with_capacity(blocks.len());\n    for block in blocks {\n        // Hold onto the block number, need it for disk call.\n        extracted_blocks.push(block.block);\n    }\n\n    // Go zero out the blocks on the disk, but all of the IO will be cached. Thus if\n    // we are trying to free blocks on a disk that isn't in the drive right now, we\n    // might need to swap, also this will auto-chunk the writes, which is faster.\n    debug!(\"Zeroing out blocks...\");\n    for block in blocks {\n        let empty: RawBlock = RawBlock {\n            block_origin: *block,\n            data: [0_u8; 512],\n        };\n        // Zero em out with the cache.\n        CachedBlockIO::update_block(&empty)?;\n        NotifyTui::complete_task_step(&handle);\n    }\n    \n    // Now go to and free the blocks from the allocation table.\n    debug!(\"Freeing blocks from the disk header...\");\n    let mut disk: CachedAllocationDisk = CachedAllocationDisk::open(starter.disk)?;\n    let blocks_freed = disk.free_blocks(&extracted_blocks)?;\n\n    // Drop it to flush the updated header to cache.\n    drop(disk);\n\n    // If the current disk in the pool marked with free blocks is higher than the blocks we just freed,\n    // we need to move back the search start for finding new free blocks.\n    //\n    // Then update the free count, since new blocks are available.\n\n    {\n        let header = &mut get_pool!().header;\n        if header.disk_with_next_free_block > starter.disk {\n            // It's higher, we need to move the pool back.\n            header.disk_with_next_free_block = starter.disk;\n        }\n\n        header.pool_standard_blocks_free += blocks_freed as u32;\n    }\n\n    // Return the number of blocks freed.\n    debug!(\"Done. {blocks_freed} pool blocks freed.\");\n    NotifyTui::finish_task(handle);\n    Ok(blocks_freed)\n    \n}"
  },
  {
    "path": "src/pool/io/mod.rs",
    "content": "pub mod allocate;\n"
  },
  {
    "path": "src/pool/mod.rs",
    "content": "pub(crate) mod disk;\npub mod io;\npub mod pool_actions;\n"
  },
  {
    "path": "src/pool/pool_actions/mod.rs",
    "content": "pub mod pool_methods;\npub mod pool_struct;\n"
  },
  {
    "path": "src/pool/pool_actions/pool_methods.rs",
    "content": "// Interacting with the pool\n\n// Imports\n\nuse super::pool_struct::GLOBAL_POOL;\nuse super::pool_struct::Pool;\nuse crate::error_types::drive::DriveError;\nuse crate::pool::disk::blank_disk::blank_disk_struct::BlankDisk;\nuse crate::pool::disk::drive_struct::DiskBootstrap;\nuse crate::pool::disk::drive_struct::FloppyDrive;\nuse crate::pool::disk::generic::block::block_structs::RawBlock;\nuse crate::pool::disk::generic::disk_trait::GenericDiskMethods;\nuse crate::pool::disk::generic::generic_structs::pointer_struct::DiskPointer;\nuse crate::pool::disk::generic::io::cache::cache_io::CachedBlockIO;\nuse crate::pool::disk::pool_disk::block::header::header_struct::PoolDiskHeader;\nuse crate::pool::disk::standard_disk::block::directory::directory_struct::DirectoryBlock;\nuse crate::pool::disk::standard_disk::block::directory::directory_struct::DirectoryItemFlags;\nuse crate::pool::disk::standard_disk::block::directory::directory_struct::DirectoryItem;\nuse crate::pool::disk::standard_disk::block::inode::inode_struct::InodeLocation;\nuse crate::pool::disk::standard_disk::standard_disk_struct::StandardDisk;\nuse crate::tui::notify::NotifyTui;\nuse crate::tui::tasks::TaskType;\nuse log::debug;\nuse log::error;\nuse std::sync::Arc;\nuse std::sync::Mutex;\n\n// Implementations\n\nimpl Pool {\n    /// Flush all info about the pool to the pool disk.\n    pub fn flush() -> Result<(), DriveError> {\n        flush_pool()\n    }\n    /// Read in pool information from disk\n    /// Returns a handle/pointer/whatever\n    pub fn load() -> Arc<Mutex<Pool>> {\n        load()\n    }\n    /// Create a new disk of type and add it to the pool\n    /// Returns that new disk.\n    pub fn new_disk<T: DiskBootstrap>() -> Result<T, DriveError> {\n        add_disk::<T>()\n    }\n    /// Brand new pools need to run some setup functions to get everything in a ready to use state.\n    fn initalize() -> Result<(), DriveError> {\n        initalize_pool()\n    }\n    /// Get the root inode block\n    ///\n    /// May swap disks, but you should be working with enough abstractions to not care.\n    pub fn get_root_directory() -> Result<DirectoryBlock, DriveError> {\n        pool_get_root_directory()\n    }\n    /// Get a DirectoryItem that has details about the root directory.\n    pub fn get_root_directory_item() -> DirectoryItem {\n        pool_get_root_directory_item()\n    }\n}\n\n/// Sync information about the pool to disk\npub(super) fn flush_pool() -> Result<(), DriveError> {\n    debug!(\"Flushing pool info to disk...\");\n    \n    // Grab the pool\n    let global_pool = GLOBAL_POOL\n        .get()\n        .expect(\"The pool has to exist, otherwise we couldn't shut it down.\");\n\n    // Now, since we're flushing info, if we're shutting down after a panic, we need to be able to\n    // flush the pool even if it got poisoned.\n    global_pool.clear_poison();\n\n    let pool_header:PoolDiskHeader = \n        global_pool.try_lock()\n        .expect(\"Single threaded, already cleared poison.\")\n        .header;\n\n    // Now write that back to disk.\n    pool_header.write()?;\n    debug!(\"Pool flushed.\");\n    Ok(())\n}\n\n/// Read in pool information from disk.\n/// Will prompt to make new pools if needed.\n/// Returns a pointer thingy to to the global.\npub(super) fn load() -> Arc<Mutex<Pool>> {\n    debug!(\"Loading in pool information...\");\n    // Read in the header. If this fails, we cannot start the filesystem.\n\n    // We try at most 10 times.\n    let mut header: Option<PoolDiskHeader> = None;\n    for _ in 0..10 {\n        match PoolDiskHeader::read() {\n            Ok(ok) =>{\n                header = Some(ok);\n                break\n            },\n            Err(error) => {\n                // If no disk was inserted, yell at the user and try again\n                if error == DriveError::DriveEmpty {\n                    // Dumbass.\n                    println!(\"Yo. The drive is empty. Actually put in the disk.\");\n                    continue;\n                }\n            }\n        };\n    };\n\n    // Did we get it?\n    let header: PoolDiskHeader = if let Some(read) = header {\n        // All good.\n        read\n    } else {\n        // Failed to load in the disk header.\n        error!(\"Failed to acquire pool header after 10 tries! Giving up!\");\n        error!(\"Fluster has failed to load the pool header.\");\n        error!(\"Fluster will now exit.\");\n        panic!(\"Failed to get pool header!\");\n    };\n    \n\n    let pool = Pool {\n        header,\n    };\n\n    // Wrap it for sharing.\n    let shared_pool = Arc::new(Mutex::new(pool));\n\n    // Set the global static. This will only work the first time.\n    // Since this is only called on fluster startup, this should only ever be called once.\n    // If we somehow hit here again, we'll just exit\n    \n    if GLOBAL_POOL.set(shared_pool.clone()).is_err() {\n        // wow!\n        panic!(\"Somehow we've loaded the pool twice!\");\n    }\n    \n    // All operations after this point use the global pool.\n    \n    // We're the only place that could possibly have access to the pool right now.\n    // So if this lock fails, cooked.\n    \n    let highest_known: u16 = if let Some(global_pool) = GLOBAL_POOL.get() {\n        if let Ok(innards) = global_pool.try_lock() {\n            innards.header.highest_known_disk\n        } else {\n            panic!(\"Locking the global pool immediately after creation failed!\");\n        }\n    } else {\n        // ??????????\n        panic!(\"The global pool does not exist, even though we JUST made it?\");\n    };\n\n    // Check if this is a brand new pool\n    if highest_known == 0 {\n        // This is a brand new pool, we need to initialize it.\n        match Pool::initalize() {\n            Ok(ok) => ok,\n            Err(error) => {\n                // Initializing the pool failed. This cannot continue.\n                error!(\"Failed to load the pool.\");\n                error!(\"Reason: {error}\");\n                error!(\"Fluster will now exit.\");\n                panic!(\"Failed to initalize pool! {error}\");\n            }\n        };\n    };\n\n    // All done\n    shared_pool\n}\n\n/// Set up stuff for a brand new pool\nfn initalize_pool() -> Result<(), DriveError> {\n    debug!(\"Doing first time pool setup...\");\n    // Things a pool needs:\n    // A second disk to start storing inodes on.\n    // A root directory.\n\n    // Lets get that second disk going\n    // First we need to make a standard disk\n    debug!(\"Creating the standard disk (disk 1)...\");\n    let _ = add_disk::<StandardDisk>()?;\n\n    // The root directory is set up on the disk side, so we're done.\n    debug!(\"Finished first time pool setup.\");\n    Ok(())\n}\n\n/// Add a new disk of Type to the pool.\n/// Takes the next available disk number.\n/// Returns the newly created disk of type T.\nfn add_disk<T: DiskBootstrap>() -> Result<T, DriveError> {\n    let handle = NotifyTui::start_task(TaskType::CreateNewDisk, 2);\n    debug!(\n        \"Attempting to add new disk to the pool of type: {}\",\n        std::any::type_name::<T>()\n    );\n\n\n    let highest_known: u16 = GLOBAL_POOL\n        .get()\n        .expect(\"Pool must exist at to add disks to it.\")\n        .try_lock()\n        .expect(\"Cannot add disks to poisoned pool. Also single threaded, so should not block.\")\n        .header\n        .highest_known_disk;\n    let next_open_disk = highest_known + 1;\n\n    // First, we need a blank disk in the drive.\n    // For virtual disk reasons, we still need to pass in the disk number that\n    // we wish to create.\n    debug!(\"Getting a new blank disk...\");\n    // We loop until there is a disk in the drive, just in case.\n    // try 10 times\n    let blank_disk: BlankDisk;\n    let mut tries: u8 = 0;\n    loop {\n        if let Ok(disk) = FloppyDrive::get_blank_disk(next_open_disk){\n            blank_disk = disk;\n            break\n        };\n        // We need that blank mf.\n        if tries == 10 {\n            // shiet\n            error!(\"Couldnt get a blank disk! cooked!\");\n            return Err(DriveError::DriveEmpty); // I mean, good enough.\n        }\n        tries += 1;\n    }\n\n    NotifyTui::complete_task_step(&handle);\n    \n    // Now we need to create a disk to put in there from the supplied generic\n    debug!(\"Bootstrapping the new disk...\");\n    let bootstrapped = T::bootstrap(blank_disk.disk_file(), next_open_disk)?;\n\n    NotifyTui::complete_task_step(&handle);\n    \n    // The disk has now bootstrapped itself, we are done here.\n    // We already locked earlier, so this can't be poisoned, unless maybe making the disks also panicked?\n    if let Ok(mut inner) = GLOBAL_POOL.get().expect(\"Pool has to be set up before we can make disks.\").try_lock() {\n        inner.header.highest_known_disk += 1;\n    } else {\n        // Poisoned again! We're probably in really bad shape. Just give up.\n        // In theory this cant even get poisoned at this point due to bootstrapping happening\n        // in the same thread but whatever, compiler doesn't know ig.\n        panic!(\"Poisoned on disk bootstrapping!\");\n    }\n    \n\n    debug!(\"Done adding new disk.\");\n    NotifyTui::finish_task(handle);\n    Ok(bootstrapped)\n}\n\n/// Grabs the root inode block\nfn pool_get_root_directory() -> Result<DirectoryBlock, DriveError> {\n    // Root directory should always be at disk 1 block 2. We just assume that to be the case.\n    // Why do we have a root inode that points to the root directory when its always in a static location?\n    // Beats me, I forgot why I did that.\n\n    let root_pointer: DiskPointer = DiskPointer {\n        disk: 1,\n        block: 2,\n    };\n\n    // Get the root directory block\n    let block_reader: RawBlock = CachedBlockIO::read_block(root_pointer)?;\n    let block = DirectoryBlock::from_block(&block_reader);\n\n    Ok(block)\n}\n\n/// Grabs the root inode location, duh\nfn pool_get_root_inode_location() -> InodeLocation {\n    let pointer = DiskPointer {\n        disk: 1,\n        block: 1,\n    };\n    InodeLocation::new(pointer, 0)\n}\n\n/// Constructs a directory item with info about the root.\nfn pool_get_root_directory_item() -> DirectoryItem {\n    // The name of the root directory entry is always the delimiter.\n    static DELIMITER: char = std::path::MAIN_SEPARATOR;\n    DirectoryItem {\n        flags: DirectoryItemFlags::IsDirectory,\n        name_length: 0,\n        name: DELIMITER.into(),\n        location: pool_get_root_inode_location(),\n    }\n}"
  },
  {
    "path": "src/pool/pool_actions/pool_struct.rs",
    "content": "// Did you know, if lightning struct a pool, everyone dies?\n// Imports\n\nuse crate::pool::disk::pool_disk::block::header::header_struct::PoolDiskHeader;\n\nuse lazy_static::lazy_static;\nuse once_cell::sync::OnceCell;\nuse std::sync::{Arc, Mutex};\n\n// The global access to the pool.\n// It was either have a globally accessible pool, or put a reference to the pool in every method... No thanks.\n// Know a cleaner way? Make a pull request :D\n\n// This is done with a OnceCell so I dont have to spoof a fake pool into here before actually loading one up.\n\nlazy_static! {\n    pub(crate) static ref GLOBAL_POOL: OnceCell<Arc<Mutex<Pool>>> = OnceCell::new();\n}\n\n// Structs, Enums, Flags\n\n// All of the information we need about a pool to do our job.\n#[derive(Debug)]\npub struct Pool {\n    pub(crate) header: PoolDiskHeader,\n}"
  },
  {
    "path": "src/tui/layout.rs",
    "content": "// how da tui looks.\n\nuse std::{\n    time::{\n        Duration,\n        Instant\n    }\n};\n\nuse ratatui::{\n    crossterm,\n    layout::{\n        Alignment,\n        Constraint,\n        Direction,\n        Layout,\n        Rect\n    },\n    style::{\n        Color,\n        Style,\n        Stylize\n    },\n    text::Text,\n    widgets::{\n        Block,\n        Borders,\n        Clear,\n        Gauge,\n        List\n    },\n    Frame\n};\n\nuse tui_logger::{\n    TuiLoggerLevelOutput,\n    TuiLoggerWidget\n};\n\nuse tui_textarea::{\n    Input,\n    Key\n};\n\nuse crate::tui::{\n    prompts::TuiPrompt,\n    state::FlusterTUIState,\n    tasks::TaskInfo\n};\n\n/// The TUI interface of fluster. Call methods on this to update the interface as often as you'd like.\npub struct FlusterTUI<'a> {\n    /// The actual internal state\n    pub(super) state: FlusterTUIState,\n    /// The last time the interface was updated\n    pub(super) last_update: Instant,\n    /// When fluster was first launched.\n    pub(super) started: Instant,\n    /// User prompt, if any.\n    pub(super) user_prompt: Option<TuiPrompt<'a>>,\n}\n\n\n// Drawing time!\nimpl FlusterTUI<'_> {\n    /// Draw the TUI interface for Fluster\n    /// \n    /// Takes in a frame from the terminal.\n    pub fn draw(&mut self, frame: &mut Frame) {\n        // Split the window into sections\n        let layout = Layout::default().margin(0).direction(Direction::Vertical).constraints([\n            // Progress bars\n            Constraint::Ratio(3, 6),\n            // Statistics\n            Constraint::Ratio(2, 6),\n            // Logging\n            Constraint::Ratio(1, 6),\n        ]).split(frame.area());\n\n        // Draw the progress bars if needed.\n        let progress_area = layout[0];\n        if let Some(task) = &self.state.task {\n            // Collect information about the sub tasks if there are any\n            let tasks = task.get_tasks_info();\n\n            // Now we need to put all of the progress bars we'll be creating into a\n            // block, then we will put the block into the layout at the end.\n            let container_block: Block = Block::bordered().title(\"Running tasks:\");\n\n            // Now based on the number of tasks we have, and how much room we get\n            // from layout constraints, we will draw the progress bars.\n\n            // If we have more bars than can be drawn into the space we have, we will\n            // truncate the list from the front, effectively scrolling it upwards.\n\n            // Gauges are 3 characters tall, top border, bar, and bottom border.\n            \n            // Get the amount of vertical space we have inside of our containing block\n            let inner_height = &container_block.inner(progress_area).height;\n            // Now, how many bars will fit?\n            let max_tasks = inner_height / 3;\n\n            // if we cant render any tasks, the window is too small.\n            if max_tasks == 0 {\n                return // just skip rendering entirely, lazy solution.\n            }\n\n            // Since we wouldn't be able to render them anyways, we can discard any tasks\n            // we dont need.\n            // Skip as many as we cant render. Saturating subtraction, because we can\n            // have more room than tasks.\n            let incoming_tasks = tasks.len();\n            let visible_tasks: Vec<TaskInfo> = tasks.into_iter().skip(incoming_tasks.saturating_sub(max_tasks.into())).collect();\n\n            // Render the container block, not sure why this needs to happen now, but im writing practically the entire\n            // tui before testing it so... YOLO\n            frame.render_widget(&container_block, progress_area);\n\n            \n            // Now we need constraints for each of the gauges to render into.\n            let bar_layout = Layout::vertical(\n                vec![Constraint::Length(3); visible_tasks.len()]\n            ).split(container_block.inner(progress_area));\n\n            // Now for each of the task progress bars that are visible, we render\n            // them into their constraints.\n            // Man, UI logic is confusing.\n            // Zipping so i can iterate in pairs\n\n            // Also for fun factor, each bar will have a different color.\n            // We'll cycle through them.\n            // Would do rainbow, but orange is missing!\n\n            let colors: Vec<ratatui::style::Color> = vec![\n                Color::LightRed,\n                Color::LightYellow,\n                Color::LightCyan,\n                Color::LightGreen,\n                Color::LightMagenta\n            ];\n            let mut color_index = 0;\n\n\n            for (task, area) in visible_tasks.iter().zip(bar_layout.iter()) {\n                // Make the gauge\n                let gauge = Gauge::default()\n                    // Surround the gauge with a border, and put the title of the task on it\n                    .block(\n                        Block::default()\n                        .borders(\n                            Borders::ALL\n                        )\n                        .title(task.name())\n                        // Add a note for how long this has been running\n                        .title_bottom(format!(\"{} | Guesstimated time remaining: {}\",task.time_passed(), task.time_remaining()))\n                    )\n                    // Cycling colors\n                    .gauge_style(Style::default().fg(colors[color_index]))\n                    // Add the percentage\n                    .ratio(task.progress());\n\n                // Render the gauge into its area\n                frame.render_widget(gauge, *area);\n\n                // Increment color, looping around\n                color_index = (color_index + 1) % colors.len();\n            }\n        } else {\n            // No tasks to display.\n            let container_block: Block = Block::bordered().title(\"Idle, waiting for tasks...\");\n            frame.render_widget(&container_block, progress_area);\n        }\n        // Done with progress bars\n\n        // Now for the statistics.\n        let statistics_area = layout[1];\n\n        // Fancy box for the statistics\n        let stat_box: Block = Block::bordered().title(\"Statistics:\");\n\n        // We will split the statistics window in two horizontally.\n        // One side is for disk related information, the other is for cache.\n\n        // Get the size of the stat box\n        let stat_box_size = &stat_box.inner(statistics_area);\n\n        // Split it in half\n        let stat_layout = Layout::default().margin(0).direction(Direction::Horizontal).constraints([\n            // Left\n            Constraint::Ratio(1, 2),\n            // Right\n            Constraint::Ratio(1, 2),\n        ]).split(*stat_box_size);\n        let stat_left: Rect = stat_layout[0];\n        let stat_right: Rect = stat_layout[1];\n\n        // Render the box\n        frame.render_widget(&stat_box, statistics_area);\n\n        //\n        // Disk statistics\n        //\n\n        let disk_box: Block = Block::bordered().title(\"Disk:\");\n        \n        // We'll use a list for statistics.\n\n        // Turn all the info into strings.\n        let mut disk_strings: Vec<String> = Vec::with_capacity(3);\n        disk_strings.push(format!(\"Disks swapped: {}\", self.state.disk_swap_count));\n        disk_strings.push(format!(\"Blocks read: {}\", self.state.disk_blocks_read));\n        disk_strings.push(format!(\"Blocks written: {}\", self.state.disk_blocks_written));\n        // Now a newline for spacing\n        disk_strings.push(\"\".to_string());\n        // And the current disk in the drive\n        disk_strings.push(format!(\"Current disk in drive: {}\", self.state.current_disk_in_drive));\n        disk_strings.push(\"\".to_string());\n        // How long fluster has been running\n        let seconds =  self.started.elapsed().as_secs();\n        let minutes = seconds / 60;\n        let hours = minutes / 60;\n        disk_strings.push(format!(\"Uptime: [{:0>2}:{:0>2}:{:0>2}]\", hours, minutes%60, seconds%60));\n\n        // Listify it.\n        // We'll also surround it with our block\n        let disk_list: List = List::new(disk_strings).block(disk_box);\n\n        // Render that into the left side\n        frame.render_widget(disk_list, stat_left);\n\n        //\n        // Disk statistics\n        //\n\n        let cache_box: Block = Block::bordered().title(\"Cache:\");\n        let cache_box_size = cache_box.inner(stat_right);\n        frame.render_widget(&cache_box, stat_right);\n\n        // The cache is a bit more complicated, since we need to throw a gauge in there to\n        // visualize the current cache hit rate.\n\n        // So we split our space again.\n        let cache_split = Layout::default().margin(0).direction(Direction::Vertical).constraints([\n            // 3 lines for each gauge\n            Constraint::Min(3),\n            Constraint::Min(3),\n            // The rest of the room is for other stats.\n            Constraint::Percentage(100),\n        ]).split(cache_box_size);\n        let hit_gauge_space = cache_split[0];\n        let pressure_gauge_space = cache_split[1];\n        let cache_text = cache_split[2];\n\n        // Make the hit gauge\n        let hit_gauge: Gauge = Gauge::default()\n        .block(Block::bordered()\n        .title(\"Cache hit rate:\"))\n        .ratio(self.state.cache_hit_rate);\n        \n        // Render it in\n        frame.render_widget(hit_gauge, hit_gauge_space);\n\n        // Make the pressure gauge\n        let hit_gauge: Gauge = Gauge::default()\n        .block(Block::bordered()\n        .title(\"Cache pressure:\"))\n        .ratio(self.state.cache_pressure);\n        frame.render_widget(hit_gauge, pressure_gauge_space);\n\n        // Now for the boring text.\n\n        let mut cache_strings: Vec<String> = Vec::with_capacity(3);\n\n        cache_strings.push(format!(\"Swaps saved: {}\", self.state.cache_swaps_saved));\n        cache_strings.push(format!(\"Reads cached: {}\", self.state.cache_blocks_read));\n        cache_strings.push(format!(\"Writes cached: {}\", self.state.cache_blocks_written));\n        cache_strings.push(format!(\"Cache flushes: {}\", self.state.cache_flushes));\n        \n        let cache_list: List = List::new(cache_strings);\n\n        // Render that into the remaining stats space\n        frame.render_widget(cache_list, cache_text);\n\n\n        // Now finally, for the logs\n        let logs_area = layout[2];\n\n        // There's a widget for this!\n        let logs = TuiLoggerWidget::default()\n            .block(\n                Block::bordered().title(\"Logs:\")\n            )\n            .style_error(Style::default().red())\n            .style_warn(Style::default().yellow())\n            .style_info(Style::default().white())\n            .style_debug(Style::default().green())\n            .style_trace(Style::default().light_blue())\n            .output_level(Some(TuiLoggerLevelOutput::Abbreviated));\n\n        // Render it in!\n        frame.render_widget(logs, logs_area);\n\n        // Finally, handle pop-ups if needed.\n        pop_up_handler(frame, &mut self.user_prompt);\n\n        // Update the render time\n        self.last_update = Instant::now();\n\n        // Done!\n    }\n}\n\n// Display pop-ups and prompt for input.\nfn pop_up_handler(frame: &mut Frame, incoming_pop_up: &mut Option<TuiPrompt>) {\n\n    // Grab the popup if it exists.\n    let pop_up = if let Some(up) = incoming_pop_up {\n        up\n    } else {\n        // No pop up, nothing to do.\n        return\n    };\n\n    // We'll be putting a box in the middle of the screen.\n    // Annoyingly we have to create a grid then just pull the middle out\n    let row  = Layout::default().direction(Direction::Vertical).constraints([\n        Constraint::Ratio(1, 10),\n        Constraint::Ratio(8, 10),\n        Constraint::Ratio(1, 10),\n    ]).split(frame.area())[1];\n    let popup_layout = Layout::default().direction(Direction::Horizontal).constraints([\n        Constraint::Ratio(1, 10),\n        Constraint::Ratio(8, 10),\n        Constraint::Ratio(1, 10),\n    ]).split(row)[1];\n    // Darken everything by dimming everything\n    let frame_size = frame.area();\n    frame.buffer_mut().set_style(frame_size, Style::new().dim());\n\n    // Clear out the area that the pop-up is about to draw into\n    frame.render_widget(Clear, popup_layout);\n\n    // Now make the prompting window.\n    let pop_up_block = Block::bordered()\n        .title(pop_up.title.clone())\n        .title_alignment(Alignment::Center)\n        // Blinking!\n        .border_style(if pop_up.flash {Style::new().rapid_blink().red().on_black()} else {Style::new().white().on_black()})\n        .border_set(ratatui::symbols::border::FULL)\n        // Make the inside of the pop-up white on cyan.\n        .style(Style::new().on_cyan().white());\n\n    // Get the side of the inside of that\n    let popup_inside = pop_up_block.inner(popup_layout);\n\n    // Render the pop up window\n    frame.render_widget(pop_up_block, popup_layout);\n\n    // Now for the inside of the window.\n    // Split into 2 parts, the top for the message, and the bottom for the input.\n\n    let top_bottom = Layout::default().direction(Direction::Vertical).constraints([\n        Constraint::Ratio(7, 10),\n        Constraint::Ratio(3, 10),\n    ]).split(popup_inside);\n    let top = top_bottom[0];\n    let bottom = top_bottom[1];\n\n    // Assemble the top half of the window, which is just text.\n    let text = Text::from(pop_up.content.clone()).centered();\n\n    // Render it\n    frame.render_widget(text, top);\n\n\n    // And finally, for the part the user interacts with.\n    // If the caller wants a string back, we'll return it. Otherwise we'll just discard it.\n    \n    // Render either the text box, or the continue prompt.\n    // If in manual mode, dont add anything.\n    if pop_up.expects_string {\n        frame.render_widget(&pop_up.text_area, bottom);\n    } else if !pop_up.manual_close {\n        let prompt = Text::from(\"Press enter to continue.\").centered();\n        frame.render_widget(prompt, bottom);\n    } else {\n        // Manual mode, widget\n    }\n\n    // Now we check for input, if it's enter, we will return the string if needed, and\n    // remove the prompt.\n\n    // But we don't block here, we just see if a key has been pressed.\n    // If the polling fails, we just move on, and hope it gets hit next time.\n    let event_waiting = crossterm::event::poll(Duration::from_millis(0)).unwrap_or(false);\n    if !event_waiting {\n        // No events, so we're done.\n        return\n    }\n\n    // Handle the event.\n    let read = if let Ok(event) = crossterm::event::read() {\n        event\n    } else {\n        // Tried to read, got nothing. Hopefully we'll catch this on the next time around.\n        return\n    };\n\n    match read.into() {\n        Input {\n            key: Key::Esc | Key::Enter,\n            ..\n        } => {\n            // The user has exited the prompt, we're done!\n            // Unless the caller is manually handling things.\n            if pop_up.manual_close {\n                // Those keys have no power here.\n                return\n            }\n            // To respond, we need to pull out the TuiPrompt and swap a None into its place.\n            // Higher up we already checked that the pop-up exists, so we can safely extract it.\n            let extracted = incoming_pop_up.take().expect(\"Guarded, already checked that the pop-up exists.\");\n\n            \n            // We always send the string back, caller will toss it if they dont need it.\n            // It would be really strange for the caller to have died, since they should just be blocking until hearing a result, so\n            // if they dont eat the result, there's not really much we can do.\n            let _ = extracted.callback.send(extracted.text_area.into_lines().concat());\n        },\n        input => {\n            // User typed\n            // This still updates the invisible text area, even if we\n            // dont expect input.\n            let _ = &pop_up.text_area.input(input);\n        }\n    }    \n}"
  },
  {
    "path": "src/tui/mod.rs",
    "content": "pub(crate) mod layout;\npub(crate) mod state;\npub(crate) mod tui_struct;\npub mod notify;\npub(crate) mod tasks;\npub(crate) mod prompts;"
  },
  {
    "path": "src/tui/notify.rs",
    "content": "// Notify the TUI about changes in Fluster!\n// This is the only place we lock the TUI state.\n\nuse std::sync::Mutex;\n\nuse lazy_static::lazy_static;\nuse log::error;\n\nuse crate::{filesystem::filesystem_struct::USE_TUI, tui::{layout::FlusterTUI, tasks::{ProgressableTask, TaskHandle, TaskType}}};\n\n// Global TUI state\nlazy_static! {\n    pub static ref TUI_MANAGER: Mutex<FlusterTUI<'static>> = Mutex::new(FlusterTUI::new());\n}\n\n// We only run some logic if the TUI is enabled.\n// Plus this is a great excuse to learn macros!\n// This just sticks a return into the function if the TUI is enabled.\nmacro_rules! skip_if_tui_disabled {\n    () => {\n        if let Some(got) = USE_TUI.get() {\n            if !got {\n                // TUI is not enabled.\n                return\n            }\n        } else {\n            // The flag is not currently set, we'll just do all of the logic behind the scenes just\n            // in case.\n\n        }\n    };\n}\n\n// Since we expect things to be single threaded when interacting with the TUI, we have a lot of\n// locks and expects, thus we will just abstract that out. If the lock fails, the task state would become\n// desynced, which would almost guarantee a crash. Thus we will exit out if that happens.\n//\n// Karen, because it gets the manager\nmacro_rules! karen {\n    () => {\n        if let Ok(got) = TUI_MANAGER.lock() {\n            got\n        } else {\n            // Manager is out, which mean's its poisoned due to a panic or such.\n            // Not great. I really doubt we could recover from that.\n            error!(\"Couldn't get the TUI manager, giving up!\");\n            error!(\"{}\", std::backtrace::Backtrace::force_capture());\n            panic!(\"TUI manager is poisoned!\");\n        }\n    };\n}\n\n\n// Now we have a bunch of public functions for updating the TUI.\n\n// Its on a struct, just because I like how that looks.\npub(crate) struct NotifyTui {\n    // dummy\n}\n\nimpl NotifyTui {\n    //\n    // Disk\n    //\n\n    /// A _real_ disk swap has occurred, you must provide the disk that is now in the drive.\n    pub(crate) fn disk_swapped(new_disk: u16) {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        manager.state.disk_swap_count += 1;\n        manager.state.current_disk_in_drive = new_disk;\n    }\n\n    /// A block, or multiple blocks have been read from disk.\n    pub(crate) fn block_read(number: u16) {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        manager.state.disk_blocks_read += number as u64;\n    }\n\n    /// Block(s) has been written to disk.\n    pub(crate) fn block_written(amount: u16) {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        manager.state.disk_blocks_written += amount as u64;\n    }\n\n    //\n    // Cache\n    //\n\n    /// The cache saved a swap.\n    pub(crate) fn swap_saved() {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        manager.state.cache_swaps_saved += 1;\n    }\n\n    /// A tier of the cache was flushed to disk.\n    pub(crate) fn cache_flushed() {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        manager.state.cache_flushes += 1;\n    }\n\n    /// A read was cached instead of read from disk.\n    pub(crate) fn read_cached() {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        manager.state.cache_blocks_read += 1;\n    }\n\n    /// A write was cached instead of written to disk.\n    pub(crate) fn write_cached() {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        manager.state.cache_blocks_written += 1;\n    }\n\n    /// Update the cache hit-rate\n    pub(crate) fn set_cache_hit_rate(rate: f64) {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        manager.state.cache_hit_rate = rate;\n    }\n\n    /// Update the cache pressure\n    pub(crate) fn set_cache_pressure(pressure: f64) {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        manager.state.cache_pressure = pressure;\n    }\n\n    //\n    // Task\n    //\n\n    /// Start a new task.\n    /// \n    /// You must keep the TaskHandle to be able to update this task.\n    #[must_use] // Cant ignore the handle!\n    pub(crate) fn start_task(task_type: TaskType, steps: u64) -> TaskHandle {\n        // Return a dummy handle if TUI is disabled.\n        if let Some(flag) = USE_TUI.get() {\n            if !flag {\n                return TaskHandle::new();\n            }\n        } else {\n            // USE_TUI is not set, this really should be set already...\n            // If we give out a fake handle:\n            // - Caller cancels/finishes a task, but now USE_TUI is set, causing\n            //    either nothing to happen, or canceling an unrelated task. BAD!\n            // If we give out a TUI handle in non-TUI mode:\n            // - Handle would be given out, added to the TUI state, now USE_TUI is set,\n            //    if its set to true, all is good. If its false, still okay, since it just will chill\n            //    in the tui manager without ever being cleaned up. Which is fine.\n\n            // Thus we will give out a handle that is already marked as finished, so when the deconstruction on it runs,\n            // It'll just drop silently. Hopefully? In theory we can only give out one handle at a time, so if this is the\n            // only handle that's out, and the flag is not set, chances are this is the first handle _ever_ so ignoring it is fine,\n            // because the TUI would just try to clean up nothing, which should finish.\n\n            // Also before adding this if let some, there was an expect here, and i never saw that get hit, so chances are\n            // this will never run anyways.\n\n            let mut pre_finished = TaskHandle::new();\n            pre_finished.task_was_finished_or_canceled = true;\n            return pre_finished;\n        }\n\n        // Create the task and make a new handle\n        let new_task = ProgressableTask::new(task_type, steps);\n        let handle: TaskHandle = TaskHandle::new();\n\n        let mut manager = karen!();\n        let state = &mut manager.state;\n        // If we already have a task, append it\n        if let Some(ref mut task) = state.task {\n            task.add_sub_task(new_task);\n            return handle;\n        }\n        // Currently don't have any tasks, add it directly.\n        state.task = Some(new_task);\n        handle\n    }\n\n    /// Complete one step of a task\n    /// \n    /// Handle required to ensure you actually have a task you're working on\n    pub(crate) fn complete_task_step(_handle: &TaskHandle) {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        if let Some(task) = manager.state.task.as_mut() {\n            task.finish_steps(1);\n        } else {\n            // No task to work on! Out of sync!\n            // Cooked for sure.\n            error!(\"{}\", std::backtrace::Backtrace::force_capture());\n            panic!(\"Task state desync! Expected task, got none!\");\n        }\n    }\n\n    /// Complete multiple steps of a task.\n    /// \n    /// Handle required to ensure you actually have a task you're working on\n    pub(crate) fn complete_multiple_task_steps(_handle: &TaskHandle, steps: u64) {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        if let Some(task) = manager.state.task.as_mut() {\n            task.finish_steps(steps);\n        } else {\n            // No task to work on! Out of sync!\n            // Cooked for sure.\n            error!(\"{}\", std::backtrace::Backtrace::force_capture());\n            panic!(\"Task state desync! Expected task, got none!\");\n        }\n    }\n\n    /// Add more steps to the current task.\n    ///\n    /// Handle required to ensure you actually have a task you're working on\n    pub(crate) fn add_steps_to_task(_handle: &TaskHandle, steps: u64) {\n                skip_if_tui_disabled!();\n        let mut manager = karen!();\n        if let Some(task) = manager.state.task.as_mut() {\n            task.add_work(steps);\n        } else {\n            // No task to work on! Out of sync!\n            // Cooked for sure.\n            error!(\"{}\", std::backtrace::Backtrace::force_capture());\n            panic!(\"Task state desync! Expected task, got none!\");\n        }\n    }\n\n    /// Finish a task.\n    /// \n    /// Handle required to ensure you actually have a task you're working on\n    pub(crate) fn finish_task(mut handle: TaskHandle) {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        let stored_task = &mut manager\n        .state\n        .task;\n\n        if let Some(took) = stored_task.take() {\n            *stored_task = took.finish_task();\n        } else {\n            // We have a handle, but no task?\n            // I mean, there's nothing to finish, but removing nothing shouldn't be an issue.\n            // Good enough?\n        }\n\n        // Update and drop handle by letting it fall out of scope.\n        handle.task_was_finished_or_canceled = true;\n    }\n\n    /// Cancel a task.\n    /// \n    /// Handle required to ensure you actually have a task you're working on\n    pub(crate) fn cancel_task(mut handle: TaskHandle) {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        let stored_task = &mut manager\n        .state\n        .task;\n\n        if let Some(took) = stored_task.take() {\n            *stored_task = took.cancel_task();\n        } else {\n            // We have a handle, but no task?\n            // I mean, there's nothing to finish, but removing nothing shouldn't be an issue.\n            // Good enough?\n        }\n\n        // Update and drop handle by letting it fall out of scope.\n        handle.task_was_finished_or_canceled = true;\n    }\n\n    /// Forcibly cancel a task without a handle.\n    /// Only used for dropping\n    pub(super) fn force_cancel_task() {\n        skip_if_tui_disabled!();\n        let mut manager = karen!();\n        let stored_task = &mut manager\n        .state\n        .task;\n\n        // Only try to cancel if there's actually a task\n        if let Some(task) = stored_task.take() {\n            *stored_task = task.cancel_task();\n        }\n    }\n    \n}\n\n\n\n\n"
  },
  {
    "path": "src/tui/prompts.rs",
    "content": "// Gotta talk to people sometimes.\n\nuse std::path::Path;\nuse std::path::PathBuf;\nuse std::time::Instant;\n\nuse log::debug;\nuse log::error;\nuse log::info;\nuse ratatui::style::{Style, Stylize};\nuse tui_textarea::TextArea;\n\nuse crate::error_types::drive::DriveError;\nuse crate::filesystem::filesystem_struct::FLOPPY_PATH;\nuse crate::{filesystem::filesystem_struct::USE_TUI, tui::notify::TUI_MANAGER};\n\n#[derive(Debug)]\npub(crate) struct TuiPrompt<'a> {\n    /// Title of the prompt\n    pub(super) title: String,\n    /// What the prompt is telling the user\n    pub(super) content: String,\n    /// Do we expect to get a string back from this prompt?\n    pub(super) expects_string: bool,\n    /// Where we send the response to. Even if the prompt\n    /// doesn't require a response, we still use the oneshot to more\n    /// easily block the caller of the prompt.\n    /// \n    /// I'm calling this a callback because i feel like it.\n    pub(super) callback: oneshot::Sender<String>,\n    /// Should the window flash to get the user's attention?\n    pub(super) flash: bool,\n    /// The persistent text entry field, persists between\n    /// frames so we don't have to extract input handling out\n    /// to main.rs\n    pub(super) text_area: tui_textarea::TextArea<'a>,\n    /// Allow the caller to manually manipulate the pop-up, IE when enter is\n    /// pressed, the layout code will not close the window. It's up to the caller to close\n    /// the pop up by swapping in a none.\n    pub(super) manual_close: bool,\n}\n\n\n\n\n// if the TUI is disabled, we still need to be able to prompt for input.\nimpl TuiPrompt<'_> {\n    /// Make a new prompt for pressing enter.\n    /// \n    /// This will block until the user presses enter.\n    pub(crate) fn prompt_enter(title: String, content: String, flash: bool) {\n        // We need the channel even if we arent getting a string back, since we wanna\n        // block the caller thread without having to spin in a loop lockin stuff.\n        let (response_tx, response_rx) = oneshot::channel();\n        // Assemble and start the prompt.\n        let prompt = TuiPrompt {\n            title,\n            content,\n            expects_string: false,\n            callback: response_tx,\n            flash,\n            text_area: TextArea::default(), // Not actually used.\n            manual_close: false\n        };\n\n        if let Some(flag) = USE_TUI.get() {\n            if !flag {\n                // Tui is disabled.\n                return disabled_prompt_enter(prompt);\n            }\n        } else {\n            // USE_TUI is not set yet, it really should be at this point.\n            // But since it isn't, we'll just fall back to disabled mode.\n            return disabled_prompt_enter(prompt);\n        }\n\n        // Run the prompt\n        loop {\n            if let Ok(mut lock) = TUI_MANAGER.lock() {\n                lock.user_prompt = Some(prompt);\n                break\n            }\n        }\n\n        // Wait for prompt to close\n        let _ = response_rx.recv();\n\n        // All done.\n    }\n\n    /// Make a new prompt for text input.\n    /// \n    /// This will block until the user responds.\n    pub(crate) fn prompt_input(title: String, content: String, flash: bool) -> String {\n        // Get the channel for communicating the result of the prompt\n        let (response_tx, response_rx) = oneshot::channel();\n\n\n        // Assemble and start the prompt.\n        // green text box\n        let mut text_area = TextArea::default();\n        text_area.set_style(Style::reset().on_black().green());\n        let prompt = TuiPrompt {\n            title,\n            content,\n            expects_string: true,\n            callback: response_tx,\n            flash,\n            text_area,\n            manual_close: false\n        };\n\n        if let Some(flag) = USE_TUI.get() {\n            if !flag {\n                // Legacy mode\n                return disabled_prompt_input(prompt);\n            }\n        } else {\n            // USE_TUI is not set yet, it really should be at this point.\n            // But since it isn't, we'll just fall back to legacy prompting\n            return disabled_prompt_input(prompt);\n        }\n\n        // Run the prompt\n        loop {\n            if let Ok(mut lock) = TUI_MANAGER.lock() {\n                lock.user_prompt = Some(prompt);\n                break\n            }\n        }\n\n        // Wait for a response, and return it.\n        // If we got no response for some reason, safest bet is to return nothing.\n        response_rx.recv().unwrap_or_default()\n    }\n\n    /// Does not actually check what disk was swapped to, just waits for a swap to happen.\n    /// \n    /// Returns an error if we were unable to detect disk swaps.\n    /// \n    /// This function also assumes that the floppy drive is a block device, and checks agains /sys/ to deduce that.\n    /// \n    /// Will this work on MacOS? Probably not. Def wont work on windows.\n    pub(crate) fn prompt_wait_for_disk_swap(title: String, content: String, flash: bool) -> Result<(), DriveError> {\n\n        // We dont actually care about this response at all, nothing ever gets sent\n        // I'm just too lazy to remove the requirement for it right now.\n        let (response_tx, _response_rx) = oneshot::channel();\n\n\n        // Assemble\n        let prompt = TuiPrompt {\n            title,\n            content,\n            expects_string: false,\n            callback: response_tx,\n            flash,\n            text_area: TextArea::default(), // Not actually used.\n            manual_close: true\n        };\n\n        if let Some(flag) = USE_TUI.get() {\n            if !flag {\n                // Legacy mode is not supported at all here,\n                // since test cases should never hit the disk swap prompt.\n                panic!(\"Cannot swap disks without a tui!\");\n            }\n        } else {\n            // USE_TUI really must be set at this point, this is a dead path as far as i can tell\n            unreachable!(\"Shouldn't be able to swap disks without ever setting USE_TUI\");\n        }\n\n        // Get the disk path.\n        let disk_path = if let Ok(guard) = FLOPPY_PATH.try_lock() {\n            guard.clone()\n        } else {\n            // Cant lock it, chances are its poisoned.\n            // Return a retry, since the caller needs to clear the poison (troubleshooter does that)\n            return Err(DriveError::Retry);\n        };\n\n        // Now we put the prompt into the TUI, since if we put it in before checking the poison,\n        // it would never close.\n        loop {\n            if let Ok(mut lock) = TUI_MANAGER.lock() {\n                lock.user_prompt = Some(prompt);\n                break\n            }\n        }\n\n        // Time to wait for a disk swap.\n\n        // Now we need to wait for the state of the drive to transition to being empty, then full again.\n        // There's probably a smarter way to do this, but I'm not smarter than the compiler anyways.\n        let mut disk_in_drive: bool = get_block_device_size(&disk_path)? > 0;\n        let mut previous_state: bool = disk_in_drive;\n        let mut changes: u8 = 0;\n\n        // If the drive was already empty when we checked (weird) then we need to increment changes, since\n        // we're waiting for the state to change once (disk inserted) instead of twice (removal, insertion)\n        if !disk_in_drive {\n            changes += 1;\n        }\n\n        // Now loop till the disk has been swapped.\n        // We keep track of how long this has been going, if over 2 minutes pass with no input, we'll bail out,\n        // just in case we get stuck here, since we'd need to escape out to get to the troubleshooter.\n        let started_waiting: Instant = Instant::now();\n        while changes < 2 {\n            // Check if there is a disk\n            disk_in_drive = get_block_device_size(&disk_path)? > 0;\n            \n            // Did the state change?\n            if disk_in_drive != previous_state {\n                debug!(\"Drive state has changed.\");\n                // Change!\n                changes += 1;\n                previous_state = !previous_state;\n                // Keep going, the while loop will end itself automagically.\n                continue;\n            }\n\n            // No change, wait\n            // half second pauses, as far as i can tell, the drive doesn't instantly know when it has a disk in it,\n            // so that should be granular enough.\n            std::thread::sleep(std::time::Duration::from_millis(500));\n            \n            // If more than 2 minutes have passed, bail\n            if started_waiting.elapsed().as_secs() > 120 {\n                debug!(\"Disk swap did not occur within 2 minutes, bailing.\");\n                // New prompt on screen, this will replace the old prompt.\n\n                let (timeout_response_tx, timeout_response_rx) = oneshot::channel();\n                let timeout_prompt = TuiPrompt {\n                    title: \"Anybody home?\".to_string(),\n                    content: \"No disk was swapped within 2 minutes of requesting. Bailing out.\".to_string(),\n                    expects_string: false,\n                    callback: timeout_response_tx,\n                    flash: false,\n                    text_area: TextArea::default(),\n                    manual_close: false\n                };\n\n                // Send the timeout prompt\n                loop {\n                    if let Ok(mut lock) = TUI_MANAGER.lock() {\n                        lock.user_prompt = Some(timeout_prompt);\n                        break\n                    }\n                }\n\n                // wait\n                let _ = timeout_response_rx.recv().unwrap_or_default();\n\n                // Error out.\n                return Err(DriveError::TakingTooLong)\n            }\n        };\n\n        // Disk has been swapped, remove the prompt\n        loop {\n            if let Ok(mut lock) = TUI_MANAGER.lock() {\n                lock.user_prompt = None;\n                break\n            }\n        }\n\n        // All done.\n        Ok(())\n\n    }\n\n}\n\n/// Abstraction for getting the size of a block device.\n/// \n/// Returns how many blocks the block device has.\n/// \n/// This assumes the mount point is `/dev/sd*`, if its not, we cant do anything.\n/// \n/// Yes this is hacky. I'm not happy about it either, but std::fs::metadata always returns `0` on\n/// block devices it seems, and I just need to get this working.\n/// \n/// We look in /sys/block/sd*/size to get the size.\n/// \n/// That file contains how many blocks are on that device total, and i see dmesg entries reporting\n/// a block size change whenever the disk is removed, so this should work.\n/// \n/// This also requires Fluster! to be ran as root/sudo lmao\n/// \n/// Returns an error if the path does not exist (ie the mount point is not there.)\nfn get_block_device_size(path: &Path) -> Result<u64, DriveError> {\n    // inspired by http://syhpoon.ca/posts/how-to-get-block-device-size-on-linux-with-rust/\n    // But I dont wanna use unsafe.\n\n    // This code path also assumes that your floppy drives report a block size of 512.\n    // Mine does at least. lol.\n\n    // Make sure the path actually exists\n    if !path.exists() {\n        // Nope.\n        // Just tell it to retry, which should hit the retry max count elsewhere and hit the troubleshooter up\n        return Err(DriveError::Retry);\n    }\n\n\n    // Make sure the floppy path makes sense\n    assert!(path.has_root(), \"Floppy paths must be fully qualified.\");\n    // Must be in /dev/\n    assert!(path.parent()\n        .expect(\"Floppy path must have have a parent, since its in /dev/\")\n        .to_str()\n        .expect(\"path has to be utf8 compat\")\n        .contains(\"dev\"),\n        \"Path to the floppy drive must have a parent.\"\n    );\n\n    // Not gonna check if its a block device, its your problem if you dont point at one, and the expect should catch it.\n    // Now we just need the end of the path. IE `/dev/sdf` -> `sdf`\n    let block_device_name = path.file_name()\n        .expect(\"There must be a file, cant use a folder as a floppy\")\n        .to_str()\n        .expect(\"Should be valid UTF8\");\n\n    // Now we look into /sys/block/ to get how many blocks are on that device\n    let path_to_info = PathBuf::from(format!(\"/sys/block/{block_device_name}/size\"));\n    let blocks_count_string = std::fs::read_to_string(&path_to_info).expect(\"If we cant access this file, fluster is doomed to fail.\");\n\n    // Now parse that into a number\n    let block_count: u64 = blocks_count_string.trim().parse().expect(\"The size file should not contain non-number info.\\n\");\n\n    // This is also a nice spot to make sure this is not bigger than a floppy drive. If it is,\n    // chances are the user pointed at another disk and is gonna wipe their drive lmao, we'll save them.\n    if block_count > 3000 { // A little wiggle room just in-case the drive mounts weird.\n        // This is most likely not a floppy drive.\n        error!(\"The provided block device that's supposed to be a floppy disk is too big.\");\n        error!(\"Chances are, you accidentally passed in a different drive.\");\n        error!(\"Fluster will now exit, as we don't want to accidentally wipe one of your drives.\");\n        error!(\"If you're trying to use another device as a floppy drive for some reason (unlikely), you're probably\");\n        error!(\"smart enough to disable this check.\");\n        panic!(\"Floppy drive is too big! See logs!\");\n    }\n\n    // All done\n    Ok(block_count)\n}\n\n\n// User input only works with the TUI enabled.\nfn disabled_prompt_enter(prompt: TuiPrompt) {\n    info!(\"Skipping prompt...\");\n    info!(\"Enter prompt: [{}]: {}\", prompt.title, prompt.content);\n}\n\nfn disabled_prompt_input(_prompt: TuiPrompt) -> String {\n    error!(\"You might not like TUI's, but this setting is secretly just for test cases.\");\n    panic!(\"You need to use the TUI to use fluster.\");\n}"
  },
  {
    "path": "src/tui/state.rs",
    "content": "// Stuff for tracking the state of the TUI and Fluster.\n// Need to be careful in here not to lock at the same time\n// as Fluster locking other things.\n\nuse crate::tui::tasks::ProgressableTask;\n\n/// Struct for holding information about Fluster's current state.\npub(super) struct FlusterTUIState {\n    //\n    // Disk stats\n    //\n\n    // All stats are from the perspective of the current run. Fluster does not\n    // store statistics on shutdown.\n    /// How many times total the disk has been swapped\n    pub(super) disk_swap_count: u64,\n    /// The total number of blocks that have been read from the physical disk, note that\n    /// this is seperate from cache reads, since we \n    pub(super) disk_blocks_read: u64,\n    /// The total number of blocks that have been written to the physical disk.\n    pub(super) disk_blocks_written: u64,\n    /// What disk is currently in the drive, is updated _after_ disk switches.\n    pub(super) current_disk_in_drive: u16,\n\n    //\n    // Cache stats\n    //\n\n    /// The current hit rate of the cache (Only needs to be updated on\n    /// disk swap.)\n    pub(super) cache_hit_rate: f64,\n    /// The current pressure of the cache, aka how full tier 0 is.\n    pub(super) cache_pressure: f64,\n    /// Number of times we went to read a block, but got it from\n    /// the cache instead of hitting the disk\n    pub(super) cache_blocks_read: u64,\n    /// Number of times we went to write a block, but were able to\n    /// temporarily store it in the cache.\n    pub(super) cache_blocks_written: u64,\n    /// Number of times we've flushed tier 0 of the cache to disk.\n    pub(super) cache_flushes: u64,\n    /// Number of times we avoided swapping disks by doing a cached operation\n    pub(super) cache_swaps_saved: u64,\n\n    //\n    // Tasks\n    //\n\n    // Current in-progress task, if there is one.\n    pub(super) task: Option<ProgressableTask>,\n}\n\nimpl FlusterTUIState {\n    /// Brand new state, used for init\n    pub(super) fn new() -> Self {\n        Self {\n            disk_swap_count: 0,\n            disk_blocks_read: 0,\n            disk_blocks_written: 0,\n            cache_hit_rate: 0.0,\n            cache_pressure: 0.0,\n            cache_blocks_read: 0,\n            cache_blocks_written: 0,\n            cache_flushes: 0,\n            cache_swaps_saved: 0,\n            task: None,\n            current_disk_in_drive: 420, // no idea yet.\n        }\n    }\n}"
  },
  {
    "path": "src/tui/tasks.rs",
    "content": "// Keeping track of what we're working on\n\nuse std::time::Instant;\n\nuse crate::tui::notify::NotifyTui;\n\n\n/// Progress of events.\n/// \n/// Updating an event's progress is an indirect action, you can say you've completed a \"steps\" of a\n/// task, or add more \"steps\" to the task. Thus you don't have to keep track of the percentages yourself,\n/// just if you need to add more work to your task, or if you've completed some of it.\n/// \n/// Tasks can have sub-tasks.\n/// \n/// Keep in mind that steps only increment, you cannot go backwards.\n/// \n/// All actions on a ProgressableTask implicitly apply to the final task in the chain of\n/// sub-tasks. IE, if you have a->b->c, calling finish_step() will affect c.\n#[derive(Debug)]\npub(crate) struct ProgressableTask {\n    /// What the task is\n    task: TaskInfo,\n    /// A sub task, if any.\n    sub_task: Option<Box<ProgressableTask>>\n}\n\n/// Task information is stored in a second struct for ease of use, since\n/// everything besides sub-tasks can be Copy-ed.\n#[derive(Clone, Debug)]\npub(super) struct TaskInfo {\n    /// The type of task being performed.\n    task_type: TaskType,\n    /// How many \"steps\" of the task are needed to finish this task.\n    steps: u64,\n    /// How many \"steps\" have been completed so far.\n    steps_finished: u64,\n    /// When this task was started\n    start_time: Instant,\n}\n\n/// Every kind of task that can indicate its progress.\n#[derive(Clone, Debug)]\npub(crate) enum TaskType {\n    DiskWriteBlock,\n    DiskWriteLarge,\n    WaitingForDriveSpinUp,\n    DiskReadBlock,\n    /// Includes the name of the file.\n    FilesystemReadFile(String), // man strings are annoying, no more copy\n    /// Includes the name of the file.\n    FilesystemWriteFile(String),\n    /// Includes the name of the file.\n    FilesystemOpenFile(String),\n    /// Includes the name of the file.\n    FilesystemTruncateFile(String),\n    /// Includes name of new file duh\n    FilesystemCreateFile(String),\n    /// Includes the name of the new directory.\n    FilesystemMakeDirectory(String),\n    /// Includes the name of directory to list.\n    FilesystemReadDirectory(String),\n    /// Includes the name of the directory that is about to be removed.\n    FilesystemRemoveDirectory(String),\n    /// Includes the name of the file that is kill.\n    FilesystemDeleteFile(String),\n    /// Includes the name of the file / folder.\n    GetMetadata(String),\n    GetSize,\n    WipeDisk,\n    RestoreDisk,\n    FlushCurrentDisk,\n    FlushTier,\n    FileReadBytes,\n    FileWriteBytes,\n    /// Includes number of requested blocks.\n    PoolAllocateBlocks(u16),\n    /// Includes number of blocks being deallocated.\n    DiskDeallocateBlocks(u16),\n    CreateNewDisk,\n    WriteCRC,\n    /// Includes the name of the directory we're trying to open\n    ChangingDirectory(String),\n    ListingDirectory,\n    /// Includes the name of the item we're looking for\n    FindItemInDirectory(String),\n    CreateDirectoryItem,\n}\n\n/// When we start a task, we are promising to finish it. We need a way to know\n/// if the task never finished. Thus we hand out a little struct that you need to\n/// hold onto. If the struct gets dropped, we assume the lowest task in the chain\n/// was canceled, or has finished.\n/// \n/// DO NOT implement clone or copy.\npub(crate) struct TaskHandle {\n    /// This is set if the the task this handle was dispatched for\n    /// completed _or_ was canceled, indicating that cleanup is not needed.\n    /// \n    /// Defaults to false\n    pub(super) task_was_finished_or_canceled: bool,\n}\n\n\n\n//\n// Implementations\n//\n\nimpl Drop for TaskHandle {\n    fn drop(&mut self) {\n        // Are we done?\n        if self.task_was_finished_or_canceled {\n            // Cool! Don't need to do anything.\n        } else {\n            // The task was never finished or canceled, we must cancel it ourselves\n            // We cant actual use ourselves here, since we need to pass in an owned\n            // value that will be dropped. So we just make a new one. weird, i know\n            NotifyTui::force_cancel_task()\n        }\n    }\n}\n\nimpl TaskHandle {\n    pub(super) fn new() -> Self {\n        Self {\n            task_was_finished_or_canceled: false,\n        }\n    }\n}\n\n\n\n\nimpl TaskInfo {\n    // yeah\n    fn new(task_type: TaskType, steps: u64) -> TaskInfo {\n        Self {\n            task_type,\n            steps,\n            steps_finished: 0,\n            start_time: Instant::now(),\n        }\n    }\n\n    /// Get the string name of this task\n    pub(super) fn name(&self) -> String {\n        match &self.task_type {\n            TaskType::WaitingForDriveSpinUp => \"Waiting for floppy drive to spin up...\".to_string(),\n            TaskType::DiskWriteBlock => \"Writing a block...\".to_string(),\n            TaskType::DiskReadBlock => \"Reading a block...\".to_string(),\n            TaskType::WipeDisk => \"Wiping disk...\".to_string(),\n            TaskType::DiskWriteLarge => \"Writing several blocks...\".to_string(),\n            TaskType::CreateNewDisk => \"Creating new disk...\".to_string(),\n            TaskType::WriteCRC => \"Writing CRC to block...\".to_string(),\n            TaskType::GetSize => \"Getting the size of an item...\".to_string(),\n            TaskType::ListingDirectory => \"Listing a directory...\".to_string(),\n            TaskType::CreateDirectoryItem => \"Creating new directory item...\".to_string(),\n            TaskType::FlushCurrentDisk => \"Flushing current disk...\".to_string(),\n            TaskType::FlushTier => \"Flushing a tier of cache...\".to_string(),\n            TaskType::FilesystemReadFile(name) => {\n                format!(\"Reading from file \\\"{name}\\\"...\")\n            },\n            TaskType::FilesystemWriteFile(name) => {\n                format!(\"Writing to file \\\"{name}\\\"...\")\n            },\n            TaskType::FilesystemTruncateFile(name) => {\n                format!(\"Truncating \\\"{name}\\\"...\")\n            },\n            TaskType::PoolAllocateBlocks(number) => {\n                format!(\"Allocating {number} blocks across disk pool...\")\n            },\n            TaskType::DiskDeallocateBlocks(number) => {\n                format!(\"Freeing {number} blocks from current disk...\")\n            },\n            TaskType::GetMetadata(name) => {\n                format!(\"Getting {name}'s metadata...\")\n            },\n            TaskType::ChangingDirectory(name) => {\n                format!(\"Trying to open directory {name}...\")\n            },\n            TaskType::FindItemInDirectory(name) => {\n                format!(\"Looking for {name} in current directory...\")\n            },\n            TaskType::FilesystemMakeDirectory(name) => {\n                format!(\"Making new directory {name}...\")\n            },\n            TaskType::FilesystemOpenFile(name) => {\n                format!(\"Opening file {name}...\")\n            },\n            TaskType::FilesystemCreateFile(name) => {\n                format!(\"Creating file {name}...\")\n            },\n            TaskType::FilesystemReadDirectory(name) => {\n                format!(\"Reading directory {name}...\")\n            },\n            TaskType::FilesystemRemoveDirectory(name) => {\n                format!(\"Removing directory {name}...\")\n            },\n            TaskType::FilesystemDeleteFile(name) => {\n                format!(\"Deleting file {name}...\")\n            },\n            TaskType::FileReadBytes => \"Reading bytes from file...\".to_string(),\n            TaskType::FileWriteBytes => \"Writing bytes to file...\".to_string(),\n            TaskType::RestoreDisk => \"Restoring a disk from backup...\".to_string(),\n        }\n    }\n\n    /// Get a float for how finished the task is\n    pub(super) fn progress(&self) -> f64 {\n        self.steps_finished as f64 / self.steps as f64\n    }\n\n    /// Returns a string `[hh:mm:ss]` of how long the task has been running\n    pub(super) fn time_passed(&self) -> String {\n        let elapsed_seconds = self.start_time.elapsed().as_secs();\n        format!(\"[{:0>2}:{:0>2}:{:0>2}]\", elapsed_seconds/(60*60), (elapsed_seconds/60)%60, elapsed_seconds%60)\n    }\n    \n    /// Returns a string `[hh:mm:ss]` which guesstimates how long the task is going to take\n    /// \n    /// Barely useful, but fun!\n    pub(super) fn time_remaining(&self) -> String {\n        let elapsed_seconds = self.start_time.elapsed().as_secs();\n        // Now based on how far we've come, estimate how much longer it'll take\n        let mut estimated_seconds = if self.steps_finished > 0 {\n            (elapsed_seconds as f64 * (self.steps as f64 / self.steps_finished as f64)) as u64\n        } else {\n            // div zero moment, just return all nines lmao\n            return \"[99:99:99]\".to_string();\n        };\n        estimated_seconds = estimated_seconds.saturating_sub(elapsed_seconds);\n        format!(\"[{:0>2}:{:0>2}:{:0>2}]\", estimated_seconds/(60*60), (estimated_seconds/60)%60, estimated_seconds%60)\n    }\n}\n\nimpl ProgressableTask {\n    /// Create a new task.\n    /// \n    /// New tasks cannot have a sub task pre-attached.\n    pub(super) fn new(task_type: TaskType, steps: u64) -> ProgressableTask {\n        ProgressableTask {\n            task: TaskInfo::new(task_type, steps),\n            sub_task: None,\n        }\n    }\n\n    /// Add more steps to a currently in-progress task.\n    /// \n    /// Updates how many steps _need_ to be taken, not how many _have_ been taken.\n    pub(super) fn add_work(&mut self, steps_to_add: u64) {\n        // Recurse if there are sub-tasks\n        if let Some(sub_task) = &mut self.sub_task {\n            return sub_task.add_work(steps_to_add)\n        }\n\n        self.task.steps += steps_to_add;\n    }\n\n    /// Indicate that a step has been completed.\n    /// \n    /// You can only finish one step at a time.\n    pub(super) fn finish_steps(&mut self, steps: u64) {\n        // Recurse if there are sub-tasks\n        if let Some(sub_task) = &mut self.sub_task {\n            return sub_task.finish_steps(steps)\n        }\n\n        self.task.steps_finished += steps;\n        // You cannot finish more steps than you need to complete.\n        assert!(self.task.steps_finished <= self.task.steps, \"Tasks cannot complete more steps than they contain!\");\n    }\n\n    /// Add a sub-task.\n    /// \n    /// New task will be added to the last task in the chain.\n    pub(super) fn add_sub_task(&mut self, new_sub_task: ProgressableTask) {\n        // Recurse if there are sub-tasks\n        if let Some(sub_task) = &mut self.sub_task {\n            return sub_task.add_sub_task(new_sub_task)\n        }\n\n        self.sub_task = Some(Box::new(new_sub_task));\n    }\n\n    /// Finishes the task (or subtask) currently in progress.\n    /// \n    /// Does not return the finished task, it only removes it.\n    /// \n    /// For a task to end, it must have a step count equal to its finished step count. IE\n    /// you cannot finish a task without completing all of the work you promised you would complete.\n    /// \n    /// If you need to end a task due to a failure, you need to use cancel_task()\n    /// \n    /// Will return None if this is the final task in the chain.\n    pub(super) fn finish_task(mut self) -> Option<ProgressableTask> {\n        // Recurse if there are sub-tasks\n        if let Some(sub_task) = self.sub_task {\n            let new_subtask = sub_task.finish_task();\n            self.sub_task = new_subtask.map(Box::new);\n            return Some(self)\n        }\n        \n        // This is the final task in the chain.\n        // Make sure we finished all of the steps we set out to do.\n        assert!(self.task.steps == self.task.steps_finished, \"Task was not finished! {:#?}\", self.task);\n\n        // Remove the task.\n        None\n    }\n\n    /// Cancels an in-progress task (or subtask).\n    /// \n    /// This ignores the requirement for tasks to have all of their steps completed.\n    /// \n    /// This is an explicit function, as you should do proper handling for canceling tasks.\n    pub(super) fn cancel_task(mut self) -> Option<ProgressableTask> {\n        // Recurse if there are sub-tasks\n        if let Some(sub_task) = self.sub_task {\n            let new_subtask = sub_task.cancel_task();\n            self.sub_task = new_subtask.map(Box::new);\n            return Some(self)\n        }\n        // This is the last task, remove it.\n        None\n    }\n\n    /// Extract a Vec<TaskInfo> from this task.\n    /// \n    /// The head task is the first in the vec.\n    pub(super) fn get_tasks_info(&self) -> Vec<TaskInfo> {\n        // Recurse downwards, adding a copy of each task info as we go down.\n        let mut tasks: Vec<TaskInfo> = Vec::new();\n        tasks.push(self.task.clone());\n\n        let mut sub = &self.sub_task;\n        while let Some(new_sub) = sub.as_deref() {\n            // Get the task out\n            tasks.push(new_sub.task.clone());\n            // go deeper\n            sub = &new_sub.sub_task;\n        }\n\n        // All tasks have been collected.\n        tasks\n    }\n}"
  },
  {
    "path": "src/tui/tui_struct.rs",
    "content": "// The struct that holds everything needed to render the tui\n\nuse std::time::Instant;\n\nuse crate::tui::{\n    layout::FlusterTUI,\n    state::FlusterTUIState\n};\n\nimpl FlusterTUI<'_> {\n    /// Brand new state, only for initialization.\n    pub(super) fn new() -> Self {\n        Self {\n            state: FlusterTUIState::new(),\n            last_update: Instant::now(),\n            started: Instant::now(),\n            user_prompt: None,\n        }\n    }\n}"
  },
  {
    "path": "tests/directory.rs",
    "content": "use std::{\n    error::Error,\n    ffi::OsStr,\n    thread,\n    time::Duration\n};\n\nuse log::{\n    error,\n    info\n};\nuse rand::{\n    rngs::ThreadRng,\n    seq::SliceRandom\n};\n\n// We want to see logs while testing.\nuse test_log::test;\n\nuse crate::test_common::test_mount_options; \npub mod test_common;\n\n#[test]\n// Try creating a directory\nfn create_directory() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    let mounted_fs_path = mount_point.path().to_path_buf();\n    \n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n\n    // make a new dir\n    let mut new_dir = mounted_fs_path.clone();\n    new_dir.push(\"testdir\");\n\n    info!(\"Attempting to create a new directory...\");\n    info!(\"It will go at `{}`.\", new_dir.display());\n    let creation_result = std::fs::create_dir(new_dir);\n    info!(\"Finished attempting creation.\");\n    \n    // See if it's there\n    info!(\"Checking if directory exists...\");\n    let find_result = std::fs::read_dir(mounted_fs_path);\n    let mut file_found: bool = false;\n    if let Ok(items) = find_result {\n        info!(\"Directory read succeeded, checking for the test dir...\");\n        for i in items {\n            // Check the results\n            if let Ok(good) = i {\n                // is this testdir?\n                let item_name = good.file_name();\n                info!(\"found {}\", item_name.display());\n                if item_name == \"testdir\" {\n                    // It exists!\n                    file_found = true;\n                }\n                // Ignore anything that isnt the directory we are looking for.\n            } else {\n                // Item was an error. uh oh\n                let extracted_error = i.unwrap();\n                error!(\"Error directory item: {extracted_error:#?}\");\n            }\n            \n        }\n    } else {\n        error!(\"Reading the directory failed.\");\n        let read_error = find_result.unwrap_err();\n        if let Some(src) = read_error.source() {\n            error!(\"source: {src}\");\n        } else {\n            error!(\"source not marked.\");\n        }\n        error!(\"error: {read_error}\");\n        error!(\"kind: {}\", read_error.kind());\n    }\n\n    // cleanup\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n\n    // Do the unwrap after unmounting, so we unmount even if it failed.\n\n    // Did the creation fail?\n    if let Err(error) = creation_result {\n        // why?\n        error!(\"Folder creation failed.\");\n        if let Some(src) = error.source() {\n            error!(\"source: {src}\");\n        } else {\n            error!(\"source not marked.\");\n        }\n        error!(\"error: {error}\");\n        error!(\"kind: {}\", error.kind());\n        panic!()\n    }\n    // Was the folder there?\n    assert!(file_found, \"Directory was not created, or did not show up when listed.\");\n}\n\n#[test]\n// make dir, and some test items. list items.\nfn enter_and_list_directory() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    let mut mounted_fs_path = mount_point.path().to_path_buf();\n    \n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n\n    // Make test folder\n    mounted_fs_path.push(\"test\");\n    std::fs::create_dir(&mounted_fs_path).unwrap();\n\n    // Make a directory to look for.\n    let mut hidden_folder_path = mounted_fs_path.clone();\n    hidden_folder_path.push(\"hidden\");\n\n    std::fs::create_dir(hidden_folder_path).unwrap();\n\n    // Read test folder\n    let result = std::fs::read_dir(&mounted_fs_path).unwrap();\n\n    let mut found: bool = false;\n    for i in result {\n        if i.unwrap().file_name() == OsStr::new(\"hidden\") {\n            found = true;\n        }\n    }\n\n    \n    // cleanup\n    thread::sleep(Duration::from_millis(500));\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n    assert!(found);\n}\n\n\n#[test]\n// Make sure the dot directory exists and refers to the parent when listing.\nfn check_for_dot() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    let mut mounted_fs_path = mount_point.path().to_path_buf();\n    \n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n    \n    // Make test folder\n    mounted_fs_path.push(\"test\");\n    std::fs::create_dir(&mounted_fs_path).unwrap();\n\n    // Make marker folder\n    let mut mark_applier = mounted_fs_path.clone();\n    mark_applier.push(\"hello_everybody\");\n    std::fs::create_dir(mark_applier).unwrap();\n\n    // Check listing the dot is the same as listing the parent.\n    let mut dotted = mounted_fs_path.clone();\n    dotted.push(\".\");\n    let dot_result = std::fs::read_dir(&dotted).unwrap();\n    let parent_result = std::fs::read_dir(&mounted_fs_path).unwrap();\n\n    // Do they match?\n    // This is the grossest thing ever\n    let mut any_different: bool = false;\n    for i in dot_result.into_iter().zip(parent_result.into_iter()) {\n        let (a, b) = i;\n        if a.unwrap().path() != b.unwrap().path() {\n            any_different = true;\n        }\n    }\n    \n    // cleanup\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n    assert!(!any_different);\n}\n\n#[test]\n// Make a dir and move it to see if rename is working.\nfn move_empty_directory() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    let mut test_dir = mount_point.path().to_path_buf();\n    test_dir.push(\"test\");\n    let mut moved_dir = mount_point.path().to_path_buf();\n    moved_dir.push(\"moved\");\n    \n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n    \n    // Make test folder\n    std::fs::create_dir(&test_dir).unwrap();\n\n    // move it\n    std::fs::rename(&test_dir, &moved_dir).unwrap();\n\n    // Does it exist?\n    let moved: bool = std::fs::exists(&moved_dir).unwrap();\n    \n    // cleanup\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n\n    // Make sure it moved.\n    assert!(moved);\n}\n\n#[test]\n// Try removing a directory\nfn directory_creation_and_removal() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    // Test dir\n    let mut test_dir = mount_point.path().to_path_buf();\n    test_dir.push(\"test\");\n\n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n    \n    // Make test folder\n    std::fs::create_dir(&test_dir).unwrap();\n\n    // move it\n    let deleted = std::fs::remove_dir(&test_dir);\n    \n    // cleanup\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n\n    // Make sure it was deleted\n    assert!(deleted.is_ok());\n}\n\n#[test]\n#[ignore = \"Directory rename bug is too illusive, cant fix rn.\"]\n// Higher level renaming test\nfn rename_lots_of_items() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n\n    // Hilariously we will just collect all the results we get so we can check\n    // them all at the end after unmount.\n    let mut results: Vec<std::io::Result<()>> = Vec::new();\n    \n    // A lot of directories\n    let mut directories: Vec<String> = Vec::new();\n    for i in 0..1000 {\n        let mut new_dir = mount_point.path().to_path_buf();\n        let dir_name = format!(\"dir_{i}\");\n        new_dir.push(dir_name.clone());\n        directories.push(dir_name);\n        let go = std::fs::create_dir(&new_dir);\n        results.push(go);\n    }\n\n    // A lot of files\n    let mut files: Vec<String> = Vec::new();\n    for i in 0..1000 {\n        let mut new_file = mount_point.path().to_path_buf();\n        let dir_name = format!(\"file_{i}.txt\");\n        new_file.push(dir_name.clone());\n        files.push(dir_name);\n        // Doesn't need any particular content.\n        let go = std::fs::write(new_file, [1,2,3,4]);\n        results.push(go);\n    }\n    \n    // Shuffle for fun\n    let mut all_items: Vec<String> = Vec::new();\n    all_items.extend(directories);\n    all_items.extend(files);\n    \n    let mut random: ThreadRng = rand::rng();\n    all_items.shuffle(&mut random);\n    \n    // How many do we have\n    let number_made: usize = all_items.len();\n    \n    // Go rename all of them\n    for name in all_items {\n        let new_name: String = format!(\"new_{name}\");\n        let mut previous = mount_point.path().to_path_buf();\n        let mut new = mount_point.path().to_path_buf();\n        previous.push(name);\n        new.push(new_name);\n        let go = std::fs::rename(previous, new);\n        results.push(go);\n    }\n\n    // Make sure the directory still contains the correct number of items. (ie we didn't duplicate anything.)\n    // Listing also returns `.`, but rust (or linux?) drops it here if the directory is not empty. (in theory)\n    \n    // We also cant error out before the unmount, so we get a lil goofy here.\n    let mut list_count: usize = 0;\n    let mut list_names: Vec<String> = Vec::new();\n    let list = std::fs::read_dir(&mount_point);\n    if let Ok(listed) = list {\n        // listing was ok\n        for i in listed {\n            if let Err(error) = i {\n                // Something is amiss about this entry\n                results.push(Err(error));\n            } else {\n                // i is good\n                let entry = i.unwrap();\n                // Skip if this is the `.` item\n                if entry.file_name().to_string_lossy().into_owned() == \".\" {\n                    // skip\n                    continue;\n                }\n                list_count += 1;\n                list_names.push(entry.file_name().to_string_lossy().into_owned());\n            }\n        };\n    } else {\n        // Listing failed\n        results.push(Err(std::io::Error::other(\"Listing failed.\")));\n    }\n    \n    // Now, because somebody thought this was a good idea (it probably is overall, just not great for FS work) the\n    // returned Result<> from filesystem operations seems to be holding references to the currently open filesystem.\n    // So we have to extract everything. We will turn the errors into strings if they exist.\n\n    // Loop over them, and only on the errors, make strings\n    // Tried doing this with iter, but couldn't finish writing it bc rust analyzer kept crashing lmao\n    let mut error_strings: Vec<String> = Vec::new();\n    for result in &results {\n        if let Err(error) = result {\n            // Make a string from that\n            let strung = error.to_string();\n            error_strings.push(strung);\n        }\n    }\n\n    // now drop the old results, we cant hold them past the unmount\n    drop(results);\n    \n    // cleanup\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n    \n    // Check all the results\n    if !error_strings.is_empty() {\n        // Something failed\n        for string in error_strings {\n            error!(\"{string}\");\n        }\n        panic!();\n    };\n\n    // Make sure there are no duplicates\n    list_names.sort_unstable();\n    let old_list_len: usize = list_names.len();\n    list_names.dedup();\n    assert_eq!(old_list_len, list_names.len());\n\n    // Every item should contain the word `new`\n    // Every item should only contain `new` once.\n    for name in &list_names {\n        // Check if it contains `new`, if it doesnt, we would like more info for testing\n        if !name.contains(\"new\") {\n            // Not great.\n            error!(\"`{name}` does not contain new.\");\n            // See if there is at least a new version of the directory.\n            if list_names.contains(&format!(\"new_{name}\")) {\n                // It did! So the move did not remove the old directory, but did make the new one.\n                error!(\"`new_{name}` does exist though.\")\n            }\n            panic!(\"New issue\")\n        }\n        assert!(name.matches(\"new\").count() == 1, \"`{name}` has new in it more than once.\");\n    }\n    \n    // Make sure we have the correct number of items.\n    assert_eq!(number_made, list_count);\n}\n\n\n// Renaming burn in test\n#[test]\n#[ignore = \"Slow.\"]\nfn rename_burn_in() {\n    for _ in 0..1000 {\n        rename_lots_of_items()\n    }\n}"
  },
  {
    "path": "tests/file.rs",
    "content": "use std::{thread, time::Duration};\n\nuse rand::{rng, rngs::ThreadRng, Rng, RngCore};\n// We want to see logs while testing.\nuse test_log::test;\n\nuse crate::test_common::test_mount_options; \npub mod test_common;\n\n#[test]\n// Make a small file (512 bytes)\nfn make_file_small() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    // Test dir\n    let mut test_dir = mount_point.path().to_path_buf();\n    test_dir.push(\"test.txt\");\n\n    // Make some content\n    let mut random: ThreadRng = rng();\n    let mut bytes: [u8; 512] = [0_u8; 512];\n    random.fill(&mut bytes);\n\n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n    \n    // Make test file\n    let write_result = std::fs::write(&test_dir, bytes);\n    \n    // cleanup\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n\n    // Make sure it was written\n    assert!(write_result.is_ok());\n}\n\n\n#[test]\n// Make a large file (8MB)\nfn make_file_large() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    // Test dir\n    let mut test_dir = mount_point.path().to_path_buf();\n    test_dir.push(\"test.txt\");\n\n    // The buffer needs to be big enough to store everything.\n    // Arrays cannot be used here, you will overflow the stack.\n    let data_size =  1024*1024*8;\n    let mut bytes: Vec<u8> = vec![0u8; data_size as usize];\n\n    // Fill-er up!\n    let mut random: ThreadRng = rng();\n    random.fill_bytes(&mut bytes);\n\n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n    \n    // Make test file\n    let write_result = std::fs::write(&test_dir, bytes);\n    \n    // cleanup\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n\n    // Make sure it was written\n    assert!(write_result.is_ok());\n}\n\n#[test]\n// Make and read file, make sure the contents match. (512 bytes)\nfn make_and_read_file_small() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    // Test dir\n    let mut test_dir = mount_point.path().to_path_buf();\n    test_dir.push(\"test.txt\");\n\n    // Make some content\n    let mut random: ThreadRng = rng();\n    let mut bytes: [u8; 512] = [0_u8; 512];\n    random.fill(&mut bytes);\n\n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n    \n    // Make test file\n    let write_result = std::fs::write(&test_dir, bytes);\n\n    // Now read it back in\n    let read_result = std::fs::read(&test_dir);\n\n    // Does it match?\n    let matched: bool;\n    if let Ok(ref read) = read_result {\n        matched = *read == bytes.to_vec();\n    } else {\n        matched = false;\n    }\n    \n    // cleanup\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n\n    // Make sure it matched.\n    assert!(write_result.is_ok());\n    assert!(read_result.is_ok());\n    assert!(matched);\n}\n\n\n#[test]\n// Make and read file, make sure the contents match. (8MB)\nfn make_and_read_file_large() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    // Test dir\n    let mut test_dir = mount_point.path().to_path_buf();\n    test_dir.push(\"test.txt\");\n\n    // The buffer needs to be big enough to store everything.\n    // Arrays cannot be used here, you will overflow the stack.\n    let data_size =  1024*1024*8;\n    let mut bytes: Vec<u8> = vec![0u8; data_size as usize];\n\n    // Fill-er up!\n    let mut random: ThreadRng = rng();\n    random.fill_bytes(&mut bytes);\n\n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n    \n    // Make test file\n    let write_result = std::fs::write(&test_dir, &bytes);\n\n    // Now read it back in\n    let read_result = std::fs::read(&test_dir);\n\n    // Does it match?\n    let matched: bool;\n    if let Ok(ref read) = read_result {\n        matched = *read == bytes.to_vec();\n    } else {\n        matched = false;\n    }\n    \n    // cleanup\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n\n    // Make sure it matched.\n    assert!(write_result.is_ok());\n    assert!(read_result.is_ok());\n    assert!(matched);\n}\n\n#[test]\n// Make a file and rename it\nfn move_file() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    let mut test_dir = mount_point.path().to_path_buf();\n    test_dir.push(\"test.txt\");\n    let mut moved_dir = mount_point.path().to_path_buf();\n    moved_dir.push(\"moved.txt\");\n    \n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n    \n    // Make test file\n    let content: [u8; 512] = [0_u8; 512];\n    let write_result = std::fs::write(&test_dir, content);\n\n    // Rename it\n    let rename_result = std::fs::rename(&test_dir, &moved_dir);\n\n    // Does it exist?\n    let moved: bool = std::fs::exists(&moved_dir).unwrap();\n    \n    // cleanup\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n\n    // Make sure it moved.\n    assert!(write_result.is_ok());\n    assert!(rename_result.is_ok());\n    assert!(moved);\n}\n\n#[test]\n// Delete a file\nfn delete_file() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    let mut test_file = mount_point.path().to_path_buf();\n    test_file.push(\"test.txt\");\n    \n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n    \n    // Make test file\n    let content: [u8; 512] = [0_u8; 512];\n    let write_result = std::fs::write(&test_file, content);\n\n    // Delete it\n    let delete_result = std::fs::remove_file(&test_file);\n\n    // Does it exist?\n    let removed: bool = std::fs::exists(&test_file).unwrap();\n    \n    // cleanup\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n\n    // Make sure it moved.\n    assert!(write_result.is_ok());\n    assert!(delete_result.is_ok());\n    assert!(!removed);\n}\n\n\n#[test]\n// Make and read file, make sure the contents match, then do it again to the same file. (512 bytes, 1MB)\nfn update_file_small() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    // Test dir\n    let mut test_dir = mount_point.path().to_path_buf();\n    test_dir.push(\"test.txt\");\n\n    // The buffer needs to be big enough to store everything.\n    // Arrays cannot be used here, you will overflow the stack.\n    let data_size =  512;\n    let mut bytes: Vec<u8> = vec![0u8; data_size as usize];\n\n    // Fill-er up!\n    let mut random: ThreadRng = rng();\n    random.fill_bytes(&mut bytes);\n\n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n    \n    // Make test file\n    let write_result = std::fs::write(&test_dir, &bytes);\n\n    // Now read it back in\n    let read_result = std::fs::read(&test_dir);\n\n    // Does it match?\n    let first_matched: bool;\n    if let Ok(ref read) = read_result {\n        first_matched = *read == bytes.to_vec();\n    } else {\n        first_matched = false;\n    }\n    \n\n    //\n    // Now we will update the file\n    //\n\n    // New bytes will also be twice as large\n\n    let mut new_bytes: Vec<u8> = vec![0u8; data_size as usize * 2];\n\n    // Fill-er up!\n    random.fill_bytes(&mut new_bytes);\n\n    // write again\n    let second_write_result = std::fs::write(&test_dir, &new_bytes);\n\n    // read again\n    let second_read_result = std::fs::read(&test_dir);\n\n    let second_matched: bool;\n    if let Ok(ref read) = second_read_result {\n        second_matched = *read == new_bytes.to_vec();\n    } else {\n        second_matched = false;\n    }\n\n    // cleanup\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n\n    // Did all that work?\n    assert!(write_result.is_ok());\n    assert!(read_result.is_ok());\n    assert!(first_matched);\n    assert!(second_write_result.is_ok());\n    assert!(second_read_result.is_ok());\n    assert_eq!(new_bytes.to_vec(), second_read_result.unwrap());\n    assert!(second_matched);\n}\n\n\n#[test]\n// Make and read file, make sure the contents match, then do it again to the same file. (8MB, 16MB)\nfn update_file_large() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let thread_mount_path = mount_point.path().to_path_buf();\n    let mount_options = test_mount_options();\n    \n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread = thread::spawn(move || {\n        thread::sleep(Duration::from_millis(100)); // Pause to let the debugger see the thread\n        // If we dont pause, breakpoints dont work.\n        // This blocks until the unmount happens.\n        fuse_mt::mount(fs, &thread_mount_path, &mount_options)\n    });\n\n    // Test dir\n    let mut test_dir = mount_point.path().to_path_buf();\n    test_dir.push(\"test.txt\");\n\n    // The buffer needs to be big enough to store everything.\n    // Arrays cannot be used here, you will overflow the stack.\n    let data_size =  1024*1024*8;\n    let mut bytes: Vec<u8> = vec![0u8; data_size as usize];\n\n    // Fill-er up!\n    let mut random: ThreadRng = rng();\n    random.fill_bytes(&mut bytes);\n\n    // wait for it to start...\n    thread::sleep(Duration::from_millis(500));\n    \n    // Make test file\n    let write_result = std::fs::write(&test_dir, &bytes);\n\n    // Now read it back in\n    let read_result = std::fs::read(&test_dir);\n\n    // Does it match?\n    let first_matched: bool;\n    if let Ok(ref read) = read_result {\n        first_matched = *read == bytes.to_vec();\n    } else {\n        first_matched = false;\n    }\n    \n\n    //\n    // Now we will update the file\n    //\n\n    // New bytes will also be twice as large\n\n    let mut new_bytes: Vec<u8> = vec![0u8; data_size as usize * 2];\n\n    // Fill-er up!\n    random.fill_bytes(&mut new_bytes);\n\n    // write again\n    let second_write_result = std::fs::write(&test_dir, &new_bytes);\n\n    // read again\n    let second_read_result = std::fs::read(&test_dir);\n\n    let second_matched: bool;\n    if let Ok(ref read) = second_read_result {\n        second_matched = *read == new_bytes.to_vec();\n    } else {\n        second_matched = false;\n    }\n\n    // cleanup\n    test_common::unmount(mount_point.path().to_path_buf());\n    let unmount_result = mount_thread.join();\n    unmount_result.unwrap().unwrap(); // Unmounting the fs should not fail.\n\n    // Did all that work?\n    assert!(write_result.is_ok());\n    assert!(read_result.is_ok());\n    assert!(first_matched);\n    assert!(second_write_result.is_ok());\n    assert!(second_read_result.is_ok());\n    assert!(second_matched);\n}"
  },
  {
    "path": "tests/mount_filesystem.rs",
    "content": "use std::{thread, time::Duration};\n\nuse test_log::test; // We want to see logs while testing.\nuse crate::test_common::test_mount_options;\npub mod test_common;\n\n#[test]\n// Try starting up the filesystem\nfn mount_filesystem() {\n    let fs = test_common::start_filesystem();\n    let mount_point = test_common::get_actually_temp_dir();\n    let mount_path = mount_point.path().to_path_buf();\n\n    // fs needs to be mounted in another thread bc it blocks\n    let mount_thread_result = thread::spawn(move || {\n        // This blocks this thread until the unmount happens.\n        fuse_mt::mount(fs, &mount_path, &test_mount_options())\n    });\n\n    // wait for it to start...\n    thread::sleep(Duration::from_millis(1000));\n\n    // Immediately unmount.\n    // The mounted fs makes/lives in a folder named `fluster_test`, but we just unmount everything\n    // in the folder that containers `fluster_test`\n    test_common::unmount(mount_point.path().to_path_buf());\n    \n    // Make sure the mount actually happened.\n    // Two unwraps, one for the join, one for the result of fuse_mf::mount\n    mount_thread_result.join().unwrap().unwrap();\n}\n"
  },
  {
    "path": "tests/start_filesystem.rs",
    "content": "use test_log::test; // We want to see logs while testing.\npub mod test_common;\n\n#[test]\n// Try starting up the filesystem\nfn filesystem_starts() {\n    test_common::start_filesystem();\n}\n"
  },
  {
    "path": "tests/test_common.rs",
    "content": "use std::{ffi::OsStr, path::PathBuf};\n\nuse fluster_fs::filesystem::filesystem_struct::{FilesystemOptions, FlusterFS};\nuse fuse_mt::FuseMT;\nuse log::{debug, info};\nuse tempfile::{TempDir, tempdir};\n\n//\n// Helper functions\n//\n\n// Temporary directories for virtual disks\npub fn get_new_temp_dir() -> TempDir {\n    info!(\"Getting a persistent temp dir for testing...\");\n    let mut dir = tempdir().unwrap();\n    dir.disable_cleanup(true);\n    debug!(\n        \"Created a temp directory at {}, it will not be deleted on exit.\",\n        dir.path().to_string_lossy()\n    );\n    dir\n}\n\n// Temporary directories for virtual disks\npub fn get_actually_temp_dir() -> TempDir {\n    info!(\"Getting a non-persistent temp dir for testing...\");\n    tempdir().unwrap()\n}\n\n// Create a temporary filesystem, and returns the mt thing used to do the mount.\npub fn start_filesystem() -> FuseMT<FlusterFS> {\n    info!(\"Starting temp test filesystem...\");\n    let temp_dir = get_new_temp_dir();\n    let floppy_drive: PathBuf = PathBuf::new(); // This is never read since we are using temporary disks.\n    // Disable backups, since we don't use those in tests for obvious reasons.\n    let fs_options = FilesystemOptions::new(Some(temp_dir.path().to_path_buf()), floppy_drive, Some(false), false);\n    let started = FlusterFS::start(&fs_options);\n    // MT thing that is actually used for mounting.\n    // Zero threads for fully sync.\n    fuse_mt::FuseMT::new(started, 0)\n}\n\npub fn unmount(mount_point: PathBuf) {\n    info!(\"Unmounting filesystem....\");\n    let result = std::process::Command::new(\"fusermount\")\n        .arg(\"-u\")\n        .arg(mount_point)\n        .status().unwrap();\n    assert!(result.success());\n}\n\npub fn test_mount_options() -> Vec<&'static OsStr> {\n    [\n        // No spaces after `-o` or it does not work lol.\n        OsStr::new(\"-onodev\"), // Disable dev devices\n        OsStr::new(\"-onoatime\"), // No access times\n        OsStr::new(\"-onosuid\"), // Ignore file/folder permissions (lol)\n        OsStr::new(\"-orw\"), // Read/Write\n        OsStr::new(\"-oexec\"), // Files are executable\n        OsStr::new(\"-osync\"), // No async.\n        OsStr::new(\"-odirsync\"), // No async\n        // Set the name of the mount point. This should create a\n        // directory in the temp folder with this same name.\n        OsStr::new(\"-ofsname=fluster_test\"), \n    ].to_vec()\n}"
  },
  {
    "path": "windows.md",
    "content": "This is my punishment for not using Linux in the year of the Linux desktop smh.\nSpecial thanks to [Chris Harringon](https://chris.harrington.mn/)!\n\n# How to use fluster on windows\n\nYou cannot build fluster on windows due to the `fuser` crate linking to unix stuff in libc, even if you install a compatibility library like WinFsp. Trust me, I tried.\n\nYou need to build/run from inside of WSL.\n\n# Step 0: Usb storage module\nThe default WSL kernels do not have the usb_storage module enabled, we will need to build our own.\n\nFollow [this](https://chris.harrington.mn/project/2022/07/30/wsl2-usb-storage.html) up until the `Windows USB-IP` section.\n`sudo modprobe usb-storage` should not fail. You can check if it was actually loaded with `lsmod`.\n\n# Step 1: USB floppy passthrough.\n\nYou will also need to pass through the floppy drive to WSL.\nThis will not persist through a reboot, or restarting WSL.\n\nYou cannot mount a floppy drive normally with `wsl --mount`, so we will have to pass through the USB device.\nThis obviously assumes you have a USB floppy drive. If you have an internal drive you want to use, I cannot help you. Good luck!\n\nOn windows you will need the `usbipd-win` package\n`winget install --interactive --exact dorssel.usbipd-win`\n\nThen find your drive with `usbipd list`\n\nFor reference, my drive shows up like so:\n```\n> usbipd list\n\nConnected:\nBUSID  VID:PID    DEVICE                                                        STATE\n...\n6-11   0644:0000  TEAC USB Floppy                                               Not shared\n```\n\nNow we will bind the floppy to WSL using the bus id from the above command. (This requires an elevated command prompt / powershell window)\n```\nusbipd bind --busid X-X\n```\nThere will be no output.\n\nNow we attatch it to WSL\n```\nusbipd attach --wsl --busid X-X\n```\nYou will see some information about it finding your WSL distribution, and probably hear a USB discconect sound.\nThe floppy drive will also (probably) spin up, assuming you have a disk inserted.\n\nDouble check that the floppy is there\n```\n$ lsblk\nsde   8:64   1   1.4M  0 disk\n```\n\nTake note of which `sdX` device it is. Mine happens to be `sde`.\nThis will be the path that you use when starting Fluster.\n\n***A grain of salt***\nThis is SO unstable, you might also have to disable, then uninstall the floppy drive in device manager first.\nIf your disk is spinning forever without seeming to do anything, you need to wait it out. Pulling out the disk\n(at least on my drive) in that state makes USBIPD give up and unmount the drive.\n\n# Step 2.5:\nYou need to allow `other user` mounting in fuse to let you access Fluster! from windows through WSL.\n`sudo nano /etc/fuse.conf`\nUncomment `#user_allow_other`\n\n# Step 3: Building and running: \nRequired dependancies (non exhaustive):\n- build-essential\n- libfuse3-dev\n- rust (duh)\n\nOpen the Fluster! source code directory (the one that contains cargo.toml).\nBuild with `cargo build --release`\n(You can also build with `--profile floppy` for a smaller binary. Using `upx --best` on it should make it fit on a floppy if you\nreally wanna have fun with it. Currently shrinks to under 800kb!)\n\nPick a folder to mount to, for this example I will mount in `~/mounted/`.\nRemember your floppy drive path (The SD one). For this example, my drive is at `sde`.\n\n### YOU BETTER MAKE DAMN SURE YOU'RE PASSING IN THE FLOPPY DRIVE\n\n### READ THIS\nFluster! WILL overwrite data on whatever block device you pass it.\nIf you do not know what \"block device\" even means, this is your final warning. Do NOT continue further.\n\nSince we will be writing directly to the floppy disk without a pre-existing filesystem (we are the file system!) we\nunfortunately need to escalate permissions.\n\nSo we'll just run it at sudo. Great idea I know.\n\nThere is probably a safer way to do this (Such as using udev rules), but I got bored reading the documentation.\n\n\nRun fluster:\n- Go to the output directory for the binary you built (./target/release/)\n\nRun it as sudo with:\n`sudo ./fluster_fs --block-device-path \"/dev/sdX\" --mount-point \"/home/(your username here)/mounted/fluster\"`\n\nIf you don't want to run as sudo/root, you're smart.\nSmart enough to figure out another solution. Good luck!\n\nBe warned, this will consume the terminal window you started the program with.\nTry using `tmux` to split your window if needed. (Must be installed ofc, Google it.)\n\nYou can also run with debug info by pre-pending `RUST_LOG=debug` to the command if you're a nerd.\nThis does add some performance overhead, which would only really matter if you're using the\nsecret `--use-virtual-disks` option.\n"
  }
]