vfs: symlink creation
This commit is contained in:
parent
f13f756c20
commit
df0a48ca42
@ -28,8 +28,11 @@ impl Ext2Fs {
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
self.with_superblock_mut(|sb| Ok(superblock_mapper(sb)))
|
||||
.await?;
|
||||
self.with_superblock_mut(|sb| {
|
||||
superblock_mapper(sb);
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -282,7 +282,7 @@ impl InodeMode {
|
||||
match ty {
|
||||
FileType::File => Self(0o644 | 0x8000),
|
||||
FileType::Directory => Self(0o755 | 0x4000),
|
||||
FileType::Symlink => todo!(),
|
||||
FileType::Symlink => Self(0o755 | 0xA000),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
@ -11,14 +11,14 @@ use libk::{
|
||||
block,
|
||||
error::Error,
|
||||
vfs::{
|
||||
CommonImpl, DirectoryImpl, DirectoryOpenPosition, Filename, Metadata, Node, NodeFlags,
|
||||
NodeRef,
|
||||
CommonImpl, CreateFileType, CreateInfo, DirectoryImpl, DirectoryOpenPosition, Filename,
|
||||
Metadata, Node, NodeFlags, NodeRef,
|
||||
},
|
||||
};
|
||||
use walk::{DirentIter, DirentIterMut};
|
||||
use yggdrasil_abi::io::{DirectoryEntry, FileMode, FileType};
|
||||
use yggdrasil_abi::io::{DirectoryEntry, FileType};
|
||||
|
||||
use crate::{file::RegularNode, inode::InodeAccess, Dirent, Ext2Fs, Inode};
|
||||
use crate::{file::RegularNode, inode::InodeAccess, symlink::SymlinkNode, Dirent, Ext2Fs, Inode};
|
||||
|
||||
mod walk;
|
||||
|
||||
@ -245,6 +245,9 @@ impl DirectoryNode {
|
||||
data if let Some(file) = data.downcast_ref::<RegularNode>() => {
|
||||
(file.inode.ino(), false)
|
||||
}
|
||||
data if let Some(link) = data.downcast_ref::<SymlinkNode>() => {
|
||||
(link.inode.ino(), false)
|
||||
}
|
||||
_ => return Err(Error::InvalidOperation),
|
||||
};
|
||||
|
||||
@ -379,15 +382,20 @@ impl DirectoryImpl for DirectoryNode {
|
||||
block!(self.lookup_entry(name).await)?
|
||||
}
|
||||
|
||||
fn create_node(&self, _parent: &NodeRef, ty: FileType) -> Result<NodeRef, Error> {
|
||||
fn create_node(&self, _parent: &NodeRef, info: &CreateInfo) -> Result<NodeRef, Error> {
|
||||
if self.fs.force_readonly {
|
||||
return Err(Error::ReadOnly);
|
||||
}
|
||||
let mode = match ty {
|
||||
FileType::Directory | FileType::Symlink => FileMode::default_dir(),
|
||||
_ => FileMode::default_file(),
|
||||
|
||||
let (ty, symlink) = match info.ty {
|
||||
CreateFileType::File => (FileType::File, None),
|
||||
CreateFileType::Directory => (FileType::Directory, None),
|
||||
CreateFileType::Symlink(symlink) => (FileType::Symlink, Some(symlink)),
|
||||
};
|
||||
block!(InodeAccess::allocate(&self.fs, ty, mode, Some(self.inode.ino())).await)?
|
||||
|
||||
block!(
|
||||
InodeAccess::allocate(&self.fs, ty, info.mode, Some(self.inode.ino()), symlink).await
|
||||
)?
|
||||
}
|
||||
|
||||
fn attach_node(
|
||||
@ -404,6 +412,7 @@ impl DirectoryImpl for DirectoryNode {
|
||||
let child_ino = match child.data_as_any() {
|
||||
data if let Some(dir) = data.downcast_ref::<DirectoryNode>() => dir.inode.ino(),
|
||||
data if let Some(file) = data.downcast_ref::<RegularNode>() => file.inode.ino(),
|
||||
data if let Some(link) = data.downcast_ref::<SymlinkNode>() => link.inode.ino(),
|
||||
_ => return Err(Error::InvalidOperation),
|
||||
};
|
||||
block!(self.create_entry(name, child_ino).await)??;
|
||||
|
@ -9,9 +9,14 @@ use libk::{
|
||||
vfs::{Metadata, NodeRef},
|
||||
};
|
||||
use libk_util::{lru_hash_table::LruCache, sync::spin_rwlock::IrqSafeRwLock};
|
||||
use yggdrasil_abi::io::{FileMode, FileType};
|
||||
use yggdrasil_abi::{
|
||||
io::{FileMode, FileType},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use crate::{data::InodeMode, dir::DirectoryNode, file::RegularNode, Ext2Fs, Inode};
|
||||
use crate::{
|
||||
data::InodeMode, dir::DirectoryNode, file::RegularNode, symlink::SymlinkNode, Ext2Fs, Inode,
|
||||
};
|
||||
|
||||
pub struct InodeHolder {
|
||||
inode: Inode,
|
||||
@ -189,6 +194,7 @@ impl InodeAccess {
|
||||
mode: FileMode,
|
||||
parent_ino: Option<u32>,
|
||||
ino: u32,
|
||||
symlink_target: Option<&Path>,
|
||||
) -> Result<NodeRef, Error> {
|
||||
log::info!("ext2: allocated inode #{ino}");
|
||||
|
||||
@ -198,24 +204,39 @@ impl InodeAccess {
|
||||
let mut imode = InodeMode::default_for_type(ty);
|
||||
imode.update_permissions(mode);
|
||||
|
||||
fs.write_inode(
|
||||
ino,
|
||||
&Inode {
|
||||
ctime: now,
|
||||
mtime: now,
|
||||
atime: now,
|
||||
mode: imode,
|
||||
..Inode::zeroed()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let mut inode = Inode {
|
||||
ctime: now,
|
||||
mtime: now,
|
||||
atime: now,
|
||||
mode: imode,
|
||||
..Inode::zeroed()
|
||||
};
|
||||
|
||||
if let Some(symlink_target) = symlink_target {
|
||||
log::info!("Write inode path: {symlink_target:?}");
|
||||
let symlink_target = symlink_target.as_str().as_bytes();
|
||||
if symlink_target.len() < 60 {
|
||||
// Place into block pointers
|
||||
unsafe { SymlinkNode::write_link_to_inode_blocks(&mut inode, symlink_target) }
|
||||
} else {
|
||||
// Or allocate a block
|
||||
inode.resize(fs, symlink_target.len() as u64).await?;
|
||||
fs.with_inode_block_mut(&inode, 0, symlink_target.len(), |block| {
|
||||
block[..symlink_target.len()].copy_from_slice(symlink_target);
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
fs.write_inode(ino, &inode).await?;
|
||||
|
||||
let this = InodeAccess::new(cache, ino);
|
||||
let fs = fs.clone();
|
||||
let node = match ty {
|
||||
FileType::Directory => DirectoryNode::create(fs, this, parent_ino).await?,
|
||||
FileType::File => RegularNode::new(fs, this),
|
||||
FileType::Symlink => todo!(),
|
||||
FileType::Symlink => SymlinkNode::new(fs, this),
|
||||
_ => return Err(Error::NotImplemented),
|
||||
};
|
||||
|
||||
@ -236,7 +257,15 @@ impl InodeAccess {
|
||||
ty: FileType,
|
||||
mode: FileMode,
|
||||
parent_ino: Option<u32>,
|
||||
symlink_target: Option<&Path>,
|
||||
) -> Result<NodeRef, Error> {
|
||||
if symlink_target
|
||||
.map(|link| link.len() > fs.block_size)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
log::warn!("ext2: symlink target too long");
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
if parent_ino.is_none() && ty != FileType::Directory {
|
||||
log::warn!("ext2: cannot allocate non-directory inode without a parent");
|
||||
return Err(Error::InvalidOperation);
|
||||
@ -248,7 +277,7 @@ impl InodeAccess {
|
||||
|
||||
let ino = fs.allocate_inode(ty == FileType::Directory).await?;
|
||||
|
||||
match Self::populate_inode(fs, ty, mode, parent_ino, ino).await {
|
||||
match Self::populate_inode(fs, ty, mode, parent_ino, ino, symlink_target).await {
|
||||
Ok(node) => Ok(node),
|
||||
Err(error) => {
|
||||
log::warn!("ext2: couldn't set up inode #{ino}: {error:?}");
|
||||
|
@ -12,7 +12,7 @@ use crate::{inode::InodeAccess, Ext2Fs, Inode};
|
||||
|
||||
pub struct SymlinkNode {
|
||||
fs: Arc<Ext2Fs>,
|
||||
inode: InodeAccess,
|
||||
pub(crate) inode: InodeAccess,
|
||||
cache: IrqSafeRwLock<Vec<u8>>,
|
||||
}
|
||||
|
||||
@ -70,6 +70,12 @@ impl SymlinkNode {
|
||||
debug_assert!(len < 60);
|
||||
&bytemuck::bytes_of(&inode.blocks)[..len]
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn write_link_to_inode_blocks(inode: &mut Inode, link: &[u8]) {
|
||||
debug_assert!(link.len() < 60);
|
||||
bytemuck::bytes_of_mut(&mut inode.blocks)[..link.len()].copy_from_slice(link);
|
||||
inode.size_lower = link.len() as _;
|
||||
}
|
||||
}
|
||||
|
||||
impl CommonImpl for SymlinkNode {
|
||||
|
@ -1,12 +1,10 @@
|
||||
use alloc::sync::Arc;
|
||||
|
||||
use libk::vfs::{
|
||||
CommonImpl, DirectoryImpl, DirectoryOpenPosition, Metadata, Node, NodeFlags, NodeRef,
|
||||
};
|
||||
use yggdrasil_abi::{
|
||||
error::Error,
|
||||
io::{FileMode, FileType},
|
||||
impls::fixed_path_symlink_ext, CommonImpl, CreateFileType, CreateInfo, DirectoryImpl,
|
||||
DirectoryOpenPosition, Metadata, Node, NodeFlags, NodeRef,
|
||||
};
|
||||
use yggdrasil_abi::error::Error;
|
||||
|
||||
use crate::{block::BlockAllocator, file::FileNode, MemoryFilesystem};
|
||||
|
||||
@ -32,17 +30,14 @@ impl<A: BlockAllocator> DirectoryImpl for DirectoryNode<A> {
|
||||
Ok(DirectoryOpenPosition::FromCache)
|
||||
}
|
||||
|
||||
fn create_node(&self, _parent: &NodeRef, ty: FileType) -> Result<NodeRef, Error> {
|
||||
match ty {
|
||||
FileType::File => Ok(FileNode::<A>::new(
|
||||
self.fs.clone(),
|
||||
Metadata::now_root(FileMode::new(0o644)),
|
||||
)),
|
||||
FileType::Directory => Ok(DirectoryNode::<A>::new(
|
||||
self.fs.clone(),
|
||||
Metadata::now_root(FileMode::new(0o755)),
|
||||
)),
|
||||
_ => todo!(),
|
||||
fn create_node(&self, _parent: &NodeRef, info: &CreateInfo) -> Result<NodeRef, Error> {
|
||||
let metadata = Metadata::now(info.uid, info.gid, info.mode);
|
||||
match info.ty {
|
||||
CreateFileType::File => Ok(FileNode::<A>::new(self.fs.clone(), metadata)),
|
||||
CreateFileType::Directory => Ok(DirectoryNode::<A>::new(self.fs.clone(), metadata)),
|
||||
CreateFileType::Symlink(target) => {
|
||||
Ok(fixed_path_symlink_ext(target.to_string(), metadata))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use yggdrasil_abi::{
|
||||
error::Error,
|
||||
io::{AccessMode, FileMode, FileType, GroupId, OpenOptions, UserId},
|
||||
io::{AccessMode, FileMode, GroupId, OpenOptions, UserId},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
@ -9,7 +9,7 @@ use crate::vfs::{
|
||||
FileRef, Filename, NodeRef,
|
||||
};
|
||||
|
||||
use super::{path::OwnedFilename, Node};
|
||||
use super::{node::CreateFileType, Node};
|
||||
|
||||
/// Describes a general filesystem access
|
||||
pub enum Action {
|
||||
@ -189,13 +189,13 @@ impl IoContext {
|
||||
// let create_mode = mode & !self.umask;
|
||||
let (parent, name) = path.split_right();
|
||||
let parent = self.find(at, parent, true)?;
|
||||
let filename = OwnedFilename::new(name)?;
|
||||
let filename = Filename::new(name)?;
|
||||
let create_info = CreateInfo {
|
||||
name: filename,
|
||||
mode: mode & !self.umask,
|
||||
uid: self.uid,
|
||||
gid: self.gid,
|
||||
ty: FileType::File,
|
||||
ty: CreateFileType::File,
|
||||
};
|
||||
let access = self.check_access(&parent, AccessMode::WRITE)?;
|
||||
parent.create(create_info, access)?
|
||||
@ -233,10 +233,34 @@ impl IoContext {
|
||||
let (parent, name) = path.split_right();
|
||||
let parent = self.find(at, parent, true)?;
|
||||
let access = self.check_access(&parent, AccessMode::WRITE)?;
|
||||
let filename = OwnedFilename::new(name)?;
|
||||
let filename = Filename::new(name)?;
|
||||
let create_info = CreateInfo {
|
||||
ty: CreateFileType::Directory,
|
||||
name: filename,
|
||||
uid: self.uid,
|
||||
gid: self.gid,
|
||||
mode: mode & !self.umask,
|
||||
};
|
||||
|
||||
parent.create(create_info, access)
|
||||
}
|
||||
|
||||
pub fn create_symlink<P: AsRef<Path>, Q: AsRef<Path>>(
|
||||
&mut self,
|
||||
at: Option<NodeRef>,
|
||||
target: P,
|
||||
path: Q,
|
||||
mode: FileMode,
|
||||
) -> Result<NodeRef, Error> {
|
||||
let target = target.as_ref();
|
||||
let path = path.as_ref();
|
||||
let (parent, name) = path.split_right();
|
||||
let parent = self.find(at, parent, true)?;
|
||||
let access = self.check_access(&parent, AccessMode::WRITE)?;
|
||||
let filename = Filename::new(name)?;
|
||||
let create_info = CreateInfo {
|
||||
ty: CreateFileType::Symlink(target),
|
||||
name: filename,
|
||||
ty: FileType::Directory,
|
||||
uid: self.uid,
|
||||
gid: self.gid,
|
||||
mode: mode & !self.umask,
|
||||
|
@ -28,8 +28,8 @@ pub use filesystem::{
|
||||
};
|
||||
pub use ioctx::{Action, IoContext};
|
||||
pub use node::{
|
||||
impls, AccessToken, CommonImpl, CreateInfo, DirectoryImpl, Metadata, Node, NodeFlags, NodeRef,
|
||||
RegularImpl, SymlinkImpl,
|
||||
impls, AccessToken, CommonImpl, CreateFileType, CreateInfo, DirectoryImpl, Metadata, Node,
|
||||
NodeFlags, NodeRef, RegularImpl, SymlinkImpl,
|
||||
};
|
||||
pub use path::{Filename, OwnedFilename};
|
||||
pub use poll::FdPoll;
|
||||
|
@ -480,17 +480,21 @@ where
|
||||
dir
|
||||
}
|
||||
|
||||
pub fn fixed_path_symlink(target: impl Into<String>) -> NodeRef {
|
||||
pub fn fixed_path_symlink_ext(target: impl Into<String>, metadata: Metadata) -> NodeRef {
|
||||
Node::symlink(
|
||||
FixedPathSymlink {
|
||||
target: target.into(),
|
||||
},
|
||||
NodeFlags::IN_MEMORY_PROPS,
|
||||
Some(Metadata::now_root(FileMode::new(0o555))),
|
||||
Some(metadata),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fixed_path_symlink(target: impl Into<String>) -> NodeRef {
|
||||
fixed_path_symlink_ext(target, Metadata::now_root(FileMode::new(0o555)))
|
||||
}
|
||||
|
||||
pub fn fixed_hardlink(target: NodeRef) -> Result<NodeRef, Error> {
|
||||
if target.is_hard_link() || target.ty() != FileType::File {
|
||||
return Err(Error::InvalidOperation);
|
||||
|
@ -15,6 +15,7 @@ use yggdrasil_abi::{
|
||||
bitflags,
|
||||
error::Error,
|
||||
io::{FileMode, FileType, GroupId, UserId},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
mod access;
|
||||
@ -39,7 +40,7 @@ use crate::{
|
||||
use super::{
|
||||
path::OwnedFilename,
|
||||
pty::{PseudoTerminalMaster, PseudoTerminalSlave},
|
||||
Filesystem,
|
||||
Filename, Filesystem,
|
||||
};
|
||||
|
||||
/// Wrapper type for a [Node] shared reference
|
||||
@ -57,19 +58,26 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CreateFileType<'a> {
|
||||
Symlink(&'a Path),
|
||||
Directory,
|
||||
File,
|
||||
}
|
||||
|
||||
/// Information used when creating an entry within a directory
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateInfo {
|
||||
pub struct CreateInfo<'a> {
|
||||
/// New entry name
|
||||
pub name: OwnedFilename,
|
||||
pub name: &'a Filename,
|
||||
/// User ID of the entry
|
||||
pub uid: UserId,
|
||||
/// Group ID of the entry
|
||||
pub gid: GroupId,
|
||||
/// Access mode of the entry
|
||||
pub mode: FileMode,
|
||||
/// Entry type
|
||||
pub ty: FileType,
|
||||
/// Node type to be created
|
||||
pub ty: CreateFileType<'a>,
|
||||
}
|
||||
|
||||
pub(crate) struct DirectoryData {
|
||||
|
@ -105,7 +105,7 @@ impl Node {
|
||||
/// Creates an entry within a directory with given [CreateInfo].
|
||||
pub fn create(self: &NodeRef, info: CreateInfo, check: AccessToken) -> Result<NodeRef, Error> {
|
||||
let directory = self.as_directory()?;
|
||||
let node = directory.imp.create_node(self, info.ty)?;
|
||||
let node = directory.imp.create_node(self, &info)?;
|
||||
|
||||
self.create_node(node.clone(), info.name, check.clone())?;
|
||||
|
||||
|
@ -3,7 +3,7 @@ use core::{any::Any, mem::MaybeUninit};
|
||||
|
||||
use yggdrasil_abi::{
|
||||
error::Error,
|
||||
io::{DirectoryEntry, FileType, OpenOptions},
|
||||
io::{DirectoryEntry, OpenOptions},
|
||||
};
|
||||
|
||||
use crate::vfs::{
|
||||
@ -11,7 +11,7 @@ use crate::vfs::{
|
||||
Filename,
|
||||
};
|
||||
|
||||
use super::{Metadata, NodeRef};
|
||||
use super::{CreateInfo, Metadata, NodeRef};
|
||||
|
||||
/// Common interface shared by all filesystem nodes
|
||||
pub trait CommonImpl: Send + Sync {
|
||||
@ -120,9 +120,9 @@ pub trait DirectoryImpl: CommonImpl {
|
||||
}
|
||||
|
||||
/// Creates a child node, but does not associate it with the directory yet
|
||||
fn create_node(&self, parent: &NodeRef, ty: FileType) -> Result<NodeRef, Error> {
|
||||
fn create_node(&self, parent: &NodeRef, info: &CreateInfo) -> Result<NodeRef, Error> {
|
||||
let _ = parent;
|
||||
let _ = ty;
|
||||
let _ = info;
|
||||
Err(Error::ReadOnly)
|
||||
}
|
||||
|
||||
|
@ -153,6 +153,17 @@ pub(crate) fn create_directory(at: Option<RawFd>, path: &str, mode: FileMode) ->
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn create_symlink(at: Option<RawFd>, target: &str, path: &str) -> Result<(), Error> {
|
||||
let thread = Thread::current();
|
||||
let process = thread.process();
|
||||
|
||||
run_with_io_at(&process, at, |at, mut io| {
|
||||
io.ioctx_mut()
|
||||
.create_symlink(Some(at), target, path, FileMode::default_dir())?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn remove_directory(_at: Option<RawFd>, _path: &str) -> Result<(), Error> {
|
||||
todo!()
|
||||
}
|
||||
|
@ -124,6 +124,7 @@ syscall open_directory(at: Option<RawFd>, path: &str) -> Result<RawFd>;
|
||||
syscall read_directory_entries(fd: RawFd, entries: &mut [MaybeUninit<DirectoryEntry>]) -> Result<usize>;
|
||||
syscall create_directory(at: Option<RawFd>, path: &str, mode: FileMode) -> Result<()>;
|
||||
syscall remove_directory(at: Option<RawFd>, path: &str) -> Result<()>;
|
||||
syscall create_symlink(at: Option<RawFd>, target: &str, path: &str) -> Result<()>;
|
||||
|
||||
syscall read_link(at: Option<RawFd>, path: &str, buf: &mut [u8]) -> Result<usize>;
|
||||
syscall remove(at: Option<RawFd>, path: &str) -> Result<()>;
|
||||
|
@ -1,5 +1,7 @@
|
||||
use core::ffi::CStr;
|
||||
|
||||
use alloc::string::String;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
@ -106,6 +108,12 @@ impl Path {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
#[inline]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Path {
|
||||
|
@ -56,6 +56,10 @@ path = "src/ls.rs"
|
||||
name = "mv"
|
||||
path = "src/mv.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "ln"
|
||||
path = "src/ln.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "mkdir"
|
||||
path = "src/mkdir.rs"
|
||||
|
22
userspace/sysutils/src/ln.rs
Normal file
22
userspace/sysutils/src/ln.rs
Normal file
@ -0,0 +1,22 @@
|
||||
#![feature(yggdrasil_os)]
|
||||
use std::{path::PathBuf, process::ExitCode, os::yggdrasil::fs};
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Args {
|
||||
target: PathBuf,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Args::parse();
|
||||
|
||||
match fs::symlink(&args.target, &args.path) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("{}: {error}", args.path.display());
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ const PROGRAMS: &[(&str, &str)] = &[
|
||||
("login", "sbin/login"),
|
||||
("ls", "bin/ls"),
|
||||
("mv", "bin/mv"),
|
||||
("ln", "bin/ln"),
|
||||
("mkdir", "bin/mkdir"),
|
||||
("touch", "bin/touch"),
|
||||
("rm", "bin/rm"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user