243 lines
7.8 KiB
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")
|