First prototype

This commit is contained in:
Fabian Stamm 2024-08-18 15:41:31 +02:00
commit c970c1dfbd
13 changed files with 3498 additions and 0 deletions

2
.editorconfig Normal file
View File

@ -0,0 +1,2 @@
[*.rs]
indent_size = 4

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

45
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,45 @@
{
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'ACL_Editor'",
"cargo": {
"args": [
"build",
"--bin=ACL_Editor",
"--package=ACL_Editor"
],
"filter": {
"name": "ACL_Editor",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'ACL_Editor'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=ACL_Editor",
"--package=ACL_Editor"
],
"filter": {
"name": "ACL_Editor",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

2810
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "ACL_Editor"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1"
eframe = { version = "0.28.1", features = [
"wayland",
"wgpu",
"default_fonts",
], default-features = false }
egui_extras = "0.28.1"
env_logger = { version = "0.11", features = ["auto-color", "humantime"] }
posix-acl = { git = "https://git.hibas.dev/hibas123/PosixACL", tag = "0.1.5" }
walkdir = "2"

Binary file not shown.

24
src/helper/acl_writer.rs Normal file
View File

@ -0,0 +1,24 @@
use std::path::Path;
use anyhow::Result;
use posix_acl::PosixACL;
use walkdir::WalkDir;
pub fn write_acl_recursive<P: AsRef<Path>>(path: P, acl: PosixACL) -> Result<()> {
for entry in WalkDir::new(path).min_depth(0).follow_links(false) {
let entry = entry?;
let path = entry.path();
println!("Writing ACL for: {:?} {:?}", path, acl);
match entry.file_type().is_dir() {
true => {
acl.write(&path)?;
acl.write_default(&path)?;
}
false => {
acl.write(&path)?;
}
}
}
Ok(())
}

49
src/helper/getent.rs Normal file
View File

@ -0,0 +1,49 @@
use anyhow::Result;
use std::process::Command;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct User {
pub name: String,
pub uid: u32,
}
pub fn get_users() -> Result<Vec<User>> {
let raw = Command::new("getent").arg("passwd").output()?;
let raw = String::from_utf8_lossy(&raw.stdout);
let users: Vec<User> = raw
.lines()
.map(|line| {
let parts: Vec<&str> = line.split(':').collect();
User {
name: parts[0].to_string(),
uid: parts[2].parse().unwrap(),
}
})
.collect();
Ok(users)
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Group {
pub name: String,
pub gid: u32,
}
pub fn get_groups() -> Result<Vec<Group>> {
let raw = Command::new("getent").arg("group").output()?;
let raw = String::from_utf8_lossy(&raw.stdout);
let groups: Vec<Group> = raw
.lines()
.map(|line| {
let parts: Vec<&str> = line.split(':').collect();
Group {
name: parts[0].to_string(),
gid: parts[2].parse().unwrap(),
}
})
.collect();
Ok(groups)
}

2
src/helper/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod acl_writer;
pub mod getent;

22
src/main.rs Normal file
View File

@ -0,0 +1,22 @@
use anyhow::Result;
use eframe::egui;
mod helper;
mod ui;
fn main() -> Result<()> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]),
hardware_acceleration: eframe::HardwareAcceleration::Preferred,
..Default::default()
};
eframe::run_native(
"ACL Editor",
options,
Box::new(|cc| Ok(Box::new(ui::App::new(&cc.egui_ctx)))),
)
.expect("Failed to run eframe app");
Ok(())
}

345
src/ui/editor.rs Normal file
View File

@ -0,0 +1,345 @@
use std::{borrow::Borrow, path::PathBuf, thread::JoinHandle};
use anyhow::Result;
use eframe::egui;
use egui_extras::{Column, TableBuilder};
use crate::helper::getent::{get_groups, get_users, Group, User};
use posix_acl::{ACLEntry, PermSet, PosixACL, Qualifier, ACL_RWX};
pub struct ACLEditor {
pub path: PathBuf,
pub is_changed: bool,
pub available_users: Vec<User>,
pub available_groups: Vec<Group>,
pub acl: PosixACL,
pub search_user: String,
pub search_group: String,
pub save_thread: Option<JoinHandle<Result<()>>>,
pub save_thread_error: Option<String>,
}
impl ACLEditor {
pub fn new() -> Self {
Self {
path: PathBuf::new(),
is_changed: false,
available_groups: get_groups().unwrap(),
available_users: get_users().unwrap(),
acl: PosixACL::new(ACL_RWX, ACL_RWX, ACL_RWX),
search_user: String::new(),
search_group: String::new(),
save_thread: None,
save_thread_error: None,
}
}
pub fn show(&mut self, ui: &mut egui::Ui, selected_path: &PathBuf) {
let mut show_save_or_dicard_message = false;
ui.vertical(|ui| {
if self.save_thread.is_some() {
if self.save_thread.as_ref().unwrap().is_finished() {
let save_thread = std::mem::replace(&mut self.save_thread, None);
let result = save_thread.unwrap().join();
match result {
Ok(Ok(_)) => {
ui.label("Saved successfully!");
self.save_thread_error = None;
self.is_changed = false;
}
Ok(Err(e)) => {
self.save_thread_error = Some(format!("Failed to save {:?}", e));
}
Err(e) => {
self.save_thread_error = Some(format!("Failed to join {:?}", e));
}
}
} else {
egui::Frame::default()
.fill(egui::Color32::GREEN)
.inner_margin(4.0)
.show(ui, |ui| {
ui.label("Apply changes...");
});
}
} else if let Some(err) = &self.save_thread_error.clone() {
egui::Frame::default()
.fill(egui::Color32::DARK_RED)
.inner_margin(4.0)
.show(ui, |ui| {
ui.label(err);
if ui.button("OK").clicked() {
self.save_thread_error = None;
}
});
} else {
ui.heading(format!("Editing: {:?}", self.path));
ui.separator();
if self.path != *selected_path {
if !self.is_changed {
// Load ACLs
self.path = selected_path.clone();
self.acl = PosixACL::new_from_file(&self.path, false).unwrap();
} else {
show_save_or_dicard_message = true;
}
}
self.acl_table(ui);
ui.separator();
ui.label("Add new entry:");
self.new_user(ui);
self.new_group(ui);
ui.separator();
if show_save_or_dicard_message {
egui::Frame::default()
.fill(egui::Color32::YELLOW)
.inner_margin(4.0)
.show(ui, |ui| {
ui.label("You have unsaved changes! Save or discard to continue.");
});
}
ui.horizontal(|ui| {
if ui.button("Save").clicked() {
// TODO: Save ACLs
let path = self.path.clone();
let acl = self.acl.clone();
self.save_thread_error = None;
self.save_thread = Some(std::thread::spawn(move || {
crate::helper::acl_writer::write_acl_recursive(path, acl)
}));
}
if ui.button("Discard").clicked() {
self.is_changed = false;
self.path.clear();
}
});
}
});
}
fn new_group(&mut self, ui: &mut egui::Ui) {
ui.label("Group:");
let selected = filter_dropbox(
ui,
"group_popup",
&mut self.search_group,
self.available_groups.iter(),
|group, filter| group.name.contains(filter),
|group| group.name.as_str(),
);
if let Some(selected) = selected {
self.acl
.entries
.push(ACLEntry(Qualifier::Group(selected.gid), PermSet::ACL_RWX));
self.is_changed = true;
}
}
fn new_user(&mut self, ui: &mut egui::Ui) {
ui.label("User:");
let selected = filter_dropbox(
ui,
"user_popup",
&mut self.search_user,
self.available_users.iter(),
|user, filter| user.name.contains(filter),
|user| user.name.as_str(),
);
if let Some(selected) = selected {
self.acl
.entries
.push(ACLEntry(Qualifier::User(selected.uid), PermSet::ACL_RWX));
self.is_changed = true;
}
}
fn acl_table(&mut self, ui: &mut egui::Ui) {
TableBuilder::new(ui)
.auto_shrink([false, true])
.striped(true)
.column(Column::auto().resizable(false))
.column(Column::remainder().resizable(false))
.column(Column::auto().resizable(false))
.column(Column::auto().resizable(false))
.column(Column::auto().resizable(false))
.column(Column::auto().resizable(false))
.header(20.0, |mut header| {
header.col(|ui| {
ui.heading("Type");
});
header.col(|ui| {
ui.heading("Name");
});
header.col(|ui| {
ui.heading("R");
});
header.col(|ui| {
ui.heading("W");
});
header.col(|ui| {
ui.heading("X");
});
header.col(|ui| {
ui.heading("D");
});
})
.body(|mut body| {
let mut to_delete: Vec<Qualifier> = Vec::new();
self.acl.entries.sort();
for entry in &mut self.acl.entries {
body.row(30.0, |mut row| {
row.col(|ui| {
let label = match entry.0 {
Qualifier::User(_) => "U",
Qualifier::Group(_) => "G",
Qualifier::Other => "",
Qualifier::GroupObj => "G",
Qualifier::UserObj => "U",
Qualifier::Mask => "",
};
ui.label(label);
});
row.col(|ui| {
let label = match entry.0 {
Qualifier::User(uid) => self
.available_users
.iter()
.find(|user| user.uid == uid)
.map(|user| user.name.clone())
.unwrap_or_else(|| format!("Unknown user {}", uid)),
Qualifier::Group(gid) => self
.available_groups
.iter()
.find(|group| group.gid == gid)
.map(|group| group.name.clone())
.unwrap_or_else(|| format!("Unknown group {}", gid)),
Qualifier::Other => "Other".to_string(),
Qualifier::GroupObj => "GroupObj".to_string(),
Qualifier::UserObj => "UserObj".to_string(),
Qualifier::Mask => "Mask".to_string(),
};
ui.label(label);
});
row.col(|ui| {
let mut checked = entry.1.contains(PermSet::ACL_READ);
if ui.checkbox(&mut checked, "").changed() {
if checked {
entry.1.insert(PermSet::ACL_READ);
self.is_changed = true;
} else {
entry.1.remove(PermSet::ACL_READ);
self.is_changed = true;
}
}
});
row.col(|ui| {
let mut checked = entry.1.contains(PermSet::ACL_WRITE);
if ui.checkbox(&mut checked, "").changed() {
if checked {
entry.1.insert(PermSet::ACL_WRITE);
self.is_changed = true;
} else {
entry.1.remove(PermSet::ACL_WRITE);
self.is_changed = true;
}
}
});
row.col(|ui| {
let mut checked = entry.1.contains(PermSet::ACL_EXECUTE);
if ui.checkbox(&mut checked, "").changed() {
if checked {
entry.1.insert(PermSet::ACL_EXECUTE);
self.is_changed = true;
} else {
entry.1.remove(PermSet::ACL_EXECUTE);
self.is_changed = true;
}
}
});
row.col(|ui| match entry.0 {
Qualifier::User(_) | Qualifier::Group(_) => {
if ui.button("🗑").clicked() {
to_delete.push(entry.0.clone());
}
}
_ => {}
});
});
}
for entry in to_delete {
self.acl.entries.retain(|e| e.0 != entry);
self.is_changed = true;
}
});
}
}
// fn searchable_dropdown()
fn filter_dropbox<
'a,
T: PartialEq + 'a,
I: Iterator<Item = &'a T> + 'a,
F: Fn(&'a T, &str) -> bool,
L: Fn(&'a T) -> &str,
>(
ui: &mut egui::Ui,
popup_id: &str,
search_buf: &mut String,
items: I,
filter_items: F,
get_label: L,
) -> Option<&'a T> {
let search_field = ui.text_edit_singleline(search_buf);
let popup_id = ui.make_persistent_id(popup_id);
if search_field.gained_focus() {
ui.memory_mut(|mem| mem.open_popup(popup_id));
}
let mut selected = None;
egui::popup_above_or_below_widget(
&ui,
popup_id,
&search_field,
egui::AboveOrBelow::Below,
egui::PopupCloseBehavior::CloseOnClick,
|ui| {
ui.set_min_width(200.0);
ui.set_max_height(300.0);
egui::ScrollArea::vertical().show(ui, |ui| {
let filtered = items.filter(|elm| filter_items(elm, &search_buf));
for user in filtered {
let value = ui.selectable_value(&mut selected, Some(user), get_label(user));
if value.clicked() {
search_field.surrender_focus();
}
}
});
},
);
if selected.is_some() {
search_buf.clear();
}
selected
}

60
src/ui/mod.rs Normal file
View File

@ -0,0 +1,60 @@
use editor::ACLEditor;
use eframe::egui::{self};
use tree::Tree;
mod editor;
mod tree;
pub struct App {
tree: Tree,
editor: ACLEditor,
}
impl App {
pub fn new(ctx: &egui::Context) -> Self {
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"FiraCode".to_owned(),
egui::FontData::from_static(include_bytes!("../../fonts/FiraCodeNerdFont-Regular.ttf")),
);
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.insert(0, "FiraCode".to_owned());
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "FiraCode".to_owned());
ctx.set_fonts(fonts);
App {
tree: Tree::new(),
editor: ACLEditor::new(),
}
}
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::SidePanel::right("side_panel")
.min_width(300.0)
.max_width(ctx.available_rect().width() - 100.0)
.resizable(true)
.show(ctx, |ui| {
self.editor.show(ui, &self.tree.selected);
});
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::both()
.auto_shrink([false; 2])
.show(ui, |ui| {
self.tree.show(ui);
});
});
}
}

122
src/ui/tree.rs Normal file
View File

@ -0,0 +1,122 @@
// Build a tree of folders (not files)
// Folders can be expanded and collapsed
// Folders can by selected
// Folders will fetch their children when expanded
use std::path::PathBuf;
use eframe::egui;
pub struct Tree {
root: Folder,
pub selected: PathBuf,
}
impl Tree {
pub fn new() -> Self {
Self {
root: Folder::new("./tmp", "./tmp"),
selected: PathBuf::new(),
}
}
pub fn set_root_path(&mut self, path: impl Into<PathBuf>) {
let path: PathBuf = path.into();
let name = path.to_string_lossy().to_string();
self.root = Folder::new(name, path);
}
pub fn show(&mut self, ui: &mut egui::Ui) {
ui.vertical(|ui| {
self.root.show(ui, &mut self.selected);
});
}
}
pub struct Folder {
name: String,
path: PathBuf,
expanded: bool,
children: Option<Vec<Folder>>,
// selected: Arc<RwLock<PathBuf>>,
}
impl Folder {
pub fn new(name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
Self {
name: name.into(),
path: path.into(),
expanded: false,
children: Default::default(),
}
}
pub fn show(&mut self, ui: &mut egui::Ui, selected: &mut PathBuf) {
let Self {
name,
path,
expanded,
children,
} = self;
if children.is_none() {
let mut children_data = vec![];
println!("Reading dir: {:?}", path);
std::fs::read_dir(&path)
.map(|entries| {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
println!(
"Found dir: {:?}, file {}, link {}",
path,
path.is_file(),
path.is_symlink()
);
let path2 = path.clone();
let name = path2.file_name().unwrap().to_string_lossy();
children_data.push(Folder::new(name, path));
}
}
})
.unwrap_or_else(|err| {
eprintln!("Failed to read dir: {}", err);
});
children.replace(children_data);
}
ui.horizontal(|ui| {
if let Some(children) = children {
if !children.is_empty() {
let label = if *expanded { "" } else { "" };
let response = ui.selectable_label(false, label);
if response.clicked() && !children.is_empty() {
*expanded = !*expanded;
}
}
} else {
ui.label("");
}
if ui
.selectable_label(*selected == *path, name.to_owned())
.clicked()
{
*selected = path.clone();
}
});
if let Some(children) = children {
if *expanded {
ui.indent("", |ui| {
for child in children {
child.show(ui, selected);
}
});
}
}
}
}