read_dir/
lib.rs

1use std::io::Read;
2
3use std::os::unix::fs::MetadataExt;
4use std::os::unix::fs::PermissionsExt;
5
6pub mod args;
7
8mod color;
9use color::Colorize;
10
11// File permissions in octal notation
12const OWNER_READ: u16 = 0o0400;
13const OWNER_WRITE: u16 = 0o0200;
14const OWNER_EXECUTE: u16 = 0o0100;
15
16const GROUP_READ: u16 = 0o0040;
17const GROUP_WRITE: u16 = 0o0020;
18const GROUP_EXECUTE: u16 = 0o0010;
19
20const OTHER_READ: u16 = 0o0004;
21const OTHER_WRITE: u16 = 0o0002;
22const OTHER_EXECUTE: u16 = 0o0001;
23
24pub enum Format {
25    // Files will be .join()ed with a ' '
26    Horizontal,
27
28    // Files will be capped vertically at `.0`
29    // NOTE: I have no clue how to do this
30    // HorizontalMatrix(u32),
31
32    // Files will be .join()ed with a '\n'
33    Vertical,
34
35    // Files will be capped horizontally at `.0` as well as vertically aligned
36    // NOTE: Horizontal capping will be done by inserting a '\n' at each `.0`th index
37    VerticalMatrix(usize),
38
39    // Files will have additional metadata tacked on and be .join()ed with a '\n'
40    // Metadata: permissions owner group size
41    // NOTE: size will be in bytes
42    Long,
43}
44
45pub struct ReadDirOptions {
46    // Show files beginning with a '.'
47    pub dotfiles: bool,
48
49    // Show '.' and '..' (overriden by `dotfiles`)
50    pub implied: bool,
51
52    // Should the files be sorted
53    pub sort: bool,
54
55    // Reverse the finalized vector
56    pub reverse: bool,
57
58    // Directories will be blue & bold
59    pub colored: bool,
60
61    // See `Format` enum (no formating will be some if `None`)
62    pub format: Option<Format>,
63
64    // Directory to read from (relative)
65    pub directory: String,
66}
67
68impl ReadDirOptions {
69    pub fn new() -> Self {
70        Self {
71            dotfiles: false,
72            implied: false,
73            sort: true,
74            reverse: false,
75            colored: true,
76            format: Some(Format::VerticalMatrix(5)),
77            directory: String::from("./"),
78        }
79    }
80}
81
82pub fn read_dir_to_string(options: ReadDirOptions) -> String {
83    let mut paths: Vec<String> = match std::fs::read_dir(options.directory.as_str()) {
84        Ok(entry) => entry,
85        Err(err) => match err.kind() {
86            std::io::ErrorKind::PermissionDenied => {
87                eprintln!("Error: you don't have permission to read {}", &options.directory);
88                std::process::exit(1);
89            }
90            _ => {
91                eprintln!("Internal Error: failed to directory {}", &options.directory);
92                std::process::exit(1);
93            }
94        },
95    }
96    .map(|entry| match entry {
97        Ok(entry) => entry
98            .file_name()
99            .into_string()
100            .unwrap_or(String::from("invalid unicode").blue_fg().red_bg().bold()),
101        Err(_) => {
102            eprintln!("Internal Error: failed to read entry");
103            std::process::exit(1);
104        }
105    })
106    .collect();
107
108    if !options.dotfiles {
109        paths = paths
110            .into_iter()
111            .filter(|path| path.as_bytes()[0] != b'.')
112            .collect();
113    }
114
115    if options.sort {
116        paths.sort();
117    }
118
119    if options.reverse {
120        paths.reverse();
121    }
122
123    let paths_bland = paths.clone(); // This must be cloned otherwise trying to build a vertical
124                                     // matrix would be a huge hassle
125    if options.colored {
126        for path in &mut paths {
127            if std::path::Path::new(&(format!("{}/{path}", &options.directory))).is_dir() {
128                *path = path.bold().blue_fg();
129            }
130        }
131    }
132
133    match options.format {
134        Some(format) => match format {
135            Format::Horizontal => paths.join(" "),
136
137            // Format::HorizontalMatrix => {}
138
139            Format::Long => std::iter::zip(paths, paths_bland)
140                .map(|(path, bland)| {
141                    let metadata = std::fs::metadata(&bland).unwrap_or_else(|_| {
142                        eprintln!("Internal Error: unable to read metadata for file {bland:?}");
143                        std::process::exit(1);
144                    });
145
146                    let is_dir = if metadata.is_dir() { 'd' } else { '-' };
147
148                    let mode = metadata.permissions().mode() as u16;
149
150                    // u = "Owner"
151                    let ur = if mode & OWNER_READ == 0 { '-' } else { 'r' };
152                    let uw = if mode & OWNER_WRITE == 0 { '-' } else { 'w' };
153                    let ux = if mode & OWNER_EXECUTE == 0 { '-' } else { 'x' };
154
155                    // g = "Group"
156                    let gr = if mode & GROUP_READ == 0 { '-' } else { 'r' };
157                    let gw = if mode & GROUP_WRITE == 0 { '-' } else { 'w' };
158                    let gx = if mode & GROUP_EXECUTE == 0 { '-' } else { 'x' };
159
160                    // o = "Other"
161                    let or = if mode & OTHER_READ == 0 { '-' } else { 'r' };
162                    let ow = if mode & OTHER_WRITE == 0 { '-' } else { 'w' };
163                    let ox = if mode & OTHER_EXECUTE == 0 { '-' } else { 'x' };
164
165                    let owner = match uid_to_user(metadata.uid()) {
166                        Ok(user) => user,
167                        Err(err) => match err.kind() {
168                            std::io::ErrorKind::NotFound => {
169                                eprintln!("Internal Error: could not open /etc/passwd; file not found");
170                                eprintln!("opened due to -l needing user id information");
171                                std::process::exit(1);
172                            }
173
174                            std::io::ErrorKind::PermissionDenied => {
175                                eprintln!("Internal Error: could not open /etc/passwd; permission denied;");
176                                eprintln!("opened due to -l needing user id information");
177                                std::process::exit(1);
178                            }
179
180                            std::io::ErrorKind::InvalidData => {
181                                eprintln!("Internal Error: /etc/passwd did not contain UTF-8 valid data;");
182                                eprintln!("opened due to -l needing user id information");
183                                std::process::exit(1);
184                            }
185
186                            _ => {
187                                eprintln!("Internal Error: unknown error opening /etc/passwd;");
188                                eprintln!("opened due to -l needing user id information");
189                                std::process::exit(1);
190                            }
191                        }
192                    };
193
194                    let group = match gid_to_group(metadata.uid()) {
195                        Ok(group) => group,
196                        Err(err) => match err.kind() {
197                            std::io::ErrorKind::NotFound => {
198                                eprintln!("Internal Error: could not open /etc/group; file not found");
199                                eprintln!("opened due to -l needing user id information");
200                                std::process::exit(1);
201                            }
202
203                            std::io::ErrorKind::PermissionDenied => {
204                                eprintln!("Internal Error: could not open /etc/group; permission denied;");
205                                eprintln!("opened due to -l needing user id information");
206                                std::process::exit(1);
207                            }
208
209                            std::io::ErrorKind::InvalidData => {
210                                eprintln!("Internal Error: /etc/group did not contain UTF-8 valid data;");
211                                eprintln!("opened due to -l needing user id information");
212                                std::process::exit(1);
213                            }
214
215                            _ => {
216                                eprintln!("Internal Error: unknown error opening /etc/group;");
217                                eprintln!("opened due to -l needing user id information");
218                                std::process::exit(1);
219                            }
220                        }
221                    };
222
223                    let size = metadata.size();
224
225                    format!("{is_dir}{ur}{uw}{ux}{gr}{gw}{gx}{or}{ow}{ox} {owner} {group} {size} {path}")
226                })
227                .collect::<Vec<String>>()
228                .join("\n"),
229
230            Format::Vertical => paths.join("\n"),
231
232            Format::VerticalMatrix(length) => {
233                paths
234                    .into_iter()
235                    .enumerate()
236                    .map(|(i, mut path)| {
237                        if i % length != 0 {
238                            // Add new lines
239                            if (i + 1) % length == 0 {
240                                path.push('\n');
241                            }
242
243                            // Align to matrix
244                            let longest = paths_bland
245                                .iter()
246                                .enumerate()
247                                .filter(|(j, _)| (i - 1) % length == j % length)
248                                .map(|(_, path)| path.len())
249                                .max()
250                                .expect("Iterator should never be empty");
251
252                            return String::from(
253                                " ".repeat(longest - paths_bland[i - 1].len() + 1),
254                            ) + &path;
255                        }
256
257                        path
258                    })
259                    .collect::<Vec<String>>()
260                    .join("")
261            }
262        }
263
264        None => paths.join(""),
265    }
266}
267
268fn uid_to_user(id: u32) -> Result<String, std::io::Error> {
269    let mut etc_passwd_file = std::fs::File::open("/etc/passwd")?;
270    let mut etc_passwd = String::new();
271
272    etc_passwd_file.read_to_string(&mut etc_passwd)?;
273
274    let mut users: Vec<(String, u32)> = Vec::new();
275    let mut user = (String::new(), 0);
276
277    for entry in etc_passwd.split("\n") {
278        for str in entry
279            .split_inclusive(":")
280            .enumerate()
281            .filter(|(i, _)| i % 7 == 2 || i % 7 == 0)
282            .map(|(_, str)| str.to_string())
283            .collect::<String>()
284            .split(":")
285        {
286            if str.chars().all(|char| char.is_numeric()) && !str.is_empty() {
287                user.1 = str.parse().expect("This should be a number");
288                users.push(user.clone());
289            } else {
290                user.0 = str.to_string();
291            }
292        }
293    }
294
295    for user in users {
296        if user.1 == id {
297            return Ok(user.0);
298        }
299    }
300
301    Ok(String::new())
302}
303
304fn gid_to_group(id: u32) -> Result<String, std::io::Error> {
305    let mut etc_group_file = std::fs::File::open("/etc/passwd")?;
306    let mut etc_group = String::new();
307
308    etc_group_file.read_to_string(&mut etc_group)?;
309
310    let mut groups: Vec<(String, u32)> = Vec::new();
311    let mut group = (String::new(), 0);
312
313    for entry in etc_group.split("\n") {
314        for str in entry
315            .split_inclusive(":")
316            .enumerate()
317            .filter(|(i, _)| i % 7 == 2 || i % 7 == 0)
318            .map(|(_, str)| str.to_string())
319            .collect::<String>()
320            .split(":")
321        {
322            if str.chars().all(|char| char.is_numeric()) && !str.is_empty() {
323                group.1 = str.parse().expect("This should be a number");
324                groups.push(group.clone());
325            } else {
326                group.0 = str.to_string();
327            }
328        }
329    }
330
331    for group in groups {
332        if group.1 == id {
333            return Ok(group.0);
334        }
335    }
336
337    Ok(String::new())
338}