packages feed

hnix-store-nar 0.1.1.0 → 0.1.2.0

raw patch · 14 files changed

+477/−91 lines, 14 filesdep +cryptondep −cryptonitedep ~basedep ~tasty-hspecPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: crypton

Dependencies removed: cryptonite

Dependency ranges changed: base, tasty-hspec

API changes (from Hackage documentation)

- System.Nix.Nar: dumpPath :: forall m. MonadIO m => FilePath -> NarSource m
+ System.Nix.Nar: dumpPath :: MonadIO m => FilePath -> NarSource m
- System.Nix.Nar: dumpString :: forall m. MonadIO m => ByteString -> NarSource m
+ System.Nix.Nar: dumpString :: MonadIO m => ByteString -> NarSource m
- System.Nix.Nar: narEffectsIO :: (MonadIO m, MonadFail m, MonadBaseControl IO m) => NarEffects m
+ System.Nix.Nar: narEffectsIO :: forall (m :: Type -> Type). (MonadIO m, MonadFail m, MonadBaseControl IO m) => NarEffects m
- System.Nix.Nar: parseNar :: (MonadIO m, MonadFail m) => NarParser m ()
+ System.Nix.Nar: parseNar :: forall (m :: Type -> Type). (MonadIO m, MonadFail m) => NarParser m ()
- System.Nix.Nar: runParser :: forall m a. (MonadIO m, MonadBaseControl IO m) => NarEffects m -> NarParser m a -> Handle -> FilePath -> m (Either String a)
+ System.Nix.Nar: runParser :: (MonadIO m, MonadBaseControl IO m) => NarEffects m -> NarParser m a -> Handle -> FilePath -> m (Either String a)
- System.Nix.Nar: runParserWithOptions :: forall m a. (MonadIO m, MonadBaseControl IO m) => NarOptions -> NarEffects m -> NarParser m a -> Handle -> FilePath -> m (Either String a)
+ System.Nix.Nar: runParserWithOptions :: (MonadIO m, MonadBaseControl IO m) => NarOptions -> NarEffects m -> NarParser m a -> Handle -> FilePath -> m (Either String a)
- System.Nix.Nar: streamNarIO :: forall m. MonadIO m => NarEffects IO -> FilePath -> NarSource m
+ System.Nix.Nar: streamNarIO :: MonadIO m => NarEffects IO -> FilePath -> NarSource m
- System.Nix.Nar: streamNarIOWithOptions :: forall m. MonadIO m => NarOptions -> NarEffects IO -> FilePath -> NarSource m
+ System.Nix.Nar: streamNarIOWithOptions :: MonadIO m => NarOptions -> NarEffects IO -> FilePath -> NarSource m
- System.Nix.Nar: testParser' :: m ~ IO => FilePath -> IO (Either String ())
+ System.Nix.Nar: testParser' :: forall (m :: Type -> Type). m ~ IO => FilePath -> IO (Either String ())
- System.Nix.Nar: type NarSource m = (ByteString -> m ()) -> m ()
+ System.Nix.Nar: type NarSource (m :: Type -> Type) = ByteString -> m () -> m ()
- System.Nix.Nar.Effects: narEffectsIO :: (MonadIO m, MonadFail m, MonadBaseControl IO m) => NarEffects m
+ System.Nix.Nar.Effects: narEffectsIO :: forall (m :: Type -> Type). (MonadIO m, MonadFail m, MonadBaseControl IO m) => NarEffects m
- System.Nix.Nar.Parser: parseNar :: (MonadIO m, MonadFail m) => NarParser m ()
+ System.Nix.Nar.Parser: parseNar :: forall (m :: Type -> Type). (MonadIO m, MonadFail m) => NarParser m ()
- System.Nix.Nar.Parser: runParser :: forall m a. (MonadIO m, MonadBaseControl IO m) => NarEffects m -> NarParser m a -> Handle -> FilePath -> m (Either String a)
+ System.Nix.Nar.Parser: runParser :: (MonadIO m, MonadBaseControl IO m) => NarEffects m -> NarParser m a -> Handle -> FilePath -> m (Either String a)
- System.Nix.Nar.Parser: runParserWithOptions :: forall m a. (MonadIO m, MonadBaseControl IO m) => NarOptions -> NarEffects m -> NarParser m a -> Handle -> FilePath -> m (Either String a)
+ System.Nix.Nar.Parser: runParserWithOptions :: (MonadIO m, MonadBaseControl IO m) => NarOptions -> NarEffects m -> NarParser m a -> Handle -> FilePath -> m (Either String a)
- System.Nix.Nar.Parser: testParser' :: m ~ IO => FilePath -> IO (Either String ())
+ System.Nix.Nar.Parser: testParser' :: forall (m :: Type -> Type). m ~ IO => FilePath -> IO (Either String ())
- System.Nix.Nar.Streamer: dumpPath :: forall m. MonadIO m => FilePath -> NarSource m
+ System.Nix.Nar.Streamer: dumpPath :: MonadIO m => FilePath -> NarSource m
- System.Nix.Nar.Streamer: dumpString :: forall m. MonadIO m => ByteString -> NarSource m
+ System.Nix.Nar.Streamer: dumpString :: MonadIO m => ByteString -> NarSource m
- System.Nix.Nar.Streamer: streamNarIO :: forall m. MonadIO m => NarEffects IO -> FilePath -> NarSource m
+ System.Nix.Nar.Streamer: streamNarIO :: MonadIO m => NarEffects IO -> FilePath -> NarSource m
- System.Nix.Nar.Streamer: streamNarIOWithOptions :: forall m. MonadIO m => NarOptions -> NarEffects IO -> FilePath -> NarSource m
+ System.Nix.Nar.Streamer: streamNarIOWithOptions :: MonadIO m => NarOptions -> NarEffects IO -> FilePath -> NarSource m
- System.Nix.Nar.Streamer: type NarSource m = (ByteString -> m ()) -> m ()
+ System.Nix.Nar.Streamer: type NarSource (m :: Type -> Type) = ByteString -> m () -> m ()

Files

CHANGELOG.md view
@@ -1,3 +1,8 @@+# [0.1.2.0](https://github.com/haskell-nix/hnix-store/compare/nar-0.1.1.0...nar-0.1.2.0)  2026-08-10++* Switch from `cryptonite` to `crypton`+* Preserve non-UTF-8 filename and symlink target bytes when streaming NARs [#320](https://github.com/haskell-nix/hnix-store/pull/320)+ # [0.1.1.0](https://github.com/haskell-nix/hnix-store/compare/nar-0.1.0.0...nar-0.1.1.0)  2024-10-09  * Fix ordering of case-hacked paths on macOS [#286](https://github.com/haskell-nix/hnix-store/pull/286)
README.md view
@@ -4,5 +4,8 @@  For a description of the NAR format, see [`Eelco's thesis`](https://nixos.org/~eelco/pubs/phd-thesis.pdf). +The [NAR streaming benchmark](./benchmarks/README.md) provides a reproducible+Hyperfine comparison between the working tree and a baseline Git revision.+ [System.Nix.Nar]: ./src/System/Nix/Nar.hs [System.Nix.Nar.Effects]: ./src/System/Nix/Nar/Effects.hs
+ benchmarks/NarStream.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE ImportQualifiedPost #-}++module Main (main) where++import Control.Monad (forM_, replicateM_, unless)+import Data.ByteString qualified as Bytes+import Data.IORef (modifyIORef', newIORef, readIORef)+import System.Directory qualified as Directory+import System.Environment (getArgs)+import System.Exit (die)+import System.FilePath ((</>))+import Text.Printf (printf)+import Text.Read (readMaybe)++import System.Nix.Nar.Streamer (dumpPath)++main :: IO ()+main = do+  args <- getArgs+  case args of+    ["prepare", fixturePath, fileCountText] -> do+      fileCount <- readPositive "file count" fileCountText+      prepareFixture fixturePath fileCount+    ["run", fixturePath, iterationsText] -> do+      iterations <- readPositive "iteration count" iterationsText+      runBenchmark fixturePath iterations+    [] -> putStrLn usage+    _ -> die usage++usage :: String+usage = unlines+  [ "Usage: nar-stream prepare FIXTURE_PATH FILE_COUNT"+  , "       nar-stream run FIXTURE_PATH ITERATIONS"+  ]++readPositive :: String -> String -> IO Int+readPositive label input =+  case readMaybe input of+    Just value | value > 0 -> pure value+    _ -> die $ label <> " must be a positive integer"++prepareFixture :: FilePath -> Int -> IO ()+prepareFixture fixturePath fileCount = do+  exists <- Directory.doesPathExist fixturePath+  if exists+    then die $ "fixture path already exists: " <> fixturePath+    else Directory.createDirectory fixturePath++  forM_ [1 .. fileCount] $ \index ->+    Bytes.writeFile (fixturePath </> fileName index) Bytes.empty+ where+  fileName :: Int -> FilePath+  fileName = printf "file-%08d"++runBenchmark :: FilePath -> Int -> IO ()+runBenchmark fixturePath iterations = do+  exists <- Directory.doesDirectoryExist fixturePath+  unless exists $ die $ "fixture directory does not exist: " <> fixturePath++  byteCount <- newIORef (0 :: Int)+  replicateM_ iterations $+    dumpPath fixturePath $ \chunk ->+      modifyIORef' byteCount (+ Bytes.length chunk)++  streamedBytes <- readIORef byteCount+  unless (streamedBytes > 0) $ die "NAR streamer produced no output"
+ benchmarks/README.md view
@@ -0,0 +1,57 @@+# NAR streaming wall-time benchmark++This benchmark compares the working tree's NAR streamer with a baseline Git+revision. Both binaries archive the same pre-created wide directory, so fixture+creation, compilation, and cleanup are excluded from Hyperfine's measurements.++## 1. Prepare++From the repository root:++```console+nix-shell hnix-store-nar/benchmarks/shell.nix --run \+  'hnix-store-nar/benchmarks/prepare-hyperfine.sh'+```++The default baseline is `HEAD`. This is appropriate while the fix is still an+uncommitted working-tree change. After committing the fix, select its parent or+another known revision explicitly:++```console+HNIX_NAR_BASELINE_REF=HEAD^ \+  nix-shell hnix-store-nar/benchmarks/shell.nix --run \+    'hnix-store-nar/benchmarks/prepare-hyperfine.sh'+```++Preparation creates ignored state under `benchmark-results/nar-stream-state`:+the two prebuilt runners, a shared 5,000-file fixture, build metadata,+and the working-tree patch. It does not run Hyperfine.++## 2. Measure when the machine is idle++```console+nix-shell hnix-store-nar/benchmarks/shell.nix --run \+  'hnix-store-nar/benchmarks/run-hyperfine.sh'+```++The runner refuses to start when one-minute load divided by the CPU count is+above `0.5`. It pins both binaries to the same CPU, performs five warmups and 30+measurements, then repeats the comparison in reverse order to expose drift.+JSON and Markdown reports are written below the prepared state's `results/`+directory.++Useful overrides:++```console+HNIX_NAR_FILES=10000             # preparation only+HNIX_NAR_ITERATIONS=10           # dumps per measured process+HNIX_NAR_WARMUPS=5+HNIX_NAR_RUNS=30+HNIX_NAR_CPU=14+HNIX_NAR_MAX_LOAD_PER_CPU=0.5+HNIX_NAR_STATE_DIR=/some/path+```++This is a warm-cache benchmark of NAR traversal and encoding. Do not clear the+machine's page cache between runs; doing so needs elevated privileges, disrupts+other work, and measures a different workload.
+ benchmarks/prepare-hyperfine.sh view
@@ -0,0 +1,85 @@+#!/usr/bin/env bash+set -euo pipefail++benchmark_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)+repo_root=$(git -C "$benchmark_dir" rev-parse --show-toplevel)+state_dir=${HNIX_NAR_STATE_DIR:-"$repo_root/benchmark-results/nar-stream-state"}+baseline_ref=${HNIX_NAR_BASELINE_REF:-HEAD}+file_count=${HNIX_NAR_FILES:-5000}++if [[ -e "$state_dir" ]]; then+  echo "Benchmark state already exists: $state_dir" >&2+  echo "Set HNIX_NAR_STATE_DIR to a new path or remove the old state first." >&2+  exit 1+fi++scratch_dir=$(mktemp -d "${TMPDIR:-/tmp}/hnix-nar-prepare.XXXXXX")+baseline_tree="$scratch_dir/baseline"+baseline_added=false++cleanup() {+  if [[ "$baseline_added" == true ]]; then+    git -C "$repo_root" worktree remove --force "$baseline_tree" >/dev/null 2>&1 || true+  fi+  rm -rf -- "$scratch_dir"+}+trap cleanup EXIT++mkdir -p "$state_dir/bin"++build_runner() {+  local source_tree=$1+  local label=$2+  local build_dir="$scratch_dir/dist-$label"+  local object_dir="$scratch_dir/objects-$label"++  mkdir -p "$object_dir"+  (+    cd "$source_tree"+    cabal build hnix-store-nar \+      --builddir="$build_dir" \+      --disable-tests \+      --disable-benchmarks \+      -j1+    cabal exec --builddir="$build_dir" -- \+      ghc -O2 -threaded -rtsopts "-with-rtsopts=-N1" \+        -odir "$object_dir" \+        -hidir "$object_dir" \+        -package bytestring \+        -package directory \+        -package filepath \+        -package hnix-store-nar \+        "$benchmark_dir/NarStream.hs" \+        -o "$state_dir/bin/$label"+  )+}++echo "Building fixed benchmark runner..."+build_runner "$repo_root" fixed++echo "Creating baseline worktree at $baseline_ref..."+git -C "$repo_root" worktree add --detach "$baseline_tree" "$baseline_ref"+baseline_added=true++echo "Building baseline benchmark runner..."+build_runner "$baseline_tree" baseline++echo "Creating the shared $file_count-file fixture..."+"$state_dir/bin/fixed" prepare "$state_dir/fixture" "$file_count"++baseline_commit=$(git -C "$baseline_tree" rev-parse HEAD)+fixed_commit=$(git -C "$repo_root" rev-parse HEAD)+{+  printf 'baseline_ref=%s\n' "$baseline_ref"+  printf 'baseline_commit=%s\n' "$baseline_commit"+  printf 'fixed_commit=%s\n' "$fixed_commit"+  printf 'file_count=%s\n' "$file_count"+  printf 'ghc=%s\n' "$(ghc --numeric-version)"+  printf 'cabal=%s\n' "$(cabal --numeric-version)"+} > "$state_dir/metadata.txt"+git -C "$repo_root" diff --binary > "$state_dir/fixed.patch"++echo+echo "Benchmark state is ready: $state_dir"+echo "When the machine is idle, run:"+echo "  nix-shell $benchmark_dir/shell.nix --run '$benchmark_dir/run-hyperfine.sh'"
+ benchmarks/run-hyperfine.sh view
@@ -0,0 +1,83 @@+#!/usr/bin/env bash+set -euo pipefail++benchmark_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)+repo_root=$(git -C "$benchmark_dir" rev-parse --show-toplevel)+state_dir=${HNIX_NAR_STATE_DIR:-"$repo_root/benchmark-results/nar-stream-state"}+iterations=${HNIX_NAR_ITERATIONS:-7}+warmups=${HNIX_NAR_WARMUPS:-5}+runs=${HNIX_NAR_RUNS:-30}+max_load_per_cpu=${HNIX_NAR_MAX_LOAD_PER_CPU:-0.5}+cpu_count=$(nproc)+default_cpu=$((cpu_count > 1 ? cpu_count - 2 : 0))+benchmark_cpu=${HNIX_NAR_CPU:-$default_cpu}++baseline_bin="$state_dir/bin/baseline"+fixed_bin="$state_dir/bin/fixed"+fixture="$state_dir/fixture"++for required_path in "$baseline_bin" "$fixed_bin" "$fixture" "$state_dir/metadata.txt"; do+  if [[ ! -e "$required_path" ]]; then+    echo "Missing benchmark state: $required_path" >&2+    echo "Run prepare-hyperfine.sh first." >&2+    exit 1+  fi+done++check_load() {+  local load_one+  load_one=$(awk '{ print $1 }' /proc/loadavg)+  if ! awk \+    -v current_load="$load_one" \+    -v cpus="$cpu_count" \+    -v maximum="$max_load_per_cpu" \+    'BEGIN { exit !((current_load / cpus) <= maximum) }'+  then+    echo "Refusing to benchmark: load is $load_one across $cpu_count CPUs." >&2+    echo "Wait for load/CPU <= $max_load_per_cpu, or override HNIX_NAR_MAX_LOAD_PER_CPU." >&2+    exit 75+  fi+}++if ! taskset -c "$benchmark_cpu" true; then+  echo "CPU $benchmark_cpu is not available to taskset." >&2+  exit 1+fi++check_load++timestamp=$(date -u +%Y%m%dT%H%M%SZ)+results_dir="$state_dir/results/$timestamp"+mkdir -p "$results_dir"++baseline_command="taskset -c $benchmark_cpu $baseline_bin run $fixture $iterations +RTS -N1 -A64m -RTS"+fixed_command="taskset -c $benchmark_cpu $fixed_bin run $fixture $iterations +RTS -N1 -A64m -RTS"++common_options=(+  --warmup "$warmups"+  --runs "$runs"+  --shell=none+)++echo "Benchmarking baseline first on CPU $benchmark_cpu..."+hyperfine \+  "${common_options[@]}" \+  --export-json "$results_dir/baseline-first.json" \+  --export-markdown "$results_dir/baseline-first.md" \+  --command-name baseline "$baseline_command" \+  --command-name fixed "$fixed_command"++check_load++echo "Benchmarking fixed first on CPU $benchmark_cpu..."+hyperfine \+  "${common_options[@]}" \+  --export-json "$results_dir/fixed-first.json" \+  --export-markdown "$results_dir/fixed-first.md" \+  --command-name fixed "$fixed_command" \+  --command-name baseline "$baseline_command"++cp "$state_dir/metadata.txt" "$results_dir/metadata.txt"++echo+echo "Benchmark results: $results_dir"
+ benchmarks/shell.nix view
@@ -0,0 +1,13 @@+{ pkgs ? (import ../../default.nix {}).pkgs }:++let+  narShell = (import ../../shell.nix { inherit pkgs; }).hnix-store-nar;+in+pkgs.mkShell {+  inputsFrom = [ narShell ];+  packages = [+    pkgs.cabal-install+    pkgs.hyperfine+    pkgs.util-linux+  ];+}
hnix-store-nar.cabal view
@@ -1,6 +1,6 @@ cabal-version:       2.2 name:                hnix-store-nar-version:             0.1.1.0+version:             0.1.2.0 synopsis:            NAR file format description:   Packing and unpacking for NAR file format used by Nix.@@ -16,6 +16,10 @@     CHANGELOG.md extra-source-files:     README.md+  , benchmarks/README.md+  , benchmarks/prepare-hyperfine.sh+  , benchmarks/run-hyperfine.sh+  , benchmarks/shell.nix   , tests/fixtures/case-conflict.nar  flag bounded_memory@@ -40,6 +44,7 @@     , FlexibleContexts     , FlexibleInstances     , GADTs+    , ImportQualifiedPost     , StandaloneDeriving     , ScopedTypeVariables     , StandaloneDeriving@@ -68,7 +73,7 @@     , System.Nix.Nar.Options   build-depends:       base >=4.12 && <5-    , algebraic-graphs >= 0.5 && < 0.8+    , algebraic-graphs >= 0.5 && < 0.9     , bytestring     , case-insensitive     , cereal@@ -98,7 +103,7 @@     tasty-discover:tasty-discover   build-depends:       base-    , cryptonite+    , crypton     , hnix-store-nar     , base64-bytestring     , cereal@@ -110,8 +115,21 @@     , process     , temporary     , tasty-    , tasty-hspec+    , tasty-hspec >= 1.1     , tasty-hunit     , tasty-quickcheck     , text     , unix++benchmark nar-stream+  import: commons+  type: exitcode-stdio-1.0+  main-is: NarStream.hs+  hs-source-dirs: benchmarks+  ghc-options: -O2 -threaded -rtsopts "-with-rtsopts -N1"+  build-depends:+      base+    , bytestring+    , directory+    , filepath+    , hnix-store-nar
src/System/Nix/Nar.hs view
@@ -33,14 +33,14 @@   , Nar.NarSource   ) where -import qualified Control.Concurrent                as Concurrent-import qualified Data.ByteString                   as BS-import qualified System.IO                         as IO+import Control.Concurrent qualified                as Concurrent+import Data.ByteString qualified                   as BS+import System.IO qualified                         as IO -import qualified System.Nix.Nar.Effects   as Nar-import qualified System.Nix.Nar.Options   as Nar-import qualified System.Nix.Nar.Parser    as Nar-import qualified System.Nix.Nar.Streamer  as Nar+import System.Nix.Nar.Effects qualified   as Nar+import System.Nix.Nar.Options qualified   as Nar+import System.Nix.Nar.Parser qualified    as Nar+import System.Nix.Nar.Streamer qualified  as Nar  -- For a description of the NAR format, see Eelco's thesis -- https://nixos.org/~eelco/pubs/phd-thesis.pdf
src/System/Nix/Nar/Effects.hs view
@@ -16,10 +16,10 @@ import Data.Kind (Type) import System.IO (Handle, IOMode(WriteMode)) -import qualified Control.Monad-import qualified Data.ByteString-import qualified Data.ByteString.Lazy        as Bytes.Lazy-import qualified System.Directory            as Directory+import Control.Monad qualified+import Data.ByteString qualified+import Data.ByteString.Lazy qualified        as Bytes.Lazy+import System.Directory qualified            as Directory import           System.Posix.Files          ( createSymbolicLink                                              , fileMode                                              , fileSize@@ -37,8 +37,8 @@                                              , setFileMode                                              , unionFileModes                                              )-import qualified System.IO                   as IO-import qualified Control.Exception.Lifted    as Exception.Lifted+import System.IO qualified                   as IO+import Control.Exception.Lifted qualified    as Exception.Lifted  data IsExecutable = NonExecutable | Executable   deriving (Eq, Show)
src/System/Nix/Nar/Options.hs view
@@ -6,7 +6,7 @@   ) where  import Data.Text (Text)-import qualified System.Info+import System.Info qualified  -- | Options for configuring how NAR files are encoded and decoded. data NarOptions = NarOptions {
src/System/Nix/Nar/Parser.hs view
@@ -11,42 +11,42 @@   ) where  -import qualified Algebra.Graph                   as Graph-import qualified Algebra.Graph.ToGraph           as Graph-import qualified Control.Concurrent              as Concurrent-import qualified Control.Exception.Lifted        as Exception.Lifted+import Algebra.Graph qualified                   as Graph+import Algebra.Graph.ToGraph qualified           as Graph+import Control.Concurrent qualified              as Concurrent+import Control.Exception.Lifted qualified        as Exception.Lifted import           Control.Monad                    ( forM                                                   , when                                                   , forM_                                                   )-import qualified Control.Monad.Except            as Except-import qualified Control.Monad.Fail              as Fail-import qualified Control.Monad.IO.Class          as IO-import qualified Control.Monad.Reader            as Reader-import qualified Control.Monad.State             as State-import qualified Control.Monad.Trans             as Trans-import qualified Control.Monad.Trans.Control     as Base+import Control.Monad.Except qualified            as Except+import Control.Monad.Fail qualified              as Fail+import Control.Monad.IO.Class qualified          as IO+import Control.Monad.Reader qualified            as Reader+import Control.Monad.State qualified             as State+import Control.Monad.Trans qualified             as Trans+import Control.Monad.Trans.Control qualified     as Base import           Data.ByteString                  (ByteString)-import qualified Data.ByteString                 as Bytes+import Data.ByteString qualified                 as Bytes import           Data.Bool                        ( bool )-import qualified Data.Either                     as Either+import Data.Either qualified                     as Either import           Data.Int                         ( Int64 )-import qualified Data.IORef                      as IORef-import qualified Data.CaseInsensitive            as CI-import qualified Data.HashMap.Strict             as HashMap-import qualified Data.List                       as List-import qualified Data.Map                        as Map+import Data.IORef qualified                      as IORef+import Data.CaseInsensitive qualified            as CI+import Data.HashMap.Strict qualified             as HashMap+import Data.List qualified                       as List+import Data.Map qualified                        as Map import           Data.Maybe                       ( catMaybes )-import qualified Data.Serialize                  as Serialize+import Data.Serialize qualified                  as Serialize import           Data.Text                        ( Text )-import qualified Data.Text                       as Text-import qualified Data.Text.Encoding              as Text-import qualified System.Directory                as Directory+import Data.Text qualified                       as Text+import Data.Text.Encoding qualified              as Text+import System.Directory qualified                as Directory import           System.FilePath                 as FilePath-import qualified System.IO                       as IO+import System.IO qualified                       as IO -import qualified System.Nix.Nar.Effects as Nar-import qualified System.Nix.Nar.Options as Nar+import System.Nix.Nar.Effects qualified as Nar+import System.Nix.Nar.Options qualified as Nar  -- | NarParser is a monad for parsing a Nar file as a byte stream --   and reconstructing the file system objects inside
src/System/Nix/Nar/Streamer.hs view
@@ -12,23 +12,26 @@  import Data.ByteString (ByteString) import Data.Int (Int64)-import qualified Data.Map.Strict                 as Map+import Data.Map.Strict qualified                 as Map -import           Control.Monad                    ( forM_+import           Control.Monad                    ( forM+                                                  , forM_                                                   , when                                                   )-import qualified Control.Monad.IO.Class          as IO-import qualified Data.ByteString                 as Bytes-import qualified Data.ByteString.Lazy            as Bytes.Lazy-import qualified Data.Foldable-import qualified Data.List-import qualified Data.Serialize                  as Serial-import qualified Data.Text                       as T (pack, unpack)-import qualified Data.Text.Encoding              as TE (encodeUtf8)+import Control.Monad.IO.Class qualified          as IO+import Data.ByteString qualified                 as Bytes+import Data.ByteString.Lazy qualified            as Bytes.Lazy+import Data.Foldable qualified+import Data.List qualified+import Data.Serialize qualified                  as Serial+import Data.Text qualified                       as T (unpack)+import GHC.Foreign qualified                     as Foreign+import GHC.IO.Encoding qualified                 as Encoding import           System.FilePath                 ((</>))+import           System.IO                       (TextEncoding) -import qualified System.Nix.Nar.Effects as Nar-import qualified System.Nix.Nar.Options as Nar+import System.Nix.Nar.Effects qualified as Nar+import System.Nix.Nar.Options qualified as Nar  -- | NarSource -- The source to provide nar to the handler `(ByteString -> m ())`.@@ -78,38 +81,41 @@   -> FilePath   -> NarSource m streamNarIOWithOptions opts effs basePath yield = do+  fileSystemEncoding <- IO.liftIO Encoding.getFileSystemEncoding   yield $ str "nix-archive-1"-  parens $ go basePath+  parens $ go fileSystemEncoding basePath  where-  go :: FilePath -> m ()-  go path = do+  go :: TextEncoding -> FilePath -> m ()+  go fileSystemEncoding path = do     isSymLink <- IO.liftIO $ Nar.narIsSymLink effs path     if isSymLink then do       target <- IO.liftIO $ Nar.narReadLink effs path+      targetBytes <- IO.liftIO $ filePathToBS fileSystemEncoding target       yield $-        strs ["type", "symlink", "target", filePathToBS target]+        strs ["type", "symlink", "target", targetBytes]       else do         isDir <- IO.liftIO $ Nar.narIsDir effs path         if isDir then do           fs <- IO.liftIO (Nar.narListDir effs path)+          names <- IO.liftIO $ forM fs $ \f -> do+            let name =+                  if Nar.optUseCaseHack opts+                  then undoCaseHack f+                  else f+            nameBytes <- filePathToBS fileSystemEncoding name+            pure (nameBytes, name, f)           let entries =-                foldr (\f acc ->-                  let-                    name =-                      if Nar.optUseCaseHack opts-                      then undoCaseHack f-                      else f-                  in-                  case Map.insertLookupWithKey (\_ n _ -> n) name f acc of+                foldr (\(nameBytes, name, original) acc ->+                  case Map.insertLookupWithKey (\_ n _ -> n) nameBytes (name, original) acc of                     (Nothing, newMap) -> newMap-                    (Just conflict, _) -> error $ "File name collision between " ++ (path </> name) ++ " and " ++ (path </> conflict)-                ) Map.empty fs+                    (Just (conflict, _), _) -> error $ "File name collision between " ++ (path </> name) ++ " and " ++ (path </> conflict)+                ) Map.empty names           yield $ strs ["type", "directory"]-          forM_ (Map.toAscList entries) $ \(unhacked, original) -> do+          forM_ (Map.toAscList entries) $ \(nameBytes, (_, original)) -> do             yield $ str "entry"             parens $ do-              yield $ strs ["name", filePathToBS unhacked, "node"]-              parens $ go (path </> original)+              yield $ strs ["name", nameBytes, "node"]+              parens $ go fileSystemEncoding (path </> original)         else do           isExec <- IO.liftIO $ Nar.narIsExec effs path           yield $ strs ["type", "regular"]@@ -151,8 +157,9 @@ strs :: [ByteString] -> ByteString strs xs = Bytes.concat $ str <$> xs -filePathToBS :: FilePath -> ByteString-filePathToBS = TE.encodeUtf8 . T.pack+filePathToBS :: TextEncoding -> FilePath -> IO ByteString+filePathToBS encoding filePath =+  Foreign.withCStringLen encoding filePath Bytes.packCStringLen  undoCaseHack :: FilePath -> FilePath undoCaseHack f =
tests/NarFormat.hs view
@@ -4,7 +4,7 @@ module NarFormat where  import           Control.Applicative              (many, optional, (<|>))-import qualified Control.Concurrent               as Concurrent+import Control.Concurrent qualified               as Concurrent import           Control.Exception                (SomeException, try) import           Control.Monad                    (replicateM, void, forM_, when) import           Crypto.Hash                      (hash, Digest, SHA256)@@ -15,35 +15,38 @@ import           Data.Serialize                   (Putter, putInt64le,                                                    putByteString, runPut) import           Data.Bool                        (bool)-import qualified Data.ByteString                  as BS-import qualified Data.ByteString.Base64           as B64-import qualified Data.ByteString.Char8            as BSC-import qualified Data.ByteString.Lazy             as BSL-import qualified Data.ByteString.Lazy.Char8       as BSLC+import Data.ByteString qualified                  as BS+import Data.ByteString.Base64 qualified           as B64+import Data.ByteString.Char8 qualified            as BSC+import Data.ByteString.Lazy qualified             as BSL+import Data.ByteString.Lazy.Char8 qualified       as BSLC import           Data.Int                         ( Int64 )-import qualified Data.Map                         as Map+import Data.Map qualified                         as Map import           Data.Maybe                       (fromMaybe)-import qualified Data.Text                        as T-import qualified Data.Text.Encoding               as E+import Data.Text qualified                        as T+import Data.Text.Encoding qualified               as E import           GHC.Generics                     ( Generic ) import           System.Directory                 ( doesDirectoryExist                                                   , doesPathExist                                                   , removeDirectoryRecursive                                                   , removeFile                                                   )-import qualified System.Directory                 as Directory+import System.Directory qualified                 as Directory import           System.Environment               (getEnv) import           System.FilePath                  ((<.>), (</>))-import qualified System.IO                        as IO-import qualified System.IO.Temp                   as Temp-import qualified System.Posix.Files               as Unix-import qualified System.Posix.Process             as Unix-import qualified System.Process                   as P+import System.Info qualified                      as Info+import System.IO qualified                        as IO+import System.IO.Temp qualified                   as Temp+import System.Posix.Files qualified               as Unix+import System.Posix.Files.ByteString qualified    as UnixFilesBS+import System.Posix.IO.ByteString qualified       as UnixBS+import System.Posix.Process qualified             as Unix+import System.Process qualified                   as P import           Test.Tasty                       as T import           Test.Hspec-import qualified Test.Tasty.HUnit                 as HU+import Test.Tasty.HUnit qualified                 as HU import           Test.Tasty.QuickCheck-import qualified Text.Printf                      as Printf+import Text.Printf qualified                      as Printf import           Text.Read                        (readMaybe)  import System.Nix.Nar.Streamer (IsExecutable(Executable, NonExecutable))@@ -143,6 +146,52 @@  unit_nixStoreDirectory' :: HU.Assertion unit_nixStoreDirectory' = filesystemNixStore "directory'" (Nar sampleDirectory')++unit_nixStoreNonUtf8FilePaths :: HU.Assertion+unit_nixStoreNonUtf8FilePaths+  -- APFS only permits valid UTF-8 filenames, so the fixture cannot be+  -- constructed on macOS. Linux still exercises the filesystem integration.+  | Info.os == "darwin" = pure ()+  | otherwise = Temp.withSystemTempDirectory "hnix-store-non-utf8" $ \baseDir -> do+    let rawBaseDir = BSC.pack baseDir+        rawFileNames =+          [ "NetLock_Arany_=Class_Gold=_F" <> BS.pack [0xf5]+              <> "tan" <> BS.pack [0xfa, 0x73, 0xed, 0x74, 0x76, 0xe1]+              <> "ny.crt"+          -- These two names sort in the opposite order after the invalid byte+          -- is decoded to a surrogate, so they also check raw-byte ordering.+          , "sort-" <> BS.pack [0x80]+          , "sort-" <> BS.pack [0xc2, 0x80]+          ]+        rawLinkTarget = "broken-target-" <> BS.pack [0xff]+        rawLinkName = "link"++    forM_ rawFileNames $ \rawFileName -> do+      fd <- UnixBS.createFile+        (rawBaseDir <> "/" <> rawFileName)+        Unix.ownerReadMode+      UnixBS.closeFd fd+    UnixFilesBS.createSymbolicLink+      rawLinkTarget+      (rawBaseDir <> "/" <> rawLinkName)++    hnixNar <- Temp.withSystemTempFile "hnix-store-non-utf8.nar" $ \narPath h -> do+      buildNarIO narEffectsIO baseDir h+      IO.hClose h+      BS.readFile narPath++    forM_ (rawLinkTarget : rawFileNames) $ \rawPath ->+      rawPath `BS.isInfixOf` hnixNar `shouldBe` True++    ver <- try (P.readProcess "nix-store" ["--version"] "")+    case ver of+      Left (_ :: SomeException) -> print ("No nix-store on system" :: String)+      Right _ -> do+        nixStoreNar <- BSL.toStrict <$> getNixStoreDump baseDir+        HU.assertEqual+          "non-UTF-8 paths serialize the same between hnix-store and nix-store"+          nixStoreNar+          hnixNar  -- | Test that the executable permissions are handled correctly in app bundles on macOS. --   In this case, access() returns false for a file under this specific path, even when the executable bit is set.