Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab50acfcc7 |
+4
-4
@@ -1,4 +1,4 @@
|
||||
pkg
|
||||
sysroot
|
||||
/target/
|
||||
/test.bin
|
||||
target
|
||||
image.bin
|
||||
image
|
||||
image*
|
||||
|
||||
+25
-19
@@ -1,24 +1,30 @@
|
||||
image: "rust:latest"
|
||||
image: "redoxos/redoxer"
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- test
|
||||
- build
|
||||
- test
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'
|
||||
cache:
|
||||
paths:
|
||||
- target/
|
||||
|
||||
fmt:
|
||||
stage: lint
|
||||
script:
|
||||
- rustup component add rustfmt
|
||||
- cargo fmt -- --check
|
||||
build:linux:
|
||||
stage: build
|
||||
script: cargo +nightly build --verbose
|
||||
|
||||
cargo-test:
|
||||
stage: test
|
||||
script:
|
||||
- apt update && apt install -y fuse3 libfuse3-dev
|
||||
- cargo build --locked
|
||||
- cargo test
|
||||
- ./target/debug/redox_installer -c res/test.toml test.bin --no-mount
|
||||
build:redox:
|
||||
stage: build
|
||||
script: redoxer build --verbose
|
||||
|
||||
test:linux:
|
||||
stage: test
|
||||
dependencies:
|
||||
- build:linux
|
||||
script: cargo +nightly test --verbose
|
||||
|
||||
test:redox:
|
||||
stage: test
|
||||
dependencies:
|
||||
- build:redox
|
||||
# only run integration test as without KVM unit tests is super slow
|
||||
script: redoxer test --verbose -- --test '*' -- --nocapture
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
sudo: required
|
||||
language: rust
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
dist: trusty
|
||||
before_install:
|
||||
- if [ "$TRAVIS_OS_NAME" = "linux" ]; then
|
||||
sudo apt-get install -qq pkg-config fuse libfuse-dev;
|
||||
sudo modprobe fuse;
|
||||
sudo chmod 666 /dev/fuse;
|
||||
sudo chown root:$USER /etc/fuse.conf;
|
||||
fi
|
||||
- if [ "$TRAVIS_OS_NAME" = "osx" ]; then
|
||||
brew update;
|
||||
brew install Caskroom/cask/osxfuse;
|
||||
fi
|
||||
notifications:
|
||||
email: false
|
||||
Generated
+242
-1853
File diff suppressed because it is too large
Load Diff
+84
-62
@@ -1,74 +1,96 @@
|
||||
[package]
|
||||
name = "redox_installer"
|
||||
version = "0.2.42"
|
||||
description = "A Redox filesystem builder"
|
||||
license = "MIT"
|
||||
name = "redoxfs"
|
||||
description = "The Redox Filesystem"
|
||||
repository = "https://gitlab.redox-os.org/redox-os/redoxfs"
|
||||
version = "0.9.0"
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
authors = ["Jeremy Soller <jackpot51@gmail.com>"]
|
||||
repository = "https://gitlab.redox-os.org/redox-os/installer"
|
||||
default-run = "redox_installer"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "redox_installer"
|
||||
path = "src/bin/installer.rs"
|
||||
required-features = ["installer"]
|
||||
|
||||
[[bin]]
|
||||
name = "redox_installer_tui"
|
||||
path = "src/bin/installer_tui.rs"
|
||||
required-features = ["installer"]
|
||||
|
||||
[lib]
|
||||
name = "redox_installer"
|
||||
name = "redoxfs"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
arg_parser = "0.1.0"
|
||||
fatfs = { version = "0.3.0", optional = true }
|
||||
fscommon = { version = "0.1.1", optional = true }
|
||||
gpt = { version = "3.0.0", optional = true }
|
||||
libc = { version = "0.2.70", optional = true }
|
||||
pkgar = { version = "0.2.2", optional = true }
|
||||
pkgar-core = { version = "0.2.2", optional = true }
|
||||
pkgar-keys = { version = "0.2.2", optional = true }
|
||||
rand = { version = "0.9", optional = true }
|
||||
redox-pkg = { version = "0.3.1", features = ["indicatif"], optional = true }
|
||||
redox_syscall = { version = "0.7", optional = true }
|
||||
redoxfs = { version = "0.9", optional = true, default-features = false, features = ["std", "log"] }
|
||||
rust-argon2 = { version = "3", optional = true }
|
||||
serde = "1"
|
||||
serde_derive = "1.0"
|
||||
termion = { version = "4", optional = true }
|
||||
toml = "0.8"
|
||||
uuid = { version = "1.4", features = ["v4"], optional = true }
|
||||
[[bin]]
|
||||
name = "redoxfs"
|
||||
path = "src/bin/mount.rs"
|
||||
doc = false
|
||||
required-features = ["std"]
|
||||
|
||||
[target.'cfg(target_os = "redox")'.dependencies]
|
||||
libredox = { version = "0.1", optional = true }
|
||||
ring = { version = "=0.17.8", optional = true }
|
||||
[[bin]]
|
||||
name = "redoxfs-ar"
|
||||
path = "src/bin/ar.rs"
|
||||
doc = false
|
||||
required-features = ["std"]
|
||||
|
||||
[[bin]]
|
||||
name = "redoxfs-clone"
|
||||
path = "src/bin/clone.rs"
|
||||
doc = false
|
||||
required-features = ["std"]
|
||||
|
||||
[[bin]]
|
||||
name = "redoxfs-mkfs"
|
||||
path = "src/bin/mkfs.rs"
|
||||
doc = false
|
||||
required-features = ["std"]
|
||||
|
||||
[[bin]]
|
||||
name = "redoxfs-resize"
|
||||
path = "src/bin/resize.rs"
|
||||
doc = false
|
||||
required-features = ["std"]
|
||||
|
||||
[dependencies]
|
||||
aes = { version = "0.8", default-features = false }
|
||||
argon2 = { version = "0.4", default-features = false, features = ["alloc"] }
|
||||
base64ct = { version = "1", default-features = false }
|
||||
bitflags = "2"
|
||||
endian-num = "0.1"
|
||||
env_logger = { version = "0.11", optional = true }
|
||||
getrandom = { version = "0.2.5", optional = true }
|
||||
humansize = { version = "2", optional = true }
|
||||
libc = "0.2"
|
||||
log = { version = "0.4.14", default-features = false, optional = true }
|
||||
lz4_flex = { version = "0.11", default-features = false, features = ["checked-decode"] }
|
||||
parse-size = { version = "1", optional = true }
|
||||
range-tree = { version = "0.1", optional = true }
|
||||
redox_syscall = "0.7.3"
|
||||
seahash = { version = "4.1.0", default-features = false }
|
||||
termion = { version = "4", optional = true }
|
||||
uuid = { version = "1.4", default-features = false }
|
||||
xts-mode = { version = "0.5", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["installer", "fuse"]
|
||||
installer = [
|
||||
"fatfs",
|
||||
"fscommon",
|
||||
"gpt",
|
||||
"libc",
|
||||
"libredox",
|
||||
"pkgar",
|
||||
"pkgar-core",
|
||||
"pkgar-keys",
|
||||
"rand",
|
||||
"redox-pkg",
|
||||
"redox_syscall",
|
||||
"redoxfs",
|
||||
"ring",
|
||||
"rust-argon2",
|
||||
"termion",
|
||||
"uuid",
|
||||
default = ["std", "log", "fuse"]
|
||||
fuse = [
|
||||
"fuser",
|
||||
"std",
|
||||
]
|
||||
std = [
|
||||
"env_logger",
|
||||
"getrandom",
|
||||
"humansize",
|
||||
"libredox",
|
||||
"parse-size",
|
||||
"range-tree",
|
||||
"termion",
|
||||
"uuid/v4",
|
||||
"redox_syscall/std",
|
||||
"redox-scheme",
|
||||
]
|
||||
fuse = ["redoxfs/fuse"]
|
||||
|
||||
[patch.crates-io]
|
||||
# https://github.com/briansmith/ring/issues/1999
|
||||
ring = { git = "https://gitlab.redox-os.org/redox-os/ring.git", branch = "redox-0.17.8" }
|
||||
[target.'cfg(not(target_os = "redox"))'.dependencies]
|
||||
fuser = { version = "0.16", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "redox")'.dependencies]
|
||||
libredox = { version = "0.1.13", optional = true }
|
||||
redox-path = "0.3.0"
|
||||
redox-scheme = { version = "0.11.0", optional = true }
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.17"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Redox OS
|
||||
Copyright (c) 2016 Jeremy Soller
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
UNAME := $(shell uname)
|
||||
|
||||
ifeq ($(UNAME),Darwin)
|
||||
FUMOUNT=umount
|
||||
else ifeq ($(UNAME),FreeBSD)
|
||||
FUMOUNT=sudo umount
|
||||
else
|
||||
# Detect which version of the fusermount binary is available.
|
||||
ifneq (, $(shell which fusermount3))
|
||||
FUMOUNT=fusermount3 -u
|
||||
else
|
||||
FUMOUNT=fusermount -u
|
||||
endif
|
||||
endif
|
||||
|
||||
image.bin:
|
||||
cargo build --release --bin redoxfs-mkfs
|
||||
dd if=/dev/zero of=image.bin bs=1048576 count=1024
|
||||
target/release/redoxfs-mkfs image.bin
|
||||
|
||||
mount: image.bin FORCE
|
||||
mkdir -p image
|
||||
cargo build --release --bin redoxfs
|
||||
target/release/redoxfs image.bin image
|
||||
|
||||
unmount: FORCE
|
||||
sync
|
||||
-${FUMOUNT} image
|
||||
rm -rf image
|
||||
|
||||
clean: FORCE
|
||||
sync
|
||||
-${FUMOUNT} image
|
||||
rm -rf image image.bin
|
||||
cargo clean
|
||||
|
||||
FORCE:
|
||||
@@ -1,50 +1,53 @@
|
||||
# Redox OS installer
|
||||
# RedoxFS
|
||||
|
||||
The Redox installer will allow you to produce a Redox OS image. You will
|
||||
be able to specify:
|
||||
- Output device (raw image, ISO, QEMU, VirtualBox, drive)
|
||||
- Filesystem
|
||||
- Included packages
|
||||
- Method of installation (from source, from binary)
|
||||
- User accounts
|
||||
This is the default filesystem of Redox OS inspired by [ZFS](https://docs.freebsd.org/en/books/handbook/zfs/) and adapted to a microkernel architecture.
|
||||
|
||||
You will be prompted to install dependencies, based on your OS and method of
|
||||
installation. The easiest method is to install from binaries.
|
||||
(It's a replacement for [TFS](https://gitlab.redox-os.org/redox-os/tfs))
|
||||
|
||||
## Usage
|
||||
Current features:
|
||||
|
||||
It is recommended to compile with `cargo`, in release mode:
|
||||
```bash
|
||||
cargo build --release
|
||||
- Compatible with Redox and Linux (FUSE)
|
||||
- Copy-on-write
|
||||
- Data/metadata checksums
|
||||
- Transparent encryption
|
||||
- Standard Unix file attributes
|
||||
- File/directory size limit up to 193TiB (212TB)
|
||||
- File/directory quantity limit up to 4 billion per 193TiB (2^32 - 1 = 4294967295)
|
||||
- MIT licensed
|
||||
- Disk encryption fully supported by the Redox bootloader, letting it load the kernel off an encrypted partition.
|
||||
|
||||
Being MIT licensed, RedoxFS can be bundled on GPL-licensed operating systems (Linux, for example).
|
||||
|
||||
### Install RedoxFS
|
||||
|
||||
```sh
|
||||
cargo install redoxfs
|
||||
```
|
||||
|
||||
By default, you will be prompted to supply configuration options. You can
|
||||
use the scripted mode by supplying a configuration file:
|
||||
```bash
|
||||
cargo run --release -- config/example.toml
|
||||
```
|
||||
An example configuration can be found in [config/example.toml](./config/example.toml).
|
||||
Unsuplied configuration will use the default. You can use the `general.prompt`
|
||||
setting to prompt when configuration is not set. Multiple configurations can
|
||||
be specified, they will be built in order.
|
||||
You can also build RedoxFS from this repository.
|
||||
|
||||
## Embedding
|
||||
### Configure your storage device to allow rootless usage
|
||||
|
||||
The installer can also be used inside of other crates, as a library:
|
||||
If you are on Linux you need root permission to acess block devices (storage), but it's recommended to run RedoxFS as rootless.
|
||||
|
||||
```toml
|
||||
# Cargo.toml
|
||||
[dependencies]
|
||||
redox_installer = "0.1"
|
||||
To do that you need to configure your storage device permission to your user with the following command:
|
||||
|
||||
```sh
|
||||
sudo setfacl -m u:your-username:rw /path/to/disk
|
||||
```
|
||||
|
||||
```rust
|
||||
// src/main.rs
|
||||
extern crate redox_installer;
|
||||
### Create, mount and customize your RedoxFS partition
|
||||
|
||||
fn main() {
|
||||
let mut config = redox_installer::Config::default();
|
||||
...
|
||||
redox_installer::install(config);
|
||||
}
|
||||
```
|
||||
See [the instructions in the book](https://doc.redox-os.org/book/redoxfs.html) for RedoxFS tooling usage.
|
||||
|
||||
Currently RedoxFS tooling are:
|
||||
|
||||
- `redoxfs` mount a RedoxFS disk
|
||||
- `redoxfs-ar` write files to a RedoxFS disk
|
||||
- `redoxfs-clone` clone a RedoxFS disk
|
||||
- `redoxfs-mkfs` create an empty RedoxFS disk
|
||||
- `redoxfs-resize` resize a RedoxFS disk
|
||||
|
||||
[](./LICENSE)
|
||||
[](https://crates.io/crates/redoxfs)
|
||||
[](https://docs.rs/redoxfs)
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
# This is the default configuration file
|
||||
|
||||
# General settings
|
||||
[general]
|
||||
# Do not prompt if settings are not defined
|
||||
prompt = false
|
||||
|
||||
# Package settings
|
||||
[packages]
|
||||
#acid = {}
|
||||
#autoconf = {}
|
||||
#automake = {}
|
||||
#bash = {}
|
||||
#binutils = {}
|
||||
#ca-certificates = {}
|
||||
#cargo = {}
|
||||
#contain = {}
|
||||
coreutils = {}
|
||||
#curl = {}
|
||||
#dash = {}
|
||||
#diffutils = {}
|
||||
drivers = {}
|
||||
extrautils = {}
|
||||
findutils = {}
|
||||
#games = {}
|
||||
#gawk = {}
|
||||
#gcc = {}
|
||||
#git = {}
|
||||
#gnu-binutils = {}
|
||||
#gnu-make = {}
|
||||
#installer = {}
|
||||
ion = {}
|
||||
#lua = {}
|
||||
#nasm = {}
|
||||
netstack = {}
|
||||
netutils = {}
|
||||
#newlib = {}
|
||||
#openssl = {}
|
||||
orbdata = {}
|
||||
orbital = {}
|
||||
orbterm = {}
|
||||
orbutils = {}
|
||||
pastel = {}
|
||||
#patch = {}
|
||||
#pixelcannon = {}
|
||||
pkgutils = {}
|
||||
ptyd = {}
|
||||
#python = {}
|
||||
randd = {}
|
||||
#redoxfs = {}
|
||||
#rust = {}
|
||||
#rustual-boy = {}
|
||||
#sed = {}
|
||||
smith = {}
|
||||
sodium = {}
|
||||
userutils = {}
|
||||
uutils = {}
|
||||
#xz = {}
|
||||
|
||||
# User settings
|
||||
[users.root]
|
||||
password = "password"
|
||||
uid = 0
|
||||
gid = 0
|
||||
name = "root"
|
||||
home = "/root"
|
||||
|
||||
[users.user]
|
||||
# Password is unset
|
||||
password = ""
|
||||
|
||||
[groups.sudo]
|
||||
gid = 1
|
||||
members = ["user"]
|
||||
|
||||
[[files]]
|
||||
path = "/etc/init.d/00_base"
|
||||
data = """
|
||||
pcid /etc/pcid/filesystem.toml
|
||||
randd
|
||||
ptyd
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/init.d/10_net"
|
||||
data = """
|
||||
ethernetd
|
||||
ipd
|
||||
icmpd
|
||||
tcpd
|
||||
udpd
|
||||
dhcpd -b
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/init.d/20_orbital"
|
||||
data = """
|
||||
orbital orblogin launcher
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/init.d/30_console"
|
||||
data = """
|
||||
getty display/vesa:2
|
||||
getty debug: -J
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/net/dns"
|
||||
data = """
|
||||
208.67.222.222
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/net/ip"
|
||||
data = """
|
||||
10.0.2.15
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/net/ip_router"
|
||||
data = """
|
||||
10.0.2.2
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/net/ip_subnet"
|
||||
data = """
|
||||
255.255.255.0
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/net/mac"
|
||||
data = """
|
||||
54-52-00-ab-cd-ef
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/pkg.d/50_redox"
|
||||
data = "https://static.redox-os.org/pkg"
|
||||
|
||||
[[files]]
|
||||
path = "/etc/hostname"
|
||||
data = "redox"
|
||||
|
||||
[[files]]
|
||||
path = "/etc/issue"
|
||||
data = """
|
||||
########## Redox OS ##########
|
||||
# Login with the following: #
|
||||
# `user` #
|
||||
# `root`:`password` #
|
||||
##############################
|
||||
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/motd"
|
||||
data = """
|
||||
Welcome to Redox OS!
|
||||
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr"
|
||||
data = "/"
|
||||
symlink = true
|
||||
|
||||
[[files]]
|
||||
path = "/tmp"
|
||||
data = ""
|
||||
directory = true
|
||||
# 0o1777
|
||||
mode = 1023
|
||||
@@ -1,24 +0,0 @@
|
||||
# This is the default configuration file
|
||||
|
||||
# General settings
|
||||
[general]
|
||||
# Do not prompt if settings are not defined
|
||||
prompt = false
|
||||
|
||||
# Package settings
|
||||
[packages]
|
||||
binutils = {}
|
||||
coreutils = {}
|
||||
extrautils = {}
|
||||
ion = {}
|
||||
netutils = {}
|
||||
pkgutils = {}
|
||||
userutils = {}
|
||||
|
||||
# User settings
|
||||
[users.root]
|
||||
password = "password"
|
||||
uid = 0
|
||||
gid = 0
|
||||
name = "root"
|
||||
home = "/root"
|
||||
@@ -0,0 +1,4 @@
|
||||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
||||
Generated
+858
@@ -0,0 +1,858 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.99"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endian-num"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad847bb2094f110bbdd6fa564894ca4556fd978958e93985420d680d3cb6d14"
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
||||
|
||||
[[package]]
|
||||
name = "fuser"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bb29a3ae32279fe3e79a958fe01899f5fb23eadccee919cf88e145b54ed9367"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"memchr",
|
||||
"nix",
|
||||
"page_size",
|
||||
"smallvec",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5ad87c89110f55e4cd4dc2893a9790820206729eaf221555f742d540b0724a0"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d076d5b64a7e2fe6f0743f02c43ca4a6725c0f904203bfe276a5b3e793103605"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"redox_syscall 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "lz4_flex"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numtoa"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "page_size"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-size"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "range-tree"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384c2842d4e069d5ccacf5fe1dca4ef8d07a5444329715f0fc3c61813502d4d1"
|
||||
|
||||
[[package]]
|
||||
name = "redox-path"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "436d45c2b6a5b159d43da708e62b25be3a4a3d5550d654b72216ade4c4bfd717"
|
||||
|
||||
[[package]]
|
||||
name = "redox-scheme"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cea2668a5932f878a4298a1d7f8950249bbbb77120fb263da252c589152f5ea"
|
||||
dependencies = [
|
||||
"libredox",
|
||||
"redox_syscall 0.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_termios"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb"
|
||||
|
||||
[[package]]
|
||||
name = "redoxfs"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"argon2",
|
||||
"base64ct",
|
||||
"bitflags",
|
||||
"endian-num",
|
||||
"env_logger",
|
||||
"fuser",
|
||||
"getrandom",
|
||||
"humansize",
|
||||
"libc",
|
||||
"libredox",
|
||||
"log",
|
||||
"lz4_flex",
|
||||
"parse-size",
|
||||
"range-tree",
|
||||
"redox-path",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.6.0",
|
||||
"seahash",
|
||||
"termion",
|
||||
"uuid",
|
||||
"xts-mode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redoxfs-fuzz"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arbitrary",
|
||||
"fuser",
|
||||
"libfuzzer-sys",
|
||||
"nix",
|
||||
"redoxfs",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "seahash"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "4.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3669a69de26799d6321a5aa713f55f7e2cd37bd47be044b50f2acafc42c122bb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libredox",
|
||||
"numtoa",
|
||||
"redox_termios",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "xts-mode"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cbddb7545ca0b9ffa7bdc653e8743303e1712687a6918ced25f2cdbed42520"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "redoxfs-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
log = []
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
arbitrary = { version = "1.3.2", features = ["derive"] }
|
||||
fuser = { version = "0.16" }
|
||||
libfuzzer-sys = "0.4"
|
||||
nix = { version = "0.29.0", features = ["fs"] }
|
||||
tempfile = "3.10.1"
|
||||
|
||||
[dependencies.redoxfs]
|
||||
path = ".."
|
||||
|
||||
[[bin]]
|
||||
name = "fuse_fuzz_target"
|
||||
path = "fuzz_targets/fuse_fuzz_target.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
@@ -0,0 +1,338 @@
|
||||
//! Fuzzer that exercises random file system operations against a FUSE-mounted redoxfs.
|
||||
|
||||
#![no_main]
|
||||
|
||||
use anyhow::{ensure, Result};
|
||||
use fuser;
|
||||
use libfuzzer_sys::{arbitrary::Arbitrary, fuzz_target, Corpus};
|
||||
use nix::sys::statvfs::statvfs;
|
||||
use std::{
|
||||
fs::{self, File, FileTimes, OpenOptions},
|
||||
io::{Read, Seek, SeekFrom, Write},
|
||||
os::unix::fs::{self as unix_fs, PermissionsExt},
|
||||
path::{Path, PathBuf},
|
||||
thread,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tempfile;
|
||||
|
||||
use redoxfs::{mount::fuse::Fuse, DiskSparse, FileSystem};
|
||||
|
||||
/// Maximum size for files and buffers. Chosen arbitrarily with fuzzing performance in mind.
|
||||
const MAX_SIZE: u64 = 10_000_000;
|
||||
/// Limit on the number of remounts in a single test case. Chosen arbitrarily with fuzzing
|
||||
/// performance in mind: remounts are costly.
|
||||
const MAX_MOUNT_SEQUENCES: usize = 3;
|
||||
|
||||
/// An operation to be performed by the fuzzer.
|
||||
#[derive(Arbitrary, Clone, Debug)]
|
||||
enum Operation {
|
||||
Chown {
|
||||
path: PathBuf,
|
||||
uid: Option<u32>,
|
||||
gid: Option<u32>,
|
||||
},
|
||||
CreateDir {
|
||||
path: PathBuf,
|
||||
},
|
||||
HardLink {
|
||||
original: PathBuf,
|
||||
link: PathBuf,
|
||||
},
|
||||
Metadata {
|
||||
path: PathBuf,
|
||||
},
|
||||
Read {
|
||||
path: PathBuf,
|
||||
},
|
||||
ReadDir {
|
||||
path: PathBuf,
|
||||
},
|
||||
ReadLink {
|
||||
path: PathBuf,
|
||||
},
|
||||
RemoveDir {
|
||||
path: PathBuf,
|
||||
},
|
||||
RemoveFile {
|
||||
path: PathBuf,
|
||||
},
|
||||
Rename {
|
||||
from: PathBuf,
|
||||
to: PathBuf,
|
||||
},
|
||||
SeekRead {
|
||||
path: PathBuf,
|
||||
seek_pos: u64,
|
||||
buf_size: usize,
|
||||
},
|
||||
SeekWrite {
|
||||
path: PathBuf,
|
||||
seek_pos: u64,
|
||||
buf_size: usize,
|
||||
},
|
||||
SetLen {
|
||||
path: PathBuf,
|
||||
size: u64,
|
||||
},
|
||||
SetPermissions {
|
||||
path: PathBuf,
|
||||
readonly: Option<bool>,
|
||||
mode: Option<u32>,
|
||||
},
|
||||
SetTimes {
|
||||
path: PathBuf,
|
||||
accessed_since_epoch: Option<Duration>,
|
||||
modified_since_epoch: Option<Duration>,
|
||||
},
|
||||
Statvfs {},
|
||||
SymLink {
|
||||
original: PathBuf,
|
||||
link: PathBuf,
|
||||
},
|
||||
Write {
|
||||
path: PathBuf,
|
||||
buf_size: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// Parameters for mounting the file system and operations to be performed afterwards.
|
||||
#[derive(Arbitrary, Clone, Debug)]
|
||||
struct MountSequence {
|
||||
cleanup: bool,
|
||||
operations: Vec<Operation>,
|
||||
}
|
||||
|
||||
/// The whole input to a single fuzzer invocation.
|
||||
#[derive(Arbitrary, Clone, Debug)]
|
||||
struct TestCase {
|
||||
disk_size: u64,
|
||||
reserved_size: u64,
|
||||
mount_sequences: Vec<MountSequence>,
|
||||
}
|
||||
|
||||
/// Creates the disk for backing the Redoxfs.
|
||||
fn create_disk(temp_path: &Path, disk_size: u64) -> DiskSparse {
|
||||
let disk_path = temp_path.join("disk.img");
|
||||
DiskSparse::create(disk_path, disk_size).unwrap()
|
||||
}
|
||||
|
||||
/// Creates an empty Redoxfs.
|
||||
fn create_redoxfs(disk: DiskSparse, reserved_size: u64) -> bool {
|
||||
let password = None;
|
||||
let reserved = vec![0; reserved_size as usize];
|
||||
let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
FileSystem::create_reserved(
|
||||
disk,
|
||||
password,
|
||||
&reserved,
|
||||
ctime.as_secs(),
|
||||
ctime.subsec_nanos(),
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Mounts an existing Redoxfs, runs the callback and performs the unmount.
|
||||
fn with_redoxfs_mount<F>(temp_path: &Path, disk: DiskSparse, cleanup: bool, callback: F)
|
||||
where
|
||||
F: FnOnce(&Path) + Send + 'static,
|
||||
{
|
||||
let password = None;
|
||||
let block = None;
|
||||
let mut fs = FileSystem::open(disk, password, block, cleanup).unwrap();
|
||||
|
||||
let mount_path = temp_path.join("mount");
|
||||
fs::create_dir_all(&mount_path).unwrap();
|
||||
let mut session = fuser::Session::new(Fuse { fs: &mut fs }, &mount_path, &[]).unwrap();
|
||||
let mut unmounter = session.unmount_callable();
|
||||
|
||||
let join_handle = thread::spawn(move || {
|
||||
callback(&mount_path);
|
||||
unmounter.unmount().unwrap();
|
||||
});
|
||||
|
||||
session.run().unwrap();
|
||||
join_handle.join().unwrap();
|
||||
}
|
||||
|
||||
fn get_path_within_fs(fs_path: &Path, path_to_add: &Path) -> Result<PathBuf> {
|
||||
ensure!(path_to_add.is_relative());
|
||||
ensure!(path_to_add
|
||||
.components()
|
||||
.all(|c| c != std::path::Component::ParentDir));
|
||||
Ok(fs_path.join(path_to_add))
|
||||
}
|
||||
|
||||
fn do_operation(fs_path: &Path, op: &Operation) -> Result<()> {
|
||||
match op {
|
||||
Operation::Chown { path, uid, gid } => {
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
unix_fs::chown(path, *uid, *gid)?;
|
||||
}
|
||||
Operation::CreateDir { path } => {
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
fs::create_dir(path)?;
|
||||
}
|
||||
Operation::HardLink { original, link } => {
|
||||
let original = get_path_within_fs(fs_path, original)?;
|
||||
let link = get_path_within_fs(fs_path, link)?;
|
||||
fs::hard_link(original, link)?;
|
||||
}
|
||||
Operation::Metadata { path } => {
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
fs::metadata(path)?;
|
||||
}
|
||||
Operation::Read { path } => {
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
fs::read(path)?;
|
||||
}
|
||||
Operation::ReadDir { path } => {
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
let _ = fs::read_dir(path)?.count();
|
||||
}
|
||||
Operation::ReadLink { path } => {
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
fs::read_link(path)?;
|
||||
}
|
||||
Operation::RemoveDir { path } => {
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
fs::remove_dir(path)?;
|
||||
}
|
||||
Operation::RemoveFile { path } => {
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
Operation::Rename { from, to } => {
|
||||
let from = get_path_within_fs(fs_path, from)?;
|
||||
let to = get_path_within_fs(fs_path, to)?;
|
||||
fs::rename(from, to)?;
|
||||
}
|
||||
Operation::SeekRead {
|
||||
path,
|
||||
seek_pos,
|
||||
buf_size,
|
||||
} => {
|
||||
ensure!(*buf_size as u64 <= MAX_SIZE);
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
let mut file = File::open(path)?;
|
||||
file.seek(SeekFrom::Start(*seek_pos))?;
|
||||
let mut buf = vec![0; *buf_size];
|
||||
file.read(&mut buf)?;
|
||||
}
|
||||
Operation::SeekWrite {
|
||||
path,
|
||||
seek_pos,
|
||||
buf_size,
|
||||
} => {
|
||||
ensure!(*seek_pos <= MAX_SIZE);
|
||||
ensure!(*buf_size as u64 <= MAX_SIZE);
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
let mut file = OpenOptions::new().write(true).open(path)?;
|
||||
file.seek(SeekFrom::Start(*seek_pos))?;
|
||||
let buf = vec![0; *buf_size];
|
||||
file.write(&buf)?;
|
||||
}
|
||||
Operation::SetLen { path, size } => {
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
let file = OpenOptions::new().write(true).open(path)?;
|
||||
file.set_len(*size)?;
|
||||
}
|
||||
Operation::SetPermissions {
|
||||
path,
|
||||
readonly,
|
||||
mode,
|
||||
} => {
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
let metadata = fs::metadata(&path)?;
|
||||
let mut perms = metadata.permissions();
|
||||
if let Some(readonly) = readonly {
|
||||
perms.set_readonly(*readonly);
|
||||
}
|
||||
if let Some(mode) = mode {
|
||||
perms.set_mode(*mode);
|
||||
}
|
||||
fs::set_permissions(path, perms)?;
|
||||
}
|
||||
Operation::SetTimes {
|
||||
path,
|
||||
accessed_since_epoch,
|
||||
modified_since_epoch,
|
||||
} => {
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
let file = File::options().write(true).open(path)?;
|
||||
let mut times = FileTimes::new();
|
||||
if let Some(accessed_since_epoch) = accessed_since_epoch {
|
||||
if let Some(accessed) = UNIX_EPOCH.checked_add(*accessed_since_epoch) {
|
||||
times = times.set_accessed(accessed);
|
||||
}
|
||||
}
|
||||
if let Some(modified_since_epoch) = modified_since_epoch {
|
||||
if let Some(modified) = UNIX_EPOCH.checked_add(*modified_since_epoch) {
|
||||
times = times.set_modified(modified);
|
||||
}
|
||||
}
|
||||
file.set_times(times)?;
|
||||
}
|
||||
Operation::Statvfs {} => {
|
||||
statvfs(fs_path)?;
|
||||
}
|
||||
Operation::SymLink { original, link } => {
|
||||
let original = get_path_within_fs(fs_path, original)?;
|
||||
let link = get_path_within_fs(fs_path, link)?;
|
||||
unix_fs::symlink(original, link)?;
|
||||
}
|
||||
Operation::Write { path, buf_size } => {
|
||||
ensure!(*buf_size as u64 <= MAX_SIZE);
|
||||
let path = get_path_within_fs(fs_path, path)?;
|
||||
let buf = vec![0; *buf_size];
|
||||
fs::write(path, &buf)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fuzz_target!(|test_case: TestCase| -> Corpus {
|
||||
if test_case.disk_size > MAX_SIZE
|
||||
|| test_case.reserved_size > MAX_SIZE
|
||||
|| test_case.mount_sequences.len() > MAX_MOUNT_SEQUENCES
|
||||
{
|
||||
return Corpus::Reject;
|
||||
}
|
||||
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
.prefix("fuse_fuzz_target")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
|
||||
#[cfg(feature = "log")]
|
||||
eprintln!("create fs");
|
||||
let disk = create_disk(temp_dir.path(), test_case.disk_size);
|
||||
if !create_redoxfs(disk, test_case.reserved_size) {
|
||||
// File system creation failed (e.g., due to insufficient space) so we bail out, still
|
||||
// exercising this code path is useful.
|
||||
return Corpus::Keep;
|
||||
}
|
||||
|
||||
for mount_seq in test_case.mount_sequences.iter() {
|
||||
#[cfg(feature = "log")]
|
||||
eprintln!("mount fs: path {:?}, size{}", temp_dir.path(), test_case.disk_size);
|
||||
|
||||
let disk = create_disk(temp_dir.path(), test_case.disk_size);
|
||||
let operations = mount_seq.operations.clone();
|
||||
with_redoxfs_mount(temp_dir.path(), disk, mount_seq.cleanup, move |fs_path| {
|
||||
for operation in operations.iter() {
|
||||
#[cfg(feature = "log")]
|
||||
eprintln!("do operation {operation:?}");
|
||||
|
||||
let _result = do_operation(fs_path, operation);
|
||||
|
||||
#[cfg(feature = "log")]
|
||||
eprintln!("operation result {:?}", _result.err());
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(feature = "log")]
|
||||
eprintln!("unmounted fs");
|
||||
}
|
||||
Corpus::Keep
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
/target/
|
||||
Generated
-7205
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
||||
[package]
|
||||
name = "redox_installer_gui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
libredox = "0.1"
|
||||
pkgar = "0.2"
|
||||
pkgar-core = "0.2"
|
||||
pkgar-keys = "0.2"
|
||||
redox_installer = { path = ".." }
|
||||
redox_syscall = "0.7"
|
||||
toml = "0.8"
|
||||
|
||||
[dependencies.libcosmic]
|
||||
git = "https://github.com/pop-os/libcosmic.git"
|
||||
# use the same rev with other cosmic app
|
||||
rev = "384e8f6e219bb458720eafa5bb971b832c057f23"
|
||||
default-features = false
|
||||
features = ["winit"]
|
||||
|
||||
[patch.crates-io]
|
||||
ring = { git = "https://gitlab.redox-os.org/redox-os/ring.git", branch = "redox-0.17.8" }
|
||||
|
||||
[patch.'https://github.com/pop-os/winit']
|
||||
winit = { git = "https://gitlab.redox-os.org/redox-os/winit", branch = "redox-0.30.5" }
|
||||
@@ -1,92 +0,0 @@
|
||||
# installer_gui
|
||||
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
||||
|
||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
||||
|
||||
## Add your files
|
||||
|
||||
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
||||
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
|
||||
|
||||
```
|
||||
cd existing_repo
|
||||
git remote add origin https://gitlab.redox-os.org/redox-os/installer_gui.git
|
||||
git branch -M main
|
||||
git push -uf origin main
|
||||
```
|
||||
|
||||
## Integrate with your tools
|
||||
|
||||
- [ ] [Set up project integrations](https://gitlab.redox-os.org/redox-os/installer_gui/-/settings/integrations)
|
||||
|
||||
## Collaborate with your team
|
||||
|
||||
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
||||
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
||||
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
||||
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
||||
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
|
||||
|
||||
## Test and Deploy
|
||||
|
||||
Use the built-in continuous integration in GitLab.
|
||||
|
||||
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
|
||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
||||
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
||||
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
||||
|
||||
***
|
||||
|
||||
# Editing this README
|
||||
|
||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
||||
|
||||
## Suggestions for a good README
|
||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
||||
|
||||
## Name
|
||||
Choose a self-explaining name for your project.
|
||||
|
||||
## Description
|
||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
||||
|
||||
## Badges
|
||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
||||
|
||||
## Visuals
|
||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
||||
|
||||
## Installation
|
||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
||||
|
||||
## Usage
|
||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
||||
|
||||
## Support
|
||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
||||
|
||||
## Roadmap
|
||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
||||
|
||||
## Contributing
|
||||
State if you are open to contributions and what your requirements are for accepting them.
|
||||
|
||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
||||
|
||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
||||
|
||||
## Authors and acknowledgment
|
||||
Show your appreciation to those who have contributed to the project.
|
||||
|
||||
## License
|
||||
For open source projects, say how it is licensed.
|
||||
|
||||
## Project status
|
||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
||||
-705
@@ -1,705 +0,0 @@
|
||||
use anyhow::format_err;
|
||||
use cosmic::{
|
||||
app::{self, Task},
|
||||
iced::{
|
||||
self, executor, futures::sink::SinkExt, stream, widget::row, window, Alignment, Size,
|
||||
Subscription,
|
||||
},
|
||||
widget::{
|
||||
button, column, horizontal_space, progress_bar, radio, text, text_input, vertical_space,
|
||||
},
|
||||
Application, ApplicationExt, Core, Element,
|
||||
};
|
||||
use pkgar::{ext::EntryExt, PackageHead};
|
||||
use pkgar_core::PackageSrc;
|
||||
use pkgar_keys::PublicKeyFile;
|
||||
use redox_installer::{try_fast_install, with_redoxfs_mount, with_whole_disk, Config, DiskOption};
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
io::{self, Read, Write},
|
||||
os::unix::fs::{symlink, MetadataExt, OpenOptionsExt},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
fn main() -> iced::Result {
|
||||
let mut settings = app::Settings::default();
|
||||
settings = settings.size(Size::new(608.0, 416.0));
|
||||
settings = settings.exit_on_close(false);
|
||||
app::run::<Window>(settings, ())
|
||||
}
|
||||
|
||||
fn sudo(password: &str) -> Result<(), String> {
|
||||
let file = libredox::call::open("/scheme/sudo", libredox::flag::O_CLOEXEC, 0)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
libredox::call::write(file, password.as_bytes()).map_err(|err| err.to_string())?;
|
||||
|
||||
// FIXME move to libredox
|
||||
unsafe extern "C" {
|
||||
safe fn redox_cur_procfd_v0() -> usize;
|
||||
}
|
||||
|
||||
// Elevate privileges of our own process with help from the sudo daemon
|
||||
syscall::sendfd(
|
||||
file,
|
||||
syscall::dup(redox_cur_procfd_v0(), &[]).map_err(|err| err.to_string())?,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disk_paths() -> Result<Vec<(String, u64)>, String> {
|
||||
let mut schemes = Vec::new();
|
||||
match fs::read_dir("/scheme/") {
|
||||
Ok(entries) => {
|
||||
for entry_res in entries {
|
||||
if let Ok(entry) = entry_res {
|
||||
let path = entry.path();
|
||||
if let Ok(path_str) = path.into_os_string().into_string() {
|
||||
let scheme = path_str.trim_start_matches("/scheme/").trim_matches('/');
|
||||
if scheme.starts_with("disk") {
|
||||
if scheme == "disk/live" {
|
||||
// Skip live disks
|
||||
continue;
|
||||
}
|
||||
|
||||
schemes.push(format!("/scheme/{}", scheme));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(format!("failed to list schemes: {}", err));
|
||||
}
|
||||
}
|
||||
|
||||
let mut paths = Vec::new();
|
||||
for scheme in schemes {
|
||||
let is_dir = fs::metadata(&scheme).map(|x| x.is_dir()).unwrap_or(false);
|
||||
if is_dir {
|
||||
match fs::read_dir(&scheme) {
|
||||
Ok(entries) => {
|
||||
for entry_res in entries {
|
||||
if let Ok(entry) = entry_res {
|
||||
if let Ok(file_name) = entry.file_name().into_string() {
|
||||
if file_name.contains('p') {
|
||||
// Skip partitions
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(path) = entry.path().into_os_string().into_string() {
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
let size = metadata.len();
|
||||
if size > 0 {
|
||||
paths.push((path, size));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(format!("failed to list '{}': {}", scheme, err));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
const KIB: u64 = 1024;
|
||||
const MIB: u64 = 1024 * KIB;
|
||||
const GIB: u64 = 1024 * MIB;
|
||||
const TIB: u64 = 1024 * GIB;
|
||||
|
||||
fn format_size(size: u64) -> String {
|
||||
if size >= 4 * TIB {
|
||||
format!("{:.1} TiB", size as f64 / TIB as f64)
|
||||
} else if size >= GIB {
|
||||
format!("{:.1} GiB", size as f64 / GIB as f64)
|
||||
} else if size >= MIB {
|
||||
format!("{:.1} MiB", size as f64 / MIB as f64)
|
||||
} else if size >= KIB {
|
||||
format!("{:.1} KiB", size as f64 / KIB as f64)
|
||||
} else {
|
||||
format!("{} B", size)
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_file(src: &Path, dest: &Path, buf: &mut [u8]) -> anyhow::Result<()> {
|
||||
if let Some(parent) = dest.parent() {
|
||||
// Parent may be a symlink
|
||||
if !parent.is_symlink() {
|
||||
match fs::create_dir_all(&parent) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
return Err(format_err!(
|
||||
"failed to create directory {}: {}",
|
||||
parent.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = match fs::symlink_metadata(&src) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
return Err(format_err!(
|
||||
"failed to read metadata of {}: {}",
|
||||
src.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if metadata.file_type().is_symlink() {
|
||||
let real_src = match fs::read_link(&src) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
return Err(format_err!(
|
||||
"failed to read link {}: {}",
|
||||
src.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
match symlink(&real_src, &dest) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
return Err(format_err!(
|
||||
"failed to copy link {} ({}) to {}: {}",
|
||||
src.display(),
|
||||
real_src.display(),
|
||||
dest.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut src_file = match fs::File::open(&src) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
return Err(format_err!(
|
||||
"failed to open file {}: {}",
|
||||
src.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mut dest_file = match fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.mode(metadata.mode())
|
||||
.open(&dest)
|
||||
{
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
return Err(format_err!(
|
||||
"failed to create file {}: {}",
|
||||
dest.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
let count = match src_file.read(buf) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
return Err(format_err!(
|
||||
"failed to read file {}: {}",
|
||||
src.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
match dest_file.write_all(&buf[..count]) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
return Err(format_err!(
|
||||
"failed to write file {}: {}",
|
||||
dest.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn package_files(
|
||||
root_path: &Path,
|
||||
config: &mut Config,
|
||||
files: &mut Vec<String>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
//TODO: Remove packages from config where all files are located (and have valid shasum?)
|
||||
config.packages.clear();
|
||||
|
||||
let pkey_path = "pkg/id_ed25519.pub.toml";
|
||||
let pkey = PublicKeyFile::open(&root_path.join(pkey_path))?.pkey;
|
||||
files.push(pkey_path.to_string());
|
||||
|
||||
for item_res in fs::read_dir(&root_path.join("pkg"))? {
|
||||
let item = item_res?;
|
||||
let pkg_path = item.path();
|
||||
if pkg_path.extension() == Some(OsStr::new("pkgar_head")) {
|
||||
let mut pkg = PackageHead::new(&pkg_path, &root_path, &pkey)?;
|
||||
for entry in pkg.read_entries()? {
|
||||
files.push(entry.check_path()?.to_str().unwrap().to_string());
|
||||
}
|
||||
files.push(
|
||||
pkg_path
|
||||
.strip_prefix(root_path)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install<F: FnMut(Message)>(disk_path: String, password_opt: Option<String>, mut f: F) {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let mut progress = 0;
|
||||
|
||||
macro_rules! message {
|
||||
($($arg:tt)*) => {{
|
||||
eprintln!($($arg)*);
|
||||
f(Message::Install(
|
||||
progress,
|
||||
format!($($arg)*)
|
||||
));
|
||||
}}
|
||||
}
|
||||
|
||||
let root_path = Path::new("/scheme/file/");
|
||||
|
||||
message!("Loading bootloader");
|
||||
let bootloader_bios = {
|
||||
let path = root_path.join("boot").join("bootloader.bios");
|
||||
if path.exists() {
|
||||
match fs::read(&path) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
f(Message::Error(format!(
|
||||
"{}: failed to read: {}",
|
||||
path.display(),
|
||||
err
|
||||
)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
message!("Loading bootloader.efi");
|
||||
let bootloader_efi = {
|
||||
let path = root_path.join("boot").join("bootloader.efi");
|
||||
if path.exists() {
|
||||
match fs::read(&path) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
f(Message::Error(format!(
|
||||
"{}: failed to read: {}",
|
||||
path.display(),
|
||||
err
|
||||
)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
message!("Formatting disk");
|
||||
let disk_option = DiskOption {
|
||||
bootloader_bios: &bootloader_bios,
|
||||
bootloader_efi: &bootloader_efi,
|
||||
password_opt: password_opt.as_ref().map(|x| x.as_bytes()),
|
||||
efi_partition_size: None,
|
||||
skip_partitions: false,
|
||||
};
|
||||
let res = with_whole_disk(&disk_path, &disk_option, |mut fs| -> anyhow::Result<()> {
|
||||
// Fast install method via filesystem clone
|
||||
let mut last_progress = 0;
|
||||
if try_fast_install(&mut fs, |used, used_old| {
|
||||
progress = ((used * 100) / used_old) as usize;
|
||||
if progress != last_progress {
|
||||
message!(
|
||||
"{}%: {} MB/{} MB",
|
||||
progress,
|
||||
used / 1000 / 1000,
|
||||
used_old / 1000 / 1000
|
||||
);
|
||||
last_progress = progress;
|
||||
}
|
||||
})? {
|
||||
progress = 100;
|
||||
message!("Finished installing using fast mode");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
with_redoxfs_mount(fs, None, |mount_path: &Path| -> anyhow::Result<()> {
|
||||
message!("Loading filesystem.toml");
|
||||
let mut config: Config = {
|
||||
let path = root_path.join("filesystem.toml");
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(config_data) => match toml::from_str(&config_data) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
return Err(format_err!(
|
||||
"{}: failed to decode: {}",
|
||||
path.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
return Err(format_err!("{}: failed to read: {}", path.display(), err));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Copy filesystem.toml, which is not packaged
|
||||
let mut files = vec!["filesystem.toml".to_string()];
|
||||
|
||||
// Copy files from locally installed packages
|
||||
message!("Loading package files");
|
||||
if let Err(err) = package_files(&root_path, &mut config, &mut files) {
|
||||
return Err(format_err!("failed to read package files: {}", err));
|
||||
}
|
||||
|
||||
// Sort and remove duplicates
|
||||
files.sort();
|
||||
files.dedup();
|
||||
|
||||
// Perform config install (after packages have been converted to files)
|
||||
message!("Configuring system");
|
||||
let cookbook: Option<&'static str> = None;
|
||||
redox_installer::install_dir(config, mount_path, cookbook)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
||||
|
||||
// Install files
|
||||
let mut buf = vec![0; 4 * MIB as usize];
|
||||
for (i, name) in files.iter().enumerate() {
|
||||
progress = (i * 100) / files.len();
|
||||
message!("Copy {} [{}/{}]", name, i, files.len());
|
||||
|
||||
let src = root_path.join(name);
|
||||
let dest = mount_path.join(name);
|
||||
copy_file(&src, &dest, &mut buf)?;
|
||||
}
|
||||
|
||||
progress = 100;
|
||||
message!("Finished installing, unmounting filesystem");
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(()) => {
|
||||
f(Message::Success(format!(
|
||||
"Finished installing in {:?}, ready to reboot",
|
||||
start.elapsed()
|
||||
)));
|
||||
}
|
||||
Err(err) => {
|
||||
f(Message::Error(format!("Failed to install: {}", err)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Page {
|
||||
Sudo(String),
|
||||
Disk(Option<usize>),
|
||||
Install(usize, String),
|
||||
Success(String),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Worker {
|
||||
command_sender: std::sync::mpsc::Sender<(String, Option<String>)>,
|
||||
join_handle: Arc<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Message {
|
||||
None,
|
||||
Worker(Worker),
|
||||
SudoInput(String),
|
||||
SudoSubmit,
|
||||
DiskChoose(usize),
|
||||
DiskConfirm(usize),
|
||||
Install(usize, String),
|
||||
Success(String),
|
||||
Exit,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
struct Window {
|
||||
core: Core,
|
||||
page: Page,
|
||||
disk_paths: Vec<(String, u64)>,
|
||||
worker_opt: Option<Worker>,
|
||||
}
|
||||
|
||||
impl Application for Window {
|
||||
type Executor = executor::Default;
|
||||
type Flags = ();
|
||||
type Message = Message;
|
||||
|
||||
const APP_ID: &'static str = "org.redox-os.InstallerGui";
|
||||
|
||||
fn init(core: Core, _flags: ()) -> (Self, Task<Message>) {
|
||||
let uid = libredox::call::geteuid().unwrap();
|
||||
let (page, disk_paths) = if uid == 0 {
|
||||
//TODO: load in background
|
||||
match disk_paths() {
|
||||
Ok(disk_paths) => (Page::Disk(None), disk_paths),
|
||||
Err(err) => (Page::Error(err), Vec::new()),
|
||||
}
|
||||
} else {
|
||||
(Page::Sudo(String::new()), Vec::new())
|
||||
};
|
||||
|
||||
let mut app = Self {
|
||||
core,
|
||||
page,
|
||||
disk_paths,
|
||||
worker_opt: None,
|
||||
};
|
||||
let task = app.set_window_title("Redox OS Installer".to_string());
|
||||
(app, task)
|
||||
}
|
||||
|
||||
fn core(&self) -> &Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::None => {}
|
||||
Message::Worker(worker) => {
|
||||
self.worker_opt = Some(worker);
|
||||
}
|
||||
Message::SudoInput(password) => {
|
||||
self.page = Page::Sudo(password);
|
||||
}
|
||||
Message::SudoSubmit => {
|
||||
if let Page::Sudo(password) = &self.page {
|
||||
//TODO: run async?
|
||||
match sudo(password) {
|
||||
Ok(()) => {
|
||||
(self.page, self.disk_paths) = match disk_paths() {
|
||||
Ok(disk_paths) => (Page::Disk(None), disk_paths),
|
||||
Err(err) => (Page::Error(err), Vec::new()),
|
||||
};
|
||||
}
|
||||
Err(err) => {
|
||||
//TODO: show error in GUI
|
||||
eprintln!("{err}");
|
||||
self.page = Page::Sudo(String::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::DiskChoose(disk_i) => {
|
||||
self.page = Page::Disk(Some(disk_i));
|
||||
}
|
||||
Message::DiskConfirm(disk_i) => match self.disk_paths.get(disk_i) {
|
||||
Some((disk_path, _disk_size)) => match &self.worker_opt {
|
||||
Some(worker) => match worker.command_sender.send((disk_path.clone(), None)) {
|
||||
Ok(()) => self.page = Page::Install(0, format!("Starting install...")),
|
||||
Err(err) => {
|
||||
self.page = Page::Error(format!("failed to send command: {}", err));
|
||||
}
|
||||
},
|
||||
None => {
|
||||
self.page = Page::Error(format!("command sender not found"));
|
||||
}
|
||||
},
|
||||
None => {
|
||||
self.page = Page::Error(format!("invalid disk number {} chosen", disk_i));
|
||||
}
|
||||
},
|
||||
Message::Install(progress, description) => {
|
||||
self.page = Page::Install(progress, description);
|
||||
}
|
||||
Message::Success(description) => {
|
||||
self.page = Page::Success(description);
|
||||
}
|
||||
Message::Error(err) => {
|
||||
self.page = Page::Error(err);
|
||||
}
|
||||
Message::Exit => {
|
||||
if let Some(worker) = self.worker_opt.take() {
|
||||
drop(worker.command_sender);
|
||||
let join_handle = Arc::try_unwrap(worker.join_handle).unwrap();
|
||||
join_handle.join().unwrap();
|
||||
}
|
||||
if let Some(window_id) = self.core.main_window_id() {
|
||||
return window::close(window_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<'_, Message> {
|
||||
let mut widgets = Vec::new();
|
||||
match &self.page {
|
||||
Page::Sudo(password) => {
|
||||
widgets.push(text("Enter your password:").into());
|
||||
widgets.push(
|
||||
text_input("", password)
|
||||
.password()
|
||||
.on_input(Message::SudoInput)
|
||||
.on_submit(|_| Message::SudoSubmit)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
Page::Disk(disk_i_opt) => {
|
||||
if !self.disk_paths.is_empty() {
|
||||
widgets.push(text("Choose a drive:").size(24).into());
|
||||
|
||||
for (disk_i, (disk_path, disk_size)) in self.disk_paths.iter().enumerate() {
|
||||
widgets.push(
|
||||
row![
|
||||
radio(text(disk_path), disk_i, *disk_i_opt, Message::DiskChoose),
|
||||
horizontal_space(),
|
||||
text(format_size(*disk_size)),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(disk_i) = *disk_i_opt {
|
||||
widgets.push(vertical_space().into());
|
||||
widgets.push(
|
||||
row![
|
||||
horizontal_space(),
|
||||
button::destructive("Confirm")
|
||||
.on_press(Message::DiskConfirm(disk_i)),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
widgets.push(text("No drives found").into());
|
||||
// TODO: expose disk.pci-*-*nvme/* */ scheme to user
|
||||
widgets.push(text("(try to rerun with sudo)").into());
|
||||
}
|
||||
}
|
||||
Page::Install(progress, description) => {
|
||||
widgets.push(text("Installation progress:").size(24).into());
|
||||
widgets.push(progress_bar(0.0..=100.0, *progress as f32).into());
|
||||
widgets.push(text(description).into());
|
||||
}
|
||||
Page::Success(description) => {
|
||||
widgets.push(text("Installation complete!").size(24).into());
|
||||
widgets.push(text(description).into());
|
||||
widgets.push(vertical_space().into());
|
||||
widgets.push(
|
||||
row![
|
||||
horizontal_space(),
|
||||
button::standard("Exit").on_press(Message::Exit),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
Page::Error(err) => {
|
||||
widgets.push(text(format!("{}", err)).into());
|
||||
}
|
||||
};
|
||||
|
||||
column::with_children(widgets)
|
||||
.spacing(8)
|
||||
.padding(24)
|
||||
.align_x(Alignment::Start)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
enum State {
|
||||
Ready,
|
||||
Waiting(iced::futures::channel::mpsc::UnboundedReceiver<Message>),
|
||||
Finished,
|
||||
}
|
||||
|
||||
Subscription::run_with_id(
|
||||
std::any::TypeId::of::<Worker>(),
|
||||
stream::channel(100, |mut output| async move {
|
||||
let mut state = State::Ready;
|
||||
loop {
|
||||
let (message, new_state) = match state {
|
||||
State::Ready => {
|
||||
let (command_sender, command_receiver) = std::sync::mpsc::channel();
|
||||
|
||||
let (message_sender, message_receiver) =
|
||||
iced::futures::channel::mpsc::unbounded();
|
||||
|
||||
//TODO: kill worker thread?
|
||||
let join_handle = std::thread::spawn(move || {
|
||||
while let Ok((disk_path, password_opt)) = command_receiver.recv() {
|
||||
println!("Installing to {:?}", disk_path);
|
||||
install(disk_path, password_opt, |message| {
|
||||
message_sender.unbounded_send(message).unwrap();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let worker = Worker {
|
||||
command_sender,
|
||||
join_handle: Arc::new(join_handle),
|
||||
};
|
||||
|
||||
(Message::Worker(worker), State::Waiting(message_receiver))
|
||||
}
|
||||
State::Waiting(mut message_receiver) => {
|
||||
use iced::futures::StreamExt;
|
||||
match message_receiver.next().await {
|
||||
Some(message) => (message, State::Waiting(message_receiver)),
|
||||
None => (Message::None, State::Finished),
|
||||
}
|
||||
}
|
||||
State::Finished => iced::futures::future::pending().await,
|
||||
};
|
||||
output.send(message).await.unwrap();
|
||||
state = new_state;
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
-411
@@ -1,411 +0,0 @@
|
||||
# Automatically generated by update.sh
|
||||
|
||||
include = []
|
||||
|
||||
[general]
|
||||
prompt = false
|
||||
filesystem_size = 256
|
||||
|
||||
[packages.base]
|
||||
|
||||
[packages.base-initfs]
|
||||
|
||||
[packages.bootloader]
|
||||
|
||||
[packages.ca-certificates]
|
||||
|
||||
[packages.coreutils]
|
||||
|
||||
[packages.extrautils]
|
||||
|
||||
[packages.findutils]
|
||||
|
||||
[packages.ion]
|
||||
|
||||
[packages.kernel]
|
||||
|
||||
[packages.kibi]
|
||||
|
||||
[packages.libgcc]
|
||||
|
||||
[packages.libstdcxx]
|
||||
|
||||
[packages.netdb]
|
||||
|
||||
[packages.netutils]
|
||||
|
||||
[packages.pkgutils]
|
||||
|
||||
[packages.relibc]
|
||||
|
||||
[packages.userutils]
|
||||
|
||||
[packages.uutils]
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/00_base"
|
||||
data = """
|
||||
# clear and recreate tmpdir with 0o1777 permission
|
||||
rm -rf /tmp
|
||||
mkdir -m a=rwxt /tmp
|
||||
|
||||
ipcd
|
||||
ptyd
|
||||
nowait sudo --daemon
|
||||
"""
|
||||
symlink = false
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/00_drivers"
|
||||
data = """
|
||||
pcid-spawner /etc/pcid.d/
|
||||
"""
|
||||
symlink = false
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/etc/hostname"
|
||||
data = "redox"
|
||||
symlink = false
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/os-release"
|
||||
data = """
|
||||
PRETTY_NAME="Redox OS 0.9.0"
|
||||
NAME="Redox OS"
|
||||
VERSION_ID="0.9.0"
|
||||
VERSION="0.9.0"
|
||||
ID="redox-os"
|
||||
|
||||
HOME_URL="https://redox-os.org/"
|
||||
DOCUMENTATION_URL="https://redox-os.org/docs/"
|
||||
SUPPORT_URL="https://redox-os.org/community/"
|
||||
"""
|
||||
symlink = false
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/etc/os-release"
|
||||
data = "../usr/lib/os-release"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/etc/pkg.d/50_redox"
|
||||
data = "https://static.redox-os.org/pkg"
|
||||
symlink = false
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/usr"
|
||||
data = ""
|
||||
symlink = false
|
||||
directory = true
|
||||
mode = 493
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/usr/bin"
|
||||
data = ""
|
||||
symlink = false
|
||||
directory = true
|
||||
mode = 493
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/bin"
|
||||
data = "usr/bin"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/usr/include"
|
||||
data = ""
|
||||
symlink = false
|
||||
directory = true
|
||||
mode = 493
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/include"
|
||||
data = "usr/include"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib"
|
||||
data = ""
|
||||
symlink = false
|
||||
directory = true
|
||||
mode = 493
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/lib"
|
||||
data = "usr/lib"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/usr/libexec"
|
||||
data = ""
|
||||
symlink = false
|
||||
directory = true
|
||||
mode = 493
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/usr/share"
|
||||
data = ""
|
||||
symlink = false
|
||||
directory = true
|
||||
mode = 493
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/share"
|
||||
data = "usr/share"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/usr/share/fonts"
|
||||
data = "../../ui/fonts"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/var"
|
||||
data = ""
|
||||
symlink = false
|
||||
directory = true
|
||||
mode = 493
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/var/cache"
|
||||
data = ""
|
||||
symlink = false
|
||||
directory = true
|
||||
mode = 493
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/var/lib"
|
||||
data = ""
|
||||
symlink = false
|
||||
directory = true
|
||||
mode = 493
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/var/lock"
|
||||
data = ""
|
||||
symlink = false
|
||||
directory = true
|
||||
mode = 1023
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/var/log"
|
||||
data = ""
|
||||
symlink = false
|
||||
directory = true
|
||||
mode = 493
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/var/run"
|
||||
data = ""
|
||||
symlink = false
|
||||
directory = true
|
||||
mode = 493
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/var/tmp"
|
||||
data = ""
|
||||
symlink = false
|
||||
directory = true
|
||||
mode = 1023
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/dev/null"
|
||||
data = "/scheme/null"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/dev/random"
|
||||
data = "/scheme/rand"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/dev/urandom"
|
||||
data = "/scheme/rand"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/dev/zero"
|
||||
data = "/scheme/zero"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/dev/tty"
|
||||
data = "libc:tty"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/dev/stdin"
|
||||
data = "libc:stdin"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/dev/stdout"
|
||||
data = "libc:stdout"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/dev/stderr"
|
||||
data = "libc:stderr"
|
||||
symlink = true
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/10_net"
|
||||
data = """
|
||||
smolnetd
|
||||
nowait dhcpd
|
||||
"""
|
||||
symlink = false
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/etc/net/dns"
|
||||
data = """
|
||||
9.9.9.9
|
||||
"""
|
||||
symlink = false
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/etc/net/ip"
|
||||
data = """
|
||||
10.0.2.15
|
||||
"""
|
||||
symlink = false
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/etc/net/ip_router"
|
||||
data = """
|
||||
10.0.2.2
|
||||
"""
|
||||
symlink = false
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/etc/net/ip_subnet"
|
||||
data = """
|
||||
255.255.255.0
|
||||
"""
|
||||
symlink = false
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/30_console"
|
||||
data = """
|
||||
inputd -A 2
|
||||
nowait getty 2
|
||||
nowait getty /scheme/debug -J
|
||||
"""
|
||||
symlink = false
|
||||
directory = false
|
||||
recursive_chown = false
|
||||
postinstall = false
|
||||
|
||||
[users.root]
|
||||
password = "password"
|
||||
uid = 0
|
||||
gid = 0
|
||||
name = "root"
|
||||
home = "/root"
|
||||
shell = "/usr/bin/ion"
|
||||
|
||||
[users.user]
|
||||
password = ""
|
||||
shell = "/usr/bin/ion"
|
||||
|
||||
[groups.sudo]
|
||||
gid = 1
|
||||
members = ["user"]
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
RES_PATH="$(dirname "$0")"
|
||||
|
||||
if [ -d "$1" ]
|
||||
then
|
||||
REDOX_PATH="$1"
|
||||
else
|
||||
echo "$0 [path to redox repository]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -x
|
||||
|
||||
# Update res/test.toml from the redoxer.toml template
|
||||
"${REDOX_PATH}/build/fstools/bin/redox_installer" \
|
||||
--config="${REDOX_PATH}/config/x86_64/minimal-net.toml" \
|
||||
--output-config="${RES_PATH}/test.toml"
|
||||
sed -i '1s/^/# Automatically generated by update.sh\n\n/' "${RES_PATH}/test.toml"
|
||||
@@ -0,0 +1,394 @@
|
||||
use alloc::{collections::BTreeSet, vec::Vec};
|
||||
use core::{fmt, mem, ops, slice};
|
||||
use endian_num::Le;
|
||||
|
||||
use crate::{BlockAddr, BlockLevel, BlockMeta, BlockPtr, BlockTrait, Node, TreePtr, BLOCK_SIZE};
|
||||
|
||||
pub const ALLOC_LIST_ENTRIES: usize =
|
||||
(BLOCK_SIZE as usize - mem::size_of::<BlockPtr<AllocList>>()) / mem::size_of::<AllocEntry>();
|
||||
pub const RELEASE_LIST_ENTRIES: usize = (BLOCK_SIZE as usize
|
||||
- mem::size_of::<BlockPtr<ReleaseList>>())
|
||||
/ mem::size_of::<TreePtr<Node>>();
|
||||
|
||||
/// The RedoxFS block allocator. This struct manages all "data" blocks in RedoxFS
|
||||
/// (i.e, all blocks that aren't reserved or part of the header chain).
|
||||
///
|
||||
/// [`Allocator`] can allocate blocks of many "levels"---that is, it can
|
||||
/// allocate multiple consecutive [`BLOCK_SIZE`] blocks in one operation.
|
||||
///
|
||||
/// This reduces the amount of memory that the [`Allocator`] uses:
|
||||
/// Instead of storing the index of each free [`BLOCK_SIZE`] block,
|
||||
/// the `levels` array can keep track of higher-level blocks, splitting
|
||||
/// them when a smaller block is requested.
|
||||
///
|
||||
/// Higher-level blocks also allow us to more efficiently allocate memory
|
||||
/// for large files.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Allocator {
|
||||
/// This array keeps track of all free blocks of each level,
|
||||
/// and is initialized using the AllocList chain when we open the filesystem.
|
||||
///
|
||||
/// Every element of the outer array represents a block level:
|
||||
/// - item 0: free level 0 blocks (with size [`BLOCK_SIZE`])
|
||||
/// - item 1: free level 1 blocks (with size 2*[`BLOCK_SIZE`])
|
||||
/// - item 2: free level 2 blocks (with size 4*[`BLOCK_SIZE`])
|
||||
/// ...and so on.
|
||||
///
|
||||
/// Each inner array contains a list of free block indices,
|
||||
levels: Vec<BTreeSet<u64>>,
|
||||
}
|
||||
|
||||
impl Allocator {
|
||||
pub fn levels(&self) -> &Vec<BTreeSet<u64>> {
|
||||
&self.levels
|
||||
}
|
||||
|
||||
/// Count the number of free [`BLOCK_SIZE`] available to this [`Allocator`].
|
||||
pub fn free(&self) -> u64 {
|
||||
let mut free = 0;
|
||||
for level in 0..self.levels.len() {
|
||||
let level_size = 1 << level;
|
||||
free += self.levels[level].len() as u64 * level_size;
|
||||
}
|
||||
free
|
||||
}
|
||||
|
||||
/// Find a free block of the given level, mark it as "used", and return its address.
|
||||
/// Returns [`None`] if there are no free blocks with this level.
|
||||
pub fn allocate(&mut self, meta: BlockMeta) -> Option<BlockAddr> {
|
||||
// First, find the lowest level with a free block
|
||||
let mut free_opt = None;
|
||||
{
|
||||
let mut level = meta.level.0;
|
||||
// Start searching at the level we want. Smaller levels are too small!
|
||||
while level < self.levels.len() {
|
||||
if let Some(&index) = self.levels[level].first() {
|
||||
// Find the index closest to the start of the filesystem
|
||||
free_opt = match free_opt {
|
||||
Some((free_level, free_index)) if free_index <= index => {
|
||||
Some((free_level, free_index))
|
||||
}
|
||||
_ => Some((level, index)),
|
||||
};
|
||||
}
|
||||
level += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If a free block was found, split it until we find a usable block of the right level.
|
||||
// The left side of the split block is kept free, and the right side is allocated.
|
||||
let (mut level, index) = free_opt?;
|
||||
self.levels[level].remove(&index);
|
||||
while level > meta.level.0 {
|
||||
level -= 1;
|
||||
let level_size = 1 << level;
|
||||
self.levels[level].insert(index + level_size);
|
||||
}
|
||||
|
||||
Some(unsafe { BlockAddr::new(index, meta) })
|
||||
}
|
||||
|
||||
/// Try to allocate the exact block specified, making all necessary splits.
|
||||
/// Returns [`None`] if this some (or all) of this block is already allocated.
|
||||
///
|
||||
/// Note that [`BlockAddr`] encodes the blocks location _and_ level.
|
||||
pub fn allocate_exact(&mut self, exact_addr: BlockAddr) -> Option<BlockAddr> {
|
||||
// This function only supports level 0 right now
|
||||
assert_eq!(exact_addr.level().0, 0);
|
||||
let exact_index = exact_addr.index();
|
||||
|
||||
let mut index_opt = None;
|
||||
|
||||
// Go from the highest to the lowest level
|
||||
for level in (0..self.levels.len()).rev() {
|
||||
let level_size = 1 << level;
|
||||
|
||||
// Split higher block if found
|
||||
if let Some(index) = index_opt.take() {
|
||||
self.levels[level].insert(index);
|
||||
self.levels[level].insert(index + level_size);
|
||||
}
|
||||
|
||||
// Look for matching block and remove it
|
||||
for &start in self.levels[level].iter() {
|
||||
if start <= exact_index {
|
||||
let end = start + level_size;
|
||||
if end > exact_index {
|
||||
self.levels[level].remove(&start);
|
||||
index_opt = Some(start);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(unsafe { BlockAddr::new(index_opt?, exact_addr.meta()) })
|
||||
}
|
||||
|
||||
/// Deallocate the given block, marking it "free" so that it can be re-used later.
|
||||
pub fn deallocate(&mut self, addr: BlockAddr) {
|
||||
// When we deallocate, we check if block we're deallocating has a free sibling.
|
||||
// If it does, we join the two to create one free block in the next (higher) level.
|
||||
//
|
||||
// We repeat this until we no longer have a sibling to join.
|
||||
let mut index = addr.index();
|
||||
let mut level = addr.level().0;
|
||||
loop {
|
||||
while level >= self.levels.len() {
|
||||
self.levels.push(BTreeSet::new());
|
||||
}
|
||||
|
||||
let level_size = 1 << level;
|
||||
let next_size = level_size << 1;
|
||||
|
||||
let mut found = false;
|
||||
// look at all free blocks in the current level...
|
||||
for &level_index in self.levels[level].iter() {
|
||||
// - the block we just freed aligns with the next largest block, and
|
||||
// - the second block we're looking at is the right sibling of this block
|
||||
if index % next_size == 0 && index + level_size == level_index {
|
||||
// "alloc" the next highest block, repeat deallocation process.
|
||||
self.levels[level].remove(&level_index);
|
||||
found = true;
|
||||
break;
|
||||
// - the index of this block doesn't align with the next largest block, and
|
||||
// - the block we're looking at is the left neighbor of this block
|
||||
} else if level_index % next_size == 0 && level_index + level_size == index {
|
||||
// "alloc" the next highest block, repeat deallocation process.
|
||||
self.levels[level].remove(&level_index);
|
||||
index = level_index; // index moves to left block
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We couldn't find a higher block,
|
||||
// deallocate this one and finish
|
||||
if !found {
|
||||
self.levels[level].insert(index);
|
||||
return;
|
||||
}
|
||||
|
||||
// repeat deallocation process on the
|
||||
// higher-level block we just created.
|
||||
level += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
pub struct AllocEntry {
|
||||
/// The index of the first block this [`AllocEntry`] refers to
|
||||
index: Le<u64>,
|
||||
|
||||
/// The number of blocks after (and including) `index` that are are free or used.
|
||||
/// If negative, they are used; if positive, they are free.
|
||||
count: Le<i64>,
|
||||
}
|
||||
|
||||
impl AllocEntry {
|
||||
pub fn new(index: u64, count: i64) -> Self {
|
||||
Self {
|
||||
index: index.into(),
|
||||
count: count.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocate(addr: BlockAddr) -> Self {
|
||||
Self::new(addr.index(), -addr.level().blocks::<i64>())
|
||||
}
|
||||
|
||||
pub fn deallocate(addr: BlockAddr) -> Self {
|
||||
Self::new(addr.index(), addr.level().blocks::<i64>())
|
||||
}
|
||||
|
||||
pub fn index(&self) -> u64 {
|
||||
self.index.to_ne()
|
||||
}
|
||||
|
||||
pub fn count(&self) -> i64 {
|
||||
self.count.to_ne()
|
||||
}
|
||||
|
||||
pub fn is_null(&self) -> bool {
|
||||
self.count() == 0
|
||||
}
|
||||
}
|
||||
|
||||
/// A node in the allocation chain.
|
||||
#[repr(C, packed)]
|
||||
pub struct AllocList {
|
||||
/// A pointer to the previous AllocList.
|
||||
/// If this is the null pointer, this is the first element of the chain.
|
||||
pub prev: BlockPtr<AllocList>,
|
||||
|
||||
/// Allocation entries.
|
||||
pub entries: [AllocEntry; ALLOC_LIST_ENTRIES],
|
||||
}
|
||||
|
||||
unsafe impl BlockTrait for AllocList {
|
||||
fn empty(level: BlockLevel) -> Option<Self> {
|
||||
if level.0 == 0 {
|
||||
Some(Self {
|
||||
prev: BlockPtr::default(),
|
||||
entries: [AllocEntry::default(); ALLOC_LIST_ENTRIES],
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for AllocList {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let prev = self.prev;
|
||||
let entries: Vec<&AllocEntry> = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.count() > 0)
|
||||
.collect();
|
||||
f.debug_struct("AllocList")
|
||||
.field("prev", &prev)
|
||||
.field("entries", &entries)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for AllocList {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts(
|
||||
self as *const AllocList as *const u8,
|
||||
mem::size_of::<AllocList>(),
|
||||
) as &[u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for AllocList {
|
||||
fn deref_mut(&mut self) -> &mut [u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts_mut(
|
||||
self as *mut AllocList as *mut u8,
|
||||
mem::size_of::<AllocList>(),
|
||||
) as &mut [u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of nodes pending release.
|
||||
#[repr(C, packed)]
|
||||
pub struct ReleaseList {
|
||||
/// A pointer to the previous ReleaseList.
|
||||
/// If this is the null pointer, this is the first element of the chain.
|
||||
pub prev: BlockPtr<ReleaseList>,
|
||||
|
||||
/// Allocation entries.
|
||||
pub entries: [TreePtr<Node>; RELEASE_LIST_ENTRIES],
|
||||
}
|
||||
|
||||
unsafe impl BlockTrait for ReleaseList {
|
||||
fn empty(level: BlockLevel) -> Option<Self> {
|
||||
if level.0 == 0 {
|
||||
Some(Self {
|
||||
prev: BlockPtr::default(),
|
||||
entries: [TreePtr::<Node>::default(); RELEASE_LIST_ENTRIES],
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ReleaseList {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let prev = self.prev;
|
||||
let entries: Vec<_> = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|entry| !entry.is_null())
|
||||
.map(|entry| entry.id())
|
||||
.collect();
|
||||
f.debug_struct("ReleaseList")
|
||||
.field("prev", &prev)
|
||||
.field("entries", &entries)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for ReleaseList {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts(
|
||||
self as *const ReleaseList as *const u8,
|
||||
mem::size_of::<ReleaseList>(),
|
||||
) as &[u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for ReleaseList {
|
||||
fn deref_mut(&mut self) -> &mut [u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts_mut(
|
||||
self as *mut ReleaseList as *mut u8,
|
||||
mem::size_of::<ReleaseList>(),
|
||||
) as &mut [u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alloc_node_size_test() {
|
||||
assert_eq!(mem::size_of::<AllocList>(), crate::BLOCK_SIZE as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_node_size_test() {
|
||||
assert_eq!(mem::size_of::<ReleaseList>(), crate::BLOCK_SIZE as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocator_test() {
|
||||
let mut alloc = Allocator::default();
|
||||
|
||||
assert_eq!(alloc.allocate(BlockMeta::default()), None);
|
||||
|
||||
alloc.deallocate(unsafe { BlockAddr::new(1, BlockMeta::default()) });
|
||||
assert_eq!(
|
||||
alloc.allocate(BlockMeta::default()),
|
||||
Some(unsafe { BlockAddr::new(1, BlockMeta::default()) })
|
||||
);
|
||||
assert_eq!(alloc.allocate(BlockMeta::default()), None);
|
||||
|
||||
for addr in 1023..2048 {
|
||||
alloc.deallocate(unsafe { BlockAddr::new(addr, BlockMeta::default()) });
|
||||
}
|
||||
|
||||
assert_eq!(alloc.levels.len(), 11);
|
||||
for level in 0..alloc.levels.len() {
|
||||
if level == 0 {
|
||||
assert_eq!(alloc.levels[level], [1023].into());
|
||||
} else if level == 10 {
|
||||
assert_eq!(alloc.levels[level], [1024].into());
|
||||
} else {
|
||||
assert_eq!(alloc.levels[level], [0u64; 0].into());
|
||||
}
|
||||
}
|
||||
|
||||
for addr in 1023..2048 {
|
||||
assert_eq!(
|
||||
alloc.allocate(BlockMeta::default()),
|
||||
Some(unsafe { BlockAddr::new(addr, BlockMeta::default()) })
|
||||
);
|
||||
}
|
||||
assert_eq!(alloc.allocate(BlockMeta::default()), None);
|
||||
|
||||
assert_eq!(alloc.levels.len(), 11);
|
||||
for level in 0..alloc.levels.len() {
|
||||
assert_eq!(alloc.levels[level], [0u64; 0].into());
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{Disk, FileSystem, Node, Transaction, TreePtr, BLOCK_SIZE};
|
||||
|
||||
fn syscall_err(err: syscall::Error) -> io::Error {
|
||||
io::Error::from_raw_os_error(err.errno)
|
||||
}
|
||||
|
||||
pub fn archive_at<D: Disk, P: AsRef<Path>>(
|
||||
tx: &mut Transaction<D>,
|
||||
parent_path: P,
|
||||
parent_ptr: TreePtr<Node>,
|
||||
) -> io::Result<()> {
|
||||
for entry_res in fs::read_dir(parent_path)? {
|
||||
let entry = entry_res?;
|
||||
|
||||
let metadata = entry.metadata()?;
|
||||
let file_type = metadata.file_type();
|
||||
|
||||
let name = entry.file_name().into_string().map_err(|_| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "filename is not valid UTF-8")
|
||||
})?;
|
||||
|
||||
let mode_type = if file_type.is_dir() {
|
||||
Node::MODE_DIR
|
||||
} else if file_type.is_file() {
|
||||
Node::MODE_FILE
|
||||
} else if file_type.is_symlink() {
|
||||
Node::MODE_SYMLINK
|
||||
} else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Does not support parsing {:?}", file_type),
|
||||
));
|
||||
};
|
||||
|
||||
let node_ptr;
|
||||
{
|
||||
let mode = mode_type | (metadata.mode() as u16 & Node::MODE_PERM);
|
||||
let mut node = tx
|
||||
.create_node(
|
||||
parent_ptr,
|
||||
&name,
|
||||
mode,
|
||||
metadata.ctime() as u64,
|
||||
metadata.ctime_nsec() as u32,
|
||||
)
|
||||
.map_err(syscall_err)?;
|
||||
|
||||
node_ptr = node.ptr();
|
||||
|
||||
if node.data().uid() != metadata.uid() || node.data().gid() != metadata.gid() {
|
||||
node.data_mut().set_uid(metadata.uid());
|
||||
node.data_mut().set_gid(metadata.gid());
|
||||
tx.sync_tree(node).map_err(syscall_err)?;
|
||||
}
|
||||
}
|
||||
|
||||
let path = entry.path();
|
||||
if file_type.is_dir() {
|
||||
archive_at(tx, path, node_ptr)?;
|
||||
} else if file_type.is_file() {
|
||||
let data = fs::read(path)?;
|
||||
let count = tx
|
||||
.write_node(
|
||||
node_ptr,
|
||||
0,
|
||||
&data,
|
||||
metadata.mtime() as u64,
|
||||
metadata.mtime_nsec() as u32,
|
||||
)
|
||||
.map_err(syscall_err)?;
|
||||
if count != data.len() {
|
||||
panic!("file write count {} != {}", count, data.len());
|
||||
}
|
||||
} else if file_type.is_symlink() {
|
||||
let destination = fs::read_link(path)?;
|
||||
let data = destination.as_os_str().as_bytes();
|
||||
let count = tx
|
||||
.write_node(
|
||||
node_ptr,
|
||||
0,
|
||||
data,
|
||||
metadata.mtime() as u64,
|
||||
metadata.mtime_nsec() as u32,
|
||||
)
|
||||
.map_err(syscall_err)?;
|
||||
if count != data.len() {
|
||||
panic!("symlink write count {} != {}", count, data.len());
|
||||
}
|
||||
} else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Does not support creating {:?}", file_type),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn archive<D: Disk, P: AsRef<Path>>(fs: &mut FileSystem<D>, parent_path: P) -> io::Result<u64> {
|
||||
let end_block = fs
|
||||
.tx(|tx| {
|
||||
// Archive_at root node
|
||||
archive_at(tx, parent_path, TreePtr::root())
|
||||
.map_err(|err| syscall::Error::new(err.raw_os_error().unwrap()))?;
|
||||
|
||||
// Squash alloc log
|
||||
tx.sync(true)?;
|
||||
|
||||
let end_block = tx.header.size() / BLOCK_SIZE;
|
||||
/* TODO: Cut off any free blocks at the end of the filesystem
|
||||
let mut end_changed = true;
|
||||
while end_changed {
|
||||
end_changed = false;
|
||||
|
||||
let allocator = fs.allocator();
|
||||
let levels = allocator.levels();
|
||||
for level in 0..levels.len() {
|
||||
let level_size = 1 << level;
|
||||
for &block in levels[level].iter() {
|
||||
if block < end_block && block + level_size >= end_block {
|
||||
end_block = block;
|
||||
end_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Update header
|
||||
tx.header.size = (end_block * BLOCK_SIZE).into();
|
||||
tx.header_changed = true;
|
||||
tx.sync(false)?;
|
||||
|
||||
Ok(end_block)
|
||||
})
|
||||
.map_err(syscall_err)?;
|
||||
|
||||
Ok((fs.block + end_block) * BLOCK_SIZE)
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
extern crate redoxfs;
|
||||
extern crate syscall;
|
||||
extern crate uuid;
|
||||
|
||||
use std::io::Read;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{env, fs, process};
|
||||
|
||||
use redoxfs::{archive, DiskFile, FileSystem};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let mut args = env::args().skip(1);
|
||||
|
||||
let disk_path = if let Some(path) = args.next() {
|
||||
path
|
||||
} else {
|
||||
println!("redoxfs-ar: no disk image provided");
|
||||
println!("redoxfs-ar DISK FOLDER [BOOTLOADER]");
|
||||
process::exit(1);
|
||||
};
|
||||
|
||||
let folder_path = if let Some(path) = args.next() {
|
||||
path
|
||||
} else {
|
||||
println!("redoxfs-ar: no folder provided");
|
||||
println!("redoxfs-ar DISK FOLDER [BOOTLOADER]");
|
||||
process::exit(1);
|
||||
};
|
||||
|
||||
let bootloader_path_opt = args.next();
|
||||
|
||||
let disk = match DiskFile::open(&disk_path) {
|
||||
Ok(disk) => disk,
|
||||
Err(err) => {
|
||||
println!("redoxfs-ar: failed to open image {}: {}", disk_path, err);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mut bootloader = vec![];
|
||||
if let Some(bootloader_path) = bootloader_path_opt {
|
||||
match fs::File::open(&bootloader_path) {
|
||||
Ok(mut file) => match file.read_to_end(&mut bootloader) {
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
println!(
|
||||
"redoxfs-ar: failed to read bootloader {}: {}",
|
||||
bootloader_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
println!(
|
||||
"redoxfs-ar: failed to open bootloader {}: {}",
|
||||
bootloader_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
match FileSystem::create_reserved(
|
||||
disk,
|
||||
None,
|
||||
&bootloader,
|
||||
ctime.as_secs(),
|
||||
ctime.subsec_nanos(),
|
||||
) {
|
||||
Ok(mut fs) => {
|
||||
let size = match archive(&mut fs, &folder_path) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
println!("redoxfs-ar: failed to archive {}: {}", folder_path, err);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = fs.disk.file.set_len(size) {
|
||||
println!(
|
||||
"redoxfs-ar: failed to truncate {} to {}: {}",
|
||||
disk_path, size, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let uuid = Uuid::from_bytes(fs.header.uuid());
|
||||
println!(
|
||||
"redoxfs-ar: created filesystem on {}, reserved {} blocks, size {} MB, uuid {}",
|
||||
disk_path,
|
||||
fs.block,
|
||||
fs.header.size() / 1000 / 1000,
|
||||
uuid.hyphenated()
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
println!(
|
||||
"redoxfs-ar: failed to create filesystem on {}: {}",
|
||||
disk_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
extern crate redoxfs;
|
||||
extern crate syscall;
|
||||
extern crate uuid;
|
||||
|
||||
use std::io::Read;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{env, fs, process};
|
||||
|
||||
use redoxfs::{clone, DiskFile, FileSystem};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let mut args = env::args().skip(1);
|
||||
|
||||
let disk_path_old = if let Some(path) = args.next() {
|
||||
path
|
||||
} else {
|
||||
println!("redoxfs-clone: no old disk image provided");
|
||||
println!("redoxfs-clone NEW-DISK OLD-DISK [BOOTLOADER]");
|
||||
process::exit(1);
|
||||
};
|
||||
|
||||
let disk_path = if let Some(path) = args.next() {
|
||||
path
|
||||
} else {
|
||||
println!("redoxfs-clone: no new disk image provided");
|
||||
println!("redoxfs-clone NEW-DISK OLD-DISK [BOOTLOADER]");
|
||||
process::exit(1);
|
||||
};
|
||||
|
||||
let bootloader_path_opt = args.next();
|
||||
|
||||
// Open old disk in readonly mode
|
||||
let disk_old = match fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(false)
|
||||
.open(&disk_path_old)
|
||||
.map(DiskFile::from)
|
||||
{
|
||||
Ok(disk) => disk,
|
||||
Err(err) => {
|
||||
println!(
|
||||
"redoxfs-clone: failed to open old disk image {}: {}",
|
||||
disk_path_old, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mut fs_old = match FileSystem::open(disk_old, None, None, false) {
|
||||
Ok(fs) => fs,
|
||||
Err(err) => {
|
||||
println!(
|
||||
"redoxfs-clone: failed to open filesystem on {}: {}",
|
||||
disk_path_old, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let disk = match DiskFile::open(&disk_path) {
|
||||
Ok(disk) => disk,
|
||||
Err(err) => {
|
||||
println!(
|
||||
"redoxfs-clone: failed to open new disk image {}: {}",
|
||||
disk_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mut bootloader = vec![];
|
||||
if let Some(bootloader_path) = bootloader_path_opt {
|
||||
match fs::File::open(&bootloader_path) {
|
||||
Ok(mut file) => match file.read_to_end(&mut bootloader) {
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
println!(
|
||||
"redoxfs-clone: failed to read bootloader {}: {}",
|
||||
bootloader_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
println!(
|
||||
"redoxfs-clone: failed to open bootloader {}: {}",
|
||||
bootloader_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
let mut fs = match FileSystem::create_reserved(
|
||||
disk,
|
||||
None,
|
||||
&bootloader,
|
||||
ctime.as_secs(),
|
||||
ctime.subsec_nanos(),
|
||||
) {
|
||||
Ok(fs) => fs,
|
||||
Err(err) => {
|
||||
println!(
|
||||
"redoxfs-clone: failed to create filesystem on {}: {}",
|
||||
disk_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let size_old = fs_old.header.size();
|
||||
let free_old = fs_old.allocator().free() * redoxfs::BLOCK_SIZE;
|
||||
let used_old = size_old - free_old;
|
||||
let mut last_percent = 0;
|
||||
let clone_res = clone(&mut fs_old, &mut fs, move |used| {
|
||||
let percent = (used * 100) / used_old;
|
||||
if percent != last_percent {
|
||||
eprint!(
|
||||
"\r{}%: {} MB/{} MB",
|
||||
percent,
|
||||
used / 1000 / 1000,
|
||||
used_old / 1000 / 1000
|
||||
);
|
||||
last_percent = percent;
|
||||
}
|
||||
});
|
||||
eprintln!();
|
||||
match clone_res {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
println!(
|
||||
"redoxfs-clone: failed to clone {} to {}: {}",
|
||||
disk_path_old, disk_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let uuid = Uuid::from_bytes(fs.header.uuid());
|
||||
let size = fs.header.size();
|
||||
let free = fs.allocator().free() * redoxfs::BLOCK_SIZE;
|
||||
let used = size - free;
|
||||
println!("redoxfs-clone: created filesystem on {}", disk_path,);
|
||||
println!("\treserved: {} blocks", fs.block);
|
||||
println!("\tuuid: {}", uuid.hyphenated());
|
||||
println!("\tsize: {} MB", size / 1000 / 1000);
|
||||
println!("\tused: {} MB", used / 1000 / 1000);
|
||||
println!("\tfree: {} MB", free / 1000 / 1000);
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
extern crate arg_parser;
|
||||
extern crate redox_installer;
|
||||
extern crate serde;
|
||||
extern crate toml;
|
||||
|
||||
use std::path::Path;
|
||||
use std::{env, fs, process};
|
||||
|
||||
use arg_parser::ArgParser;
|
||||
|
||||
use redox_installer::{Config, PackageConfig};
|
||||
|
||||
const HELP_STR: &str = r#"
|
||||
redox_installer - Redox Installer.
|
||||
Refer to link below for filesystem config reference:
|
||||
https://doc.redox-os.org/book/configuration-settings.html
|
||||
|
||||
Using redox_installer as an installer:
|
||||
redox_installer <diskpath.img> [--config=file.toml] [--write-bootloader=file.img] [--live] [--no-mount] [--skip-partition]
|
||||
<diskpath.img> Disk file to write
|
||||
--config Path to filesystem config TOML
|
||||
--write-bootloader Path to write UEFI bootloader to in addition to the embedded ESP
|
||||
--skip-partition Skip writing GPT partition tables
|
||||
Use this only if you plan to use other partition tool
|
||||
--live Use bootloader configured for live disk
|
||||
--no-mount Use RedoxFS AR instead of FUSE to write files
|
||||
--cookbook Use local Redox OS build system rather than downloading packages
|
||||
|
||||
Using redox_installer as a configuration parser:
|
||||
redox_installer --config=file.toml [--list-packages|--filesystem-size|--output-config path]
|
||||
--list-packages List packages will be installed
|
||||
--filesystem-size Output filesystem size in MB
|
||||
--output-config Path to write the parsed config as another TOML
|
||||
"#;
|
||||
|
||||
fn main() {
|
||||
let mut parser = ArgParser::new(4)
|
||||
.add_opt("b", "cookbook")
|
||||
.add_opt("c", "config")
|
||||
.add_opt("o", "output-config")
|
||||
.add_opt("", "write-bootloader")
|
||||
.add_flag(&["skip-partition"])
|
||||
.add_flag(&["filesystem-size"])
|
||||
.add_flag(&["r", "repo-binary"]) // TODO: Remove
|
||||
.add_flag(&["l", "list-packages"])
|
||||
.add_flag(&["live"])
|
||||
.add_flag(&["no-mount"]);
|
||||
parser.parse(env::args());
|
||||
|
||||
let skip_partition = parser.found("skip-partition");
|
||||
|
||||
let mut config = if let Some(path) = parser.get_opt("config") {
|
||||
match Config::from_file(Path::new(&path)) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
eprintln!("installer: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
redox_installer::Config::default()
|
||||
};
|
||||
|
||||
// Get toml of merged config
|
||||
let merged_toml = toml::to_string_pretty(&config).unwrap();
|
||||
|
||||
// Just output merged config and exit
|
||||
if let Some(path) = parser.get_opt("output-config") {
|
||||
fs::write(path, merged_toml).unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add filesystem.toml to config
|
||||
config.files.push(redox_installer::FileConfig {
|
||||
path: "filesystem.toml".to_string(),
|
||||
data: merged_toml,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
if skip_partition {
|
||||
config.general.skip_partitions = Some(true);
|
||||
}
|
||||
|
||||
if parser.found("filesystem-size") {
|
||||
println!("{}", config.general.filesystem_size.unwrap_or(0));
|
||||
} else if parser.found("list-packages") {
|
||||
// List the packages that should be fetched or built by the cookbook
|
||||
for (packagename, package) in &config.packages {
|
||||
match package {
|
||||
PackageConfig::Build(rule) if rule == "ignore" => {
|
||||
// skip this package
|
||||
}
|
||||
_ => {
|
||||
println!("{}", packagename);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let cookbook = if let Some(path) = parser.get_opt("cookbook") {
|
||||
if !Path::new(&path).is_dir() {
|
||||
eprintln!("installer: {}: cookbook not found", path);
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if cookbook.is_some() {
|
||||
config.general.cookbook = cookbook;
|
||||
}
|
||||
if parser.found("live") {
|
||||
config.general.live_disk = Some(true);
|
||||
}
|
||||
if parser.found("no-mount") {
|
||||
config.general.no_mount = Some(true);
|
||||
}
|
||||
let write_bootloader = parser.get_opt("write-bootloader");
|
||||
if write_bootloader.is_some() {
|
||||
config.general.write_bootloader = write_bootloader;
|
||||
}
|
||||
|
||||
if let Some(path) = parser.args.first() {
|
||||
if let Err(err) = redox_installer::install(config, path) {
|
||||
eprintln!("installer: failed to install: {:?}", err);
|
||||
process::exit(1);
|
||||
}
|
||||
} else {
|
||||
eprint!("{}", HELP_STR);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,392 +0,0 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use pkgar::{ext::EntryExt, PackageHead};
|
||||
use pkgar_core::PackageSrc;
|
||||
use pkgar_keys::PublicKeyFile;
|
||||
use redox_installer::{try_fast_install, with_redoxfs_mount, with_whole_disk, Config, DiskOption};
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
io::{self, Read, Write},
|
||||
os::unix::fs::{symlink, MetadataExt, OpenOptionsExt},
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
};
|
||||
|
||||
// TODO: This is not the TUI a regular user would expect it does
|
||||
// 1. Linux: Implement disk listing, use "dd" to write into whole disk
|
||||
// 2. Allow partitioning to allow dual boot, possibly an integration with systemd-boot/grub
|
||||
// 3. Prompt everything (disk password, users, preconfigured packages, import from existing img)
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn disk_paths(_paths: &mut Vec<(PathBuf, u64)>) {}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn disk_paths(paths: &mut Vec<(PathBuf, u64)>) {
|
||||
let mut schemes = Vec::new();
|
||||
match fs::read_dir("/scheme") {
|
||||
Ok(entries) => {
|
||||
for entry_res in entries {
|
||||
if let Ok(entry) = entry_res {
|
||||
if let Ok(file_name) = entry.file_name().into_string() {
|
||||
if file_name.starts_with("disk") {
|
||||
schemes.push(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("redox_installer_tui: failed to list schemes: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
for scheme in schemes {
|
||||
if scheme.is_dir() {
|
||||
match fs::read_dir(&scheme) {
|
||||
Ok(entries) => {
|
||||
for entry_res in entries {
|
||||
if let Ok(entry) = entry_res {
|
||||
if let Ok(file_name) = entry.file_name().into_string() {
|
||||
if file_name.contains('p') {
|
||||
// Skip partitions
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
let size = metadata.len();
|
||||
if size > 0 {
|
||||
paths.push((entry.path(), size));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"redox_installer_tui: failed to list '{}': {}",
|
||||
scheme.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const KIB: u64 = 1024;
|
||||
const MIB: u64 = 1024 * KIB;
|
||||
const GIB: u64 = 1024 * MIB;
|
||||
const TIB: u64 = 1024 * GIB;
|
||||
|
||||
fn format_size(size: u64) -> String {
|
||||
if size >= 4 * TIB {
|
||||
format!("{:.1} TiB", size as f64 / TIB as f64)
|
||||
} else if size >= GIB {
|
||||
format!("{:.1} GiB", size as f64 / GIB as f64)
|
||||
} else if size >= MIB {
|
||||
format!("{:.1} MiB", size as f64 / MIB as f64)
|
||||
} else if size >= KIB {
|
||||
format!("{:.1} KiB", size as f64 / KIB as f64)
|
||||
} else {
|
||||
format!("{} B", size)
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_file(src: &Path, dest: &Path, buf: &mut [u8]) -> Result<()> {
|
||||
if let Some(parent) = dest.parent() {
|
||||
// Parent may be a symlink
|
||||
if !parent.is_symlink() {
|
||||
match fs::create_dir_all(&parent) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
bail!("failed to create directory {}: {}", parent.display(), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = match fs::symlink_metadata(&src) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
bail!("failed to read metadata of {}: {}", src.display(), err);
|
||||
}
|
||||
};
|
||||
|
||||
if metadata.file_type().is_symlink() {
|
||||
let real_src = match fs::read_link(&src) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
bail!("failed to read link {}: {}", src.display(), err);
|
||||
}
|
||||
};
|
||||
|
||||
match symlink(&real_src, &dest) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
bail!(
|
||||
"failed to copy link {} ({}) to {}: {}",
|
||||
src.display(),
|
||||
real_src.display(),
|
||||
dest.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut src_file = match fs::File::open(&src) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
bail!("failed to open file {}: {}", src.display(), err);
|
||||
}
|
||||
};
|
||||
|
||||
let mut dest_file = match fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.mode(metadata.mode())
|
||||
.open(&dest)
|
||||
{
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
bail!("failed to create file {}: {}", dest.display(), err);
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
let count = match src_file.read(buf) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
bail!("failed to read file {}: {}", src.display(), err);
|
||||
}
|
||||
};
|
||||
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
match dest_file.write_all(&buf[..count]) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
bail!("failed to write file {}: {}", dest.display(), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn package_files(
|
||||
root_path: &Path,
|
||||
config: &mut Config,
|
||||
files: &mut Vec<String>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
//TODO: Remove packages from config where all files are located (and have valid shasum?)
|
||||
config.packages.clear();
|
||||
|
||||
let pkey_path = "pkg/id_ed25519.pub.toml";
|
||||
let pkey = PublicKeyFile::open(&root_path.join(pkey_path))?.pkey;
|
||||
files.push(pkey_path.to_string());
|
||||
|
||||
for item_res in fs::read_dir(&root_path.join("pkg"))? {
|
||||
let item = item_res?;
|
||||
let pkg_path = item.path();
|
||||
if pkg_path.extension() == Some(OsStr::new("pkgar_head")) {
|
||||
let mut pkg = PackageHead::new(&pkg_path, &root_path, &pkey)?;
|
||||
for entry in pkg.read_entries()? {
|
||||
files.push(entry.check_path()?.to_str().unwrap().to_string());
|
||||
}
|
||||
files.push(
|
||||
pkg_path
|
||||
.strip_prefix(root_path)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn choose_disk() -> PathBuf {
|
||||
let mut paths = Vec::new();
|
||||
disk_paths(&mut paths);
|
||||
loop {
|
||||
for (i, (path, size)) in paths.iter().enumerate() {
|
||||
eprintln!(
|
||||
"\x1B[1m{}\x1B[0m: {}: {}",
|
||||
i + 1,
|
||||
path.display(),
|
||||
format_size(*size)
|
||||
);
|
||||
}
|
||||
|
||||
if paths.is_empty() {
|
||||
eprintln!("redox_installer_tui: no RedoxFS partition found");
|
||||
eprintln!("redox_installer_tui: this tool is used to overwrite unmounted RedoxFS disk in Redox OS");
|
||||
process::exit(1);
|
||||
} else {
|
||||
eprint!("Select a drive from 1 to {}: ", paths.len());
|
||||
|
||||
let mut line = String::new();
|
||||
match io::stdin().read_line(&mut line) {
|
||||
Ok(0) => {
|
||||
eprintln!("redox_installer_tui: failed to read line: end of input");
|
||||
process::exit(1);
|
||||
}
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
eprintln!("redox_installer_tui: failed to read line: {}", err);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
match line.trim().parse::<usize>() {
|
||||
Ok(i) => {
|
||||
if i >= 1 && i <= paths.len() {
|
||||
break paths[i - 1].0.clone();
|
||||
} else {
|
||||
eprintln!("{} not from 1 to {}", i, paths.len());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("invalid input: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root_path = Path::new("/");
|
||||
|
||||
let disk_path = choose_disk();
|
||||
|
||||
let Ok(password_opt) = redox_installer::prompt_password(
|
||||
"redox_installer_tui: redoxfs password (empty for none)",
|
||||
"redox_installer_tui: confirm password",
|
||||
) else {
|
||||
process::exit(1);
|
||||
};
|
||||
|
||||
let instant = std::time::Instant::now();
|
||||
|
||||
let bootloader_bios = {
|
||||
let path = root_path.join("usr/lib/boot/bootloader.bios");
|
||||
if path.exists() {
|
||||
match fs::read(&path) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"redox_installer_tui: {}: failed to read: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
let bootloader_efi = {
|
||||
let path = root_path.join("usr/lib/boot/bootloader.efi");
|
||||
if path.exists() {
|
||||
match fs::read(&path) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"redox_installer_tui: {}: failed to read: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
let disk_option = DiskOption {
|
||||
bootloader_bios: &bootloader_bios,
|
||||
bootloader_efi: &bootloader_efi,
|
||||
password_opt: password_opt.as_ref().map(|x| x.as_bytes()),
|
||||
efi_partition_size: None,
|
||||
skip_partitions: false, // TODO?
|
||||
};
|
||||
let res = with_whole_disk(&disk_path, &disk_option, |mut fs| {
|
||||
// Fast install method via filesystem clone
|
||||
let mut last_percent = 0;
|
||||
if try_fast_install(&mut fs, move |used, used_old| {
|
||||
let percent = (used * 100) / used_old;
|
||||
if percent != last_percent {
|
||||
eprint!(
|
||||
"\r{}%: {} MB/{} MB",
|
||||
percent,
|
||||
used / 1000 / 1000,
|
||||
used_old / 1000 / 1000
|
||||
);
|
||||
last_percent = percent;
|
||||
}
|
||||
})? {
|
||||
eprintln!("\rfinished installing using fast mode");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Slow install method via file copy
|
||||
with_redoxfs_mount(fs, None, |mount_path| {
|
||||
let mut config: Config = Config::from_file(&root_path.join("filesystem.toml"))?;
|
||||
|
||||
// Copy filesystem.toml, which is not packaged
|
||||
let mut files = vec!["filesystem.toml".to_string()];
|
||||
|
||||
// Copy files from locally installed packages
|
||||
package_files(&root_path, &mut config, &mut files)
|
||||
// TODO: implement Error trait
|
||||
.map_err(|err| anyhow!("failed to read package files: {err}"))?;
|
||||
|
||||
// Perform config install (after packages have been converted to files)
|
||||
eprintln!("configuring system");
|
||||
let cookbook: Option<&'static str> = None;
|
||||
redox_installer::install_dir(config, mount_path, cookbook)
|
||||
.map_err(|err| io::Error::other(err))?;
|
||||
|
||||
// Sort and remove duplicates
|
||||
files.sort();
|
||||
files.dedup();
|
||||
|
||||
// Install files
|
||||
let mut buf = vec![0; 4 * MIB as usize];
|
||||
for (i, name) in files.iter().enumerate() {
|
||||
eprintln!("copy {} [{}/{}]", name, i, files.len());
|
||||
|
||||
let src = root_path.join(name);
|
||||
let dest = mount_path.join(name);
|
||||
copy_file(&src, &dest, &mut buf)?;
|
||||
}
|
||||
|
||||
eprintln!("finished installing, unmounting filesystem");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(()) => {
|
||||
eprintln!(
|
||||
"redox_installer_tui: installed successfully in {:?}",
|
||||
instant.elapsed()
|
||||
);
|
||||
process::exit(0);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("redox_installer_tui: failed to install: {:?}", err);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
extern crate redoxfs;
|
||||
extern crate uuid;
|
||||
|
||||
use std::io::Read;
|
||||
use std::{env, fs, io, process, time};
|
||||
|
||||
use redoxfs::{DiskFile, FileSystem};
|
||||
use termion::input::TermRead;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn usage() -> ! {
|
||||
eprintln!("redoxfs-mkfs [--encrypt] DISK [BOOTLOADER]");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let mut encrypt = false;
|
||||
let mut disk_path_opt = None;
|
||||
let mut bootloader_path_opt = None;
|
||||
for arg in env::args().skip(1) {
|
||||
if arg == "--encrypt" {
|
||||
encrypt = true;
|
||||
} else if disk_path_opt.is_none() {
|
||||
disk_path_opt = Some(arg);
|
||||
} else if bootloader_path_opt.is_none() {
|
||||
bootloader_path_opt = Some(arg);
|
||||
} else {
|
||||
eprintln!("redoxfs-mkfs: too many arguments provided");
|
||||
usage();
|
||||
}
|
||||
}
|
||||
|
||||
let disk_path = if let Some(path) = disk_path_opt {
|
||||
path
|
||||
} else {
|
||||
eprintln!("redoxfs-mkfs: no disk image provided");
|
||||
usage();
|
||||
};
|
||||
|
||||
let disk = match DiskFile::open(&disk_path) {
|
||||
Ok(disk) => disk,
|
||||
Err(err) => {
|
||||
eprintln!("redoxfs-mkfs: failed to open image {}: {}", disk_path, err);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mut bootloader = vec![];
|
||||
if let Some(bootloader_path) = bootloader_path_opt {
|
||||
match fs::File::open(&bootloader_path) {
|
||||
Ok(mut file) => match file.read_to_end(&mut bootloader) {
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"redoxfs-mkfs: failed to read bootloader {}: {}",
|
||||
bootloader_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"redoxfs-mkfs: failed to open bootloader {}: {}",
|
||||
bootloader_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let password_opt = if encrypt {
|
||||
eprint!("redoxfs-mkfs: password: ");
|
||||
|
||||
let password = io::stdin()
|
||||
.read_passwd(&mut io::stderr())
|
||||
.unwrap()
|
||||
.unwrap_or_default();
|
||||
|
||||
eprintln!();
|
||||
|
||||
if password.is_empty() {
|
||||
eprintln!("redoxfs-mkfs: empty password, giving up");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
Some(password)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let ctime = time::SystemTime::now()
|
||||
.duration_since(time::UNIX_EPOCH)
|
||||
.unwrap();
|
||||
match FileSystem::create_reserved(
|
||||
disk,
|
||||
password_opt.as_ref().map(|x| x.as_bytes()),
|
||||
&bootloader,
|
||||
ctime.as_secs(),
|
||||
ctime.subsec_nanos(),
|
||||
) {
|
||||
Ok(filesystem) => {
|
||||
let uuid = Uuid::from_bytes(filesystem.header.uuid());
|
||||
eprintln!(
|
||||
"redoxfs-mkfs: created filesystem on {}, reserved {} blocks, size {} MB, uuid {}",
|
||||
disk_path,
|
||||
filesystem.block,
|
||||
filesystem.header.size() / 1000 / 1000,
|
||||
uuid.hyphenated()
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"redoxfs-mkfs: failed to create filesystem on {}: {}",
|
||||
disk_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
extern crate libc;
|
||||
extern crate redoxfs;
|
||||
#[cfg(target_os = "redox")]
|
||||
extern crate syscall;
|
||||
extern crate uuid;
|
||||
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::os::unix::io::{FromRawFd, RawFd};
|
||||
use std::process;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::{mem::MaybeUninit, ptr::addr_of_mut, sync::atomic::Ordering};
|
||||
|
||||
use redoxfs::{mount, DiskCache, DiskFile, FileSystem};
|
||||
use termion::input::TermRead;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
extern "C" fn unmount_handler(_s: usize) {
|
||||
redoxfs::IS_UMT.store(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
//set up a signal handler on redox, this implements unmounting. I have no idea what sa_flags is
|
||||
//for, so I put 2. I don't think 0,0 is a valid sa_mask. I don't know what i'm doing here. When u
|
||||
//send it a sigkill, it shuts off the filesystem
|
||||
fn setsig() {
|
||||
// TODO: High-level wrapper like the nix crate?
|
||||
unsafe {
|
||||
let mut action = MaybeUninit::<libc::sigaction>::uninit();
|
||||
|
||||
assert_eq!(
|
||||
libc::sigemptyset(addr_of_mut!((*action.as_mut_ptr()).sa_mask)),
|
||||
0
|
||||
);
|
||||
addr_of_mut!((*action.as_mut_ptr()).sa_flags).write(0);
|
||||
addr_of_mut!((*action.as_mut_ptr()).sa_sigaction).write(unmount_handler as usize);
|
||||
|
||||
assert_eq!(
|
||||
libc::sigaction(libc::SIGTERM, action.as_ptr(), core::ptr::null_mut()),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
// on linux, this is implemented properly, so no need for this unscrupulous nonsense!
|
||||
fn setsig() {}
|
||||
|
||||
fn fork() -> isize {
|
||||
unsafe { libc::fork() as isize }
|
||||
}
|
||||
|
||||
fn pipe(pipes: &mut [i32; 2]) -> isize {
|
||||
unsafe { libc::pipe(pipes.as_mut_ptr()) as isize }
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn capability_mode() {}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn bootloader_password() -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn capability_mode() {
|
||||
libredox::call::setrens(0, 0).expect("redoxfs: failed to enter null namespace");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn bootloader_password() -> Option<Vec<u8>> {
|
||||
use libredox::call::MmapArgs;
|
||||
|
||||
let addr_env = env::var_os("REDOXFS_PASSWORD_ADDR")?;
|
||||
let size_env = env::var_os("REDOXFS_PASSWORD_SIZE")?;
|
||||
|
||||
let addr = usize::from_str_radix(
|
||||
addr_env.to_str().expect("REDOXFS_PASSWORD_ADDR not valid"),
|
||||
16,
|
||||
)
|
||||
.expect("failed to parse REDOXFS_PASSWORD_ADDR");
|
||||
|
||||
let size = usize::from_str_radix(
|
||||
size_env.to_str().expect("REDOXFS_PASSWORD_SIZE not valid"),
|
||||
16,
|
||||
)
|
||||
.expect("failed to parse REDOXFS_PASSWORD_SIZE");
|
||||
|
||||
let mut password = Vec::with_capacity(size);
|
||||
unsafe {
|
||||
let aligned_size = size.next_multiple_of(syscall::PAGE_SIZE);
|
||||
|
||||
let fd = libredox::Fd::open("memory:physical", libredox::flag::O_CLOEXEC, 0)
|
||||
.expect("failed to open physical memory file");
|
||||
|
||||
let password_map = libredox::call::mmap(MmapArgs {
|
||||
addr: core::ptr::null_mut(),
|
||||
length: aligned_size,
|
||||
prot: libredox::flag::PROT_READ,
|
||||
flags: libredox::flag::MAP_SHARED,
|
||||
fd: fd.raw(),
|
||||
offset: addr as u64,
|
||||
})
|
||||
.expect("failed to map REDOXFS_PASSWORD")
|
||||
.cast::<u8>();
|
||||
|
||||
for i in 0..size {
|
||||
password.push(password_map.add(i).read());
|
||||
}
|
||||
|
||||
let _ = libredox::call::munmap(password_map.cast(), aligned_size);
|
||||
}
|
||||
Some(password)
|
||||
}
|
||||
|
||||
fn print_err_exit(err: impl AsRef<str>) -> ! {
|
||||
eprintln!("redoxfs: {}", err.as_ref());
|
||||
usage();
|
||||
process::exit(1)
|
||||
}
|
||||
|
||||
fn print_usage_exit() -> ! {
|
||||
usage();
|
||||
process::exit(1)
|
||||
}
|
||||
|
||||
fn usage() {
|
||||
eprintln!("redoxfs [--no-daemon|-d] [--uuid] [disk or uuid] [mountpoint] [block in hex]");
|
||||
}
|
||||
|
||||
enum DiskId {
|
||||
Path(String),
|
||||
Uuid(Uuid),
|
||||
}
|
||||
|
||||
fn filesystem_by_path(
|
||||
path: &str,
|
||||
block_opt: Option<u64>,
|
||||
log_errors: bool,
|
||||
) -> Option<(String, FileSystem<DiskCache<DiskFile>>)> {
|
||||
log::debug!("opening {}", path);
|
||||
let attempts = 10;
|
||||
for attempt in 0..=attempts {
|
||||
let password_opt = if attempt > 0 {
|
||||
eprint!("redoxfs: password: ");
|
||||
|
||||
let password = io::stdin()
|
||||
.read_passwd(&mut io::stderr())
|
||||
.unwrap()
|
||||
.unwrap_or_default();
|
||||
|
||||
eprintln!();
|
||||
|
||||
if password.is_empty() {
|
||||
eprintln!("redoxfs: empty password, giving up");
|
||||
|
||||
// Password is empty, exit loop
|
||||
break;
|
||||
}
|
||||
|
||||
Some(password.into_bytes())
|
||||
} else {
|
||||
bootloader_password()
|
||||
};
|
||||
|
||||
match DiskFile::open(path).map(DiskCache::new) {
|
||||
Ok(disk) => {
|
||||
match redoxfs::FileSystem::open(disk, password_opt.as_deref(), block_opt, true) {
|
||||
Ok(filesystem) => {
|
||||
log::debug!(
|
||||
"opened filesystem on {} with uuid {}",
|
||||
path,
|
||||
Uuid::from_bytes(filesystem.header.uuid()).hyphenated()
|
||||
);
|
||||
|
||||
return Some((path.to_string(), filesystem));
|
||||
}
|
||||
Err(err) => match err.errno {
|
||||
syscall::ENOKEY => {
|
||||
if password_opt.is_some() {
|
||||
eprintln!("redoxfs: incorrect password ({}/{})", attempt, attempts);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if log_errors {
|
||||
log::error!("failed to open filesystem {}: {}", path, err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if log_errors {
|
||||
log::error!("failed to open image {}: {}", path, err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn filesystem_by_uuid(
|
||||
_uuid: &Uuid,
|
||||
_block_opt: Option<u64>,
|
||||
) -> Option<(String, FileSystem<DiskCache<DiskFile>>)> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn filesystem_by_uuid(
|
||||
uuid: &Uuid,
|
||||
block_opt: Option<u64>,
|
||||
) -> Option<(String, FileSystem<DiskCache<DiskFile>>)> {
|
||||
use std::fs;
|
||||
|
||||
use redox_path::RedoxPath;
|
||||
|
||||
match fs::read_dir("/scheme") {
|
||||
Ok(entries) => {
|
||||
for entry_res in entries {
|
||||
if let Ok(entry) = entry_res {
|
||||
if let Some(disk) = entry.path().to_str() {
|
||||
if RedoxPath::from_absolute(disk)
|
||||
.unwrap_or(RedoxPath::from_absolute("/")?)
|
||||
.is_scheme_category("disk")
|
||||
{
|
||||
log::debug!("found scheme {}", disk);
|
||||
match fs::read_dir(disk) {
|
||||
Ok(entries) => {
|
||||
for entry_res in entries {
|
||||
if let Ok(entry) = entry_res {
|
||||
if let Ok(path) =
|
||||
entry.path().into_os_string().into_string()
|
||||
{
|
||||
log::debug!("found path {}", path);
|
||||
if let Some((path, filesystem)) =
|
||||
filesystem_by_path(&path, block_opt, false)
|
||||
{
|
||||
if &filesystem.header.uuid() == uuid.as_bytes()
|
||||
{
|
||||
log::debug!(
|
||||
"filesystem on {} matches uuid {}",
|
||||
path,
|
||||
uuid.hyphenated()
|
||||
);
|
||||
return Some((path, filesystem));
|
||||
} else {
|
||||
log::debug!(
|
||||
"filesystem on {} does not match uuid {}",
|
||||
path,
|
||||
uuid.hyphenated()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::debug!("failed to list '{}': {}", disk, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to list schemes: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn daemon(
|
||||
disk_id: &DiskId,
|
||||
mountpoint: &str,
|
||||
block_opt: Option<u64>,
|
||||
mut write: Option<File>,
|
||||
) -> ! {
|
||||
setsig();
|
||||
|
||||
let filesystem_opt = match *disk_id {
|
||||
DiskId::Path(ref path) => filesystem_by_path(path, block_opt, true),
|
||||
DiskId::Uuid(ref uuid) => filesystem_by_uuid(uuid, block_opt),
|
||||
};
|
||||
|
||||
if let Some((path, filesystem)) = filesystem_opt {
|
||||
match mount(filesystem, mountpoint, |mounted_path| {
|
||||
capability_mode();
|
||||
|
||||
log::info!(
|
||||
"mounted filesystem on {} to {}",
|
||||
path,
|
||||
mounted_path.display()
|
||||
);
|
||||
|
||||
if let Some(ref mut write) = write {
|
||||
let _ = write.write(&[0]);
|
||||
}
|
||||
}) {
|
||||
Ok(()) => {
|
||||
process::exit(0);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to mount {} to {}: {}", path, mountpoint, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match *disk_id {
|
||||
DiskId::Path(ref path) => {
|
||||
log::error!("not able to mount path {}", path);
|
||||
}
|
||||
DiskId::Uuid(ref uuid) => {
|
||||
log::error!("not able to mount uuid {}", uuid.hyphenated());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref mut write) = write {
|
||||
let _ = write.write(&[1]);
|
||||
}
|
||||
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let mut args = env::args().skip(1);
|
||||
|
||||
let mut daemonise = true;
|
||||
let mut disk_id: Option<DiskId> = None;
|
||||
let mut mountpoint: Option<String> = None;
|
||||
let mut block_opt: Option<u64> = None;
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--no-daemon" | "-d" => daemonise = false,
|
||||
|
||||
"--uuid" if disk_id.is_none() => {
|
||||
disk_id = Some(DiskId::Uuid(
|
||||
match args.next().as_deref().map(Uuid::parse_str) {
|
||||
Some(Ok(uuid)) => uuid,
|
||||
Some(Err(err)) => {
|
||||
print_err_exit(format!("invalid uuid '{}': {}", arg, err))
|
||||
}
|
||||
None => print_err_exit("no uuid provided"),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
disk if disk_id.is_none() => disk_id = Some(DiskId::Path(disk.to_owned())),
|
||||
|
||||
mnt if disk_id.is_some() && mountpoint.is_none() => mountpoint = Some(mnt.to_owned()),
|
||||
|
||||
opts if mountpoint.is_some() => match u64::from_str_radix(opts, 16) {
|
||||
Ok(block) => block_opt = Some(block),
|
||||
Err(err) => print_err_exit(format!("invalid block '{}': {}", opts, err)),
|
||||
},
|
||||
|
||||
_ => print_usage_exit(),
|
||||
}
|
||||
}
|
||||
|
||||
let Some(disk_id) = disk_id else {
|
||||
print_err_exit("no disk provided");
|
||||
};
|
||||
|
||||
let Some(mountpoint) = mountpoint else {
|
||||
print_err_exit("no mountpoint provided");
|
||||
};
|
||||
|
||||
if daemonise {
|
||||
let mut pipes = [0; 2];
|
||||
if pipe(&mut pipes) == 0 {
|
||||
let mut read = unsafe { File::from_raw_fd(pipes[0] as RawFd) };
|
||||
let write = unsafe { File::from_raw_fd(pipes[1] as RawFd) };
|
||||
|
||||
let pid = fork();
|
||||
if pid == 0 {
|
||||
drop(read);
|
||||
|
||||
daemon(&disk_id, &mountpoint, block_opt, Some(write));
|
||||
} else if pid > 0 {
|
||||
drop(write);
|
||||
|
||||
let mut res = [0];
|
||||
read.read_exact(&mut res).unwrap();
|
||||
|
||||
process::exit(res[0] as i32);
|
||||
} else {
|
||||
panic!("redoxfs: failed to fork");
|
||||
}
|
||||
} else {
|
||||
panic!("redoxfs: failed to create pipe");
|
||||
}
|
||||
} else {
|
||||
log::info!("running in foreground");
|
||||
daemon(&disk_id, &mountpoint, block_opt, None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
use std::{env, process};
|
||||
|
||||
use humansize::{format_size, BINARY, DECIMAL};
|
||||
use redoxfs::{BlockAddr, BlockMeta, Disk, DiskFile, FileSystem};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn resize<D: Disk>(fs: &mut FileSystem<D>, size_arg: String) -> Result<(), String> {
|
||||
let disk_size = fs
|
||||
.disk
|
||||
.size()
|
||||
.map_err(|err| format!("failed to read disk size: {}", err))?;
|
||||
|
||||
// Find contiguous free region
|
||||
//TODO: better error management
|
||||
let mut last_free = None;
|
||||
let mut last_end = 0;
|
||||
fs.tx(|tx| {
|
||||
let mut alloc_ptr = tx.header.alloc;
|
||||
while !alloc_ptr.is_null() {
|
||||
let alloc = tx.read_block(alloc_ptr)?;
|
||||
alloc_ptr = alloc.data().prev;
|
||||
for entry in alloc.data().entries.iter() {
|
||||
let count = entry.count();
|
||||
if count <= 0 {
|
||||
continue;
|
||||
}
|
||||
let end = entry.index() + count as u64;
|
||||
if end > last_end {
|
||||
last_free = Some(*entry);
|
||||
last_end = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|err| format!("failed to read alloc log: {}", err))?;
|
||||
|
||||
let old_size = fs.header.size();
|
||||
let min_size = if let Some(entry) = last_free {
|
||||
entry.index() * redoxfs::BLOCK_SIZE
|
||||
} else {
|
||||
old_size
|
||||
};
|
||||
let max_size = disk_size - (fs.block * redoxfs::BLOCK_SIZE);
|
||||
|
||||
let new_size = match size_arg.to_lowercase().as_str() {
|
||||
"min" | "minimum" => min_size,
|
||||
"" | "max" | "maximum" => max_size,
|
||||
_ => match parse_size::parse_size(&size_arg) {
|
||||
Ok(new_size) => {
|
||||
if new_size < min_size {
|
||||
return Err(format!(
|
||||
"requested size {} is smaller than {} by {}",
|
||||
new_size,
|
||||
min_size,
|
||||
min_size - new_size
|
||||
));
|
||||
}
|
||||
|
||||
if new_size > max_size {
|
||||
return Err(format!(
|
||||
"requested size {} is larger than {} by {}",
|
||||
new_size,
|
||||
max_size,
|
||||
new_size - max_size
|
||||
));
|
||||
}
|
||||
|
||||
new_size
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"failed to parse size argument {:?}: {}",
|
||||
size_arg, err
|
||||
));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
println!(
|
||||
"minimum size: {} ({})",
|
||||
format_size(min_size, DECIMAL),
|
||||
format_size(min_size, BINARY)
|
||||
);
|
||||
println!(
|
||||
"maximum size: {} ({})",
|
||||
format_size(max_size, DECIMAL),
|
||||
format_size(max_size, BINARY)
|
||||
);
|
||||
println!(
|
||||
"new size: {} ({})",
|
||||
format_size(new_size, DECIMAL),
|
||||
format_size(new_size, BINARY)
|
||||
);
|
||||
|
||||
let old_blocks = old_size / redoxfs::BLOCK_SIZE;
|
||||
let new_blocks = new_size / redoxfs::BLOCK_SIZE;
|
||||
let (start, end, shrink) = if new_size == old_size {
|
||||
println!("already requested size");
|
||||
return Ok(());
|
||||
} else if new_size < old_size {
|
||||
println!("shrinking by {}", old_size - new_size);
|
||||
(new_blocks, old_blocks, true)
|
||||
} else {
|
||||
println!("growing by {}", new_size - old_size);
|
||||
(old_blocks, new_blocks, false)
|
||||
};
|
||||
|
||||
// Allocate or deallocate blocks as needed
|
||||
unsafe {
|
||||
let allocator = fs.allocator_mut();
|
||||
for index in start..end {
|
||||
if shrink {
|
||||
//TODO: replace assert with error?
|
||||
let addr = BlockAddr::new(index as u64, BlockMeta::default());
|
||||
assert_eq!(allocator.allocate_exact(addr), Some(addr));
|
||||
} else {
|
||||
let addr = BlockAddr::new(index as u64, BlockMeta::default());
|
||||
allocator.deallocate(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.tx(|tx| {
|
||||
// Update header
|
||||
tx.header.size = new_size.into();
|
||||
tx.header_changed = true;
|
||||
|
||||
// Sync with squash
|
||||
tx.sync(true)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|err| format!("transaction failed: {}", err))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let mut args = env::args().skip(1);
|
||||
|
||||
let disk_path = if let Some(path) = args.next() {
|
||||
path
|
||||
} else {
|
||||
eprintln!("redoxfs-resize: no new disk image provided");
|
||||
eprintln!("redoxfs-resize NEW-DISK [SIZE]");
|
||||
process::exit(1);
|
||||
};
|
||||
|
||||
let size_arg = args.next().unwrap_or_default();
|
||||
|
||||
let disk = match DiskFile::open(&disk_path) {
|
||||
Ok(disk) => disk,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"redoxfs-resize: failed to open disk image {}: {}",
|
||||
disk_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mut fs = match FileSystem::open(disk, None, None, true) {
|
||||
Ok(fs) => fs,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"redoxfs-resize: failed to open filesystem on {}: {}",
|
||||
disk_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
match resize(&mut fs, size_arg) {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"redoxfs-resize: failed to resize filesystem on {}: {}",
|
||||
disk_path, err
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let uuid = Uuid::from_bytes(fs.header.uuid());
|
||||
let size = fs.header.size();
|
||||
let free = fs.allocator().free() * redoxfs::BLOCK_SIZE;
|
||||
let used = size - free;
|
||||
println!("redoxfs-resize: resized filesystem on {}", disk_path);
|
||||
println!("\tuuid: {}", uuid.hyphenated());
|
||||
println!(
|
||||
"\tsize: {} ({})",
|
||||
format_size(size, DECIMAL),
|
||||
format_size(size, BINARY)
|
||||
);
|
||||
println!(
|
||||
"\tused: {} ({})",
|
||||
format_size(used, DECIMAL),
|
||||
format_size(used, BINARY)
|
||||
);
|
||||
println!(
|
||||
"\tfree: {} ({})",
|
||||
format_size(free, DECIMAL),
|
||||
format_size(free, BINARY)
|
||||
);
|
||||
}
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
use core::{fmt, marker::PhantomData, mem, ops, slice};
|
||||
use endian_num::Le;
|
||||
|
||||
use crate::BLOCK_SIZE;
|
||||
|
||||
const BLOCK_LIST_ENTRIES: usize = BLOCK_SIZE as usize / mem::size_of::<BlockPtr<BlockRaw>>();
|
||||
|
||||
/// An address of a data block.
|
||||
///
|
||||
/// This encodes a block's position _and_ [`BlockLevel`]:
|
||||
/// the first four bits of this `u64` encode the block's level,
|
||||
/// the next four bits indicates decompression level,
|
||||
/// the rest encode its index.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct BlockAddr(u64);
|
||||
|
||||
impl BlockAddr {
|
||||
const INDEX_SHIFT: u64 = 8;
|
||||
const DECOMP_LEVEL_MASK: u64 = 0xF0;
|
||||
const DECOMP_LEVEL_SHIFT: u64 = 4;
|
||||
const LEVEL_MASK: u64 = 0xF;
|
||||
|
||||
// Unsafe because this can create invalid blocks
|
||||
pub unsafe fn new(index: u64, meta: BlockMeta) -> Self {
|
||||
// Level must fit within LEVEL_MASK
|
||||
if meta.level.0 > Self::LEVEL_MASK as usize {
|
||||
panic!("block level too large");
|
||||
}
|
||||
|
||||
// Decomp level must fit within DECOMP_LEVEL_MASK
|
||||
let decomp_level = meta.decomp_level.unwrap_or_default();
|
||||
if (decomp_level.0 << Self::DECOMP_LEVEL_SHIFT) > Self::DECOMP_LEVEL_MASK as usize {
|
||||
panic!("decompressed block level too large");
|
||||
}
|
||||
|
||||
// Index must not use the metadata bits
|
||||
let inner = index
|
||||
.checked_shl(Self::INDEX_SHIFT as u32)
|
||||
.expect("block index too large")
|
||||
| ((decomp_level.0 as u64) << Self::DECOMP_LEVEL_SHIFT)
|
||||
| (meta.level.0 as u64);
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
pub fn null(meta: BlockMeta) -> Self {
|
||||
unsafe { Self::new(0, meta) }
|
||||
}
|
||||
|
||||
pub fn index(&self) -> u64 {
|
||||
// The first four bits store the level
|
||||
self.0 >> Self::INDEX_SHIFT
|
||||
}
|
||||
|
||||
pub fn level(&self) -> BlockLevel {
|
||||
// The first four bits store the level
|
||||
BlockLevel((self.0 & Self::LEVEL_MASK) as usize)
|
||||
}
|
||||
|
||||
pub fn decomp_level(&self) -> Option<BlockLevel> {
|
||||
let value = (self.0 & Self::DECOMP_LEVEL_MASK) >> Self::DECOMP_LEVEL_SHIFT;
|
||||
if value != 0 {
|
||||
Some(BlockLevel(value as usize))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn meta(&self) -> BlockMeta {
|
||||
BlockMeta {
|
||||
level: self.level(),
|
||||
decomp_level: self.decomp_level(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_null(&self) -> bool {
|
||||
self.index() == 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||
pub struct BlockMeta {
|
||||
pub(crate) level: BlockLevel,
|
||||
pub(crate) decomp_level: Option<BlockLevel>,
|
||||
}
|
||||
|
||||
impl BlockMeta {
|
||||
pub fn new(level: BlockLevel) -> Self {
|
||||
Self {
|
||||
level,
|
||||
decomp_level: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_compressed(level: BlockLevel, decomp_level: BlockLevel) -> Self {
|
||||
Self {
|
||||
level,
|
||||
decomp_level: Some(decomp_level),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The size of a block.
|
||||
///
|
||||
/// Level 0 blocks are blocks of [`BLOCK_SIZE`] bytes.
|
||||
/// A level 1 block consists of two consecutive level 0 blocks.
|
||||
/// A level n block consists of two consecutive level n-1 blocks.
|
||||
///
|
||||
/// See [`crate::Allocator`] docs for more details.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct BlockLevel(pub(crate) usize);
|
||||
|
||||
impl BlockLevel {
|
||||
/// Returns the smallest block level that can contain
|
||||
/// the given number of bytes.
|
||||
pub(crate) fn for_bytes(bytes: u64) -> Self {
|
||||
if bytes == 0 {
|
||||
return BlockLevel(0);
|
||||
}
|
||||
let level = bytes
|
||||
.div_ceil(BLOCK_SIZE)
|
||||
.next_power_of_two()
|
||||
.trailing_zeros() as usize;
|
||||
BlockLevel(level)
|
||||
}
|
||||
|
||||
/// The number of [`BLOCK_SIZE`] blocks (i.e, level 0 blocks)
|
||||
/// in a block of this level
|
||||
pub fn blocks<T: From<u32>>(self) -> T {
|
||||
T::from(1u32 << self.0)
|
||||
}
|
||||
|
||||
/// The number of bytes in a block of this level
|
||||
pub fn bytes(self) -> u64 {
|
||||
BLOCK_SIZE << self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe trait BlockTrait {
|
||||
/// Create an empty block of this type.
|
||||
fn empty(level: BlockLevel) -> Option<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
/// A [`BlockAddr`] and the data it points to.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct BlockData<T> {
|
||||
addr: BlockAddr,
|
||||
data: T,
|
||||
}
|
||||
|
||||
impl<T> BlockData<T> {
|
||||
pub fn new(addr: BlockAddr, data: T) -> Self {
|
||||
Self { addr, data }
|
||||
}
|
||||
|
||||
pub fn addr(&self) -> BlockAddr {
|
||||
self.addr
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &T {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn data_mut(&mut self) -> &mut T {
|
||||
&mut self.data
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn into_parts(self) -> (BlockAddr, T) {
|
||||
(self.addr, self.data)
|
||||
}
|
||||
|
||||
/// Set the address of this [`BlockData`] to `addr`, returning this
|
||||
/// block's old address. This method does not update block data.
|
||||
///
|
||||
/// `addr` must point to a block with the same level as this block.
|
||||
#[must_use = "don't forget to de-allocate old block address"]
|
||||
pub fn swap_addr(&mut self, addr: BlockAddr) -> BlockAddr {
|
||||
// Address levels must match
|
||||
assert_eq!(self.addr.level(), addr.level());
|
||||
let old = self.addr;
|
||||
self.addr = addr;
|
||||
old
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BlockTrait> BlockData<T> {
|
||||
pub fn empty(addr: BlockAddr) -> Option<Self> {
|
||||
let empty = T::empty(addr.level())?;
|
||||
Some(Self::new(addr, empty))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ops::Deref<Target = [u8]>> BlockData<T> {
|
||||
pub fn create_ptr(&self) -> BlockPtr<T> {
|
||||
BlockPtr {
|
||||
addr: self.addr.0.into(),
|
||||
hash: seahash::hash(self.data.deref()).into(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
pub struct BlockList<T> {
|
||||
pub ptrs: [BlockPtr<T>; BLOCK_LIST_ENTRIES],
|
||||
}
|
||||
|
||||
unsafe impl<T> BlockTrait for BlockList<T> {
|
||||
fn empty(level: BlockLevel) -> Option<Self> {
|
||||
if level.0 == 0 {
|
||||
Some(Self {
|
||||
ptrs: [BlockPtr::default(); BLOCK_LIST_ENTRIES],
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BlockList<T> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.ptrs.iter().all(|ptr| ptr.is_null())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ops::Deref for BlockList<T> {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts(
|
||||
self as *const BlockList<T> as *const u8,
|
||||
mem::size_of::<BlockList<T>>(),
|
||||
) as &[u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ops::DerefMut for BlockList<T> {
|
||||
fn deref_mut(&mut self) -> &mut [u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts_mut(
|
||||
self as *mut BlockList<T> as *mut u8,
|
||||
mem::size_of::<BlockList<T>>(),
|
||||
) as &mut [u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An address of a data block, along with a checksum of its data.
|
||||
///
|
||||
/// This encodes a block's position _and_ [`BlockLevel`].
|
||||
/// the first four bits of `addr` encode the block's level,
|
||||
/// the rest encode its index.
|
||||
///
|
||||
/// Also see [`BlockAddr`].
|
||||
#[repr(C, packed)]
|
||||
pub struct BlockPtr<T> {
|
||||
addr: Le<u64>,
|
||||
hash: Le<u64>,
|
||||
phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> BlockPtr<T> {
|
||||
pub fn null(meta: BlockMeta) -> Self {
|
||||
Self {
|
||||
addr: BlockAddr::null(meta).0.into(),
|
||||
hash: 0.into(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn addr(&self) -> BlockAddr {
|
||||
BlockAddr(self.addr.to_ne())
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> u64 {
|
||||
self.hash.to_ne()
|
||||
}
|
||||
|
||||
pub fn is_null(&self) -> bool {
|
||||
self.addr().is_null()
|
||||
}
|
||||
|
||||
pub fn marker(level: u8) -> Self {
|
||||
assert!(level <= 0xF);
|
||||
Self {
|
||||
addr: (0xFFFF_FFFF_FFFF_FFF0 | (level as u64)).into(),
|
||||
hash: u64::MAX.into(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_marker(&self) -> bool {
|
||||
(self.addr.to_ne() | 0xF) == u64::MAX && self.hash.to_ne() == u64::MAX
|
||||
}
|
||||
|
||||
/// Cast BlockPtr to another type
|
||||
///
|
||||
/// # Safety
|
||||
/// Unsafe because it can be used to transmute types
|
||||
pub unsafe fn cast<U>(self) -> BlockPtr<U> {
|
||||
BlockPtr {
|
||||
addr: self.addr,
|
||||
hash: self.hash,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use = "the returned pointer should usually be deallocated"]
|
||||
pub fn clear(&mut self) -> BlockPtr<T> {
|
||||
let mut ptr = Self::default();
|
||||
mem::swap(self, &mut ptr);
|
||||
ptr
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for BlockPtr<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for BlockPtr<T> {}
|
||||
|
||||
impl<T> Default for BlockPtr<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
addr: 0.into(),
|
||||
hash: 0.into(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for BlockPtr<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let addr = self.addr();
|
||||
let hash = self.hash();
|
||||
f.debug_struct("BlockPtr")
|
||||
.field("addr", &addr)
|
||||
.field("hash", &hash)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Clone)]
|
||||
pub struct BlockRaw([u8; BLOCK_SIZE as usize]);
|
||||
|
||||
unsafe impl BlockTrait for BlockRaw {
|
||||
fn empty(level: BlockLevel) -> Option<Self> {
|
||||
if level.0 == 0 {
|
||||
Some(Self([0; BLOCK_SIZE as usize]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for BlockRaw {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for BlockRaw {
|
||||
fn deref_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_list_size_test() {
|
||||
assert_eq!(mem::size_of::<BlockList<BlockRaw>>(), BLOCK_SIZE as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_raw_size_test() {
|
||||
assert_eq!(mem::size_of::<BlockRaw>(), BLOCK_SIZE as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_ptr_marker_test() {
|
||||
let ptr = BlockPtr::<BlockRaw>::marker(0);
|
||||
assert_eq!(ptr.addr().level().0, 0);
|
||||
assert!(ptr.is_marker());
|
||||
|
||||
let ptr = BlockPtr::<BlockRaw>::marker(2);
|
||||
assert_eq!(ptr.addr().level().0, 2);
|
||||
assert!(ptr.is_marker());
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
use crate::{Disk, FileSystem, Node, Transaction, TreePtr, BLOCK_SIZE};
|
||||
|
||||
fn tx_progress<D: Disk, F: FnMut(u64)>(tx: &mut Transaction<D>, progress: &mut F) {
|
||||
let size = tx.header.size();
|
||||
let free = tx.allocator.free() * BLOCK_SIZE;
|
||||
progress(size - free);
|
||||
}
|
||||
|
||||
//TODO: handle hard links
|
||||
fn clone_at<D: Disk, E: Disk, F: FnMut(u64)>(
|
||||
tx_old: &mut Transaction<D>,
|
||||
parent_ptr_old: TreePtr<Node>,
|
||||
tx: &mut Transaction<E>,
|
||||
parent_ptr: TreePtr<Node>,
|
||||
buf: &mut [u8],
|
||||
progress: &mut F,
|
||||
) -> syscall::Result<()> {
|
||||
let mut entries = Vec::new();
|
||||
tx_old.child_nodes(parent_ptr_old, &mut entries)?;
|
||||
for entry in entries {
|
||||
//TODO: return error instead?
|
||||
let Some(name) = entry.name() else {
|
||||
continue;
|
||||
};
|
||||
let node_ptr_old = entry.node_ptr();
|
||||
let node_old = tx_old.read_tree(node_ptr_old)?;
|
||||
|
||||
//TODO: this slows down the clone, but Redox has issues without this (Linux is fine)
|
||||
if tx.write_cache.len() > 64 {
|
||||
tx.sync(false)?;
|
||||
}
|
||||
|
||||
let node_ptr = {
|
||||
let mode = node_old.data().mode();
|
||||
let (ctime, ctime_nsec) = node_old.data().ctime();
|
||||
let (mtime, mtime_nsec) = node_old.data().mtime();
|
||||
let mut node = tx.create_node(parent_ptr, &name, mode, ctime, ctime_nsec)?;
|
||||
node.data_mut().set_uid(node_old.data().uid());
|
||||
node.data_mut().set_gid(node_old.data().gid());
|
||||
node.data_mut().set_mtime(mtime, mtime_nsec);
|
||||
|
||||
if !node_old.data().is_dir() {
|
||||
let mut offset = 0;
|
||||
loop {
|
||||
let count = tx_old.read_node_inner(&node_old, offset, buf)?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
tx.write_node_inner(&mut node, &mut offset, &buf[..count])?;
|
||||
}
|
||||
}
|
||||
|
||||
let node_ptr = node.ptr();
|
||||
tx.sync_tree(node)?;
|
||||
node_ptr
|
||||
};
|
||||
|
||||
tx_progress(tx, progress);
|
||||
|
||||
if node_old.data().is_dir() {
|
||||
clone_at(tx_old, node_ptr_old, tx, node_ptr, buf, progress)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clone<D: Disk, E: Disk, F: FnMut(u64)>(
|
||||
fs_old: &mut FileSystem<D>,
|
||||
fs: &mut FileSystem<E>,
|
||||
mut progress: F,
|
||||
) -> syscall::Result<()> {
|
||||
fs_old.tx(|tx_old| {
|
||||
let mut tx = Transaction::new(fs);
|
||||
|
||||
// Clone at root node
|
||||
let mut buf = vec![0; 4 * 1024 * 1024];
|
||||
clone_at(
|
||||
tx_old,
|
||||
TreePtr::root(),
|
||||
&mut tx,
|
||||
TreePtr::root(),
|
||||
&mut buf,
|
||||
&mut progress,
|
||||
)?;
|
||||
|
||||
// Commit and squash alloc log
|
||||
tx.commit(true)
|
||||
})
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct FileConfig {
|
||||
pub path: String,
|
||||
pub data: String,
|
||||
#[serde(default)]
|
||||
pub symlink: bool,
|
||||
#[serde(default)]
|
||||
pub directory: bool,
|
||||
pub mode: Option<u32>,
|
||||
pub uid: Option<u32>,
|
||||
pub gid: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub recursive_chown: bool,
|
||||
#[serde(default)]
|
||||
pub postinstall: bool,
|
||||
}
|
||||
|
||||
impl FileConfig {
|
||||
pub fn new_file(path: String, data: String) -> FileConfig {
|
||||
FileConfig {
|
||||
path,
|
||||
data,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_directory(path: String) -> FileConfig {
|
||||
FileConfig {
|
||||
path,
|
||||
data: String::new(),
|
||||
directory: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_mod(&mut self, mode: u32, uid: u32, gid: u32) -> &mut FileConfig {
|
||||
self.mode = Some(mode);
|
||||
self.uid = Some(uid);
|
||||
self.gid = Some(gid);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_recursive_mod(&mut self, mode: u32, uid: u32, gid: u32) -> &mut FileConfig {
|
||||
self.with_mod(mode, uid, gid);
|
||||
self.recursive_chown = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FileConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.path)?;
|
||||
if self.symlink {
|
||||
write!(f, " -> {}", self.data)?;
|
||||
} else if self.directory {
|
||||
write!(f, " type=dir")?;
|
||||
if self.recursive_chown {
|
||||
write!(f, " chown=yes")?;
|
||||
}
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
" size={}B",
|
||||
arg_parser::to_human_readable_string(self.data.len() as u64)
|
||||
)?;
|
||||
if self.postinstall {
|
||||
write!(f, "!")?;
|
||||
}
|
||||
}
|
||||
if let Some(uid) = self.uid {
|
||||
write!(f, " uid={}", uid)?;
|
||||
}
|
||||
if let Some(uid) = self.uid {
|
||||
write!(f, " gid={}", uid)?;
|
||||
}
|
||||
if let Some(mode) = self.mode {
|
||||
write!(f, " mode={:3o}", mode)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use libc::{gid_t, uid_t};
|
||||
|
||||
use std::ffi::{CString, OsStr};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Error, Write};
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::fs::{symlink, PermissionsExt};
|
||||
use std::path::Path;
|
||||
|
||||
fn chown<P: AsRef<Path>>(path: P, uid: uid_t, gid: gid_t, recursive: bool) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let c_path = CString::new(path.as_os_str().as_bytes()).unwrap();
|
||||
if unsafe { libc::chown(c_path.as_ptr(), uid, gid) } != 0 {
|
||||
return Err(Error::last_os_error().into());
|
||||
}
|
||||
|
||||
if recursive && path.is_dir() {
|
||||
for entry_res in fs::read_dir(path)? {
|
||||
let entry = entry_res?;
|
||||
chown(entry.path(), uid, gid, recursive)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: Rewrite impls
|
||||
impl crate::FileConfig {
|
||||
pub(crate) fn create<P: AsRef<Path>>(&self, prefix: P) -> Result<()> {
|
||||
let path = self.path.trim_start_matches('/');
|
||||
let target_file = prefix.as_ref().join(path);
|
||||
|
||||
if self.directory {
|
||||
println!("Create directory {}", target_file.display());
|
||||
fs::create_dir_all(&target_file)
|
||||
.with_context(|| format!("failed to create directory {}", target_file.display()))?;
|
||||
self.apply_perms(&target_file)?;
|
||||
return Ok(());
|
||||
} else if let Some(parent) = target_file.parent() {
|
||||
println!("Create file parent {}", parent.display());
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create file parent {}", parent.display()))?;
|
||||
}
|
||||
|
||||
if self.symlink {
|
||||
println!("Create symlink {} to {}", target_file.display(), self.data);
|
||||
if target_file.is_symlink() {
|
||||
fs::remove_file(&target_file).with_context(|| {
|
||||
format!("failed to remove old symlink {}", target_file.display())
|
||||
})?;
|
||||
}
|
||||
symlink(&OsStr::new(&self.data), &target_file).with_context(|| {
|
||||
format!(
|
||||
"failed to create symlink {} to {}",
|
||||
target_file.display(),
|
||||
self.data
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
} else {
|
||||
println!("Create file {}", target_file.display());
|
||||
let mut file = File::create(&target_file)
|
||||
.with_context(|| format!("failed to create file {}", target_file.display()))?;
|
||||
file.write_all(self.data.as_bytes())?;
|
||||
|
||||
self.apply_perms(target_file)
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_perms<P: AsRef<Path>>(&self, target: P) -> Result<()> {
|
||||
let path = target.as_ref();
|
||||
let mode = self
|
||||
.mode
|
||||
.unwrap_or_else(|| if self.directory { 0o0755 } else { 0o0644 });
|
||||
let uid = self.uid.unwrap_or(!0);
|
||||
let gid = self.gid.unwrap_or(!0);
|
||||
|
||||
// chmod
|
||||
fs::set_permissions(path, fs::Permissions::from_mode(mode))
|
||||
.with_context(|| format!("failed to set permissions on {}", path.display()))?;
|
||||
|
||||
// chown
|
||||
chown(path, uid, gid, self.recursive_chown)
|
||||
.with_context(|| format!("failed to chown {}", path.display()))
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct GeneralConfig {
|
||||
/// Specify a path where cookbook exists, all packages will be installed locally
|
||||
pub cookbook: Option<String>,
|
||||
/// Allow prompts for missing information such as user password
|
||||
pub prompt: Option<bool>,
|
||||
/// Total filesystem size in MB
|
||||
pub filesystem_size: Option<u32>,
|
||||
/// EFI partition size in MB, default to 2MB
|
||||
pub efi_partition_size: Option<u32>,
|
||||
/// Skip disk partitioning, assume whole disk is a partition
|
||||
pub skip_partitions: Option<bool>,
|
||||
/// Set a plain text password to encrypt the disk
|
||||
pub encrypt_disk: Option<String>,
|
||||
/// Use live disk for bootloader config, default is false
|
||||
pub live_disk: Option<bool>,
|
||||
/// If set, write bootloader disk into this path
|
||||
pub write_bootloader: Option<String>,
|
||||
/// Use AR to write files instead of FUSE-based mount
|
||||
/// (bypasses FUSE, but slower and requires namespaced context such as "podman unshare")
|
||||
pub no_mount: Option<bool>,
|
||||
}
|
||||
|
||||
impl GeneralConfig {
|
||||
/// Merge two config, "other" is more dominant
|
||||
pub(super) fn merge(&mut self, other: GeneralConfig) {
|
||||
if let Some(cookbook) = other.cookbook {
|
||||
self.cookbook = Some(cookbook);
|
||||
}
|
||||
self.filesystem_size = other.filesystem_size.or(self.filesystem_size);
|
||||
self.efi_partition_size = other.efi_partition_size.or(self.efi_partition_size);
|
||||
self.skip_partitions = other.skip_partitions.or(self.skip_partitions);
|
||||
if let Some(encrypt_disk) = other.encrypt_disk {
|
||||
self.encrypt_disk = Some(encrypt_disk);
|
||||
}
|
||||
self.live_disk = other.live_disk.or(self.live_disk);
|
||||
if let Some(write_bootloader) = other.write_bootloader {
|
||||
self.write_bootloader = Some(write_bootloader);
|
||||
}
|
||||
self.no_mount = other.no_mount.or(self.no_mount);
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Display;
|
||||
use std::fs;
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::bail;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::PackageConfig;
|
||||
|
||||
pub mod file;
|
||||
#[cfg(feature = "installer")]
|
||||
pub mod file_impl;
|
||||
pub mod general;
|
||||
pub mod package;
|
||||
pub mod user;
|
||||
|
||||
/// A named group of packages that can be referenced from the `[packages]` section
|
||||
/// of a config TOML. When a group name appears in `[packages]`, the resolver
|
||||
/// expands it to the individual package entries listed here.
|
||||
///
|
||||
/// Groups may reference other groups for hierarchical composition:
|
||||
///
|
||||
/// ```toml
|
||||
/// [package_groups.qt6-core]
|
||||
/// description = "Qt 6 Core modules"
|
||||
/// packages = ["qtbase", "qtdeclarative", "qtsvg"]
|
||||
///
|
||||
/// [package_groups.kde-desktop]
|
||||
/// description = "Complete KDE Plasma desktop session"
|
||||
/// packages = ["qt6-core", "kwin", "sddm"]
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct PackageGroup {
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
pub packages: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub include: Vec<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub general: general::GeneralConfig,
|
||||
#[serde(default)]
|
||||
pub packages: BTreeMap<String, package::PackageConfig>,
|
||||
#[serde(default)]
|
||||
pub package_groups: BTreeMap<String, PackageGroup>,
|
||||
#[serde(default)]
|
||||
pub files: Vec<file::FileConfig>,
|
||||
#[serde(default)]
|
||||
pub users: BTreeMap<String, user::UserConfig>,
|
||||
#[serde(default)]
|
||||
pub groups: BTreeMap<String, user::GroupConfig>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load installer config from a TOML path
|
||||
pub fn from_file(path: &Path) -> Result<Self> {
|
||||
let mut config: Config = match fs::read_to_string(&path) {
|
||||
Ok(config_data) => match toml::from_str(&config_data) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
bail!("failed to decode '{}': {}", path.display(), err);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
bail!("failed to read '{}': {}", path.display(), err);
|
||||
}
|
||||
};
|
||||
|
||||
let config_dir = path.parent().unwrap();
|
||||
|
||||
let mut configs = mem::take(&mut config.include)
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
Config::from_file(&config_dir.join(&path))
|
||||
.with_context(|| format!("Importing from {}", path.display()))
|
||||
})
|
||||
.collect::<Result<Vec<Config>>>()?;
|
||||
configs.push(config); // Put ourself last to ensure that it overwrites anything else.
|
||||
|
||||
config = configs.remove(0);
|
||||
|
||||
for other_config in configs {
|
||||
config.merge(other_config);
|
||||
}
|
||||
|
||||
config.resolve_package_groups()?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Load hardcoded install config to fetch bootloaders
|
||||
pub fn bootloader_config() -> Self {
|
||||
let mut bootloader_config = Config::default();
|
||||
// TODO: This is unused
|
||||
bootloader_config.files.push(file::FileConfig {
|
||||
path: "/etc/pkg.d/50_redox".to_string(),
|
||||
data: "https://static.redox-os.org/pkg".to_string(),
|
||||
..Default::default()
|
||||
});
|
||||
bootloader_config
|
||||
.packages
|
||||
.insert("bootloader".to_string(), PackageConfig::default());
|
||||
bootloader_config
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: Config) {
|
||||
assert!(self.include.is_empty());
|
||||
assert!(other.include.is_empty());
|
||||
|
||||
let Config {
|
||||
include: _,
|
||||
general: other_general,
|
||||
packages: other_packages,
|
||||
package_groups: other_package_groups,
|
||||
files: other_files,
|
||||
users: other_users,
|
||||
groups: other_groups,
|
||||
} = other;
|
||||
|
||||
self.general.merge(other_general);
|
||||
|
||||
for (package, package_config) in other_packages {
|
||||
self.packages.insert(package, package_config);
|
||||
}
|
||||
|
||||
for (group_name, group) in other_package_groups {
|
||||
self.package_groups.insert(group_name, group);
|
||||
}
|
||||
|
||||
self.files.extend(other_files);
|
||||
|
||||
for (user, user_config) in other_users {
|
||||
self.users.insert(user, user_config);
|
||||
}
|
||||
|
||||
for (group, group_config) in other_groups {
|
||||
self.groups.insert(group, group_config);
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand all `[package_groups]` references in `packages` into individual
|
||||
/// package entries. Must be called after `merge()` so that groups from all
|
||||
/// included configs are collected.
|
||||
///
|
||||
/// Explicit package entries always take priority over group-expanded entries.
|
||||
/// Circular group references are detected and rejected.
|
||||
pub fn resolve_package_groups(&mut self) -> Result<()> {
|
||||
if self.package_groups.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut resolved: BTreeMap<String, package::PackageConfig> = BTreeMap::new();
|
||||
|
||||
for (name, config) in &self.packages {
|
||||
if !self.package_groups.contains_key(name) {
|
||||
resolved.insert(name.clone(), config.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (name, config) in &self.packages {
|
||||
if self.package_groups.contains_key(name) {
|
||||
let mut visiting = BTreeSet::new();
|
||||
let expanded = self.expand_group(name, &mut visiting)?;
|
||||
for pkg in expanded {
|
||||
resolved.entry(pkg).or_insert_with(|| config.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.packages = resolved;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expand_group(
|
||||
&self,
|
||||
name: &str,
|
||||
visiting: &mut BTreeSet<String>,
|
||||
) -> Result<Vec<String>> {
|
||||
if !visiting.insert(name.to_string()) {
|
||||
bail!("circular package group reference involving '{}'", name);
|
||||
}
|
||||
|
||||
let group = self
|
||||
.package_groups
|
||||
.get(name)
|
||||
.ok_or_else(|| anyhow!("package group '{}' not found", name))?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for pkg in &group.packages {
|
||||
if self.package_groups.contains_key(pkg) {
|
||||
result.extend(self.expand_group(pkg, visiting)?);
|
||||
} else {
|
||||
result.push(pkg.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
visiting.remove(name);
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Config {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "files:")?;
|
||||
for file in &self.files {
|
||||
writeln!(f, "- {}", file)?;
|
||||
}
|
||||
writeln!(f, "users:")?;
|
||||
for (name, user) in &self.users {
|
||||
writeln!(f, "- {}:{}", name, user)?;
|
||||
}
|
||||
write!(f, "packages: ")?;
|
||||
for name in self.packages.keys() {
|
||||
write!(f, " {}", name)?;
|
||||
}
|
||||
writeln!(f, "")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_resolve_nested_groups() {
|
||||
let toml_str = r#"
|
||||
[package_groups.qt6-core]
|
||||
packages = ["qtbase", "qtdeclarative", "qtsvg"]
|
||||
|
||||
[package_groups.qt6-extras]
|
||||
packages = ["qtwayland", "qt6-sensors"]
|
||||
|
||||
[package_groups.qt6-all]
|
||||
packages = ["qt6-core", "qt6-extras"]
|
||||
|
||||
[package_groups.kde-desktop]
|
||||
packages = ["qt6-all", "kwin", "sddm"]
|
||||
|
||||
[packages]
|
||||
kde-desktop = {}
|
||||
"#;
|
||||
let mut config: Config = toml::from_str(toml_str).unwrap();
|
||||
config.resolve_package_groups().unwrap();
|
||||
|
||||
assert!(config.packages.contains_key("qtbase"));
|
||||
assert!(config.packages.contains_key("qtdeclarative"));
|
||||
assert!(config.packages.contains_key("qtsvg"));
|
||||
assert!(config.packages.contains_key("qtwayland"));
|
||||
assert!(config.packages.contains_key("qt6-sensors"));
|
||||
assert!(config.packages.contains_key("kwin"));
|
||||
assert!(config.packages.contains_key("sddm"));
|
||||
assert!(!config.packages.contains_key("kde-desktop"));
|
||||
assert!(!config.packages.contains_key("qt6-all"));
|
||||
assert!(!config.packages.contains_key("qt6-core"));
|
||||
assert!(!config.packages.contains_key("qt6-extras"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_explicit_overrides_group() {
|
||||
let toml_str = r#"
|
||||
[package_groups.qt6-core]
|
||||
packages = ["qtbase", "qtdeclarative"]
|
||||
|
||||
[packages]
|
||||
qt6-core = {}
|
||||
qtbase = "ignore"
|
||||
"#;
|
||||
let mut config: Config = toml::from_str(toml_str).unwrap();
|
||||
config.resolve_package_groups().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config.packages.get("qtbase").unwrap(),
|
||||
&package::PackageConfig::Build("ignore".to_string())
|
||||
);
|
||||
assert!(config.packages.contains_key("qtdeclarative"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_groups_no_change() {
|
||||
let toml_str = r#"
|
||||
[packages]
|
||||
foo = {}
|
||||
bar = {}
|
||||
"#;
|
||||
let mut config: Config = toml::from_str(toml_str).unwrap();
|
||||
config.resolve_package_groups().unwrap();
|
||||
|
||||
assert_eq!(config.packages.len(), 2);
|
||||
assert!(config.packages.contains_key("foo"));
|
||||
assert!(config.packages.contains_key("bar"));
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum PackageConfig {
|
||||
Empty,
|
||||
Build(String),
|
||||
|
||||
// TODO: Sum type
|
||||
Spec {
|
||||
version: Option<String>,
|
||||
git: Option<String>,
|
||||
path: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for PackageConfig {
|
||||
fn default() -> Self {
|
||||
Self::Empty
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct UserConfig {
|
||||
pub password: Option<String>,
|
||||
pub uid: Option<u32>,
|
||||
pub gid: Option<u32>,
|
||||
pub name: Option<String>,
|
||||
pub home: Option<String>,
|
||||
pub shell: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct GroupConfig {
|
||||
pub gid: Option<u32>,
|
||||
// FIXME move this to the UserConfig struct as extra_groups
|
||||
pub members: Vec<String>,
|
||||
}
|
||||
|
||||
impl Display for UserConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(uid) = &self.uid {
|
||||
write!(f, " uid={}", uid)?;
|
||||
}
|
||||
if let Some(gid) = &self.gid {
|
||||
write!(f, " gid={}", gid)?;
|
||||
}
|
||||
if let Some(name) = &self.name {
|
||||
write!(f, " name={}", name)?;
|
||||
}
|
||||
if let Some(home) = &self.home {
|
||||
write!(f, " home={}", home)?;
|
||||
}
|
||||
if let Some(shell) = &self.shell {
|
||||
write!(f, " shell={}", shell)?;
|
||||
}
|
||||
if self.password.as_ref().is_some_and(|s| !s.is_empty()) {
|
||||
write!(f, " password=yes")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
use core::{mem, ops, slice, str};
|
||||
|
||||
use crate::{BlockLevel, BlockTrait, Node, TreePtr, BLOCK_SIZE, DIR_ENTRY_MAX_LENGTH};
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct DirEntry {
|
||||
node_ptr: TreePtr<Node>,
|
||||
name: [u8; DIR_ENTRY_MAX_LENGTH],
|
||||
}
|
||||
|
||||
impl DirEntry {
|
||||
pub fn new(node_ptr: TreePtr<Node>, name: &str) -> DirEntry {
|
||||
let mut entry = DirEntry {
|
||||
node_ptr,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
entry.name[..name.len()].copy_from_slice(name.as_bytes());
|
||||
|
||||
entry
|
||||
}
|
||||
|
||||
pub fn node_ptr(&self) -> TreePtr<Node> {
|
||||
self.node_ptr
|
||||
}
|
||||
|
||||
fn name_len(&self) -> usize {
|
||||
let mut len = 0;
|
||||
while len < self.name.len() {
|
||||
if self.name[len] == 0 {
|
||||
break;
|
||||
}
|
||||
len += 1;
|
||||
}
|
||||
len
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
let len = self.name_len();
|
||||
//TODO: report utf8 error?
|
||||
str::from_utf8(&self.name[..len]).ok()
|
||||
}
|
||||
|
||||
// 4 bytes TreePtr
|
||||
// 1 byte name_len
|
||||
const SERIALIZED_PREFIX_SIZE: usize = mem::size_of::<TreePtr<Node>>() + 1;
|
||||
|
||||
pub fn serialized_size(&self) -> usize {
|
||||
DirEntry::SERIALIZED_PREFIX_SIZE + self.name_len()
|
||||
}
|
||||
|
||||
fn serialize_into(&self, buf: &mut [u8]) -> Option<usize> {
|
||||
let required = self.serialized_size();
|
||||
if buf.len() < required {
|
||||
return None;
|
||||
}
|
||||
|
||||
buf[0..4].copy_from_slice(&self.node_ptr().to_bytes());
|
||||
buf[4] = self.name_len() as u8;
|
||||
buf[5..5 + self.name_len()].copy_from_slice(&self.name[..self.name_len()]);
|
||||
|
||||
Some(required)
|
||||
}
|
||||
|
||||
fn deserialize_from(buf: &[u8]) -> Result<(Self, usize), &'static str> {
|
||||
if buf.len() <= DirEntry::SERIALIZED_PREFIX_SIZE {
|
||||
return Err("Buffer too small");
|
||||
}
|
||||
|
||||
let node_ptr: TreePtr<Node> =
|
||||
TreePtr::from_bytes(buf[0..4].try_into().expect("Slice must be 4 bytes long"));
|
||||
let name_len = buf[4] as usize;
|
||||
|
||||
if name_len < 1 || name_len > DIR_ENTRY_MAX_LENGTH {
|
||||
return Err("Invalid name length");
|
||||
}
|
||||
|
||||
if buf.len() < DirEntry::SERIALIZED_PREFIX_SIZE + name_len {
|
||||
return Err("Buffer too small");
|
||||
}
|
||||
|
||||
let mut name = [0u8; DIR_ENTRY_MAX_LENGTH];
|
||||
name[..name_len].copy_from_slice(
|
||||
&buf[DirEntry::SERIALIZED_PREFIX_SIZE..DirEntry::SERIALIZED_PREFIX_SIZE + name_len],
|
||||
);
|
||||
|
||||
Ok((
|
||||
DirEntry { node_ptr, name },
|
||||
DirEntry::SERIALIZED_PREFIX_SIZE + name_len,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DirEntry {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
node_ptr: TreePtr::default(),
|
||||
name: [0; DIR_ENTRY_MAX_LENGTH],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DirList {
|
||||
count: u16,
|
||||
entry_bytes_len: u16,
|
||||
entry_bytes: [u8; BLOCK_SIZE as usize - 4],
|
||||
}
|
||||
|
||||
unsafe impl BlockTrait for DirList {
|
||||
fn empty(level: BlockLevel) -> Option<Self> {
|
||||
if level.0 == 0 {
|
||||
Some(Self {
|
||||
count: 0,
|
||||
entry_bytes_len: 0,
|
||||
entry_bytes: [0; BLOCK_SIZE as usize - 4],
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DirList {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.count == 0
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> DirEntryIterator<'_> {
|
||||
DirEntryIterator {
|
||||
dir_list: self,
|
||||
emit_count: 0,
|
||||
position: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_position_for_name(&self, name: &str) -> Option<usize> {
|
||||
let name_len = name.len();
|
||||
let mut position = 0;
|
||||
let mut entry_id = 0;
|
||||
|
||||
while entry_id < self.count {
|
||||
let entry_name_len = self.entry_bytes[position + 4] as usize;
|
||||
if entry_name_len == name_len {
|
||||
let start = DirEntry::SERIALIZED_PREFIX_SIZE + position;
|
||||
let entry_name = &self.entry_bytes[start..start + entry_name_len];
|
||||
if entry_name == name.as_bytes() {
|
||||
return Some(position);
|
||||
}
|
||||
}
|
||||
position += DirEntry::SERIALIZED_PREFIX_SIZE + entry_name_len;
|
||||
entry_id += 1;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn find_entry(&self, name: &str) -> Option<DirEntry> {
|
||||
if let Some(position) = self.entry_position_for_name(name) {
|
||||
let (entry, _) = DirEntry::deserialize_from(&self.entry_bytes[position..]).unwrap();
|
||||
return Some(entry);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn remove_entry(&mut self, name: &str) -> bool {
|
||||
if let Some(position) = self.entry_position_for_name(name) {
|
||||
let entry_size =
|
||||
DirEntry::SERIALIZED_PREFIX_SIZE + self.entry_bytes[position + 4] as usize;
|
||||
let remaining_size = self.entry_bytes_len as usize - position - entry_size;
|
||||
if remaining_size > 0 {
|
||||
self.entry_bytes.copy_within(
|
||||
position + entry_size..self.entry_bytes_len as usize,
|
||||
position,
|
||||
);
|
||||
}
|
||||
self.entry_bytes_len -= entry_size as u16;
|
||||
self.count -= 1;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn for_each_entry<F>(&self, mut f: F)
|
||||
where
|
||||
F: FnMut(&[u8; 4], &[u8]),
|
||||
{
|
||||
let mut position = 0;
|
||||
let mut entry_id = 0;
|
||||
|
||||
while entry_id < self.count {
|
||||
let node_ptr_bytes = &self.entry_bytes[position..position + 4];
|
||||
//let node_ptr = TreePtr::<Node>::from_bytes(node_ptr_bytes.try_into().unwrap());
|
||||
let entry_name_len = self.entry_bytes[position + 4] as usize;
|
||||
let start = DirEntry::SERIALIZED_PREFIX_SIZE + position;
|
||||
let entry_name = &self.entry_bytes[start..start + entry_name_len];
|
||||
|
||||
f(node_ptr_bytes.try_into().unwrap(), entry_name);
|
||||
|
||||
position += DirEntry::SERIALIZED_PREFIX_SIZE + entry_name_len;
|
||||
entry_id += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append(&mut self, entry: &DirEntry) -> bool {
|
||||
let entry_bytes_len = self.entry_bytes_len as usize;
|
||||
if let Some(size) = entry.serialize_into(&mut self.entry_bytes[entry_bytes_len..]) {
|
||||
self.count += 1;
|
||||
self.entry_bytes_len += size as u16;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn entry_count(&self) -> usize {
|
||||
self.count as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for DirList {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts(
|
||||
self as *const DirList as *const u8,
|
||||
mem::size_of::<DirList>(),
|
||||
) as &[u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for DirList {
|
||||
fn deref_mut(&mut self) -> &mut [u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts_mut(self as *mut DirList as *mut u8, mem::size_of::<DirList>())
|
||||
as &mut [u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DirEntryIterator<'a> {
|
||||
dir_list: &'a DirList,
|
||||
emit_count: usize,
|
||||
position: usize,
|
||||
}
|
||||
|
||||
impl Iterator for DirEntryIterator<'_> {
|
||||
type Item = DirEntry;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.emit_count < self.dir_list.entry_count() {
|
||||
let position = self.position;
|
||||
let (entry, bytes_read) =
|
||||
DirEntry::deserialize_from(&self.dir_list.entry_bytes[position..]).unwrap();
|
||||
|
||||
self.emit_count += 1;
|
||||
self.position += bytes_read;
|
||||
|
||||
Some(entry)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use alloc::format;
|
||||
|
||||
#[test]
|
||||
fn dir_list_size_test() {
|
||||
use core::ops::Deref;
|
||||
assert_eq!(
|
||||
DirList::empty(BlockLevel(0)).unwrap().deref().len(),
|
||||
BLOCK_SIZE as usize
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_append() {
|
||||
let mut dir_list = DirList::empty(BlockLevel(0)).unwrap();
|
||||
let dirent = DirEntry::new(TreePtr::new(123), "test000");
|
||||
|
||||
assert!(dir_list.append(&dirent));
|
||||
assert_eq!(dir_list.entry_count(), 1);
|
||||
assert_eq!(dir_list.entry_bytes_len as usize, dirent.serialized_size());
|
||||
|
||||
let max_entries = dir_list.entry_bytes.len() / dirent.serialized_size();
|
||||
for i in 1..max_entries {
|
||||
let dirent = DirEntry::new(TreePtr::new(123), format!("test{i:03}").as_str());
|
||||
assert!(dir_list.append(&dirent), "Failed on iteration {i}");
|
||||
}
|
||||
let dirent = DirEntry::new(TreePtr::new(123), format!("test{max_entries}").as_str());
|
||||
assert!(!dir_list.append(&dirent));
|
||||
|
||||
for (i, entry) in dir_list.entries().enumerate() {
|
||||
assert_eq!(entry.name().unwrap(), format!("test{i:03}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::{cmp, ptr};
|
||||
use syscall::error::Result;
|
||||
|
||||
use crate::disk::Disk;
|
||||
use crate::BLOCK_SIZE;
|
||||
|
||||
fn copy_memory(src: &[u8], dest: &mut [u8]) -> usize {
|
||||
let len = cmp::min(src.len(), dest.len());
|
||||
unsafe { ptr::copy(src.as_ptr(), dest.as_mut_ptr(), len) };
|
||||
len
|
||||
}
|
||||
|
||||
pub struct DiskCache<T> {
|
||||
inner: T,
|
||||
cache: HashMap<u64, [u8; BLOCK_SIZE as usize]>,
|
||||
order: VecDeque<u64>,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
impl<T: Disk> DiskCache<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
// 16 MB cache
|
||||
let size = 16 * 1024 * 1024 / BLOCK_SIZE as usize;
|
||||
DiskCache {
|
||||
inner,
|
||||
cache: HashMap::with_capacity(size),
|
||||
order: VecDeque::with_capacity(size),
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
fn insert(&mut self, i: u64, data: [u8; BLOCK_SIZE as usize]) {
|
||||
while self.order.len() >= self.size {
|
||||
let removed = self.order.pop_front().unwrap();
|
||||
self.cache.remove(&removed);
|
||||
}
|
||||
|
||||
self.cache.insert(i, data);
|
||||
self.order.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Disk> Disk for DiskCache<T> {
|
||||
unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result<usize> {
|
||||
// println!("Cache read at {}", block);
|
||||
|
||||
let mut read = 0;
|
||||
let mut failed = false;
|
||||
for i in 0..buffer.len().div_ceil(BLOCK_SIZE as usize) {
|
||||
let block_i = block + i as u64;
|
||||
|
||||
let buffer_i = i * BLOCK_SIZE as usize;
|
||||
let buffer_j = cmp::min(buffer_i + BLOCK_SIZE as usize, buffer.len());
|
||||
let buffer_slice = &mut buffer[buffer_i..buffer_j];
|
||||
|
||||
if let Some(cache_buf) = self.cache.get_mut(&block_i) {
|
||||
read += copy_memory(cache_buf, buffer_slice);
|
||||
} else {
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if failed {
|
||||
self.inner.read_at(block, buffer)?;
|
||||
|
||||
read = 0;
|
||||
for i in 0..buffer.len().div_ceil(BLOCK_SIZE as usize) {
|
||||
let block_i = block + i as u64;
|
||||
|
||||
let buffer_i = i * BLOCK_SIZE as usize;
|
||||
let buffer_j = cmp::min(buffer_i + BLOCK_SIZE as usize, buffer.len());
|
||||
let buffer_slice = &buffer[buffer_i..buffer_j];
|
||||
|
||||
let mut cache_buf = [0; BLOCK_SIZE as usize];
|
||||
read += copy_memory(buffer_slice, &mut cache_buf);
|
||||
self.insert(block_i, cache_buf);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(read)
|
||||
}
|
||||
|
||||
unsafe fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result<usize> {
|
||||
//TODO: Write only blocks that have changed
|
||||
// println!("Cache write at {}", block);
|
||||
|
||||
self.inner.write_at(block, buffer)?;
|
||||
|
||||
let mut written = 0;
|
||||
for i in 0..buffer.len().div_ceil(BLOCK_SIZE as usize) {
|
||||
let block_i = block + i as u64;
|
||||
|
||||
let buffer_i = i * BLOCK_SIZE as usize;
|
||||
let buffer_j = cmp::min(buffer_i + BLOCK_SIZE as usize, buffer.len());
|
||||
let buffer_slice = &buffer[buffer_i..buffer_j];
|
||||
|
||||
let mut cache_buf = [0; BLOCK_SIZE as usize];
|
||||
written += copy_memory(buffer_slice, &mut cache_buf);
|
||||
self.insert(block_i, cache_buf);
|
||||
}
|
||||
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
fn size(&mut self) -> Result<u64> {
|
||||
self.inner.size()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{Seek, SeekFrom};
|
||||
use std::os::unix::fs::FileExt;
|
||||
use std::path::Path;
|
||||
|
||||
use syscall::error::{Error, Result, EIO};
|
||||
|
||||
use crate::disk::Disk;
|
||||
use crate::BLOCK_SIZE;
|
||||
|
||||
pub struct DiskFile {
|
||||
pub file: File,
|
||||
}
|
||||
|
||||
trait ResultExt {
|
||||
type T;
|
||||
fn or_eio(self) -> Result<Self::T>;
|
||||
}
|
||||
impl<T> ResultExt for Result<T> {
|
||||
type T = T;
|
||||
fn or_eio(self) -> Result<Self::T> {
|
||||
match self {
|
||||
Ok(t) => Ok(t),
|
||||
Err(err) => {
|
||||
eprintln!("RedoxFS: IO ERROR: {err}");
|
||||
Err(Error::new(EIO))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> ResultExt for std::io::Result<T> {
|
||||
type T = T;
|
||||
fn or_eio(self) -> Result<Self::T> {
|
||||
match self {
|
||||
Ok(t) => Ok(t),
|
||||
Err(err) => {
|
||||
eprintln!("RedoxFS: IO ERROR: {err}");
|
||||
Err(Error::new(EIO))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiskFile {
|
||||
pub fn open(path: impl AsRef<Path>) -> Result<DiskFile> {
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(path)
|
||||
.or_eio()?;
|
||||
Ok(DiskFile { file })
|
||||
}
|
||||
|
||||
pub fn create(path: impl AsRef<Path>, size: u64) -> Result<DiskFile> {
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(path)
|
||||
.or_eio()?;
|
||||
file.set_len(size).or_eio()?;
|
||||
Ok(DiskFile { file })
|
||||
}
|
||||
}
|
||||
|
||||
impl Disk for DiskFile {
|
||||
unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result<usize> {
|
||||
self.file.read_at(buffer, block * BLOCK_SIZE).or_eio()
|
||||
}
|
||||
|
||||
unsafe fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result<usize> {
|
||||
self.file.write_at(buffer, block * BLOCK_SIZE).or_eio()
|
||||
}
|
||||
|
||||
fn size(&mut self) -> Result<u64> {
|
||||
self.file.seek(SeekFrom::End(0)).or_eio()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<File> for DiskFile {
|
||||
fn from(file: File) -> Self {
|
||||
Self { file }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use syscall::error::{Error, Result, EIO};
|
||||
|
||||
use crate::disk::Disk;
|
||||
use crate::BLOCK_SIZE;
|
||||
|
||||
macro_rules! try_disk {
|
||||
($expr:expr) => {
|
||||
match $expr {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
eprintln!("Disk I/O Error: {}", err);
|
||||
return Err(Error::new(EIO));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub struct DiskIo<T>(pub T);
|
||||
|
||||
impl<T: Read + Write + Seek> Disk for DiskIo<T> {
|
||||
unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result<usize> {
|
||||
try_disk!(self.0.seek(SeekFrom::Start(block * BLOCK_SIZE)));
|
||||
let count = try_disk!(self.0.read(buffer));
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
unsafe fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result<usize> {
|
||||
try_disk!(self.0.seek(SeekFrom::Start(block * BLOCK_SIZE)));
|
||||
let count = try_disk!(self.0.write(buffer));
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn size(&mut self) -> Result<u64> {
|
||||
let size = try_disk!(self.0.seek(SeekFrom::End(0)));
|
||||
Ok(size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use syscall::error::{Error, Result, EIO};
|
||||
|
||||
use crate::disk::Disk;
|
||||
use crate::BLOCK_SIZE;
|
||||
|
||||
pub struct DiskMemory {
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DiskMemory {
|
||||
pub fn new(size: u64) -> DiskMemory {
|
||||
DiskMemory {
|
||||
data: vec![0; size as usize],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Disk for DiskMemory {
|
||||
unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result<usize> {
|
||||
let offset = (block * BLOCK_SIZE) as usize;
|
||||
let end = offset + buffer.len();
|
||||
if end > self.data.len() {
|
||||
return Err(Error::new(EIO));
|
||||
}
|
||||
buffer.copy_from_slice(&self.data[offset..end]);
|
||||
Ok(buffer.len())
|
||||
}
|
||||
|
||||
unsafe fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result<usize> {
|
||||
let offset = (block * BLOCK_SIZE) as usize;
|
||||
let end = offset + buffer.len();
|
||||
if end > self.data.len() {
|
||||
return Err(Error::new(EIO));
|
||||
}
|
||||
self.data[offset..end].copy_from_slice(buffer);
|
||||
Ok(buffer.len())
|
||||
}
|
||||
|
||||
fn size(&mut self) -> Result<u64> {
|
||||
Ok(self.data.len() as u64)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use syscall::error::Result;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub use self::cache::DiskCache;
|
||||
#[cfg(feature = "std")]
|
||||
pub use self::file::DiskFile;
|
||||
#[cfg(feature = "std")]
|
||||
pub use self::io::DiskIo;
|
||||
#[cfg(feature = "std")]
|
||||
pub use self::memory::DiskMemory;
|
||||
#[cfg(feature = "std")]
|
||||
pub use self::sparse::DiskSparse;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
mod cache;
|
||||
#[cfg(feature = "std")]
|
||||
mod file;
|
||||
#[cfg(feature = "std")]
|
||||
mod io;
|
||||
#[cfg(feature = "std")]
|
||||
mod memory;
|
||||
#[cfg(feature = "std")]
|
||||
mod sparse;
|
||||
|
||||
/// A disk
|
||||
pub trait Disk {
|
||||
/// Read blocks from disk
|
||||
///
|
||||
/// # Safety
|
||||
/// Unsafe to discourage use, use filesystem wrappers instead
|
||||
unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result<usize>;
|
||||
|
||||
/// Write blocks from disk
|
||||
///
|
||||
/// # Safety
|
||||
/// Unsafe to discourage use, use filesystem wrappers instead
|
||||
unsafe fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result<usize>;
|
||||
|
||||
/// Get size of disk in bytes
|
||||
fn size(&mut self) -> Result<u64>;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::path::Path;
|
||||
use syscall::error::{Error, Result, EIO};
|
||||
|
||||
use crate::disk::Disk;
|
||||
use crate::BLOCK_SIZE;
|
||||
|
||||
macro_rules! try_disk {
|
||||
($expr:expr) => {
|
||||
match $expr {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
eprintln!("Disk I/O Error: {}", err);
|
||||
return Err(Error::new(EIO));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub struct DiskSparse {
|
||||
pub file: File,
|
||||
pub max_size: u64,
|
||||
}
|
||||
|
||||
impl DiskSparse {
|
||||
pub fn create<P: AsRef<Path>>(path: P, max_size: u64) -> Result<DiskSparse> {
|
||||
let file = try_disk!(OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(path));
|
||||
Ok(DiskSparse { file, max_size })
|
||||
}
|
||||
}
|
||||
|
||||
impl Disk for DiskSparse {
|
||||
unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result<usize> {
|
||||
try_disk!(self.file.seek(SeekFrom::Start(block * BLOCK_SIZE)));
|
||||
let count = try_disk!(self.file.read(buffer));
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
unsafe fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result<usize> {
|
||||
try_disk!(self.file.seek(SeekFrom::Start(block * BLOCK_SIZE)));
|
||||
let count = try_disk!(self.file.write(buffer));
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn size(&mut self) -> Result<u64> {
|
||||
Ok(self.max_size)
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
use std::{
|
||||
cmp,
|
||||
convert::TryInto,
|
||||
fs::{File, OpenOptions},
|
||||
io::{Read, Result, Seek, SeekFrom, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DiskWrapper {
|
||||
disk: File,
|
||||
size: u64,
|
||||
block: Box<[u8]>,
|
||||
seek: u64,
|
||||
}
|
||||
|
||||
enum Buffer<'a> {
|
||||
Read(&'a mut [u8]),
|
||||
Write(&'a [u8]),
|
||||
}
|
||||
|
||||
impl DiskWrapper {
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let disk = OpenOptions::new().read(true).write(true).open(path)?;
|
||||
let metadata = disk.metadata()?;
|
||||
let size = metadata.len();
|
||||
// TODO: get real block size: disk_metadata.blksize() works on disks but not image files
|
||||
let block_size = 512;
|
||||
let block = vec![0u8; block_size].into_boxed_slice();
|
||||
Ok(Self {
|
||||
disk,
|
||||
size,
|
||||
block,
|
||||
seek: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn block_size(&self) -> usize {
|
||||
self.block.len()
|
||||
}
|
||||
|
||||
pub fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn io<'a>(&mut self, buf: &mut Buffer<'a>) -> Result<usize> {
|
||||
let buf_len = match buf {
|
||||
Buffer::Read(read) => read.len(),
|
||||
Buffer::Write(write) => write.len(),
|
||||
};
|
||||
let block_len: u64 = self.block.len().try_into().unwrap();
|
||||
|
||||
// Do aligned I/O quickly
|
||||
if self.seek % block_len == 0 && buf_len as u64 % block_len == 0 {
|
||||
self.disk.seek(SeekFrom::Start(self.seek))?;
|
||||
match buf {
|
||||
Buffer::Read(read) => self.disk.read_exact(read)?,
|
||||
Buffer::Write(write) => self.disk.write_all(write)?,
|
||||
}
|
||||
self.seek = self.seek.checked_add(buf_len.try_into().unwrap()).unwrap();
|
||||
return Ok(buf_len);
|
||||
}
|
||||
|
||||
let mut i = 0;
|
||||
while i < buf_len {
|
||||
let block = self.seek / block_len;
|
||||
let offset: usize = (self.seek % block_len).try_into().unwrap();
|
||||
let remaining = buf_len.checked_sub(i).unwrap();
|
||||
let len = cmp::min(remaining, self.block.len().checked_sub(offset).unwrap());
|
||||
|
||||
self.disk
|
||||
.seek(SeekFrom::Start(block.checked_mul(block_len).unwrap()))?;
|
||||
self.disk.read_exact(&mut self.block)?;
|
||||
|
||||
match buf {
|
||||
Buffer::Read(read) => {
|
||||
read[i..i.checked_add(len).unwrap()]
|
||||
.copy_from_slice(&self.block[offset..offset.checked_add(len).unwrap()]);
|
||||
}
|
||||
Buffer::Write(write) => {
|
||||
self.block[offset..offset.checked_add(len).unwrap()]
|
||||
.copy_from_slice(&write[i..i.checked_add(len).unwrap()]);
|
||||
|
||||
self.disk
|
||||
.seek(SeekFrom::Start(block.checked_mul(block_len).unwrap()))?;
|
||||
self.disk.write_all(&mut self.block)?;
|
||||
}
|
||||
}
|
||||
|
||||
i = i.checked_add(len).unwrap();
|
||||
self.seek = self.seek.checked_add(len.try_into().unwrap()).unwrap();
|
||||
}
|
||||
|
||||
Ok(i)
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for DiskWrapper {
|
||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
|
||||
self.io(&mut Buffer::Read(buf))
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for DiskWrapper {
|
||||
fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
|
||||
let current: i64 = self.seek.try_into().unwrap();
|
||||
let end: i64 = self.size.try_into().unwrap();
|
||||
self.seek = match pos {
|
||||
SeekFrom::Start(offset) => cmp::min(self.size, offset),
|
||||
SeekFrom::End(offset) => cmp::max(0, cmp::min(end, end.wrapping_add(offset))) as u64,
|
||||
SeekFrom::Current(offset) => {
|
||||
cmp::max(0, cmp::min(end, current.wrapping_add(offset))) as u64
|
||||
}
|
||||
};
|
||||
Ok(self.seek)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for DiskWrapper {
|
||||
fn write(&mut self, buf: &[u8]) -> Result<usize> {
|
||||
self.io(&mut Buffer::Write(buf))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<()> {
|
||||
self.disk.flush()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
use aes::Aes128;
|
||||
use alloc::{
|
||||
boxed::Box,
|
||||
collections::{BTreeMap, VecDeque},
|
||||
vec,
|
||||
};
|
||||
use syscall::error::{Error, Result, EKEYREJECTED, ENOENT, ENOKEY};
|
||||
use xts_mode::{get_tweak_default, Xts128};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use crate::{AllocEntry, AllocList, BlockData, BlockTrait, Key, KeySlot, Node, Salt, TreeList};
|
||||
use crate::{
|
||||
Allocator, BlockAddr, BlockLevel, BlockMeta, Disk, Header, Transaction, BLOCK_SIZE,
|
||||
HEADER_RING, RECORD_SIZE,
|
||||
};
|
||||
|
||||
fn compress_cache() -> Box<[u8]> {
|
||||
vec![0; lz4_flex::block::get_maximum_output_size(RECORD_SIZE as usize)].into_boxed_slice()
|
||||
}
|
||||
|
||||
/// A file system
|
||||
pub struct FileSystem<D: Disk> {
|
||||
//TODO: make private
|
||||
pub disk: D,
|
||||
//TODO: make private
|
||||
pub block: u64,
|
||||
//TODO: make private
|
||||
pub header: Header,
|
||||
pub(crate) allocator: Allocator,
|
||||
pub(crate) cipher_opt: Option<Xts128<Aes128>>,
|
||||
pub(crate) compress_cache: Box<[u8]>,
|
||||
pub node_usages: BTreeMap<u32, u64>,
|
||||
}
|
||||
|
||||
impl<D: Disk> FileSystem<D> {
|
||||
/// Open a file system on a disk
|
||||
pub fn open(
|
||||
mut disk: D,
|
||||
password_opt: Option<&[u8]>,
|
||||
block_opt: Option<u64>,
|
||||
cleanup: bool,
|
||||
) -> Result<Self> {
|
||||
for ring_block in block_opt.map_or(0..65536, |x| x..x + 1) {
|
||||
let mut header = Header::default();
|
||||
unsafe { disk.read_at(ring_block, &mut header)? };
|
||||
|
||||
// Skip invalid headers
|
||||
if !header.valid() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let block = ring_block - (header.generation() % HEADER_RING);
|
||||
for i in 0..HEADER_RING {
|
||||
let mut other_header = Header::default();
|
||||
unsafe { disk.read_at(block + i, &mut other_header)? };
|
||||
|
||||
// Skip invalid headers
|
||||
if !other_header.valid() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this is a newer header, use it
|
||||
if other_header.generation() > header.generation() {
|
||||
header = other_header;
|
||||
}
|
||||
}
|
||||
|
||||
let cipher_opt = match password_opt {
|
||||
Some(password) => {
|
||||
if !header.encrypted() {
|
||||
// Header not encrypted but password provided
|
||||
return Err(Error::new(EKEYREJECTED));
|
||||
}
|
||||
match header.cipher(password) {
|
||||
Some(cipher) => Some(cipher),
|
||||
None => {
|
||||
// Header encrypted with a different password
|
||||
return Err(Error::new(ENOKEY));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if header.encrypted() {
|
||||
// Header encrypted but no password provided
|
||||
return Err(Error::new(ENOKEY));
|
||||
}
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let mut fs = FileSystem {
|
||||
disk,
|
||||
block,
|
||||
header,
|
||||
allocator: Allocator::default(),
|
||||
cipher_opt,
|
||||
compress_cache: compress_cache(),
|
||||
node_usages: BTreeMap::new(),
|
||||
};
|
||||
|
||||
unsafe { fs.reset_allocator()? };
|
||||
|
||||
if cleanup {
|
||||
fs.cleanup()?
|
||||
}
|
||||
|
||||
return Ok(fs);
|
||||
}
|
||||
|
||||
Err(Error::new(ENOENT))
|
||||
}
|
||||
|
||||
/// Create a file system on a disk
|
||||
#[cfg(feature = "std")]
|
||||
pub fn create(
|
||||
disk: D,
|
||||
password_opt: Option<&[u8]>,
|
||||
ctime: u64,
|
||||
ctime_nsec: u32,
|
||||
) -> Result<Self> {
|
||||
Self::create_reserved(disk, password_opt, &[], ctime, ctime_nsec)
|
||||
}
|
||||
|
||||
/// Create a file system on a disk, with reserved data at the beginning
|
||||
/// Reserved data will be zero padded up to the nearest block
|
||||
/// We need to pass ctime and ctime_nsec in order to initialize the unix timestamps
|
||||
#[cfg(feature = "std")]
|
||||
pub fn create_reserved(
|
||||
mut disk: D,
|
||||
password_opt: Option<&[u8]>,
|
||||
reserved: &[u8],
|
||||
ctime: u64,
|
||||
ctime_nsec: u32,
|
||||
) -> Result<Self> {
|
||||
let disk_size = disk.size()?;
|
||||
let disk_blocks = disk_size / BLOCK_SIZE;
|
||||
let block_offset = (reserved.len() as u64).div_ceil(BLOCK_SIZE);
|
||||
if disk_blocks < (block_offset + HEADER_RING + 4) {
|
||||
return Err(Error::new(syscall::error::ENOSPC));
|
||||
}
|
||||
let fs_blocks = disk_blocks - block_offset;
|
||||
|
||||
// Fill reserved data, pad with zeroes
|
||||
for block in 0..block_offset as usize {
|
||||
let mut data = [0; BLOCK_SIZE as usize];
|
||||
|
||||
let mut i = 0;
|
||||
while i < data.len() && block * BLOCK_SIZE as usize + i < reserved.len() {
|
||||
data[i] = reserved[block * BLOCK_SIZE as usize + i];
|
||||
i += 1;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
disk.write_at(block as u64, &data)?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut header = Header::new(fs_blocks * BLOCK_SIZE);
|
||||
|
||||
let cipher_opt = match password_opt {
|
||||
Some(password) => {
|
||||
//TODO: handle errors
|
||||
header.key_slots[0] = KeySlot::new(
|
||||
password,
|
||||
Salt::new().unwrap(),
|
||||
(Key::new().unwrap(), Key::new().unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
Some(header.key_slots[0].cipher(password).unwrap())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let mut fs = FileSystem {
|
||||
disk,
|
||||
block: block_offset,
|
||||
header,
|
||||
allocator: Allocator::default(),
|
||||
cipher_opt,
|
||||
compress_cache: compress_cache(),
|
||||
node_usages: BTreeMap::new(),
|
||||
};
|
||||
|
||||
// Write header generation zero
|
||||
let count = unsafe { fs.disk.write_at(fs.block, &fs.header)? };
|
||||
if count != core::mem::size_of_val(&fs.header) {
|
||||
// Wrote wrong number of bytes
|
||||
#[cfg(feature = "log")]
|
||||
log::error!("CREATE: WRONG NUMBER OF BYTES");
|
||||
return Err(Error::new(syscall::error::EIO));
|
||||
}
|
||||
|
||||
// Set tree and alloc pointers and write header generation one
|
||||
fs.tx(|tx| unsafe {
|
||||
let tree = BlockData::new(
|
||||
BlockAddr::new(HEADER_RING + 1, BlockMeta::default()),
|
||||
TreeList::empty(BlockLevel::default()).unwrap(),
|
||||
);
|
||||
|
||||
let mut alloc = BlockData::new(
|
||||
BlockAddr::new(HEADER_RING + 2, BlockMeta::default()),
|
||||
AllocList::empty(BlockLevel::default()).unwrap(),
|
||||
);
|
||||
|
||||
let alloc_free = fs_blocks - (HEADER_RING + 4);
|
||||
alloc.data_mut().entries[0] = AllocEntry::new(HEADER_RING + 4, alloc_free as i64);
|
||||
|
||||
tx.header.tree = tx.write_block(tree)?;
|
||||
tx.header.alloc = tx.write_block(alloc)?;
|
||||
tx.header_changed = true;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
unsafe {
|
||||
fs.reset_allocator()?;
|
||||
}
|
||||
|
||||
fs.tx(|tx| unsafe {
|
||||
let mut root = BlockData::new(
|
||||
BlockAddr::new(HEADER_RING + 3, BlockMeta::default()),
|
||||
Node::new(Node::MODE_DIR | 0o755, 0, 0, ctime, ctime_nsec),
|
||||
);
|
||||
root.data_mut().set_links(1);
|
||||
let root_ptr = tx.write_block(root)?;
|
||||
assert_eq!(tx.insert_tree(root_ptr)?.id(), 1);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
fs.cleanup()?;
|
||||
|
||||
Ok(fs)
|
||||
}
|
||||
|
||||
/// Release unused nodes and squash allocation log, happens on mount (with cleanup) and unmount
|
||||
pub fn cleanup(&mut self) -> Result<()> {
|
||||
let mut tx = Transaction::new(self);
|
||||
tx.release_unused_nodes()?;
|
||||
tx.commit(true)
|
||||
}
|
||||
|
||||
/// start a filesystem transaction, required for making any changes
|
||||
pub fn tx<F: FnOnce(&mut Transaction<D>) -> Result<T>, T>(&mut self, f: F) -> Result<T> {
|
||||
let mut tx = Transaction::new(self);
|
||||
let t = f(&mut tx)?;
|
||||
tx.commit(false)?;
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
pub fn allocator(&self) -> &Allocator {
|
||||
&self.allocator
|
||||
}
|
||||
|
||||
/// Unsafe as it can corrupt the filesystem
|
||||
pub unsafe fn allocator_mut(&mut self) -> &mut Allocator {
|
||||
&mut self.allocator
|
||||
}
|
||||
|
||||
/// Reset allocator to state stored on disk
|
||||
///
|
||||
/// # Safety
|
||||
/// Unsafe, it must only be called when opening the filesystem
|
||||
unsafe fn reset_allocator(&mut self) -> Result<()> {
|
||||
self.allocator = Allocator::default();
|
||||
|
||||
// To avoid having to update all prior alloc blocks, there is only a previous pointer
|
||||
// This means we need to roll back all allocations. Currently we do this by reading the
|
||||
// alloc log into a buffer to reverse it.
|
||||
let mut allocs = VecDeque::new();
|
||||
self.tx(|tx| {
|
||||
let mut alloc_ptr = tx.header.alloc;
|
||||
while !alloc_ptr.is_null() {
|
||||
let alloc = tx.read_block(alloc_ptr)?;
|
||||
alloc_ptr = alloc.data().prev;
|
||||
allocs.push_front(alloc);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
for alloc in allocs {
|
||||
for entry in alloc.data().entries.iter() {
|
||||
let index = entry.index();
|
||||
let count = entry.count();
|
||||
if count < 0 {
|
||||
for i in 0..-count {
|
||||
//TODO: replace assert with error?
|
||||
let addr = BlockAddr::new(index + i as u64, BlockMeta::default());
|
||||
assert_eq!(self.allocator.allocate_exact(addr), Some(addr));
|
||||
}
|
||||
} else {
|
||||
for i in 0..count {
|
||||
let addr = BlockAddr::new(index + i as u64, BlockMeta::default());
|
||||
self.allocator.deallocate(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt(&mut self, data: &mut [u8], addr: BlockAddr) -> bool {
|
||||
if let Some(ref cipher) = self.cipher_opt {
|
||||
cipher.decrypt_area(
|
||||
data,
|
||||
BLOCK_SIZE as usize,
|
||||
addr.index().into(),
|
||||
get_tweak_default,
|
||||
);
|
||||
true
|
||||
} else {
|
||||
// Do nothing if encryption is disabled
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encrypt(&mut self, data: &mut [u8], addr: BlockAddr) -> bool {
|
||||
if let Some(ref cipher) = self.cipher_opt {
|
||||
cipher.encrypt_area(
|
||||
data,
|
||||
BLOCK_SIZE as usize,
|
||||
addr.index().into(),
|
||||
get_tweak_default,
|
||||
);
|
||||
true
|
||||
} else {
|
||||
// Do nothing if encryption is disabled
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
use core::ops::{Deref, DerefMut};
|
||||
use core::{fmt, mem, slice};
|
||||
use endian_num::Le;
|
||||
|
||||
use aes::Aes128;
|
||||
use xts_mode::{get_tweak_default, Xts128};
|
||||
|
||||
use crate::{AllocList, BlockPtr, KeySlot, ReleaseList, Tree, BLOCK_SIZE, SIGNATURE, VERSION};
|
||||
|
||||
pub const HEADER_RING: u64 = 256;
|
||||
|
||||
/// The header of the filesystem
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C, packed)]
|
||||
pub struct Header {
|
||||
/// Signature, should be SIGNATURE
|
||||
pub signature: [u8; 8],
|
||||
/// Version, should be VERSION
|
||||
pub version: Le<u64>,
|
||||
/// Disk ID, a 128-bit unique identifier
|
||||
pub uuid: [u8; 16],
|
||||
/// Disk size, in number of BLOCK_SIZE sectors
|
||||
pub size: Le<u64>,
|
||||
/// Generation of header
|
||||
pub generation: Le<u64>,
|
||||
/// Block of first tree node
|
||||
pub tree: BlockPtr<Tree>,
|
||||
/// Block of last alloc node
|
||||
pub alloc: BlockPtr<AllocList>,
|
||||
/// Key slots
|
||||
pub key_slots: [KeySlot; 64],
|
||||
/// Nodes pending release, may be null
|
||||
pub release: BlockPtr<ReleaseList>,
|
||||
/// Padding
|
||||
pub padding: [u8; BLOCK_SIZE as usize - 3192],
|
||||
/// encrypted hash of header data without hash, set to hash and padded if disk is not encrypted
|
||||
pub encrypted_hash: [u8; 16],
|
||||
/// hash of header data without hash
|
||||
pub hash: Le<u64>,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
#[cfg(feature = "std")]
|
||||
pub fn new(size: u64) -> Header {
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
let mut header = Header {
|
||||
signature: *SIGNATURE,
|
||||
version: VERSION.into(),
|
||||
uuid: *uuid.as_bytes(),
|
||||
size: size.into(),
|
||||
..Default::default()
|
||||
};
|
||||
header.update_hash(None);
|
||||
header
|
||||
}
|
||||
|
||||
pub fn valid(&self) -> bool {
|
||||
if &self.signature != SIGNATURE {
|
||||
// Signature does not match
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.version.to_ne() != VERSION {
|
||||
// Version does not match
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.hash.to_ne() != self.create_hash() {
|
||||
// Hash does not match
|
||||
return false;
|
||||
}
|
||||
|
||||
// All tests passed, header is valid
|
||||
true
|
||||
}
|
||||
|
||||
pub fn uuid(&self) -> [u8; 16] {
|
||||
self.uuid
|
||||
}
|
||||
|
||||
pub fn size(&self) -> u64 {
|
||||
self.size.to_ne()
|
||||
}
|
||||
|
||||
pub fn generation(&self) -> u64 {
|
||||
self.generation.to_ne()
|
||||
}
|
||||
|
||||
fn create_hash(&self) -> u64 {
|
||||
// Calculate part of header to hash (everything before the hashes)
|
||||
let end = mem::size_of_val(self)
|
||||
- mem::size_of_val(&{ self.hash })
|
||||
- mem::size_of_val(&{ self.encrypted_hash });
|
||||
seahash::hash(&self[..end])
|
||||
}
|
||||
|
||||
fn create_encrypted_hash(&self, cipher_opt: Option<&Xts128<Aes128>>) -> [u8; 16] {
|
||||
let mut encrypted_hash = [0; 16];
|
||||
for (i, b) in self.hash.to_le_bytes().iter().enumerate() {
|
||||
encrypted_hash[i] = *b;
|
||||
}
|
||||
if let Some(cipher) = cipher_opt {
|
||||
let mut block = aes::Block::from(encrypted_hash);
|
||||
cipher.encrypt_area(
|
||||
&mut block,
|
||||
BLOCK_SIZE as usize,
|
||||
self.generation().into(),
|
||||
get_tweak_default,
|
||||
);
|
||||
encrypted_hash = block.into();
|
||||
}
|
||||
encrypted_hash
|
||||
}
|
||||
|
||||
pub fn encrypted(&self) -> bool {
|
||||
(self.encrypted_hash) != self.create_encrypted_hash(None)
|
||||
}
|
||||
|
||||
pub fn cipher(&self, password: &[u8]) -> Option<Xts128<Aes128>> {
|
||||
let hash = self.create_encrypted_hash(None);
|
||||
for slot in self.key_slots.iter() {
|
||||
//TODO: handle errors
|
||||
let cipher = slot.cipher(password).unwrap();
|
||||
let mut block = aes::Block::from(self.encrypted_hash);
|
||||
cipher.decrypt_area(
|
||||
&mut block,
|
||||
BLOCK_SIZE as usize,
|
||||
self.generation().into(),
|
||||
get_tweak_default,
|
||||
);
|
||||
if block == aes::Block::from(hash) {
|
||||
return Some(cipher);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn update_hash(&mut self, cipher_opt: Option<&Xts128<Aes128>>) {
|
||||
self.hash = self.create_hash().into();
|
||||
// Make sure to do this second, it relies on the hash being up to date
|
||||
self.encrypted_hash = self.create_encrypted_hash(cipher_opt);
|
||||
}
|
||||
|
||||
pub fn update(&mut self, cipher_opt: Option<&Xts128<Aes128>>) -> u64 {
|
||||
let mut generation = self.generation();
|
||||
generation += 1;
|
||||
self.generation = generation.into();
|
||||
self.update_hash(cipher_opt);
|
||||
generation
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Header {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
signature: [0; 8],
|
||||
version: 0.into(),
|
||||
uuid: [0; 16],
|
||||
size: 0.into(),
|
||||
generation: 0.into(),
|
||||
tree: BlockPtr::<Tree>::default(),
|
||||
alloc: BlockPtr::<AllocList>::default(),
|
||||
key_slots: [KeySlot::default(); 64],
|
||||
release: BlockPtr::<ReleaseList>::default(),
|
||||
padding: [0; BLOCK_SIZE as usize - 3192],
|
||||
encrypted_hash: [0; 16],
|
||||
hash: 0.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Header {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let signature = self.signature;
|
||||
let version = self.version;
|
||||
let uuid = self.uuid;
|
||||
let size = self.size;
|
||||
let generation = self.generation;
|
||||
let tree = self.tree;
|
||||
let alloc = self.alloc;
|
||||
let release = self.release;
|
||||
let hash = self.hash;
|
||||
f.debug_struct("Header")
|
||||
.field("signature", &signature)
|
||||
.field("version", &version)
|
||||
.field("uuid", &uuid)
|
||||
.field("size", &size)
|
||||
.field("generation", &generation)
|
||||
.field("tree", &tree)
|
||||
.field("alloc", &alloc)
|
||||
.field("release", &release)
|
||||
.field("hash", &hash)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Header {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts(self as *const Header as *const u8, mem::size_of::<Header>())
|
||||
as &[u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Header {
|
||||
fn deref_mut(&mut self) -> &mut [u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts_mut(self as *mut Header as *mut u8, mem::size_of::<Header>())
|
||||
as &mut [u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_not_valid_test() {
|
||||
assert_eq!(Header::default().valid(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_size_test() {
|
||||
assert_eq!(mem::size_of::<Header>(), BLOCK_SIZE as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_hash_test() {
|
||||
let mut header = Header::default();
|
||||
assert_eq!(header.create_hash(), 0xe81ffcb86026ff96);
|
||||
header.update_hash(None);
|
||||
assert_eq!(header.hash.to_ne(), 0xe81ffcb86026ff96);
|
||||
assert_eq!(
|
||||
header.encrypted_hash,
|
||||
[0x96, 0xff, 0x26, 0x60, 0xb8, 0xfc, 0x1f, 0xe8, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn header_valid_test() {
|
||||
assert_eq!(Header::new(0).valid(), true);
|
||||
}
|
||||
+691
@@ -0,0 +1,691 @@
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::{fmt, mem, ops, slice};
|
||||
use endian_num::Le;
|
||||
use syscall::error::{Error, Result, EEXIST, EIO};
|
||||
|
||||
use crate::{
|
||||
BlockLevel, BlockPtr, BlockRaw, BlockTrait, DirEntry, DirList, BLOCK_SIZE, RECORD_LEVEL,
|
||||
};
|
||||
|
||||
pub const HTREE_IDX_ENTRIES: usize = BLOCK_SIZE as usize / mem::size_of::<HTreePtr<BlockRaw>>();
|
||||
const HTREE_IDX_PADDING: usize =
|
||||
BLOCK_SIZE as usize - mem::size_of::<[HTreePtr<BlockRaw>; HTREE_IDX_ENTRIES]>();
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
#[repr(C, packed)]
|
||||
pub struct HTreeHash(Le<u32>);
|
||||
|
||||
impl HTreeHash {
|
||||
// Create a MAX constant populated iwth the maximum value of Le<u32> minus 1
|
||||
pub const MAX: HTreeHash = HTreeHash(Le(u32::MAX - 1));
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub fn from_name(name: &str) -> Self {
|
||||
let hash = seahash::hash(name.as_bytes()) as u32;
|
||||
// Don't allow the default hash value to be calculated for a real hash
|
||||
if hash == u32::MAX {
|
||||
return Self::MAX;
|
||||
}
|
||||
Self(hash.into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn from_name(name: &str) -> Self {
|
||||
// Allow overriding the hashing function to something easily controled for testing.
|
||||
let hash = if let Some(pos) = name.rfind("__") {
|
||||
let number_str = &name[pos + 2..];
|
||||
number_str.parse::<u32>().unwrap()
|
||||
} else {
|
||||
seahash::hash(name.as_bytes()) as u32
|
||||
};
|
||||
|
||||
// Don't allow the default hash value to be calculated for a real hash
|
||||
if hash == u32::MAX {
|
||||
return Self::MAX;
|
||||
}
|
||||
Self(hash.into())
|
||||
}
|
||||
|
||||
/// Returns the maximum of two `HTreeHash` values, ignoring the default hash value.
|
||||
pub fn max_ignoring_default(&self, other: Self) -> Self {
|
||||
let default = HTreeHash::default();
|
||||
if *self == default {
|
||||
return other;
|
||||
}
|
||||
if other == default {
|
||||
return *self;
|
||||
}
|
||||
if *self > other {
|
||||
*self
|
||||
} else {
|
||||
other
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_max(dir_list: &DirList) -> Option<HTreeHash> {
|
||||
let mut max_hash = HTreeHash::default();
|
||||
dir_list.for_each_entry(|_ptr_bytes, name_bytes| {
|
||||
let name = String::from_utf8_lossy(name_bytes);
|
||||
let hash = HTreeHash::from_name(name.as_ref());
|
||||
max_hash = max_hash.max_ignoring_default(hash);
|
||||
});
|
||||
|
||||
if max_hash == HTreeHash::default() {
|
||||
None
|
||||
} else {
|
||||
Some(max_hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HTreeHash {
|
||||
/// The default hash value is the maximum possible value to push it to the end of the list when sorting.
|
||||
fn default() -> Self {
|
||||
Self(u32::MAX.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
pub struct HTreePtr<T> {
|
||||
pub htree_hash: HTreeHash,
|
||||
pub ptr: BlockPtr<T>,
|
||||
}
|
||||
|
||||
impl<T> HTreePtr<T> {
|
||||
pub fn new(htree_hash: HTreeHash, ptr: BlockPtr<T>) -> Self {
|
||||
Self { htree_hash, ptr }
|
||||
}
|
||||
|
||||
/// Cast HTreePtr to another type
|
||||
///
|
||||
/// # Safety
|
||||
/// Unsafe because it can be used to transmute types
|
||||
pub unsafe fn cast<U>(self) -> HTreePtr<U> {
|
||||
HTreePtr {
|
||||
htree_hash: self.htree_hash,
|
||||
ptr: self.ptr.cast(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> HTreePtr<T> {
|
||||
pub fn is_null(&self) -> bool {
|
||||
self.ptr.is_null()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for HTreePtr<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for HTreePtr<T> {}
|
||||
|
||||
impl<T> Default for HTreePtr<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
htree_hash: HTreeHash::default(),
|
||||
ptr: BlockPtr::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for HTreePtr<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let htree_hash = self.htree_hash;
|
||||
let ptr = self.ptr;
|
||||
f.debug_struct("BlockPtr")
|
||||
.field("htree_hash", &htree_hash)
|
||||
.field("ptr", &ptr)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
pub struct HTreeNode<T> {
|
||||
pub ptrs: [HTreePtr<T>; HTREE_IDX_ENTRIES],
|
||||
padding: [u8; HTREE_IDX_PADDING],
|
||||
}
|
||||
|
||||
impl<T> HTreeNode<T> {
|
||||
pub fn find_max_htree_hash(&self) -> Option<HTreeHash> {
|
||||
let mut hash = HTreeHash::default();
|
||||
for entry in self.ptrs.iter() {
|
||||
hash = hash.max_ignoring_default(entry.htree_hash);
|
||||
}
|
||||
|
||||
if hash != HTreeHash::default() {
|
||||
Some(hash)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_ptrs_for_read(
|
||||
&self,
|
||||
htree_hash: HTreeHash,
|
||||
) -> impl Iterator<Item = (usize, &HTreePtr<T>)> {
|
||||
let mut last_hash = HTreeHash(0.into());
|
||||
self.ptrs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(move |(_idx, entry)| entry.htree_hash >= htree_hash)
|
||||
.take_while(move |(_idx, entry)| {
|
||||
let should_take = !entry.is_null() && last_hash <= htree_hash;
|
||||
last_hash = entry.htree_hash;
|
||||
should_take
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T> BlockTrait for HTreeNode<T> {
|
||||
fn empty(level: BlockLevel) -> Option<Self> {
|
||||
if level.0 <= RECORD_LEVEL {
|
||||
Some(Self {
|
||||
ptrs: [HTreePtr::default(); HTREE_IDX_ENTRIES],
|
||||
padding: [0; HTREE_IDX_PADDING],
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ops::Deref for HTreeNode<T> {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts(
|
||||
self as *const HTreeNode<T> as *const u8,
|
||||
mem::size_of::<HTreeNode<T>>(),
|
||||
) as &[u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ops::DerefMut for HTreeNode<T> {
|
||||
fn deref_mut(&mut self) -> &mut [u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts_mut(
|
||||
self as *mut HTreeNode<T> as *mut u8,
|
||||
mem::size_of::<HTreeNode<T>>(),
|
||||
) as &mut [u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_inner_node<T>(
|
||||
parent: &mut HTreeNode<T>,
|
||||
new_ptr: HTreePtr<T>,
|
||||
) -> Result<Option<(HTreeHash, HTreeNode<T>)>> {
|
||||
// Update the input htree parameters in place
|
||||
for ptr in parent.ptrs.iter_mut() {
|
||||
if ptr.is_null() {
|
||||
*ptr = new_ptr;
|
||||
parent.ptrs.sort_by(|a, b| a.htree_hash.cmp(&b.htree_hash));
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
// The parent is full. We need to split it into two by half, ordered by the htree hash.
|
||||
let mut all_ptrs = Vec::with_capacity(parent.ptrs.len() + 1);
|
||||
for ptr in parent.ptrs.iter() {
|
||||
all_ptrs.push(*ptr);
|
||||
}
|
||||
all_ptrs.push(new_ptr);
|
||||
all_ptrs.sort_by(|a, b| a.htree_hash.cmp(&b.htree_hash));
|
||||
let half_idx = all_ptrs.len() / 2;
|
||||
|
||||
// Find if there are duplicate name hashes on the boundary of where we want to split
|
||||
let half_name_hash = all_ptrs[half_idx].htree_hash;
|
||||
let mut first_idx = half_idx;
|
||||
let mut last_idx = half_idx;
|
||||
for (i, ptr) in all_ptrs.iter().enumerate() {
|
||||
if ptr.htree_hash == half_name_hash {
|
||||
if i < first_idx {
|
||||
first_idx = i;
|
||||
}
|
||||
if i > last_idx {
|
||||
last_idx = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Split the entries_with_name_hash list at the index that minimizes the number of entries in each list while keeping the duplicate name hashes together
|
||||
let split = if (half_idx - first_idx) < (last_idx - half_idx) {
|
||||
first_idx
|
||||
} else {
|
||||
last_idx
|
||||
};
|
||||
|
||||
let (ptrs1, ptrs2) = all_ptrs.split_at(split);
|
||||
|
||||
// Update the existing parent with the first half of the entries
|
||||
let mut htree_idx1 = HTreeNode::empty(BlockLevel::default()).ok_or(Error::new(EIO))?;
|
||||
htree_idx1.ptrs[..ptrs1.len()].copy_from_slice(ptrs1);
|
||||
let _ = mem::replace(parent, htree_idx1);
|
||||
|
||||
// Return the second half as a new sibling parent
|
||||
let mut htree_idx2 = HTreeNode::empty(BlockLevel::default()).ok_or(Error::new(EIO))?;
|
||||
htree_idx2.ptrs[..ptrs2.len()].copy_from_slice(ptrs2);
|
||||
|
||||
let htree_hash2 = ptrs2[ptrs2.len() - 1].htree_hash;
|
||||
Ok(Some((htree_hash2, htree_idx2)))
|
||||
}
|
||||
|
||||
pub fn add_dir_entry(
|
||||
dir_list: &mut DirList,
|
||||
htree_hash: &mut HTreeHash,
|
||||
dirent: DirEntry,
|
||||
) -> Result<Option<(HTreeHash, DirList)>> {
|
||||
if let Some(name) = dirent.name() {
|
||||
if dir_list.find_entry(name).is_some() {
|
||||
return Err(Error::new(EEXIST));
|
||||
}
|
||||
}
|
||||
|
||||
// Update the input htree parameters in place
|
||||
let name = dirent.name().ok_or(Error::new(EIO))?;
|
||||
if dir_list.append(&dirent) {
|
||||
*htree_hash = HTreeHash::from_name(name).max_ignoring_default(*htree_hash);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// The dir_list is full. We need to split it into two dir_lists by half, ordered by the name hash.
|
||||
let mut entries_with_name_hash = Vec::with_capacity(dir_list.entry_count() + 1);
|
||||
for entry in dir_list.entries() {
|
||||
entries_with_name_hash.push((
|
||||
HTreeHash::from_name(entry.name().ok_or(Error::new(EIO))?),
|
||||
entry,
|
||||
));
|
||||
}
|
||||
entries_with_name_hash.push((HTreeHash::from_name(dirent.name().unwrap()), dirent));
|
||||
entries_with_name_hash.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
let half = entries_with_name_hash.len() / 2;
|
||||
let half_name_hash = entries_with_name_hash[half].0;
|
||||
|
||||
// Find if there are duplicate name hashes on the boundary of where we want to split
|
||||
let mut first_idx = half;
|
||||
let mut last_idx = half;
|
||||
for (i, (name_hash, _)) in entries_with_name_hash.iter().enumerate() {
|
||||
if *name_hash == half_name_hash {
|
||||
if i < first_idx {
|
||||
first_idx = i;
|
||||
}
|
||||
if i > last_idx {
|
||||
last_idx = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
last_idx += 1;
|
||||
|
||||
// Split the entries_with_name_hash list at the index that minimizes the number of entries in each list while keeping the duplicate name hashes together
|
||||
let split = if (half - first_idx) < (last_idx - half) {
|
||||
first_idx
|
||||
} else {
|
||||
last_idx
|
||||
};
|
||||
let split = split.max(1);
|
||||
|
||||
let sorted_entries = entries_with_name_hash
|
||||
.iter()
|
||||
.map(|(_, entry)| *entry)
|
||||
.collect::<Vec<DirEntry>>();
|
||||
|
||||
let (entries1, entries2) = sorted_entries.split_at(split);
|
||||
|
||||
// Update the existing dir_list with the first half of the entries
|
||||
let mut new_dir_list = DirList::empty(BlockLevel::default()).ok_or(Error::new(EIO))?;
|
||||
for entry in entries1.iter() {
|
||||
new_dir_list.append(entry);
|
||||
}
|
||||
let _ = mem::replace(dir_list, new_dir_list);
|
||||
*htree_hash = entries_with_name_hash[entries1.len() - 1].0;
|
||||
|
||||
// Return the second half of the entries as a new dir_list
|
||||
let mut new_dir_list = DirList::empty(BlockLevel::default()).ok_or(Error::new(EIO))?;
|
||||
for entry in entries2.iter() {
|
||||
new_dir_list.append(entry);
|
||||
}
|
||||
let new_name_hash = entries_with_name_hash[entries_with_name_hash.len() - 1].0;
|
||||
Ok(Some((new_name_hash, new_dir_list)))
|
||||
}
|
||||
|
||||
//
|
||||
// MARK: Unit Tests
|
||||
//
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::alloc::string::ToString;
|
||||
use crate::TreePtr;
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
|
||||
#[test]
|
||||
fn htree_ptr_size_test() {
|
||||
assert_eq!(mem::size_of::<HTreePtr<BlockRaw>>(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn htree_node_size_test() {
|
||||
assert_eq!(mem::size_of::<HTreeNode<BlockRaw>>(), BLOCK_SIZE as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn htree_hash_max_test() {
|
||||
assert_eq!(HTreeHash::MAX, HTreeHash((u32::MAX - 1).into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn htree_hash_max_ignoring_default_test() {
|
||||
let default = HTreeHash::default();
|
||||
let hash1 = HTreeHash(0.into());
|
||||
let hash2 = HTreeHash(1.into());
|
||||
|
||||
assert_eq!(hash1.max_ignoring_default(default), hash1);
|
||||
assert_eq!(default.max_ignoring_default(hash1), hash1);
|
||||
assert_eq!(hash1.max_ignoring_default(hash2), hash2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn htree_node_find_max_htree_hash() {
|
||||
// In practice, the HTreeHash values should always be in sorted order
|
||||
let mut htree_node: HTreeNode<String> = HTreeNode::empty(BlockLevel::default()).unwrap();
|
||||
htree_node.ptrs[0] = HTreePtr::new(HTreeHash(0.into()), BlockPtr::marker(0));
|
||||
htree_node.ptrs[1] = HTreePtr::new(HTreeHash(1.into()), BlockPtr::marker(0));
|
||||
htree_node.ptrs[2] = HTreePtr::new(HTreeHash(2.into()), BlockPtr::marker(0));
|
||||
|
||||
assert_eq!(
|
||||
htree_node.find_max_htree_hash().unwrap(),
|
||||
HTreeHash(2.into())
|
||||
);
|
||||
|
||||
htree_node.ptrs[2] = HTreePtr::default();
|
||||
assert_eq!(
|
||||
htree_node.find_max_htree_hash().unwrap(),
|
||||
HTreeHash(1.into())
|
||||
);
|
||||
|
||||
htree_node.ptrs[1] = HTreePtr::default();
|
||||
assert_eq!(
|
||||
htree_node.find_max_htree_hash().unwrap(),
|
||||
HTreeHash(0.into())
|
||||
);
|
||||
|
||||
htree_node.ptrs[0] = HTreePtr::default();
|
||||
assert!(htree_node.find_max_htree_hash().is_none());
|
||||
|
||||
// For thoroughness, test with HTreeHash out of order
|
||||
htree_node.ptrs[2] = HTreePtr::new(HTreeHash(4.into()), BlockPtr::marker(0));
|
||||
htree_node.ptrs[4] = HTreePtr::new(HTreeHash(6.into()), BlockPtr::marker(0));
|
||||
htree_node.ptrs[6] = HTreePtr::new(HTreeHash(2.into()), BlockPtr::marker(0));
|
||||
assert_eq!(
|
||||
htree_node.find_max_htree_hash().unwrap(),
|
||||
HTreeHash(6.into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn htree_node_find_for_read() {
|
||||
let mut htree_node: HTreeNode<String> = HTreeNode::empty(BlockLevel::default()).unwrap();
|
||||
htree_node.ptrs[0] = HTreePtr::new(HTreeHash(0.into()), BlockPtr::marker(0));
|
||||
htree_node.ptrs[1] = HTreePtr::new(HTreeHash(1.into()), BlockPtr::marker(0));
|
||||
htree_node.ptrs[2] = HTreePtr::new(HTreeHash(2.into()), BlockPtr::marker(0));
|
||||
htree_node.ptrs[3] = HTreePtr::new(HTreeHash(2.into()), BlockPtr::marker(0));
|
||||
htree_node.ptrs[4] = HTreePtr::new(HTreeHash(3.into()), BlockPtr::marker(0));
|
||||
htree_node.ptrs[5] = HTreePtr::new(HTreeHash(3.into()), BlockPtr::marker(0));
|
||||
htree_node.ptrs[6] = HTreePtr::new(HTreeHash(5.into()), BlockPtr::marker(0));
|
||||
htree_node.ptrs[7] = HTreePtr::new(HTreeHash(6.into()), BlockPtr::marker(0));
|
||||
|
||||
// Confirm that a hash that does not exist, but is less than an existing hash results in a single entry
|
||||
let mut iter = htree_node.find_ptrs_for_read(HTreeHash(4.into()));
|
||||
let mut val = iter.next().unwrap();
|
||||
assert_eq!(val.0, 6);
|
||||
assert_eq!(val.1.htree_hash, HTreeHash(5.into()));
|
||||
assert!(iter.next().is_none());
|
||||
|
||||
// Confirm that a hash that equals an existing hash results in the match and one following entry
|
||||
let mut iter = htree_node.find_ptrs_for_read(HTreeHash(1.into()));
|
||||
val = iter.next().unwrap();
|
||||
assert_eq!(val.0, 1);
|
||||
assert_eq!(val.1.htree_hash, HTreeHash(1.into()));
|
||||
val = iter.next().unwrap();
|
||||
assert_eq!(val.0, 2);
|
||||
assert_eq!(val.1.htree_hash, HTreeHash(2.into()));
|
||||
assert!(iter.next().is_none());
|
||||
|
||||
// Confirm that multiple exact hash matches are all returned plus the next entry
|
||||
let mut iter = htree_node.find_ptrs_for_read(HTreeHash(2.into()));
|
||||
val = iter.next().unwrap();
|
||||
assert_eq!(val.0, 2);
|
||||
assert_eq!(val.1.htree_hash, HTreeHash(2.into()));
|
||||
val = iter.next().unwrap();
|
||||
assert_eq!(val.0, 3);
|
||||
assert_eq!(val.1.htree_hash, HTreeHash(2.into()));
|
||||
val = iter.next().unwrap();
|
||||
assert_eq!(val.0, 4);
|
||||
assert_eq!(val.1.htree_hash, HTreeHash(3.into()));
|
||||
assert!(iter.next().is_none());
|
||||
|
||||
// Confirm that if the last entry matches and the next entry is null, only the match is returned
|
||||
let mut iter = htree_node.find_ptrs_for_read(HTreeHash(6.into()));
|
||||
val = iter.next().unwrap();
|
||||
assert_eq!(val.0, 7);
|
||||
assert_eq!(val.1.htree_hash, HTreeHash(6.into()));
|
||||
assert!(iter.next().is_none());
|
||||
|
||||
// Confirm that if a hash that is larger than any existing entries, then no entries are returned
|
||||
let mut iter = htree_node.find_ptrs_for_read(HTreeHash(7.into()));
|
||||
assert!(iter.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_dir_entry_exists_test() {
|
||||
let mut dir_list = DirList::empty(BlockLevel::default()).unwrap();
|
||||
let mut htree_hash = HTreeHash::default();
|
||||
let dirent = DirEntry::new(TreePtr::new(123), "test");
|
||||
let new_sibling = add_dir_entry(&mut dir_list, &mut htree_hash, dirent).unwrap();
|
||||
assert!(new_sibling.is_none());
|
||||
assert_eq!(htree_hash, HTreeHash::from_name("test"));
|
||||
assert_eq!(dir_list.entries().next().unwrap().name(), Some("test"));
|
||||
|
||||
// Add the same entry again, and it should fail with an appropriate IO error
|
||||
let dirent = DirEntry::new(TreePtr::new(123), "test");
|
||||
let error_expected = add_dir_entry(&mut dir_list, &mut htree_hash, dirent);
|
||||
assert!(error_expected.is_err());
|
||||
assert_eq!(error_expected.err().unwrap().errno, EEXIST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_dir_entry_many_test() {
|
||||
let mut dir_list = DirList::empty(BlockLevel::default()).unwrap();
|
||||
let mut htree_hash = HTreeHash::default();
|
||||
let total_count = 16;
|
||||
|
||||
// Fill up the dir_list
|
||||
for i in 0..total_count {
|
||||
let v: usize = i % 10;
|
||||
let dirent = DirEntry::new(TreePtr::new(123), format!("test{v}_{i:0244}").as_str());
|
||||
let new_sibling = add_dir_entry(&mut dir_list, &mut htree_hash, dirent).unwrap();
|
||||
assert!(new_sibling.is_none());
|
||||
}
|
||||
|
||||
// The maximum htree_hash should be retained
|
||||
let max_tree_hash =
|
||||
dir_list
|
||||
.entries()
|
||||
.enumerate()
|
||||
.fold(HTreeHash::default(), |max, (i, _)| {
|
||||
let v = i % 10;
|
||||
let hash = HTreeHash::from_name(format!("test{v}_{i:0244}").as_str());
|
||||
max.max_ignoring_default(hash)
|
||||
});
|
||||
assert_eq!(htree_hash, max_tree_hash);
|
||||
|
||||
// Confirm all the entries exist. Note they happen to be in insert order
|
||||
for (i, entry) in dir_list.entries().enumerate() {
|
||||
let v = i % 10;
|
||||
assert_eq!(entry.name(), Some(format!("test{v}_{i:0244}").as_str()));
|
||||
}
|
||||
|
||||
// Test a split by adding one more entry
|
||||
let dirent = DirEntry::new(TreePtr::new(123), "test_split");
|
||||
let new_sibling = add_dir_entry(&mut dir_list, &mut htree_hash, dirent).unwrap();
|
||||
let (new_sibling_htree_hash, new_sibling_dir_list) =
|
||||
new_sibling.expect("new_sibling should be created");
|
||||
// assert!(new_sibling_dir_list.entries.len() );
|
||||
assert!(new_sibling_htree_hash > htree_hash);
|
||||
|
||||
// The htree_hash should be less than the minimum htree_hash in new_sibling_dir_list
|
||||
let new_sibling_min_htree_hash = new_sibling_dir_list
|
||||
.entries()
|
||||
.filter(|entry| !entry.node_ptr().is_null())
|
||||
.fold(HTreeHash::default(), |min, entry| {
|
||||
let hash = HTreeHash::from_name(entry.name().unwrap());
|
||||
min.min(hash)
|
||||
});
|
||||
assert!(htree_hash < new_sibling_min_htree_hash);
|
||||
|
||||
// Confirm all the entries exist across both dir_lists
|
||||
let mut expected_names: Vec<String> = (0..total_count)
|
||||
.map(|i| {
|
||||
let v = i % 10;
|
||||
format!("test{v}_{i:0244}")
|
||||
})
|
||||
.collect();
|
||||
expected_names.push("test_split".to_string());
|
||||
expected_names.sort();
|
||||
|
||||
let mut dir_list_entry_count = 0;
|
||||
for entry in dir_list.entries() {
|
||||
dir_list_entry_count += 1;
|
||||
let name = entry.name().unwrap().to_string();
|
||||
let _ = expected_names.remove(expected_names.binary_search(&name).unwrap());
|
||||
}
|
||||
|
||||
let mut new_sibling_entry_count = 0;
|
||||
for entry in new_sibling_dir_list.entries() {
|
||||
new_sibling_entry_count += 1;
|
||||
let name = entry.name().unwrap().to_string();
|
||||
let _ = expected_names.remove(expected_names.binary_search(&name).unwrap());
|
||||
}
|
||||
assert!(expected_names.is_empty());
|
||||
|
||||
// Confirm that the split is in half
|
||||
assert!((dir_list_entry_count as i32 - new_sibling_entry_count).abs() <= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_inner_node_simple_test() {
|
||||
let mut htree_node: HTreeNode<_> = HTreeNode::empty(BlockLevel::default()).unwrap();
|
||||
let htree_ptr: HTreePtr<_> = HTreePtr::<BlockRaw> {
|
||||
htree_hash: HTreeHash::from_name("test"),
|
||||
ptr: BlockPtr::marker(0),
|
||||
};
|
||||
let new_sibling = add_inner_node(&mut htree_node, htree_ptr).unwrap();
|
||||
assert!(new_sibling.is_none());
|
||||
assert_eq!(htree_node.ptrs[0].htree_hash, HTreeHash::from_name("test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_inner_node_multiple_test() {
|
||||
let mut htree_node: HTreeNode<_> = HTreeNode::empty(BlockLevel::default()).unwrap();
|
||||
|
||||
for i in 0..HTREE_IDX_ENTRIES {
|
||||
let htree_ptr: HTreePtr<_> = HTreePtr::<BlockRaw> {
|
||||
htree_hash: HTreeHash(((100_000 + (i % 10) * 1000 + i) as u32).into()),
|
||||
ptr: BlockPtr::marker(0),
|
||||
};
|
||||
let new_sibling = add_inner_node(&mut htree_node, htree_ptr).unwrap();
|
||||
assert!(new_sibling.is_none());
|
||||
|
||||
// Confirm that the htree_ptrs are in sorted order at the start of the ptrs list
|
||||
let mut prev_hash = HTreeHash::default();
|
||||
let mut count = 0;
|
||||
for ptr in htree_node.ptrs.iter() {
|
||||
if ptr.is_null() {
|
||||
continue;
|
||||
}
|
||||
assert!(
|
||||
ptr.htree_hash.max_ignoring_default(prev_hash) == ptr.htree_hash,
|
||||
"index {i}: {:?} > {:?}",
|
||||
ptr.htree_hash,
|
||||
prev_hash
|
||||
);
|
||||
prev_hash = ptr.htree_hash;
|
||||
count += 1;
|
||||
}
|
||||
assert_eq!(count, i + 1);
|
||||
}
|
||||
|
||||
// Confirm all expected hashes are present
|
||||
let mut expected_hashes: Vec<u32> = (0..HTREE_IDX_ENTRIES)
|
||||
.map(|i| (100_000 + (i % 10) * 1000 + i) as u32)
|
||||
.collect();
|
||||
expected_hashes.sort();
|
||||
|
||||
for ptr in htree_node.ptrs.iter() {
|
||||
if ptr.is_null() {
|
||||
break;
|
||||
}
|
||||
let idx = expected_hashes
|
||||
.binary_search(&ptr.htree_hash.0.into())
|
||||
.unwrap();
|
||||
expected_hashes.remove(idx);
|
||||
}
|
||||
assert!(expected_hashes.is_empty());
|
||||
|
||||
// Force a split by adding one more entry
|
||||
let htree_ptr: HTreePtr<_> = HTreePtr::<BlockRaw> {
|
||||
htree_hash: HTreeHash(130_000.into()),
|
||||
ptr: BlockPtr::marker(0),
|
||||
};
|
||||
|
||||
let mut expected_hashes: Vec<u32> = (0..HTREE_IDX_ENTRIES)
|
||||
.map(|i| (100_000 + (i % 10) * 1000 + i) as u32)
|
||||
.collect();
|
||||
expected_hashes.push(130_000);
|
||||
expected_hashes.sort();
|
||||
|
||||
let new_sibling = add_inner_node(&mut htree_node, htree_ptr).unwrap();
|
||||
let new_sibling = new_sibling.expect("new_sibling should be created");
|
||||
|
||||
// Confirm all the entries exist across both htree_nodes
|
||||
let mut htree_node_entry_count = 0;
|
||||
for ptr in htree_node.ptrs.iter() {
|
||||
if ptr.ptr.is_null() {
|
||||
break;
|
||||
}
|
||||
htree_node_entry_count += 1;
|
||||
let idx = expected_hashes
|
||||
.binary_search(&ptr.htree_hash.0.into())
|
||||
.unwrap();
|
||||
expected_hashes.remove(idx);
|
||||
}
|
||||
|
||||
let mut new_sibling_entry_count = 0;
|
||||
for ptr in new_sibling.1.ptrs.iter() {
|
||||
if ptr.ptr.is_null() {
|
||||
break;
|
||||
}
|
||||
new_sibling_entry_count += 1;
|
||||
let idx = expected_hashes
|
||||
.binary_search(&ptr.htree_hash.0.into())
|
||||
.unwrap();
|
||||
expected_hashes.remove(idx);
|
||||
}
|
||||
assert!(
|
||||
expected_hashes.is_empty(),
|
||||
"expected_hashes should be empty, but had length {}: {:?}",
|
||||
expected_hashes.len(),
|
||||
expected_hashes
|
||||
);
|
||||
|
||||
// Confirm that the split is in half
|
||||
assert!((htree_node_entry_count as i32 - new_sibling_entry_count).abs() <= 1);
|
||||
}
|
||||
}
|
||||
@@ -1,855 +0,0 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::{bail, Result};
|
||||
use pkg::Library;
|
||||
use rand::{rngs::OsRng, TryRngCore};
|
||||
use redoxfs::{unmount_path, Disk, DiskIo, FileSystem, BLOCK_SIZE};
|
||||
use termion::input::TermRead;
|
||||
|
||||
use crate::config::file::FileConfig;
|
||||
use crate::config::package::PackageConfig;
|
||||
use crate::config::Config;
|
||||
use crate::disk_wrapper::DiskWrapper;
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::BTreeMap,
|
||||
env, fs,
|
||||
io::{self, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
rc::Rc,
|
||||
sync::mpsc::channel,
|
||||
thread,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
pub struct DiskOption<'a> {
|
||||
pub bootloader_bios: &'a [u8],
|
||||
pub bootloader_efi: &'a [u8],
|
||||
pub password_opt: Option<&'a [u8]>,
|
||||
pub efi_partition_size: Option<u32>, //MiB
|
||||
pub skip_partitions: bool,
|
||||
}
|
||||
|
||||
fn get_target() -> String {
|
||||
// TODO: Configurable from filesystem config?
|
||||
env::var("TARGET").unwrap_or(
|
||||
option_env!("TARGET").map_or("x86_64-unknown-redox".to_string(), |x| x.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Converts a password to a serialized argon2rs hash, understandable
|
||||
/// by redox_users. If the password is blank, the hash is blank.
|
||||
fn hash_password(password: &str) -> Result<String> {
|
||||
if !password.is_empty() {
|
||||
let salt = format!("{:X}", OsRng.try_next_u64()?);
|
||||
let config = argon2::Config::default();
|
||||
let hash = argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &config)?;
|
||||
Ok(hash)
|
||||
} else {
|
||||
Ok("".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn syscall_error(err: syscall::Error) -> io::Error {
|
||||
io::Error::from_raw_os_error(err.errno)
|
||||
}
|
||||
|
||||
/// Returns a password collected from the user (plaintext)
|
||||
pub fn prompt_password(prompt: &str, confirm_prompt: &str) -> Result<Option<String>> {
|
||||
let stdin = io::stdin();
|
||||
let mut stdin = stdin.lock();
|
||||
let stdout = io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
for i in 0..3 {
|
||||
print!("{}", prompt);
|
||||
let mut password = stdin.read_passwd(&mut stdout)?;
|
||||
if let Some(password) = password.as_mut() {
|
||||
*password = password.trim().to_string();
|
||||
}
|
||||
password.take_if(|s| s.is_empty());
|
||||
|
||||
if password.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
print!("\n{}", confirm_prompt);
|
||||
let confirm_password = stdin.read_passwd(&mut stdout)?;
|
||||
|
||||
// Note: Actually comparing two Option<String> values
|
||||
if confirm_password == password {
|
||||
return Ok(password);
|
||||
} else if i < 2 {
|
||||
eprintln!("passwords do not match, please try again");
|
||||
}
|
||||
}
|
||||
bail!("passwords do not match, giving up");
|
||||
}
|
||||
|
||||
fn install_packages(config: &Config, dest: &Path, cookbook: Option<&str>) -> anyhow::Result<()> {
|
||||
let target = &get_target();
|
||||
|
||||
let packages: Vec<&String> = config
|
||||
.packages
|
||||
.iter()
|
||||
.filter_map(|(packagename, package)| match package {
|
||||
PackageConfig::Build(rule) if rule == "ignore" => None,
|
||||
_ => Some(packagename),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut library = if let Some(cookbook) = cookbook {
|
||||
let callback = pkg::callback::PlainCallback::new();
|
||||
let repo = Path::new(cookbook).join("repo");
|
||||
let pubkey = Path::new(cookbook).join("build");
|
||||
Library::new_local(
|
||||
repo,
|
||||
pubkey,
|
||||
dest.to_path_buf(),
|
||||
target,
|
||||
Rc::new(RefCell::new(callback)),
|
||||
)
|
||||
} else {
|
||||
let callback = pkg::callback::IndicatifCallback::new();
|
||||
Library::new_remote(
|
||||
&vec!["https://static.redox-os.org/pkg"],
|
||||
dest,
|
||||
target,
|
||||
Rc::new(RefCell::new(callback)),
|
||||
)
|
||||
}?;
|
||||
|
||||
let packages = pkg::PackageName::from_list(packages)?;
|
||||
library.install(packages)?;
|
||||
library.apply()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_dir(
|
||||
config: Config,
|
||||
output_dir: impl AsRef<Path>,
|
||||
cookbook: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let output_dir = output_dir.as_ref();
|
||||
|
||||
let output_dir = output_dir.to_owned();
|
||||
|
||||
for file in &config.files {
|
||||
if !file.postinstall {
|
||||
file.create(&output_dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
install_packages(&config, &output_dir, cookbook)?;
|
||||
|
||||
for file in &config.files {
|
||||
if file.postinstall {
|
||||
file.create(&output_dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut passwd = String::new();
|
||||
let mut shadow = String::new();
|
||||
let mut next_uid = 1000;
|
||||
let mut next_gid = 1000;
|
||||
|
||||
let mut groups = vec![];
|
||||
|
||||
for (username, user) in config.users {
|
||||
// plaintext
|
||||
let password = if let Some(password) = user.password {
|
||||
password
|
||||
} else if config.general.prompt.unwrap_or(true) {
|
||||
prompt_password(
|
||||
&format!("{}: enter password: ", username),
|
||||
&format!("{}: confirm password: ", username),
|
||||
)?
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let uid = user.uid.unwrap_or(next_uid);
|
||||
|
||||
if uid >= next_uid {
|
||||
next_uid = uid + 1;
|
||||
}
|
||||
|
||||
let gid = user.gid.unwrap_or(next_gid);
|
||||
|
||||
if gid >= next_gid {
|
||||
next_gid = gid + 1;
|
||||
}
|
||||
|
||||
let name = user.name.unwrap_or(username.clone());
|
||||
let home = user.home.unwrap_or(format!("/home/{}", username));
|
||||
let shell = user.shell.unwrap_or("/bin/ion".into());
|
||||
|
||||
println!("Adding user {username}:");
|
||||
if password.is_empty() {
|
||||
println!("\tPassword: unset");
|
||||
} else {
|
||||
println!("\tPassword: set");
|
||||
}
|
||||
println!("\tUID: {uid}");
|
||||
println!("\tGID: {gid}");
|
||||
println!("\tName: {name}");
|
||||
println!("\tHome: {home}");
|
||||
println!("\tShell: {shell}");
|
||||
|
||||
FileConfig::new_directory(home.clone())
|
||||
.with_recursive_mod(0o700, uid, gid)
|
||||
.create(&output_dir)?;
|
||||
|
||||
if uid >= 1000 {
|
||||
prepare_user_home(&output_dir, uid, gid, &home)?;
|
||||
}
|
||||
|
||||
let password = hash_password(&password)?;
|
||||
|
||||
passwd.push_str(&format!("{username};{uid};{gid};{name};{home};{shell}\n",));
|
||||
shadow.push_str(&format!("{username};{password}\n"));
|
||||
groups.push((username.clone(), gid, vec![username]));
|
||||
}
|
||||
|
||||
for (group, group_config) in config.groups {
|
||||
// FIXME this assumes there is no overlap between auto-created groups for users
|
||||
// and explicitly specified groups.
|
||||
let gid = group_config.gid.unwrap_or(next_gid);
|
||||
|
||||
if gid >= next_gid {
|
||||
next_gid = gid + 1;
|
||||
}
|
||||
|
||||
groups.push((group, gid, group_config.members));
|
||||
}
|
||||
|
||||
if !passwd.is_empty() {
|
||||
FileConfig::new_file("/etc/passwd".to_string(), passwd).create(&output_dir)?;
|
||||
}
|
||||
|
||||
if !shadow.is_empty() {
|
||||
FileConfig::new_file("/etc/shadow".to_string(), shadow)
|
||||
.with_mod(0o0600, 0, 0)
|
||||
.create(&output_dir)?;
|
||||
}
|
||||
|
||||
if !groups.is_empty() {
|
||||
let mut groups_data = String::new();
|
||||
|
||||
for (name, gid, members) in groups {
|
||||
use std::fmt::Write;
|
||||
writeln!(groups_data, "{name};x;{gid};{}", members.join(","))?;
|
||||
|
||||
println!("Adding group {name}:");
|
||||
println!("\tGID: {gid}");
|
||||
println!("\tMembers: {}", members.join(", "));
|
||||
}
|
||||
|
||||
FileConfig::new_file("/etc/group".to_string(), groups_data)
|
||||
.with_mod(0o0600, 0, 0)
|
||||
.create(&output_dir)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare_user_home(
|
||||
output_dir: &PathBuf,
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
home: &String,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
for xdg_folder in &[
|
||||
"Desktop",
|
||||
"Documents",
|
||||
"Downloads",
|
||||
"Music",
|
||||
"Pictures",
|
||||
"Public",
|
||||
"Templates",
|
||||
"Videos",
|
||||
".config",
|
||||
".local",
|
||||
".local/share",
|
||||
".local/share/Trash",
|
||||
".local/share/Trash/info",
|
||||
] {
|
||||
FileConfig::new_directory(format!("{}/{}", home, xdg_folder))
|
||||
.with_mod(0o0700, uid, gid)
|
||||
.create(output_dir)?;
|
||||
}
|
||||
FileConfig::new_file(
|
||||
format!("{}/.config/user-dirs.dirs", home),
|
||||
r#"# Produced by redox installer
|
||||
XDG_DESKTOP_DIR="$HOME/Desktop"
|
||||
XDG_DOCUMENTS_DIR="$HOME/Documents"
|
||||
XDG_DOWNLOAD_DIR="$HOME/Downloads"
|
||||
XDG_MUSIC_DIR="$HOME/Music"
|
||||
XDG_PICTURES_DIR="$HOME/Pictures"
|
||||
XDG_PUBLICSHARE_DIR="$HOME/Public"
|
||||
XDG_TEMPLATES_DIR="$HOME/Templates"
|
||||
XDG_VIDEOS_DIR="$HOME/Videos"
|
||||
"#
|
||||
.to_string(),
|
||||
)
|
||||
.with_mod(0o0600, uid, gid)
|
||||
.create(output_dir)?;
|
||||
|
||||
let skel_dir = output_dir.join("etc/skel");
|
||||
if skel_dir.is_dir() {
|
||||
copy_dir_all(&skel_dir, home.clone(), output_dir, uid, gid)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_dir_all(
|
||||
src: impl AsRef<Path>,
|
||||
dst: String,
|
||||
output_dir: &Path,
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
) -> anyhow::Result<()> {
|
||||
if !Path::new(dst.as_str()).is_dir() {
|
||||
FileConfig::new_directory(dst.clone())
|
||||
.with_mod(0o0700, uid, gid)
|
||||
.create(&output_dir)?;
|
||||
}
|
||||
for entry in fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let file_type = entry.file_type()?;
|
||||
let dst_path = format!("{}/{}", dst, entry.file_name().display());
|
||||
if file_type.is_dir() {
|
||||
copy_dir_all(entry.path(), dst_path, output_dir, uid, gid)?;
|
||||
} else if file_type.is_file() {
|
||||
FileConfig::new_file(
|
||||
dst_path,
|
||||
fs::read_to_string(entry.path())
|
||||
.with_context(|| format!("Reading {}", entry.path().display()))?,
|
||||
)
|
||||
.with_mod(0o0600, uid, gid)
|
||||
.create(&output_dir)?;
|
||||
} else if file_type.is_symlink() {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn with_redoxfs<D, T, F>(disk: D, password_opt: Option<&[u8]>, callback: F) -> Result<T>
|
||||
where
|
||||
D: Disk + Send + 'static,
|
||||
F: FnOnce(FileSystem<D>) -> Result<T>,
|
||||
{
|
||||
let ctime = SystemTime::now().duration_since(UNIX_EPOCH)?;
|
||||
let fs = FileSystem::create(disk, password_opt, ctime.as_secs(), ctime.subsec_nanos())
|
||||
.map_err(syscall_error)?;
|
||||
callback(fs)
|
||||
}
|
||||
|
||||
fn decide_mount_path(mount_path: Option<&Path>) -> PathBuf {
|
||||
let mount_path = mount_path.map(|p| p.to_path_buf()).unwrap_or_else(|| {
|
||||
PathBuf::from(if cfg!(target_os = "redox") {
|
||||
format!("file.redox_installer_{}", process::id())
|
||||
} else {
|
||||
format!("/tmp/redox_installer_{}", process::id())
|
||||
})
|
||||
});
|
||||
mount_path
|
||||
}
|
||||
|
||||
pub fn with_redoxfs_mount<D, T, F>(
|
||||
fs: FileSystem<D>,
|
||||
mount_path: Option<&Path>,
|
||||
callback: F,
|
||||
) -> Result<T>
|
||||
where
|
||||
D: Disk + Send + 'static,
|
||||
F: FnOnce(&Path) -> Result<T>,
|
||||
{
|
||||
let mount_path = decide_mount_path(mount_path);
|
||||
|
||||
if cfg!(not(target_os = "redox")) && !mount_path.exists() {
|
||||
fs::create_dir(&mount_path)?;
|
||||
}
|
||||
|
||||
let (tx, rx) = channel();
|
||||
let join_handle = {
|
||||
let mount_path = mount_path.clone();
|
||||
thread::spawn(move || {
|
||||
let res = redoxfs::mount(fs, &mount_path, |real_path| {
|
||||
tx.send(Ok(real_path.to_owned())).unwrap();
|
||||
});
|
||||
match res {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
tx.send(Err(err)).unwrap();
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
let res = match rx.recv() {
|
||||
Ok(ok) => match ok {
|
||||
Ok(real_path) => callback(&real_path),
|
||||
Err(err) => return Err(err.into()),
|
||||
},
|
||||
Err(_) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotConnected,
|
||||
"redoxfs thread did not send a result",
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
unmount_path(&mount_path.as_os_str().to_str().unwrap())?;
|
||||
|
||||
join_handle.join().unwrap();
|
||||
|
||||
if cfg!(not(target_os = "redox")) {
|
||||
fs::remove_dir_all(&mount_path)?;
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub fn with_redoxfs_ar<D, T, F>(
|
||||
mut fs: FileSystem<D>,
|
||||
mount_path: Option<&Path>,
|
||||
callback: F,
|
||||
) -> Result<T>
|
||||
where
|
||||
D: Disk + Send + 'static,
|
||||
F: FnOnce(&Path) -> Result<T>,
|
||||
{
|
||||
let mount_path = decide_mount_path(mount_path);
|
||||
|
||||
let res = callback(Path::new(&mount_path));
|
||||
|
||||
if res.is_ok() {
|
||||
let _end_block = fs
|
||||
.tx(|tx| {
|
||||
// Archive_at root node
|
||||
redoxfs::archive_at(tx, Path::new(&mount_path), redoxfs::TreePtr::root())
|
||||
.map_err(|err| syscall::Error::new(err.raw_os_error().unwrap()))?;
|
||||
|
||||
// Squash alloc log
|
||||
tx.sync(true)?;
|
||||
|
||||
let end_block = tx.header.size() / BLOCK_SIZE;
|
||||
/* TODO: Cut off any free blocks at the end of the filesystem
|
||||
let mut end_changed = true;
|
||||
while end_changed {
|
||||
end_changed = false;
|
||||
|
||||
let allocator = fs.allocator();
|
||||
let levels = allocator.levels();
|
||||
for level in 0..levels.len() {
|
||||
let level_size = 1 << level;
|
||||
for &block in levels[level].iter() {
|
||||
if block < end_block && block + level_size >= end_block {
|
||||
end_block = block;
|
||||
end_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Update header
|
||||
tx.header.size = (end_block * BLOCK_SIZE).into();
|
||||
tx.header_changed = true;
|
||||
tx.sync(false)?;
|
||||
|
||||
Ok(end_block)
|
||||
})
|
||||
.map_err(syscall_error)?;
|
||||
|
||||
// let size = (fs.block + end_block) * BLOCK_SIZE;
|
||||
// fs.disk.file.set_len(size)?;
|
||||
}
|
||||
|
||||
fs::remove_dir_all(&mount_path)?;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub fn fetch_bootloaders(
|
||||
config: &Config,
|
||||
cookbook: Option<&str>,
|
||||
live: bool,
|
||||
) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
let bootloader_dir =
|
||||
PathBuf::from(format!("/tmp/redox_installer_bootloader_{}", process::id()));
|
||||
|
||||
if bootloader_dir.exists() {
|
||||
fs::remove_dir_all(&bootloader_dir)?;
|
||||
}
|
||||
|
||||
fs::create_dir(&bootloader_dir)?;
|
||||
|
||||
let mut bootloader_config = Config::bootloader_config();
|
||||
bootloader_config.general = config.general.clone();
|
||||
install_packages(&bootloader_config, &bootloader_dir, cookbook)?;
|
||||
|
||||
let boot_dir = bootloader_dir.join("usr/lib/boot");
|
||||
let bios_path = boot_dir.join(if live {
|
||||
"bootloader-live.bios"
|
||||
} else {
|
||||
"bootloader.bios"
|
||||
});
|
||||
let efi_path = boot_dir.join(if live {
|
||||
"bootloader-live.efi"
|
||||
} else {
|
||||
"bootloader.efi"
|
||||
});
|
||||
|
||||
let bios_data = if bios_path.exists() {
|
||||
fs::read(bios_path)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let efi_data = if efi_path.exists() {
|
||||
fs::read(efi_path)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
fs::remove_dir_all(&bootloader_dir)?;
|
||||
|
||||
Ok((bios_data, efi_data))
|
||||
}
|
||||
|
||||
//TODO: make bootloaders use Option, dynamically create BIOS and EFI partitions
|
||||
pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback: F) -> Result<T>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
F: FnOnce(FileSystem<DiskIo<fscommon::StreamSlice<DiskWrapper>>>) -> Result<T>,
|
||||
{
|
||||
let target = get_target();
|
||||
|
||||
let bootloader_efi_name = match target.as_str() {
|
||||
"aarch64-unknown-redox" => "BOOTAA64.EFI",
|
||||
"i586-unknown-redox" | "i686-unknown-redox" => "BOOTIA32.EFI",
|
||||
"x86_64-unknown-redox" => "BOOTX64.EFI",
|
||||
"riscv64gc-unknown-redox" => "BOOTRISCV64.EFI",
|
||||
_ => {
|
||||
bail!("target '{target}' not supported");
|
||||
}
|
||||
};
|
||||
// Open disk and read metadata
|
||||
eprintln!("Opening disk {}", disk_path.as_ref().display());
|
||||
let mut disk_file = DiskWrapper::open(disk_path.as_ref())?;
|
||||
let disk_size = disk_file.size();
|
||||
let block_size = disk_file.block_size() as u64;
|
||||
|
||||
if disk_option.skip_partitions {
|
||||
return with_redoxfs(
|
||||
DiskIo(fscommon::StreamSlice::new(
|
||||
disk_file,
|
||||
0,
|
||||
disk_size.next_multiple_of(block_size),
|
||||
)?),
|
||||
disk_option.password_opt,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
let gpt_block_size = match block_size {
|
||||
512 => gpt::disk::LogicalBlockSize::Lb512,
|
||||
_ => {
|
||||
// TODO: support (and test) other block sizes
|
||||
bail!("block size {block_size} not supported");
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate partition offsets
|
||||
let gpt_reserved = 34 * 512; // GPT always reserves 34 512-byte sectors
|
||||
let mibi = 1024 * 1024;
|
||||
|
||||
// First megabyte of the disk is reserved for BIOS partition, wich includes GPT tables
|
||||
let bios_start = gpt_reserved / block_size;
|
||||
let bios_end = (mibi / block_size) - 1;
|
||||
|
||||
// Second megabyte of the disk is reserved for EFI partition
|
||||
let efi_start = bios_end + 1;
|
||||
let efi_size = if let Some(size) = disk_option.efi_partition_size {
|
||||
size as u64
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let efi_end = efi_start + (efi_size * mibi / block_size) - 1;
|
||||
|
||||
// The rest of the disk is RedoxFS, reserving the GPT table mirror at the end of disk
|
||||
let redoxfs_start = efi_end + 1;
|
||||
let redoxfs_end = ((((disk_size - gpt_reserved) / mibi) * mibi) / block_size) - 1;
|
||||
|
||||
// Format and install BIOS partition
|
||||
{
|
||||
// Write BIOS bootloader to disk
|
||||
eprintln!(
|
||||
"Write bootloader with size {:#x}",
|
||||
disk_option.bootloader_bios.len()
|
||||
);
|
||||
disk_file.seek(SeekFrom::Start(0))?;
|
||||
disk_file.write_all(&disk_option.bootloader_bios)?;
|
||||
|
||||
// Replace MBR tables with protective MBR
|
||||
// TODO: div_ceil
|
||||
let mbr_blocks = ((disk_size + block_size - 1) / block_size) - 1;
|
||||
eprintln!("Writing protective MBR with disk blocks {mbr_blocks:#x}");
|
||||
gpt::mbr::ProtectiveMBR::with_lb_size(mbr_blocks as u32)
|
||||
.update_conservative(&mut disk_file)?;
|
||||
|
||||
// Open disk, mark it as not initialized
|
||||
let mut gpt_disk = gpt::GptConfig::new()
|
||||
.initialized(false)
|
||||
.writable(true)
|
||||
.logical_block_size(gpt_block_size)
|
||||
.create_from_device(Box::new(&mut disk_file), None)?;
|
||||
|
||||
// Add BIOS boot partition
|
||||
let mut partitions = BTreeMap::new();
|
||||
let mut partition_id = 1;
|
||||
partitions.insert(
|
||||
partition_id,
|
||||
gpt::partition::Partition {
|
||||
part_type_guid: gpt::partition_types::BIOS,
|
||||
part_guid: uuid::Uuid::new_v4(),
|
||||
first_lba: bios_start,
|
||||
last_lba: bios_end,
|
||||
flags: 0, // TODO
|
||||
name: "BIOS".to_string(),
|
||||
},
|
||||
);
|
||||
partition_id += 1;
|
||||
|
||||
// Add EFI boot partition
|
||||
partitions.insert(
|
||||
partition_id,
|
||||
gpt::partition::Partition {
|
||||
part_type_guid: gpt::partition_types::EFI,
|
||||
part_guid: uuid::Uuid::new_v4(),
|
||||
first_lba: efi_start,
|
||||
last_lba: efi_end,
|
||||
flags: 0, // TODO
|
||||
name: "EFI".to_string(),
|
||||
},
|
||||
);
|
||||
partition_id += 1;
|
||||
|
||||
// Add RedoxFS partition
|
||||
partitions.insert(
|
||||
partition_id,
|
||||
gpt::partition::Partition {
|
||||
//TODO: Use REDOX_REDOXFS type (needs GPT crate changes)
|
||||
part_type_guid: gpt::partition_types::LINUX_FS,
|
||||
part_guid: uuid::Uuid::new_v4(),
|
||||
first_lba: redoxfs_start,
|
||||
last_lba: redoxfs_end,
|
||||
flags: 0,
|
||||
name: "REDOX".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
eprintln!("Writing GPT tables: {partitions:#?}");
|
||||
|
||||
// Initialize GPT table
|
||||
gpt_disk.update_partitions(partitions)?;
|
||||
|
||||
// Write partition layout, returning disk file
|
||||
gpt_disk.write()?;
|
||||
}
|
||||
|
||||
// Format and install EFI partition
|
||||
{
|
||||
let disk_efi_start = efi_start * block_size;
|
||||
let disk_efi_end = (efi_end + 1) * block_size;
|
||||
let mut disk_efi =
|
||||
fscommon::StreamSlice::new(&mut disk_file, disk_efi_start, disk_efi_end)?;
|
||||
|
||||
eprintln!(
|
||||
"Formatting EFI partition with size {:#x}",
|
||||
disk_efi_end - disk_efi_start
|
||||
);
|
||||
fatfs::format_volume(&mut disk_efi, fatfs::FormatVolumeOptions::new())?;
|
||||
|
||||
eprintln!("Opening EFI partition");
|
||||
let fs = fatfs::FileSystem::new(&mut disk_efi, fatfs::FsOptions::new())?;
|
||||
|
||||
eprintln!("Creating EFI directory");
|
||||
let root_dir = fs.root_dir();
|
||||
root_dir.create_dir("EFI")?;
|
||||
|
||||
eprintln!("Creating EFI/BOOT directory");
|
||||
let efi_dir = root_dir.open_dir("EFI")?;
|
||||
efi_dir.create_dir("BOOT")?;
|
||||
|
||||
eprintln!(
|
||||
"Writing EFI/BOOT/{} file with size {:#x}",
|
||||
bootloader_efi_name,
|
||||
disk_option.bootloader_efi.len()
|
||||
);
|
||||
let boot_dir = efi_dir.open_dir("BOOT")?;
|
||||
let mut file = boot_dir.create_file(bootloader_efi_name)?;
|
||||
file.truncate()?;
|
||||
file.write_all(&disk_option.bootloader_efi)?;
|
||||
}
|
||||
|
||||
// Format and install RedoxFS partition
|
||||
eprintln!(
|
||||
"Installing to RedoxFS partition with size {:#x}",
|
||||
(redoxfs_end - redoxfs_start) * block_size
|
||||
);
|
||||
let disk_redoxfs = DiskIo(fscommon::StreamSlice::new(
|
||||
disk_file,
|
||||
redoxfs_start * block_size,
|
||||
(redoxfs_end + 1) * block_size,
|
||||
)?);
|
||||
with_redoxfs(disk_redoxfs, disk_option.password_opt, callback)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
pub fn try_fast_install<D: redoxfs::Disk, F: FnMut(u64, u64)>(
|
||||
_fs: &mut redoxfs::FileSystem<D>,
|
||||
_progress: F,
|
||||
) -> Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Try fast install using live disk memory
|
||||
#[cfg(target_os = "redox")]
|
||||
pub fn try_fast_install<D: redoxfs::Disk, F: FnMut(u64, u64)>(
|
||||
fs: &mut redoxfs::FileSystem<D>,
|
||||
mut progress: F,
|
||||
) -> Result<bool> {
|
||||
use libredox::{call::MmapArgs, flag};
|
||||
use std::os::fd::AsRawFd;
|
||||
use syscall::PAGE_SIZE;
|
||||
|
||||
let phys = env::var("DISK_LIVE_ADDR")
|
||||
.ok()
|
||||
.and_then(|x| usize::from_str_radix(&x, 16).ok())
|
||||
.unwrap_or(0);
|
||||
let size = env::var("DISK_LIVE_SIZE")
|
||||
.ok()
|
||||
.and_then(|x| usize::from_str_radix(&x, 16).ok())
|
||||
.unwrap_or(0);
|
||||
if phys == 0 || size == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let start = (phys / PAGE_SIZE) * PAGE_SIZE;
|
||||
let end = phys
|
||||
.checked_add(size)
|
||||
.context("phys + size overflow")?
|
||||
.next_multiple_of(PAGE_SIZE);
|
||||
let size = end - start;
|
||||
|
||||
let original = unsafe {
|
||||
//TODO: unmap this memory
|
||||
let file = fs::File::open("/scheme/memory/physical")?;
|
||||
let base = libredox::call::mmap(MmapArgs {
|
||||
fd: file.as_raw_fd() as usize,
|
||||
addr: core::ptr::null_mut(),
|
||||
offset: start as u64,
|
||||
length: size,
|
||||
prot: flag::PROT_READ,
|
||||
flags: flag::MAP_SHARED,
|
||||
})
|
||||
.map_err(|err| anyhow::anyhow!("failed to mmap livedisk: {}", err))?;
|
||||
|
||||
std::slice::from_raw_parts(base as *const u8, size)
|
||||
};
|
||||
|
||||
struct DiskLive {
|
||||
original: &'static [u8],
|
||||
}
|
||||
|
||||
impl redoxfs::Disk for DiskLive {
|
||||
unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> syscall::Result<usize> {
|
||||
let offset = (block * redoxfs::BLOCK_SIZE) as usize;
|
||||
if offset + buffer.len() > self.original.len() {
|
||||
return Err(syscall::Error::new(syscall::EINVAL));
|
||||
}
|
||||
buffer.copy_from_slice(&self.original[offset..offset + buffer.len()]);
|
||||
Ok(buffer.len())
|
||||
}
|
||||
|
||||
unsafe fn write_at(&mut self, _block: u64, _buffer: &[u8]) -> syscall::Result<usize> {
|
||||
Err(syscall::Error::new(syscall::EINVAL))
|
||||
}
|
||||
|
||||
fn size(&mut self) -> syscall::Result<u64> {
|
||||
Ok(self.original.len() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
let mut fs_old = redoxfs::FileSystem::open(DiskLive { original }, None, None, false)?;
|
||||
let size_old = fs_old.header.size();
|
||||
let free_old = fs_old.allocator().free() * redoxfs::BLOCK_SIZE;
|
||||
let used_old = size_old - free_old;
|
||||
redoxfs::clone(&mut fs_old, fs, move |used| {
|
||||
progress(used, used_old);
|
||||
})?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn install_inner(config: Config, output: &Path) -> Result<()> {
|
||||
println!("Installing to {}:\n{}", output.display(), config);
|
||||
let cookbook = config.general.cookbook.clone();
|
||||
let cookbook = cookbook.as_ref().map(|p| p.as_str());
|
||||
if output.is_dir() {
|
||||
install_dir(config, output, cookbook)
|
||||
} else {
|
||||
if !output.is_file() {
|
||||
let fs_size = config.general.filesystem_size.unwrap_or(0) as u64;
|
||||
// arbitrary size approximately fit just for initfs
|
||||
if fs_size < 32 {
|
||||
bail!("Refusing to create image disk less than 32 MB");
|
||||
}
|
||||
eprintln!(
|
||||
"Creating a new file to {} with size {} MB",
|
||||
output.display(),
|
||||
fs_size
|
||||
);
|
||||
let file = fs::File::create(output)?;
|
||||
file.set_len(fs_size * 1024 * 1024)?;
|
||||
}
|
||||
let live = config.general.live_disk.unwrap_or(false);
|
||||
let password_opt = config.general.encrypt_disk.clone();
|
||||
let password_opt = password_opt.as_ref().map(|p| p.as_bytes());
|
||||
let (bootloader_bios, bootloader_efi) = fetch_bootloaders(&config, cookbook, live)?;
|
||||
if let Some(write_bootloader) = &config.general.write_bootloader {
|
||||
std::fs::write(write_bootloader, &bootloader_efi)?;
|
||||
}
|
||||
let disk_option = DiskOption {
|
||||
bootloader_bios: &bootloader_bios,
|
||||
bootloader_efi: &bootloader_efi,
|
||||
password_opt: password_opt,
|
||||
efi_partition_size: config.general.efi_partition_size,
|
||||
skip_partitions: config.general.skip_partitions.unwrap_or(false),
|
||||
};
|
||||
with_whole_disk(output, &disk_option, move |fs| {
|
||||
if config.general.no_mount.unwrap_or(false) {
|
||||
with_redoxfs_ar(fs, None, move |mount_path| {
|
||||
install_dir(config, mount_path, cookbook)
|
||||
})
|
||||
} else {
|
||||
with_redoxfs_mount(fs, None, move |mount_path| {
|
||||
install_dir(config, mount_path, cookbook)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Install RedoxFS into a new disk file, or a sysroot directory.
|
||||
/// This function assumes all interactive prompts resolved by the caller.
|
||||
pub fn install(config: Config, output: impl AsRef<Path>) -> Result<()> {
|
||||
install_inner(config, output.as_ref())
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
use aes::{
|
||||
cipher::{BlockDecrypt, BlockEncrypt, KeyInit},
|
||||
Aes128,
|
||||
};
|
||||
use xts_mode::Xts128;
|
||||
|
||||
// The raw key, keep secret!
|
||||
#[repr(transparent)]
|
||||
pub struct Key([u8; 16]);
|
||||
|
||||
impl Key {
|
||||
/// Generate a random key
|
||||
#[cfg(feature = "std")]
|
||||
pub fn new() -> Result<Self, getrandom::Error> {
|
||||
let mut bytes = [0; 16];
|
||||
getrandom::getrandom(&mut bytes)?;
|
||||
Ok(Self(bytes))
|
||||
}
|
||||
|
||||
pub fn encrypt(&self, password_aes: &Aes128) -> EncryptedKey {
|
||||
let mut block = aes::Block::from(self.0);
|
||||
password_aes.encrypt_block(&mut block);
|
||||
EncryptedKey(block.into())
|
||||
}
|
||||
|
||||
pub fn into_aes(self) -> Aes128 {
|
||||
Aes128::new(&aes::Block::from(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// The encrypted key, encrypted with AES using the salt and password
|
||||
#[derive(Clone, Copy, Default)]
|
||||
#[repr(transparent)]
|
||||
pub struct EncryptedKey([u8; 16]);
|
||||
|
||||
impl EncryptedKey {
|
||||
pub fn decrypt(&self, password_aes: &Aes128) -> Key {
|
||||
let mut block = aes::Block::from(self.0);
|
||||
password_aes.decrypt_block(&mut block);
|
||||
Key(block.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Salt used to prevent rainbow table attacks on the encryption password
|
||||
#[derive(Clone, Copy, Default)]
|
||||
#[repr(transparent)]
|
||||
pub struct Salt([u8; 16]);
|
||||
|
||||
impl Salt {
|
||||
/// Generate a random salt
|
||||
#[cfg(feature = "std")]
|
||||
pub fn new() -> Result<Self, getrandom::Error> {
|
||||
let mut bytes = [0; 16];
|
||||
getrandom::getrandom(&mut bytes)?;
|
||||
Ok(Self(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
/// The key slot, containing the salt and encrypted key that are used with one password
|
||||
#[derive(Clone, Copy, Default)]
|
||||
#[repr(C, packed)]
|
||||
pub struct KeySlot {
|
||||
salt: Salt,
|
||||
// Two keys for AES XTS 128
|
||||
encrypted_keys: (EncryptedKey, EncryptedKey),
|
||||
}
|
||||
|
||||
impl KeySlot {
|
||||
/// Get the password AES key (generated from the password and salt, encrypts the real key)
|
||||
pub fn password_aes(password: &[u8], salt: &Salt) -> Result<Aes128, argon2::Error> {
|
||||
let mut key = Key([0; 16]);
|
||||
|
||||
let mut params_builder = argon2::ParamsBuilder::new();
|
||||
params_builder.output_len(key.0.len())?;
|
||||
|
||||
let argon2 = argon2::Argon2::new(
|
||||
argon2::Algorithm::Argon2id,
|
||||
argon2::Version::V0x13,
|
||||
params_builder.params()?,
|
||||
);
|
||||
|
||||
argon2.hash_password_into(password, &salt.0, &mut key.0)?;
|
||||
|
||||
Ok(key.into_aes())
|
||||
}
|
||||
|
||||
/// Create a new key slot from a password, salt, and encryption key
|
||||
pub fn new(password: &[u8], salt: Salt, keys: (Key, Key)) -> Result<Self, argon2::Error> {
|
||||
let password_aes = Self::password_aes(password, &salt)?;
|
||||
Ok(Self {
|
||||
salt,
|
||||
encrypted_keys: (keys.0.encrypt(&password_aes), keys.1.encrypt(&password_aes)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the encryption cipher from this key slot
|
||||
pub fn cipher(&self, password: &[u8]) -> Result<Xts128<Aes128>, argon2::Error> {
|
||||
let password_aes = Self::password_aes(password, &self.salt)?;
|
||||
Ok(Xts128::new(
|
||||
self.encrypted_keys.0.decrypt(&password_aes).into_aes(),
|
||||
self.encrypted_keys.1.decrypt(&password_aes).into_aes(),
|
||||
))
|
||||
}
|
||||
}
|
||||
+68
-12
@@ -1,14 +1,70 @@
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#![crate_name = "redoxfs"]
|
||||
#![crate_type = "lib"]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
// Used often in generating redox_syscall errors
|
||||
#![allow(clippy::or_fun_call)]
|
||||
#![allow(unexpected_cfgs)]
|
||||
|
||||
mod config;
|
||||
#[cfg(feature = "installer")]
|
||||
mod disk_wrapper;
|
||||
#[cfg(feature = "installer")]
|
||||
mod installer;
|
||||
#[cfg(feature = "installer")]
|
||||
pub use crate::installer::*;
|
||||
extern crate alloc;
|
||||
|
||||
pub use crate::config::file::FileConfig;
|
||||
pub use crate::config::package::PackageConfig;
|
||||
pub use crate::config::Config;
|
||||
use core::sync::atomic::AtomicUsize;
|
||||
|
||||
// The alloc log grows by 1 block about every 21 generations
|
||||
pub const ALLOC_GC_THRESHOLD: u64 = 1024;
|
||||
pub const BLOCK_SIZE: u64 = 4096;
|
||||
// A record is 4KiB << 5 = 128KiB
|
||||
pub const RECORD_LEVEL: usize = 5;
|
||||
pub const RECORD_SIZE: u64 = BLOCK_SIZE << RECORD_LEVEL;
|
||||
pub const SIGNATURE: &[u8; 8] = b"RedoxFS\0";
|
||||
pub const VERSION: u64 = 8;
|
||||
pub const DIR_ENTRY_MAX_LENGTH: usize = 252;
|
||||
|
||||
pub static IS_UMT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
pub use self::allocator::{AllocEntry, AllocList, Allocator, ReleaseList, ALLOC_LIST_ENTRIES};
|
||||
#[cfg(feature = "std")]
|
||||
pub use self::archive::{archive, archive_at};
|
||||
pub use self::block::{
|
||||
BlockAddr, BlockData, BlockLevel, BlockList, BlockMeta, BlockPtr, BlockRaw, BlockTrait,
|
||||
};
|
||||
#[cfg(feature = "std")]
|
||||
pub use self::clone::clone;
|
||||
pub use self::dir::{DirEntry, DirList};
|
||||
pub use self::disk::*;
|
||||
pub use self::filesystem::FileSystem;
|
||||
pub use self::header::{Header, HEADER_RING};
|
||||
pub use self::key::{Key, KeySlot, Salt};
|
||||
#[cfg(feature = "std")]
|
||||
pub use self::mount::mount;
|
||||
pub use self::node::{Node, NodeFlags, NodeLevel, NodeLevelData};
|
||||
pub use self::record::RecordRaw;
|
||||
pub use self::transaction::Transaction;
|
||||
pub use self::tree::{Tree, TreeData, TreeList, TreePtr};
|
||||
#[cfg(feature = "std")]
|
||||
pub use self::unmount::unmount_path;
|
||||
|
||||
mod allocator;
|
||||
#[cfg(feature = "std")]
|
||||
mod archive;
|
||||
mod block;
|
||||
#[cfg(feature = "std")]
|
||||
mod clone;
|
||||
mod dir;
|
||||
mod disk;
|
||||
mod filesystem;
|
||||
mod header;
|
||||
mod htree;
|
||||
mod key;
|
||||
#[cfg(all(feature = "std", not(fuzzing)))]
|
||||
mod mount;
|
||||
#[cfg(all(feature = "std", fuzzing))]
|
||||
pub mod mount;
|
||||
mod node;
|
||||
mod record;
|
||||
mod transaction;
|
||||
mod tree;
|
||||
#[cfg(feature = "std")]
|
||||
mod unmount;
|
||||
|
||||
#[cfg(all(feature = "std", test))]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,580 @@
|
||||
extern crate fuser;
|
||||
|
||||
use std::cmp;
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use self::fuser::MountOption;
|
||||
use self::fuser::TimeOrNow;
|
||||
use crate::mount::fuse::TimeOrNow::Now;
|
||||
use crate::mount::fuse::TimeOrNow::SpecificTime;
|
||||
|
||||
use crate::{filesystem, Disk, Node, TreeData, TreePtr, BLOCK_SIZE};
|
||||
|
||||
use self::fuser::{
|
||||
FileAttr, FileType, Filesystem, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty,
|
||||
ReplyEntry, ReplyOpen, ReplyStatfs, ReplyWrite, Request, Session,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
const TTL: Duration = Duration::new(1, 0); // 1 second
|
||||
|
||||
const NULL_TIME: Duration = Duration::new(0, 0);
|
||||
|
||||
pub fn mount<D, P, T, F>(
|
||||
mut filesystem: filesystem::FileSystem<D>,
|
||||
mountpoint: P,
|
||||
callback: F,
|
||||
) -> io::Result<T>
|
||||
where
|
||||
D: Disk,
|
||||
P: AsRef<Path>,
|
||||
F: FnOnce(&Path) -> T,
|
||||
{
|
||||
let mountpoint = mountpoint.as_ref();
|
||||
|
||||
// One of the uses of this redoxfs fuse wrapper is to populate a filesystem
|
||||
// while building the Redox OS kernel. This means that we need to write on
|
||||
// a filesystem that belongs to `root`, which in turn means that we need to
|
||||
// be `root`, thus that we need to allow `root` to have access.
|
||||
let defer_permissions = [MountOption::CUSTOM("defer_permissions".to_owned())];
|
||||
|
||||
let res = {
|
||||
let mut session = Session::new(
|
||||
Fuse {
|
||||
fs: &mut filesystem,
|
||||
},
|
||||
mountpoint,
|
||||
if cfg!(target_os = "macos") {
|
||||
&defer_permissions
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
)?;
|
||||
|
||||
let res = callback(mountpoint);
|
||||
|
||||
session.run()?;
|
||||
|
||||
res
|
||||
};
|
||||
|
||||
// Cleanup on unmount
|
||||
filesystem.cleanup()?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub struct Fuse<'f, D: Disk> {
|
||||
pub fs: &'f mut filesystem::FileSystem<D>,
|
||||
}
|
||||
|
||||
fn node_attr(node: &TreeData<Node>) -> FileAttr {
|
||||
FileAttr {
|
||||
ino: node.id() as u64,
|
||||
size: node.data().size(),
|
||||
// Blocks is in 512 byte blocks, not in our block size
|
||||
blocks: node.data().blocks() * (BLOCK_SIZE / 512),
|
||||
blksize: 512,
|
||||
atime: SystemTime::UNIX_EPOCH + Duration::new(node.data().atime().0, node.data().atime().1),
|
||||
mtime: SystemTime::UNIX_EPOCH + Duration::new(node.data().mtime().0, node.data().mtime().1),
|
||||
ctime: SystemTime::UNIX_EPOCH + Duration::new(node.data().ctime().0, node.data().ctime().1),
|
||||
crtime: UNIX_EPOCH + NULL_TIME,
|
||||
kind: if node.data().is_dir() {
|
||||
FileType::Directory
|
||||
} else if node.data().is_symlink() {
|
||||
FileType::Symlink
|
||||
} else {
|
||||
FileType::RegularFile
|
||||
},
|
||||
perm: node.data().mode() & Node::MODE_PERM,
|
||||
nlink: node.data().links(),
|
||||
uid: node.data().uid(),
|
||||
gid: node.data().gid(),
|
||||
rdev: 0,
|
||||
flags: 0,
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Disk> Filesystem for Fuse<'_, D> {
|
||||
fn lookup(&mut self, _req: &Request, parent_id: u64, name: &OsStr, reply: ReplyEntry) {
|
||||
let parent_ptr = TreePtr::new(parent_id as u32);
|
||||
match self
|
||||
.fs
|
||||
.tx(|tx| tx.find_node(parent_ptr, name.to_str().unwrap()))
|
||||
{
|
||||
Ok(node) => {
|
||||
reply.entry(&TTL, &node_attr(&node), 0);
|
||||
}
|
||||
Err(err) => {
|
||||
reply.error(err.errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn getattr(&mut self, _req: &Request, node_id: u64, _fh: Option<u64>, reply: ReplyAttr) {
|
||||
let node_ptr = TreePtr::<Node>::new(node_id as u32);
|
||||
match self.fs.tx(|tx| tx.read_tree(node_ptr)) {
|
||||
Ok(node) => {
|
||||
reply.attr(&TTL, &node_attr(&node));
|
||||
}
|
||||
Err(err) => {
|
||||
reply.error(err.errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setattr(
|
||||
&mut self,
|
||||
_req: &Request,
|
||||
node_id: u64,
|
||||
mode: Option<u32>,
|
||||
uid: Option<u32>,
|
||||
gid: Option<u32>,
|
||||
size: Option<u64>,
|
||||
atime: Option<TimeOrNow>,
|
||||
mtime: Option<TimeOrNow>,
|
||||
_ctime: Option<SystemTime>,
|
||||
_fh: Option<u64>,
|
||||
_crtime: Option<SystemTime>,
|
||||
_chgtime: Option<SystemTime>,
|
||||
_bkuptime: Option<SystemTime>,
|
||||
_flags: Option<u32>,
|
||||
reply: ReplyAttr,
|
||||
) {
|
||||
let node_ptr = TreePtr::<Node>::new(node_id as u32);
|
||||
|
||||
let mut node = match self.fs.tx(|tx| tx.read_tree(node_ptr)) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
reply.error(err.errno);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut node_changed = false;
|
||||
|
||||
if let Some(mode) = mode {
|
||||
if node.data().mode() & Node::MODE_PERM != mode as u16 & Node::MODE_PERM {
|
||||
let new_mode =
|
||||
(node.data().mode() & Node::MODE_TYPE) | (mode as u16 & Node::MODE_PERM);
|
||||
node.data_mut().set_mode(new_mode);
|
||||
node_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(uid) = uid {
|
||||
if node.data().uid() != uid {
|
||||
node.data_mut().set_uid(uid);
|
||||
node_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(gid) = gid {
|
||||
if node.data().gid() != gid {
|
||||
node.data_mut().set_gid(gid);
|
||||
node_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(atime) = atime {
|
||||
let atime_c = match atime {
|
||||
SpecificTime(st) => st.duration_since(UNIX_EPOCH).unwrap(),
|
||||
Now => SystemTime::now().duration_since(UNIX_EPOCH).unwrap(),
|
||||
};
|
||||
node.data_mut()
|
||||
.set_atime(atime_c.as_secs(), atime_c.subsec_nanos());
|
||||
node_changed = true;
|
||||
}
|
||||
|
||||
if let Some(mtime) = mtime {
|
||||
let mtime_c = match mtime {
|
||||
SpecificTime(st) => st.duration_since(UNIX_EPOCH).unwrap(),
|
||||
Now => SystemTime::now().duration_since(UNIX_EPOCH).unwrap(),
|
||||
};
|
||||
node.data_mut()
|
||||
.set_mtime(mtime_c.as_secs(), mtime_c.subsec_nanos());
|
||||
node_changed = true;
|
||||
}
|
||||
|
||||
if let Some(size) = size {
|
||||
match self.fs.tx(|tx| tx.truncate_node_inner(&mut node, size)) {
|
||||
Ok(ok) => {
|
||||
if ok {
|
||||
node_changed = true;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
reply.error(err.errno);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let attr = node_attr(&node);
|
||||
|
||||
if node_changed {
|
||||
if let Err(err) = self.fs.tx(|tx| tx.sync_tree(node)) {
|
||||
reply.error(err.errno);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
reply.attr(&TTL, &attr);
|
||||
}
|
||||
|
||||
fn open(&mut self, _req: &Request<'_>, node_id: u64, _flags: i32, reply: ReplyOpen) {
|
||||
let node_ptr = TreePtr::<Node>::new(node_id as u32);
|
||||
match self.fs.tx(|tx| tx.on_open_node(node_ptr)) {
|
||||
Ok(()) => reply.opened(0, 0),
|
||||
Err(err) => reply.error(err.errno),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(
|
||||
&mut self,
|
||||
_req: &Request,
|
||||
node_id: u64,
|
||||
_fh: u64,
|
||||
offset: i64,
|
||||
size: u32,
|
||||
_flags: i32,
|
||||
_lock_owner: Option<u64>,
|
||||
reply: ReplyData,
|
||||
) {
|
||||
let node_ptr = TreePtr::<Node>::new(node_id as u32);
|
||||
|
||||
let atime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
let mut data = vec![0; size as usize];
|
||||
match self.fs.tx(|tx| {
|
||||
tx.read_node(
|
||||
node_ptr,
|
||||
cmp::max(0, offset) as u64,
|
||||
&mut data,
|
||||
atime.as_secs(),
|
||||
atime.subsec_nanos(),
|
||||
)
|
||||
}) {
|
||||
Ok(count) => {
|
||||
reply.data(&data[..count]);
|
||||
}
|
||||
Err(err) => {
|
||||
reply.error(err.errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write(
|
||||
&mut self,
|
||||
_req: &Request,
|
||||
node_id: u64,
|
||||
_fh: u64,
|
||||
offset: i64,
|
||||
data: &[u8],
|
||||
_write_flags: u32,
|
||||
_flags: i32,
|
||||
_lock_owner: Option<u64>,
|
||||
reply: ReplyWrite,
|
||||
) {
|
||||
let node_ptr = TreePtr::<Node>::new(node_id as u32);
|
||||
|
||||
let mtime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
match self.fs.tx(|tx| {
|
||||
tx.write_node(
|
||||
node_ptr,
|
||||
cmp::max(0, offset) as u64,
|
||||
data,
|
||||
mtime.as_secs(),
|
||||
mtime.subsec_nanos(),
|
||||
)
|
||||
}) {
|
||||
Ok(count) => {
|
||||
reply.written(count as u32);
|
||||
}
|
||||
Err(err) => {
|
||||
reply.error(err.errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self, _req: &Request, _ino: u64, _fh: u64, _lock_owner: u64, reply: ReplyEmpty) {
|
||||
reply.ok();
|
||||
}
|
||||
|
||||
fn release(
|
||||
&mut self,
|
||||
_req: &Request,
|
||||
node_id: u64,
|
||||
_fh: u64,
|
||||
_flags: i32,
|
||||
_lock_owner: Option<u64>,
|
||||
_flush: bool,
|
||||
reply: ReplyEmpty,
|
||||
) {
|
||||
let node_ptr = TreePtr::new(node_id as u32);
|
||||
match self.fs.tx(|tx| tx.on_close_node(node_ptr)) {
|
||||
Ok(()) => reply.ok(),
|
||||
Err(err) => reply.error(err.errno),
|
||||
}
|
||||
}
|
||||
|
||||
fn fsync(&mut self, _req: &Request, _ino: u64, _fh: u64, _datasync: bool, reply: ReplyEmpty) {
|
||||
reply.ok();
|
||||
}
|
||||
|
||||
fn readdir(
|
||||
&mut self,
|
||||
_req: &Request,
|
||||
parent_id: u64,
|
||||
_fh: u64,
|
||||
offset: i64,
|
||||
mut reply: ReplyDirectory,
|
||||
) {
|
||||
let parent_ptr = TreePtr::new(parent_id as u32);
|
||||
let mut children = Vec::new();
|
||||
match self.fs.tx(|tx| tx.child_nodes(parent_ptr, &mut children)) {
|
||||
Ok(()) => {
|
||||
let mut i;
|
||||
let skip;
|
||||
if offset == 0 {
|
||||
skip = 0;
|
||||
i = 0;
|
||||
let _full = reply.add(parent_id, i, FileType::Directory, ".");
|
||||
|
||||
i += 1;
|
||||
let _full = reply.add(
|
||||
//TODO: get parent?
|
||||
parent_id,
|
||||
i,
|
||||
FileType::Directory,
|
||||
"..",
|
||||
);
|
||||
i += 1;
|
||||
} else {
|
||||
i = offset + 1;
|
||||
skip = offset as usize - 1;
|
||||
}
|
||||
|
||||
for child in children.iter().skip(skip) {
|
||||
//TODO: make it possible to get file type from directory entry
|
||||
let node = match self.fs.tx(|tx| tx.read_tree(child.node_ptr())) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
reply.error(err.errno);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let full = reply.add(
|
||||
child.node_ptr().id() as u64,
|
||||
i,
|
||||
if node.data().is_dir() {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::RegularFile
|
||||
},
|
||||
child.name().unwrap(),
|
||||
);
|
||||
|
||||
if full {
|
||||
break;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
reply.ok();
|
||||
}
|
||||
Err(err) => {
|
||||
reply.error(err.errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create(
|
||||
&mut self,
|
||||
_req: &Request,
|
||||
parent_id: u64,
|
||||
name: &OsStr,
|
||||
mode: u32,
|
||||
_umask: u32,
|
||||
_flags: i32,
|
||||
reply: ReplyCreate,
|
||||
) {
|
||||
let parent_ptr = TreePtr::<Node>::new(parent_id as u32);
|
||||
let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
match self.fs.tx(|tx| {
|
||||
let node = tx.create_node(
|
||||
parent_ptr,
|
||||
name.to_str().unwrap(),
|
||||
Node::MODE_FILE | (mode as u16 & Node::MODE_PERM),
|
||||
ctime.as_secs(),
|
||||
ctime.subsec_nanos(),
|
||||
)?;
|
||||
tx.on_open_node(node.ptr())?;
|
||||
Ok(node)
|
||||
}) {
|
||||
Ok(node) => {
|
||||
// println!("Create {:?}:{:o}:{:o}", node.1.name(), node.1.mode, mode);
|
||||
reply.created(&TTL, &node_attr(&node), 0, 0, 0);
|
||||
}
|
||||
Err(error) => {
|
||||
reply.error(error.errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mkdir(
|
||||
&mut self,
|
||||
_req: &Request,
|
||||
parent_id: u64,
|
||||
name: &OsStr,
|
||||
mode: u32,
|
||||
_umask: u32,
|
||||
reply: ReplyEntry,
|
||||
) {
|
||||
let parent_ptr = TreePtr::<Node>::new(parent_id as u32);
|
||||
let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
match self.fs.tx(|tx| {
|
||||
tx.create_node(
|
||||
parent_ptr,
|
||||
name.to_str().unwrap(),
|
||||
Node::MODE_DIR | (mode as u16 & Node::MODE_PERM),
|
||||
ctime.as_secs(),
|
||||
ctime.subsec_nanos(),
|
||||
)
|
||||
}) {
|
||||
Ok(node) => {
|
||||
// println!("Mkdir {:?}:{:o}:{:o}", node.1.name(), node.1.mode, mode);
|
||||
reply.entry(&TTL, &node_attr(&node), 0);
|
||||
}
|
||||
Err(error) => {
|
||||
reply.error(error.errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rmdir(&mut self, _req: &Request, parent_id: u64, name: &OsStr, reply: ReplyEmpty) {
|
||||
let parent_ptr = TreePtr::<Node>::new(parent_id as u32);
|
||||
match self
|
||||
.fs
|
||||
.tx(|tx| tx.remove_node(parent_ptr, name.to_str().unwrap(), Node::MODE_DIR))
|
||||
{
|
||||
Ok(_) => {
|
||||
reply.ok();
|
||||
}
|
||||
Err(err) => {
|
||||
reply.error(err.errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unlink(&mut self, _req: &Request, parent_id: u64, name: &OsStr, reply: ReplyEmpty) {
|
||||
let parent_ptr = TreePtr::<Node>::new(parent_id as u32);
|
||||
match self
|
||||
.fs
|
||||
.tx(|tx| tx.remove_node(parent_ptr, name.to_str().unwrap(), Node::MODE_FILE))
|
||||
{
|
||||
Ok(_) => {
|
||||
reply.ok();
|
||||
}
|
||||
Err(err) => {
|
||||
reply.error(err.errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn statfs(&mut self, _req: &Request, _ino: u64, reply: ReplyStatfs) {
|
||||
let bsize = BLOCK_SIZE;
|
||||
let blocks = self.fs.header.size() / bsize;
|
||||
let bfree = self.fs.allocator().free();
|
||||
reply.statfs(blocks, bfree, bfree, 0, 0, bsize as u32, 256, 0);
|
||||
}
|
||||
|
||||
fn symlink(
|
||||
&mut self,
|
||||
_req: &Request,
|
||||
parent_id: u64,
|
||||
name: &OsStr,
|
||||
link: &Path,
|
||||
reply: ReplyEntry,
|
||||
) {
|
||||
let parent_ptr = TreePtr::<Node>::new(parent_id as u32);
|
||||
let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
match self.fs.tx(|tx| {
|
||||
let node = tx.create_node(
|
||||
parent_ptr,
|
||||
name.to_str().unwrap(),
|
||||
Node::MODE_SYMLINK | 0o777,
|
||||
ctime.as_secs(),
|
||||
ctime.subsec_nanos(),
|
||||
)?;
|
||||
|
||||
let mtime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
tx.write_node(
|
||||
node.ptr(),
|
||||
0,
|
||||
link.as_os_str().as_bytes(),
|
||||
mtime.as_secs(),
|
||||
mtime.subsec_nanos(),
|
||||
)?;
|
||||
|
||||
Ok(node)
|
||||
}) {
|
||||
Ok(node) => {
|
||||
reply.entry(&TTL, &node_attr(&node), 0);
|
||||
}
|
||||
Err(error) => {
|
||||
reply.error(error.errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn readlink(&mut self, _req: &Request, node_id: u64, reply: ReplyData) {
|
||||
let node_ptr = TreePtr::<Node>::new(node_id as u32);
|
||||
let atime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
let mut data = vec![0; 4096];
|
||||
match self.fs.tx(|tx| {
|
||||
tx.read_node(
|
||||
node_ptr,
|
||||
0,
|
||||
&mut data,
|
||||
atime.as_secs(),
|
||||
atime.subsec_nanos(),
|
||||
)
|
||||
}) {
|
||||
Ok(count) => {
|
||||
reply.data(&data[..count]);
|
||||
}
|
||||
Err(err) => {
|
||||
reply.error(err.errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rename(
|
||||
&mut self,
|
||||
_req: &Request,
|
||||
orig_parent: u64,
|
||||
orig_name: &OsStr,
|
||||
new_parent: u64,
|
||||
new_name: &OsStr,
|
||||
_flags: u32,
|
||||
reply: ReplyEmpty,
|
||||
) {
|
||||
let orig_parent_ptr = TreePtr::<Node>::new(orig_parent as u32);
|
||||
let orig_name = orig_name.to_str().expect("name is not utf-8");
|
||||
let new_parent_ptr = TreePtr::<Node>::new(new_parent as u32);
|
||||
let new_name = new_name.to_str().expect("name is not utf-8");
|
||||
|
||||
// TODO: improve performance
|
||||
match self
|
||||
.fs
|
||||
.tx(|tx| tx.rename_node(orig_parent_ptr, orig_name, new_parent_ptr, new_name))
|
||||
{
|
||||
Ok(()) => reply.ok(),
|
||||
Err(err) => reply.error(err.errno),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#[cfg(all(not(target_os = "redox"), not(fuzzing), feature = "fuse"))]
|
||||
mod fuse;
|
||||
|
||||
#[cfg(all(not(target_os = "redox"), fuzzing, feature = "fuse"))]
|
||||
pub mod fuse;
|
||||
|
||||
#[cfg(all(not(target_os = "redox"), feature = "fuse"))]
|
||||
pub use self::fuse::mount;
|
||||
|
||||
#[cfg(all(not(target_os = "redox"), not(fuzzing), not(feature = "fuse")))]
|
||||
mod stub;
|
||||
|
||||
#[cfg(all(not(target_os = "redox"), fuzzing, not(feature = "fuse")))]
|
||||
pub mod stub;
|
||||
|
||||
#[cfg(all(not(target_os = "redox"), not(feature = "fuse")))]
|
||||
pub use self::stub::mount;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
mod redox;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
pub use self::redox::mount;
|
||||
@@ -0,0 +1,89 @@
|
||||
use redox_scheme::{
|
||||
scheme::{SchemeState, SchemeSync},
|
||||
RequestKind, Response, SignalBehavior, Socket,
|
||||
};
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::{Disk, FileSystem, IS_UMT};
|
||||
|
||||
use self::scheme::FileScheme;
|
||||
|
||||
pub mod resource;
|
||||
pub mod scheme;
|
||||
|
||||
//FIXME: mut callback is not mut
|
||||
#[allow(unused_mut)]
|
||||
|
||||
pub fn mount<D, P, T, F>(filesystem: FileSystem<D>, mountpoint: P, mut callback: F) -> io::Result<T>
|
||||
where
|
||||
D: Disk,
|
||||
P: AsRef<Path>,
|
||||
F: FnOnce(&Path) -> T,
|
||||
{
|
||||
let mountpoint = mountpoint.as_ref();
|
||||
let socket = Socket::create()?;
|
||||
|
||||
let scheme_name = format!("{}", mountpoint.display());
|
||||
let mounted_path = format!("/scheme/{}", mountpoint.display());
|
||||
|
||||
let mut state = SchemeState::new();
|
||||
let mut scheme = FileScheme::new(scheme_name, mounted_path.clone(), filesystem, &socket)?;
|
||||
|
||||
redox_scheme::scheme::register_sync_scheme(
|
||||
&socket,
|
||||
&format!("{}", mountpoint.display()),
|
||||
&mut scheme,
|
||||
)?;
|
||||
|
||||
let res = callback(Path::new(&mounted_path));
|
||||
|
||||
while IS_UMT.load(Ordering::SeqCst) == 0 {
|
||||
let req = match socket.next_request(SignalBehavior::Restart)? {
|
||||
None => break,
|
||||
Some(req) => {
|
||||
match req.kind() {
|
||||
RequestKind::Call(r) => r,
|
||||
RequestKind::SendFd(sendfd_request) => {
|
||||
let result = scheme.on_sendfd(&sendfd_request);
|
||||
let response = Response::new(result, sendfd_request);
|
||||
|
||||
if !socket.write_response(response, SignalBehavior::Restart)? {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
RequestKind::OnClose { id } => {
|
||||
scheme.on_close(id);
|
||||
state.on_close(id);
|
||||
continue;
|
||||
}
|
||||
RequestKind::OnDetach { id, pid } => {
|
||||
let Ok(inode) = scheme.inode(id) else {
|
||||
log::warn!("RequestKind::OnDetach with invalid `id`");
|
||||
continue;
|
||||
};
|
||||
state.on_detach(id, inode, pid);
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
// TODO: Redoxfs does not yet support asynchronous file IO. It might still make
|
||||
// sense to implement cancellation for huge buffers, e.g. dd bs=1G
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let response = req.handle_sync(&mut scheme, &mut state);
|
||||
|
||||
if !socket.write_response(response, SignalBehavior::Restart)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
scheme.fs.cleanup()?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
@@ -0,0 +1,794 @@
|
||||
use std::slice;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use libredox::call::MmapArgs;
|
||||
use range_tree::RangeTree;
|
||||
|
||||
use syscall::data::{Stat, TimeSpec};
|
||||
use syscall::dirent::{DirEntry, DirentBuf, DirentKind};
|
||||
use syscall::error::{Error, Result, EBADF, EINVAL, EISDIR, ENOTDIR, EPERM};
|
||||
use syscall::flag::{
|
||||
MapFlags, F_GETFL, F_SETFL, MODE_PERM, O_ACCMODE, O_APPEND, O_RDONLY, O_RDWR, O_WRONLY,
|
||||
PROT_READ, PROT_WRITE,
|
||||
};
|
||||
use syscall::{EBADFD, ENOENT, PAGE_SIZE};
|
||||
|
||||
use crate::{Disk, Node, Transaction, TreePtr, BLOCK_SIZE};
|
||||
|
||||
pub type Fmaps = BTreeMap<u32, FileMmapInfo>;
|
||||
|
||||
pub trait Resource<D: Disk> {
|
||||
fn parent_ptr_opt(&self) -> Option<TreePtr<Node>>;
|
||||
|
||||
fn node_ptr(&self) -> TreePtr<Node>;
|
||||
|
||||
fn uid(&self) -> u32;
|
||||
|
||||
fn set_path(&mut self, path: &str);
|
||||
|
||||
fn read(&mut self, buf: &mut [u8], offset: u64, tx: &mut Transaction<D>) -> Result<usize>;
|
||||
|
||||
fn write(&mut self, buf: &[u8], offset: u64, tx: &mut Transaction<D>) -> Result<usize>;
|
||||
|
||||
fn fsize(&mut self, tx: &mut Transaction<D>) -> Result<u64>;
|
||||
|
||||
fn fmap(
|
||||
&mut self,
|
||||
fmaps: &mut Fmaps,
|
||||
flags: MapFlags,
|
||||
size: usize,
|
||||
offset: u64,
|
||||
tx: &mut Transaction<D>,
|
||||
) -> Result<usize>;
|
||||
|
||||
fn funmap(
|
||||
&mut self,
|
||||
fmaps: &mut Fmaps,
|
||||
offset: u64,
|
||||
size: usize,
|
||||
tx: &mut Transaction<D>,
|
||||
) -> Result<()>;
|
||||
|
||||
fn fchmod(&mut self, mode: u16, tx: &mut Transaction<D>) -> Result<()> {
|
||||
let mut node = tx.read_tree(self.node_ptr())?;
|
||||
|
||||
if node.data().uid() == self.uid() || self.uid() == 0 {
|
||||
let old_mode = node.data().mode();
|
||||
let new_mode = (old_mode & !MODE_PERM) | (mode & MODE_PERM);
|
||||
if old_mode != new_mode {
|
||||
node.data_mut().set_mode(new_mode);
|
||||
tx.sync_tree(node)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(EPERM))
|
||||
}
|
||||
}
|
||||
|
||||
fn fchown(&mut self, uid: u32, gid: u32, tx: &mut Transaction<D>) -> Result<()> {
|
||||
let mut node = tx.read_tree(self.node_ptr())?;
|
||||
|
||||
let old_uid = node.data().uid();
|
||||
if old_uid == self.uid() || self.uid() == 0 {
|
||||
let mut node_changed = false;
|
||||
|
||||
if uid as i32 != -1 {
|
||||
if uid != old_uid {
|
||||
node.data_mut().set_uid(uid);
|
||||
node_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if gid as i32 != -1 {
|
||||
let old_gid = node.data().gid();
|
||||
if gid != old_gid {
|
||||
node.data_mut().set_gid(gid);
|
||||
node_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if node_changed {
|
||||
tx.sync_tree(node)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(EPERM))
|
||||
}
|
||||
}
|
||||
|
||||
fn fcntl(&mut self, cmd: usize, arg: usize) -> Result<usize>;
|
||||
|
||||
fn path(&self) -> &str;
|
||||
|
||||
fn stat(&self, stat: &mut Stat, tx: &mut Transaction<D>) -> Result<()> {
|
||||
let node = tx.read_tree(self.node_ptr())?;
|
||||
|
||||
let ctime = node.data().ctime();
|
||||
let mtime = node.data().mtime();
|
||||
let atime = node.data().atime();
|
||||
*stat = Stat {
|
||||
st_dev: 0, // TODO
|
||||
st_ino: node.id() as u64,
|
||||
st_mode: node.data().mode(),
|
||||
st_nlink: node.data().links(),
|
||||
st_uid: node.data().uid(),
|
||||
st_gid: node.data().gid(),
|
||||
st_size: node.data().size(),
|
||||
st_blksize: 512,
|
||||
// Blocks is in 512 byte blocks, not in our block size
|
||||
st_blocks: node.data().blocks() * (BLOCK_SIZE / 512),
|
||||
st_mtime: mtime.0,
|
||||
st_mtime_nsec: mtime.1,
|
||||
st_atime: atime.0,
|
||||
st_atime_nsec: atime.1,
|
||||
st_ctime: ctime.0,
|
||||
st_ctime_nsec: ctime.1,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync(&mut self, fmaps: &mut Fmaps, tx: &mut Transaction<D>) -> Result<()>;
|
||||
|
||||
fn truncate(&mut self, len: u64, tx: &mut Transaction<D>) -> Result<()>;
|
||||
|
||||
fn utimens(&mut self, times: &[TimeSpec], tx: &mut Transaction<D>) -> Result<()>;
|
||||
|
||||
fn getdents<'buf>(
|
||||
&mut self,
|
||||
buf: DirentBuf<&'buf mut [u8]>,
|
||||
opaque_offset: u64,
|
||||
tx: &mut Transaction<D>,
|
||||
) -> Result<DirentBuf<&'buf mut [u8]>>;
|
||||
}
|
||||
|
||||
pub struct Entry {
|
||||
pub node_ptr: TreePtr<Node>,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct DirResource {
|
||||
path: String,
|
||||
parent_ptr_opt: Option<TreePtr<Node>>,
|
||||
node_ptr: TreePtr<Node>,
|
||||
data: Option<Vec<Entry>>,
|
||||
uid: u32,
|
||||
}
|
||||
|
||||
impl DirResource {
|
||||
pub fn new(
|
||||
path: String,
|
||||
parent_ptr_opt: Option<TreePtr<Node>>,
|
||||
node_ptr: TreePtr<Node>,
|
||||
data: Option<Vec<Entry>>,
|
||||
uid: u32,
|
||||
) -> DirResource {
|
||||
DirResource {
|
||||
path,
|
||||
parent_ptr_opt,
|
||||
node_ptr,
|
||||
data,
|
||||
uid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Disk> Resource<D> for DirResource {
|
||||
fn parent_ptr_opt(&self) -> Option<TreePtr<Node>> {
|
||||
self.parent_ptr_opt
|
||||
}
|
||||
|
||||
fn node_ptr(&self) -> TreePtr<Node> {
|
||||
self.node_ptr
|
||||
}
|
||||
|
||||
fn uid(&self) -> u32 {
|
||||
self.uid
|
||||
}
|
||||
|
||||
fn set_path(&mut self, path: &str) {
|
||||
self.path = path.to_string();
|
||||
}
|
||||
|
||||
fn read(&mut self, _buf: &mut [u8], _offset: u64, _tx: &mut Transaction<D>) -> Result<usize> {
|
||||
Err(Error::new(EISDIR))
|
||||
}
|
||||
|
||||
fn write(&mut self, _buf: &[u8], _offset: u64, _tx: &mut Transaction<D>) -> Result<usize> {
|
||||
Err(Error::new(EBADF))
|
||||
}
|
||||
|
||||
fn fsize(&mut self, _tx: &mut Transaction<D>) -> Result<u64> {
|
||||
Ok(self.data.as_ref().ok_or(Error::new(EBADF))?.len() as u64)
|
||||
}
|
||||
|
||||
fn fmap(
|
||||
&mut self,
|
||||
_fmaps: &mut Fmaps,
|
||||
_flags: MapFlags,
|
||||
_size: usize,
|
||||
_offset: u64,
|
||||
_tx: &mut Transaction<D>,
|
||||
) -> Result<usize> {
|
||||
Err(Error::new(EBADF))
|
||||
}
|
||||
fn funmap(
|
||||
&mut self,
|
||||
_fmaps: &mut Fmaps,
|
||||
_offset: u64,
|
||||
_size: usize,
|
||||
_tx: &mut Transaction<D>,
|
||||
) -> Result<()> {
|
||||
Err(Error::new(EBADF))
|
||||
}
|
||||
|
||||
fn fcntl(&mut self, _cmd: usize, _arg: usize) -> Result<usize> {
|
||||
Err(Error::new(EBADF))
|
||||
}
|
||||
|
||||
fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn sync(&mut self, _fmaps: &mut Fmaps, _tx: &mut Transaction<D>) -> Result<()> {
|
||||
Err(Error::new(EBADF))
|
||||
}
|
||||
|
||||
fn truncate(&mut self, _len: u64, _tx: &mut Transaction<D>) -> Result<()> {
|
||||
Err(Error::new(EBADF))
|
||||
}
|
||||
|
||||
fn utimens(&mut self, times: &[TimeSpec], tx: &mut Transaction<D>) -> Result<()> {
|
||||
let mut node = tx.read_tree(self.node_ptr)?;
|
||||
|
||||
if node.data().uid() == self.uid || self.uid == 0 {
|
||||
if let &[atime, mtime] = times {
|
||||
let mut node_changed = false;
|
||||
|
||||
let old_mtime = node.data().mtime();
|
||||
let new_mtime = (mtime.tv_sec as u64, mtime.tv_nsec as u32);
|
||||
if old_mtime != new_mtime {
|
||||
node.data_mut().set_mtime(new_mtime.0, new_mtime.1);
|
||||
node_changed = true;
|
||||
}
|
||||
|
||||
let old_atime = node.data().atime();
|
||||
let new_atime = (atime.tv_sec as u64, atime.tv_nsec as u32);
|
||||
if old_atime != new_atime {
|
||||
node.data_mut().set_atime(new_atime.0, new_atime.1);
|
||||
node_changed = true;
|
||||
}
|
||||
|
||||
if node_changed {
|
||||
tx.sync_tree(node)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(EPERM))
|
||||
}
|
||||
}
|
||||
|
||||
fn getdents<'buf>(
|
||||
&mut self,
|
||||
mut buf: DirentBuf<&'buf mut [u8]>,
|
||||
opaque_offset: u64,
|
||||
tx: &mut Transaction<D>,
|
||||
) -> Result<DirentBuf<&'buf mut [u8]>> {
|
||||
match &self.data {
|
||||
Some(data) => {
|
||||
let opaque_offset = opaque_offset as usize;
|
||||
for (idx, entry) in data.iter().enumerate().skip(opaque_offset) {
|
||||
let child = match tx.read_tree(entry.node_ptr) {
|
||||
Ok(r) => r,
|
||||
Err(Error { errno: ENOENT }) => continue,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let result = buf.entry(DirEntry {
|
||||
inode: child.id() as u64,
|
||||
next_opaque_id: idx as u64 + 1,
|
||||
name: &entry.name,
|
||||
kind: match child.data().mode() & Node::MODE_TYPE {
|
||||
Node::MODE_DIR => DirentKind::Directory,
|
||||
Node::MODE_FILE => DirentKind::Regular,
|
||||
Node::MODE_SYMLINK => DirentKind::Symlink,
|
||||
//TODO: more types?
|
||||
_ => DirentKind::Unspecified,
|
||||
},
|
||||
});
|
||||
if let Err(err) = result {
|
||||
if err.errno == EINVAL && idx > opaque_offset {
|
||||
// POSIX allows partial result of getdents
|
||||
break;
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
None => Err(Error::new(EBADF)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Fmap {
|
||||
rc: usize,
|
||||
flags: MapFlags,
|
||||
last_page_tail: u16,
|
||||
}
|
||||
|
||||
impl Fmap {
|
||||
pub unsafe fn new<D: Disk>(
|
||||
node_ptr: TreePtr<Node>,
|
||||
flags: MapFlags,
|
||||
unaligned_size: usize,
|
||||
offset: u64,
|
||||
base: *mut u8,
|
||||
tx: &mut Transaction<D>,
|
||||
) -> Result<Self> {
|
||||
// Memory provided to fmap must be page aligned and sized
|
||||
let aligned_size = unaligned_size.next_multiple_of(syscall::PAGE_SIZE);
|
||||
|
||||
let address = base.add(offset as usize);
|
||||
//println!("ADDR {:p} {:p}", base, address);
|
||||
|
||||
// Read buffer from disk
|
||||
let atime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
|
||||
let buf = slice::from_raw_parts_mut(address, unaligned_size);
|
||||
|
||||
let count = match tx.read_node(node_ptr, offset, buf, atime.as_secs(), atime.subsec_nanos())
|
||||
{
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
let _ = libredox::call::munmap(address.cast(), aligned_size);
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
// Make sure remaining data is zeroed
|
||||
buf[count..].fill(0_u8);
|
||||
|
||||
Ok(Self {
|
||||
rc: 1,
|
||||
flags,
|
||||
last_page_tail: (unaligned_size % PAGE_SIZE) as u16,
|
||||
})
|
||||
}
|
||||
|
||||
pub unsafe fn sync<D: Disk>(
|
||||
&mut self,
|
||||
node_ptr: TreePtr<Node>,
|
||||
base: *mut u8,
|
||||
offset: u64,
|
||||
size: usize,
|
||||
tx: &mut Transaction<D>,
|
||||
) -> Result<()> {
|
||||
if self.flags & PROT_WRITE == PROT_WRITE {
|
||||
let mtime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
tx.write_node(
|
||||
node_ptr,
|
||||
offset,
|
||||
unsafe { core::slice::from_raw_parts(base.add(offset as usize), size) },
|
||||
mtime.as_secs(),
|
||||
mtime.subsec_nanos(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileResource {
|
||||
path: String,
|
||||
parent_ptr_opt: Option<TreePtr<Node>>,
|
||||
node_ptr: TreePtr<Node>,
|
||||
flags: usize,
|
||||
uid: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FileMmapInfo {
|
||||
base: *mut u8,
|
||||
size: usize,
|
||||
pub ranges: RangeTree<Fmap>,
|
||||
pub open_fds: usize,
|
||||
}
|
||||
|
||||
impl FileMmapInfo {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base: core::ptr::null_mut(),
|
||||
size: 0,
|
||||
ranges: RangeTree::new(),
|
||||
open_fds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_use(&self) -> bool {
|
||||
self.open_fds > 0 || !self.ranges.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FileMmapInfo {
|
||||
fn drop(&mut self) {
|
||||
if self.in_use() {
|
||||
log::error!("FileMmapInfo dropped while in use");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileResource {
|
||||
pub fn new(
|
||||
path: String,
|
||||
parent_ptr_opt: Option<TreePtr<Node>>,
|
||||
node_ptr: TreePtr<Node>,
|
||||
flags: usize,
|
||||
uid: u32,
|
||||
) -> FileResource {
|
||||
FileResource {
|
||||
path,
|
||||
parent_ptr_opt,
|
||||
node_ptr,
|
||||
flags,
|
||||
uid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Disk> Resource<D> for FileResource {
|
||||
fn parent_ptr_opt(&self) -> Option<TreePtr<Node>> {
|
||||
self.parent_ptr_opt
|
||||
}
|
||||
|
||||
fn node_ptr(&self) -> TreePtr<Node> {
|
||||
self.node_ptr
|
||||
}
|
||||
|
||||
fn uid(&self) -> u32 {
|
||||
self.uid
|
||||
}
|
||||
|
||||
fn set_path(&mut self, path: &str) {
|
||||
self.path = path.to_string();
|
||||
}
|
||||
|
||||
fn read(&mut self, buf: &mut [u8], offset: u64, tx: &mut Transaction<D>) -> Result<usize> {
|
||||
if self.flags & O_ACCMODE != O_RDWR && self.flags & O_ACCMODE != O_RDONLY {
|
||||
return Err(Error::new(EBADF));
|
||||
}
|
||||
let atime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
tx.read_node(
|
||||
self.node_ptr,
|
||||
offset,
|
||||
buf,
|
||||
atime.as_secs(),
|
||||
atime.subsec_nanos(),
|
||||
)
|
||||
}
|
||||
|
||||
fn write(&mut self, buf: &[u8], offset: u64, tx: &mut Transaction<D>) -> Result<usize> {
|
||||
if self.flags & O_ACCMODE != O_RDWR && self.flags & O_ACCMODE != O_WRONLY {
|
||||
return Err(Error::new(EBADF));
|
||||
}
|
||||
let effective_offset = if self.flags & O_APPEND == O_APPEND {
|
||||
let node = tx.read_tree(self.node_ptr)?;
|
||||
node.data().size()
|
||||
} else {
|
||||
offset
|
||||
};
|
||||
let mtime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
tx.write_node(
|
||||
self.node_ptr,
|
||||
effective_offset,
|
||||
buf,
|
||||
mtime.as_secs(),
|
||||
mtime.subsec_nanos(),
|
||||
)
|
||||
}
|
||||
|
||||
fn fsize(&mut self, tx: &mut Transaction<D>) -> Result<u64> {
|
||||
let node = tx.read_tree(self.node_ptr)?;
|
||||
Ok(node.data().size())
|
||||
}
|
||||
|
||||
fn fmap(
|
||||
&mut self,
|
||||
fmaps: &mut Fmaps,
|
||||
flags: MapFlags,
|
||||
unaligned_size: usize,
|
||||
offset: u64,
|
||||
tx: &mut Transaction<D>,
|
||||
) -> Result<usize> {
|
||||
//dbg!(&self.fmaps);
|
||||
let accmode = self.flags & O_ACCMODE;
|
||||
if flags.contains(PROT_READ) && !(accmode == O_RDWR || accmode == O_RDONLY) {
|
||||
return Err(Error::new(EBADF));
|
||||
}
|
||||
if flags.contains(PROT_WRITE) && !(accmode == O_RDWR || accmode == O_WRONLY) {
|
||||
return Err(Error::new(EBADF));
|
||||
}
|
||||
|
||||
let aligned_size = unaligned_size.next_multiple_of(PAGE_SIZE);
|
||||
|
||||
// TODO: PROT_EXEC? It is however unenforcable without restricting anonymous mmap, since a
|
||||
// program can always map anonymous RW-, read from a file, then remap as R-E. But it might
|
||||
// be usable as a hint, prohibiting direct executable mmaps at least.
|
||||
|
||||
// TODO: Pass entry directory to Resource trait functions, since the node_ptr can be
|
||||
// obtained by the caller.
|
||||
let fmap_info = fmaps
|
||||
.get_mut(&self.node_ptr.id())
|
||||
.ok_or(Error::new(EBADFD))?;
|
||||
|
||||
if !fmap_info.in_use() {
|
||||
// Notify filesystem of open
|
||||
tx.on_open_node(self.node_ptr)?;
|
||||
}
|
||||
|
||||
let new_size = (offset as usize + aligned_size).next_multiple_of(PAGE_SIZE);
|
||||
if new_size > fmap_info.size {
|
||||
fmap_info.base = if fmap_info.base.is_null() {
|
||||
unsafe {
|
||||
libredox::call::mmap(MmapArgs {
|
||||
length: new_size,
|
||||
// PRIVATE/SHARED doesn't matter once the pages are passed in the fmap
|
||||
// handler.
|
||||
prot: libredox::flag::PROT_READ | libredox::flag::PROT_WRITE,
|
||||
flags: libredox::flag::MAP_PRIVATE,
|
||||
|
||||
offset: 0,
|
||||
fd: !0,
|
||||
addr: core::ptr::null_mut(),
|
||||
})? as *mut u8
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
syscall::syscall5(
|
||||
syscall::SYS_MREMAP,
|
||||
fmap_info.base as usize,
|
||||
fmap_info.size,
|
||||
0,
|
||||
new_size,
|
||||
syscall::MremapFlags::empty().bits() | (PROT_READ | PROT_WRITE).bits(),
|
||||
)? as *mut u8
|
||||
}
|
||||
};
|
||||
fmap_info.size = new_size;
|
||||
}
|
||||
|
||||
let affected_fmaps = fmap_info
|
||||
.ranges
|
||||
.remove_and_unused(offset..offset + aligned_size as u64);
|
||||
|
||||
for (range, v_opt) in affected_fmaps {
|
||||
//dbg!(&range);
|
||||
if let Some(mut fmap) = v_opt {
|
||||
fmap.rc += 1;
|
||||
fmap.flags |= flags;
|
||||
//FIXME: Use result?
|
||||
let _ = fmap_info
|
||||
.ranges
|
||||
.insert(range.start, range.end - range.start, fmap);
|
||||
} else {
|
||||
let map = unsafe {
|
||||
Fmap::new(
|
||||
self.node_ptr,
|
||||
flags,
|
||||
unaligned_size,
|
||||
offset,
|
||||
fmap_info.base,
|
||||
tx,
|
||||
)?
|
||||
};
|
||||
//FIXME: Use result?
|
||||
let _ = fmap_info.ranges.insert(offset, aligned_size as u64, map);
|
||||
}
|
||||
}
|
||||
//dbg!(&self.fmaps);
|
||||
|
||||
Ok(fmap_info.base as usize + offset as usize)
|
||||
}
|
||||
|
||||
fn funmap(
|
||||
&mut self,
|
||||
fmaps: &mut Fmaps,
|
||||
offset: u64,
|
||||
size: usize,
|
||||
tx: &mut Transaction<D>,
|
||||
) -> Result<()> {
|
||||
let fmap_info = fmaps
|
||||
.get_mut(&self.node_ptr.id())
|
||||
.ok_or(Error::new(EBADFD))?;
|
||||
|
||||
//dbg!(&self.fmaps);
|
||||
//dbg!(self.fmaps.conflicts(offset..offset + size as u64).collect::<Vec<_>>());
|
||||
#[allow(unused_mut)]
|
||||
let mut affected_fmaps = fmap_info.ranges.remove(offset..offset + size as u64);
|
||||
|
||||
for (range, mut fmap) in affected_fmaps {
|
||||
fmap.rc = fmap.rc.checked_sub(1).unwrap();
|
||||
|
||||
//log::info!("SYNCING {}..{}", range.start, range.end);
|
||||
unsafe {
|
||||
fmap.sync(
|
||||
self.node_ptr,
|
||||
fmap_info.base,
|
||||
range.start,
|
||||
(range.end - range.start) as usize,
|
||||
tx,
|
||||
)?;
|
||||
}
|
||||
|
||||
if fmap.rc > 0 {
|
||||
//FIXME: Use result?
|
||||
let _ = fmap_info
|
||||
.ranges
|
||||
.insert(range.start, range.end - range.start, fmap);
|
||||
}
|
||||
}
|
||||
//dbg!(&self.fmaps);
|
||||
|
||||
// Allow release of node if not in use anymore
|
||||
if !fmap_info.in_use() {
|
||||
// Notify filesystem of close
|
||||
tx.on_close_node(self.node_ptr)?;
|
||||
|
||||
/*TODO: leaks memory, but why?
|
||||
// Remove from fmaps list
|
||||
fmaps.remove(&self.node_ptr.id());
|
||||
*/
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fcntl(&mut self, cmd: usize, arg: usize) -> Result<usize> {
|
||||
match cmd {
|
||||
F_GETFL => Ok(self.flags),
|
||||
F_SETFL => {
|
||||
self.flags = (self.flags & O_ACCMODE) | (arg & !O_ACCMODE);
|
||||
Ok(0)
|
||||
}
|
||||
_ => Err(Error::new(EINVAL)),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn sync(&mut self, fmaps: &mut Fmaps, tx: &mut Transaction<D>) -> Result<()> {
|
||||
if let Some(fmap_info) = fmaps.get_mut(&self.node_ptr.id()) {
|
||||
for (range, fmap) in fmap_info.ranges.iter_mut() {
|
||||
unsafe {
|
||||
fmap.sync(
|
||||
self.node_ptr,
|
||||
fmap_info.base,
|
||||
range.start,
|
||||
(range.end - range.start) as usize,
|
||||
tx,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn truncate(&mut self, len: u64, tx: &mut Transaction<D>) -> Result<()> {
|
||||
if self.flags & O_ACCMODE == O_RDWR || self.flags & O_ACCMODE == O_WRONLY {
|
||||
let mtime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
tx.truncate_node(self.node_ptr, len, mtime.as_secs(), mtime.subsec_nanos())?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(EBADF))
|
||||
}
|
||||
}
|
||||
|
||||
fn utimens(&mut self, times: &[TimeSpec], tx: &mut Transaction<D>) -> Result<()> {
|
||||
let mut node = tx.read_tree(self.node_ptr)?;
|
||||
|
||||
if node.data().uid() == self.uid || self.uid == 0 {
|
||||
if let &[atime, mtime] = times {
|
||||
let mut node_changed = false;
|
||||
|
||||
let old_mtime = node.data().mtime();
|
||||
let new_mtime = (mtime.tv_sec as u64, mtime.tv_nsec as u32);
|
||||
if old_mtime != new_mtime {
|
||||
node.data_mut().set_mtime(new_mtime.0, new_mtime.1);
|
||||
node_changed = true;
|
||||
}
|
||||
|
||||
let old_atime = node.data().atime();
|
||||
let new_atime = (atime.tv_sec as u64, atime.tv_nsec as u32);
|
||||
if old_atime != new_atime {
|
||||
node.data_mut().set_atime(new_atime.0, new_atime.1);
|
||||
node_changed = true;
|
||||
}
|
||||
|
||||
if node_changed {
|
||||
tx.sync_tree(node)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(EPERM))
|
||||
}
|
||||
}
|
||||
|
||||
fn getdents<'buf>(
|
||||
&mut self,
|
||||
_buf: DirentBuf<&'buf mut [u8]>,
|
||||
_opaque_offset: u64,
|
||||
_tx: &mut Transaction<D>,
|
||||
) -> Result<DirentBuf<&'buf mut [u8]>> {
|
||||
Err(Error::new(ENOTDIR))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FileResource {
|
||||
fn drop(&mut self) {
|
||||
/*
|
||||
if !self.fmaps.is_empty() {
|
||||
eprintln!(
|
||||
"redoxfs: file {} still has {} fmaps!",
|
||||
self.path,
|
||||
self.fmaps.len()
|
||||
);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
impl range_tree::Value for Fmap {
|
||||
type K = u64;
|
||||
|
||||
fn try_merge_forward(self, other: &Self) -> core::result::Result<Self, Self> {
|
||||
if self.rc == other.rc && self.flags == other.flags && self.last_page_tail == 0 {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
fn try_merge_backwards(self, other: &Self) -> core::result::Result<Self, Self> {
|
||||
if self.rc == other.rc && self.flags == other.flags && other.last_page_tail == 0 {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
fn split(
|
||||
self,
|
||||
prev_range: Option<core::ops::Range<Self::K>>,
|
||||
range: core::ops::Range<Self::K>,
|
||||
next_range: Option<core::ops::Range<Self::K>>,
|
||||
) -> (Option<Self>, Self, Option<Self>) {
|
||||
(
|
||||
prev_range.map(|_range| Fmap {
|
||||
rc: self.rc,
|
||||
flags: self.flags,
|
||||
last_page_tail: 0,
|
||||
}),
|
||||
Fmap {
|
||||
rc: self.rc,
|
||||
flags: self.flags,
|
||||
last_page_tail: if next_range.is_none() {
|
||||
self.last_page_tail
|
||||
} else {
|
||||
0
|
||||
},
|
||||
},
|
||||
next_range.map(|_range| Fmap {
|
||||
rc: self.rc,
|
||||
flags: self.flags,
|
||||
last_page_tail: self.last_page_tail,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
use std::{io, path::Path};
|
||||
|
||||
use crate::{filesystem, Disk};
|
||||
|
||||
pub fn mount<D, P, T, F>(
|
||||
mut _filesystem: filesystem::FileSystem<D>,
|
||||
_mountpoint: P,
|
||||
_callback: F,
|
||||
) -> io::Result<T>
|
||||
where
|
||||
D: Disk,
|
||||
P: AsRef<Path>,
|
||||
F: FnOnce(&Path) -> T,
|
||||
{
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"FUSE mount feature is disabled",
|
||||
))
|
||||
}
|
||||
+584
@@ -0,0 +1,584 @@
|
||||
use core::{fmt, mem, ops, slice};
|
||||
use endian_num::Le;
|
||||
|
||||
use crate::{BlockLevel, BlockList, BlockPtr, BlockTrait, RecordRaw, BLOCK_SIZE, RECORD_LEVEL};
|
||||
|
||||
bitflags::bitflags! {
|
||||
pub struct NodeFlags: u32 {
|
||||
const INLINE_DATA = 0x1;
|
||||
}
|
||||
}
|
||||
|
||||
/// An index into a [`Node`]'s block table.
|
||||
pub enum NodeLevel {
|
||||
L0(usize),
|
||||
L1(usize, usize),
|
||||
L2(usize, usize, usize),
|
||||
L3(usize, usize, usize, usize),
|
||||
L4(usize, usize, usize, usize, usize),
|
||||
}
|
||||
|
||||
impl NodeLevel {
|
||||
// Warning: this uses constant record offsets, make sure to sync with Node
|
||||
|
||||
/// Return the [`NodeLevel`] of the record with the given index.
|
||||
/// - the first 128 are level 0,
|
||||
/// - the next 64*256 are level 1,
|
||||
/// - ...and so on.
|
||||
pub fn new(mut record_offset: u64) -> Option<Self> {
|
||||
// 1 << 8 = 256, this is the number of entries in a BlockList
|
||||
const SHIFT: u64 = 8;
|
||||
const NUM: u64 = 1 << SHIFT;
|
||||
const MASK: u64 = NUM - 1;
|
||||
|
||||
const L0: u64 = 128;
|
||||
if record_offset < L0 {
|
||||
return Some(Self::L0((record_offset & MASK) as usize));
|
||||
} else {
|
||||
record_offset -= L0;
|
||||
}
|
||||
|
||||
const L1: u64 = 64 * NUM;
|
||||
if record_offset < L1 {
|
||||
return Some(Self::L1(
|
||||
((record_offset >> SHIFT) & MASK) as usize,
|
||||
(record_offset & MASK) as usize,
|
||||
));
|
||||
} else {
|
||||
record_offset -= L1;
|
||||
}
|
||||
|
||||
const L2: u64 = 32 * NUM * NUM;
|
||||
if record_offset < L2 {
|
||||
return Some(Self::L2(
|
||||
((record_offset >> (2 * SHIFT)) & MASK) as usize,
|
||||
((record_offset >> SHIFT) & MASK) as usize,
|
||||
(record_offset & MASK) as usize,
|
||||
));
|
||||
} else {
|
||||
record_offset -= L2;
|
||||
}
|
||||
|
||||
const L3: u64 = 16 * NUM * NUM * NUM;
|
||||
if record_offset < L3 {
|
||||
return Some(Self::L3(
|
||||
((record_offset >> (3 * SHIFT)) & MASK) as usize,
|
||||
((record_offset >> (2 * SHIFT)) & MASK) as usize,
|
||||
((record_offset >> SHIFT) & MASK) as usize,
|
||||
(record_offset & MASK) as usize,
|
||||
));
|
||||
} else {
|
||||
record_offset -= L3;
|
||||
}
|
||||
|
||||
const L4: u64 = 12 * NUM * NUM * NUM * NUM;
|
||||
if record_offset < L4 {
|
||||
Some(Self::L4(
|
||||
((record_offset >> (4 * SHIFT)) & MASK) as usize,
|
||||
((record_offset >> (3 * SHIFT)) & MASK) as usize,
|
||||
((record_offset >> (2 * SHIFT)) & MASK) as usize,
|
||||
((record_offset >> SHIFT) & MASK) as usize,
|
||||
(record_offset & MASK) as usize,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type BlockListL1 = BlockList<RecordRaw>;
|
||||
type BlockListL2 = BlockList<BlockListL1>;
|
||||
type BlockListL3 = BlockList<BlockListL2>;
|
||||
type BlockListL4 = BlockList<BlockListL3>;
|
||||
|
||||
#[repr(C, packed)]
|
||||
pub struct NodeLevelData {
|
||||
/// The first 128 blocks of this file.
|
||||
///
|
||||
/// Total size: 128 * RECORD_SIZE (16 MiB, 128 KiB each)
|
||||
pub level0: [BlockPtr<RecordRaw>; 128],
|
||||
|
||||
/// The next 64 * 256 blocks of this file,
|
||||
/// stored behind 64 level one tables.
|
||||
///
|
||||
/// Total size: 64 * 256 * RECORD_SIZE (2 GiB, 32 MiB each)
|
||||
pub level1: [BlockPtr<BlockListL1>; 64],
|
||||
|
||||
/// The next 32 * 256 * 256 blocks of this file,
|
||||
/// stored behind 32 level two tables.
|
||||
/// Each level two table points to 256 level one tables.
|
||||
///
|
||||
/// Total size: 32 * 256 * 256 * RECORD_SIZE (256 GiB, 8 GiB each)
|
||||
pub level2: [BlockPtr<BlockListL2>; 32],
|
||||
|
||||
/// The next 16 * 256 * 256 * 256 blocks of this file,
|
||||
/// stored behind 16 level three tables.
|
||||
///
|
||||
/// Total size: 16 * 256 * 256 * 256 * RECORD_SIZE (32 TiB, 2 TiB each)
|
||||
pub level3: [BlockPtr<BlockListL3>; 16],
|
||||
|
||||
/// The next 8 * 256 * 256 * 256 * 256 blocks of this file,
|
||||
/// stored behind 8 level four tables.
|
||||
///
|
||||
/// Total size: 8 * 256 * 256 * 256 * 256 * RECORD_SIZE (4 PiB, 512 TiB each)
|
||||
pub level4: [BlockPtr<BlockListL4>; 8],
|
||||
}
|
||||
|
||||
impl Default for NodeLevelData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level0: [BlockPtr::default(); 128],
|
||||
level1: [BlockPtr::default(); 64],
|
||||
level2: [BlockPtr::default(); 32],
|
||||
level3: [BlockPtr::default(); 16],
|
||||
level4: [BlockPtr::default(); 8],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A file/folder node
|
||||
#[repr(C, packed)]
|
||||
pub struct Node {
|
||||
/// This node's type & permissions.
|
||||
/// - four most significant bits are the node's type
|
||||
/// - next four bits are permissions for the node's user
|
||||
/// - next four bits are permissions for the node's group
|
||||
/// - four least significant bits are permissions for everyone else
|
||||
pub mode: Le<u16>,
|
||||
|
||||
/// The uid that owns this file
|
||||
pub uid: Le<u32>,
|
||||
|
||||
/// The gid that owns this file
|
||||
pub gid: Le<u32>,
|
||||
|
||||
/// The number of hard links to this file
|
||||
pub links: Le<u32>,
|
||||
|
||||
/// The length of this file, in bytes
|
||||
pub size: Le<u64>,
|
||||
/// The disk usage of this file, in blocks
|
||||
pub blocks: Le<u64>,
|
||||
|
||||
/// Creation time
|
||||
pub ctime: Le<u64>,
|
||||
pub ctime_nsec: Le<u32>,
|
||||
|
||||
/// Modification time
|
||||
pub mtime: Le<u64>,
|
||||
pub mtime_nsec: Le<u32>,
|
||||
|
||||
/// Access time
|
||||
pub atime: Le<u64>,
|
||||
pub atime_nsec: Le<u32>,
|
||||
|
||||
/// Record level
|
||||
pub record_level: Le<u32>,
|
||||
|
||||
/// Flags
|
||||
pub flags: Le<u32>,
|
||||
|
||||
/// Padding
|
||||
pub padding: [u8; BLOCK_SIZE as usize - 4042],
|
||||
|
||||
/// Level data, should not be used directly so inline data can be supported
|
||||
pub(crate) level_data: NodeLevelData,
|
||||
}
|
||||
|
||||
unsafe impl BlockTrait for Node {
|
||||
fn empty(level: BlockLevel) -> Option<Self> {
|
||||
if level.0 == 0 {
|
||||
Some(Self::default())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Node {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: 0.into(),
|
||||
uid: 0.into(),
|
||||
gid: 0.into(),
|
||||
links: 0.into(),
|
||||
size: 0.into(),
|
||||
// This node counts as a block
|
||||
blocks: 1.into(),
|
||||
ctime: 0.into(),
|
||||
ctime_nsec: 0.into(),
|
||||
mtime: 0.into(),
|
||||
mtime_nsec: 0.into(),
|
||||
atime: 0.into(),
|
||||
atime_nsec: 0.into(),
|
||||
record_level: 0.into(),
|
||||
flags: 0.into(),
|
||||
padding: [0; BLOCK_SIZE as usize - 4042],
|
||||
level_data: NodeLevelData::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub const MODE_TYPE: u16 = 0xF000;
|
||||
pub const MODE_FILE: u16 = 0x8000;
|
||||
pub const MODE_DIR: u16 = 0x4000;
|
||||
pub const MODE_SYMLINK: u16 = 0xA000;
|
||||
pub const MODE_SOCK: u16 = 0xC000;
|
||||
|
||||
/// Mask for node permission bits
|
||||
pub const MODE_PERM: u16 = 0x0FFF;
|
||||
pub const MODE_EXEC: u16 = 0o1;
|
||||
pub const MODE_WRITE: u16 = 0o2;
|
||||
pub const MODE_READ: u16 = 0o4;
|
||||
|
||||
/// Create a new, empty node with the given metadata
|
||||
pub fn new(mode: u16, uid: u32, gid: u32, ctime: u64, ctime_nsec: u32) -> Self {
|
||||
Self {
|
||||
mode: mode.into(),
|
||||
uid: uid.into(),
|
||||
gid: gid.into(),
|
||||
links: 0.into(),
|
||||
ctime: ctime.into(),
|
||||
ctime_nsec: ctime_nsec.into(),
|
||||
mtime: ctime.into(),
|
||||
mtime_nsec: ctime_nsec.into(),
|
||||
atime: ctime.into(),
|
||||
atime_nsec: ctime_nsec.into(),
|
||||
record_level: if mode & Self::MODE_TYPE == Self::MODE_FILE {
|
||||
// Files take on record level
|
||||
RECORD_LEVEL as u32
|
||||
} else {
|
||||
// Folders do not
|
||||
0
|
||||
}
|
||||
.into(),
|
||||
flags: if mode & Self::MODE_TYPE == Self::MODE_DIR {
|
||||
// Directories must not use inline data (until h-tree supports it)
|
||||
NodeFlags::empty()
|
||||
} else {
|
||||
NodeFlags::INLINE_DATA
|
||||
}
|
||||
.bits()
|
||||
.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// This node's type & permissions.
|
||||
/// - four most significant bits are the node's type
|
||||
/// - next four bits are permissions for the node's user
|
||||
/// - next four bits are permissions for the node's group
|
||||
/// - four least significant bits are permissions for everyone else
|
||||
pub fn mode(&self) -> u16 {
|
||||
self.mode.to_ne()
|
||||
}
|
||||
|
||||
/// The uid that owns this file
|
||||
pub fn uid(&self) -> u32 {
|
||||
self.uid.to_ne()
|
||||
}
|
||||
|
||||
/// The gid that owns this file
|
||||
pub fn gid(&self) -> u32 {
|
||||
self.gid.to_ne()
|
||||
}
|
||||
|
||||
/// The number of links to this file
|
||||
/// (directory entries, symlinks, etc)
|
||||
pub fn links(&self) -> u32 {
|
||||
self.links.to_ne()
|
||||
}
|
||||
|
||||
/// The length of this file, in bytes.
|
||||
pub fn size(&self) -> u64 {
|
||||
self.size.to_ne()
|
||||
}
|
||||
|
||||
/// The disk usage of this file, in blocks.
|
||||
pub fn blocks(&self) -> u64 {
|
||||
self.blocks.to_ne()
|
||||
}
|
||||
|
||||
pub fn ctime(&self) -> (u64, u32) {
|
||||
(self.ctime.to_ne(), self.ctime_nsec.to_ne())
|
||||
}
|
||||
|
||||
pub fn mtime(&self) -> (u64, u32) {
|
||||
(self.mtime.to_ne(), self.mtime_nsec.to_ne())
|
||||
}
|
||||
|
||||
pub fn atime(&self) -> (u64, u32) {
|
||||
(self.atime.to_ne(), self.atime_nsec.to_ne())
|
||||
}
|
||||
|
||||
pub fn record_level(&self) -> BlockLevel {
|
||||
BlockLevel(self.record_level.to_ne() as usize)
|
||||
}
|
||||
|
||||
pub fn flags(&self) -> NodeFlags {
|
||||
NodeFlags::from_bits_retain(self.flags.to_ne())
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, mode: u16) {
|
||||
self.mode = mode.into();
|
||||
}
|
||||
|
||||
pub fn set_uid(&mut self, uid: u32) {
|
||||
self.uid = uid.into();
|
||||
}
|
||||
|
||||
pub fn set_gid(&mut self, gid: u32) {
|
||||
self.gid = gid.into();
|
||||
}
|
||||
|
||||
pub fn set_links(&mut self, links: u32) {
|
||||
self.links = links.into();
|
||||
}
|
||||
|
||||
pub fn set_size(&mut self, size: u64) {
|
||||
self.size = size.into();
|
||||
}
|
||||
|
||||
pub fn set_blocks(&mut self, blocks: u64) {
|
||||
self.blocks = blocks.into();
|
||||
}
|
||||
|
||||
pub fn set_mtime(&mut self, mtime: u64, mtime_nsec: u32) {
|
||||
self.mtime = mtime.into();
|
||||
self.mtime_nsec = mtime_nsec.into();
|
||||
}
|
||||
|
||||
pub fn set_atime(&mut self, atime: u64, atime_nsec: u32) {
|
||||
self.atime = atime.into();
|
||||
self.atime_nsec = atime_nsec.into();
|
||||
}
|
||||
|
||||
pub fn set_flags(&mut self, flags: NodeFlags) {
|
||||
self.flags = flags.bits().into();
|
||||
}
|
||||
|
||||
pub fn has_inline_data(&self) -> bool {
|
||||
self.flags().contains(NodeFlags::INLINE_DATA)
|
||||
}
|
||||
|
||||
pub fn inline_data(&self) -> Option<&[u8]> {
|
||||
if self.has_inline_data() {
|
||||
Some(unsafe {
|
||||
slice::from_raw_parts(
|
||||
&self.level_data as *const NodeLevelData as *const u8,
|
||||
mem::size_of::<NodeLevelData>(),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inline_data_mut(&mut self) -> Option<&mut [u8]> {
|
||||
if self.has_inline_data() {
|
||||
Some(unsafe {
|
||||
slice::from_raw_parts_mut(
|
||||
&mut self.level_data as *mut NodeLevelData as *mut u8,
|
||||
mem::size_of::<NodeLevelData>(),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn level_data(&self) -> Option<&NodeLevelData> {
|
||||
if !self.has_inline_data() {
|
||||
Some(&self.level_data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn level_data_mut(&mut self) -> Option<&mut NodeLevelData> {
|
||||
if !self.has_inline_data() {
|
||||
Some(&mut self.level_data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dir(&self) -> bool {
|
||||
self.mode() & Self::MODE_TYPE == Self::MODE_DIR
|
||||
}
|
||||
|
||||
pub fn is_file(&self) -> bool {
|
||||
self.mode() & Self::MODE_TYPE == Self::MODE_FILE
|
||||
}
|
||||
|
||||
pub fn is_symlink(&self) -> bool {
|
||||
self.mode() & Self::MODE_TYPE == Self::MODE_SYMLINK
|
||||
}
|
||||
|
||||
pub fn is_sock(&self) -> bool {
|
||||
self.mode() & Self::MODE_SOCK == Self::MODE_SOCK
|
||||
}
|
||||
|
||||
/// Tests if UID is the owner of that file, only true when uid=0 or when the UID stored in metadata is equal to the UID you supply
|
||||
pub fn owner(&self, uid: u32) -> bool {
|
||||
uid == 0 || self.uid() == uid
|
||||
}
|
||||
|
||||
/// Tests if the current user has enough permissions to view the file, op is the operation,
|
||||
/// like read and write, these modes are MODE_EXEC, MODE_READ, and MODE_WRITE
|
||||
pub fn permission(&self, uid: u32, gid: u32, op: u16) -> bool {
|
||||
let mut perm = self.mode() & 0o7;
|
||||
if self.uid() == uid {
|
||||
// If self.mode is 101100110, >> 6 would be 000000101
|
||||
// 0o7 is octal for 111, or, when expanded to 9 digits is 000000111
|
||||
perm |= (self.mode() >> 6) & 0o7;
|
||||
// Since we erased the GID and OTHER bits when >>6'ing, |= will keep those bits in place.
|
||||
}
|
||||
if self.gid() == gid || gid == 0 {
|
||||
perm |= (self.mode() >> 3) & 0o7;
|
||||
}
|
||||
if uid == 0 {
|
||||
//set the `other` bits to 111
|
||||
perm |= 0o7;
|
||||
}
|
||||
perm & op == op
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Node {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mode = self.mode;
|
||||
let uid = self.uid;
|
||||
let gid = self.gid;
|
||||
let links = self.links;
|
||||
let size = self.size;
|
||||
let blocks = self.blocks;
|
||||
let ctime = self.ctime;
|
||||
let ctime_nsec = self.ctime_nsec;
|
||||
let mtime = self.mtime;
|
||||
let mtime_nsec = self.mtime_nsec;
|
||||
let atime = self.atime;
|
||||
let atime_nsec = self.atime_nsec;
|
||||
f.debug_struct("Node")
|
||||
.field("mode", &mode)
|
||||
.field("uid", &uid)
|
||||
.field("gid", &gid)
|
||||
.field("links", &links)
|
||||
.field("size", &size)
|
||||
.field("blocks", &blocks)
|
||||
.field("ctime", &ctime)
|
||||
.field("ctime_nsec", &ctime_nsec)
|
||||
.field("mtime", &mtime)
|
||||
.field("mtime_nsec", &mtime_nsec)
|
||||
.field("atime", &atime)
|
||||
.field("atime_nsec", &atime_nsec)
|
||||
//TODO: level0/1/2/3
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for Node {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts(self as *const Node as *const u8, mem::size_of::<Node>())
|
||||
as &[u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for Node {
|
||||
fn deref_mut(&mut self) -> &mut [u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts_mut(self as *mut Node as *mut u8, mem::size_of::<Node>())
|
||||
as &mut [u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_size_test() {
|
||||
assert_eq!(mem::size_of::<Node>(), crate::BLOCK_SIZE as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_inline_data_test() {
|
||||
let mut node = Node::default();
|
||||
assert!(!node.has_inline_data());
|
||||
assert!(node.inline_data().is_none());
|
||||
assert!(node.inline_data_mut().is_none());
|
||||
assert!(node.level_data().is_some());
|
||||
assert!(node.level_data_mut().is_some());
|
||||
|
||||
node.set_flags(NodeFlags::INLINE_DATA);
|
||||
assert!(node.has_inline_data());
|
||||
assert!(node.level_data().is_none());
|
||||
assert!(node.level_data_mut().is_none());
|
||||
|
||||
let node_addr = &node as *const Node as usize;
|
||||
let meta_size = 128;
|
||||
{
|
||||
let inline_data = node.inline_data().unwrap();
|
||||
let inline_data_addr = inline_data.as_ptr() as usize;
|
||||
assert_eq!(node_addr + meta_size, inline_data_addr);
|
||||
assert_eq!(inline_data.len(), (crate::BLOCK_SIZE as usize) - meta_size);
|
||||
}
|
||||
{
|
||||
let inline_data = node.inline_data_mut().unwrap();
|
||||
let inline_data_addr = inline_data.as_ptr() as usize;
|
||||
assert_eq!(node_addr + meta_size, inline_data_addr);
|
||||
assert_eq!(inline_data.len(), (crate::BLOCK_SIZE as usize) - meta_size);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(kani)]
|
||||
#[kani::proof]
|
||||
fn check_node_level() {
|
||||
let offset = kani::any();
|
||||
NodeLevel::new(offset);
|
||||
}
|
||||
|
||||
#[cfg(kani)]
|
||||
#[kani::proof]
|
||||
fn check_node_perms() {
|
||||
let mode = 0o750;
|
||||
|
||||
let uid = kani::any();
|
||||
let gid = kani::any();
|
||||
|
||||
let ctime = kani::any();
|
||||
let ctime_nsec = kani::any();
|
||||
|
||||
let node = Node::new(mode, uid, gid, ctime, ctime_nsec);
|
||||
|
||||
let root_uid = 0;
|
||||
let root_gid = 0;
|
||||
|
||||
let other_uid = kani::any();
|
||||
kani::assume(other_uid != uid);
|
||||
kani::assume(other_uid != root_uid);
|
||||
let other_gid = kani::any();
|
||||
kani::assume(other_gid != gid);
|
||||
kani::assume(other_gid != root_gid);
|
||||
|
||||
assert!(node.owner(uid));
|
||||
assert!(node.permission(uid, gid, 0o7));
|
||||
assert!(node.permission(uid, gid, 0o5));
|
||||
assert!(node.permission(uid, other_gid, 0o7));
|
||||
assert!(node.permission(uid, other_gid, 0o5));
|
||||
assert!(!node.permission(other_uid, gid, 0o7));
|
||||
assert!(node.permission(other_uid, gid, 0o5));
|
||||
|
||||
assert!(node.owner(root_uid));
|
||||
assert!(node.permission(root_uid, root_gid, 0o7));
|
||||
assert!(node.permission(root_uid, root_gid, 0o5));
|
||||
assert!(node.permission(root_uid, other_gid, 0o7));
|
||||
assert!(node.permission(root_uid, other_gid, 0o5));
|
||||
assert!(!node.permission(other_uid, root_gid, 0o7));
|
||||
assert!(node.permission(other_uid, root_gid, 0o5));
|
||||
|
||||
assert!(!node.owner(other_uid));
|
||||
assert!(!node.permission(other_uid, other_gid, 0o7));
|
||||
assert!(!node.permission(other_uid, other_gid, 0o5));
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use alloc::{boxed::Box, vec};
|
||||
use core::ops;
|
||||
|
||||
use crate::{BlockLevel, BlockTrait, RECORD_LEVEL};
|
||||
|
||||
//TODO: this is a box to prevent stack overflows
|
||||
#[derive(Clone)]
|
||||
pub struct RecordRaw(pub(crate) Box<[u8]>);
|
||||
|
||||
unsafe impl BlockTrait for RecordRaw {
|
||||
fn empty(level: BlockLevel) -> Option<Self> {
|
||||
if level.0 <= RECORD_LEVEL {
|
||||
Some(Self(vec![0; level.bytes() as usize].into_boxed_slice()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for RecordRaw {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for RecordRaw {
|
||||
fn deref_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_raw_size_test() {
|
||||
for level_i in 0..RECORD_LEVEL {
|
||||
let level = BlockLevel(level_i);
|
||||
assert_eq!(
|
||||
RecordRaw::empty(level).unwrap().len(),
|
||||
level.bytes() as usize
|
||||
);
|
||||
}
|
||||
}
|
||||
+1104
File diff suppressed because it is too large
Load Diff
+2053
File diff suppressed because it is too large
Load Diff
+280
@@ -0,0 +1,280 @@
|
||||
use core::{marker::PhantomData, mem, ops, slice};
|
||||
use endian_num::Le;
|
||||
|
||||
use crate::{BlockLevel, BlockPtr, BlockRaw, BlockTrait};
|
||||
|
||||
// 1 << 8 = 256, this is the number of entries in a TreeList
|
||||
const TREE_LIST_SHIFT: u32 = 8;
|
||||
const TREE_LIST_ENTRIES: usize = (1 << TREE_LIST_SHIFT) - 2;
|
||||
|
||||
/// A tree with 4 levels
|
||||
pub type Tree = TreeList<TreeList<TreeList<TreeList<BlockRaw>>>>;
|
||||
|
||||
/// A [`TreePtr`] and the contents of the block it references.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct TreeData<T> {
|
||||
/// The value of the [`TreePtr`]
|
||||
id: u32,
|
||||
|
||||
// The data
|
||||
data: T,
|
||||
}
|
||||
|
||||
impl<T> TreeData<T> {
|
||||
pub fn new(id: u32, data: T) -> Self {
|
||||
Self { id, data }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &T {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn data_mut(&mut self) -> &mut T {
|
||||
&mut self.data
|
||||
}
|
||||
|
||||
pub fn into_data(self) -> T {
|
||||
self.data
|
||||
}
|
||||
|
||||
pub fn ptr(&self) -> TreePtr<T> {
|
||||
TreePtr {
|
||||
id: self.id.into(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of pointers to blocks of type `T`.
|
||||
/// This is one level of a [`Tree`], defined above.
|
||||
#[repr(C, packed)]
|
||||
pub struct TreeList<T> {
|
||||
pub ptrs: [BlockPtr<T>; TREE_LIST_ENTRIES],
|
||||
pub full_flags: [u128; 2],
|
||||
}
|
||||
|
||||
impl<T> TreeList<T> {
|
||||
pub fn tree_list_is_full(&self) -> bool {
|
||||
self.full_flags[1] == u128::MAX & !(3 << 126) && self.full_flags[0] == u128::MAX
|
||||
}
|
||||
|
||||
pub fn tree_list_is_empty(&self) -> bool {
|
||||
for ptr in self.ptrs.iter() {
|
||||
if !ptr.is_null() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn branch_is_full(&self, index: usize) -> bool {
|
||||
assert!(index < TREE_LIST_ENTRIES);
|
||||
let shift = index % 128;
|
||||
let full_flags_index = index / 128;
|
||||
self.full_flags[full_flags_index] & (1 << shift) != 0
|
||||
}
|
||||
|
||||
pub fn set_branch_full(&mut self, index: usize, full: bool) {
|
||||
assert!(index < TREE_LIST_ENTRIES);
|
||||
let shift = index % 128;
|
||||
let full_flags_index = index / 128;
|
||||
|
||||
if full {
|
||||
self.full_flags[full_flags_index] |= 1 << shift;
|
||||
} else {
|
||||
self.full_flags[full_flags_index] &= !(1 << shift);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T> BlockTrait for TreeList<T> {
|
||||
fn empty(level: BlockLevel) -> Option<Self> {
|
||||
if level.0 == 0 {
|
||||
Some(Self {
|
||||
ptrs: [BlockPtr::default(); TREE_LIST_ENTRIES],
|
||||
full_flags: [0; 2],
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ops::Deref for TreeList<T> {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts(
|
||||
self as *const TreeList<T> as *const u8,
|
||||
mem::size_of::<TreeList<T>>(),
|
||||
) as &[u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ops::DerefMut for TreeList<T> {
|
||||
fn deref_mut(&mut self) -> &mut [u8] {
|
||||
unsafe {
|
||||
slice::from_raw_parts_mut(
|
||||
self as *mut TreeList<T> as *mut u8,
|
||||
mem::size_of::<TreeList<T>>(),
|
||||
) as &mut [u8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A pointer to an entry in a [`Tree`].
|
||||
#[repr(C, packed)]
|
||||
pub struct TreePtr<T> {
|
||||
id: Le<u32>,
|
||||
phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> TreePtr<T> {
|
||||
/// Get a [`TreePtr`] to the filesystem root
|
||||
/// directory's node.
|
||||
pub fn root() -> Self {
|
||||
Self::new(1)
|
||||
}
|
||||
|
||||
pub fn new(id: u32) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`TreePtr`] from [`Tree`] indices,
|
||||
/// Where `indexes` is `(i3, i2, i1, i0)`.
|
||||
/// - `i3` is the index into the level 3 table,
|
||||
/// - `i2` is the index into the level 2 table at `i3`
|
||||
/// - ...and so on.
|
||||
pub fn from_indexes(indexes: (usize, usize, usize, usize)) -> Self {
|
||||
const SHIFT: u32 = TREE_LIST_SHIFT;
|
||||
let id = ((indexes.0 << (3 * SHIFT)) as u32)
|
||||
| ((indexes.1 << (2 * SHIFT)) as u32)
|
||||
| ((indexes.2 << SHIFT) as u32)
|
||||
| (indexes.3 as u32);
|
||||
Self {
|
||||
id: id.into(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id.to_ne()
|
||||
}
|
||||
|
||||
pub fn is_null(&self) -> bool {
|
||||
self.id() == 0
|
||||
}
|
||||
|
||||
/// Get this indices of this [`TreePtr`] in a [`Tree`].
|
||||
/// Returns `(i3, i2, i1, i0)`:
|
||||
/// - `i3` is the index into the level 3 table,
|
||||
/// - `i2` is the index into the level 2 table at `i3`
|
||||
/// - ...and so on.
|
||||
pub fn indexes(&self) -> (usize, usize, usize, usize) {
|
||||
const SHIFT: u32 = TREE_LIST_SHIFT;
|
||||
const NUM: u32 = 1 << SHIFT;
|
||||
const MASK: u32 = NUM - 1;
|
||||
let id = self.id();
|
||||
|
||||
let i3 = ((id >> (3 * SHIFT)) & MASK) as usize;
|
||||
let i2 = ((id >> (2 * SHIFT)) & MASK) as usize;
|
||||
let i1 = ((id >> SHIFT) & MASK) as usize;
|
||||
let i0 = (id & MASK) as usize;
|
||||
|
||||
(i3, i2, i1, i0)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 4] {
|
||||
self.id.to_le_bytes()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: [u8; 4]) -> Self {
|
||||
let val = u32::from_le_bytes(bytes);
|
||||
Self {
|
||||
id: Le(val),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for TreePtr<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for TreePtr<T> {}
|
||||
|
||||
impl<T> Default for TreePtr<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: 0.into(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{BlockAddr, BlockData, BlockMeta};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tree_list_size_test() {
|
||||
assert_eq!(
|
||||
mem::size_of::<TreeList<BlockRaw>>(),
|
||||
crate::BLOCK_SIZE as usize
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_list_is_full_test() {
|
||||
let mut tree_list = TreeList::<BlockRaw>::empty(BlockLevel::default()).unwrap();
|
||||
assert!(!tree_list.tree_list_is_full());
|
||||
|
||||
for i in 0..TREE_LIST_ENTRIES {
|
||||
assert!(!tree_list.branch_is_full(i));
|
||||
tree_list.set_branch_full(i, true);
|
||||
assert!(tree_list.branch_is_full(i));
|
||||
}
|
||||
|
||||
assert!(tree_list.tree_list_is_full());
|
||||
|
||||
for i in 0..TREE_LIST_ENTRIES {
|
||||
assert!(tree_list.branch_is_full(i));
|
||||
tree_list.set_branch_full(i, false);
|
||||
assert!(!tree_list.branch_is_full(i));
|
||||
}
|
||||
}
|
||||
|
||||
fn mock_block(addr: u64) -> BlockPtr<BlockRaw> {
|
||||
let block_addr = unsafe { BlockAddr::new(addr, BlockMeta::default()) };
|
||||
BlockData::empty(block_addr).unwrap().create_ptr()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_list_is_empty() {
|
||||
let mut tree_list = TreeList::<BlockRaw>::empty(BlockLevel::default()).unwrap();
|
||||
assert!(tree_list.tree_list_is_empty());
|
||||
|
||||
tree_list.ptrs[3] = mock_block(123);
|
||||
assert!(!tree_list.tree_list_is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_ptr_to_and_from_bytes() {
|
||||
let ptr: TreePtr<BlockRaw> = TreePtr::new(123456);
|
||||
let bytes = ptr.to_bytes();
|
||||
let ptr2: TreePtr<BlockRaw> = TreePtr::from_bytes(bytes);
|
||||
assert_eq!(ptr.id(), ptr2.id());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::{self},
|
||||
process::{Command, ExitStatus},
|
||||
};
|
||||
|
||||
fn unmount_linux_path(mount_path: &str) -> io::Result<ExitStatus> {
|
||||
// Different distributions can have various fusermount binaries. Try
|
||||
// them all.
|
||||
let commands = ["fusermount", "fusermount3"];
|
||||
|
||||
for command in commands {
|
||||
let status = Command::new(command).arg("-u").arg(mount_path).status();
|
||||
if status.is_ok() {
|
||||
return status;
|
||||
}
|
||||
if let Err(ref e) = status {
|
||||
if e.kind() == io::ErrorKind::NotFound {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unmounting failed since no suitable command was found
|
||||
Err(std::io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!(
|
||||
"Unable to locate any fusermount binaries. Tried {:?}. Is fuse installed?",
|
||||
commands
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn unmount_path(mount_path: &str) -> Result<(), io::Error> {
|
||||
if cfg!(target_os = "redox") {
|
||||
fs::remove_dir(format!("/scheme/{}", mount_path))?
|
||||
} else {
|
||||
let status_res = if cfg!(target_os = "linux") {
|
||||
unmount_linux_path(mount_path)
|
||||
} else {
|
||||
Command::new("umount").arg(mount_path).status()
|
||||
};
|
||||
|
||||
let status = status_res?;
|
||||
if !status.success() {
|
||||
return Err(io::Error::other("redoxfs umount failed"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,29 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
IMAGE=test.bin
|
||||
CARGO_ARGS=(--release)
|
||||
TARGET=target/release
|
||||
export RUST_BACKTRACE=full
|
||||
export RUST_LOG=info
|
||||
|
||||
QEMU_ARGS=(
|
||||
-cpu max
|
||||
-machine q35
|
||||
-m 2048
|
||||
-smp 4
|
||||
-serial mon:stdio
|
||||
-netdev user,id=net0
|
||||
-device e1000,netdev=net0
|
||||
)
|
||||
function cleanup {
|
||||
sync
|
||||
fusermount -u image || true
|
||||
fusermount3 -u image || true
|
||||
}
|
||||
|
||||
if [ -e /dev/kvm ]
|
||||
then
|
||||
QEMU_ARGS+=(-accel kvm)
|
||||
fi
|
||||
trap 'cleanup' ERR
|
||||
|
||||
set -ex
|
||||
set -eEx
|
||||
|
||||
cargo build --release
|
||||
cleanup
|
||||
|
||||
rm -f "${IMAGE}"
|
||||
fallocate -l 1GiB "${IMAGE}"
|
||||
redoxer test -- --lib -- --nocapture
|
||||
cargo test --lib --no-default-features -- --nocapture
|
||||
cargo test --lib -- --nocapture
|
||||
cargo build "${CARGO_ARGS[@]}"
|
||||
|
||||
target/release/redox_installer -c res/test.toml "${IMAGE}"
|
||||
rm -f image.bin
|
||||
fallocate -l 1G image.bin
|
||||
time "${TARGET}/redoxfs-mkfs" image.bin
|
||||
|
||||
qemu-system-x86_64 "${QEMU_ARGS[@]}" -drive "file=${IMAGE},format=raw"
|
||||
mkdir -p image
|
||||
"${TARGET}/redoxfs" image.bin image
|
||||
|
||||
df -h image
|
||||
ls -lah image
|
||||
|
||||
mkdir image/test
|
||||
time cp -r src image/test/src
|
||||
|
||||
dd if=/dev/urandom of=image/test/random bs=1M count=256
|
||||
dd if=image/test/random of=/dev/null bs=1M count=256
|
||||
|
||||
time truncate --size=256M image/test/sparse
|
||||
dd if=image/test/sparse of=/dev/null bs=1M count=256
|
||||
|
||||
dd if=/dev/zero of=image/test/zero bs=1M count=256
|
||||
dd if=image/test/zero of=/dev/null bs=1M count=256
|
||||
|
||||
ls -lah image/test
|
||||
|
||||
df -h image
|
||||
|
||||
rm image/test/random
|
||||
rm image/test/sparse
|
||||
rm image/test/zero
|
||||
rm -rf image/test/src
|
||||
rmdir image/test
|
||||
|
||||
df -h image
|
||||
ls -lah image
|
||||
|
||||
cleanup
|
||||
|
||||
"${TARGET}/redoxfs" image.bin image
|
||||
|
||||
df -h image
|
||||
ls -lah image
|
||||
|
||||
cleanup
|
||||
|
||||
+639
@@ -0,0 +1,639 @@
|
||||
use core::panic::AssertUnwindSafe;
|
||||
use redoxfs::{unmount_path, DirEntry, DiskMemory, DiskSparse, FileSystem, Node, TreePtr};
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::panic::catch_unwind;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::{env, fs, time};
|
||||
|
||||
static IMAGE_SEQ: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
fn with_redoxfs<T, F>(callback: F) -> T
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
F: FnOnce(&str) -> T + Send + Sync + 'static,
|
||||
{
|
||||
let disk_path = format!("image{}.bin", IMAGE_SEQ.fetch_add(1, Relaxed));
|
||||
|
||||
{
|
||||
let disk = DiskSparse::create(dbg!(&disk_path), 1024 * 1024 * 1024).unwrap();
|
||||
let ctime = dbg!(time::SystemTime::now().duration_since(time::UNIX_EPOCH)).unwrap();
|
||||
FileSystem::create(disk, None, ctime.as_secs(), ctime.subsec_nanos()).unwrap();
|
||||
}
|
||||
let res = callback(&disk_path);
|
||||
|
||||
dbg!(fs::remove_file(dbg!(disk_path))).unwrap();
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn with_mounted<T, F>(callback: F) -> T
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
F: FnOnce(&Path) -> T + Send + Sync + 'static,
|
||||
{
|
||||
let mount_path_o = format!("image{}", IMAGE_SEQ.fetch_add(1, Relaxed));
|
||||
let mount_path = mount_path_o.clone();
|
||||
|
||||
let res = with_redoxfs(move |fs| {
|
||||
// At redox, we mount on /scheme/ path, no need an empty dir
|
||||
if cfg!(not(target_os = "redox")) {
|
||||
if !Path::new(&mount_path).exists() {
|
||||
dbg!(fs::create_dir(dbg!(&mount_path))).unwrap();
|
||||
}
|
||||
} else {
|
||||
//FIXME: cargo_bin is broken when cross compiling. This is redoxer specific workaround
|
||||
env::set_var(
|
||||
"CARGO_BIN_EXE_redoxfs",
|
||||
"/root/target/x86_64-unknown-redox/debug/redoxfs",
|
||||
);
|
||||
}
|
||||
let mut mount_cmd = Command::new(assert_cmd::cargo_bin!("redoxfs"));
|
||||
mount_cmd.arg("-d").arg(dbg!(&fs)).arg(dbg!(&mount_path));
|
||||
let mut child = mount_cmd.spawn().expect("mount failed to run");
|
||||
|
||||
let real_path = if cfg!(target_os = "redox") {
|
||||
let real_path = dbg!(Path::new("/scheme").join(&mount_path));
|
||||
let mut tries = 0;
|
||||
loop {
|
||||
if real_path.exists() {
|
||||
break;
|
||||
}
|
||||
tries += 1;
|
||||
if tries == 10 {
|
||||
panic!("Fail to wait for mount")
|
||||
}
|
||||
println!("{tries}");
|
||||
sleep(Duration::from_millis(500));
|
||||
}
|
||||
real_path
|
||||
} else {
|
||||
sleep(Duration::from_millis(200));
|
||||
let r = Path::new(".").join(&mount_path);
|
||||
r
|
||||
};
|
||||
|
||||
let res = catch_unwind(AssertUnwindSafe(|| callback(&real_path)));
|
||||
|
||||
sleep(Duration::from_millis(200));
|
||||
|
||||
child.kill().expect("Can't kill");
|
||||
let _ = child.wait();
|
||||
|
||||
if cfg!(target_os = "redox") {
|
||||
unmount_path(&mount_path).unwrap();
|
||||
} else {
|
||||
if !dbg!(Command::new("sync").status()).unwrap().success() {
|
||||
panic!("sync failed");
|
||||
}
|
||||
|
||||
if unmount_path(&mount_path).is_err() {
|
||||
// There seems to be a race condition where the device can be busy when trying to unmount.
|
||||
// So, we pause for a moment and retry. There will still be an error output to the logs
|
||||
// for the first failed attempt.
|
||||
sleep(Duration::from_millis(200));
|
||||
if unmount_path(&mount_path).is_err() {
|
||||
panic!("umount failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.expect("Test failed")
|
||||
});
|
||||
|
||||
if cfg!(not(target_os = "redox")) {
|
||||
dbg!(fs::remove_dir(dbg!(mount_path_o))).unwrap();
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
with_mounted(|path| {
|
||||
dbg!(fs::create_dir(path.join("test"))).unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_remove_file() {
|
||||
with_mounted(|path| {
|
||||
let file_name = "test_file.txt";
|
||||
let file_path = path.join(file_name);
|
||||
|
||||
// Create the file
|
||||
fs::write(&file_path, "Hello, world!").unwrap();
|
||||
assert!(fs::exists(&file_path).unwrap());
|
||||
|
||||
// Read the file
|
||||
let contents = fs::read_to_string(&file_path).unwrap();
|
||||
assert_eq!(contents, "Hello, world!");
|
||||
|
||||
// Remove the file
|
||||
fs::remove_file(&file_path).unwrap();
|
||||
assert!(!fs::exists(&file_path).unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_remove_directory() {
|
||||
with_mounted(|path| {
|
||||
let dir_name = "test_dir";
|
||||
let dir_path = path.join(dir_name);
|
||||
|
||||
// Create the directory
|
||||
fs::create_dir(&dir_path)
|
||||
.unwrap_or_else(|_| panic!("cannot create dir {}", &dir_path.display()));
|
||||
assert!(fs::exists(&dir_path).unwrap());
|
||||
|
||||
// Check that the directory is empty
|
||||
let entries: Vec<_> = fs::read_dir(&dir_path)
|
||||
.unwrap()
|
||||
.map(|e| e.unwrap().file_name())
|
||||
.collect();
|
||||
assert!(entries.is_empty());
|
||||
|
||||
// Add a file to the directory
|
||||
let file_name = "test_file.txt";
|
||||
let file_path = dir_path.join(file_name);
|
||||
fs::write(&file_path, "Hello, world!").unwrap();
|
||||
|
||||
// Check that the dir cannot be removed when not empty
|
||||
let error = fs::remove_dir(&dir_path);
|
||||
assert!(error.is_err());
|
||||
assert_eq!(
|
||||
error.unwrap_err().kind(),
|
||||
std::io::ErrorKind::DirectoryNotEmpty
|
||||
);
|
||||
|
||||
// Remove the file
|
||||
fs::remove_file(&file_path).unwrap();
|
||||
|
||||
// Remove the directory
|
||||
fs::remove_dir(&dir_path).unwrap();
|
||||
assert!(!fs::exists(&dir_path).unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_remove_symlink() {
|
||||
with_mounted(|path| {
|
||||
let real_file = "real_file.txt";
|
||||
let real_path = path.join(real_file);
|
||||
let symlink_file = "symlink_to_real_file.txt";
|
||||
let symlink_path = path.join(symlink_file);
|
||||
|
||||
// Create the real file
|
||||
fs::write(&real_path, "Hello, world!").unwrap();
|
||||
|
||||
// Create the symmlink according to the platform
|
||||
#[cfg(unix)]
|
||||
std::os::unix::fs::symlink(real_file, &symlink_path).unwrap();
|
||||
|
||||
#[cfg(windows)]
|
||||
std::os::windows::fs::symlink_file(&real_file, &symlink_path).unwrap();
|
||||
|
||||
// Check that the symlink exists and points to the correct target
|
||||
let exists = fs::exists(&symlink_path);
|
||||
assert!(
|
||||
exists.is_ok() && exists.unwrap(),
|
||||
"Symlink should exist but was: {:?}",
|
||||
fs::exists(&symlink_path)
|
||||
);
|
||||
let symlink_metadata = fs::symlink_metadata(&symlink_path).unwrap();
|
||||
assert!(symlink_metadata.file_type().is_symlink());
|
||||
let target = fs::read_link(&symlink_path).unwrap();
|
||||
assert_eq!(target.to_str().unwrap(), real_file);
|
||||
assert_eq!(fs::read(&symlink_path).unwrap(), b"Hello, world!");
|
||||
|
||||
// Confirm the symlink cannot be removed as a directory
|
||||
let error = fs::remove_dir(&symlink_path);
|
||||
assert!(error.is_err());
|
||||
assert_eq!(error.unwrap_err().kind(), std::io::ErrorKind::NotADirectory);
|
||||
|
||||
// Remove the symlink
|
||||
fs::remove_file(&symlink_path).unwrap();
|
||||
assert!(!fs::exists(&symlink_path).unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[test]
|
||||
fn mmap() {
|
||||
//TODO
|
||||
with_mounted(|path| {
|
||||
use std::slice;
|
||||
|
||||
let path = dbg!(path.join("test"));
|
||||
|
||||
let mmap_inner = |write: bool| {
|
||||
let fd = dbg!(libredox::call::open(
|
||||
path.to_str().unwrap(),
|
||||
libredox::flag::O_CREAT | libredox::flag::O_RDWR | libredox::flag::O_CLOEXEC,
|
||||
0,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let map = unsafe {
|
||||
slice::from_raw_parts_mut(
|
||||
dbg!(libredox::call::mmap(libredox::call::MmapArgs {
|
||||
fd,
|
||||
offset: 0,
|
||||
length: 128,
|
||||
prot: libredox::flag::PROT_READ | libredox::flag::PROT_WRITE,
|
||||
flags: libredox::flag::MAP_SHARED,
|
||||
addr: core::ptr::null_mut(),
|
||||
}))
|
||||
.unwrap() as *mut u8,
|
||||
128,
|
||||
)
|
||||
};
|
||||
|
||||
// Maps should be available after closing
|
||||
assert_eq!(dbg!(libredox::call::close(fd)), Ok(()));
|
||||
|
||||
for i in 0..128 {
|
||||
if write {
|
||||
map[i as usize] = i;
|
||||
}
|
||||
assert_eq!(map[i as usize], i);
|
||||
}
|
||||
|
||||
//TODO: add msync
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
dbg!(libredox::call::munmap(map.as_mut_ptr().cast(), map.len())),
|
||||
Ok(())
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
mmap_inner(true);
|
||||
mmap_inner(false);
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: When increasing the total_count to 8000, the Allocator's deallocate() function surfaces as "slow" according to flamegraph. This
|
||||
// appears to be the result of bulk deleting in this test, but I would bet that any filesystem that has lived for a long time would
|
||||
// start to see degraded performance due to this.
|
||||
#[test]
|
||||
fn many_create_write_list_find_read_delete() {
|
||||
let disk = DiskMemory::new(1024 * 1024 * 1024);
|
||||
let ctime = time::SystemTime::now()
|
||||
.duration_since(time::UNIX_EPOCH)
|
||||
.unwrap();
|
||||
let mut fs = FileSystem::create(disk, None, ctime.as_secs(), ctime.subsec_nanos()).unwrap();
|
||||
let tree_ptr = TreePtr::<Node>::root();
|
||||
let total_count = 3000;
|
||||
|
||||
// Create a bunch of files
|
||||
for i in 0..total_count {
|
||||
let result = fs.tx(|tx| {
|
||||
tx.create_node(
|
||||
tree_ptr,
|
||||
&format!("file{i:05}"),
|
||||
Node::MODE_FILE | 0o644,
|
||||
1,
|
||||
0,
|
||||
)
|
||||
});
|
||||
if result.is_err() {
|
||||
println!("Failure on create iteration {i}");
|
||||
}
|
||||
|
||||
let file_node = result.unwrap();
|
||||
let result = fs.tx(|tx| {
|
||||
tx.write_node(
|
||||
file_node.ptr(),
|
||||
0,
|
||||
format!("Hello World! #{i}").as_bytes(),
|
||||
ctime.as_secs(),
|
||||
ctime.subsec_nanos(),
|
||||
)
|
||||
});
|
||||
if result.is_err() {
|
||||
println!("Failure on write iteration {i}");
|
||||
}
|
||||
assert!(result.unwrap() > 0)
|
||||
}
|
||||
|
||||
// Confirm that they can be listed
|
||||
{
|
||||
let mut children = Vec::<DirEntry>::with_capacity(total_count);
|
||||
fs.tx(|tx| tx.child_nodes(tree_ptr, &mut children)).unwrap();
|
||||
assert_eq!(
|
||||
children.len(),
|
||||
total_count,
|
||||
"The list of children should match the number of files created."
|
||||
);
|
||||
let mut children: Vec<String> = children
|
||||
.iter()
|
||||
.map(|entry| entry.name().unwrap_or_default().to_string())
|
||||
.collect();
|
||||
children.sort();
|
||||
|
||||
for i in 0..total_count {
|
||||
let expected = format!("file{i:05}");
|
||||
let idx = children.binary_search(&expected);
|
||||
assert!(idx.is_ok(), "Children did not contain '{}'", expected);
|
||||
}
|
||||
}
|
||||
|
||||
// Find and read the files
|
||||
for i in 0..total_count {
|
||||
let result = fs.tx(|tx| tx.find_node(tree_ptr, &format!("file{i:05}")));
|
||||
if result.is_err() {
|
||||
println!("Failure on find node iteration {i}");
|
||||
}
|
||||
|
||||
let file_node = result.unwrap();
|
||||
let offset = 0;
|
||||
let mut buf = [0_u8; 32];
|
||||
let result = fs.tx(|tx| {
|
||||
tx.read_node(
|
||||
file_node.ptr(),
|
||||
offset,
|
||||
&mut buf,
|
||||
ctime.as_secs(),
|
||||
ctime.subsec_nanos(),
|
||||
)
|
||||
});
|
||||
if result.is_err() {
|
||||
println!("Failure on read iteration {i}");
|
||||
}
|
||||
let size = result.unwrap();
|
||||
let body = std::str::from_utf8(&buf[..size]).unwrap();
|
||||
assert_eq!(body, format!("Hello World! #{i}"));
|
||||
}
|
||||
|
||||
// Delete all the files
|
||||
for i in 0..total_count {
|
||||
let file_name = format!("file{i:05}");
|
||||
if let Err(e) = fs.tx(|tx| tx.remove_node(tree_ptr, &file_name, Node::MODE_FILE)) {
|
||||
println!("Failure on delete iteration {i}");
|
||||
panic!("{e}");
|
||||
}
|
||||
|
||||
let result = fs.tx(|tx| tx.find_node(tree_ptr, &file_name));
|
||||
if result.is_ok() || result.unwrap_err().errno != syscall::error::ENOENT {
|
||||
println!("Failure on delete verification iteration {i}");
|
||||
panic!("Deletion appears to have failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_write_read_delete_mounted() {
|
||||
with_mounted(|path| {
|
||||
let total_count = 500;
|
||||
|
||||
for i in 0..total_count {
|
||||
fs::write(
|
||||
path.join(format!("file{}", i)),
|
||||
format!("Hello, number {i}!"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Confirm each of the created files can be found and read
|
||||
for i in 0..total_count {
|
||||
let contents = fs::read_to_string(path.join(format!("file{}", i))).unwrap();
|
||||
assert_eq!(contents, format!("Hello, number {i}!"));
|
||||
}
|
||||
|
||||
// Remove all the files
|
||||
for i in 0..total_count {
|
||||
let file_path = path.join(format!("file{i}"));
|
||||
assert!(fs::exists(&file_path).unwrap());
|
||||
fs::remove_file(&file_path).unwrap();
|
||||
assert!(!fs::exists(&file_path).unwrap());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_no_replace() {
|
||||
let disk = DiskMemory::new(1024 * 1024 * 1024);
|
||||
let mut fs = FileSystem::create(disk, None, 0, 0)
|
||||
.expect("Creating in memory file system should succeed");
|
||||
|
||||
let root = TreePtr::root();
|
||||
let dir = fs
|
||||
.tx(|tx| tx.create_node(root, "dir", Node::MODE_DIR, 0, 0))
|
||||
.expect("Creating a directory should succeed");
|
||||
let source_file = fs
|
||||
.tx(|tx| tx.create_node(root, "source", Node::MODE_FILE, 0, 0))
|
||||
.expect("Creating source file to copy should succeed");
|
||||
let no_clobber_file = fs
|
||||
.tx(|tx| tx.create_node(root, "no_clobber", Node::MODE_FILE, 0, 0))
|
||||
.expect("Creating second file to not clobber should succeed");
|
||||
|
||||
// Rename /source to /target
|
||||
fs.tx(|tx| tx.rename_node_no_replace(root, "source", root, "target"))
|
||||
.expect("Renaming existing 'source' to non-existing 'target' should succeed");
|
||||
let target_file = fs
|
||||
.tx(|tx| tx.find_node(root, "target"))
|
||||
.expect("'target' should exist because we just renamed 'source' to 'target'");
|
||||
assert_eq!(
|
||||
source_file.id(),
|
||||
target_file.id(),
|
||||
"source and target are most definitely the same file"
|
||||
);
|
||||
|
||||
// Don't rename /target to /no_clobber
|
||||
let err = fs
|
||||
.tx(|tx| tx.rename_node_no_replace(root, "target", root, "no_clobber"))
|
||||
.expect_err("Renaming 'target' to existing 'no_clobber' should fail");
|
||||
assert_eq!(
|
||||
syscall::EEXIST,
|
||||
err.errno,
|
||||
"Renaming to existing file should fail with EEXIST"
|
||||
);
|
||||
assert_ne!(
|
||||
no_clobber_file.id(),
|
||||
target_file.id(),
|
||||
"'target' and 'no_clobber' should be distinct files"
|
||||
);
|
||||
|
||||
// Don't rename /target to /dir
|
||||
let err = fs
|
||||
.tx(|tx| tx.rename_node_no_replace(root, "target", root, "dir"))
|
||||
.expect_err("Renaming 'target' to existing directory 'dir' should fail");
|
||||
assert_eq!(
|
||||
syscall::EEXIST,
|
||||
err.errno,
|
||||
"Renaming to existing file should fail with EEXIST"
|
||||
);
|
||||
assert_ne!(
|
||||
dir.id(),
|
||||
target_file.id(),
|
||||
"'target' and 'dir' should be distinct nodes"
|
||||
);
|
||||
|
||||
// Don't rename /dir to /target
|
||||
let err = fs
|
||||
.tx(|tx| tx.rename_node_no_replace(root, "dir", root, "target"))
|
||||
.expect_err("Renaming 'dir' to existing file 'target' should fail");
|
||||
assert_eq!(
|
||||
syscall::EEXIST,
|
||||
err.errno,
|
||||
"Renaming to existing file should fail with EEXIST"
|
||||
);
|
||||
assert_ne!(
|
||||
target_file.id(),
|
||||
dir.id(),
|
||||
"'dir' and 'target' should be distinct nodes"
|
||||
);
|
||||
|
||||
// Don't rename /target to /target
|
||||
let err = fs
|
||||
.tx(|tx| tx.rename_node_no_replace(root, "target", root, "target"))
|
||||
.expect_err("Renaming 'target' to itself should fail");
|
||||
assert_eq!(
|
||||
syscall::EEXIST,
|
||||
err.errno,
|
||||
"Renaming file to itself should fail with EEXIST"
|
||||
);
|
||||
|
||||
// Rename /target to /dir/target
|
||||
fs.tx(|tx| tx.rename_node_no_replace(root, "target", dir.ptr(), "target"))
|
||||
.expect("Renaming /target to /dir/target should succeed");
|
||||
let moved_target = fs
|
||||
.tx(|tx| tx.find_node(dir.ptr(), "target"))
|
||||
.expect("'target' should have moved to /dir/target");
|
||||
assert_eq!(target_file.id(), moved_target.id());
|
||||
|
||||
// Rename /dir to /newdir
|
||||
fs.tx(|tx| tx.rename_node_no_replace(root, "dir", root, "newdir"))
|
||||
.expect("Renaming 'dir' to 'newdir' should succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_works() {
|
||||
let disk = DiskMemory::new(1024 * 1024 * 1024);
|
||||
let mut fs = FileSystem::create(disk, None, 0, 0)
|
||||
.expect("Creating in memory file system should succeed");
|
||||
|
||||
let root = TreePtr::root();
|
||||
let dir = fs
|
||||
.tx(|tx| tx.create_node(root, "dir", Node::MODE_DIR, 0, 0))
|
||||
.expect("Creating a directory should succeed");
|
||||
let source_file = fs
|
||||
.tx(|tx| tx.create_node(root, "source", Node::MODE_FILE, 0, 0))
|
||||
.expect("Creating source file should succeed");
|
||||
let target_file_orig = fs
|
||||
.tx(|tx| tx.create_node(root, "target", Node::MODE_FILE, 0, 0))
|
||||
.expect("Creating target file should succeed");
|
||||
|
||||
// Rename /source to /source2
|
||||
fs.tx(|tx| tx.rename_node(root, "source", root, "source2"))
|
||||
.expect("Renaming existing 'source' to non-existing 'source2' should succeed");
|
||||
let source2_file = fs
|
||||
.tx(|tx| tx.find_node(root, "source2"))
|
||||
.expect("'source2' should exist because we just renamed 'source' to 'source2'");
|
||||
assert_eq!(source_file.id(), source2_file.id());
|
||||
let err = fs
|
||||
.tx(|tx| tx.find_node(root, "source"))
|
||||
.expect_err("'source' should not exist because it was moved");
|
||||
assert_eq!(syscall::ENOENT, err.errno);
|
||||
|
||||
// Rename /source2 to /target
|
||||
fs.tx(|tx| tx.rename_node(root, "source2", root, "target"))
|
||||
.expect("Renaming existing 'source2' to existing 'target' should succeed");
|
||||
let target_file_mv = fs
|
||||
.tx(|tx| tx.find_node(root, "target"))
|
||||
.expect("'target' should exist because the rename succeeded");
|
||||
assert_ne!(
|
||||
target_file_orig.id(),
|
||||
target_file_mv.id(),
|
||||
"Move failed because 'target' is still the same"
|
||||
);
|
||||
assert_eq!(
|
||||
source2_file.id(),
|
||||
target_file_mv.id(),
|
||||
"Move failed because 'source2' != 'target'"
|
||||
);
|
||||
|
||||
// Don't rename /target to /dir
|
||||
// XXX: A similar test fails on Linux using rename(). Not sure if the discrepancy matters.
|
||||
// let err = fs
|
||||
// .tx(|tx| tx.rename_node(root, "target", root, "dir"))
|
||||
// .expect_err("Renaming 'target' to existing directory 'dir' should fail");
|
||||
// assert_eq!(
|
||||
// syscall::EEXIST,
|
||||
// err.errno,
|
||||
// "Renaming to existing file should fail with EEXIST"
|
||||
// );
|
||||
// assert_ne!(
|
||||
// dir.id(),
|
||||
// target_file_mv.id(),
|
||||
// "'target' and 'dir' should be distinct nodes"
|
||||
// );
|
||||
|
||||
// Don't rename /dir to /target
|
||||
// XXX: A similar test fails on Linux using rename().
|
||||
// let err = fs
|
||||
// .tx(|tx| tx.rename_node(root, "dir", root, "target"))
|
||||
// .expect_err("Renaming 'dir' to existing file 'target' should fail");
|
||||
// assert_eq!(
|
||||
// syscall::EEXIST,
|
||||
// err.errno,
|
||||
// "Renaming to existing file should fail with EEXIST"
|
||||
// );
|
||||
// assert_ne!(
|
||||
// target_file_mv.id(),
|
||||
// dir.id(),
|
||||
// "'dir' and 'target' should be distinct nodes"
|
||||
// );
|
||||
|
||||
// Rename /target to /target
|
||||
fs.tx(|tx| tx.rename_node(root, "target", root, "target"))
|
||||
.expect("Renaming 'target' to itself should succeed");
|
||||
let target_self_mv = fs
|
||||
.tx(|tx| tx.find_node(root, "target"))
|
||||
.expect("'target' should exist because rename succeeded");
|
||||
assert_eq!(
|
||||
target_file_mv.id(),
|
||||
target_self_mv.id(),
|
||||
"'target' shouldn't have changed during a move to self"
|
||||
);
|
||||
|
||||
// Rename /target to /dir/target
|
||||
fs.tx(|tx| tx.rename_node(root, "target", dir.ptr(), "target"))
|
||||
.expect("Renaming /target to /dir/target should succeed");
|
||||
let moved_target = fs
|
||||
.tx(|tx| tx.find_node(dir.ptr(), "target"))
|
||||
.expect("'target' should have moved to /dir/target");
|
||||
assert_eq!(target_file_mv.id(), moved_target.id());
|
||||
|
||||
// Rename /dir to /newdir
|
||||
fs.tx(|tx| tx.rename_node(root, "dir", root, "newdir"))
|
||||
.expect("Renaming 'dir' to 'newdir' should succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temporary_file() {
|
||||
with_mounted(|path| {
|
||||
let file_path = path.join("temp");
|
||||
let mut file = fs::File::create_new(&file_path).expect("failed to create temp file");
|
||||
|
||||
fs::remove_file(&file_path).expect("failed to unlink temp file");
|
||||
|
||||
let write_data = "Test\n";
|
||||
file.write_all(write_data.as_bytes())
|
||||
.expect("failed to write temp file");
|
||||
|
||||
let mut read_data = String::new();
|
||||
file.seek(SeekFrom::Start(0))
|
||||
.expect("failed to seek temp file");
|
||||
file.read_to_string(&mut read_data)
|
||||
.expect("failed to read temp file");
|
||||
|
||||
assert_eq!(read_data, write_data);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user