move ls selinux functionality to a plugin

This commit is contained in:
Solomon Victorino 2024-12-16 15:48:31 -07:00
parent ee513e4630
commit c4cc6b6a90
20 changed files with 370 additions and 141 deletions

12
Cargo.lock generated
View File

@ -3615,6 +3615,7 @@ dependencies = [
name = "nu-plugin-protocol"
version = "0.100.1"
dependencies = [
"nu-engine",
"nu-protocol",
"nu-utils",
"rmp-serde",
@ -3871,6 +3872,17 @@ dependencies = [
"webpage",
]
[[package]]
name = "nu_plugin_selinux"
version = "0.100.1"
dependencies = [
"nu-command",
"nu-plugin",
"nu-plugin-test-support",
"nu-protocol",
"selinux",
]
[[package]]
name = "nu_plugin_stress_internals"
version = "0.100.1"

View File

@ -52,6 +52,7 @@ members = [
"crates/nu_plugin_custom_values",
"crates/nu_plugin_formats",
"crates/nu_plugin_polars",
"crates/nu_plugin_selinux",
"crates/nu_plugin_stress_internals",
"crates/nu-std",
"crates/nu-table",

View File

@ -1073,11 +1073,10 @@ fn flag_completions() {
// Test completions for the 'ls' flags
let suggestions = completer.complete("ls -", 4);
assert_eq!(20, suggestions.len());
assert_eq!(18, suggestions.len());
let expected: Vec<String> = vec![
"--all".into(),
"--context".into(),
"--directory".into(),
"--du".into(),
"--full-paths".into(),
@ -1095,7 +1094,6 @@ fn flag_completions() {
"-m".into(),
"-s".into(),
"-t".into(),
"-Z".into(),
];
// Match results

View File

@ -21,6 +21,8 @@ use std::{
#[derive(Clone)]
pub struct Ls;
pub type EntryMapper = (dyn Fn(&std::path::Path, Value) -> Value + Send + Sync);
#[derive(Clone, Copy)]
struct Args {
all: bool,
@ -31,12 +33,66 @@ struct Args {
directory: bool,
use_mime_type: bool,
use_threads: bool,
security_context: bool,
call_span: Span,
}
const GLOB_CHARS: &[char] = &['*', '?', '['];
impl Ls {
pub fn run_ls(
call_span: Span,
get_signals: &(dyn Fn() -> Signals),
has_flag: &(dyn Fn(&str) -> Result<bool, ShellError>),
has_pattern_arg: bool,
pattern_arg: Vec<Option<Spanned<NuGlob>>>,
cwd: PathBuf,
map_entry: &EntryMapper,
) -> Result<PipelineData, ShellError> {
let args = Args {
all: has_flag("all")?,
long: has_flag("long")?,
short_names: has_flag("short-names")?,
full_paths: has_flag("full-paths")?,
du: has_flag("du")?,
directory: has_flag("directory")?,
use_mime_type: has_flag("mime-type")?,
use_threads: has_flag("threads")?,
call_span,
};
let input_pattern_arg = match has_pattern_arg {
true if pattern_arg.is_empty() => vec![],
true => pattern_arg.clone(),
false => vec![None],
};
let mut result_iters = vec![];
for pat in input_pattern_arg {
result_iters.push(ls_for_one_pattern(
pat,
args,
get_signals(),
cwd.clone(),
map_entry,
)?)
}
// Here nushell needs to use
// use `flatten` to chain all iterators into one.
Ok(result_iters
.into_iter()
.flatten()
.into_pipeline_data_with_metadata(
call_span,
get_signals(),
PipelineMetadata {
data_source: DataSource::Ls,
content_type: None,
},
))
}
}
impl Command for Ls {
fn name(&self) -> &str {
"ls"
@ -50,6 +106,34 @@ impl Command for Ls {
vec!["dir"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let get_signals = &|| engine_state.signals().clone();
let has_flag = &|flag: &str| call.has_flag(engine_state, &mut (stack.clone()), flag);
let pattern_arg = call
.rest::<Spanned<NuGlob>>(engine_state, &mut stack.clone(), 0)?
.into_iter()
.map(Some)
.collect();
let has_pattern_arg = call.has_positional_args(stack, 0);
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?;
Ls::run_ls(
call.span(),
get_signals,
has_flag,
has_pattern_arg,
pattern_arg,
cwd,
&|_path, entry| entry,
)
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("ls")
.input_output_types(vec![(Type::Nothing, Type::table())])
@ -59,7 +143,7 @@ impl Command for Ls {
.switch("all", "Show hidden files", Some('a'))
.switch(
"long",
"Get almost all available columns for each entry (slower; columns are platform-dependent)",
"Get all available columns for each entry (slower; columns are platform-dependent)",
Some('l'),
)
.switch(
@ -80,96 +164,9 @@ impl Command for Ls {
)
.switch("mime-type", "Show mime-type in type column instead of 'file' (based on filenames only; files' contents are not examined)", Some('m'))
.switch("threads", "Use multiple threads to list contents. Output will be non-deterministic.", Some('t'))
.switch(
"context",
match cfg!(feature = "selinux") {
true => "Get the SELinux security context for each entry, if available",
false => "Get the SELinux security context for each entry (disabled)"
},
Some('Z'),
)
.category(Category::FileSystem)
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let all = call.has_flag(engine_state, stack, "all")?;
let long = call.has_flag(engine_state, stack, "long")?;
let short_names = call.has_flag(engine_state, stack, "short-names")?;
let full_paths = call.has_flag(engine_state, stack, "full-paths")?;
let du = call.has_flag(engine_state, stack, "du")?;
let directory = call.has_flag(engine_state, stack, "directory")?;
let use_mime_type = call.has_flag(engine_state, stack, "mime-type")?;
let use_threads = call.has_flag(engine_state, stack, "threads")?;
let security_context = call.has_flag(engine_state, stack, "context")?;
let call_span = call.head;
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?;
let args = Args {
all,
long,
short_names,
full_paths,
du,
directory,
use_mime_type,
use_threads,
security_context,
call_span,
};
let pattern_arg = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
let input_pattern_arg = if !call.has_positional_args(stack, 0) {
None
} else {
Some(pattern_arg)
};
match input_pattern_arg {
None => Ok(
ls_for_one_pattern(None, args, engine_state.signals().clone(), cwd)?
.into_pipeline_data_with_metadata(
call_span,
engine_state.signals().clone(),
PipelineMetadata {
data_source: DataSource::Ls,
content_type: None,
},
),
),
Some(pattern) => {
let mut result_iters = vec![];
for pat in pattern {
result_iters.push(ls_for_one_pattern(
Some(pat),
args,
engine_state.signals().clone(),
cwd.clone(),
)?)
}
// Here nushell needs to use
// use `flatten` to chain all iterators into one.
Ok(result_iters
.into_iter()
.flatten()
.into_pipeline_data_with_metadata(
call_span,
engine_state.signals().clone(),
PipelineMetadata {
data_source: DataSource::Ls,
content_type: None,
},
))
}
}
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
@ -234,6 +231,7 @@ fn ls_for_one_pattern(
args: Args,
signals: Signals,
cwd: PathBuf,
map_entry: &EntryMapper,
) -> Result<PipelineData, ShellError> {
fn create_pool(num_threads: usize) -> Result<rayon::ThreadPool, ShellError> {
match rayon::ThreadPoolBuilder::new()
@ -263,7 +261,6 @@ fn ls_for_one_pattern(
use_mime_type,
use_threads,
call_span,
security_context,
} = args;
let pattern_arg = {
if let Some(path) = pattern_arg {
@ -450,8 +447,8 @@ fn ls_for_one_pattern(
du,
&signals_clone,
use_mime_type,
args.full_paths,
security_context,
full_paths,
map_entry,
);
match entry {
Ok(value) => Some(value),
@ -570,7 +567,7 @@ pub(crate) fn dir_entry_dict(
signals: &Signals,
use_mime_type: bool,
full_symlink_target: bool,
security_context: bool,
map_entry: &EntryMapper,
) -> Result<Value, ShellError> {
#[cfg(windows)]
if metadata.is_none() {
@ -627,13 +624,6 @@ pub(crate) fn dir_entry_dict(
}
}
if security_context {
record.push(
"security_context",
security_context_value(filename, span).unwrap_or(Value::nothing(span)), // TODO: consider report_shell_warning
)
}
if long {
if let Some(md) = metadata {
record.push("readonly", Value::bool(md.permissions().readonly(), span));
@ -754,7 +744,7 @@ pub(crate) fn dir_entry_dict(
record.push("modified", Value::nothing(span));
}
Ok(Value::record(record, span))
Ok(map_entry(filename, Value::record(record, span)))
}
// TODO: can we get away from local times in `ls`? internals might be cleaner if we worked in UTC
@ -786,28 +776,6 @@ fn try_convert_to_local_date_time(t: SystemTime) -> Option<DateTime<Local>> {
}
}
fn security_context_value(_path: &Path, span: Span) -> Result<Value, ShellError> {
#[cfg(not(all(feature = "selinux", target_os = "linux")))]
return Ok(Value::nothing(span));
#[cfg(all(feature = "selinux", target_os = "linux"))]
{
use selinux;
match selinux::SecurityContext::of_path(_path, false, false)
.map_err(|e| ShellError::IOError { msg: e.to_string() })?
{
Some(con) => {
let bytes = con.as_bytes();
Ok(Value::string(
String::from_utf8_lossy(&bytes[0..bytes.len().saturating_sub(1)]),
span,
))
}
None => Ok(Value::nothing(span)),
}
}
}
// #[cfg(windows)] is just to make Clippy happy, remove if you ever want to use this on other platforms
#[cfg(windows)]
fn unix_time_to_local_date_time(secs: i64) -> Option<DateTime<Local>> {

View File

@ -19,6 +19,7 @@ pub use self::open::Open;
pub use cd::Cd;
pub use du::Du;
pub use glob::Glob;
pub use ls::EntryMapper as LsEntryMapper;
pub use ls::Ls;
pub use mktemp::Mktemp;
pub use rm::Rm;

View File

@ -862,17 +862,3 @@ fn consistent_list_order() {
assert_eq!(no_arg.out, with_arg.out);
})
}
#[cfg(all(feature = "selinux", target_os = "linux"))]
#[test]
fn returns_correct_security_context() {
use nu_test_support::nu_with_std;
let input = "
use std assert
^ls -Z / | lines | each { |e| $e | str trim | split column ' ' 'coreutils_scontext' 'name' | first } \
| join (ls -Z / | each { default '?' security_context }) name \
| each { |e| assert equal $e.security_context $e.coreutils_scontext $'For entry ($e.name) expected ($e.coreutils_scontext), got ($e.security_context)' }
";
assert_eq!(nu_with_std!(input).err, "");
}

View File

@ -115,6 +115,7 @@ macro_rules! generate_tests {
Value::float(1.0, Span::new(0, 10)),
Value::string("something", Span::new(0, 10)),
],
has_positional_args: true,
named: vec![(
Spanned {
item: "name".to_string(),

View File

@ -809,6 +809,7 @@ fn interface_write_plugin_call_writes_run_with_value_input() -> Result<(), Shell
call: EvaluatedCall {
head: Span::test_data(),
positional: vec![],
has_positional_args: false,
named: vec![],
},
input: PipelineData::Value(Value::test_int(-1), Some(metadata0.clone())),
@ -850,6 +851,7 @@ fn interface_write_plugin_call_writes_run_with_stream_input() -> Result<(), Shel
call: EvaluatedCall {
head: Span::test_data(),
positional: vec![],
has_positional_args: false,
named: vec![],
},
input: values
@ -1078,6 +1080,7 @@ fn interface_run() -> Result<(), ShellError> {
call: EvaluatedCall {
head: Span::test_data(),
positional: vec![],
has_positional_args: false,
named: vec![],
},
input: PipelineData::Empty,
@ -1351,6 +1354,7 @@ fn prepare_plugin_call_run() {
call: EvaluatedCall {
head: span,
positional: vec![Value::test_int(4)],
has_positional_args: true,
named: vec![("x".to_owned().into_spanned(span), Some(Value::test_int(6)))],
},
input: PipelineData::Empty,
@ -1363,6 +1367,7 @@ fn prepare_plugin_call_run() {
call: EvaluatedCall {
head: span,
positional: vec![cv_ok.clone()],
has_positional_args: true,
named: vec![("ok".to_owned().into_spanned(span), Some(cv_ok.clone()))],
},
input: PipelineData::Empty,
@ -1375,6 +1380,7 @@ fn prepare_plugin_call_run() {
call: EvaluatedCall {
head: span,
positional: vec![cv_bad.clone()],
has_positional_args: true,
named: vec![],
},
input: PipelineData::Empty,
@ -1387,6 +1393,7 @@ fn prepare_plugin_call_run() {
call: EvaluatedCall {
head: span,
positional: vec![],
has_positional_args: false,
named: vec![("bad".to_owned().into_spanned(span), Some(cv_bad.clone()))],
},
input: PipelineData::Empty,
@ -1399,6 +1406,7 @@ fn prepare_plugin_call_run() {
call: EvaluatedCall {
head: span,
positional: vec![],
has_positional_args: false,
named: vec![],
},
// Shouldn't check input - that happens somewhere else

View File

@ -14,6 +14,7 @@ bench = false
workspace = true
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.100.1" }
nu-protocol = { path = "../nu-protocol", version = "0.100.1", features = ["plugin"] }
nu-utils = { path = "../nu-utils", version = "0.100.1" }

View File

@ -1,3 +1,4 @@
use nu_engine::CallExt;
use nu_protocol::{
ast::{self, Expression},
engine::{Call, CallImpl, EngineState, Stack},
@ -22,6 +23,8 @@ pub struct EvaluatedCall {
pub head: Span,
/// Values of positional arguments
pub positional: Vec<Value>,
/// Whether positional arguments were used, including spreads
pub has_positional_args: bool,
/// Names and values of named arguments
pub named: Vec<(Spanned<String>, Option<Value>)>,
}
@ -32,6 +35,7 @@ impl EvaluatedCall {
EvaluatedCall {
head,
positional: vec![],
has_positional_args: false,
named: vec![],
}
}
@ -49,6 +53,7 @@ impl EvaluatedCall {
/// ```
pub fn add_positional(&mut self, value: Value) -> &mut Self {
self.positional.push(value);
self.has_positional_args = true;
self
}
@ -88,6 +93,7 @@ impl EvaluatedCall {
/// Builder variant of [`.add_positional()`](Self::add_positional).
pub fn with_positional(mut self, value: Value) -> Self {
self.add_positional(value);
self.has_positional_args = true;
self
}
@ -144,6 +150,7 @@ impl EvaluatedCall {
Ok(Self {
head: call.head,
positional,
has_positional_args: call.has_positional_args(stack, 0),
named,
})
}
@ -160,6 +167,7 @@ impl EvaluatedCall {
Ok(Self {
head: call.head,
positional,
has_positional_args: call.has_positional_args(stack, 0),
named,
})
}
@ -178,6 +186,7 @@ impl EvaluatedCall {
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # has_positional_args: false,
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # None
@ -194,6 +203,7 @@ impl EvaluatedCall {
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # has_positional_args: false,
/// # named: vec![(
/// # Spanned { item: "bar".to_owned(), span: null_span},
/// # None
@ -210,6 +220,7 @@ impl EvaluatedCall {
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # has_positional_args: false,
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # Some(Value::bool(true, Span::unknown()))
@ -226,6 +237,7 @@ impl EvaluatedCall {
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # has_positional_args: false,
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # Some(Value::bool(false, Span::unknown()))
@ -242,6 +254,7 @@ impl EvaluatedCall {
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # has_positional_args: true,
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # Some(Value::int(1, Span::unknown()))
@ -289,6 +302,7 @@ impl EvaluatedCall {
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # has_positional_args: true,
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # Some(Value::int(123, null_span))
@ -310,6 +324,7 @@ impl EvaluatedCall {
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # has_positional_args: false,
/// # named: vec![],
/// # };
/// let opt_foo = match call.get_flag_value("foo") {
@ -344,6 +359,7 @@ impl EvaluatedCall {
/// # Value::string("b".to_owned(), null_span),
/// # Value::string("c".to_owned(), null_span),
/// # ],
/// # has_positional_args: true,
/// # named: vec![],
/// # };
/// let arg = match call.nth(1) {
@ -370,6 +386,7 @@ impl EvaluatedCall {
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # has_positional_args: false,
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # Some(Value::int(123, null_span))
@ -387,6 +404,7 @@ impl EvaluatedCall {
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # has_positional_args: false,
/// # named: vec![(
/// # Spanned { item: "bar".to_owned(), span: null_span},
/// # Some(Value::int(123, null_span))
@ -404,6 +422,7 @@ impl EvaluatedCall {
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # has_positional_args: false,
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # Some(Value::string("abc".to_owned(), null_span))
@ -436,6 +455,7 @@ impl EvaluatedCall {
/// # Value::string("two".to_owned(), null_span),
/// # Value::string("three".to_owned(), null_span),
/// # ],
/// # has_positional_args: true,
/// # named: Vec::new(),
/// # };
/// let args = call.rest::<String>(0);
@ -497,6 +517,7 @@ mod test {
Value::float(1.0, Span::new(0, 10)),
Value::string("something", Span::new(0, 10)),
],
has_positional_args: true,
named: vec![
(
Spanned {

View File

@ -372,6 +372,7 @@ fn manager_consume_call_run_forwards_to_receiver_with_context() -> Result<(), Sh
call: EvaluatedCall {
head: Span::test_data(),
positional: vec![],
has_positional_args: false,
named: vec![],
},
input: PipelineDataHeader::Empty,
@ -406,6 +407,7 @@ fn manager_consume_call_run_forwards_to_receiver_with_pipeline_data() -> Result<
call: EvaluatedCall {
head: Span::test_data(),
positional: vec![],
has_positional_args: false,
named: vec![],
},
input: PipelineDataHeader::list_stream(ListStreamInfo::new(6, Span::test_data())),
@ -450,6 +452,7 @@ fn manager_consume_call_run_deserializes_custom_values_in_args() -> Result<(), S
call: EvaluatedCall {
head: Span::test_data(),
positional: vec![value.clone()],
has_positional_args: true,
named: vec![(
Spanned {
item: "flag".into(),

View File

@ -139,6 +139,7 @@ mod tests {
let call = EvaluatedCall {
head: Span::test_data(),
positional: vec![],
has_positional_args: false,
named: vec![],
};
@ -168,6 +169,7 @@ mod tests {
let call = EvaluatedCall {
head: Span::test_data(),
positional: vec![],
has_positional_args: false,
named: vec![],
};

View File

@ -0,0 +1,27 @@
[package]
authors = ["The Nushell Project Developers"]
description = "Nushell commands with SELinux functionality."
edition = "2021"
license = "MIT"
name = "nu_plugin_selinux"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu_plugin_selinux"
version = "0.100.1"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "nu_plugin_selinux"
bench = false
[lib]
bench = false
[dependencies]
nu-protocol = { path = "../nu-protocol", version = "0.100.1" }
nu-plugin = { path = "../nu-plugin", version = "0.100.1" }
nu-command = { path = "../nu-command", version = "0.100.1" }
selinux = "0.4.6"
[dev-dependencies]
nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.100.1" }

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,152 @@
use std::path::Path;
use nu_command::{Ls, LsEntryMapper};
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand};
use nu_protocol::{engine::Command, Example, LabeledError, PipelineData, ShellError, Span, Value};
use crate::SELinuxPlugin;
#[derive(Clone)]
pub struct SELinuxLs {
pub ls: Ls,
}
impl PluginCommand for SELinuxLs {
type Plugin = SELinuxPlugin;
fn name(&self) -> &str {
"selinux ls"
}
fn description(&self) -> &str {
self.ls.description()
}
fn search_terms(&self) -> Vec<&str> {
self.ls.search_terms()
}
fn signature(&self) -> nu_protocol::Signature {
let mut signature = self.ls.signature().switch(
"context",
"Get the SELinux security context for each entry, if available",
Some('Z'),
);
signature.name = "selinux ls".into();
signature
}
fn run(
&self,
_plugin: &SELinuxPlugin,
engine: &EngineInterface,
call: &EvaluatedCall,
_input: PipelineData,
) -> Result<PipelineData, LabeledError> {
let security_context = call.has_flag("context")?;
let get_signals = &|| engine.signals().clone();
let has_flag = &|flag: &str| call.has_flag(flag);
let has_pattern_arg = call.has_positional_args;
let pattern_arg = call.rest(0)?;
let cwd = engine.get_current_dir()?.into();
let call_head = call.head;
let map_entry: &LsEntryMapper = &move |path, record| {
match record {
Value::Record { val, internal_span } if security_context => {
let mut val = val.into_owned();
val.push(
"security_context",
security_context_value(path, call_head)
.unwrap_or(Value::nothing(call_head)), // TODO: consider report_shell_warning
);
Value::record(val, internal_span)
}
_ => record,
}
};
let data = Ls::run_ls(
call_head,
get_signals,
has_flag,
has_pattern_arg,
pattern_arg,
cwd,
map_entry,
)?;
Ok(data)
}
fn examples(&self) -> Vec<Example> {
self.ls.examples()
}
}
fn security_context_value(_path: &Path, span: Span) -> Result<Value, ShellError> {
#[cfg(not(target_os = "linux"))]
return Ok(Value::nothing(span));
#[cfg(target_os = "linux")]
{
use selinux;
match selinux::SecurityContext::of_path(_path, false, false)
.map_err(|e| ShellError::IOError { msg: e.to_string() })?
{
Some(con) => {
let bytes = con.as_bytes();
Ok(Value::string(
String::from_utf8_lossy(&bytes[0..bytes.len().saturating_sub(1)]),
span,
))
}
None => Ok(Value::nothing(span)),
}
}
}
#[cfg(test)]
#[cfg(target_os = "linux")]
mod test {
use crate::SELinuxPlugin;
use nu_command::{All, Each, External, First, Join, Lines, SplitColumn, StrTrim};
use nu_plugin_test_support::PluginTest;
use nu_protocol::{engine::Command, ShellError, Value};
use std::{env, sync::Arc};
#[test]
fn returns_correct_security_context() -> Result<(), ShellError> {
let plugin: Arc<SELinuxPlugin> = Arc::new(SELinuxPlugin {});
let mut plugin_test = PluginTest::new("selinux", plugin)?;
let engine_state = plugin_test.engine_state_mut();
engine_state.add_env_var("PWD".to_owned(), Value::test_string("/"));
engine_state.add_env_var("PATH".into(), Value::test_string(env::var("PATH").unwrap()));
let deps: Vec<Box<dyn Command>> = vec![
Box::new(External),
Box::new(Lines),
Box::new(Each),
Box::new(StrTrim),
Box::new(SplitColumn),
Box::new(First),
Box::new(Join),
Box::new(All),
];
for decl in deps {
plugin_test.add_decl(decl)?;
}
let input = "
^ls -Z / | lines | each { |e| $e | str trim | split column ' ' 'coreutils_scontext' 'name' | first } \
| join (selinux ls -sZ /) name \
| all { |e|
let valid = $e.coreutils_scontext == '?' or $e.security_context == $e.coreutils_scontext
if not $valid {
error make { msg: $'For entry ($e.name) expected ($e.coreutils_scontext), got ($e.security_context)' }
}
}";
plugin_test.eval(input)?;
Ok(())
}
}

View File

@ -0,0 +1,3 @@
mod ls;
pub use ls::SELinuxLs;

View File

@ -0,0 +1,15 @@
mod commands;
pub use commands::*;
use nu_command::Ls;
use nu_plugin::{Plugin, PluginCommand};
pub struct SELinuxPlugin;
impl Plugin for SELinuxPlugin {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![Box::new(SELinuxLs { ls: Ls })]
}
}

View File

@ -0,0 +1,6 @@
use nu_plugin::{serve_plugin, MsgPackSerializer};
use nu_plugin_selinux::SELinuxPlugin;
fn main() {
serve_plugin(&SELinuxPlugin {}, MsgPackSerializer {})
}

View File

@ -33,7 +33,8 @@ let plugins = [
nu_plugin_example,
nu_plugin_custom_values,
nu_plugin_formats,
nu_plugin_polars
nu_plugin_polars,
nu_plugin_selinux
]
for plugin in $plugins {

View File

@ -356,6 +356,7 @@ export def build [
nu_plugin_example,
nu_plugin_custom_values,
nu_plugin_formats,
nu_plugin_selinux
]
for plugin in $plugins {
@ -428,6 +429,7 @@ export def install [
nu_plugin_example,
nu_plugin_custom_values,
nu_plugin_formats,
nu_plugin_selinux
]
for plugin in $plugins {