From 3e2ed624b8f5496101163a08dde397a0e4cdfd81 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sat, 24 Jan 2026 19:14:31 +0100 Subject: [PATCH 01/78] refactor: remove socket module and related components --- modules/virtual_file_system/src/lib.rs | 2 - .../virtual_file_system/src/socket/address.rs | 45 ------------------- .../src/socket/file_system.rs | 41 ----------------- modules/virtual_file_system/src/socket/mod.rs | 3 -- 4 files changed, 91 deletions(-) delete mode 100644 modules/virtual_file_system/src/socket/address.rs delete mode 100644 modules/virtual_file_system/src/socket/file_system.rs delete mode 100644 modules/virtual_file_system/src/socket/mod.rs diff --git a/modules/virtual_file_system/src/lib.rs b/modules/virtual_file_system/src/lib.rs index 8f784918..cafb0a08 100644 --- a/modules/virtual_file_system/src/lib.rs +++ b/modules/virtual_file_system/src/lib.rs @@ -10,7 +10,6 @@ mod hierarchy; mod item; mod r#macro; mod pipe; -mod socket; mod synchronous_directory; mod synchronous_file; @@ -21,7 +20,6 @@ pub use file::*; pub use file_system::*; pub use hierarchy::*; pub use item::*; -pub use socket::SockerAddress; pub use synchronous_directory::*; pub use synchronous_file::*; diff --git a/modules/virtual_file_system/src/socket/address.rs b/modules/virtual_file_system/src/socket/address.rs deleted file mode 100644 index 2312d5f6..00000000 --- a/modules/virtual_file_system/src/socket/address.rs +++ /dev/null @@ -1,45 +0,0 @@ -use file_system::PathOwned; - -use network::{IP, IPv4, IPv6, Port}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SockerAddress { - IPv4(IPv4, Port), - IPv6(IPv6, Port), - Local(PathOwned), -} - -impl SockerAddress { - pub fn into_ip_and_port(self) -> Option<(IP, Port)> { - match self { - Self::IPv4(ip, port) => Some((ip.into(), port)), - Self::IPv6(ip, port) => Some((ip.into(), port)), - _ => None, - } - } - - pub const fn from_ip_and_port(ip: IP, port: Port) -> Self { - match ip { - IP::IPv4(ip) => Self::IPv4(ip, port), - IP::IPv6(ip) => Self::IPv6(ip, port), - } - } -} - -impl From<(IPv4, Port)> for SockerAddress { - fn from((ip, port): (IPv4, Port)) -> Self { - Self::IPv4(ip, port) - } -} - -impl From<(IPv6, Port)> for SockerAddress { - fn from((ip, port): (IPv6, Port)) -> Self { - Self::IPv6(ip, port) - } -} - -impl From for SockerAddress { - fn from(path: PathOwned) -> Self { - Self::Local(path) - } -} diff --git a/modules/virtual_file_system/src/socket/file_system.rs b/modules/virtual_file_system/src/socket/file_system.rs deleted file mode 100644 index 177f14b7..00000000 --- a/modules/virtual_file_system/src/socket/file_system.rs +++ /dev/null @@ -1,41 +0,0 @@ -use core::{ - collections::{BTreeMap, VecDeque}, - sync::{Arc, RwLock}, -}; - -use file_system::{Inode, LocalFileIdentifier, Path_owned_type}; -use virtual_file_system::VirtualFileSystem; - -use crate::Result; - -struct Socket_type<'a> { - Data: VecDeque<&'a [u8]>, - Connection: Option<()>, -} - -pub struct Local_socket_manager_type<'a> { - Virtual_file_system: &'a VirtualFileSystem<'a>, - Open_sockets: RwLock>>>, - Sockets: RwLock>>>, -} - -impl<'a> Local_socket_manager_type<'a> { - pub fn is_socket_identifier_used( - &self, - Socket: LocalFileIdentifier, - ) -> Result { - Ok(self.Open_sockets.read().unwrap().contains_key(&Socket)) - } - - pub fn New(Virtual_file_system: &'a VirtualFileSystem<'a>) -> Self { - Self { - Virtual_file_system, - Open_sockets: RwLock::new(BTreeMap::new()), - Sockets: RwLock::new(BTreeMap::new()), - } - } - - pub fn Bind(Path: Path_owned_type, Socket: LocalFileIdentifier) -> Result<()> { - todo!() - } -} diff --git a/modules/virtual_file_system/src/socket/mod.rs b/modules/virtual_file_system/src/socket/mod.rs deleted file mode 100644 index cef9176d..00000000 --- a/modules/virtual_file_system/src/socket/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod address; - -pub use address::*; From d51fa1346b8118ba3701763e24fbef94d9edb1a3 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sat, 24 Jan 2026 19:14:59 +0100 Subject: [PATCH 02/78] feat: add strip_prefix method to Components for prefix stripping --- .../src/fundamentals/path/components.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/modules/file_system/src/fundamentals/path/components.rs b/modules/file_system/src/fundamentals/path/components.rs index 5a335014..1dad3e14 100644 --- a/modules/file_system/src/fundamentals/path/components.rs +++ b/modules/file_system/src/fundamentals/path/components.rs @@ -30,6 +30,20 @@ impl<'a> Components<'a> { Components(path.as_str().split(SEPARATOR)) } + pub fn strip_prefix(self, components: &Components<'a>) -> Option> { + let mut self_iter = self.clone(); + let mut components_iter = components.clone(); + + while let Some(component) = components_iter.next() { + match self_iter.next() { + Some(self_component) if self_component == component => {} + _ => return None, + } + } + + Some(Components(self_iter.0)) + } + pub fn get_common_components(self, other: Components<'a>) -> usize { self.zip(other).take_while(|(a, b)| a == b).count() } From 0b287ae0faec72ad0520191ffbbe1d043d81b721 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sat, 24 Jan 2026 19:15:24 +0100 Subject: [PATCH 03/78] feat: add join_path method to Entry for path joining functionality --- modules/file_system/src/fundamentals/entry.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/file_system/src/fundamentals/entry.rs b/modules/file_system/src/fundamentals/entry.rs index d32f5848..ab6a847a 100644 --- a/modules/file_system/src/fundamentals/entry.rs +++ b/modules/file_system/src/fundamentals/entry.rs @@ -6,7 +6,7 @@ use alloc::string::String; -use crate::Kind; +use crate::{Kind, Path, PathOwned}; use super::{Inode, Size}; @@ -59,6 +59,10 @@ impl Entry { size, } } + + pub fn join_path(&self, base_path: impl AsRef) -> Option { + base_path.as_ref().join(Path::from_str(&self.name)) + } } impl AsMut<[u8]> for Entry { From 7f1899f85507e04a9fc8dd31852e64584645be06 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sat, 24 Jan 2026 19:15:34 +0100 Subject: [PATCH 04/78] feat: add InvalidContext error variant and display message to Error enum --- modules/file_system/src/error.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/file_system/src/error.rs b/modules/file_system/src/error.rs index 3915d3bc..89e14ef1 100644 --- a/modules/file_system/src/error.rs +++ b/modules/file_system/src/error.rs @@ -116,6 +116,8 @@ pub enum Error { NotMounted, /// Already mounted. AlreadyMounted, + /// Invalid context + InvalidContext, /// Other unclassified error. Other, } @@ -221,6 +223,7 @@ impl Display for Error { Error::InvalidInode => "Invalid inode", Error::NotMounted => "Not mounted", Error::AlreadyMounted => "Already mounted", + Error::InvalidContext => "Invalid context", Error::Other => "Other", }; From c83bfa86fbf921f3774f22e77394bfb3e027ebf9 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sat, 24 Jan 2026 19:15:40 +0100 Subject: [PATCH 05/78] feat: add NETWORK_DEVICES constant for network device path --- modules/file_system/src/fundamentals/path/path_reference.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/file_system/src/fundamentals/path/path_reference.rs b/modules/file_system/src/fundamentals/path/path_reference.rs index 81dc3cd3..130712d4 100644 --- a/modules/file_system/src/fundamentals/path/path_reference.rs +++ b/modules/file_system/src/fundamentals/path/path_reference.rs @@ -21,6 +21,7 @@ impl Path { /// Stores system-wide settings in a structured format (e.g., JSON, TOML). pub const DEVICES: &'static Path = Self::from_str("/devices"); + pub const NETWORK_DEVICES: &'static Path = Self::from_str("/devices/network"); /// Hardware devices, symlinks for human-friendly names. pub const CONFIGURATION: &'static Path = Self::from_str("/configuration"); From ad6d831df92d8f942d843079af19d78d27e84bd4 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sat, 24 Jan 2026 19:16:30 +0100 Subject: [PATCH 06/78] feat: refactor VirtualFileSystem implementation for improved path handling and permission checks --- .../src/file_system/utilities.rs | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/modules/virtual_file_system/src/file_system/utilities.rs b/modules/virtual_file_system/src/file_system/utilities.rs index dd0af279..c74506aa 100644 --- a/modules/virtual_file_system/src/file_system/utilities.rs +++ b/modules/virtual_file_system/src/file_system/utilities.rs @@ -33,7 +33,7 @@ pub(super) type CharacterDevicesMap = BTreeMap; pub(super) type BlockDevicesMap = BTreeMap; pub(super) type PipeMap = BTreeMap; -impl<'a> VirtualFileSystem<'a> { +impl VirtualFileSystem { pub(super) async fn has_permissions( users_manager: &users::Manager, current_user: UserIdentifier, @@ -77,10 +77,17 @@ impl<'a> VirtualFileSystem<'a> { let mount_point: &Path = file_system.mount_point.as_ref(); let mount_point_components = mount_point.get_components(); - let score = path_components + let striped_components = path_components .clone() - .get_common_components(mount_point_components); + .strip_prefix(&mount_point_components); + if striped_components.is_none() { + continue; + } + + let score = mount_point_components.count(); + + // Only consider this file system if all mount point components match if result_score < score { result_score = score; result = i; @@ -101,7 +108,15 @@ impl<'a> VirtualFileSystem<'a> { let relative_path = path .as_ref() .strip_prefix_absolute(&internal_file_system.mount_point) - .ok_or(Error::InvalidPath)?; + .ok_or_else(|| { + log::error!( + "Error stripping prefix {:?} from path {:?}", + internal_file_system.mount_point, + path.as_ref() + ); + + Error::InvalidPath + })?; Ok((internal_file_system, relative_path, i)) } @@ -212,7 +227,7 @@ impl<'a> VirtualFileSystem<'a> { } pub(super) async fn check_permissions_with_parent( - file_system: &dyn FileSystemOperations, + file_systems: &FileSystemsArray, path: impl AsRef, current_permission: Permission, parent_permission: Permission, @@ -221,10 +236,28 @@ impl<'a> VirtualFileSystem<'a> { if !path.as_ref().is_root() { let parent_path = path.as_ref().go_parent().ok_or(Error::InvalidPath)?; - Self::check_permissions(file_system, parent_path, parent_permission, user).await?; + let (parent_file_system, relative_path, _) = + Self::get_file_system_from_path(&file_systems, &parent_path)?; // Get the file system identifier and the relative path + + Self::check_permissions( + parent_file_system.file_system, + relative_path, + parent_permission, + user, + ) + .await?; } - Self::check_permissions(file_system, path.as_ref(), current_permission, user).await?; + let (file_system, relative_path, _) = + Self::get_file_system_from_path(&file_systems, &path)?; // Get the file system identifier and the relative path + + Self::check_permissions( + file_system.file_system, + relative_path, + current_permission, + user, + ) + .await?; Ok(()) } From f3cb28119160df98342883462d57b1987ac64fe8 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sat, 24 Jan 2026 19:16:36 +0100 Subject: [PATCH 07/78] feat: remove unused network socket driver and related code from VirtualFileSystem --- .../src/file_system/mod.rs | 129 +++++------------- 1 file changed, 37 insertions(+), 92 deletions(-) diff --git a/modules/virtual_file_system/src/file_system/mod.rs b/modules/virtual_file_system/src/file_system/mod.rs index 83bad7f8..58d445d2 100644 --- a/modules/virtual_file_system/src/file_system/mod.rs +++ b/modules/virtual_file_system/src/file_system/mod.rs @@ -1,7 +1,7 @@ mod utilities; use crate::pipe::Pipe; -use crate::{Directory, Error, File, ItemStatic, Result, SockerAddress, poll}; +use crate::{Directory, Error, File, ItemStatic, Result, poll}; use alloc::borrow::ToOwned; use alloc::vec; use alloc::{boxed::Box, collections::BTreeMap}; @@ -13,12 +13,10 @@ use file_system::{ AccessFlags, AttributeFlags, Attributes, Context, FileSystemOperations, Flags, Kind, Path, StateFlags, Statistics, }; -use network::{IP, Port, Protocol, SocketDriver}; use synchronization::{ blocking_mutex::raw::CriticalSectionRawMutex, once_lock::OnceLock, rwlock::RwLock, }; use task::TaskIdentifier; -use time::Duration; use users::{GroupIdentifier, UserIdentifier}; use utilities::*; @@ -30,20 +28,14 @@ pub fn initialize( users_manager: &'static users::Manager, time_manager: &'static time::Manager, root_file_system: impl FileSystemOperations + 'static, - network_socket_driver: Option<&'static dyn SocketDriver>, -) -> Result<&'static VirtualFileSystem<'static>> { - let virtual_file_system = VirtualFileSystem::new( - task_manager, - users_manager, - time_manager, - root_file_system, - network_socket_driver, - ); +) -> Result<&'static VirtualFileSystem> { + let virtual_file_system = + VirtualFileSystem::new(task_manager, users_manager, time_manager, root_file_system); Ok(VIRTUAL_FILE_SYSTEM_INSTANCE.get_or_init(|| virtual_file_system)) } -pub fn get_instance() -> &'static VirtualFileSystem<'static> { +pub fn get_instance() -> &'static VirtualFileSystem { VIRTUAL_FILE_SYSTEM_INSTANCE .try_get() .expect("Virtual file system is not initialized") @@ -52,7 +44,7 @@ pub fn get_instance() -> &'static VirtualFileSystem<'static> { /// The virtual file system. /// /// It is a singleton. -pub struct VirtualFileSystem<'a> { +pub struct VirtualFileSystem { /// Mounted file systems. file_systems: RwLock, /// Character devices. @@ -61,17 +53,14 @@ pub struct VirtualFileSystem<'a> { block_device: RwLock, /// Pipes. pipes: RwLock, - /// Network sockets. - _network_socket_driver: Option<&'a dyn SocketDriver>, } -impl<'a> VirtualFileSystem<'a> { +impl VirtualFileSystem { pub fn new( _: &'static task::Manager, _: &'static users::Manager, _: &'static time::Manager, root_file_system: impl FileSystemOperations + 'static, - _network_socket_driver: Option<&'a dyn SocketDriver>, ) -> Self { let file_systems = vec![InternalFileSystem { reference_count: 1, @@ -84,7 +73,6 @@ impl<'a> VirtualFileSystem<'a> { character_device: RwLock::new(BTreeMap::new()), block_device: RwLock::new(BTreeMap::new()), pipes: RwLock::new(BTreeMap::new()), - _network_socket_driver, } } @@ -213,13 +201,10 @@ impl<'a> VirtualFileSystem<'a> { let mut file_systems = self.file_systems.write().await; // Get the file systems - let (file_system, relative_path, _) = - Self::get_mutable_file_system_from_path(&mut file_systems, &path)?; // Get the file system identifier and the relative path - let (time, user, _) = self.get_time_user_group(task).await?; Self::check_permissions_with_parent( - file_system.file_system, + &file_systems, path, Permission::Read, Permission::Execute, @@ -227,13 +212,16 @@ impl<'a> VirtualFileSystem<'a> { ) .await?; + let (file_system, relative_path, _) = + Self::get_mutable_file_system_from_path(&mut file_systems, &path)?; // Get the file system identifier and the relative path + let mut attributes = Attributes::new().set_mask( AttributeFlags::Kind | AttributeFlags::User | AttributeFlags::Group | AttributeFlags::Permissions, ); - Self::get_attributes(file_system.file_system, path, &mut attributes).await?; + Self::get_attributes(file_system.file_system, relative_path, &mut attributes).await?; let kind = attributes.get_kind().ok_or(Error::MissingAttribute)?; if *kind != Kind::Directory { @@ -271,20 +259,23 @@ impl<'a> VirtualFileSystem<'a> { let mut file_systems = self.file_systems.write().await; // Get the file systems - let (file_system, relative_path, _) = - Self::get_mutable_file_system_from_path(&mut file_systems, &path)?; // Get the file system identifier and the relative path - let (time, user, group) = self.get_time_user_group(task).await?; let (mode, open, _) = flags.split(); if open.contains(CreateFlags::Create) { + let (file_system, relative_path, _) = + Self::get_mutable_file_system_from_path(&mut file_systems, &path)?; // Get the file system identifier and the relative path + let result = poll(|| Ok(file_system.file_system.create_file(relative_path)?)).await; match result { Ok(()) => { Self::check_permissions( file_system.file_system, - path.go_parent().ok_or(Error::InvalidPath)?, + path.go_parent().ok_or_else(|| { + log::error!("Error getting parent path for {:?}", path); + Error::InvalidPath + })?, Permission::Write | Permission::Execute, user, ) @@ -313,7 +304,7 @@ impl<'a> VirtualFileSystem<'a> { } } else { Self::check_permissions_with_parent( - file_system.file_system, + &file_systems, path, mode.into_permission(), Permission::Execute, @@ -322,6 +313,9 @@ impl<'a> VirtualFileSystem<'a> { .await?; } + let (file_system, relative_path, _) = + Self::get_mutable_file_system_from_path(&mut file_systems, &path)?; // Get the file system identifier and the relative path + let attributes = if mode.contains(AccessFlags::Write) { Attributes::new().set_modification(time).set_access(time) } else { @@ -394,7 +388,7 @@ impl<'a> VirtualFileSystem<'a> { path: impl AsRef, item: ItemStatic, ) -> Result<()> { - let path = path.as_ref(); + let path: &Path = path.as_ref(); if !path.is_valid() || !path.is_absolute() || path.is_root() { return Err(Error::InvalidPath); } @@ -406,9 +400,11 @@ impl<'a> VirtualFileSystem<'a> { let (_, user, _) = self.get_time_user_group(task).await?; + let parent_path = path.go_parent().ok_or(Error::InvalidPath)?; + Self::check_permissions( parent_file_system.file_system, - path.go_parent().ok_or(Error::InvalidPath)?, + parent_path, Permission::Write, user, ) @@ -447,15 +443,7 @@ impl<'a> VirtualFileSystem<'a> { ); inode } - ItemStatic::FileSystem(file_system) => { - let mut file_systems = self.file_systems.write().await; - file_systems.push(InternalFileSystem { - reference_count: 1, - mount_point: path.to_owned(), - file_system, - }); - 0 - } + ItemStatic::FileSystem(_) => 0, ItemStatic::Pipe(pipe) => { let mut pipes = self.pipes.write().await; let inode = Self::get_new_inode(&*pipes).ok_or(Error::TooManyInodes)?; @@ -483,6 +471,7 @@ impl<'a> VirtualFileSystem<'a> { return Err(Error::InvalidIdentifier); } }; + let attributes = Attributes::new() .set_user(user) .set_group(group) @@ -495,6 +484,14 @@ impl<'a> VirtualFileSystem<'a> { .set_inode(inode); Self::set_attributes(parent_file_system.file_system, relative_path, &attributes).await?; + if let ItemStatic::FileSystem(file_system) = item { + file_systems.push(InternalFileSystem { + reference_count: 1, + mount_point: path.to_owned(), + file_system, + }); + } + poll(|| { Ok(item .as_mount_operations() @@ -607,9 +604,6 @@ impl<'a> VirtualFileSystem<'a> { let kind = attributes.get_kind().ok_or(Error::MissingAttribute)?; match kind { - Kind::Directory => { - return Err(Error::UnsupportedOperation); - } Kind::Pipe => { let mut named_pipes = self.pipes.write().await; @@ -842,53 +836,4 @@ impl<'a> VirtualFileSystem<'a> { let attributes = Attributes::new().set_permissions(permissions); Self::set_attributes(file_system.file_system, relative_path, &attributes).await } - - pub async fn send(&self, _task: TaskIdentifier, _data: &[u8]) -> Result<()> { - todo!() - } - - pub async fn receive(&self, _task: TaskIdentifier, _data: &mut [u8]) -> Result { - todo!() - } - - pub async fn send_to( - &self, - _task: TaskIdentifier, - _data: &[u8], - _address: SockerAddress, - ) -> Result<()> { - todo!() - } - - pub async fn receive_from(&self, _data: &mut [u8]) -> Result<(usize, SockerAddress)> { - todo!() - } - - pub async fn bind(&self, _address: SockerAddress, _protocol: Protocol) -> Result<()> { - todo!() - } - - pub async fn connect(&self, _address: SockerAddress) -> Result<()> { - todo!() - } - - pub async fn accept(&self) -> Result> { - todo!() - } - - pub async fn set_send_timeout(&self, _timeout: Duration) -> Result<()> { - todo!() - } - - pub async fn set_receive_timeout(&self, _timeout: Duration) -> Result<()> { - todo!() - } - - pub async fn get_send_timeout(&self) -> Result> { - todo!() - } - - pub async fn get_receive_timeout(&self) -> Result> { - todo!() - } } From 841290d6ad309607fb00a527803a614db69928b5 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sat, 24 Jan 2026 19:16:59 +0100 Subject: [PATCH 08/78] feat: remove Network error variant from Error enum and related implementations --- modules/virtual_file_system/src/error.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/modules/virtual_file_system/src/error.rs b/modules/virtual_file_system/src/error.rs index 2e787ea8..f3883776 100644 --- a/modules/virtual_file_system/src/error.rs +++ b/modules/virtual_file_system/src/error.rs @@ -17,7 +17,6 @@ pub enum Error { AlreadyExists, Time(time::Error), FileSystem(file_system::Error) = 0x100, - Network(network::Error) = 0x200, Users(users::Error) = 0x300, Task(task::Error) = 0x400, MissingAttribute, @@ -48,7 +47,6 @@ impl From for NonZeroU32 { let offset = match value { Error::FileSystem(error_type) => error_type.get_discriminant().get(), - Error::Network(error_type) => error_type.get_discriminant().get() as u32, _ => 0, }; @@ -74,12 +72,6 @@ impl From for Error { } } -impl From for Error { - fn from(value: network::Error) -> Self { - Self::Network(value) - } -} - impl From for Error { fn from(value: task::Error) -> Self { Self::Task(value) @@ -98,7 +90,6 @@ impl Display for Error { write!(f, "Failed to get task informations") } Error::FileSystem(err) => write!(f, "File system error: {err}"), - Error::Network(err) => write!(f, "Network error: {err}"), Error::InvalidIdentifier => write!(f, "Invalid identifier"), Error::AlreadyExists => write!(f, "Already exists"), Error::Time(err) => write!(f, "Time error: {err}"), From 4f8e5bc569f43671f25efbf7a3e6070d1145a8e1 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sat, 24 Jan 2026 19:17:34 +0100 Subject: [PATCH 09/78] feat: refactor create_default_hierarchy to use a loop for directory creation --- modules/virtual_file_system/src/hierarchy.rs | 61 +++++++++----------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/modules/virtual_file_system/src/hierarchy.rs b/modules/virtual_file_system/src/hierarchy.rs index 767ec099..a28c61df 100644 --- a/modules/virtual_file_system/src/hierarchy.rs +++ b/modules/virtual_file_system/src/hierarchy.rs @@ -5,53 +5,48 @@ use task::TaskIdentifier; use crate::{Directory, Error, Result, VirtualFileSystem}; +pub fn ignore_already_exists_error(result: Result) -> Result<()> { + match result { + Ok(_) | Err(Error::AlreadyExists) => Ok(()), + Err(error) => Err(error), + } +} + /// Create the default hierarchy of the file system. pub async fn create_default_hierarchy( - virtual_file_system: &VirtualFileSystem<'_>, + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, ) -> Result<()> { virtual_file_system .set_permissions(task, &Path::ROOT, Permissions::DIRECTORY_DEFAULT) .await?; - virtual_file_system - .create_directory(task, &Path::SYSTEM) - .await?; - virtual_file_system - .create_directory(task, &Path::CONFIGURATION) - .await?; - virtual_file_system - .create_directory(task, &Path::SHARED_CONFIGURATION) - .await?; - virtual_file_system - .create_directory(task, &Path::DEVICES) - .await?; + + let paths = [ + Path::SYSTEM, + Path::CONFIGURATION, + Path::SHARED_CONFIGURATION, + Path::DEVICES, + Path::USERS, + Path::DATA, + Path::SHARED_DATA, + Path::BINARIES, + Path::TEMPORARY, + Path::LOGS, + ]; + + for path in paths { + ignore_already_exists_error(virtual_file_system.create_directory(task, &path).await)?; + } + virtual_file_system .set_permissions(task, &Path::DEVICES, Permissions::ALL_FULL) .await?; - virtual_file_system - .create_directory(task, &Path::USERS) - .await?; - virtual_file_system - .create_directory(task, &Path::DATA) - .await?; - virtual_file_system - .create_directory(task, &Path::SHARED_DATA) - .await?; - virtual_file_system - .create_directory(task, &Path::BINARIES) - .await?; - virtual_file_system - .create_directory(task, &Path::TEMPORARY) - .await?; - virtual_file_system - .create_directory(task, &Path::LOGS) - .await?; Ok(()) } pub async fn clean_devices_in_directory<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, + virtual_file_system: &'a VirtualFileSystem, task: TaskIdentifier, path: &Path, ) -> Result<()> { @@ -81,7 +76,7 @@ pub async fn clean_devices_in_directory<'a>( } pub async fn clean_devices<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, + virtual_file_system: &'a VirtualFileSystem, task: TaskIdentifier, ) -> Result<()> { clean_devices_in_directory(virtual_file_system, task, Path::DEVICES).await?; From 41c51dba0df96e31379ab0efde23c61d2d7f6c85 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sat, 24 Jan 2026 19:17:50 +0100 Subject: [PATCH 10/78] feat: remove lifetime parameters from VirtualFileSystem references in directory and file operations --- modules/virtual_file_system/src/directory.rs | 6 +++--- modules/virtual_file_system/src/file.rs | 12 ++++++------ .../virtual_file_system/src/synchronous_directory.rs | 11 ++++------- modules/virtual_file_system/src/synchronous_file.rs | 9 +++------ 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/modules/virtual_file_system/src/directory.rs b/modules/virtual_file_system/src/directory.rs index e6cc18fa..15680e99 100644 --- a/modules/virtual_file_system/src/directory.rs +++ b/modules/virtual_file_system/src/directory.rs @@ -25,7 +25,7 @@ impl Directory { } pub async fn create<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, + virtual_file_system: &'a VirtualFileSystem, task: TaskIdentifier, path: impl AsRef, ) -> Result<()> { @@ -33,7 +33,7 @@ impl Directory { } pub async fn open<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, + virtual_file_system: &'a VirtualFileSystem, task: TaskIdentifier, path: impl AsRef, ) -> Result { @@ -56,7 +56,7 @@ impl Directory { poll(|| self.0.set_position(0)).await } - pub async fn close(mut self, virtual_file_system: &VirtualFileSystem<'_>) -> Result<()> { + pub async fn close(mut self, virtual_file_system: &VirtualFileSystem) -> Result<()> { let result = virtual_file_system .close( &ItemStatic::Directory(self.0.directory), diff --git a/modules/virtual_file_system/src/file.rs b/modules/virtual_file_system/src/file.rs index 14a07c65..aa36fa6b 100644 --- a/modules/virtual_file_system/src/file.rs +++ b/modules/virtual_file_system/src/file.rs @@ -46,7 +46,7 @@ impl File { } pub async fn open<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, + virtual_file_system: &'a VirtualFileSystem, task: task::TaskIdentifier, path: impl AsRef, flags: Flags, @@ -57,7 +57,7 @@ impl File { } pub async fn create_unnamed_pipe<'a>( - file_system: &'a VirtualFileSystem<'a>, + file_system: &'a VirtualFileSystem, size: usize, status: StateFlags, ) -> Result<(Self, Self)> { @@ -75,7 +75,7 @@ impl File { } pub async fn read_slice_from_path( - virtual_file_system: &VirtualFileSystem<'_>, + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, path: impl AsRef, buffer: &mut [u8], @@ -96,7 +96,7 @@ impl File { } pub async fn read_from_path( - virtual_file_system: &VirtualFileSystem<'_>, + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, path: impl AsRef, buffer: &mut Vec, @@ -119,7 +119,7 @@ impl File { } pub async fn write_to_path( - virtual_file_system: &VirtualFileSystem<'_>, + virtual_file_system: &VirtualFileSystem, task: task::TaskIdentifier, path: impl AsRef, buffer: &[u8], @@ -210,7 +210,7 @@ impl File { Ok(self.0.flags.get_access()) } - pub async fn close(mut self, virtual_file_system: &VirtualFileSystem<'_>) -> crate::Result<()> { + pub async fn close(mut self, virtual_file_system: &VirtualFileSystem) -> crate::Result<()> { let result = virtual_file_system .close(&self.0.item, &mut self.0.context) .await; diff --git a/modules/virtual_file_system/src/synchronous_directory.rs b/modules/virtual_file_system/src/synchronous_directory.rs index 6c2d8519..d26c3505 100644 --- a/modules/virtual_file_system/src/synchronous_directory.rs +++ b/modules/virtual_file_system/src/synchronous_directory.rs @@ -35,7 +35,7 @@ impl SynchronousDirectory { } pub fn create<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, + virtual_file_system: &'a VirtualFileSystem, task: TaskIdentifier, path: impl AsRef, ) -> Result<()> { @@ -43,7 +43,7 @@ impl SynchronousDirectory { } pub fn open<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, + virtual_file_system: &'a VirtualFileSystem, task: TaskIdentifier, path: impl AsRef, ) -> Result { @@ -114,16 +114,13 @@ impl SynchronousDirectory { Ok(self.flags.get_access()) } - pub fn close_internal<'a>( - &mut self, - virtual_file_system: &'a VirtualFileSystem<'a>, - ) -> Result<()> { + pub fn close_internal<'a>(&mut self, virtual_file_system: &'a VirtualFileSystem) -> Result<()> { block_on( virtual_file_system.close(&ItemStatic::Directory(self.directory), &mut self.context), ) } - pub fn close(mut self, virtual_file_system: &VirtualFileSystem<'_>) -> Result<()> { + pub fn close(mut self, virtual_file_system: &VirtualFileSystem) -> Result<()> { let result = self.close_internal(virtual_file_system); forget(self); diff --git a/modules/virtual_file_system/src/synchronous_file.rs b/modules/virtual_file_system/src/synchronous_file.rs index be225902..811a2dbf 100644 --- a/modules/virtual_file_system/src/synchronous_file.rs +++ b/modules/virtual_file_system/src/synchronous_file.rs @@ -44,7 +44,7 @@ impl SynchronousFile { } pub fn open<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, + virtual_file_system: &'a VirtualFileSystem, task: TaskIdentifier, path: impl AsRef, flags: Flags, @@ -249,14 +249,11 @@ impl SynchronousFile { Ok(self.flags.get_access()) } - pub fn close_internal( - &mut self, - virtual_file_system: &VirtualFileSystem<'_>, - ) -> crate::Result<()> { + pub fn close_internal(&mut self, virtual_file_system: &VirtualFileSystem) -> crate::Result<()> { block_on(virtual_file_system.close(&self.item, &mut self.context)) } - pub fn close(mut self, virtual_file_system: &VirtualFileSystem<'_>) -> crate::Result<()> { + pub fn close(mut self, virtual_file_system: &VirtualFileSystem) -> crate::Result<()> { let result = self.close_internal(virtual_file_system); forget(self); From bf03e4e153edf42934b22234e33255aca471ec7e Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sat, 24 Jan 2026 19:17:58 +0100 Subject: [PATCH 11/78] feat: remove network dependency from virtual_file_system Cargo.toml --- modules/virtual_file_system/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/virtual_file_system/Cargo.toml b/modules/virtual_file_system/Cargo.toml index a88517d8..ffd18ca0 100644 --- a/modules/virtual_file_system/Cargo.toml +++ b/modules/virtual_file_system/Cargo.toml @@ -8,7 +8,6 @@ file_system = { workspace = true } task = { workspace = true } users = { workspace = true } time = { workspace = true } -network = { workspace = true } synchronization = { workspace = true } shared = { workspace = true } log = { workspace = true } From 2c140648efcd7c40a616ccfd85f2949575a466da Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:18:00 +0100 Subject: [PATCH 12/78] feat: simplify pipe removal logic in mount_file_system --- modules/virtual_file_system/src/file_system/mod.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/modules/virtual_file_system/src/file_system/mod.rs b/modules/virtual_file_system/src/file_system/mod.rs index 58d445d2..cfcca582 100644 --- a/modules/virtual_file_system/src/file_system/mod.rs +++ b/modules/virtual_file_system/src/file_system/mod.rs @@ -603,13 +603,10 @@ impl VirtualFileSystem { let inode = *attributes.get_inode().ok_or(Error::MissingAttribute)?; let kind = attributes.get_kind().ok_or(Error::MissingAttribute)?; - match kind { - Kind::Pipe => { - let mut named_pipes = self.pipes.write().await; + if kind == &Kind::Pipe { + let mut named_pipes = self.pipes.write().await; - named_pipes.remove(&inode); - } - _ => {} + named_pipes.remove(&inode); } poll(|| Ok(file_system.file_system.remove(relative_path)?)).await?; From 13f52c82353ed5e4f35f0739343c4d7e042fc2d9 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:18:31 +0100 Subject: [PATCH 13/78] refactor: simplify calls to get_file_system_from_path by removing redundant references --- modules/virtual_file_system/src/file_system/utilities.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/virtual_file_system/src/file_system/utilities.rs b/modules/virtual_file_system/src/file_system/utilities.rs index c74506aa..8b7efe92 100644 --- a/modules/virtual_file_system/src/file_system/utilities.rs +++ b/modules/virtual_file_system/src/file_system/utilities.rs @@ -237,7 +237,7 @@ impl VirtualFileSystem { let parent_path = path.as_ref().go_parent().ok_or(Error::InvalidPath)?; let (parent_file_system, relative_path, _) = - Self::get_file_system_from_path(&file_systems, &parent_path)?; // Get the file system identifier and the relative path + Self::get_file_system_from_path(file_systems, &parent_path)?; // Get the file system identifier and the relative path Self::check_permissions( parent_file_system.file_system, @@ -248,8 +248,7 @@ impl VirtualFileSystem { .await?; } - let (file_system, relative_path, _) = - Self::get_file_system_from_path(&file_systems, &path)?; // Get the file system identifier and the relative path + let (file_system, relative_path, _) = Self::get_file_system_from_path(file_systems, &path)?; // Get the file system identifier and the relative path Self::check_permissions( file_system.file_system, From 4a6a7aa252f26d49a6db26ece40057269b8ec4d8 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:18:44 +0100 Subject: [PATCH 14/78] feat: remove lifetime parameters from VirtualFileSystem references in directory and file operations --- modules/virtual_file_system/src/directory.rs | 8 ++++---- modules/virtual_file_system/src/file.rs | 8 ++++---- modules/virtual_file_system/src/hierarchy.rs | 8 ++++---- .../virtual_file_system/src/synchronous_directory.rs | 10 +++++----- modules/virtual_file_system/src/synchronous_file.rs | 4 ++-- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/modules/virtual_file_system/src/directory.rs b/modules/virtual_file_system/src/directory.rs index 15680e99..2a69268b 100644 --- a/modules/virtual_file_system/src/directory.rs +++ b/modules/virtual_file_system/src/directory.rs @@ -24,16 +24,16 @@ impl Directory { Self(SynchronousDirectory::new(directory, flags, context)) } - pub async fn create<'a>( - virtual_file_system: &'a VirtualFileSystem, + pub async fn create( + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, path: impl AsRef, ) -> Result<()> { virtual_file_system.create_directory(task, &path).await } - pub async fn open<'a>( - virtual_file_system: &'a VirtualFileSystem, + pub async fn open( + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, path: impl AsRef, ) -> Result { diff --git a/modules/virtual_file_system/src/file.rs b/modules/virtual_file_system/src/file.rs index aa36fa6b..64f863ed 100644 --- a/modules/virtual_file_system/src/file.rs +++ b/modules/virtual_file_system/src/file.rs @@ -45,8 +45,8 @@ impl File { Self(SynchronousFile::new(item, 0, flags, context)) } - pub async fn open<'a>( - virtual_file_system: &'a VirtualFileSystem, + pub async fn open( + virtual_file_system: &VirtualFileSystem, task: task::TaskIdentifier, path: impl AsRef, flags: Flags, @@ -56,8 +56,8 @@ impl File { Ok(file_identifier) } - pub async fn create_unnamed_pipe<'a>( - file_system: &'a VirtualFileSystem, + pub async fn create_unnamed_pipe( + file_system: &VirtualFileSystem, size: usize, status: StateFlags, ) -> Result<(Self, Self)> { diff --git a/modules/virtual_file_system/src/hierarchy.rs b/modules/virtual_file_system/src/hierarchy.rs index a28c61df..3c54ee0c 100644 --- a/modules/virtual_file_system/src/hierarchy.rs +++ b/modules/virtual_file_system/src/hierarchy.rs @@ -45,8 +45,8 @@ pub async fn create_default_hierarchy( Ok(()) } -pub async fn clean_devices_in_directory<'a>( - virtual_file_system: &'a VirtualFileSystem, +pub async fn clean_devices_in_directory( + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, path: &Path, ) -> Result<()> { @@ -75,8 +75,8 @@ pub async fn clean_devices_in_directory<'a>( Ok(()) } -pub async fn clean_devices<'a>( - virtual_file_system: &'a VirtualFileSystem, +pub async fn clean_devices( + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, ) -> Result<()> { clean_devices_in_directory(virtual_file_system, task, Path::DEVICES).await?; diff --git a/modules/virtual_file_system/src/synchronous_directory.rs b/modules/virtual_file_system/src/synchronous_directory.rs index d26c3505..772d041b 100644 --- a/modules/virtual_file_system/src/synchronous_directory.rs +++ b/modules/virtual_file_system/src/synchronous_directory.rs @@ -34,16 +34,16 @@ impl SynchronousDirectory { blocking_operation(self.flags, || operation(self)) } - pub fn create<'a>( - virtual_file_system: &'a VirtualFileSystem, + pub fn create( + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, path: impl AsRef, ) -> Result<()> { block_on(virtual_file_system.create_directory(task, &path)) } - pub fn open<'a>( - virtual_file_system: &'a VirtualFileSystem, + pub fn open( + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, path: impl AsRef, ) -> Result { @@ -114,7 +114,7 @@ impl SynchronousDirectory { Ok(self.flags.get_access()) } - pub fn close_internal<'a>(&mut self, virtual_file_system: &'a VirtualFileSystem) -> Result<()> { + pub fn close_internal(&mut self, virtual_file_system: &VirtualFileSystem) -> Result<()> { block_on( virtual_file_system.close(&ItemStatic::Directory(self.directory), &mut self.context), ) diff --git a/modules/virtual_file_system/src/synchronous_file.rs b/modules/virtual_file_system/src/synchronous_file.rs index 811a2dbf..d3cebf8b 100644 --- a/modules/virtual_file_system/src/synchronous_file.rs +++ b/modules/virtual_file_system/src/synchronous_file.rs @@ -43,8 +43,8 @@ impl SynchronousFile { } } - pub fn open<'a>( - virtual_file_system: &'a VirtualFileSystem, + pub fn open( + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, path: impl AsRef, flags: Flags, From 7b5f066cadf13d93a52b1561f082622d388b4add Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:19:03 +0100 Subject: [PATCH 15/78] refactor: remove lifetime parameters from initialize function in integration tests --- modules/virtual_file_system/tests/integration.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/modules/virtual_file_system/tests/integration.rs b/modules/virtual_file_system/tests/integration.rs index 417e6cf6..45c036b2 100644 --- a/modules/virtual_file_system/tests/integration.rs +++ b/modules/virtual_file_system/tests/integration.rs @@ -11,7 +11,7 @@ use virtual_file_system::{File, VirtualFileSystem}; drivers_std::instantiate_global_allocator!(); -async fn initialize<'a>() -> (TaskIdentifier, &'a VirtualFileSystem<'a>) { +async fn initialize<'a>() -> (TaskIdentifier, &'a VirtualFileSystem) { let task_instance = task::initialize(); let users_manager = users::initialize(); @@ -31,14 +31,9 @@ async fn initialize<'a>() -> (TaskIdentifier, &'a VirtualFileSystem<'a>) { little_fs::FileSystem::format(device, cache_size).unwrap(); let file_system = little_fs::FileSystem::new(device, cache_size).unwrap(); - let virtual_file_system = virtual_file_system::initialize( - task_instance, - users_manager, - time_manager, - file_system, - None, - ) - .unwrap(); + let virtual_file_system = + virtual_file_system::initialize(task_instance, users_manager, time_manager, file_system) + .unwrap(); (task, virtual_file_system) } From b5db26ff15144bc33179a4071f7476c19a865226 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:19:19 +0100 Subject: [PATCH 16/78] feat: allow flexible duration types in sleep function --- modules/task/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/task/src/lib.rs b/modules/task/src/lib.rs index affa8acd..2f86ee6e 100644 --- a/modules/task/src/lib.rs +++ b/modules/task/src/lib.rs @@ -25,8 +25,8 @@ pub use task::*; pub use task_macros::{run, test}; /// Sleep the current thread for a given duration. -pub async fn sleep(duration: Duration) { - let nano_seconds = duration.as_nanos(); +pub async fn sleep(duration: impl Into) { + let nano_seconds = duration.into().as_nanos(); Timer::after(embassy_time::Duration::from_nanos(nano_seconds as u64)).await } From 6a74fca71875d1dc76cb94b47ca5a0639be0dbe6 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:19:47 +0100 Subject: [PATCH 17/78] refactor: update documentation for from_raw_parts function in AnyByLayout --- modules/shared/src/any.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/shared/src/any.rs b/modules/shared/src/any.rs index 9670442b..20a1769e 100644 --- a/modules/shared/src/any.rs +++ b/modules/shared/src/any.rs @@ -19,11 +19,13 @@ impl<'a, T> From<&'a T> for &'a AnyByLayout { impl AnyByLayout { pub const NONE: &mut Self = Self::from_mutable(&mut [0u8; 0]); - /// Gets a mutable reference to an `AnyByLayout` from raw parts. + /// Creates an `AnyByLayout` from raw parts. /// /// # Safety - /// The caller must ensure that the provided data pointer is valid for reads and writes - /// for the specified size, and that the memory is properly aligned. + /// + /// This function is unsafe because it creates a reference from a raw pointer. + /// The caller must ensure that the pointer is valid for reads and writes + /// for `size` bytes and properly aligned. pub unsafe fn from_raw_parts<'a>(data: *mut u8, size: usize) -> &'a mut Self { unsafe { let slice = slice::from_raw_parts_mut(data, size); From 13a802e327e4d330b7a2aeff66bc7eb7909f9efe Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:19:56 +0100 Subject: [PATCH 18/78] feat: integrate task module with poll_ready and poll_pin_ready macros --- modules/shared/src/lib.rs | 1 + modules/shared/src/task.rs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 modules/shared/src/task.rs diff --git a/modules/shared/src/lib.rs b/modules/shared/src/lib.rs index 9e71b392..7cb22fe7 100644 --- a/modules/shared/src/lib.rs +++ b/modules/shared/src/lib.rs @@ -8,6 +8,7 @@ pub mod flags; mod http; mod size; mod slice; +pub mod task; mod time; mod unit; mod utf8; diff --git a/modules/shared/src/task.rs b/modules/shared/src/task.rs new file mode 100644 index 00000000..f6b82a9d --- /dev/null +++ b/modules/shared/src/task.rs @@ -0,0 +1,18 @@ +#[macro_export] +macro_rules! poll_ready { + ($expr:expr) => { + match $expr { + Poll::Ready(val) => val, + Poll::Pending => { + return Poll::Pending; + } + } + }; +} + +#[macro_export] +macro_rules! poll_pin_ready { + ($pin:expr, $context:expr) => { + $crate::poll_ready!(::core::pin::pin!($pin).poll($context)) + }; +} From 75b159d8b2a723decca65c72cf9fc347aa57c88b Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:20:56 +0100 Subject: [PATCH 19/78] feat: add radio button creation and event handling functionality --- modules/graphics/src/lvgl.rs | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/modules/graphics/src/lvgl.rs b/modules/graphics/src/lvgl.rs index 751ac1a8..8bf68100 100644 --- a/modules/graphics/src/lvgl.rs +++ b/modules/graphics/src/lvgl.rs @@ -94,3 +94,48 @@ pub unsafe fn lv_tabview_add_tab(tabview: *mut lv_obj_t, name: *const c_char) -> page } } + +unsafe extern "C" fn radio_event_handler(event: *mut lv_event_t) { + unsafe { + let code = lvgl_rust_sys::lv_event_get_code(event); + let target = lvgl_rust_sys::lv_event_get_target(event) as *mut lv_obj_t; + let user_data = lvgl_rust_sys::lv_event_get_user_data(event) as *mut lv_obj_t; + + if code == lv_event_code_t_LV_EVENT_CLICKED { + let parent = user_data as *mut lv_obj_t; + let child_count = lvgl_rust_sys::lv_obj_get_child_count(parent); + + for i in 0..child_count { + let child = lvgl_rust_sys::lv_obj_get_child(parent, i as _); + if child != target { + lvgl_rust_sys::lv_obj_remove_state(child, lvgl_rust_sys::LV_STATE_CHECKED as _); + } + } + + lvgl_rust_sys::lv_obj_add_state(target, lvgl_rust_sys::LV_STATE_CHECKED as _); + } + } +} + +/// Create a radio button (a checkbox that behaves like a radio button) +/// +/// # Arguments +/// +/// * `parent` - The parent object of the radio button. +/// +/// # Safety +/// This function is unsafe because it may dereference raw pointers (e.g. `parent`). +pub unsafe fn lv_radiobox_create(parent: *mut lv_obj_t) -> *mut lv_obj_t { + unsafe { + let checkbox = lvgl_rust_sys::lv_checkbox_create(parent); + + lvgl_rust_sys::lv_obj_add_event_cb( + checkbox, + Some(radio_event_handler), + lvgl_rust_sys::lv_event_code_t_LV_EVENT_CLICKED, + parent as _, + ); + + checkbox + } +} From 29d3dc4080f456b1212f73ce861efbe63884e043 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:21:09 +0100 Subject: [PATCH 20/78] fix: correct syntax in synchronous_lock macro to properly handle block body --- modules/graphics/src/macros.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/graphics/src/macros.rs b/modules/graphics/src/macros.rs index 2400d767..52ba90a2 100644 --- a/modules/graphics/src/macros.rs +++ b/modules/graphics/src/macros.rs @@ -12,7 +12,7 @@ macro_rules! lock { macro_rules! synchronous_lock { ($body:block) => {{ let _lock = $crate::get_instance().synchronous_lock(); - let __result = { $($body)* }; + let __result = { $body }; ::core::mem::drop(_lock); __result }}; From fb6e4f87817a4c1287db4927effae8a734babeed Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:21:42 +0100 Subject: [PATCH 21/78] feat: add Font Awesome 7 font files and update font range in main.rs --- .../fonts/Font Awesome 7 Free-Solid-900.otf | Bin 0 -> 410592 bytes .../FontAwesome7-Solid+Brands+Regular.otf | Bin 0 -> 10160 bytes .../fonts/FontAwesome7-Solid.woff2 | Bin 0 -> 113152 bytes modules/graphics/fonts_generator/src/main.rs | 3 ++- 4 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 modules/graphics/fonts_generator/fonts/Font Awesome 7 Free-Solid-900.otf create mode 100644 modules/graphics/fonts_generator/fonts/FontAwesome7-Solid+Brands+Regular.otf create mode 100644 modules/graphics/fonts_generator/fonts/FontAwesome7-Solid.woff2 diff --git a/modules/graphics/fonts_generator/fonts/Font Awesome 7 Free-Solid-900.otf b/modules/graphics/fonts_generator/fonts/Font Awesome 7 Free-Solid-900.otf new file mode 100644 index 0000000000000000000000000000000000000000..881f5d3d79956c817ddeeee27bbd421b3640be57 GIT binary patch literal 410592 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 o=*<>9diaW9rQPEw&%AOU_CXBR~ibwBY* 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 z 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%0A;<%kV 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 literal 0 HcmV?d00001 diff --git a/modules/graphics/fonts_generator/fonts/FontAwesome7-Solid+Brands+Regular.otf b/modules/graphics/fonts_generator/fonts/FontAwesome7-Solid+Brands+Regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..184f7569f32ad87caa21e10c14f1964f5b9a4028 GIT binary patch literal 10160 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 EUtaTy&Hw-a literal 0 HcmV?d00001 diff --git a/modules/graphics/fonts_generator/fonts/FontAwesome7-Solid.woff2 b/modules/graphics/fonts_generator/fonts/FontAwesome7-Solid.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..c20c7f4feb4e9ff727e4e675b167fa4fc4400d81 GIT binary patch literal 113152 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

;chMGNtQX7BwzxTo9ZPoT_>&Uj+b8J` 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

{})IQs7PWypp>^u_v9 z*x}57xJw65ZWm)lz~m(Wa8}6jzw$uwC1dT2lz*N^M(+CitiupOx3Dz~&Dr@usd;A% zlWLAUrvt34q_L;uWf{HdJ6)w`UZEu7g@a8cJ(XwJ%gR| zyORyy%qdfP|>!T^^mP4WZ{3E*CG zedKw*Z&}8%T+^E7SvZ$z4L+2c&&7HI4&@m-{8Mc1lNld7jtUuimKRZy9+r{Lrx^}!WzwQX{>g_fOkS@BofB1eT*FmTLME23@ zq@x(52d5%-;@+>Gdq$@ibhvSt5}pyOiMq}sbp?N`9}J4}94(4h`ej0Rkf3(YRmtA^ z)%?s);w%qGZ3HN}GyGw0bv$26&QYV#3_5HR z;q-~6cfgSbIJd&<45?E%HAbflP^I8X59c{La+tzl*+mt$sd$5zE8WE`x;PS8E{`vW zRpmyPTwcSfBY1rl%u|T*Xlx9HkH}l<;v3fUWT;usNjY|W$BPQdPLHUQPeC|NKd_Tp zCVItc0yp<)J!2F7QvO;_>Jn>lM}gHVBLLOw>P+9I}z6D;zoOPx9hOZq+&NEVYD^Wzw>IQKEq%= zue2gL8UjA4Z*a9PgHs1uTd?P{E4~4y%vE~=P%`^4D1I0}O?TqZ!f!7AKox~AT?suo zaZUZQ@`W#e5C~s>q!0hW#Wce=Zg$H9<64^e8#>cq8Fh}>nu8Ryc5)a}+aL(!y8U_r zauC-jIEFp%E+MWXf5KcQSzCaaqDdlNyZ(~L#aE-gJfA$!ddipC~SPS;HQjX_=wzgF05U>tg9aZ>8Guyz^z)$whlIjHnA}qN~MKJ4h-Dg7Ny8R118vmy%Pv%Mq(+D z8^%BC*?qHH9`G&c9}VrwnRn9*)(II4D13%>`$sOx>cX3m5`6iUK~UmxFzpJrUyZ>tZ3jHY zOy${Y-ei#+Yc1^Im-dd3&e-~#3%-<*L7%2T{!m({C0Sc5vAkyILh+A&!)oW&;LRV{ z_~eN$stVqE0AA~;i{pGl745n97RLppDUwvF zH0i!nZ*S4Bt-fJj&yZ$?gSrgz?BT;a4oaX+%?c3x)kLRK9saXviFjvRVZncYaQ zdGaLzNWZ!(y<4xe?(%5Yx%py~v0WtJ$|9pXY~6%H2_@=ns1ftG(xwiQ@8HX1@n0Mf zo+dIu7DpGDbUGCk-O-_Qk}O=GVrKmH+Jy=EdAz#U=XZVWsT9kjM0W}c8Qlq)KxO6p z!a#333vO0;diRwBUgu8>)|5DjL|6;h`;ScOD4>2&L8!P+9y(D#Gg%0DJ!II7O(Spz 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 literal 0 HcmV?d00001 diff --git a/modules/graphics/fonts_generator/src/main.rs b/modules/graphics/fonts_generator/src/main.rs index eb400b4a..68c09971 100644 --- a/modules/graphics/fonts_generator/src/main.rs +++ b/modules/graphics/fonts_generator/src/main.rs @@ -17,7 +17,7 @@ pub const FONT_AWESOME_RANGE: &[u32] = &[ 61479, 61480, 61502, 61512, 61515, 61516, 61517, 61521, 61522, 61523, 61524, 61543, 61544, 61550, 61552, 61553, 61556, 61559, 61560, 61561, 61563, 61587, 61589, 61636, 61637, 61639, 61671, 61674, 61683, 61724, 61732, 61787, 61931, 62016, 62017, 62018, 62019, 62020, 62087, - 62099, 62212, 62189, 62810, 63426, 63650, + 62099, 62212, 62189, 62810, 63231, 63426, 63650, ]; pub const FONTS_DIRECTORY: &str = "fonts"; pub const GENERATED_FONTS_DIRECTORY: &str = "generated_fonts"; @@ -156,6 +156,7 @@ fn main() { let font_path = fonts_directory.join(path); let font_awesome_path = if *enable_fontawesome { Some(fonts_directory.join("FontAwesome5-Solid+Brands+Regular.woff")) + //Some(fonts_directory.join("FontAwesome7-Solid+Brands+Regular.otf")) } else { None }; From 8d62c2dbc9d2c9605e1ae7a0360897b83300a461 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:21:52 +0100 Subject: [PATCH 22/78] feat: add symbol module with LVGL constants and update lib.rs --- modules/graphics/src/lib.rs | 2 +- modules/graphics/src/symbol.rs | 86 ++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 modules/graphics/src/symbol.rs diff --git a/modules/graphics/src/lib.rs b/modules/graphics/src/lib.rs index 578d1474..7a350097 100644 --- a/modules/graphics/src/lib.rs +++ b/modules/graphics/src/lib.rs @@ -14,7 +14,7 @@ pub mod macros; mod manager; mod point; mod screen; -pub mod symbols; +pub mod symbol; mod window; pub mod lvgl; diff --git a/modules/graphics/src/symbol.rs b/modules/graphics/src/symbol.rs new file mode 100644 index 00000000..0242f0da --- /dev/null +++ b/modules/graphics/src/symbol.rs @@ -0,0 +1,86 @@ +use core::ffi::CStr; + +use crate::lvgl; + +// LVGL symbols +pub const BULLET: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BULLET) }; +pub const AUDIO: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_AUDIO) }; +pub const VIDEO: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_VIDEO) }; +pub const LIST: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_LIST) }; +pub const OK: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_OK) }; +pub const CLOSE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_CLOSE) }; +pub const POWER: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_POWER) }; +pub const SETTINGS: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_SETTINGS) }; +pub const HOME: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_HOME) }; +pub const DOWNLOAD: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_DOWNLOAD) }; +pub const DRIVE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_DRIVE) }; +pub const REFRESH: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_REFRESH) }; +pub const MUTE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_MUTE) }; +pub const VOLUME_MID: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_VOLUME_MID) }; +pub const VOLUME_MAX: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_VOLUME_MAX) }; +pub const IMAGE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_IMAGE) }; +pub const TINT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_TINT) }; +pub const PREV: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_PREV) }; +pub const PLAY: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_PLAY) }; +pub const PAUSE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_PAUSE) }; +pub const STOP: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_STOP) }; +pub const NEXT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_NEXT) }; +pub const EJECT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_EJECT) }; +pub const LEFT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_LEFT) }; +pub const RIGHT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_RIGHT) }; +pub const PLUS: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_PLUS) }; +pub const MINUS: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_MINUS) }; +pub const EYE_OPEN: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_EYE_OPEN) }; +pub const EYE_CLOSE: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_EYE_CLOSE) }; +pub const WARNING: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_WARNING) }; +pub const SHUFFLE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_SHUFFLE) }; +pub const UP: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_UP) }; +pub const DOWN: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_DOWN) }; +pub const LOOP: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_LOOP) }; +pub const DIRECTORY: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_DIRECTORY) }; +pub const UPLOAD: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_UPLOAD) }; +pub const CALL: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_CALL) }; +pub const CUT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_CUT) }; +pub const COPY: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_COPY) }; +pub const SAVE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_SAVE) }; +pub const BARS: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BARS) }; +pub const ENVELOPE: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_ENVELOPE) }; +pub const CHARGE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_CHARGE) }; +pub const PASTE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_PASTE) }; +pub const BELL: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BELL) }; +pub const KEYBOARD: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_KEYBOARD) }; +pub const GPS: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_GPS) }; +pub const FILE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_FILE) }; +pub const WIFI: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_WIFI) }; +pub const BATTERY_FULL: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BATTERY_FULL) }; +pub const BATTERY_3: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BATTERY_3) }; +pub const BATTERY_2: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BATTERY_2) }; +pub const BATTERY_1: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BATTERY_1) }; +pub const BATTERY_EMPTY: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BATTERY_EMPTY) }; +pub const USB: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_USB) }; +pub const BLUETOOTH: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BLUETOOTH) }; +pub const TRASH: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_TRASH) }; +pub const EDIT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_EDIT) }; +pub const BACKSPACE: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BACKSPACE) }; +pub const SD_CARD: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_SD_CARD) }; +pub const NEW_LINE: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_NEW_LINE) }; +pub const DUMMY: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_DUMMY) }; +// Additional symbols +pub const NETWORK_WIRED: &CStr = c"\xEF\x9B\xBF"; From 9ffc4289306e72e5abf85d52f24dbb3f53bd9438 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:22:12 +0100 Subject: [PATCH 23/78] refactor: update strip_prefix method to use for loop and improve join method signature --- modules/file_system/src/fundamentals/path/components.rs | 4 ++-- modules/file_system/src/fundamentals/path/path_reference.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/file_system/src/fundamentals/path/components.rs b/modules/file_system/src/fundamentals/path/components.rs index 1dad3e14..ccab031c 100644 --- a/modules/file_system/src/fundamentals/path/components.rs +++ b/modules/file_system/src/fundamentals/path/components.rs @@ -32,9 +32,9 @@ impl<'a> Components<'a> { pub fn strip_prefix(self, components: &Components<'a>) -> Option> { let mut self_iter = self.clone(); - let mut components_iter = components.clone(); + let components_iter = components.clone(); - while let Some(component) = components_iter.next() { + for component in components_iter { match self_iter.next() { Some(self_component) if self_component == component => {} _ => return None, diff --git a/modules/file_system/src/fundamentals/path/path_reference.rs b/modules/file_system/src/fundamentals/path/path_reference.rs index 130712d4..3c729393 100644 --- a/modules/file_system/src/fundamentals/path/path_reference.rs +++ b/modules/file_system/src/fundamentals/path/path_reference.rs @@ -189,7 +189,7 @@ impl Path { Components::new(self) } - pub fn join(&self, path: &Path) -> Option { + pub fn join(&self, path: impl AsRef) -> Option { self.to_owned().join(path) } From 62b898b66f4280dbb5d6c02be2c832597f499ffd Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:22:22 +0100 Subject: [PATCH 24/78] fix: remove unnecessary lifetime annotations from VirtualFileSystem references --- modules/executable/src/standard.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/executable/src/standard.rs b/modules/executable/src/standard.rs index 6319f514..7918568e 100644 --- a/modules/executable/src/standard.rs +++ b/modules/executable/src/standard.rs @@ -18,7 +18,7 @@ impl Standard { standard_out: &impl AsRef, standard_error: &impl AsRef, task: TaskIdentifier, - virtual_file_system: &'static VirtualFileSystem<'static>, + virtual_file_system: &'static VirtualFileSystem, ) -> Result { let standard_in = virtual_file_system .open(standard_in, AccessFlags::Read.into(), task) @@ -95,7 +95,7 @@ impl Standard { pub async fn close( self, - virtual_file_system: &VirtualFileSystem<'_>, + virtual_file_system: &VirtualFileSystem, ) -> virtual_file_system::Result<()> { self.standard_in.close(virtual_file_system).await?; self.standard_out.close(virtual_file_system).await?; From e7f9e5fd23aa9003e772c15748cbe70e5371eb36 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:23:23 +0100 Subject: [PATCH 25/78] refactor: simplify VirtualFileSystem references in async functions across authentication module --- modules/authentication/src/group.rs | 8 ++++---- modules/authentication/src/hash.rs | 4 ++-- modules/authentication/src/user.rs | 20 ++++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/modules/authentication/src/group.rs b/modules/authentication/src/group.rs index 4dbfe464..bad2a5b9 100644 --- a/modules/authentication/src/group.rs +++ b/modules/authentication/src/group.rs @@ -131,8 +131,8 @@ pub fn get_group_file_path(group_name: &str) -> Result { /// - Path construction failures /// - File system errors (opening, reading) /// - JSON parsing errors -pub async fn read_group_file<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, +pub async fn read_group_file( + virtual_file_system: &VirtualFileSystem, buffer: &mut Vec, file: &str, ) -> Result { @@ -194,8 +194,8 @@ pub async fn read_group_file<'a>( /// - Group identifier generation or assignment failures /// - File system operations (directory creation, file writing) /// - Users manager operations (adding group) -pub async fn create_group<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, +pub async fn create_group( + virtual_file_system: &VirtualFileSystem, group_name: &str, group_identifier: Option, ) -> Result { diff --git a/modules/authentication/src/hash.rs b/modules/authentication/src/hash.rs index 77477e8e..3e0a1705 100644 --- a/modules/authentication/src/hash.rs +++ b/modules/authentication/src/hash.rs @@ -44,7 +44,7 @@ use crate::{Error, RANDOM_DEVICE_PATH, Result}; /// The salt generation converts random bytes to lowercase letters (a-z) /// for readability while maintaining sufficient entropy for security. pub async fn generate_salt( - virtual_file_system: &VirtualFileSystem<'_>, + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, ) -> Result { let mut buffer = [0_u8; 16]; @@ -81,7 +81,7 @@ pub async fn generate_salt( /// to collision attacks. The salt prevents rainbow table attacks and ensures /// that identical passwords have different hashes. pub async fn hash_password( - virtual_file_system: &VirtualFileSystem<'_>, + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, password: &str, salt: &str, diff --git a/modules/authentication/src/user.rs b/modules/authentication/src/user.rs index 619893d6..8fa5ddc5 100644 --- a/modules/authentication/src/user.rs +++ b/modules/authentication/src/user.rs @@ -197,8 +197,8 @@ pub fn get_user_file_path(user_name: &str) -> Result { /// - `Failed_to_read_user_file` - I/O error reading user file /// - `Failed_to_parse_user_file` - Invalid JSON format in user file /// - `Invalid_password` - Password doesn't match stored hash -pub async fn authenticate_user<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, +pub async fn authenticate_user( + virtual_file_system: &VirtualFileSystem, user_name: &str, password: &str, ) -> Result { @@ -263,8 +263,8 @@ pub async fn authenticate_user<'a>( /// - File system operations (directory creation, file writing) /// - Users manager operations (adding user) /// - Random salt generation failures -pub async fn create_user<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, +pub async fn create_user( + virtual_file_system: &VirtualFileSystem, user_name: &str, password: &str, primary_group: GroupIdentifier, @@ -355,8 +355,8 @@ pub async fn create_user<'a>( /// - File system errors (opening, reading, writing user file) /// - Salt generation failures /// - JSON parsing errors -pub async fn change_user_password<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, +pub async fn change_user_password( + virtual_file_system: &VirtualFileSystem, user_name: &str, new_password: &str, ) -> Result<()> { @@ -419,8 +419,8 @@ pub async fn change_user_password<'a>( /// - File system errors (opening, reading, writing user file) /// - JSON parsing errors /// - Path construction failures -pub async fn change_user_name<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, +pub async fn change_user_name( + virtual_file_system: &VirtualFileSystem, current_name: &str, new_name: &str, ) -> Result<()> { @@ -479,8 +479,8 @@ pub async fn change_user_name<'a>( /// - Path construction failures /// - File system errors (opening, reading) /// - JSON parsing errors -pub async fn read_user_file<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, +pub async fn read_user_file( + virtual_file_system: &VirtualFileSystem, buffer: &mut Vec, file: &str, ) -> Result { From c87c5154f1dcaaecf9ee57ad1e2fca837a9b0e89 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:23:28 +0100 Subject: [PATCH 26/78] refactor: simplify VirtualFileSystem type in async initialize function --- .../abi/definitions/src/file_system/directory.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/modules/abi/definitions/src/file_system/directory.rs b/modules/abi/definitions/src/file_system/directory.rs index 6b54d463..84393d52 100644 --- a/modules/abi/definitions/src/file_system/directory.rs +++ b/modules/abi/definitions/src/file_system/directory.rs @@ -208,7 +208,7 @@ mod tests { use task::{TaskIdentifier, test}; use virtual_file_system::{Directory, File, VirtualFileSystem}; - async fn initialize() -> (TaskIdentifier, &'static VirtualFileSystem<'static>) { + async fn initialize() -> (TaskIdentifier, &'static VirtualFileSystem) { if !log::is_initialized() { log::initialize(&drivers_std::log::Logger).unwrap(); } @@ -228,14 +228,9 @@ mod tests { little_fs::FileSystem::format(device, cache_size).unwrap(); let file_system = little_fs::FileSystem::new(device, cache_size).unwrap(); - let virtual_file_system = virtual_file_system::initialize( - task_manager, - users_manager, - time_manager, - file_system, - None, - ) - .unwrap(); + let virtual_file_system = + virtual_file_system::initialize(task_manager, users_manager, time_manager, file_system) + .unwrap(); (task, virtual_file_system) } From 72cb5cbbaff50e24446efad57e51d996207bb628 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:23:50 +0100 Subject: [PATCH 27/78] fix: truncate file before writing in test_directory function --- modules/virtual_machine/tests/wasm_test/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/virtual_machine/tests/wasm_test/src/main.rs b/modules/virtual_machine/tests/wasm_test/src/main.rs index a5419a89..d9117c64 100644 --- a/modules/virtual_machine/tests/wasm_test/src/main.rs +++ b/modules/virtual_machine/tests/wasm_test/src/main.rs @@ -91,6 +91,7 @@ fn test_directory() { { let mut file = OpenOptions::new() .write(true) + .truncate(true) .create(true) .open("/test_dir/file1.txt") .unwrap(); From d7ad643aeb60095833be112f33056872033adfe1 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:24:36 +0100 Subject: [PATCH 28/78] refactor: remove unused network module components including IP, Port, Protocol, and SocketDriver traits --- modules/network/src/ip.rs | 171 -------------------------------- modules/network/src/protocol.rs | 6 -- modules/network/src/service.rs | 27 ----- modules/network/src/traits.rs | 37 ------- 4 files changed, 241 deletions(-) delete mode 100644 modules/network/src/ip.rs delete mode 100644 modules/network/src/protocol.rs delete mode 100644 modules/network/src/service.rs delete mode 100644 modules/network/src/traits.rs diff --git a/modules/network/src/ip.rs b/modules/network/src/ip.rs deleted file mode 100644 index 7e17f17e..00000000 --- a/modules/network/src/ip.rs +++ /dev/null @@ -1,171 +0,0 @@ -use core::fmt::Display; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[repr(transparent)] -pub struct IPv4([u8; 4]); - -impl IPv4 { - pub const LOCALHOST: Self = Self([127, 0, 0, 1]); - - pub const fn new(value: [u8; 4]) -> Self { - Self(value) - } - - pub const fn into_inner(self) -> [u8; 4] { - self.0 - } - - pub const fn from_inner(value: [u8; 4]) -> Self { - Self(value) - } -} - -impl TryFrom<&str> for IPv4 { - type Error = (); - - fn try_from(value: &str) -> Result { - let mut result = [0; 4]; - let mut index = 0; - - for part in value.split('.') { - if index >= 4 { - return Err(()); - } - let part = part.parse::().map_err(|_| ())?; - result[index] = part; - index += 1; - } - if index != 4 { - return Err(()); - } - - Ok(Self::new(result)) - } -} - -impl Display for IPv4 { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}.{}.{}.{}", self.0[0], self.0[1], self.0[2], self.0[3]) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[repr(transparent)] -pub struct IPv6([u16; 8]); - -impl IPv6 { - pub const fn new(value: [u16; 8]) -> Self { - Self(value) - } - - pub const fn into_inner(self) -> [u16; 8] { - self.0 - } - - pub const fn from_inner(value: [u16; 8]) -> Self { - Self(value) - } -} - -impl TryFrom<&str> for IPv6 { - type Error = (); - - fn try_from(value: &str) -> Result { - let mut result = [0; 8]; - let mut index = 0; - - for part in value.split(':') { - if index >= result.len() { - return Err(()); - } - - let part = u16::from_str_radix(part, 16).map_err(|_| ())?; - result[index] = part; - index += 1; - } - if index != result.len() { - return Err(()); - } - - Ok(Self::new(result)) - } -} - -impl Display for IPv6 { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!( - f, - "{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x}", - self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7] - ) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum IP { - IPv4(IPv4), - IPv6(IPv6), -} - -impl Display for IP { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - IP::IPv4(value) => write!(f, "{value}"), - IP::IPv6(value) => write!(f, "{value}"), - } - } -} - -impl From for IP { - fn from(value: IPv4) -> Self { - Self::IPv4(value) - } -} - -impl From for IP { - fn from(value: IPv6) -> Self { - Self::IPv6(value) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_ipv4_try_from() { - let ip = IPv4::try_from("0.0.0.0").unwrap(); - - assert_eq!(ip.0, [0, 0, 0, 0]); - - IPv4::try_from("1.2b.3.4").unwrap_err(); - - IPv4::try_from("1.2.3.4.5").unwrap_err(); - - IPv4::try_from("1.2.3").unwrap_err(); - - let ip = IPv4::try_from("4.3.2.1").unwrap(); - - assert_eq!(ip.0, [4, 3, 2, 1]); - } - - #[test] - fn test_ipv6_try_from() { - let ip = IPv6::try_from("0:0:0:0:0:0:0:0").unwrap(); - - assert_eq!(ip.0, [0; 8]); - - IPv6::try_from("0:0:0:0:0:0:0:0:0").unwrap_err(); - - IPv6::try_from("0:0:0:0:0:0:0").unwrap_err(); - - let ip = IPv6::try_from("1234:5678:9abc:def0:1234:5678:9abc:def0").unwrap(); - - assert_eq!( - ip.0, - [ - 0x1234, 0x5678, 0x9abc, 0xdef0, 0x1234, 0x5678, 0x9abc, 0xdef0 - ] - ); - } -} diff --git a/modules/network/src/protocol.rs b/modules/network/src/protocol.rs deleted file mode 100644 index 7e7c8237..00000000 --- a/modules/network/src/protocol.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub enum Protocol { - TCP, - UDP, - ICMP, - Local, -} diff --git a/modules/network/src/service.rs b/modules/network/src/service.rs deleted file mode 100644 index 75cecb61..00000000 --- a/modules/network/src/service.rs +++ /dev/null @@ -1,27 +0,0 @@ -use core::fmt::Display; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[repr(transparent)] -pub struct Port(u16); - -impl Port { - pub const ANY: Self = Self(0); - - pub const fn new(value: u16) -> Self { - Self(value) - } - - pub const fn into_inner(self) -> u16 { - self.0 - } - - pub const fn from_inner(value: u16) -> Self { - Self(value) - } -} - -impl Display for Port { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", self.0) - } -} diff --git a/modules/network/src/traits.rs b/modules/network/src/traits.rs deleted file mode 100644 index 3387901a..00000000 --- a/modules/network/src/traits.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::{IP, Port, Protocol}; -use file_system::Context; -use time::Duration; - -use crate::Result; - -pub trait SocketDriver: Send + Sync { - fn close(&self, context: &mut Context) -> Result<()>; - - fn bind(&self, ip: IP, port: Port, protocol: Protocol, context: &mut Context) -> Result<()>; - - fn connect(&self, ip: IP, port: Port, context: &mut Context) -> Result<()>; - - fn accept(&self, context: &mut Context) -> Result<(IP, Port)>; - - fn send(&self, context: &mut Context, data: &[u8]) -> Result<()>; - - fn send_to(&self, context: &mut Context, data: &[u8], ip: IP, port: Port) -> Result<()>; - - fn receive(&self, context: &mut Context, data: &mut [u8]) -> Result; - - fn receive_from(&self, context: &mut Context, data: &mut [u8]) -> Result<(usize, IP, Port)>; - - fn get_local_address(&self, context: &mut Context) -> Result<(IP, Port)>; - - fn get_remote_address(&self, context: &mut Context) -> Result<(IP, Port)>; - - fn set_send_timeout(&self, context: &mut Context, timeout: Duration) -> Result<()>; - - fn set_receive_timeout(&self, context: &mut Context, timeout: Duration) -> Result<()>; - - fn get_send_timeout(&self, context: &mut Context) -> Result>; - - fn get_receive_timeout(&self, context: &mut Context) -> Result>; -} - -mod tests {} From c7afb2f6b75fa76dd9e44ed6b9b803784428a3cc Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:25:44 +0100 Subject: [PATCH 29/78] feat: add Cidr, Ipv4, Ipv6, and Port structs with associated methods for IP address handling --- modules/network/src/fundamentals/cidr.rs | 103 +++++++++++++++++ modules/network/src/fundamentals/ip/ipv4.rs | 113 +++++++++++++++++++ modules/network/src/fundamentals/ip/ipv6.rs | 119 ++++++++++++++++++++ modules/network/src/fundamentals/ip/mod.rs | 119 ++++++++++++++++++++ modules/network/src/fundamentals/port.rs | 35 ++++++ 5 files changed, 489 insertions(+) create mode 100644 modules/network/src/fundamentals/cidr.rs create mode 100644 modules/network/src/fundamentals/ip/ipv4.rs create mode 100644 modules/network/src/fundamentals/ip/ipv6.rs create mode 100644 modules/network/src/fundamentals/ip/mod.rs create mode 100644 modules/network/src/fundamentals/port.rs diff --git a/modules/network/src/fundamentals/cidr.rs b/modules/network/src/fundamentals/cidr.rs new file mode 100644 index 00000000..65e46d74 --- /dev/null +++ b/modules/network/src/fundamentals/cidr.rs @@ -0,0 +1,103 @@ +use core::fmt::{Debug, Display}; + +use crate::{Ipv4, Ipv6}; + +#[repr(C)] +#[derive(Default, Clone, PartialEq, Eq)] +pub struct Cidr { + pub address: T, + pub prefix_length: u8, +} + +impl Cidr { + pub const fn new(address: T, prefix_length: u8) -> Self { + Self { + address, + prefix_length, + } + } +} + +impl Debug for Cidr { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:?}/{}", self.address, self.prefix_length) + } +} + +impl Display for Cidr { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}/{}", self.address, self.prefix_length) + } +} + +impl Cidr { + pub const fn into_smoltcp(&self) -> smoltcp::wire::Ipv4Cidr { + smoltcp::wire::Ipv4Cidr::new(self.address.into_smoltcp(), self.prefix_length) + } + + pub const fn from_smoltcp(value: &smoltcp::wire::Ipv4Cidr) -> Self { + Self { + address: Ipv4::from_smoltcp(&value.address()), + prefix_length: value.prefix_len(), + } + } +} + +impl Cidr { + pub const fn into_smoltcp(&self) -> smoltcp::wire::Ipv6Cidr { + smoltcp::wire::Ipv6Cidr::new(self.address.into_smoltcp(), self.prefix_length) + } + + pub const fn from_smoltcp(value: &smoltcp::wire::Ipv6Cidr) -> Self { + Self { + address: Ipv6::from_smoltcp(&value.address()), + prefix_length: value.prefix_len(), + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IpCidr { + IPv4(Cidr), + IPv6(Cidr), +} + +impl Default for IpCidr { + fn default() -> Self { + IpCidr::IPv4(Cidr::default()) + } +} + +impl IpCidr { + pub const fn new_ipv4(address: [u8; 4], prefix_length: u8) -> Self { + IpCidr::IPv4(Cidr::new(Ipv4::new(address), prefix_length)) + } + + pub const fn new_ipv6(address: [u16; 8], prefix_length: u8) -> Self { + IpCidr::IPv6(Cidr::new(Ipv6::new(address), prefix_length)) + } + + pub const fn into_smoltcp(&self) -> smoltcp::wire::IpCidr { + match self { + IpCidr::IPv4(cidr) => smoltcp::wire::IpCidr::Ipv4(cidr.into_smoltcp()), + IpCidr::IPv6(cidr) => smoltcp::wire::IpCidr::Ipv6(cidr.into_smoltcp()), + } + } + + pub const fn from_smoltcp(value: &smoltcp::wire::IpCidr) -> Self { + match value { + smoltcp::wire::IpCidr::Ipv4(cidr) => IpCidr::IPv4(Cidr::::from_smoltcp(cidr)), + smoltcp::wire::IpCidr::Ipv6(cidr) => IpCidr::IPv6(Cidr::::from_smoltcp(cidr)), + } + } +} + +impl Display for IpCidr { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + IpCidr::IPv4(cidr) => write!(f, "{cidr}"), + IpCidr::IPv6(cidr) => write!(f, "{cidr}"), + } + } +} diff --git a/modules/network/src/fundamentals/ip/ipv4.rs b/modules/network/src/fundamentals/ip/ipv4.rs new file mode 100644 index 00000000..cde9f9f8 --- /dev/null +++ b/modules/network/src/fundamentals/ip/ipv4.rs @@ -0,0 +1,113 @@ +use core::fmt::Display; + +use crate::Ipv6; + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[repr(transparent)] +pub struct Ipv4([u8; 4]); + +impl Ipv4 { + pub const LOCALHOST: Self = Self([127, 0, 0, 1]); + pub const BROADCAST: Self = Self([255, 255, 255, 255]); + + pub const fn new(value: [u8; 4]) -> Self { + Self(value) + } + + pub const fn into_inner(self) -> [u8; 4] { + self.0 + } + + pub const fn from_inner(value: [u8; 4]) -> Self { + Self(value) + } + + pub const fn is_multicast(&self) -> bool { + self.0[0] >= 224 && self.0[0] <= 239 + } + + pub const fn is_broadcast(&self) -> bool { + u32::from_be_bytes(self.0) == u32::from_be_bytes(Self::BROADCAST.0) + } + + pub const fn to_ipv6_mapped(&self) -> Ipv6 { + Ipv6::new([ + 0, + 0, + 0, + 0, + 0, + 0xFFFF, + u16::from_be_bytes([self.0[0], self.0[1]]), + u16::from_be_bytes([self.0[2], self.0[3]]), + ]) + } + + pub const fn into_smoltcp(self) -> core::net::Ipv4Addr { + core::net::Ipv4Addr::new(self.0[0], self.0[1], self.0[2], self.0[3]) + } + + pub const fn from_smoltcp(value: &core::net::Ipv4Addr) -> Self { + Self(value.octets()) + } +} + +impl TryFrom<&str> for Ipv4 { + type Error = (); + + fn try_from(value: &str) -> Result { + let mut result = [0; 4]; + let mut index = 0; + + for part in value.split('.') { + if index >= 4 { + return Err(()); + } + let part = part.parse::().map_err(|_| ())?; + result[index] = part; + index += 1; + } + if index != 4 { + return Err(()); + } + + Ok(Self::new(result)) + } +} + +impl Display for Ipv4 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}.{}.{}.{}", self.0[0], self.0[1], self.0[2], self.0[3]) + } +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use super::*; + + #[test] + fn test_ipv4_display() { + let ip = Ipv4::new([192, 168, 1, 1]); + + assert_eq!(ip.to_string(), "192.168.1.1"); + } + + #[test] + fn test_ipv4_try_from() { + let ip = Ipv4::try_from("0.0.0.0").unwrap(); + + assert_eq!(ip.0, [0, 0, 0, 0]); + + Ipv4::try_from("1.2b.3.4").unwrap_err(); + + Ipv4::try_from("1.2.3.4.5").unwrap_err(); + + Ipv4::try_from("1.2.3").unwrap_err(); + + let ip = Ipv4::try_from("4.3.2.1").unwrap(); + + assert_eq!(ip.0, [4, 3, 2, 1]); + } +} diff --git a/modules/network/src/fundamentals/ip/ipv6.rs b/modules/network/src/fundamentals/ip/ipv6.rs new file mode 100644 index 00000000..53af63b7 --- /dev/null +++ b/modules/network/src/fundamentals/ip/ipv6.rs @@ -0,0 +1,119 @@ +use core::fmt::Display; + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[repr(transparent)] +pub struct Ipv6([u8; 16]); // Avoid 2 byte alignment issues + +impl Ipv6 { + pub const fn new(value: [u16; 8]) -> Self { + let mut bytes = [0; 16]; + let mut i = 0; + while i < 8 { + let segment = value[i].to_be_bytes(); + bytes[i * 2] = segment[0]; + bytes[i * 2 + 1] = segment[1]; + i += 1; + } + Self(bytes) + } + + pub const fn into_inner(self) -> [u8; 16] { + self.0 + } + + pub const fn from_inner(value: [u8; 16]) -> Self { + Self(value) + } + + pub const fn into_smoltcp(self) -> core::net::Ipv6Addr { + core::net::Ipv6Addr::from_octets(self.0) + } + + pub const fn from_smoltcp(value: &core::net::Ipv6Addr) -> Self { + Self(value.octets()) + } +} + +impl TryFrom<&str> for Ipv6 { + type Error = (); + + fn try_from(value: &str) -> Result { + let mut result = [0; 8]; + let mut index = 0; + + for part in value.split(':') { + if index >= 8 { + return Err(()); + } + let part = u16::from_str_radix(part, 16).map_err(|_| ())?; + result[index] = part; + index += 1; + } + + if index != 8 { + return Err(()); + } + + Ok(Self::new(result)) + } +} + +impl Display for Ipv6 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "{:x}{:x}:{:x}{:x}:{:x}{:x}:{:x}{:x}:{:x}{:x}:{:x}{:x}:{:x}{:x}:{:x}{:x}", + self.0[0], + self.0[1], + self.0[2], + self.0[3], + self.0[4], + self.0[5], + self.0[6], + self.0[7], + self.0[8], + self.0[9], + self.0[10], + self.0[11], + self.0[12], + self.0[13], + self.0[14], + self.0[15] + ) + } +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use super::*; + + #[test] + fn test_ipv6_display() { + let ip = Ipv6::new([0; 8]); + + assert_eq!(ip.to_string(), "00:00:00:00:00:00:00:00"); + } + + #[test] + fn test_ipv6_try_from() { + let ip = Ipv6::try_from("0:0:0:0:0:0:0:0").unwrap(); + + assert_eq!(ip.0, [0; 16]); + + Ipv6::try_from("0:0:0:0:0:0:0:0:0").unwrap_err(); + + Ipv6::try_from("0:0:0:0:0:0:0").unwrap_err(); + + let ip = Ipv6::try_from("1234:5678:9abc:def0:1234:5678:9abc:def0").unwrap(); + + assert_eq!( + ip.0, + [ + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, + 0xde, 0xf0 + ] + ); + } +} diff --git a/modules/network/src/fundamentals/ip/mod.rs b/modules/network/src/fundamentals/ip/mod.rs new file mode 100644 index 00000000..8dd9f6d4 --- /dev/null +++ b/modules/network/src/fundamentals/ip/mod.rs @@ -0,0 +1,119 @@ +mod ipv4; +mod ipv6; + +use core::fmt::Display; + +pub use ipv4::*; +pub use ipv6::*; + +#[repr(C)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum IpAddress { + IPv4(Ipv4), + IPv6(Ipv6), +} + +impl Default for IpAddress { + fn default() -> Self { + IpAddress::IPv4(Ipv4::default()) + } +} + +impl IpAddress { + pub const fn new_ipv4(value: [u8; 4]) -> Self { + Self::IPv4(Ipv4::new(value)) + } + + pub const fn new_ipv6(value: [u16; 8]) -> Self { + Self::IPv6(Ipv6::new(value)) + } + + pub const fn into_smoltcp(&self) -> smoltcp::wire::IpAddress { + match self { + IpAddress::IPv4(value) => smoltcp::wire::IpAddress::Ipv4(value.into_smoltcp()), + IpAddress::IPv6(value) => smoltcp::wire::IpAddress::Ipv6(value.into_smoltcp()), + } + } + + pub const fn from_smoltcp(value: &smoltcp::wire::IpAddress) -> Self { + match value { + smoltcp::wire::IpAddress::Ipv4(v4_addr) => { + IpAddress::IPv4(crate::Ipv4::from_smoltcp(v4_addr)) + } + smoltcp::wire::IpAddress::Ipv6(v6_addr) => { + IpAddress::IPv6(crate::Ipv6::from_smoltcp(v6_addr)) + } + } + } +} + +impl TryFrom<&str> for IpAddress { + type Error = (); + + fn try_from(value: &str) -> Result { + if let Ok(ipv4) = Ipv4::try_from(value) { + Ok(IpAddress::IPv4(ipv4)) + } else if let Ok(ipv6) = Ipv6::try_from(value) { + Ok(IpAddress::IPv6(ipv6)) + } else { + Err(()) + } + } +} + +impl From<[u8; 4]> for IpAddress { + fn from(value: [u8; 4]) -> Self { + IpAddress::IPv4(Ipv4::new(value)) + } +} + +impl From<&[u8; 4]> for IpAddress { + fn from(value: &[u8; 4]) -> Self { + IpAddress::IPv4(Ipv4::new(*value)) + } +} + +impl From<[u8; 16]> for IpAddress { + fn from(value: [u8; 16]) -> Self { + IpAddress::IPv6(Ipv6::from_inner(value)) + } +} + +impl From<&[u8; 16]> for IpAddress { + fn from(value: &[u8; 16]) -> Self { + IpAddress::IPv6(Ipv6::from_inner(*value)) + } +} + +impl From for smoltcp::wire::IpAddress { + fn from(value: IpAddress) -> Self { + value.into_smoltcp() + } +} + +impl From for IpAddress { + fn from(value: smoltcp::wire::IpAddress) -> Self { + IpAddress::from_smoltcp(&value) + } +} + +impl Display for IpAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + IpAddress::IPv4(value) => write!(f, "{value}"), + IpAddress::IPv6(value) => write!(f, "{value}"), + } + } +} + +impl From for IpAddress { + fn from(value: Ipv4) -> Self { + Self::IPv4(value) + } +} + +impl From for IpAddress { + fn from(value: Ipv6) -> Self { + Self::IPv6(value) + } +} diff --git a/modules/network/src/fundamentals/port.rs b/modules/network/src/fundamentals/port.rs new file mode 100644 index 00000000..827909eb --- /dev/null +++ b/modules/network/src/fundamentals/port.rs @@ -0,0 +1,35 @@ +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Port(u16); + +impl Port { + pub const MINIMUM_USER: Self = Self(1025); + pub const MAXIMUM: Self = Self(u16::MAX); + + pub const DHCP_SERVER: Self = Self(67); + pub const DHCP_CLIENT: Self = Self(68); + + pub const fn new(value: u16) -> Self { + Self(value) + } + + pub const fn into_inner(self) -> u16 { + self.0 + } + + pub const fn from_inner(value: u16) -> Self { + Self(value) + } +} + +impl From for Port { + fn from(value: u16) -> Self { + Self::new(value) + } +} + +impl From for u16 { + fn from(value: Port) -> Self { + value.into_inner() + } +} From baa43d201ee02ea0a8df988812fa9e89afbb73e7 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:28:02 +0100 Subject: [PATCH 30/78] feat: add InterfaceKind enum for network interface types --- modules/network/src/device/command.rs | 203 +++++++++++++++++++++++ modules/network/src/fundamentals/kind.rs | 8 + 2 files changed, 211 insertions(+) create mode 100644 modules/network/src/device/command.rs create mode 100644 modules/network/src/fundamentals/kind.rs diff --git a/modules/network/src/device/command.rs b/modules/network/src/device/command.rs new file mode 100644 index 00000000..38df74ff --- /dev/null +++ b/modules/network/src/device/command.rs @@ -0,0 +1,203 @@ +use crate::{InterfaceKind, IpAddress, IpCidr, MacAddress, Route}; +use file_system::{ControlCommand, define_command}; + +#[repr(C)] +pub struct WifiClientConfiguration { + // TODO: Add fields +} + +define_command!(GET_KIND, Read, b'n', 1, (), InterfaceKind); + +#[repr(u8)] +enum CommandNumber { + SetState, + GetState, + GetHardwareAddress, + SetHardwareAddress, + GetMtu, + GetMaximumBurstSize, + IsLinkUp, + GetRouteCount, + GetRoute, + AddRoute, + RemoveRoute, + GetDnsServerCount, + GetDnsServer, + AddDnsServer, + RemoveDnsServer, + SetDhcpState, + GetDhcpState, + GetIpAddressCount, + GetIpAddress, + AddIpAddress, + RemoveIpAddress, +} + +define_command!( + SET_STATE, + Write, + b'n', + CommandNumber::SetState as u8, + bool, + () +); +define_command!( + GET_STATE, + Read, + b'n', + CommandNumber::GetState as u8, + (), + bool +); +define_command!( + GET_HARDWARE_ADDRESS, + Read, + b'n', + CommandNumber::GetHardwareAddress as u8, + (), + MacAddress +); +define_command!( + SET_HARDWARE_ADDRESS, + Write, + b'n', + CommandNumber::SetHardwareAddress as u8, + MacAddress, + () +); +define_command!( + GET_MAXIMUM_TRANSMISSION_UNIT, + Read, + b'n', + CommandNumber::GetMtu as u8, + (), + usize +); +define_command!( + GET_MAXIMUM_BURST_SIZE, + Read, + b'n', + CommandNumber::GetMaximumBurstSize as u8, + (), + Option +); +define_command!( + IS_LINK_UP, + Read, + b'n', + CommandNumber::IsLinkUp as u8, + (), + bool +); +define_command!( + GET_ROUTE_COUNT, + Read, + b'n', + CommandNumber::GetRouteCount as u8, + (), + usize +); +define_command!( + GET_ROUTE, + Read, + b'n', + CommandNumber::GetRoute as u8, + usize, + Route +); +define_command!( + ADD_ROUTE, + Write, + b'n', + CommandNumber::AddRoute as u8, + Route, + () +); +define_command!( + REMOVE_ROUTE, + Write, + b'n', + CommandNumber::RemoveRoute as u8, + usize, + () +); +define_command!( + GET_DNS_SERVER_COUNT, + Read, + b'n', + CommandNumber::GetDnsServerCount as u8, + (), + usize +); +define_command!( + GET_DNS_SERVER, + Read, + b'n', + CommandNumber::GetDnsServer as u8, + usize, + IpAddress +); +define_command!( + ADD_DNS_SERVER, + Write, + b'n', + CommandNumber::AddDnsServer as u8, + IpAddress, + () +); +define_command!( + REMOVE_DNS_SERVER, + Write, + b'n', + CommandNumber::RemoveDnsServer as u8, + usize, + () +); +define_command!( + SET_DHCP_STATE, + Write, + b'n', + CommandNumber::SetDhcpState as u8, + bool, + () +); +define_command!( + GET_DHCP_STATE, + Read, + b'n', + CommandNumber::GetDhcpState as u8, + (), + bool +); +define_command!( + GET_IP_ADDRESS_COUNT, + Read, + b'n', + CommandNumber::GetIpAddressCount as u8, + (), + usize +); +define_command!( + GET_IP_ADDRESS, + Read, + b'n', + CommandNumber::GetIpAddress as u8, + usize, + IpCidr +); +define_command!( + ADD_IP_ADDRESS, + Write, + b'n', + CommandNumber::AddIpAddress as u8, + IpCidr, + () +); +define_command!( + REMOVE_IP_ADDRESS, + Write, + b'n', + CommandNumber::RemoveIpAddress as u8, + usize, + () +); diff --git a/modules/network/src/fundamentals/kind.rs b/modules/network/src/fundamentals/kind.rs new file mode 100644 index 00000000..143b854f --- /dev/null +++ b/modules/network/src/fundamentals/kind.rs @@ -0,0 +1,8 @@ +#[repr(u8)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum InterfaceKind { + #[default] + Unknown, + Ethernet, + WiFi, +} From e0740e502a6556a844d7d5563a6490401002c1a8 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:28:09 +0100 Subject: [PATCH 31/78] feat: implement LoopbackControllerDevice for network operations --- modules/network/src/device/loopback.rs | 44 ++++++++++++++++++++++++++ modules/network/src/device/mod.rs | 5 +++ 2 files changed, 49 insertions(+) create mode 100644 modules/network/src/device/loopback.rs create mode 100644 modules/network/src/device/mod.rs diff --git a/modules/network/src/device/loopback.rs b/modules/network/src/device/loopback.rs new file mode 100644 index 00000000..7577abdb --- /dev/null +++ b/modules/network/src/device/loopback.rs @@ -0,0 +1,44 @@ +use crate::{GET_KIND, InterfaceKind}; +use file_system::{ + ControlCommand, ControlCommandIdentifier, DirectBaseOperations, DirectCharacterDevice, Error, + MountOperations, +}; +use shared::AnyByLayout; +pub use smoltcp::phy::Loopback; +use smoltcp::phy::Medium; + +pub struct LoopbackControllerDevice; + +impl DirectBaseOperations for LoopbackControllerDevice { + fn read(&self, _: &mut [u8], _: file_system::Size) -> file_system::Result { + Err(Error::UnsupportedOperation) + } + + fn write(&self, _: &[u8], _: file_system::Size) -> file_system::Result { + Err(Error::UnsupportedOperation) + } + + fn control( + &self, + command: ControlCommandIdentifier, + _: &AnyByLayout, + output: &mut AnyByLayout, + ) -> file_system::Result<()> { + match command { + GET_KIND::IDENTIFIER => { + let kind = GET_KIND::cast_output(output)?; + *kind = InterfaceKind::Ethernet; + Ok(()) + } + _ => Err(Error::UnsupportedOperation), + } + } +} + +impl MountOperations for LoopbackControllerDevice {} + +impl DirectCharacterDevice for LoopbackControllerDevice {} + +pub fn create_loopback_device() -> (Loopback, LoopbackControllerDevice) { + (Loopback::new(Medium::Ethernet), LoopbackControllerDevice) +} diff --git a/modules/network/src/device/mod.rs b/modules/network/src/device/mod.rs new file mode 100644 index 00000000..fd4d27dc --- /dev/null +++ b/modules/network/src/device/mod.rs @@ -0,0 +1,5 @@ +mod command; +mod loopback; + +pub use command::*; +pub use loopback::*; From 10001e9d0fd2c74240c6faf74c00110495563ef4 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:28:16 +0100 Subject: [PATCH 32/78] feat: add Duration struct with conversion methods for time handling --- modules/network/src/fundamentals/duration.rs | 80 ++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 modules/network/src/fundamentals/duration.rs diff --git a/modules/network/src/fundamentals/duration.rs b/modules/network/src/fundamentals/duration.rs new file mode 100644 index 00000000..e27395ac --- /dev/null +++ b/modules/network/src/fundamentals/duration.rs @@ -0,0 +1,80 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +pub struct Duration(u64); + +impl Duration { + pub const MINIMUM: Self = Self(u64::MIN); + pub const MAXIMUM: Self = Self(u64::MAX); + + pub const fn from_seconds(seconds: u64) -> Self { + Self(embassy_time::Duration::from_secs(seconds).as_ticks()) + } + + pub const fn from_milliseconds(milliseconds: u64) -> Self { + Self(embassy_time::Duration::from_millis(milliseconds).as_ticks()) + } + + pub const fn from_microseconds(microseconds: u64) -> Self { + Self(embassy_time::Duration::from_micros(microseconds).as_ticks()) + } + + pub const fn from_nanoseconds(nanoseconds: u64) -> Self { + Self(embassy_time::Duration::from_nanos(nanoseconds).as_ticks()) + } + + pub const fn as_ticks(&self) -> u64 { + self.0 + } + + pub const fn as_seconds(&self) -> u64 { + embassy_time::Duration::from_ticks(self.0).as_secs() + } + + pub const fn as_milliseconds(&self) -> u64 { + embassy_time::Duration::from_ticks(self.0).as_millis() + } + + pub const fn as_microseconds(&self) -> u64 { + embassy_time::Duration::from_ticks(self.0).as_micros() + } + + pub const fn into_smoltcp(self) -> smoltcp::time::Duration { + smoltcp::time::Duration::from_micros(self.as_microseconds()) + } + + pub const fn into_embassy(self) -> embassy_time::Duration { + embassy_time::Duration::from_ticks(self.0) + } + + pub const fn from_embassy(value: embassy_time::Duration) -> Self { + Self(value.as_ticks()) + } + + pub const fn from_smoltcp(value: smoltcp::time::Duration) -> Self { + Self::from_microseconds(value.micros()) + } +} + +impl From for Duration { + fn from(value: embassy_time::Duration) -> Self { + Self::from_embassy(value) + } +} + +impl From for embassy_time::Duration { + fn from(value: Duration) -> Self { + value.into_embassy() + } +} + +impl From for Duration { + fn from(value: core::time::Duration) -> Self { + Self::from_microseconds(value.as_micros() as u64) + } +} + +impl From for core::time::Duration { + fn from(value: Duration) -> Self { + core::time::Duration::from_micros(value.as_microseconds()) + } +} From 74b3a9c019bc2b16954c6d337acd668ea67b32a8 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:28:27 +0100 Subject: [PATCH 33/78] feat: add IpEndpoint and IpListenEndpoint structs with conversion methods for smoltcp integration --- modules/network/src/fundamentals/endpoint.rs | 67 +++++++++++++++++++ modules/network/src/fundamentals/route.rs | 70 ++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 modules/network/src/fundamentals/endpoint.rs create mode 100644 modules/network/src/fundamentals/route.rs diff --git a/modules/network/src/fundamentals/endpoint.rs b/modules/network/src/fundamentals/endpoint.rs new file mode 100644 index 00000000..b15596bd --- /dev/null +++ b/modules/network/src/fundamentals/endpoint.rs @@ -0,0 +1,67 @@ +use smoltcp::wire; + +use crate::{IpAddress, Port}; + +#[repr(C)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct IpEndpoint { + pub address: IpAddress, + pub port: Port, +} + +impl IpEndpoint { + pub const fn new(address: IpAddress, port: Port) -> Self { + Self { address, port } + } + + pub const fn into_smoltcp(&self) -> wire::IpEndpoint { + wire::IpEndpoint { + addr: self.address.into_smoltcp(), + port: self.port.into_inner(), + } + } + + pub const fn from_smoltcp(value: &wire::IpEndpoint) -> Self { + Self { + address: IpAddress::from_smoltcp(&value.addr), + port: Port::from_inner(value.port), + } + } +} + +#[repr(C)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct IpListenEndpoint { + pub address: Option, + pub port: Port, +} + +impl IpListenEndpoint { + pub const fn new(address: Option, port: Port) -> Self { + Self { address, port } + } + + pub const fn into_smoltcp(&self) -> wire::IpListenEndpoint { + let smoltcp_address = match &self.address { + Some(ip) => Some(ip.into_smoltcp()), + None => None, + }; + + wire::IpListenEndpoint { + addr: smoltcp_address, + port: self.port.into_inner(), + } + } + + pub const fn from_smoltcp(value: &wire::IpListenEndpoint) -> Self { + let address = match &value.addr { + Some(ip) => Some(IpAddress::from_smoltcp(ip)), + None => None, + }; + + Self { + address, + port: Port::from_inner(value.port), + } + } +} diff --git a/modules/network/src/fundamentals/route.rs b/modules/network/src/fundamentals/route.rs new file mode 100644 index 00000000..ff17116f --- /dev/null +++ b/modules/network/src/fundamentals/route.rs @@ -0,0 +1,70 @@ +use core::time::Duration; + +use crate::{IpAddress, IpCidr}; + +#[repr(C)] +#[derive(Default, Clone, PartialEq, Eq)] +pub struct Route { + pub cidr: IpCidr, + pub via_router: IpAddress, + pub preferred_until: Option, + pub expires_at: Option, +} + +impl Route { + pub const fn new_default_ipv4(via_router: [u8; 4]) -> Self { + Self { + cidr: IpCidr::new_ipv4([0, 0, 0, 0], 0), + via_router: IpAddress::new_ipv4(via_router), + preferred_until: None, + expires_at: None, + } + } + + pub const fn new_default_ipv6(via_router: [u16; 8]) -> Self { + Self { + cidr: IpCidr::new_ipv6([0, 0, 0, 0, 0, 0, 0, 0], 0), + via_router: IpAddress::new_ipv6(via_router), + preferred_until: None, + expires_at: None, + } + } + + pub const fn from_smoltcp(route: smoltcp::iface::Route) -> Self { + let preferred_until = if let Some(dur) = route.preferred_until { + Some(Duration::from_micros(dur.micros() as u64)) + } else { + None + }; + + let expires_at = if let Some(dur) = route.expires_at { + Some(Duration::from_micros(dur.micros() as u64)) + } else { + None + }; + + Self { + cidr: IpCidr::from_smoltcp(&route.cidr), + via_router: IpAddress::from_smoltcp(&route.via_router), + preferred_until, + expires_at, + } + } + + pub fn into_smoltcp(&self) -> smoltcp::iface::Route { + let preferred_until = self + .preferred_until + .map(|dur| smoltcp::time::Instant::from_micros(dur.as_micros() as i64)); + + let expires_at = self + .expires_at + .map(|dur| smoltcp::time::Instant::from_micros(dur.as_micros() as i64)); + + smoltcp::iface::Route { + cidr: self.cidr.into_smoltcp(), + via_router: self.via_router.into_smoltcp(), + preferred_until, + expires_at, + } + } +} From b2dba636f47cf503cfa3a96b2ad5590ce98b75a3 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:28:37 +0100 Subject: [PATCH 34/78] feat: add DnsQueryKind enum for DNS query types --- modules/network/src/fundamentals/dns.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 modules/network/src/fundamentals/dns.rs diff --git a/modules/network/src/fundamentals/dns.rs b/modules/network/src/fundamentals/dns.rs new file mode 100644 index 00000000..ae11a100 --- /dev/null +++ b/modules/network/src/fundamentals/dns.rs @@ -0,0 +1,11 @@ +use shared::flags; + +flags! { + pub enum DnsQueryKind: u8 { + A, + Aaaa, + Cname, + Ns, + Soa, + } +} From 74e54c2a89c03b247ce731b46a0f4560a9de94e0 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:28:41 +0100 Subject: [PATCH 35/78] feat: implement UdpMetadata struct for UDP metadata handling --- modules/network/src/fundamentals/udp.rs | 58 +++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 modules/network/src/fundamentals/udp.rs diff --git a/modules/network/src/fundamentals/udp.rs b/modules/network/src/fundamentals/udp.rs new file mode 100644 index 00000000..aa632a7b --- /dev/null +++ b/modules/network/src/fundamentals/udp.rs @@ -0,0 +1,58 @@ +use smoltcp::{phy::PacketMeta, socket::udp, wire}; + +use crate::{IpAddress, Port}; + +#[repr(C)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UdpMetadata { + pub remote_address: IpAddress, + pub local_address: Option, + pub remote_port: Port, + pub meta: PacketMeta, +} + +impl UdpMetadata { + pub fn new( + remote_address: impl Into, + remote_port: impl Into, + local_address: Option, + meta: PacketMeta, + ) -> Self { + Self { + remote_address: remote_address.into(), + local_address, + remote_port: remote_port.into(), + meta, + } + } + + pub const fn from_smoltcp(value: &udp::UdpMetadata) -> Self { + let local_address = match value.local_address { + Some(addr) => Some(IpAddress::from_smoltcp(&addr)), + None => None, + }; + + Self { + remote_address: IpAddress::from_smoltcp(&value.endpoint.addr), + local_address, + remote_port: Port::from_inner(value.endpoint.port), + meta: value.meta, + } + } + + pub const fn to_smoltcp(&self) -> udp::UdpMetadata { + let local_address = match &self.local_address { + Some(addr) => Some(addr.into_smoltcp()), + None => None, + }; + + udp::UdpMetadata { + endpoint: wire::IpEndpoint::new( + self.remote_address.into_smoltcp(), + self.remote_port.into_inner(), + ), + local_address, + meta: self.meta, + } + } +} From 4f570c56b40783ff3e4d544374b4075a5fba1c3c Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:28:46 +0100 Subject: [PATCH 36/78] feat: create mod.rs for network fundamentals module structure --- modules/network/src/fundamentals/mod.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 modules/network/src/fundamentals/mod.rs diff --git a/modules/network/src/fundamentals/mod.rs b/modules/network/src/fundamentals/mod.rs new file mode 100644 index 00000000..ace6afe8 --- /dev/null +++ b/modules/network/src/fundamentals/mod.rs @@ -0,0 +1,21 @@ +mod cidr; +mod dns; +mod duration; +mod endpoint; +mod ip; +mod kind; +mod port; +mod route; +mod udp; + +pub use cidr::*; +pub use dns::*; +pub use duration::*; +pub use endpoint::*; +pub use ip::*; +pub use kind::*; +pub use port::*; +pub use route::*; +pub use udp::*; + +pub type MacAddress = [u8; 6]; From df890e54d16b0f3f153b6c57c5b098ae7823faf9 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:29:05 +0100 Subject: [PATCH 37/78] feat: implement Stack and StackInner structs for smoltcp integration --- modules/network/src/manager/stack.rs | 383 +++++++++++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 modules/network/src/manager/stack.rs diff --git a/modules/network/src/manager/stack.rs b/modules/network/src/manager/stack.rs new file mode 100644 index 00000000..8e01b59c --- /dev/null +++ b/modules/network/src/manager/stack.rs @@ -0,0 +1,383 @@ +use crate::{ + Error, IpAddress, IpCidr, Ipv4, Ipv6, MacAddress, Port, Result, Route, WakeSignal, + get_smoltcp_time, +}; +use alloc::{boxed::Box, vec, vec::Vec}; +use core::{ + task::{Context, Poll}, + time::Duration, +}; +use file_system::DirectCharacterDevice; +use shared::poll_pin_ready; +use smol_str::SmolStr; +use smoltcp::{ + config::{DNS_MAX_SERVER_COUNT, IFACE_MAX_ADDR_COUNT}, + iface::{self, SocketSet}, + phy::{Device, Medium}, + socket::{AnySocket, Socket, dhcpv4}, + wire::{self, EthernetAddress}, +}; +use synchronization::{Arc, blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex}; + +pub struct StackInner { + pub name: SmolStr, + pub is_up: bool, + pub enabled: bool, + pub interface: smoltcp::iface::Interface, + pub controller: Box, + pub sockets: smoltcp::iface::SocketSet<'static>, + pub dhcp_socket: Option, + pub dns_servers: Vec, + pub maximum_transmission_unit: usize, + pub maximum_burst_size: Option, + pub next_local_port: Port, +} + +#[derive(Clone)] +pub struct Stack { + inner: Arc>, + wake_signal: WakeSignal, +} + +impl Stack { + pub fn new(inner: StackInner, wake_signal: WakeSignal) -> Self { + Stack { + inner: Arc::new(Mutex::new(inner)), + wake_signal, + } + } + + pub fn wake_up(&self) -> &WakeSignal { + &self.wake_signal + } + + /// Try to lock the inner stack without blocking. Returns None if the lock is held. + pub fn try_lock( + &self, + ) -> Option> { + self.inner.try_lock().ok() + } + + /// Lock the inner stack asynchronously. + pub async fn lock( + &self, + ) -> synchronization::mutex::MutexGuard<'_, CriticalSectionRawMutex, StackInner> { + self.inner.lock().await + } + + /// Signal the runner to wake up (call after modifying the stack outside of with_mutable). + pub fn wake_runner(&self) { + self.wake_signal.signal(()); + } + + pub async fn with R>(&self, f: F) -> R { + let stack = self.inner.lock().await; + f(&stack) + } + + pub async fn with_mutable R>(&self, f: F) -> R { + let mut stack = self.inner.lock().await; + let r = f(&mut stack); + // Wake the runner via signal + self.wake_signal.signal(()); + r + } + + pub async fn with_mutable_no_wake R>(&self, f: F) -> R { + let mut stack = self.inner.lock().await; + f(&mut stack) + } + + pub fn poll_with) -> Poll>( + &self, + context: &mut Context<'_>, + f: F, + ) -> Poll { + let stack = poll_pin_ready!(self.inner.lock(), context); + f(&stack, context) + } + + fn poll_with_mutable_impl) -> Poll>( + &self, + context: &mut core::task::Context<'_>, + f: F, + wake: bool, + ) -> Poll { + let mut stack = poll_pin_ready!(self.inner.lock(), context); + + let r = f(&mut stack, context); + if wake { + // Wake the runner via signal + self.wake_signal.signal(()); + } + r + } + + pub fn poll_with_mutable) -> Poll>( + &self, + context: &mut core::task::Context<'_>, + f: F, + ) -> Poll { + self.poll_with_mutable_impl(context, f, true) + } +} + +impl StackInner { + pub fn new( + name: impl AsRef, + device: &mut (impl Device + 'static), + controller_device: impl DirectCharacterDevice + 'static, + random_seed: u64, + now: smoltcp::time::Instant, + ) -> Self { + let capabilities = device.capabilities(); + + let mut config = match capabilities.medium { + Medium::Ethernet => { + iface::Config::new(EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]).into()) + } + Medium::Ip => iface::Config::new(smoltcp::wire::HardwareAddress::Ip), + Medium::Ieee802154 => todo!(), + }; + config.random_seed = random_seed; + + let interface = smoltcp::iface::Interface::new(config, device, now); + + let sockets = SocketSet::new(vec![]); + + let next_local_port = (random_seed + % (Port::MAXIMUM.into_inner() - Port::MINIMUM_USER.into_inner()) as u64) + as u16 + + Port::MINIMUM_USER.into_inner(); + + StackInner { + name: name.as_ref().into(), + is_up: true, + enabled: true, + interface, + controller: Box::new(controller_device), + sockets, + dhcp_socket: None, + dns_servers: Vec::with_capacity(DNS_MAX_SERVER_COUNT), + maximum_transmission_unit: capabilities.max_transmission_unit, + maximum_burst_size: capabilities.max_burst_size, + next_local_port: Port::from_inner(next_local_port), + } + } + + pub fn is_available(&self) -> bool { + self.enabled && self.is_up + } + + pub fn is_link_up(&self) -> bool { + self.is_up + } + + pub fn get_state(&self) -> bool { + self.enabled + } + + pub fn set_state(&mut self, enabled: bool) { + self.enabled = enabled; + } + + pub fn get_route(&mut self, index: usize) -> Option { + let mut route = None; + + self.interface.routes_mut().update(|v| { + if let Some(r) = v.get(index) { + route = Some(*r); + } + }); + + route.map(Route::from_smoltcp) + } + + pub fn get_route_count(&mut self) -> usize { + let mut count = 0; + + self.interface.routes_mut().update(|v| count = v.len()); + + count + } + + pub fn add_route(&mut self, route: Route) -> Result<()> { + let mut result = Ok(()); + + self.interface + .routes_mut() + .update(|v| result = v.push(route.into_smoltcp()).map_err(|_| Error::NoFreeSlot)); + + result + } + + pub fn remove_route(&mut self, index: usize) -> Option { + let mut route = None; + + self.interface.routes_mut().update(|v| { + if index < v.len() { + route = Some(v.remove(index)); + } + }); + + route.map(Route::from_smoltcp) + } + + pub fn get_dns_servers_count(&self) -> usize { + self.dns_servers.len() + } + + pub fn get_dns_server(&self, index: usize) -> Option { + self.dns_servers.get(index).map(IpAddress::from_smoltcp) + } + + pub fn add_dns_server(&mut self, server: IpAddress) -> Result<()> { + if self.dns_servers.len() >= DNS_MAX_SERVER_COUNT { + return Err(Error::NoFreeSlot); + } + + self.dns_servers.push(server.into_smoltcp()); + Ok(()) + } + + pub fn remove_dns_server(&mut self, index: usize) -> Option { + if index >= self.dns_servers.len() { + return None; + } + + let server = self.dns_servers.remove(index); + Some(IpAddress::from_smoltcp(&server)) + } + + pub fn get_dns_servers(&self) -> &[wire::IpAddress] { + &self.dns_servers + } + + pub fn set_dhcp_state(&mut self, enabled: bool) { + if enabled { + let dhcp_socket = dhcpv4::Socket::new(); + let handle = self.sockets.add(dhcp_socket); + self.dhcp_socket = Some(handle); + } else { + self.dhcp_socket.take(); + } + } + + pub fn get_dhcp_state(&self) -> bool { + self.dhcp_socket.is_some() + } + + pub fn get_ip_addresses_count(&self) -> usize { + self.interface.ip_addrs().len() + } + + pub fn get_ip_address(&self, index: usize) -> Option { + self.interface + .ip_addrs() + .get(index) + .map(IpCidr::from_smoltcp) + } + + pub fn add_ip_address(&mut self, cidr: IpCidr) -> Result<()> { + if self.get_ip_addresses_count() >= IFACE_MAX_ADDR_COUNT { + return Err(Error::NoFreeSlot); + } + + let mut result = Ok(()); + + self.interface.update_ip_addrs(|addrs| { + result = addrs + .push(cidr.into_smoltcp()) + .map_err(|_| Error::NoFreeSlot); + }); + + result + } + + pub fn remove_ip_address(&mut self, index: usize) -> Option { + let mut cidr = None; + + self.interface.update_ip_addrs(|addrs| { + if index < addrs.len() { + cidr = Some(addrs.remove(index)); + } + }); + + cidr.as_ref().map(IpCidr::from_smoltcp) + } + + pub fn set_hardware_address(&mut self, address: &MacAddress) { + self.interface + .set_hardware_addr(smoltcp::wire::HardwareAddress::Ethernet( + smoltcp::wire::EthernetAddress(*address), + )); + } + + pub fn get_hardware_address(&self) -> Option { + match self.interface.hardware_addr() { + smoltcp::wire::HardwareAddress::Ethernet(addr) => Some(MacAddress::from(addr.0)), + _ => None, + } + } + + pub fn get_maximum_transmission_unit(&self) -> usize { + self.maximum_transmission_unit + } + + pub fn get_maximum_burst_size(&self) -> Option { + self.maximum_burst_size + } + + pub fn add_socket(&mut self, socket: impl AnySocket<'static>) -> smoltcp::iface::SocketHandle { + self.sockets.add(socket) + } + + pub fn remove_socket<'a>(&'a mut self, handle: smoltcp::iface::SocketHandle) -> Socket<'a> { + self.sockets.remove(handle) + } + + pub fn get_source_ip_v6_address(&mut self, remote: Ipv6) -> Ipv6 { + let ip = self + .interface + .get_source_address_ipv6(&remote.into_smoltcp()); + + Ipv6::from_smoltcp(&ip) + } + + pub fn get_source_ip_v4_address(&mut self, remote: Ipv4) -> Option { + self.interface + .get_source_address_ipv4(&remote.into_smoltcp()) + .map(|ip| Ipv4::from_smoltcp(&ip)) + } + + pub fn poll(&mut self, device: &mut impl Device) -> Option { + let now = get_smoltcp_time(); + + self.interface.poll(now, device, &mut self.sockets); + + let poll_at = self.interface.poll_at(now, &self.sockets); + + poll_at + .map(|instant| instant - now) + .map(|smoltcp_duration| Duration::from_micros(smoltcp_duration.total_micros())) + } + + pub fn get_socket>( + &mut self, + handle: smoltcp::iface::SocketHandle, + ) -> &mut S { + self.sockets.get_mut::(handle) + } + + pub fn get_next_port(&mut self) -> Port { + let port = self.next_local_port; + + self.next_local_port = if port.into_inner() == Port::MAXIMUM.into_inner() { + Port::MINIMUM_USER + } else { + Port::from_inner(port.into_inner() + 1) + }; + + port + } +} From ceab0a03cf3331303c96776116e331b9bec292cc Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:29:11 +0100 Subject: [PATCH 38/78] feat: implement StackRunner struct for managing smoltcp stack operations --- modules/network/src/manager/runner.rs | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 modules/network/src/manager/runner.rs diff --git a/modules/network/src/manager/runner.rs b/modules/network/src/manager/runner.rs new file mode 100644 index 00000000..bc216eb8 --- /dev/null +++ b/modules/network/src/manager/runner.rs @@ -0,0 +1,53 @@ +use crate::manager::stack::Stack; +use core::time::Duration; +use embassy_futures::select::select; +use smoltcp::phy::Device; +use synchronization::{Arc, blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal}; + +/// A signal used to wake the runner when socket activity occurs. +pub type WakeSignal = Arc>; + +pub struct StackRunner { + stack: Stack, + device: T, + wake_signal: WakeSignal, +} + +impl StackRunner +where + T: Device, +{ + pub fn new(stack: Stack, device: T, wake_signal: WakeSignal) -> Self { + Self { + stack, + device, + wake_signal, + } + } + + pub async fn run(&mut self) -> ! { + loop { + let next_poll_in = self + .stack + .with_mutable_no_wake(|stack_inner| { + if stack_inner.enabled { + stack_inner.poll(&mut self.device) + } else { + None + } + }) + .await; + + let sleep_duration = match next_poll_in { + Some(d) if d.is_zero() => { + embassy_futures::yield_now().await; + continue; + } + Some(d) => d, + None => Duration::from_millis(200), + }; + + select(task::sleep(sleep_duration), self.wake_signal.wait()).await; + } + } +} From 1458138664998245620d0ebdb4162cd41c103112 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:29:17 +0100 Subject: [PATCH 39/78] feat: add SocketContext struct for managing socket operations in smoltcp --- modules/network/src/manager/context.rs | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 modules/network/src/manager/context.rs diff --git a/modules/network/src/manager/context.rs b/modules/network/src/manager/context.rs new file mode 100644 index 00000000..34bb8dca --- /dev/null +++ b/modules/network/src/manager/context.rs @@ -0,0 +1,63 @@ +use core::task::{Context, Poll}; + +use crate::manager::stack::Stack; +use smoltcp::iface::SocketHandle; + +pub struct SocketContext { + pub handle: SocketHandle, + pub stack: Stack, + pub closed: bool, +} + +impl SocketContext { + pub async fn with(&self, f: F) -> R + where + F: FnOnce(&S) -> R, + S: smoltcp::socket::AnySocket<'static>, + { + self.stack + .with(|stack_inner| { + let socket = stack_inner.sockets.get::(self.handle); + f(socket) + }) + .await + } + + pub async fn with_mutable(&self, f: F) -> R + where + F: FnOnce(&mut S) -> R, + S: smoltcp::socket::AnySocket<'static>, + { + self.stack + .with_mutable(|stack_inner| { + let socket = stack_inner.sockets.get_mut::(self.handle); + f(socket) + }) + .await + } + + pub fn poll_with(&self, context: &mut Context<'_>, f: F) -> Poll + where + F: FnOnce(&S, &mut Context<'_>) -> Poll, + S: smoltcp::socket::AnySocket<'static>, + { + self.stack.poll_with(context, |stack_inner, context| { + let socket = stack_inner.sockets.get::(self.handle); + + f(socket, context) + }) + } + + pub fn poll_with_mutable(&self, context: &mut core::task::Context<'_>, f: F) -> Poll + where + F: FnOnce(&mut S, &mut Context<'_>) -> Poll, + S: smoltcp::socket::AnySocket<'static>, + { + self.stack + .poll_with_mutable(context, |stack_inner, context| { + let socket = stack_inner.sockets.get_mut::(self.handle); + + f(socket, context) + }) + } +} From f7d8b720f3133053942cdf307311c8fd432f84c2 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:29:23 +0100 Subject: [PATCH 40/78] feat: implement NetworkDevice struct with control operations for network management --- modules/network/src/manager/device.rs | 163 ++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 modules/network/src/manager/device.rs diff --git a/modules/network/src/manager/device.rs b/modules/network/src/manager/device.rs new file mode 100644 index 00000000..4d0a4155 --- /dev/null +++ b/modules/network/src/manager/device.rs @@ -0,0 +1,163 @@ +use crate::{ + ADD_DNS_SERVER, ADD_IP_ADDRESS, ADD_ROUTE, GET_DHCP_STATE, GET_DNS_SERVER, + GET_DNS_SERVER_COUNT, GET_HARDWARE_ADDRESS, GET_IP_ADDRESS, GET_IP_ADDRESS_COUNT, + GET_MAXIMUM_BURST_SIZE, GET_MAXIMUM_TRANSMISSION_UNIT, GET_ROUTE, GET_ROUTE_COUNT, GET_STATE, + IS_LINK_UP, IpAddress, IpCidr, MacAddress, REMOVE_DNS_SERVER, REMOVE_IP_ADDRESS, REMOVE_ROUTE, + Route, SET_DHCP_STATE, SET_HARDWARE_ADDRESS, SET_STATE, + manager::stack::{Stack, StackInner}, +}; +use file_system::{ + ControlCommand, ControlCommandIdentifier, DirectBaseOperations, DirectCharacterDevice, Error, + MountOperations, Result, +}; +use shared::AnyByLayout; + +#[repr(transparent)] +pub struct NetworkDevice { + stack: Stack, +} + +impl NetworkDevice { + pub fn with R>(&self, f: F) -> Result { + let stack = self.stack.try_lock().ok_or(Error::RessourceBusy)?; + + Ok(f(&stack)) + } + + pub fn with_mut R>(&self, f: F) -> Result { + let mut stack = self.stack.try_lock().ok_or(Error::RessourceBusy)?; + + Ok(f(&mut stack)) + } +} + +impl NetworkDevice { + pub fn new(stack: Stack) -> NetworkDevice { + NetworkDevice { stack } + } +} + +impl DirectBaseOperations for NetworkDevice { + fn read(&self, _: &mut [u8], _: file_system::Size) -> file_system::Result { + Err(Error::UnsupportedOperation) + } + + fn write(&self, _: &[u8], _: file_system::Size) -> file_system::Result { + Err(Error::UnsupportedOperation) + } + + fn control( + &self, + command: ControlCommandIdentifier, + input: &AnyByLayout, + output: &mut AnyByLayout, + ) -> Result<()> { + match command { + SET_STATE::IDENTIFIER => { + let state: &bool = SET_STATE::cast_input(input)?; + self.with_mut(|s| s.set_state(*state))?; + } + GET_STATE::IDENTIFIER => { + let state: &mut bool = GET_STATE::cast_output(output)?; + *state = self.with(|s| s.get_state())?; + } + IS_LINK_UP::IDENTIFIER => { + let is_up: &mut bool = IS_LINK_UP::cast_output(output)?; + *is_up = self.with(|s| s.is_link_up())?; + } + GET_HARDWARE_ADDRESS::IDENTIFIER => { + let hardware_address: &mut MacAddress = GET_HARDWARE_ADDRESS::cast_output(output)?; + let address = self.with(|s| s.interface.hardware_addr())?; + hardware_address.copy_from_slice(address.as_bytes()); + } + SET_HARDWARE_ADDRESS::IDENTIFIER => { + let hardware_address: &MacAddress = SET_HARDWARE_ADDRESS::cast_input(input)?; + self.with_mut(|s| s.set_hardware_address(hardware_address))?; + } + GET_MAXIMUM_TRANSMISSION_UNIT::IDENTIFIER => { + let mtu: &mut usize = GET_MAXIMUM_TRANSMISSION_UNIT::cast_output(output)?; + *mtu = self.with(|s| s.get_maximum_transmission_unit())?; + } + GET_MAXIMUM_BURST_SIZE::IDENTIFIER => { + let maximum_burst_size: &mut Option = + GET_MAXIMUM_BURST_SIZE::cast_output(output)?; + *maximum_burst_size = self.with(|s| s.get_maximum_burst_size())?; + } + GET_IP_ADDRESS_COUNT::IDENTIFIER => { + let ip_address_count: &mut usize = GET_IP_ADDRESS_COUNT::cast_output(output)?; + *ip_address_count = self.with(|s| s.get_ip_addresses_count())?; + } + GET_IP_ADDRESS::IDENTIFIER => { + let (input, output) = GET_IP_ADDRESS::cast(input, output)?; + *output = self + .with(|s| s.get_ip_address(*input))? + .ok_or(Error::InvalidParameter)?; + } + ADD_IP_ADDRESS::IDENTIFIER => { + let indexed: &IpCidr = ADD_IP_ADDRESS::cast_input(input)?; + self.with_mut(|s| s.add_ip_address(indexed.clone()))? + .map_err(|_| Error::InternalError)?; + } + REMOVE_IP_ADDRESS::IDENTIFIER => { + let ip_address_index: &usize = REMOVE_IP_ADDRESS::cast_input(input)?; + self.with_mut(|s| s.remove_ip_address(*ip_address_index))?; + } + GET_ROUTE_COUNT::IDENTIFIER => { + let route_count: &mut usize = GET_ROUTE_COUNT::cast_output(output)?; + *route_count = self.with_mut(|s| s.get_route_count())?; + } + GET_ROUTE::IDENTIFIER => { + let (input, output) = GET_ROUTE::cast(input, output)?; + *output = self + .with_mut(|s| s.get_route(*input))? + .ok_or(Error::InvalidParameter)?; + } + ADD_ROUTE::IDENTIFIER => { + let route_index: &Route = ADD_ROUTE::cast_input(input)?; + self.with_mut(|s| s.add_route(route_index.clone()))? + .map_err(|_| Error::InternalError)?; + } + REMOVE_ROUTE::IDENTIFIER => { + let route_index: &usize = REMOVE_ROUTE::cast_input(input)?; + self.with_mut(|s| s.remove_route(*route_index))?; + } + GET_DNS_SERVER_COUNT::IDENTIFIER => { + let dns_server_count: &mut usize = GET_DNS_SERVER_COUNT::cast_output(output)?; + *dns_server_count = self.with_mut(|s| s.get_dns_servers_count())?; + } + GET_DNS_SERVER::IDENTIFIER => { + let (input, output) = GET_DNS_SERVER::cast(input, output)?; + + *output = self + .with_mut(|s| s.get_dns_server(*input))? + .ok_or(Error::InvalidParameter)?; + } + ADD_DNS_SERVER::IDENTIFIER => { + let dns_server: &IpAddress = ADD_DNS_SERVER::cast_input(input)?; + self.with_mut(|s| s.add_dns_server(dns_server.clone()))? + .map_err(|_| Error::InternalError)?; + } + REMOVE_DNS_SERVER::IDENTIFIER => { + let index: &usize = REMOVE_DNS_SERVER::cast_input(input)?; + self.with_mut(|s| s.remove_dns_server(*index))?; + } + SET_DHCP_STATE::IDENTIFIER => { + let dhcp_enabled: &bool = SET_DHCP_STATE::cast_input(input)?; + self.with_mut(|s| s.set_dhcp_state(*dhcp_enabled))?; + } + GET_DHCP_STATE::IDENTIFIER => { + let dhcp_enabled: &mut bool = GET_DHCP_STATE::cast_output(output)?; + *dhcp_enabled = self.with(|s| s.get_dhcp_state())?; + } + command => { + self.with(|s| s.controller.control(command, input, output))??; + } + } + + Ok(()) + } +} + +impl MountOperations for NetworkDevice {} + +impl DirectCharacterDevice for NetworkDevice {} From 960bea42924508b09e348767a5d677358fdd5a73 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:29:33 +0100 Subject: [PATCH 41/78] feat: implement IcmpSocket and IcmpEndpoint for ICMP socket operations --- modules/network/src/socket/icmp.rs | 380 +++++++++++++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 modules/network/src/socket/icmp.rs diff --git a/modules/network/src/socket/icmp.rs b/modules/network/src/socket/icmp.rs new file mode 100644 index 00000000..9444ec44 --- /dev/null +++ b/modules/network/src/socket/icmp.rs @@ -0,0 +1,380 @@ +use crate::{Duration, Error, IpAddress, Port, Result, SocketContext}; +use alloc::vec; +use core::{ + future::poll_fn, + task::{Context, Poll}, +}; +use smoltcp::{socket::icmp, wire}; + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub enum IcmpEndpoint { + #[default] + Unspecified, + Identifier(u16), + Udp((Option, Port)), +} + +impl IcmpEndpoint { + pub const fn into_smoltcp(&self) -> icmp::Endpoint { + match self { + IcmpEndpoint::Unspecified => icmp::Endpoint::Unspecified, + IcmpEndpoint::Identifier(id) => icmp::Endpoint::Ident(*id), + IcmpEndpoint::Udp((addr_opt, port)) => { + let addr = match addr_opt { + Some(a) => Some(a.into_smoltcp()), + None => None, + }; + + let endpoint = wire::IpListenEndpoint { + addr, + port: port.into_inner(), + }; + + icmp::Endpoint::Udp(endpoint) + } + } + } +} + +pub struct IcmpSocket { + context: SocketContext, +} + +impl IcmpSocket { + pub(crate) fn new(context: SocketContext) -> Self { + Self { context } + } + + pub async fn with(&self, f: F) -> R + where + F: FnOnce(&icmp::Socket<'static>) -> R, + { + self.context.with(f).await + } + + pub async fn with_mutable(&self, f: F) -> R + where + F: FnOnce(&mut icmp::Socket<'static>) -> R, + { + self.context.with_mutable(f).await + } + + pub fn poll_with(&self, context: &mut Context<'_>, f: F) -> Poll + where + F: FnOnce(&icmp::Socket, &mut Context<'_>) -> Poll, + { + self.context.poll_with(context, f) + } + + pub fn poll_with_mutable(&self, context: &mut Context<'_>, f: F) -> Poll + where + F: FnOnce(&mut icmp::Socket, &mut Context<'_>) -> Poll, + { + self.context.poll_with_mutable(context, f) + } + + fn poll_send_to( + &self, + context: &mut Context<'_>, + buffer: &[u8], + remote_endpoint: &IpAddress, + ) -> Poll> { + self.poll_with_mutable(context, |socket, context| { + let send_capacity_too_small = socket.payload_send_capacity() < buffer.len(); + if send_capacity_too_small { + return Poll::Ready(Err(Error::PacketTooLarge)); + } + + let remote_endpoint = remote_endpoint.into_smoltcp(); + + match socket.send_slice(buffer, remote_endpoint) { + Ok(()) => Poll::Ready(Ok(())), + Err(icmp::SendError::BufferFull) => { + socket.register_send_waker(context.waker()); + Poll::Pending + } + Err(icmp::SendError::Unaddressable) => { + if socket.is_open() { + Poll::Ready(Err(Error::NoRoute)) + } else { + Poll::Ready(Err(Error::SocketNotBound)) + } + } + } + }) + } + + fn poll_receive_from( + &self, + context: &mut Context<'_>, + buffer: &mut [u8], + ) -> Poll> { + self.poll_with_mutable(context, |socket, context| match socket.recv_slice(buffer) { + Ok((size, remote_endpoint)) => { + let remote_endpoint = IpAddress::from_smoltcp(&remote_endpoint); + Poll::Ready(Ok((size, remote_endpoint))) + } + Err(icmp::RecvError::Truncated) => Poll::Ready(Err(Error::Truncated)), + Err(icmp::RecvError::Exhausted) => { + socket.register_recv_waker(context.waker()); + + self.context.stack.wake_runner(); + Poll::Pending + } + }) + } + + pub async fn bind(&self, endpoint: IcmpEndpoint) -> Result<()> { + let endpoint = endpoint.into_smoltcp(); + + self.with_mutable(|socket: &mut icmp::Socket| socket.bind(endpoint)) + .await?; + Ok(()) + } + + pub async fn can_write(&self) -> bool { + self.with(icmp::Socket::can_send).await + } + + pub async fn can_read(&self) -> bool { + self.with(icmp::Socket::can_recv).await + } + + pub async fn write_to(&self, buffer: &[u8], endpoint: impl Into) -> Result<()> { + let address: IpAddress = endpoint.into(); + + poll_fn(|context| self.poll_send_to(context, buffer, &address)).await + } + + pub async fn read_from(&self, buffer: &mut [u8]) -> Result<(usize, IpAddress)> { + poll_fn(|context| self.poll_receive_from(context, buffer)).await + } + + pub async fn read_from_with_timeout( + &self, + buffer: &mut [u8], + timeout: impl Into, + ) -> Result<(usize, IpAddress)> { + use embassy_futures::select::{Either, select}; + + let receive = poll_fn(|context| self.poll_receive_from(context, buffer)); + let sleep = task::sleep(timeout.into()); + + match select(receive, sleep).await { + Either::First(result) => result, + Either::Second(_) => Err(Error::TimedOut), + } + } + + /// Sends an ICMP echo request (ping) to the specified remote address and waits for a reply. + /// Returns the round-trip time if successful. + /// + /// # Errors + /// + /// Returns an error if the ping request fails or times out. + pub async fn ping( + &self, + remote_address: &IpAddress, + sequence_number: u16, + identifier: u16, + timeout: Duration, + payload_size: usize, + ) -> Result { + use wire::{Icmpv4Packet, Icmpv4Repr, Icmpv6Packet, Icmpv6Repr}; + + let mut echo_payload = vec![0u8; payload_size]; + let start_time = crate::get_smoltcp_time(); + + let timestamp_millis = start_time.total_millis() as u64; + echo_payload[0..8].copy_from_slice(×tamp_millis.to_be_bytes()); + + let mut stack_lock = self.context.stack.lock().await; + + let src_addr_v6 = if let IpAddress::IPv6(v6_addr) = remote_address { + Some( + stack_lock + .interface + .get_source_address_ipv6(&v6_addr.into_smoltcp()), + ) + } else { + None + }; + + let socket = stack_lock + .sockets + .get_mut::(self.context.handle); + + let remote_endpoint = remote_address.into_smoltcp(); + + let checksum_caps = smoltcp::phy::ChecksumCapabilities::default(); + + match remote_address { + IpAddress::IPv4(_) => { + let icmp_repr = Icmpv4Repr::EchoRequest { + ident: identifier, + seq_no: sequence_number, + data: &echo_payload, + }; + + let icmp_payload = socket + .send(icmp_repr.buffer_len(), remote_endpoint) + .map_err(|e| match e { + icmp::SendError::BufferFull => Error::ResourceBusy, + icmp::SendError::Unaddressable => Error::NoRoute, + })?; + + let mut icmp_packet = Icmpv4Packet::new_unchecked(icmp_payload); + icmp_repr.emit(&mut icmp_packet, &checksum_caps); + } + IpAddress::IPv6(v6_addr) => { + let icmp_repr = Icmpv6Repr::EchoRequest { + ident: identifier, + seq_no: sequence_number, + data: &echo_payload, + }; + + let icmp_payload = socket + .send(icmp_repr.buffer_len(), remote_endpoint) + .map_err(|e| match e { + icmp::SendError::BufferFull => Error::ResourceBusy, + icmp::SendError::Unaddressable => Error::NoRoute, + })?; + + let src_addr = src_addr_v6.unwrap(); + let mut icmp_packet = Icmpv6Packet::new_unchecked(icmp_payload); + icmp_repr.emit( + &src_addr, + &v6_addr.into_smoltcp(), + &mut icmp_packet, + &checksum_caps, + ); + } + } + + drop(stack_lock); + + self.context.stack.wake_runner(); + + let timeout_end = start_time + timeout.into_smoltcp(); + + loop { + let now = crate::get_smoltcp_time(); + if now >= timeout_end { + return Err(Error::TimedOut); + } + + let mut recv_buffer = [0u8; 256]; + let result = self.read_from_with_timeout(&mut recv_buffer, timeout).await; + + match result { + Ok((size, addr)) if addr == *remote_address => { + // Parse the received packet + let is_valid_reply = match remote_address { + IpAddress::IPv4(_) => { + if let Ok(packet) = Icmpv4Packet::new_checked(&recv_buffer[..size]) { + if let Ok(repr) = Icmpv4Repr::parse(&packet, &checksum_caps) { + matches!( + repr, + Icmpv4Repr::EchoReply { + ident: id, + seq_no, + .. + } if id == identifier && seq_no == sequence_number + ) + } else { + false + } + } else { + false + } + } + IpAddress::IPv6(v6_addr) => { + if let Ok(packet) = Icmpv6Packet::new_checked(&recv_buffer[..size]) { + let src_addr = self + .context + .stack + .with_mutable(|s| s.get_source_ip_v6_address(*v6_addr)) + .await; + + if let Ok(repr) = Icmpv6Repr::parse( + &v6_addr.into_smoltcp(), + &src_addr.into_smoltcp(), + &packet, + &checksum_caps, + ) { + matches!( + repr, + Icmpv6Repr::EchoReply { + ident: id, + seq_no, + .. + } if id == identifier && seq_no == sequence_number + ) + } else { + false + } + } else { + false + } + } + }; + + if is_valid_reply { + let end_time = crate::get_smoltcp_time(); + let rtt = end_time - start_time; + return Ok(Duration::from_milliseconds(rtt.total_millis() as u64)); + } + } + Ok(_) => { + continue; + } + Err(e) => return Err(e), + } + } + } + + pub async fn flush(&self) -> () { + poll_fn(|context| { + self.poll_with_mutable(context, |socket, context| { + if socket.send_queue() == 0 { + Poll::Ready(()) + } else { + socket.register_send_waker(context.waker()); + Poll::Pending + } + }) + }) + .await + } + + pub async fn is_open(&self) -> bool { + self.with(icmp::Socket::is_open).await + } + + pub async fn get_packet_read_capacity(&self) -> usize { + self.with(icmp::Socket::packet_recv_capacity).await + } + + pub async fn get_packet_write_capacity(&self) -> usize { + self.with(icmp::Socket::packet_send_capacity).await + } + + pub async fn get_payload_read_capacity(&self) -> usize { + self.with(icmp::Socket::payload_recv_capacity).await + } + + pub async fn get_payload_write_capacity(&self) -> usize { + self.with(icmp::Socket::payload_send_capacity).await + } + + pub async fn get_hop_limit(&self) -> Option { + self.with(icmp::Socket::hop_limit).await + } + + pub async fn set_hop_limit(&self, hop_limit: Option) -> () { + self.with_mutable(|socket: &mut icmp::Socket| { + socket.set_hop_limit(hop_limit); + }) + .await + } +} From 5fc6e513dd86581587177f2dfe397dc3d5765ad2 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:29:39 +0100 Subject: [PATCH 42/78] feat: implement UdpSocket for UDP socket operations --- modules/network/src/socket/udp.rs | 369 ++++++++++++++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 modules/network/src/socket/udp.rs diff --git a/modules/network/src/socket/udp.rs b/modules/network/src/socket/udp.rs new file mode 100644 index 00000000..735c18ae --- /dev/null +++ b/modules/network/src/socket/udp.rs @@ -0,0 +1,369 @@ +use core::{ + future::poll_fn, + task::{Context, Poll}, +}; + +use embassy_futures::block_on; +use smoltcp::socket::udp; + +use crate::{Error, IpAddress, Port, Result, SocketContext, UdpMetadata}; + +pub struct UdpSocket { + context: SocketContext, +} + +impl UdpSocket { + pub(crate) fn new(context: SocketContext) -> Self { + Self { context } + } + + pub async fn with(&self, f: F) -> R + where + F: FnOnce(&udp::Socket<'static>) -> R, + { + self.context.with(f).await + } + + pub async fn with_mutable(&self, f: F) -> R + where + F: FnOnce(&mut udp::Socket<'static>) -> R, + { + self.context.with_mutable(f).await + } + + pub fn poll_with(&self, context: &mut Context<'_>, f: F) -> Poll + where + F: FnOnce(&udp::Socket, &mut Context<'_>) -> Poll, + { + self.context.poll_with(context, f) + } + + pub fn poll_with_mutable(&self, context: &mut Context<'_>, f: F) -> Poll + where + F: FnOnce(&mut udp::Socket, &mut Context<'_>) -> Poll, + { + self.context.poll_with_mutable(context, f) + } + + pub async fn bind(&mut self, port: Port) -> Result<()> { + let port = port.into_inner(); + + self.with_mutable(|socket| socket.bind(port)).await?; + + Ok(()) + } + + pub fn poll_send_to( + &mut self, + context: &mut Context<'_>, + buffer: &[u8], + metadata: &UdpMetadata, + ) -> Poll> { + log::debug!("UDP poll_send_to: sending {} bytes", buffer.len()); + self.poll_with_mutable(context, |socket, cx| { + let capacity = socket.payload_send_capacity(); + log::debug!("UDP send capacity: {}, needed: {}", capacity, buffer.len()); + + if capacity < buffer.len() { + log::warning!( + "UDP send buffer too small: capacity={}, needed={}", + capacity, + buffer.len() + ); + return Poll::Ready(Err(Error::PacketTooLarge)); + } + + let metadata = metadata.to_smoltcp(); + + match socket.send_slice(buffer, metadata) { + Ok(()) => { + log::debug!("UDP send_slice succeeded"); + Poll::Ready(Ok(())) + } + Err(udp::SendError::BufferFull) => { + log::information!("UDP send buffer full, registering waker"); + socket.register_send_waker(cx.waker()); + Poll::Pending + } + Err(udp::SendError::Unaddressable) => { + if socket.endpoint().port == 0 { + log::error!("UDP send failed: socket not bound"); + Poll::Ready(Err(Error::SocketNotBound)) + } else { + log::error!("UDP send failed: no route"); + Poll::Ready(Err(Error::NoRoute)) + } + } + } + }) + } + + pub fn poll_receive_from( + &self, + context: &mut Context<'_>, + buffer: &mut [u8], + ) -> Poll> { + log::debug!("UDP poll_receive_from: buffer size={}", buffer.len()); + self.poll_with_mutable(context, |socket, cx| { + log::debug!( + "UDP recv: checking socket for data (can_recv={}, buffered={})", + socket.can_recv(), + socket.recv_queue() + ); + match socket.recv_slice(buffer) { + Ok((n, meta)) => { + log::information!("UDP received {} bytes", n); + Poll::Ready(Ok((n, UdpMetadata::from_smoltcp(&meta)))) + } + Err(udp::RecvError::Truncated) => { + log::warning!("UDP receive truncated"); + Poll::Ready(Err(Error::Truncated)) + } + Err(udp::RecvError::Exhausted) => { + log::information!( + "UDP receive buffer exhausted (can_recv={}, buffered={})", + socket.can_recv(), + socket.recv_queue() + ); + socket.register_recv_waker(cx.waker()); + log::information!("UDP receive waker registered"); + Poll::Pending + } + } + }) + } + + pub async fn write_to(&mut self, buffer: &[u8], metadata: &UdpMetadata) -> Result<()> { + log::debug!( + "UDP write_to: starting async send of {} bytes", + buffer.len() + ); + let result = poll_fn(|cx| self.poll_send_to(cx, buffer, metadata)).await; + log::debug!("UDP write_to: completed with result {:?}", result.is_ok()); + result + } + + pub async fn read_from(&self, buffer: &mut [u8]) -> Result<(usize, UdpMetadata)> { + poll_fn(|cx| { + log::information!("Polling UDP read"); + let r = self.poll_receive_from(cx, buffer); + log::information!("UDP read poll completed"); + r + }) + .await + } + + pub fn flush(&mut self) -> impl Future + '_ { + poll_fn(|cx| { + self.poll_with_mutable(cx, |socket, cx| { + if socket.can_send() { + Poll::Ready(()) + } else { + socket.register_send_waker(cx.waker()); + Poll::Pending + } + }) + }) + } + + pub async fn close(mut self) { + self.context.closed = true; + self.with_mutable(|s| { + log::information!("Closing UDP socket : {:?}", s.endpoint()); + udp::Socket::close(s); + log::information!("UDP socket closed"); + }) + .await; + } + + pub async fn get_endpoint(&self) -> Result<(Option, Port)> { + let endpoint = self.with(udp::Socket::endpoint).await; + + let ip_address = endpoint.addr.as_ref().map(IpAddress::from_smoltcp); + let port = Port::from_inner(endpoint.port); + + Ok((ip_address, port)) + } + + pub async fn get_packet_read_capacity(&self) -> usize { + self.with(udp::Socket::packet_recv_capacity).await + } + + pub async fn get_packet_write_capacity(&self) -> usize { + self.with(udp::Socket::packet_send_capacity).await + } + + pub async fn get_payload_read_capacity(&self) -> usize { + self.with(udp::Socket::payload_recv_capacity).await + } + + pub async fn get_payload_write_capacity(&self) -> usize { + self.with(udp::Socket::payload_send_capacity).await + } + + pub async fn set_hop_limit(&mut self, hop_limit: Option) { + self.with_mutable(|socket| socket.set_hop_limit(hop_limit)) + .await + } +} + +impl Drop for UdpSocket { + fn drop(&mut self) { + if self.context.closed { + return; + } + log::warning!("UDP socket dropped without being closed. Forcing closure."); + block_on(self.with_mutable(udp::Socket::close)); + } +} + +#[cfg(test)] +mod tests { + + extern crate std; + + use crate::tests::initialize; + + use super::*; + use smoltcp::phy::PacketMeta; + + #[task::test] + async fn test_udp_bind() { + let network_manager = initialize().await; + + let mut socket = network_manager + .new_udp_socket(1024, 1024, 10, 10, None) + .await + .expect("Failed to create UDP socket"); + + let port = Port::from_inner(10001); + let result = socket.bind(port).await; + + assert!(result.is_ok(), "Failed to bind UDP socket"); + + let (_ip, bound_port) = socket.get_endpoint().await.expect("Failed to get endpoint"); + assert_eq!(bound_port, port, "Port mismatch"); + + socket.close().await; + } + + #[task::test] + async fn test_udp_send_receive() { + let network_manager = initialize().await; + + // Create sender socket + let mut sender = network_manager + .new_udp_socket(1024, 1024, 10, 10, None) + .await + .expect("Failed to create sender socket"); + + // Create receiver socket + let mut receiver = network_manager + .new_udp_socket(65535, 65535, 10, 10, None) + .await + .expect("Failed to create receiver socket"); + + // Bind receiver to a specific port + let receiver_port = Port::from_inner(10003); + receiver + .bind(receiver_port) + .await + .expect("Failed to bind receiver"); + + // Prepare test data + let test_data = b"Hello, UDP!"; + + let remote_ip: IpAddress = [127, 0, 0, 1].into(); + + let metadata = UdpMetadata::new(remote_ip, receiver_port, None, PacketMeta::default()); + + sender + .bind(Port::from_inner(10002)) + .await + .expect("Failed to bind sender"); + + log::information!("Sending data"); + + // Send data + let send_result = sender.write_to(test_data, &metadata).await; + assert_eq!(send_result, Ok(())); + + log::information!("Data sent, waiting to receive..."); + + // Receive data + let mut buffer = [0u8; 1024]; + let receive_result = receiver.read_from(&mut buffer).await; + + log::information!("Data received"); + + if let Ok((size, _recv_metadata)) = receive_result { + assert_eq!(size, test_data.len(), "Received data size mismatch"); + assert_eq!(&buffer[..size], test_data, "Received data mismatch"); + } + + sender.close().await; + receiver.close().await; + } + + #[task::test] + async fn test_udp_endpoint() { + let network_manager = initialize().await; + + let mut socket = network_manager + .new_udp_socket(1024, 1024, 10, 10, None) + .await + .expect("Failed to create UDP socket"); + + // Before binding, endpoint should have port 0 + let (_ip, port) = socket + .get_endpoint() + .await + .expect("Failed to get initial endpoint"); + assert_eq!(port.into_inner(), 0, "Initial port should be 0"); + + // After binding, endpoint should have the bound port + let bind_port = Port::from_inner(10004); + socket.bind(bind_port).await.expect("Failed to bind"); + + let (_, bound_port) = socket + .get_endpoint() + .await + .expect("Failed to get bound endpoint"); + assert_eq!(bound_port, bind_port, "Bound port mismatch"); + + socket.close().await; + } + + #[task::test] + async fn test_udp_capacities() { + let network_manager = initialize().await; + + let tx_buffer = 2048; + let rx_buffer = 1024; + let rx_meta = 15; + let tx_meta = 20; + + let socket = network_manager + .new_udp_socket(tx_buffer, rx_buffer, rx_meta, tx_meta, None) + .await + .expect("Failed to create UDP socket"); + + let packet_read_cap = socket.get_packet_read_capacity().await; + let packet_write_cap = socket.get_packet_write_capacity().await; + let payload_read_cap = socket.get_payload_read_capacity().await; + let payload_write_cap = socket.get_payload_write_capacity().await; + + assert_eq!(packet_read_cap, rx_meta, "Packet read capacity mismatch"); + assert_eq!(packet_write_cap, tx_meta, "Packet write capacity mismatch"); + assert_eq!( + payload_read_cap, rx_buffer, + "Payload read capacity mismatch" + ); + assert_eq!( + payload_write_cap, tx_buffer, + "Payload write capacity mismatch" + ); + + socket.close().await; + } +} From 6d82c60ded03173a3947ed7d45c51f662ab68cfa Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:29:47 +0100 Subject: [PATCH 43/78] feat: implement TcpSocket for TCP socket operations --- modules/network/src/socket/tcp.rs | 560 ++++++++++++++++++++++++++++++ 1 file changed, 560 insertions(+) create mode 100644 modules/network/src/socket/tcp.rs diff --git a/modules/network/src/socket/tcp.rs b/modules/network/src/socket/tcp.rs new file mode 100644 index 00000000..bafc3abe --- /dev/null +++ b/modules/network/src/socket/tcp.rs @@ -0,0 +1,560 @@ +use core::{ + future::poll_fn, + task::{Context, Poll}, +}; + +use crate::{ + Duration, Error, IpAddress, IpEndpoint, IpListenEndpoint, Port, Result, SocketContext, +}; +use embassy_futures::block_on; +use smoltcp::socket::tcp; + +#[repr(transparent)] +pub struct TcpSocket { + context: SocketContext, +} + +impl TcpSocket { + pub(crate) fn new(context: SocketContext) -> Self { + Self { context } + } + pub async fn with(&self, f: F) -> R + where + F: FnOnce(&tcp::Socket<'static>) -> R, + { + self.context.with(f).await + } + + pub async fn with_mutable(&self, f: F) -> R + where + F: FnOnce(&mut tcp::Socket<'static>) -> R, + { + self.context.with_mutable(f).await + } + + pub fn poll_with(&self, context: &mut Context<'_>, f: F) -> Poll + where + F: FnOnce(&tcp::Socket, &mut Context<'_>) -> Poll, + { + self.context.poll_with(context, f) + } + + pub fn poll_with_mutable(&self, context: &mut Context<'_>, f: F) -> Poll + where + F: FnOnce(&mut tcp::Socket, &mut Context<'_>) -> Poll, + { + self.context.poll_with_mutable(context, f) + } + + pub async fn set_timeout(&mut self, timeout: Option) { + let timeout = timeout.map(Duration::into_smoltcp); + + self.with_mutable(|socket| socket.set_timeout(timeout)) + .await + } + + pub async fn accept( + &mut self, + address: Option>, + port: impl Into, + ) -> Result<()> { + let endpoint = IpListenEndpoint::new(address.map(Into::into), port.into()).into_smoltcp(); + + self.with_mutable(|s| { + if s.state() == tcp::State::Closed { + s.listen(endpoint).map_err(|e| match e { + tcp::ListenError::InvalidState => Error::InvalidState, + tcp::ListenError::Unaddressable => Error::InvalidPort, + }) + } else { + Ok(()) + } + }) + .await?; + + poll_fn(|cx| { + self.poll_with_mutable(cx, |s, cx| match s.state() { + tcp::State::Listen | tcp::State::SynSent | tcp::State::SynReceived => { + s.register_send_waker(cx.waker()); + Poll::Pending + } + _ => Poll::Ready(Ok(())), + }) + }) + .await + } + + pub async fn connect( + &mut self, + address: impl Into, + port: impl Into, + ) -> Result<()> { + let endpoint = IpEndpoint::new(address.into(), port.into()).into_smoltcp(); + + self.context + .stack + .with_mutable(|stack| { + let local_port = stack.get_next_port().into_inner(); + + let socket: &mut tcp::Socket<'static> = stack.sockets.get_mut(self.context.handle); + + match socket.connect(stack.interface.context(), endpoint, local_port) { + Ok(()) => Ok(()), + Err(tcp::ConnectError::InvalidState) => Err(Error::InvalidState), + Err(tcp::ConnectError::Unaddressable) => Err(Error::NoRoute), + } + }) + .await?; + + poll_fn(|cx| { + self.poll_with_mutable(cx, |socket, cx| match socket.state() { + tcp::State::Closed | tcp::State::TimeWait => { + Poll::Ready(Err(Error::ConnectionReset)) + } + tcp::State::Listen => unreachable!(), + tcp::State::SynSent | tcp::State::SynReceived => { + socket.register_send_waker(cx.waker()); + Poll::Pending + } + _ => Poll::Ready(Ok(())), + }) + }) + .await + } + + pub async fn read(&mut self, buffer: &mut [u8]) -> Result { + poll_fn(|cx| { + self.poll_with_mutable(cx, |s, cx| match s.recv_slice(buffer) { + Ok(0) if buffer.is_empty() => Poll::Ready(Ok(0)), + Ok(0) => { + s.register_recv_waker(cx.waker()); + Poll::Pending + } + Ok(n) => Poll::Ready(Ok(n)), + Err(tcp::RecvError::Finished) => Poll::Ready(Ok(0)), + Err(tcp::RecvError::InvalidState) => Poll::Ready(Err(Error::ConnectionReset)), + }) + }) + .await + } + + pub async fn write(&mut self, buffer: &[u8]) -> Result { + poll_fn(|cx| { + self.poll_with_mutable(cx, |s, cx| match s.send_slice(buffer) { + Ok(0) => { + s.register_send_waker(cx.waker()); + Poll::Pending + } + Ok(n) => Poll::Ready(Ok(n)), + Err(tcp::SendError::InvalidState) => Poll::Ready(Err(Error::ConnectionReset)), + }) + }) + .await + } + + pub async fn flush(&mut self) -> Result<()> { + poll_fn(|cx| { + self.poll_with_mutable(cx, |s, cx| { + let data_pending = (s.send_queue() > 0) && s.state() != tcp::State::Closed; + let fin_pending = matches!( + s.state(), + tcp::State::FinWait1 | tcp::State::Closing | tcp::State::LastAck + ); + let rst_pending = s.state() == tcp::State::Closed && s.remote_endpoint().is_some(); + + if data_pending || fin_pending || rst_pending { + s.register_send_waker(cx.waker()); + Poll::Pending + } else { + Poll::Ready(Ok(())) + } + }) + }) + .await + } + + pub async fn close(&mut self) { + self.context.closed = true; + self.with_mutable(tcp::Socket::close).await + } + + pub async fn close_forced(&mut self) { + self.context.closed = true; + self.with_mutable(tcp::Socket::abort).await + } + + pub async fn get_read_capacity(&self) -> usize { + self.with(tcp::Socket::recv_capacity).await + } + + pub async fn get_write_capacity(&self) -> usize { + self.with(tcp::Socket::send_capacity).await + } + + pub async fn get_write_queue_size(&self) -> usize { + self.with(tcp::Socket::send_queue).await + } + + pub async fn get_read_queue_size(&self) -> usize { + self.with(tcp::Socket::recv_queue).await + } + + pub async fn get_local_endpoint(&self) -> Result> { + let endpoint = self.with(tcp::Socket::local_endpoint).await; + + Ok(endpoint.map(|e| (IpAddress::from_smoltcp(&e.addr), Port::from_inner(e.port)))) + } + + pub async fn get_remote_endpoint(&self) -> Result> { + let endpoint = self.with(tcp::Socket::remote_endpoint).await; + + Ok(endpoint.map(|e| (IpAddress::from_smoltcp(&e.addr), Port::from_inner(e.port)))) + } + + pub async fn set_keep_alive(&mut self, keep_alive: Option) { + let keep_alive = keep_alive.map(Duration::into_smoltcp); + self.with_mutable(|socket| socket.set_keep_alive(keep_alive)) + .await + } + + pub async fn set_hop_limit(&mut self, hop_limit: Option) { + self.with_mutable(|socket| socket.set_hop_limit(hop_limit)) + .await + } + + pub async fn can_read(&self) -> bool { + self.with(tcp::Socket::can_recv).await + } + + pub async fn can_write(&self) -> bool { + self.with(tcp::Socket::can_send).await + } + + pub async fn may_read(&self) -> bool { + self.with(tcp::Socket::may_recv).await + } + + pub async fn may_write(&self) -> bool { + self.with(tcp::Socket::may_send).await + } +} + +impl Drop for TcpSocket { + fn drop(&mut self) { + if self.context.closed { + return; + } + log::warning!("TCP socket dropped without being closed. Forcing closure."); + block_on(self.with_mutable(tcp::Socket::close)); + } +} + +#[cfg(test)] +mod tests { + + extern crate std; + + use synchronization::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex}; + + use crate::tests::initialize; + + use super::*; + + static TEST_MUTEX: Mutex = Mutex::new(()); + + #[task::test] + async fn test_tcp_connect() { + let _lock = TEST_MUTEX.lock().await; + let network_manager = initialize().await; + let port = Port::from_inner(51001); + use synchronization::{Arc, blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal}; + let server_ready = Arc::new(Signal::::new()); + let connection_established = Arc::new(Signal::::new()); + let client_done = Arc::new(Signal::::new()); + let server_ready_clone = server_ready.clone(); + let connection_established_clone = connection_established.clone(); + let client_done_clone = client_done.clone(); + let mut listener = network_manager + .new_tcp_socket(1024, 1024, None) + .await + .expect("Failed to create listener socket"); + let task_manager = task::get_instance(); + let current_task = task_manager.get_current_task_identifier().await; + let (listen_task, _) = task_manager + .spawn(current_task, "TCP Listen Task", None, move |_| async move { + server_ready_clone.signal(()); + let accept_result = listener.accept(Some([127, 0, 0, 1]), port).await; + if accept_result.is_err() { + return; + } + connection_established_clone.signal(()); + client_done_clone.wait().await; + listener.close_forced().await; + }) + .await + .unwrap(); + for _ in 0..5 { + task::sleep(Duration::from_milliseconds(20)).await; + } + server_ready.wait().await; + task::sleep(Duration::from_milliseconds(200)).await; + let mut client = network_manager + .new_tcp_socket(1024, 1024, None) + .await + .expect("Failed to create client socket"); + let connect_result = client.connect([127, 0, 0, 1], port).await; + if connect_result.is_err() { + return; + } + connection_established.wait().await; + let _endpoint = client.get_local_endpoint().await; + task::sleep(Duration::from_milliseconds(100)).await; + client.close_forced().await; + client_done.signal(()); + listen_task.join().await; + } + + #[task::test] + async fn test_tcp_send_receive() { + use synchronization::{Arc, blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal}; + let _lock = TEST_MUTEX.lock().await; + let network_manager = initialize().await; + let port = Port::from_inner(51002); + let mut listener = network_manager + .new_tcp_socket(2048, 2048, None) + .await + .expect("Failed to create listener"); + let server_ready = Arc::new(Signal::::new()); + let server_ready_clone = server_ready.clone(); + let task_manager = task::get_instance(); + let current_task = task_manager.get_current_task_identifier().await; + let (_server_task, _) = task_manager + .spawn(current_task, "TCP Server Task", None, move |_| async move { + server_ready_clone.signal(()); + let accept_result = listener.accept(Some([127, 0, 0, 1]), port).await; + if accept_result.is_err() { + return; + } + let mut buffer = [0u8; 1024]; + match listener.read(&mut buffer).await { + Ok(size) => { + assert_eq!(&buffer[..size], b"Hello, TCP!", "Received data mismatch"); + } + Err(_) => { + return; + } + } + let response = b"Hello back!"; + if let Err(_) = listener.write(response).await { + return; + } + if let Err(_) = listener.flush().await { + return; + } + task::sleep(Duration::from_milliseconds(100)).await; + listener.close_forced().await; + }) + .await + .unwrap(); + server_ready.wait().await; + let mut client = network_manager + .new_tcp_socket(2048, 2048, None) + .await + .expect("Failed to create client"); + let connect_result = client.connect([127, 0, 0, 1], port).await; + if connect_result.is_err() { + return; + } + task::sleep(Duration::from_milliseconds(100)).await; + if let Err(_) = client.write(b"Hello, TCP!").await { + return; + } + if let Err(_) = client.flush().await { + return; + } + let mut response_buffer = [0u8; 1024]; + match client.read(&mut response_buffer).await { + Ok(size) => { + assert_eq!( + &response_buffer[..size], + b"Hello back!", + "Response data mismatch" + ); + } + Err(_) => { + return; + } + } + client.close_forced().await; + } + + #[task::test] + async fn test_tcp_endpoints() { + let _lock = TEST_MUTEX.lock().await; + let network_manager = initialize().await; + let mut socket = network_manager + .new_tcp_socket(1024, 1024, None) + .await + .expect("Failed to create socket"); + let local = socket + .get_local_endpoint() + .await + .expect("Failed to get local endpoint"); + let remote = socket + .get_remote_endpoint() + .await + .expect("Failed to get remote endpoint"); + assert!( + local.is_none(), + "TCP endpoint | Local endpoint should be None before connection" + ); + assert!( + remote.is_none(), + "Remote endpoint should be None before connection" + ); + let port = Port::from_inner(51003); + let mut listener = network_manager + .new_tcp_socket(1024, 1024, None) + .await + .expect("Failed to create listener"); + use synchronization::{Arc, blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal}; + let server_ready = Arc::new(Signal::::new()); + let connection_ready = Arc::new(Signal::::new()); + let endpoints_checked = Arc::new(Signal::::new()); + let server_ready_clone = server_ready.clone(); + let connection_ready_clone = connection_ready.clone(); + let endpoints_checked_clone = endpoints_checked.clone(); + let task_manager = task::get_instance(); + let current_task = task_manager.get_current_task_identifier().await; + let (listen_task, _) = task_manager + .spawn( + current_task, + "TCP Endpoint Listen", + None, + move |_| async move { + server_ready_clone.signal(()); + let accept_result = listener.accept(Some([127, 0, 0, 1]), port).await; + if accept_result.is_err() { + return; + } + connection_ready_clone.signal(()); + endpoints_checked_clone.wait().await; + task::sleep(Duration::from_milliseconds(100)).await; + listener.close_forced().await; + }, + ) + .await + .unwrap(); + for _ in 0..5 { + task::sleep(Duration::from_milliseconds(20)).await; + } + server_ready.wait().await; + task::sleep(Duration::from_milliseconds(200)).await; + let connect_result = socket.connect([127, 0, 0, 1], port).await; + if connect_result.is_err() { + return; + } + task::sleep(Duration::from_milliseconds(50)).await; + connection_ready.wait().await; + task::sleep(Duration::from_milliseconds(50)).await; + let local = socket + .get_local_endpoint() + .await + .expect("Failed to get local endpoint"); + let remote = socket + .get_remote_endpoint() + .await + .expect("Failed to get remote endpoint"); + assert!( + local.is_some(), + "Local endpoint should be set after connection" + ); + assert!( + remote.is_some(), + "Remote endpoint should be set after connection" + ); + if let Some((addr, p)) = remote { + assert_eq!( + addr, + IpAddress::from([127, 0, 0, 1]), + "Remote address mismatch" + ); + assert_eq!(p, port, "Remote port mismatch"); + } + endpoints_checked.signal(()); + task::sleep(Duration::from_milliseconds(100)).await; + socket.close_forced().await; + listen_task.join().await; + } + + #[task::test] + async fn test_tcp_capacities() { + let _lock = TEST_MUTEX.lock().await; + let network_manager = initialize().await; + let tx_buffer = 2048; + let rx_buffer = 1024; + let mut socket = network_manager + .new_tcp_socket(tx_buffer, rx_buffer, None) + .await + .expect("Failed to create socket"); + let read_cap = socket.get_read_capacity().await; + let write_cap = socket.get_write_capacity().await; + let read_queue = socket.get_read_queue_size().await; + let write_queue = socket.get_write_queue_size().await; + assert_eq!(read_cap, rx_buffer, "Read capacity mismatch"); + assert_eq!(write_cap, tx_buffer, "Write capacity mismatch"); + assert_eq!(read_queue, 0, "Read queue should be empty initially"); + assert_eq!(write_queue, 0, "Write queue should be empty initially"); + socket.close_forced().await; + } + + #[task::test] + async fn test_tcp_flush() { + let _lock = TEST_MUTEX.lock().await; + let network_manager = initialize().await; + let port = Port::from_inner(51004); + let mut listener = network_manager + .new_tcp_socket(1024, 1024, None) + .await + .expect("Failed to create listener"); + let task_manager = task::get_instance(); + let current_task = task_manager.get_current_task_identifier().await; + let (_server_task, _) = task_manager + .spawn( + current_task, + "TCP Flush Server", + None, + move |_| async move { + let accept_result = listener.accept(Some([127, 0, 0, 1]), port).await; + if accept_result.is_err() { + return; + } + let mut buffer = [0u8; 1024]; + let _ = listener.read(&mut buffer).await; + task::sleep(Duration::from_milliseconds(200)).await; + listener.close_forced().await; + }, + ) + .await + .unwrap(); + task::sleep(Duration::from_milliseconds(300)).await; + let mut client = network_manager + .new_tcp_socket(1024, 1024, None) + .await + .expect("Failed to create client"); + let connect_result = client.connect([127, 0, 0, 1], port).await; + if connect_result.is_err() { + return; + } + task::sleep(Duration::from_milliseconds(100)).await; + let write_result = client.write(b"Test data").await; + if write_result.is_err() { + return; + } + if let Err(_) = client.flush().await { + return; + } + task::sleep(Duration::from_milliseconds(200)).await; + client.close_forced().await; + } +} From 036ac8dd00a67cb0a724db4f665003171b2404b2 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:29:52 +0100 Subject: [PATCH 44/78] feat: implement DnsSocket for DNS query operations --- modules/network/src/socket/dns.rs | 139 ++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 modules/network/src/socket/dns.rs diff --git a/modules/network/src/socket/dns.rs b/modules/network/src/socket/dns.rs new file mode 100644 index 00000000..39fe4834 --- /dev/null +++ b/modules/network/src/socket/dns.rs @@ -0,0 +1,139 @@ +use core::{ + future::poll_fn, + task::{Context, Poll}, +}; + +use alloc::{vec, vec::Vec}; +use embassy_futures::block_on; +use smoltcp::{socket::dns, wire::DnsQueryType}; + +use crate::{DnsQueryKind, IpAddress, Result, SocketContext}; + +pub struct DnsSocket { + context: SocketContext, +} + +impl DnsSocket { + pub fn new(context: SocketContext) -> Self { + Self { context } + } + + fn poll_with_mutable(&self, context: &mut Context<'_>, f: F) -> Poll + where + F: FnOnce(&mut dns::Socket<'static>, &mut Context<'_>) -> Poll, + { + self.context.poll_with_mutable(context, f) + } + + pub async fn update_servers(&self) -> Result<()> { + self.context + .stack + .with_mutable(|s| { + let dns_servers = s.get_dns_servers().to_vec(); + let socket = s.sockets.get_mut::(self.context.handle); + socket.update_servers(&dns_servers); + }) + .await; + + Ok(()) + } + + pub async fn resolve_for_kind(&self, host: &str, kind: DnsQueryType) -> Result> { + if let Ok(host) = IpAddress::try_from(host) { + return Ok(vec![host]); + } + + let query = self + .context + .stack + .with_mutable(|s| { + let socket = s.sockets.get_mut::(self.context.handle); + + socket.start_query(s.interface.context(), host, kind) + }) + .await?; + + self.context.stack.wake_up(); + + poll_fn(|cx| { + self.poll_with_mutable(cx, |socket, cx| match socket.get_query_result(query) { + Err(dns::GetQueryResultError::Pending) => { + socket.register_query_waker(query, cx.waker()); + Poll::Pending + } + Err(e) => Poll::Ready(Err(e.into())), + Ok(ip_addresses) => { + let ip_addresses = ip_addresses + .into_iter() + .map(|a| IpAddress::from_smoltcp(&a)) + .collect(); + + Poll::Ready(Ok(ip_addresses)) + } + }) + }) + .await + } + + pub async fn resolve(&self, host: &str, kind: DnsQueryKind) -> Result> { + let mut results = Vec::new(); + + if kind.contains(DnsQueryKind::A) { + let mut a_results = self.resolve_for_kind(host, DnsQueryType::A).await?; + results.append(&mut a_results); + } + + if kind.contains(DnsQueryKind::Aaaa) { + let mut aaaa_results = self.resolve_for_kind(host, DnsQueryType::Aaaa).await?; + results.append(&mut aaaa_results); + } + + if kind.contains(DnsQueryKind::Cname) { + let mut cname_results = self.resolve_for_kind(host, DnsQueryType::Cname).await?; + results.append(&mut cname_results); + } + + if kind.contains(DnsQueryKind::Ns) { + let mut ns_results = self.resolve_for_kind(host, DnsQueryType::Ns).await?; + results.append(&mut ns_results); + } + + if kind.contains(DnsQueryKind::Soa) { + let mut soa_results = self.resolve_for_kind(host, DnsQueryType::Soa).await?; + results.append(&mut soa_results); + } + + Ok(results) + } + + pub async fn close(mut self) -> Result<()> { + if self.context.closed { + return Ok(()); + } + + self.context + .stack + .with_mutable(|s| { + let _ = s.remove_socket(self.context.handle); + }) + .await; + + self.context.closed = true; + + Ok(()) + } +} + +impl Drop for DnsSocket { + fn drop(&mut self) { + if self.context.closed { + return; + } + + log::warning!("DNS socket dropped without being closed. Forcing closure..."); + + block_on(self.context.stack.with_mutable(|s| { + let _ = s.remove_socket(self.context.handle); + })); + } +} From 7df5d0d987a1f7ad3c27ba9a03fa1ef8c0484b02 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:30:04 +0100 Subject: [PATCH 45/78] feat: add socket module to integrate DNS, ICMP, TCP, and UDP functionalities --- modules/network/src/socket/mod.rs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 modules/network/src/socket/mod.rs diff --git a/modules/network/src/socket/mod.rs b/modules/network/src/socket/mod.rs new file mode 100644 index 00000000..b9d852d6 --- /dev/null +++ b/modules/network/src/socket/mod.rs @@ -0,0 +1,9 @@ +mod dns; +mod icmp; +mod tcp; +mod udp; + +pub use dns::*; +pub use icmp::*; +pub use tcp::*; +pub use udp::*; From 1a8770b9f3ca5e41aeabfaa548434de98f6e6dd3 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:30:12 +0100 Subject: [PATCH 46/78] feat: enhance error handling with additional error variants and conversions for DNS, ICMP, and UDP --- modules/network/src/error.rs | 97 ++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 5 deletions(-) diff --git a/modules/network/src/error.rs b/modules/network/src/error.rs index 6d40c803..cce9bc36 100644 --- a/modules/network/src/error.rs +++ b/modules/network/src/error.rs @@ -1,4 +1,5 @@ use core::{fmt::Display, num::NonZeroU8}; +use smoltcp::socket::{dns, icmp, udp}; pub type Result = core::result::Result; @@ -30,17 +31,38 @@ pub enum Error { Unsupported, UnexpectedEndOfFile, OutOfMemory, - InProgress, - PoisonnedLock, + Pending, UnsupportedProtocol, InvalidIdentifier, DuplicateIdentifier, + FailedToGenerateSeed(file_system::Error), + FailedToSpawnNetworkTask(task::Error), + // - DNS + InvalidName, + NameTooLong, + Failed, + // - Accept / Connect + InvalidState, + InvalidPort, + NoRoute, + // Udp + Truncated, + SocketNotBound, + PacketTooLarge, + InvalidEndpoint, + + FailedToMountDevice(virtual_file_system::Error), + + NoFreeSlot, + Other, } +impl core::error::Error for Error {} + impl Error { pub const fn get_discriminant(&self) -> NonZeroU8 { - unsafe { NonZeroU8::new_unchecked(*self as u8) } + unsafe { *(self as *const Self as *const NonZeroU8) } } } @@ -78,8 +100,7 @@ impl Display for Error { Error::Unsupported => write!(f, "Unsupported operation"), Error::UnexpectedEndOfFile => write!(f, "Unexpected end of file"), Error::OutOfMemory => write!(f, "Out of memory"), - Error::InProgress => write!(f, "In progress operation not completed yet"), - Error::PoisonnedLock => write!(f, "Poisoned lock encountered an error state"), + Error::Pending => write!(f, "In progress operation not completed yet"), Error::UnsupportedProtocol => write!(f, "Unsupported protocol used in operation"), Error::InvalidIdentifier => { write!(f, "Invalid identifier provided for operation") @@ -87,7 +108,73 @@ impl Display for Error { Error::DuplicateIdentifier => { write!(f, "Duplicate identifier found in operation") } + Error::FailedToGenerateSeed(e) => { + write!(f, "Failed to generate seed: {}", e) + } + Error::FailedToSpawnNetworkTask(e) => { + write!(f, "Failed to spawn network task: {}", e) + } + Error::InvalidName => write!(f, "Invalid name"), + Error::NameTooLong => write!(f, "Name too long"), + Error::Failed => write!(f, "Failed"), + Error::InvalidState => write!(f, "Invalid state for operation"), + Error::InvalidPort => write!(f, "Invalid port specified"), + Error::NoRoute => write!(f, "No route to host"), + Error::Truncated => write!(f, "Truncated packet received"), + Error::SocketNotBound => write!(f, "Socket not bound"), + Error::PacketTooLarge => write!(f, "Packet too large to send"), + Error::InvalidEndpoint => write!(f, "Invalid endpoint specified"), + Error::FailedToMountDevice(e) => { + write!(f, "Failed to mount device: {}", e) + } + Error::NoFreeSlot => write!(f, "No free slot available"), Error::Other => write!(f, "Other error occurred"), } } } + +impl From for Error { + fn from(e: dns::StartQueryError) -> Self { + match e { + dns::StartQueryError::InvalidName => Error::InvalidName, + dns::StartQueryError::NameTooLong => Error::NameTooLong, + dns::StartQueryError::NoFreeSlot => Error::NoFreeSlot, + } + } +} + +impl From for Error { + fn from(e: dns::GetQueryResultError) -> Self { + match e { + dns::GetQueryResultError::Pending => Error::Pending, + dns::GetQueryResultError::Failed => Error::Failed, + } + } +} + +impl From for Error { + fn from(e: icmp::SendError) -> Self { + match e { + icmp::SendError::Unaddressable => Error::InvalidEndpoint, + icmp::SendError::BufferFull => Error::ResourceBusy, + } + } +} + +impl From for Error { + fn from(e: icmp::BindError) -> Self { + match e { + icmp::BindError::InvalidState => Error::InvalidState, + icmp::BindError::Unaddressable => Error::NoRoute, + } + } +} + +impl From for Error { + fn from(e: udp::BindError) -> Self { + match e { + udp::BindError::InvalidState => Error::InvalidState, + udp::BindError::Unaddressable => Error::NoRoute, + } + } +} From b6742aa0a7e19581067e995e1d8f3b19780f8b26 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:30:22 +0100 Subject: [PATCH 47/78] feat: integrate smoltcp stack with network manager and socket implementations --- modules/network/src/manager/mod.rs | 320 +++++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 modules/network/src/manager/mod.rs diff --git a/modules/network/src/manager/mod.rs b/modules/network/src/manager/mod.rs new file mode 100644 index 00000000..1034e245 --- /dev/null +++ b/modules/network/src/manager/mod.rs @@ -0,0 +1,320 @@ +mod context; +mod device; +mod runner; +mod stack; + +use alloc::{vec, vec::Vec}; +pub use context::*; +use file_system::{DirectCharacterDevice, Path}; +pub use runner::*; +use smoltcp::{ + phy::Device, + socket::{dns, icmp, tcp, udp}, +}; +use synchronization::once_lock::OnceLock; +use synchronization::{ + Arc, blocking_mutex::raw::CriticalSectionRawMutex, rwlock::RwLock, signal::Signal, +}; +use task::{SpawnerIdentifier, TaskIdentifier}; +use virtual_file_system::VirtualFileSystem; + +use crate::{ + DnsSocket, Error, IcmpSocket, Result, TcpSocket, UdpSocket, + manager::{ + device::NetworkDevice, + stack::{Stack, StackInner}, + }, +}; + +static MANAGER_INSTANCE: OnceLock = OnceLock::new(); + +pub fn get_instance() -> &'static Manager { + MANAGER_INSTANCE + .try_get() + .expect("Manager is not initialized") +} + +pub fn initialize( + _task_manager: &'static task::Manager, + _virtual_file_system: &'static VirtualFileSystem, + random_device: &'static dyn DirectCharacterDevice, +) -> &'static Manager { + MANAGER_INSTANCE.get_or_init(|| Manager::new(random_device)) +} + +pub fn get_smoltcp_time() -> smoltcp::time::Instant { + let time_manager = time::get_instance(); + let current_time = time_manager + .get_current_time() + .expect("Failed to get current time"); + + smoltcp::time::Instant::from_millis(current_time.as_millis() as i64) +} + +type StackList = Vec; + +pub struct Manager { + pub(crate) random_device: &'static dyn DirectCharacterDevice, + pub(crate) stacks: RwLock, +} + +impl Manager { + pub fn new(random_device: &'static dyn DirectCharacterDevice) -> Self { + Manager { + random_device, + stacks: RwLock::new(Vec::new()), + } + } + + fn generate_seed(&self) -> Result { + let mut buffer = [0u8; 8]; + self.random_device + .read(&mut buffer, 0) + .map_err(Error::FailedToGenerateSeed)?; + Ok(u64::from_le_bytes(buffer)) + } + + pub(crate) async fn find_first_available_stack(stacks: &StackList) -> Option { + for stack in stacks { + let available = stack.with(|s| s.is_available()).await; + + if available { + return Some(stack.clone()); + } + } + + None + } + + pub(crate) async fn find_stack(stacks: &StackList, name: &str) -> Option { + for stack in stacks { + let stack_name = stack.with(|s| s.name.clone()).await; + + if stack_name.as_str() == name { + return Some(stack.clone()); + } + } + + None + } + + pub async fn mount_interface( + &self, + task: TaskIdentifier, + name: &str, + mut device: impl Device + 'static, + controller_device: impl DirectCharacterDevice + 'static, + spawner: Option, + ) -> Result<()> { + let mut stacks = self.stacks.write().await; + + if Self::find_stack(&stacks, name).await.is_some() { + return Err(Error::DuplicateIdentifier); + } + + let random_seed = self.generate_seed()?; + let now = get_smoltcp_time(); + + let stack_inner = StackInner::new(name, &mut device, controller_device, random_seed, now); + + // Create a wake signal for runner/stack communication + let wake_signal: WakeSignal = Arc::new(Signal::new()); + + let stack = Stack::new(stack_inner, wake_signal.clone()); + + let mut runner = StackRunner::new(stack.clone(), device, wake_signal); + + let task_manager = task::get_instance(); + + task_manager + .spawn( + task, + "Network Interface Runner", + spawner, + move |_| async move { + runner.run().await; + }, + ) + .await + .map_err(Error::FailedToSpawnNetworkTask)?; + + let path = Path::NETWORK_DEVICES + .join(Path::from_str(name)) + .ok_or(Error::InvalidIdentifier)?; + + let device = NetworkDevice::new(stack.clone()); + + let virtual_file_system = virtual_file_system::get_instance(); + + match virtual_file_system + .create_directory(task, &Path::NETWORK_DEVICES) + .await + { + Ok(_) => {} + Err(virtual_file_system::Error::AlreadyExists) => {} + Err(e) => return Err(Error::FailedToMountDevice(e)), + }; + + match virtual_file_system.remove(task, &path).await { + Ok(_) => {} + Err(virtual_file_system::Error::FileSystem(file_system::Error::NotFound)) => {} + Err(e) => return Err(Error::FailedToMountDevice(e)), + }; + + virtual_file_system + .mount_character_device(task, path, device) + .await + .map_err(Error::FailedToMountDevice)?; + + stacks.push(stack); + + Ok(()) + } + + pub async fn new_dns_socket(&self, interface_name: Option<&str>) -> Result { + let stacks = self.stacks.read().await; + + let stack = if let Some(name) = interface_name { + Self::find_stack(&stacks, name) + .await + .ok_or(Error::NotFound)? + } else { + Self::find_first_available_stack(&stacks) + .await + .ok_or(Error::NotFound)? + }; + + let handle = stack + .with_mutable(|s| { + let socket = dns::Socket::new(&s.dns_servers, vec![]); + s.add_socket(socket) + }) + .await; + + let context = SocketContext { + handle, + stack: stack.clone(), + closed: false, + }; + let socket = DnsSocket::new(context); + + Ok(socket) + } + + pub async fn new_tcp_socket( + &self, + transmit_buffer_size: usize, + receive_buffer_size: usize, + interface_name: Option<&str>, + ) -> Result { + let stacks = self.stacks.read().await; + + let stack = if let Some(name) = interface_name { + Self::find_stack(&stacks, name) + .await + .ok_or(Error::NotFound)? + } else { + Self::find_first_available_stack(&stacks) + .await + .ok_or(Error::NotFound)? + }; + + let send_buffer = tcp::SocketBuffer::new(vec![0u8; transmit_buffer_size]); + let receive_buffer = tcp::SocketBuffer::new(vec![0u8; receive_buffer_size]); + + let socket = tcp::Socket::new(receive_buffer, send_buffer); + let handle = stack.with_mutable(|s| s.add_socket(socket)).await; + + let context = SocketContext { + handle, + stack: stack.clone(), + closed: false, + }; + + Ok(TcpSocket::new(context)) + } + + pub async fn new_udp_socket( + &self, + transmit_buffer_size: usize, + receive_buffer_size: usize, + receive_meta_buffer_size: usize, + transmit_meta_buffer_size: usize, + interface_name: Option<&str>, + ) -> Result { + let stacks = self.stacks.read().await; + + let stack = if let Some(name) = interface_name { + Self::find_stack(&stacks, name) + .await + .ok_or(Error::NotFound)? + } else { + Self::find_first_available_stack(&stacks) + .await + .ok_or(Error::NotFound)? + }; + + let receive_meta_buffer = udp::PacketBuffer::new( + vec![udp::PacketMetadata::EMPTY; receive_meta_buffer_size], + vec![0u8; receive_buffer_size], + ); + let transmit_meta_buffer = udp::PacketBuffer::new( + vec![udp::PacketMetadata::EMPTY; transmit_meta_buffer_size], + vec![0u8; transmit_buffer_size], + ); + + let socket = udp::Socket::new(receive_meta_buffer, transmit_meta_buffer); + let handle = stack.with_mutable(|s| s.add_socket(socket)).await; + + let context = SocketContext { + handle, + stack: stack.clone(), + closed: false, + }; + + Ok(UdpSocket::new(context)) + } + + pub async fn new_icmp_socket( + &self, + receive_buffer_size: usize, + transmit_buffer_size: usize, + receive_meta_buffer_size: usize, + transmit_meta_buffer_size: usize, + interface_name: Option<&str>, + ) -> Result { + let stacks = self.stacks.read().await; + + let stack = if let Some(name) = interface_name { + Self::find_stack(&stacks, name) + .await + .ok_or(Error::NotFound)? + } else { + Self::find_first_available_stack(&stacks) + .await + .ok_or(Error::NotFound)? + }; + + let receive_buffer = icmp::PacketBuffer::new( + vec![icmp::PacketMetadata::EMPTY; receive_meta_buffer_size], + vec![0u8; receive_buffer_size], + ); + let transmit_buffer = icmp::PacketBuffer::new( + vec![icmp::PacketMetadata::EMPTY; transmit_meta_buffer_size], + vec![0u8; transmit_buffer_size], + ); + + let socket = icmp::Socket::new(receive_buffer, transmit_buffer); + let handle = stack.with_mutable(|s| s.add_socket(socket)).await; + + let context = SocketContext { + handle, + stack: stack.clone(), + closed: false, + }; + + let socket = IcmpSocket::new(context); + + Ok(socket) + } +} From 79fc031c358ea3f723eff9121f36ce6aef6d0712 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:30:31 +0100 Subject: [PATCH 48/78] feat: update Cargo.toml and lib.rs to enhance network module with additional dependencies and features --- modules/network/Cargo.toml | 46 +++++++++++++++++- modules/network/src/lib.rs | 98 ++++++++++++++++++++++++++++++++++---- 2 files changed, 135 insertions(+), 9 deletions(-) diff --git a/modules/network/Cargo.toml b/modules/network/Cargo.toml index 39b2301a..c3ae4eef 100644 --- a/modules/network/Cargo.toml +++ b/modules/network/Cargo.toml @@ -5,12 +5,56 @@ edition = "2024" [dependencies] file_system = { workspace = true } - synchronization = { workspace = true } time = { workspace = true } +task = { workspace = true } +users = { workspace = true } +log = { workspace = true } +virtual_file_system = { workspace = true } +shared = { workspace = true } + embassy-net = { workspace = true, features = [ + "icmp", + "raw", + "udp", + "tcp", + "dns", + "mdns", "proto-ipv4", "proto-ipv6", "dhcpv4", "dhcpv4-hostname", + "medium-ip", + "medium-ethernet", + "log", + "alloc", +] } +embassy-net-driver = { workspace = true } +embedded-io-async = { workspace = true } +embassy-time = { workspace = true } +embassy-futures = { workspace = true } +smol_str = { workspace = true } +smoltcp = { workspace = true, features = [ + "async", + "alloc", + "log", + "medium-ethernet", + "medium-ip", + "packetmeta-id", + "proto-ipv4", + "proto-ipv6", + "socket-raw", + "socket-udp", + "socket-tcp", + "socket-dns", + "socket-icmp", + "socket-dhcpv4", ] } +heapless = { version = "0.8" } + +[dev-dependencies] +testing = { workspace = true } +drivers_std = { workspace = true } +drivers_shared = { workspace = true } +little_fs = { workspace = true } +abi_definitions = { workspace = true } diff --git a/modules/network/src/lib.rs b/modules/network/src/lib.rs index 3e3a200d..e638bc18 100644 --- a/modules/network/src/lib.rs +++ b/modules/network/src/lib.rs @@ -2,14 +2,96 @@ extern crate alloc; +mod device; mod error; -mod ip; -mod protocol; -mod service; -mod traits; +mod fundamentals; +mod manager; +mod socket; +pub use device::*; pub use error::*; -pub use ip::*; -pub use protocol::*; -pub use service::*; -pub use traits::*; +pub use fundamentals::*; +pub use manager::*; +pub use socket::*; + +#[cfg(test)] +pub mod tests { + use file_system::AccessFlags; + use synchronization::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex}; + use virtual_file_system::{File, create_default_hierarchy}; + + use super::*; + + extern crate abi_definitions; + + drivers_std::memory::instantiate_global_allocator!(); + + pub(crate) async fn initialize() -> &'static crate::Manager { + static INITIALIZE_MUTEX: Mutex = Mutex::new(false); + + let mut initialized = INITIALIZE_MUTEX.lock().await; + + if *initialized { + return crate::get_instance(); + } + + *initialized = true; + + static RANDOM_DEVICE: drivers_shared::devices::RandomDevice = + drivers_shared::devices::RandomDevice; + static TIME_DEVICE: drivers_std::devices::TimeDevice = drivers_std::devices::TimeDevice; + + let task_manager = task::initialize(); + let task = task_manager.get_current_task_identifier().await; + + log::initialize(&drivers_std::log::Logger).unwrap(); + + let user_manager = users::initialize(); + + let time_manager = time::initialize(&TIME_DEVICE).unwrap(); + + let memory_device = file_system::MemoryDevice::<512>::new_static(10 * 1024 * 1024); + + let root_file_system = little_fs::FileSystem::get_or_format(memory_device, 512).unwrap(); + + let virtual_file_system = virtual_file_system::initialize( + task_manager, + user_manager, + time_manager, + root_file_system, + ) + .unwrap(); + + create_default_hierarchy(virtual_file_system, task) + .await + .unwrap(); + + let network_manager = crate::initialize(task_manager, virtual_file_system, &RANDOM_DEVICE); + + let (device, controler_device) = crate::create_loopback_device(); + + let spawner = drivers_std::executor::new_thread_executor().await; + + network_manager + .mount_interface(task, "loopback0", device, controler_device, Some(spawner)) + .await + .unwrap(); + + let mut file = File::open( + virtual_file_system, + task, + "/devices/network/loopback0", + AccessFlags::Write.into(), + ) + .await + .unwrap(); + + file.control(ADD_IP_ADDRESS, &IpCidr::new_ipv4([127, 0, 0, 1], 8)) + .await + .unwrap(); + + file.close(virtual_file_system).await.unwrap(); + + network_manager + } +} From 1ea3d3a62685716d6bcf44afa3c20cf635631799 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:30:56 +0100 Subject: [PATCH 49/78] fix: correct error handling in paste method for clipboard text retrieval --- drivers/native/src/devices/window_screen/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/native/src/devices/window_screen/window.rs b/drivers/native/src/devices/window_screen/window.rs index 640c6889..e0f185cb 100644 --- a/drivers/native/src/devices/window_screen/window.rs +++ b/drivers/native/src/devices/window_screen/window.rs @@ -40,7 +40,7 @@ impl<'a> Window<'a> { } pub fn paste(&mut self) -> Option<()> { - if let Some(text) = self.clipboard.get_text().ok() { + if let Ok(text) = self.clipboard.get_text() { for character in text.chars() { let key = Key::Character(character as u8); From 0cf69ceac99f88058871451eab0c23913b8625be Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:31:05 +0100 Subject: [PATCH 50/78] fix: simplify error handling in HashDevice operations by removing closures --- drivers/shared/src/devices/hash.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/shared/src/devices/hash.rs b/drivers/shared/src/devices/hash.rs index 6e420dca..7cbfe281 100644 --- a/drivers/shared/src/devices/hash.rs +++ b/drivers/shared/src/devices/hash.rs @@ -46,7 +46,7 @@ impl BaseOperations for HashDevice { fn read(&self, context: &mut Context, buffer: &mut [u8], _: Size) -> Result { let hash_context = context .get_private_data_mutable_of_type::() - .ok_or_else(|| file_system::Error::InvalidParameter)?; + .ok_or(file_system::Error::InvalidParameter)?; if buffer.len() < hash_context.hasher.output_size() { return Err(Error::InvalidParameter); @@ -61,7 +61,7 @@ impl BaseOperations for HashDevice { fn write(&self, context: &mut Context, buffer: &[u8], _: Size) -> Result { let hash_context = context .get_private_data_mutable_of_type::() - .ok_or_else(|| file_system::Error::InvalidParameter)?; + .ok_or(file_system::Error::InvalidParameter)?; hash_context.hasher.update(buffer); Ok(buffer.len()) @@ -76,7 +76,7 @@ impl BaseOperations for HashDevice { ) -> Result<()> { let hash_context = context .get_private_data_mutable_of_type::() - .ok_or_else(|| file_system::Error::InvalidParameter)?; + .ok_or(file_system::Error::InvalidParameter)?; match command { hash::RESET::IDENTIFIER => { @@ -96,7 +96,7 @@ impl BaseOperations for HashDevice { fn clone_context(&self, context: &Context) -> Result { let hash_context = context .get_private_data_of_type::() - .ok_or_else(|| file_system::Error::InvalidParameter)?; + .ok_or(file_system::Error::InvalidParameter)?; Ok(Context::new(Some(hash_context.clone()))) } From f846722df517b182e8a1a55eef815148f773cb4d Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:31:15 +0100 Subject: [PATCH 51/78] refactor: simplify load_to_virtual_file_system function signature and remove unused parameter in tests --- drivers/std/src/loader.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/drivers/std/src/loader.rs b/drivers/std/src/loader.rs index 2bf8317d..4497fe74 100644 --- a/drivers/std/src/loader.rs +++ b/drivers/std/src/loader.rs @@ -24,8 +24,8 @@ impl From for Error { pub type Result = core::result::Result; -pub async fn load_to_virtual_file_system<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, +pub async fn load_to_virtual_file_system( + virtual_file_system: &VirtualFileSystem, source_path: impl AsRef, destination_path: impl AsRef, ) -> Result<()> { @@ -89,7 +89,6 @@ mod tests { users::get_instance(), time::get_instance(), file_system, - None, ) .unwrap(); From bcc280124c10c535832d9d8d9504c764eb597b07 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:31:21 +0100 Subject: [PATCH 52/78] feat: implement Default trait for MemoryManager to simplify instantiation --- drivers/std/src/memory.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/std/src/memory.rs b/drivers/std/src/memory.rs index e173d072..34c833d8 100644 --- a/drivers/std/src/memory.rs +++ b/drivers/std/src/memory.rs @@ -26,6 +26,12 @@ pub struct MemoryManager { regions: CriticalSectionMutex>, } +impl Default for MemoryManager { + fn default() -> Self { + Self::new() + } +} + impl MemoryManager { pub const fn new() -> Self { MemoryManager { From e3347c0304ee17bebc0d37fedc0aa9326aae249d Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:31:35 +0100 Subject: [PATCH 53/78] fix: correct error mapping for InvalidInput in map_error function --- drivers/std/src/io.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/std/src/io.rs b/drivers/std/src/io.rs index ec58badf..d3491844 100644 --- a/drivers/std/src/io.rs +++ b/drivers/std/src/io.rs @@ -5,7 +5,7 @@ pub fn map_error(error: io::Error) -> file_system::Error { io::ErrorKind::PermissionDenied => file_system::Error::PermissionDenied, io::ErrorKind::NotFound => file_system::Error::NotFound, io::ErrorKind::AlreadyExists => file_system::Error::AlreadyExists, - io::ErrorKind::InvalidInput => file_system::Error::InvalidPath, + io::ErrorKind::InvalidInput => file_system::Error::InvalidParameter, io::ErrorKind::InvalidData => file_system::Error::InvalidFile, _ => file_system::Error::Unknown, } From d2ac46a1c40a3fef13457e7d0100268799b10a9e Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:31:49 +0100 Subject: [PATCH 54/78] feat: integrate smoltcp stack and implement TunTap controller for network interface management --- drivers/std/Cargo.toml | 1 + drivers/std/src/lib.rs | 22 ++-- drivers/std/src/network/error.rs | 45 -------- drivers/std/src/network/mod.rs | 5 - drivers/std/src/network/resolver.rs | 13 --- drivers/std/src/tuntap/controller.rs | 38 +++++++ drivers/std/src/tuntap/mod.rs | 158 +++++++++++++++++++++++++++ 7 files changed, 204 insertions(+), 78 deletions(-) delete mode 100644 drivers/std/src/network/error.rs delete mode 100644 drivers/std/src/network/mod.rs delete mode 100644 drivers/std/src/network/resolver.rs create mode 100644 drivers/std/src/tuntap/controller.rs create mode 100644 drivers/std/src/tuntap/mod.rs diff --git a/drivers/std/Cargo.toml b/drivers/std/Cargo.toml index 95a7a6fd..06f01986 100644 --- a/drivers/std/Cargo.toml +++ b/drivers/std/Cargo.toml @@ -23,6 +23,7 @@ embassy-executor = { workspace = true, default-features = false, features = [ "arch-std", "executor-thread", ] } +smoltcp = { workspace = true, features = ["phy-tuntap_interface"] } [dev-dependencies] little_fs = { workspace = true } diff --git a/drivers/std/src/lib.rs b/drivers/std/src/lib.rs index 16dfd309..2f283033 100644 --- a/drivers/std/src/lib.rs +++ b/drivers/std/src/lib.rs @@ -1,22 +1,14 @@ #![cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] -pub mod network; - -pub mod memory; - -pub mod io; - -pub mod executor; - -pub mod log; - -pub mod loader; - -pub mod drive_file; - pub mod console; - pub mod devices; +pub mod drive_file; +pub mod executor; +pub mod io; +pub mod loader; +pub mod log; +pub mod memory; +pub mod tuntap; pub extern crate memory as memory_exported; diff --git a/drivers/std/src/network/error.rs b/drivers/std/src/network/error.rs deleted file mode 100644 index 443cf872..00000000 --- a/drivers/std/src/network/error.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::io::{self, ErrorKind}; - -use network::Error; - -pub fn into_socket_error(error: io::Error) -> Error { - match error.kind() { - ErrorKind::NotFound => Error::NotFound, - ErrorKind::PermissionDenied => Error::PermissionDenied, - ErrorKind::ConnectionRefused => Error::ConnectionRefused, - ErrorKind::ConnectionReset => Error::ConnectionReset, - ErrorKind::ConnectionAborted => Error::ConnectionAborted, - ErrorKind::HostUnreachable => Error::HostUnreachable, - ErrorKind::NetworkUnreachable => Error::NetworkUnreachable, - ErrorKind::NotConnected => Error::NotConnected, - ErrorKind::AddrInUse => Error::AddressInUse, - ErrorKind::AddrNotAvailable => Error::AddressNotAvailable, - ErrorKind::NetworkDown => Error::NetworkDown, - ErrorKind::BrokenPipe => Error::BrokenPipe, - ErrorKind::AlreadyExists => Error::AlreadyExists, - ErrorKind::WouldBlock => Error::WouldBlock, - - // ErrorKind::FilesystemLoop => Error::Filesystem_loop, - ErrorKind::InvalidInput => Error::InvalidInput, - ErrorKind::InvalidData => Error::InvalidData, - ErrorKind::TimedOut => Error::TimedOut, - ErrorKind::WriteZero => Error::WriteZero, - ErrorKind::StorageFull => Error::StorageFull, - - // ErrorKind::FilesystemQuotaExceeded => Error::Filesystem_quota_exceeded, - ErrorKind::ResourceBusy => Error::ResourceBusy, - - ErrorKind::Deadlock => Error::Deadlock, - // ErrorKind::CrossesDevices => todo!(), - - // ErrorKind::InvalidFilename => todo!(), - ErrorKind::ArgumentListTooLong => todo!(), - ErrorKind::Interrupted => Error::Interrupted, - ErrorKind::Unsupported => Error::Unsupported, - ErrorKind::UnexpectedEof => Error::UnexpectedEndOfFile, - ErrorKind::OutOfMemory => Error::OutOfMemory, - // ErrorKind::InProgress => todo!(), - ErrorKind::Other => Error::Other, - _ => todo!(), - } -} diff --git a/drivers/std/src/network/mod.rs b/drivers/std/src/network/mod.rs deleted file mode 100644 index b7e53371..00000000 --- a/drivers/std/src/network/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod error; -mod resolver; - -pub use error::*; -pub use resolver::*; diff --git a/drivers/std/src/network/resolver.rs b/drivers/std/src/network/resolver.rs deleted file mode 100644 index f997d16b..00000000 --- a/drivers/std/src/network/resolver.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub struct Resolver; - -impl Default for Resolver { - fn default() -> Self { - Self::new() - } -} - -impl Resolver { - pub fn new() -> Self { - Self - } -} diff --git a/drivers/std/src/tuntap/controller.rs b/drivers/std/src/tuntap/controller.rs new file mode 100644 index 00000000..aa0124d1 --- /dev/null +++ b/drivers/std/src/tuntap/controller.rs @@ -0,0 +1,38 @@ +use file_system::{ + ControlCommand, ControlCommandIdentifier, DirectBaseOperations, DirectCharacterDevice, Error, + MountOperations, +}; +use network::{GET_KIND, InterfaceKind}; +use shared::AnyByLayout; + +pub struct TunTapControllerDevice; + +impl DirectBaseOperations for TunTapControllerDevice { + fn read(&self, _: &mut [u8], _: file_system::Size) -> file_system::Result { + Err(Error::UnsupportedOperation) + } + + fn write(&self, _: &[u8], _: file_system::Size) -> file_system::Result { + Err(Error::UnsupportedOperation) + } + + fn control( + &self, + command: ControlCommandIdentifier, + _: &AnyByLayout, + output: &mut AnyByLayout, + ) -> file_system::Result<()> { + match command { + GET_KIND::IDENTIFIER => { + let kind = GET_KIND::cast_output(output)?; + *kind = InterfaceKind::Ethernet; + Ok(()) + } + _ => Err(Error::UnsupportedOperation), + } + } +} + +impl MountOperations for TunTapControllerDevice {} + +impl DirectCharacterDevice for TunTapControllerDevice {} diff --git a/drivers/std/src/tuntap/mod.rs b/drivers/std/src/tuntap/mod.rs new file mode 100644 index 00000000..1b4bcb1e --- /dev/null +++ b/drivers/std/src/tuntap/mod.rs @@ -0,0 +1,158 @@ +mod controller; + +pub use controller::*; + +use network::{IpAddress, IpCidr, Route}; +use smoltcp::phy::{Medium, TunTapInterface}; +use std::process::Command; + +pub const IP_ADDRESSES: &[IpCidr] = &[ + IpCidr::new_ipv4([192, 168, 69, 1], 24), + IpCidr::new_ipv4([172, 0, 0, 1], 8), + IpCidr::new_ipv6([0xfdaa, 0, 0, 0, 0, 0, 0, 1], 64), + IpCidr::new_ipv6([0xfe80, 0, 0, 0, 0, 0, 0, 1], 64), +]; + +pub const ROUTES: &[Route] = &[ + Route::new_default_ipv4([192, 168, 69, 100]), + Route::new_default_ipv6([0xfe80, 0, 0, 0, 0, 0, 0, 100]), +]; + +pub const DEFAULT_DNS_SERVERS: &[IpAddress] = &[ + IpAddress::new_ipv4([1, 1, 1, 1]), + IpAddress::new_ipv4([1, 0, 0, 1]), + IpAddress::new_ipv6([0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1111]), + IpAddress::new_ipv6([0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1001]), +]; + +fn interface_exists(name: &str) -> bool { + std::fs::metadata(format!("/sys/class/net/{}", name)).is_ok() +} + +fn setup_tap_interface(name: &str) -> Result<(), String> { + // Get the user from SUDO_USER environment variable + log::information!( + "Setting up TAP interface: {} (sudo permissions required)", + name + ); + + let user = std::env::var("SUDO_USER") + .unwrap_or_else(|_| std::env::var("USER").unwrap_or_else(|_| "root".to_string())); + + log::information!("Using user: {}", user); + + let commands: &[&[&str]] = &[ + &[ + "ip", "tuntap", "add", "name", name, "mode", "tap", "user", &user, + ], + &["ip", "link", "set", name, "up"], + &["ip", "addr", "add", "192.168.69.100/24", "dev", name], + &["ip", "-6", "addr", "add", "fe80::100/64", "dev", name], + &["ip", "-6", "addr", "add", "fdaa::100/64", "dev", name], + &[ + "iptables", + "-t", + "nat", + "-I", // Change -A to -I + "POSTROUTING", + "1", // Insert at position 1 + "-s", + "192.168.69.0/24", + "-j", + "MASQUERADE", + ], + &[ + "iptables", + "-I", // Change -A to -I + "FORWARD", + "1", // Insert at position 1 + "-i", + name, + "-s", + "192.168.69.0/24", + "-j", + "ACCEPT", + ], + &[ + "iptables", + "-I", // Change -A to -I + "FORWARD", + "1", // Insert at position 1 + "-o", + name, + "-d", + "192.168.69.0/24", + "-j", + "ACCEPT", + ], + &["sysctl", "-w", "net.ipv4.ip_forward=1"], + ]; + + // Commands that can fail if already exist (routes) + let optional_commands: &[&[&str]] = &[ + &["ip", "-6", "route", "add", "fe80::/64", "dev", name], + &["ip", "-6", "route", "add", "fdaa::/64", "dev", name], + ]; + + for &cmd_args in commands { + let output = Command::new("sudo") + .args(cmd_args) + .output() + .map_err(|e| format!("Failed to execute command: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!( + "Command 'sudo {}' failed: {}", + cmd_args.join(" "), + stderr + )); + } + } + + // Execute optional commands (ignore "File exists" errors) + for &cmd_args in optional_commands { + let output = Command::new("sudo") + .args(cmd_args) + .output() + .map_err(|e| format!("Failed to execute command: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + // Ignore "File exists" errors for routes + if !stderr.contains("File exists") { + log::warning!("Command 'sudo {}' failed: {}", cmd_args.join(" "), stderr); + } + } + } + + Ok(()) +} + +pub fn new(name: &str, tun: bool, tap: bool) -> Option<(TunTapInterface, TunTapControllerDevice)> { + let medium = if tun { + Medium::Ip + } else if tap { + Medium::Ethernet + } else { + log::error!("Either TUN or TAP mode must be specified."); + return None; + }; + + if !interface_exists(name) { + if let Err(e) = setup_tap_interface(name) { + log::error!("Failed to setup TAP interface: {}", e); + return None; + } + } else { + log::information!("TAP interface {} already exists.", name); + } + + let tuntap_device = TunTapInterface::new(name, medium) + .map_err(|e| log::error!("Failed to create TUN/TAP device: {}", e)) + .ok()?; + + let controller = TunTapControllerDevice {}; + + Some((tuntap_device, controller)) +} From daa254944a3e29655c6df9bd9a6e74a093d41e2a Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:32:02 +0100 Subject: [PATCH 55/78] feat: enhance initialize function to support network management and interface setup --- modules/testing/Cargo.toml | 1 + modules/testing/src/lib.rs | 88 +++++++++++++++++++++++++++----------- 2 files changed, 65 insertions(+), 24 deletions(-) diff --git a/modules/testing/Cargo.toml b/modules/testing/Cargo.toml index eea1b406..589c8882 100644 --- a/modules/testing/Cargo.toml +++ b/modules/testing/Cargo.toml @@ -19,3 +19,4 @@ drivers_shared = { workspace = true } drivers_std = { workspace = true } executable = { workspace = true } abi_definitions = { workspace = true } +network = { workspace = true } diff --git a/modules/testing/src/lib.rs b/modules/testing/src/lib.rs index 95909715..0dd410b9 100644 --- a/modules/testing/src/lib.rs +++ b/modules/testing/src/lib.rs @@ -9,11 +9,12 @@ use drivers_native::window_screen; use drivers_shared::devices::RandomDevice; use drivers_std::{devices::TimeDevice, log::Logger}; use executable::Standard; -use file_system::MemoryDevice; +use file_system::{AccessFlags, MemoryDevice}; +use network::{ADD_DNS_SERVER, ADD_IP_ADDRESS, ADD_ROUTE}; use users::GroupIdentifier; -use virtual_file_system::{ItemStatic, create_default_hierarchy, mount_static}; +use virtual_file_system::{File, ItemStatic, create_default_hierarchy, mount_static}; -pub async fn initialize(graphics_enabled: bool) -> Standard { +pub async fn initialize(graphics_enabled: bool, network_enabled: bool) -> Standard { log::initialize(&Logger).unwrap(); let task_manager = task::initialize(); @@ -71,7 +72,7 @@ pub async fn initialize(graphics_enabled: bool) -> Standard { let file_system = little_fs::FileSystem::new_format(memory_device, 256).unwrap(); let virtual_file_system = - virtual_file_system::initialize(task_manager, users, time, file_system, None).unwrap(); + virtual_file_system::initialize(task_manager, users, time, file_system).unwrap(); let task = task_manager.get_current_task_identifier().await; @@ -88,31 +89,44 @@ pub async fn initialize(graphics_enabled: bool) -> Standard { .await .unwrap(); - let group_identifier = GroupIdentifier::new(1000); + if network_enabled { + let network_manager = network::initialize( + task_manager, + virtual_file_system, + &drivers_shared::devices::RandomDevice, + ); - authentication::create_group(virtual_file_system, "administrator", Some(group_identifier)) - .await - .unwrap(); + let (interface_device, controller_device) = + drivers_std::tuntap::new("xila0", false, true).unwrap(); - authentication::create_user( - virtual_file_system, - "administrator", - "", - group_identifier, - None, - ) - .await - .unwrap(); + network_manager + .mount_interface(task, "tunnel0", interface_device, controller_device, None) + .await + .expect("Failed to mount network interface."); - task_manager - .set_environment_variable(task, "Paths", "/") + let mut file = File::open( + virtual_file_system, + task, + "/devices/network/tunnel0", + AccessFlags::READ_WRITE.into(), + ) .await - .unwrap(); + .expect("Failed to open network interface file."); - task_manager - .set_environment_variable(task, "Host", "xila") - .await - .unwrap(); + for ip_cidr in drivers_std::tuntap::IP_ADDRESSES { + file.control(ADD_IP_ADDRESS, ip_cidr).await.ok(); + } + + for route in drivers_std::tuntap::ROUTES { + file.control(ADD_ROUTE, route).await.ok(); + } + + for dns_server in drivers_std::tuntap::DEFAULT_DNS_SERVERS { + file.control(ADD_DNS_SERVER, dns_server).await.ok(); + } + + file.close(virtual_file_system).await.unwrap(); + } mount_static!( virtual_file_system, @@ -149,6 +163,32 @@ pub async fn initialize(graphics_enabled: bool) -> Standard { .await .unwrap(); + let group_identifier = GroupIdentifier::new(1000); + + authentication::create_group(virtual_file_system, "administrator", Some(group_identifier)) + .await + .unwrap(); + + authentication::create_user( + virtual_file_system, + "administrator", + "", + group_identifier, + None, + ) + .await + .unwrap(); + + task_manager + .set_environment_variable(task, "Paths", "/") + .await + .unwrap(); + + task_manager + .set_environment_variable(task, "Host", "xila") + .await + .unwrap(); + Standard::open( &"/devices/standard_in", &"/devices/standard_out", From 4f90fb22af454aa6e8eb5b17e192e5aaa1d165df Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:33:14 +0100 Subject: [PATCH 56/78] feat: add network dependency and expose network module for host feature --- Cargo.toml | 4 +++- modules/network/Cargo.toml | 17 ----------------- src/lib.rs | 2 ++ 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 60a8cf5b..5ca75638 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ shared = { workspace = true, optional = true } synchronization = { workspace = true, optional = true } host_bindings = { workspace = true, optional = true } bootsplash = { workspace = true, optional = true } +network = { workspace = true, optional = true } [features] default = [] @@ -68,6 +69,7 @@ host = [ "dep:synchronization", "dep:bootsplash", "abi_definitions", + "dep:network", ] abi_definitions = ["dep:abi_definitions"] virtual_machine = ["dep:virtual_machine", "dep:host_bindings"] @@ -138,7 +140,7 @@ embassy-futures = { version = "0.1" } embassy-sync = { version = "0.7" } embassy-time = { version = "0.5" } critical-section = { version = "1.2" } -embassy-net = { version = "0.7", default-features = false } +smoltcp = { version = "0.12" } linked_list_allocator = { version = "0.10", default-features = false, features = [ "const_mut_refs", ] } diff --git a/modules/network/Cargo.toml b/modules/network/Cargo.toml index c3ae4eef..1f676896 100644 --- a/modules/network/Cargo.toml +++ b/modules/network/Cargo.toml @@ -13,23 +13,6 @@ log = { workspace = true } virtual_file_system = { workspace = true } shared = { workspace = true } -embassy-net = { workspace = true, features = [ - "icmp", - "raw", - "udp", - "tcp", - "dns", - "mdns", - "proto-ipv4", - "proto-ipv6", - "dhcpv4", - "dhcpv4-hostname", - "medium-ip", - "medium-ethernet", - "log", - "alloc", -] } -embassy-net-driver = { workspace = true } embedded-io-async = { workspace = true } embassy-time = { workspace = true } embassy-futures = { workspace = true } diff --git a/src/lib.rs b/src/lib.rs index 85d02715..10db419b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,8 @@ pub use log; #[cfg(feature = "host")] pub use memory; #[cfg(feature = "host")] +pub use network; +#[cfg(feature = "host")] pub use shared; #[cfg(feature = "host")] pub use synchronization; From a82e506613ab1748dad8ed5a94b630e79bfc468e Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:33:31 +0100 Subject: [PATCH 57/78] feat: update testing initialization to include additional parameter for consistency --- modules/bindings/host/tests/graphics.rs | 2 +- modules/virtual_machine/tests/test.rs | 3 ++- modules/virtual_machine/tests/test_2.rs | 2 +- modules/virtual_machine/tests/test_3.rs | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/bindings/host/tests/graphics.rs b/modules/bindings/host/tests/graphics.rs index dcd9f9e7..b1c96cbb 100644 --- a/modules/bindings/host/tests/graphics.rs +++ b/modules/bindings/host/tests/graphics.rs @@ -15,7 +15,7 @@ async fn test() { let binary_path = build_crate(&"host_bindings_wasm_test").unwrap(); let binary_buffer = fs::read(&binary_path).unwrap(); - let standard = testing::initialize(true).await.split(); + let standard = testing::initialize(true, false).await.split(); let virtual_machine = virtual_machine::initialize(&[&host_bindings::GraphicsBindings]); diff --git a/modules/virtual_machine/tests/test.rs b/modules/virtual_machine/tests/test.rs index e873cc7e..42657fa1 100644 --- a/modules/virtual_machine/tests/test.rs +++ b/modules/virtual_machine/tests/test.rs @@ -31,7 +31,8 @@ const FUNCTIONS: [FunctionDescriptor; 0] = Function_descriptors! {}; #[ignore] #[test] async fn integration_test() { - let (standard_in, standard_out, standard_error) = testing::initialize(false).await.split(); + let (standard_in, standard_out, standard_error) = + testing::initialize(false, false).await.split(); let task_instance = task::get_instance(); let task = task_instance.get_current_task_identifier().await; diff --git a/modules/virtual_machine/tests/test_2.rs b/modules/virtual_machine/tests/test_2.rs index 6cbf90c5..33d0fc74 100644 --- a/modules/virtual_machine/tests/test_2.rs +++ b/modules/virtual_machine/tests/test_2.rs @@ -26,7 +26,7 @@ const FUNCTIONS: [FunctionDescriptor; 0] = Function_descriptors! {}; #[ignore] #[test] async fn integration_test_2() { - let standard = testing::initialize(false).await.split(); + let standard = testing::initialize(false, false).await.split(); let task_instance = task::get_instance(); let task = task_instance.get_current_task_identifier().await; diff --git a/modules/virtual_machine/tests/test_3.rs b/modules/virtual_machine/tests/test_3.rs index 53404df9..ed7b78ba 100644 --- a/modules/virtual_machine/tests/test_3.rs +++ b/modules/virtual_machine/tests/test_3.rs @@ -32,7 +32,8 @@ const FUNCTIONS: [FunctionDescriptor; 0] = Function_descriptors! {}; #[ignore] #[test] async fn integration_test() { - let (standard_in, standard_out, standard_error) = testing::initialize(false).await.split(); + let (standard_in, standard_out, standard_error) = + testing::initialize(false, false).await.split(); let task_instance = task::get_instance(); let task = task_instance.get_current_task_identifier().await; From 87e2c4520fdb3b512e9b06c5d09cbc267a35b3c9 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:33:42 +0100 Subject: [PATCH 58/78] feat: update testing initialization to include additional parameter for consistency --- executables/calculator/tests/test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/executables/calculator/tests/test.rs b/executables/calculator/tests/test.rs index da27e21a..1123071a 100644 --- a/executables/calculator/tests/test.rs +++ b/executables/calculator/tests/test.rs @@ -17,7 +17,7 @@ async fn main() { let binary_path = build_crate("calculator").unwrap(); let binary_buffer = fs::read(binary_path).unwrap(); - let standard = testing::initialize(true).await.split(); + let standard = testing::initialize(true, false).await.split(); let task_manager = task::get_instance(); let virtual_machine = virtual_machine::initialize(&[&host_bindings::GraphicsBindings]); From 50de59a0a201fb3c946a6083238c6618b57eb65b Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:33:56 +0100 Subject: [PATCH 59/78] refactor: simplify new function signature in FileManagerExecutable --- executables/file_manager/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/executables/file_manager/src/lib.rs b/executables/file_manager/src/lib.rs index 23645f9c..86f23a4f 100644 --- a/executables/file_manager/src/lib.rs +++ b/executables/file_manager/src/lib.rs @@ -30,8 +30,8 @@ pub const SHORTCUT: &str = r#" pub struct FileManagerExecutable; impl FileManagerExecutable { - pub async fn new<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, + pub async fn new( + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, ) -> core::result::Result { let _ = virtual_file_system From d4479fd76b6617488d7c4290ac0a888ddef7fca8 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:34:06 +0100 Subject: [PATCH 60/78] feat: update FileManager to use LV_SYMBOL constants for button labels --- executables/file_manager/src/file_manager.rs | 24 +++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/executables/file_manager/src/file_manager.rs b/executables/file_manager/src/file_manager.rs index 49e2a2e9..ea934913 100644 --- a/executables/file_manager/src/file_manager.rs +++ b/executables/file_manager/src/file_manager.rs @@ -6,16 +6,15 @@ pub(crate) use alloc::{ vec::Vec, }; use core::ptr::null_mut; -use xila::graphics::{ - self, EventKind, Window, lvgl, - palette::{self, Hue}, -}; use xila::log; use xila::task; use xila::virtual_file_system::{Directory, get_instance}; use xila::{ file_system::{Kind, Path, PathOwned}, - graphics::symbols, + graphics::{ + self, EventKind, Window, lvgl, + palette::{self, Hue}, + }, }; pub struct FileManager { @@ -157,7 +156,7 @@ impl FileManager { return Err(Error::FailedToCreateObject); } let up_label = lvgl::lv_label_create(self.up_button); - lvgl::lv_label_set_text(up_label, symbols::UP.as_ptr()); + lvgl::lv_label_set_text(up_label, lvgl::LV_SYMBOL_UP as *const _ as *const i8); lvgl::lv_obj_center(up_label); // Remove event handler - events bubble up to window @@ -169,7 +168,7 @@ impl FileManager { } let home_label = lvgl::lv_label_create(self.home_button); - lvgl::lv_label_set_text(home_label, symbols::HOME.as_ptr()); + lvgl::lv_label_set_text(home_label, lvgl::LV_SYMBOL_HOME as *const _ as *const i8); lvgl::lv_obj_center(home_label); // Remove event handler - events bubble up to window @@ -182,7 +181,10 @@ impl FileManager { let refresh_label = lvgl::lv_label_create(self.refresh_button); - lvgl::lv_label_set_text(refresh_label, symbols::REFRESH.as_ptr()); + lvgl::lv_label_set_text( + refresh_label, + lvgl::LV_SYMBOL_REFRESH as *const _ as *const i8, + ); lvgl::lv_obj_center(refresh_label); // Remove event handler - events bubble up to window @@ -207,7 +209,7 @@ impl FileManager { } let go_label = lvgl::lv_label_create(self.go_button); - lvgl::lv_label_set_text(go_label, symbols::RIGHT.as_ptr()); + lvgl::lv_label_set_text(go_label, lvgl::LV_SYMBOL_RIGHT as *const _ as *const i8); lvgl::lv_obj_center(go_label); self.update_path_label(); @@ -284,8 +286,8 @@ impl FileManager { let file = &self.files[index]; let icon_symbol = match file.kind { - Kind::Directory => symbols::DIRECTORY, - _ => symbols::FILE, + Kind::Directory => lvgl::LV_SYMBOL_DIRECTORY, + _ => lvgl::LV_SYMBOL_FILE, }; let name_cstring = CString::new(file.name.clone()).unwrap(); From 0a4718dd22ff105cd385d0bbacfb177c9dc1c20b Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:34:13 +0100 Subject: [PATCH 61/78] feat: update testing initialization to include additional parameter for consistency --- executables/file_manager/tests/integration_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/executables/file_manager/tests/integration_test.rs b/executables/file_manager/tests/integration_test.rs index ebf628a7..c5bdb6be 100644 --- a/executables/file_manager/tests/integration_test.rs +++ b/executables/file_manager/tests/integration_test.rs @@ -13,7 +13,7 @@ async fn main() { task, virtual_file_system, }; - let standard = testing::initialize(true).await; + let standard = testing::initialize(true, false).await; mount_executables!( virtual_file_system::get_instance(), From 6eea969d08044ec04a049a03cec0b3135fad0763 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:34:44 +0100 Subject: [PATCH 62/78] feat: refactor AboutTab to use lv_list for UI representation and improve memory display --- executables/settings/src/tabs/about.rs | 133 +++++++++++-------------- 1 file changed, 59 insertions(+), 74 deletions(-) diff --git a/executables/settings/src/tabs/about.rs b/executables/settings/src/tabs/about.rs index 1dd75511..a0c8029f 100644 --- a/executables/settings/src/tabs/about.rs +++ b/executables/settings/src/tabs/about.rs @@ -1,36 +1,31 @@ use crate::error::Result; -use alloc::{vec, vec::Vec}; -use core::ptr::null_mut; -use embedded_io::Write as _; +use alloc::{ffi::CString, format}; +use core::{ffi::CStr, ptr::null_mut}; use xila::{ about, graphics::{ Event, - lvgl::{self, lv_pct}, + lvgl::{self}, }, internationalization::{self, translate}, + memory, + shared::{BYTES_SUFFIX, Unit}, }; -const TABLE_ROWS: usize = 6; - pub struct AboutTab { container: *mut lvgl::lv_obj_t, + list: *mut lvgl::lv_obj_t, } impl AboutTab { pub fn new() -> Self { Self { container: null_mut() as *mut _, + list: null_mut() as *mut _, } } - fn str_to_cstring(buffer: &mut Vec, s: &str) -> Result<*const i8> { - buffer.clear(); - write!(buffer, "{}\0", s)?; - Ok(buffer.as_ptr() as *const i8) - } - - pub fn create_ui( + pub async fn create_ui( &mut self, parent_tabview: *mut lvgl::lv_obj_t, ) -> Result<*mut lvgl::lv_obj_t> { @@ -41,76 +36,66 @@ impl AboutTab { return Err(crate::error::Error::FailedToCreateUiElement); } - let table = unsafe { - let table = lvgl::lv_table_create(self.container); + // Create list + unsafe { + self.list = lvgl::lv_list_create(self.container); - if table.is_null() { + if self.list.is_null() { return Err(crate::error::Error::FailedToCreateUiElement); } - lvgl::lv_obj_align(table, lvgl::lv_align_t_LV_ALIGN_CENTER, 0, 0); - lvgl::lv_table_set_row_count(table, TABLE_ROWS as _); - lvgl::lv_table_set_column_count(table, 2); - lvgl::lv_obj_set_height(table, lv_pct(100)); - lvgl::lv_table_set_column_width(table, 0, 100); - lvgl::lv_table_set_column_width(table, 1, 200); + // List properties - fill container + lvgl::lv_obj_set_size(self.list, lvgl::lv_pct(100), lvgl::lv_pct(100)); + lvgl::lv_obj_set_style_pad_all(self.list, 0, lvgl::LV_STATE_DEFAULT); + } + + // Populate items - convert CStr to String + self.create_list_item(translate!(c"Operating System:"), c"Xila")?; + + let description = CString::new(about::get_description()) + .map_err(|_| crate::error::Error::FailedToCreateUiElement)?; + self.create_list_item(translate!(c"Description:"), &description)?; + + let authors = CString::new(about::get_authors()) + .map_err(|_| crate::error::Error::FailedToCreateUiElement)?; + self.create_list_item(translate!(c"Developed by:"), &authors)?; + + let license = CString::new(about::get_license()) + .map_err(|_| crate::error::Error::FailedToCreateUiElement)?; + self.create_list_item(translate!(c"License:"), &license)?; + + let version = CString::new(about::get_version_string()) + .map_err(|_| crate::error::Error::FailedToCreateUiElement)?; + self.create_list_item(translate!(c"Version:"), &version)?; - table - }; + let locale = CString::new(format!( + "{} ({})", + internationalization::get_locale(), + internationalization::get_fallback_locale(), + ))?; + self.create_list_item(translate!(c"Locale:"), &locale)?; + let memory = memory::get_instance().get_total_size(); + let memory = Unit::new(memory as f32, BYTES_SUFFIX.symbol); + let memory = CString::new(format!("{}", memory)) + .map_err(|_| crate::error::Error::FailedToCreateUiElement)?; + self.create_list_item(translate!(c"Memory:"), &memory)?; + + Ok(self.container) + } + + fn create_list_item(&mut self, name: &CStr, value: &CStr) -> Result<()> { unsafe { - let mut buffer = vec![]; - - lvgl::lv_table_set_cell_value(table, 0, 0, c"Xila".as_ptr()); - lvgl::lv_table_set_cell_value( - table, - 0, - 1, - Self::str_to_cstring(&mut buffer, about::get_description())?, - ); - - lvgl::lv_table_set_cell_value(table, 1, 0, translate!(c"Developed by:").as_ptr()); - lvgl::lv_table_set_cell_value( - table, - 1, - 1, - Self::str_to_cstring(&mut buffer, about::get_authors())?, - ); - - lvgl::lv_table_set_cell_value(table, 2, 0, translate!(c"License:").as_ptr()); - lvgl::lv_table_set_cell_value( - table, - 2, - 1, - Self::str_to_cstring(&mut buffer, about::get_license())?, - ); - - lvgl::lv_table_set_cell_value(table, 3, 0, translate!(c"Version:").as_ptr()); - lvgl::lv_table_set_cell_value( - table, - 3, - 1, - Self::str_to_cstring(&mut buffer, about::get_version_string())?, - ); - - lvgl::lv_table_set_cell_value(table, 4, 0, translate!(c"Locale:").as_ptr()); - lvgl::lv_table_set_cell_value( - table, - 4, - 1, - Self::str_to_cstring(&mut buffer, internationalization::get_locale())?, - ); - - lvgl::lv_table_set_cell_value(table, 5, 0, translate!(c"Fallback:").as_ptr()); - lvgl::lv_table_set_cell_value( - table, - 5, - 1, - Self::str_to_cstring(&mut buffer, internationalization::get_fallback_locale())?, - ); + lvgl::lv_list_add_text(self.list, name.as_ptr()); + + let button = lvgl::lv_list_add_button(self.list, core::ptr::null(), value.as_ptr()); + + if button.is_null() { + return Err(crate::error::Error::FailedToCreateUiElement); + } } - Ok(self.container) + Ok(()) } pub async fn handle_event(&mut self, _event: &Event) -> bool { From d401e0999dd4404169e064b395323bf60dfde784 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:35:36 +0100 Subject: [PATCH 63/78] refactor: simplify function signature in SettingsExecutable::new --- executables/settings/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/executables/settings/src/lib.rs b/executables/settings/src/lib.rs index 3ebfef37..4b1869c5 100644 --- a/executables/settings/src/lib.rs +++ b/executables/settings/src/lib.rs @@ -37,8 +37,8 @@ pub fn get_shortcut() -> alloc::string::String { pub struct SettingsExecutable; impl SettingsExecutable { - pub async fn new<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, + pub async fn new( + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, ) -> core::result::Result { let _ = virtual_file_system From c9ae63c8371af58ca7451c1febd96ef07d986894 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:35:57 +0100 Subject: [PATCH 64/78] feat: integrate NetworkTab and update Settings to include NetworkTab functionality --- executables/settings/src/settings.rs | 13 +- executables/settings/src/tabs/general.rs | 2 +- executables/settings/src/tabs/mod.rs | 13 +- .../settings/src/tabs/network/interface.rs | 392 ++++++++++++++++++ executables/settings/src/tabs/network/mod.rs | 242 +++++++++++ executables/settings/src/tabs/password.rs | 60 +-- 6 files changed, 663 insertions(+), 59 deletions(-) create mode 100644 executables/settings/src/tabs/network/interface.rs create mode 100644 executables/settings/src/tabs/network/mod.rs diff --git a/executables/settings/src/settings.rs b/executables/settings/src/settings.rs index 7b845587..3ddc743a 100644 --- a/executables/settings/src/settings.rs +++ b/executables/settings/src/settings.rs @@ -6,11 +6,11 @@ use xila::graphics::{ }; use crate::error::Result; -use crate::tabs::{AboutTab, GeneralTab, PasswordTab, Tab}; +use crate::tabs::{AboutTab, GeneralTab, NetworkTab, PasswordTab, Tab}; pub struct Settings { window: Window, - tabs: [Tab; 3], + tabs: [Tab; 4], } #[derive(Clone)] @@ -42,12 +42,15 @@ impl Settings { let mut tabs = [ Tab::General(GeneralTab::new()), Tab::Password(PasswordTab::new()), + Tab::Network(NetworkTab::new()), Tab::About(AboutTab::new()), ]; - tabs.iter_mut().for_each(|tab| { - tab.create_ui(tabview).expect("Failed to create tab UI"); - }); + for tab in &mut tabs { + tab.create_ui(tabview) + .await + .expect("Failed to create tab UI"); + } let manager = Self { window, tabs }; diff --git a/executables/settings/src/tabs/general.rs b/executables/settings/src/tabs/general.rs index b7480c24..efca75a9 100644 --- a/executables/settings/src/tabs/general.rs +++ b/executables/settings/src/tabs/general.rs @@ -13,7 +13,7 @@ impl GeneralTab { } } - pub fn create_ui( + pub async fn create_ui( &mut self, parent_tabview: *mut lvgl::lv_obj_t, ) -> Result<*mut lvgl::lv_obj_t> { diff --git a/executables/settings/src/tabs/mod.rs b/executables/settings/src/tabs/mod.rs index 1cefeceb..0230472a 100644 --- a/executables/settings/src/tabs/mod.rs +++ b/executables/settings/src/tabs/mod.rs @@ -6,14 +6,16 @@ pub enum Tab { General(GeneralTab), Password(PasswordTab), About(AboutTab), + Network(NetworkTab), } impl Tab { - pub fn create_ui(&mut self, parent: *mut lvgl::lv_obj_t) -> Result<*mut lvgl::lv_obj_t> { + pub async fn create_ui(&mut self, parent: *mut lvgl::lv_obj_t) -> Result<*mut lvgl::lv_obj_t> { match self { - Tab::General(tab) => tab.create_ui(parent), - Tab::Password(tab) => tab.create_ui(parent), - Tab::About(tab) => tab.create_ui(parent), + Tab::General(tab) => tab.create_ui(parent).await, + Tab::Password(tab) => tab.create_ui(parent).await, + Tab::About(tab) => tab.create_ui(parent).await, + Tab::Network(tab) => tab.create_ui(parent).await, } } @@ -22,6 +24,7 @@ impl Tab { Tab::General(tab) => tab.handle_event(event).await, Tab::Password(tab) => tab.handle_event(event).await, Tab::About(tab) => tab.handle_event(event).await, + Tab::Network(tab) => tab.handle_event(event).await, } } } @@ -29,8 +32,10 @@ impl Tab { // Re-export tab modules pub mod about; pub mod general; +pub mod network; pub mod password; pub use about::AboutTab; pub use general::GeneralTab; +pub use network::NetworkTab; pub use password::PasswordTab; diff --git a/executables/settings/src/tabs/network/interface.rs b/executables/settings/src/tabs/network/interface.rs new file mode 100644 index 00000000..5f42cdfd --- /dev/null +++ b/executables/settings/src/tabs/network/interface.rs @@ -0,0 +1,392 @@ +use crate::{Error, Result, tabs::network::open_interface}; +use alloc::string::String; +use core::fmt::Write; +use core::ptr::null_mut; +use xila::{ + graphics::{EventKind, lvgl}, + internationalization::translate, + log, + network::{ + GET_DNS_SERVER, GET_DNS_SERVER_COUNT, GET_HARDWARE_ADDRESS, GET_IP_ADDRESS, + GET_IP_ADDRESS_COUNT, GET_ROUTE, GET_ROUTE_COUNT, MacAddress, + }, + virtual_file_system::{self, File, FileControlIterator}, +}; + +#[derive(Default)] +struct IpConfigurationTab { + pub _container: *mut lvgl::lv_obj_t, + pub _radio_group: *mut lvgl::lv_obj_t, + pub _radio_none: *mut lvgl::lv_obj_t, + pub _radio_dhcp: *mut lvgl::lv_obj_t, + pub _radio_static: *mut lvgl::lv_obj_t, + pub _address_input: *mut lvgl::lv_obj_t, + pub _gateway_input: *mut lvgl::lv_obj_t, + pub _dns_inputs: [*mut lvgl::lv_obj_t; 3], +} + +struct GeneralTab { + pub _list: *mut lvgl::lv_obj_t, +} + +pub struct InterfacePanel { + main_container: *mut lvgl::lv_obj_t, + + cancel_button: *mut lvgl::lv_obj_t, + apply_button: *mut lvgl::lv_obj_t, + interface: String, +} + +impl InterfacePanel { + async fn create_general_tab( + parent: *mut lvgl::lv_obj_t, + file: &mut File, + ) -> Result { + unsafe { + let list = lvgl::lv_list_create(parent); + + let mut format_buffer = String::with_capacity(64); + + { + let hardware_address = Self::get_mac_address(file).await?; + + lvgl::lv_obj_set_size(list, lvgl::lv_pct(100), lvgl::lv_pct(100)); + + write!( + format_buffer, + "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}\0", + hardware_address[0], + hardware_address[1], + hardware_address[2], + hardware_address[3], + hardware_address[4], + hardware_address[5] + ) + .ok(); + lvgl::lv_list_add_text(list, translate!(c"MAC Address").as_ptr()); + + lvgl::lv_list_add_button(list, null_mut(), format_buffer.as_ptr() as _); + } + + { + let mut ip_addresses = Self::get_ip_addresses(file).await?; + + lvgl::lv_list_add_text(list, translate!(c"Address").as_ptr()); + + while let Some(ip) = ip_addresses.next().await? { + format_buffer.clear(); + write!(format_buffer, "{}\0", ip).ok(); + + lvgl::lv_list_add_button(list, null_mut(), format_buffer.as_ptr() as _); + } + } + + { + let mut routes = Self::get_routes(file).await?; + + lvgl::lv_list_add_text(list, translate!(c"Routes").as_ptr()); + + while let Some(route) = routes.next().await? { + format_buffer.clear(); + write!(format_buffer, "{} via {}\0", route.cidr, route.via_router).ok(); + lvgl::lv_list_add_button(list, null_mut(), format_buffer.as_ptr() as _); + } + } + + { + let mut dns_servers = Self::get_dns_servers(file).await?; + + lvgl::lv_list_add_text(list, translate!(c"DNS Servers").as_ptr()); + + while let Some(dns) = dns_servers.next().await? { + format_buffer.clear(); + write!(format_buffer, "{}\0", dns).ok(); + lvgl::lv_list_add_button(list, null_mut(), format_buffer.as_ptr() as _); + } + } + + Ok(GeneralTab { _list: list }) + } + } + + fn create_ip_configuration_tab( + parent: *mut lvgl::lv_obj_t, + is_ipv6: bool, + tab_name: &[u8], + ) -> Result { + unsafe { + // Create tab for this IP version + let container = lvgl::lv_tabview_add_tab(parent, tab_name.as_ptr() as *const _); + if container.is_null() { + return Err(Error::FailedToCreateObject); + } + + lvgl::lv_obj_set_size(container, lvgl::lv_pct(100), lvgl::lv_pct(100)); + lvgl::lv_obj_set_flex_flow(container, lvgl::lv_flex_flow_t_LV_FLEX_FLOW_COLUMN); + lvgl::lv_obj_set_flex_align( + container, + lvgl::lv_flex_align_t_LV_FLEX_ALIGN_START, + lvgl::lv_flex_align_t_LV_FLEX_ALIGN_START, + lvgl::lv_flex_align_t_LV_FLEX_ALIGN_CENTER, + ); + lvgl::lv_obj_set_style_pad_all(container, 10, lvgl::LV_STATE_DEFAULT); + + // Label: Configuration Mode + let mode_label = lvgl::lv_label_create(container); + let mode_text = translate!(c"Configuration Mode"); + lvgl::lv_label_set_text(mode_label, mode_text.as_ptr() as *const _); + + // Radio group for configuration modew + let radio_group = lvgl::lv_obj_create(container); + lvgl::lv_obj_set_size(radio_group, lvgl::LV_SIZE_CONTENT, lvgl::LV_SIZE_CONTENT); + lvgl::lv_obj_set_flex_flow(radio_group, lvgl::lv_flex_flow_t_LV_FLEX_FLOW_ROW); + + // Radio: None + let radio_none = lvgl::lv_radiobox_create(radio_group); + let none_text = translate!(c"None"); + lvgl::lv_checkbox_set_text(radio_none, none_text.as_ptr() as *const _); + + // Radio: DHCP + let radio_dhcp = lvgl::lv_radiobox_create(radio_group); + let dhcp_text = translate!(c"DHCP"); + lvgl::lv_checkbox_set_text(radio_dhcp, dhcp_text.as_ptr() as *const _); + + // Radio: Static + let radio_static = lvgl::lv_radiobox_create(radio_group); + let static_text = translate!(c"Static"); + lvgl::lv_checkbox_set_text(radio_static, static_text.as_ptr() as *const _); + + // Container for static configuration (hidden by default) + + // IP Address + CIDR input + let address_label = lvgl::lv_label_create(container); + let address_text = translate!(c"Address"); + lvgl::lv_label_set_text(address_label, address_text.as_ptr() as *const _); + + let address_input = lvgl::lv_textarea_create(container); + if is_ipv6 { + lvgl::lv_textarea_set_placeholder_text( + address_input, + c"2001:0db8:85a3::8a2e:0370:7334/64".as_ptr(), + ); + } else { + lvgl::lv_textarea_set_placeholder_text(address_input, c"192.168.1.100/24".as_ptr()); + } + lvgl::lv_textarea_set_one_line(address_input, true); + + // Gateway input + let gateway_label = lvgl::lv_label_create(container); + let gateway_text = translate!(c"Gateway"); + lvgl::lv_label_set_text(gateway_label, gateway_text.as_ptr() as *const _); + + let gateway_input = lvgl::lv_textarea_create(container); + if is_ipv6 { + lvgl::lv_textarea_set_placeholder_text(gateway_input, c"fe80::1".as_ptr()); + } else { + lvgl::lv_textarea_set_placeholder_text(gateway_input, c"192.168.1.1".as_ptr()); + } + lvgl::lv_textarea_set_one_line(gateway_input, true); + + // DNS inputs + let dns_label = lvgl::lv_label_create(container); + let dns_text = translate!(c"DNS Servers"); + lvgl::lv_label_set_text(dns_label, dns_text.as_ptr() as *const _); + + let mut dns_inputs = [null_mut(); 3]; + + for (i, dns_input) in dns_inputs.iter_mut().enumerate() { + *dns_input = lvgl::lv_textarea_create(container); + + if is_ipv6 { + lvgl::lv_textarea_set_placeholder_text( + *dns_input, + if i == 0 { + c"2606:4700:4700::1111".as_ptr() + } else if i == 1 { + c"2001:4860:4860::8888".as_ptr() + } else { + c"2001:4860:4860::8844".as_ptr() + }, + ); + } else { + lvgl::lv_textarea_set_placeholder_text( + *dns_input, + if i == 0 { + c"1.1.1.1".as_ptr() + } else if i == 1 { + c"8.8.8.8".as_ptr() + } else { + c"8.8.4.4".as_ptr() + }, + ); + } + + lvgl::lv_textarea_set_one_line(*dns_input, true); + } + + // TODO: Connect radio buttons to show/hide static_container + // TODO: Implement radio button group behavior (only one can be selected) + + Ok(IpConfigurationTab { + _container: container, + _radio_group: radio_group, + _radio_none: radio_none, + _radio_dhcp: radio_dhcp, + _radio_static: radio_static, + _address_input: address_input, + _gateway_input: gateway_input, + _dns_inputs: dns_inputs, + }) + } + } + + async fn get_dns_servers(file: &mut File) -> Result> { + Ok(FileControlIterator::new(file, GET_DNS_SERVER_COUNT, GET_DNS_SERVER).await?) + } + + async fn get_routes(file: &mut File) -> Result> { + Ok(FileControlIterator::new(file, GET_ROUTE_COUNT, GET_ROUTE).await?) + } + + async fn get_ip_addresses(file: &mut File) -> Result> { + Ok(FileControlIterator::new(file, GET_IP_ADDRESS_COUNT, GET_IP_ADDRESS).await?) + } + + async fn get_mac_address(file: &mut File) -> Result { + Ok(file.control(GET_HARDWARE_ADDRESS, &()).await?) + } + + pub async fn new(interface: String, parent_tabview: *mut lvgl::lv_obj_t) -> Result { + // Create a container for the entire configuration panel + let main_container = unsafe { lvgl::lv_obj_create(parent_tabview) }; + if main_container.is_null() { + return Err(Error::FailedToCreateObject); + } + + unsafe { + lvgl::lv_obj_add_flag(main_container, lvgl::lv_obj_flag_t_LV_OBJ_FLAG_FLOATING); + lvgl::lv_obj_set_size(main_container, lvgl::lv_pct(100), lvgl::lv_pct(100)); + lvgl::lv_obj_set_flex_flow(main_container, lvgl::lv_flex_flow_t_LV_FLEX_FLOW_COLUMN); + lvgl::lv_obj_set_style_pad_all(main_container, 0, lvgl::LV_STATE_DEFAULT); + lvgl::lv_obj_set_style_border_width(main_container, 0, lvgl::LV_STATE_DEFAULT); + } + + // Create button container + let button_container = unsafe { lvgl::lv_obj_create(main_container) }; + if button_container.is_null() { + return Err(Error::FailedToCreateObject); + } + + unsafe { + lvgl::lv_obj_set_size(button_container, lvgl::lv_pct(100), lvgl::LV_SIZE_CONTENT); + lvgl::lv_obj_set_style_border_side( + button_container, + lvgl::lv_border_side_t_LV_BORDER_SIDE_BOTTOM, + lvgl::LV_PART_MAIN, + ); + } + + let cancel_button = unsafe { + // Cancel button + let cancel_button = lvgl::lv_button_create(button_container); + let cancel_label = lvgl::lv_label_create(cancel_button); + let cancel_text = translate!(c"Cancel"); + lvgl::lv_label_set_text(cancel_label, cancel_text.as_ptr() as *const _); + lvgl::lv_obj_set_align(cancel_button, lvgl::lv_align_t_LV_ALIGN_LEFT_MID); + + cancel_button + }; + + // Title + unsafe { + let title_label = lvgl::lv_label_create(button_container); + let title_text = c"Interface Configuration"; + lvgl::lv_label_set_text(title_label, title_text.as_ptr() as *const _); + lvgl::lv_obj_set_align(title_label, lvgl::lv_align_t_LV_ALIGN_CENTER); + } + + // Apply button + let apply_button = unsafe { + let apply_button = lvgl::lv_button_create(button_container); + let apply_label = lvgl::lv_label_create(apply_button); + let apply_text = translate!(c"Apply"); + lvgl::lv_label_set_text(apply_label, apply_text.as_ptr() as *const _); + lvgl::lv_obj_set_align(apply_button, lvgl::lv_align_t_LV_ALIGN_RIGHT_MID); + apply_button + }; + + // Create a tabview for IPv4 and IPv6 configurations + let config_tabview = unsafe { lvgl::lv_tabview_create(main_container) }; + if config_tabview.is_null() { + return Err(Error::FailedToCreateObject); + } + + unsafe { + lvgl::lv_obj_set_width(config_tabview, lvgl::lv_pct(100)); + lvgl::lv_obj_set_flex_grow(config_tabview, 1); + } + + // Create general tab + let general_tab = unsafe { + let tab = lvgl::lv_tabview_add_tab( + config_tabview, + translate!(c"General").as_ptr() as *const _, + ); + if tab.is_null() { + return Err(Error::FailedToCreateObject); + } + tab + }; + + // Create general tab content + let virtual_file_system = virtual_file_system::get_instance(); + + let mut file = open_interface(virtual_file_system, &interface).await?; + + let _general_tab = Self::create_general_tab(general_tab, &mut file).await?; + + file.close(virtual_file_system).await?; + + // Create IPv4 tab + let _ipv4_tab = Self::create_ip_configuration_tab(config_tabview, false, b"IPv4\0")?; + + // Create IPv6 tab + let _ipv6_tab = Self::create_ip_configuration_tab(config_tabview, true, b"IPv6\0")?; + + let interface = Self { + main_container, + cancel_button, + apply_button, + interface, + }; + + Ok(interface) + } + + pub async fn handle_event(&mut self, event: &xila::graphics::Event) -> bool { + // Allow only selection of one radio button at a time + + if event.code == EventKind::Clicked { + if event.target == self.cancel_button { + log::information!("Cancel button clicked in interface panel"); + // Close the panel without applying changes + return false; + } else if event.target == self.apply_button { + log::information!("Apply button clicked in interface panel"); + // Apply the configuration changes + return false; + } + } + + true + } +} + +impl Drop for InterfacePanel { + fn drop(&mut self) { + unsafe { + lvgl::lv_obj_delete(self.main_container); + log::information!("Interface panel for {} has been deleted", self.interface); + } + } +} diff --git a/executables/settings/src/tabs/network/mod.rs b/executables/settings/src/tabs/network/mod.rs new file mode 100644 index 00000000..25b71eac --- /dev/null +++ b/executables/settings/src/tabs/network/mod.rs @@ -0,0 +1,242 @@ +mod interface; + +use crate::{Error, Result}; +use alloc::{ffi::CString, string::ToString}; +use core::{ffi::CStr, ptr::null_mut, time::Duration}; +use interface::*; +use xila::{ + file_system::{AccessFlags, Path}, + graphics::{Event, EventKind, lvgl, symbol}, + log, + network::{self, InterfaceKind}, + virtual_file_system::{self, Directory, File, VirtualFileSystem}, +}; + +pub struct NetworkTab { + tab_container: *mut lvgl::lv_obj_t, + interfaces_list: *mut lvgl::lv_obj_t, + last_update: Duration, + configuration_panel: Option, + parent_tabview: *mut lvgl::lv_obj_t, +} + +impl NetworkTab { + pub const UPDATE_INTERVAL: Duration = Duration::from_secs(30); + + pub fn new() -> Self { + Self { + tab_container: null_mut(), + interfaces_list: null_mut(), + last_update: Duration::from_secs(0), + configuration_panel: None, + parent_tabview: null_mut(), + } + } + + pub async fn get_interface_kind_status( + &self, + interface_name: &str, + ) -> Result<(InterfaceKind, bool)> { + let virtual_file_system = virtual_file_system::get_instance(); + + let mut file = open_interface(virtual_file_system, interface_name).await?; + + let kind = file.control(network::GET_KIND, &()).await?; + let is_up = file.control(network::IS_LINK_UP, &()).await?; + + file.close(virtual_file_system).await?; + + Ok((kind, is_up)) + } + pub async fn update_interfaces(&mut self) -> Result<()> { + // Clear existing list items + unsafe { + lvgl::lv_obj_clean(self.interfaces_list); + } + + let virtual_file_system = xila::virtual_file_system::get_instance(); + + let task_manager = xila::task::get_instance(); + + let task = task_manager.get_current_task_identifier().await; + + let mut directory = + Directory::open(virtual_file_system, task, Path::NETWORK_DEVICES).await?; + + while let Some(entry) = directory.read().await? { + if entry.name == "." || entry.name == ".." { + continue; + } + + let (kind, is_up) = self.get_interface_kind_status(&entry.name).await?; + + let label_text = + CString::new(entry.name.as_str()).map_err(|_| Error::FailedToCreateUiElement)?; + + let symbol = match kind { + InterfaceKind::Ethernet => symbol::NETWORK_WIRED, + InterfaceKind::WiFi => symbol::WIFI, + InterfaceKind::Unknown => c"?", + }; + + unsafe { + let button = lvgl::lv_list_add_button( + self.interfaces_list, + symbol.as_ptr() as _, + label_text.as_ptr() as *const _, + ); + + // center button content + lvgl::lv_obj_set_style_pad_all(button, 10, lvgl::LV_STATE_DEFAULT); + lvgl::lv_obj_set_flex_align( + button, + lvgl::lv_flex_align_t_LV_FLEX_ALIGN_CENTER, + lvgl::lv_flex_align_t_LV_FLEX_ALIGN_CENTER, + lvgl::lv_flex_align_t_LV_FLEX_ALIGN_SPACE_AROUND as _, + ); + + let switch = lvgl::lv_switch_create(button); + + log::information!( + "Interface {} is {:?}, link is {}", + entry.name, + kind, + if is_up { "up" } else { "down" } + ); + + lvgl::lv_obj_set_state(switch, lvgl::LV_STATE_CHECKED as _, is_up); + } + } + + Ok(()) + } + + pub async fn create_ui( + &mut self, + parent_tabview: *mut lvgl::lv_obj_t, + ) -> crate::error::Result<*mut lvgl::lv_obj_t> { + self.parent_tabview = parent_tabview; + + self.tab_container = + unsafe { lvgl::lv_tabview_add_tab(parent_tabview, c"Network".as_ptr() as *const _) }; + + if self.tab_container.is_null() { + return Err(crate::error::Error::FailedToCreateUiElement); + } + + // Create interface list + + unsafe { + self.interfaces_list = lvgl::lv_list_create(self.tab_container); + if self.interfaces_list.is_null() { + return Err(Error::FailedToCreateObject); + } + + // File list properties - use flex grow to fill remaining space + lvgl::lv_obj_set_width(self.interfaces_list, lvgl::lv_pct(100)); + lvgl::lv_obj_set_flex_grow(self.interfaces_list, 1); // Take remaining vertical space + + // Ensure proper scrolling behavior + lvgl::lv_obj_set_style_pad_all(self.interfaces_list, 0, lvgl::LV_STATE_DEFAULT); + } + + self.update_interfaces().await?; + + Ok(self.tab_container) + } + + pub async fn handle_event(&mut self, event: &Event) -> bool { + let time_manager = xila::time::get_instance(); + + let current_time = match time_manager.get_current_time() { + Ok(time) => time, + Err(_) => { + log::error!("Failed to get current time for network tab update"); + return false; + } + }; + + if current_time - self.last_update >= Self::UPDATE_INTERVAL { + if let Err(e) = self.update_interfaces().await { + log::error!("Failed to update network interfaces: {:?}", e); + } + + self.last_update = current_time; + } + + if let Some(panel) = &mut self.configuration_panel { + if !panel.handle_event(event).await { + self.configuration_panel.take(); + } + + return true; + } + + // check if any specific events need to be handled here + if event.code == EventKind::Clicked { + // Find which interface button was clicked + let parent = unsafe { lvgl::lv_obj_get_parent(event.target) }; + if parent == self.interfaces_list + && unsafe { lvgl::lv_obj_check_type(event.target, &lvgl::lv_list_button_class) } + { + let interface = unsafe { + let label = lvgl::lv_obj_get_child(event.target, 1); + + if label.is_null() { + log::error!("Failed to get label child from interface button"); + return false; + } + + let text = lvgl::lv_label_get_text(label); + + if text.is_null() { + log::error!("Failed to get text from label"); + return false; + } + + CStr::from_ptr(text as *const _) + .to_str() + .unwrap_or_default() + }; + + log::information!("Opening configuration panel for interface: {}", interface); + + match InterfacePanel::new(interface.to_string(), self.tab_container).await { + Ok(panel) => { + self.configuration_panel.replace(panel); + } + Err(e) => { + log::error!("Failed to create configuration panel: {:?}", e); + } + } + + return true; + } + } + + false + } +} + +pub async fn open_interface( + virtual_file_system: &VirtualFileSystem, + interface: &str, +) -> Result { + let task_manager = xila::task::get_instance(); + + let task = task_manager.get_current_task_identifier().await; + + let path = Path::NETWORK_DEVICES + .join(Path::from_str(interface)) + .ok_or(Error::FailedToCreateUiElement)?; + + let file = File::open( + virtual_file_system, + task, + &path, + AccessFlags::READ_WRITE.into(), + ) + .await?; + + Ok(file) +} diff --git a/executables/settings/src/tabs/password.rs b/executables/settings/src/tabs/password.rs index 0024a4a5..f402930b 100644 --- a/executables/settings/src/tabs/password.rs +++ b/executables/settings/src/tabs/password.rs @@ -186,13 +186,23 @@ impl PasswordTab { } impl PasswordTab { - pub fn create_ui( + pub async fn create_ui( &mut self, parent_tabview: *mut lvgl::lv_obj_t, ) -> Result<*mut lvgl::lv_obj_t> { let tab_container = unsafe { lvgl::lv_tabview_add_tab(parent_tabview, translate!(c"Password").as_ptr()) }; + unsafe { + lvgl::lv_obj_set_flex_flow(tab_container, lvgl::lv_flex_flow_t_LV_FLEX_FLOW_COLUMN); + lvgl::lv_obj_set_flex_align( + tab_container, + lvgl::lv_flex_align_t_LV_FLEX_ALIGN_CENTER, + lvgl::lv_flex_align_t_LV_FLEX_ALIGN_START, + lvgl::lv_flex_align_t_LV_FLEX_ALIGN_CENTER, + ); + } + if tab_container.is_null() { return Err(crate::error::Error::FailedToCreateUiElement); } @@ -213,45 +223,18 @@ impl PasswordTab { current_password_label, translate!(c"Current Password").as_ptr(), ); - lvgl::lv_obj_align( - current_password_label, - lvgl::lv_align_t_LV_ALIGN_TOP_LEFT, - 10, - 10, - ); let current_password_text_area = lvgl::lv_textarea_create(tab_container); lvgl::lv_textarea_set_password_mode(current_password_text_area, true); lvgl::lv_textarea_set_one_line(current_password_text_area, true); - lvgl::lv_obj_align_to( - current_password_text_area, - current_password_label, - lvgl::lv_align_t_LV_ALIGN_OUT_BOTTOM_LEFT, - 0, - 5, - ); // New password let new_password_label = lvgl::lv_label_create(tab_container); lvgl::lv_label_set_text(new_password_label, translate!(c"New Password").as_ptr()); - lvgl::lv_obj_align_to( - new_password_label, - current_password_text_area, - lvgl::lv_align_t_LV_ALIGN_OUT_BOTTOM_LEFT, - 0, - 20, - ); let new_password_text_area = lvgl::lv_textarea_create(tab_container); lvgl::lv_textarea_set_password_mode(new_password_text_area, true); lvgl::lv_textarea_set_one_line(new_password_text_area, true); - lvgl::lv_obj_align_to( - new_password_text_area, - new_password_label, - lvgl::lv_align_t_LV_ALIGN_OUT_BOTTOM_LEFT, - 0, - 5, - ); // Confirm password let confirm_password_label = lvgl::lv_label_create(tab_container); @@ -259,34 +242,13 @@ impl PasswordTab { confirm_password_label, translate!(c"Confirm Password").as_ptr(), ); - lvgl::lv_obj_align_to( - confirm_password_label, - new_password_text_area, - lvgl::lv_align_t_LV_ALIGN_OUT_BOTTOM_LEFT, - 0, - 20, - ); let confirm_password_text_area = lvgl::lv_textarea_create(tab_container); lvgl::lv_textarea_set_password_mode(confirm_password_text_area, true); lvgl::lv_textarea_set_one_line(confirm_password_text_area, true); - lvgl::lv_obj_align_to( - confirm_password_text_area, - confirm_password_label, - lvgl::lv_align_t_LV_ALIGN_OUT_BOTTOM_LEFT, - 0, - 5, - ); // Change password button let change_password_button = lvgl::lv_button_create(tab_container); - lvgl::lv_obj_align_to( - change_password_button, - confirm_password_text_area, - lvgl::lv_align_t_LV_ALIGN_OUT_BOTTOM_MID, - 0, - 30, - ); let button_label = lvgl::lv_label_create(change_password_button); lvgl::lv_label_set_text(button_label, translate!(c"Change Password").as_ptr()); From d73e6108b94dfac7013cc76733e39dad6d6168a8 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:36:05 +0100 Subject: [PATCH 65/78] feat: add new localization strings for system settings and network configuration --- executables/settings/locales/en/messages.po | 43 ++++++++++++++++++++- executables/settings/locales/fr/messages.po | 43 ++++++++++++++++++++- executables/settings/locales/messages.pot | 43 ++++++++++++++++++++- 3 files changed, 123 insertions(+), 6 deletions(-) diff --git a/executables/settings/locales/en/messages.po b/executables/settings/locales/en/messages.po index 54943a36..0259c9f2 100644 --- a/executables/settings/locales/en/messages.po +++ b/executables/settings/locales/en/messages.po @@ -96,6 +96,12 @@ msgstr "Change Password" msgid "About" msgstr "About" +msgid "Operating System:" +msgstr "Operating System:" + +msgid "Description:" +msgstr "Description:" + msgid "Developed by:" msgstr "Developed by:" @@ -108,5 +114,38 @@ msgstr "Version:" msgid "Locale:" msgstr "Locale:" -msgid "Fallback:" -msgstr "Fallback:" \ No newline at end of file +msgid "Memory:" +msgstr "Memory:" + +msgid "Cancel" +msgstr "Cancel" + +msgid "Apply" +msgstr "Apply" + +msgid "None" +msgstr "None" + +msgid "DHCP" +msgstr "DHCP" + +msgid "Static" +msgstr "Static" + +msgid "Configuration Mode" +msgstr "Configuration Mode" + +msgid "MAC Address" +msgstr "MAC Address" + +msgid "Address" +msgstr "Address" + +msgid "Routes" +msgstr "Routes" + +msgid "Gateway" +msgstr "Gateway" + +msgid "DNS Servers" +msgstr "DNS Servers" diff --git a/executables/settings/locales/fr/messages.po b/executables/settings/locales/fr/messages.po index 14462438..10b5b6be 100644 --- a/executables/settings/locales/fr/messages.po +++ b/executables/settings/locales/fr/messages.po @@ -96,6 +96,12 @@ msgstr "Changer le mot de passe" msgid "About" msgstr "À propos" +msgid "Operating System:" +msgstr "Système d'exploitation :" + +msgid "Description:" +msgstr "Description :" + msgid "Developed by:" msgstr "Développé par :" @@ -108,5 +114,38 @@ msgstr "Version :" msgid "Locale:" msgstr "Langue :" -msgid "Fallback:" -msgstr "De secours :" \ No newline at end of file +msgid "Memory:" +msgstr "Mémoire :" + +msgid "Cancel" +msgstr "Annuler" + +msgid "Apply" +msgstr "Appliquer" + +msgid "None" +msgstr "Aucun" + +msgid "DHCP" +msgstr "DHCP" + +msgid "Static" +msgstr "Statique" + +msgid "Configuration Mode" +msgstr "Mode de configuration" + +msgid "MAC Address" +msgstr "Adresse MAC" + +msgid "Address" +msgstr "Adresse" + +msgid "Routes" +msgstr "Routes" + +msgid "Gateway" +msgstr "Passerelle" + +msgid "DNS Servers" +msgstr "Serveurs DNS" diff --git a/executables/settings/locales/messages.pot b/executables/settings/locales/messages.pot index 220f4cda..192d10d5 100644 --- a/executables/settings/locales/messages.pot +++ b/executables/settings/locales/messages.pot @@ -96,6 +96,12 @@ msgstr "" msgid "About" msgstr "" +msgid "Operating System:" +msgstr "" + +msgid "Description:" +msgstr "" + msgid "Developed by:" msgstr "" @@ -108,5 +114,38 @@ msgstr "" msgid "Locale:" msgstr "" -msgid "Fallback:" -msgstr "" \ No newline at end of file +msgid "Memory:" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "Apply" +msgstr "" + +msgid "None" +msgstr "" + +msgid "DHCP" +msgstr "" + +msgid "Static" +msgstr "" + +msgid "Configuration Mode" +msgstr "" + +msgid "MAC Address" +msgstr "" + +msgid "Address" +msgstr "" + +msgid "Routes" +msgstr "" + +msgid "Gateway" +msgstr "" + +msgid "DNS Servers" +msgstr "" From 57d330e8ac9d6d9d669a90d496fba83500ed2295 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:36:14 +0100 Subject: [PATCH 66/78] fix: update testing initialization to include additional parameter for consistency --- executables/settings/tests/integration_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/executables/settings/tests/integration_test.rs b/executables/settings/tests/integration_test.rs index 3c21cf8d..6e53192b 100644 --- a/executables/settings/tests/integration_test.rs +++ b/executables/settings/tests/integration_test.rs @@ -12,7 +12,7 @@ async fn main() { use xila::executable::mount_executables; use xila::{executable, task, virtual_file_system}; - let standard = testing::initialize(true).await; + let standard = testing::initialize(true, true).await; mount_executables!( virtual_file_system::get_instance(), From 4f89217316e2c6e4fba14b6afa4c4a639ea7be55 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:37:05 +0100 Subject: [PATCH 67/78] feat: integrate getargs for argument parsing and enhance keyboard option handling --- executables/shell/graphical/Cargo.toml | 1 + executables/shell/graphical/src/lib.rs | 28 +++++++++++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/executables/shell/graphical/Cargo.toml b/executables/shell/graphical/Cargo.toml index a64d9a13..7ec2fd98 100644 --- a/executables/shell/graphical/Cargo.toml +++ b/executables/shell/graphical/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] xila = { path = "../../../", features = ["host"] } miniserde = { workspace = true } +getargs = { version = "0.5" } [target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))'.dev-dependencies] drivers_native = { workspace = true } diff --git a/executables/shell/graphical/src/lib.rs b/executables/shell/graphical/src/lib.rs index 88c6ba33..2b614008 100644 --- a/executables/shell/graphical/src/lib.rs +++ b/executables/shell/graphical/src/lib.rs @@ -12,21 +12,35 @@ extern crate alloc; use crate::{desk::Desk, error::Error}; use alloc::{boxed::Box, string::String, vec::Vec}; +use core::fmt::Write; use core::num::NonZeroUsize; use core::time::Duration; +use getargs::Arg; use home::Home; use layout::Layout; use login::Login; -use xila::executable::{self, ArgumentsParser, ExecutableTrait, Standard}; +use xila::executable::{self, ExecutableTrait, Standard}; use xila::task; use xila::users; -pub async fn main(standard: Standard, arguments: Vec) -> Result<(), NonZeroUsize> { - let mut parsed_arguments = ArgumentsParser::new(&arguments); +pub async fn main(mut standard: Standard, arguments: Vec) -> Result<(), NonZeroUsize> { + let arguments = arguments.iter().map(|s| s.as_str()); - let show_keyboard = parsed_arguments - .find_map(|argument| Some(argument.options.get_option("show-keyboard").is_some())) - .unwrap_or(false); + let mut options = getargs::Options::new(arguments); + + let mut show_keyboard = false; + + while let Some(argument) = options.next_arg().map_err(|e| { + writeln!(standard.error(), "{}", e).ok(); + NonZeroUsize::new(1).unwrap() + })? { + match argument { + Arg::Short('k') | Arg::Long("show-keyboard") => { + show_keyboard = true; + } + _ => {} + } + } Shell::new(standard, show_keyboard).await.main().await } @@ -66,7 +80,7 @@ impl Shell { pub async fn main(&mut self) -> Result<(), NonZeroUsize> { while self.running { - self.layout.r#loop().await; + self.layout.run().await; if let Some(login) = &mut self.login { login.event_handler().await; From 421de5fbd989dff4c5c96911a3cdd17cd254231c Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:37:22 +0100 Subject: [PATCH 68/78] feat: add error handling for directory opening and update network icon functionality --- .../shell/graphical/locales/en/messages.po | 3 + .../shell/graphical/locales/fr/messages.po | 3 + .../shell/graphical/locales/messages.pot | 3 + executables/shell/graphical/src/error.rs | 4 + executables/shell/graphical/src/layout.rs | 156 +++++++++++++++--- 5 files changed, 144 insertions(+), 25 deletions(-) diff --git a/executables/shell/graphical/locales/en/messages.po b/executables/shell/graphical/locales/en/messages.po index e3065174..112732ec 100644 --- a/executables/shell/graphical/locales/en/messages.po +++ b/executables/shell/graphical/locales/en/messages.po @@ -70,6 +70,9 @@ msgstr "Missing arguments" msgid "Failed to add shortcut: {}" msgstr "Failed to add shortcut: {}" +msgid "Failed to open directory: {}" +msgstr "Failed to open directory: {}" + #: login.user_name msgid "User name" msgstr "User name" diff --git a/executables/shell/graphical/locales/fr/messages.po b/executables/shell/graphical/locales/fr/messages.po index f5a08c45..df66d366 100644 --- a/executables/shell/graphical/locales/fr/messages.po +++ b/executables/shell/graphical/locales/fr/messages.po @@ -62,6 +62,9 @@ msgstr "Arguments manquants" msgid "Failed to add shortcut: {}" msgstr "Échec de l'ajout du raccourci: {}" +msgid "Failed to open directory: {}" +msgstr "Échec de l'ouverture du répertoire: {}" + #: login.user_name msgid "User name" msgstr "Nom d'utilisateur" diff --git a/executables/shell/graphical/locales/messages.pot b/executables/shell/graphical/locales/messages.pot index ed09bfb6..2b6e921c 100644 --- a/executables/shell/graphical/locales/messages.pot +++ b/executables/shell/graphical/locales/messages.pot @@ -70,6 +70,9 @@ msgstr "" msgid "Failed to add shortcut: {}" msgstr "" +msgid "Failed to open directory: {}" +msgstr "" + #: login.user_name msgid "User name" msgstr "" diff --git a/executables/shell/graphical/src/error.rs b/executables/shell/graphical/src/error.rs index b54798b3..cc97e6b1 100644 --- a/executables/shell/graphical/src/error.rs +++ b/executables/shell/graphical/src/error.rs @@ -30,6 +30,7 @@ pub enum Error { NullCharacterInString(alloc::ffi::NulError), MissingArguments, FailedToAddShortcut(virtual_file_system::Error), + FailedToOpenDirectory(virtual_file_system::Error), } impl Error { @@ -133,6 +134,9 @@ impl Display for Error { Self::FailedToAddShortcut(error) => { write!(formatter, translate!("Failed to add shortcut: {}"), error) } + Self::FailedToOpenDirectory(error) => { + write!(formatter, translate!("Failed to open directory: {}"), error) + } } } } diff --git a/executables/shell/graphical/src/layout.rs b/executables/shell/graphical/src/layout.rs index 67bc03be..b41905a9 100644 --- a/executables/shell/graphical/src/layout.rs +++ b/executables/shell/graphical/src/layout.rs @@ -1,9 +1,15 @@ use crate::error::{Error, Result}; use alloc::{format, string::String}; +use core::ffi::CStr; use core::ptr::null_mut; -use xila::graphics::{self, EventKind, lvgl, symbols, theme}; +use core::time::Duration; +use xila::file_system::{AccessFlags, Path}; +use xila::graphics::{self, EventKind, lvgl, symbol, theme}; +use xila::log; +use xila::network::InterfaceKind; use xila::shared::unix_to_human_time; -use xila::time; +use xila::virtual_file_system::{Directory, File}; +use xila::{network, time, virtual_file_system}; const KEYBOARD_SIZE_RATIO: f64 = 3.0 / 1.0; @@ -14,7 +20,8 @@ pub struct Layout { clock: *mut lvgl::lv_obj_t, clock_string: String, _battery: *mut lvgl::lv_obj_t, - _wi_fi: *mut lvgl::lv_obj_t, + network: *mut lvgl::lv_obj_t, + last_update: Duration, } impl Drop for Layout { @@ -104,28 +111,121 @@ pub unsafe extern "C" fn screen_event_handler(event: *mut lvgl::lv_event_t) { } impl Layout { - pub async fn r#loop(&mut self) { - self.update_clock().await; + pub const UPDATE_INTERVAL: Duration = Duration::from_secs(30); + + pub async fn run(&mut self) { + let current_time = match time::get_instance().get_current_time() { + Ok(time) => time, + Err(e) => { + log::error!("Failed to get current time: {}", e); + return; + } + }; + + if current_time - self.last_update < Self::UPDATE_INTERVAL { + return; + } + + self.update_clock(current_time).await; + + if let Err(e) = self.update_network_icon().await { + log::error!("Failed to update network icon: {}", e); + } + + self.last_update = current_time; } - async fn update_clock(&mut self) { - // - Update the clock - let current_time = time::get_instance().get_current_time(); + async fn get_interface_symbol(&self, file: &mut File) -> Result> { + let is_up = file + .control(network::IS_LINK_UP, &()) + .await + .map_err(Error::FailedToOpenDirectory)?; - if let Ok(current_time) = current_time { - let (_, _, _, hour, minute, _) = unix_to_human_time(current_time.as_secs() as i64); + if !is_up { + return Ok(None); + } - graphics::lock!({ - self.clock_string = format!("{hour:02}:{minute:02}\0"); + let kind = file + .control(network::GET_KIND, &()) + .await + .map_err(Error::FailedToOpenDirectory)?; - unsafe { - lvgl::lv_label_set_text_static( - self.clock, - self.clock_string.as_ptr() as *const i8, - ); + let symbol = match kind { + InterfaceKind::WiFi => symbol::WIFI, + InterfaceKind::Ethernet => symbol::NETWORK_WIRED, + InterfaceKind::Unknown => c"?", + }; + + Ok(Some(symbol)) + } + + async fn get_network_symbol(&self) -> Result<&CStr> { + // Browse the network interfaces in the /devices/network directory + + let virtual_file_system = virtual_file_system::get_instance(); + + let task_manager = xila::task::get_instance(); + + let task = task_manager.get_current_task_identifier().await; + + let mut directory = Directory::open(virtual_file_system, task, Path::NETWORK_DEVICES) + .await + .map_err(Error::FailedToOpenDirectory)?; + + while let Some(entry) = directory + .read() + .await + .map_err(Error::FailedToOpenDirectory)? + { + if entry.name == "." || entry.name == ".." { + continue; + } + + let entry_path = entry.join_path(Path::NETWORK_DEVICES); + + if let Some(entry_path) = entry_path { + let mut file = File::open( + virtual_file_system, + task, + &entry_path, + AccessFlags::Read.into(), + ) + .await + .map_err(Error::FailedToOpenDirectory)?; + + let symbol = self.get_interface_symbol(&mut file).await?; + + if let Some(symbol) = symbol { + return Ok(symbol); } - }); + } } + + Ok(c"") + } + + async fn update_network_icon(&mut self) -> Result<()> { + let symbol = self.get_network_symbol().await?; + + graphics::lock!({ + unsafe { + lvgl::lv_label_set_text_static(self.network, symbol.as_ptr()); + } + }); + + Ok(()) + } + + async fn update_clock(&mut self, current_time: Duration) { + let (_, _, _, hour, minute, _) = unix_to_human_time(current_time.as_secs() as i64); + + graphics::lock!({ + self.clock_string = format!("{hour:02}:{minute:02}\0"); + + unsafe { + lvgl::lv_label_set_text_static(self.clock, self.clock_string.as_ptr() as *const i8); + } + }); } pub fn get_windows_parent(&self) -> *mut lvgl::lv_obj_t { @@ -207,6 +307,11 @@ impl Layout { lvgl::lv_obj_set_style_pad_all(tray, 0, lvgl::LV_STATE_DEFAULT); lvgl::lv_obj_set_style_border_width(tray, 0, lvgl::LV_STATE_DEFAULT); lvgl::lv_obj_align(tray, lvgl::lv_align_t_LV_ALIGN_RIGHT_MID, 0, 0); + lvgl::lv_obj_set_style_bg_opa( + tray, + lvgl::LV_OPA_TRANSP as _, + lvgl::LV_STATE_DEFAULT, + ); tray } @@ -214,18 +319,18 @@ impl Layout { // - - Create a label for the WiFi - let wi_fi = unsafe { + let network = unsafe { // - - Create a label for the WiFi - let wi_fi = lvgl::lv_label_create(tray); + let network = lvgl::lv_label_create(tray); - if wi_fi.is_null() { + if network.is_null() { return Err(Error::FailedToCreateObject); } - lvgl::lv_label_set_text(wi_fi, symbols::WIFI.as_ptr()); + lvgl::lv_label_set_text(network, c"".as_ptr()); - wi_fi + network }; // - - Create a label for the battery @@ -237,7 +342,7 @@ impl Layout { return Err(Error::FailedToCreateObject); } - lvgl::lv_label_set_text(battery, symbols::BATTERY_3.as_ptr()); + lvgl::lv_label_set_text_static(battery, symbol::BATTERY_3.as_ptr()); battery }; @@ -304,7 +409,8 @@ impl Layout { clock, clock_string: String::with_capacity(6), _battery: battery, - _wi_fi: wi_fi, + network, + last_update: Duration::ZERO, } }); From e0e6a478192dac1d145119217888ebc41747ff24 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:37:28 +0100 Subject: [PATCH 69/78] fix: update testing initialization to include additional parameter for consistency --- executables/shell/graphical/tests/integration_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/executables/shell/graphical/tests/integration_test.rs b/executables/shell/graphical/tests/integration_test.rs index 636921f5..1b37e9de 100644 --- a/executables/shell/graphical/tests/integration_test.rs +++ b/executables/shell/graphical/tests/integration_test.rs @@ -13,7 +13,7 @@ async fn main() { use xila::virtual_file_system::File; use xila::{executable, task, virtual_file_system}; - let standard = testing::initialize(true).await; + let standard = testing::initialize(true, true).await; let task_manager = task::get_instance(); let virtual_file_system = virtual_file_system::get_instance(); From a68af02d14da8d6323a6b3e379f03560afad1058 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:37:40 +0100 Subject: [PATCH 70/78] refactor: simplify lifetime annotations in TerminalExecutable::new method --- executables/terminal/src/executable.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/executables/terminal/src/executable.rs b/executables/terminal/src/executable.rs index 1e98c888..9e4524d5 100644 --- a/executables/terminal/src/executable.rs +++ b/executables/terminal/src/executable.rs @@ -8,8 +8,8 @@ use xila::virtual_file_system::{File, VirtualFileSystem}; pub struct TerminalExecutable; impl TerminalExecutable { - pub async fn new<'a>( - virtual_file_system: &'a VirtualFileSystem<'a>, + pub async fn new( + virtual_file_system: &VirtualFileSystem, task: TaskIdentifier, ) -> Result { let _ = virtual_file_system From a6251628dbc65f1a92f51879ad7fd26c099b4385 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:37:52 +0100 Subject: [PATCH 71/78] fix: update testing initialization to include additional parameter for consistency --- executables/shell/command_line/tests/integration_test.rs | 2 +- executables/terminal/tests/integration_test.rs | 2 +- executables/wasm/tests/integration_test.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/executables/shell/command_line/tests/integration_test.rs b/executables/shell/command_line/tests/integration_test.rs index 1e1dbc6c..f75c3af8 100644 --- a/executables/shell/command_line/tests/integration_test.rs +++ b/executables/shell/command_line/tests/integration_test.rs @@ -11,7 +11,7 @@ async fn main() { task, virtual_file_system, }; - let standard = testing::initialize(false).await; + let standard = testing::initialize(false, true).await; let virtual_file_system = virtual_file_system::get_instance(); let task = task::get_instance().get_current_task_identifier().await; diff --git a/executables/terminal/tests/integration_test.rs b/executables/terminal/tests/integration_test.rs index d121904d..cc8bfb3e 100644 --- a/executables/terminal/tests/integration_test.rs +++ b/executables/terminal/tests/integration_test.rs @@ -13,7 +13,7 @@ async fn main() { use xila::executable::mount_executables; use xila::{executable, task, virtual_file_system}; - let standard = testing::initialize(true).await; + let standard = testing::initialize(true, false).await; let virtual_file_system = virtual_file_system::get_instance(); let task_instance = task::get_instance(); diff --git a/executables/wasm/tests/integration_test.rs b/executables/wasm/tests/integration_test.rs index 29ba4e0b..61e5423d 100644 --- a/executables/wasm/tests/integration_test.rs +++ b/executables/wasm/tests/integration_test.rs @@ -15,7 +15,7 @@ async fn main() { use xila::virtual_file_system; use xila::virtual_machine; - let standard = testing::initialize(false).await; + let standard = testing::initialize(false, false).await; let virtual_file_system = virtual_file_system::get_instance(); let task_instance = task::get_instance(); From c462da91a55b5518d25f698baf05ac84f2e49a93 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:38:18 +0100 Subject: [PATCH 72/78] feat: implement DNS resolution functionality with support for multiple record types --- .../shell/command_line/src/commands/dns.rs | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 executables/shell/command_line/src/commands/dns.rs diff --git a/executables/shell/command_line/src/commands/dns.rs b/executables/shell/command_line/src/commands/dns.rs new file mode 100644 index 00000000..539a4211 --- /dev/null +++ b/executables/shell/command_line/src/commands/dns.rs @@ -0,0 +1,132 @@ +use crate::{Error, Result, Shell, commands::check_no_more_arguments}; +use core::fmt::Write; +use getargs::{Arg, Options}; +use xila::{ + internationalization::translate, + network::{self, DnsQueryKind, DnsSocket}, +}; + +impl Shell { + fn format_kind(kind: DnsQueryKind) -> &'static str { + match kind { + DnsQueryKind::A => "A", + DnsQueryKind::Aaaa => "AAAA", + DnsQueryKind::Cname => "CNAME", + DnsQueryKind::Ns => "NS", + DnsQueryKind::Soa => "SOA", + _ => "UNKNOWN", + } + } + + async fn resolve( + &mut self, + socket: &DnsSocket, + domain: &str, + kind: DnsQueryKind, + ) -> Result<()> { + match socket.resolve(domain, kind).await { + Ok(ip) => { + writeln!( + self.standard.out(), + translate!("{} record(s) for domain '{}':"), + Self::format_kind(kind), + domain + )?; + for address in &ip { + writeln!(self.standard.out(), " - {}", address)?; + } + } + Err(network::Error::Failed) => { + writeln!( + self.standard.out(), + translate!("No {} records found for domain '{}'"), + Self::format_kind(kind), + domain + )?; + } + Err(e) => { + write!( + self.standard.out(), + translate!("Failed to resolve domain '{}' for {} records: {}"), + domain, + Self::format_kind(kind), + e + )?; + } + } + Ok(()) + } + + pub async fn dns_resolve<'a, I>(&mut self, options: &mut Options<&'a str, I>) -> Result<()> + where + I: Iterator, + { + let mut domain = ""; + let mut a_enabled = false; + let mut aaaa_enabled = false; + let mut cname_enabled = false; + let mut ns_enabled = false; + let mut soa_enabled = false; + let mut default = true; + + while let Some(argument) = options.next_arg()? { + if let Arg::Long(_) | Arg::Short(_) = &argument { + default = false; + } + + match argument { + Arg::Short('a') | Arg::Long("a") => { + a_enabled = true; + } + Arg::Short('A') | Arg::Long("aaaa") => { + aaaa_enabled = true; + } + Arg::Short('c') | Arg::Long("cname") => { + cname_enabled = true; + } + Arg::Short('n') | Arg::Long("ns") => { + ns_enabled = true; + } + Arg::Short('s') | Arg::Long("soa") => { + soa_enabled = true; + } + Arg::Positional(p) => { + if !domain.is_empty() { + return Err(crate::Error::InvalidNumberOfArguments); + } + domain = p; + } + _ => { + return Err(crate::Error::InvalidOption); + } + } + } + + check_no_more_arguments(options)?; + + let socket = network::get_instance() + .new_dns_socket(None) + .await + .map_err(Error::FailedToCreateSocket)?; + + if a_enabled || default { + self.resolve(&socket, domain, DnsQueryKind::A).await?; + } + if aaaa_enabled || default { + self.resolve(&socket, domain, DnsQueryKind::Aaaa).await?; + } + if cname_enabled { + self.resolve(&socket, domain, DnsQueryKind::Cname).await?; + } + if ns_enabled { + self.resolve(&socket, domain, DnsQueryKind::Ns).await?; + } + if soa_enabled { + self.resolve(&socket, domain, DnsQueryKind::Soa).await?; + } + + socket.close().await.map_err(Error::FailedToCreateSocket)?; + + Ok(()) + } +} From b44dbb074eb4e4a0b8a54b66803eeccdb673e5e1 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:38:27 +0100 Subject: [PATCH 73/78] feat: implement ping command with DNS resolution and customizable options --- .../shell/command_line/src/commands/ping.rs | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 executables/shell/command_line/src/commands/ping.rs diff --git a/executables/shell/command_line/src/commands/ping.rs b/executables/shell/command_line/src/commands/ping.rs new file mode 100644 index 00000000..ec850503 --- /dev/null +++ b/executables/shell/command_line/src/commands/ping.rs @@ -0,0 +1,141 @@ +use core::fmt::Write; +use getargs::Arg; +use xila::{ + internationalization::translate, + network::{self, DnsQueryKind, Duration, IcmpEndpoint}, +}; + +use crate::{Error, Shell}; + +const ICMP_IDENTIFIER: u16 = 0x22b; + +impl Shell { + pub async fn ping<'a, I>( + &mut self, + options: &mut getargs::Options<&'a str, I>, + ) -> crate::Result<()> + where + I: Iterator, + { + let mut count = 4; + let mut timeout_seconds = 5; + let mut target: &'a str = ""; + let mut payload_size = 56; + + while let Some(argument) = options.next_arg()? { + match argument { + Arg::Short('c') | Arg::Long("count") => { + let value = options + .next_positional() + .ok_or(crate::Error::MissingPositionalArgument("count"))?; + count = value.parse().map_err(|_| crate::Error::InvalidOption)?; + } + Arg::Short('t') | Arg::Long("timeout") => { + let value = options + .next_positional() + .ok_or(crate::Error::MissingPositionalArgument("timeout"))?; + timeout_seconds = value.parse().map_err(|_| crate::Error::InvalidOption)?; + } + Arg::Short('s') | Arg::Long("size") => { + let value = options + .next_positional() + .ok_or(crate::Error::MissingPositionalArgument("size"))?; + payload_size = value.parse().map_err(|_| crate::Error::InvalidOption)?; + } + Arg::Positional(p) => { + if !target.is_empty() { + return Err(crate::Error::InvalidNumberOfArguments); + } + target = p; + } + _ => { + return Err(crate::Error::InvalidOption); + } + } + } + + let network = network::get_instance(); + + let dns_socket = network + .new_dns_socket(None) + .await + .map_err(Error::FailedToCreateSocket)?; + + let resolved_target = dns_socket + .resolve(target, DnsQueryKind::A | DnsQueryKind::Aaaa) + .await + .map(|s| s.first().cloned()) + .map_err(Error::FailedToResolve)?; + + dns_socket + .close() + .await + .map_err(Error::FailedToCreateSocket)?; + + let resolved_target = match resolved_target { + Some(ip) => ip, + None => { + writeln!( + self.standard.out(), + translate!("Cannot resolve {}: Unknown host"), + target + )?; + return Ok(()); + } + }; + + writeln!( + self.standard.out(), + translate!("PING {} ({}): {} data bytes"), + target, + &resolved_target, + 56 + )?; + + let socket = network + .new_icmp_socket(256, 256, 1, 1, None) + .await + .map_err(Error::FailedToCreateSocket)?; + + socket + .bind(IcmpEndpoint::Identifier(ICMP_IDENTIFIER)) + .await + .map_err(Error::FailedToCreateSocket)?; + + for i in 0..count { + match socket + .ping( + &resolved_target, + i, + ICMP_IDENTIFIER, + Duration::from_seconds(timeout_seconds), + payload_size, + ) + .await + { + Ok(duration) => { + writeln!( + self.standard.out(), + translate!("{} bytes from {}: icmp_seq={} time={:.2} ms"), + payload_size, + resolved_target, + i, + duration.as_milliseconds() + )?; + } + Err(network::Error::TimedOut) => { + writeln!( + self.standard.out(), + translate!("Request timeout for icmp_seq {}"), + i + )?; + } + Err(e) => { + writeln!(self.standard.out(), translate!("Error: {}"), e)?; + } + } + } + + Ok(()) + } +} From 47e9018c35d13b942c30a1142bf34d4aec588f3c Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:38:40 +0100 Subject: [PATCH 74/78] fix: simplify entry path joining in list command --- executables/shell/command_line/src/commands/list.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/executables/shell/command_line/src/commands/list.rs b/executables/shell/command_line/src/commands/list.rs index b7270145..f7ab4a6d 100644 --- a/executables/shell/command_line/src/commands/list.rs +++ b/executables/shell/command_line/src/commands/list.rs @@ -43,9 +43,7 @@ impl Shell { .map_err(Error::FailedToReadDirectoryEntry)? { if long { - let entry_path = path - .join(Path::from_str(&entry.name)) - .ok_or(Error::FailedToJoinPath)?; + let entry_path = entry.join_path(path).ok_or(Error::FailedToJoinPath)?; let statistics = virtual_file_system .get_statistics(&entry_path) From 19a9ac5bde9b169aabc4ce1e627b05fbcc4d89b2 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:38:48 +0100 Subject: [PATCH 75/78] feat: implement IP command with address and route display functionality --- .../shell/command_line/src/commands/ip.rs | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 executables/shell/command_line/src/commands/ip.rs diff --git a/executables/shell/command_line/src/commands/ip.rs b/executables/shell/command_line/src/commands/ip.rs new file mode 100644 index 00000000..f1226928 --- /dev/null +++ b/executables/shell/command_line/src/commands/ip.rs @@ -0,0 +1,193 @@ +use crate::{ + Shell, + error::{Error, Result}, +}; +use core::fmt::Write; +use xila::{ + file_system::{AccessFlags, Path}, + log, + network::{GET_IP_ADDRESS, GET_IP_ADDRESS_COUNT, GET_ROUTE, GET_ROUTE_COUNT, GET_STATE}, + task::{self, TaskIdentifier}, + virtual_file_system::{self, Directory, File, FileControlIterator, VirtualFileSystem}, +}; + +impl Shell { + pub async fn open_interface( + virtual_file_system: &VirtualFileSystem, + interface: &str, + ) -> Result { + let task_manager = xila::task::get_instance(); + + let task = task_manager.get_current_task_identifier().await; + + let path = Path::NETWORK_DEVICES + .join(interface) + .ok_or(Error::FailedToJoinPath)?; + + File::open(virtual_file_system, task, &path, AccessFlags::Read.into()) + .await + .map_err(Error::FailedToOpenFile) + } + + async fn show_routes_interface( + &mut self, + interface: &str, + file: &mut File, + ) -> crate::Result<()> { + let mut routes = FileControlIterator::new(file, GET_ROUTE_COUNT, GET_ROUTE) + .await + .map_err(Error::FailedToOpenFile)?; + + while let Some(route) = routes.next().await.map_err(Error::FailedToOpenFile)? { + writeln!( + self.standard.out(), + "{} via {} device {}", + route.cidr, + route.via_router, + interface + )?; + } + + Ok(()) + } + + async fn show_routes( + &mut self, + virtual_file_system: &VirtualFileSystem, + task: TaskIdentifier, + ) -> crate::Result<()> { + let mut directory = Directory::open(virtual_file_system, task, Path::NETWORK_DEVICES) + .await + .map_err(Error::FailedToOpenDirectory)?; + + while let Some(entry) = directory + .read() + .await + .map_err(Error::FailedToReadDirectoryEntry)? + { + if entry.name == "." || entry.name == ".." { + continue; + } + + let mut file = Self::open_interface(virtual_file_system, &entry.name).await?; + + self.show_routes_interface(&entry.name, &mut file).await?; + } + + Ok(()) + } + + async fn show_address_interface(&mut self, file: &mut File) -> crate::Result<()> { + let mut addresses = FileControlIterator::new(file, GET_IP_ADDRESS_COUNT, GET_IP_ADDRESS) + .await + .map_err(Error::FailedToOpenFile)?; + + while let Some(address) = addresses.next().await.map_err(Error::FailedToOpenFile)? { + writeln!(self.standard.out(), " {} ", address)?; + } + + Ok(()) + } + + async fn show_address( + &mut self, + virtual_file_system: &VirtualFileSystem, + task: TaskIdentifier, + ) -> crate::Result<()> { + let mut directory = Directory::open(virtual_file_system, task, Path::NETWORK_DEVICES) + .await + .map_err(Error::FailedToOpenDirectory)?; + + let mut index = 1; + + while let Some(entry) = directory + .read() + .await + .map_err(Error::FailedToReadDirectoryEntry)? + { + if entry.name == "." || entry.name == ".." { + continue; + } + + log::information!("Showing address for interface {}", entry.name); + + let mut file = Self::open_interface(virtual_file_system, &entry.name).await?; + + let state = file + .control(GET_STATE, &()) + .await + .map_err(Error::FailedToOpenFile)?; + + let state = if state { "Enabled" } else { "Disabled" }; + + let status = file + .control(xila::network::IS_LINK_UP, &()) + .await + .map_err(Error::FailedToOpenFile)?; + + let status: &str = if status { "Up" } else { "Down" }; + + let hardware_address = file + .control(xila::network::GET_HARDWARE_ADDRESS, &()) + .await + .map_err(Error::FailedToOpenFile)?; + + let maximum_transmission_unit = file + .control(xila::network::GET_MAXIMUM_TRANSMISSION_UNIT, &()) + .await + .map_err(Error::FailedToOpenFile)?; + + writeln!( + self.standard.out(), + "{}. {} [{}, {}, MTU: {}]", + index, + entry.name, + state, + status, + maximum_transmission_unit + )?; + writeln!( + self.standard.out(), + " Hardware Address: {:x}:{:x}:{:x}:{:x}:{:x}:{:x}", + hardware_address[0], + hardware_address[1], + hardware_address[2], + hardware_address[3], + hardware_address[4], + hardware_address[5], + )?; + + self.show_address_interface(&mut file).await?; + + index += 1; + } + + Ok(()) + } + + pub async fn ip<'a, I>( + &mut self, + options: &mut getargs::Options<&'a str, I>, + ) -> crate::Result<()> + where + I: Iterator, + { + let command = options + .next_positional() + .ok_or(crate::Error::MissingPositionalArgument("command"))?; + + let virtual_file_system = virtual_file_system::get_instance(); + let task_manager = task::get_instance(); + let task = task_manager.get_current_task_identifier().await; + + match command { + "address" | "a" => self.show_address(virtual_file_system, task).await?, + "route" | "r" => self.show_routes(virtual_file_system, task).await?, + _ => { + return Err(crate::Error::InvalidOption); + } + } + + Ok(()) + } +} From 5a2729a7c0d6d9f759aba20e085ad06f11471da7 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:39:04 +0100 Subject: [PATCH 76/78] feat: integrate smoltcp stack with DNS resolution and socket creation error handling --- .../shell/command_line/locales/en/messages.po | 30 +++++++++++++++++++ .../shell/command_line/locales/fr/messages.po | 30 +++++++++++++++++++ .../shell/command_line/locales/messages.pot | 30 +++++++++++++++++++ .../shell/command_line/src/commands/mod.rs | 3 ++ executables/shell/command_line/src/error.rs | 10 ++++++- executables/shell/command_line/src/lib.rs | 6 ++-- 6 files changed, 106 insertions(+), 3 deletions(-) diff --git a/executables/shell/command_line/locales/en/messages.po b/executables/shell/command_line/locales/en/messages.po index 4f61e077..32c55935 100644 --- a/executables/shell/command_line/locales/en/messages.po +++ b/executables/shell/command_line/locales/en/messages.po @@ -108,6 +108,12 @@ msgstr "Failed to set current directory: {}" msgid "Failed to read directory entry: {}" msgstr "Failed to read directory entry: {}" +msgid "Failed to resolve domain: {}" +msgstr "Failed to resolve domain: {}" + +msgid "Failed to create socket: {}" +msgstr "Failed to create socket: {}" + msgid "Format error" msgstr "Format error" @@ -152,3 +158,27 @@ msgstr "Links" msgid "Size" msgstr "Size" + +msgid "{} record(s) for domain '{}':" +msgstr "{} record(s) for domain '{}':" + +msgid "No {} records found for domain '{}'" +msgstr "No {} records found for domain '{}'" + +msgid "Failed to resolve domain '{}' for {} records: {}" +msgstr "Failed to resolve domain '{}' for {} records: {}" + +msgid "Cannot resolve {}: Unknown host" +msgstr "Cannot resolve {}: Unknown host" + +msgid "PING {} ({}): {} data bytes" +msgstr "PING {} ({}): {} data bytes" + +msgid "{} bytes from {}: icmp_seq={} time={:.2} ms" +msgstr "{} bytes from {}: icmp_seq={} time={:.2} ms" + +msgid "Request timeout for icmp_seq {}" +msgstr "Request timeout for icmp_seq {}" + +msgid "Error: {}" +msgstr "Error: {}" \ No newline at end of file diff --git a/executables/shell/command_line/locales/fr/messages.po b/executables/shell/command_line/locales/fr/messages.po index 5408176b..53732da2 100644 --- a/executables/shell/command_line/locales/fr/messages.po +++ b/executables/shell/command_line/locales/fr/messages.po @@ -108,6 +108,12 @@ msgstr "Échec de la définition du répertoire courant: {}" msgid "Failed to read directory entry: {}" msgstr "Échec de la lecture de l'entrée du répertoire: {}" +msgid "Failed to resolve domain: {}" +msgstr "Échec de la résolution du domaine: {}" + +msgid "Failed to create socket: {}" +msgstr "Échec de la création du socket: {}" + msgid "Format error" msgstr "Erreur de formatage" @@ -152,3 +158,27 @@ msgstr "Liens" msgid "Size" msgstr "Taille" + +msgid "{} record(s) for domain '{}':" +msgstr "Enregistrement(s) {} pour le domaine '{}':" + +msgid "No {} records found for domain '{}'" +msgstr "Aucun enregistrement {} trouvé pour le domaine '{}'" + +msgid "Failed to resolve domain '{}' for {} records: {}" +msgstr "Échec de la résolution du domaine '{}' pour les enregistrements {}: {}" + +msgid "Cannot resolve {}: Unknown host" +msgstr "Impossible de résoudre {}: Hôte inconnu" + +msgid "PING {} ({}): {} data bytes" +msgstr "PING {} ({}): {} octets de données" + +msgid "{} bytes from {}: icmp_seq={} time={:.2} ms" +msgstr "{} octets de {}: icmp_seq={} temps={:.2} ms" + +msgid "Request timeout for icmp_seq {}" +msgstr "Délai d'attente dépassé pour icmp_seq {}" + +msgid "Error: {}" +msgstr "Erreur: {}" diff --git a/executables/shell/command_line/locales/messages.pot b/executables/shell/command_line/locales/messages.pot index 55a9c867..ae198a69 100644 --- a/executables/shell/command_line/locales/messages.pot +++ b/executables/shell/command_line/locales/messages.pot @@ -108,6 +108,12 @@ msgstr "" msgid "Failed to read directory entry: {}" msgstr "" +msgid "Failed to resolve domain: {}" +msgstr "" + +msgid "Failed to create socket: {}" +msgstr "" + msgid "Format error" msgstr "" @@ -152,3 +158,27 @@ msgstr "" msgid "Size" msgstr "" + +msgid "{} record(s) for domain '{}':" +msgstr "" + +msgid "No {} records found for domain '{}'" +msgstr "" + +msgid "Failed to resolve domain '{}' for {} records: {}" +msgstr "" + +msgid "Cannot resolve {}: Unknown host" +msgstr "" + +msgid "PING {} ({}): {} data bytes" +msgstr "" + +msgid "{} bytes from {}: icmp_seq={} time={:.2} ms" +msgstr "" + +msgid "Request timeout for icmp_seq {}" +msgstr "" + +msgid "Error: {}" +msgstr "" \ No newline at end of file diff --git a/executables/shell/command_line/src/commands/mod.rs b/executables/shell/command_line/src/commands/mod.rs index 00a49075..31a096c5 100644 --- a/executables/shell/command_line/src/commands/mod.rs +++ b/executables/shell/command_line/src/commands/mod.rs @@ -3,11 +3,14 @@ mod change_directory; mod clear; mod concatenate; mod directory; +mod dns; mod echo; mod environment_variables; mod execute; mod exit; +mod ip; mod list; +mod ping; mod statistics; mod web_request; diff --git a/executables/shell/command_line/src/error.rs b/executables/shell/command_line/src/error.rs index da29bd65..bd67a920 100644 --- a/executables/shell/command_line/src/error.rs +++ b/executables/shell/command_line/src/error.rs @@ -2,8 +2,8 @@ use core::fmt::Display; use core::num::{NonZeroU16, NonZeroUsize}; use alloc::fmt; -use xila::virtual_file_system; use xila::{authentication, internationalization::translate, task}; +use xila::{network, virtual_file_system}; pub type Result = core::result::Result; @@ -39,6 +39,8 @@ pub enum Error { InvalidOption, FailedToGetMetadata(virtual_file_system::Error), FailedToReadDirectoryEntry(virtual_file_system::Error), + FailedToResolve(network::Error), + FailedToCreateSocket(network::Error), Format, } @@ -176,6 +178,12 @@ impl Display for Error { error ) } + Error::FailedToResolve(error) => { + write!(formatter, translate!("Failed to resolve domain: {}"), error) + } + Error::FailedToCreateSocket(error) => { + write!(formatter, translate!("Failed to create socket: {}"), error) + } Error::Format => { write!(formatter, translate!("Format error")) } diff --git a/executables/shell/command_line/src/lib.rs b/executables/shell/command_line/src/lib.rs index 3bfba01a..4d01b1bf 100644 --- a/executables/shell/command_line/src/lib.rs +++ b/executables/shell/command_line/src/lib.rs @@ -66,8 +66,7 @@ impl Shell { where I: IntoIterator + Clone, { - let mut options: getargs::Options<&'a str, ::IntoIter> = - getargs::Options::new(input.clone().into_iter()); + let mut options = getargs::Options::new(input.clone().into_iter()); let next_positional = match options.next_positional() { Some(arg) => arg, @@ -87,6 +86,9 @@ impl Shell { "unset" => self.remove_environment_variable(&mut options).await, "rm" => self.remove(&mut options).await, "web_request" => self.web_request(&mut options).await, + "dns_resolve" => self.dns_resolve(&mut options).await, + "ping" => self.ping(&mut options).await, + "ip" => self.ip(&mut options).await, _ => self.execute(input, paths).await, }; From 0b3d4f9eca6528a099f1069556c77bcfcd1c9a57 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 12:39:14 +0100 Subject: [PATCH 77/78] feat: enhance network interface setup with IP address and DNS server configuration --- examples/native/src/main.rs | 53 +++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/examples/native/src/main.rs b/examples/native/src/main.rs index 8c041389..1eb95156 100644 --- a/examples/native/src/main.rs +++ b/examples/native/src/main.rs @@ -15,6 +15,7 @@ async fn main() { use xila::executable::Standard; use xila::executable::build_crate; use xila::executable::mount_executables; + use xila::file_system::AccessFlags; use xila::file_system::XILA_DISK_SIGNATURE; use xila::file_system::mbr::Mbr; use xila::file_system::mbr::PartitionKind; @@ -22,10 +23,12 @@ async fn main() { use xila::host_bindings; use xila::little_fs; use xila::log; + use xila::network::{self, ADD_DNS_SERVER, ADD_IP_ADDRESS, ADD_ROUTE}; use xila::task; use xila::time; use xila::users; use xila::virtual_file_system; + use xila::virtual_file_system::File; use xila::virtual_file_system::mount_static; use xila::virtual_machine; @@ -108,14 +111,9 @@ async fn main() { let file_system = little_fs::FileSystem::get_or_format(partition, 256).unwrap(); // Initialize the virtual file system - let virtual_file_system = virtual_file_system::initialize( - task_manager, - users_manager, - time_manager, - file_system, - None, - ) - .unwrap(); + let virtual_file_system = + virtual_file_system::initialize(task_manager, users_manager, time_manager, file_system) + .unwrap(); log::information!("Virtual file system initialized."); @@ -166,7 +164,42 @@ async fn main() { .await .unwrap(); - log::information!("Devices mounted."); + let (interface_device, controller_device) = drivers_std::tuntap::new("xila0", false, true) + .expect("Failed to create network interface."); + + let network_manager = network::initialize( + task_manager, + virtual_file_system, + &drivers_shared::devices::RandomDevice, + ); + + network_manager + .mount_interface(task, "tunnel0", interface_device, controller_device, None) + .await + .expect("Failed to mount network interface."); + + let mut file = File::open( + virtual_file_system, + task, + "/devices/network/tunnel0", + AccessFlags::READ_WRITE.into(), + ) + .await + .expect("Failed to open network interface file."); + + for ip_cidr in drivers_std::tuntap::IP_ADDRESSES { + file.control(ADD_IP_ADDRESS, ip_cidr).await.ok(); + } + + for route in drivers_std::tuntap::ROUTES { + file.control(ADD_ROUTE, route).await.ok(); + } + + for dns_server in drivers_std::tuntap::DEFAULT_DNS_SERVERS { + file.control(ADD_DNS_SERVER, dns_server).await.ok(); + } + + file.close(virtual_file_system).await.unwrap(); // Initialize the virtual machine virtual_machine::initialize(&[&host_bindings::GraphicsBindings]); @@ -180,8 +213,6 @@ async fn main() { Box::pin(new_thread_executor()) } - log::information!("Mounting executables..."); - mount_executables!( virtual_file_system, task, From 55ce2a50663605ce373c9b71b67a9bfd7012040a Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sun, 25 Jan 2026 13:06:10 +0100 Subject: [PATCH 78/78] refactor: streamline virtual file system initialization --- examples/wasm/src/main.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/examples/wasm/src/main.rs b/examples/wasm/src/main.rs index 2cbfe67f..9edd1580 100644 --- a/examples/wasm/src/main.rs +++ b/examples/wasm/src/main.rs @@ -93,14 +93,9 @@ async fn main() { let file_system = little_fs::FileSystem::get_or_format(partition, 256).unwrap(); // Initialize the virtual file system - let virtual_file_system = virtual_file_system::initialize( - task_manager, - users_manager, - time_manager, - file_system, - None, - ) - .unwrap(); + let virtual_file_system = + virtual_file_system::initialize(task_manager, users_manager, time_manager, file_system) + .unwrap(); // - - Mount the devices