use std::io::Write;
use anyhow::{Context, Result};
use clap::{Parser, ValueEnum};
use crate::cli::drive::format::{output_as, sanitize_for_terminal, OutputFormat};
use crate::cli::drive::helpers::active_account_rules;
use crate::cli::drive::permissions::check::OperationArg;
use crate::drive::write_gate::{DriveOperation, FolderPermissionRule, Verdict};
#[derive(Parser)]
pub struct ShowCommand {
#[arg(short = 'o', long, value_enum, default_value_t = OutputFormat::Table)]
pub output: OutputFormat,
}
impl ShowCommand {
pub fn execute(self) -> Result<()> {
let rules = active_account_rules()?;
run_show(&rules, &self.output)
}
}
fn run_show(rules: &[FolderPermissionRule], output: &OutputFormat) -> Result<()> {
if output_as(&rules.to_vec(), output)? {
return Ok(());
}
let stdout = std::io::stdout();
let mut handle = stdout.lock();
render_rules_table(rules, &mut handle)
}
fn render_rules_table(rules: &[FolderPermissionRule], out: &mut dyn Write) -> Result<()> {
if rules.is_empty() {
writeln!(
out,
"No write-permission rules configured for this account — every \
{} is refused everywhere. Add rules under \
drive.accounts.<name>.write_permissions.rules in \
~/.omni-dev/settings.json, keyed on either a folder_id or a file_id.",
default_deny_operations_list()
)
.context("Failed to write empty-table message")?;
return Ok(());
}
let scopes: Vec<&str> = rules.iter().map(rule_scope).collect();
let scope_width = "SCOPE"
.len()
.max(scopes.iter().map(|s| s.len()).max().unwrap_or(0));
let ids: Vec<String> = rules
.iter()
.map(|r| sanitize_for_terminal(rule_target_id(r)))
.collect();
let id_width = "TARGET_ID"
.len()
.max(ids.iter().map(String::len).max().unwrap_or(0));
let recursives: Vec<String> = rules.iter().map(format_recursive).collect();
let leases: Vec<String> = rules.iter().map(|r| r.require_lease.to_string()).collect();
let allow_strings: Vec<String> = rules.iter().map(|r| format_op_set(&r.allow)).collect();
let allow_width = "ALLOW"
.len()
.max(allow_strings.iter().map(String::len).max().unwrap_or(0));
let deny_strings: Vec<String> = rules.iter().map(|r| format_op_set(&r.deny)).collect();
let deny_width = "DENY"
.len()
.max(deny_strings.iter().map(String::len).max().unwrap_or(0));
writeln!(
out,
"{:<scope_width$} {:<id_width$} RECURSIVE LEASE {:<allow_width$} {:<deny_width$}",
"SCOPE", "TARGET_ID", "ALLOW", "DENY"
)
.context("Failed to write header row")?;
for i in 0..rules.len() {
writeln!(
out,
"{:<scope_width$} {:<id_width$} {:<9} {:<5} {:<allow_width$} {:<deny_width$}",
scopes[i], ids[i], recursives[i], leases[i], allow_strings[i], deny_strings[i],
)
.context("Failed to write rule row")?;
}
Ok(())
}
fn default_deny_operations_list() -> String {
let list = OperationArg::value_variants()
.iter()
.map(|&arg| DriveOperation::from(arg))
.filter(|op| op.default_policy() == Verdict::Deny)
.map(|op| op.to_string())
.collect::<Vec<_>>();
debug_assert!(
!list.is_empty(),
"at least one DriveOperation must default-deny, or this message's wording breaks"
);
list.join("/")
}
fn rule_scope(rule: &FolderPermissionRule) -> &'static str {
if rule.file_id.is_some() {
"file"
} else {
"folder"
}
}
fn rule_target_id(rule: &FolderPermissionRule) -> &str {
rule.file_id
.as_deref()
.or(rule.folder_id.as_deref())
.unwrap_or("-")
}
fn format_recursive(rule: &FolderPermissionRule) -> String {
if rule.file_id.is_some() {
"-".to_string()
} else {
rule.recursive.to_string()
}
}
fn format_op_set(
ops: &std::collections::HashSet<crate::drive::write_gate::DriveOperation>,
) -> String {
if ops.is_empty() {
return "-".to_string();
}
let mut rendered: Vec<String> = ops.iter().map(ToString::to_string).collect();
rendered.sort();
rendered.join(",")
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::drive::write_gate::DriveOperation;
fn sample_rule() -> FolderPermissionRule {
FolderPermissionRule {
folder_id: Some("folder-1".to_string()),
file_id: None,
recursive: true,
allow: [DriveOperation::Create, DriveOperation::Upload]
.into_iter()
.collect(),
deny: std::iter::once(DriveOperation::Edit).collect(),
require_lease: true,
}
}
#[test]
fn render_table_empty_explains_default_deny() {
let mut buf = Vec::new();
render_rules_table(&[], &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("No write-permission rules configured"));
assert!(out.contains("write_permissions"));
}
#[test]
fn render_table_writes_header_and_rows() {
let rules = [sample_rule()];
let mut buf = Vec::new();
render_rules_table(&rules, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("SCOPE"));
assert!(out.contains("TARGET_ID"));
assert!(out.contains("RECURSIVE"));
assert!(out.contains("LEASE"));
assert!(out.contains("ALLOW"));
assert!(out.contains("DENY"));
assert!(out.contains("folder-1"));
assert!(out.contains("true"));
assert!(out.contains("create,upload"));
assert!(out.contains("edit"));
}
#[test]
fn render_table_shows_require_lease_false_for_an_opted_out_rule() {
let rules = [FolderPermissionRule::folder("folder-1")
.allowing([DriveOperation::Edit])
.requiring_lease(false)];
let mut buf = Vec::new();
render_rules_table(&rules, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("LEASE"), "{out}");
assert!(out.contains("false"), "{out}");
}
#[test]
fn render_table_uses_dash_for_empty_op_sets() {
let rules = [FolderPermissionRule {
folder_id: Some("folder-1".to_string()),
file_id: None,
recursive: false,
allow: std::collections::HashSet::default(),
deny: std::collections::HashSet::default(),
require_lease: true,
}];
let mut buf = Vec::new();
render_rules_table(&rules, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains('-'));
}
#[test]
fn render_table_strips_control_bytes_from_folder_id() {
let rules = [FolderPermissionRule {
folder_id: Some("fo\rlder\x1b[31m".to_string()),
file_id: None,
recursive: false,
allow: std::collections::HashSet::default(),
deny: std::collections::HashSet::default(),
require_lease: true,
}];
let mut buf = Vec::new();
render_rules_table(&rules, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(
!out.contains(|c: char| c.is_control() && c != '\n'),
"{out:?}"
);
}
#[test]
fn format_op_set_sorts_alphabetically_and_dedupes_via_hashset() {
let ops: std::collections::HashSet<_> = [DriveOperation::Upload, DriveOperation::Create]
.into_iter()
.collect();
assert_eq!(format_op_set(&ops), "create,upload");
}
#[test]
fn run_show_table_path_writes_to_stdout() {
run_show(&[sample_rule()], &OutputFormat::Table).unwrap();
}
#[test]
fn run_show_json_path_returns_ok() {
run_show(&[sample_rule()], &OutputFormat::Json).unwrap();
}
fn sample_file_rule() -> FolderPermissionRule {
FolderPermissionRule::file("sheet-1").allowing([DriveOperation::SheetsWrite])
}
#[test]
fn render_table_labels_each_rule_with_its_scope() {
let rules = [sample_rule(), sample_file_rule()];
let mut buf = Vec::new();
render_rules_table(&rules, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("SCOPE"), "{out}");
assert!(out.contains("TARGET_ID"), "{out}");
assert!(out.contains("folder"), "{out}");
assert!(out.contains("file"), "{out}");
assert!(out.contains("sheet-1"), "{out}");
}
#[test]
fn render_table_shows_a_dash_not_false_for_a_file_rules_recursive() {
let rules = [sample_file_rule()];
let mut buf = Vec::new();
render_rules_table(&rules, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(!out.contains("false"), "{out}");
}
#[test]
fn render_table_strips_control_bytes_from_a_file_id_too() {
let rules = [FolderPermissionRule::file("sh\reet\x1b[31m")];
let mut buf = Vec::new();
render_rules_table(&rules, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(
!out.contains(|c: char| c.is_control() && c != '\n'),
"{out:?}"
);
}
#[test]
fn the_empty_table_message_names_every_gated_operation() {
let mut buf = Vec::new();
render_rules_table(&[], &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
let list = default_deny_operations_list();
assert!(
out.contains(&list),
"{out} should embed the operations list {list} verbatim"
);
let named: std::collections::HashSet<&str> = list.split('/').collect();
assert!(
!named.contains("read"),
"{out} should not name read, which is allowed by default"
);
for op in [
"create",
"upload",
"edit",
"sheets-write",
"sheets-structure",
"sheets-delete",
"sheets-protection",
"docs-write",
"trash",
"slides-write",
] {
assert!(named.contains(op), "{out} should name {op}");
}
}
#[test]
fn default_deny_operations_list_matches_default_policy() {
let list = default_deny_operations_list();
let named: std::collections::HashSet<&str> = list.split('/').collect();
for &arg in OperationArg::value_variants() {
let op = DriveOperation::from(arg);
assert_eq!(
named.contains(op.to_string().as_str()),
op.default_policy() == Verdict::Deny,
"{op:?}: list={list}"
);
}
}
#[test]
fn the_empty_table_message_names_both_rule_keys() {
let mut buf = Vec::new();
render_rules_table(&[], &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("folder_id"), "{out}");
assert!(out.contains("file_id"), "{out}");
}
}