From 1c8badd19c19e6f0a285138a608e9c78fdcd7b9c Mon Sep 17 00:00:00 2001 From: Allison Karlitskaya Date: Wed, 6 Nov 2024 20:46:17 +0100 Subject: [PATCH] rust: drop all Rust code from this repository We've decided to focus on https://github.com/containers/composefs-rs as our Rust API for composefs. --- .github/workflows/rust.yml | 27 - .github/workflows/test.yaml | 20 - Cargo.toml | 18 - rust/composefs-sys/Cargo.toml | 30 - rust/composefs-sys/LICENSE-APACHE | 202 ----- rust/composefs-sys/LICENSE-MIT | 19 - rust/composefs-sys/README.md | 6 - rust/composefs-sys/build.rs | 7 - rust/composefs-sys/src/lib.rs | 62 -- rust/composefs/Cargo.toml | 34 - rust/composefs/LICENSE-APACHE | 202 ----- rust/composefs/LICENSE-MIT | 19 - rust/composefs/README.md | 7 - rust/composefs/examples/cfs-dumpfile2tar.rs | 161 ---- rust/composefs/src/dumpfile.rs | 867 -------------------- rust/composefs/src/fsverity.rs | 69 -- rust/composefs/src/lib.rs | 70 -- rust/composefs/src/mkcomposefs.rs | 119 --- 18 files changed, 1939 deletions(-) delete mode 100644 .github/workflows/rust.yml delete mode 100644 Cargo.toml delete mode 100644 rust/composefs-sys/Cargo.toml delete mode 100644 rust/composefs-sys/LICENSE-APACHE delete mode 100644 rust/composefs-sys/LICENSE-MIT delete mode 100644 rust/composefs-sys/README.md delete mode 100644 rust/composefs-sys/build.rs delete mode 100644 rust/composefs-sys/src/lib.rs delete mode 100644 rust/composefs/Cargo.toml delete mode 100644 rust/composefs/LICENSE-APACHE delete mode 100644 rust/composefs/LICENSE-MIT delete mode 100644 rust/composefs/README.md delete mode 100644 rust/composefs/examples/cfs-dumpfile2tar.rs delete mode 100644 rust/composefs/src/dumpfile.rs delete mode 100644 rust/composefs/src/fsverity.rs delete mode 100644 rust/composefs/src/lib.rs delete mode 100644 rust/composefs/src/mkcomposefs.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 443e1631..00000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Inspired by https://github.com/rust-analyzer/rust-analyzer/blob/master/.github/workflows/ci.yaml -# but tweaked in several ways. If you make changes here, consider doing so across other -# repositories in e.g. ostreedev etc. -name: Rust - -permissions: - actions: read - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: {} - -env: - CARGO_TERM_COLOR: always - -jobs: - cargo-deny: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: EmbarkStudios/cargo-deny-action@v1 - with: - log-level: warn - command: check bans sources licenses diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0f3bd595..01119ba9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -169,26 +169,6 @@ jobs: run: sudo ./tests/integration.sh - name: Integration tests (fsverity required) run: sudo env WITH_TEMP_VERITY=1 unshare -m ./tests/integration.sh - rust: - needs: build-noasan - runs-on: ubuntu-latest - steps: - - run: sudo apt-get update -y - - name: Checkout repository - uses: actions/checkout@v3 - - name: Download - uses: actions/download-artifact@v4 - with: - name: composefs-noasan.tar - - run: sudo tar -C / -xvf composefs.tar - - name: Cache Dependencies - uses: Swatinem/rust-cache@v2 - with: - key: "rust-main" - - name: Rust (default features) - run: cargo test - - name: Rust (no features) - run: cargo test --no-default-features distcheck: runs-on: ubuntu-latest steps: diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index b8574490..00000000 --- a/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[workspace] -members = ["rust/composefs", "rust/composefs-sys"] -resolver = "2" - -[profile.dev] -opt-level = 1 # No optimizations are too slow for us. - -[profile.release] -lto = "thin" -# No need to support unwinding -panic = "abort" -# We assume we're being delivered via e.g. RPM which supports split debuginfo -debug = true - -[profile.releaselto] -codegen-units = 1 -inherits = "release" -lto = "yes" diff --git a/rust/composefs-sys/Cargo.toml b/rust/composefs-sys/Cargo.toml deleted file mode 100644 index d3ce70f7..00000000 --- a/rust/composefs-sys/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "composefs-sys" -description = "Rust library wrapping the libcomposefs C library" -keywords = ["composefs"] -version = "0.1.3" -edition = "2021" -links = "composefs" -build = "build.rs" -license = "MIT OR Apache-2.0" -readme = "README.md" - -[package.metadata.docs.rs] -all-features = true -rustc-args = ["--cfg", "docsrs"] -rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] - -[package.metadata.system-deps.composefs] -name = "composefs" -version = "1" - -[features] -# Depend on 1.0.4 APIs -v1_0_4 = [] - -[build-dependencies] -system-deps = "6" - -[dev-dependencies] -anyhow = "1" -tempfile = "3" diff --git a/rust/composefs-sys/LICENSE-APACHE b/rust/composefs-sys/LICENSE-APACHE deleted file mode 100644 index 8f71f43f..00000000 --- a/rust/composefs-sys/LICENSE-APACHE +++ /dev/null @@ -1,202 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/rust/composefs-sys/LICENSE-MIT b/rust/composefs-sys/LICENSE-MIT deleted file mode 100644 index dbd7f657..00000000 --- a/rust/composefs-sys/LICENSE-MIT +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2016 The openat Developers - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/rust/composefs-sys/README.md b/rust/composefs-sys/README.md deleted file mode 100644 index b3c6ccca..00000000 --- a/rust/composefs-sys/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# composefs-sys - -[![composefs-sys on crates.io](https://img.shields.io/crates/v/composefs-sys)](https://crates.io/crates/composefs-sys) -[![Documentation](https://img.shields.io/badge/docs-latest%20version-brightgreen.svg)](https://docs.rs/composefs-sys) - -Low level unsafe `-sys` style wrapper library for linking to the `libcomposefs` C library. diff --git a/rust/composefs-sys/build.rs b/rust/composefs-sys/build.rs deleted file mode 100644 index cfabe828..00000000 --- a/rust/composefs-sys/build.rs +++ /dev/null @@ -1,7 +0,0 @@ -fn main() { - #[cfg(not(docsrs))] - if let Err(s) = system_deps::Config::new().probe() { - println!("cargo:warning={s}"); - std::process::exit(1); - } -} diff --git a/rust/composefs-sys/src/lib.rs b/rust/composefs-sys/src/lib.rs deleted file mode 100644 index 4ade0725..00000000 --- a/rust/composefs-sys/src/lib.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! # Bindings for libcomposefs -//! -//! This crate contains a few manually maintained system bindings for libcomposefs. -pub const LCFS_SHA256_DIGEST_LEN: usize = 32; - -extern "C" { - pub fn lcfs_compute_fsverity_from_fd( - digest: *mut u8, - fd: std::os::raw::c_int, - ) -> std::os::raw::c_int; - pub fn lcfs_fd_get_fsverity(digest: *mut u8, fd: std::os::raw::c_int) -> std::os::raw::c_int; - #[cfg(feature = "v1_0_4")] - pub fn lcfs_fd_enable_fsverity(fd: std::os::raw::c_int) -> std::os::raw::c_int; -} - -/// Convert an integer return value into a `Result`. -pub fn map_result(r: std::os::raw::c_int) -> std::io::Result<()> { - match r { - 0 => Ok(()), - _ => Err(std::io::Error::last_os_error()), - } -} - -#[cfg(test)] -mod tests { - use anyhow::Result; - use std::io::{Seek, Write}; - use std::os::fd::AsRawFd; - - use super::*; - - #[test] - #[cfg(feature = "v1_0_4")] - fn test_fd_enable_fsverity() -> Result<()> { - // We can't require fsverity in our test suite, so just verify we can call the - // function. - let mut tf = tempfile::NamedTempFile::new()?; - tf.write_all(b"hello")?; - let tf = std::fs::File::open(tf.path())?; - let _ = unsafe { lcfs_fd_enable_fsverity(tf.as_raw_fd()) }; - Ok(()) - } - - #[test] - fn test_digest() -> Result<()> { - for f in [lcfs_compute_fsverity_from_fd, lcfs_fd_get_fsverity] { - let mut tf = tempfile::tempfile()?; - tf.write_all(b"hello world")?; - let mut buf = [0u8; LCFS_SHA256_DIGEST_LEN]; - tf.seek(std::io::SeekFrom::Start(0))?; - unsafe { f(buf.as_mut_ptr(), tf.as_raw_fd()) }; - assert_eq!( - buf, - [ - 30, 46, 170, 66, 2, 215, 80, 164, 17, 116, 238, 69, 73, 112, 185, 44, 27, 194, - 249, 37, 177, 227, 80, 118, 216, 199, 213, 245, 99, 98, 186, 100 - ] - ); - } - Ok(()) - } -} diff --git a/rust/composefs/Cargo.toml b/rust/composefs/Cargo.toml deleted file mode 100644 index d9f6d76d..00000000 --- a/rust/composefs/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "composefs" -version = "0.1.3" -edition = "2021" -description = "Rust library for the composefs filesystem" -keywords = ["composefs"] -license = "MIT OR Apache-2.0" -repository = "https://github.com/containers/composefs" -rust-version = "1.70.0" -readme = "README.md" - -[package.metadata.docs.rs] -all-features = true -rustc-args = ["--cfg", "docsrs"] -rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] - -[lib] -name = "composefs" -path = "src/lib.rs" - -[features] -# Depend on 1.0.4 APIs -v1_0_4 = ["composefs-sys/v1_0_4"] -default = ["v1_0_4"] - -[dependencies] -anyhow = "1.0" -libc = "0.2" -composefs-sys = { version = "0.1.0", path = "../composefs-sys" } - -[dev-dependencies] -similar-asserts = "1.5.0" -tar = "0.4.38" -tempfile = "3.2.0" diff --git a/rust/composefs/LICENSE-APACHE b/rust/composefs/LICENSE-APACHE deleted file mode 100644 index 8f71f43f..00000000 --- a/rust/composefs/LICENSE-APACHE +++ /dev/null @@ -1,202 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/rust/composefs/LICENSE-MIT b/rust/composefs/LICENSE-MIT deleted file mode 100644 index dbd7f657..00000000 --- a/rust/composefs/LICENSE-MIT +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2016 The openat Developers - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/rust/composefs/README.md b/rust/composefs/README.md deleted file mode 100644 index 95f10b7d..00000000 --- a/rust/composefs/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# composefs Rust crate - -[![composefs on crates.io](https://img.shields.io/crates/v/composefs)](https://crates.io/crates/composefs) -[![Documentation](https://img.shields.io/badge/docs-latest%20version-brightgreen.svg)](https://docs.rs/composefs) - -This is a Rust crate that builds on `composefs-sys` and also -adds wrappers for invoking the external `mkfs.composefs` and `composefs-info dump` binaries. diff --git a/rust/composefs/examples/cfs-dumpfile2tar.rs b/rust/composefs/examples/cfs-dumpfile2tar.rs deleted file mode 100644 index 07bcee80..00000000 --- a/rust/composefs/examples/cfs-dumpfile2tar.rs +++ /dev/null @@ -1,161 +0,0 @@ -use std::fs::File; -use std::io::BufRead; -use std::io::BufReader; -use std::io::BufWriter; -use std::io::Read; -use std::io::Write; -use std::os::unix::ffi::OsStrExt as _; -use std::path::Path; -use std::path::PathBuf; - -use anyhow::anyhow as error; -use anyhow::Context; -use anyhow::Result; -use composefs::dumpfile::Entry; -use composefs::dumpfile::Item; - -const PAX_SCHILY_XATTR: &[u8] = b"SCHILY.xattr."; - -/// Convert an input path to relative as it's what tar wants. -fn make_relative(p: &Path) -> &Path { - let p = p.strip_prefix("/").unwrap_or(p); - // Special case `/` -> `.` - // All other paths just have the leading `/` removed. - if p.as_os_str().is_empty() { - Path::new(".") - } else { - p - } -} - -fn entry_to_tar(e: &Entry, w: &mut tar::Builder) -> Result<()> { - let path = make_relative(&e.path); - let mut h = tar::Header::new_ustar(); - let fmt = e.mode & libc::S_IFMT; - h.set_mode(e.mode); - h.set_uid(e.uid.into()); - h.set_gid(e.gid.into()); - // Discard nanos currently - h.set_mtime(e.mtime.sec); - match e.xattrs.as_slice() { - [] => {} - xattrs => { - // Match "--pax-option=exthdr.name=%d/PaxHeaders/%f" as recommended by - // https://reproducible-builds.org/docs/archives/ - let dirname = path.parent().unwrap_or(Path::new("/")); - let name = path.file_name().unwrap_or(path.as_os_str()); - let mut header_name = PathBuf::from(dirname); - header_name.push("PaxHeaders"); - header_name.push(name); - let mut pax_header = tar::Header::new_ustar(); - let mut pax_data = Vec::new(); - for xattr in xattrs { - let key = xattr.key.as_bytes(); - let value = &xattr.value; - let data_len = PAX_SCHILY_XATTR.len() + key.len() + value.len() + 3; - // Calculate the total length, including the length of the length field - let mut len_len = 1; - while data_len + len_len >= 10usize.pow(len_len.try_into().unwrap()) { - len_len += 1; - } - write!(pax_data, "{} ", data_len + len_len)?; - pax_data.write_all(PAX_SCHILY_XATTR)?; - pax_data.write_all(key)?; - pax_data.write_all(b"=")?; - pax_data.write_all(value)?; - pax_data.write_all(b"\n")?; - } - assert!(!pax_data.is_empty()); - pax_header.set_path(header_name)?; - pax_header.set_size(pax_data.len().try_into().unwrap()); - pax_header.set_entry_type(tar::EntryType::XHeader); - pax_header.set_cksum(); - w.append(&pax_header, &*pax_data)?; - } - } - match &e.item { - Item::Regular { - inline_content, - size, - .. - } => { - h.set_entry_type(tar::EntryType::Regular); - if let Some(inline_content) = inline_content.as_deref() { - h.set_size(inline_content.len().try_into()?); - w.append_data(&mut h, path, inline_content)?; - } else if *size == 0 { - h.set_size(0); - w.append_data(&mut h, path, std::io::empty())?; - } else { - anyhow::bail!("Cannot convert non-inline/non-zero-size file to tar"); - } - } - Item::Device { rdev, .. } => { - let rdev: u64 = (*rdev).into(); - if fmt == libc::S_IFBLK { - h.set_entry_type(tar::EntryType::Block); - } else if fmt == libc::S_IFCHR { - h.set_entry_type(tar::EntryType::Char) - } else { - panic!("Unhandled mode for device entry: {e}"); - } - let major = ((rdev >> 32) & 0xffff_f000) | ((rdev >> 8) & 0x0000_0fff); - let minor = ((rdev >> 12) & 0xffff_ff00) | ((rdev) & 0x0000_00ff); - h.set_device_major(major as u32)?; - h.set_device_minor(minor as u32)?; - w.append_data(&mut h, path, std::io::empty())?; - } - Item::Symlink { target, .. } => { - h.set_entry_type(tar::EntryType::Symlink); - w.append_link(&mut h, path, target)?; - } - Item::Hardlink { target } => { - h.set_entry_type(tar::EntryType::Link); - w.append_link(&mut h, path, target)?; - } - Item::Fifo { .. } => { - h.set_entry_type(tar::EntryType::Fifo); - w.append_data(&mut h, path, std::io::empty())?; - } - Item::Directory { .. } => { - h.set_entry_type(tar::EntryType::Directory); - w.append_data(&mut h, path, std::io::empty())?; - } - } - - Ok(()) -} - -fn dumpfile_to_tar(src: impl Read, dst: impl Write) -> Result<()> { - let src = BufReader::new(src); - let dst = BufWriter::new(dst); - let mut dst = tar::Builder::new(dst); - for line in src.lines() { - let line = line?; - let entry = Entry::parse(&line)?; - entry_to_tar(&entry, &mut dst).with_context(|| format!("Processing entry: {entry}"))?; - } - dst.into_inner()?.into_inner().map_err(|e| e.into_error())?; - Ok(()) -} - -fn run(args: &[String]) -> Result<()> { - let src = args.get(1).ok_or_else(|| error!("Missing src"))?; - let dst = args.get(2).ok_or_else(|| error!("Missing dest"))?; - - let src = File::open(src) - .map(BufReader::new) - .with_context(|| format!("Opening {src}"))?; - let dst = File::create(dst) - .map(BufWriter::new) - .with_context(|| format!("Opening {dst}"))?; - - dumpfile_to_tar(src, dst) -} - -fn main() { - let args = std::env::args().collect::>(); - if let Err(e) = run(&args) { - eprintln!("{:#}", e); - } -} diff --git a/rust/composefs/src/dumpfile.rs b/rust/composefs/src/dumpfile.rs deleted file mode 100644 index 92d4e5c3..00000000 --- a/rust/composefs/src/dumpfile.rs +++ /dev/null @@ -1,867 +0,0 @@ -//! # Parsing and generating composefs dump file entry -//! -//! The composefs project defines a "dump file" which is a textual -//! serializion of the metadata file. This module supports parsing -//! and generating dump file entries. -use std::borrow::Cow; -use std::ffi::OsStr; -use std::ffi::OsString; -use std::fmt::Display; -use std::fmt::Write as WriteFmt; -use std::fs::File; -use std::io::BufRead; -use std::io::Write; -use std::os::unix::ffi::{OsStrExt, OsStringExt}; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::str::FromStr; -use std::usize; - -use anyhow::Context; -use anyhow::{anyhow, Result}; -use libc::S_IFDIR; - -/// Maximum size accepted for inline content. -const MAX_INLINE_CONTENT: u16 = 5000; -/// https://github.com/torvalds/linux/blob/47ac09b91befbb6a235ab620c32af719f8208399/include/uapi/linux/limits.h#L15 -/// This isn't exposed in libc/rustix, and in any case we should be conservative...if this ever -/// gets bumped it'd be a hazard. -const XATTR_NAME_MAX: usize = 255; -// See above -const XATTR_LIST_MAX: usize = u16::MAX as usize; -// See above -const XATTR_SIZE_MAX: usize = u16::MAX as usize; - -#[derive(Debug, PartialEq, Eq)] -/// An extended attribute entry -pub struct Xattr<'k> { - /// key - pub key: Cow<'k, OsStr>, - /// value - pub value: Cow<'k, [u8]>, -} -/// A full set of extended attributes -pub type Xattrs<'k> = Vec>; - -/// Modification time -#[derive(Debug, PartialEq, Eq)] -pub struct Mtime { - /// Seconds - pub sec: u64, - /// Nanoseconds - pub nsec: u64, -} - -/// A composefs dumpfile entry -#[derive(Debug, PartialEq, Eq)] -pub struct Entry<'p> { - /// The filename - pub path: Cow<'p, Path>, - /// uid - pub uid: u32, - /// gid - pub gid: u32, - /// mode (includes file type) - pub mode: u32, - /// Modification time - pub mtime: Mtime, - /// The specific file/directory data - pub item: Item<'p>, - /// Extended attributes - pub xattrs: Xattrs<'p>, -} - -#[derive(Debug, PartialEq, Eq)] -/// A serializable composefs entry. -/// -/// The `Display` implementation for this type is defined to serialize -/// into a format consumable by `mkcomposefs --from-file`. -pub enum Item<'p> { - /// A regular file - Regular { - /// Size of the file - size: u64, - /// Number of links - nlink: u32, - /// Inline content - inline_content: Option>, - /// The fsverity digest - fsverity_digest: Option, - }, - /// A character or block device node - Device { - /// Number of links - nlink: u32, - /// The device number - rdev: u64, - }, - /// A symbolic link - Symlink { - /// Number of links - nlink: u32, - /// Symlink target - target: Cow<'p, Path>, - }, - /// A hardlink entry - Hardlink { - /// The hardlink target - target: Cow<'p, Path>, - }, - /// FIFO - Fifo { - /// Number of links - nlink: u32, - }, - /// A directory - Directory { - /// Size of a directory is not necessarily meaningful - size: u64, - /// Number of links - nlink: u32, - }, -} - -/// Unescape a byte array according to the composefs dump file escaping format, -/// limiting the maximum possible size. -fn unescape_limited(s: &str, max: usize) -> Result> { - // If there are no escapes, just return the input unchanged. However, - // it must also be ASCII to maintain a 1-1 correspondence between byte - // and character. - if !s.contains('\\') && s.chars().all(|c| c.is_ascii()) { - let len = s.len(); - if len > max { - anyhow::bail!("Input {len} exceeded maximum length {max}"); - } - return Ok(Cow::Borrowed(s.as_bytes())); - } - let mut it = s.chars(); - let mut r = Vec::new(); - while let Some(c) = it.next() { - if r.len() == max { - anyhow::bail!("Input exceeded maximum length {max}"); - } - if c != '\\' { - write!(r, "{c}").unwrap(); - continue; - } - let c = it.next().ok_or_else(|| anyhow!("Unterminated escape"))?; - let c = match c { - '\\' => b'\\', - 'n' => b'\n', - 'r' => b'\r', - 't' => b'\t', - 'x' => { - let mut s = String::new(); - s.push( - it.next() - .ok_or_else(|| anyhow!("Unterminated hex escape"))?, - ); - s.push( - it.next() - .ok_or_else(|| anyhow!("Unterminated hex escape"))?, - ); - - u8::from_str_radix(&s, 16).with_context(|| anyhow!("Invalid hex escape {s}"))? - } - o => anyhow::bail!("Invalid escape {o}"), - }; - r.push(c); - } - Ok(r.into()) -} - -/// Unescape a byte array according to the composefs dump file escaping format. -fn unescape(s: &str) -> Result> { - return unescape_limited(s, usize::MAX); -} - -/// Unescape a string into a Rust `OsStr` which is really just an alias for a byte array, -/// but we also impose a constraint that it can not have an embedded NUL byte. -fn unescape_to_osstr(s: &str) -> Result> { - let v = unescape(s)?; - if v.contains(&0u8) { - anyhow::bail!("Invalid embedded NUL"); - } - let r = match v { - Cow::Borrowed(v) => Cow::Borrowed(OsStr::from_bytes(v)), - Cow::Owned(v) => Cow::Owned(OsString::from_vec(v)), - }; - Ok(r) -} - -/// Unescape a string into a Rust `Path`, which is like a byte array but -/// with a few constraints: -/// - Cannot contain an embedded NUL -/// - Cannot be empty, or longer than PATH_MAX -fn unescape_to_path(s: &str) -> Result> { - let v = unescape_to_osstr(s).and_then(|v| { - if v.is_empty() { - anyhow::bail!("Invalid empty path"); - } - let l = v.len(); - if l > libc::PATH_MAX as usize { - anyhow::bail!("Path is too long: {l} bytes"); - } - Ok(v) - })?; - let r = match v { - Cow::Borrowed(v) => Cow::Borrowed(Path::new(v)), - Cow::Owned(v) => Cow::Owned(PathBuf::from(v)), - }; - Ok(r) -} - -/// Like [`unescape_to_path`], but also ensures the path is in "canonical" -/// form; this has the same semantics as Rust https://doc.rust-lang.org/std/path/struct.Path.html#method.components -/// which in particular removes `.` and extra `//`. -/// -/// We also deny uplinks `..` and empty paths. -fn unescape_to_path_canonical(s: &str) -> Result> { - let p = unescape_to_path(s)?; - let mut components = p.components(); - let mut r = std::path::PathBuf::new(); - let Some(first) = components.next() else { - anyhow::bail!("Invalid empty path"); - }; - if first != std::path::Component::RootDir { - anyhow::bail!("Invalid non-absolute path"); - } - r.push(first); - for component in components { - match component { - // Prefix is a windows thing; I don't think RootDir or CurDir are reachable - // after the first component has been RootDir. - std::path::Component::Prefix(_) - | std::path::Component::RootDir - | std::path::Component::CurDir => { - anyhow::bail!("Internal error in unescape_to_path_canonical"); - } - std::path::Component::ParentDir => { - anyhow::bail!("Invalid \"..\" in path"); - } - std::path::Component::Normal(_) => { - r.push(component); - } - } - } - // If the input was already in normal form, - // then we can just return the original version, which - // may itself be a Cow::Borrowed, and hence we free our malloc buffer. - if r.as_os_str().as_bytes() == p.as_os_str().as_bytes() { - Ok(p) - } else { - // Otherwise return our copy. - Ok(r.into()) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum EscapeMode { - Standard, - XattrKey, -} - -/// Escape a byte array according to the composefs dump file text format. -fn escape(out: &mut W, s: &[u8], mode: EscapeMode) -> std::fmt::Result { - // Special case a single `-` as that means "no value". - if s == b"-" { - return out.write_str(r"\x2d"); - } - for c in s.iter().copied() { - // Escape `=` as hex in xattr keys. - let is_special = c == b'\\' || (matches!((mode, c), (EscapeMode::XattrKey, b'='))); - let is_printable = c.is_ascii_alphanumeric() || c.is_ascii_punctuation(); - if is_printable && !is_special { - out.write_char(c as char)?; - } else { - match c { - b'\\' => out.write_str(r"\\")?, - b'\n' => out.write_str(r"\n")?, - b'\t' => out.write_str(r"\t")?, - b'\r' => out.write_str(r"\r")?, - o => write!(out, "\\x{:02x}", o)?, - } - } - } - std::fmt::Result::Ok(()) -} - -/// If the provided string is empty, map it to `-`. -fn optional_str(s: &str) -> Option<&str> { - match s { - "-" => None, - o => Some(o), - } -} - -impl FromStr for Mtime { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let (sec, nsec) = s - .split_once('.') - .ok_or_else(|| anyhow!("Missing . in mtime"))?; - Ok(Self { - sec: u64::from_str(sec)?, - nsec: u64::from_str(nsec)?, - }) - } -} - -impl<'k> Xattr<'k> { - fn parse(s: &'k str) -> Result { - let (key, value) = s - .split_once('=') - .ok_or_else(|| anyhow!("Missing = in xattrs"))?; - let key = unescape_to_osstr(key)?; - let keylen = key.as_bytes().len(); - if keylen > XATTR_NAME_MAX { - anyhow::bail!( - "xattr name too long; max={} found={}", - XATTR_NAME_MAX, - keylen - ); - } - let value = unescape(value)?; - let valuelen = value.len(); - if valuelen > XATTR_SIZE_MAX { - anyhow::bail!( - "xattr value too long; max={} found={}", - XATTR_SIZE_MAX, - keylen - ); - } - Ok(Self { key, value }) - } -} - -impl<'p> Entry<'p> { - fn check_nonregfile(content: Option<&str>, fsverity_digest: Option<&str>) -> Result<()> { - if content.is_some() { - anyhow::bail!("entry cannot have content"); - } - if fsverity_digest.is_some() { - anyhow::bail!("entry cannot have fsverity digest"); - } - Ok(()) - } - - fn check_rdev(rdev: u64) -> Result<()> { - if rdev != 0 { - anyhow::bail!("entry cannot have device (rdev) {rdev}"); - } - Ok(()) - } - - /// Parse an entry from a composefs dump file line. - pub fn parse(s: &'p str) -> Result> { - let mut components = s.split(' '); - let mut next = |name: &str| components.next().ok_or_else(|| anyhow!("Missing {name}")); - let path = unescape_to_path_canonical(next("path")?)?; - let size = u64::from_str(next("size")?)?; - let modeval = next("mode")?; - let (is_hardlink, mode) = if let Some((_, rest)) = modeval.split_once('@') { - (true, u32::from_str_radix(rest, 8)?) - } else { - (false, u32::from_str_radix(modeval, 8)?) - }; - let nlink = u32::from_str(next("nlink")?)?; - let uid = u32::from_str(next("uid")?)?; - let gid = u32::from_str(next("gid")?)?; - let rdev = u64::from_str(next("rdev")?)?; - let mtime = Mtime::from_str(next("mtime")?)?; - let payload = optional_str(next("payload")?); - let content = optional_str(next("content")?); - let fsverity_digest = optional_str(next("digest")?); - let xattrs = components - .try_fold((Vec::new(), 0usize), |(mut acc, total_namelen), line| { - let xattr = Xattr::parse(line)?; - // Limit the total length of keys. - let total_namelen = total_namelen.saturating_add(xattr.key.len()); - if total_namelen > XATTR_LIST_MAX { - anyhow::bail!("Too many xattrs"); - } - acc.push(xattr); - Ok((acc, total_namelen)) - })? - .0; - - let ty = libc::S_IFMT & mode; - let item = if is_hardlink { - if ty == S_IFDIR { - anyhow::bail!("Invalid hardlinked directory"); - } - let target = - unescape_to_path_canonical(payload.ok_or_else(|| anyhow!("Missing payload"))?)?; - // TODO: the dumpfile format suggests to retain all the metadata on hardlink lines - Item::Hardlink { target } - } else { - match ty { - libc::S_IFREG => { - Self::check_rdev(rdev)?; - Item::Regular { - size, - nlink, - inline_content: content - .map(|c| unescape_limited(c, MAX_INLINE_CONTENT.into())) - .transpose()?, - fsverity_digest: fsverity_digest.map(ToOwned::to_owned), - } - } - libc::S_IFLNK => { - Self::check_nonregfile(content, fsverity_digest)?; - Self::check_rdev(rdev)?; - - // Note that the target of *symlinks* is not required to be in canonical form, - // as we don't actually traverse those links on our own, and we need to support - // symlinks that e.g. contain `//` or other things. - let target = - unescape_to_path(payload.ok_or_else(|| anyhow!("Missing payload"))?)?; - let targetlen = target.as_os_str().as_bytes().len(); - if targetlen > libc::PATH_MAX as usize { - anyhow::bail!("Target length too large {}", targetlen); - } - Item::Symlink { nlink, target } - } - libc::S_IFIFO => { - Self::check_nonregfile(content, fsverity_digest)?; - Self::check_rdev(rdev)?; - - Item::Fifo { nlink } - } - libc::S_IFCHR | libc::S_IFBLK => { - Self::check_nonregfile(content, fsverity_digest)?; - Item::Device { nlink, rdev } - } - libc::S_IFDIR => { - Self::check_nonregfile(content, fsverity_digest)?; - Self::check_rdev(rdev)?; - - Item::Directory { size, nlink } - } - o => { - anyhow::bail!("Unhandled mode {o:o}") - } - } - }; - Ok(Entry { - path, - uid, - gid, - mode, - mtime, - item, - xattrs, - }) - } - - /// Remove internal entries - /// FIXME: This is arguably a composefs-info dump bug? - pub fn filter_special(mut self) -> Self { - self.xattrs.retain(|v| { - !matches!( - (v.key.as_bytes(), &*v.value), - (b"trusted.overlay.opaque" | b"user.overlay.opaque", b"x") - ) - }); - self - } -} - -impl<'p> Item<'p> { - pub(crate) fn size(&self) -> u64 { - match self { - Item::Regular { size, .. } | Item::Directory { size, .. } => *size, - _ => 0, - } - } - - pub(crate) fn nlink(&self) -> u32 { - match self { - Item::Regular { nlink, .. } => *nlink, - Item::Device { nlink, .. } => *nlink, - Item::Symlink { nlink, .. } => *nlink, - Item::Directory { nlink, .. } => *nlink, - Item::Fifo { nlink, .. } => *nlink, - _ => 0, - } - } - - pub(crate) fn rdev(&self) -> u64 { - match self { - Item::Device { rdev, .. } => *rdev, - _ => 0, - } - } - - pub(crate) fn payload(&self) -> Option<&Path> { - match self { - Item::Symlink { target, .. } => Some(target), - Item::Hardlink { target } => Some(target), - _ => None, - } - } -} - -impl Display for Mtime { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}.{}", self.sec, self.nsec) - } -} - -impl<'p> Display for Entry<'p> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - escape(f, self.path.as_os_str().as_bytes(), EscapeMode::Standard)?; - write!( - f, - " {} {:o} {} {} {} {} {} ", - self.item.size(), - self.mode, - self.item.nlink(), - self.uid, - self.gid, - self.item.rdev(), - self.mtime, - )?; - if let Some(payload) = self.item.payload() { - escape(f, payload.as_os_str().as_bytes(), EscapeMode::Standard)?; - f.write_char(' ')?; - } else { - write!(f, "- ")?; - } - match &self.item { - Item::Regular { - fsverity_digest, - inline_content, - .. - } => { - if let Some(content) = inline_content { - escape(f, content, EscapeMode::Standard)?; - f.write_char(' ')?; - } else { - write!(f, "- ")?; - } - let fsverity_digest = fsverity_digest.as_deref().unwrap_or("-"); - write!(f, "{fsverity_digest}")?; - } - _ => { - write!(f, "- -")?; - } - } - for xattr in self.xattrs.iter() { - f.write_char(' ')?; - escape(f, xattr.key.as_bytes(), EscapeMode::XattrKey)?; - f.write_char('=')?; - escape(f, &xattr.value, EscapeMode::Standard)?; - } - std::fmt::Result::Ok(()) - } -} - -/// Configuration for parsing a dumpfile -#[derive(Debug, Default)] -pub struct DumpConfig<'a> { - /// Only dump these toplevel filenames - pub filters: Option<&'a [&'a str]>, -} - -/// Parse the provided composefs into dumpfile entries. -pub fn dump(input: File, config: DumpConfig, mut handler: F) -> Result<()> -where - F: FnMut(Entry<'_>) -> Result<()> + Send, -{ - let mut proc = Command::new("composefs-info"); - proc.arg("dump"); - if let Some(filter) = config.filters { - proc.args(filter.iter().flat_map(|f| ["--filter", f])); - } - proc.args(["/dev/stdin"]) - .stdin(std::process::Stdio::from(input)) - .stderr(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()); - let mut proc = proc.spawn().context("Spawning composefs-info")?; - - // SAFETY: we set up these streams - let child_stdout = proc.stdout.take().unwrap(); - let child_stderr = proc.stderr.take().unwrap(); - - std::thread::scope(|s| { - let stderr_copier = s.spawn(move || { - let mut child_stderr = std::io::BufReader::new(child_stderr); - let mut buf = Vec::new(); - std::io::copy(&mut child_stderr, &mut buf)?; - anyhow::Ok(buf) - }); - - let child_stdout = std::io::BufReader::new(child_stdout); - for line in child_stdout.lines() { - let line = line.context("Reading dump stdout")?; - let entry = Entry::parse(&line)?.filter_special(); - handler(entry)?; - } - - let r = proc.wait()?; - let stderr = stderr_copier.join().unwrap()?; - if !r.success() { - let stderr = String::from_utf8_lossy(&stderr); - let stderr = stderr.trim(); - anyhow::bail!("composefs-info dump failed: {r}: {stderr}") - } - - Ok(()) - }) -} - -#[cfg(test)] -mod tests { - use std::fs::File; - - use crate::mkcomposefs::{mkcomposefs_from_buf, Config}; - - use super::*; - - const SPECIAL_DUMP: &str = include_str!("../../../tests/assets/special.dump"); - const SPECIALS: &[&str] = &["", "foo=bar=baz", r"\x01\x02", "-"]; - const UNQUOTED: &[&str] = &["foo!bar", "hello-world", "--"]; - - #[test] - fn test_escape_roundtrip() { - let cases = SPECIALS.iter().chain(UNQUOTED); - for case in cases { - let mut buf = String::new(); - escape(&mut buf, case.as_bytes(), EscapeMode::Standard).unwrap(); - let case2 = unescape(&buf).unwrap(); - assert_eq!(case, &String::from_utf8(case2.into()).unwrap()); - } - } - - #[test] - fn test_escape_unquoted() { - let cases = UNQUOTED; - for case in cases { - let mut buf = String::new(); - escape(&mut buf, case.as_bytes(), EscapeMode::Standard).unwrap(); - assert_eq!(case, &buf); - } - } - - #[test] - fn test_escape_quoted() { - // We don't escape `=` in standard mode - { - let mut buf = String::new(); - escape(&mut buf, b"=", EscapeMode::Standard).unwrap(); - assert_eq!(buf, "="); - } - // Verify other special cases - let cases = &[("=", r"\x3d"), ("-", r"\x2d")]; - for (src, expected) in cases { - let mut buf = String::new(); - escape(&mut buf, src.as_bytes(), EscapeMode::XattrKey).unwrap(); - assert_eq!(expected, &buf); - } - } - - #[test] - fn test_unescape() { - assert_eq!(unescape("").unwrap().len(), 0); - assert_eq!(unescape_limited("", 0).unwrap().len(), 0); - assert!(unescape_limited("foobar", 3).is_err()); - // This is borrowed input - assert!(matches!( - unescape_limited("foobar", 6).unwrap(), - Cow::Borrowed(_) - )); - // But non-ASCII is currently owned out of conservatism - assert!(matches!(unescape_limited("→", 6).unwrap(), Cow::Owned(_))); - assert!(unescape_limited("foo→bar", 3).is_err()); - } - - #[test] - fn test_unescape_path() { - // Empty - assert!(unescape_to_path("").is_err()); - // Embedded NUL - assert!(unescape_to_path("\0").is_err()); - assert!(unescape_to_path("foo\0bar").is_err()); - assert!(unescape_to_path("\0foobar").is_err()); - assert!(unescape_to_path("foobar\0").is_err()); - assert!(unescape_to_path("foo\\x00bar").is_err()); - let mut p = "a".repeat(libc::PATH_MAX.try_into().unwrap()); - assert!(unescape_to_path(&p).is_ok()); - p.push('a'); - assert!(unescape_to_path(&p).is_err()); - } - - #[test] - fn test_unescape_path_canonical() { - // Invalid cases - assert!(unescape_to_path_canonical("").is_err()); - assert!(unescape_to_path_canonical("foo").is_err()); - assert!(unescape_to_path_canonical("../blah").is_err()); - assert!(unescape_to_path_canonical("/foo/..").is_err()); - assert!(unescape_to_path_canonical("/foo/../blah").is_err()); - // Verify that we return borrowed input where possible - assert!(matches!( - unescape_to_path_canonical("/foo").unwrap(), - Cow::Borrowed(v) if v.to_str() == Some("/foo") - )); - // But an escaped version must be owned - assert!(matches!( - unescape_to_path_canonical(r#"/\x66oo"#).unwrap(), - Cow::Owned(v) if v.to_str() == Some("/foo") - )); - // Test successful normalization - assert_eq!( - unescape_to_path_canonical("///foo/bar//baz") - .unwrap() - .to_str() - .unwrap(), - "/foo/bar/baz" - ); - assert_eq!( - unescape_to_path_canonical("/.").unwrap().to_str().unwrap(), - "/" - ); - } - - #[test] - fn test_xattr() { - let v = Xattr::parse("foo=bar").unwrap(); - assert_eq!(v.key.as_bytes(), b"foo"); - assert_eq!(&*v.value, b"bar"); - // Invalid embedded NUL in keys - assert!(Xattr::parse("foo\0bar=baz").is_err()); - assert!(Xattr::parse("foo\x00bar=baz").is_err()); - // But embedded NUL in values is OK - let v = Xattr::parse("security.selinux=bar\x00").unwrap(); - assert_eq!(v.key.as_bytes(), b"security.selinux"); - assert_eq!(&*v.value, b"bar\0"); - } - - #[test] - fn long_xattrs() { - let mut s = String::from("/file 0 100755 1 0 0 0 0.0 - - -"); - Entry::parse(&s).unwrap(); - let xattrs_to_fill = XATTR_LIST_MAX / XATTR_NAME_MAX; - let xattr_name_remainder = XATTR_LIST_MAX % XATTR_NAME_MAX; - assert_eq!(xattr_name_remainder, 0); - let uniqueidlen = 8u8; - let xattr_prefix_len = XATTR_NAME_MAX.checked_sub(uniqueidlen.into()).unwrap(); - let push_long_xattr = |s: &mut String, n| { - s.push(' '); - for _ in 0..xattr_prefix_len { - s.push('a'); - } - write!(s, "{n:08x}=x").unwrap(); - }; - for i in 0..xattrs_to_fill { - push_long_xattr(&mut s, i); - } - Entry::parse(&s).unwrap(); - push_long_xattr(&mut s, xattrs_to_fill); - assert!(Entry::parse(&s).is_err()); - } - - #[test] - fn test_parse() { - const CONTENT: &str = include_str!("../../../tests/assets/special.dump"); - for line in CONTENT.lines() { - // Test a full round trip by parsing, serialize, parsing again - let e = Entry::parse(line).unwrap(); - let serialized = e.to_string(); - assert_eq!(line, serialized); - let e2 = Entry::parse(&serialized).unwrap(); - assert_eq!(e, e2); - } - } - - fn parse_all(name: &str, s: &str) -> Result<()> { - for line in s.lines() { - if line.is_empty() { - continue; - } - let _: Entry = - Entry::parse(line).with_context(|| format!("Test case={name:?} line={line:?}"))?; - } - Ok(()) - } - - #[test] - fn test_should_fail() { - const CASES: &[(&str, &str)] = &[ - ( - "long link", - include_str!("../../../tests/assets/should-fail-long-link.dump"), - ), - ( - "no ftype", - include_str!("../../../tests/assets/should-fail-no-ftype.dump"), - ), - ( - "long xattr key", - include_str!("../../../tests/assets/should-fail-long-xattr-key.dump"), - ), - ( - "long xattr value", - include_str!("../../../tests/assets/should-fail-long-xattr-value.dump"), - ), - ( - "dir hardlink", - include_str!("../../../tests/assets/should-fail-dir-hardlink.dump"), - ), - ( - "content in fifo", - "/ 4096 40755 2 0 0 0 0.0 - - -\n/fifo 0 10777 1 0 0 0 0.0 - foobar -", - ), - ("root with rdev", "/ 4096 40755 2 0 0 42 0.0 - - -"), - ("root with fsverity", "/ 4096 40755 2 0 0 0 0.0 - - 35d02f81325122d77ec1d11baba655bc9bf8a891ab26119a41c50fa03ddfb408"), - ]; - for (name, case) in CASES.iter().copied() { - assert!( - parse_all(name, case).is_err(), - "Expected case {name} to fail" - ); - } - } - - #[test] - fn test_load_cfs() -> Result<()> { - let td = tempfile::tempdir()?; - let out_cfs = td.path().join("out.cfs"); - let o = File::create_new(&out_cfs)?; - mkcomposefs_from_buf(Config::default(), SPECIAL_DUMP, o).unwrap(); - let mut entries = String::new(); - let input = File::open(&out_cfs)?; - dump(input, DumpConfig::default(), |e| { - writeln!(entries, "{e}")?; - Ok(()) - }) - .unwrap(); - similar_asserts::assert_eq!(SPECIAL_DUMP, &entries); - Ok(()) - } - - #[test] - fn test_load_cfs_filtered() -> Result<()> { - const FILTERED: &str = - "/ 4096 40555 2 0 0 0 1633950376.0 - - - trusted.foo1=bar-1 user.foo2=bar-2\n\ -/blockdev 0 60777 1 0 0 107690 1633950376.0 - - - trusted.bar=bar-2\n\ -/inline 15 100777 1 0 0 0 1633950376.0 - FOOBAR\\nINAFILE\\n - user.foo=bar-2\n"; - let td = tempfile::tempdir()?; - let out_cfs = td.path().join("out.cfs"); - let o = File::create_new(&out_cfs)?; - mkcomposefs_from_buf(Config::default(), SPECIAL_DUMP, o).unwrap(); - let mut entries = String::new(); - let input = File::open(&out_cfs)?; - let mut filter = DumpConfig::default(); - filter.filters = Some(&["blockdev", "inline"]); - dump(input, filter, |e| { - writeln!(entries, "{e}")?; - Ok(()) - }) - .unwrap(); - similar_asserts::assert_eq!(FILTERED, &entries); - Ok(()) - } -} diff --git a/rust/composefs/src/fsverity.rs b/rust/composefs/src/fsverity.rs deleted file mode 100644 index 0119488a..00000000 --- a/rust/composefs/src/fsverity.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! # Bindings for fsverity -//! -//! This collection of APIs is for fsverity as used by composefs. - -use std::os::fd::{AsRawFd, BorrowedFd}; - -use composefs_sys::{map_result, LCFS_SHA256_DIGEST_LEN}; - -/// The binary composefs digest -#[derive(Debug, PartialEq, Eq, Clone, Default)] -pub struct Digest([u8; LCFS_SHA256_DIGEST_LEN]); - -impl Digest { - /// Create an uninitialized digest. - pub fn new() -> Self { - Self::default() - } - - /// Retrieve the digest bytes - pub fn get(&self) -> &[u8; LCFS_SHA256_DIGEST_LEN] { - &self.0 - } -} - -/// Compute the composefs fsverity digest from the provided file descriptor. -/// If fsverity is already enabled, this will return the digest computed by the kernel. -#[allow(unsafe_code)] -pub fn fsverity_digest_from_fd(fd: BorrowedFd, digest: &mut Digest) -> std::io::Result<()> { - unsafe { - map_result(composefs_sys::lcfs_fd_get_fsverity( - digest.0.as_mut_ptr(), - fd.as_raw_fd(), - )) - } -} - -/// Enable fsverity on the provided file descriptor. This function is not idempotent; -/// it is an error if fsverity is already enabled. -#[allow(unsafe_code)] -#[cfg(feature = "v1_0_4")] -pub fn fsverity_enable(fd: BorrowedFd) -> std::io::Result<()> { - unsafe { map_result(composefs_sys::lcfs_fd_enable_fsverity(fd.as_raw_fd())) } -} - -#[cfg(test)] -mod tests { - use anyhow::Result; - use std::io::{Seek, Write}; - use std::os::fd::AsFd; - - use super::*; - - #[test] - fn test_digest() -> Result<()> { - let mut tf = tempfile::tempfile()?; - tf.write_all(b"hello world")?; - let mut digest = Digest::new(); - tf.seek(std::io::SeekFrom::Start(0))?; - fsverity_digest_from_fd(tf.as_fd(), &mut digest)?; - assert_eq!( - digest.get(), - &[ - 30, 46, 170, 66, 2, 215, 80, 164, 17, 116, 238, 69, 73, 112, 185, 44, 27, 194, 249, - 37, 177, 227, 80, 118, 216, 199, 213, 245, 99, 98, 186, 100 - ] - ); - Ok(()) - } -} diff --git a/rust/composefs/src/lib.rs b/rust/composefs/src/lib.rs deleted file mode 100644 index 620f9202..00000000 --- a/rust/composefs/src/lib.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! # Rust composefs library -//! -//! This crate builds on top of the core composefs tooling; it currently requires -//! both the `libcomposefs` C library as well as the external executables -//! `mkcomposefs` and `composefs-info`. -//! -//! The core functionality exposed at the moment is just support for creating -//! and parsing composefs "superblock" entries. - -// See https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html -#![deny(missing_docs)] -#![deny(missing_debug_implementations)] -#![forbid(unused_must_use)] -#![deny(unsafe_code)] -#![deny(clippy::dbg_macro)] -#![deny(clippy::todo)] - -use std::{ - fs::File, - io::{BufRead, BufReader}, -}; - -use anyhow::{Context, Result}; -use dumpfile::Entry; - -pub mod dumpfile; -pub mod mkcomposefs; - -pub mod fsverity; - -/// Parse a composefs superblock. The provided callback will be invoked -/// for each entry in the target image, containing exactly one parsed entry. -/// -/// This function depends on an external `composefs-info` binary currently. -pub fn dump(f: File, mut callback: F) -> Result<()> -where - F: FnMut(&'_ Entry) -> Result<()>, -{ - let mut cmd = std::process::Command::new("composefs-info"); - cmd.args(["dump", "/proc/self/fd/0"]) - .stdin(std::process::Stdio::from(f)) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()); - let mut proc = cmd.spawn().context("spawning composefs-info dump")?; - // SAFETY: We provided a pipe - let child_stdout = BufReader::new(proc.stdout.take().unwrap()); - std::thread::scope(|s| { - let reader = s.spawn(move || -> anyhow::Result<()> { - let r = proc.wait_with_output()?; - if !r.status.success() { - let stderr = String::from_utf8_lossy(&r.stderr); - let stderr = stderr.trim(); - anyhow::bail!("composefs-info dump failed: {}: {stderr}", r.status) - } - Ok(()) - }); - for line in child_stdout.lines() { - let line = line?; - // FIXME: try removing filter_special - let entry = Entry::parse(&line)?.filter_special(); - callback(&entry)?; - } - // SAFETY: We shouldn't fail to join the thread - reader - .join() - .unwrap() - .context("Processing composefs-info dump")?; - anyhow::Ok(()) - }) -} diff --git a/rust/composefs/src/mkcomposefs.rs b/rust/composefs/src/mkcomposefs.rs deleted file mode 100644 index d77591d9..00000000 --- a/rust/composefs/src/mkcomposefs.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! # Creating composefs images -//! -//! This code wraps `mkcomposefs`, supporting synthesizing a composefs -//! from dump file entries. - -use std::fs::File; -use std::io::Write; -use std::process::Command; -use std::sync::mpsc; - -use anyhow::{Context, Result}; - -/// Configuration for `mkcomposefs` -#[derive(Debug, Default)] -pub struct Config { - digest_store: Option, - min_version: Option, - max_version: Option, -} - -impl Config { - fn to_args(&self) -> impl Iterator { - self.digest_store - .as_deref() - .map(|v| format!("--digest-store={v}")) - .into_iter() - .chain(self.min_version.map(|v| format!("--min-version={v}"))) - .chain(self.max_version.map(|v| format!("--max-version={v}"))) - } -} - -/// Prepare a child process invocation of `mkcomposefs`. It will accept -/// serialized dumpfile lines on stdin, and write output to stdout. -fn new_mkcomposefs_command(config: Config, output: File) -> Result { - let mut proc = Command::new("mkcomposefs"); - proc.args(config.to_args()) - .args(["--from-file", "-", "-"]) - .stdin(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .stdout(std::process::Stdio::from(output)); - Ok(proc) -} - -/// Given the provided configuration and dumpfile entries, write a composefs metadata file to `output`. -pub fn mkcomposefs( - config: Config, - entries: mpsc::Receiver>, - output: File, -) -> Result<()> { - let mut cmd = new_mkcomposefs_command(config, output)?; - let mut proc = cmd.spawn().context("Spawning mkcomposefs")?; - // SAFETY: we set up stdin - let mut child_stdin = std::io::BufWriter::new(proc.stdin.take().unwrap()); - std::thread::scope(|s| { - // Spawn a helper thread which handles writing to the child stdin, while the main - // thread handles reading from stderr (if any) and otherwise just being blocked in wait(). - // The composefs subprocess itself writes to the output file. - let writer = s.spawn(move || -> anyhow::Result<()> { - for entry in entries { - writeln!(child_stdin, "{entry}")?; - } - // Flush and close child's stdin - drop(child_stdin.into_inner()?); - Ok(()) - }); - let r = proc.wait_with_output()?; - if !r.status.success() { - let stderr = String::from_utf8_lossy(&r.stderr); - let stderr = stderr.trim(); - anyhow::bail!("mkcomposefs failed: {}: {stderr}", r.status) - } - // SAFETY: We shouldn't fail to join the thread - writer.join().unwrap()?; - anyhow::Ok(()) - }) -} - -// Generate a composefs from an input string, which must -// be a textual composefs dump file. -#[cfg(test)] -pub(crate) fn mkcomposefs_from_buf(config: Config, buf: &str, output: File) -> Result<()> { - let (send, recv) = mpsc::sync_channel(5); - std::thread::scope(|s| { - let producer = s.spawn(move || { - for line in buf.lines() { - if send.send(crate::dumpfile::Entry::parse(line)?).is_err() { - break; - } - } - anyhow::Ok(()) - }); - mkcomposefs(config, recv, output)?; - producer.join().unwrap()?; - anyhow::Ok(()) - }) -} - -#[test] -fn test_mkcomposefs() -> Result<()> { - use std::fmt::Write as _; - let td = tempfile::tempdir()?; - let td = td.path(); - let outpath = &td.join("out"); - let o = File::create(outpath)?; - const CONTENT: &str = include_str!("../../../tests/assets/special.dump"); - mkcomposefs_from_buf(Config::default(), CONTENT, o)?; - let mut reparsed_content = String::new(); - let o = File::open(outpath)?; - super::dump(o, |entry| { - writeln!(reparsed_content, "{entry}").map_err(anyhow::Error::from) - }) - .unwrap(); - let mut reparsed_content = reparsed_content.lines().fuse(); - for line in CONTENT.lines() { - assert_eq!(line, reparsed_content.next().unwrap()); - } - assert!(reparsed_content.next().is_none()); - Ok(()) -}