1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
|
/******************************************************************************
__ ____ __
/ / ___ ____ _/ __/_____________ _/ /__
/ / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
/ /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/
/_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
(C)opyright 2025, Leafscale, LLC - https://www.leafscale.com
Project: Zygaena
Filename: archive.reef
Authors: Chris Tusa <chris.tusa@leafscale.com>
License: <see LICENSE file included with this source code>
Description: Tar/xz archive operations using shell commands
******************************************************************************/
module util.archive
import sys.process
import io.file
import io.dir
import io.path
import core.str
import core.result as res
export
// Archive type enumeration (as strings for simplicity)
const ARCHIVE_TAR_GZ: string
const ARCHIVE_TAR_XZ: string
const ARCHIVE_TAR_BZ2: string
const ARCHIVE_ZIP: string
const ARCHIVE_UNKNOWN: string
// Detect archive type from filename extension
fn detect_type(archive_path: string): string
// Extract an archive to destination directory
fn extract(archive_path: string, dest_dir: string): bool
// Create a .pkg.tar.xz package from a directory
fn create_package(source_dir: string, output_path: string): bool
// List contents of an archive (returns count, fills files array)
fn list_contents(archive_path: string, files: [string], max_count: int): int
end export
// Archive type constants
const ARCHIVE_TAR_GZ: string = "tar.gz"
const ARCHIVE_TAR_XZ: string = "tar.xz"
const ARCHIVE_TAR_BZ2: string = "tar.bz2"
const ARCHIVE_ZIP: string = "zip"
const ARCHIVE_UNKNOWN: string = "unknown"
// Get the tar command (use gtar on illumos/OmniOS, tar on Linux)
fn get_tar_cmd(): string
// On illumos, we need GNU tar for full compatibility
// Check if gtar exists, otherwise fall back to tar (for Linux)
if file.fileExists("/usr/bin/gtar")
return "/usr/bin/gtar"
elif file.fileExists("/usr/gnu/bin/tar")
return "/usr/gnu/bin/tar"
end if
// Fall back to system tar (Linux)
return "tar"
end get_tar_cmd
// Detect archive type from filename
fn detect_type(archive_path: string): string
let lower = str.to_lower(archive_path)
if str.ends_with(lower, ".tar.xz") or str.ends_with(lower, ".txz")
return ARCHIVE_TAR_XZ
elif str.ends_with(lower, ".tar.gz") or str.ends_with(lower, ".tgz")
return ARCHIVE_TAR_GZ
elif str.ends_with(lower, ".tar.bz2") or str.ends_with(lower, ".tbz2") or str.ends_with(lower, ".tbz")
return ARCHIVE_TAR_BZ2
elif str.ends_with(lower, ".zip")
return ARCHIVE_ZIP
elif str.ends_with(lower, ".tar")
// Plain tar (no compression)
return "tar"
end if
return ARCHIVE_UNKNOWN
end detect_type
// Build the tar extraction flags based on archive type
fn get_extract_flags(archive_type: string): string
if archive_type == ARCHIVE_TAR_XZ
return "-Jxf"
elif archive_type == ARCHIVE_TAR_GZ
return "-zxf"
elif archive_type == ARCHIVE_TAR_BZ2
return "-jxf"
elif archive_type == "tar"
return "-xf"
end if
return ""
end get_extract_flags
// Build the tar list flags based on archive type
fn get_list_flags(archive_type: string): string
if archive_type == ARCHIVE_TAR_XZ
return "-Jtf"
elif archive_type == ARCHIVE_TAR_GZ
return "-ztf"
elif archive_type == ARCHIVE_TAR_BZ2
return "-jtf"
elif archive_type == "tar"
return "-tf"
end if
return ""
end get_list_flags
// Extract an archive to the destination directory
fn extract(archive_path: string, dest_dir: string): bool
// Verify archive exists
if not file.fileExists(archive_path)
return false
end if
// Ensure destination directory exists
if not dir.dir_exists(dest_dir)
if not res.is_ok(dir.create_dir_all(dest_dir))
return false
end if
end if
let archive_type = detect_type(archive_path)
if archive_type == ARCHIVE_ZIP
// Use unzip for zip files
let cmd = "unzip -q -o \"" + archive_path + "\" -d \"" + dest_dir + "\""
let pid = process.process_spawn_shell(cmd)
if pid < 0
return false
end if
let exit_code = process.process_wait(pid)
return exit_code == 0
end if
let flags = get_extract_flags(archive_type)
if str.length(flags) == 0
return false
end if
// Build tar command
let tar = get_tar_cmd()
let cmd = tar + " " + flags + " \"" + archive_path + "\" -C \"" + dest_dir + "\""
let pid = process.process_spawn_shell(cmd)
if pid < 0
return false
end if
let exit_code = process.process_wait(pid)
return exit_code == 0
end extract
// Create a .pkg.tar.xz package from a source directory
fn create_package(source_dir: string, output_path: string): bool
// Verify source directory exists
if not dir.dir_exists(source_dir)
return false
end if
// Ensure parent directory of output exists
let output_dir = path.dirname(output_path)
if str.length(output_dir) > 0 and not dir.dir_exists(output_dir)
if not res.is_ok(dir.create_dir_all(output_dir))
return false
end if
end if
// Create tar.xz archive
// Command: cd source_dir && tar -Jcf output_path .
let tar = get_tar_cmd()
let cmd = "cd \"" + source_dir + "\" && " + tar + " -Jcf \"" + output_path + "\" ."
let pid = process.process_spawn_shell(cmd)
if pid < 0
return false
end if
let exit_code = process.process_wait(pid)
return exit_code == 0
end create_package
// List contents of an archive
fn list_contents(archive_path: string, files: [string], max_count: int): int
if not file.fileExists(archive_path)
return 0
end if
let archive_type = detect_type(archive_path)
if archive_type == ARCHIVE_ZIP
// For zip files, use unzip -l and parse output
// This is more complex, so for now return 0
// TODO: Implement zip listing if needed
return 0
end if
let flags = get_list_flags(archive_type)
if str.length(flags) == 0
return 0
end if
// Build tar command with output to temp file
let tar = get_tar_cmd()
let tmp_file = "/tmp/coral_archive_list_" + int_to_string(process.getpid()) + ".txt"
let cmd = tar + " " + flags + " \"" + archive_path + "\" > \"" + tmp_file + "\" 2>/dev/null"
let pid = process.process_spawn_shell(cmd)
if pid < 0
return 0
end if
let exit_code = process.process_wait(pid)
if exit_code != 0
return 0
end if
// Read the temp file
if not file.fileExists(tmp_file)
return 0
end if
let content = res.unwrap_or(file.readFile(tmp_file), "")
// Clean up temp file
cleanup_temp_file(tmp_file)
if str.length(content) == 0
return 0
end if
// Split by newlines
mut lines: [string] = new [string](max_count)
let line_count = str.split(content, '\n', lines, max_count)
// Copy non-empty lines to output
mut count = 0
mut i = 0
while i < line_count and count < max_count
let line = str.trim_ws(lines[i])
if str.length(line) > 0
files[count] = line
count = count + 1
end if
i = i + 1
end while
return count
end list_contents
// Helper: remove temp file
fn cleanup_temp_file(path: string): bool
let cmd = "rm -f \"" + path + "\""
let pid = process.process_spawn_shell(cmd)
if pid < 0
return false
end if
process.process_wait(pid)
return true
end cleanup_temp_file
// Helper: convert int to string (local copy to avoid circular import)
fn int_to_string(n: int): string
if n == 0
return "0"
end if
mut negative = false
mut value = n
if n < 0
negative = true
value = 0 - n
end if
mut result = ""
while value > 0
let digit = value % 10
result = str.concat(str.substring("0123456789", digit, 1), result)
value = value / 10
end while
if negative
result = str.concat("-", result)
end if
return result
end int_to_string
end module
|