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