1use super::Formatter;
2use crate::scanner::{FileNode, TreeStats};
3use anyhow::Result;
4use serde_json::{json, Value};
5use std::collections::HashMap;
6use std::io::Write;
7use std::path::{Path, PathBuf};
8
9pub struct JsonFormatter {
10 pub compact: bool,
11}
12
13impl JsonFormatter {
14 pub fn new(compact: bool) -> Self {
15 Self { compact }
16 }
17
18 fn build_json_tree(&self, nodes: &[FileNode], root_path: &Path) -> Value {
19 let mut children_map: HashMap<PathBuf, Vec<&FileNode>> = HashMap::new();
21 let mut root_node = None;
22
23 for node in nodes {
24 if node.path == root_path {
25 root_node = Some(node);
26 } else if let Some(parent) = node.path.parent() {
27 children_map
28 .entry(parent.to_path_buf())
29 .or_default()
30 .push(node);
31 }
32 }
33
34 fn node_to_json(
35 node: &FileNode,
36 children_map: &HashMap<PathBuf, Vec<&FileNode>>,
37 _root_path: &Path,
38 ) -> Value {
39 let name = node
40 .path
41 .file_name()
42 .unwrap_or(node.path.as_os_str())
43 .to_string_lossy()
44 .to_string();
45
46 let mut obj = json!({
47 "name": name,
48 "type": match node.file_type {
49 crate::scanner::FileType::Directory => "directory",
50 crate::scanner::FileType::RegularFile => "file",
51 crate::scanner::FileType::Symlink => "symlink",
52 crate::scanner::FileType::Executable => "executable",
53 crate::scanner::FileType::Socket => "socket",
54 crate::scanner::FileType::Pipe => "pipe",
55 crate::scanner::FileType::BlockDevice => "block_device",
56 crate::scanner::FileType::CharDevice => "char_device",
57 },
58 });
59
60 if !node.is_dir {
62 obj["size"] = json!(node.size);
63 }
64
65 if node.permission_denied {
67 obj["permission_denied"] = json!(true);
68 }
69
70 if node.is_ignored {
71 obj["ignored"] = json!(true);
72 }
73
74 if node.is_hidden {
75 obj["hidden"] = json!(true);
76 }
77
78 if node.is_symlink {
79 obj["symlink"] = json!(true);
80 }
81
82 if let Some(children) = children_map.get(&node.path) {
84 let mut sorted_children = children.to_vec();
85 sorted_children.sort_by(|a, b| match (a.is_dir, b.is_dir) {
86 (true, false) => std::cmp::Ordering::Less,
87 (false, true) => std::cmp::Ordering::Greater,
88 _ => a.path.file_name().cmp(&b.path.file_name()),
89 });
90
91 obj["children"] = json!(sorted_children
92 .iter()
93 .map(|child| node_to_json(child, children_map, _root_path))
94 .collect::<Vec<_>>());
95 }
96
97 obj
98 }
99
100 if let Some(root) = root_node {
101 node_to_json(root, &children_map, root_path)
102 } else {
103 json!({})
104 }
105 }
106}
107
108impl Formatter for JsonFormatter {
109 fn format(
110 &self,
111 writer: &mut dyn Write,
112 nodes: &[FileNode],
113 _stats: &TreeStats,
114 root_path: &Path,
115 ) -> Result<()> {
116 let json_tree = self.build_json_tree(nodes, root_path);
117
118 if self.compact {
119 writeln!(writer, "{}", serde_json::to_string(&json_tree)?)?;
120 } else {
121 writeln!(writer, "{}", serde_json::to_string_pretty(&json_tree)?)?;
122 }
123
124 Ok(())
125 }
126}