nip/src/nimpak/variant_fingerprint.nim

243 lines
7.8 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.
## variant_fingerprint.nim
## Variant fingerprint calculation using BLAKE2b
## Provides deterministic content-addressed identifiers for package variants
import std/[tables, algorithm, strutils, sequtils]
import nimcrypto/hash
import nimcrypto/blake2
import variant_types
import config # For CompilerFlags
# #############################################################################
# BLAKE2b String Hashing
# #############################################################################
proc calculateBlake2bString*(input: string): string =
## Calculate BLAKE2b hash of a string and return it in the format "blake2b-[hash]"
## This is similar to calculateBlake2b but works on strings instead of files
## Uses BLAKE2b-256 (32 bytes) for shorter fingerprints
try:
let digest = blake2_256.digest(input)
var hexDigest = ""
for b in digest.data:
hexDigest.add(b.toHex(2).toLowerAscii())
result = "blake2b-" & hexDigest
except Exception as e:
raise newException(ValueError, "Failed to calculate BLAKE2b hash: " & e.msg)
# #############################################################################
# Variant Fingerprint Calculation
# #############################################################################
proc calculateVariantFingerprint*(
packageName: string,
version: string,
domains: Table[string, seq[string]],
compilerFlags: CompilerFlags,
toolchain: ToolchainInfo,
target: TargetInfo
): string =
## Calculate deterministic BLAKE2b hash for variant
##
## This function ensures:
## - Identical inputs always produce identical fingerprints
## - Different inputs produce different fingerprints
## - Reproducible across systems and time
##
## Returns: blake2b-[12-char-prefix]
var hashInput = ""
# 1. Package identity
hashInput.add(packageName & "\n")
hashInput.add(version & "\n")
# 2. Sorted domain flags (deterministic ordering)
var sortedDomains = toSeq(domains.keys).sorted()
for domain in sortedDomains:
hashInput.add(domain & ":")
var sortedFlags = domains[domain].sorted()
hashInput.add(sortedFlags.join(",") & "\n")
# 3. Compiler flags
hashInput.add("cflags:" & compilerFlags.cflags & "\n")
hashInput.add("ldflags:" & compilerFlags.ldflags & "\n")
# 4. Toolchain
hashInput.add("toolchain:" & toolchain.name & "-" & toolchain.version & "\n")
# 5. Target
hashInput.add("target:" & target.arch & "-" & target.os & "\n")
# Calculate BLAKE2b hash
let fullHash = calculateBlake2bString(hashInput)
# Return format: blake2b-[12-char-prefix]
# fullHash format is "blake2b-[64-char-hex]"
# We want "blake2b-" (8 chars) + 12 chars = 20 chars total
if fullHash.len >= 20:
result = fullHash[0..19]
else:
result = fullHash
proc buildVariantFingerprint*(
packageName: string,
version: string,
domains: Table[string, seq[string]],
compilerFlags: CompilerFlags,
toolchain: ToolchainInfo,
target: TargetInfo
): VariantFingerprint =
## Build a complete VariantFingerprint object with calculated hash
let hash = calculateVariantFingerprint(
packageName, version, domains, compilerFlags, toolchain, target
)
result = VariantFingerprint(
packageName: packageName,
version: version,
domainFlags: domains,
compilerFlags: compilerFlags,
toolchain: toolchain,
target: target,
hash: hash
)
# #############################################################################
# Fingerprint Validation and Utilities
# #############################################################################
proc isValidFingerprint*(fingerprint: string): bool =
## Validate fingerprint format: blake2b-[12-hex-chars]
if fingerprint.len != 20:
return false
if not fingerprint.startsWith("blake2b-"):
return false
# Check that remaining chars are hex
let hexPart = fingerprint[8..^1]
for c in hexPart:
if c notin {'0'..'9', 'a'..'f', 'A'..'F'}:
return false
return true
proc extractFingerprintPrefix*(fullFingerprint: string): string =
## Extract 12-char prefix from full fingerprint
## Input: "blake2b-[64-char-hex]"
## Output: "blake2b-[12-char]"
if fullFingerprint.len >= 20 and fullFingerprint.startsWith("blake2b-"):
result = fullFingerprint[0..19]
else:
result = fullFingerprint
proc compareFingerprintInputs*(
fp1: VariantFingerprint,
fp2: VariantFingerprint
): seq[string] =
## Compare two fingerprints and return list of differences
result = @[]
if fp1.packageName != fp2.packageName:
result.add("packageName: " & fp1.packageName & " vs " & fp2.packageName)
if fp1.version != fp2.version:
result.add("version: " & fp1.version & " vs " & fp2.version)
# Compare domains
var allDomains: seq[string] = @[]
for domain in fp1.domainFlags.keys:
if domain notin allDomains:
allDomains.add(domain)
for domain in fp2.domainFlags.keys:
if domain notin allDomains:
allDomains.add(domain)
for domain in allDomains:
let flags1 = if fp1.domainFlags.hasKey(domain): fp1.domainFlags[domain] else: @[]
let flags2 = if fp2.domainFlags.hasKey(domain): fp2.domainFlags[domain] else: @[]
if flags1 != flags2:
result.add("domain." & domain & ": " & flags1.join(",") & " vs " & flags2.join(","))
# Compare compiler flags
if fp1.compilerFlags.cflags != fp2.compilerFlags.cflags:
result.add("cflags: " & fp1.compilerFlags.cflags & " vs " & fp2.compilerFlags.cflags)
if fp1.compilerFlags.ldflags != fp2.compilerFlags.ldflags:
result.add("ldflags: " & fp1.compilerFlags.ldflags & " vs " & fp2.compilerFlags.ldflags)
# Compare toolchain
if fp1.toolchain != fp2.toolchain:
result.add("toolchain: " & $fp1.toolchain & " vs " & $fp2.toolchain)
# Compare target
if fp1.target != fp2.target:
result.add("target: " & $fp1.target & " vs " & $fp2.target)
# #############################################################################
# Debug and Inspection
# #############################################################################
proc getFingerprintInputString*(
packageName: string,
version: string,
domains: Table[string, seq[string]],
compilerFlags: CompilerFlags,
toolchain: ToolchainInfo,
target: TargetInfo
): string =
## Get the exact string that will be hashed for fingerprint calculation
## Useful for debugging and understanding what contributes to the hash
result = ""
# 1. Package identity
result.add(packageName & "\n")
result.add(version & "\n")
# 2. Sorted domain flags
var sortedDomains = toSeq(domains.keys).sorted()
for domain in sortedDomains:
result.add(domain & ":")
var sortedFlags = domains[domain].sorted()
result.add(sortedFlags.join(",") & "\n")
# 3. Compiler flags
result.add("cflags:" & compilerFlags.cflags & "\n")
result.add("ldflags:" & compilerFlags.ldflags & "\n")
# 4. Toolchain
result.add("toolchain:" & toolchain.name & "-" & toolchain.version & "\n")
# 5. Target
result.add("target:" & target.arch & "-" & target.os & "\n")
proc debugFingerprint*(fp: VariantFingerprint): string =
## Generate debug output for a fingerprint
result = "VariantFingerprint:\n"
result.add(" Package: " & fp.packageName & " " & fp.version & "\n")
result.add(" Hash: " & fp.hash & "\n")
result.add(" Domains:\n")
var sortedDomains = toSeq(fp.domainFlags.keys).sorted()
for domain in sortedDomains:
result.add(" " & domain & ": " & fp.domainFlags[domain].join(", ") & "\n")
result.add(" Compiler Flags:\n")
result.add(" cflags: " & fp.compilerFlags.cflags & "\n")
result.add(" ldflags: " & fp.compilerFlags.ldflags & "\n")
result.add(" Toolchain: " & $fp.toolchain & "\n")
result.add(" Target: " & $fp.target & "\n")