nip/src/nimpak/overlays.nim

595 lines
22 KiB
Nim

# 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)