Add 'userspace/' from commit '2b418dfb5cfc0673a3afda6eda5957abaaf7a8ff'

git-subtree-dir: userspace
git-subtree-mainline: c4a5ad22c1fddf7ac8649939e2f1f3ae7aa0bb39
git-subtree-split: 2b418dfb5cfc0673a3afda6eda5957abaaf7a8ff
This commit is contained in:
Mark Poliakov 2024-03-12 15:52:25 +02:00
commit 817f71f90f
93 changed files with 10498 additions and 0 deletions

2
userspace/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/extra.sh

View File

@ -0,0 +1,7 @@
{
"rust-analyzer.cargo.target": "aarch64-unknown-yggdrasil",
"rust-analyzer.server.extraEnv": {
"RUSTUP_TOOLCHAIN": "ygg-stage1",
"RA_LOG": "debug"
}
}

1133
userspace/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

21
userspace/Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[workspace]
resolver = "1"
members = [
"init",
"shell",
"sysutils",
"red",
"colors",
"term",
"lib/libcolors",
"lib/serde-ipc",
"lib/libterm",
"netutils"
]
[patch.'https://git.alnyan.me/yggdrasil/yggdrasil-rt.git']
yggdrasil-rt = { path = "../yggdrasil-rt" }
[patch.'https://git.alnyan.me/yggdrasil/yggdrasil-abi.git']
yggdrasil-abi = { path = "../abi" }
[patch.'https://git.alnyan.me/yggdrasil/yggdrasil-abi-def.git']
yggdrasil-abi-def = { path = "../abi-def" }

View File

@ -0,0 +1,4 @@
init:1:wait:/sbin/rc default
logd:1:once:/sbin/logd
user:1:once:/sbin/login /dev/ttyS0

View File

@ -0,0 +1,2 @@
init:1:wait:/sbin/rc default
logd:1:once:/sbin/logd

View File

@ -0,0 +1,3 @@
#!/bin/sh
/sbin/service start /bin/colors

View File

@ -0,0 +1,15 @@
[package]
name = "colors"
version = "0.1.0"
edition = "2021"
authors = ["Mark Poliakov <mark@alnyan.me>"]
[dependencies]
yggdrasil-abi = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-abi.git", features = ["serde"] }
serde-ipc = { path = "../lib/serde-ipc" }
libcolors = { path = "../lib/libcolors", default_features = false }
flexbuffers = "2.0.0"
lazy_static = "1.4.0"
serde = { version = "1.0.193", features = ["derive"] }
thiserror = "1.0.56"

View File

@ -0,0 +1,90 @@
use std::{
fs::OpenOptions,
os::yggdrasil::io::{
device::{DeviceRequest, FdDeviceRequest},
mapping::FileMapping,
},
};
use crate::error::Error;
pub struct Display<'a> {
#[allow(unused)]
mapping: FileMapping<'a>,
data: &'a mut [u32],
width: usize,
height: usize,
}
pub struct Point<T>(pub T, pub T);
impl<'a> Display<'a> {
pub fn open() -> Result<Self, Error> {
let file = OpenOptions::new().open("/dev/fb0")?;
unsafe {
file.device_request(&mut DeviceRequest::AcquireDevice)?;
}
let width = 640;
let height = 480;
let mut mapping = FileMapping::new(file, 0, width * height * 4)?;
let data = unsafe {
std::slice::from_raw_parts_mut(mapping.as_mut_ptr() as *mut u32, width * height)
};
Ok(Self {
mapping,
data,
width,
height,
})
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn fill(&mut self, color: u32) {
self.data.fill(color);
}
pub fn row(&mut self, y: usize) -> &mut [u32] {
&mut self.data[y * self.width..y * self.width + self.width]
}
pub fn copy_from_slice(&mut self, slice: &[u32]) {
if slice.len() != self.data.len() {
panic!("Invalid copy source size");
}
self.data.copy_from_slice(slice);
}
pub fn blit_buffer(
&mut self,
source: &[u32],
dst: Point<usize>,
src: Point<usize>,
w: usize,
h: usize,
src_stride: usize,
) {
for y in 0..h {
let dst_offset = (y + src.1 + dst.1) * self.width + dst.0 + src.0;
let src_offset = (y + src.1) * src_stride + src.0;
let src_chunk = &source[src_offset..src_offset + w];
let dst_chunk = &mut self.data[dst_offset..dst_offset + w];
dst_chunk.copy_from_slice(src_chunk);
}
}
}

View File

@ -0,0 +1,7 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Communication error: {0}")]
IpcError(#[from] serde_ipc::Error),
}

View File

@ -0,0 +1,113 @@
use std::{
fs::File,
io::Read,
os::fd::{AsRawFd, RawFd},
};
use libcolors::event::{KeyInput, KeyModifiers};
use yggdrasil_abi::io::{KeyboardKey, KeyboardKeyEvent};
use crate::error::Error;
pub struct KeyboardInput(File);
#[derive(Default)]
pub struct InputState {
lshift: bool,
rshift: bool,
lctrl: bool,
rctrl: bool,
lalt: bool,
ralt: bool,
}
impl KeyboardInput {
pub fn open() -> Result<Self, Error> {
let file = File::open("/dev/kbd")?;
Ok(Self(file))
}
pub fn as_poll_fd(&self) -> RawFd {
self.0.as_raw_fd()
}
pub fn read_event(&mut self) -> Result<KeyboardKeyEvent, Error> {
let mut buf = [0; 4];
let len = self.0.read(&mut buf)?;
if len == 4 {
Ok(KeyboardKeyEvent::from_bytes(buf))
} else {
todo!()
}
}
}
impl InputState {
pub fn update(&mut self, key: KeyboardKey, state: bool) {
match key {
KeyboardKey::LAlt => self.lalt = state,
KeyboardKey::RAlt => self.ralt = state,
KeyboardKey::LShift => self.lshift = state,
KeyboardKey::RShift => self.rshift = state,
KeyboardKey::LControl => self.lctrl = state,
KeyboardKey::RControl => self.rctrl = state,
_ => (),
}
}
fn translate_shift(ch: u8) -> u8 {
static DIGIT_TRANS: &[u8] = b")!@#$%^&*(";
match ch {
ch if ch.is_ascii_lowercase() => ch.to_ascii_uppercase(),
b'0'..=b'9' => DIGIT_TRANS[(ch - b'0') as usize],
b';' => b':',
b'\'' => b'"',
b'/' => b'?',
b'-' => b'_',
b'=' => b'+',
b',' => b'<',
b'.' => b'>',
b'`' => b'~',
b'\\' => b'|',
_ => ch,
}
}
pub fn make_input(&self, key: KeyboardKey) -> KeyInput {
let modifiers = self.modifiers();
let input = match (key, modifiers) {
(KeyboardKey::Char(ch), KeyModifiers::NONE) => Some(ch as _),
// TODO proper shift key translation
(KeyboardKey::Char(ch), KeyModifiers::SHIFT) => Some(Self::translate_shift(ch) as _),
(KeyboardKey::Tab, KeyModifiers::NONE) => Some('\t'),
(KeyboardKey::Enter, KeyModifiers::NONE) => Some('\n'),
(_, _) => None,
};
KeyInput {
modifiers,
key,
input,
}
}
pub fn modifiers(&self) -> KeyModifiers {
KeyModifiers {
shift: self.lshift || self.rshift,
ctrl: self.lctrl || self.rctrl,
alt: self.lalt || self.ralt,
}
}
}
// impl FdEventSource<ServerEvent> for KeyboardInput {
// fn poll_fd(&self) -> RawFd {
// self.0.as_raw_fd()
// }
//
// fn read_event(&mut self) -> ServerEvent {
// }
// }

View File

@ -0,0 +1,586 @@
#![feature(yggdrasil_os, rustc_private)]
// TODO rewrite and split this into meaningful components
use std::{
collections::BTreeMap,
os::{
fd::{AsRawFd, RawFd},
yggdrasil::io::{
mapping::FileMapping, message_channel::ChannelPublisherId, poll::PollChannel,
shared_memory::SharedMemory,
},
},
process::{Command, ExitCode},
};
use display::Display;
use error::Error;
use input::{InputState, KeyboardInput};
use libcolors::{
event::{Event, KeyModifiers, WindowEvent, WindowInfo},
message::{ClientMessage, ServerMessage},
};
use serde_ipc::{Receiver, Sender};
use yggdrasil_abi::io::{KeyboardKey, KeyboardKeyEvent};
pub mod display;
pub mod error;
pub mod input;
pub struct Window<'a> {
window_id: u32,
client_id: ChannelPublisherId,
surface_mapping: FileMapping<'a>,
surface_data: &'a [u32],
}
pub struct Frame {
x: u32,
y: u32,
w: u32,
h: u32,
dirty: bool,
window: Option<u32>,
}
pub struct Row {
frames: Vec<Frame>,
x: u32,
y: u32,
width: u32,
height: u32,
}
pub struct ServerSender(Sender<ServerMessage>);
pub struct Server<'a, 'd> {
display: Display<'d>,
input_state: InputState,
// Window management
windows: BTreeMap<u32, Window<'a>>,
rows: Vec<Row>,
last_window_id: u32,
focused_frame: Option<(usize, usize)>,
// Outer frame
padding: usize,
background: u32,
// Event generators
poll: PollChannel,
receiver: Receiver<ClientMessage>,
input: KeyboardInput,
// Comms
sender: ServerSender,
}
impl Row {
pub fn new(x: u32, y: u32, w: u32, h: u32) -> Self {
Self {
frames: vec![],
x,
y,
width: w,
height: h,
}
}
pub fn balance_frames(&mut self) {
if self.frames.is_empty() {
return;
}
let spacing = 4;
let wc = self.frames.len() as u32;
let w = (self.width - spacing * (wc - 1)) / wc;
let h = self.height;
let mut x = self.x;
let y = self.y;
for frame in self.frames.iter_mut() {
frame.dirty = true;
frame.x = x;
frame.y = y;
frame.w = w;
frame.h = h;
x += w + spacing;
}
}
pub fn place_frame(&mut self) -> &mut Frame {
self.frames.push(Frame {
x: 0,
y: 0,
w: 0,
h: 0,
dirty: true,
window: None,
});
self.balance_frames();
self.frames.last_mut().unwrap()
}
pub fn remove_frame(&mut self, col: usize) {
self.frames.remove(col);
self.balance_frames();
}
}
impl<'a, 'd> Server<'a, 'd> {
pub fn new() -> Result<Self, Error> {
let mut poll = PollChannel::new()?;
let mut display = Display::open()?;
let input = KeyboardInput::open()?;
let (sender, receiver) = serde_ipc::channel(libcolors::CHANNEL_NAME)?;
let sender = ServerSender(sender);
poll.add(input.as_poll_fd())?;
poll.add(receiver.as_poll_fd())?;
let background = 0xFFCCCCCC;
display.fill(background);
Ok(Self {
display,
input_state: InputState::default(),
poll,
receiver,
input,
sender,
padding: 4,
background,
windows: BTreeMap::new(),
rows: vec![],
last_window_id: 1,
focused_frame: None,
})
}
fn create_window(
&mut self,
client_id: ChannelPublisherId,
) -> Result<(WindowInfo, RawFd), Error> {
if self.rows.is_empty() {
self.rows.push(Row::new(
self.padding as _,
self.padding as _,
(self.display.width() - self.padding * 2) as _,
(self.display.height() - self.padding * 2) as _,
));
}
// Create a frame
let row = self.rows.last_mut().unwrap();
let frame = row.place_frame();
// Create the actual window
let window_id = self.last_window_id;
self.last_window_id += 1;
let mapping_size = self.display.width() * self.display.height() * 4;
let surface_shm = SharedMemory::new(mapping_size).unwrap();
let fd = surface_shm.as_raw_fd();
let mut surface_mapping = surface_shm.into_mapping().unwrap();
let surface_data = unsafe {
std::slice::from_raw_parts_mut(
surface_mapping.as_mut_ptr() as *mut u32,
(frame.w * frame.h) as usize,
)
};
frame.window = Some(window_id);
let window = Window {
window_id,
client_id,
surface_mapping,
surface_data,
};
self.windows.insert(window_id, window);
let info = WindowInfo {
window_id,
surface_stride: self.display.width() * 4,
surface_mapping_size: mapping_size,
width: frame.w,
height: frame.h,
};
self.display.fill(self.background);
self.set_focused_window(window_id)?;
self.flush_dirty_frames();
Ok((info, fd))
}
fn remove_window(&mut self, window_id: u32) {
// Find the window
if !self.windows.contains_key(&window_id) {
return;
}
// TODO this is ugly
let mut res = None;
for (i, row) in self.rows.iter().enumerate() {
let j = row
.frames
.iter()
.position(|f| f.window.map(|w| w == window_id).unwrap_or(false));
if let Some(j) = j {
res = Some((i, j));
}
}
// Remove the frame
if let Some((row, col)) = res {
self.rows[row].remove_frame(col);
self.display.fill(self.background);
self.flush_dirty_frames();
}
self.windows.remove(&window_id);
if self.focused_frame == res {
self.focused_frame = None;
let new_focus = if let Some((row, col)) = res {
// Focus some other frame in the same row
if let Some(f_row) = self.rows.get(row) {
let row_len = f_row.frames.len();
if col == 0 && row_len != 0 {
Some((row, 1))
} else if col > 0 {
Some((row, col - 1))
} else {
// Empty row
None
}
} else {
// No row exists
None
}
} else {
// No frames?
None
};
self.set_focused_frame(new_focus);
}
}
fn handle_keyboard_event(&mut self, event: KeyboardKeyEvent) -> Result<(), Error> {
let (key, state) = event.split();
self.input_state.update(key, state);
if state {
let input = self.input_state.make_input(key);
// Non-window keys
#[allow(clippy::single_match)]
match (input.modifiers, input.key) {
(KeyModifiers::ALT, KeyboardKey::Enter) => {
// TODO do something with spawned child
Command::new("/bin/term").spawn().ok();
return Ok(());
}
_ => (),
}
// Window keys
if let Some((row, col)) = self.focused_frame {
let row_len = self.rows[row].frames.len();
match (input.modifiers, input.key) {
(KeyModifiers::ALT, KeyboardKey::Char(b'l')) => {
if col + 1 < row_len {
self.set_focused_frame(Some((row, col + 1)));
} else {
self.set_focused_frame(Some((row, 0)));
}
return Ok(());
}
(KeyModifiers::ALT, KeyboardKey::Char(b'h')) => {
if col > 0 {
self.set_focused_frame(Some((row, col - 1)));
} else if row_len != 0 {
self.set_focused_frame(Some((row, row_len - 1)));
}
return Ok(());
}
_ => (),
}
}
if let Some((_, window)) = self.get_focused_window() {
// Deliver event to the window
self.sender
.send_event(
Event::WindowEvent(window.window_id, WindowEvent::KeyInput(input)),
window.client_id,
)
.ok();
} else {
self.focused_frame = None;
}
}
Ok(())
}
fn get_window(&self, window_id: u32) -> Option<(&Frame, &Window<'a>)> {
let window = self.windows.get(&window_id)?;
for row in self.rows.iter() {
if let Some(f) = row
.frames
.iter()
.find(|f| f.window.map(|w| w == window_id).unwrap_or(false))
{
return Some((f, window));
}
}
// TODO Orphaned frame/window?
None
}
fn get_focused_window(&self) -> Option<(&Frame, &Window<'a>)> {
let (row, col) = self.focused_frame?;
let frame = &self.rows[row].frames[col];
let Some(window) = frame.window.and_then(|w| self.windows.get(&w)) else {
return None;
};
Some((frame, window))
}
fn set_focused_frame(&mut self, focus: Option<(usize, usize)>) {
if self.focused_frame == focus {
return;
}
if let Some((_, old_window)) = self.get_focused_window() {
self.sender
.send_event(
Event::WindowEvent(old_window.window_id, WindowEvent::FocusChanged(false)),
old_window.client_id,
)
.ok();
}
self.focused_frame = focus;
if let Some((row, col)) = focus {
let Some(f_row) = self.rows.get(row) else {
return;
};
let Some(frame) = f_row.frames.get(col) else {
return;
};
let Some(window) = frame.window.and_then(|w| self.windows.get(&w)) else {
return;
};
self.sender
.send_event(
Event::WindowEvent(window.window_id, WindowEvent::FocusChanged(true)),
window.client_id,
)
.ok();
}
}
fn set_focused_window(&mut self, window_id: u32) -> Result<(), Error> {
// TODO this is ugly
let mut res = None;
for (i, row) in self.rows.iter().enumerate() {
let j = row
.frames
.iter()
.position(|f| f.window.map(|w| w == window_id).unwrap_or(false));
if let Some(j) = j {
res = Some((i, j));
}
}
self.set_focused_frame(res);
Ok(())
}
fn flush_dirty_frames(&mut self) {
for row in self.rows.iter() {
for frame in row.frames.iter() {
if !frame.dirty {
continue;
}
let Some(window) = frame.window.and_then(|w| self.windows.get_mut(&w)) else {
// TODO handle orphaned frame
continue;
};
let new_surface_data = unsafe {
std::slice::from_raw_parts_mut(
window.surface_mapping.as_mut_ptr() as *mut u32,
(frame.w * frame.h) as usize,
)
};
window.surface_data = new_surface_data;
self.sender
.send_event(
Event::WindowEvent(
window.window_id,
WindowEvent::Resized {
width: frame.w,
height: frame.h,
},
),
window.client_id,
)
.ok();
}
}
}
fn handle_client_message(
&mut self,
client_id: ChannelPublisherId,
message: ClientMessage,
) -> Result<(), Error> {
match message {
ClientMessage::ClientHello => {
debug_trace!("{:?}: ClientHello", client_id);
// Echo the ID back
self.sender
.send_event(Event::ServerHello(client_id.into()), client_id)
}
ClientMessage::CreateWindow => {
debug_trace!("{:?}: CreateWindow", client_id);
let (info, shm_fd) = self.create_window(client_id)?;
let window_id = info.window_id;
self.sender
.send_event(Event::NewWindowInfo(info), client_id)?;
self.sender.send_fd(shm_fd, client_id)?;
self.sender.send_event(
Event::WindowEvent(window_id, WindowEvent::RedrawRequested),
client_id,
)?;
Ok(())
}
ClientMessage::BlitWindow {
window_id,
x,
y,
w,
h,
} => {
if let Some((frame, window)) = self.get_window(window_id) {
let x = x.min(frame.w);
let y = y.min(frame.h);
let w = w.min(frame.w - x);
let h = h.min(frame.h - y);
if w == 0 || h == 0 {
// Invalid rectangle, skip it
return Ok(());
}
self.display.blit_buffer(
window.surface_data,
display::Point(frame.x as _, frame.y as _),
display::Point(x as _, y as _),
w as _,
h as _,
frame.w as usize,
);
}
Ok(())
}
ClientMessage::DestroyWindow(window_id) => {
debug_trace!("{:?}: DestroyWindow {}", client_id, window_id);
self.remove_window(window_id);
Ok(())
}
}
}
fn run_inner(mut self) -> Result<(), Error> {
loop {
match self.poll.wait(None)? {
Some((fd, Ok(_))) if fd == self.input.as_poll_fd() => {
let event = self.input.read_event()?;
self.handle_keyboard_event(event)?;
}
Some((fd, Ok(_))) if fd == self.receiver.as_poll_fd() => {
let (client_id, message) = self.receiver.receive_message()?;
self.handle_client_message(client_id, message)?;
}
Some((_, Ok(_))) => {
todo!()
}
Some((_, Err(error))) => {
return Err(Error::from(error));
}
None => (),
}
}
}
pub fn run(self) -> ExitCode {
match self.run_inner() {
Ok(_) => ExitCode::SUCCESS,
Err(error) => {
debug_trace!("colors server finished with an error: {}", error);
ExitCode::FAILURE
}
}
}
}
impl ServerSender {
pub fn send_event(&mut self, event: Event, client_id: ChannelPublisherId) -> Result<(), Error> {
self.0
.send_to(&ServerMessage::Event(event), client_id)
.map_err(Error::from)
}
pub fn send_fd(&mut self, fd: RawFd, client_id: ChannelPublisherId) -> Result<(), Error> {
self.0.send_file(fd, client_id).map_err(Error::from)
}
}
fn main() -> ExitCode {
let server = Server::new().unwrap();
server.run()
}

Binary file not shown.

Binary file not shown.

1
userspace/etc/hosts Normal file
View File

@ -0,0 +1 @@
127.0.0.1 localhost

1
userspace/etc/profile Normal file
View File

@ -0,0 +1 @@
set PATH /bin:/sbin

4
userspace/etc/rc.d/00-mount Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
/sbin/mount -t devfs /dev
/sbin/mount -t sysfs /sys

3
userspace/etc/rc.d/10-resolver Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
/sbin/service start -- /bin/dnsq -n 11.0.0.1 -s 127.0.0.1:53

3
userspace/etc/rc.d/44-netconf Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
/sbin/dhcp-client eth0

20
userspace/init/Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "init"
version = "0.1.0"
edition = "2021"
authors = ["Mark Poliakov <mark@alnyan.me>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "init"
path = "src/main.rs"
[[bin]]
name = "rc"
path = "src/rc.rs"
[dependencies]
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
yggdrasil-rt = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-rt.git" }

12
userspace/init/src/lib.rs Normal file
View File

@ -0,0 +1,12 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct StartService {
pub binary: String,
pub args: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum InitMsg {
StartService(StartService)
}

199
userspace/init/src/main.rs Normal file
View File

@ -0,0 +1,199 @@
#![feature(yggdrasil_os)]
use std::{
fmt,
fs::File,
io::{self, BufRead, BufReader},
os::yggdrasil::io::message_channel::{MessageChannel, MessageReceiver},
path::Path,
process::{Command, ExitCode, Stdio},
str::FromStr,
};
use init::InitMsg;
use yggdrasil_rt::debug_trace;
const INITTAB_PATH: &str = "/etc/inittab";
pub enum InitError {
IoError(std::io::Error),
OsError(yggdrasil_rt::Error),
CustomError(String),
}
impl From<std::io::Error> for InitError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
impl From<yggdrasil_rt::Error> for InitError {
fn from(value: yggdrasil_rt::Error) -> Self {
Self::OsError(value)
}
}
impl fmt::Debug for InitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IoError(e) => fmt::Debug::fmt(e, f),
Self::OsError(e) => fmt::Debug::fmt(e, f),
Self::CustomError(e) => fmt::Debug::fmt(e, f),
}
}
}
#[derive(Debug, PartialEq)]
pub enum RuleAction {
Wait,
Once,
Boot,
}
#[derive(Debug)]
pub struct Rule {
#[allow(unused)]
id: String,
// runlevels ignored here
action: RuleAction,
program: String,
arguments: Vec<String>,
}
impl Rule {
pub fn run(&self) -> Result<(), InitError> {
let arguments: Vec<_> = self.arguments.iter().map(String::as_str).collect();
let mut child = match self.action {
RuleAction::Wait | RuleAction::Once => Command::new(&self.program)
.args(&arguments)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?,
RuleAction::Boot => todo!(),
};
if self.action == RuleAction::Wait {
child.wait()?;
}
Ok(())
}
}
impl FromStr for RuleAction {
type Err = InitError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"wait" => Ok(Self::Wait),
"once" => Ok(Self::Once),
"boot" => Ok(Self::Boot),
_ => Err(InitError::CustomError(format!(
"Unrecognized rule action: {:?}",
s
))),
}
}
}
fn parse_rule(line: &str) -> Result<Rule, InitError> {
let terms: Vec<_> = line.split(':').collect();
if terms.len() != 4 {
return Err(InitError::CustomError(format!(
"Expected 4 terms in init rule, got {}",
terms.len()
)));
}
let id = terms[0].to_owned();
// Runlevel (term 1) ignored
let action = RuleAction::from_str(terms[2])?;
let command_terms: Vec<_> = terms[3].split(' ').collect();
let program = command_terms[0].to_owned();
let arguments = command_terms
.into_iter()
.skip(1)
.map(str::to_owned)
.collect();
Ok(Rule {
id,
action,
program,
arguments,
})
}
fn load_rules<P: AsRef<Path>>(path: P) -> Result<Vec<Rule>, InitError> {
let file = BufReader::new(File::open(path)?);
let mut rules = vec![];
for line in file.lines() {
let line = line?;
let line = line.trim();
let (line, _) = line.split_once('#').unwrap_or((line, ""));
if line.is_empty() {
continue;
}
rules.push(parse_rule(line)?);
}
Ok(rules)
}
fn handle_message(msg: InitMsg) -> io::Result<()> {
match msg {
InitMsg::StartService(init::StartService { binary, args }) => {
Command::new(binary).args(args).spawn()?;
Ok(())
}
}
}
fn main_loop(channel: MessageChannel) -> io::Result<()> {
let mut buf = [0; 1024];
loop {
let (_, len) = channel.receive_message(&mut buf)?;
if let Ok(msg) = serde_json::from_slice::<InitMsg>(&buf[..len]) {
if let Err(err) = handle_message(msg) {
debug_trace!("init::handle_message: {}", err);
}
}
}
}
fn main() -> ExitCode {
debug_trace!("Userspace init starting");
let rules = match load_rules(INITTAB_PATH) {
Ok(s) => s,
Err(e) => {
debug_trace!("init: failed to load rules: {:?}", e);
return ExitCode::FAILURE;
}
};
let channel = MessageChannel::open("service-control", true).unwrap();
debug_trace!("Rules loaded");
for rule in rules {
if let Err(err) = rule.run() {
debug_trace!("rc: failed to execute rule {:?}: {:?}", rule, err);
}
}
match main_loop(channel) {
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
debug_trace!("init: main_loop returned {}", e);
ExitCode::FAILURE
}
}
}

104
userspace/init/src/rc.rs Normal file
View File

@ -0,0 +1,104 @@
use std::{
env,
fs::read_dir,
path::{Path, PathBuf},
process::{Command, ExitCode},
str::FromStr,
};
const RC_DIR: &str = "/etc/rc.d";
#[derive(Debug)]
#[allow(dead_code)]
enum Error {
IncorrectUsage(String),
IoError(std::io::Error),
}
impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
#[derive(Debug)]
enum Mode {
Default,
}
impl FromStr for Mode {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"default" => Ok(Self::Default),
_ => Err(Error::IncorrectUsage(format!("Incorrect mode: {:?}", s))),
}
}
}
fn exec_script<P: AsRef<Path>>(path: P, arg: &str) -> Result<(), Error> {
let path = path.as_ref();
yggdrasil_rt::debug_trace!("rc: {:?} {}", path, arg);
// TODO run those in parallel, if allowed
// TODO binfmt guessing
let mut process = Command::new(path).arg(arg).spawn()?;
if !process.wait()?.success() {
eprintln!("{:?}: Failed", path);
}
Ok(())
}
fn get_scripts<P: AsRef<Path>>(dir: P) -> Result<Vec<String>, Error> {
let mut items = read_dir(dir)?
.map(|item| item.map(|item| item.file_name().to_str().unwrap().to_owned()))
.collect::<Result<Vec<_>, _>>()?;
items.sort();
Ok(items)
}
fn _main(mode: &str) -> Result<(), Error> {
// TODO only 1 mode supported, so ignore it
let _mode = Mode::from_str(mode)?;
let path = PathBuf::from(RC_DIR);
let scripts = get_scripts(&path)?;
// Execute scripts in order
for script in scripts {
if let Err(err) = exec_script(path.join(&script), "start") {
eprintln!("{}: {:?}", script, err);
}
}
Ok(())
}
fn usage(_cmd: &str) {
eprintln!("Usage: ...");
}
fn main() -> ExitCode {
let args: Vec<_> = env::args().collect();
if args.len() != 2 {
eprintln!("Incorrect rc usage, expected 2 args, got {}", args.len());
usage(&args[0]);
return ExitCode::FAILURE;
}
match _main(&args[1]) {
Ok(_) => ExitCode::SUCCESS,
Err(Error::IncorrectUsage(e)) => {
eprintln!("rc: incorrect usage: {:?}", e);
usage(&args[0]);
ExitCode::FAILURE
}
Err(e) => {
eprintln!("rc: {:?}", e);
ExitCode::FAILURE
}
}
}

View File

@ -0,0 +1,20 @@
[package]
name = "libcolors"
version = "0.1.0"
edition = "2021"
authors = ["Mark Poliakov <mark@alnyan.me>"]
[dependencies]
serde-ipc = { path = "../serde-ipc" }
yggdrasil-abi = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-abi.git", features = ["serde"] }
serde = { version = "1.0.193", features = ["derive"] }
thiserror = "1.0.56"
# client_raqote
raqote = { version = "0.8.3", default-features = false, optional = true }
[features]
default = []
client_raqote = ["client", "raqote"]
client = []

View File

@ -0,0 +1,136 @@
use std::{
collections::VecDeque,
os::{
fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
yggdrasil::io::{message_channel::ChannelPublisherId, poll::PollChannel},
},
time::Duration,
};
use serde_ipc::{Message, Receiver, Sender};
use crate::{
error::Error,
event::Event,
message::{ClientMessage, ServerMessage},
};
pub struct Connection {
sender: Sender<ClientMessage>,
receiver: Receiver<ServerMessage>,
event_queue: VecDeque<Event>,
poll: PollChannel,
timeout: Duration,
}
impl Connection {
pub fn new() -> Result<Self, Error> {
let (sender, receiver) = serde_ipc::channel(crate::CHANNEL_NAME)?;
let timeout = Duration::from_secs(1);
let mut poll = PollChannel::new()?;
let event_queue = VecDeque::new();
poll.add(receiver.as_raw_fd())?;
Ok(Self {
sender,
receiver,
timeout,
poll,
event_queue,
})
}
pub fn as_poll_fd(&self) -> RawFd {
self.poll.as_raw_fd()
}
pub fn receive_file(&mut self) -> Result<OwnedFd, Error> {
loop {
let Some((_, Ok(_))) = self.poll.wait(Some(self.timeout))? else {
return Err(Error::CommunicationTimeout);
};
// TODO ignore non-server messages
let (_, msg) = self.receiver.receive_raw()?;
match msg {
Message::File(fd) => {
let file = unsafe { OwnedFd::from_raw_fd(fd) };
break Ok(file);
}
Message::Data(ServerMessage::Event(event)) => {
self.event_queue.push_back(event);
}
}
}
}
pub fn receive_map<T, F: Fn(Event) -> Result<T, Event>>(
&mut self,
predicate: F,
) -> Result<T, Error> {
loop {
let Some((_, Ok(_))) = self.poll.wait(Some(self.timeout))? else {
return Err(Error::CommunicationTimeout);
};
// Unless we're doing a request, the server should not send any FDs, so just drop
// anything that's not a message
let (_, Message::Data(ServerMessage::Event(event))) = self.receiver.receive_raw()?
else {
continue;
};
match predicate(event) {
Ok(val) => break Ok(val),
Err(ev) => {
// Predicate rejected the event
self.event_queue.push_back(ev);
}
}
}
}
pub fn receive_event(&mut self) -> Result<Event, Error> {
if let Some(event) = self.event_queue.pop_front() {
return Ok(event);
}
loop {
match self.poll.wait(Some(self.timeout))? {
Some((_, Ok(_))) => (),
Some((_, Err(e))) => {
todo!("Connection error: {:?}", e)
}
None => continue,
};
let (_, Message::Data(ServerMessage::Event(event))) = self.receiver.receive_raw()?
else {
continue;
};
break Ok(event);
}
}
pub fn send(&mut self, msg: ClientMessage) -> Result<(), Error> {
self.sender
.send_to(&msg, ChannelPublisherId::ZERO)
.map_err(Error::from)
}
pub fn connect(&mut self) -> Result<u32, Error> {
self.sender
.send_to(&ClientMessage::ClientHello, ChannelPublisherId::ZERO)?;
self.receive_map(|ev| {
if let Event::ServerHello(id) = ev {
Ok(id)
} else {
Err(ev)
}
})
}
}

View File

@ -0,0 +1,96 @@
use std::{
collections::BTreeMap,
process::ExitCode,
sync::{Arc, Mutex},
};
use crate::{
error::Error,
event::{Event, WindowEvent},
message::ClientMessage,
};
use self::{connection::Connection, window::Window};
pub mod connection;
pub mod window;
pub struct Application<'a> {
pub(crate) connection: Arc<Mutex<Connection>>,
windows: BTreeMap<u32, Window<'a>>,
}
impl<'a> Application<'a> {
pub fn new() -> Result<Self, Error> {
let mut connection = Connection::new()?;
connection.connect()?;
Ok(Self {
connection: Arc::new(Mutex::new(connection)),
windows: BTreeMap::new(),
})
}
pub fn add_window(&mut self, window: Window<'a>) {
assert!(!self.windows.contains_key(&window.id()));
self.windows.insert(window.id(), window);
}
fn run_inner(mut self) -> Result<ExitCode, Error> {
loop {
self.poll_events()?;
}
}
pub fn handle_event(&mut self, event: Event) -> Result<(), Error> {
if let Event::WindowEvent(window_id, ev) = event {
if let Some(window) = self.windows.get_mut(&window_id) {
window.handle_event(ev)?;
} else {
debug_trace!("Unexpected window_id received: {:?}", window_id);
}
}
Ok(())
}
pub fn redraw(&mut self) -> Result<(), Error> {
for (_, window) in self.windows.iter_mut() {
// Inject RedrawRequested
window.handle_event(WindowEvent::RedrawRequested)?;
}
Ok(())
}
pub fn poll_events(&mut self) -> Result<(), Error> {
let event = {
let mut connection = self.connection.lock().unwrap();
connection.receive_event()?
};
self.handle_event(event)
}
pub fn run(self) -> ExitCode {
match self.run_inner() {
Ok(exit) => exit,
Err(e) => {
debug_trace!("Application finished with error {:?}", e);
ExitCode::FAILURE
}
}
}
pub fn connection(&self) -> &Arc<Mutex<Connection>> {
&self.connection
}
}
impl Drop for Application<'_> {
fn drop(&mut self) {
let mut conn = self.connection.lock().unwrap();
for (window_id, _) in self.windows.iter() {
conn.send(ClientMessage::DestroyWindow(*window_id)).ok();
}
}
}

View File

@ -0,0 +1,225 @@
use std::{
os::yggdrasil::io::mapping::FileMapping,
sync::{Arc, Mutex},
};
use crate::{
error::Error,
event::{Event, KeyInput, WindowEvent},
message::ClientMessage,
};
use super::{connection::Connection, Application};
pub trait OnCloseRequested = Fn() -> EventOutcome;
pub trait OnKeyInput = Fn(KeyInput) -> EventOutcome;
pub trait OnResized = Fn(u32, u32) -> EventOutcome;
pub trait OnFocusChanged = Fn(bool) -> EventOutcome;
#[cfg(feature = "client_raqote")]
pub trait OnRedrawRequested = Fn(&mut raqote::DrawTarget<&mut [u32]>);
#[cfg(not(feature = "client_raqote"))]
pub trait OnRedrawRequested = Fn(&mut [u32]);
#[derive(Debug, PartialEq, Eq)]
pub enum EventOutcome {
None,
Redraw,
Destroy,
}
pub struct Window<'a> {
connection: Arc<Mutex<Connection>>,
window_id: u32,
surface_mapping: FileMapping<'a>,
#[cfg(feature = "client_raqote")]
surface_draw_target: raqote::DrawTarget<&'a mut [u32]>,
#[cfg(not(feature = "client_raqote"))]
surface_data: &'a mut [u32],
width: u32,
height: u32,
focused: bool,
on_close_requested: Box<dyn OnCloseRequested>,
on_redraw_requested: Box<dyn OnRedrawRequested>,
on_key_input: Box<dyn OnKeyInput>,
on_resized: Box<dyn OnResized>,
on_focus_changed: Box<dyn OnFocusChanged>,
}
impl<'a> Window<'a> {
pub fn new(application: &Application) -> Result<Self, Error> {
let mut connection = application.connection.lock().unwrap();
connection.send(ClientMessage::CreateWindow)?;
let create_info = connection.receive_map(|r| match r {
Event::NewWindowInfo(info) => Ok(info),
_ => Err(r),
})?;
assert_eq!(create_info.surface_stride % 4, 0);
let surface_shm_fd = connection.receive_file()?;
let mut surface_mapping =
FileMapping::new(surface_shm_fd, 0, create_info.surface_mapping_size)?;
let surface_data = unsafe {
std::slice::from_raw_parts_mut(
surface_mapping.as_mut_ptr() as *mut u32,
(create_info.width * create_info.height) as usize,
)
};
#[cfg(feature = "client_raqote")]
let surface_draw_target = raqote::DrawTarget::from_backing(
create_info.width as _,
create_info.height as _,
surface_data,
);
Ok(Self {
connection: application.connection.clone(),
window_id: create_info.window_id,
width: create_info.width,
height: create_info.height,
surface_mapping,
#[cfg(feature = "client_raqote")]
surface_draw_target,
#[cfg(not(feature = "client_raqote"))]
surface_data,
focused: false,
on_close_requested: Box::new(|| {
// Do nothing
EventOutcome::Destroy
}),
#[cfg(feature = "client_raqote")]
on_redraw_requested: Box::new(|dt| {
dt.clear(SolidSource::from_unpremultiplied_argb(255, 127, 127, 127));
}),
#[cfg(not(feature = "client_raqote"))]
on_redraw_requested: Box::new(|dt| {
dt.fill(0xFF888888);
}),
on_key_input: Box::new(|_ev| EventOutcome::None),
on_resized: Box::new(|_w, _h| EventOutcome::None),
on_focus_changed: Box::new(|_| EventOutcome::None),
})
}
pub fn id(&self) -> u32 {
self.window_id
}
pub fn set_on_key_input<H: OnKeyInput + 'static>(&mut self, handler: H) {
self.on_key_input = Box::new(handler);
}
pub fn set_on_resized<H: OnResized + 'static>(&mut self, handler: H) {
self.on_resized = Box::new(handler);
}
pub fn set_on_redraw_requested<H: OnRedrawRequested + 'static>(&mut self, handler: H) {
self.on_redraw_requested = Box::new(handler);
}
pub fn set_on_focus_changed<H: OnFocusChanged + 'static>(&mut self, handler: H) {
self.on_focus_changed = Box::new(handler);
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn redraw(&mut self) -> Result<(), Error> {
#[cfg(feature = "client_raqote")]
{
let dt = &mut self.surface_draw_target;
(self.on_redraw_requested)(dt);
}
#[cfg(not(feature = "client_raqote"))]
{
(self.on_redraw_requested)(self.surface_data);
}
// Blit
self.blit_rect(0, 0, self.width, self.height)
}
pub fn handle_event(&mut self, ev: WindowEvent) -> Result<EventOutcome, Error> {
let outcome = match ev {
WindowEvent::RedrawRequested => {
self.redraw()?;
EventOutcome::None
}
WindowEvent::Resized { width, height } => {
let new_surface_data = unsafe {
std::slice::from_raw_parts_mut(
self.surface_mapping.as_mut_ptr() as *mut u32,
(width * height) as usize,
)
};
#[cfg(feature = "client_raqote")]
{
let new_draw_target =
raqote::DrawTarget::from_backing(width as _, height as _, new_surface_data);
self.surface_draw_target = new_draw_target;
}
#[cfg(not(feature = "client_raqote"))]
{
self.surface_data = new_surface_data;
}
(self.on_resized)(width, height)
}
WindowEvent::KeyInput(input) => (self.on_key_input)(input),
WindowEvent::FocusChanged(focused) => {
self.focused = focused;
(self.on_focus_changed)(focused)
}
WindowEvent::CloseRequested => (self.on_close_requested)(),
_ => EventOutcome::None,
};
if outcome == EventOutcome::Redraw {
self.redraw()?;
}
Ok(outcome)
}
#[cfg(feature = "client_raqote")]
pub fn as_draw_target(&mut self) -> &mut raqote::DrawTarget<&'a mut [u32]> {
&mut self.surface_draw_target
}
fn blit_rect(&mut self, x: u32, y: u32, w: u32, h: u32) -> Result<(), Error> {
// Clip to self bounds
let x = x.min(self.width);
let y = y.min(self.height);
let w = w.min(self.width - x);
let h = h.min(self.height - y);
if w == 0 || h == 0 {
return Ok(());
}
let mut connection = self.connection.lock().unwrap();
connection.send(ClientMessage::BlitWindow {
window_id: self.window_id,
x,
y,
w,
h,
})?;
Ok(())
}
}

View File

@ -0,0 +1,11 @@
use std::io;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Communication timeout")]
CommunicationTimeout,
#[error("I/O error: {0}")]
IoError(#[from] io::Error),
#[error("Communication error: {0:?}")]
CommunicationError(#[from] serde_ipc::Error),
}

View File

@ -0,0 +1,90 @@
pub use yggdrasil_abi::io::{KeyboardKey, KeyboardKeyEvent};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct WindowInfo {
pub window_id: u32,
pub surface_stride: usize,
pub surface_mapping_size: usize,
pub width: u32,
pub height: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeyModifiers {
pub ctrl: bool,
pub shift: bool,
pub alt: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct KeyEvent {
pub modifiers: KeyModifiers,
pub key: KeyboardKey,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct KeyInput {
pub modifiers: KeyModifiers,
pub key: KeyboardKey,
pub input: Option<char>,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum WindowEvent {
KeyPressed(KeyEvent),
KeyReleased(KeyEvent),
KeyInput(KeyInput),
Resized { width: u32, height: u32 },
FocusChanged(bool),
Destroyed,
RedrawRequested,
CloseRequested,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum Event {
// Server events
KeyboardKey(KeyboardKeyEvent),
RedrawRequested,
// Window events
WindowEvent(u32, WindowEvent),
// Request-responses
ServerHello(u32),
NewWindowInfo(WindowInfo),
}
impl KeyModifiers {
pub const SHIFT: Self = Self {
shift: true,
ctrl: false,
alt: false,
};
pub const CTRL: Self = Self {
shift: false,
ctrl: true,
alt: false,
};
pub const CTRL_SHIFT: Self = Self {
shift: true,
ctrl: true,
alt: false,
};
pub const ALT: Self = Self {
shift: false,
ctrl: false,
alt: true,
};
pub const NONE: Self = Self {
shift: false,
ctrl: false,
alt: false,
};
}

View File

@ -0,0 +1,12 @@
#![feature(yggdrasil_os, rustc_private)]
#![cfg_attr(feature = "client", feature(trait_alias))]
pub const CHANNEL_NAME: &str = "colors";
#[cfg(feature = "client")]
pub mod application;
#[cfg(feature = "client")]
pub mod error;
pub mod event;
pub mod message;

View File

@ -0,0 +1,30 @@
use crate::event::Event;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub enum ServerMessage {
Event(Event),
}
#[derive(Serialize, Deserialize, Debug)]
pub enum ClientMessage {
ClientHello,
CreateWindow,
BlitWindow {
window_id: u32,
x: u32,
y: u32,
w: u32,
h: u32,
},
DestroyWindow(u32),
}
impl From<(u32, ServerMessage)> for Event {
fn from((_, msg): (u32, ServerMessage)) -> Self {
let ServerMessage::Event(ev) = msg;
ev
}
}

View File

@ -0,0 +1,12 @@
[package]
name = "libterm"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
thiserror = "1.0.50"
[target.'cfg(unix)'.dependencies]
libc = "0.2.150"

View File

@ -0,0 +1,82 @@
use std::{
fmt,
io::{self, Read, Stdin},
};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum TermKey {
Char(char),
Escape,
}
#[derive(thiserror::Error, Debug)]
pub enum InputError {
#[error("Invalid UTF-8 prefix byte: {0:#x}")]
InvalidPrefixByte(u8),
#[error("Invalid UTF-8: {0}")]
DecodeError(std::str::Utf8Error),
#[error("I/O error: {0}")]
IoError(io::Error),
}
pub trait ReadChar {
fn read_char(&mut self) -> Result<char, InputError>;
}
impl ReadChar for Stdin {
fn read_char(&mut self) -> Result<char, InputError> {
let mut buf = [0; 4];
self.read_exact(&mut buf[..1])
.map_err(InputError::IoError)?;
let len = utf8_len_prefix(buf[0]).ok_or(InputError::InvalidPrefixByte(buf[0]))?;
if len != 0 {
self.read_exact(&mut buf[1..=len])
.map_err(InputError::IoError)?;
}
// TODO optimize
let s = core::str::from_utf8(&buf[..len + 1]).map_err(InputError::DecodeError)?;
Ok(s.chars().next().unwrap())
}
}
impl From<char> for TermKey {
fn from(value: char) -> Self {
Self::Char(value)
}
}
impl fmt::Display for TermKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Char('\t') => f.write_str("<tab>"),
Self::Char(key) => fmt::Display::fmt(key, f),
Self::Escape => f.write_str("<esc>"),
}
}
}
const fn utf8_len_prefix(l: u8) -> Option<usize> {
let mask0 = 0b10000000;
let val0 = 0;
let mask1 = 0b11100000;
let val1 = 0b11000000;
let mask2 = 0b11110000;
let val2 = 0b11100000;
let mask3 = 0b11111000;
let val3 = 0b11110000;
if l & mask3 == val3 {
Some(3)
} else if l & mask2 == val2 {
Some(2)
} else if l & mask1 == val1 {
Some(1)
} else if l & mask0 == val0 {
Some(0)
} else {
None
}
}

View File

@ -0,0 +1,228 @@
#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os, rustc_private))]
use std::{
fmt,
io::{self, stdin, stdout, Stdin, Stdout, Write},
};
use self::{input::ReadChar, sys::RawMode};
#[cfg(target_os = "yggdrasil")]
mod yggdrasil;
#[cfg(target_os = "yggdrasil")]
use yggdrasil as sys;
#[cfg(unix)]
mod unix;
#[cfg(unix)]
use unix as sys;
mod input;
pub use input::{InputError, TermKey};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("I/O error: {0}")]
IoError(#[from] io::Error),
#[error("Input error: {0}")]
InputError(#[from] InputError),
}
pub trait RawTerminal {
fn raw_enter_alternate_mode(&mut self) -> io::Result<()>;
fn raw_leave_alternate_mode(&mut self) -> io::Result<()>;
fn raw_clear_all(&mut self) -> io::Result<()>;
fn raw_clear_line(&mut self, what: u32) -> io::Result<()>;
fn raw_move_cursor(&mut self, row: usize, column: usize) -> io::Result<()>;
fn raw_set_cursor_style(&mut self, style: CursorStyle) -> io::Result<()>;
fn raw_set_color(&mut self, fgbg: u32, color: Color) -> io::Result<()>;
fn raw_set_style(&mut self, what: u32) -> io::Result<()>;
fn raw_set_cursor_visible(&mut self, visible: bool) -> io::Result<()>;
}
pub struct Term {
stdin: Stdin,
stdout: Stdout,
raw: RawMode,
}
#[derive(Debug, Clone, Copy)]
pub enum CursorStyle {
Default,
Line,
}
#[derive(Debug, Clone, Copy)]
#[repr(u32)]
pub enum Color {
Black = 0,
Red = 1,
Green = 2,
Yellow = 3,
Blue = 4,
Magenta = 5,
Cyan = 6,
White = 7,
Default = 9,
}
#[derive(Debug, Clone, Copy)]
pub enum Clear {
All,
LineToEnd,
}
impl RawTerminal for Stdout {
fn raw_enter_alternate_mode(&mut self) -> io::Result<()> {
self.write_all(b"\x1B[?1049h")
}
fn raw_leave_alternate_mode(&mut self) -> io::Result<()> {
self.write_all(b"\x1B[?1049l")
}
fn raw_clear_all(&mut self) -> io::Result<()> {
self.write_all(b"\x1B[2J")
}
fn raw_clear_line(&mut self, what: u32) -> io::Result<()> {
write!(self, "\x1B[{}K", what)
}
fn raw_move_cursor(&mut self, row: usize, column: usize) -> io::Result<()> {
write!(self, "\x1B[{};{}f", row + 1, column + 1)
}
fn raw_set_cursor_style(&mut self, style: CursorStyle) -> io::Result<()> {
// TODO yggdrasil support for cursor styles
#[cfg(not(target_os = "yggdrasil"))]
{
match style {
CursorStyle::Default => self.write_all(b"\x1B[0 q")?,
CursorStyle::Line => self.write_all(b"\x1B[6 q")?,
}
}
#[cfg(target_os = "yggdrasil")]
{
let _ = style;
}
Ok(())
}
fn raw_set_color(&mut self, fgbg: u32, color: Color) -> io::Result<()> {
write!(self, "\x1B[{}{}m", fgbg, color as u32)
}
fn raw_set_style(&mut self, what: u32) -> io::Result<()> {
write!(self, "\x1B[{}m", what)
}
fn raw_set_cursor_visible(&mut self, visible: bool) -> io::Result<()> {
if visible {
write!(self, "\x1B[?25h")
} else {
write!(self, "\x1B[?25l")
}
}
}
impl Term {
pub fn is_tty() -> bool {
// TODO
true
}
pub fn open() -> Result<Self, Error> {
let stdin = stdin();
let mut stdout = stdout();
// Set stdin to raw mode
let raw = unsafe { RawMode::enter(&stdin)? };
stdout.raw_enter_alternate_mode()?;
stdout.raw_clear_all()?;
stdout.raw_move_cursor(0, 0)?;
Ok(Self { stdin, stdout, raw })
}
pub fn set_cursor_position(&mut self, row: usize, column: usize) -> Result<(), Error> {
self.stdout
.raw_move_cursor(row, column)
.map_err(Error::from)
}
pub fn set_cursor_visible(&mut self, visible: bool) -> Result<(), Error> {
#[cfg(unix)]
{
self.stdout
.raw_set_cursor_visible(visible)
.map_err(Error::from)
}
#[cfg(target_os = "yggdrasil")]
{
let _ = visible;
Ok(())
}
}
pub fn set_cursor_style(&mut self, style: CursorStyle) -> Result<(), Error> {
self.stdout.raw_set_cursor_style(style).map_err(Error::from)
}
pub fn size(&self) -> Result<(usize, usize), Error> {
unsafe { sys::terminal_size(&self.stdout).map_err(Error::from) }
}
pub fn set_foreground(&mut self, color: Color) -> Result<(), Error> {
self.stdout.raw_set_color(3, color).map_err(Error::from)
}
pub fn set_background(&mut self, color: Color) -> Result<(), Error> {
self.stdout.raw_set_color(4, color).map_err(Error::from)
}
pub fn set_bright(&mut self, bright: bool) -> Result<(), Error> {
if bright {
self.stdout.raw_set_style(2)
} else {
self.stdout.raw_set_style(22)
}
.map_err(Error::from)
}
pub fn reset_style(&mut self) -> Result<(), Error> {
self.stdout.raw_set_style(0).map_err(Error::from)
}
pub fn clear(&mut self, clear: Clear) -> Result<(), Error> {
match clear {
Clear::All => self.stdout.raw_clear_all(),
Clear::LineToEnd => self.stdout.raw_clear_line(0),
}
.map_err(Error::from)
}
pub fn read_key(&mut self) -> Result<TermKey, Error> {
let ch = self.stdin.read_char().map_err(Error::from)?;
if ch == '\x1B' {
return Ok(TermKey::Escape);
}
Ok(TermKey::Char(ch))
}
pub fn flush(&mut self) -> Result<(), Error> {
self.stdout.flush().map_err(Error::from)
}
}
impl fmt::Write for Term {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.stdout.write_all(s.as_bytes()).map_err(|_| fmt::Error)
}
}
impl Drop for Term {
fn drop(&mut self) {
unsafe {
self.raw.leave(&self.stdin);
}
self.stdout.raw_leave_alternate_mode().ok();
}
}

View File

@ -0,0 +1,53 @@
use std::{
io::{self, Stdin, Stdout},
mem::MaybeUninit,
os::fd::AsRawFd,
};
pub struct RawMode {
saved_termios: libc::termios,
}
impl RawMode {
pub unsafe fn enter(stdin: &Stdin) -> Result<Self, io::Error> {
let mut old = MaybeUninit::uninit();
if libc::tcgetattr(stdin.as_raw_fd(), old.as_mut_ptr()) != 0 {
return Err(io::Error::last_os_error());
}
let old = old.assume_init();
let mut new = old;
new.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN);
new.c_iflag &= !(libc::IGNBRK
| libc::BRKINT
| libc::PARMRK
| libc::ISTRIP
| libc::INLCR
| libc::IGNCR
| libc::ICRNL
| libc::IXON);
new.c_oflag &= !libc::OPOST;
new.c_cflag &= !(libc::PARENB | libc::CSIZE);
new.c_cflag |= libc::CS8;
if libc::tcsetattr(stdin.as_raw_fd(), libc::TCSANOW, &new) != 0 {
return Err(io::Error::last_os_error());
}
Ok(Self { saved_termios: old })
}
pub unsafe fn leave(&self, stdin: &Stdin) {
libc::tcsetattr(stdin.as_raw_fd(), libc::TCSANOW, &self.saved_termios);
}
}
pub unsafe fn terminal_size(stdout: &Stdout) -> io::Result<(usize, usize)> {
let mut size: MaybeUninit<libc::winsize> = MaybeUninit::uninit();
if libc::ioctl(stdout.as_raw_fd(), libc::TIOCGWINSZ, size.as_mut_ptr()) != 0 {
return Err(io::Error::last_os_error());
}
let size = size.assume_init();
Ok((size.ws_col as _, size.ws_row as _))
}

View File

@ -0,0 +1,33 @@
use std::{
io::{self, Stdin, Stdout},
mem::MaybeUninit,
os::yggdrasil::io::{
device::{DeviceRequest, FdDeviceRequest},
terminal::{update_terminal_options, TerminalOptions},
},
};
pub struct RawMode(TerminalOptions);
impl RawMode {
pub unsafe fn enter(stdin: &Stdin) -> io::Result<Self> {
update_terminal_options(stdin, |_| TerminalOptions::raw_input()).map(RawMode)
}
pub unsafe fn leave(&self, stdin: &Stdin) {
update_terminal_options(stdin, |_| self.0).ok();
}
}
pub unsafe fn terminal_size(stdout: &Stdout) -> io::Result<(usize, usize)> {
let mut req = DeviceRequest::GetTerminalSize(MaybeUninit::uninit());
if stdout.device_request(&mut req).is_err() {
// Fallback
return Ok((60, 20));
}
let DeviceRequest::GetTerminalSize(size) = req else {
unreachable!();
};
let size = size.assume_init();
Ok((size.columns, size.rows))
}

View File

@ -0,0 +1,10 @@
[package]
name = "serde-ipc"
version = "0.1.0"
edition = "2021"
authors = ["Mark Poliakov <mark@alnyan.me>"]
[dependencies]
flexbuffers = "2.0.0"
serde = { version = "1.0.193", features = ["derive"] }
thiserror = "1.0.56"

View File

@ -0,0 +1,142 @@
#![feature(yggdrasil_os, rustc_private)]
use std::{
io,
marker::PhantomData,
os::{
fd::{AsRawFd, RawFd},
yggdrasil::io::message_channel::{
ChannelPublisherId, MessageChannel, MessageChannelReceiver, MessageChannelSender,
MessageDestination, MessageReceiver, MessageSender, ReceivedMessageMetadata,
},
},
};
use serde::{de::DeserializeOwned, Serialize};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Serialization error: {0}")]
SerializeError(flexbuffers::SerializationError),
#[error("Deserialization error: {0}")]
DeserializeError(flexbuffers::DeserializationError),
#[error("I/O error: {0}")]
IoError(io::Error),
}
pub struct Sender<M: Serialize> {
inner: MessageChannelSender,
_pd: PhantomData<M>,
}
pub struct Receiver<M: DeserializeOwned> {
inner: MessageChannelReceiver,
buffer: [u8; 1024],
_pd: PhantomData<M>,
}
pub enum Message<T> {
Data(T),
File(RawFd),
}
fn raw_send_message_to<T: Serialize, S: MessageSender>(
sender: &S,
msg: &T,
id: ChannelPublisherId,
) -> Result<(), Error> {
let msg = flexbuffers::to_vec(msg)?;
sender
.send_message(&msg, MessageDestination::Specific(id.into()))
.map_err(Error::from)
}
fn raw_send_file_to<F: AsRawFd, S: MessageSender>(
sender: &S,
file: &F,
id: ChannelPublisherId,
) -> Result<(), Error> {
sender
.send_raw_fd(file.as_raw_fd(), MessageDestination::Specific(id.into()))
.map_err(Error::from)
}
pub fn channel<T: Serialize, U: DeserializeOwned>(
name: &str,
) -> Result<(Sender<T>, Receiver<U>), Error> {
let raw = MessageChannel::open(name, true)?;
let (raw_sender, raw_receiver) = raw.split();
Ok((
Sender {
inner: raw_sender,
_pd: PhantomData,
},
Receiver {
inner: raw_receiver,
buffer: [0; 1024],
_pd: PhantomData,
},
))
}
impl<T: Serialize> Sender<T> {
pub fn send_to(&mut self, msg: &T, id: ChannelPublisherId) -> Result<(), Error> {
raw_send_message_to(&self.inner, msg, id)
}
pub fn send_file(&mut self, fd: RawFd, id: ChannelPublisherId) -> Result<(), Error> {
raw_send_file_to(&self.inner, &fd, id)
}
}
impl<T: DeserializeOwned> Receiver<T> {
pub fn receive_message(&mut self) -> Result<(ChannelPublisherId, T), Error> {
loop {
let (id, message) = self.receive_raw()?;
if let Message::Data(data) = message {
break Ok((id, data));
}
}
}
pub fn receive_raw(&mut self) -> Result<(ChannelPublisherId, Message<T>), Error> {
let (id, metadata) = self.inner.receive_raw(&mut self.buffer)?;
match metadata {
ReceivedMessageMetadata::Data(len) => {
let msg = flexbuffers::from_slice(&self.buffer[..len])?;
Ok((id, Message::Data(msg)))
}
ReceivedMessageMetadata::File(fd) => Ok((id, Message::File(fd))),
}
}
pub fn as_poll_fd(&self) -> RawFd {
self.as_raw_fd()
}
}
impl<T: DeserializeOwned> AsRawFd for Receiver<T> {
fn as_raw_fd(&self) -> RawFd {
self.inner.as_raw_fd()
}
}
impl From<flexbuffers::SerializationError> for Error {
fn from(value: flexbuffers::SerializationError) -> Self {
Self::SerializeError(value)
}
}
impl From<flexbuffers::DeserializationError> for Error {
fn from(value: flexbuffers::DeserializationError) -> Self {
Self::DeserializeError(value)
}
}
impl From<io::Error> for Error {
fn from(value: io::Error) -> Self {
Self::IoError(value)
}
}

View File

@ -0,0 +1,45 @@
[package]
name = "netutils"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
yggdrasil-abi = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-abi.git", features = ["serde", "alloc", "bytemuck"] }
clap = { version = "4.3.19", features = ["std", "derive", "help", "usage"], default-features = false }
thiserror = "1.0.50"
bytemuck = { version = "1.14.0", features = ["derive"] }
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.111"
rand = { git = "https://git.alnyan.me/yggdrasil/rand.git", branch = "alnyan/yggdrasil" }
url = "2.5.0"
clap-num = "1.1.1"
[lib]
path = "src/lib.rs"
[[bin]]
name = "netconf"
path = "src/netconf.rs"
[[bin]]
name = "dhcp-client"
path = "src/dhcp_client.rs"
[[bin]]
name = "nc"
path = "src/netcat.rs"
[[bin]]
name = "http"
path = "src/http.rs"
[[bin]]
name = "dnsq"
path = "src/dnsq.rs"
[[bin]]
name = "ping"
path = "src/ping.rs"

View File

@ -0,0 +1,570 @@
#![feature(yggdrasil_os)]
use std::os::{
fd::AsRawFd,
yggdrasil::io::{poll::PollChannel, raw_socket::RawSocket, timer::TimerFd},
};
use std::{io, mem::size_of, process::ExitCode, time::Duration};
use bytemuck::{Pod, Zeroable};
use netutils::{parse_udp_protocol, Error, NetConfig};
use yggdrasil_abi::net::protocols::{
EtherType, EthernetFrame, InetChecksum, IpProtocol, Ipv4Frame, UdpFrame,
};
use yggdrasil_abi::net::types::NetValueImpl;
use yggdrasil_abi::net::{IpAddr, Ipv4Addr, MacAddress, SubnetAddr, SubnetV4Addr};
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
#[repr(C, packed)]
struct DhcpMessageHeader {
op: u8,
htype: u8,
hlen: u8,
hops: u8,
xid: u32,
secs: u16,
flags: u16,
ciaddr: u32,
yiaddr: u32,
siaddr: u32,
giaddr: u32,
chaddr: [u8; 16],
sname: [u8; 64],
file: [u8; 128],
cookie: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DhcpMessageType {
Offer,
Discover,
Request,
Acknowledge,
}
#[derive(Debug)]
enum DhcpOption {
MessageType(DhcpMessageType),
SubnetMask(u32),
Router(Ipv4Addr),
ServerId(Ipv4Addr),
// ParameterRequestList(Vec<u8>),
RequestedAddress(Ipv4Addr),
}
#[derive(Debug)]
enum ParsedOption {
Option(usize, DhcpOption),
Unknown(usize),
Pad,
End,
}
#[derive(Debug)]
struct DhcpMessage {
header: DhcpMessageHeader,
options: Vec<DhcpOption>,
}
#[derive(Debug)]
struct DhcpOffer {
router_address: Ipv4Addr,
server_address: Ipv4Addr,
your_address: Ipv4Addr,
subnet_mask: u32,
}
fn pad_to_align(buffer: &mut Vec<u8>) {
let offset = buffer.len() % 4;
for _ in offset..4 {
buffer.push(0);
}
}
impl From<DhcpMessageType> for u8 {
fn from(value: DhcpMessageType) -> u8 {
match value {
DhcpMessageType::Discover => 0x01,
DhcpMessageType::Offer => 0x02,
DhcpMessageType::Request => 0x03,
DhcpMessageType::Acknowledge => 0x05,
}
}
}
impl TryFrom<u8> for DhcpMessageType {
type Error = ();
fn try_from(value: u8) -> Result<DhcpMessageType, Self::Error> {
match value {
0x01 => Ok(DhcpMessageType::Discover),
0x02 => Ok(DhcpMessageType::Offer),
0x03 => Ok(DhcpMessageType::Request),
0x05 => Ok(DhcpMessageType::Acknowledge),
_ => Err(()),
}
}
}
impl DhcpOption {
fn write(&self, buffer: &mut Vec<u8>) {
match self {
Self::MessageType(ty) => {
buffer.extend_from_slice(&[53, 0x01, (*ty).into()]);
}
// Self::ParameterRequestList(list) => todo!(),
Self::RequestedAddress(addr) => {
buffer.extend_from_slice(&[50, 0x04]);
buffer.extend_from_slice(&u32::from(*addr).to_be_bytes());
}
Self::ServerId(addr) => {
buffer.extend_from_slice(&[54, 0x04]);
buffer.extend_from_slice(&u32::from(*addr).to_be_bytes());
}
_ => todo!(),
}
}
fn parse(data: &[u8]) -> Option<ParsedOption> {
if data.is_empty() {
return None;
}
if data[0] == 0 {
return Some(ParsedOption::Pad);
}
if data[0] == 0xFF {
return Some(ParsedOption::End);
}
if data.len() < 2 {
return None;
}
let opcode = data[0];
let length = data[1] as usize;
if data.len() < 2 + length {
return None;
}
Some(ParsedOption::Option(
2 + length,
match opcode {
// DHCP message type
1 if length == 4 => {
let value = u32::from_be_bytes([data[2], data[3], data[4], data[5]]);
DhcpOption::SubnetMask(value)
}
3 if length == 4 => {
let value = u32::from_be_bytes([data[2], data[3], data[4], data[5]]);
DhcpOption::Router(value.into())
}
53 if length == 1 => {
let ty = DhcpMessageType::try_from(data[2]).ok()?;
DhcpOption::MessageType(ty)
}
54 if length == 4 => {
let value = u32::from_be_bytes([data[2], data[3], data[4], data[5]]);
DhcpOption::ServerId(value.into())
}
_ => return Some(ParsedOption::Unknown(2 + length)),
},
))
}
}
impl DhcpMessage {
fn to_vec(&self) -> Vec<u8> {
let mut result = vec![];
result.extend_from_slice(bytemuck::bytes_of(&self.header));
for option in self.options.iter() {
option.write(&mut result);
pad_to_align(&mut result);
}
// End option
result.push(0xFF);
pad_to_align(&mut result);
result
}
fn parse(data: &[u8]) -> Option<Self> {
if data.len() < size_of::<DhcpMessageHeader>() {
return None;
}
let header: &DhcpMessageHeader =
bytemuck::from_bytes(&data[..size_of::<DhcpMessageHeader>()]);
let mut options = vec![];
let mut offset = size_of::<DhcpMessageHeader>();
loop {
let data = &data[offset..];
let option = DhcpOption::parse(data)?;
match option {
ParsedOption::Option(len, value) => {
options.push(value);
offset += len;
}
ParsedOption::Unknown(len) => {
offset += len;
}
ParsedOption::Pad => {
offset += 1;
}
ParsedOption::End => break,
}
}
Some(Self {
header: *header,
options,
})
}
fn message_type(&self) -> Option<DhcpMessageType> {
self.options.iter().find_map(|option| match option {
DhcpOption::MessageType(ty) => Some(*ty),
_ => None,
})
}
fn as_dhcp_acknowledge(&self, requested_address: Ipv4Addr) -> Option<()> {
let ty = self.message_type()?;
if ty != DhcpMessageType::Acknowledge {
return None;
}
let yiaddr = u32::from_be(self.header.yiaddr);
if yiaddr == 0 {
return None;
}
if Ipv4Addr::from(yiaddr) != requested_address {
return None;
}
Some(())
}
fn as_dhcp_offer(&self) -> Option<DhcpOffer> {
let ty = self.message_type()?;
if ty != DhcpMessageType::Offer {
return None;
}
let yiaddr = u32::from_be(self.header.yiaddr);
if yiaddr == 0 {
return None;
}
let mut router = None;
let mut server = None;
let mut subnet_mask = None;
for option in self.options.iter() {
match option {
DhcpOption::Router(address) if router.is_none() => {
router.replace(*address);
}
DhcpOption::ServerId(address) if server.is_none() => {
server.replace(*address);
}
DhcpOption::SubnetMask(mask) if subnet_mask.is_none() => {
subnet_mask.replace(*mask);
}
_ => (),
}
}
let router_address = router?;
let server_address = server?;
let subnet_mask = subnet_mask?;
Some(DhcpOffer {
server_address,
router_address,
your_address: Ipv4Addr::from(yiaddr),
subnet_mask,
})
}
}
fn send_split_packet(
socket: &RawSocket,
l2_frame: &[u8],
l3_frame: &[u8],
l4_frame: &[u8],
l4_data: &[u8],
) -> Result<(), io::Error> {
let mut buffer =
Vec::with_capacity(l2_frame.len() + l3_frame.len() + l4_frame.len() + l4_data.len());
buffer.extend_from_slice(l2_frame);
buffer.extend_from_slice(l3_frame);
buffer.extend_from_slice(l4_frame);
buffer.extend_from_slice(l4_data);
socket.send(&buffer)?;
Ok(())
}
fn send_udp_broadcast(
socket: &RawSocket,
source_mac: MacAddress,
source_port: u16,
destination_port: u16,
data: &[u8],
) -> Result<(), io::Error> {
let ip_size = size_of::<Ipv4Frame>() + size_of::<UdpFrame>() + data.len();
let udp_size = size_of::<UdpFrame>() + data.len();
let l2_frame = EthernetFrame {
source_mac,
destination_mac: MacAddress::BROADCAST,
ethertype: EtherType::IPV4.to_network_order(),
};
let mut l3_frame = Ipv4Frame {
source_address: u32::from(Ipv4Addr::UNSPECIFIED).to_network_order(),
destination_address: u32::from(Ipv4Addr::BROADCAST).to_network_order(),
protocol: IpProtocol::UDP,
version_length: 0x45,
total_length: u16::try_from(ip_size).unwrap().to_network_order(),
dscp_flags: 0,
flags_frag: 0x4000u16.to_network_order(),
id: 0u16.to_network_order(),
header_checksum: 0u16.to_network_order(),
ttl: 64,
};
let l4_frame = UdpFrame {
source_port: source_port.to_network_order(),
destination_port: destination_port.to_network_order(),
length: u16::try_from(udp_size).unwrap().to_network_order(),
checksum: 0u16.to_network_order(),
};
let mut ip_checksum = InetChecksum::new();
ip_checksum.add_bytes(bytemuck::bytes_of(&l3_frame), true);
let ip_checksum = ip_checksum.finish();
l3_frame.header_checksum = ip_checksum.to_network_order();
send_split_packet(
socket,
bytemuck::bytes_of(&l2_frame),
bytemuck::bytes_of(&l3_frame),
bytemuck::bytes_of(&l4_frame),
data,
)
}
fn wait_for_dhcp_offer(
poll: &mut PollChannel,
timer: &mut TimerFd,
socket: &RawSocket,
) -> Result<DhcpOffer, Error> {
let mut packet = [0; 4096];
let socket_fd = socket.as_raw_fd();
let timer_fd = timer.as_raw_fd();
timer.start(Duration::from_secs(1))?;
loop {
let (fd, result) = poll.wait(None)?.unwrap();
result?;
match fd {
fd if fd == socket_fd => {
let len = socket.recv(&mut packet)?;
let Some((source, destination, data)) = parse_udp_protocol(&packet[..len]) else {
continue;
};
if source.port() != 67 || destination.port() != 68 {
continue;
}
let Some(msg) = DhcpMessage::parse(data) else {
continue;
};
let Some(offer) = msg.as_dhcp_offer() else {
continue;
};
return Ok(offer);
}
fd if fd == timer_fd => {
return Err(Error::TimedOut);
}
_ => unreachable!(),
}
}
}
fn wait_for_dhcp_acknowledge(
poll: &mut PollChannel,
timer: &mut TimerFd,
socket: &RawSocket,
requested_address: Ipv4Addr,
) -> Result<(), Error> {
let mut packet = [0; 4096];
let socket_fd = socket.as_raw_fd();
let timer_fd = timer.as_raw_fd();
timer.start(Duration::from_secs(1))?;
loop {
let (fd, result) = poll.wait(None)?.unwrap();
result?;
match fd {
fd if fd == socket_fd => {
let len = socket.recv(&mut packet)?;
let Some((source, destination, data)) = parse_udp_protocol(&packet[..len]) else {
continue;
};
if source.port() != 67 || destination.port() != 68 {
continue;
}
let Some(msg) = DhcpMessage::parse(data) else {
continue;
};
if msg.as_dhcp_acknowledge(requested_address).is_some() {
return Ok(());
}
}
fd if fd == timer_fd => {
return Err(Error::TimedOut);
}
_ => unreachable!(),
}
}
}
fn attempt_request(
poll: &mut PollChannel,
timer: &mut TimerFd,
socket: &RawSocket,
iface_mac_bytes: [u8; 6],
) -> Result<DhcpOffer, Error> {
let iface_mac = MacAddress::from(iface_mac_bytes);
let transaction_id = rand::random::<u32>();
let mut message = DhcpMessage {
header: DhcpMessageHeader {
op: 1,
htype: 1,
hlen: 6,
xid: transaction_id.to_be(),
chaddr: [0; 16],
cookie: 0x63825363u32.to_be(),
..DhcpMessageHeader::zeroed()
},
options: vec![DhcpOption::MessageType(DhcpMessageType::Discover)],
};
message.header.chaddr[..6].copy_from_slice(&iface_mac_bytes);
let data = message.to_vec();
send_udp_broadcast(socket, iface_mac, 68, 67, &data)?;
let offer = wait_for_dhcp_offer(poll, timer, socket)?;
println!(
"Got DHCP offer: address={} router={} server={} mask={}",
offer.your_address,
offer.router_address,
offer.server_address,
Ipv4Addr::from(offer.subnet_mask)
);
// Send DHCP request
let mut message = DhcpMessage {
header: DhcpMessageHeader {
op: 1,
htype: 1,
hlen: 6,
xid: transaction_id.to_be(),
chaddr: [0; 16],
siaddr: u32::from(offer.server_address).to_be(),
cookie: 0x63825363u32.to_be(),
..DhcpMessageHeader::zeroed()
},
options: vec![
DhcpOption::MessageType(DhcpMessageType::Request),
DhcpOption::RequestedAddress(offer.your_address),
DhcpOption::ServerId(offer.server_address),
],
};
message.header.chaddr[..6].copy_from_slice(&iface_mac_bytes);
let data = message.to_vec();
send_udp_broadcast(socket, iface_mac, 68, 67, &data)?;
wait_for_dhcp_acknowledge(poll, timer, socket, offer.your_address)?;
println!("Got DHCP acknowledge for {}", offer.your_address);
Ok(offer)
}
fn request_address(interface: &str) -> Result<DhcpOffer, Error> {
let mut poll = PollChannel::new()?;
let mut timer = TimerFd::new(false)?;
let socket = RawSocket::bind(interface)?;
let iface_mac_bytes = socket.hardware_address()?;
poll.add(socket.as_raw_fd())?;
poll.add(timer.as_raw_fd())?;
for _ in 0..5 {
match attempt_request(&mut poll, &mut timer, &socket, iface_mac_bytes) {
Ok(offer) => {
return Ok(offer);
}
Err(error) => {
eprintln!("Error: {}, trying again", error);
}
}
}
Err(Error::TimedOut)
}
fn configure_address(interface: &str, offer: DhcpOffer) -> Result<(), Error> {
let mut netconf = NetConfig::open()?;
netconf.set_interface_address(interface, IpAddr::V4(offer.your_address))?;
netconf.add_route(
interface,
SubnetAddr::V4(SubnetV4Addr::UNSPECIFIED),
Some(IpAddr::V4(offer.router_address)),
)?;
Ok(())
}
fn main() -> ExitCode {
let args: Vec<_> = std::env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} INTERFACE", args[0]);
return ExitCode::FAILURE;
}
let offer = match request_address(&args[1]) {
Ok(offer) => offer,
Err(error) => {
eprintln!("Could not obtain an address from DHCP: {}", error);
return ExitCode::FAILURE;
}
};
match configure_address(&args[1], offer) {
Ok(_) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("Could not assign offered address to {}: {}", args[1], error);
ExitCode::FAILURE
}
}
}

View File

@ -0,0 +1,260 @@
#![feature(yggdrasil_os)]
use std::{
collections::HashMap,
os::{
fd::AsRawFd,
yggdrasil::io::{poll::PollChannel, timer::TimerFd},
},
};
use std::{
io,
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
process::ExitCode,
time::Duration,
};
use clap::{Parser, ValueEnum};
use yggdrasil_abi::net::dns::{
DnsClass, DnsMessage, DnsMethod, DnsRecordData, DnsReplyCode, DnsSerialize, DnsType,
};
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("{0}")]
IoError(#[from] io::Error),
#[error("Timed out")]
TimedOut,
}
#[derive(Parser)]
struct Arguments {
#[clap(short, long)]
server: Option<SocketAddr>,
#[clap(short, long)]
nameservers: Vec<IpAddr>,
#[clap(short, long, value_enum)]
record_type: Option<RecordType>,
name: Option<String>,
}
#[derive(Clone, ValueEnum)]
enum RecordType {
A,
}
#[derive(Debug, thiserror::Error)]
enum ServerError {
#[error("{0}")]
Error(#[from] Error),
#[error("Invalid query")]
InvalidQuery,
#[error("Name does not exist")]
NameDoesNotExist,
}
fn handle_query(
nameservers: &[SocketAddr],
a_cache: &mut HashMap<String, Ipv4Addr>,
ty: DnsType,
class: DnsClass,
name: &str,
) -> Result<IpAddr, ServerError> {
match (ty, class) {
(DnsType::A, DnsClass::IN) => {
if let Some(ip) = a_cache.get(name) {
debug_trace!("Cache hit: {} -> {}", name, ip);
return Ok(IpAddr::V4(*ip));
}
debug_trace!("Cache miss: {}", name);
let result = perform_query(nameservers, name, ty)?;
for answer in result.answers {
let Some(name) = answer.name.0 else {
continue;
};
#[allow(clippy::single_match)]
match (answer.ty, answer.class, answer.rdata) {
(DnsType::A, DnsClass::IN, DnsRecordData::A(ip)) => {
debug_trace!("Resolved: {} -> {}", name, ip);
a_cache.insert(name, ip.into());
}
_ => (),
}
}
a_cache
.get(name)
.copied()
.map(IpAddr::V4)
.ok_or(ServerError::NameDoesNotExist)
}
_ => Err(ServerError::InvalidQuery),
}
}
fn run_server(addr: SocketAddr, nameservers: &[SocketAddr]) -> Result<(), Error> {
// TODO cache TTL
let mut a_cache = HashMap::new();
let mut buffer = [0; 2048];
let socket = UdpSocket::bind(addr)?;
loop {
let (len, remote) = socket.recv_from(&mut buffer)?;
let Some(message) = DnsMessage::parse(&buffer[..len]) else {
continue;
};
let mut answers = vec![];
for (name, ty, class) in message
.questions
.iter()
.filter_map(|q| q.name.0.as_deref().map(|name| (name, q.ty, q.class)))
{
match handle_query(nameservers, &mut a_cache, ty, class, name) {
Ok(ip) => answers.push((name.to_owned(), ip.into())),
Err(_) => {
continue;
}
}
}
let reply_code = if answers.len() == message.questions.len() {
DnsReplyCode::NO_ERROR
} else {
DnsReplyCode::NAME_ERROR
};
let mut packet = vec![];
let reply = DnsMessage::reply(reply_code, message.questions, answers, message.xid);
reply.serialize(&mut packet);
if let Err(error) = socket.send_to(&packet, remote) {
debug_trace!("sendto: {}", error);
}
}
}
fn perform_query(nameservers: &[SocketAddr], name: &str, ty: DnsType) -> Result<DnsMessage, Error> {
let mut buffer = [0; 2048];
let socket = UdpSocket::bind("0.0.0.0:0")?;
let mut timer = TimerFd::new(false)?;
let mut poll = PollChannel::new()?;
let timer_fd = timer.as_raw_fd();
let socket_fd = socket.as_raw_fd();
poll.add(socket_fd)?;
poll.add(timer_fd)?;
for _ in 0..5 {
let mut packet = vec![];
let xid = rand::random();
let cookie = rand::random();
let query = DnsMessage::query(name, ty, xid, cookie);
query.serialize(&mut packet);
for nameserver in nameservers.iter() {
socket.send_to(&packet, nameserver)?;
}
timer.start(Duration::from_millis(500))?;
loop {
let (poll_fd, result) = poll.wait(None)?.unwrap();
result?;
match poll_fd {
fd if fd == socket_fd => {
let (len, _) = socket.recv_from(&mut buffer)?;
let Some(message) = DnsMessage::parse(&buffer[..len]) else {
continue;
};
if message.xid != xid {
continue;
}
if message.method != DnsMethod::REPLY {
eprintln!("WARN: server sent a QUERY instead of REPLY");
continue;
}
return Ok(message);
}
fd if fd == timer_fd => {
break;
}
_ => unreachable!(),
}
}
}
Err(Error::TimedOut)
}
fn main() -> ExitCode {
let mut args: Arguments = Arguments::parse();
if let Some(server) = args.server {
let nameservers: Vec<_> = args
.nameservers
.into_iter()
.map(|ip| SocketAddr::new(ip, 53))
.collect();
if nameservers.is_empty() {
eprintln!("-s requires one or more nameservers");
return ExitCode::FAILURE;
}
match run_server(server, &nameservers) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("Error: {}", error);
ExitCode::FAILURE
}
}
} else if let Some(name) = args.name {
if args.nameservers.is_empty() {
args.nameservers
.push(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
}
let nameservers: Vec<_> = args
.nameservers
.into_iter()
.map(|ip| SocketAddr::new(ip, 53))
.collect();
let ty = match args.record_type.unwrap_or(RecordType::A) {
RecordType::A => DnsType::A,
};
let message = match perform_query(&nameservers, &name, ty) {
Ok(message) => message,
Err(error) => {
eprintln!("Error: {}", error);
return ExitCode::FAILURE;
}
};
if message.reply_code == DnsReplyCode::NO_ERROR {
for answer in message.answers.iter() {
let name = answer.name.0.as_deref().unwrap_or("<NULL>");
#[allow(clippy::single_match)]
match (answer.ty, answer.class, &answer.rdata) {
(DnsType::A, DnsClass::IN, DnsRecordData::A(address)) => {
println!("{}: {}", name, address);
}
_ => (),
}
}
ExitCode::SUCCESS
} else {
eprintln!("Server returned error: {}", message.reply_code);
ExitCode::FAILURE
}
} else {
eprintln!("Either -s LISTEN-ADDRESS or NAME must be specified");
ExitCode::FAILURE
}
}

View File

@ -0,0 +1,148 @@
use std::{
fs::File,
io::{self, stdout, Read, Stdout, Write},
net::TcpStream,
path::{Path, PathBuf},
process::ExitCode,
str::FromStr,
};
use clap::{Parser, Subcommand};
use url::{Host, Url};
#[derive(Debug, Parser)]
struct Arguments {
#[clap(short, long)]
output: Option<PathBuf>,
#[clap(subcommand)]
method: Method,
}
#[derive(Debug, Subcommand)]
enum Method {
#[clap(arg_required_else_help = true)]
Get {
#[clap(help = "URL to GET")]
url: Url,
},
}
enum Output {
File(File),
Stdout(Stdout),
}
impl Output {
fn open<P: AsRef<Path>>(path: Option<P>) -> Result<Self, io::Error> {
match path {
Some(path) => File::create(path).map(Self::File),
None => Ok(Self::Stdout(stdout())),
}
}
}
impl Write for Output {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::File(file) => file.write(buf),
Self::Stdout(file) => file.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::File(file) => file.flush(),
Self::Stdout(file) => file.flush(),
}
}
}
fn receive_header<R: Read>(stream: &mut R) -> Result<Option<String>, io::Error> {
let mut buf = [0];
let mut line = Vec::new();
loop {
stream.read_exact(&mut buf[..1])?;
if buf[0] == b'\r' {
continue;
}
if buf[0] == b'\n' {
break;
}
line.push(buf[0]);
}
if line.is_empty() {
return Ok(None);
}
let line = String::from_utf8(line).unwrap();
Ok(Some(line))
}
fn get(url: Url, output: Option<PathBuf>) -> Result<(), io::Error> {
let mut output = Output::open(output)?;
let host = url.host().unwrap();
let request = format!(
"GET {} HTTP/1.1\r\nHost: {}\r\nAccept: */*\r\n\r\n",
url.path(),
host
);
let port = url.port_or_known_default().unwrap();
let mut stream = match host {
Host::Domain(hostname) => TcpStream::connect((hostname, port))?,
Host::Ipv4(address) => TcpStream::connect((address, port))?,
Host::Ipv6(address) => TcpStream::connect((address, port))?,
};
eprintln!("Connecting to {}:{}...", host, port);
//let mut stream = TcpStream::connect(remote)?;
let mut buf = [0; 2048];
for line in request.split('\n') {
eprintln!("> {}", line.trim());
}
stream.write_all(request.as_bytes())?;
let mut content_length = 0;
while let Some(header) = receive_header(&mut stream)? {
let Some((key, value)) = header.split_once(':') else {
continue;
};
let key = key.trim();
let value = value.trim();
if key == "Content-Length" {
content_length = usize::from_str(value).unwrap();
}
eprintln!("< {}", header);
}
while content_length != 0 {
let limit = buf.len().min(content_length);
let amount = stream.read(&mut buf[..limit])?;
output.write_all(&buf[..amount])?;
content_length -= amount;
}
Ok(())
}
fn main() -> ExitCode {
let args = Arguments::parse();
let result = match args.method {
Method::Get { url } => get(url, args.output),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("Error: {}", error);
ExitCode::FAILURE
}
}
}

View File

@ -0,0 +1,171 @@
#![feature(yggdrasil_os, rustc_private)]
use std::{
io,
mem::size_of,
os::yggdrasil::io::message_channel::{
MessageChannel, MessageChannelReceiver, MessageChannelSender, MessageDestination,
MessageReceiver, MessageSender,
},
};
use serde::Deserialize;
use yggdrasil_abi::net::{
netconfig::{InterfaceQuery, NetConfigRequest, NetConfigResult, RoutingInfo},
protocols::{EtherType, EthernetFrame, IpProtocol, Ipv4Frame, UdpFrame},
types::NetValueImpl,
IpAddr, Ipv4Addr, MacAddress, SocketAddr, SubnetAddr,
};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("I/O error: {0}")]
IoError(#[from] io::Error),
#[error("NetConfig returned error: {0}")]
NetConfError(Box<str>),
#[error("Serialize/deserialize error: {0}")]
SerializeError(#[from] serde_json::Error),
#[error("Timed out")]
TimedOut,
}
pub struct NetConfig {
sender: MessageChannelSender,
receiver: MessageChannelReceiver,
buffer: [u8; 4096],
}
impl NetConfig {
pub fn open() -> Result<Self, Error> {
let channel = MessageChannel::open("@kernel-netconf", true)?;
let (sender, receiver) = channel.split();
Ok(Self {
sender,
receiver,
buffer: [0; 4096],
})
}
pub fn send(&mut self, request: &NetConfigRequest) -> Result<(), Error> {
let bytes = serde_json::to_vec(&request)?;
self.sender
.send_message(&bytes, MessageDestination::Specific(0))?;
Ok(())
}
pub fn request<'de, T: Deserialize<'de>>(
&'de mut self,
request: &NetConfigRequest,
) -> Result<T, Error> {
self.send(request)?;
let (_sender, len) = self.receiver.receive_message(&mut self.buffer)?;
let msg: NetConfigResult<T> = serde_json::from_slice(&self.buffer[..len])?;
match msg {
NetConfigResult::Ok(value) => Ok(value),
NetConfigResult::Err(error) => Err(Error::NetConfError(error)),
}
}
pub fn query_route(&mut self, address: IpAddr) -> Result<RoutingInfo, Error> {
self.request(&NetConfigRequest::QueryRoute(address))
}
pub fn query_arp(
&mut self,
interface_id: u32,
address: IpAddr,
perform_query: bool,
) -> Result<MacAddress, Error> {
self.request(&NetConfigRequest::QueryArp(
interface_id,
address,
perform_query,
))
}
pub fn set_interface_address<Q: Into<InterfaceQuery>>(
&mut self,
interface: Q,
address: IpAddr,
) -> Result<(), Error> {
self.request(&NetConfigRequest::SetNetworkAddress {
interface: interface.into(),
address,
})
}
pub fn add_route<Q: Into<InterfaceQuery>>(
&mut self,
interface: Q,
subnet: SubnetAddr,
gateway: Option<IpAddr>,
) -> Result<(), Error> {
self.request(&NetConfigRequest::AddRoute {
interface: interface.into(),
gateway,
subnet,
})
}
}
pub fn parse_l2_protocol(packet: &[u8]) -> Option<(EthernetFrame, &[u8])> {
if packet.len() < size_of::<EthernetFrame>() {
return None;
}
let l2_frame: &EthernetFrame = bytemuck::from_bytes(&packet[..size_of::<EthernetFrame>()]);
let l2_data = &packet[size_of::<EthernetFrame>()..];
Some((*l2_frame, l2_data))
}
pub fn parse_ip_protocol(packet: &[u8]) -> Option<(IpProtocol, IpAddr, IpAddr, &[u8])> {
let (l2_frame, l2_data) = parse_l2_protocol(packet)?;
match EtherType::from_network_order(l2_frame.ethertype) {
EtherType::IPV4 if l2_data.len() >= size_of::<Ipv4Frame>() => {
let l3_frame: &Ipv4Frame = bytemuck::from_bytes(&l2_data[..size_of::<Ipv4Frame>()]);
let source_addr = IpAddr::V4(Ipv4Addr::from(u32::from_network_order(
l3_frame.source_address,
)));
let destination_addr = IpAddr::V4(Ipv4Addr::from(u32::from_network_order(
l3_frame.destination_address,
)));
Some((
l3_frame.protocol,
source_addr,
destination_addr,
&l2_data[l3_frame.header_length()..l3_frame.total_length()],
))
}
_ => None,
}
}
pub fn parse_udp_protocol(packet: &[u8]) -> Option<(SocketAddr, SocketAddr, &[u8])> {
let (protocol, source_ip, destination_ip, l3_data) = parse_ip_protocol(packet)?;
if protocol != IpProtocol::UDP || l3_data.len() < size_of::<UdpFrame>() {
return None;
}
let l4_frame: &UdpFrame = bytemuck::from_bytes(&l3_data[..size_of::<UdpFrame>()]);
let l4_data_size = core::cmp::min(
l3_data.len() - size_of::<UdpFrame>(),
l4_frame.data_length(),
);
let source_addr = SocketAddr::new(source_ip, u16::from_network_order(l4_frame.source_port));
let destination_addr = SocketAddr::new(
destination_ip,
u16::from_network_order(l4_frame.destination_port),
);
Some((
source_addr,
destination_addr,
&l3_data[size_of::<UdpFrame>()..size_of::<UdpFrame>() + l4_data_size],
))
}

View File

@ -0,0 +1,164 @@
#![feature(yggdrasil_os)]
use std::os::fd::AsRawFd;
use std::os::yggdrasil::io::poll::PollChannel;
use std::{
io::{self, stdin, stdout, Read, Write},
net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, TcpStream, UdpSocket},
process::ExitCode,
};
use clap::Parser;
#[derive(Debug, Parser)]
struct Args {
#[clap(short, long)]
listen: bool,
#[clap(short, long)]
udp: bool,
#[clap(short = 'p', long)]
listen_port: Option<u16>,
address: Option<SocketAddr>,
}
fn udp_client(address: SocketAddr) -> io::Result<()> {
let mut stdin = stdin();
let mut stdout = stdout();
let mut poll = PollChannel::new()?;
// TODO use ephemeral port?
// TODO use socket.connect()
let socket = UdpSocket::bind(SocketAddr::new(
IpAddr::V4(Ipv4Addr::UNSPECIFIED),
address.port(),
))?;
let mut buffer = [0; 512];
let socket_fd = socket.as_raw_fd();
let stdin_fd = stdin.as_raw_fd();
poll.add(socket_fd)?;
poll.add(stdin_fd)?;
loop {
let (fd, status) = poll.wait(None)?.unwrap();
status?;
match fd {
fd if fd == socket_fd => {
let (len, _) = socket.recv_from(&mut buffer)?;
stdout.write_all(&buffer[..len])?;
}
fd if fd == stdin_fd => {
let len = stdin.read(&mut buffer)?;
if len == 0 {
break;
}
socket.send_to(&buffer[..len], address)?;
}
_ => unreachable!(),
}
}
Ok(())
}
fn udp_server(port: u16) -> io::Result<()> {
let mut stdout = stdout();
let socket = UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port))?;
let mut buffer = [0; 2048];
loop {
let (len, _) = socket.recv_from(&mut buffer)?;
stdout.write_all(&buffer[..len])?;
}
}
fn tcp_client(address: SocketAddr) -> io::Result<()> {
let mut stdin = stdin();
let mut stdout = stdout();
let mut poll = PollChannel::new()?;
let mut socket = TcpStream::connect(address)?;
let mut buffer = [0; 512];
let socket_fd = socket.as_raw_fd();
let stdin_fd = stdin.as_raw_fd();
poll.add(socket_fd)?;
poll.add(stdin_fd)?;
loop {
let (fd, status) = poll.wait(None)?.unwrap();
status?;
match fd {
fd if fd == socket_fd => {
let len = socket.read(&mut buffer)?;
stdout.write_all(&buffer[..len])?;
}
fd if fd == stdin_fd => {
let len = stdin.read(&mut buffer)?;
if len == 0 {
break;
}
socket.write_all(&buffer[..len])?;
}
_ => unreachable!(),
}
}
Ok(())
}
fn tcp_server(port: u16) -> io::Result<()> {
let mut stdout = stdout();
let listener = TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port))?;
let mut buffer = [0; 2048];
let (mut stream, _) = listener.accept()?;
loop {
let len = stream.read(&mut buffer)?;
stdout.write_all(&buffer[..len])?;
}
}
fn main() -> ExitCode {
let args = Args::parse();
if args.listen && args.address.is_some() {
eprintln!("--listen/-l and remote address cannot be used simultaneously");
return ExitCode::FAILURE;
}
let result = if args.listen {
let Some(port) = args.listen_port else {
eprintln!("--listen/-l requires --listen-port/-p PORT");
return ExitCode::FAILURE;
};
if args.udp {
udp_server(port)
} else {
tcp_server(port)
}
} else {
let Some(address) = args.address else {
eprintln!("Remote address required");
return ExitCode::FAILURE;
};
if args.udp {
udp_client(address)
} else {
tcp_client(address)
}
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("Error: {}", error);
ExitCode::FAILURE
}
}
}

View File

@ -0,0 +1,277 @@
use std::{net::IpAddr, process::ExitCode, str::FromStr};
use clap::{Args, Parser, Subcommand};
use netutils::{Error, NetConfig};
use yggdrasil_abi::net::netconfig::{InterfaceInfo, InterfaceQuery, NetConfigRequest, RouteInfo};
#[derive(Debug, Parser)]
#[clap(author, version, about, long_about = None)]
pub struct Arguments {
#[clap(subcommand)]
section: Section,
}
#[derive(Debug, Subcommand)]
pub enum Section {
#[clap(arg_required_else_help = true, about = "Route query and manipulation")]
Route(#[clap(subcommand)] RouteAction),
#[clap(
arg_required_else_help = true,
about = "Address query and manipulation"
)]
Addr(#[clap(subcommand)] AddrAction),
#[clap(
arg_required_else_help = true,
about = "Link information query and manipulation"
)]
Link(#[clap(subcommand)] LinkAction),
}
#[derive(Debug, Args)]
pub struct RouteAction {
#[clap(subcommand)]
command: RouteCommands,
}
#[derive(Debug, Args)]
pub struct AddrAction {
#[clap(subcommand)]
command: AddrCommands,
}
#[derive(Debug, Args)]
pub struct LinkAction {
#[clap(subcommand)]
command: LinkCommands,
}
#[derive(Debug, Subcommand)]
pub enum RouteCommands {
#[clap(about = "List routes")]
List {
#[clap(help = "Filter routes by given network interface")]
interface: Option<String>,
},
#[clap(about = "Add a route", arg_required_else_help = true)]
Add {
#[clap(help = "Network interface the route is associated with")]
interface: String,
#[clap(
help = "Subnetwork the route is directed to",
value_parser = SubnetAddr::from_str
)]
subnet: SubnetAddr,
#[clap(help = "Gateway the route should be reached through")]
gateway: Option<IpAddr>,
},
}
#[derive(Debug, Subcommand)]
pub enum AddrCommands {
#[clap(about = "List network interface IP addresses")]
List,
#[clap(about = "Get interface IP address")]
Get {
#[clap(help = "Interface to query")]
interface: String,
},
#[clap(about = "Set interface's IP address")]
Set {
#[clap(help = "Interface to assign address to")]
interface: String,
#[clap(help = "IPv4/IPv6 address for the interface")]
address: IpAddr,
},
}
#[derive(Debug, Subcommand)]
pub enum LinkCommands {
#[clap(about = "List network interfaces")]
List,
#[clap(about = "Describe a network interface")]
Show {
#[clap(help = "Interface to describe")]
interface: String,
},
#[clap(about = "Get interface MAC address")]
GetMac {
#[clap(help = "Interface to describe")]
interface: String,
},
}
#[derive(Clone, Debug)]
pub struct SubnetAddr(yggdrasil_abi::net::SubnetAddr);
impl FromStr for SubnetAddr {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let inner =
yggdrasil_abi::net::SubnetAddr::from_str(s).map_err(|_| "Invalid subnet format")?;
Ok(Self(inner))
}
}
pub fn run_route_action(nc: &mut NetConfig, action: RouteCommands) -> Result<(), Error> {
match action {
RouteCommands::List { interface: None } => {
let list = nc.request::<Vec<RouteInfo>>(&NetConfigRequest::ListRoutes)?;
for route in list {
print_route_info(&route);
}
Ok(())
}
RouteCommands::List {
interface: Some(interface),
} => {
let list = nc.request::<Vec<RouteInfo>>(&NetConfigRequest::DescribeRoutes(
InterfaceQuery::ByName(interface.into_boxed_str()),
))?;
for route in list {
print_route_info(&route);
}
Ok(())
}
RouteCommands::Add {
interface,
subnet,
gateway,
} => nc.request::<()>(&NetConfigRequest::AddRoute {
interface: InterfaceQuery::ByName(interface.into_boxed_str()),
gateway: gateway.map(Into::into),
subnet: subnet.0,
}),
}
}
pub fn run_addr_action(nc: &mut NetConfig, action: AddrCommands) -> Result<(), Error> {
match action {
AddrCommands::List => {
let list = nc.request::<Vec<(Box<str>, u32)>>(&NetConfigRequest::ListInterfaces)?;
for (name, id) in list {
let info = match nc.request::<InterfaceInfo>(&NetConfigRequest::DescribeInterface(
InterfaceQuery::ById(id),
)) {
Ok(info) => info,
Err(error) => {
eprintln!("{}: {}", name, error);
continue;
}
};
if let Some(address) = info.address {
println!("{}: {}", name, address);
}
}
Ok(())
}
AddrCommands::Get { interface } => {
let info = nc.request::<InterfaceInfo>(&NetConfigRequest::DescribeInterface(
InterfaceQuery::ByName(interface.into_boxed_str()),
))?;
if let Some(address) = info.address {
println!("{}", address);
} else {
println!("<No address>");
}
Ok(())
}
AddrCommands::Set { interface, address } => {
nc.request::<()>(&NetConfigRequest::SetNetworkAddress {
interface: InterfaceQuery::ByName(interface.into_boxed_str()),
address: address.into(),
})
}
}
}
pub fn run_link_action(nc: &mut NetConfig, action: LinkCommands) -> Result<(), Error> {
match action {
LinkCommands::List => {
let list = nc.request::<Vec<(Box<str>, u32)>>(&NetConfigRequest::ListInterfaces)?;
for (name, id) in list {
let info = match nc.request::<InterfaceInfo>(&NetConfigRequest::DescribeInterface(
InterfaceQuery::ById(id),
)) {
Ok(info) => info,
Err(error) => {
eprintln!("{}: {}", name, error);
continue;
}
};
print_interface_info(&info);
}
Ok(())
}
LinkCommands::Show { interface } => {
let info = nc.request::<InterfaceInfo>(&NetConfigRequest::DescribeInterface(
InterfaceQuery::ByName(interface.into_boxed_str()),
))?;
print_interface_info(&info);
Ok(())
}
LinkCommands::GetMac { interface } => {
let info = nc.request::<InterfaceInfo>(&NetConfigRequest::DescribeInterface(
InterfaceQuery::ByName(interface.into_boxed_str()),
))?;
println!("{}", info.mac);
Ok(())
}
}
}
pub fn run_action(section: Section) -> Result<(), Error> {
let mut nc = NetConfig::open()?;
match section {
Section::Route(RouteAction { command }) => run_route_action(&mut nc, command),
Section::Addr(AddrAction { command }) => run_addr_action(&mut nc, command),
Section::Link(LinkAction { command }) => run_link_action(&mut nc, command),
}
}
fn print_interface_info(info: &InterfaceInfo) {
println!("{}:", info.interface_name);
println!(" Id: #{}", info.interface_id);
if let Some(address) = info.address {
println!(" Address: {}", address);
} else {
println!(" <No address>");
}
println!(" Hardware address: {}", info.mac);
}
fn print_route_info(info: &RouteInfo) {
print!("{}", info.subnet);
if let Some(gw) = info.gateway {
print!(" via {}", gw);
}
println!(" dev {}", info.interface_name);
}
fn main() -> ExitCode {
let args = Arguments::parse();
match run_action(args.section) {
Ok(_) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("Error: {}", error);
ExitCode::FAILURE
}
}
}

View File

@ -0,0 +1,354 @@
#![feature(yggdrasil_os, rustc_private)]
use std::{
mem::size_of,
os::{
fd::AsRawFd,
yggdrasil::{
io::{poll::PollChannel, raw_socket::RawSocket, timer::TimerFd},
signal::{set_signal_handler, Signal, SignalHandler},
},
},
process::ExitCode,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use bytemuck::Zeroable;
use clap::Parser;
use netutils::{Error, NetConfig};
use yggdrasil_abi::net::{
protocols::{EtherType, EthernetFrame, IcmpV4Frame, InetChecksum, IpProtocol, Ipv4Frame},
types::NetValueImpl,
IpAddr, Ipv4Addr, MacAddress,
};
#[derive(Parser)]
struct Args {
#[clap(
help = "Time (ms) between a reply is received and the next request is sent",
short,
long,
default_value_t = 1000,
value_parser = valid_interval
)]
inteval: u32,
#[clap(
help = "Time (ms) after which the request is considered unanswered",
short,
long,
default_value_t = 500,
value_parser = valid_timeout,
)]
timeout: u32,
#[clap(
help = "Number of requests to perform",
short,
long,
default_value_t = 10
)]
count: usize,
#[clap(
help = "Amount of bytes to include as data",
short,
long,
default_value_t = 64,
value_parser = valid_data_size
)]
data_size: usize,
#[clap(help = "Address to ping")]
address: core::net::IpAddr,
}
fn valid_interval(s: &str) -> Result<u32, String> {
clap_num::number_range(s, 100, 10000)
}
fn valid_timeout(s: &str) -> Result<u32, String> {
clap_num::number_range(s, 100, 5000)
}
fn valid_data_size(s: &str) -> Result<usize, String> {
clap_num::number_range(s, 4, 128)
}
struct PingRouting {
interface_id: u32,
source_ip: IpAddr,
destination_ip: IpAddr,
source_mac: MacAddress,
gateway_mac: MacAddress,
}
struct PingStats {
packets_sent: usize,
packets_received: usize,
}
fn resolve_routing(address: IpAddr) -> Result<PingRouting, Error> {
let mut nc = NetConfig::open()?;
let routing = nc.query_route(address)?;
let Some(source) = routing.source else {
todo!();
};
let Some(gateway) = routing.gateway else {
todo!();
};
let gateway_mac = nc.query_arp(routing.interface_id, gateway, true)?;
Ok(PingRouting {
interface_id: routing.interface_id,
source_ip: source,
destination_ip: routing.destination,
source_mac: routing.source_mac,
gateway_mac,
})
}
fn validate_ping_reply(
packet: &[u8],
local: Ipv4Addr,
remote: Ipv4Addr,
expect_l4_data: &[u8],
expect_id: u16,
expect_seq: u16,
) -> bool {
if packet.len() < size_of::<EthernetFrame>() + size_of::<Ipv4Frame>() {
return false;
}
let l3_offset = size_of::<EthernetFrame>();
let l2_frame: &EthernetFrame = bytemuck::from_bytes(&packet[..l3_offset]);
if EtherType::from_network_order(l2_frame.ethertype) != EtherType::IPV4 {
return false;
}
let l3_frame: &Ipv4Frame =
bytemuck::from_bytes(&packet[l3_offset..l3_offset + size_of::<Ipv4Frame>()]);
if l3_frame.protocol != IpProtocol::ICMP
|| u32::from_network_order(l3_frame.source_address) != u32::from(remote)
|| u32::from_network_order(l3_frame.destination_address) != u32::from(local)
{
return false;
}
let mut ip_checksum = InetChecksum::new();
ip_checksum.add_value(l3_frame, true);
let ip_checksum = ip_checksum.finish();
if ip_checksum != 0 {
eprintln!("IP checksum mismatch: {:#06x}", ip_checksum);
return false;
}
let l4_offset = l3_offset + l3_frame.header_length();
let l4_size = l3_frame
.total_length()
.saturating_sub(l3_frame.header_length());
if packet.len() < l4_offset + size_of::<IcmpV4Frame>() + expect_l4_data.len() {
return false;
}
let l4_frame: &IcmpV4Frame =
bytemuck::from_bytes(&packet[l4_offset..l4_offset + size_of::<IcmpV4Frame>()]);
let l4_data = &packet[l4_offset + size_of::<IcmpV4Frame>()..l4_offset + l4_size];
if l4_frame.ty != 0 || l4_frame.code != 0 {
return false;
}
let rest = u32::from_network_order(l4_frame.rest);
let reply_id = (rest >> 16) as u16;
let reply_seq = rest as u16;
if reply_id != expect_id || reply_seq != expect_seq {
eprintln!(
"ICMP seq/id mismatch: sent {}/{}, got {}/{}",
expect_id, expect_seq, reply_id, reply_seq
);
return false;
}
let mut icmp_checksum = InetChecksum::new();
icmp_checksum.add_value(l4_frame, true);
icmp_checksum.add_bytes(l4_data, true);
let icmp_checksum = icmp_checksum.finish();
if icmp_checksum != 0 {
eprintln!("ICMP checksum mismatch: {:#06x}", icmp_checksum);
return false;
}
l4_data == expect_l4_data
}
fn ping_once(
socket: &mut RawSocket,
poll: &mut PollChannel,
timer: &mut TimerFd,
info: &PingRouting,
timeout: Duration,
data_len: usize,
id: u16,
seq: u16,
) -> Result<bool, Error> {
let mut buffer = [0; 4096];
let source_ip = info.source_ip.into_ipv4().unwrap();
let destination_ip = info.destination_ip.into_ipv4().unwrap();
let mut l4_data = vec![];
l4_data.reserve(data_len);
for _ in 0..data_len {
l4_data.push(rand::random());
}
let ip_len = (size_of::<Ipv4Frame>() + size_of::<IcmpV4Frame>() + data_len)
.try_into()
.unwrap();
let l2_frame = EthernetFrame {
source_mac: info.source_mac,
destination_mac: info.gateway_mac,
ethertype: EtherType::IPV4.to_network_order(),
};
let mut l3_frame = Ipv4Frame {
source_address: u32::from(source_ip).to_network_order(),
destination_address: u32::from(destination_ip).to_network_order(),
protocol: IpProtocol::ICMP,
version_length: 0x45,
total_length: u16::to_network_order(ip_len),
flags_frag: u16::to_network_order(0x4000),
id: u16::to_network_order(0),
ttl: 255,
..Ipv4Frame::zeroed()
};
let mut l4_frame = IcmpV4Frame {
ty: 8,
code: 0,
checksum: u16::to_network_order(0),
rest: u32::to_network_order(((id as u32) << 16) | (seq as u32)),
};
let mut ip_checksum = InetChecksum::new();
ip_checksum.add_value(&l3_frame, true);
l3_frame.header_checksum = ip_checksum.finish().to_network_order();
let mut icmp_checksum = InetChecksum::new();
icmp_checksum.add_value(&l4_frame, true);
icmp_checksum.add_bytes(&l4_data, true);
l4_frame.checksum = icmp_checksum.finish().to_network_order();
let mut packet = vec![];
packet.extend_from_slice(bytemuck::bytes_of(&l2_frame));
packet.extend_from_slice(bytemuck::bytes_of(&l3_frame));
packet.extend_from_slice(bytemuck::bytes_of(&l4_frame));
packet.extend_from_slice(&l4_data);
timer.start(timeout)?;
socket.send(&packet)?;
loop {
let (fd, result) = poll.wait(None)?.unwrap();
result?;
match fd {
fd if fd == socket.as_raw_fd() => {
// TODO
let len = socket.recv(&mut buffer)?;
if validate_ping_reply(&buffer[..len], source_ip, destination_ip, &l4_data, id, seq)
{
return Ok(true);
}
}
fd if fd == timer.as_raw_fd() => {
return Ok(false);
}
_ => unreachable!(),
}
}
}
fn ping(
address: IpAddr,
times: usize,
data_len: usize,
interval: Duration,
timeout: Duration,
) -> Result<PingStats, Error> {
let routing = resolve_routing(address)?;
let mut stats = PingStats {
packets_sent: 0,
packets_received: 0,
};
let mut poll = PollChannel::new()?;
let mut timer = TimerFd::new(false)?;
let mut socket = RawSocket::bind(routing.interface_id)?;
poll.add(timer.as_raw_fd())?;
poll.add(socket.as_raw_fd())?;
let id = rand::random();
for i in 0..times {
if INTERRUPTED.load(Ordering::Acquire) {
break;
}
let result = ping_once(
&mut socket,
&mut poll,
&mut timer,
&routing,
timeout,
data_len,
id,
i as u16,
)?;
stats.packets_sent += 1;
if result {
stats.packets_received += 1;
println!("[{}/{}] {}: PONG", i + 1, times, address);
}
std::thread::sleep(interval);
}
Ok(stats)
}
fn interrupt(_: Signal) {
INTERRUPTED.store(true, Ordering::Release);
}
static INTERRUPTED: AtomicBool = AtomicBool::new(false);
fn main() -> ExitCode {
set_signal_handler(Signal::Interrupted, SignalHandler::Function(interrupt));
let args = Args::parse();
let stats = match ping(
args.address.into(),
args.count,
args.data_size,
Duration::from_millis(args.inteval.into()),
Duration::from_millis(args.timeout.into()),
) {
Ok(stats) => stats,
Err(error) => {
eprintln!("ping: {}", error);
return ExitCode::FAILURE;
}
};
let loss = (stats.packets_sent - stats.packets_received) * 100 / stats.packets_sent;
println!(
"{} sent, {} received, {}% loss",
stats.packets_sent, stats.packets_received, loss
);
ExitCode::SUCCESS
}

424
userspace/red/Cargo.lock generated Normal file
View File

@ -0,0 +1,424 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "crossterm"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.4.1",
"crossterm_winapi",
"libc",
"mio",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]]
name = "deranged"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
dependencies = [
"powerfmt",
]
[[package]]
name = "error-chain"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc"
dependencies = [
"version_check",
]
[[package]]
name = "hostname"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
dependencies = [
"libc",
"match_cfg",
"winapi",
]
[[package]]
name = "itoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "libc"
version = "0.2.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
[[package]]
name = "lock_api"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "mio"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys",
]
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "red"
version = "0.1.0"
dependencies = [
"crossterm",
"libc",
"syslog",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "smallvec"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
[[package]]
name = "syn"
version = "2.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syslog"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7434e95bcccce1215d30f4bf84fe8c00e8de1b9be4fb736d747ca53d36e7f96f"
dependencies = [
"error-chain",
"hostname",
"libc",
"log",
"time",
]
[[package]]
name = "time"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
dependencies = [
"deranged",
"itoa",
"libc",
"num_threads",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
dependencies = [
"time-core",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[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.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"

18
userspace/red/Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "red"
version = "0.1.0"
edition = "2021"
authors = ["Mark Poliakov <mark@alnyan.me>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
libterm = { path = "../lib/libterm" }
thiserror = "1.0.50"
unicode-width = "0.1.11"
[target.'cfg(not(target_os = "yggdrasil"))'.dependencies]
libc = "0.2.150"
crossterm = "0.27.0"
syslog = "6.1.0"

View File

@ -0,0 +1,543 @@
use std::{
ffi::OsStr,
fmt::Write as FmtWrite,
fs::File,
io::{self, BufRead, BufReader, BufWriter, Write as IoWrite},
path::{Path, PathBuf},
};
use libterm::{Color, CursorStyle, Term};
use unicode_width::UnicodeWidthChar;
use crate::{
config::Config,
error::Error,
line::{Line, TextLike},
};
#[derive(Default)]
pub struct View {
cursor_column: usize,
cursor_row: usize,
column_offset: usize,
row_offset: usize,
width: usize,
height: usize,
offset_x: usize,
}
#[derive(Clone, Copy, PartialEq)]
pub enum Mode {
Normal,
Insert,
}
pub enum SetMode {
Normal,
InsertBefore,
InsertAfter,
}
pub struct Buffer {
lines: Vec<Line>,
dirty: bool,
mode_dirty: bool,
mode: Mode,
view: View,
name: Option<String>,
path: Option<PathBuf>,
modified: bool,
}
impl Mode {
pub fn as_str(&self) -> &str {
match self {
Self::Normal => "NORMAL",
Self::Insert => "INSERT",
}
}
}
impl View {
pub fn set_row(&mut self, row: usize) {
self.cursor_row = row;
if self.cursor_row < self.row_offset {
self.row_offset = self.cursor_row;
} else if self.cursor_row >= self.row_offset + self.height {
self.row_offset = self.cursor_row - self.height + 1;
}
}
pub fn set_column(&mut self, config: &Config, col: usize, line: Option<&Line>) {
let Some(line) = line else {
self.column_offset = 0;
self.cursor_column = 0;
return;
};
self.cursor_column = col;
if line.display_width(config.tab_width) < self.width {
self.column_offset = 0;
return;
}
let width_to_cursor = line
.span(..self.cursor_column)
.display_width(config.tab_width);
if width_to_cursor < self.column_offset {
self.column_offset = width_to_cursor;
} else if width_to_cursor >= self.column_offset + self.width {
self.column_offset = width_to_cursor - self.width + 1;
}
}
pub fn reset(&mut self) {
self.column_offset = 0;
self.row_offset = 0;
self.cursor_row = 0;
self.cursor_column = 0;
}
// pub fn resize(&mut self, width: usize, height: usize) {
// self.width = width;
// self.height = height;
// self.column_offset = 0;
// self.row_offset = 0;
// }
}
impl Buffer {
pub fn empty() -> Self {
Self {
lines: vec![],
dirty: true,
mode_dirty: true,
view: View::default(),
mode: Mode::Normal,
name: None,
path: None,
modified: false,
}
}
fn read_lines<P: AsRef<Path>>(path: P) -> io::Result<Vec<Line>> {
let input = BufReader::new(File::open(path)?);
let lines = input.lines().collect::<Result<Vec<_>, _>>()?;
let lines = lines
.into_iter()
.map(|line| Line::from_str(line.trim_end_matches('\n')))
.collect();
Ok(lines)
}
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let path = path.as_ref();
let name = path.file_name().and_then(OsStr::to_str).map(String::from);
let lines = if path.exists() {
Self::read_lines(path)?
} else {
vec![]
};
Ok(Self {
lines,
name,
path: Some(path.into()),
mode: Mode::Normal,
dirty: true,
mode_dirty: true,
view: View::default(),
modified: false,
})
}
pub fn reopen<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref();
let name = path.file_name().and_then(OsStr::to_str).map(String::from);
let lines = if path.exists() {
Self::read_lines(path)?
} else {
vec![]
};
self.lines = lines;
self.modified = false;
self.mode = Mode::Normal;
self.dirty = true;
self.mode_dirty = true;
self.view.reset();
self.path = Some(path.into());
self.name = name;
Ok(())
}
pub fn save(&mut self) -> Result<(), Error> {
let path = self.path.as_ref().ok_or(Error::NoPath)?;
let mut writer = BufWriter::new(File::create(path).map_err(Error::WriteError)?);
for line in self.lines.iter() {
writer
.write_all(line.to_string().as_ref())
.map_err(Error::WriteError)?;
writer.write_all(b"\n").map_err(Error::WriteError)?;
}
self.modified = false;
Ok(())
}
pub fn set_path<P: AsRef<Path>>(&mut self, path: P) {
let path = PathBuf::from(path.as_ref());
let name = path.file_name().and_then(OsStr::to_str).map(String::from);
self.path = Some(path);
self.name = name;
}
pub fn set_mode(&mut self, config: &Config, mode: SetMode) {
let dst_mode = match mode {
SetMode::Normal => Mode::Normal,
SetMode::InsertAfter | SetMode::InsertBefore => Mode::Insert,
};
if dst_mode == self.mode {
return;
}
self.mode = dst_mode;
self.mode_dirty = true;
match mode {
SetMode::Normal => self.move_cursor(config, -1, 0),
SetMode::InsertBefore => self.move_cursor(config, 0, 0),
SetMode::InsertAfter => self.move_cursor(config, 1, 0),
}
}
pub fn mode(&self) -> Mode {
self.mode
}
pub fn name(&self) -> Option<&String> {
self.name.as_ref()
}
pub fn path(&self) -> Option<&PathBuf> {
self.path.as_ref()
}
pub fn row_offset(&self) -> usize {
self.view.row_offset
}
pub fn len(&self) -> usize {
self.lines.len()
}
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
pub fn cursor_row(&self) -> usize {
self.view.cursor_row
}
pub fn set_position(&mut self, config: &Config, px: usize, py: usize) {
self.dirty = true;
if self.lines.is_empty() {
self.view.reset();
return;
}
// Move to row
self.view.set_row(py.min(self.lines.len() - 1));
// Set mode- and line-len-adjusted column
if let Some(line) = self.lines.get(self.view.cursor_row)
&& !line.is_empty()
{
match self.mode {
// Limited by line.len()
Mode::Normal => self
.view
.set_column(config, px.min(line.len() - 1), Some(line)),
Mode::Insert => self.view.set_column(config, px.min(line.len()), Some(line)),
}
} else {
self.view.set_column(config, 0, None);
}
}
pub fn to_line_end(&mut self, config: &Config) {
let len = self
.lines
.get(self.view.cursor_row)
.map(Line::len)
.unwrap_or(0);
self.set_position(config, len, self.view.cursor_row);
}
pub fn to_first_line(&mut self, config: &Config) {
let len = self
.lines
.get(self.view.cursor_row)
.map(Line::len)
.unwrap_or(0);
self.set_position(config, len, 0);
}
pub fn to_last_line(&mut self, config: &Config) {
if self.lines.is_empty() {
return;
}
let len = self
.lines
.get(self.view.cursor_row)
.map(Line::len)
.unwrap_or(0);
self.set_position(config, len, self.lines.len() - 1);
}
pub fn set_column(&mut self, config: &Config, x: usize) {
self.set_position(config, x, self.view.cursor_row);
}
pub fn move_cursor(&mut self, config: &Config, dx: isize, dy: isize) {
let px = (self.view.cursor_column as isize + dx).max(0) as usize;
let py = (self.view.cursor_row as isize + dy).max(0) as usize;
self.set_position(config, px, py);
}
pub fn resize(&mut self, config: &Config, offset_x: usize, width: usize, height: usize) {
self.dirty = true;
self.view.height = height;
self.view.width = width;
self.view.offset_x = offset_x;
self.view.set_row(self.view.cursor_row);
self.view.set_column(
config,
self.view.cursor_column,
self.lines.get(self.view.cursor_row),
);
}
pub fn display_cursor(&self, config: &Config) -> (usize, usize) {
if self.lines.is_empty() {
return (0, 0);
}
// assert!(self.view.column_offset <= self.view.cursor_column);
assert!(self.view.row_offset <= self.view.cursor_row);
let line = &self.lines[self.view.cursor_row];
assert!(self.view.cursor_column <= line.len());
let column = line
.span(..self.view.cursor_column)
.display_width(config.tab_width);
assert!(self.view.column_offset <= column);
(
column - self.view.column_offset,
self.view.cursor_row - self.view.row_offset,
)
}
pub fn width(&self) -> usize {
self.view.width
}
pub fn height(&self) -> usize {
self.view.height
}
pub fn is_modified(&self) -> bool {
self.modified
}
pub fn is_dirty(&self) -> bool {
self.dirty
}
fn display_line(
&self,
config: &Config,
term: &mut Term,
row: usize,
line: &Line,
) -> Result<(), Error> {
let mut pos = 0;
term.set_cursor_position(row, self.view.offset_x)?;
let span = line.skip_to_width(self.view.column_offset, config.tab_width);
let long_line = span.display_width(config.tab_width) > self.view.width;
for &ch in span.iter() {
if pos >= self.view.width {
break;
}
if ch == '\t' {
let old_pos = pos;
let new_pos = (pos + config.tab_width) & !(config.tab_width - 1);
pos = new_pos;
for i in old_pos..new_pos {
if i >= self.view.width {
break;
}
if i == old_pos {
term.set_foreground(Color::Blue)?;
term.write_char('>').map_err(Error::TerminalFmtError)?;
term.set_foreground(Color::Default)?;
} else {
term.write_char(' ').map_err(Error::TerminalFmtError)?;
}
}
} else {
write!(term, "{}", ch).map_err(Error::TerminalFmtError)?;
pos += ch.width().unwrap_or(1);
}
}
if long_line {
term.set_cursor_position(row, self.view.width + self.view.offset_x)?;
term.set_foreground(Color::Black)?;
term.set_background(Color::White)?;
term.write_char('>').map_err(Error::TerminalFmtError)?;
term.set_foreground(Color::Default)?;
term.set_background(Color::Default)?;
}
Ok(())
}
pub fn display(&mut self, config: &Config, term: &mut Term) -> Result<(), Error> {
for (row, line) in self
.lines
.iter()
.skip(self.view.row_offset)
.take(self.view.height)
.enumerate()
{
self.display_line(config, term, row, line)?;
}
Ok(())
}
pub fn set_terminal_cursor(&mut self, config: &Config, term: &mut Term) -> Result<(), Error> {
let (x, y) = self.display_cursor(config);
if self.mode_dirty {
match self.mode {
Mode::Normal => term.set_cursor_style(CursorStyle::Default)?,
Mode::Insert => term.set_cursor_style(CursorStyle::Line)?,
}
}
term.set_cursor_position(y, x + self.view.offset_x)?;
self.mode_dirty = false;
Ok(())
}
pub fn newline_before(&mut self) {
self.modified = true;
self.lines.insert(self.view.cursor_row, Line::new());
}
pub fn newline_after(&mut self, break_line: bool) {
if self.lines.is_empty() {
self.lines.push(Line::new());
return;
}
self.modified = true;
let newline = if break_line {
self.lines[self.view.cursor_row].split_off(self.view.cursor_column)
} else {
Line::new()
};
self.lines.insert(self.view.cursor_row + 1, newline);
}
pub fn insert(&mut self, config: &Config, ch: char) {
if self.lines.is_empty() {
assert_eq!(self.view.cursor_row, 0);
self.lines.push(Line::new());
}
let line = &mut self.lines[self.view.cursor_row];
line.insert(self.view.cursor_column, ch);
self.move_cursor(config, 1, 0);
self.modified = true;
}
pub fn erase_backward(&mut self, config: &Config) {
if self.lines.is_empty() {
return;
}
if self.view.cursor_column == 0 {
if self.view.cursor_row != 0 {
let line = self.lines.remove(self.view.cursor_row);
let prev_line = &mut self.lines[self.view.cursor_row - 1];
let len = prev_line.len();
prev_line.extend(line);
self.set_position(config, len, self.view.cursor_row - 1);
self.modified = true;
}
return;
}
let line = &mut self.lines[self.view.cursor_row];
line.remove(self.view.cursor_column - 1);
self.move_cursor(config, -1, 0);
self.modified = true;
}
pub fn erase_forward(&mut self) {
if self.lines.is_empty() {
return;
}
let line = &mut self.lines[self.view.cursor_row];
if self.view.cursor_column == line.len() {
return;
}
line.remove(self.view.cursor_column);
self.dirty = true;
self.modified = true;
}
pub fn kill_line(&mut self, config: &Config) {
if self.lines.is_empty() {
return;
}
self.lines.remove(self.view.cursor_row);
self.move_cursor(config, 0, 1);
self.modified = true;
}
pub fn number_width(&mut self) -> usize {
if self.lines.is_empty() {
1
} else {
self.lines.len().ilog10() as usize + 1
}
}
}

View File

@ -0,0 +1,137 @@
use std::ops::RangeInclusive;
use crate::{error::Error, State, buffer::{Buffer, SetMode}, config::Config};
pub type CommandFn = fn(&mut State, &[&str]) -> Result<(), Error>;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Action {
// Editing
EraseBackward,
InsertBefore,
InsertAfter,
NewlineBefore,
NewlineAfter,
BreakLine,
KillLine,
// Movement
MoveFirstLine,
MoveLastLine,
MoveCharPrev,
MoveCharNext,
MoveLineBack(usize),
MoveLineForward(usize),
MoveLineStart,
MoveLineEnd,
}
static COMMANDS: &[(&str, RangeInclusive<usize>, CommandFn)] = &[
("w", 0..=1, cmd_write),
("w!", 0..=1, cmd_force_write),
("q", 0..=0, cmd_exit),
("q!", 0..=0, cmd_force_exit),
("e", 1..=1, cmd_edit),
("e!", 0..=1, cmd_force_edit),
];
// Commands
fn cmd_write(state: &mut State, args: &[&str]) -> Result<(), Error> {
if args.len() == 1 && state.buffer().is_modified() && state.buffer().path().is_some() {
return Err(Error::UnsavedBuffer("Use :w! FILE to force write to another file"));
}
cmd_force_write(state, args)
}
fn cmd_force_write(state: &mut State, args: &[&str]) -> Result<(), Error> {
let buffer = state.buffer_mut();
if let Some(&path) = args.first() {
buffer.set_path(path);
}
buffer.save()?;
if let Some(name) = buffer.name() {
let status = format!("{:?} written", name);
state.set_status(status);
}
Ok(())
}
fn cmd_edit(state: &mut State, args: &[&str]) -> Result<(), Error> {
if state.buffer().is_modified() {
return Err(Error::UnsavedBuffer("Use :e! [FILE] to open another file"));
}
cmd_force_edit(state, args)
}
fn cmd_force_edit(state: &mut State, args: &[&str]) -> Result<(), Error> {
if let Some(&path) = args.first() {
state.buffer_mut().reopen(path).map_err(Error::OpenError)
} else if let Some(path) = state.buffer().path().cloned() {
state.buffer_mut().reopen(path).map_err(Error::OpenError)
} else {
Err(Error::NoPath)
}
}
fn cmd_exit(state: &mut State, _args: &[&str]) -> Result<(), Error> {
let buffer = state.buffer();
if buffer.is_modified() {
return Err(Error::UnsavedBuffer("Use :q! to force exit"));
}
state.exit();
Ok(())
}
fn cmd_force_exit(state: &mut State, _args: &[&str]) -> Result<(), Error> {
state.exit();
Ok(())
}
pub fn execute(state: &mut State, command: String) -> Result<(), Error> {
let words = command.split(' ').collect::<Vec<_>>();
let Some((&cmd, args)) = words.split_first() else {
return Ok(());
};
for (name, nargs, f) in COMMANDS.iter() {
if *name == cmd {
if !nargs.contains(&args.len()) {
todo!();
}
return f(state, args);
}
}
Err(Error::UnknownCommand(cmd.into()))
}
pub fn perform(buffer: &mut Buffer, config: &Config, action: Action) -> Result<(), Error> {
match action {
// Editing
Action::EraseBackward => buffer.erase_backward(config),
Action::InsertBefore => buffer.set_mode(config, SetMode::InsertBefore),
Action::InsertAfter => buffer.set_mode(config, SetMode::InsertAfter),
Action::NewlineBefore => buffer.newline_before(),
Action::NewlineAfter => buffer.newline_after(false),
Action::BreakLine => buffer.newline_after(true),
Action::KillLine => buffer.kill_line(config),
// Movement
Action::MoveCharPrev => buffer.move_cursor(config, -1, 0),
Action::MoveCharNext => buffer.move_cursor(config, 1, 0),
Action::MoveLineBack(count) => buffer.move_cursor(config, 0, -(count as isize)),
Action::MoveLineForward(count) => buffer.move_cursor(config, 0, count as isize),
Action::MoveLineStart => buffer.set_column(config, 0),
Action::MoveLineEnd => buffer.to_line_end(config),
Action::MoveFirstLine => buffer.to_first_line(config),
Action::MoveLastLine => buffer.to_last_line(config),
}
Ok(())
}

View File

@ -0,0 +1,66 @@
use crate::{
buffer::Mode,
command::Action,
keymap::{bindn, bind1, KeyMap, KeySeq, PrefixNode},
};
pub struct Config {
// TODO must be a power of 2, lol
pub tab_width: usize,
pub number: bool,
pub nmap: KeyMap,
pub imap: KeyMap,
}
impl Default for Config {
fn default() -> Self {
use Action::*;
let nmap = KeyMap::from_iter([
bind1('i', [InsertBefore]),
bind1('a', [InsertAfter]),
bind1('h', [MoveCharPrev]),
bind1('l', [MoveCharNext]),
bind1('j', [MoveLineForward(1)]),
bind1('J', [MoveLineForward(25)]),
bind1('k', [MoveLineBack(1)]),
bind1('K', [MoveLineBack(25)]),
bindn(['g', 'g'], [MoveFirstLine]),
bind1('G', [MoveLastLine]),
bind1('I', [MoveLineStart, InsertBefore]),
bind1('A', [MoveLineEnd, InsertAfter]),
bind1('o', [NewlineAfter, MoveLineForward(1), InsertBefore]),
bind1('O', [NewlineBefore, MoveLineBack(1), InsertBefore]),
bindn(['d', 'd'], [KillLine]),
]);
let imap = KeyMap::from_iter([
bind1('\x7F', [EraseBackward]),
bind1(
'\n',
[BreakLine, MoveLineForward(1), MoveLineStart, InsertBefore],
),
bind1(
'\x0D',
[BreakLine, MoveLineForward(1), MoveLineStart, InsertBefore],
),
]);
Self {
tab_width: 4,
number: true,
nmap,
imap,
}
}
}
impl Config {
pub fn key_seq(&self, mode: Mode, seq: &KeySeq) -> Option<&PrefixNode<KeySeq, Vec<Action>>> {
match mode {
Mode::Normal => self.nmap.get(seq),
Mode::Insert => self.imap.get(seq),
}
}
}

View File

@ -0,0 +1,22 @@
use std::{fmt, io};
#[derive(Debug, thiserror::Error)]
pub enum Error {
// I/O errors
#[error("Could not open file: {0}")]
OpenError(io::Error),
#[error("Could not write file: {0}")]
WriteError(io::Error),
#[error("Buffer does not have a path")]
NoPath,
#[error("Buffer has unsaved changes: {0}")]
UnsavedBuffer(&'static str),
#[error("Invalid command, usage: {0}")]
InvalidCommand(&'static str),
#[error("Unknown command: `{0}`")]
UnknownCommand(String),
#[error("Terminal error: {0}")]
TerminalError(#[from] libterm::Error),
#[error("Terminal error: {0:?}")]
TerminalFmtError(fmt::Error),
}

View File

@ -0,0 +1,50 @@
use std::fmt::{self, Write};
use libterm::TermKey;
use super::map::Prefix;
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct KeySeq(Vec<TermKey>);
impl KeySeq {
pub fn one<I: Into<TermKey>>(k: I) -> Self {
Self(vec![k.into()])
}
pub fn empty() -> Self {
Self(vec![])
}
pub fn push<I: Into<TermKey>>(&mut self, k: I) {
self.0.push(k.into());
}
pub fn clear(&mut self) {
self.0.clear();
}
}
impl Prefix for KeySeq {
fn prefix(&self) -> Option<Self> {
self.0.split_last().map(|(_, vs)| vs.to_vec()).map(Self)
}
}
impl<K: Into<TermKey>, I: IntoIterator<Item = K>> From<I> for KeySeq {
fn from(value: I) -> Self {
Self(value.into_iter().map(Into::into).collect())
}
}
impl fmt::Display for KeySeq {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, key) in self.0.iter().enumerate() {
if i != 0 {
f.write_char('+')?;
}
fmt::Display::fmt(key, f)?;
}
Ok(())
}
}

View File

@ -0,0 +1,100 @@
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
fmt,
hash::Hash,
};
pub trait Prefix: Sized + Eq + Hash + Clone {
fn prefix(&self) -> Option<Self>;
}
#[derive(PartialEq)]
pub enum PrefixNode<K: Prefix, V> {
Prefix(HashSet<K>),
Leaf(V),
}
#[derive(Default)]
pub struct PrefixMap<K: Prefix, V> {
map: HashMap<K, PrefixNode<K, V>>,
}
impl<K: Prefix, V> PrefixNode<K, V> {
pub fn empty_prefix() -> Self {
Self::Prefix(HashSet::new())
}
pub fn add_suffix(&mut self, suffix: K) {
if let Self::Prefix(set) = self {
set.insert(suffix);
} else {
*self = Self::Prefix(HashSet::from_iter([suffix]));
}
}
}
impl<K: Prefix + fmt::Debug, V: fmt::Debug> fmt::Debug for PrefixNode<K, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Leaf(leaf) => f.debug_struct("Leaf").field("value", leaf).finish(),
Self::Prefix(suffixes) => f.debug_struct("Prefix").field("suffixes", suffixes).finish(),
}
}
}
impl<K: Prefix, V> PrefixMap<K, V> {
pub fn new() -> Self {
Self {
map: HashMap::new(),
}
}
pub fn insert(&mut self, key: K, value: V) {
if let Some(prefix) = key.prefix() {
self.map
.entry(prefix)
.or_insert_with(PrefixNode::empty_prefix)
.add_suffix(key.clone());
}
// TODO remove all suffixes of `key`, if those exist
self.map.insert(key, PrefixNode::Leaf(value));
}
pub fn get<N>(&self, key: &N) -> Option<&PrefixNode<K, V>>
where
K: Borrow<N>,
N: Eq + Hash + ?Sized,
{
self.map.get(key)
}
}
impl<K: Prefix, V> FromIterator<(K, V)> for PrefixMap<K, V> {
fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
let mut this = Self::new();
for (k, v) in iter {
this.insert(k, v);
}
this
}
}
impl<K: Prefix + fmt::Debug, V: fmt::Debug> fmt::Debug for PrefixMap<K, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut dm = f.debug_map();
for (k, v) in self.map.iter().filter_map(|(k, v)| {
if let PrefixNode::Leaf(leaf) = v {
Some((k, leaf))
} else {
None
}
}) {
dm.entry(k, v);
}
dm.finish()
}
}

View File

@ -0,0 +1,61 @@
use std::{borrow::Borrow, hash::Hash};
use crate::command::Action;
use self::map::PrefixMap;
pub use self::map::PrefixNode;
mod key;
mod map;
pub use key::KeySeq;
use libterm::TermKey;
#[derive(Debug)]
pub struct KeyMap {
map: PrefixMap<KeySeq, Vec<Action>>,
}
impl KeyMap {
pub fn new() -> Self {
Self {
map: PrefixMap::new(),
}
}
pub fn get<N>(&self, key: &N) -> Option<&PrefixNode<KeySeq, Vec<Action>>>
where
KeySeq: Borrow<N>,
N: Eq + Hash + ?Sized,
{
self.map.get(key)
}
}
impl FromIterator<(KeySeq, Vec<Action>)> for KeyMap {
fn from_iter<T: IntoIterator<Item = (KeySeq, Vec<Action>)>>(iter: T) -> Self {
Self {
map: PrefixMap::from_iter(iter),
}
}
}
pub fn bindn<I: Into<KeySeq>, V: IntoIterator<Item = Action>>(
key: I,
actions: V,
) -> (KeySeq, Vec<Action>) {
(key.into(), actions.into_iter().collect())
}
pub fn bind1<I: Into<TermKey>, V: IntoIterator<Item = Action>>(
key: I,
actions: V,
) -> (KeySeq, Vec<Action>) {
(KeySeq::one(key), actions.into_iter().collect())
}
#[cfg(test)]
mod tests {
#[test]
fn from_iter() {}
}

229
userspace/red/src/line.rs Normal file
View File

@ -0,0 +1,229 @@
use std::{ops::Index, slice::SliceIndex};
use unicode_width::UnicodeWidthChar;
#[derive(Debug, PartialEq, Default)]
pub struct Line {
data: Vec<char>,
}
#[derive(Debug, PartialEq)]
pub struct Span<'a>(&'a [char]);
pub trait TextLike: Index<usize, Output = char> + ToString {
type Iter<'a>: Iterator<Item = &'a char>
where
Self: 'a;
type Span<'a>: TextLike + 'a
where
Self: 'a;
fn display_width(&self, tab_width: usize) -> usize;
fn span<R: SliceIndex<[char], Output = [char]>>(&self, range: R) -> Self::Span<'_>;
fn skip_to_width(&self, offset: usize, tab_width: usize) -> Self::Span<'_>;
fn iter(&self) -> Self::Iter<'_>;
}
// Line
impl Line {
pub fn new() -> Self {
Self { data: vec![] }
}
#[allow(clippy::should_implement_trait)]
pub fn from_str<S: AsRef<str>>(s: S) -> Self {
let chars = s.as_ref().chars();
Self {
data: Vec::from_iter(chars),
}
}
pub fn as_span(&self) -> Span {
Span(self.data.as_ref())
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn split_off(&mut self, at: usize) -> Line {
let data = self.data.split_off(at);
Line { data }
}
pub fn insert(&mut self, at: usize, ch: char) {
self.data.insert(at, ch);
}
pub fn remove(&mut self, at: usize) {
self.data.remove(at);
}
pub fn extend(&mut self, other: Line) {
self.data.extend(other.data);
}
}
impl IntoIterator for Line {
type Item = char;
type IntoIter = std::vec::IntoIter<char>;
fn into_iter(self) -> Self::IntoIter {
self.data.into_iter()
}
}
impl TextLike for Line {
type Span<'a> = Span<'a>;
type Iter<'a> = std::slice::Iter<'a, char>;
fn display_width(&self, tab_width: usize) -> usize {
self.as_span().display_width(tab_width)
}
fn span<R: SliceIndex<[char], Output = [char]>>(&self, range: R) -> Self::Span<'_> {
self.as_span().span(range)
}
fn skip_to_width(&self, offset: usize, tab_width: usize) -> Self::Span<'_> {
self.as_span().skip_to_width(offset, tab_width)
}
fn iter(&self) -> Self::Iter<'_> {
self.data.iter()
}
}
impl Index<usize> for Line {
type Output = char;
fn index(&self, index: usize) -> &Self::Output {
&self.data[index]
}
}
impl ToString for Line {
fn to_string(&self) -> String {
self.as_span().to_string()
}
}
// Span
impl<'s> TextLike for Span<'s> {
type Iter<'a> = std::slice::Iter<'a, char> where 's: 'a;
type Span<'a> = Span<'s> where 's: 'a;
fn display_width(&self, tab_width: usize) -> usize {
self.0.iter().fold(0, |pos, &ch| match ch {
'\t' => (pos + tab_width) & !(tab_width - 1),
_ => pos + ch.width().unwrap_or(1),
})
}
fn span<R: SliceIndex<[char], Output = [char]>>(&self, range: R) -> Self::Span<'_> {
Span(&self.0[range])
}
fn skip_to_width(&self, offset: usize, tab_width: usize) -> Self::Span<'_> {
let mut index = 0;
let mut pos = 0;
for &ch in self.0.iter() {
if pos >= offset {
break;
}
match ch {
'\t' => pos = (pos + tab_width) & !(tab_width - 1),
_ => pos += ch.width().unwrap_or(1),
}
index += 1;
}
self.span(index..)
}
fn iter(&self) -> Self::Iter<'_> {
self.0.iter()
}
}
impl Index<usize> for Span<'_> {
type Output = char;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl ToString for Span<'_> {
fn to_string(&self) -> String {
self.0.iter().collect()
}
}
#[cfg(test)]
mod tests {
use crate::line::{Span, TextLike};
use super::Line;
#[test]
fn line_from_str() {
// pure ASCII
let text = "abc123\n\t xyz";
let line = Line::from_str(text);
assert_eq!(
line.data,
vec!['a', 'b', 'c', '1', '2', '3', '\n', '\t', ' ', 'x', 'y', 'z']
);
// cyrillic unicode
let text = "це тест123";
let line = Line::from_str(text);
assert_eq!(
line.data,
vec!['ц', 'е', ' ', 'т', 'е', 'с', 'т', '1', '2', '3']
);
// japanese unicode
let text = "1日本2";
let line = Line::from_str(text);
assert_eq!(line.data, vec!['1', '日', '本', '2']);
}
#[test]
fn line_to_string() {
let line = Line {
data: vec!['a', 'b', 'c', 'т', 'е', 'с', 'т', '1', '2', '3', '\n'],
};
assert_eq!(line.to_string().as_str(), "abcтест123\n");
}
#[test]
fn line_span() {
// All span
let line = Line::from_str("abcdef");
assert_eq!(line.as_span(), Span(&['a', 'b', 'c', 'd', 'e', 'f']));
assert_eq!(line.span(..3), Span(&['a', 'b', 'c']));
assert_eq!(line.span(..=3), Span(&['a', 'b', 'c', 'd']));
assert_eq!(line.span(..=3).span(2..), Span(&['c', 'd']));
assert_eq!(line.span(2..=3), Span(&['c', 'd']));
}
#[test]
fn line_width() {
// No tabs
let line = Line::from_str("abcdef");
assert_eq!(line.display_width(4), line.len());
// Tabs
let line = Line::from_str("\ta\tbcdef");
assert_eq!(line.display_width(4), 8 + 5);
}
}

371
userspace/red/src/main.rs Normal file
View File

@ -0,0 +1,371 @@
#![feature(let_chains, rustc_private)]
#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os))]
#![allow(clippy::new_without_default)]
use std::{env, fmt::Write, path::Path};
use libterm::{Clear, Color, Term, TermKey};
use buffer::{Buffer, Mode, SetMode};
use config::Config;
use error::Error;
use keymap::{KeySeq, PrefixNode};
pub mod buffer;
pub mod command;
pub mod config;
pub mod error;
pub mod keymap;
pub mod line;
pub mod term;
#[derive(Clone, Copy, PartialEq)]
pub enum TopMode {
Normal,
Command,
}
pub struct State {
term: Term,
buffer: Buffer,
command: String,
message: Option<String>,
status: Option<String>,
key_seq: KeySeq,
top_mode: TopMode,
config: Config,
running: bool,
number_width: usize,
}
impl State {
pub fn open<P: AsRef<Path>>(path: Option<P>) -> Result<Self, Error> {
let config = Config::default();
let mut buffer = match path {
Some(path) => Buffer::open(path).unwrap(),
None => Buffer::empty(),
};
let term = Term::open()?;
let (w, h) = term.size()?;
if config.number {
let nw = buffer.number_width() + 2;
buffer.resize(&config, nw, w - nw - 1, h - 2);
} else {
buffer.resize(&config, 0, w - 1, h - 2);
}
Ok(Self {
number_width: buffer.number_width(),
top_mode: TopMode::Normal,
message: None,
status: None,
command: String::new(),
key_seq: KeySeq::empty(),
running: true,
buffer,
term,
config,
})
}
pub fn buffer(&self) -> &Buffer {
&self.buffer
}
pub fn buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffer
}
pub fn exit(&mut self) {
self.running = false;
}
pub fn set_status<S: Into<String>>(&mut self, status: S) {
self.status.replace(status.into());
}
fn display_number(&mut self) -> Result<(), Error> {
let start = self.buffer.row_offset();
let end = self.buffer.len();
for i in 0.. {
self.term.set_cursor_position(i, 0)?;
if i + start == self.buffer.cursor_row() {
self.term.set_bright(true)?;
self.term.set_foreground(Color::Yellow)?;
}
if i + start < end {
write!(self.term, " {0:1$} ", i + start + 1, self.number_width)
.map_err(Error::TerminalFmtError)?;
}
if i == self.buffer.height() {
break;
}
if i + start == self.buffer.cursor_row() {
self.term.reset_style()?;
}
}
self.term.reset_style()?;
Ok(())
}
fn display_modeline(&mut self) -> Result<(), Error> {
self.term.set_cursor_position(self.buffer.height(), 0)?;
let bg = match (self.top_mode, self.buffer.mode()) {
(TopMode::Normal, Mode::Normal) => Color::Yellow,
(TopMode::Normal, Mode::Insert) => Color::Cyan,
(TopMode::Command, _) => Color::Green,
};
self.term.set_background(bg)?;
self.term.set_foreground(Color::Black)?;
match self.top_mode {
TopMode::Normal => {
write!(self.term, " {} ", self.buffer.mode().as_str())
.map_err(Error::TerminalFmtError)?;
if self.buffer.is_modified() {
self.term.set_background(Color::Magenta)?;
self.term.set_foreground(Color::Default)?;
} else {
self.term.set_foreground(Color::Green)?;
self.term.set_background(Color::Default)?;
}
}
TopMode::Command => {
write!(self.term, " COMMAND ").map_err(Error::TerminalFmtError)?;
self.term.set_foreground(Color::Green)?;
self.term.set_background(Color::Default)?;
}
}
let name = self
.buffer
.name()
.map(String::as_str)
.unwrap_or("<unnamed>");
write!(self.term, " {}", name).map_err(Error::TerminalFmtError)?;
self.term.clear(Clear::LineToEnd)?;
self.term
.set_cursor_position(self.buffer.height(), self.buffer.width() - 10)?;
self.term.set_foreground(Color::White)?;
write!(self.term, "{}", self.key_seq).map_err(Error::TerminalFmtError)?;
self.term.reset_style()?;
Ok(())
}
fn display(&mut self) -> Result<(), Error> {
if self.buffer.is_dirty() {
self.term.clear(Clear::All)?;
}
if self.config.number && self.buffer.is_dirty() {
self.display_number()?;
}
self.buffer.display(&self.config, &mut self.term)?;
if self.top_mode != TopMode::Command {
if let Some(status) = &self.status {
self.term
.set_cursor_position(self.buffer().height() + 1, 0)?;
self.term
.write_str(status.as_str())
.map_err(Error::TerminalFmtError)?;
}
}
if let Some(msg) = &self.message {
self.term.set_cursor_position(self.buffer.height(), 0)?;
self.term.write_str(msg).map_err(Error::TerminalFmtError)?;
self.term.flush()?;
return Ok(());
}
self.display_modeline()?;
match self.top_mode {
TopMode::Normal => {
self.buffer
.set_terminal_cursor(&self.config, &mut self.term)?;
}
TopMode::Command => {
self.term.set_cursor_position(self.buffer.height() + 1, 0)?;
write!(self.term, ":{}", self.command.as_str()).map_err(Error::TerminalFmtError)?;
}
}
self.term.flush()?;
Ok(())
}
fn handle_command(&mut self) -> Result<(), Error> {
let cmd = self.command.clone();
command::execute(self, cmd)
}
fn handle_command_key(&mut self, key: TermKey) -> Result<(), Error> {
match key {
TermKey::Char('\n') | TermKey::Char('\x0D') => {
self.top_mode = TopMode::Normal;
self.handle_command()?;
}
TermKey::Char('\x7F') => {
if self.command.is_empty() {
self.top_mode = TopMode::Normal;
} else {
self.command.pop();
}
}
TermKey::Escape => {
self.top_mode = TopMode::Normal;
}
TermKey::Char(c) if c.is_ascii_graphic() || c == ' ' => self.command.push(c),
_ => (),
}
Ok(())
}
fn handle_mode_key(&mut self, mode: Mode, key: TermKey) -> Result<(), Error> {
let buffer = &mut self.buffer;
self.key_seq.push(key);
match self.config.key_seq(mode, &self.key_seq) {
Some(PrefixNode::Leaf(actions)) => {
self.key_seq.clear();
for &action in actions {
command::perform(buffer, &self.config, action)?;
}
}
Some(PrefixNode::Prefix(_)) => {}
None => {
self.key_seq.clear();
}
}
if self.buffer().mode() != Mode::Normal {
self.status = None;
}
Ok(())
}
fn handle_normal_key(&mut self, key: TermKey) -> Result<(), Error> {
match key {
TermKey::Escape => {
self.key_seq.clear();
self.buffer.set_mode(&self.config, SetMode::Normal);
Ok(())
}
TermKey::Char(':') => {
self.key_seq.clear();
self.command.clear();
self.status = None;
self.top_mode = TopMode::Command;
Ok(())
}
_ => self.handle_mode_key(Mode::Normal, key),
}
}
fn handle_insert_key(&mut self, key: TermKey) -> Result<(), Error> {
match key {
TermKey::Escape => {
self.buffer.set_mode(&self.config, SetMode::Normal);
Ok(())
}
TermKey::Char(key)
if !key.is_ascii() || key == ' ' || key == '\t' || key.is_ascii_graphic() =>
{
self.buffer.insert(&self.config, key);
Ok(())
}
_ => self.handle_mode_key(Mode::Insert, key),
}
}
pub fn update(&mut self) -> Result<(), Error> {
if self.config.number {
let nw = self.buffer.number_width();
if nw != self.number_width {
self.number_width = nw;
let nw = nw + 2;
let (w, h) = self.term.size()?;
self.buffer.resize(&self.config, nw, w - nw - 1, h - 2);
}
}
self.display()?;
let key = self.term.read_key()?;
if self.message.is_some() {
self.message = None;
if key != TermKey::Char(':') {
return Ok(());
}
}
let result = match (self.top_mode, self.buffer.mode()) {
(TopMode::Normal, Mode::Normal) => self.handle_normal_key(key),
(TopMode::Normal, Mode::Insert) => self.handle_insert_key(key),
(TopMode::Command, _) => self.handle_command_key(key),
};
match result {
Ok(()) => Ok(()),
Err(e) => {
self.message = Some(format!("Error: {}", e));
Ok(())
}
}
}
pub fn cleanup(&mut self) {
self.term.clear(Clear::All).ok();
}
}
fn main() {
let args = env::args().collect::<Vec<_>>();
if args.len() > 2 {
eprintln!("Usage: red [FILE]");
return;
}
if !Term::is_tty() {
eprintln!("Not a tty");
return;
}
let path = args.get(1);
let mut state = State::open(path).unwrap();
let error = loop {
if !state.running {
break None;
}
if let Err(error) = state.update() {
break Some(error);
}
};
state.cleanup();
if let Some(error) = error {
eprintln!("Error: {:?}", error);
}
}

View File

View File

View File

View File

1
userspace/rt Symbolic link
View File

@ -0,0 +1 @@
../../sandbox/yggdrasil-rust/yggdrasil-rt

View File

@ -0,0 +1,19 @@
[package]
name = "shell"
version = "0.1.0"
edition = "2021"
authors = ["Mark Poliakov <mark@alnyan.me>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.3.19", features = ["std", "derive"], default-features = false }
thiserror = "1.0.50"
nom = "7.1.3"
[target.'cfg(target_os = "yggdrasil")'.dependencies]
yggdrasil-rt = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-rt.git" }
yggdrasil-abi = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-abi.git" }
[target.'cfg(unix)'.dependencies]
libc = "*"

View File

@ -0,0 +1,93 @@
use std::{
collections::HashMap,
env,
path::{Path, PathBuf},
process::ExitCode,
};
use crate::{Error, Outcome};
pub type BuiltinCommand = fn(&[String], &mut HashMap<String, String>) -> Result<Outcome, Error>;
static BUILTINS: &[(&str, BuiltinCommand)] = &[
("echo", b_echo),
("set", b_set),
("which", b_which),
("exit", b_exit),
];
pub fn get_builtin(name: &str) -> Option<BuiltinCommand> {
BUILTINS
.iter()
.find_map(|&(key, value)| if key == name { Some(value) } else { None })
}
fn b_which(args: &[String], _envs: &mut HashMap<String, String>) -> Result<Outcome, Error> {
fn find_in_path(path: &str, program: &str) -> Option<String> {
for entry in path.split(':') {
let full_path = PathBuf::from(entry).join(program);
if full_path.exists() {
return Some(full_path.to_str().unwrap().to_owned());
}
}
None
}
if args.len() != 1 {
eprintln!("which usage: which PROGRAM");
return Ok(Outcome::Exited(1));
}
let program = args[0].as_str();
let resolution = if program.starts_with('/') || program.starts_with('.') {
if Path::new(program).exists() {
Some(program.to_owned())
} else {
None
}
} else if let Ok(path) = env::var("PATH") {
find_in_path(&path, program)
} else {
None
};
match resolution {
Some(path) => {
println!("{}: {}", program, path);
Ok(Outcome::Exited(0))
}
_ => Ok(Outcome::Exited(1)),
}
}
fn b_set(args: &[String], envs: &mut HashMap<String, String>) -> Result<Outcome, Error> {
if args.len() != 2 {
eprintln!("set usage: set VAR VALUE");
return Ok(Outcome::Exited(1));
}
envs.insert(args[0].clone(), args[1].clone());
Ok(Outcome::ok())
}
fn b_echo(args: &[String], _envs: &mut HashMap<String, String>) -> Result<Outcome, Error> {
for (i, arg) in args.iter().enumerate() {
if i != 0 {
print!(" ");
}
print!("{}", arg);
}
println!();
Ok(Outcome::ok())
}
fn b_exit(args: &[String], _envs: &mut HashMap<String, String>) -> Result<Outcome, Error> {
match args.len() {
0 => Ok(Outcome::ExitShell(ExitCode::SUCCESS)),
_ => {
eprintln!("Usage: exit [CODE]");
Ok(Outcome::Exited(1))
}
}
}

224
userspace/shell/src/main.rs Normal file
View File

@ -0,0 +1,224 @@
#![cfg_attr(not(unix), feature(yggdrasil_os))]
use std::{
collections::HashMap,
env,
fs::File,
io::{self, stdin, stdout, BufRead, BufReader, Stdin, Write},
os::fd::{FromRawFd, IntoRawFd, OwnedFd},
path::Path,
process::{ExitCode, Stdio},
};
use clap::Parser;
mod builtins;
mod parser;
mod sys;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
IoError(#[from] io::Error),
}
#[derive(Parser)]
pub struct Args {
#[arg(short)]
login: bool,
script: Option<String>,
args: Vec<String>,
}
pub enum Outcome {
Exited(i32),
Killed(i32),
ExitShell(ExitCode),
}
pub enum Input {
Interactive(Stdin),
File(BufReader<File>),
}
impl Outcome {
pub const fn ok() -> Self {
Self::Exited(0)
}
}
impl Input {
pub fn getline(&mut self, buf: &mut String) -> io::Result<usize> {
match self {
Self::Interactive(input) => {
let mut stdout = stdout();
print!("$ ");
stdout.flush().ok();
input.read_line(buf)
}
Self::File(input) => input.read_line(buf),
}
}
pub fn is_interactive(&self) -> bool {
matches!(self, Self::Interactive(_))
}
}
// TODO group pipeline commands into a single process group
pub fn exec(
interactive: bool,
pipeline: &[parser::Command],
env: &mut HashMap<String, String>,
) -> Result<Outcome, Error> {
// Pipeline "a | b | c" execution:
//
// 1. a.stdin = STDIN, a.stdout = pipe0
// 2. b.stdin = pipe0, b.stdout = pipe1
// 3. c.stdin = pipe1, c.stdout = STDOUT
//
// Pipe count: command count - 1
if pipeline.is_empty() {
return Ok(Outcome::ok());
}
if pipeline.len() == 1 {
let command = &pipeline[0];
let (cmd, args) = command.words.split_first().unwrap();
if let Some(builtin) = builtins::get_builtin(cmd) {
return builtin(args, env);
}
}
let mut inputs = vec![];
let mut outputs = vec![];
let mut children = vec![];
let mut pipe_fds = vec![];
inputs.push(Stdio::inherit());
for _ in 1..pipeline.len() {
let pipe = sys::create_pipe()?;
let read_fd = pipe.read.into_raw_fd();
let write_fd = pipe.write.into_raw_fd();
pipe_fds.push(unsafe { OwnedFd::from_raw_fd(read_fd) });
pipe_fds.push(unsafe { OwnedFd::from_raw_fd(write_fd) });
let input = unsafe { Stdio::from_raw_fd(read_fd) };
let output = unsafe { Stdio::from_raw_fd(write_fd) };
inputs.push(input);
outputs.push(output);
}
outputs.push(Stdio::inherit());
assert_eq!(inputs.len(), outputs.len());
assert_eq!(inputs.len(), pipeline.len());
let ios = inputs.drain(..).zip(outputs.drain(..));
for (command, (input, output)) in pipeline.iter().zip(ios) {
let (cmd, args) = command.words.split_first().unwrap();
let child = sys::exec_binary(interactive, cmd, args, env, input, output)?;
children.push(child);
}
drop(pipe_fds);
for mut child in children.drain(..) {
let status = child.wait()?;
if !status.success() {
return Ok(Outcome::from(status));
}
}
Ok(Outcome::ok())
}
fn run(mut input: Input, vars: &mut HashMap<String, String>) -> io::Result<ExitCode> {
let mut line = String::new();
let code = loop {
line.clear();
let len = input.getline(&mut line)?;
if len == 0 {
break ExitCode::SUCCESS;
}
let line = line.trim();
let line = match line.split_once('#') {
Some((line, _)) => line.trim(),
None => line,
};
let cmd = parser::parse_line(vars, line).unwrap();
let q_code = match exec(input.is_interactive(), &cmd, vars) {
Ok(Outcome::ExitShell(code)) => {
break code;
}
Ok(Outcome::Killed(signal)) => {
if input.is_interactive() {
eprintln!("Killed: {}", signal);
}
signal as i32 + 128
}
Ok(Outcome::Exited(code)) => code % 256,
Err(e) => {
eprintln!("{}: {}", "<command>", e);
127
}
};
vars.insert("?".to_owned(), q_code.to_string());
};
Ok(code)
}
fn run_file<P: AsRef<Path>>(path: P, env: &mut HashMap<String, String>) -> io::Result<ExitCode> {
let input = BufReader::new(File::open(path)?);
run(Input::File(input), env)
}
fn run_stdin(env: &mut HashMap<String, String>) -> io::Result<ExitCode> {
run(Input::Interactive(stdin()), env)
}
fn main() -> ExitCode {
let args = Args::parse();
let mut vars = HashMap::new();
for (key, value) in env::vars() {
vars.insert(key, value);
}
if args.login {
run_file("/etc/profile", &mut vars).ok();
}
// Insert PATH to current process env
if let Some(path) = vars.get("PATH") {
env::set_var("PATH", path);
}
let result = if let Some(script) = &args.script {
run_file(script, &mut vars)
} else {
run_stdin(&mut vars)
};
match result {
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{:?}", e);
ExitCode::FAILURE
}
}
}

View File

@ -0,0 +1,157 @@
use std::collections::HashMap;
#[derive(Debug, PartialEq)]
pub struct Command {
pub words: Vec<String>,
}
#[derive(Debug, PartialEq)]
pub enum Token {
Word(String),
Pipe,
}
fn lex_skip_whitespace(mut input: &[u8]) -> &[u8] {
while input.first().map(u8::is_ascii_whitespace).unwrap_or(false) {
input = &input[1..];
}
input
}
pub fn lex_word(mut input: &[u8]) -> Result<(Token, &[u8]), &[u8]> {
let mut buffer = String::new();
while !input.is_empty() {
if input[0].is_ascii_whitespace() || input[0] == b'"' {
break;
}
buffer.push(input[0] as char);
input = &input[1..];
}
Ok((Token::Word(buffer), input))
}
pub fn lex_token(mut input: &[u8]) -> Result<(Option<Token>, &[u8]), &[u8]> {
input = lex_skip_whitespace(input);
let Some(&ch) = input.first() else {
return Ok((None, &[]));
};
match ch {
b'|' => Ok((Some(Token::Pipe), &input[1..])),
b'"' => todo!(),
_ => lex_word(input).map(|(x, y)| (Some(x), y)),
}
}
pub fn lex_line(mut input: &[u8]) -> Result<Vec<Token>, &[u8]> {
let mut res = Vec::new();
while let (Some(token), output) = lex_token(input)? {
res.push(token);
input = output;
}
Ok(res)
}
pub fn collect_pipeline(tokens: &[Token]) -> Vec<Command> {
let mut pipeline = Vec::new();
let mut current = None;
for token in tokens {
match token {
Token::Word(word) => {
let current = current.get_or_insert_with(|| Command { words: vec![] });
current.words.push(word.clone());
}
Token::Pipe => {
if let Some(current) = current.take() {
pipeline.push(current);
}
}
}
}
if let Some(current) = current {
pipeline.push(current);
}
pipeline
}
pub fn parse_line(_env: &HashMap<String, String>, input: &str) -> Result<Vec<Command>, ()> {
let tokens = lex_line(input.as_bytes()).map_err(|_| ())?;
let pipeline = collect_pipeline(&tokens);
Ok(pipeline)
}
#[cfg(test)]
mod tests {
use crate::parser::{lex_line, lex_skip_whitespace, lex_token, Command, Token};
use super::collect_pipeline;
#[test]
fn collect() {
let tokens = lex_line(b"abc def | ghi jkl | mno pqr").unwrap();
let pipeline = collect_pipeline(&tokens);
assert_eq!(
&pipeline,
&[
Command {
words: vec!["abc".to_owned(), "def".to_owned()]
},
Command {
words: vec!["ghi".to_owned(), "jkl".to_owned()]
},
Command {
words: vec!["mno".to_owned(), "pqr".to_owned()]
},
]
);
}
#[test]
fn skip_whitespace() {
let w = b" \t\na";
assert_eq!(lex_skip_whitespace(w), b"a");
let w = b"";
assert_eq!(lex_skip_whitespace(w), b"");
let w = b"a";
assert_eq!(lex_skip_whitespace(w), b"a");
}
#[test]
fn line_tokens() {
let w = b"abc def";
assert_eq!(
lex_line(w).unwrap(),
vec![Token::Word("abc".to_owned()), Token::Word("def".to_owned())]
);
let w = b"abc def | ghi jkl";
assert_eq!(
lex_line(w).unwrap(),
vec![
Token::Word("abc".to_owned()),
Token::Word("def".to_owned()),
Token::Pipe,
Token::Word("ghi".to_owned()),
Token::Word("jkl".to_owned()),
]
);
}
#[test]
fn token() {
let w = b"abc def";
let (t0, w) = lex_token(w).unwrap();
assert_eq!(t0, Some(Token::Word("abc".to_owned())));
let (t1, w) = lex_token(w).unwrap();
assert_eq!(t1, Some(Token::Word("def".to_owned())));
}
}

View File

@ -0,0 +1,11 @@
#[cfg(unix)]
pub mod unix;
#[cfg(unix)]
pub use unix as imp;
#[cfg(target_os = "yggdrasil")]
pub mod yggdrasil;
#[cfg(target_os = "yggdrasil")]
pub use yggdrasil as imp;
pub use imp::*;

View File

@ -0,0 +1,65 @@
use std::{
collections::HashMap,
io,
os::{
fd::{FromRawFd, OwnedFd, RawFd},
unix::process::{CommandExt, ExitStatusExt},
},
process::{Child, Command, ExitStatus, Stdio},
};
use crate::Outcome;
pub struct Pipe {
pub read: OwnedFd,
pub write: OwnedFd,
}
pub fn exec_binary(
interactive: bool,
binary: &str,
args: &[String],
env: &HashMap<String, String>,
input: Stdio,
output: Stdio,
) -> Result<Child, io::Error> {
let mut command = Command::new(binary);
let mut command = command
.args(args)
.envs(env.iter())
.stdin(input)
.stdout(output);
if interactive {
unsafe {
command = command.process_group(0);
}
}
command.spawn()
}
pub fn create_pipe() -> Result<Pipe, io::Error> {
let mut fds = [0; 2];
let (read, write) = unsafe {
if libc::pipe(fds.as_mut_ptr()) != 0 {
return Err(io::Error::last_os_error());
}
(OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1]))
};
Ok(Pipe { read, write })
}
impl From<ExitStatus> for Outcome {
fn from(value: ExitStatus) -> Self {
if let Some(code) = value.code() {
Self::Exited(code)
} else if let Some(sig) = value.signal() {
Self::Killed(sig)
} else {
todo!()
}
}
}

View File

@ -0,0 +1,58 @@
use std::os::yggdrasil::io::pipe;
use std::{
collections::HashMap,
io,
os::fd::OwnedFd,
process::{Child, Command, ExitStatus, Stdio},
};
use crate::Outcome;
pub struct Pipe {
pub read: OwnedFd,
pub write: OwnedFd,
}
pub fn exec_binary(
interactive: bool,
binary: &str,
args: &[String],
env: &HashMap<String, String>,
input: Stdio,
output: Stdio,
) -> Result<Child, io::Error> {
use std::os::yggdrasil::process::CommandExt;
let mut command = Command::new(binary);
let mut command = command
.args(args)
.envs(env.iter())
.stdin(input)
.stdout(output);
if interactive {
unsafe {
command = command.process_group_raw(0);
}
}
command.spawn()
}
pub fn create_pipe() -> Result<Pipe, io::Error> {
let (read, write) = pipe::create_pipe_pair()?;
Ok(Pipe { read, write })
}
impl From<ExitStatus> for Outcome {
fn from(value: ExitStatus) -> Self {
use std::os::yggdrasil::process::ExitStatusExt;
if let Some(code) = value.code() {
Self::Exited(code)
} else if let Some(sig) = value.signal() {
Self::Killed(sig as _)
} else {
todo!()
}
}
}

View File

@ -0,0 +1,91 @@
[package]
name = "sysutils"
version = "0.1.0"
edition = "2021"
authors = ["Mark Poliakov <mark@alnyan.me>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
libterm = { path = "../lib/libterm" }
yggdrasil-abi = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-abi.git", features = ["serde", "alloc", "bytemuck"] }
thiserror = "1.0.50"
clap = { version = "4.3.19", features = ["std", "derive", "help", "usage"], default-features = false }
# TODO own impl
humansize = { version = "2.1.3", features = ["impl_style"] }
rand = { git = "https://git.alnyan.me/yggdrasil/rand.git", branch = "alnyan/yggdrasil" }
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.111"
sha2 = { version = "0.10.8", features = ["force-soft"] }
init = { path = "../init" }
[lib]
path = "src/lib.rs"
# /sbin
[[bin]]
name = "mount"
path = "src/mount.rs"
[[bin]]
name = "login"
path = "src/login.rs"
[[bin]]
name = "service"
path = "src/service.rs"
[[bin]]
name = "logd"
path = "src/logd.rs"
# /bin
[[bin]]
name = "ls"
path = "src/ls.rs"
[[bin]]
name = "mkdir"
path = "src/mkdir.rs"
[[bin]]
name = "touch"
path = "src/touch.rs"
[[bin]]
name = "rm"
path = "src/rm.rs"
[[bin]]
name = "cat"
path = "src/cat.rs"
[[bin]]
name = "hexd"
path = "src/hexd.rs"
[[bin]]
name = "random"
path = "src/random.rs"
[[bin]]
name = "dd"
path = "src/dd.rs"
[[bin]]
name = "view"
path = "src/view.rs"
[[bin]]
name = "chmod"
path = "src/chmod.rs"
[[bin]]
name = "sha256sum"
path = "src/sha256sum.rs"
[[bin]]
name = "sysmon"
path = "src/sysmon.rs"

View File

@ -0,0 +1,36 @@
use std::{
env,
fs::File,
io::{self, stdout, BufReader, Read, Stdout, Write},
path::Path,
process::ExitCode,
};
fn cat_file<P: AsRef<Path>>(stdout: &mut Stdout, path: P) -> io::Result<()> {
let mut buf = [0; 4096];
let mut reader = BufReader::new(File::open(path)?);
loop {
let count = reader.read(&mut buf)?;
if count == 0 {
return Ok(());
}
stdout.write_all(&buf[..count])?;
}
}
fn main() -> ExitCode {
let mut stdout = stdout();
let mut exit = ExitCode::SUCCESS;
for arg in env::args().skip(1) {
if let Err(error) = cat_file(&mut stdout, &arg) {
eprintln!("{}: {}", arg, error);
exit = ExitCode::FAILURE;
}
}
exit
}

View File

@ -0,0 +1,22 @@
#![feature(rustc_private, yggdrasil_os)]
use std::{
env,
os::yggdrasil::io::{self, FileMetadataUpdate, FileMetadataUpdateMode, RawFileMode},
};
fn main() {
let args: Vec<_> = env::args().collect();
if args.len() != 3 {
panic!();
}
let mode_str = &args[1];
let filename = &args[2];
let mode_oct = u32::from_str_radix(mode_str, 8).unwrap();
let update =
FileMetadataUpdate::Permissions(RawFileMode::new(mode_oct), FileMetadataUpdateMode::Set);
io::update_metadata(filename, &update).unwrap();
}

View File

@ -0,0 +1,138 @@
#![feature(yggdrasil_os)]
use std::{
io::{self, Read, Seek, SeekFrom, Write},
process::ExitCode,
};
use clap::Parser;
use sysutils::{Input, Output};
#[derive(Parser)]
struct Args {
#[arg(short)]
source: Option<String>,
#[arg(short)]
destination: Option<String>,
#[arg(long)]
src_skip: Option<u64>,
#[arg(long, default_value_t = 512)]
src_bs: u64,
#[arg(short, long, default_value_t = usize::MAX)]
count: usize,
// TODO: remove this when pipes are a thing
#[arg(short = 'x', long)]
as_hex: bool,
}
fn dump_block(offset: u64, data: &[u8]) {
const WINDOW_SIZE: usize = 16;
let window_count = (data.len() + WINDOW_SIZE) / WINDOW_SIZE;
for iw in 0..window_count {
let off = iw * WINDOW_SIZE;
let len = core::cmp::min(data.len() - off, WINDOW_SIZE);
if len == 0 {
break;
}
let window = &data[off..off + len];
print!("{:08X}: ", offset + off as u64);
for i in 0..WINDOW_SIZE {
if i < window.len() {
print!("{:02X}", window[i]);
} else {
print!(" ");
}
if i % 2 == 1 {
print!(" ");
}
}
for &ch in window {
if ch.is_ascii_graphic() || ch == b' ' {
print!("{}", ch as char);
} else {
print!(".");
}
}
println!();
}
}
fn run<I: Read + Seek, O: Write>(
mut input: I,
mut output: O,
src_position: u64,
src_block_size: u64,
mut count: usize,
as_hex: bool,
) -> io::Result<()> {
let mut block = vec![0; src_block_size as usize];
let mut offset = 0;
input.seek(SeekFrom::Start(src_position * src_block_size))?;
while count != 0 {
let read_count = input.read(&mut block)?;
if read_count == 0 {
break;
}
if as_hex {
dump_block(
(src_position + offset) * src_block_size,
&block[..read_count],
);
} else {
output.write_all(&block[..read_count])?;
}
count -= 1;
offset += 1;
}
Ok(())
}
fn main() -> ExitCode {
let args = Args::parse();
let src_path = args.source.as_deref().unwrap_or("-");
let dst_path = args.destination.as_deref().unwrap_or("-");
if src_path == dst_path && src_path != "-" {
eprintln!("Input and output cannot be the same: {}", src_path);
return ExitCode::FAILURE;
}
if args.as_hex && dst_path != "-" {
eprintln!("--as-hex requires stdout output");
return ExitCode::FAILURE;
}
let input = Input::open_str(src_path).unwrap();
let output = Output::open_str(dst_path).unwrap();
let src_position = args.src_skip.unwrap_or(0);
match run(
input,
output,
src_position,
args.src_bs,
args.count,
args.as_hex,
) {
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("Error: {}", e);
ExitCode::FAILURE
}
}
}

View File

@ -0,0 +1,77 @@
use std::{
env,
io::{self, Read},
process::ExitCode,
};
use sysutils::{Input, ToExitCode};
const WINDOW_SIZE: usize = 16;
fn do_line(offset: usize, line: &[u8]) {
print!("{:08x} |", offset);
for i in 0..WINDOW_SIZE {
if i % 2 == 0 {
print!(" ");
}
if i < line.len() {
print!("{:02X}", line[i]);
} else {
print!(" ");
}
}
print!(" | ");
for &ch in line {
let ch = if ch.is_ascii_control() { b'.' } else { ch };
print!("{}", ch as char);
}
println!();
}
fn do_file(path: &str) -> io::Result<()> {
let mut input = Input::open_str(path)?;
let mut buf = [0; WINDOW_SIZE];
let mut offset = 0;
loop {
let len = input.read(&mut buf)?;
if len == 0 {
break;
}
do_line(offset, &buf[..len]);
offset += len;
}
Ok(())
}
pub fn main() -> ExitCode {
let args = env::args().collect::<Vec<_>>();
let result = match args.len() {
0..=1 => do_file("-").to_exit_code(),
2 => do_file(&args[1]).to_exit_code(),
_ => {
let mut result = 0;
for arg in args[1..].iter() {
println!("{}:", arg);
if do_file(arg).to_exit_code() != 0 {
result = 1;
}
}
result
}
};
if result == 0 {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}

View File

@ -0,0 +1,194 @@
#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os, rustc_private))]
use std::{
fs::File,
io::{self, BufRead, Read, Seek, SeekFrom, Write},
};
#[cfg(unix)]
pub mod unix {
use std::fmt;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct FileMode(u32);
impl From<u32> for FileMode {
fn from(value: u32) -> Self {
Self(value)
}
}
impl fmt::Display for FileMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
macro_rules! print_bit {
($res:expr, $val:expr, $self:expr, $bit:expr) => {
if $self.0 & $bit == $bit {
$res = $val;
}
};
}
let mut buf = ['-'; 9];
print_bit!(buf[0], 'r', self, 0o400);
print_bit!(buf[1], 'w', self, 0o200);
print_bit!(buf[2], 'x', self, 0o100);
print_bit!(buf[3], 'r', self, 0o040);
print_bit!(buf[4], 'w', self, 0o020);
print_bit!(buf[5], 'x', self, 0o010);
print_bit!(buf[6], 'r', self, 0o004);
print_bit!(buf[7], 'w', self, 0o002);
print_bit!(buf[8], 'x', self, 0o001);
for ch in buf.iter() {
fmt::Display::fmt(ch, f)?;
}
Ok(())
}
}
}
pub struct Syslog {
#[cfg(target_os = "yggdrasil")]
channel: std::os::yggdrasil::io::message_channel::MessageChannel,
}
// TODO replace this
pub trait ToExitCode {
fn to_exit_code(&self) -> i32;
}
#[cfg(target_os = "yggdrasil")]
impl Syslog {
pub fn open() -> io::Result<Self> {
use std::os::yggdrasil::io::message_channel::MessageChannel;
let channel = MessageChannel::open("log", false)?;
Ok(Self { channel })
}
pub fn send_str(&mut self, msg: &str) -> io::Result<()> {
use std::os::yggdrasil::io::message_channel::{MessageDestination, MessageSender};
self.channel
.send_message(msg.as_bytes(), MessageDestination::Specific(0))
}
}
impl<T> ToExitCode for io::Result<T> {
fn to_exit_code(&self) -> i32 {
match self {
Ok(_) => 0,
_ => 1,
}
}
}
pub enum Input {
Stdin(io::Stdin),
File(io::BufReader<File>),
}
pub enum Output {
Stdout(io::Stdout),
File(io::BufWriter<File>),
}
impl Input {
pub fn open_str(arg: &str) -> io::Result<Self> {
if arg == "-" {
Ok(Self::Stdin(io::stdin()))
} else {
let file = File::open(arg)?;
let reader = io::BufReader::new(file);
Ok(Self::File(reader))
}
}
pub fn read_line(&mut self, line: &mut String) -> io::Result<usize> {
match self {
Self::Stdin(f) => f.read_line(line),
Self::File(f) => f.read_line(line),
}
}
}
impl From<io::Stdin> for Input {
fn from(value: io::Stdin) -> Self {
Self::Stdin(value)
}
}
impl From<File> for Input {
fn from(value: File) -> Self {
Self::File(io::BufReader::new(value))
}
}
impl Read for Input {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Stdin(value) => value.read(buf),
Self::File(value) => value.read(buf),
}
}
}
impl Seek for Input {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match self {
Self::Stdin(_) => todo!(),
Self::File(value) => value.seek(pos),
}
}
}
impl Output {
pub fn open_str(arg: &str) -> io::Result<Self> {
if arg == "-" {
Ok(Self::Stdout(io::stdout()))
} else {
let file = File::create(arg)?;
let writer = io::BufWriter::new(file);
Ok(Self::File(writer))
}
}
}
impl From<io::Stdout> for Output {
fn from(value: io::Stdout) -> Self {
Self::Stdout(value)
}
}
impl From<File> for Output {
fn from(value: File) -> Self {
Self::File(io::BufWriter::new(value))
}
}
impl Write for Output {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::Stdout(value) => value.write(buf),
Self::File(value) => value.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Stdout(value) => value.flush(),
Self::File(value) => value.flush(),
}
}
}
impl Seek for Output {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match self {
Self::Stdout(_) => todo!(),
Self::File(value) => value.seek(pos),
}
}
}

View File

@ -0,0 +1,18 @@
#![feature(yggdrasil_os)]
use std::os::yggdrasil::io::message_channel::{MessageChannel, MessageReceiver};
fn main() {
let channel = MessageChannel::open("log", true).unwrap();
let mut buf = [0; 1024];
loop {
let Ok((_, len)) = channel.receive_message(&mut buf) else {
continue;
};
if let Ok(msg) = std::str::from_utf8(&buf[..len]) {
debug_trace!("log::message: {:?}", msg);
}
}
}

View File

@ -0,0 +1,84 @@
#![feature(yggdrasil_os, rustc_private)]
use std::{
env,
io::{self, stdin, stdout, BufRead, Write},
os::{
fd::AsRawFd,
yggdrasil::io::{
device::{DeviceRequest, FdDeviceRequest},
terminal::start_terminal_session,
},
},
process::{Command, ExitCode},
};
fn login_readline<R: BufRead + AsRawFd>(
reader: &mut R,
buf: &mut String,
_secret: bool,
) -> Result<usize, io::Error> {
reader.read_line(buf)
}
fn login_as(username: &str, _password: &str) -> Result<(), io::Error> {
let mut shell = Command::new("/bin/sh").arg("-l").spawn()?;
println!("Hello {:?}", username);
shell.wait()?;
Ok(())
}
fn login_attempt(erase: bool) -> Result<(), io::Error> {
let mut stdin = stdin().lock();
let mut stdout = stdout();
if erase {
print!("\x1b[1;1f\x1b[2J");
stdout.flush().ok();
}
let mut username = String::new();
print!("Username: ");
stdout.flush().ok();
if login_readline(&mut stdin, &mut username, false)? == 0 {
return Ok(());
}
login_as(username.trim(), "")
}
fn main() -> ExitCode {
let args: Vec<_> = env::args().skip(1).collect();
if args.len() != 1 {
eprintln!("Usage: /sbin/login TTY");
return ExitCode::FAILURE;
}
let terminal = args[0].as_str();
// TODO check that `terminal` is a terminal
debug_trace!("Starting a session at {}", terminal);
if let Err(err) = unsafe { start_terminal_session(terminal) } {
debug_trace!("Error: {:?}", err);
eprintln!("Could not setup a session at {}: {:?}", terminal, err);
return ExitCode::FAILURE;
}
let mut attempt_number = 0;
loop {
debug_trace!("Login attempt {}", attempt_number);
// "Attach" the terminal
unsafe {
let pid = std::os::yggdrasil::process::id_ext();
stdin()
.device_request(&mut DeviceRequest::SetTerminalGroup(pid))
.expect("Could not attach the terminal");
}
if let Err(err) = login_attempt(attempt_number % 3 == 0) {
eprintln!("login: {}", err.to_string());
}
attempt_number += 1;
}
}

View File

@ -0,0 +1,215 @@
#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os))]
use std::{
fmt,
fs::{read_dir, FileType, Metadata},
io,
path::Path,
};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(target_os = "yggdrasil")]
use std::os::yggdrasil::fs::MetadataExt;
use clap::Parser;
use humansize::{FormatSize, BINARY};
#[derive(Parser)]
#[clap(disable_help_flag = true)]
pub struct Args {
#[arg(short)]
long: bool,
#[arg(short, long)]
human_readable: bool,
paths: Vec<String>,
}
trait DisplayBit {
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result;
fn display_with<'a, 'b>(&'a self, opts: &'b Args) -> DisplayWith<'a, 'b, Self>
where
Self: Sized,
{
DisplayWith { item: self, opts }
}
}
trait DisplaySizeBit: Sized {
fn display_size_bit(self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result;
fn display_size_with(self, opts: &Args) -> DisplaySizeWith<'_, Self> {
DisplaySizeWith { size: self, opts }
}
}
impl DisplaySizeBit for u64 {
fn display_size_bit(self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if opts.human_readable {
fmt::Display::fmt(&self.format_size(BINARY), f)
} else {
fmt::Display::fmt(&self, f)
}
}
}
struct DisplaySizeWith<'a, T> {
size: T,
opts: &'a Args,
}
struct DisplayWith<'a, 'b, T: DisplayBit> {
item: &'a T,
opts: &'b Args,
}
impl<T: DisplayBit> fmt::Display for DisplayWith<'_, '_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.item.display_bit(self.opts, f)
}
}
impl<T: DisplaySizeBit + Copy> fmt::Display for DisplaySizeWith<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.size.display_size_bit(self.opts, f)
}
}
struct Entry {
name: String,
ty: Option<FileType>,
attrs: Option<Metadata>,
}
impl DisplayBit for Option<FileType> {
fn display_bit(&self, _opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(ty) = self {
if ty.is_dir() {
f.write_str("d")
} else if ty.is_symlink() {
f.write_str("l")
} else {
f.write_str("-")
}
} else {
f.write_str("?")
}
}
}
#[cfg(target_os = "yggdrasil")]
impl DisplayBit for Option<Metadata> {
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Some(attrs) = self else {
return write!(f, "--------- {:<8}", "???");
};
let mode = attrs.mode_ext();
write!(f, "{} {:>8}", mode, attrs.len().display_size_with(opts))
}
}
#[cfg(unix)]
impl DisplayBit for Option<Metadata> {
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use sysutils::unix::FileMode;
let Some(attrs) = self else {
return write!(f, "--------- {:<8}", "???");
};
let mode = FileMode::from(attrs.mode());
write!(f, "{} {:>8}", mode, attrs.len().display_size_with(opts))
}
}
impl DisplayBit for Entry {
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if opts.long {
write!(
f,
"{}{} {}",
self.ty.display_with(opts),
self.attrs.display_with(opts),
self.name
)
} else {
f.write_str(&self.name)
}
}
}
impl Entry {
fn invalid() -> Self {
Self {
name: "???".to_owned(),
ty: None,
attrs: None,
}
}
}
fn list_directory(path: &Path) -> io::Result<Vec<Entry>> {
let mut entries = vec![];
for entry in read_dir(path)? {
let Ok(entry) = entry else {
entries.push(Entry::invalid());
continue;
};
let os_filename = entry.file_name();
let ty = entry.file_type().ok();
let attrs = entry.path().metadata().ok();
entries.push(Entry {
name: os_filename.to_string_lossy().to_string(),
ty,
attrs,
});
}
Ok(entries)
}
fn list(opts: &Args, path: &Path) -> io::Result<()> {
if path.is_dir() {
let entries = list_directory(path)?;
for entry in entries {
println!("{}", entry.display_with(opts));
}
} else {
// TODO fetch info
println!("{}", path.display());
}
Ok(())
}
fn list_wrap<P: AsRef<Path>>(opts: &Args, path: P) {
let path = path.as_ref();
match list(opts, path) {
Ok(_) => (),
Err(e) => {
eprintln!("{}: {}", path.display(), e);
}
}
}
pub fn main() {
let args = Args::parse();
if args.paths.is_empty() {
list_wrap(&args, ".");
} else {
for path in args.paths.iter() {
if args.paths.len() > 1 {
println!("{}:", path);
}
list_wrap(&args, path);
}
}
}

View File

@ -0,0 +1,37 @@
use std::{
io,
path::{Path, PathBuf},
process::ExitCode,
};
use clap::Parser;
#[derive(Parser)]
struct Args {
#[arg(short)]
parents: bool,
#[clap(required = true)]
dirs: Vec<PathBuf>,
}
fn mkdir<P: AsRef<Path>>(path: P, parents: bool) -> Result<(), io::Error> {
if parents {
std::fs::create_dir_all(path)
} else {
std::fs::create_dir(path)
}
}
fn main() -> ExitCode {
let args = Args::parse();
let mut result = ExitCode::SUCCESS;
for arg in args.dirs {
if let Err(e) = mkdir(&arg, args.parents) {
eprintln!("{}: {}", arg.display(), e);
result = ExitCode::FAILURE;
}
}
result
}

View File

@ -0,0 +1,46 @@
#![feature(rustc_private, yggdrasil_os)]
use std::{
os::yggdrasil::io::device::{mount_raw, MountOptions},
process::ExitCode,
};
use clap::Parser;
#[derive(Parser, Debug)]
struct Args {
#[arg(short)]
ty: Option<String>,
source: String,
target: Option<String>,
}
fn main() -> ExitCode {
let args = Args::parse();
let source = if args.target.is_some() {
Some(args.source.as_str())
} else {
None
};
let target = args.target.as_deref().unwrap_or(args.source.as_str());
let filesystem = args.ty.as_deref();
// Permissions are not yet implemented, lol
let result = unsafe {
let options = MountOptions {
source,
filesystem,
target,
};
mount_raw(&options)
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("mount: {:?}", err);
ExitCode::FAILURE
}
}
}

View File

@ -0,0 +1,27 @@
use std::env;
fn main() {
let args: Vec<_> = env::args().collect();
let (lower, upper, count) = match args.len() {
1 => (0, u32::MAX, 1),
2 => (0, args[1].parse().unwrap(), 1),
3 => (args[1].parse().unwrap(), args[2].parse().unwrap(), 1),
4 => (
args[1].parse().unwrap(),
args[2].parse().unwrap(),
args[3].parse::<usize>().unwrap(),
),
_ => panic!("Incorrect usage"),
};
assert!(lower < upper);
for _ in 0..count {
let value: u32 = rand::random();
let res =
(((value as u64) * (upper - lower) as u64) / u32::MAX as u64 + lower as u64) as u32;
println!("{}", res);
}
}

View File

@ -0,0 +1,71 @@
#![feature(io_error_more)]
use std::{io, fs, path::{Path, PathBuf}, process::ExitCode};
use clap::Parser;
#[derive(Parser)]
struct Args {
#[arg(short)]
recurse: bool,
#[clap(required = true)]
paths: Vec<PathBuf>
}
fn rm<P: AsRef<Path>>(path: P, recurse: bool) -> bool {
let path = path.as_ref();
println!("remove {}", path.display());
match fs::remove_file(path) {
Err(e) if recurse && e.kind() == io::ErrorKind::IsADirectory => {
let readdir = match fs::read_dir(path) {
Ok(dir) => dir,
Err(error) => {
eprintln!("{}: {}", path.display(), error);
return false;
}
};
let mut result = true;
for entry in readdir {
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
eprintln!("{}/: {}", path.display(), error);
result = false;
continue;
}
};
result &= rm(entry.path(), recurse);
}
if result {
if let Err(error) = fs::remove_dir(path) {
eprintln!("{}: {}", path.display(), error);
result = false;
}
}
result
},
Err(e) => {
eprintln!("{}: {}", path.display(), e);
false
}
Ok(_) => true
}
}
fn main() -> ExitCode {
let args = Args::parse();
let mut result = ExitCode::SUCCESS;
for arg in args.paths {
if !rm(arg, args.recurse) {
result = ExitCode::FAILURE;
}
}
result
}

View File

@ -0,0 +1,56 @@
#![feature(yggdrasil_os, rustc_private)]
use std::{
io,
os::yggdrasil::io::message_channel::{MessageChannel, MessageDestination, MessageSender},
process::ExitCode,
};
use clap::{Parser, Subcommand};
#[derive(Debug, Parser)]
struct Args {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
#[command(arg_required_else_help = true)]
Start { name: Vec<String> },
}
fn send_message(msg: init::InitMsg) -> io::Result<()> {
let channel = MessageChannel::open("service-control", false)?;
let msg = serde_json::to_string(&msg).unwrap();
channel.send_message(msg.as_bytes(), MessageDestination::Specific(0))
}
fn run(command: Command) -> io::Result<()> {
match command {
Command::Start { name } => {
let Some((binary, args)) = name.split_first() else {
return Ok(());
};
let msg = init::InitMsg::StartService(init::StartService {
binary: binary.to_owned(),
args: args.into(),
});
send_message(msg)
}
}
}
fn main() -> ExitCode {
let args = Args::parse();
match run(args.command) {
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{}", e);
ExitCode::FAILURE
}
}
}

View File

@ -0,0 +1,46 @@
use std::{
env,
fs::File,
io::{self, Read},
path::Path,
process::ExitCode,
};
use sha2::{Digest, Sha256};
fn do_digest<P: AsRef<Path>>(path: P) -> Result<(), io::Error> {
let mut buf = [0; 4096];
let mut file = File::open(path)?;
let mut hasher = Sha256::new();
loop {
let count = file.read(&mut buf)?;
if count == 0 {
break;
}
hasher.update(&buf[..count]);
}
let result = hasher.finalize();
for byte in result {
print!("{:02x}", byte);
}
println!();
Ok(())
}
fn main() -> ExitCode {
let args: Vec<_> = env::args().collect();
if args.len() != 2 {
eprintln!("TODO: {} FILENAME", args[0]);
return ExitCode::FAILURE;
}
match do_digest(&args[1]) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("{}: {}", args[1], error);
ExitCode::FAILURE
}
}
}

View File

@ -0,0 +1,70 @@
#![feature(yggdrasil_os, rustc_private)]
use std::{
collections::VecDeque,
fmt::Write,
os::yggdrasil::{get_system_info, SystemInfo, SystemMemoryStats},
};
use humansize::FormatSize;
use libterm::{Clear, Term};
fn get_memory_stats() -> SystemMemoryStats {
let mut info = SystemInfo::MemoryStats(SystemMemoryStats::default());
get_system_info(&mut info).unwrap();
let SystemInfo::MemoryStats(stats) = info;
stats
}
fn main() {
// Retrieve the stats once for total cap
let stats = get_memory_stats();
let total = stats.total_usable_pages;
let mut term = Term::open().unwrap();
let mut points = VecDeque::new();
loop {
let (width, height) = term.size().unwrap();
term.clear(Clear::All).ok();
let stats = get_memory_stats();
points.push_back(stats.allocated_pages);
let h = height - 1;
for (i, point) in points.iter().enumerate() {
let v = (*point * h) / total;
for j in 0..v {
term.set_cursor_position(h - j - 1, i).ok();
term.write_char('*').ok();
}
}
while points.len() >= width - 1 {
points.pop_front();
}
term.set_cursor_position(h, 1).ok();
let allocated_bytes = stats.allocated_pages * stats.page_size;
let free_bytes = stats.free_pages * stats.page_size;
let total_usable_bytes = stats.total_usable_pages * stats.page_size;
write!(
term,
"U: {} F: {} T: {} ({}%)",
allocated_bytes.format_size(Default::default()),
free_bytes.format_size(Default::default()),
total_usable_bytes.format_size(Default::default()),
100 * stats.allocated_pages / stats.total_usable_pages
)
.ok();
term.flush().ok();
std::thread::sleep(std::time::Duration::from_millis(50));
}
}

View File

@ -0,0 +1,26 @@
use std::{env, fs::OpenOptions, io, path::Path, process::ExitCode};
fn touch<P: AsRef<Path>>(path: P) -> Result<(), io::Error> {
OpenOptions::new().create(true).write(true).open(path)?;
Ok(())
}
fn main() -> ExitCode {
let mut args = env::args();
if args.len() < 2 {
eprintln!("Usage: {} FILES", args.next().unwrap());
return ExitCode::FAILURE;
}
let mut result = ExitCode::SUCCESS;
for arg in args.skip(1) {
if let Err(err) = touch(&arg) {
eprintln!("{}: {}", arg, err);
result = ExitCode::FAILURE;
}
}
result
}

View File

@ -0,0 +1,168 @@
use std::{
fmt::{self, Write},
io,
};
use clap::Parser;
use libterm::{Clear, Color, Term, TermKey};
use sysutils::Input;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Terminal error: {0}")]
TerminalError(#[from] libterm::Error),
#[error("I/O error: {0}")]
IoError(#[from] io::Error),
#[error("Terminal output error: {0}")]
FmtError(#[from] fmt::Error),
}
pub struct View {
term: Term,
source: Input,
eof: bool,
show_bar: bool,
cursor: usize,
buffer: Vec<String>,
}
impl View {
pub fn open(src: &str, show_bar: bool) -> Result<Self, Error> {
let term = Term::open()?;
let source = Input::open_str(src)?;
Ok(Self {
term,
source,
cursor: 0,
show_bar,
buffer: vec![],
eof: false,
})
}
fn fetch_more(&mut self, height: usize) -> Result<(), Error> {
if self.eof || self.cursor + height < self.buffer.len() {
return Ok(());
}
let need_lines = self.cursor + height + 1 - self.buffer.len();
for _ in 0..need_lines {
let mut line = String::new();
let len = self.source.read_line(&mut line)?;
if len == 0 {
self.eof = true;
break;
}
self.buffer.push(line.trim().to_owned());
}
Ok(())
}
fn display(&mut self, width: usize, height: usize) -> Result<(), Error> {
// TODO don't redraw every time
self.term.clear(Clear::All)?;
self.term.set_cursor_visible(false)?;
let bar_height = self.show_bar as usize;
for y in 0..height - bar_height {
let i = y + self.cursor;
let Some(line) = self.buffer.get(i) else {
break;
};
let limit = std::cmp::min(line.len(), width);
self.term.set_cursor_position(y, 0)?;
write!(self.term, "{}", &line[..limit])?;
}
if self.show_bar {
let line_end = std::cmp::min(self.cursor + height, self.buffer.len());
self.term.set_cursor_position(height - 1, 0)?;
if self.eof {
self.term.set_background(Color::Green)?;
} else {
self.term.set_background(Color::Yellow)?;
}
self.term.set_foreground(Color::Black)?;
self.term.set_bright(true)?;
self.term.clear(Clear::LineToEnd)?;
if self.eof {
let p = line_end * 100 / self.buffer.len();
write!(
self.term,
"[{}..{}/{}] ({}%)",
self.cursor,
line_end,
self.buffer.len(),
p
)?;
} else {
write!(self.term, "[{}..{}] ...", self.cursor, line_end)?;
}
}
self.term.reset_style()?;
self.term.flush()?;
Ok(())
}
pub fn run(mut self) -> Result<(), Error> {
loop {
let (width, height) = self.term.size()?;
self.fetch_more(height)?;
self.display(width, height)?;
let key = self.term.read_key()?;
match key {
TermKey::Char('q') => break,
TermKey::Char('j') => {
if !self.eof || self.cursor + height < self.buffer.len() {
self.cursor += 1;
}
}
TermKey::Char('k') => {
if self.cursor != 0 {
self.cursor -= 1;
}
}
// TODO page down, page up
_ => (),
}
}
Ok(())
}
}
impl Drop for View {
fn drop(&mut self) {
self.term.set_cursor_visible(true).ok();
}
}
#[derive(Parser)]
struct Args {
#[arg(short, long)]
no_bar: bool,
filename: String,
}
fn main() -> Result<(), Error> {
// TODO check if running in a terminal
let args = Args::parse();
let view = View::open(&args.filename, !args.no_bar).unwrap();
view.run()
}

12
userspace/term/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "term"
version = "0.1.0"
edition = "2021"
authors = ["Mark Poliakov <mark@alnyan.me>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bytemuck = { version = "1.14.0", features = ["derive"] }
libcolors = { path = "../lib/libcolors", default_features = false, features = ["client"] }
thiserror = "1.0.56"

112
userspace/term/src/attr.rs Normal file
View File

@ -0,0 +1,112 @@
#[derive(Clone, Copy, Debug)]
#[repr(usize)]
pub enum Color {
Black = 0,
Red = 1,
Green = 2,
Yellow = 3,
Blue = 4,
Magenta = 5,
Cyan = 6,
White = 7,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[repr(C)]
pub struct DisplayColor {
r: u8,
g: u8,
b: u8,
}
#[derive(Clone, Copy, Debug)]
pub struct CellAttributes {
pub fg: Color,
pub bg: Color,
pub bright: bool,
}
impl DisplayColor {
pub const BLACK: Self = Self::new(0, 0, 0);
pub const WHITE: Self = Self::new(255, 255, 255);
pub const DARK_GRAY: Self = Self::new(60, 60, 60);
pub const LIGHT_GRAY: Self = Self::new(127, 127, 127);
pub const DARK_RED: Self = Self::new(160, 0, 0);
pub const DARK_GREEN: Self = Self::new(0, 160, 0);
pub const DARK_BLUE: Self = Self::new(0, 0, 160);
pub const DARK_YELLOW: Self = Self::new(160, 160, 0);
pub const DARK_MAGENTA: Self = Self::new(160, 0, 160);
pub const DARK_CYAN: Self = Self::new(0, 160, 160);
pub const LIGHT_RED: Self = Self::new(255, 0, 0);
pub const LIGHT_GREEN: Self = Self::new(0, 255, 0);
pub const LIGHT_BLUE: Self = Self::new(0, 0, 255);
pub const LIGHT_YELLOW: Self = Self::new(255, 255, 0);
pub const LIGHT_MAGENTA: Self = Self::new(255, 0, 255);
pub const LIGHT_CYAN: Self = Self::new(0, 255, 255);
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
pub const fn to_u32(self) -> u32 {
0xFF000000 | ((self.r as u32) << 16) | ((self.g as u32) << 8) | (self.b as u32)
}
}
impl Color {
pub fn from_esc(v: u32) -> Self {
match v {
0 => Self::Black,
1 => Self::Red,
2 => Self::Green,
3 => Self::Yellow,
4 => Self::Blue,
5 => Self::Magenta,
6 => Self::Cyan,
7 => Self::White,
_ => Self::Black,
}
}
pub fn to_display(self, bright: bool) -> DisplayColor {
if bright {
BRIGHT_COLOR_MAP[self as usize]
} else {
DIM_COLOR_MAP[self as usize]
}
}
// pub fn to_rgba(self, bright: bool) -> SolidSource {
// if bright {
// BRIGHT_COLOR_MAP[self as usize]
// } else {
// COLOR_MAP[self as usize]
// }
// }
}
const DIM_COLOR_MAP: &[DisplayColor] = &[
DisplayColor::BLACK,
DisplayColor::DARK_RED,
DisplayColor::DARK_GREEN,
DisplayColor::DARK_YELLOW,
DisplayColor::DARK_BLUE,
DisplayColor::DARK_MAGENTA,
DisplayColor::DARK_CYAN,
DisplayColor::LIGHT_GRAY,
];
const BRIGHT_COLOR_MAP: &[DisplayColor] = &[
DisplayColor::DARK_GRAY,
DisplayColor::LIGHT_RED,
DisplayColor::LIGHT_GREEN,
DisplayColor::LIGHT_YELLOW,
DisplayColor::LIGHT_BLUE,
DisplayColor::LIGHT_MAGENTA,
DisplayColor::LIGHT_CYAN,
DisplayColor::WHITE,
];

View File

@ -0,0 +1,7 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Application error: {0:?}")]
ApplicationError(#[from] libcolors::error::Error),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
}

View File

@ -0,0 +1,74 @@
use std::mem::size_of;
use bytemuck::{Pod, Zeroable};
struct Align<T, D: ?Sized> {
_align: T,
data: D,
}
static FONT_DATA: &Align<u32, [u8]> = &Align {
_align: 0,
data: *include_bytes!("../../etc/fonts/regular.psfu"),
};
#[repr(C)]
#[derive(Pod, Zeroable, Clone, Copy)]
struct PsfHeader {
magic: u32,
version: u32,
header_size: u32,
flags: u32,
num_glyph: u32,
bytes_per_glyph: u32,
height: u32,
width: u32,
}
/// Represents a PSF-format font object
#[derive(Clone, Copy)]
pub struct PcScreenFont<'a> {
header: &'a PsfHeader,
glyph_data: &'a [u8],
}
impl Default for PcScreenFont<'static> {
fn default() -> Self {
Self::from_bytes(&FONT_DATA.data)
}
}
impl<'a> PcScreenFont<'a> {
/// Constructs an instance of [PcScreenFont] from its byte representation
pub fn from_bytes(bytes: &'a [u8]) -> Self {
let header: &PsfHeader = bytemuck::from_bytes(&bytes[..size_of::<PsfHeader>()]);
let glyph_data = &bytes[header.header_size as usize..];
Self { header, glyph_data }
}
/// Returns the character width of the font
#[inline]
pub const fn width(&self) -> u32 {
self.header.width
}
/// Returns the character height of the font
#[inline]
pub const fn height(&self) -> u32 {
self.header.height
}
/// Returns the count of glyphs present in the font
#[allow(clippy::len_without_is_empty)]
#[inline]
pub const fn len(&self) -> u32 {
self.header.num_glyph
}
/// Returns the data slice of a single glyph within the font
#[inline]
pub fn raw_glyph_data(&self, index: u32) -> &[u8] {
&self.glyph_data[(index * self.header.bytes_per_glyph) as usize..]
}
}

350
userspace/term/src/main.rs Normal file
View File

@ -0,0 +1,350 @@
#![feature(yggdrasil_os, rustc_private)]
use std::{
fs::File,
io::{Read, Write},
os::{
fd::{AsRawFd, FromRawFd, RawFd},
yggdrasil::{
io::{
device::{DeviceRequest, FdDeviceRequest},
poll::PollChannel,
terminal::{create_pty, TerminalOptions, TerminalSize},
},
process::CommandExt,
},
},
process::{Child, Command, ExitCode, Stdio},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
use error::Error;
use font::PcScreenFont;
use libcolors::{
application::{
window::{EventOutcome, Window},
Application,
},
event::{KeyModifiers, KeyboardKey},
};
use state::{Cursor, State};
pub mod attr;
pub mod error;
pub mod font;
pub mod state;
struct DrawState {
width: usize,
force_redraw: bool,
focus_changed: bool,
focused: bool,
old_cursor: Cursor,
font: PcScreenFont<'static>,
}
pub struct Terminal<'a> {
poll: PollChannel,
conn_fd: RawFd,
application: Application<'a>,
state: Arc<Mutex<State>>,
pty_master: Arc<Mutex<File>>,
pty_master_fd: RawFd,
#[allow(unused)]
shell: Child,
}
impl DrawState {
pub fn new(font: PcScreenFont<'static>, width: usize) -> Self {
Self {
width,
force_redraw: true,
focus_changed: false,
focused: true,
old_cursor: Cursor { row: 0, col: 0 },
font,
}
}
pub fn draw(&mut self, dt: &mut [u32], state: &mut State) {
let default_fg = state.default_attributes.fg.to_display(false).to_u32();
let default_bg = state.default_attributes.bg.to_display(false).to_u32();
let fw = self.font.width() as usize;
let fh = self.font.height() as usize;
let cursor_dirty = self.old_cursor != state.cursor;
if self.force_redraw {
dt.fill(default_bg);
}
if cursor_dirty {
state.buffer.set_row_dirty(self.old_cursor.row);
}
let bytes_per_line = (self.font.width() as usize + 7) / 8;
for (i, row) in state.buffer.dirty_rows() {
let cy = i * fh;
for (j, cell) in row.cells().enumerate() {
let bg = cell.attrs.bg.to_display(false).to_u32();
let fg = cell.attrs.fg.to_display(cell.attrs.bright).to_u32();
let cx = j * fw;
// Fill cell
for y in 0..fh {
let off = (cy + y) * self.width + cx;
dt[off..off + fw].fill(bg);
}
if cell.char == '\0' {
continue;
}
// Draw character
let mut c = cell.char as u32;
if c >= self.font.len() {
c = b'?' as u32;
}
let mut glyph = self.font.raw_glyph_data(c);
let mut y = 0;
while y < self.font.height() as usize {
let mut mask = 1 << (self.font.width() - 1);
let mut x = 0;
let row_off = (cy + y) * self.width;
while x < self.font.width() as usize {
let v = if glyph[0] & mask != 0 { fg } else { bg };
dt[row_off + cx + x] = v;
mask >>= 1;
x += 1;
}
glyph = &glyph[bytes_per_line..];
y += 1;
}
}
}
// TODO check if there's a character under cursor
let cx = state.cursor.col * fw;
let cy = state.cursor.row * fh;
// Fill block cursor
for y in 0..fh {
let off = (cy + y) * self.width + cx;
dt[off..off + fw].fill(default_fg);
}
if !self.focused {
// Remove cursor center
for y in 1..fh - 1 {
let off = (cy + y) * self.width + cx + 1;
dt[off..off + fw - 2].fill(default_bg);
}
}
self.old_cursor = state.cursor;
self.force_redraw = false;
self.focus_changed = false;
}
}
impl<'a> Terminal<'a> {
pub fn new(font: PcScreenFont<'static>) -> Result<Self, Error> {
let mut app = Application::new()?;
let mut window = Window::new(&app)?;
let mut poll = PollChannel::new()?;
let width = window.width() as usize;
let height = window.height() as usize;
let rows = height / font.height() as usize;
let columns = width / font.width() as usize;
let termios = TerminalOptions::default();
let (pty_master, pty_slave) = create_pty(Some(termios), TerminalSize { rows, columns })?;
let pty_master_fd = pty_master.as_raw_fd();
// TODO I hate this
let pty_master = Arc::new(Mutex::new(pty_master));
let state = Arc::new(Mutex::new(State::new(columns, rows)));
let draw_state = Arc::new(Mutex::new(DrawState::new(font, width)));
let state_c = state.clone();
let draw_state_c = draw_state.clone();
let pty_master_c = pty_master.clone();
window.set_on_resized(move |width, height| {
let mut s = state_c.lock().unwrap();
let mut ds = draw_state_c.lock().unwrap();
let pty_master = pty_master_c.lock().unwrap();
let width = width as usize;
let height = height as usize;
let rows = height / ds.font.height() as usize;
let columns = width / ds.font.width() as usize;
let term_size = TerminalSize { rows, columns };
unsafe {
pty_master
.device_request(&mut DeviceRequest::SetTerminalSize(term_size))
.unwrap();
}
s.resize(
width / ds.font.width() as usize,
height / ds.font.height() as usize,
);
ds.width = width;
ds.force_redraw = true;
EventOutcome::Redraw
});
let pty_master_c = pty_master.clone();
window.set_on_key_input(move |ev| {
let mut pty_master = pty_master_c.lock().unwrap();
// TODO error reporting from handlers
if let Some(input) = ev.input {
pty_master.write_all(&[input as u8]).unwrap();
} else {
match (ev.modifiers, ev.key) {
(KeyModifiers::NONE, KeyboardKey::Escape) => {
pty_master.write_all(&[b'\x1B']).unwrap();
}
(KeyModifiers::NONE, KeyboardKey::Backspace) => {
pty_master.write_all(&[termios.chars.erase]).unwrap();
}
(KeyModifiers::CTRL, KeyboardKey::Char(b'c')) => {
pty_master.write_all(&[termios.chars.interrupt]).unwrap();
}
(KeyModifiers::CTRL, KeyboardKey::Char(b'd')) => {
pty_master.write_all(&[termios.chars.eof]).unwrap();
}
_ => (),
}
}
EventOutcome::None
});
let draw_state_c = draw_state.clone();
window.set_on_focus_changed(move |focused| {
let mut ds = draw_state_c.lock().unwrap();
ds.focused = focused;
ds.focus_changed = true;
EventOutcome::Redraw
});
let state_c = state.clone();
window.set_on_redraw_requested(move |dt: &mut [u32]| {
let mut s = state_c.lock().unwrap();
let mut ds = draw_state.lock().unwrap();
ds.draw(dt, &mut s);
});
app.add_window(window);
let conn_fd = app.connection().lock().unwrap().as_poll_fd();
poll.add(conn_fd)?;
poll.add(pty_master_fd)?;
let pty_slave_fd = pty_slave.as_raw_fd();
let shell = unsafe {
Command::new("/bin/sh")
.arg("-l")
.stdin(Stdio::from_raw_fd(pty_slave_fd))
.stdout(Stdio::from_raw_fd(pty_slave_fd))
.stderr(Stdio::from_raw_fd(pty_slave_fd))
.process_group_raw(0)
.gain_terminal(0)
.spawn()?
};
Ok(Self {
poll,
conn_fd,
state,
application: app,
pty_master,
pty_master_fd,
shell,
})
}
fn handle_shell(&mut self) -> Result<bool, Error> {
let mut buf = [0; 8192];
let mut pty = self.pty_master.lock().unwrap();
let len = pty.read(&mut buf)?;
if len == 0 {
ABORT.store(true, Ordering::Release);
return Ok(false);
}
let mut needs_redraw = false;
let mut s = self.state.lock().unwrap();
for &ch in &buf[..len] {
needs_redraw |= s.handle_shell_output(ch);
}
Ok(needs_redraw)
}
fn run_inner(mut self) -> Result<ExitCode, Error> {
while !ABORT.load(Ordering::Acquire) {
match self.poll.wait(None)? {
Some((fd, Ok(_))) if fd == self.conn_fd => {
self.application.poll_events()?;
}
Some((fd, Ok(_))) if fd == self.pty_master_fd => {
let needs_redraw = self.handle_shell()?;
if needs_redraw {
self.application.redraw()?;
}
}
Some((_, Ok(_))) => {
todo!()
}
Some((_, Err(error))) => return Err(Error::from(error)),
None => (),
}
}
Ok(ExitCode::SUCCESS)
}
pub fn run(self) -> ExitCode {
match self.run_inner() {
Ok(code) => code,
Err(error) => {
eprintln!("Error: {}", error);
ExitCode::FAILURE
}
}
}
}
static ABORT: AtomicBool = AtomicBool::new(false);
fn main() -> ExitCode {
let font = PcScreenFont::default();
let term = Terminal::new(font).unwrap();
term.run()
}

373
userspace/term/src/state.rs Normal file
View File

@ -0,0 +1,373 @@
use crate::attr::{CellAttributes, Color};
#[derive(Clone, Copy, Debug)]
pub struct GridCell {
pub char: char,
pub attrs: CellAttributes,
}
#[derive(Clone)]
pub struct GridRow {
cols: Vec<GridCell>,
dirty: bool,
}
pub struct Buffer {
rows: Vec<GridRow>,
width: usize,
height: usize,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Cursor {
pub row: usize,
pub col: usize,
}
enum EscapeState {
Normal,
Escape,
Csi,
}
pub struct State {
pub buffer: Buffer,
esc_state: EscapeState,
esc_args: Vec<u32>,
pub cursor: Cursor,
#[allow(unused)]
saved_cursor: Option<Cursor>,
pub default_attributes: CellAttributes,
pub attributes: CellAttributes,
}
impl GridCell {
pub fn new(char: char, attrs: CellAttributes) -> Self {
Self { char, attrs }
}
pub fn empty(bg: Color) -> Self {
Self {
char: '\0',
attrs: CellAttributes {
fg: Color::Black,
bg,
bright: false,
},
}
}
}
impl GridRow {
pub fn new(width: usize, bg: Color) -> Self {
Self {
cols: vec![GridCell::empty(bg); width],
dirty: true,
}
}
pub fn set_cell(&mut self, col: usize, char: char, attrs: CellAttributes) {
self.cols[col] = GridCell::new(char, attrs);
self.dirty = true;
}
pub fn is_dirty(&self) -> bool {
self.dirty
}
pub fn clear_dirty(&mut self) {
self.dirty = false;
}
pub fn cells(&self) -> impl Iterator<Item = &GridCell> {
self.cols.iter()
}
fn clear(&mut self, bg: Color) {
self.cols.fill(GridCell::empty(bg));
self.dirty = true;
}
fn erase_to_right(&mut self, start: usize, bg: Color) {
self.cols[start..].fill(GridCell::empty(bg));
self.dirty = true;
}
fn resize(&mut self, width: usize, bg: Color) {
self.cols.resize(width, GridCell::empty(bg));
self.dirty = true;
}
}
impl Buffer {
pub fn new(width: usize, height: usize, bg: Color) -> Self {
Self {
rows: vec![GridRow::new(width, bg); height],
width,
height,
}
}
pub fn clear(&mut self, bg: Color) {
self.rows.fill(GridRow::new(self.width, bg));
}
pub fn dirty_rows(&mut self) -> impl Iterator<Item = (usize, &mut GridRow)> {
self.rows.iter_mut().enumerate().filter_map(|(i, row)| {
if row.dirty {
row.dirty = false;
Some((i, row))
} else {
None
}
})
}
pub fn resize(&mut self, width: usize, height: usize, bg: Color) {
self.rows.resize(height, GridRow::new(width, bg));
for row in self.rows.iter_mut() {
row.resize(width, bg);
}
self.width = width;
self.height = height;
}
pub fn set_cell(&mut self, cur: Cursor, cell: GridCell) {
self.rows[cur.row].cols[cur.col] = cell;
self.rows[cur.row].dirty = true;
}
pub fn scroll_once(&mut self, bg: Color) {
for i in 1..self.height {
self.rows[i - 1] = self.rows[i].clone();
self.rows[i - 1].dirty = true;
}
self.rows[self.height - 1] = GridRow::new(self.width, bg);
}
pub fn erase_row(&mut self, row: usize, bg: Color) {
self.rows[row].clear(bg);
}
pub fn set_row_dirty(&mut self, row: usize) {
self.rows[row].dirty = true;
}
}
impl State {
pub fn new(width: usize, height: usize) -> Self {
let default_attributes = CellAttributes {
fg: Color::White,
bg: Color::Black,
bright: false,
};
Self {
buffer: Buffer::new(width, height, default_attributes.bg),
esc_args: Vec::new(),
esc_state: EscapeState::Normal,
cursor: Cursor { row: 0, col: 0 },
saved_cursor: None,
default_attributes,
attributes: default_attributes,
}
}
pub fn resize(&mut self, width: usize, height: usize) {
self.buffer
.resize(width, height, self.default_attributes.bg);
if self.cursor.row >= height {
self.cursor.row = height - 1;
}
if self.cursor.col >= width {
self.cursor.col = width - 1;
}
}
fn putc_normal(&mut self, ch: u8) -> bool {
let mut redraw = match ch {
c if c >= 127 => {
let attr = CellAttributes {
fg: Color::Black,
bg: Color::Red,
bright: false,
};
self.buffer.set_cell(self.cursor, GridCell::new('?', attr));
self.cursor.col += 1;
true
}
b'\x1B' => {
self.esc_state = EscapeState::Escape;
self.esc_args.clear();
self.esc_args.push(0);
return false;
}
b'\r' => {
self.buffer.rows[self.cursor.row].dirty = true;
self.cursor.col = 0;
true
}
b'\n' => {
self.buffer.rows[self.cursor.row].dirty = true;
self.cursor.row += 1;
self.cursor.col = 0;
true
}
_ => {
self.buffer
.set_cell(self.cursor, GridCell::new(ch as char, self.attributes));
self.cursor.col += 1;
true
}
};
if self.cursor.col >= self.buffer.width {
if self.cursor.row < self.buffer.height {
self.buffer.rows[self.cursor.row].dirty = true;
}
self.cursor.row += 1;
self.cursor.col = 0;
redraw = true;
}
while self.cursor.row >= self.buffer.height {
self.buffer.scroll_once(self.default_attributes.bg);
self.cursor.row -= 1;
redraw = true;
}
redraw
}
fn handle_ctlseq(&mut self, c: u8) -> bool {
let redraw = match c {
// Move back one character
b'D' => {
if self.cursor.col > 0 {
self.buffer.set_row_dirty(self.cursor.row);
self.cursor.col -= 1;
true
} else {
false
}
}
// Character attributes
b'm' => match self.esc_args[0] {
0 => {
self.attributes = self.default_attributes;
false
}
1 => {
self.attributes.bright = true;
false
}
30..=39 => {
let vt_color = self.esc_args[0] % 10;
if vt_color == 9 {
self.attributes.fg = Color::Black;
} else {
self.attributes.fg = Color::from_esc(vt_color);
}
false
}
40..=49 => {
let vt_color = self.esc_args[0] % 10;
if vt_color == 9 {
self.attributes.bg = Color::Black;
} else {
self.attributes.bg = Color::from_esc(vt_color);
}
false
}
_ => false,
},
// Move cursor to position
b'f' => {
let row = self.esc_args[0].clamp(1, self.buffer.height as u32) - 1;
let col = self.esc_args[1].clamp(1, self.buffer.width as u32) - 1;
self.buffer.set_row_dirty(self.cursor.row);
self.cursor = Cursor {
row: row as _,
col: col as _,
};
true
}
// Clear rows/columns/screen
b'J' => match self.esc_args[0] {
// Erase lines down
0 => false,
// Erase lines up
1 => false,
// Erase all
2 => {
self.buffer.clear(self.attributes.bg);
true
}
_ => false,
},
b'K' => match self.esc_args[0] {
// Erase to right
0 => {
self.buffer.rows[self.cursor.row]
.erase_to_right(self.cursor.col, self.attributes.bg);
true
}
// Erase All
2 => {
self.buffer.erase_row(self.cursor.row, self.attributes.bg);
true
}
_ => false,
},
_ => false,
};
self.esc_state = EscapeState::Normal;
redraw
}
fn handle_ctlseq_byte(&mut self, c: u8) -> bool {
match c {
b'0'..=b'9' => {
let arg = self.esc_args.last_mut().unwrap();
*arg *= 10;
*arg += (c - b'0') as u32;
false
}
b';' => {
self.esc_args.push(0);
false
}
_ => self.handle_ctlseq(c),
}
}
pub fn handle_shell_output(&mut self, ch: u8) -> bool {
match self.esc_state {
EscapeState::Normal => self.putc_normal(ch),
EscapeState::Escape => match ch {
b'[' => {
self.esc_state = EscapeState::Csi;
false
}
_ => {
self.esc_state = EscapeState::Normal;
false
}
},
EscapeState::Csi => self.handle_ctlseq_byte(ch),
}
}
}