LAM.lua (34276B)
1 -- LAM.lua - LuajitOS Application Manager 2 -- Package and install applications in .lam format 3 -- 4 -- LAM file format (v2 with optional signature): 5 -- [4 bytes] Magic "LAM\0" 6 -- [1 byte] Has signature (0 = no, 1 = yes) 7 -- If signed: 8 -- [64 bytes] ED25519 signature (signs everything after this field) 9 -- [2 bytes] Developer name length 10 -- [N bytes] Developer name string 11 -- [1 byte] Null separator 12 -- [4 bytes] Version (2) 13 -- [4 bytes] Manifest length 14 -- [N bytes] Manifest string (Lua code) 15 -- [1 byte] Null separator 16 -- [4 bytes] Number of directories 17 -- For each directory: 18 -- [2 bytes] Path length 19 -- [N bytes] Path string (relative to app root) 20 -- [1 byte] Null separator 21 -- [4 bytes] Number of files 22 -- For each file: 23 -- [2 bytes] Path length 24 -- [N bytes] Path string (relative to app root) 25 -- [4 bytes] Original size 26 -- [4 bytes] Compressed size 27 -- [N bytes] DEFLATE compressed data 28 -- [4 bytes] CRC32 checksum of entire file (excluding this field) 29 30 local LAM = {} 31 32 LAM.VERSION = 2 33 LAM.MAGIC = "LAM\0" 34 LAM.KEYS_DIR = "/keys" 35 36 -- Helper: Convert number to 4 bytes (little-endian) 37 local function uint32_to_bytes(n) 38 return string.char( 39 bit.band(n, 0xFF), 40 bit.band(bit.rshift(n, 8), 0xFF), 41 bit.band(bit.rshift(n, 16), 0xFF), 42 bit.band(bit.rshift(n, 24), 0xFF) 43 ) 44 end 45 46 -- Helper: Convert number to 2 bytes (little-endian) 47 local function uint16_to_bytes(n) 48 return string.char( 49 bit.band(n, 0xFF), 50 bit.band(bit.rshift(n, 8), 0xFF) 51 ) 52 end 53 54 -- Helper: Read 4 bytes as uint32 (little-endian) 55 local function bytes_to_uint32(str, pos) 56 pos = pos or 1 57 local b1, b2, b3, b4 = str:byte(pos, pos + 3) 58 return b1 + b2 * 256 + b3 * 65536 + b4 * 16777216 59 end 60 61 -- Helper: Read 2 bytes as uint16 (little-endian) 62 local function bytes_to_uint16(str, pos) 63 pos = pos or 1 64 local b1, b2 = str:byte(pos, pos + 1) 65 return b1 + b2 * 256 66 end 67 68 -- Helper: Simple CRC32 calculation 69 local function crc32(data) 70 if _G.CRC32 then 71 return _G.CRC32(data) 72 end 73 -- Fallback: simple checksum if CRC32 not available 74 local sum = 0 75 for i = 1, #data do 76 sum = bit.bxor(sum, data:byte(i)) 77 sum = bit.band(sum * 31, 0xFFFFFFFF) 78 end 79 return sum 80 end 81 82 -- Helper: List all files and directories recursively 83 local function listFilesRecursive(basePath, relativePath, files, dirs) 84 files = files or {} 85 dirs = dirs or {} 86 relativePath = relativePath or "" 87 88 local fullPath = basePath 89 if relativePath ~= "" then 90 fullPath = basePath .. "/" .. relativePath 91 end 92 93 -- List directory contents 94 local contents = nil 95 if _G.CRamdiskList then 96 contents = _G.CRamdiskList(fullPath) 97 end 98 99 if not contents then 100 return files, dirs 101 end 102 103 for _, item in ipairs(contents) do 104 local itemRelPath = relativePath == "" and item.name or (relativePath .. "/" .. item.name) 105 106 if item.type == "directory" then 107 table.insert(dirs, itemRelPath) 108 listFilesRecursive(basePath, itemRelPath, files, dirs) 109 else 110 table.insert(files, { 111 path = itemRelPath, 112 fullPath = fullPath .. "/" .. item.name 113 }) 114 end 115 end 116 117 return files, dirs 118 end 119 120 -- Helper: Expand ~ to /home and normalize path 121 local function normalizePath(path) 122 if not path then return nil end 123 -- Expand ~ to /home 124 if path:sub(1, 1) == "~" then 125 path = "/home" .. path:sub(2) 126 end 127 -- Remove trailing slash 128 if path:sub(-1) == "/" and #path > 1 then 129 path = path:sub(1, -2) 130 end 131 return path 132 end 133 134 -- Helper: Check if input looks like a path (contains / or starts with ~ or .) 135 local function isPath(input) 136 if not input then return false end 137 return input:match("^/") or input:match("^~") or input:match("^%./") or input:match("^%.%./") 138 end 139 140 -- Base64 alphabet 141 local b64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 142 143 -- Helper: Base64 encode 144 local function base64_encode(data) 145 if not data then return nil end 146 local result = {} 147 local pad = 0 148 149 for i = 1, #data, 3 do 150 local b1 = data:byte(i) or 0 151 local b2 = data:byte(i + 1) or 0 152 local b3 = data:byte(i + 2) or 0 153 154 local n = b1 * 65536 + b2 * 256 + b3 155 156 table.insert(result, b64chars:sub(bit.rshift(n, 18) + 1, bit.rshift(n, 18) + 1)) 157 table.insert(result, b64chars:sub(bit.band(bit.rshift(n, 12), 63) + 1, bit.band(bit.rshift(n, 12), 63) + 1)) 158 159 if i + 1 <= #data then 160 table.insert(result, b64chars:sub(bit.band(bit.rshift(n, 6), 63) + 1, bit.band(bit.rshift(n, 6), 63) + 1)) 161 else 162 table.insert(result, "=") 163 end 164 165 if i + 2 <= #data then 166 table.insert(result, b64chars:sub(bit.band(n, 63) + 1, bit.band(n, 63) + 1)) 167 else 168 table.insert(result, "=") 169 end 170 end 171 172 return table.concat(result) 173 end 174 175 -- Helper: Base64 decode 176 local function base64_decode(data) 177 if not data then return nil end 178 179 -- Build reverse lookup 180 local b64lookup = {} 181 for i = 1, 64 do 182 b64lookup[b64chars:sub(i, i)] = i - 1 183 end 184 185 -- Remove padding and whitespace 186 data = data:gsub("[^" .. b64chars .. "]", "") 187 188 local result = {} 189 for i = 1, #data, 4 do 190 local c1 = b64lookup[data:sub(i, i)] or 0 191 local c2 = b64lookup[data:sub(i + 1, i + 1)] or 0 192 local c3 = b64lookup[data:sub(i + 2, i + 2)] 193 local c4 = b64lookup[data:sub(i + 3, i + 3)] 194 195 local n = c1 * 262144 + c2 * 4096 196 if c3 then n = n + c3 * 64 end 197 if c4 then n = n + c4 end 198 199 table.insert(result, string.char(bit.band(bit.rshift(n, 16), 255))) 200 if c3 then 201 table.insert(result, string.char(bit.band(bit.rshift(n, 8), 255))) 202 end 203 if c4 then 204 table.insert(result, string.char(bit.band(n, 255))) 205 end 206 end 207 208 return table.concat(result) 209 end 210 211 --- Create a new developer keypair 212 -- @param developerName Name for the developer (used as folder name) 213 -- @return true on success, nil and error message on failure 214 function LAM.newDeveloper(developerName) 215 if not developerName or developerName == "" then 216 return nil, "Developer name required" 217 end 218 219 -- Sanitize name (alphanumeric, underscore, hyphen only) 220 if not developerName:match("^[%w_%-]+$") then 221 return nil, "Developer name can only contain letters, numbers, underscores, and hyphens" 222 end 223 224 -- Check if crypto is available 225 if not _G.crypto or not _G.crypto.ed25519_keypair then 226 return nil, "Crypto library not available (ed25519_keypair)" 227 end 228 229 -- Create /keys directory if it doesn't exist 230 if _G.CRamdiskMkdir and not _G.CRamdiskExists(LAM.KEYS_DIR) then 231 _G.CRamdiskMkdir(LAM.KEYS_DIR) 232 end 233 234 -- Create developer directory 235 local devDir = LAM.KEYS_DIR .. "/" .. developerName 236 if _G.CRamdiskExists and _G.CRamdiskExists(devDir) then 237 return nil, "Developer '" .. developerName .. "' already exists" 238 end 239 240 if not _G.CRamdiskMkdir(devDir) then 241 return nil, "Failed to create directory: " .. devDir 242 end 243 244 -- Generate keypair 245 local publicKey, secretKey = _G.crypto.ed25519_keypair() 246 247 if not publicKey or not secretKey then 248 return nil, "Failed to generate keypair" 249 end 250 251 -- Save public key 252 local pubPath = devDir .. "/public.key" 253 local pubHandle = _G.CRamdiskOpen(pubPath, "w") 254 if not pubHandle then 255 return nil, "Failed to create public key file" 256 end 257 _G.CRamdiskWrite(pubHandle, publicKey) 258 _G.CRamdiskClose(pubHandle) 259 260 -- Save secret key 261 local secPath = devDir .. "/secret.key" 262 local secHandle = _G.CRamdiskOpen(secPath, "w") 263 if not secHandle then 264 return nil, "Failed to create secret key file" 265 end 266 _G.CRamdiskWrite(secHandle, secretKey) 267 _G.CRamdiskClose(secHandle) 268 269 return true, { 270 name = developerName, 271 path = devDir, 272 publicKey = publicKey 273 } 274 end 275 276 --- Load developer keys 277 -- @param developerName Name of the developer 278 -- @return publicKey, secretKey (base64) or nil, error 279 function LAM.loadDeveloperKeys(developerName) 280 if not developerName then 281 return nil, nil, "Developer name required" 282 end 283 284 local devDir = LAM.KEYS_DIR .. "/" .. developerName 285 286 if not _G.CRamdiskExists or not _G.CRamdiskExists(devDir) then 287 return nil, nil, "Developer not found: " .. developerName 288 end 289 290 -- Load public key 291 local pubPath = devDir .. "/public.key" 292 local pubHandle = _G.CRamdiskOpen(pubPath, "r") 293 if not pubHandle then 294 return nil, nil, "Failed to open public key" 295 end 296 local publicKey = _G.CRamdiskRead(pubHandle) 297 _G.CRamdiskClose(pubHandle) 298 299 -- Load secret key 300 local secPath = devDir .. "/secret.key" 301 local secHandle = _G.CRamdiskOpen(secPath, "r") 302 if not secHandle then 303 return nil, nil, "Failed to open secret key" 304 end 305 local secretKey = _G.CRamdiskRead(secHandle) 306 _G.CRamdiskClose(secHandle) 307 308 return publicKey, secretKey 309 end 310 311 --- List all developers 312 -- @return Table of developer names 313 function LAM.listDevelopers() 314 local developers = {} 315 316 if not _G.CRamdiskList or not _G.CRamdiskExists(LAM.KEYS_DIR) then 317 return developers 318 end 319 320 local contents = _G.CRamdiskList(LAM.KEYS_DIR) 321 if contents then 322 for _, item in ipairs(contents) do 323 if item.type == "directory" then 324 -- Check if it has key files 325 local pubPath = LAM.KEYS_DIR .. "/" .. item.name .. "/public.key" 326 if _G.CRamdiskExists(pubPath) then 327 table.insert(developers, item.name) 328 end 329 end 330 end 331 end 332 333 return developers 334 end 335 336 --- Package an application into a .lam file 337 -- @param appIdOrPath Application ID (e.g., "com.luajitos.moonbrowser") or path (e.g., "~/Documents/myapp/") 338 -- @param outputPath Output file path (e.g., "/home/moonbrowser.lam") 339 -- @param options Optional table with: 340 -- - developer: Developer name to sign the package with 341 -- @return true on success, nil and error message on failure 342 function LAM.package(appIdOrPath, outputPath, options) 343 options = options or {} 344 if not appIdOrPath then 345 return nil, "Application ID or path required" 346 end 347 348 local appPath 349 local appId 350 351 -- Check if input is a path or an app ID 352 if isPath(appIdOrPath) then 353 -- It's a path - normalize it 354 appPath = normalizePath(appIdOrPath) 355 356 -- Check if path exists 357 if not _G.CRamdiskExists or not _G.CRamdiskExists(appPath) then 358 return nil, "Directory not found: " .. appPath 359 end 360 361 -- Extract app name from path for output filename 362 appId = appPath:match("([^/]+)$") or "app" 363 else 364 -- It's an app ID 365 appId = appIdOrPath 366 367 -- Add prefix if needed 368 if not appId:match("%.") then 369 appId = "com.luajitos." .. appId 370 end 371 372 appPath = "/apps/" .. appId 373 374 -- Check if app exists 375 if not _G.CRamdiskExists or not _G.CRamdiskExists(appPath) then 376 return nil, "Application not found: " .. appPath 377 end 378 end 379 380 -- Read manifest 381 local manifestPath = appPath .. "/manifest.lua" 382 if not _G.CRamdiskExists(manifestPath) then 383 return nil, "Manifest not found: " .. manifestPath 384 end 385 386 local manifestHandle = _G.CRamdiskOpen(manifestPath, "r") 387 if not manifestHandle then 388 return nil, "Failed to open manifest" 389 end 390 local manifestContent = _G.CRamdiskRead(manifestHandle) 391 _G.CRamdiskClose(manifestHandle) 392 393 if not manifestContent then 394 return nil, "Failed to read manifest" 395 end 396 397 -- List all files and directories 398 local files, dirs = listFilesRecursive(appPath, "") 399 400 -- Load developer keys if signing is requested 401 local developerName = options.developer 402 local publicKey, secretKey 403 if developerName then 404 publicKey, secretKey = LAM.loadDeveloperKeys(developerName) 405 if not publicKey or not secretKey then 406 return nil, "Failed to load developer keys for: " .. developerName 407 end 408 end 409 410 -- Build the package content (everything after signature header) 411 local contentParts = {} 412 413 -- Null separator after signature header 414 table.insert(contentParts, "\0") 415 416 -- Version 417 table.insert(contentParts, uint32_to_bytes(LAM.VERSION)) 418 419 -- Manifest 420 table.insert(contentParts, uint32_to_bytes(#manifestContent)) 421 table.insert(contentParts, manifestContent) 422 table.insert(contentParts, "\0") -- Null separator 423 424 -- Directories 425 table.insert(contentParts, uint32_to_bytes(#dirs)) 426 for _, dirPath in ipairs(dirs) do 427 table.insert(contentParts, uint16_to_bytes(#dirPath)) 428 table.insert(contentParts, dirPath) 429 end 430 table.insert(contentParts, "\0") -- Null separator 431 432 -- Files 433 table.insert(contentParts, uint32_to_bytes(#files)) 434 for _, file in ipairs(files) do 435 -- Read file content 436 local fileHandle = _G.CRamdiskOpen(file.fullPath, "r") 437 if not fileHandle then 438 return nil, "Failed to open file: " .. file.fullPath 439 end 440 local fileContent = _G.CRamdiskRead(fileHandle) 441 _G.CRamdiskClose(fileHandle) 442 443 if not fileContent then 444 return nil, "Failed to read file: " .. file.fullPath 445 end 446 447 -- Compress file content 448 local compressed = fileContent 449 local originalSize = #fileContent 450 451 if _G.DeflateCompress and originalSize > 0 then 452 local result = _G.DeflateCompress(fileContent, 6) -- Level 6 compression 453 if result and #result < originalSize then 454 compressed = result 455 end 456 end 457 458 -- Write file entry 459 table.insert(contentParts, uint16_to_bytes(#file.path)) 460 table.insert(contentParts, file.path) 461 table.insert(contentParts, uint32_to_bytes(originalSize)) 462 table.insert(contentParts, uint32_to_bytes(#compressed)) 463 table.insert(contentParts, compressed) 464 end 465 466 -- Combine content parts 467 local contentData = table.concat(contentParts) 468 469 -- Build final package with header 470 local headerParts = {} 471 472 -- Magic 473 table.insert(headerParts, LAM.MAGIC) 474 475 -- Has signature flag 476 if developerName then 477 table.insert(headerParts, string.char(1)) -- Has signature 478 479 -- Sign the content data 480 local secretKeyRaw = base64_decode(secretKey) 481 if not secretKeyRaw or #secretKeyRaw ~= 64 then 482 return nil, "Invalid secret key format" 483 end 484 485 local signature 486 if _G.crypto and _G.crypto.ed25519_sign then 487 local sigB64 = _G.crypto.ed25519_sign(contentData, secretKeyRaw) 488 signature = base64_decode(sigB64) 489 end 490 491 if not signature or #signature ~= 64 then 492 return nil, "Failed to sign package" 493 end 494 495 table.insert(headerParts, signature) -- 64-byte signature 496 497 -- Developer name 498 table.insert(headerParts, uint16_to_bytes(#developerName)) 499 table.insert(headerParts, developerName) 500 else 501 table.insert(headerParts, string.char(0)) -- No signature 502 end 503 504 -- Combine header and content 505 local packageData = table.concat(headerParts) .. contentData 506 507 -- Calculate and append checksum 508 local checksum = crc32(packageData) 509 packageData = packageData .. uint32_to_bytes(checksum) 510 511 -- Write to output file 512 if not outputPath then 513 outputPath = "/home/" .. appId:match("([^.]+)$") .. ".lam" 514 end 515 516 local outHandle = _G.CRamdiskOpen(outputPath, "w") 517 if not outHandle then 518 return nil, "Failed to create output file: " .. outputPath 519 end 520 521 _G.CRamdiskWrite(outHandle, packageData) 522 _G.CRamdiskClose(outHandle) 523 524 return true, { 525 path = outputPath, 526 size = #packageData, 527 files = #files, 528 dirs = #dirs, 529 manifest = manifestContent 530 } 531 end 532 533 --- Install an application from a .lam file 534 -- @param lamPath Path to the .lam file 535 -- @param options Optional table with: 536 -- - force: Overwrite existing installation 537 -- - skipPostInstall: Don't run post-install script 538 -- - requireSignature: Fail if package is not signed 539 -- @return true on success, nil and error message on failure 540 function LAM.install(lamPath, options) 541 options = options or {} 542 543 if not lamPath then 544 return nil, "LAM file path required" 545 end 546 547 -- Read the package file 548 if not _G.CRamdiskExists or not _G.CRamdiskExists(lamPath) then 549 return nil, "File not found: " .. lamPath 550 end 551 552 local handle = _G.CRamdiskOpen(lamPath, "r") 553 if not handle then 554 return nil, "Failed to open file: " .. lamPath 555 end 556 local data = _G.CRamdiskRead(handle) 557 _G.CRamdiskClose(handle) 558 559 if not data then 560 return nil, "Failed to read file" 561 end 562 563 local pos = 1 564 565 -- Verify magic 566 if data:sub(pos, pos + 3) ~= LAM.MAGIC then 567 return nil, "Invalid LAM file (bad magic)" 568 end 569 pos = pos + 4 570 571 -- Verify checksum first 572 local storedChecksum = bytes_to_uint32(data, #data - 3) 573 local calculatedChecksum = crc32(data:sub(1, #data - 4)) 574 if storedChecksum ~= calculatedChecksum then 575 return nil, "Checksum mismatch (file may be corrupted)" 576 end 577 578 -- Read signature flag 579 local hasSig = data:byte(pos) == 1 580 pos = pos + 1 581 582 local signature, developerName, signatureValid 583 local contentStart 584 585 if hasSig then 586 -- Read signature (64 bytes) 587 signature = data:sub(pos, pos + 63) 588 pos = pos + 64 589 590 -- Read developer name 591 local devNameLen = bytes_to_uint16(data, pos) 592 pos = pos + 2 593 developerName = data:sub(pos, pos + devNameLen - 1) 594 pos = pos + devNameLen 595 596 -- Content starts after developer name (at the null separator) 597 contentStart = pos 598 599 -- Verify signature 600 local contentData = data:sub(contentStart, #data - 4) -- Exclude checksum 601 local publicKey, _ = LAM.loadDeveloperKeys(developerName) 602 603 if publicKey then 604 local publicKeyRaw = base64_decode(publicKey) 605 if publicKeyRaw and #publicKeyRaw == 32 and _G.crypto and _G.crypto.ed25519_verify then 606 signatureValid = _G.crypto.ed25519_verify(contentData, signature, publicKeyRaw) 607 end 608 end 609 610 if not signatureValid then 611 print("WARNING: Package signature could not be verified for developer: " .. developerName) 612 if options.requireSignature then 613 return nil, "Signature verification failed" 614 end 615 else 616 print("Package signed by: " .. developerName .. " (verified)") 617 end 618 else 619 contentStart = pos 620 if options.requireSignature then 621 return nil, "Package is not signed (signature required)" 622 end 623 end 624 625 -- Skip null separator 626 pos = pos + 1 627 628 -- Read version 629 local version = bytes_to_uint32(data, pos) 630 pos = pos + 4 631 if version > LAM.VERSION then 632 return nil, "Unsupported LAM version: " .. version 633 end 634 635 -- Read manifest 636 local manifestLen = bytes_to_uint32(data, pos) 637 pos = pos + 4 638 local manifestContent = data:sub(pos, pos + manifestLen - 1) 639 pos = pos + manifestLen 640 641 -- Skip null separator 642 pos = pos + 1 643 644 -- Parse manifest to get app ID 645 local manifestFunc, err = loadstring("return " .. manifestContent, "manifest") 646 if not manifestFunc then 647 -- Try without "return" 648 manifestFunc, err = loadstring(manifestContent, "manifest") 649 end 650 651 if not manifestFunc then 652 return nil, "Failed to parse manifest: " .. tostring(err) 653 end 654 655 local ok, manifest = pcall(manifestFunc) 656 if not ok then 657 return nil, "Failed to execute manifest: " .. tostring(manifest) 658 end 659 660 -- Determine app ID from manifest or filename 661 local appId = manifest.id or manifest.name 662 if not appId then 663 -- Try to extract from filename 664 appId = lamPath:match("([^/]+)%.lam$") 665 end 666 if not appId then 667 return nil, "Could not determine application ID" 668 end 669 670 -- If appId doesn't have dots, assume com.luajitos prefix 671 if not appId:match("%.") then 672 appId = "com.luajitos." .. appId 673 end 674 675 local appPath = "/apps/" .. appId 676 677 -- Check if already installed 678 if _G.CRamdiskExists(appPath) and not options.force then 679 return nil, "Application already installed: " .. appId .. " (use force=true to overwrite)" 680 end 681 682 -- Create app directory 683 if not _G.CRamdiskExists(appPath) then 684 _G.CRamdiskMkdir(appPath) 685 end 686 687 -- Read directories 688 local numDirs = bytes_to_uint32(data, pos) 689 pos = pos + 4 690 691 local dirs = {} 692 for i = 1, numDirs do 693 local pathLen = bytes_to_uint16(data, pos) 694 pos = pos + 2 695 local dirPath = data:sub(pos, pos + pathLen - 1) 696 pos = pos + pathLen 697 table.insert(dirs, dirPath) 698 end 699 700 -- Skip null separator 701 pos = pos + 1 702 703 -- Create directories 704 for _, dirPath in ipairs(dirs) do 705 local fullPath = appPath .. "/" .. dirPath 706 if not _G.CRamdiskExists(fullPath) then 707 _G.CRamdiskMkdir(fullPath) 708 end 709 end 710 711 -- Read files 712 local numFiles = bytes_to_uint32(data, pos) 713 pos = pos + 4 714 715 local installedFiles = {} 716 for i = 1, numFiles do 717 local pathLen = bytes_to_uint16(data, pos) 718 pos = pos + 2 719 local filePath = data:sub(pos, pos + pathLen - 1) 720 pos = pos + pathLen 721 722 local originalSize = bytes_to_uint32(data, pos) 723 pos = pos + 4 724 local compressedSize = bytes_to_uint32(data, pos) 725 pos = pos + 4 726 727 local compressedData = data:sub(pos, pos + compressedSize - 1) 728 pos = pos + compressedSize 729 730 -- Decompress if needed 731 local fileContent 732 if compressedSize < originalSize and _G.DeflateDecompress then 733 fileContent = _G.DeflateDecompress(compressedData) 734 if not fileContent then 735 return nil, "Failed to decompress file: " .. filePath 736 end 737 else 738 fileContent = compressedData 739 end 740 741 -- Write file 742 local fullPath = appPath .. "/" .. filePath 743 744 -- Ensure parent directory exists 745 local parentDir = fullPath:match("(.*/)[^/]+$") 746 if parentDir and not _G.CRamdiskExists(parentDir) then 747 _G.CRamdiskMkdir(parentDir) 748 end 749 750 local fileHandle = _G.CRamdiskOpen(fullPath, "w") 751 if not fileHandle then 752 return nil, "Failed to create file: " .. fullPath 753 end 754 _G.CRamdiskWrite(fileHandle, fileContent) 755 _G.CRamdiskClose(fileHandle) 756 757 table.insert(installedFiles, filePath) 758 end 759 760 -- Write manifest (it's stored in package but needs to be written to disk) 761 local manifestHandle = _G.CRamdiskOpen(appPath .. "/manifest.lua", "w") 762 if manifestHandle then 763 _G.CRamdiskWrite(manifestHandle, manifestContent) 764 _G.CRamdiskClose(manifestHandle) 765 end 766 767 -- Run post-install script if specified 768 if manifest.postInstall and not options.skipPostInstall then 769 local postInstallPath = appPath .. "/src/" .. manifest.postInstall 770 if _G.CRamdiskExists(postInstallPath) then 771 -- Run the post-install script in the app's sandbox 772 if _G.run and _G.run.execute then 773 local success, result = pcall(function() 774 return _G.run.execute(appId, _G.fsRoot) 775 end) 776 if not success then 777 print("Warning: Post-install script failed: " .. tostring(result)) 778 end 779 end 780 end 781 end 782 783 return true, { 784 appId = appId, 785 path = appPath, 786 files = #installedFiles, 787 dirs = #dirs, 788 manifest = manifest 789 } 790 end 791 792 --- List installed applications 793 -- @return Table of installed app IDs 794 function LAM.list() 795 local apps = {} 796 797 if not _G.CRamdiskList then 798 return apps 799 end 800 801 local contents = _G.CRamdiskList("/apps") 802 if contents then 803 for _, item in ipairs(contents) do 804 if item.type == "directory" then 805 -- Check if it has a manifest 806 local manifestPath = "/apps/" .. item.name .. "/manifest.lua" 807 if _G.CRamdiskExists(manifestPath) then 808 table.insert(apps, item.name) 809 end 810 end 811 end 812 end 813 814 return apps 815 end 816 817 --- Get information about an installed application 818 -- @param appId Application ID 819 -- @return Manifest table or nil 820 function LAM.info(appId) 821 if not appId then 822 return nil, "Application ID required" 823 end 824 825 local manifestPath = "/apps/" .. appId .. "/manifest.lua" 826 827 if not _G.CRamdiskExists or not _G.CRamdiskExists(manifestPath) then 828 return nil, "Application not found: " .. appId 829 end 830 831 local handle = _G.CRamdiskOpen(manifestPath, "r") 832 if not handle then 833 return nil, "Failed to open manifest" 834 end 835 local content = _G.CRamdiskRead(handle) 836 _G.CRamdiskClose(handle) 837 838 local func, err = loadstring("return " .. content, "manifest") 839 if not func then 840 func, err = loadstring(content, "manifest") 841 end 842 843 if not func then 844 return nil, "Failed to parse manifest: " .. tostring(err) 845 end 846 847 local ok, manifest = pcall(func) 848 if not ok then 849 return nil, "Failed to execute manifest: " .. tostring(manifest) 850 end 851 852 return manifest 853 end 854 855 --- Uninstall an application 856 -- @param appId Application ID 857 -- @return true on success, nil and error message on failure 858 function LAM.uninstall(appId) 859 if not appId then 860 return nil, "Application ID required" 861 end 862 863 local appPath = "/apps/" .. appId 864 865 if not _G.CRamdiskExists or not _G.CRamdiskExists(appPath) then 866 return nil, "Application not found: " .. appId 867 end 868 869 -- Get manifest to check for preUninstall script 870 local manifest = LAM.info(appId) 871 872 -- Run pre-uninstall script if specified 873 if manifest and manifest.preUninstall then 874 local preUninstallPath = appPath .. "/src/" .. manifest.preUninstall 875 if _G.CRamdiskExists(preUninstallPath) then 876 -- Could run the script here 877 print("Running pre-uninstall script...") 878 end 879 end 880 881 -- Remove all files and directories recursively 882 local function removeRecursive(path) 883 local contents = _G.CRamdiskList(path) 884 if contents then 885 for _, item in ipairs(contents) do 886 local fullPath = path .. "/" .. item.name 887 if item.type == "directory" then 888 removeRecursive(fullPath) 889 else 890 _G.CRamdiskRemove(fullPath) 891 end 892 end 893 end 894 _G.CRamdiskRemove(path) 895 end 896 897 removeRecursive(appPath) 898 899 return true 900 end 901 902 --- CLI interface for LAM 903 -- @param args Command line arguments 904 function LAM.cli(args) 905 if not args or #args == 0 then 906 print("LuajitOS Application Manager (LAM)") 907 print("") 908 print("Usage:") 909 print(" lam package <appId|path> [output] [--sign <dev>]") 910 print(" - Package an app into a .lam file") 911 print(" lam install <file.lam> [--force] - Install an app from a .lam file") 912 print(" lam uninstall <appId> - Uninstall an application") 913 print(" lam list - List installed applications") 914 print(" lam info <appId> - Show app information") 915 print(" lam new-developer <name> - Create a new developer keypair") 916 print(" lam list-developers - List all developers") 917 print("") 918 print("Examples:") 919 print(" lam package moonbrowser - Package installed app (unsigned)") 920 print(" lam package moonbrowser --sign me - Package and sign with 'me' key") 921 print(" lam package ~/Documents/myapp/ - Package from directory") 922 print(" lam new-developer myname - Create keypair in /keys/myname/") 923 print("") 924 return 925 end 926 927 local cmd = args[1] 928 929 if cmd == "new-developer" then 930 local devName = args[2] 931 932 if not devName then 933 print("Error: Developer name required") 934 print("Usage: lam new-developer <name>") 935 return 936 end 937 938 print("Creating developer keypair for: " .. devName) 939 local ok, result = LAM.newDeveloper(devName) 940 941 if ok then 942 print("Developer created: " .. result.name) 943 print(" Path: " .. result.path) 944 print(" Public key: " .. result.publicKey) 945 print("") 946 print("Keep your secret key safe! It's stored at:") 947 print(" " .. result.path .. "/secret.key") 948 else 949 print("Error: " .. tostring(result)) 950 end 951 952 elseif cmd == "list-developers" then 953 local developers = LAM.listDevelopers() 954 955 if #developers == 0 then 956 print("No developers registered") 957 print("Use 'lam new-developer <name>' to create a keypair") 958 else 959 print("Registered developers:") 960 for _, name in ipairs(developers) do 961 local pubKey, _ = LAM.loadDeveloperKeys(name) 962 print(" " .. name) 963 if pubKey then 964 print(" Public key: " .. pubKey:sub(1, 20) .. "...") 965 end 966 end 967 end 968 969 elseif cmd == "package" then 970 local appIdOrPath = args[2] 971 local outputPath = nil 972 local developer = nil 973 974 -- Parse remaining args for output path and --sign 975 local i = 3 976 while i <= #args do 977 if args[i] == "--sign" and args[i + 1] then 978 developer = args[i + 1] 979 i = i + 2 980 elseif not outputPath and not args[i]:match("^%-") then 981 outputPath = args[i] 982 i = i + 1 983 else 984 i = i + 1 985 end 986 end 987 988 if not appIdOrPath then 989 print("Error: Application ID or path required") 990 print("Usage: lam package <appId|path> [outputPath] [--sign <developer>]") 991 return 992 end 993 994 if developer then 995 print("Packaging " .. appIdOrPath .. " (signed by " .. developer .. ")...") 996 else 997 print("Packaging " .. appIdOrPath .. " (unsigned)...") 998 end 999 1000 local ok, result = LAM.package(appIdOrPath, outputPath, { developer = developer }) 1001 1002 if ok then 1003 print("Package created: " .. result.path) 1004 print(" Size: " .. result.size .. " bytes") 1005 print(" Files: " .. result.files) 1006 print(" Directories: " .. result.dirs) 1007 else 1008 print("Error: " .. tostring(result)) 1009 end 1010 1011 elseif cmd == "install" then 1012 local lamPath = args[2] 1013 local force = args[3] == "--force" or args.force 1014 1015 if not lamPath then 1016 print("Error: LAM file path required") 1017 print("Usage: lam install <file.lam> [--force]") 1018 return 1019 end 1020 1021 print("Installing from " .. lamPath .. "...") 1022 local ok, result = LAM.install(lamPath, { force = force }) 1023 1024 if ok then 1025 print("Installed: " .. result.appId) 1026 print(" Path: " .. result.path) 1027 print(" Files: " .. result.files) 1028 print(" Directories: " .. result.dirs) 1029 else 1030 print("Error: " .. tostring(result)) 1031 end 1032 1033 elseif cmd == "uninstall" then 1034 local appId = args[2] 1035 1036 if not appId then 1037 print("Error: Application ID required") 1038 print("Usage: lam uninstall <appId>") 1039 return 1040 end 1041 1042 -- Add prefix if needed 1043 if not appId:match("%.") then 1044 appId = "com.luajitos." .. appId 1045 end 1046 1047 print("Uninstalling " .. appId .. "...") 1048 local ok, err = LAM.uninstall(appId) 1049 1050 if ok then 1051 print("Uninstalled: " .. appId) 1052 else 1053 print("Error: " .. tostring(err)) 1054 end 1055 1056 elseif cmd == "list" then 1057 local apps = LAM.list() 1058 1059 if #apps == 0 then 1060 print("No applications installed") 1061 else 1062 print("Installed applications:") 1063 for _, appId in ipairs(apps) do 1064 local manifest = LAM.info(appId) 1065 local name = manifest and manifest.name or appId 1066 local version = manifest and manifest.version or "?" 1067 print(" " .. appId .. " (" .. name .. " v" .. version .. ")") 1068 end 1069 end 1070 1071 elseif cmd == "info" then 1072 local appId = args[2] 1073 1074 if not appId then 1075 print("Error: Application ID required") 1076 print("Usage: lam info <appId>") 1077 return 1078 end 1079 1080 -- Add prefix if needed 1081 if not appId:match("%.") then 1082 appId = "com.luajitos." .. appId 1083 end 1084 1085 local manifest, err = LAM.info(appId) 1086 1087 if manifest then 1088 print("Application: " .. appId) 1089 print(" Name: " .. (manifest.name or "?")) 1090 print(" Version: " .. (manifest.version or "?")) 1091 print(" Developer: " .. (manifest.developer or "?")) 1092 print(" Description: " .. (manifest.description or "?")) 1093 print(" Entry: " .. (manifest.entry or "init.lua")) 1094 print(" Type: " .. (manifest.type or "gui")) 1095 if manifest.permissions then 1096 print(" Permissions: " .. table.concat(manifest.permissions, ", ")) 1097 end 1098 else 1099 print("Error: " .. tostring(err)) 1100 end 1101 1102 else 1103 print("Unknown command: " .. cmd) 1104 print("Run 'lam' without arguments for usage") 1105 end 1106 end 1107 1108 return LAM