1use f00_core::Entry;
4
5pub fn format_csv(entries: &[Entry]) -> String {
7 format_delimited(entries, ',')
8}
9
10pub fn format_tsv(entries: &[Entry]) -> String {
12 format_delimited(entries, '\t')
13}
14
15fn format_delimited(entries: &[Entry], sep: char) -> String {
16 let mut out = String::new();
17 push_row(
19 &mut out,
20 sep,
21 &[
22 "name",
23 "path",
24 "kind",
25 "size",
26 "modified",
27 "mode",
28 "inode",
29 "uid",
30 "gid",
31 "nlink",
32 "symlink_target",
33 "context",
34 ],
35 );
36 for e in entries.iter().filter(|e| !e.is_dir_header) {
37 let modified = e
38 .modified_datetime()
39 .map(|d| d.to_rfc3339())
40 .unwrap_or_default();
41 let target = e
42 .symlink_target
43 .as_ref()
44 .map(|p| p.display().to_string())
45 .unwrap_or_default();
46 let fields = [
47 e.name.clone(),
48 e.path.display().to_string(),
49 e.kind.as_str().to_string(),
50 e.size.to_string(),
51 modified,
52 format!("{:o}", e.mode),
53 e.inode.to_string(),
54 e.uid.to_string(),
55 e.gid.to_string(),
56 e.nlink.to_string(),
57 target,
58 e.context.clone(),
59 ];
60 let refs: Vec<&str> = fields.iter().map(|s| s.as_str()).collect();
61 push_row(&mut out, sep, &refs);
62 }
63 out
64}
65
66fn push_row(out: &mut String, sep: char, fields: &[&str]) {
67 for (i, f) in fields.iter().enumerate() {
68 if i > 0 {
69 out.push(sep);
70 }
71 if sep == ',' {
72 push_csv_field(out, f);
73 } else {
74 for c in f.chars() {
76 match c {
77 '\t' => out.push_str("\\t"),
78 '\n' => out.push_str("\\n"),
79 '\r' => out.push_str("\\r"),
80 _ => out.push(c),
81 }
82 }
83 }
84 }
85 out.push('\n');
86}
87
88fn push_csv_field(out: &mut String, field: &str) {
89 if field.contains([',', '"', '\n', '\r']) {
90 out.push('"');
91 for c in field.chars() {
92 if c == '"' {
93 out.push_str("\"\"");
94 } else {
95 out.push(c);
96 }
97 }
98 out.push('"');
99 } else {
100 out.push_str(field);
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use f00_core::{Entry, EntryKind, GitStatus};
108 use std::path::PathBuf;
109
110 fn ent(name: &str) -> Entry {
111 Entry {
112 path: PathBuf::from(name),
113 name: name.into(),
114 kind: EntryKind::File,
115 size: 3,
116 modified: None,
117 created: None,
118 accessed: None,
119 changed: None,
120 mode: 0o644,
121 readonly: false,
122 symlink_target: None,
123 depth: 0,
124 git_status: GitStatus::Clean,
125 is_dir_header: false,
126 nlink: 1,
127 uid: 0,
128 gid: 0,
129 inode: 1,
130 blocks: 0,
131 owner: "u".into(),
132 group: "g".into(),
133 author: "u".into(),
134 context: String::new(),
135 }
136 }
137
138 #[test]
139 fn csv_has_header_and_row() {
140 let s = format_csv(&[ent("a.txt")]);
141 assert!(s.starts_with("name,path,"));
142 assert!(s.contains("a.txt"));
143 }
144
145 #[test]
146 fn tsv_uses_tabs() {
147 let s = format_tsv(&[ent("b")]);
148 assert!(s.contains('\t'));
149 assert!(s.contains("b"));
150 }
151}