|
root / src / util / archive.reef
archive.reef Reef 297 lines 8.2 KB
  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