# SPDX-License-Identifier: LSL-1.0 # Copyright (c) 2026 Markus Maiwald # Stewardship: Self Sovereign Society Foundation # # This file is part of the Nexus Sovereign Core. # See legal/LICENSE_SOVEREIGN.md for license terms. ## NOF Overlay Fragment Format Handler (.nof) ## ## This module implements the NOF (Nexus Overlay Fragment) format for declarative ## system modifications. NOF overlays provide immutable system configuration ## changes that can be applied atomically to system generations. ## ## Format: .nof (Nexus Overlay Fragment, plain text KDL) ## - Plain-text KDL format for immutable system overlays ## - Overlay application and validation system ## - Ed25519 signature support for overlay integrity ## - Overlay conflict detection and resolution import std/[os, json, times, strutils, sequtils, tables, options, algorithm] import ./types_fixed import ./formats import ./cas type NofError* = object of NimPakError overlayName*: string OverlayValidationResult* = object valid*: bool errors*: seq[ValidationError] warnings*: seq[string] OverlayOperation* = enum ## Types of overlay operations AddFile, ## Add new file ModifyFile, ## Modify existing file RemoveFile, ## Remove file AddSymlink, ## Add symbolic link RemoveSymlink,## Remove symbolic link SetPermissions,## Set file permissions AddPackage, ## Add package to system RemovePackage,## Remove package from system SetConfig ## Set configuration value OverlayModification* = object ## Individual overlay modification operation*: OverlayOperation target*: string ## Target path or package name source*: Option[string] ## Source path (for copies/links) content*: Option[string] ## File content (for inline content) permissions*: Option[FilePermissions] ## File permissions metadata*: JsonNode ## Additional operation-specific metadata const NOF_VERSION* = "1.0" MAX_OVERLAY_SIZE* = 100 * 1024 * 1024 ## 100MB maximum overlay size # ============================================================================= # NOF Overlay Creation and Management # ============================================================================= proc createNofOverlay*(name: string, description: string, overlayConfig: OverlayConfig): NofOverlay = ## Factory method to create NOF overlay with proper defaults NofOverlay( name: name, description: description, overlayConfOverlayrlayConfig, signature: none(Signature), format: NofOverlay, cryptoAlgorithms: CryptoAlgorithms( hashAlgorithm: "BLAKE2b", signatureAlgorithm: "Ed25519", version: "1.0" ) ) proc createOverlayConfig*(name: string, description: string, targetGeneration: Option[string] = none(string), modifications: JsonNode = newJObject()): OverlayConfig = ## Factory method to create overlay configuration OverlayConfig( name: name, description: description, targetGeneration: targetGeneration, modifications: modifications ) proc createOverlayModification*(operation: OverlayOperation, target: string, source: Option[string] = none(string), content: Option[string] = none(string), permissions: Option[FilePermissions] = none(FilePermissions), metadata: JsonNode = newJObject()): OverlayModification = ## Factory method to create overlay modification OverlayModification( operation: operation, target: target, source: source, content: content, permissions: permissions, metadata: metadata ) # ============================================================================= # KDL Serialization for NOF Format # ============================================================================= proc escapeKdlString(s: string): string = ## Escape special characters in KDL strings result = "\"" for c in s: case c: of '"': result.add("\\\"") of '\\': result.add("\\\\") of '\n': result.add("\\n") of '\r': result.add("\\r") of '\t': result.add("\\t") else: result.add(c) result.add("\"") proc formatKdlBoolean(b: bool): string = ## Format boolean for KDL if b: "true" else: "false" proc formatKdlArray(items: seq[string]): string = ## Format string array for KDL if items.len == 0: return "" result = "" for i, item in items: if i > 0: result.add(" ") result.add(escapeKdlString(item)) proc toHex(b: byte): string = ## Convert byte to hex string const hexChars = "0123456789abcdef" result = $hexChars[b shr 4] & $hexChars[b and 0x0F] proc serializeModificationsToKdl(modifications: JsonNode): string = ## Serialize modifications JSON to KDL format ## This is a simplified conversion - full KDL library would be better result = "" if modifications.kind == JObject: for key, value in modifications: case value.kind: of JString: result.add(" " & escapeKdlString(key) & " " & escapeKdlString(value.getStr()) & "\n") of JInt: result.add(" " & escapeKdlString(key) & " " & $value.getInt() & "\n") of JBool: result.add(" " & escapeKdlString(key) & " " & formatKdlBoolean(value.getBool()) & "\n") of JArray: let items = value.getElems().mapIt(it.getStr()) result.add(" " & escapeKdlString(key) & " " & formatKdlArray(items) & "\n") of JObject: result.add(" " & escapeKdlString(key) & " {\n") for subKey, subValue in value: result.add(" " & escapeKdlString(subKey) & " " & escapeKdlString(subValue.getStr()) & "\n") result.add(" }\n") else: result.add(" " & escapeKdlString(key) & " " & escapeKdlString($value) & "\n") proc serializeNofToKdl*(overlay: NofOverlay): string = ## Serialize NOF overlay to KDL format with comprehensive metadata ## Plain-text format optimized for immutable system overlays result = "overlay " & escapeKdlString(overlay.name) & " {\n" result.add(" version " & escapeKdlString(NOF_VERSION) & "\n") result.add(" format " & escapeKdlString($overlay.format) & "\n") result.add(" description " & escapeKdlString(overlay.description) & "\n") result.add("\n") # Overlay configuration result.add(" config {\n") result.add(" name " & escapeKdlString(overlay.overlayConfig.name) & "\n") result.add(" description " & escapeKdlString(overlay.overlayConfig.description) & "\n") if overlay.overlayConfig.targetGeneration.isSome: result.add(" target-generation " & escapeKdlString(overlay.overlayConfig.targetGeneration.get()) & "\n") result.add(" }\n\n") # Modifications section result.add(" modifications {\n") result.add(serializeModificationsToKdl(overlay.overlayConfig.modifications)) result.add(" }\n\n") # Cryptographic integrity and signature result.add(" integrity {\n") result.add(" algorithm " & escapeKdlString(overlay.cryptoAlgorithms.hashAlgorithm) & "\n") result.add(" signature-algorithm " & escapeKdlString(overlay.cryptoAlgorithms.signatureAlgorithm) & "\n") result.add(" version " & escapeKdlString(overlay.cryptoAlgorithms.version) & "\n") if overlay.signature.isSome: let sig = overlay.signature.get() result.add(" signature " & escapeKdlString(sig.signature.mapIt(it.toHex()).join("")) & "\n") result.add(" key-id " & escapeKdlString(sig.keyId) & "\n") result.add(" }\n") result.add("}\n") proc deserializeNofFromKdl*(kdlContent: string): Result[NofOverlay, NofError] = ## Deserialize NOF overlay from KDL format ## TODO: Implement proper KDL parsing when kdl library is available ## For now, return an error indicating this is not yet implemented return err[NofOverlay, NofError](NofError( code: InvalidMetadata, msg: "KDL deserialization not yet implemented - waiting for kdl library", overlayName: "unknown" )) # ============================================================================= # Overlay Validation # ============================================================================= proc validateNofOverlay*(overlay: NofOverlay): OverlayValidationResult = ## Validate NOF overlay format and content var result = OverlayValidationResult(valid: true, errors: @[], warnings: @[]) # Validate basic metadata if overlay.name.len == 0: result.errors.add(ValidationError( field: "name", message: "Overlay name cannot be empty", suggestions: @["Provide a valid overlay name"] )) result.valid = false if overlay.description.len == 0: result.warnings.add("Overlay has no description") # Validate overlay configuration if overlay.overlayConfig.name.len == 0: result.errors.add(ValidationError( field: "overlayConfig.name", message: "Overlay config name cannot be empty", suggestions: @["Provide a valid config name"] )) result.valid = false if overlay.overlayConfig.description.len == 0: result.warnings.add("Overlay config has no description") # Validate modifications structure if overlay.overlayConfig.modifications.kind != JObject: result.errors.add(ValidationError( field: "overlayConfig.modifications", message: "Modifications must be a JSON object", suggestions: @["Provide valid modifications object"] )) result.valid = false # Validate target generation if specified if overlay.overlayConfig.targetGeneration.isSome: let targetGen = overlay.overlayConfig.targetGeneration.get() if targetGen.len == 0: result.errors.add(ValidationError( field: "overlayConfig.targetGeneration", message: "Target generation cannot be empty if specified", suggestions: @["Provide valid generation ID or remove target"] )) result.valid = false # Validate cryptographic algorithms if not isQuantumResistant(overlay.cryptoAlgorithms): result.warnings.add("Using non-quantum-resistant algorithms: " & overlay.cryptoAlgorithms.hashAlgorithm & "/" & overlay.cryptoAlgorithms.signatureAlgorithm) return result proc validateOverlayModification*(modification: OverlayModification): seq[string] = ## Validate individual overlay modification and return warnings var warnings: seq[string] = @[] case modification.operation: of AddFile, ModifyFile: if modification.content.isNone and modification.source.isNone: warnings.add("File operation without content or source") if modification.target.len == 0: warnings.add("File operation without target path") of RemoveFile, RemoveSymlink: if modification.target.len == 0: warnings.add("Remove operation without target path") of AddSymlink: if modification.source.isNone: warnings.add("Symlink operation without source") if modification.target.len == 0: warnings.add("Symlink operation without target") of SetPermissions: if modification.permissions.isNone: warnings.add("Permission operation without permissions") if modification.target.len == 0: warnings.add("Permission operation without target") of AddPackage, RemovePackage: if modification.target.len == 0: warnings.add("Package operation without package name") of SetConfig: if modification.target.len == 0: warnings.add("Config operation without config key") if modification.content.isNone: warnings.add("Config operation without value") return warnings # ============================================================================= # Overlay File Operations # ============================================================================= proc saveNofOverlay*(overlay: NofOverlay, filePath: string): Result[void, NofError] = ## Save NOF overlay to file in KDL format try: let kdlContent = serializeNofToKdl(overlay) # Ensure the file has the correct .nof extension let finalPath = if filePath.endsWith(".nof"): filePath else: filePath & ".nof" # Ensure parent directory exists let parentDir = finalPath.parentDir() if not dirExists(parentDir): createDir(parentDir) writeFile(finalPath, kdlContent) return ok[void, NofError]() except IOError as e: return err[void, NofError](NofError( code: FileWriteError, msg: "Failed to save NOF overlay: " & e.msg, overlayName: overlay.name )) proc loadNofOverlay*(filePath: string): Result[NofOverlay, NofError] = ## Load NOF overlay from file try: if not fileExists(filePath): return err[NofOverlay, NofError](NofError( code: PackageNotFound, msg: "NOF overlay file not found: " & filePath, overlayName: "unknown" )) let kdlContent = readFile(filePath) return deserializeNofFromKdl(kdlContent) except IOError as e: return err[NofOverlay, NofError](NofError( code: FileReadError, msg: "Failed to load NOF overlay: " & e.msg, overlayName: "unknown" )) # ============================================================================= # Overlay Digital Signatures # ============================================================================= proc signNofOverlay*(overlay: var NofOverlay, keyId: string, privateKey: seq[byte]): Result[void, NofError] = ## Sign NOF overlay with Ed25519 private key ## Creates a comprehensive signature payload including all critical overlay metadata try: # Create comprehensive signature payload from overlay metadata and modifications let payload = overlay.name & overlay.description & overlay.overlayConfig.name & overlay.overlayConfig.description & (if overlay.overlayConfig.targetGeneration.isSome: overlay.overlayConfig.targetGeneration.get() else: "") & $overlay.overlayConfig.modifications # TODO: Implement actual Ed25519 signing when crypto library is available # For now, create a deterministic placeholder signature based on payload let payloadHash = calculateBlake2b(payload.toOpenArrayByte(0, payload.len - 1).toSeq()) let placeholderSig = payloadHash[0..63].toOpenArrayByte(0, 63).toSeq() # 64 bytes like Ed25519 let signature = Signature( keyId: keyId, algorithm: overlay.cryptoAlgorithms.signatureAlgorithm, signature: placeholderSig ) overlay.signature = some(signature) return ok[void, NofError]() except Exception as e: return err[void, NofError](NofError( code: UnknownError, msg: "Failed to sign overlay: " & e.msg, overlayName: overlay.name )) proc verifyNofSignature*(overlay: NofOverlay, publicKey: seq[byte]): Result[bool, NofError] = ## Verify NOF overlay signature ## TODO: Implement proper Ed25519 verification when crypto library is available if overlay.signature.isNone: return ok[bool, NofError](false) # No signature to verify try: let sig = overlay.signature.get() # TODO: Implement actual Ed25519 verification # For now, just check if signature exists and has correct length let isValid = sig.signature.len == 64 and sig.keyId.len > 0 return ok[bool, NofError](isValid) except Exception as e: return err[bool, NofError](NofError( code: UnknownError, msg: "Failed to verify signature: " & e.msg, overlayName: overlay.name )) # ============================================================================= # Overlay Application System # ============================================================================= proc applyOverlay*(overlay: NofOverlay, targetDir: string, dryRun: bool = false): Result[seq[string], NofError] = ## Apply overlay modifications to target directory ## Returns list of operations performed try: var operations: seq[string] = @[] # Parse modifications from JSON if overlay.overlayConfig.modifications.kind != JObject: return err[seq[string], NofError](NofError( code: InvalidMetadata, msg: "Invalid modifications format", overlayName: overlay.name )) # Process each modification for key, value in overlay.overlayConfig.modifications: let operation = "Apply " & key & ": " & $value operations.add(operation) if not dryRun: # TODO: Implement actual overlay application logic # This would involve: # - Parsing the modification type and parameters # - Applying file operations (create, modify, delete) # - Managing symlinks and permissions # - Handling package operations # - Setting configuration values discard return ok[seq[string], NofError](operations) except Exception as e: return err[seq[string], NofError](NofError( code: UnknownError, msg: "Failed to apply overlay: " & e.msg, overlayName: overlay.name )) proc detectOverlayConflicts*(overlays: seq[NofOverlay]): seq[string] = ## Detect conflicts between multiple overlays var conflicts: seq[string] = @[] var targetPaths: Table[string, string] = initTable[string, string]() for overlay in overlays: if overlay.overlayConfig.modifications.kind == JObject: for key, value in overlay.overlayConfig.modifications: if key in targetPaths: conflicts.add("Conflict: " & key & " modified by both " & targetPaths[key] & " and " & overlay.name) else: targetPaths[key] = overlay.name return conflicts proc resolveOverlayConflicts*(overlays: seq[NofOverlay], resolution: Table[string, string]): seq[NofOverlay] = ## Resolve overlay conflicts using provided resolution strategy ## Resolution table maps conflict keys to preferred overlay names var resolved: seq[NofOverlay] = @[] for overlay in overlays: var resolvedOverlay = overlay if overlay.overlayConfig.modifications.kind == JObject: var newModifications = newJObject() for key, value in overlay.overlayConfig.modifications: if key in resolution: if resolution[key] == overlay.name: newModifications[key] = value else: newModifications[key] = value resolvedOverlay.overlayConfig.modifications = newModifications resolved.add(resolvedOverlay) return resolved # ============================================================================= # Overlay Templates and Presets # ============================================================================= proc createFileOverlay*(name: string, description: string, filePath: string, content: string, permissions: Option[FilePermissions] = none(FilePermissions)): NofOverlay = ## Create overlay for adding/modifying a file let modifications = %*{ "files": { filePath: { "operation": "add_file", "content": content, "permissions": if permissions.isSome: %*{ "mode": permissions.get().mode, "owner": permissions.get().owner, "group": permissions.get().group } else: newJNull() } } } let config = createOverlayConfig(name, description, modifications = modifications) return createNofOverlay(name, description, config) proc createPackageOverlay*(name: string, description: string, packageName: string, packageVersion: string, operation: string = "add"): NofOverlay = ## Create overlay for adding/removing a package let modifications = %*{ "packages": { packageName: { "operation": operation, "version": packageVersion } } } let config = createOverlayConfig(name, description, modifications = modifications) return createNofOverlay(name, description, config) proc createConfigOverlay*(name: string, description: string, configKey: string, configValue: string): NofOverlay = ## Create overlay for setting configuration values let modifications = %*{ "config": { configKey: { "operation": "set_config", "value": configValue } } } let config = createOverlayConfig(name, description, modifications = modifications) return createNofOverlay(name, description, config) proc createSymlinkOverlay*(name: string, description: string, linkPath: string, targetPath: string): NofOverlay = ## Create overlay for adding symbolic links let modifications = %*{ "symlinks": { linkPath: { "operation": "add_symlink", "target": targetPath } } } let config = createOverlayConfig(name, description, modifications = modifications) return createNofOverlay(name, description, config) # ============================================================================= # Utility Functions # ============================================================================= proc getNofInfo*(overlay: NofOverlay): string = ## Get human-readable overlay information result = "NOF Overlay: " & overlay.name & "\n" result.add("Description: " & overlay.description & "\n") result.add("Config: " & overlay.overlayConfig.name & "\n") if overlay.overlayConfig.targetGeneration.isSome: result.add("Target Generation: " & overlay.overlayConfig.targetGeneration.get() & "\n") result.add("Modifications: " & $overlay.overlayConfig.modifications.len & " items\n") if overlay.signature.isSome: result.add("Signed: Yes (Key: " & overlay.signature.get().keyId & ")\n") else: result.add("Signed: No\n") proc calculateBlake2b*(data: seq[byte]): string = ## Calculate BLAKE2b hash - imported from CAS module cas.calculateBlake2b(data) proc calculateBlake3*(data: seq[byte]): string = ## Calculate BLAKE3 hash - imported from CAS module cas.calculateBlake3(data)