From c9d56e6889db9cf37df06e9cc9c2346e6230eaaf Mon Sep 17 00:00:00 2001 From: Vincent Ollivier Date: Sun, 5 Jul 2020 13:31:49 +0200 Subject: [PATCH] Improve installation and documentation (#73) * Add kernel::fs::mount * Check if fs is mounted before opening dir * Save password hash during user creation * Create home directory during user creation * Improve path checking in mkfs * Improve mkfs output * Add install command * Check presence of username and password during user creation * Update documentation * Check if user already exists before creation * Split hashed_password * Add save_hashed_password * Rename login command to user * Display MOROS version in a consistent way * Use ctrl+d instead of ctrl+c to exit shell * Rename mkfs to disk format * Add disk list command * Change prompt color if last command errored * Improve installer * Use $ and > prompts in doc Use $ and > prompts in doc to indicate commands outside and inside of MOROS respectively. --- README.md | 37 +++++---- doc/filesystem.md | 140 ++++++++++++++++++++++++++------- doc/shell.md | 92 +++++++--------------- dsk/ini/banner.txt | 2 + dsk/ini/boot.sh | 3 +- src/kernel/ata.rs | 23 ++++-- src/kernel/fs.rs | 48 +++++++---- src/kernel/process.rs | 12 +-- src/lib.rs | 2 +- src/main.rs | 36 ++------- src/user/disk.rs | 59 ++++++++++++++ src/user/install.rs | 87 ++++++++++++++++++++ src/user/mkfs.rs | 24 ------ src/user/mod.rs | 5 +- src/user/shell.rs | 31 +++++--- src/user/{login.rs => user.rs} | 135 +++++++++++++++++++++++-------- 16 files changed, 503 insertions(+), 233 deletions(-) create mode 100644 src/user/disk.rs create mode 100644 src/user/install.rs delete mode 100644 src/user/mkfs.rs rename src/user/{login.rs => user.rs} (56%) diff --git a/README.md b/README.md index 9a9f9bb01..e1c6cf683 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This project started from the [seventh post][1] of the second edition of ![screenshot](screenshot.png) + ## Features - [x] External bootloader (using [bootloader](https://github.com/rust-osdev/bootloader)) @@ -34,39 +35,45 @@ This project started from the [seventh post][1] of the second edition of - [ ] Multitasking - [ ] A real userspace + ## Setup Install tools: - curl https://sh.rustup.rs -sSf | sh - rustup install nightly - rustup default nightly - rustup component add rust-src - rustup component add llvm-tools-preview - cargo install cargo-xbuild bootimage + $ curl https://sh.rustup.rs -sSf | sh + $ rustup install nightly + $ rustup default nightly + $ rustup component add rust-src + $ rustup component add llvm-tools-preview + $ cargo install cargo-xbuild bootimage + ## Usage -Build image: +Build image to `disk.img`: - make image output=vga keyboard=qwerty + $ make image output=vga keyboard=qwerty Run on QEMU: - make qemu output=vga - -MOROS will open a console in diskless mode after boot if no filesystem is -detected. Use `mkfs /dev/ata/0/0` to create a filesystem on the ATA hard drive -that will be used on the next boot. + $ make qemu output=vga Run on a native x86 computer by copying the bootloader and kernel to a hard drive or USB stick (but there is currently no USB driver so the filesystem will not be available): - sudo dd if=target/x86_64-moros/release/bootimage-moros.bin of=/dev/sdx && sync + $ sudo dd if=target/x86_64-moros/release/bootimage-moros.bin of=/dev/sdx && sync + +In both cases, MOROS will open a console in diskless mode after boot if no +filesystem is detected. The following command will setup the filesystem on the +first hard drive of the first ATA bus, allowing you to exit the diskless mode +and log in as a normal user: + + > install **Be careful not to overwrite the hard drive of your OS when using `dd` inside -your OS or `mkfs` inside MOROS.** +your OS, and `install` or `disk format` inside MOROS.** + ## LICENSE diff --git a/doc/filesystem.md b/doc/filesystem.md index 71626532d..dd6d828e8 100644 --- a/doc/filesystem.md +++ b/doc/filesystem.md @@ -2,13 +2,21 @@ ## Hard drive -A hard drive is separated in block of 512 bytes, grouped into three areas. The -first is reserved for future uses, the second is used as a bitmap mapping the -allocated blocks in the third area. The data stored on the hard drive use the -blocks of the third area. - -During the first boot of the OS, the root dir will be allocated, using the -first block of the data area. +A hard drive is separated in blocks of 512 bytes, grouped into 4 areas: + + +------------+ + | Boot | (2048 blocks) + +------------+ + | Superblock | (2 blocks) + +------------+ + | Bitmap | (512 blocks) + +------------+ + | Data | + +------------+ + +The first area contains the bootloader and the kernel, the second is a +superblock with a magic string to identify the file system, the third is a +bitmap mapping the allocated data blocks of the last area. A location on the tree of dirs and files is named a path: @@ -16,26 +24,61 @@ A location on the tree of dirs and files is named a path: - A dir inside the root will have its name appended to the slash: `/usr` - Subsequent dirs will append a slash and their names: `/usr/admin` + ### Creation with QEMU $ qemu-img create disk.img 128M Formatting 'disk.img', fmt=raw size=134217728 + ### Setup in diskless console -During boot MOROS will detect the hard drives present on the ATA buses, then -the filesystems on those hard drives. If no filesystem is found, MOROS will -open a console in diskless mode to allow the user to create one with the `mkfs` -command: +During boot MOROS will detect any hard drives present on the ATA buses, then +look for a filesystem on those hard drives. If no filesystem is found, MOROS +will open a console in diskless mode to allow the user to create one with +the `disk format` command: + + > disk format /dev/ata/0/0 + +This command will format the first disk on the first ATA bus by writing a magic +string in a superblock, mounting the filesystem, and allocating the root +directory. + +The next step during setup is to create the directory structure: + + > write /bin/ # Binaries + > write /dev/ # Devices + > write /ini/ # Initialisation files + > write /lib/ # Libraries + > write /net/ # Network + > write /src/ # Sources + > write /tmp/ # Temporary files + > write /usr/ # User directories + > write /var/ # Variable files + +Then the following should be added to the boot script with the +command `edit /ini/boot.sh` to allow MOROS to finish booting: - > mkfs /dev/ata/0/0 + user login + shell + +Finally a user can be created with the following command: + + > user create + +All of this can be made more easily by running the `install` command instead. +This installer will also add additional files contained in the `dsk` +repository of the source code, like a nice login banner :) + + +## Data Structures -## Data ### BlockBitmap Bitmap of allocated blocks in the data area. + ### Block A block is small area of 512 bytes on a hard drive, and it is also part of @@ -44,28 +87,71 @@ linked list representing a file or a directory. The first 4 bytes of a block is the address of the next block on the list and the rest of block is the data stored in the block. +Structure: + + 0 + 0 1 2 3 4 5 6 n + +-+-+-+-+-+-+-+ // +-+ + | addr | data | + +-+-+-+-+-+-+-+ // +-+ + + n = 512 + + ### DirEntry -A directory entry represent a file or a directory contained inside a directory. -Each entry use a variable number of bytes that must fit inside the data of one -block. Those bytes represent the kind of entry (file or dir), the address of -the first block, the filesize (max 4GB), and the filename (max 255 chars) of -the entry. +A directory entry represents a file or a directory contained inside a +directory. Each entry use a variable number of bytes that must fit inside the +data of one block. Those bytes represent the kind of entry (file or dir), the +address of the first block, the filesize (max 4GB), the length of the filename, +and the filename (max 255 chars) of the entry. Structure: - - 0..1: kind - - 1..5: addr - - 5..10: size - - 10..11: name len - - 11..n: name buf + 0 1 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 m + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // +-+ + |k| addr | size |n| name buffer | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // +-+ + + k = kind of entry + n = length of name buffer + m = 9 + n + ### Dir -A directory contains the address of the first block where its directory entries -are stored. +The first block of a directory contains the address of the next block where its +directory entries are stored and the first entries in the rest of the block. + +If all entries can fit into one block the address of the next block will be +empty. + +Structure: + + 0 1 + 0 1 2 3 4 5 6 7 8 9 0 n + +-+-+-+-+-+-+-+-+-+-+-+ // +-+-+-+-+-+-+-+-+ // +-+ + | addr | dir entry 1 | dir entry 2 | + +-+-+-+-+-+-+-+-+-+-+-+ // +-+-+-+-+-+-+-+-+ // +-+ + + n = 512 + ### File -A file contains the address of its first block along with its filesize and -filename, and a reference to its parent directory. +The first block of a contains the address of the next block where its contents +is stored and the beginning of its contents in the rest of the block. + +If all contents can fit into one block the address of the next block will be +empty. + +Structure: + + 0 + 0 1 2 3 4 5 6 7 8 n + +-+-+-+-+-+-+-+-+-+ // +-+ + | addr | contents | + +-+-+-+-+-+-+-+-+-+ // +-+ + + n = 512 diff --git a/doc/shell.md b/doc/shell.md index 0bb9b97a8..445a519ca 100644 --- a/doc/shell.md +++ b/doc/shell.md @@ -8,81 +8,59 @@ additional common aliases. **Delete** file: -``` -d a.txt -del a.txt -delete a.txt -``` + > d a.txt + > del a.txt + > delete a.txt **Copy** file: -``` -c a.txt b.txt -copy a.txt b.txt -``` + > c a.txt b.txt + > copy a.txt b.txt **Move** file: -``` -m a.txt b.txt -move a.txt b.txt -``` + > m a.txt b.txt + > move a.txt b.txt **Print** string: -``` -p "Hi" -print "Hi" -``` + > p "Hi" + > print "Hi" **Read** file: -``` -r a.txt -read a.txt -``` + > r a.txt + > read a.txt **Write** file: -``` -w a.txt -write a.txt -``` + > w a.txt + > write a.txt **Write** dir: -``` -write /usr/alice/ # with a trailing slash to create a dir instead of a file -``` + > write /usr/alice/ # with a trailing slash to create a dir instead of a file **List** files in dir: -``` -list /usr/alice -``` + > list /usr/alice When executed without arguments, this command will list the files of the current directory. **Go to** dir: -``` -goto /usr/alice -``` + > goto /usr/alice When executed without arguments, this command will print the current directory. @@ -93,15 +71,11 @@ double them. **And combiner:** -``` -r a.txt & r b.txt -``` + > r a.txt & r b.txt **Or combiners:** -``` -r a.txt | r b.txt -``` + > r a.txt | r b.txt ## Pipes (TODO) @@ -111,28 +85,20 @@ stdnil(3) is added to simplify writing to `/dev/null`. Read file A and redirect stdout(1) to stdin(0) of write file B: -``` -r a.txt > w b.txt -r a.txt 1>0 w b.txt # with implicit streams -r a.txt --> w b.txt # with arrow -``` + > r a.txt > w b.txt + > r a.txt 1>0 w b.txt # with implicit streams + > r a.txt --> w b.txt # with arrow Read file A and redirect stderr(2) to stdin(0) of write file B: -``` -r a.txt 2> w b.txt -r a.txt 2>0 w b.txt -``` + > r a.txt 2> w b.txt + > r a.txt 2>0 w b.txt Suppress errors by redirecting stderr(2) to stdnil(3): -``` -r a.txt 2>3 w b.txt -``` + > r a.txt 2>3 w b.txt Redirect stdout(1) to stdin(0) and stderr(2) to stdnil(3): -``` -r a.txt > 2>3 w b.txt -r a.txt 1>0 2>3 w b.txt -``` + > r a.txt > 2>3 w b.txt + > r a.txt 1>0 2>3 w b.txt diff --git a/dsk/ini/banner.txt b/dsk/ini/banner.txt index cadedade9..fba66c044 100644 --- a/dsk/ini/banner.txt +++ b/dsk/ini/banner.txt @@ -1,3 +1,4 @@ + _M_ (o o) +-------------------ooO--(_)--Ooo------------------+ @@ -14,3 +15,4 @@ | (v0.3.1) | | | +--------------------------------------------------+ + diff --git a/dsk/ini/boot.sh b/dsk/ini/boot.sh index 472c2e6d4..e7ab6a856 100644 --- a/dsk/ini/boot.sh +++ b/dsk/ini/boot.sh @@ -1,4 +1,3 @@ read /ini/banner.txt -login +user login shell -clear diff --git a/src/kernel/ata.rs b/src/kernel/ata.rs index 591b64d55..0a5a411a8 100644 --- a/src/kernel/ata.rs +++ b/src/kernel/ata.rs @@ -239,31 +239,44 @@ fn disk_size(sectors: u32) -> (u32, String) { } pub fn init() { - let mut buses = BUSES.lock(); - buses.push(Bus::new(0, 0x1F0, 0x3F6, 14)); - buses.push(Bus::new(1, 0x170, 0x376, 15)); + { + let mut buses = BUSES.lock(); + buses.push(Bus::new(0, 0x1F0, 0x3F6, 14)); + buses.push(Bus::new(1, 0x170, 0x376, 15)); + } + + for (bus, drive, model, serial, size, unit) in list() { + log!("ATA {}:{} {} {} ({} {})\n", bus, drive, model, serial, size, unit); + } +} +pub fn list() -> Vec<(u8, u8, String, String, u32, String)> { + let mut buses = BUSES.lock(); + let mut res = Vec::new(); for bus in 0..2 { for drive in 0..2 { - if let Some(buf) = buses[bus].identify_drive(drive) { + if let Some(buf) = buses[bus as usize].identify_drive(drive) { let mut serial = String::new(); for i in 10..20 { for &b in &buf[i].to_be_bytes() { serial.push(b as char); } } + serial = serial.trim().into(); let mut model = String::new(); for i in 27..47 { for &b in &buf[i].to_be_bytes() { model.push(b as char); } } + model = model.trim().into(); let sectors = (buf[61] as u32) << 16 | (buf[60] as u32); let (size, unit) = disk_size(sectors); - log!("ATA {}:{} {} {} ({} {})\n", bus, drive, model.trim(), serial.trim(), size, unit); + res.push((bus, drive, model, serial, size, unit)); } } } + res } pub fn read(bus: u8, drive: u8, block: u32, mut buf: &mut [u8]) { diff --git a/src/kernel/fs.rs b/src/kernel/fs.rs index 8671c97b2..1043bb949 100644 --- a/src/kernel/fs.rs +++ b/src/kernel/fs.rs @@ -6,6 +6,12 @@ use crate::{kernel, log}; use lazy_static::lazy_static; use spin::Mutex; +lazy_static! { + pub static ref BLOCK_DEVICE: Mutex> = Mutex::new(None); +} + +const MAGIC: &'static str = "MOROS FS"; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FileType { Dir = 0, @@ -195,12 +201,6 @@ impl Block { Self { addr, buf } } - pub fn write(&self) { - if let Some(ref block_device) = *BLOCK_DEVICE.lock() { - block_device.write(self.addr, &self.buf); - } - } - pub fn alloc() -> Option { match BlockBitmap::next_free_addr() { None => { @@ -221,6 +221,12 @@ impl Block { } } + pub fn write(&self) { + if let Some(ref block_device) = *BLOCK_DEVICE.lock() { + block_device.write(self.addr, &self.buf); + } + } + pub fn addr(&self) -> u32 { self.addr } @@ -270,7 +276,7 @@ const DATA_ADDR_OFFSET: u32 = BITMAP_ADDR_OFFSET + MAX_BLOCKS / 8; * 3 => Data (directories and files) */ -// A BlockBitmap store the allocation status of (512 -4) * 8 data blocks +// A BlockBitmap store the allocation status of (512 - 4) * 8 data blocks pub struct BlockBitmap {} impl BlockBitmap { @@ -396,9 +402,15 @@ impl Dir { pub fn open(pathname: &str) -> Option { let pathname = realpath(pathname); let mut dir = Dir::root(); + + if !is_mounted() { + return None; + } + if pathname == "/" { return Some(dir); } + for name in pathname.trim_start_matches('/').split('/') { match dir.find(name) { Some(dir_entry) => { @@ -641,25 +653,27 @@ impl BlockDevice { } } -lazy_static! { - pub static ref BLOCK_DEVICE: Mutex> = Mutex::new(None); +pub fn is_mounted() -> bool { + BLOCK_DEVICE.lock().is_some() } -const MAGIC: &'static str = "MOROS FS"; +pub fn mount(bus: u8, dsk: u8) { + let block_device = BlockDevice::new(bus, dsk); + *BLOCK_DEVICE.lock() = Some(block_device); +} -pub fn make(bus: u8, dsk: u8) { +pub fn format(bus: u8, dsk: u8) { // Write superblock let mut buf = MAGIC.as_bytes().to_vec(); buf.resize(512, 0); let block_device = BlockDevice::new(bus, dsk); block_device.write(SUPERBLOCK_ADDR, &buf); - *BLOCK_DEVICE.lock() = Some(block_device); + + mount(bus, dsk); // Allocate root dir let root = Dir::root(); - if BlockBitmap::is_free(root.addr()) { - BlockBitmap::alloc(root.addr()); - } + BlockBitmap::alloc(root.addr()); } pub fn init() { @@ -668,8 +682,8 @@ pub fn init() { let mut buf = [0u8; 512]; kernel::ata::read(bus, dsk, SUPERBLOCK_ADDR, &mut buf); if String::from_utf8(buf[0..8].to_vec()).unwrap() == MAGIC { - log!("MFS Superblock found on ATA {}:{}\n", bus, dsk); - *BLOCK_DEVICE.lock() = Some(BlockDevice::new(bus, dsk)); + log!("MFS Superblock found in ATA {}:{}\n", bus, dsk); + mount(bus, dsk); } } } diff --git a/src/kernel/process.rs b/src/kernel/process.rs index 2950e1f9c..771ec29a9 100644 --- a/src/kernel/process.rs +++ b/src/kernel/process.rs @@ -6,22 +6,22 @@ use spin::Mutex; lazy_static! { pub static ref PIDS: AtomicUsize = AtomicUsize::new(0); - pub static ref PROCESS: Mutex = Mutex::new(Process::new("/", "admin")); // TODO + pub static ref PROCESS: Mutex = Mutex::new(Process::new("/", None)); // TODO } pub struct Process { id: usize, env: BTreeMap, dir: String, - user: String, // TODO: Use uid + user: Option, } impl Process { - pub fn new(dir: &str, user: &str) -> Self { + pub fn new(dir: &str, user: Option<&str>) -> Self { let id = PIDS.fetch_add(1, Ordering::SeqCst); let env = BTreeMap::new(); let dir = dir.to_string(); - let user = user.to_string(); + let user = user.map(String::from); Self { id, env, dir, user } } } @@ -41,7 +41,7 @@ pub fn dir() -> String { PROCESS.lock().dir.clone() } -pub fn user() -> String { +pub fn user() -> Option { PROCESS.lock().user.clone() } @@ -54,5 +54,5 @@ pub fn set_dir(dir: &str) { } pub fn set_user(user: &str) { - PROCESS.lock().user = user.into(); + PROCESS.lock().user = Some(user.into()) } diff --git a/src/lib.rs b/src/lib.rs index fcc06784c..008f71bf3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ pub fn init(boot_info: &'static BootInfo) { unsafe { kernel::pic::PICS.lock().initialize() }; x86_64::instructions::interrupts::enable(); - log!("MOROS version {}\n", env!("CARGO_PKG_VERSION")); + log!("MOROS v{}\n", env!("CARGO_PKG_VERSION")); kernel::time::init(); kernel::keyboard::init(); diff --git a/src/main.rs b/src/main.rs index 446b2a251..aecc31374 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,46 +11,24 @@ entry_point!(main); fn main(boot_info: &'static BootInfo) -> ! { moros::init(boot_info); - print!("\n"); - - kernel::fs::Dir::create("/bin"); // Binaries - kernel::fs::Dir::create("/dev"); // Devices - kernel::fs::Dir::create("/ini"); // Initializers - kernel::fs::Dir::create("/lib"); // Libraries - kernel::fs::Dir::create("/net"); // Network - kernel::fs::Dir::create("/src"); // Sources - kernel::fs::Dir::create("/tmp"); // Temporaries - kernel::fs::Dir::create("/usr"); // User directories - kernel::fs::Dir::create("/var"); // Variables - - kernel::fs::Dir::create("/usr/admin"); - - include_file("/ini/boot.sh", include_str!("../dsk/ini/boot.sh")); - include_file("/ini/banner.txt", include_str!("../dsk/ini/banner.txt")); - include_file("/ini/passwords.csv", include_str!("../dsk/ini/passwords.csv")); - include_file("/tmp/alice.txt", include_str!("../dsk/tmp/alice.txt")); loop { let bootrc = "/ini/boot.sh"; if kernel::fs::File::open(bootrc).is_some() { user::shell::main(&["shell", bootrc]); } else { - print!("Could not find '{}'\n", bootrc); + if kernel::fs::is_mounted() { + print!("Could not find '{}'\n", bootrc); + } else { + print!("MFS is not mounted to '/'\n"); + } print!("Running console in diskless mode\n"); - //print!("Use `mkfs` and `reboot` to setup MOROS on disk\n"); + print!("\n"); + user::shell::main(&["shell"]); } } } -fn include_file(pathname: &str, contents: &str) { - if kernel::fs::File::open(pathname).is_some() { - return; - } - if let Some(mut file) = kernel::fs::File::create(pathname) { - file.write(&contents.as_bytes()).unwrap(); - } -} - #[panic_handler] fn panic(info: &PanicInfo) -> ! { print!("{}\n", info); diff --git a/src/user/disk.rs b/src/user/disk.rs new file mode 100644 index 000000000..35dfabd5b --- /dev/null +++ b/src/user/disk.rs @@ -0,0 +1,59 @@ +use alloc::vec::Vec; +use crate::{print, kernel, user}; + +const COMMANDS: [&'static str; 2] = ["list", "format"]; + +pub fn main(args: &[&str]) -> user::shell::ExitCode { + if args.len() == 1 || !COMMANDS.contains(&args[1]) { + return usage(); + } + + match args[1] { + "list" => { + list() + }, + "format" => { + if args.len() == 2 { + return usage(); + } + format(args[2]) + }, + _ => { + usage() + } + } +} + +fn usage() -> user::shell::ExitCode { + print!("Usage: \n"); + print!("\n"); + print!("Commands:\n"); + print!(" list\n"); + print!(" format \n"); + + user::shell::ExitCode::CommandError +} + +fn list() -> user::shell::ExitCode { + print!("Path Name (Size)\n"); + for (bus, drive, model, serial, size, unit) in kernel::ata::list() { + print!("/dev/ata/{}/{} {} {} ({} {})\n", bus, drive, model, serial, size, unit); + } + user::shell::ExitCode::CommandSuccessful +} + +fn format(pathname: &str) -> user::shell::ExitCode { + let path: Vec<_> = pathname.split('/').collect(); + if !pathname.starts_with("/dev/ata/") || path.len() != 5 { + print!("Could not find disk at '{}'\n", pathname); + return user::shell::ExitCode::CommandError; + } + + let bus = path[3].parse().expect("Could not parse "); + let dsk = path[4].parse().expect("Could not parse "); + kernel::fs::format(bus, dsk); + print!("Disk successfully formatted\n"); + print!("MFS is now mounted to '/'\n"); + + user::shell::ExitCode::CommandSuccessful +} diff --git a/src/user/install.rs b/src/user/install.rs new file mode 100644 index 000000000..8b1b68f5d --- /dev/null +++ b/src/user/install.rs @@ -0,0 +1,87 @@ +use crate::{print, kernel, user}; +use crate::kernel::vga::Color; + +pub fn main(_args: &[&str]) -> user::shell::ExitCode { + let (fg, bg) = kernel::vga::color(); + kernel::vga::set_color(Color::LightCyan, bg); + print!("Welcome to MOROS v{} installation program!\n", env!("CARGO_PKG_VERSION")); + kernel::vga::set_color(fg, bg); + print!("\n"); + + print!("Proceed? [y/N] "); + if kernel::console::get_line().trim() == "y" { + print!("\n"); + + if !kernel::fs::is_mounted() { + kernel::vga::set_color(Color::LightCyan, bg); + print!("Listing disks ...\n"); + kernel::vga::set_color(fg, bg); + user::disk::main(&["disk", "list"]); + print!("\n"); + + kernel::vga::set_color(Color::LightCyan, bg); + print!("Formatting disk ...\n"); + kernel::vga::set_color(fg, bg); + print!("Enter path of disk to format: "); + let pathname = kernel::console::get_line(); + let res = user::disk::main(&["disk", "format", pathname.trim_end()]); + if res == user::shell::ExitCode::CommandError { + return res; + } + print!("\n"); + } + + kernel::vga::set_color(Color::LightCyan, bg); + print!("Populating filesystem ...\n"); + kernel::vga::set_color(fg, bg); + create_dir("/bin"); // Binaries + create_dir("/dev"); // Devices + create_dir("/ini"); // Initializers + create_dir("/lib"); // Libraries + create_dir("/net"); // Network + create_dir("/src"); // Sources + create_dir("/tmp"); // Temporaries + create_dir("/usr"); // User directories + create_dir("/var"); // Variables + + copy_file("/ini/boot.sh", include_str!("../../dsk/ini/boot.sh")); + copy_file("/ini/banner.txt", include_str!("../../dsk/ini/banner.txt")); + copy_file("/tmp/alice.txt", include_str!("../../dsk/tmp/alice.txt")); + + if kernel::process::user().is_none() { + print!("\n"); + kernel::vga::set_color(Color::LightCyan, bg); + print!("Creating user ...\n"); + kernel::vga::set_color(fg, bg); + let res = user::user::main(&["user", "create"]); + if res == user::shell::ExitCode::CommandError { + return res; + } + } + + print!("\n"); + kernel::vga::set_color(Color::LightCyan, bg); + print!("Installation successful!\n"); + kernel::vga::set_color(fg, bg); + print!("\n"); + print!("Exit console or reboot to apply changes\n"); + } + + user::shell::ExitCode::CommandSuccessful +} + +fn create_dir(pathname: &str) { + if kernel::fs::Dir::create(pathname).is_some() { + print!("Created '{}'\n", pathname); + } +} + +fn copy_file(pathname: &str, contents: &str) { + if kernel::fs::File::open(pathname).is_some() { + return; + } + if let Some(mut file) = kernel::fs::File::create(pathname) { + file.write(&contents.as_bytes()).unwrap(); + print!("Copied '{}'\n", pathname); + } +} diff --git a/src/user/mkfs.rs b/src/user/mkfs.rs deleted file mode 100644 index 547501bd8..000000000 --- a/src/user/mkfs.rs +++ /dev/null @@ -1,24 +0,0 @@ -use alloc::vec::Vec; -use crate::{print, kernel, user}; - -// Example: mkfs /dev/ata/0/0 -pub fn main(args: &[&str]) -> user::shell::ExitCode { - if args.len() != 2 { - print!("Usage: mkfs /dev/ata//\n"); - return user::shell::ExitCode::CommandError; - } - - let path: Vec<_> = args[1].split('/').collect(); - - if path.len() != 5 { - print!("Could not recognize \n"); - return user::shell::ExitCode::CommandError; - } - - let bus = path[3].parse().expect("Could not parse "); - let dsk = path[4].parse().expect("Could not parse "); - kernel::fs::make(bus, dsk); - print!("MFS setup on ATA {}:{}\n", bus, dsk); - - user::shell::ExitCode::CommandSuccessful -} diff --git a/src/user/mod.rs b/src/user/mod.rs index eda398836..160e266c3 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -5,6 +5,7 @@ pub mod copy; pub mod date; pub mod delete; pub mod dhcp; +pub mod disk; pub mod editor; pub mod geotime; pub mod halt; @@ -12,10 +13,9 @@ pub mod help; pub mod hex; pub mod host; pub mod http; +pub mod install; pub mod ip; pub mod list; -pub mod login; -pub mod mkfs; pub mod net; pub mod print; pub mod r#move; @@ -25,4 +25,5 @@ pub mod shell; pub mod sleep; pub mod tcp; pub mod uptime; +pub mod user; pub mod write; diff --git a/src/user/shell.rs b/src/user/shell.rs index 5c36734f1..a450a16ec 100644 --- a/src/user/shell.rs +++ b/src/user/shell.rs @@ -6,6 +6,7 @@ use alloc::vec::Vec; use alloc::string::String; #[repr(u8)] +#[derive(PartialEq)] pub enum ExitCode { CommandSuccessful = 0, CommandUnknown = 1, @@ -20,6 +21,7 @@ pub struct Shell { history_index: usize, autocomplete: Vec, autocomplete_index: usize, + errored: bool, } impl Shell { @@ -31,6 +33,7 @@ impl Shell { history_index: 0, autocomplete: Vec::new(), autocomplete_index: 0, + errored: false, } } @@ -45,15 +48,18 @@ impl Shell { '\0' => { continue; } - '\x03' => { // Ctrl C - if self.cmd.len() > 0 { - self.cmd.clear(); - print!("\n\n"); - self.print_prompt(); - } else { + '\x04' => { // Ctrl D + if self.cmd.is_empty() { + kernel::vga::clear_screen(); return ExitCode::CommandSuccessful; } }, + '\x03' => { // Ctrl C + self.cmd.clear(); + self.errored = false; + print!("\n\n"); + self.print_prompt(); + }, '\n' => { // Newline self.update_history(); self.update_autocomplete(); @@ -70,16 +76,20 @@ impl Shell { let line = self.cmd.clone(); match self.exec(&line) { ExitCode::CommandSuccessful => { + self.errored = false; self.save_history(); }, ExitCode::ShellExit => { + kernel::vga::clear_screen(); return ExitCode::CommandSuccessful }, _ => { - print!("?\n") + self.errored = true; }, } self.cmd.clear(); + } else { + self.errored = false; } print!("\n"); self.print_prompt(); @@ -345,7 +355,6 @@ impl Shell { "shell" => user::shell::main(&args), "sleep" => user::sleep::main(&args), "clear" => user::clear::main(&args), - "login" => user::login::main(&args), "base64" => user::base64::main(&args), "halt" => user::halt::main(&args), "hex" => user::hex::main(&args), // TODO: Rename to `dump` @@ -355,17 +364,19 @@ impl Shell { "http" => user::http::main(&args), "tcp" => user::tcp::main(&args), "host" => user::host::main(&args), + "install" => user::install::main(&args), "ip" => user::ip::main(&args), "geotime" => user::geotime::main(&args), "colors" => user::colors::main(&args), - "mkfs" => user::mkfs::main(&args), + "disk" => user::disk::main(&args), + "user" => user::user::main(&args), _ => ExitCode::CommandUnknown, } } fn print_prompt(&self) { let (fg, bg) = kernel::vga::color(); - kernel::vga::set_color(Color::Magenta, bg); + kernel::vga::set_color(if self.errored { Color::Red } else { Color::Magenta }, bg); print!("{}", self.prompt); kernel::vga::set_color(fg, bg); } diff --git a/src/user/login.rs b/src/user/user.rs similarity index 56% rename from src/user/login.rs rename to src/user/user.rs index 7b0e25575..49943331c 100644 --- a/src/user/login.rs +++ b/src/user/user.rs @@ -8,48 +8,60 @@ use core::str; use hmac::Hmac; use sha2::Sha256; +const PASSWORDS: &'static str = "/ini/passwords.csv"; +const COMMANDS: [&'static str; 2] = ["create", "login"]; + pub fn main(args: &[&str]) -> user::shell::ExitCode { - if args.len() > 1 && args[1] == "add" { - create() + if args.len() == 1 || !COMMANDS.contains(&args[1]) { + return usage(); + } + + let username: String = if args.len() == 2 { + print!("Username: "); + kernel::console::get_line().trim_end().into() } else { - login() + args[2].into() + }; + + match args[1] { + "create" => create(&username), + "login" => login(&username), + _ => usage(), } } +fn usage() -> user::shell::ExitCode { + print!("Usage: user [{}] \n", COMMANDS.join("|")); + return user::shell::ExitCode::CommandError; +} + // TODO: Add max number of attempts -pub fn login() -> user::shell::ExitCode { - let mut hashed_passwords: BTreeMap = BTreeMap::new(); - if let Some(file) = kernel::fs::File::open("/ini/passwords.csv") { - for line in file.read_to_string().split("\n") { - let mut rows = line.split(","); - if let Some(username) = rows.next() { - if let Some(hashed_password) = rows.next() { - hashed_passwords.insert(username.into(), hashed_password.into()); - } - } - } +pub fn login(username: &str) -> user::shell::ExitCode { + if username.is_empty() { + print!("\n"); + kernel::time::sleep(1.0); + return main(&["user", "login"]); } - print!("\nUsername: "); - let mut username = kernel::console::get_line(); - username.pop(); // Trim end of string - match hashed_passwords.get(&username) { - None => { - kernel::time::sleep(1.0); - return login(); - }, - Some(hashed_password) => { + match hashed_password(&username) { + Some(hash) => { print!("Password: "); kernel::console::disable_echo(); let mut password = kernel::console::get_line(); kernel::console::enable_echo(); print!("\n"); password.pop(); - if !check(&password, hashed_password) { + if !check(&password, &hash) { + print!("\n"); kernel::time::sleep(1.0); - return login(); + return main(&["user", "login"]); } - } + }, + None => { + print!("\n"); + kernel::time::sleep(1.0); + return main(&["user", "login"]); + }, } let home = format!("/usr/{}", username); @@ -61,11 +73,15 @@ pub fn login() -> user::shell::ExitCode { user::shell::ExitCode::CommandSuccessful } -// TODO: Move that to `user add` or something -pub fn create() -> user::shell::ExitCode { - print!("\nUsername: "); - let mut username = kernel::console::get_line(); - username.pop(); // Trim end of string +pub fn create(username: &str) -> user::shell::ExitCode { + if username.is_empty() { + return user::shell::ExitCode::CommandError; + } + + if hashed_password(&username).is_some() { + print!("Username exists\n"); + return user::shell::ExitCode::CommandError; + } print!("Password: "); kernel::console::disable_echo(); @@ -74,6 +90,10 @@ pub fn create() -> user::shell::ExitCode { print!("\n"); password.pop(); + if password.is_empty() { + return user::shell::ExitCode::CommandError; + } + print!("Confirm: "); kernel::console::disable_echo(); let mut confirm = kernel::console::get_line(); @@ -86,7 +106,14 @@ pub fn create() -> user::shell::ExitCode { return user::shell::ExitCode::CommandError; } - print!("{}\n", hash(&password)); + if save_hashed_password(&username, &hash(&password)).is_err() { + print!("Could not save user\n"); + return user::shell::ExitCode::CommandError; + } + + // Create home dir + kernel::fs::Dir::create(&format!("/usr/{}", username)).unwrap(); + user::shell::ExitCode::CommandSuccessful } @@ -142,3 +169,47 @@ pub fn hash(password: &str) -> String { res.push_str(&String::from_utf8(user::base64::encode(&hash)).unwrap()); res } + +fn read_hashed_passwords() -> BTreeMap { + let mut hashed_passwords = BTreeMap::new(); + if let Some(file) = kernel::fs::File::open(PASSWORDS) { + for line in file.read_to_string().split("\n") { + let mut rows = line.split(","); + if let Some(username) = rows.next() { + if let Some(hash) = rows.next() { + hashed_passwords.insert(username.into(), hash.into()); + } + } + } + } + hashed_passwords +} + +fn hashed_password(username: &str) -> Option { + let hashed_passwords = read_hashed_passwords(); + + match hashed_passwords.get(username) { + Some(hash) => Some(hash.into()), + None => None, + } +} + +fn save_hashed_password(username: &str, hash: &str) -> Result<(), ()> { + let mut hashed_passwords = read_hashed_passwords(); + hashed_passwords.remove(username.into()); + hashed_passwords.insert(username.into(), hash.into()); + + let mut file = match kernel::fs::File::open(PASSWORDS) { + None => match kernel::fs::File::create(PASSWORDS) { + None => return Err(()), + Some(file) => file, + }, + Some(file) => file, + }; + + let mut contents = String::new(); + for (u, h) in hashed_passwords { + contents.push_str(&format!("{},{}\n", u, h)); + } + file.write(&contents.as_bytes()) +}