cobolx 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,278 @@
1
+ use crate::cobol::model::ParsedDataItem;
2
+ use std::collections::HashMap;
3
+
4
+ #[derive(Debug)]
5
+ struct Frame {
6
+ idx: usize,
7
+ level: u16,
8
+ base_offset: i64,
9
+ cursor: i64,
10
+ }
11
+
12
+ #[derive(Debug, Clone, Copy)]
13
+ struct Storage {
14
+ bytes: Option<i64>,
15
+ kind: &'static str,
16
+ status: &'static str,
17
+ }
18
+
19
+ pub(crate) fn compute_physical_layout(items: &mut [ParsedDataItem]) {
20
+ let mut stack = vec![Frame {
21
+ idx: usize::MAX,
22
+ level: 0,
23
+ base_offset: 0,
24
+ cursor: 0,
25
+ }];
26
+ let mut known_offsets = HashMap::<String, i64>::with_capacity(items.len());
27
+
28
+ for idx in 0..items.len() {
29
+ let level = items[idx].level;
30
+ while stack.last().is_some_and(|frame| frame.level >= level) {
31
+ close_group(items, &mut stack, &mut known_offsets);
32
+ }
33
+
34
+ if matches!(level, 66 | 88) {
35
+ items[idx].byte_offset = current_cursor(&stack);
36
+ items[idx].byte_size = Some(0);
37
+ items[idx].storage_kind = Some(
38
+ if level == 66 {
39
+ "rename"
40
+ } else {
41
+ "condition-name"
42
+ }
43
+ .to_string(),
44
+ );
45
+ items[idx].layout_status = Some("no-storage".to_string());
46
+ known_offsets.insert(items[idx].name.clone(), items[idx].byte_offset.unwrap_or(0));
47
+ continue;
48
+ }
49
+
50
+ let offset = items[idx]
51
+ .redefines
52
+ .as_ref()
53
+ .and_then(|name| known_offsets.get(name).copied())
54
+ .unwrap_or_else(|| current_cursor(&stack).unwrap_or(0));
55
+
56
+ items[idx].byte_offset = Some(offset);
57
+
58
+ let storage = classify_storage(&items[idx]);
59
+ if storage.kind == "group" {
60
+ items[idx].storage_kind = Some("group".to_string());
61
+ items[idx].layout_status = Some("pending".to_string());
62
+ stack.push(Frame {
63
+ idx,
64
+ level,
65
+ base_offset: offset,
66
+ cursor: offset,
67
+ });
68
+ known_offsets.insert(items[idx].name.clone(), offset);
69
+ continue;
70
+ }
71
+
72
+ let occurs = item_occurs(&items[idx]);
73
+ let total_bytes = storage.bytes.map(|bytes| bytes.saturating_mul(occurs));
74
+ items[idx].byte_size = total_bytes;
75
+ items[idx].storage_kind = Some(storage.kind.to_string());
76
+ items[idx].layout_status = Some(storage.status.to_string());
77
+
78
+ if let Some(total) = total_bytes {
79
+ advance_parent(&mut stack, offset.saturating_add(total));
80
+ }
81
+ known_offsets.insert(items[idx].name.clone(), offset);
82
+ }
83
+
84
+ while stack.len() > 1 {
85
+ close_group(items, &mut stack, &mut known_offsets);
86
+ }
87
+ }
88
+
89
+ fn close_group(
90
+ items: &mut [ParsedDataItem],
91
+ stack: &mut Vec<Frame>,
92
+ known_offsets: &mut HashMap<String, i64>,
93
+ ) {
94
+ let Some(frame) = stack.pop() else {
95
+ return;
96
+ };
97
+
98
+ let unit_bytes = frame.cursor.saturating_sub(frame.base_offset);
99
+ let total_bytes = unit_bytes.saturating_mul(item_occurs(&items[frame.idx]));
100
+ items[frame.idx].byte_size = Some(total_bytes);
101
+ items[frame.idx].layout_status = Some("derived".to_string());
102
+ advance_parent(stack, frame.base_offset.saturating_add(total_bytes));
103
+ known_offsets.insert(items[frame.idx].name.clone(), frame.base_offset);
104
+ }
105
+
106
+ fn current_cursor(stack: &[Frame]) -> Option<i64> {
107
+ stack.last().map(|frame| frame.cursor)
108
+ }
109
+
110
+ fn advance_parent(stack: &mut [Frame], end_offset: i64) {
111
+ if let Some(parent) = stack.last_mut() {
112
+ parent.cursor = parent.cursor.max(end_offset);
113
+ }
114
+ }
115
+
116
+ fn item_occurs(item: &ParsedDataItem) -> i64 {
117
+ item.occurs.unwrap_or(1).max(1)
118
+ }
119
+
120
+ fn classify_storage(item: &ParsedDataItem) -> Storage {
121
+ let usage = item
122
+ .usage_clause
123
+ .as_deref()
124
+ .map(normalize_usage)
125
+ .unwrap_or_else(|| "DISPLAY".to_string());
126
+
127
+ if item.pic.is_none() {
128
+ return match usage.as_str() {
129
+ "COMP-1" | "COMPUTATIONAL-1" => Storage {
130
+ bytes: Some(4),
131
+ kind: "float",
132
+ status: "exact",
133
+ },
134
+ "COMP-2" | "COMPUTATIONAL-2" => Storage {
135
+ bytes: Some(8),
136
+ kind: "float",
137
+ status: "exact",
138
+ },
139
+ "POINTER" => Storage {
140
+ bytes: Some(8),
141
+ kind: "pointer",
142
+ status: "estimated",
143
+ },
144
+ "INDEX" => Storage {
145
+ bytes: Some(4),
146
+ kind: "index",
147
+ status: "estimated",
148
+ },
149
+ _ => Storage {
150
+ bytes: None,
151
+ kind: "group",
152
+ status: "pending",
153
+ },
154
+ };
155
+ }
156
+
157
+ let pic = item.pic.as_deref().unwrap_or_default();
158
+ match usage.as_str() {
159
+ "COMP-3" | "COMPUTATIONAL-3" | "PACKED-DECIMAL" => {
160
+ let digits = picture_digits(pic);
161
+ Storage {
162
+ bytes: digits.map(|d| (d + 2) / 2),
163
+ kind: "packed-decimal",
164
+ status: "exact",
165
+ }
166
+ }
167
+ "BINARY" | "COMP" | "COMP-4" | "COMP-5" | "COMPUTATIONAL" | "COMPUTATIONAL-4"
168
+ | "COMPUTATIONAL-5" => {
169
+ let digits = picture_digits(pic);
170
+ Storage {
171
+ bytes: digits.and_then(binary_bytes_for_digits),
172
+ kind: "binary",
173
+ status: "estimated",
174
+ }
175
+ }
176
+ "COMP-1" | "COMPUTATIONAL-1" => Storage {
177
+ bytes: Some(4),
178
+ kind: "float",
179
+ status: "exact",
180
+ },
181
+ "COMP-2" | "COMPUTATIONAL-2" => Storage {
182
+ bytes: Some(8),
183
+ kind: "float",
184
+ status: "exact",
185
+ },
186
+ "NATIONAL" => Storage {
187
+ bytes: picture_positions(pic).map(|positions| positions.saturating_mul(2)),
188
+ kind: "national",
189
+ status: "estimated",
190
+ },
191
+ _ => {
192
+ let positions = picture_positions(pic);
193
+ let has_national = expanded_picture_chars(pic).any(|ch| ch == 'N');
194
+ Storage {
195
+ bytes: positions.map(|n| if has_national { n.saturating_mul(2) } else { n }),
196
+ kind: "display",
197
+ status: if has_national { "estimated" } else { "exact" },
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ fn normalize_usage(usage: &str) -> String {
204
+ usage
205
+ .split_whitespace()
206
+ .filter(|part| *part != "IS")
207
+ .collect::<Vec<_>>()
208
+ .join("-")
209
+ .to_ascii_uppercase()
210
+ }
211
+
212
+ fn binary_bytes_for_digits(digits: i64) -> Option<i64> {
213
+ match digits {
214
+ 1..=4 => Some(2),
215
+ 5..=9 => Some(4),
216
+ 10..=18 => Some(8),
217
+ _ => None,
218
+ }
219
+ }
220
+
221
+ fn picture_digits(pic: &str) -> Option<i64> {
222
+ let digits = expanded_picture_chars(pic)
223
+ .filter(|ch| matches!(*ch, '9' | 'Z' | '*'))
224
+ .count() as i64;
225
+ (digits > 0).then_some(digits)
226
+ }
227
+
228
+ fn picture_positions(pic: &str) -> Option<i64> {
229
+ let positions = expanded_picture_chars(pic)
230
+ .filter(|ch| !matches!(*ch, 'S' | 'V' | 'P'))
231
+ .count() as i64;
232
+ (positions > 0).then_some(positions)
233
+ }
234
+
235
+ fn expanded_picture_chars(pic: &str) -> impl Iterator<Item = char> + '_ {
236
+ PictureChars {
237
+ chars: pic.chars().peekable(),
238
+ repeat_char: None,
239
+ repeat_left: 0,
240
+ }
241
+ }
242
+
243
+ struct PictureChars<I: Iterator<Item = char>> {
244
+ chars: std::iter::Peekable<I>,
245
+ repeat_char: Option<char>,
246
+ repeat_left: usize,
247
+ }
248
+
249
+ impl<I: Iterator<Item = char>> Iterator for PictureChars<I> {
250
+ type Item = char;
251
+
252
+ fn next(&mut self) -> Option<Self::Item> {
253
+ if self.repeat_left > 0 {
254
+ self.repeat_left -= 1;
255
+ return self.repeat_char;
256
+ }
257
+
258
+ let ch = self.chars.next()?.to_ascii_uppercase();
259
+ if self.chars.peek() == Some(&'(') {
260
+ self.chars.next();
261
+ let mut n = String::new();
262
+ while let Some(next) = self.chars.peek().copied() {
263
+ self.chars.next();
264
+ if next == ')' {
265
+ break;
266
+ }
267
+ n.push(next);
268
+ }
269
+ let repeat = n.parse::<usize>().unwrap_or(1);
270
+ if repeat > 1 {
271
+ self.repeat_char = Some(ch);
272
+ self.repeat_left = repeat - 1;
273
+ }
274
+ }
275
+
276
+ Some(ch)
277
+ }
278
+ }
@@ -0,0 +1,135 @@
1
+ use crate::cobol::model::{LogicalLine, Token};
2
+
3
+ pub(crate) fn logical_lines(content: &str) -> Vec<LogicalLine> {
4
+ let mut lines = Vec::new();
5
+ let mut current = String::new();
6
+ let mut current_start = 0usize;
7
+ let mut current_len = 0usize;
8
+ let mut offset = 0usize;
9
+
10
+ for raw_line in content.lines() {
11
+ let raw_start = offset;
12
+ let raw_len = raw_line.len();
13
+ offset += raw_len + 1;
14
+
15
+ let Some(code) = code_line(raw_line) else {
16
+ continue;
17
+ };
18
+ let trimmed = code.trim();
19
+ if trimmed.is_empty() {
20
+ continue;
21
+ }
22
+
23
+ if current.is_empty() {
24
+ current_start = raw_start + code.find(trimmed).unwrap_or(0);
25
+ current_len = raw_len;
26
+ } else {
27
+ current.push(' ');
28
+ current_len = (raw_start + raw_len).saturating_sub(current_start);
29
+ }
30
+ current.push_str(trimmed);
31
+
32
+ if has_statement_terminator(trimmed) {
33
+ lines.push(LogicalLine {
34
+ text: std::mem::take(&mut current),
35
+ start_offset: current_start,
36
+ byte_len: current_len,
37
+ });
38
+ current_len = 0;
39
+ }
40
+ }
41
+
42
+ if !current.is_empty() {
43
+ lines.push(LogicalLine {
44
+ text: current,
45
+ start_offset: current_start,
46
+ byte_len: current_len,
47
+ });
48
+ }
49
+
50
+ lines
51
+ }
52
+
53
+ pub(crate) fn tokenize(line: &str) -> Vec<Token> {
54
+ let bytes = line.as_bytes();
55
+ let mut tokens = Vec::new();
56
+ let mut i = 0usize;
57
+
58
+ while i < bytes.len() {
59
+ let b = bytes[i];
60
+ if b == b'\'' || b == b'"' {
61
+ let quote = b;
62
+ let start = i;
63
+ i += 1;
64
+ let text_start = i;
65
+ while i < bytes.len() && bytes[i] != quote {
66
+ i += 1;
67
+ }
68
+ tokens.push(Token {
69
+ text: line[text_start..i].to_ascii_uppercase(),
70
+ start,
71
+ quoted: true,
72
+ });
73
+ if i < bytes.len() {
74
+ i += 1;
75
+ }
76
+ } else if is_name_byte(b) {
77
+ let start = i;
78
+ while i < bytes.len() && is_name_byte(bytes[i]) {
79
+ i += 1;
80
+ }
81
+ tokens.push(Token {
82
+ text: line[start..i].to_ascii_uppercase(),
83
+ start,
84
+ quoted: false,
85
+ });
86
+ } else {
87
+ i += 1;
88
+ }
89
+ }
90
+
91
+ tokens
92
+ }
93
+
94
+ pub(crate) fn clean_name(name: &str) -> String {
95
+ name.trim_matches('.')
96
+ .trim_matches(',')
97
+ .trim()
98
+ .to_ascii_uppercase()
99
+ }
100
+
101
+ fn code_line(line: &str) -> Option<&str> {
102
+ let trimmed = line.trim_start();
103
+ if trimmed.is_empty() || trimmed.starts_with('*') {
104
+ return None;
105
+ }
106
+ if matches!(line.as_bytes().get(6), Some(b'*' | b'/')) {
107
+ return None;
108
+ }
109
+ line.split("*>").next().map(str::trim_end)
110
+ }
111
+
112
+ fn has_statement_terminator(line: &str) -> bool {
113
+ let mut quote = None::<u8>;
114
+ let bytes = line.as_bytes();
115
+
116
+ for (idx, b) in bytes.iter().enumerate() {
117
+ match (quote, *b) {
118
+ (Some(q), c) if c == q => quote = None,
119
+ (None, b'\'' | b'"') => quote = Some(*b),
120
+ (None, b'.') => {
121
+ let next = bytes.get(idx + 1).copied();
122
+ if next.map_or(true, |c| c.is_ascii_whitespace()) {
123
+ return true;
124
+ }
125
+ }
126
+ _ => {}
127
+ }
128
+ }
129
+
130
+ false
131
+ }
132
+
133
+ fn is_name_byte(b: u8) -> bool {
134
+ b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'$' | b'#' | b'@')
135
+ }
@@ -0,0 +1,196 @@
1
+ use std::path::{Path, PathBuf};
2
+
3
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
4
+ pub enum CallKind {
5
+ Static,
6
+ Dynamic,
7
+ }
8
+
9
+ impl CallKind {
10
+ pub(crate) fn as_str(self) -> &'static str {
11
+ match self {
12
+ CallKind::Static => "static",
13
+ CallKind::Dynamic => "dynamic",
14
+ }
15
+ }
16
+ }
17
+
18
+ #[derive(Debug, Clone)]
19
+ pub struct CallSummary {
20
+ pub target: String,
21
+ pub kind: CallKind,
22
+ pub using_count: usize,
23
+ }
24
+
25
+ #[derive(Debug, Clone)]
26
+ pub struct CopybookSummary {
27
+ pub name: String,
28
+ pub resolved_path: Option<PathBuf>,
29
+ pub has_replacing: bool,
30
+ }
31
+
32
+ #[derive(Debug, Clone)]
33
+ pub struct ProgramSummary {
34
+ pub name: String,
35
+ pub path: PathBuf,
36
+ pub copybooks: Vec<CopybookSummary>,
37
+ pub calls: Vec<CallSummary>,
38
+ pub data_items: usize,
39
+ }
40
+
41
+ #[derive(Debug, Clone)]
42
+ pub struct IndexReport {
43
+ pub files: Vec<crate::cobol::scanner::CobolFileEntry>,
44
+ pub source_count: usize,
45
+ pub copybook_count: usize,
46
+ pub programs: Vec<ProgramSummary>,
47
+ pub copybook_uses: usize,
48
+ pub resolved_copybooks: usize,
49
+ pub unresolved_copybooks: Vec<String>,
50
+ pub static_calls: usize,
51
+ pub dynamic_calls: usize,
52
+ pub data_items: usize,
53
+ }
54
+
55
+ impl IndexReport {
56
+ pub fn to_message(&self, db_path: &Path) -> String {
57
+ let mut out = format!(
58
+ "Project index initialized.\n Files: {} (sources: {}, copybooks: {})\n Programs: {}\n COPY uses: {} resolved / {} total\n CALL edges: {} static, {} dynamic\n DATA items: {}\n SQLite: {}",
59
+ self.files.len(),
60
+ self.source_count,
61
+ self.copybook_count,
62
+ self.programs.len(),
63
+ self.resolved_copybooks,
64
+ self.copybook_uses,
65
+ self.static_calls,
66
+ self.dynamic_calls,
67
+ self.data_items,
68
+ db_path.to_string_lossy(),
69
+ );
70
+
71
+ if !self.programs.is_empty() {
72
+ out.push_str("\n\nPrograms:");
73
+ for program in self.programs.iter().take(20) {
74
+ out.push_str(&format!(
75
+ "\n - {} ({})",
76
+ program.name,
77
+ program.path.to_string_lossy()
78
+ ));
79
+ if !program.copybooks.is_empty() {
80
+ let names = program
81
+ .copybooks
82
+ .iter()
83
+ .map(|c| {
84
+ if c.resolved_path.is_some() {
85
+ if c.has_replacing {
86
+ format!("{}:resolved+replacing", c.name)
87
+ } else {
88
+ format!("{}:resolved", c.name)
89
+ }
90
+ } else {
91
+ format!("{}:missing", c.name)
92
+ }
93
+ })
94
+ .collect::<Vec<_>>()
95
+ .join(", ");
96
+ out.push_str(&format!("\n COPY: {}", names));
97
+ }
98
+ if !program.calls.is_empty() {
99
+ let calls = program
100
+ .calls
101
+ .iter()
102
+ .map(|c| {
103
+ format!("{}({}; USING {})", c.target, c.kind.as_str(), c.using_count)
104
+ })
105
+ .collect::<Vec<_>>()
106
+ .join(", ");
107
+ out.push_str(&format!("\n CALL: {}", calls));
108
+ }
109
+ if program.data_items > 0 {
110
+ out.push_str(&format!("\n DATA: {} item(s)", program.data_items));
111
+ }
112
+ }
113
+ if self.programs.len() > 20 {
114
+ out.push_str(&format!(
115
+ "\n ... {} more program(s)",
116
+ self.programs.len() - 20
117
+ ));
118
+ }
119
+ }
120
+
121
+ if !self.unresolved_copybooks.is_empty() {
122
+ out.push_str("\n\nUnresolved COPY:");
123
+ for name in self.unresolved_copybooks.iter().take(20) {
124
+ out.push_str(&format!("\n - {}", name));
125
+ }
126
+ }
127
+
128
+ out
129
+ }
130
+ }
131
+
132
+ #[derive(Debug)]
133
+ pub(crate) struct ParsedProgram {
134
+ pub(crate) name: String,
135
+ pub(crate) start_offset: usize,
136
+ pub(crate) byte_len: usize,
137
+ }
138
+
139
+ #[derive(Debug)]
140
+ pub(crate) struct ParsedCopy {
141
+ pub(crate) name: String,
142
+ pub(crate) start_offset: usize,
143
+ pub(crate) byte_len: usize,
144
+ pub(crate) replacing_text: Option<String>,
145
+ }
146
+
147
+ #[derive(Debug)]
148
+ pub(crate) struct ParsedCall {
149
+ pub(crate) caller_name: Option<String>,
150
+ pub(crate) target: String,
151
+ pub(crate) kind: CallKind,
152
+ pub(crate) start_offset: usize,
153
+ pub(crate) byte_len: usize,
154
+ pub(crate) using_count: usize,
155
+ }
156
+
157
+ #[derive(Debug)]
158
+ pub(crate) struct ParsedDataItem {
159
+ pub(crate) source_path: PathBuf,
160
+ pub(crate) name: String,
161
+ pub(crate) level: u16,
162
+ pub(crate) parent_name: Option<String>,
163
+ pub(crate) pic: Option<String>,
164
+ pub(crate) usage_clause: Option<String>,
165
+ pub(crate) occurs: Option<i64>,
166
+ pub(crate) redefines: Option<String>,
167
+ pub(crate) section: Option<String>,
168
+ pub(crate) byte_offset: Option<i64>,
169
+ pub(crate) byte_size: Option<i64>,
170
+ pub(crate) storage_kind: Option<String>,
171
+ pub(crate) layout_status: Option<String>,
172
+ pub(crate) start_offset: usize,
173
+ pub(crate) byte_len: usize,
174
+ }
175
+
176
+ #[derive(Debug)]
177
+ pub(crate) struct ParsedFile {
178
+ pub(crate) path: PathBuf,
179
+ pub(crate) programs: Vec<ParsedProgram>,
180
+ pub(crate) copies: Vec<ParsedCopy>,
181
+ pub(crate) calls: Vec<ParsedCall>,
182
+ }
183
+
184
+ #[derive(Debug)]
185
+ pub(crate) struct Token {
186
+ pub(crate) text: String,
187
+ pub(crate) start: usize,
188
+ pub(crate) quoted: bool,
189
+ }
190
+
191
+ #[derive(Debug)]
192
+ pub(crate) struct LogicalLine {
193
+ pub(crate) text: String,
194
+ pub(crate) start_offset: usize,
195
+ pub(crate) byte_len: usize,
196
+ }
@@ -0,0 +1,72 @@
1
+ use std::path::{Path, PathBuf};
2
+
3
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
4
+ pub enum CobolFileType {
5
+ Source, // .cbl, .cob, .coo
6
+ Copybook, // .cpy
7
+ }
8
+
9
+ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10
+ pub struct CobolFileEntry {
11
+ pub path: PathBuf,
12
+ pub file_type: CobolFileType,
13
+ pub size_bytes: u64,
14
+ }
15
+
16
+ /// Recursively scans `dir` for COBOL source and copybook files, ignoring common directories.
17
+ /// The root directory is always scanned; exclusion rules only apply to subdirectories.
18
+ pub fn scan_sandbox(dir: &Path) -> std::io::Result<Vec<CobolFileEntry>> {
19
+ let mut files = Vec::new();
20
+ scan_dir_entries(dir, &mut files)?;
21
+ files.sort_by(|a, b| a.path.cmp(&b.path));
22
+ Ok(files)
23
+ }
24
+
25
+ /// Returns true if a directory name should be excluded from scanning.
26
+ fn should_exclude_dir(name: &str) -> bool {
27
+ name.starts_with('.')
28
+ || name == "target"
29
+ || name == "node_modules"
30
+ || name == "vendor"
31
+ || name == "build"
32
+ }
33
+
34
+ /// Scans entries within `dir`. Does NOT check exclusion on `dir` itself —
35
+ /// callers are responsible for filtering before recursing.
36
+ fn scan_dir_entries(dir: &Path, files: &mut Vec<CobolFileEntry>) -> std::io::Result<()> {
37
+ if !dir.is_dir() {
38
+ return Ok(());
39
+ }
40
+
41
+ for entry in std::fs::read_dir(dir)? {
42
+ let entry = entry?;
43
+ let path = entry.path();
44
+ if path.is_dir() {
45
+ // Apply exclusion only to child directories, not the root
46
+ if let Some(name) = path.file_name().and_then(|s| s.to_str()) {
47
+ if should_exclude_dir(name) {
48
+ continue;
49
+ }
50
+ }
51
+ scan_dir_entries(&path, files)?;
52
+ } else if path.is_file() {
53
+ if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
54
+ let ext_lower = ext.to_lowercase();
55
+ let file_type = match ext_lower.as_str() {
56
+ "cbl" | "cob" | "coo" => Some(CobolFileType::Source),
57
+ "cpy" => Some(CobolFileType::Copybook),
58
+ _ => None,
59
+ };
60
+ if let Some(ft) = file_type {
61
+ let size_bytes = entry.metadata()?.len();
62
+ files.push(CobolFileEntry {
63
+ path,
64
+ file_type: ft,
65
+ size_bytes,
66
+ });
67
+ }
68
+ }
69
+ }
70
+ }
71
+ Ok(())
72
+ }