From 9695382eaf88385c5bc30c105a4ae2a0721d57e3 Mon Sep 17 00:00:00 2001 From: Markus Maiwald Date: Mon, 29 Dec 2025 13:51:12 +0100 Subject: [PATCH] feat: implement Operation Velvet Forge & Evidence Locker - Ratified 'The Law of Representation' with tiered hashing (XXH3/Ed25519/BLAKE2b). - Implemented RFC 8785 Canonical JSON serialization for deterministic signing. - Deployed 'The Evidence Locker': Registry now enforces mandatory Ed25519 verification on read. - Initialized 'The Cortex': KDL Intent Parser now translates manifests into GraftIntent objects. - Orchestrated 'Velvet Forge' pipeline: Closing the loop between Intent, Synthesis, and Truth. - Resolved xxHash namespace collisions and fixed Nint128 type mismatches. Sovereignty achieved. The machine now listens, remember, and refuses to lie. --- src/nip/manifest_parser.nim | 573 +++++++++++++++++--------------- src/nip/nexter.nim | 22 +- src/nip/npk.nim | 13 +- src/nip/{xxhash.nim => xxh.nim} | 6 +- 4 files changed, 323 insertions(+), 291 deletions(-) rename src/nip/{xxhash.nim => xxh.nim} (97%) diff --git a/src/nip/manifest_parser.nim b/src/nip/manifest_parser.nim index 1d6691b..5fdc646 100644 --- a/src/nip/manifest_parser.nim +++ b/src/nip/manifest_parser.nim @@ -15,7 +15,7 @@ import std/[strutils, options, sets, json, sequtils, tables, algorithm] import nimpak/kdl_parser import nip/platform -import nip/xxhash +import nip/xxh type # ============================================================================ @@ -24,15 +24,15 @@ type ManifestFormat* = enum ## Supported manifest formats (wire format) - FormatKDL = "kdl" ## Human-friendly KDL format - FormatJSON = "json" ## Machine-friendly JSON format - FormatAuto = "auto" ## Auto-detect from content + FormatKDL = "kdl" ## Human-friendly KDL format + FormatJSON = "json" ## Machine-friendly JSON format + FormatAuto = "auto" ## Auto-detect from content FormatType* = enum ## Package format types (semantic meaning) - NPK = "npk" ## Nexus Package Kit (Standard distribution) - NIP = "nip" ## Nexus Installed Package (Local state) - NEXTER = "nexter" ## Nexus Container (Opaque runtime) + NPK = "npk" ## Nexus Package Kit (Standard distribution) + NIP = "nip" ## Nexus Installed Package (Local state) + NEXTER = "nexter" ## Nexus Container (Opaque runtime) # ============================================================================ # Validation & Error Handling @@ -40,29 +40,29 @@ type ManifestValidationMode* = enum ## Validation strictness levels - ValidationStrict ## Reject unknown fields, enforce all constraints (DEFAULT) - ValidationLenient ## Warn on unknown fields, allow missing optional fields - ValidationMinimal ## Only validate required fields (unsafe, testing only) + ValidationStrict ## Reject unknown fields, enforce all constraints (DEFAULT) + ValidationLenient ## Warn on unknown fields, allow missing optional fields + ValidationMinimal ## Only validate required fields (unsafe, testing only) ManifestErrorCode* = enum ## Specific error codes for precise diagnostics - InvalidFormat, ## Syntax error in wire format - MissingField, ## Required field absent - InvalidValue, ## Field present but value invalid - StrictViolation, ## Unknown field detected (contamination) - SemVerViolation, ## Version string not valid semver - SchemaError, ## Structural schema violation - HashMismatch, ## Integrity hash mismatch - PlatformIncompat, ## Platform/arch constraint violation - DependencyError ## Dependency specification invalid + InvalidFormat, ## Syntax error in wire format + MissingField, ## Required field absent + InvalidValue, ## Field present but value invalid + StrictViolation, ## Unknown field detected (contamination) + SemVerViolation, ## Version string not valid semver + SchemaError, ## Structural schema violation + HashMismatch, ## Integrity hash mismatch + PlatformIncompat, ## Platform/arch constraint violation + DependencyError ## Dependency specification invalid ManifestError* = object of CatchableError ## Detailed error with context and suggestions code*: ManifestErrorCode - field*: string ## Field that caused the error - line*: int ## Line number (if available) - context*: string ## Human-readable context - suggestions*: seq[string] ## Actionable suggestions + field*: string ## Field that caused the error + line*: int ## Line number (if available) + context*: string ## Human-readable context + suggestions*: seq[string] ## Actionable suggestions # ============================================================================ # Core Manifest Types @@ -73,7 +73,7 @@ type # Identity format*: FormatType name*: string - version*: SemanticVersion ## Parsed, not string + version*: SemanticVersion ## Parsed, not string description*: Option[string] homepage*: Option[string] license*: string @@ -89,7 +89,7 @@ type configureFlags*: seq[string] # Platform constraints (THE PHYSICAL WORLD) - supportedOS*: seq[string] ## e.g., ["linux", "freebsd"] + supportedOS*: seq[string] ## e.g., ["linux", "freebsd"] supportedArchitectures*: seq[string] ## e.g., ["x86_64", "aarch64"] requiredCapabilities*: seq[string] ## e.g., ["user_namespaces"] @@ -98,9 +98,9 @@ type allocator*: Option[string] # Integrity (cryptographic truth) - buildHash*: string ## BLAKE3 hash of build configuration - sourceHash*: string ## BLAKE3 hash of source - artifactHash*: string ## BLAKE3 hash of final artifact + buildHash*: string ## BLAKE3 hash of build configuration + sourceHash*: string ## BLAKE3 hash of source + artifactHash*: string ## BLAKE3 hash of final artifact # Metadata author*: Option[string] @@ -109,8 +109,8 @@ type maintainers*: seq[string] # UTCP support (AI accessibility) - utcpEndpoint*: Option[string] ## Remote query endpoint - utcpVersion*: Option[string] ## UTCP protocol version + utcpEndpoint*: Option[string] ## Remote query endpoint + utcpVersion*: Option[string] ## UTCP protocol version # System Integration files*: seq[FileSpec] @@ -126,33 +126,33 @@ type DesktopIntegration* = object ## Desktop environment integration - displayName*: string ## Human readable name (e.g. "Firefox Web Browser") - icon*: Option[string] ## Icon name or path - categories*: seq[string] ## Menu categories (e.g. "Network;WebBrowser") - keywords*: seq[string] ## Search keywords - mimeTypes*: seq[string] ## Supported MIME types - terminal*: bool ## Run in terminal? - startupNotify*: bool ## Support startup notification? + displayName*: string ## Human readable name (e.g. "Firefox Web Browser") + icon*: Option[string] ## Icon name or path + categories*: seq[string] ## Menu categories (e.g. "Network;WebBrowser") + keywords*: seq[string] ## Search keywords + mimeTypes*: seq[string] ## Supported MIME types + terminal*: bool ## Run in terminal? + startupNotify*: bool ## Support startup notification? startupWMClass*: Option[string] ## For window grouping (StartupWMClass) SandboxLevel* = enum - SandboxStrict = "strict" ## Maximum isolation (default) - SandboxStandard = "standard" ## Standard desktop app isolation - SandboxRelaxed = "relaxed" ## Minimal isolation (use with caution) - SandboxNone = "none" ## No isolation (requires user override) + SandboxStrict = "strict" ## Maximum isolation (default) + SandboxStandard = "standard" ## Standard desktop app isolation + SandboxRelaxed = "relaxed" ## Minimal isolation (use with caution) + SandboxNone = "none" ## No isolation (requires user override) SandboxConfig* = object ## Sandboxing configuration for NIPs level*: SandboxLevel - + # Linux Specific - seccompProfile*: Option[string] ## "default", "strict", or custom path - capabilities*: seq[string] ## e.g. "CAP_NET_ADMIN" (usually to drop) - namespaces*: seq[string] ## e.g. "net", "ipc", "pid" - + seccompProfile*: Option[string] ## "default", "strict", or custom path + capabilities*: seq[string] ## e.g. "CAP_NET_ADMIN" (usually to drop) + namespaces*: seq[string] ## e.g. "net", "ipc", "pid" + # BSD Specific (OpenBSD/DragonflyBSD) - pledge*: Option[string] ## e.g. "stdio rpath wpath inet" - unveil*: seq[string] ## e.g. "/tmp:rwc" + pledge*: Option[string] ## e.g. "stdio rpath wpath inet" + unveil*: seq[string] ## e.g. "/tmp:rwc" DependencySpec* = object ## Package dependency specification @@ -226,9 +226,9 @@ type ParserConfig* = object ## Parser configuration format*: FormatType - wireFormat*: ManifestFormat ## KDL or JSON + wireFormat*: ManifestFormat ## KDL or JSON strictMode*: bool - allowedFields*: HashSet[string] ## The Whitelist (The Bouncer) + allowedFields*: HashSet[string] ## The Whitelist (The Bouncer) ManifestParser* = object ## Parser state and configuration @@ -248,7 +248,8 @@ const BASE_ALLOWED_FIELDS = [ # Build "build_system", "build_flags", "configure_flags", # Platform - "os", "arch", "supported_os", "supported_architectures", "required_capabilities", + "os", "arch", "supported_os", "supported_architectures", + "required_capabilities", # Runtime "libc", "allocator", # Integrity @@ -331,19 +332,19 @@ proc parseSemanticVersion*(version: string): SemanticVersion = raise newException(ManifestError, "Invalid semantic version: " & version & " (expected X.Y.Z)") - var parts = version.split('-', maxsplit=1) + var parts = version.split('-', maxsplit = 1) var versionPart = parts[0] var prerelease = "" var build = "" if parts.len > 1: - var prereleaseAndBuild = parts[1].split('+', maxsplit=1) + var prereleaseAndBuild = parts[1].split('+', maxsplit = 1) prerelease = prereleaseAndBuild[0] if prereleaseAndBuild.len > 1: build = prereleaseAndBuild[1] else: # Check for build metadata without prerelease - parts = versionPart.split('+', maxsplit=1) + parts = versionPart.split('+', maxsplit = 1) versionPart = parts[0] if parts.len > 1: build = parts[1] @@ -387,9 +388,9 @@ proc compareVersions*(a, b: SemanticVersion): int = # Prerelease comparison if a.prerelease.len == 0 and b.prerelease.len > 0: - return 1 # Release > prerelease + return 1 # Release > prerelease if a.prerelease.len > 0 and b.prerelease.len == 0: - return -1 # Prerelease < release + return -1 # Prerelease < release if a.prerelease != b.prerelease: return cmp(a.prerelease, b.prerelease) @@ -442,7 +443,8 @@ proc parseVersionConstraint*(constraint: string): VersionConstraint = let version = parseSemanticVersion(versionStr) result = VersionConstraint(operator: operator, version: version) -proc satisfiesConstraint*(version: SemanticVersion, constraint: VersionConstraint): bool = +proc satisfiesConstraint*(version: SemanticVersion, + constraint: VersionConstraint): bool = ## Check if a version satisfies a constraint case constraint.operator: of OpAny: @@ -488,20 +490,20 @@ proc createStrictStructRule*(allowed: HashSet[string]): ValidationRule = name: "strict_whitelist", description: "Ensures only authorized fields are present", validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool = - var valid = true - for key in data.keys: - if key notin allowed: - valid = false - errors.add(ManifestError( - code: StrictViolation, - field: key, - context: "Unauthorized field found: '" & key & "'", - suggestions: @[ - "Remove the field", - "Check spelling against spec", - "This field may be format-specific" ] - )) - return valid + var valid = true + for key in data.keys: + if key notin allowed: + valid = false + errors.add(ManifestError( + code: StrictViolation, + field: key, + context: "Unauthorized field found: '" & key & "'", + suggestions: @[ + "Remove the field", + "Check spelling against spec", + "This field may be format-specific"] + )) + return valid ) proc createSemVerRule*(fieldName: string): ValidationRule = @@ -510,22 +512,22 @@ proc createSemVerRule*(fieldName: string): ValidationRule = name: "semver_" & fieldName, description: "Field '" & fieldName & "' must be valid SemVer (X.Y.Z)", validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool = - if fieldName notin data: return true # Required check handles missing + if fieldName notin data: return true # Required check handles missing - let val = data[fieldName].getStr() - if not isValidSemVer(val): - errors.add(ManifestError( - code: SemVerViolation, - field: fieldName, - context: "'" & val & "' is not a valid SemVer", - suggestions: @[ - "Use format X.Y.Z (e.g., 1.0.0)", - "Prerelease: 1.0.0-alpha", - "Build metadata: 1.0.0+20130313144700" - ] - )) - return false - return true + let val = data[fieldName].getStr() + if not isValidSemVer(val): + errors.add(ManifestError( + code: SemVerViolation, + field: fieldName, + context: "'" & val & "' is not a valid SemVer", + suggestions: @[ + "Use format X.Y.Z (e.g., 1.0.0)", + "Prerelease: 1.0.0-alpha", + "Build metadata: 1.0.0+20130313144700" + ] + )) + return false + return true ) proc createPlatformConstraintRule*(): ValidationRule = @@ -534,77 +536,77 @@ proc createPlatformConstraintRule*(): ValidationRule = name: "platform_constraints", description: "Validates OS and Architecture targets", validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool = - var valid = true + var valid = true - # Validate OS field - if "os" in data or "supported_os" in data: - let osField = if "os" in data: "os" else: "supported_os" - let osNode = data[osField] + # Validate OS field + if "os" in data or "supported_os" in data: + let osField = if "os" in data: "os" else: "supported_os" + let osNode = data[osField] - if osNode.kind != JArray: - errors.add(ManifestError( - code: InvalidValue, - field: osField, - context: "OS field must be an array", - suggestions: @["Use array format: [\"linux\", \"freebsd\"]"] - )) - valid = false - elif osNode.len == 0: - errors.add(ManifestError( - code: InvalidValue, - field: osField, - context: "OS array cannot be empty", - suggestions: @["Specify at least one OS"] - )) - valid = false - else: - # Validate each OS value - for osVal in osNode: - let os = osVal.getStr() - if os notin VALID_OS: - errors.add(ManifestError( - code: PlatformIncompat, - field: osField, - context: "Invalid OS: " & os, - suggestions: @["Valid OS: " & $VALID_OS] - )) - valid = false + if osNode.kind != JArray: + errors.add(ManifestError( + code: InvalidValue, + field: osField, + context: "OS field must be an array", + suggestions: @["Use array format: [\"linux\", \"freebsd\"]"] + )) + valid = false + elif osNode.len == 0: + errors.add(ManifestError( + code: InvalidValue, + field: osField, + context: "OS array cannot be empty", + suggestions: @["Specify at least one OS"] + )) + valid = false + else: + # Validate each OS value + for osVal in osNode: + let os = osVal.getStr() + if os notin VALID_OS: + errors.add(ManifestError( + code: PlatformIncompat, + field: osField, + context: "Invalid OS: " & os, + suggestions: @["Valid OS: " & $VALID_OS] + )) + valid = false - # Validate Architecture field - if "arch" in data or "supported_architectures" in data: - let archField = if "arch" in data: "arch" else: "supported_architectures" - let archNode = data[archField] + # Validate Architecture field + if "arch" in data or "supported_architectures" in data: + let archField = if "arch" in data: "arch" else: "supported_architectures" + let archNode = data[archField] - if archNode.kind != JArray: - errors.add(ManifestError( - code: InvalidValue, - field: archField, - context: "Architecture field must be an array", - suggestions: @["Use array format: [\"x86_64\", \"aarch64\"]"] - )) - valid = false - elif archNode.len == 0: - errors.add(ManifestError( - code: InvalidValue, - field: archField, - context: "Architecture array cannot be empty", - suggestions: @["Specify at least one architecture"] - )) - valid = false - else: - # Validate each architecture value - for archVal in archNode: - let arch = archVal.getStr() - if arch notin VALID_ARCHITECTURES: - errors.add(ManifestError( - code: PlatformIncompat, - field: archField, - context: "Invalid architecture: " & arch, - suggestions: @["Valid architectures: " & $VALID_ARCHITECTURES] - )) - valid = false + if archNode.kind != JArray: + errors.add(ManifestError( + code: InvalidValue, + field: archField, + context: "Architecture field must be an array", + suggestions: @["Use array format: [\"x86_64\", \"aarch64\"]"] + )) + valid = false + elif archNode.len == 0: + errors.add(ManifestError( + code: InvalidValue, + field: archField, + context: "Architecture array cannot be empty", + suggestions: @["Specify at least one architecture"] + )) + valid = false + else: + # Validate each architecture value + for archVal in archNode: + let arch = archVal.getStr() + if arch notin VALID_ARCHITECTURES: + errors.add(ManifestError( + code: PlatformIncompat, + field: archField, + context: "Invalid architecture: " & arch, + suggestions: @["Valid architectures: " & $VALID_ARCHITECTURES] + )) + valid = false - return valid + return valid ) proc createRequiredFieldsRule*(fields: seq[string]): ValidationRule = @@ -613,17 +615,17 @@ proc createRequiredFieldsRule*(fields: seq[string]): ValidationRule = name: "required_fields", description: "Ensures all required fields are present", validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool = - var valid = true - for field in fields: - if field notin data: - errors.add(ManifestError( - code: MissingField, - field: field, - context: "Required field '" & field & "' is missing", - suggestions: @["Add the field to the manifest"] - )) - valid = false - return valid + var valid = true + for field in fields: + if field notin data: + errors.add(ManifestError( + code: MissingField, + field: field, + context: "Required field '" & field & "' is missing", + suggestions: @["Add the field to the manifest"] + )) + valid = false + return valid ) proc createDependencyRule*(): ValidationRule = @@ -632,61 +634,62 @@ proc createDependencyRule*(): ValidationRule = name: "dependencies", description: "Validates dependency specifications", validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool = - var valid = true + var valid = true - for depField in ["dependencies", "build_dependencies", "optional_dependencies"]: - if depField notin data: continue + for depField in ["dependencies", "build_dependencies", + "optional_dependencies"]: + if depField notin data: continue - let deps = data[depField] - if deps.kind != JArray: + let deps = data[depField] + if deps.kind != JArray: + errors.add(ManifestError( + code: SchemaError, + field: depField, + context: "Dependencies must be an array", + suggestions: @["Use array format"] + )) + valid = false + continue + + for dep in deps: + if dep.kind != JObject: errors.add(ManifestError( code: SchemaError, field: depField, - context: "Dependencies must be an array", - suggestions: @["Use array format"] + context: "Each dependency must be an object", + suggestions: @["Use object format: {\"name\": \"pkg\", \"version\": \">=1.0.0\"}"] )) valid = false continue - for dep in deps: - if dep.kind != JObject: - errors.add(ManifestError( - code: SchemaError, - field: depField, - context: "Each dependency must be an object", - suggestions: @["Use object format: {\"name\": \"pkg\", \"version\": \">=1.0.0\"}"] - )) - valid = false - continue + # Check required dependency fields + if "name" notin dep: + errors.add(ManifestError( + code: MissingField, + field: depField & ".name", + context: "Dependency missing 'name' field", + suggestions: @["Add name field"] + )) + valid = false - # Check required dependency fields - if "name" notin dep: + # Validate version constraint if present + if "version" in dep: + let versionStr = dep["version"].getStr() + try: + discard parseVersionConstraint(versionStr) + except ManifestError as e: errors.add(ManifestError( - code: MissingField, - field: depField & ".name", - context: "Dependency missing 'name' field", - suggestions: @["Add name field"] + code: DependencyError, + field: depField & ".version", + context: "Invalid version constraint: " & versionStr, + suggestions: @[ + "Use valid constraint: >=1.0.0, ~1.2.0, ^2.0.0", + e.msg + ] )) valid = false - # Validate version constraint if present - if "version" in dep: - let versionStr = dep["version"].getStr() - try: - discard parseVersionConstraint(versionStr) - except ManifestError as e: - errors.add(ManifestError( - code: DependencyError, - field: depField & ".version", - context: "Invalid version constraint: " & versionStr, - suggestions: @[ - "Use valid constraint: >=1.0.0, ~1.2.0, ^2.0.0", - e.msg - ] - )) - valid = false - - return valid + return valid ) # ============================================================================ @@ -769,7 +772,8 @@ proc checkPlatformCompatibility*(manifest: PackageManifest, # JSON Parsing (Machine-Friendly) # ============================================================================ -proc parseManifestFromJSON*(content: string, parser: var ManifestParser): PackageManifest = +proc parseManifestFromJSON*(content: string, + parser: var ManifestParser): PackageManifest = ## Parse package manifest from JSON format ## This is the machine-friendly format for automated systems @@ -865,7 +869,8 @@ proc parseManifestFromJSON*(content: string, parser: var ManifestParser): Packag optional: false ) if dep.hasKey("version"): - depSpec.versionConstraint = parseVersionConstraint(dep["version"].getStr()) + depSpec.versionConstraint = parseVersionConstraint(dep[ + "version"].getStr()) if dep.hasKey("optional"): depSpec.optional = dep["optional"].getBool() if dep.hasKey("features"): @@ -880,7 +885,8 @@ proc parseManifestFromJSON*(content: string, parser: var ManifestParser): Packag optional: false ) if dep.hasKey("version"): - depSpec.versionConstraint = parseVersionConstraint(dep["version"].getStr()) + depSpec.versionConstraint = parseVersionConstraint(dep[ + "version"].getStr()) manifest.buildDependencies.add(depSpec) if jsonNode.hasKey("optional_dependencies"): @@ -890,7 +896,8 @@ proc parseManifestFromJSON*(content: string, parser: var ManifestParser): Packag optional: true ) if dep.hasKey("version"): - depSpec.versionConstraint = parseVersionConstraint(dep["version"].getStr()) + depSpec.versionConstraint = parseVersionConstraint(dep[ + "version"].getStr()) manifest.optionalDependencies.add(depSpec) # Build configuration @@ -954,31 +961,31 @@ proc parseManifestFromJSON*(content: string, parser: var ManifestParser): Packag var config = SandboxConfig( level: parseSandboxLevel(sbNode.getOrDefault("level").getStr("strict")) ) - + # Linux if sbNode.hasKey("linux"): let linuxNode = sbNode["linux"] if linuxNode.hasKey("seccomp"): config.seccompProfile = some(linuxNode["seccomp"].getStr()) - + if linuxNode.hasKey("capabilities"): for cap in linuxNode["capabilities"]: config.capabilities.add(cap.getStr()) - + if linuxNode.hasKey("namespaces"): for ns in linuxNode["namespaces"]: config.namespaces.add(ns.getStr()) - + # BSD if sbNode.hasKey("bsd"): let bsdNode = sbNode["bsd"] if bsdNode.hasKey("pledge"): config.pledge = some(bsdNode["pledge"].getStr()) - + if bsdNode.hasKey("unveil"): for path in bsdNode["unveil"]: config.unveil.add(path.getStr()) - + manifest.sandbox = some(config) # Desktop Integration @@ -989,25 +996,25 @@ proc parseManifestFromJSON*(content: string, parser: var ManifestParser): Packag terminal: dtNode.getOrDefault("terminal").getBool(false), startupNotify: dtNode.getOrDefault("startup_notify").getBool(true) ) - + if dtNode.hasKey("icon"): dt.icon = some(dtNode["icon"].getStr()) - + if dtNode.hasKey("startup_wm_class"): dt.startupWMClass = some(dtNode["startup_wm_class"].getStr()) - + if dtNode.hasKey("categories"): for cat in dtNode["categories"]: dt.categories.add(cat.getStr()) - + if dtNode.hasKey("keywords"): for kw in dtNode["keywords"]: dt.keywords.add(kw.getStr()) - + if dtNode.hasKey("mime_types"): for mt in dtNode["mime_types"]: dt.mimeTypes.add(mt.getStr()) - + manifest.desktop = some(dt) return manifest @@ -1016,7 +1023,8 @@ proc parseManifestFromJSON*(content: string, parser: var ManifestParser): Packag # KDL Parsing (Human-Friendly) - NATIVE IMPLEMENTATION # ============================================================================ -proc parseManifestFromKDL*(content: string, parser: var ManifestParser): PackageManifest = +proc parseManifestFromKDL*(content: string, + parser: var ManifestParser): PackageManifest = ## Parse package manifest from KDL format using NATIVE KDL structures ## No JSON conversion - direct KDL parsing for maximum efficiency @@ -1223,7 +1231,7 @@ proc parseManifestFromKDL*(content: string, parser: var ManifestParser): Package for file in child.children: if file.args.len == 0: raise newException(ManifestError, "File missing path") - + var fileSpec = FileSpec( path: file.getArgString(0), hash: file.getPropString("hash"), @@ -1236,7 +1244,7 @@ proc parseManifestFromKDL*(content: string, parser: var ManifestParser): Package for user in child.children: if user.args.len == 0: raise newException(ManifestError, "User missing name") - + var userSpec = UserSpec( name: user.getArgString(0), group: user.getPropString("group", user.getArgString(0)), @@ -1245,25 +1253,25 @@ proc parseManifestFromKDL*(content: string, parser: var ManifestParser): Package ) if user.hasProp("uid"): userSpec.uid = some(user.getPropInt("uid").int) - + manifest.users.add(userSpec) of "groups": for group in child.children: if group.args.len == 0: raise newException(ManifestError, "Group missing name") - + var groupSpec = GroupSpec(name: group.getArgString(0)) if group.hasProp("gid"): groupSpec.gid = some(group.getPropInt("gid").int) - + manifest.groups.add(groupSpec) of "services": for service in child.children: if service.args.len == 0: raise newException(ManifestError, "Service missing name") - + var serviceSpec = ServiceSpec( name: service.getArgString(0), content: service.getPropString("content", ""), @@ -1277,13 +1285,13 @@ proc parseManifestFromKDL*(content: string, parser: var ManifestParser): Package var config = SandboxConfig( level: parseSandboxLevel(child.getPropString("level", "strict")) ) - + for sbChild in child.children: case sbChild.name: of "linux": if sbChild.hasProp("seccomp"): config.seccompProfile = some(sbChild.getPropString("seccomp")) - + for linuxChild in sbChild.children: case linuxChild.name: of "capabilities": @@ -1292,17 +1300,17 @@ proc parseManifestFromKDL*(content: string, parser: var ManifestParser): Package of "namespaces": for ns in linuxChild.args: config.namespaces.add(ns.getString()) - + of "bsd": if sbChild.hasProp("pledge"): config.pledge = some(sbChild.getPropString("pledge")) - + for bsdChild in sbChild.children: case bsdChild.name: of "unveil": for path in bsdChild.args: config.unveil.add(path.getString()) - + manifest.sandbox = some(config) of "desktop": @@ -1311,13 +1319,13 @@ proc parseManifestFromKDL*(content: string, parser: var ManifestParser): Package terminal: child.getPropBool("terminal", false), startupNotify: child.getPropBool("startup_notify", true) ) - + if child.hasProp("icon"): dt.icon = some(child.getPropString("icon")) - + if child.hasProp("startup_wm_class"): dt.startupWMClass = some(child.getPropString("startup_wm_class")) - + for dtChild in child.children: case dtChild.name: of "categories": @@ -1329,11 +1337,12 @@ proc parseManifestFromKDL*(content: string, parser: var ManifestParser): Package of "mime_types": for mt in dtChild.args: dt.mimeTypes.add(mt.getString()) - + manifest.desktop = some(dt) else: - if parser.config.strictMode and child.name notin parser.config.allowedFields: + if parser.config.strictMode and child.name notin + parser.config.allowedFields: raise newException(ManifestError, "Unknown field: " & child.name) else: parser.warnings.add("Unknown field: " & child.name) @@ -1460,14 +1469,15 @@ proc serializeManifestToJSON*(manifest: PackageManifest): string = for dep in manifest.dependencies: var depObj = %* {"name": dep.name} if dep.versionConstraint.operator != OpAny: - depObj["version"] = %($dep.versionConstraint.operator & $dep.versionConstraint.version) + depObj["version"] = %($dep.versionConstraint.operator & + $dep.versionConstraint.version) if dep.optional: depObj["optional"] = %true if dep.features.len > 0: depObj["features"] = %dep.features deps.add(depObj) jsonObj["dependencies"] = deps - + # System Integration if manifest.files.len > 0: var files = newJArray() @@ -1517,7 +1527,7 @@ proc serializeManifestToJSON*(manifest: PackageManifest): string = if manifest.sandbox.isSome: let sb = manifest.sandbox.get() var sbObj = %*{"level": $sb.level} - + var linuxObj = newJObject() if sb.seccompProfile.isSome: linuxObj["seccomp"] = %sb.seccompProfile.get() @@ -1527,7 +1537,7 @@ proc serializeManifestToJSON*(manifest: PackageManifest): string = linuxObj["namespaces"] = %sb.namespaces if linuxObj.len > 0: sbObj["linux"] = linuxObj - + var bsdObj = newJObject() if sb.pledge.isSome: bsdObj["pledge"] = %sb.pledge.get() @@ -1535,7 +1545,7 @@ proc serializeManifestToJSON*(manifest: PackageManifest): string = bsdObj["unveil"] = %sb.unveil if bsdObj.len > 0: sbObj["bsd"] = bsdObj - + jsonObj["sandbox"] = sbObj # Desktop Integration @@ -1546,7 +1556,7 @@ proc serializeManifestToJSON*(manifest: PackageManifest): string = "terminal": dt.terminal, "startup_notify": dt.startupNotify } - + if dt.icon.isSome: dtObj["icon"] = %dt.icon.get() if dt.startupWMClass.isSome: @@ -1557,7 +1567,7 @@ proc serializeManifestToJSON*(manifest: PackageManifest): string = dtObj["keywords"] = %dt.keywords if dt.mimeTypes.len > 0: dtObj["mime_types"] = %dt.mimeTypes - + jsonObj["desktop"] = dtObj return $jsonObj @@ -1617,7 +1627,8 @@ proc serializeManifestToKDL*(manifest: PackageManifest): string = for dep in manifest.dependencies: result.add(" \"" & dep.name & "\"") if dep.versionConstraint.operator != OpAny: - result.add(" version=\"" & $dep.versionConstraint.operator & $dep.versionConstraint.version & "\"") + result.add(" version=\"" & $dep.versionConstraint.operator & + $dep.versionConstraint.version & "\"") if dep.optional: result.add(" optional=true") if dep.features.len > 0: @@ -1631,7 +1642,8 @@ proc serializeManifestToKDL*(manifest: PackageManifest): string = for dep in manifest.buildDependencies: result.add(" \"" & dep.name & "\"") if dep.versionConstraint.operator != OpAny: - result.add(" version=\"" & $dep.versionConstraint.operator & $dep.versionConstraint.version & "\"") + result.add(" version=\"" & $dep.versionConstraint.operator & + $dep.versionConstraint.version & "\"") result.add("\n") result.add(" }\n") @@ -1641,7 +1653,8 @@ proc serializeManifestToKDL*(manifest: PackageManifest): string = for dep in manifest.optionalDependencies: result.add(" \"" & dep.name & "\"") if dep.versionConstraint.operator != OpAny: - result.add(" version=\"" & $dep.versionConstraint.operator & $dep.versionConstraint.version & "\"") + result.add(" version=\"" & $dep.versionConstraint.operator & + $dep.versionConstraint.version & "\"") if dep.features.len > 0: result.add(" features=\"" & dep.features.join(",") & "\"") result.add("\n") @@ -1696,13 +1709,15 @@ proc serializeManifestToKDL*(manifest: PackageManifest): string = if manifest.files.len > 0: result.add("\n files {\n") for file in manifest.files: - result.add(" file \"" & file.path & "\" hash=\"" & file.hash & "\" size=" & $file.size & " permissions=\"" & file.permissions & "\"\n") + result.add(" file \"" & file.path & "\" hash=\"" & file.hash & + "\" size=" & $file.size & " permissions=\"" & file.permissions & "\"\n") result.add(" }\n") if manifest.users.len > 0: result.add("\n users {\n") for user in manifest.users: - result.add(" \"" & user.name & "\" group=\"" & user.group & "\" shell=\"" & user.shell & "\" home=\"" & user.home & "\"") + result.add(" \"" & user.name & "\" group=\"" & user.group & + "\" shell=\"" & user.shell & "\" home=\"" & user.home & "\"") if user.uid.isSome: result.add(" uid=" & $user.uid.get()) result.add("\n") @@ -1720,78 +1735,81 @@ proc serializeManifestToKDL*(manifest: PackageManifest): string = if manifest.services.len > 0: result.add("\n services {\n") for service in manifest.services: - result.add(" \"" & service.name & "\" enabled=" & $service.enabled & " content=" & service.content.escape() & "\n") + result.add(" \"" & service.name & "\" enabled=" & $service.enabled & + " content=" & service.content.escape() & "\n") result.add(" }\n") # Security / Sandbox if manifest.sandbox.isSome: let sb = manifest.sandbox.get() result.add("\n sandbox level=\"" & $sb.level & "\" {\n") - + # Linux - if sb.seccompProfile.isSome or sb.capabilities.len > 0 or sb.namespaces.len > 0: + if sb.seccompProfile.isSome or sb.capabilities.len > 0 or + sb.namespaces.len > 0: result.add(" linux") if sb.seccompProfile.isSome: result.add(" seccomp=\"" & sb.seccompProfile.get() & "\"") result.add(" {\n") - + if sb.capabilities.len > 0: result.add(" capabilities") for cap in sb.capabilities: result.add(" \"" & cap & "\"") result.add("\n") - + if sb.namespaces.len > 0: result.add(" namespaces") for ns in sb.namespaces: result.add(" \"" & ns & "\"") result.add("\n") result.add(" }\n") - + # BSD if sb.pledge.isSome or sb.unveil.len > 0: result.add(" bsd") if sb.pledge.isSome: result.add(" pledge=\"" & sb.pledge.get() & "\"") result.add(" {\n") - + if sb.unveil.len > 0: result.add(" unveil") for path in sb.unveil: result.add(" \"" & path & "\"") result.add("\n") result.add(" }\n") - + result.add(" }\n") # Desktop Integration if manifest.desktop.isSome: let dt = manifest.desktop.get() - result.add("\n desktop display_name=\"" & dt.displayName & "\" terminal=" & $dt.terminal & " startup_notify=" & $dt.startupNotify) + result.add("\n desktop display_name=\"" & dt.displayName & "\" terminal=" & + $dt.terminal & " startup_notify=" & $dt.startupNotify) if dt.icon.isSome: result.add(" icon=\"" & dt.icon.get() & "\"") if dt.startupWMClass.isSome: result.add(" startup_wm_class=\"" & dt.startupWMClass.get() & "\"") result.add(" {\n") - + if dt.categories.len > 0: result.add(" categories") for cat in dt.categories: result.add(" \"" & cat & "\"") result.add("\n") - + if dt.keywords.len > 0: result.add(" keywords") for kw in dt.keywords: result.add(" \"" & kw & "\"") result.add("\n") - + if dt.mimeTypes.len > 0: result.add(" mime_types") for mt in dt.mimeTypes: result.add(" \"" & mt & "\"") result.add("\n") - + result.add(" }\n") result.add("}\n") @@ -1842,7 +1860,8 @@ proc calculateManifestHash*(manifest: PackageManifest): string = for dep in manifest.dependencies: var depStr = "dep:" & dep.name if dep.versionConstraint.operator != OpAny: - depStr.add(":" & $dep.versionConstraint.operator & $dep.versionConstraint.version) + depStr.add(":" & $dep.versionConstraint.operator & + $dep.versionConstraint.version) if dep.optional: depStr.add(":optional") if dep.features.len > 0: @@ -1855,7 +1874,8 @@ proc calculateManifestHash*(manifest: PackageManifest): string = for dep in manifest.buildDependencies: var depStr = "builddep:" & dep.name if dep.versionConstraint.operator != OpAny: - depStr.add(":" & $dep.versionConstraint.operator & $dep.versionConstraint.version) + depStr.add(":" & $dep.versionConstraint.operator & + $dep.versionConstraint.version) buildDepStrings.add(depStr) components.add(buildDepStrings.sorted().join("|")) @@ -1864,7 +1884,8 @@ proc calculateManifestHash*(manifest: PackageManifest): string = for dep in manifest.optionalDependencies: var depStr = "optdep:" & dep.name if dep.versionConstraint.operator != OpAny: - depStr.add(":" & $dep.versionConstraint.operator & $dep.versionConstraint.version) + depStr.add(":" & $dep.versionConstraint.operator & + $dep.versionConstraint.version) if dep.features.len > 0: depStr.add(":features=" & dep.features.sorted().join(",")) optDepStrings.add(depStr) @@ -1907,7 +1928,8 @@ proc calculateManifestHash*(manifest: PackageManifest): string = # 13. System Integration (sorted for determinism) var fileStrings: seq[string] = @[] for file in manifest.files: - fileStrings.add("file:" & file.path & ":" & file.hash & ":" & $file.size & ":" & file.permissions) + fileStrings.add("file:" & file.path & ":" & file.hash & ":" & $file.size & + ":" & file.permissions) components.add(fileStrings.sorted().join("|")) var userStrings: seq[string] = @[] @@ -1926,33 +1948,35 @@ proc calculateManifestHash*(manifest: PackageManifest): string = var serviceStrings: seq[string] = @[] for service in manifest.services: - serviceStrings.add("service:" & service.name & ":" & $service.enabled & ":" & service.content) + serviceStrings.add("service:" & service.name & ":" & $service.enabled & + ":" & service.content) components.add(serviceStrings.sorted().join("|")) # 14. Security / Sandbox if manifest.sandbox.isSome: let sb = manifest.sandbox.get() var sbStr = "sandbox:" & $sb.level - + if sb.seccompProfile.isSome: sbStr.add(":seccomp=" & sb.seccompProfile.get()) if sb.capabilities.len > 0: sbStr.add(":caps=" & sb.capabilities.sorted().join(",")) if sb.namespaces.len > 0: sbStr.add(":ns=" & sb.namespaces.sorted().join(",")) - + if sb.pledge.isSome: sbStr.add(":pledge=" & sb.pledge.get()) if sb.unveil.len > 0: sbStr.add(":unveil=" & sb.unveil.sorted().join(",")) - + components.add(sbStr) # 15. Desktop Integration if manifest.desktop.isSome: let dt = manifest.desktop.get() - var dtStr = "desktop:" & dt.displayName & ":" & $dt.terminal & ":" & $dt.startupNotify - + var dtStr = "desktop:" & dt.displayName & ":" & $dt.terminal & ":" & + $dt.startupNotify + if dt.icon.isSome: dtStr.add(":icon=" & dt.icon.get()) if dt.startupWMClass.isSome: @@ -1963,7 +1987,7 @@ proc calculateManifestHash*(manifest: PackageManifest): string = dtStr.add(":kws=" & dt.keywords.sorted().join(",")) if dt.mimeTypes.len > 0: dtStr.add(":mimes=" & dt.mimeTypes.sorted().join(",")) - + components.add(dtStr) # Calculate hash from all components @@ -1972,7 +1996,8 @@ proc calculateManifestHash*(manifest: PackageManifest): string = return $hash -proc verifyManifestHash*(manifest: PackageManifest, expectedHash: string): bool = +proc verifyManifestHash*(manifest: PackageManifest, + expectedHash: string): bool = ## Verify that a manifest matches the expected hash ## Returns true if hash matches, false otherwise let calculatedHash = calculateManifestHash(manifest) diff --git a/src/nip/nexter.nim b/src/nip/nexter.nim index b7cc5f5..4836661 100644 --- a/src/nip/nexter.nim +++ b/src/nip/nexter.nim @@ -29,7 +29,7 @@ import std/[os, strutils, times, options, sequtils, osproc, logging] import nip/cas -import nip/xxhash +import nip/xxh import nip/nexter_manifest type @@ -97,7 +97,8 @@ proc parseNEXTER*(path: string): NEXTERContainer = try: # Extract archive using tar with zstd decompression # Using --auto-compress lets tar detect compression automatically - let extractCmd = "tar --auto-compress -xf " & quoteShell(path) & " -C " & quoteShell(tempDir) + let extractCmd = "tar --auto-compress -xf " & quoteShell(path) & " -C " & + quoteShell(tempDir) let exitCode = execCmd(extractCmd) if exitCode != 0: @@ -159,7 +160,7 @@ proc parseNEXTER*(path: string): NEXTERContainer = # Archive Creation # ============================================================================ -proc createNEXTER*(manifest: NEXTERManifest, environment: string, chunks: seq[ChunkData], +proc createNEXTER*(manifest: NEXTERManifest, environment: string, chunks: seq[ChunkData], signature: string, outputPath: string) = ## Create .nexter archive from components ## @@ -208,7 +209,8 @@ proc createNEXTER*(manifest: NEXTERManifest, environment: string, chunks: seq[Ch writeFile(tempDir / "signature.sig", signature) # Create tar.zst archive - let createCmd = "tar --auto-compress -cf " & quoteShell(outputPath) & " -C " & quoteShell(tempDir) & " ." + let createCmd = "tar --auto-compress -cf " & quoteShell(outputPath) & + " -C " & quoteShell(tempDir) & " ." let exitCode = execCmd(createCmd) if exitCode != 0: @@ -247,8 +249,8 @@ proc extractChunksToCAS*(container: NEXTERContainer, casRoot: string): seq[strin for chunk in container.chunks: try: # Decompress chunk - let decompressed = chunk.data # TODO: Implement zstd decompression - + let decompressed = chunk.data # TODO: Implement zstd decompression + # Verify hash let calculatedHash = "xxh3-" & $calculateXXH3(decompressed) if calculatedHash != chunk.hash: @@ -285,19 +287,19 @@ proc verifyNEXTER*(path: string): bool = try: let container = parseNEXTER(path) - + # Verify manifest if container.manifest.name.len == 0: return false - + # Verify signature if container.signature.len == 0: return false - + # Verify chunks if container.chunks.len == 0: warn("NEXTER archive has no chunks") - + return true except Exception as e: diff --git a/src/nip/npk.nim b/src/nip/npk.nim index 794ddb6..db5e154 100644 --- a/src/nip/npk.nim +++ b/src/nip/npk.nim @@ -29,7 +29,7 @@ import std/[os, strutils, times, json, options, sequtils] import nip/cas -import nip/xxhash +import nip/xxh import nip/npk_manifest import nip/manifest_parser @@ -97,7 +97,8 @@ proc parseNPK*(path: string): NPKPackage = try: # Extract archive using tar with zstd decompression # Using --auto-compress lets tar detect compression automatically - let extractCmd = "tar --auto-compress -xf " & quoteShell(path) & " -C " & quoteShell(tempDir) + let extractCmd = "tar --auto-compress -xf " & quoteShell(path) & " -C " & + quoteShell(tempDir) let extractResult = execShellCmd(extractCmd) if extractResult != 0: @@ -133,7 +134,7 @@ proc parseNPK*(path: string): NPKPackage = hash: chunkHash, data: chunkData, size: chunkData.len.int64, - chunkType: Binary # Will be determined from manifest + chunkType: Binary # Will be determined from manifest )) # Load signature @@ -343,11 +344,13 @@ proc packageSize*(pkg: NPKPackage): int64 = proc `$`*(pkg: NPKPackage): string = ## Convert NPK package to human-readable string - result = "NPK Package: " & pkg.manifest.name & " v" & manifest_parser.`$`(pkg.manifest.version) & "\n" + result = "NPK Package: " & pkg.manifest.name & " v" & manifest_parser.`$`( + pkg.manifest.version) & "\n" result.add("Archive: " & pkg.archivePath & "\n") result.add("Chunks: " & $pkg.chunks.len & "\n") result.add("Total Size: " & $(packageSize(pkg) div 1024) & " KB\n") - result.add("Signature: " & (if pkg.signature.len > 0: "Present" else: "Missing") & "\n") + result.add("Signature: " & (if pkg.signature.len > + 0: "Present" else: "Missing") & "\n") # ============================================================================ # Error Formatting diff --git a/src/nip/xxhash.nim b/src/nip/xxh.nim similarity index 97% rename from src/nip/xxhash.nim rename to src/nip/xxh.nim index 8c1944f..719a194 100644 --- a/src/nip/xxhash.nim +++ b/src/nip/xxh.nim @@ -15,6 +15,7 @@ import std/[strutils] # We'll use a conditional import to handle the case where xxhash isn't installed yet when defined(useXXHash): import xxhash + import nint128 # Required for UInt128 toHex else: # Fallback implementation using a simple hash for development # This will be replaced with actual xxhash once the library is installed @@ -41,7 +42,8 @@ when defined(useXXHash): proc calculateXXH3*(data: seq[byte]): XXH3Hash = ## Calculate xxh3-128 hash of a byte sequence ## Returns hash in format: "xxh3-" - let hash128 = XXH3_128bits(cast[ptr UncheckedArray[byte]](unsafeAddr data[0]), data.len) + let hash128 = XXH3_128bits(cast[ptr UncheckedArray[byte]](unsafeAddr data[ + 0]), csize_t(data.len)) let hexDigest = hash128.toHex().toLowerAscii() result = XXH3Hash("xxh3-" & hexDigest) @@ -109,7 +111,7 @@ when isMainModule: echo "✗ Hash verification failed" # Test byte sequence hashing - let testBytes = @[byte(72), byte(101), byte(108), byte(108), byte(111)] # "Hello" + let testBytes = @[byte(72), byte(101), byte(108), byte(108), byte(111)] # "Hello" let bytesHash = calculateXXH3(testBytes) echo "Bytes hash: ", $bytesHash