/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: portindex.reef Authors: Chris Tusa License: Description: Ports index for fast port lookups The ports index provides fast indexed access to port metadata without parsing individual package.toml files from disk. Index file: /var/lib/coral/db/ports.db Format: Base64-encoded MsgPack (same as packages.db, files.db) Rebuild triggers: coral sync, coral db rebuild, coral db init Manual: coral ports cache rebuild ******************************************************************************/ module core.portindex import types import io.file import io.dir import io.path import core.str import core.port import core.config import encoding.msgpack import encoding.base64 import core.result as res export type PortIndexEntry type PortsIndex // Rebuild the ports index from the ports tree fn rebuild_port_index(): bool // Check if ports.db exists fn port_index_exists(): bool // Load the ports index from disk fn load_port_index(): PortsIndex // Find a single port by name or category/name fn find_port(name: string): PortIndexEntry // Search ports by substring match on name + description fn search_ports(term: string, results: [PortIndexEntry], max_count: int): int // List all indexed ports fn list_all_ports(entries: [PortIndexEntry], max_count: int): int // Get count of indexed ports fn get_port_count(): int // Find ports that depend on a given package name fn find_reverse_deps(name: string, results: [PortIndexEntry], max_count: int): int end export // ============================================================================ // Types // ============================================================================ type PortIndexEntry = struct name: string category: string version: string release: int description: string port_dir: string runtime_deps: [string] runtime_dep_count: int found: bool end PortIndexEntry type PortsIndex = struct version: int generated: string port_count: int entries: [PortIndexEntry] loaded: bool end PortsIndex // ============================================================================ // Constants // ============================================================================ const PORT_INDEX_VERSION: int = 1 const MAX_PORTS: int = 4096 // ============================================================================ // Path helpers // ============================================================================ fn ports_db_path(): string return types.get_index_dir() + "/ports.db" end ports_db_path // Get current timestamp as string fn get_timestamp(): string return "2026-02-19" end get_timestamp // ============================================================================ // Empty constructors // ============================================================================ fn empty_port_index(): PortsIndex return PortsIndex{ version: 0, generated: "", port_count: 0, entries: new [PortIndexEntry](MAX_PORTS), loaded: false } end empty_port_index fn empty_port_entry(): PortIndexEntry return PortIndexEntry{ name: "", category: "", version: "", release: 0, description: "", port_dir: "", runtime_deps: new [string](64), runtime_dep_count: 0, found: false } end empty_port_entry // ============================================================================ // Buffer size estimation // ============================================================================ fn estimate_buffer_size(port_count: int): int // Header: ~100 bytes // Per port: ~300 bytes (name, category, version, description, port_dir, deps) return 100 + (port_count * 300) end estimate_buffer_size // ============================================================================ // MsgPack header size helpers (same as pkgdb.reef) // ============================================================================ fn map_header_size(buf: [int], offset: int): int let b = buf[offset] & 255 if (b & 0xF0) == 0x80 return 1 elif b == 0xDE return 3 elif b == 0xDF return 5 end if return 1 end map_header_size fn array_header_size(buf: [int], offset: int): int let b = buf[offset] & 255 if (b & 0xF0) == 0x90 return 1 elif b == 0xDC return 3 elif b == 0xDD return 5 end if return 1 end array_header_size // ============================================================================ // Index existence check // ============================================================================ fn port_index_exists(): bool return file.fileExists(ports_db_path()) end port_index_exists // ============================================================================ // Rebuild: scan all ports, serialize to ports.db // ============================================================================ fn rebuild_port_index(): bool // Ensure db directory exists let db_dir = types.get_index_dir() if not dir.dir_exists(db_dir) dir.create_dir_all(db_dir) end if // Get all categories dynamically mut cats: [string] = new [string](32) let cat_count = port.get_categories(cats, 32) // Collect all port entries mut entries: [PortIndexEntry] = new [PortIndexEntry](MAX_PORTS) mut total = 0 mut i = 0 while i < cat_count and total < MAX_PORTS let category = cats[i] mut port_names: [string] = new [string](256) let port_count = port.list_category(category, port_names, 256) mut j = 0 while j < port_count and total < MAX_PORTS let result = port.load(get_port_path(category, port_names[j])) if result.success let p = result.port mut entry = empty_port_entry() entry.name = p.info.name entry.category = p.category entry.version = p.info.version entry.release = p.info.release entry.description = p.info.description entry.port_dir = p.port_dir entry.found = true // Copy runtime deps mut k = 0 while k < p.deps.runtime_count and k < 64 entry.runtime_deps[k] = p.deps.runtime[k] k = k + 1 end while entry.runtime_dep_count = p.deps.runtime_count entries[total] = entry total = total + 1 end if j = j + 1 end while i = i + 1 end while // Serialize to MsgPack let buf_size = estimate_buffer_size(total) mut buf: [int] = new [int](buf_size) let buf_len = serialize_port_index(entries, total, buf, buf_size) if buf_len == 0 return false end if // Encode to base64 and write let encoded = base64.base64_encode_bytes(buf, buf_len) return res.is_ok(file.writeFile(ports_db_path(), encoded)) end rebuild_port_index // Build a port path from category and name fn get_port_path(category: string, name: string): string let cfg = config.load() let ports_dir = config.get_ports_dir(cfg) return path.join_path(ports_dir, path.join_path(category, name)) end get_port_path // ============================================================================ // Serialization // ============================================================================ fn serialize_port_index(entries: [PortIndexEntry], count: int, buf: [int], max_size: int): int mut pos = 0 // Pack map header (4 keys: version, generated, port_count, ports) pos = pos + msgpack.msgpack_pack_map_header(4, buf, pos) // version pos = pos + msgpack.msgpack_pack_string("version", buf, pos) pos = pos + msgpack.msgpack_pack_int(PORT_INDEX_VERSION, buf, pos) // generated pos = pos + msgpack.msgpack_pack_string("generated", buf, pos) pos = pos + msgpack.msgpack_pack_string(get_timestamp(), buf, pos) // port_count pos = pos + msgpack.msgpack_pack_string("port_count", buf, pos) pos = pos + msgpack.msgpack_pack_int(count, buf, pos) // ports array pos = pos + msgpack.msgpack_pack_string("ports", buf, pos) pos = pos + msgpack.msgpack_pack_array_header(count, buf, pos) mut i = 0 while i < count and pos < max_size - 2000 let entry = entries[i] // Each port is a map with 7 keys pos = pos + msgpack.msgpack_pack_map_header(7, buf, pos) // name pos = pos + msgpack.msgpack_pack_string("name", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.name, buf, pos) // category pos = pos + msgpack.msgpack_pack_string("category", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.category, buf, pos) // version pos = pos + msgpack.msgpack_pack_string("version", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.version, buf, pos) // release pos = pos + msgpack.msgpack_pack_string("release", buf, pos) pos = pos + msgpack.msgpack_pack_int(entry.release, buf, pos) // description pos = pos + msgpack.msgpack_pack_string("description", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.description, buf, pos) // port_dir pos = pos + msgpack.msgpack_pack_string("port_dir", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.port_dir, buf, pos) // runtime_deps (array of strings) pos = pos + msgpack.msgpack_pack_string("runtime_deps", buf, pos) pos = pos + msgpack.msgpack_pack_array_header(entry.runtime_dep_count, buf, pos) mut d = 0 while d < entry.runtime_dep_count pos = pos + msgpack.msgpack_pack_string(entry.runtime_deps[d], buf, pos) d = d + 1 end while i = i + 1 end while return pos end serialize_port_index // ============================================================================ // Deserialization // ============================================================================ fn load_port_index(): PortsIndex mut index = empty_port_index() let db_path = ports_db_path() if not file.fileExists(db_path) return index end if let encoded = res.unwrap_or(file.readFile(db_path), "") if str.length(encoded) == 0 return index end if let buf_size = str.length(encoded) mut buf: [int] = new [int](buf_size) let buf_len = res.unwrap_or(base64.base64_decode_bytes(encoded, buf, buf_size), 0) if buf_len == 0 return index end if mut offset = 0 // Expect map header if not msgpack.msgpack_is_map(buf, offset) return index end if let map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) mut entry_idx = 0 while entry_idx < map_len and offset < buf_len let key = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) if str.equals(key, "version") index.version = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "generated") index.generated = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "port_count") index.port_count = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "ports") // Parse ports array let arr_len = res.unwrap_or(msgpack.msgpack_unpack_array_len(buf, offset), 0) offset = offset + array_header_size(buf, offset) mut port_idx = 0 while port_idx < arr_len and port_idx < MAX_PORTS and offset < buf_len let port_map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) mut entry = empty_port_entry() entry.found = true mut port_field = 0 while port_field < port_map_len and offset < buf_len let field_key = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) if str.equals(field_key, "name") entry.name = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "category") entry.category = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "version") entry.version = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "release") entry.release = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "description") entry.description = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "port_dir") entry.port_dir = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "runtime_deps") // Parse deps array let deps_len = res.unwrap_or(msgpack.msgpack_unpack_array_len(buf, offset), 0) offset = offset + array_header_size(buf, offset) mut dep_idx = 0 while dep_idx < deps_len and dep_idx < 64 and offset < buf_len entry.runtime_deps[dep_idx] = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) dep_idx = dep_idx + 1 end while entry.runtime_dep_count = dep_idx else offset = offset + msgpack.msgpack_value_size(buf, offset) end if port_field = port_field + 1 end while index.entries[port_idx] = entry port_idx = port_idx + 1 end while else offset = offset + msgpack.msgpack_value_size(buf, offset) end if entry_idx = entry_idx + 1 end while index.loaded = true return index end load_port_index // ============================================================================ // Query functions // ============================================================================ // Find a single port by name or category/name fn find_port(name: string): PortIndexEntry let index = load_port_index() if not index.loaded return empty_port_entry() end if // Check if name contains a slash (category/name format) let slash_idx = str.index_of(name, "/") mut i = 0 while i < index.port_count let entry = index.entries[i] if slash_idx >= 0 // Match category/name format let full_name = entry.category + "/" + entry.name // Also check directory-based name (category/dirname) let dir_name = path.basename(entry.port_dir) let full_dir_name = entry.category + "/" + dir_name if str.equals(full_name, name) or str.equals(full_dir_name, name) return entry end if else // Match just the name if str.equals(entry.name, name) return entry end if end if i = i + 1 end while return empty_port_entry() end find_port // Search ports by substring match on name + description fn search_ports(term: string, results: [PortIndexEntry], max_count: int): int let index = load_port_index() if not index.loaded return 0 end if let lower_term = str.to_lower(term) mut count = 0 mut i = 0 while i < index.port_count and count < max_count let entry = index.entries[i] let lower_name = str.to_lower(entry.name) let lower_desc = str.to_lower(entry.description) if str.contains(lower_name, lower_term) or str.contains(lower_desc, lower_term) results[count] = entry count = count + 1 end if i = i + 1 end while return count end search_ports // List all indexed ports fn list_all_ports(entries: [PortIndexEntry], max_count: int): int let index = load_port_index() if not index.loaded return 0 end if mut count = 0 while count < index.port_count and count < max_count entries[count] = index.entries[count] count = count + 1 end while return count end list_all_ports // Get count of indexed ports fn get_port_count(): int let index = load_port_index() if index.loaded return index.port_count end if return 0 end get_port_count // Find ports that have `name` in their runtime dependencies fn find_reverse_deps(name: string, results: [PortIndexEntry], max_count: int): int let index = load_port_index() if not index.loaded return 0 end if mut count = 0 mut i = 0 while i < index.port_count and count < max_count let entry = index.entries[i] mut j = 0 while j < entry.runtime_dep_count if str.equals(entry.runtime_deps[j], name) results[count] = entry count = count + 1 break end if j = j + 1 end while i = i + 1 end while return count end find_reverse_deps end module