|
root / src / core / database.reef
database.reef Reef 1062 lines 33.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
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
/******************************************************************************
               __               ____                __
              / /   ___  ____ _/ __/_____________ _/ /__
             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/

   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com

   Project: Zygaena
  Filename: database.reef
   Authors: Chris Tusa <chris.tusa@leafscale.com>
   License: <see LICENSE file included with this source code>
Description: Installed package database management

******************************************************************************/

module core.database

import types
import io.file
import io.dir
import io.path
import core.str
import encoding.toml
import util.mtree
import sys.process

export
    // Check if a package is installed
    fn is_installed(name: string): bool

    // List all installed packages (returns count, fills names array)
    fn list_installed(names: [string], max_count: int): int

    // Get installed package details
    fn get_installed(name: string): types.InstalledPackage

    // Register a new installed package
    fn register_package(pkg: types.InstalledPackage): bool

    // Register package with file list copied from extract dir (more efficient)
    fn register_from_extract_dir(pkg: types.InstalledPackage, extract_dir: string): bool

    // Unregister (remove) a package from database
    fn unregister_package(name: string): bool

    // Get list of files owned by a package (returns count, fills files array)
    fn get_files(name: string, files: [string], max_count: int): int

    // Find which package owns a given file path
    fn find_owner(file_path: string): string

    // Find which package provides an alternative
    fn find_provider(alternative: string): string

    // Ensure database directories exist
    fn init_db(): bool

    // Scripts support
    fn get_scripts_dir(name: string): string
    fn store_scripts(name: string, extract_dir: string): bool
    fn has_stored_scripts(name: string): bool

    // Config files support
    fn store_config_files(name: string, extract_dir: string): bool
    fn get_config_files(name: string, config_files: [string], max_count: int): int

    // Manifest support
    fn get_manifest_path(name: string): string

    // Root-aware versions for alternate root installation
    fn init_db_rooted(root: string): bool
    fn register_from_extract_dir_rooted(pkg: types.InstalledPackage, extract_dir: string, root: string): bool
    fn store_scripts_rooted(name: string, extract_dir: string, root: string): bool
    fn store_config_files_rooted(name: string, extract_dir: string, root: string): bool
    fn is_installed_rooted(name: string, root: string): bool
    fn list_installed_rooted(names: [string], max_count: int, root: string): int
    fn get_installed_rooted(name: string, root: string): types.InstalledPackage
    fn get_files_rooted(name: string, files: [string], max_count: int, root: string): int
    fn find_owner_rooted(file_path: string, root: string): string
    fn unregister_package_rooted(name: string, root: string): bool
end export

// Get the package database directory for a specific package
fn pkg_db_dir(name: string): string
    return path.join_path(types.get_db_dir(), name)
end pkg_db_dir

// Get the pkg.toml path for a package
fn pkg_toml_path(name: string): string
    return path.join_path(pkg_db_dir(name), "pkg.toml")
end pkg_toml_path

// Get the files.txt path for a package
fn pkg_files_path(name: string): string
    return path.join_path(pkg_db_dir(name), "files.txt")
end pkg_files_path

// Get the manifest.mtree path for a package
fn get_manifest_path(name: string): string
    return path.join_path(pkg_db_dir(name), "manifest.mtree")
end get_manifest_path

// Initialize the database directory structure
fn init_db(): bool
    if not dir.dir_exists(types.get_db_dir())
        return dir.create_dir_all(types.get_db_dir())
    end if
    return true
end init_db

// Check if a package is installed by looking for its database entry
fn is_installed(name: string): bool
    let pkg_dir = pkg_db_dir(name)
    if not dir.dir_exists(pkg_dir)
        return false
    end if
    // Also verify pkg.toml exists
    return file.fileExists(pkg_toml_path(name))
end is_installed

// List all installed packages by scanning the database directory
fn list_installed(names: [string], max_count: int): int
    if not dir.dir_exists(types.get_db_dir())
        return 0
    end if

    mut entries: [string] = new [string](256)
    let entry_count = dir.list_dir(types.get_db_dir(), entries, 256)

    mut count = 0
    mut i = 0
    while i < entry_count and count < max_count
        let entry = entries[i]
        // Skip hidden files
        if str.length(entry) > 0 and entry[0] != '.'
            // Verify it's a valid package (has pkg.toml)
            let toml_path = pkg_toml_path(entry)
            if file.fileExists(toml_path)
                names[count] = entry
                count = count + 1
            end if
        end if
        i = i + 1
    end while

    return count
end list_installed

// Create an empty InstalledPackage
fn new_installed_package(): types.InstalledPackage
    return types.new_installed_package()
end new_installed_package

// Get details of an installed package
fn get_installed(name: string): types.InstalledPackage
    if not is_installed(name)
        return new_installed_package()
    end if

    // Read pkg.toml
    let toml_path = pkg_toml_path(name)
    let content = file.readFile(toml_path)
    if str.length(content) == 0
        return new_installed_package()
    end if

    // Parse TOML
    let keys = toml.toml_alloc_keys()
    let vals = toml.toml_alloc_values()
    let entry_count = toml.toml_parse(content, keys, vals)

    if entry_count == 0
        return new_installed_package()
    end if

    // Build package info
    mut pkg = new_installed_package()
    pkg.info.name = toml.toml_get(keys, vals, entry_count, "package.name")
    pkg.info.version = toml.toml_get(keys, vals, entry_count, "package.version")
    pkg.info.release = toml.toml_get_int(keys, vals, entry_count, "package.release")
    pkg.info.description = toml.toml_get(keys, vals, entry_count, "package.description")
    pkg.info.url = toml.toml_get(keys, vals, entry_count, "package.url")
    pkg.info.license = toml.toml_get(keys, vals, entry_count, "package.license")
    pkg.info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer")
    pkg.info.arch = toml.toml_get(keys, vals, entry_count, "package.arch")

    // Install metadata
    pkg.install_date = toml.toml_get(keys, vals, entry_count, "install.date")
    pkg.install_reason = toml.toml_get(keys, vals, entry_count, "install.reason")

    // Read files list
    let files_path = pkg_files_path(name)
    if file.fileExists(files_path)
        let files_content = file.readFile(files_path)
        if str.length(files_content) > 0
            let alloc_size = count_newlines(files_content) + 16
            mut files_arr: [string] = new [string](alloc_size)
            let files_count = str.split(files_content, '\n', files_arr, alloc_size)
            // Filter out empty lines
            mut valid_count = 0
            mut j = 0
            while j < files_count
                if str.length(files_arr[j]) > 0
                    valid_count = valid_count + 1
                end if
                j = j + 1
            end while
            pkg.files = files_arr
            pkg.files_count = valid_count
        end if
    end if

    return pkg
end get_installed

// Generate TOML content for a package
fn generate_pkg_toml(pkg: types.InstalledPackage): string
    mut content = "[package]\n"
    content = content + "name = \"" + pkg.info.name + "\"\n"
    content = content + "version = \"" + pkg.info.version + "\"\n"
    content = content + "release = " + int_to_str(pkg.info.release) + "\n"
    content = content + "description = \"" + pkg.info.description + "\"\n"
    content = content + "url = \"" + pkg.info.url + "\"\n"
    content = content + "license = \"" + pkg.info.license + "\"\n"
    content = content + "maintainer = \"" + pkg.info.maintainer + "\"\n"
    content = content + "arch = \"" + pkg.info.arch + "\"\n"
    content = content + "\n"
    content = content + "[install]\n"
    content = content + "date = \"" + pkg.install_date + "\"\n"
    content = content + "reason = \"" + pkg.install_reason + "\"\n"
    return content
end generate_pkg_toml

// Helper: convert int to string
fn int_to_str(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_str

// Register a newly installed package
fn register_package(pkg: types.InstalledPackage): bool
    // Ensure database directory exists
    if not init_db()
        return false
    end if

    // Create package directory
    let pkg_dir = pkg_db_dir(pkg.info.name)
    if not dir.dir_exists(pkg_dir)
        if not dir.create_dir_all(pkg_dir)
            return false
        end if
    end if

    // Write pkg.toml
    let toml_content = generate_pkg_toml(pkg)
    let toml_path = pkg_toml_path(pkg.info.name)
    if not file.writeFile(toml_path, toml_content)
        return false
    end if

    // Note: files.txt is written by register_from_extract_dir for performance
    return true
end register_package

// Count newline characters in a string to estimate line count
fn count_newlines(s: string): int
    let slen = str.length(s)
    mut count = 0
    mut i = 0
    while i < slen
        if s[i] == '\n'
            count = count + 1
        end if
        i = i + 1
    end while
    return count
end count_newlines

// Process a .FOOTPRINT file content: filter and normalize paths
// Uses str.join() for O(n) single-allocation string building
fn process_footprint(content: string): string
    let newline_count = count_newlines(content)
    let alloc_size = newline_count + 16

    mut lines: [string] = new [string](alloc_size)
    let line_count = str.split(content, '\n', lines, alloc_size)

    mut result_lines: [string] = new [string](alloc_size)
    mut result_count = 0

    mut i = 0
    while i < line_count
        mut line = str.trim_ws(lines[i])

        if str.length(line) == 0
            i = i + 1
            continue
        end if

        // Strip leading ./
        if str.starts_with(line, "./")
            line = str.substring(line, 2, str.length(line) - 2)
        end if

        // Skip metadata files
        if str.equals(line, ".PKGINFO") or str.equals(line, ".FOOTPRINT")
            i = i + 1
            continue
        end if
        if str.starts_with(line, ".SCRIPTS")
            i = i + 1
            continue
        end if
        if str.equals(line, ".CONFIG")
            i = i + 1
            continue
        end if

        // Skip directories (end with /)
        let len = str.length(line)
        if len > 0 and line[len - 1] == '/'
            i = i + 1
            continue
        end if

        result_lines[result_count] = line
        result_count = result_count + 1
        i = i + 1
    end while

    return str.join(result_lines, result_count, "\n")
end process_footprint

// Extract file paths from mtree manifest content for files.txt
// Parses manifest entries and returns a newline-joined list of relative paths
// (files and symlinks only, no directories, no metadata)
fn extract_paths_from_manifest(content: string): string
    mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192)
    let count = mtree.parse_manifest(content, entries, 8192)

    mut result_lines: [string] = new [string](8192)
    mut result_count = 0

    mut i = 0
    while i < count
        let etype = mtree.entry_type(entries[i])
        let epath = mtree.entry_path(entries[i])

        // Only include files and symlinks (matches legacy files.txt behavior)
        if str.equals(etype, "file") or str.equals(etype, "link")
            // Strip leading ./
            if str.starts_with(epath, "./")
                result_lines[result_count] = str.substring(epath, 2, str.length(epath) - 2)
            else
                result_lines[result_count] = epath
            end if
            result_count = result_count + 1
        end if

        i = i + 1
    end while

    return str.join(result_lines, result_count, "\n")
end extract_paths_from_manifest

// Register package and copy files list from extracted package directory
fn register_from_extract_dir(pkg: types.InstalledPackage, extract_dir: string): bool
    // Register the package metadata
    if not register_package(pkg)
        return false
    end if

    let files_path = pkg_files_path(pkg.info.name)

    // Try .MANIFEST first (v2 format), fall back to .FOOTPRINT (legacy)
    let manifest_path = path.join_path(extract_dir, ".MANIFEST")
    if file.fileExists(manifest_path)
        // Store the full manifest for future use (verify, manifest-based remove)
        let manifest_store = path.join_path(pkg_db_dir(pkg.info.name), "manifest.mtree")
        let manifest_content = file.readFile(manifest_path)
        file.writeFile(manifest_store, manifest_content)

        // Also generate files.txt for backward compatibility
        let result = extract_paths_from_manifest(manifest_content)
        return file.writeFile(files_path, result)
    end if

    // Legacy: .FOOTPRINT
    let footprint_path = path.join_path(extract_dir, ".FOOTPRINT")
    if not file.fileExists(footprint_path)
        file.writeFile(files_path, "")
        return true
    end if

    let content = file.readFile(footprint_path)
    if str.length(content) == 0
        file.writeFile(files_path, "")
        return true
    end if

    let result = process_footprint(content)
    return file.writeFile(files_path, result)
end register_from_extract_dir

// Helper to remove a file
fn remove_file(file_path: string): bool
    let cmd = "rm -f \"" + file_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 remove_file

// Unregister a package from the database
fn unregister_package(name: string): bool
    if not is_installed(name)
        return true  // Already not installed
    end if

    let pkg_dir = pkg_db_dir(name)

    // Remove pkg.toml
    let toml_path = pkg_toml_path(name)
    if file.fileExists(toml_path)
        if not remove_file(toml_path)
            return false
        end if
    end if

    // Remove files.txt
    let files_path = pkg_files_path(name)
    if file.fileExists(files_path)
        if not remove_file(files_path)
            return false
        end if
    end if

    // Remove manifest.mtree
    let manifest_path = get_manifest_path(name)
    if file.fileExists(manifest_path)
        remove_file(manifest_path)
    end if

    // Remove scripts directory if exists
    let scripts_dir = get_scripts_dir(name)
    if dir.dir_exists(scripts_dir)
        let cmd = "rm -rf \"" + scripts_dir + "\""
        let pid = process.process_spawn_shell(cmd)
        if pid > 0
            process.process_wait(pid)
        end if
    end if

    // Remove config.txt if exists
    let config_path = pkg_config_path(name)
    if file.fileExists(config_path)
        remove_file(config_path)
    end if

    // Remove the package directory
    return dir.remove_dir(pkg_dir)
end unregister_package

// Get list of files owned by a package
fn get_files(name: string, files: [string], max_count: int): int
    if not is_installed(name)
        return 0
    end if

    let files_path = pkg_files_path(name)
    if not file.fileExists(files_path)
        return 0
    end if

    let content = file.readFile(files_path)
    if str.length(content) == 0
        return 0
    end if

    // Split by newlines — size dynamically
    let alloc_size = count_newlines(content) + 16
    mut all_lines: [string] = new [string](alloc_size)
    let line_count = str.split(content, '\n', all_lines, alloc_size)

    // 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(all_lines[i])
        if str.length(line) > 0
            files[count] = line
            count = count + 1
        end if
        i = i + 1
    end while

    return count
end get_files

// Find which package owns a given file
fn find_owner(file_path: string): string
    // Normalize the path - strip leading "/" if present
    mut search_path = file_path
    if str.length(search_path) > 0 and search_path[0] == '/'
        search_path = str.substring(search_path, 1, str.length(search_path) - 1)
    end if

    // Get list of all installed packages
    mut packages: [string] = new [string](256)
    let pkg_count = list_installed(packages, 256)

    mut i = 0
    while i < pkg_count
        let pkg_name = packages[i]

        // Get files for this package
        mut files: [string] = new [string](4096)
        let file_count = get_files(pkg_name, files, 4096)

        // Search for matching file
        mut j = 0
        while j < file_count
            if files[j] == search_path
                return pkg_name
            end if
            j = j + 1
        end while

        i = i + 1
    end while

    return ""  // No owner found
end find_owner

// Find which package provides an alternative
fn find_provider(alternative: string): string
    // Get list of all installed packages
    mut packages: [string] = new [string](256)
    let pkg_count = list_installed(packages, 256)

    mut i = 0
    while i < pkg_count
        let pkg_name = packages[i]
        let pkg = get_installed(pkg_name)

        // Check alternatives array
        // Note: We'd need to parse the alternatives array from pkg.toml
        // For now, check if this package provides the alternative
        let toml_path = pkg_toml_path(pkg_name)
        let content = file.readFile(toml_path)
        if str.length(content) > 0
            let keys = toml.toml_alloc_keys()
            let vals = toml.toml_alloc_values()
            let entry_count = toml.toml_parse(content, keys, vals)

            if toml.toml_has_key(keys, vals, entry_count, "package.alternatives")
                let alts_val = toml.toml_get(keys, vals, entry_count, "package.alternatives")
                // Check if the alternative is in the array
                if str.contains(alts_val, alternative)
                    return pkg_name
                end if
            end if
        end if

        i = i + 1
    end while

    return ""  // No provider found
end find_provider

// Get the scripts directory path for a package in the database
fn get_scripts_dir(name: string): string
    return path.join_path(pkg_db_dir(name), "scripts")
end get_scripts_dir

// Check if a package has stored scripts in the database
fn has_stored_scripts(name: string): bool
    let scripts_path = get_scripts_dir(name)
    return dir.dir_exists(scripts_path)
end has_stored_scripts

// Store scripts from an extracted package to the database
fn store_scripts(name: string, extract_dir: string): bool
    let src_scripts = path.join_path(extract_dir, ".SCRIPTS")

    // Check if source has scripts
    if not dir.dir_exists(src_scripts)
        return true  // No scripts is not an error
    end if

    let dest_scripts = get_scripts_dir(name)

    // Create scripts directory in database
    if not dir.dir_exists(dest_scripts)
        if not dir.create_dir_all(dest_scripts)
            return false
        end if
    end if

    // Copy all script files
    let cmd = "cp -r \"" + src_scripts + "/\"* \"" + dest_scripts + "/\" 2>/dev/null"
    let pid = process.process_spawn_shell(cmd)
    if pid < 0
        return false
    end if
    let exit_code = process.process_wait(pid)
    // Don't fail if cp fails (might be empty directory)
    return true
end store_scripts

// Get the config.txt path for a package
fn pkg_config_path(name: string): string
    return path.join_path(pkg_db_dir(name), "config.txt")
end pkg_config_path

// Store config files list from an extracted package to the database
fn store_config_files(name: string, extract_dir: string): bool
    let src_config = path.join_path(extract_dir, ".CONFIG")

    // Check if source has config files list
    if not file.fileExists(src_config)
        return true  // No config files is not an error
    end if

    let dest_config = pkg_config_path(name)

    // Copy the config files list
    let content = file.readFile(src_config)
    if str.length(content) > 0
        return file.writeFile(dest_config, content)
    end if

    return true
end store_config_files

// Get list of config files for a package
fn get_config_files(name: string, config_files: [string], max_count: int): int
    let config_path = pkg_config_path(name)
    if not file.fileExists(config_path)
        return 0
    end if

    let content = file.readFile(config_path)
    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)

    // Filter out empty lines and comments
    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 and line[0] != '#'
            config_files[count] = line
            count = count + 1
        end if
        i = i + 1
    end while

    return count
end get_config_files

// ============================================================================
// Root-aware functions for alternate root installation
// ============================================================================

// Get the rooted database directory
fn get_db_dir_rooted(root: string): string
    if str.length(root) == 0
        return types.get_db_dir()
    end if
    return root + types.get_db_dir()
end get_db_dir_rooted

// Get the rooted package database directory
fn pkg_db_dir_rooted(name: string, root: string): string
    return path.join_path(get_db_dir_rooted(root), name)
end pkg_db_dir_rooted

// Get the rooted pkg.toml path
fn pkg_toml_path_rooted(name: string, root: string): string
    return path.join_path(pkg_db_dir_rooted(name, root), "pkg.toml")
end pkg_toml_path_rooted

// Get the rooted files.txt path
fn pkg_files_path_rooted(name: string, root: string): string
    return path.join_path(pkg_db_dir_rooted(name, root), "files.txt")
end pkg_files_path_rooted

// Initialize database with alternate root
fn init_db_rooted(root: string): bool
    let db_dir = get_db_dir_rooted(root)
    if not dir.dir_exists(db_dir)
        return dir.create_dir_all(db_dir)
    end if
    return true
end init_db_rooted

// Check if package is installed in alternate root
fn is_installed_rooted(name: string, root: string): bool
    let pkg_dir = pkg_db_dir_rooted(name, root)
    if not dir.dir_exists(pkg_dir)
        return false
    end if
    return file.fileExists(pkg_toml_path_rooted(name, root))
end is_installed_rooted

// Register package with alternate root
fn register_package_rooted(pkg: types.InstalledPackage, root: string): bool
    // Ensure database directory exists
    if not init_db_rooted(root)
        return false
    end if

    // Create package directory
    let pkg_dir = pkg_db_dir_rooted(pkg.info.name, root)
    if not dir.dir_exists(pkg_dir)
        if not dir.create_dir_all(pkg_dir)
            return false
        end if
    end if

    // Write pkg.toml
    let toml_content = generate_pkg_toml(pkg)
    let toml_path = pkg_toml_path_rooted(pkg.info.name, root)
    if not file.writeFile(toml_path, toml_content)
        return false
    end if

    return true
end register_package_rooted

// Register package from extract dir with alternate root
fn register_from_extract_dir_rooted(pkg: types.InstalledPackage, extract_dir: string, root: string): bool
    // Register the package metadata
    if not register_package_rooted(pkg, root)
        return false
    end if

    let files_path = pkg_files_path_rooted(pkg.info.name, root)

    // Try .MANIFEST first (v2 format), fall back to .FOOTPRINT (legacy)
    let manifest_path = path.join_path(extract_dir, ".MANIFEST")
    if file.fileExists(manifest_path)
        // Store the full manifest
        let manifest_store = path.join_path(pkg_db_dir_rooted(pkg.info.name, root), "manifest.mtree")
        let manifest_content = file.readFile(manifest_path)
        file.writeFile(manifest_store, manifest_content)

        // Also generate files.txt for backward compatibility
        let result = extract_paths_from_manifest(manifest_content)
        return file.writeFile(files_path, result)
    end if

    // Legacy: .FOOTPRINT
    let footprint_path = path.join_path(extract_dir, ".FOOTPRINT")
    if not file.fileExists(footprint_path)
        file.writeFile(files_path, "")
        return true
    end if

    let content = file.readFile(footprint_path)
    if str.length(content) == 0
        file.writeFile(files_path, "")
        return true
    end if

    let result = process_footprint(content)
    return file.writeFile(files_path, result)
end register_from_extract_dir_rooted

// Get scripts directory with alternate root
fn get_scripts_dir_rooted(name: string, root: string): string
    return path.join_path(pkg_db_dir_rooted(name, root), "scripts")
end get_scripts_dir_rooted

// Store scripts with alternate root
fn store_scripts_rooted(name: string, extract_dir: string, root: string): bool
    let src_scripts = path.join_path(extract_dir, ".SCRIPTS")

    if not dir.dir_exists(src_scripts)
        return true
    end if

    let dest_scripts = get_scripts_dir_rooted(name, root)

    if not dir.dir_exists(dest_scripts)
        if not dir.create_dir_all(dest_scripts)
            return false
        end if
    end if

    let cmd = "cp -r \"" + src_scripts + "/\"* \"" + dest_scripts + "/\" 2>/dev/null"
    let pid = process.process_spawn_shell(cmd)
    if pid < 0
        return false
    end if
    process.process_wait(pid)
    return true
end store_scripts_rooted

// Get config path with alternate root
fn pkg_config_path_rooted(name: string, root: string): string
    return path.join_path(pkg_db_dir_rooted(name, root), "config.txt")
end pkg_config_path_rooted

// Store config files with alternate root
fn store_config_files_rooted(name: string, extract_dir: string, root: string): bool
    let src_config = path.join_path(extract_dir, ".CONFIG")

    if not file.fileExists(src_config)
        return true
    end if

    let dest_config = pkg_config_path_rooted(name, root)

    let content = file.readFile(src_config)
    if str.length(content) > 0
        return file.writeFile(dest_config, content)
    end if

    return true
end store_config_files_rooted

// List all installed packages in alternate root
fn list_installed_rooted(names: [string], max_count: int, root: string): int
    let db_dir = get_db_dir_rooted(root)
    if not dir.dir_exists(db_dir)
        return 0
    end if

    mut entries: [string] = new [string](256)
    let entry_count = dir.list_dir(db_dir, entries, 256)

    mut count = 0
    mut i = 0
    while i < entry_count and count < max_count
        let entry = entries[i]
        // Skip hidden files
        if str.length(entry) > 0 and entry[0] != '.'
            // Verify it's a valid package (has pkg.toml)
            let toml_path = pkg_toml_path_rooted(entry, root)
            if file.fileExists(toml_path)
                names[count] = entry
                count = count + 1
            end if
        end if
        i = i + 1
    end while

    return count
end list_installed_rooted

// Get details of an installed package in alternate root
fn get_installed_rooted(name: string, root: string): types.InstalledPackage
    if not is_installed_rooted(name, root)
        return new_installed_package()
    end if

    // Read pkg.toml
    let toml_path = pkg_toml_path_rooted(name, root)
    let content = file.readFile(toml_path)
    if str.length(content) == 0
        return new_installed_package()
    end if

    // Parse TOML
    let keys = toml.toml_alloc_keys()
    let vals = toml.toml_alloc_values()
    let entry_count = toml.toml_parse(content, keys, vals)

    if entry_count == 0
        return new_installed_package()
    end if

    // Build package info
    mut pkg = new_installed_package()
    pkg.info.name = toml.toml_get(keys, vals, entry_count, "package.name")
    pkg.info.version = toml.toml_get(keys, vals, entry_count, "package.version")
    pkg.info.release = toml.toml_get_int(keys, vals, entry_count, "package.release")
    pkg.info.description = toml.toml_get(keys, vals, entry_count, "package.description")
    pkg.info.url = toml.toml_get(keys, vals, entry_count, "package.url")
    pkg.info.license = toml.toml_get(keys, vals, entry_count, "package.license")
    pkg.info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer")
    pkg.info.arch = toml.toml_get(keys, vals, entry_count, "package.arch")

    // Install metadata
    pkg.install_date = toml.toml_get(keys, vals, entry_count, "install.date")
    pkg.install_reason = toml.toml_get(keys, vals, entry_count, "install.reason")

    // Read files list
    let files_path = pkg_files_path_rooted(name, root)
    if file.fileExists(files_path)
        let files_content = file.readFile(files_path)
        if str.length(files_content) > 0
            let alloc_size = count_newlines(files_content) + 16
            mut files_arr: [string] = new [string](alloc_size)
            let files_count = str.split(files_content, '\n', files_arr, alloc_size)
            // Filter out empty lines
            mut valid_count = 0
            mut j = 0
            while j < files_count
                if str.length(files_arr[j]) > 0
                    valid_count = valid_count + 1
                end if
                j = j + 1
            end while
            pkg.files = files_arr
            pkg.files_count = valid_count
        end if
    end if

    return pkg
end get_installed_rooted

// Get list of files owned by a package in alternate root
fn get_files_rooted(name: string, files: [string], max_count: int, root: string): int
    if not is_installed_rooted(name, root)
        return 0
    end if

    let files_path = pkg_files_path_rooted(name, root)
    if not file.fileExists(files_path)
        return 0
    end if

    let content = file.readFile(files_path)
    if str.length(content) == 0
        return 0
    end if

    // Split by newlines — size dynamically
    let alloc_size = count_newlines(content) + 16
    mut all_lines: [string] = new [string](alloc_size)
    let line_count = str.split(content, '\n', all_lines, alloc_size)

    // 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(all_lines[i])
        if str.length(line) > 0
            files[count] = line
            count = count + 1
        end if
        i = i + 1
    end while

    return count
end get_files_rooted

// Find which package owns a given file in alternate root
fn find_owner_rooted(file_path: string, root: string): string
    // Normalize the path - strip leading "/" if present
    mut search_path = file_path
    if str.length(search_path) > 0 and search_path[0] == '/'
        search_path = str.substring(search_path, 1, str.length(search_path) - 1)
    end if

    // Get list of all installed packages in this root
    mut packages: [string] = new [string](256)
    let pkg_count = list_installed_rooted(packages, 256, root)

    mut i = 0
    while i < pkg_count
        let pkg_name = packages[i]

        // Get files for this package
        mut files: [string] = new [string](4096)
        let file_count = get_files_rooted(pkg_name, files, 4096, root)

        // Search for matching file
        mut j = 0
        while j < file_count
            if files[j] == search_path
                return pkg_name
            end if
            j = j + 1
        end while

        i = i + 1
    end while

    return ""  // No owner found
end find_owner_rooted

// Unregister a package from the database in alternate root
fn unregister_package_rooted(name: string, root: string): bool
    if not is_installed_rooted(name, root)
        return true  // Already not installed
    end if

    let pkg_dir = pkg_db_dir_rooted(name, root)

    // Remove pkg.toml
    let toml_path = pkg_toml_path_rooted(name, root)
    if file.fileExists(toml_path)
        if not remove_file(toml_path)
            return false
        end if
    end if

    // Remove files.txt
    let files_path = pkg_files_path_rooted(name, root)
    if file.fileExists(files_path)
        if not remove_file(files_path)
            return false
        end if
    end if

    // Remove scripts directory if exists
    let scripts_dir = get_scripts_dir_rooted(name, root)
    if dir.dir_exists(scripts_dir)
        let cmd = "rm -rf \"" + scripts_dir + "\""
        let pid = process.process_spawn_shell(cmd)
        if pid > 0
            process.process_wait(pid)
        end if
    end if

    // Remove config.txt if exists
    let config_path = pkg_config_path_rooted(name, root)
    if file.fileExists(config_path)
        remove_file(config_path)
    end if

    // Remove the package directory
    return dir.remove_dir(pkg_dir)
end unregister_package_rooted

end module