qopconv; now uses cell's own qopconv for building

This commit is contained in:
2025-11-25 12:25:28 -06:00
parent 67badc3e48
commit 9b19d19698
12 changed files with 787 additions and 245 deletions

1
.gitignore vendored
View File

@@ -27,3 +27,4 @@ discord_social_sdk/
discord_partner_sdk/
steam_api64.dll
subprojects/.wraplock
.gemini

View File

@@ -364,7 +364,7 @@ cell_dep = declare_dependency(
# Create core.zip from scripts folder
qop_target = custom_target('core.qop',
output: 'core.qop',
command: ['sh', '-c', 'qopconv -d ' + meson.project_source_root() / 'scripts . core.qop'],
command: ['sh', '-c', ' mkdir -p .cell && cell qopconv -d ' + meson.project_source_root() / 'scripts . core.qop'],
build_by_default: true,
build_always_stale: true
)

View File

@@ -3,42 +3,61 @@
var io = use('cellfs')
var js = use('js')
var time = use('time')
var qop = use('qop')
var build_root = '.cell/build'
log.console("Building scripts...")
var now = time.number()
// Find and compile all .cm and .ce files from root
var files = io.globfs(['**/*.cm', '**/*.ce', '!**/*.git', '!**/*.cell', '!**/subprojects'], '')
var compiled_count = 0
var error_count = 0
for (var file of files) {
var src = io.slurp(file)
var fullpath = io.realdir(file) + "/" + file
var outpath = build_root + fullpath
io.mkdir(outpath.substring(0, outpath.lastIndexOf('/')))
var mod_name = file
function compile_file(src_path, dest_path, is_core) {
try {
var src_content
if (is_core) {
return
} else {
src_content = io.slurp(src_path)
}
io.mkdir(dest_path.substring(0, dest_path.lastIndexOf('/')))
var mod_name = src_path
.replace(/\.(cm|ce)$/, '')
.replace(/[\/\-.]/g, '_')
var src_content = io.slurp(file)
var wrapped = '(function ' + mod_name + '(arg){' + src_content + '})'
try {
var compiled = js.compile(file, wrapped)
var compiled = js.compile(src_path, wrapped)
var blob = js.compile_blob(compiled)
io.slurpwrite(outpath, blob)
io.slurpwrite(dest_path, blob)
compiled_count++
// log.console("Compiled " + src_path + " -> " + dest_path)
} catch(e) {
log.error(e)
log.console("GHELO")
log.console(`Failed to compile ${src_path}: ${e.message}`);
error_count++
}
}
// 1. Local files
var local_files = io.globfs(['**/*.cm', '**/*.ce', '!**/*.git', '!**/*.cell', '!**/subprojects'], '')
for (var file of local_files) {
var dest = build_root + '/local/' + file + '.o'
compile_file(file, dest, false)
}
// 2. Modules
var module_files = io.globfs(['**/*.cm', '**/*.ce'], '.cell/modules')
for (var file of module_files) {
// file is relative to .cell/modules, e.g. "prosperon/draw2d.cm"
var dest = build_root + '/modules/' + file + '.o'
var src = '.cell/modules/' + file
compile_file(src, dest, false)
}
log.console("Build complete: " + compiled_count + " files compiled in " + (time.number()-now) + " seconds")
if (error_count > 0) {
log.console(" " + error_count + " errors")

View File

@@ -5,6 +5,7 @@ var cellfs = this
var fd = use('fd')
var miniz = use('miniz')
var qop = use('qop')
var wildstar = use('wildstar')
// Internal state
@@ -51,6 +52,12 @@ function mount_exists(mount, path) {
} catch (e) {
return false
}
} else if (mount.type == 'qop') {
try {
return mount.handle.stat(path) != null
} catch (e) {
return false
}
} else { // fs
var full_path = join_paths(mount.source, path)
try {
@@ -72,6 +79,12 @@ function is_directory(path) {
} catch (e) {
return false;
}
} else if (mount.type == 'qop') {
try {
return mount.handle.is_directory(path);
} catch (e) {
return false;
}
} else { // fs
var full_path = join_paths(mount.source, path)
try {
@@ -146,16 +159,29 @@ function mount(source, name) {
if (st.isDirectory) {
mount_info.type = 'fs'
} else if (st.isFile) {
var zip_data = fd.slurp(source)
var blob = fd.slurp(source)
var zip = miniz.read(zip_data)
// Try QOP first (it's likely faster to fail?) or Zip?
// QOP open checks magic.
var qop_archive = null
try {
qop_archive = qop.open(blob)
} catch(e) {}
if (qop_archive) {
mount_info.type = 'qop'
mount_info.handle = qop_archive
mount_info.zip_blob = blob // keep blob alive
} else {
var zip = miniz.read(blob)
if (!zip || typeof zip.count != 'function') {
throw new Error("Invalid ZIP file: " + source)
throw new Error("Invalid archive file (not zip or qop): " + source)
}
mount_info.type = 'zip'
mount_info.handle = zip
mount_info.zip_blob = zip_data // keep blob alive for lifetime of mount
mount_info.zip_blob = blob // keep blob alive
}
} else {
throw new Error("Unsupported mount source type: " + source)
}
@@ -183,6 +209,10 @@ function slurp(path) {
if (res.mount.type == 'zip') {
return res.mount.handle.slurp(res.path)
} else if (res.mount.type == 'qop') {
var data = res.mount.handle.read(res.path)
if (!data) throw new Error("File not found in qop: " + path)
return data
} else {
var full_path = join_paths(res.mount.source, res.path)
return fd.slurp(full_path)
@@ -219,6 +249,14 @@ function stat(path) {
modtime: mod * 1000,
isDirectory: false
}
} else if (res.mount.type == 'qop') {
var s = res.mount.handle.stat(res.path)
if (!s) throw new Error("File not found in qop: " + path)
return {
filesize: s.size,
modtime: s.modtime,
isDirectory: s.isDirectory
}
} else {
var full_path = join_paths(res.mount.source, res.path)
var s = fd.stat(full_path)
@@ -320,6 +358,32 @@ function enumerate(path, recurse) {
if (st && st.isDirectory) {
visit(full, "")
}
} else if (res.mount.type == 'qop') {
var all = res.mount.handle.list()
var prefix = res.path ? res.path + "/" : ""
var prefix_len = prefix.length
// Use a set to avoid duplicates if we are simulating directories
var seen = {}
for (var p of all) {
if (p.startsWith(prefix)) {
var rel = p.substring(prefix_len)
if (rel.length == 0) continue
if (!recurse) {
var slash = rel.indexOf('/')
if (slash != -1) {
rel = rel.substring(0, slash)
}
}
if (!seen[rel]) {
seen[rel] = true
results.push(rel)
}
}
}
}
return results
@@ -374,6 +438,21 @@ function globfs(globs, dir) {
if (st && st.isDirectory) {
visit(full, "")
}
} else if (res.mount.type == 'qop') {
var all = res.mount.handle.list()
var prefix = res.path ? res.path + "/" : ""
var prefix_len = prefix.length
for (var p of all) {
if (p.startsWith(prefix)) {
var rel = p.substring(prefix_len)
if (rel.length == 0) continue
if (!check_neg(rel) && check_pos(rel)) {
results.push(rel)
}
}
}
}
return results

View File

@@ -1,10 +1,8 @@
// cell clean - Remove build artifacts from modules/
var io = use('cellfs')
var fd = use('fd')
log.console(io.searchpath())
if (!io.exists('.cell/build')) {
if (!fd.stat('.cell/build').isDirectory) {
log.console("No build directory found")
$_.stop()
return
@@ -14,10 +12,10 @@ log.console("Cleaning build artifacts...")
// Remove the build directory
try {
io.rmdir('.cell/build')
fd.rm('.cell/build')
log.console("Build directory removed")
} catch (e) {
log.error("Failed during cleanup: " + e)
log.error(e)
}
log.console("Clean complete!")

View File

@@ -52,7 +52,7 @@ log.error = function(msg = new Error())
var caller = caller_data(1)
if (msg instanceof Error)
msg = msg + "\n" + msg.stack
msg = msg.name + ": " + msg.message + "\n" + msg.stack
console_mod.print(console_rec(caller.line,caller.file,msg))
}
@@ -87,7 +87,7 @@ if (!fd.stat('.cell').isDirectory) {
function is_file(path) {
try {
var st = fd.stat(path)
return st.isFile
return !!(st && st.isFile)
} catch {
return false
}
@@ -101,9 +101,12 @@ function write_file(path, blob) {
function mkdir_p(dir) {
if (dir == '' || dir == '.') return
try { fd.stat(dir) } catch {
var st = null
try { st = fd.stat(dir) } catch {}
if (!st || !st.isDirectory) {
mkdir_p(dir.substring(0, dir.lastIndexOf('/')))
fd.mkdir(dir)
try { fd.mkdir(dir) } catch {}
}
}
@@ -183,168 +186,105 @@ function get_package_from_path(path) {
// This will be set after shop.load_config() is called
var config = null
// Resolve actor program path with package awareness
// Resolution order:
// 1. Current package (root project when pkg_context is null)
// 2. Declared dependencies (from cell.toml)
// 3. core_qop (standard library)
function resolve_actor_path(requested, pkg_context) {
// Unified path resolution function
function resolve_path(requested, pkg_context, ext) {
var dependencies = (config && config.dependencies) ? config.dependencies : {}
// Helper to check file existence and return result object
function check(path, pkg, isCore) {
if (isCore) {
try {
core_qop.read(path)
return { path: path, package_name: null, isCore: true }
} catch (e) { return null }
}
if (is_file(path)) {
return { path: path, package_name: pkg, isCore: false }
}
return null
}
// Step 1: current package
if (pkg_context) {
var pkg_actor_path = '.cell/modules/' + pkg_context + '/' + requested + ACTOR_EXT
if (is_file(pkg_actor_path)) {
return { path: pkg_actor_path, package_name: pkg_context, isCore: false }
}
var pkg_path = '.cell/modules/' + pkg_context + '/' + requested + ext
var res = check(pkg_path, pkg_context, false)
if (res) return res
// Check if package is locally replaced
if (config && config.replace && config.replace[pkg_context]) {
var replace_path = config.replace[pkg_context]
var full_path = replace_path + '/' + requested + ACTOR_EXT
if (is_file(full_path)) {
return { path: full_path, package_name: pkg_context, isCore: false }
}
var full_path = replace_path + '/' + requested + ext
res = check(full_path, pkg_context, false)
if (res) return res
}
} else {
var project_actor_path = requested + ACTOR_EXT
if (is_file(project_actor_path)) {
return { path: project_actor_path, package_name: null, isCore: false }
}
var project_path = requested + ext
var res = check(project_path, null, false)
if (res) return res
}
// Step 2: dependencies (explicit alias first) and replace directives
if (requested.includes('/')) {
var actor_parts = requested.split('/')
var actor_pkg_alias = actor_parts[0]
var actor_sub_path = actor_parts.slice(1).join('/')
var parts = requested.split('/')
var pkg_alias = parts[0]
var sub_path = parts.slice(1).join('/')
// Check for replace directive first
if (config && config.replace && config.replace[actor_pkg_alias]) {
var replace_path = config.replace[actor_pkg_alias]
var full_path = replace_path + '/' + (actor_sub_path || actor_pkg_alias) + ACTOR_EXT
if (is_file(full_path)) {
return { path: full_path, package_name: actor_pkg_alias, isCore: false }
}
} else if (dependencies[actor_pkg_alias]) {
var dep_actor_path = '.cell/modules/' + actor_pkg_alias + '/' + actor_sub_path + ACTOR_EXT
if (is_file(dep_actor_path)) {
return { path: dep_actor_path, package_name: actor_pkg_alias, isCore: false }
}
if (config && config.replace && config.replace[pkg_alias]) {
var replace_path = config.replace[pkg_alias]
var full_path = replace_path + '/' + (sub_path || pkg_alias) + ext
var res = check(full_path, pkg_alias, false)
if (res) return res
} else if (dependencies[pkg_alias]) {
var dep_path = '.cell/modules/' + pkg_alias + '/' + sub_path + ext
var res = check(dep_path, pkg_alias, false)
if (res) return res
}
} else {
// Check replace directives for simple actor names
// Check replace directives for simple names
if (config && config.replace && config.replace[requested]) {
var replace_path = config.replace[requested]
var full_path = replace_path + '/' + requested + ACTOR_EXT
if (is_file(full_path)) {
return { path: full_path, package_name: requested, isCore: false }
}
}
// Check dependencies for simple actor names
for (var actor_alias in dependencies) {
var dep_actor_simple = '.cell/modules/' + actor_alias + '/' + requested + ACTOR_EXT
if (is_file(dep_actor_simple)) {
return { path: dep_actor_simple, package_name: actor_alias, isCore: false }
var full_path = replace_path + '/' + requested + ext
var res = check(full_path, requested, false)
if (res) return res
}
// Check dependencies for simple names
for (var alias in dependencies) {
var dep_simple = '.cell/modules/' + alias + '/' + requested + ext
var res = check(dep_simple, alias, false)
if (res) return res
}
}
// Step 3: core
try {
core_qop.read(requested + ACTOR_EXT)
return { path: requested + ACTOR_EXT, package_name: null, isCore: true }
} catch (e) {
// Not in core
}
return null
return check(requested + ext, null, true)
}
// Resolve module path with package awareness
// Resolution order:
// 1. Current package (root project when pkg_context is null)
// 2. Declared dependencies (from cell.toml)
// 3. core_qop (standard library)
function resolve_module_path(requested, pkg_context) {
var dependencies = (config && config.dependencies) ? config.dependencies : {}
// Step 1: current package
if (pkg_context) {
var pkg_module_path = '.cell/modules/' + pkg_context + '/' + requested + MOD_EXT
if (is_file(pkg_module_path)) {
return { path: pkg_module_path, package_name: pkg_context, isCore: false }
function get_compiled_path(resolved) {
var build_base = '.cell/build/'
if (resolved.isCore) {
return build_base + 'core/' + resolved.path + '.o'
} else if (resolved.package_name) {
// If it's in .cell/modules/<pkg>/...
var prefix = '.cell/modules/' + resolved.package_name + '/'
if (resolved.path.startsWith(prefix)) {
return build_base + 'modules/' + resolved.package_name + '/' + resolved.path.substring(prefix.length) + '.o'
}
// Check if package is locally replaced
if (config && config.replace && config.replace[pkg_context]) {
var replace_path = config.replace[pkg_context]
var full_path = replace_path + '/' + requested + MOD_EXT
if (is_file(full_path)) {
return { path: full_path, package_name: pkg_context, isCore: false }
}
}
// This seems correct for the "modules" case.
log.console(json.encode(resolved))
return build_base + 'modules/' + resolved.package_name + '/' + resolved.path.substring(resolved.path.lastIndexOf('/') + 1) + '.o'
} else {
var project_module_path = requested + MOD_EXT
if (is_file(project_module_path)) {
return { path: project_module_path, package_name: null, isCore: false }
// Local project file
return build_base + 'local/' + resolved.path + '.o'
}
}
// Step 2: dependencies (explicit alias first) and replace directives
if (requested.includes('/')) {
var module_parts = requested.split('/')
var module_pkg_alias = module_parts[0]
var module_sub = module_parts.slice(1).join('/')
// Check for replace directive first
if (config && config.replace && config.replace[module_pkg_alias]) {
var replace_path = config.replace[module_pkg_alias]
var full_path = replace_path + '/' + (module_sub || module_pkg_alias) + MOD_EXT
if (is_file(full_path)) {
return { path: full_path, package_name: module_pkg_alias, isCore: false }
}
} else if (dependencies[module_pkg_alias]) {
var dep_module_path = '.cell/modules/' + module_pkg_alias + '/' + module_sub + MOD_EXT
if (is_file(dep_module_path)) {
return { path: dep_module_path, package_name: module_pkg_alias, isCore: false }
}
}
} else {
// Check replace directives for simple module names
if (config && config.replace && config.replace[requested]) {
var replace_path = config.replace[requested]
var full_path = replace_path + '/' + requested + MOD_EXT
if (is_file(full_path)) {
return { path: full_path, package_name: requested, isCore: false }
}
}
// Check dependencies for simple module names
for (var module_alias in dependencies) {
var dep_module_simple = '.cell/modules/' + module_alias + '/' + requested + MOD_EXT
if (is_file(dep_module_simple)) {
return { path: dep_module_simple, package_name: module_alias, isCore: false }
}
}
}
// Step 3: core
try {
core_qop.read(requested + MOD_EXT)
return { path: requested + MOD_EXT, package_name: null, isCore: true }
} catch (e) {
// Not in core
}
return null
}
globalThis.use = function use(file, ...args) {
/* Package-aware module resolution:
1. If in a package context, check within that package first
2. Check local project files
3. Check declared dependencies (from cell.toml [dependencies])
4. Check core_qop (standard library)
1. Check local package (cwd, ie '.')
2. Check declared dependencies (from cell.toml [dependencies])
3. Check core_qop (standard library)
There's also the possibility of native C code;
there may be, in a package, a .so/.dll/.dylib
@@ -361,7 +301,7 @@ globalThis.use = function use(file, ...args) {
var embed_mod = use_embed(requested)
// Resolve the module path with package awareness
var resolved = resolve_module_path(requested, current_package)
var resolved = resolve_path(requested, current_package, MOD_EXT)
// Generate cache key based on resolution
var cache_key = resolved
@@ -370,6 +310,8 @@ globalThis.use = function use(file, ...args) {
if (use_cache[cache_key]) return use_cache[cache_key]
log.console(`cache miss: ${cache_key}`)
if (!resolved && !embed_mod)
throw new Error(`Module ${file} could not be found (package context: ${current_package || 'none'})`)
@@ -387,15 +329,38 @@ globalThis.use = function use(file, ...args) {
if (isCore) {
var ret = null
try {
// Try to load compiled version first
var compiledPath = get_compiled_path(resolved)
var useCompiled = false
if (is_file(compiledPath)) {
useCompiled = true
}
var fn
if (useCompiled) {
var compiledBlob = fd.slurp(compiledPath)
fn = js.compile_unblob(compiledBlob)
fn = js.eval_compile(fn)
log.console("use: using compiled core version " + compiledPath)
} else {
var script = utf8.decode(core_qop.read(path))
var mod_script = `(function setup_${requested.replace(/[^a-zA-Z0-9_]/g, '_')}_module(arg, $_){${script};})`
var fn = js.compile(path, mod_script)
fn = js.compile(path, mod_script)
// Save compiled version
mkdir_p(compiledPath.substring(0, compiledPath.lastIndexOf('/')))
var compiled = js.compile_blob(fn)
write_file(compiledPath, compiled)
fn = js.eval_compile(fn)
}
var context = embed_mod ? embed_mod : {}
ret = fn.call(context, args, $_)
} catch (e) {
// Script component doesn't exist, fall back to embedded module
// log.console("use: core module " + path + " has no script component, using embedded module")
// Script component doesn't exist or failed, fall back to embedded module
// log.console("use: core module " + path + " failed to load script (using embedded if avail)")
}
if (!ret && embed_mod) {
@@ -429,8 +394,7 @@ globalThis.use = function use(file, ...args) {
current_package = module_package
// Determine the compiled file path in .cell directory
var cleanPath = path.replace(/[:\\]/g, '/').replace(/\/+/g, '/')
var compiledPath = ".cell/build/" + cleanPath + '.o'
var compiledPath = get_compiled_path(resolved)
mkdir_p(compiledPath.substring(0, compiledPath.lastIndexOf('/')))
@@ -438,9 +402,10 @@ globalThis.use = function use(file, ...args) {
var useCompiled = false
var srcStat = fd.stat(path)
var compiledStat = fd.stat(compiledPath)
// if (srcStat && compiledStat && compiledStat.mtime > srcStat.mtime) {
// useCompiled = true
// }
if (srcStat && srcStat.isFile && compiledStat && compiledStat.isFile && compiledStat.mtime > srcStat.mtime) {
useCompiled = true
}
var fn
var mod_name = path.substring(path.lastIndexOf('/') + 1, path.lastIndexOf('.'))
@@ -449,14 +414,15 @@ globalThis.use = function use(file, ...args) {
var compiledBlob = fd.slurp(compiledPath)
fn = js.compile_unblob(compiledBlob)
fn = js.eval_compile(fn)
log.console("use: using compiled version " + compiledPath)
} else {
var script = utf8.decode(fd.slurp(path))
var mod_script = `(function setup_${mod_name}_module(arg, $_){${script};})`
fn = js.compile(path, mod_script)
// Save compiled version to .cell directory
// var compiled = js.compile_blob(fn)
// write_file(compiledPath, compiled)
var compiled = js.compile_blob(fn)
write_file(compiledPath, compiled)
fn = js.eval_compile(fn)
}
@@ -738,7 +704,7 @@ $_.start = function start(cb, program, ...args) {
if (!program) return
// Resolve the actor program path with package awareness
var resolved_program = resolve_actor_path(program, current_package)
var resolved_program = resolve_path(program, current_package, ACTOR_EXT)
if (!resolved_program) {
throw new Error(`Actor program ${program} could not be found (package context: ${current_package || 'none'})`)
}
@@ -1074,25 +1040,54 @@ actor_mod.setname(cell.args.program)
var prog = cell.args.program
// Resolve the main program path
var resolved_prog = resolve_actor_path(cell.args.program, current_package)
var resolved_prog = resolve_path(cell.args.program, current_package, ACTOR_EXT)
if (!resolved_prog) {
throw new Error(`Main program ${cell.args.program} could not be found`)
}
prog = resolved_prog.path
var progContent
var startfn
var compiledPath = get_compiled_path(resolved_prog)
var useCompiled = false
if (resolved_prog.isCore) {
progContent = utf8.decode(core_qop.read(prog))
// For core, we check if we have a compiled version, else we compile it
if (is_file(compiledPath)) {
useCompiled = true
}
} else {
progContent = utf8.decode(fd.slurp(prog))
// For local/modules, check timestamps
var srcStat = fd.stat(prog)
var compiledStat = fd.stat(compiledPath)
if (srcStat && srcStat.isFile && compiledStat && compiledStat.isFile && compiledStat.mtime > srcStat.mtime) {
useCompiled = true
}
}
var prog_script = `(function ${cell.args.program.name()}_start($_, arg) { var args = arg; ${progContent} })`
if (useCompiled) {
var compiledBlob = fd.slurp(compiledPath)
var fn = js.compile_unblob(compiledBlob)
startfn = js.eval_compile(fn)
log.console("main: using compiled version " + compiledPath)
} else {
var progContent
if (resolved_prog.isCore) {
progContent = utf8.decode(core_qop.read(prog))
} else {
progContent = utf8.decode(fd.slurp(prog))
}
// queue up its first turn instead of run immediately
var prog_script = `(function ${cell.args.program.name()}_start($_, arg) { var args = arg; ${progContent} })`
var startfn = js.eval(cell.args.program, prog_script);
// Compile and save
var fn = js.compile(cell.args.program, prog_script)
mkdir_p(compiledPath.substring(0, compiledPath.lastIndexOf('/')))
var compiled = js.compile_blob(fn)
write_file(compiledPath, compiled)
startfn = js.eval_compile(fn)
}
log.console(`program compiled in ${time.number()-load_program_start} seconds`)

View File

@@ -1,18 +0,0 @@
var http = this
http.fetch[cell.DOC] = `Initiate an asynchronous HTTP GET request.
This function enqueues an HTTP GET request for the specified URL. It supports both buffered responses (full response collected in memory) and streaming data (processed as it arrives). The request is executed asynchronously, and its completion or streaming data is handled via callbacks provided in the options. Use 'poll' to process the results.
:param url: A string representing the URL to fetch.
:param options: Either a callback function or an object with optional properties:
- 'callback': A function invoked upon request completion, receiving an object with 'data' (string or null) and 'error' (string or null) properties.
- 'on_data': A function invoked for each chunk of streaming data, receiving a string chunk as its argument. If supplied, 'callback.data' will be null.
:return: null
:throws:
- An error if the URL is not a string or is invalid.
- An error if the options argument is neither a function nor an object.
- An error if memory allocation or CURL initialization fails.
`
return http

View File

@@ -1,16 +0,0 @@
var miniz = this
miniz.read[cell.DOC] = `Create a zip reader from the given ArrayBuffer containing an entire ZIP archive.
Return null if the data is invalid.
:param data: An ArrayBuffer with the entire ZIP file.
:return: A 'zip reader' object with methods for reading from the archive (mod, exists, slurp).
`
miniz.write[cell.DOC] = `Create a zip writer that writes to the specified file path. Overwrites the file if
it already exists. Return null on error.
:param path: The file path where the ZIP archive will be written.
:return: A 'zip writer' object with methods for adding files to the archive (add_file).
`
return miniz

147
scripts/qopconv.ce Normal file
View File

@@ -0,0 +1,147 @@
var fd = use('fd')
var qop = use('qop')
var cellfs = use('cellfs')
function print_usage() {
log.console("Usage: qopconv [OPTION...] FILE...")
log.console(" -u <archive> ... unpack archive")
log.console(" -l <archive> ... list contents of archive")
log.console(" -d <dir> ....... change read dir when creating archives")
log.console(" <sources...> <archive> ... create archive from sources")
}
function list(archive_path) {
var blob = fd.slurp(archive_path)
if (!blob) {
log.console("Could not open archive " + archive_path)
return
}
var archive = null
try {
archive = qop.open(blob)
} catch(e) {
log.console("Could not open archive " + archive_path + ": " + e.message)
return
}
var files = archive.list()
for (var f of files) {
var s = archive.stat(f)
// Format: index hash size path
// We don't have index/hash easily available in JS binding yet, just size/path
log.console(`${f} (${s.size} bytes)`)
}
archive.close()
}
function unpack(archive_path) {
var blob = fd.slurp(archive_path)
if (!blob) {
log.console("Could not open archive " + archive_path)
return
}
var archive = null
try {
archive = qop.open(blob)
} catch(e) {
log.console("Could not open archive " + archive_path + ": " + e.message)
return
}
var files = archive.list()
for (var f of files) {
var data = archive.read(f)
if (data) {
// Ensure directory exists
var dir = f.substring(0, f.lastIndexOf('/'))
if (dir) {
// recursive mkdir
var parts = dir.split('/')
var curr = "."
for (var p of parts) {
curr += "/" + p
try { fd.mkdir(curr) } catch(e) {}
}
}
var fh = fd.open(f, "w")
fd.write(fh, data)
fd.close(fh)
log.console("Extracted " + f)
}
}
archive.close()
}
function pack(sources, archive_path, read_dir) {
var writer = qop.write(archive_path)
var base_dir = read_dir || "."
function add_recursive(path) {
var full_path = base_dir + "/" + path
if (path == ".") full_path = base_dir
if (read_dir == null && path != ".") full_path = path
var st = fd.stat(full_path)
if (!st) {
log.console("Could not stat " + full_path)
return
}
if (st.isDirectory) {
var list = fd.readdir(full_path)
for (var item of list) {
if (item == "." || item == "..") continue
var sub = path == "." ? item : path + "/" + item
add_recursive(sub)
}
} else {
var data = fd.slurp(full_path)
if (data) {
writer.add_file(path, data)
log.console("Added " + path)
}
}
}
for (var s of sources) {
add_recursive(s)
}
writer.finalize()
log.console("Created " + archive_path)
}
if (typeof arg == 'undefined' || arg.length < 1) {
print_usage()
} else {
if (arg[0] == "-l") {
if (arg.length < 2) print_usage()
else list(arg[1])
} else if (arg[0] == "-u") {
if (arg.length < 2) print_usage()
else unpack(arg[1])
} else {
var sources = []
var archive = null
var read_dir = null
var i = 0
if (arg[0] == "-d") {
read_dir = arg[1]
i = 2
}
for (; i < arg.length - 1; i++) {
sources.push(arg[i])
}
archive = arg[arg.length - 1]
if (sources.length == 0) {
print_usage()
} else {
pack(sources, archive, read_dir)
}
}
}
$_.stop()

View File

@@ -79,7 +79,7 @@ JSC_SCALL(fd_open,
int fd = open(str, flags, mode);
if (fd < 0)
ret = JS_ThrowReferenceError(js, "open failed: %s", strerror(errno));
ret = JS_ThrowInternalError(js, "open failed: %s", strerror(errno));
else
ret = JS_NewInt32(js, fd);
)
@@ -90,7 +90,7 @@ JSC_CCALL(fd_write,
ssize_t wrote = js_fd_write_helper(js, fd, argv[1]);
if (wrote < 0)
return JS_ThrowReferenceError(js, "write failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "write failed: %s", strerror(errno));
return JS_NewInt64(js, wrote);
)
@@ -105,12 +105,12 @@ JSC_CCALL(fd_read,
void *buf = malloc(size);
if (!buf)
return JS_ThrowReferenceError(js, "malloc failed");
return JS_ThrowInternalError(js, "malloc failed");
ssize_t bytes_read = read(fd, buf, size);
if (bytes_read < 0) {
free(buf);
return JS_ThrowReferenceError(js, "read failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "read failed: %s", strerror(errno));
}
ret = js_new_blob_stoned_copy(js, buf, bytes_read);
@@ -121,7 +121,7 @@ JSC_CCALL(fd_read,
JSC_SCALL(fd_slurp,
struct stat st;
if (stat(str, &st) != 0)
return JS_ThrowReferenceError(js, "stat failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "stat failed: %s", strerror(errno));
if (!S_ISREG(st.st_mode))
return JS_ThrowTypeError(js, "path is not a regular file");
@@ -133,12 +133,12 @@ JSC_SCALL(fd_slurp,
#ifndef _WIN32
int fd = open(str, O_RDONLY);
if (fd < 0)
return JS_ThrowReferenceError(js, "open failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "open failed: %s", strerror(errno));
void *data = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (data == MAP_FAILED) {
close(fd);
return JS_ThrowReferenceError(js, "mmap failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "mmap failed: %s", strerror(errno));
}
ret = js_new_blob_stoned_copy(js, data, size);
munmap(data, size);
@@ -147,19 +147,19 @@ JSC_SCALL(fd_slurp,
// Windows: use memory mapping for optimal performance
HANDLE hFile = CreateFileA(str, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
return JS_ThrowReferenceError(js, "CreateFile failed: %lu", GetLastError());
return JS_ThrowInternalError(js, "CreateFile failed: %lu", GetLastError());
HANDLE hMapping = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (hMapping == NULL) {
CloseHandle(hFile);
return JS_ThrowReferenceError(js, "CreateFileMapping failed: %lu", GetLastError());
return JS_ThrowInternalError(js, "CreateFileMapping failed: %lu", GetLastError());
}
void *data = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
if (data == NULL) {
CloseHandle(hMapping);
CloseHandle(hFile);
return JS_ThrowReferenceError(js, "MapViewOfFile failed: %lu", GetLastError());
return JS_ThrowInternalError(js, "MapViewOfFile failed: %lu", GetLastError());
}
ret = js_new_blob_stoned_copy(js, data, size);
@@ -185,7 +185,7 @@ JSC_CCALL(fd_lseek,
off_t new_pos = lseek(fd, offset, whence);
if (new_pos < 0)
return JS_ThrowReferenceError(js, "lseek failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "lseek failed: %s", strerror(errno));
return JS_NewInt64(js, new_pos);
)
@@ -193,23 +193,90 @@ JSC_CCALL(fd_lseek,
JSC_CCALL(fd_getcwd,
char buf[PATH_MAX];
if (getcwd(buf, sizeof(buf)) == NULL)
return JS_ThrowReferenceError(js, "getcwd failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "getcwd failed: %s", strerror(errno));
return JS_NewString(js, buf);
)
JSC_SCALL(fd_rmdir,
if (rmdir(str) != 0)
ret = JS_ThrowReferenceError(js, "could not remove directory %s: %s", str, strerror(errno));
ret = JS_ThrowInternalError(js, "could not remove directory %s: %s", str, strerror(errno));
)
JSC_SCALL(fd_mkdir,
if (mkdir(str, 0755) != 0)
ret = JS_ThrowReferenceError(js, "could not make directory %s: %s", str, strerror(errno));
ret = JS_ThrowInternalError(js, "could not make directory %s: %s", str, strerror(errno));
)
JSC_SCALL(fd_unlink,
if (unlink(str) != 0)
ret = JS_ThrowReferenceError(js, "could not remove file %s: %s", str, strerror(errno));
ret = JS_ThrowInternalError(js, "could not remove file %s: %s", str, strerror(errno));
)
JSC_SCALL(fd_mv,
if (argc < 2)
ret = JS_ThrowTypeError(js, "fd.mv requires 2 arguments: old path and new path");
else if (!JS_IsString(argv[1]))
ret = JS_ThrowTypeError(js, "second argument must be a string (new path)");
else {
const char *new_path = JS_ToCString(js, argv[1]);
if (rename(str, new_path) != 0)
ret = JS_ThrowInternalError(js, "could not rename %s to %s: %s", str, new_path, strerror(errno));
JS_FreeCString(js, new_path);
}
)
// Helper function for recursive removal
static int remove_recursive(const char *path) {
struct stat st;
if (stat(path, &st) != 0)
return -1;
if (S_ISDIR(st.st_mode)) {
// Directory: remove contents first
#ifdef _WIN32
WIN32_FIND_DATA ffd;
char search_path[PATH_MAX];
snprintf(search_path, sizeof(search_path), "%s\\*", path);
HANDLE hFind = FindFirstFile(search_path, &ffd);
if (hFind != INVALID_HANDLE_VALUE) {
do {
if (strcmp(ffd.cFileName, ".") == 0 || strcmp(ffd.cFileName, "..") == 0) continue;
char child_path[PATH_MAX];
snprintf(child_path, sizeof(child_path), "%s\\%s", path, ffd.cFileName);
if (remove_recursive(child_path) != 0) {
FindClose(hFind);
return -1;
}
} while (FindNextFile(hFind, &ffd) != 0);
FindClose(hFind);
}
#else
DIR *d = opendir(path);
if (d) {
struct dirent *dir;
while ((dir = readdir(d)) != NULL) {
if (strcmp(dir->d_name, ".") == 0 || strcmp(dir->d_name, "..") == 0) continue;
char child_path[PATH_MAX];
snprintf(child_path, sizeof(child_path), "%s/%s", path, dir->d_name);
if (remove_recursive(child_path) != 0) {
closedir(d);
return -1;
}
}
closedir(d);
}
#endif
// Remove the now-empty directory
return rmdir(path);
} else {
// File: just unlink
return unlink(path);
}
}
JSC_SCALL(fd_rm,
if (remove_recursive(str) != 0)
ret = JS_ThrowInternalError(js, "could not remove %s: %s", str, strerror(errno));
)
JSC_CCALL(fd_fsync,
@@ -217,7 +284,7 @@ JSC_CCALL(fd_fsync,
if (fd < 0) return JS_EXCEPTION;
if (fsync(fd) != 0)
return JS_ThrowReferenceError(js, "fsync failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "fsync failed: %s", strerror(errno));
return JS_NULL;
)
@@ -227,7 +294,7 @@ JSC_CCALL(fd_close,
if (fd < 0) return JS_EXCEPTION;
if (close(fd) != 0)
return JS_ThrowReferenceError(js, "close failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "close failed: %s", strerror(errno));
return JS_NULL;
)
@@ -238,7 +305,7 @@ JSC_CCALL(fd_fstat,
struct stat st;
if (fstat(fd, &st) != 0)
return JS_ThrowReferenceError(js, "fstat failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "fstat failed: %s", strerror(errno));
printf("fstat on %s\n", argv[0]);
JSValue obj = JS_NewObject(js);
@@ -317,7 +384,7 @@ JSC_SCALL(fd_readdir,
snprintf(path, sizeof(path), "%s\\*", str);
HANDLE hFind = FindFirstFile(path, &ffd);
if (hFind == INVALID_HANDLE_VALUE) {
ret = JS_ThrowReferenceError(js, "FindFirstFile failed for %s", path);
ret = JS_ThrowInternalError(js, "FindFirstFile failed for %s", path);
} else {
ret = JS_NewArray(js);
int i = 0;
@@ -340,7 +407,7 @@ JSC_SCALL(fd_readdir,
}
closedir(d);
} else {
ret = JS_ThrowReferenceError(js, "opendir failed for %s: %s", str, strerror(errno));
ret = JS_ThrowInternalError(js, "opendir failed for %s: %s", str, strerror(errno));
}
#endif
)
@@ -355,6 +422,8 @@ static const JSCFunctionListEntry js_fd_funcs[] = {
MIST_FUNC_DEF(fd, rmdir, 1),
MIST_FUNC_DEF(fd, unlink, 1),
MIST_FUNC_DEF(fd, mkdir, 1),
MIST_FUNC_DEF(fd, mv, 2),
MIST_FUNC_DEF(fd, rm, 1),
MIST_FUNC_DEF(fd, fsync, 1),
MIST_FUNC_DEF(fd, close, 1),
MIST_FUNC_DEF(fd, stat, 1),

View File

@@ -21,6 +21,74 @@ static JSClassDef js_qop_archive_class = {
.finalizer = js_qop_archive_finalizer,
};
static JSClassID js_qop_writer_class_id;
typedef struct {
FILE *fh;
qop_file *files;
int len;
int capacity;
unsigned int size;
} qop_writer;
static void js_qop_writer_finalizer(JSRuntime *rt, JSValue val) {
qop_writer *w = JS_GetOpaque(val, js_qop_writer_class_id);
if (w) {
if (w->fh) fclose(w->fh);
if (w->files) js_free_rt(rt, w->files);
js_free_rt(rt, w);
}
}
static JSClassDef js_qop_writer_class = {
"qop writer",
.finalizer = js_qop_writer_finalizer,
};
static qop_writer *js2writer(JSContext *js, JSValue v) {
return JS_GetOpaque(v, js_qop_writer_class_id);
}
// Helper functions for writer
static void write_16(unsigned int v, FILE *fh) {
unsigned char b[2];
b[0] = 0xff & (v);
b[1] = 0xff & (v >> 8);
fwrite(b, 2, 1, fh);
}
static void write_32(unsigned int v, FILE *fh) {
unsigned char b[4];
b[0] = 0xff & (v);
b[1] = 0xff & (v >> 8);
b[2] = 0xff & (v >> 16);
b[3] = 0xff & (v >> 24);
fwrite(b, 4, 1, fh);
}
static void write_64(unsigned long long v, FILE *fh) {
unsigned char b[8];
b[0] = 0xff & (v);
b[1] = 0xff & (v >> 8);
b[2] = 0xff & (v >> 16);
b[3] = 0xff & (v >> 24);
b[4] = 0xff & (v >> 32);
b[5] = 0xff & (v >> 40);
b[6] = 0xff & (v >> 48);
b[7] = 0xff & (v >> 56);
fwrite(b, 8, 1, fh);
}
static unsigned long long qop_hash(const char *key) {
unsigned long long h = 525201411107845655ull;
for (;*key;++key) {
h ^= (unsigned char)*key;
h *= 0x5bd1e9955bd1e995ull;
h ^= h >> 47;
}
return h;
}
static qop_desc *js2qop(JSContext *js, JSValue v) {
return JS_GetOpaque(v, js_qop_archive_class_id);
}
@@ -60,6 +128,36 @@ JSC_SCALL(qop_open,
}
)
JSC_SCALL(qop_write,
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
FILE *fh = fopen(path, "wb");
JS_FreeCString(js, path);
if (!fh) return JS_ThrowInternalError(js, "Could not open file for writing");
qop_writer *w = js_malloc(js, sizeof(qop_writer));
if (!w) {
fclose(fh);
return JS_ThrowOutOfMemory(js);
}
w->fh = fh;
w->capacity = 1024;
w->len = 0;
w->size = 0;
w->files = js_malloc(js, sizeof(qop_file) * w->capacity);
if (!w->files) {
fclose(fh);
js_free(js, w);
return JS_ThrowOutOfMemory(js);
}
JSValue obj = JS_NewObjectClass(js, js_qop_writer_class_id);
JS_SetOpaque(obj, w);
ret = obj;
)
static JSValue js_qop_close(JSContext *js, JSValue self, int argc, JSValue *argv) {
qop_desc *qop = js2qop(js, self);
@@ -183,15 +281,179 @@ static JSValue js_qop_list(JSContext *js, JSValue self, int argc, JSValue *argv)
return arr;
}
static JSValue js_qop_stat(JSContext *js, JSValue self, int argc, JSValue *argv) {
qop_desc *qop = js2qop(js, self);
if (!qop) return JS_ThrowInternalError(js, "Invalid QOP archive");
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
if (!js_qop_ensure_index(js, qop)) {
JS_FreeCString(js, path);
return JS_ThrowReferenceError(js, "Failed to read QOP index");
}
qop_file *file = qop_find(qop, path);
JS_FreeCString(js, path);
if (!file) return JS_NULL;
JSValue obj = JS_NewObject(js);
JS_SetPropertyStr(js, obj, "size", JS_NewInt64(js, file->size));
JS_SetPropertyStr(js, obj, "modtime", JS_NewInt64(js, 0));
JS_SetPropertyStr(js, obj, "isDirectory", JS_NewBool(js, 0));
return obj;
}
static JSValue js_qop_is_directory(JSContext *js, JSValue self, int argc, JSValue *argv) {
qop_desc *qop = js2qop(js, self);
if (!qop) return JS_ThrowInternalError(js, "Invalid QOP archive");
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
if (!js_qop_ensure_index(js, qop)) {
JS_FreeCString(js, path);
return JS_ThrowReferenceError(js, "Failed to read QOP index");
}
// Check if any file starts with path + "/"
size_t path_len = strlen(path);
char *prefix = js_malloc(js, path_len + 2);
memcpy(prefix, path, path_len);
prefix[path_len] = '/';
prefix[path_len + 1] = '\0';
int found = 0;
// This is inefficient but simple. QOP doesn't have a directory structure.
// We iterate all files to check prefixes.
for (unsigned int i = 0; i < qop->hashmap_len; i++) {
qop_file *file = &qop->hashmap[i];
if (file->size == 0) continue;
// We need to read the path to check it
// Optimization: check if we can read just the prefix?
// qop_read_path reads the whole path.
// Let's read the path.
char file_path[1024]; // MAX_PATH_LEN
if (file->path_len > 1024) continue; // Should not happen based on spec
qop_read_path(qop, file, file_path);
if (strncmp(file_path, prefix, path_len + 1) == 0) {
found = 1;
break;
}
}
js_free(js, prefix);
JS_FreeCString(js, path);
return JS_NewBool(js, found);
}
// Writer methods
static JSValue js_writer_add_file(JSContext *js, JSValue self, int argc, JSValue *argv) {
qop_writer *w = js2writer(js, self);
if (!w) return JS_ThrowInternalError(js, "Invalid QOP writer");
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
size_t data_len;
void *data = js_get_blob_data(js, &data_len, argv[1]);
if (!data) {
JS_FreeCString(js, path);
return JS_ThrowTypeError(js, "Second argument must be a blob");
}
if (w->len >= w->capacity) {
w->capacity *= 2;
qop_file *new_files = js_realloc(js, w->files, sizeof(qop_file) * w->capacity);
if (!new_files) {
JS_FreeCString(js, path);
return JS_ThrowOutOfMemory(js);
}
w->files = new_files;
}
// Strip leading "./"
const char *archive_path = path;
if (path[0] == '.' && path[1] == '/') {
archive_path = path + 2;
}
unsigned long long hash = qop_hash(archive_path);
int path_len = strlen(archive_path) + 1;
// Write path
fwrite(archive_path, 1, path_len, w->fh);
// Write data
fwrite(data, 1, data_len, w->fh);
w->files[w->len] = (qop_file){
.hash = hash,
.offset = w->size,
.size = (unsigned int)data_len,
.path_len = (unsigned short)path_len,
.flags = QOP_FLAG_NONE
};
w->size += data_len + path_len;
w->len++;
JS_FreeCString(js, path);
return JS_NULL;
}
static JSValue js_writer_finalize(JSContext *js, JSValue self, int argc, JSValue *argv) {
qop_writer *w = js2writer(js, self);
if (!w || !w->fh) return JS_ThrowInternalError(js, "Invalid QOP writer or already closed");
unsigned int total_size = w->size + 12; // Header size
for (int i = 0; i < w->len; i++) {
write_64(w->files[i].hash, w->fh);
write_32(w->files[i].offset, w->fh);
write_32(w->files[i].size, w->fh);
write_16(w->files[i].path_len, w->fh);
write_16(w->files[i].flags, w->fh);
total_size += 20;
}
// Magic "qopf"
unsigned int magic = (((unsigned int)'q') << 0 | ((unsigned int)'o') << 8 |
((unsigned int)'p') << 16 | ((unsigned int)'f') << 24);
write_32(w->len, w->fh);
write_32(total_size, w->fh);
write_32(magic, w->fh);
fclose(w->fh);
w->fh = NULL;
return JS_NULL;
}
static const JSCFunctionListEntry js_qop_archive_funcs[] = {
JS_CFUNC_DEF("close", 0, js_qop_close),
JS_CFUNC_DEF("list", 0, js_qop_list),
JS_CFUNC_DEF("read", 1, js_qop_read),
JS_CFUNC_DEF("read_ex", 3, js_qop_read_ex),
JS_CFUNC_DEF("stat", 1, js_qop_stat),
JS_CFUNC_DEF("is_directory", 1, js_qop_is_directory),
};
static const JSCFunctionListEntry js_qop_writer_funcs[] = {
JS_CFUNC_DEF("add_file", 2, js_writer_add_file),
JS_CFUNC_DEF("finalize", 0, js_writer_finalize),
};
static const JSCFunctionListEntry js_qop_funcs[] = {
MIST_FUNC_DEF(qop, open, 1),
MIST_FUNC_DEF(qop, write, 1),
JS_PROP_INT32_DEF("FLAG_NONE", QOP_FLAG_NONE, JS_PROP_ENUMERABLE),
JS_PROP_INT32_DEF("FLAG_COMPRESSED_ZSTD", QOP_FLAG_COMPRESSED_ZSTD, JS_PROP_ENUMERABLE),
JS_PROP_INT32_DEF("FLAG_COMPRESSED_DEFLATE", QOP_FLAG_COMPRESSED_DEFLATE, JS_PROP_ENUMERABLE),
@@ -205,6 +467,12 @@ JSValue js_qop_use(JSContext *js) {
JS_SetPropertyFunctionList(js, archive_proto, js_qop_archive_funcs, countof(js_qop_archive_funcs));
JS_SetClassProto(js, js_qop_archive_class_id, archive_proto);
JS_NewClassID(&js_qop_writer_class_id);
JS_NewClass(JS_GetRuntime(js), js_qop_writer_class_id, &js_qop_writer_class);
JSValue writer_proto = JS_NewObject(js);
JS_SetPropertyFunctionList(js, writer_proto, js_qop_writer_funcs, countof(js_qop_writer_funcs));
JS_SetClassProto(js, js_qop_writer_class_id, writer_proto);
JSValue mod = JS_NewObject(js);
JS_SetPropertyFunctionList(js, mod, js_qop_funcs, countof(js_qop_funcs));
return mod;

View File

@@ -1,7 +1,7 @@
var i = 0
function loop(time)
{
log.console(`loop ${i} with time ${time}`)
// log.console(`loop ${i} with time ${time}`)
i++
if (i > 60) $_.stop()
$_.clock(loop)