fasttree-sys 0.2.0

Surowe bindingi FFI (extern "C") do biblioteki FastTree (Nim). Nie używaj bezpośrednio — patrz crate `fasttree`.
import std/[os, osproc, strformat, strutils, tables]

type LayerError* = object of CatchableError

proc extractLayerTar*(layerTarPath, destDir: string) =
  ## `tar` z GNU tar auto-wykrywa gzip/zstd po zawartości niezależnie od
  ## rozszerzenia, więc jedno wywołanie obsługuje zarówno `.tar`, jak
  ## i `.tar.gz` (docker) czy warstwy OCI serwowane bez rozszerzenia.
  ## `--xattrs` prosi GNU tar o odtworzenie rozszerzonych atrybutów (np.
  ## `security.capability` na binarkach z setcap) — bez efektu, jeśli
  ## archiwum ich nie zawiera, więc bezpieczne do podania zawsze.
  createDir(destDir)
  let cmd = &"tar --xattrs --xattrs-include='*' -xf \"{layerTarPath}\" -C \"{destDir}\""
  let (output, code) = execCmdEx(cmd)
  if code != 0:
    raise newException(LayerError, &"rozpakowanie warstwy '{layerTarPath}' nie powiodło się: {output}")

const WhiteoutPrefix = ".wh."
const OpaqueWhiteoutName = ".wh..wh..opq"

proc isOpaqueWhiteout(name: string): bool = name == OpaqueWhiteoutName
proc isWhiteout(name: string): bool = name.startsWith(WhiteoutPrefix) and not isOpaqueWhiteout(name)
proc whiteoutTarget(name: string): string = name[WhiteoutPrefix.len .. ^1]

proc copyEntry(srcPath, destPath: string, seenInodes: var Table[(DeviceId, FileId), string]) =
  let info = getFileInfo(srcPath, followSymlink = false)
  case info.kind
  of pcDir:
    createDir(destPath)
  of pcLinkToFile, pcLinkToDir:
    if symlinkExists(destPath) or fileExists(destPath): removeFile(destPath)
    createSymlink(expandSymlink(srcPath), destPath)
  else:
    createDir(destPath.parentDir)
    if fileExists(destPath): removeFile(destPath)

    # Hardlinki: jeśli linkCount > 1, ten plik dzieli i-węzeł z innym wpisem
    # w TEJ SAMEJ warstwie (tar domyślnie zachowuje relacje hardlinków przy
    # rozpakowaniu). Odtwarzamy tę relację w destDir zamiast kopiować
    # zawartość N razy — ważne dla poprawności (dwie ścieżki, które MUSZĄ
    # zawsze mieć identyczną treść z definicji, nie przez przypadek).
    if info.linkCount > 1:
      let key = info.id
      if key in seenInodes:
        createHardlink(seenInodes[key], destPath)
        return
      else:
        seenInodes[key] = destPath
        # spada do zwykłego kopiowania niżej — to PIERWSZE wystąpienie tego i-węzła

    # `cp --preserve=mode,ownership,timestamps,xattr` zamiast ręcznego
    # read/write: zachowuje właściciela (uid/gid — istotne w obrazach
    # kontenerowych, gdzie pliki bywają nie-rootowe) i rozszerzone atrybuty
    # (np. `security.capability` na binarkach z setcap), czego Nimowe
    # `copyFileWithPermissions` (tylko bity uprawnień) nie robi.
    let (output, code) = execCmdEx(
      &"cp --preserve=mode,ownership,timestamps,xattr \"{srcPath}\" \"{destPath}\"")
    if code != 0:
      raise newException(LayerError, &"kopiowanie '{srcPath}' -> '{destPath}' nie powiodło się: {output}")

proc mergeLayerInto*(layerDir, destDir: string) =
  ## Nakłada JEDNĄ już rozpakowaną warstwę na materializowane drzewo docelowe.
  ## Wołane w kolejności warstw od najstarszej do najnowszej.

  # Krok 1: opaque whiteouts — wyczyść odziedziczoną zawartość katalogów,
  # które ta warstwa jawnie "resetuje", zanim skopiujemy jej własne pliki.
  for path in walkDirRec(layerDir, yieldFilter = {pcFile}):
    if path.extractFilename == OpaqueWhiteoutName:
      let relDir = path.parentDir.relativePath(layerDir)
      let targetDir = if relDir == ".": destDir else: destDir / relDir
      if dirExists(targetDir):
        removeDir(targetDir)
      createDir(targetDir)

  # Krok 2: skopiuj zwykłe pliki tej warstwy (pomijając same znaczniki whiteout).
  # `seenInodes` jest per-warstwa (nie per-cały-merge): hardlinki mają sens
  # tylko WEWNĄTRZ jednej warstwy tar — dwie różne warstwy nie dzielą i-węzłów
  # (każda to osobne rozpakowanie do osobnego katalogu roboczego).
  var seenInodes = initTable[(DeviceId, FileId), string]()
  for path in walkDirRec(layerDir, yieldFilter = {pcFile, pcDir, pcLinkToFile, pcLinkToDir}):
    let rel = path.relativePath(layerDir)
    if rel == ".": continue
    let name = path.extractFilename
    if isWhiteout(name) or isOpaqueWhiteout(name):
      continue
    copyEntry(path, destDir / rel, seenInodes)

  # Krok 3: whiteout pojedynczych plików — usuń odpowiedniki z destDir
  # (czyli z niższych warstw), PO skopiowaniu własnej zawartości warstwy.
  for path in walkDirRec(layerDir, yieldFilter = {pcFile}):
    let name = path.extractFilename
    if isWhiteout(name):
      let relDir = path.parentDir.relativePath(layerDir)
      let targetName = whiteoutTarget(name)
      let target = (if relDir == ".": destDir else: destDir / relDir) / targetName
      if dirExists(target): removeDir(target)
      elif fileExists(target) or symlinkExists(target): removeFile(target)

proc materializeLayers*(layerTarPaths: seq[string], workDir, destDir: string): string =
  ## Rozpakowuje i scala listę warstw (w podanej kolejności: dół -> góra)
  ## do `destDir`. Zwraca destDir dla wygody łańcuchowania.
  createDir(destDir)
  for i, layerTar in layerTarPaths:
    let layerDir = workDir / &"layer-{i}"
    extractLayerTar(layerTar, layerDir)
    mergeLayerInto(layerDir, destDir)
  destDir