Imported Upstream version 4.4 upstream/4.4
authorJinWang An <jinwang.an@samsung.com>
Fri, 20 Aug 2021 05:52:14 +0000 (14:52 +0900)
committerJinWang An <jinwang.an@samsung.com>
Fri, 20 Aug 2021 05:52:14 +0000 (14:52 +0900)
300 files changed:
.clang-format
.github/workflows/build.yaml
.github/workflows/codeql-analysis.yaml
.gitignore
.mailmap
ARCHITECTURE.md [new file with mode: 0644]
CMakeLists.txt
CONTRIBUTING.md
LICENSE.adoc
README.md
ci/collect-testdir
cmake/CcachePackConfig.cmake
cmake/CcacheVersion.cmake
cmake/CodeAnalysis.cmake
cmake/DevModeWarnings.cmake
cmake/Findhiredis.cmake [new file with mode: 0644]
cmake/Findzstd.cmake
cmake/GenerateConfigurationFile.cmake
cmake/StandardSettings.cmake
cmake/UseFastestLinker.cmake
cmake/config.h.in
doc/AUTHORS.adoc
doc/CMakeLists.txt
doc/INSTALL.md
doc/MANUAL.adoc
doc/NEWS.adoc
doc/ccache-doc.css [new file with mode: 0644]
dockerfiles/README
dockerfiles/alpine-3.12/Dockerfile [deleted file]
dockerfiles/alpine-3.14/Dockerfile [new file with mode: 0644]
dockerfiles/alpine-3.4/Dockerfile [deleted file]
dockerfiles/alpine-3.8/Dockerfile [new file with mode: 0644]
dockerfiles/centos-7/Dockerfile
dockerfiles/centos-8/Dockerfile
dockerfiles/debian-10/Dockerfile
dockerfiles/debian-11/Dockerfile [new file with mode: 0644]
dockerfiles/debian-9/Dockerfile [deleted file]
dockerfiles/fedora-32/Dockerfile
dockerfiles/ubuntu-14.04/Dockerfile [deleted file]
dockerfiles/ubuntu-16.04/Dockerfile [deleted file]
dockerfiles/ubuntu-18.04/Dockerfile
dockerfiles/ubuntu-20.04/Dockerfile
misc/ccache.magic [new file with mode: 0644]
misc/codespell-allowlist.txt
misc/performance
misc/test-all-systems
misc/upload-redis [new file with mode: 0755]
src/.clang-tidy
src/Args.cpp
src/Args.hpp
src/ArgsInfo.hpp
src/AtomicFile.cpp
src/AtomicFile.hpp
src/CMakeLists.txt
src/CacheEntryReader.cpp
src/CacheEntryReader.hpp
src/CacheEntryWriter.cpp
src/CacheEntryWriter.hpp
src/CacheFile.cpp [deleted file]
src/CacheFile.hpp [deleted file]
src/Checksum.hpp
src/Compression.cpp [deleted file]
src/Compression.hpp [deleted file]
src/Compressor.cpp [deleted file]
src/Compressor.hpp [deleted file]
src/Config.cpp
src/Config.hpp
src/Context.cpp
src/Context.hpp
src/Counters.cpp [deleted file]
src/Counters.hpp [deleted file]
src/Decompressor.cpp [deleted file]
src/Decompressor.hpp [deleted file]
src/Depfile.cpp
src/Depfile.hpp
src/Digest.hpp
src/Fd.cpp [new file with mode: 0644]
src/Fd.hpp
src/File.hpp
src/Finalizer.hpp
src/FormatNonstdStringView.hpp
src/Hash.cpp
src/Hash.hpp
src/InodeCache.cpp
src/InodeCache.hpp
src/Lockfile.cpp
src/Lockfile.hpp
src/Logging.cpp
src/Logging.hpp
src/Manifest.cpp
src/Manifest.hpp
src/MiniTrace.cpp
src/MiniTrace.hpp
src/NullCompressor.cpp [deleted file]
src/NullCompressor.hpp [deleted file]
src/NullDecompressor.cpp [deleted file]
src/NullDecompressor.hpp [deleted file]
src/ProgressBar.cpp
src/ProgressBar.hpp
src/Result.cpp
src/Result.hpp
src/ResultDumper.hpp
src/ResultExtractor.cpp
src/ResultExtractor.hpp
src/ResultRetriever.cpp
src/ResultRetriever.hpp
src/SignalHandler.cpp
src/SignalHandler.hpp
src/Sloppiness.hpp [deleted file]
src/Stat.cpp
src/Stat.hpp
src/Statistic.hpp [deleted file]
src/Statistics.cpp [deleted file]
src/Statistics.hpp [deleted file]
src/StdMakeUnique.hpp [deleted file]
src/TemporaryFile.cpp
src/TemporaryFile.hpp
src/ThreadPool.hpp
src/UmaskScope.hpp
src/Util.cpp
src/Util.hpp
src/Win32Util.cpp
src/Win32Util.hpp
src/ZstdCompressor.cpp [deleted file]
src/ZstdCompressor.hpp [deleted file]
src/ZstdDecompressor.cpp [deleted file]
src/ZstdDecompressor.hpp [deleted file]
src/argprocessing.cpp
src/argprocessing.hpp
src/assertions.hpp
src/ccache.cpp
src/ccache.hpp
src/cleanup.cpp [deleted file]
src/cleanup.hpp [deleted file]
src/compopt.cpp
src/compopt.hpp
src/compress.cpp [deleted file]
src/compress.hpp [deleted file]
src/compression/CMakeLists.txt [new file with mode: 0644]
src/compression/Compressor.cpp [new file with mode: 0644]
src/compression/Compressor.hpp [new file with mode: 0644]
src/compression/Decompressor.cpp [new file with mode: 0644]
src/compression/Decompressor.hpp [new file with mode: 0644]
src/compression/NullCompressor.cpp [new file with mode: 0644]
src/compression/NullCompressor.hpp [new file with mode: 0644]
src/compression/NullDecompressor.cpp [new file with mode: 0644]
src/compression/NullDecompressor.hpp [new file with mode: 0644]
src/compression/ZstdCompressor.cpp [new file with mode: 0644]
src/compression/ZstdCompressor.hpp [new file with mode: 0644]
src/compression/ZstdDecompressor.cpp [new file with mode: 0644]
src/compression/ZstdDecompressor.hpp [new file with mode: 0644]
src/compression/types.cpp [new file with mode: 0644]
src/compression/types.hpp [new file with mode: 0644]
src/core/CMakeLists.txt [new file with mode: 0644]
src/core/Sloppiness.hpp [new file with mode: 0644]
src/core/Statistic.hpp [new file with mode: 0644]
src/core/Statistics.cpp [new file with mode: 0644]
src/core/Statistics.hpp [new file with mode: 0644]
src/core/StatisticsCounters.cpp [new file with mode: 0644]
src/core/StatisticsCounters.hpp [new file with mode: 0644]
src/core/StatsLog.cpp [new file with mode: 0644]
src/core/StatsLog.hpp [new file with mode: 0644]
src/core/exceptions.hpp [new file with mode: 0644]
src/core/mainoptions.cpp [new file with mode: 0644]
src/core/mainoptions.hpp [new file with mode: 0644]
src/core/types.hpp [new file with mode: 0644]
src/core/wincompat.hpp [new file with mode: 0644]
src/exceptions.hpp [deleted file]
src/execute.cpp
src/execute.hpp
src/fmtmacros.hpp
src/hashutil.cpp
src/hashutil.hpp
src/language.hpp
src/macroskip.hpp
src/storage/CMakeLists.txt [new file with mode: 0644]
src/storage/Storage.cpp [new file with mode: 0644]
src/storage/Storage.hpp [new file with mode: 0644]
src/storage/primary/CMakeLists.txt [new file with mode: 0644]
src/storage/primary/CacheFile.cpp [new file with mode: 0644]
src/storage/primary/CacheFile.hpp [new file with mode: 0644]
src/storage/primary/PrimaryStorage.cpp [new file with mode: 0644]
src/storage/primary/PrimaryStorage.hpp [new file with mode: 0644]
src/storage/primary/PrimaryStorage_cleanup.cpp [new file with mode: 0644]
src/storage/primary/PrimaryStorage_compress.cpp [new file with mode: 0644]
src/storage/primary/PrimaryStorage_statistics.cpp [new file with mode: 0644]
src/storage/primary/StatsFile.cpp [new file with mode: 0644]
src/storage/primary/StatsFile.hpp [new file with mode: 0644]
src/storage/primary/util.cpp [new file with mode: 0644]
src/storage/primary/util.hpp [new file with mode: 0644]
src/storage/secondary/CMakeLists.txt [new file with mode: 0644]
src/storage/secondary/FileStorage.cpp [new file with mode: 0644]
src/storage/secondary/FileStorage.hpp [new file with mode: 0644]
src/storage/secondary/HttpStorage.cpp [new file with mode: 0644]
src/storage/secondary/HttpStorage.hpp [new file with mode: 0644]
src/storage/secondary/RedisStorage.cpp [new file with mode: 0644]
src/storage/secondary/RedisStorage.hpp [new file with mode: 0644]
src/storage/secondary/SecondaryStorage.cpp [new file with mode: 0644]
src/storage/secondary/SecondaryStorage.hpp [new file with mode: 0644]
src/storage/types.hpp [new file with mode: 0644]
src/system.hpp [deleted file]
src/third_party/CMakeLists.txt
src/third_party/blake3/CMakeLists.txt
src/third_party/blake3/blake3.c
src/third_party/blake3/blake3.h
src/third_party/blake3/blake3_avx512_x86-64_windows_msvc.asm
src/third_party/blake3/blake3_sse2_x86-64_unix.S
src/third_party/blake3/blake3_sse2_x86-64_windows_gnu.S
src/third_party/httplib.cpp [new file with mode: 0644]
src/third_party/httplib.h [new file with mode: 0644]
src/third_party/minitrace.c
src/third_party/nonstd/expected.hpp [new file with mode: 0644]
src/third_party/url.cpp [new file with mode: 0644]
src/third_party/url.hpp [new file with mode: 0644]
src/third_party/win32/mktemp.c
src/util/CMakeLists.txt [new file with mode: 0644]
src/util/TextTable.cpp [new file with mode: 0644]
src/util/TextTable.hpp [new file with mode: 0644]
src/util/Timer.hpp [new file with mode: 0644]
src/util/Tokenizer.cpp [new file with mode: 0644]
src/util/Tokenizer.hpp [new file with mode: 0644]
src/util/expected.hpp [new file with mode: 0644]
src/util/file.cpp [new file with mode: 0644]
src/util/file.hpp [new file with mode: 0644]
src/util/path.cpp [new file with mode: 0644]
src/util/path.hpp [new file with mode: 0644]
src/util/string.cpp [new file with mode: 0644]
src/util/string.hpp [new file with mode: 0644]
test/CMakeLists.txt
test/http-client [new file with mode: 0755]
test/http-server [new file with mode: 0755]
test/run
test/suites/base.bash
test/suites/basedir.bash
test/suites/cache_levels.bash
test/suites/cleanup.bash
test/suites/color_diagnostics.bash
test/suites/cpp1.bash
test/suites/debug_prefix_map.bash
test/suites/depend.bash
test/suites/direct.bash
test/suites/direct_gcc.bash
test/suites/fileclone.bash
test/suites/hardlink.bash
test/suites/inode_cache.bash
test/suites/input_charset.bash
test/suites/ivfsoverlay.bash
test/suites/masquerading.bash
test/suites/modules.bash
test/suites/multi_arch.bash
test/suites/no_compression.bash
test/suites/nvcc.bash
test/suites/nvcc_direct.bash
test/suites/nvcc_ldir.bash
test/suites/pch.bash
test/suites/profiling.bash
test/suites/profiling_clang.bash
test/suites/profiling_gcc.bash
test/suites/profiling_hip_clang.bash
test/suites/readonly.bash
test/suites/readonly_direct.bash
test/suites/sanitize_blacklist.bash
test/suites/secondary_file.bash [new file with mode: 0644]
test/suites/secondary_http.bash [new file with mode: 0644]
test/suites/secondary_redis.bash [new file with mode: 0644]
test/suites/secondary_url.bash [new file with mode: 0644]
test/suites/serialize_diagnostics.bash
test/suites/source_date_epoch.bash
test/suites/split_dwarf.bash
test/suites/stats_log.bash [new file with mode: 0644]
test/suites/trim_dir.bash [new file with mode: 0644]
test/suites/upgrade.bash
unittest/CMakeLists.txt
unittest/TestUtil.cpp
unittest/TestUtil.hpp
unittest/test_Compression.cpp [deleted file]
unittest/test_Config.cpp
unittest/test_Counters.cpp [deleted file]
unittest/test_Depfile.cpp
unittest/test_Lockfile.cpp
unittest/test_NullCompression.cpp
unittest/test_Stat.cpp
unittest/test_Statistics.cpp [deleted file]
unittest/test_Util.cpp
unittest/test_ZstdCompression.cpp
unittest/test_argprocessing.cpp
unittest/test_bsdmkstemp.cpp
unittest/test_ccache.cpp
unittest/test_compression_types.cpp [new file with mode: 0644]
unittest/test_core_Statistics.cpp [new file with mode: 0644]
unittest/test_core_StatisticsCounters.cpp [new file with mode: 0644]
unittest/test_core_StatsLog.cpp [new file with mode: 0644]
unittest/test_hashutil.cpp
unittest/test_storage_primary_StatsFile.cpp [new file with mode: 0644]
unittest/test_storage_primary_util.cpp [new file with mode: 0644]
unittest/test_util_TextTable.cpp [new file with mode: 0644]
unittest/test_util_Tokenizer.cpp [new file with mode: 0644]
unittest/test_util_expected.cpp [new file with mode: 0644]
unittest/test_util_path.cpp [new file with mode: 0644]
unittest/test_util_string.cpp [new file with mode: 0644]

index c5ffe564b36d545f58c89a85e72b0473f21f2982..67f0665b28671ff28ad4abb3e95be80c1e50c9a5 100644 (file)
@@ -26,12 +26,18 @@ IncludeBlocks: Regroup
 IncludeCategories:
   - Regex: '^"system.hpp"$'
     Priority: 1
-  - Regex: '^"third_party/'
-    Priority: 3
+  - Regex: '^["<]third_party/'
+    Priority: 4
+  # System headers:
+  - Regex: '\.h>$'
+    Priority: 5
+  # C++ headers:
+  - Regex: '^<[^.]+>$'
+    Priority: 6
   - Regex: '^"'
     Priority: 2
   - Regex: '.*'
-    Priority: 4
+    Priority: 3
 IndentPPDirectives: AfterHash
 KeepEmptyLinesAtTheStartOfBlocks: false
 PointerAlignment: Left
index 07f2904642f7698c999feca2d7af809e6e27db80..36f8a5116a6288b1d276a4a1be36f79a5903f1ae 100644 (file)
@@ -22,14 +22,6 @@ jobs:
       fail-fast: false
       matrix:
         config:
-          - os: ubuntu-16.04
-            compiler: gcc
-            version: "4.8" # results in 4.8.5
-
-          - os: ubuntu-16.04
-            compiler: gcc
-            version: "5"
-
           - os: ubuntu-18.04
             compiler: gcc
             version: "6"
@@ -50,11 +42,11 @@ jobs:
             compiler: gcc
             version: "10"
 
-          - os: ubuntu-16.04
+          - os: ubuntu-18.04
             compiler: clang
             version: "5.0"
 
-          - os: ubuntu-16.04
+          - os: ubuntu-18.04
             compiler: clang
             version: "6.0"
 
@@ -74,6 +66,10 @@ jobs:
             compiler: clang
             version: "10"
 
+          - os: ubuntu-20.04
+            compiler: clang
+            version: "11"
+
           - os: macOS-latest
             compiler: xcode
             version: "10.3"
@@ -91,11 +87,12 @@ jobs:
           if [ "${{ runner.os }}" = "Linux" ]; then
             sudo apt-get update
 
-            # Install ld.gold (binutils) and ld.lld on different runs.
-            if [ "${{ matrix.config.os }}" = "ubuntu-16.04" ]; then
-              sudo apt-get install -y ninja-build elfutils libzstd1-dev binutils
+            packages="elfutils libhiredis-dev libzstd-dev ninja-build pkg-config python3 redis-server redis-tools"
+            # Install ld.gold (binutils) and ld.lld (lld) on different runs.
+            if [ "${{ matrix.config.os }}" = "ubuntu-18.04" ]; then
+              sudo apt-get install -y $packages binutils
             else
-              sudo apt-get install -y ninja-build elfutils libzstd-dev lld
+              sudo apt-get install -y $packages lld
             fi
 
             if [ "${{ matrix.config.compiler }}" = "gcc" ]; then
@@ -111,7 +108,7 @@ jobs:
             fi
           elif [ "${{ runner.os }}" = "macOS" ]; then
             HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \
-              brew install ninja
+              brew install ninja pkg-config hiredis redis
 
             if [ "${{ matrix.config.compiler }}" = "gcc" ]; then
               brew install gcc@${{ matrix.config.version }}
@@ -151,15 +148,15 @@ jobs:
       fail-fast: false
       matrix:
         config:
-          - name: Linux GCC debug + C++14 + in source + tracing
+          - name: Linux GCC debug + in source + tracing
             os: ubuntu-18.04
             CC: gcc
             CXX: g++
             ENABLE_CACHE_CLEANUP_TESTS: 1
             BUILDDIR: .
             CCACHE_LOC: .
-            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=Debug -DENABLE_TRACING=1 -DCMAKE_CXX_STANDARD=14
-            apt_get: elfutils libzstd-dev
+            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=Debug -DENABLE_TRACING=1
+            apt_get: elfutils libzstd-dev pkg-config libhiredis-dev
 
           - name: Linux GCC 32-bit
             os: ubuntu-18.04
@@ -168,7 +165,7 @@ jobs:
             CFLAGS: -m32 -g -O2
             CXXFLAGS: -m32 -g -O2
             LDFLAGS: -m32
-            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=CI -DZSTD_FROM_INTERNET=ON
+            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=CI -DZSTD_FROM_INTERNET=ON -DHIREDIS_FROM_INTERNET=ON
             ENABLE_CACHE_CLEANUP_TESTS: 1
             apt_get: elfutils gcc-multilib g++-multilib lib32stdc++-5-dev
 
@@ -176,7 +173,7 @@ jobs:
             os: ubuntu-18.04
             CC: gcc
             CXX: g++
-            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=CI -DZSTD_FROM_INTERNET=ON
+            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=CI -DZSTD_FROM_INTERNET=ON -DHIREDIS_FROM_INTERNET=ON
             ENABLE_CACHE_CLEANUP_TESTS: 1
             CUDA: 10.1.243-1
             apt_get: elfutils libzstd-dev
@@ -185,7 +182,7 @@ jobs:
             os: ubuntu-18.04
             CC: i686-w64-mingw32-gcc-posix
             CXX: i686-w64-mingw32-g++-posix
-            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=CI -DCMAKE_SYSTEM_NAME=Windows -DZSTD_FROM_INTERNET=ON
+            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=CI -DCMAKE_SYSTEM_NAME=Windows -DZSTD_FROM_INTERNET=ON -DHIREDIS_FROM_INTERNET=ON
             RUN_TESTS: none
             apt_get: elfutils mingw-w64
 
@@ -194,7 +191,7 @@ jobs:
             CC: x86_64-w64-mingw32-gcc-posix
             CXX: x86_64-w64-mingw32-g++-posix
             ENABLE_CACHE_CLEANUP_TESTS: 1
-            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=CI -DCMAKE_SYSTEM_NAME=Windows -DZSTD_FROM_INTERNET=ON
+            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=CI -DCMAKE_SYSTEM_NAME=Windows -DZSTD_FROM_INTERNET=ON -DHIREDIS_FROM_INTERNET=ON
             RUN_TESTS: unittest-in-wine
             apt_get: elfutils mingw-w64 wine
 
@@ -206,7 +203,7 @@ jobs:
             CXX: cl
             ENABLE_CACHE_CLEANUP_TESTS: 1
             CMAKE_GENERATOR: Ninja
-            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=CI -DZSTD_FROM_INTERNET=ON
+            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=CI -DZSTD_FROM_INTERNET=ON -DHIREDIS_FROM_INTERNET=ON
             TEST_CC: clang -target i686-pc-windows-msvc
 
           - name: Windows VS2019 64-bit
@@ -217,7 +214,7 @@ jobs:
             CXX: cl
             ENABLE_CACHE_CLEANUP_TESTS: 1
             CMAKE_GENERATOR: Ninja
-            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=CI -DZSTD_FROM_INTERNET=ON
+            CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=CI -DZSTD_FROM_INTERNET=ON -DHIREDIS_FROM_INTERNET=ON
             TEST_CC: clang -target x86_64-pc-windows-msvc
 
           - name: Clang address & UB sanitizer
@@ -227,7 +224,7 @@ jobs:
             ENABLE_CACHE_CLEANUP_TESTS: 1
             CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=CI -DENABLE_SANITIZER_ADDRESS=ON -DENABLE_SANITIZER_UNDEFINED_BEHAVIOR=ON
             ASAN_OPTIONS: detect_leaks=0
-            apt_get: elfutils libzstd-dev
+            apt_get: elfutils libzstd-dev pkg-config libhiredis-dev
 
           - name: Clang static analyzer
             os: ubuntu-20.04
@@ -236,7 +233,7 @@ jobs:
             ENABLE_CACHE_CLEANUP_TESTS: 1
             CMAKE_PREFIX: scan-build
             RUN_TESTS: none
-            apt_get: libzstd-dev
+            apt_get: libzstd-dev pkg-config libhiredis-dev
 
           - name: Linux binary
             os: ubuntu-20.04
@@ -244,26 +241,26 @@ jobs:
             CXX: g++
             SPECIAL: build-and-verify-package
             CMAKE_PARAMS: -DCMAKE_BUILD_TYPE=Release
-            apt_get: elfutils libzstd-dev ninja-build
+            apt_get: elfutils libzstd-dev pkg-config libhiredis-dev ninja-build
 
           - name: Source package
             os: ubuntu-20.04
             CC: gcc
             CXX: g++
             SPECIAL: build-and-verify-source-package
-            apt_get: elfutils libzstd-dev ninja-build asciidoc xsltproc docbook-xml docbook-xsl
+            apt_get: elfutils libzstd-dev pkg-config libhiredis-dev ninja-build asciidoctor
 
           - name: HTML documentation
             os: ubuntu-18.04
             EXTRA_CMAKE_BUILD_FLAGS: --target doc-html
             RUN_TESTS: none
-            apt_get: libzstd-dev asciidoc docbook-xml docbook-xsl
+            apt_get: libzstd-dev pkg-config libhiredis-dev asciidoctor
 
           - name: Manual page
             os: ubuntu-18.04
             EXTRA_CMAKE_BUILD_FLAGS: --target doc-man-page
             RUN_TESTS: none
-            apt_get: libzstd-dev asciidoc xsltproc docbook-xml docbook-xsl
+            apt_get: libzstd-dev pkg-config libhiredis-dev asciidoctor
 
           - name: Clang-Tidy
             os: ubuntu-18.04
@@ -271,7 +268,7 @@ jobs:
             CXX: clang++-9
             RUN_TESTS: none
             CMAKE_PARAMS: -DENABLE_CLANG_TIDY=ON -DCLANGTIDY=/usr/bin/clang-tidy-9
-            apt_get: libzstd-dev clang-9 clang-tidy-9
+            apt_get: libzstd-dev pkg-config libhiredis-dev clang-9 clang-tidy-9
 
     steps:
       - name: Get source
@@ -371,7 +368,10 @@ jobs:
         uses: actions/checkout@v2
 
       - name: Install codespell
-        run: sudo apt-get update && sudo apt-get install codespell
+        run: |
+          sudo apt-get update
+          sudo apt-get install python3-pip
+          pip3 install codespell==2.1.0
 
       - name: Run codespell
-        run: codespell -q 7 -S ".git,LICENSE.adoc,./src/third_party/*" -I misc/codespell-allowlist.txt
+        run: codespell -q 7 -S ".git,build*,./src/third_party/*" -I misc/codespell-allowlist.txt
index b19e807632d29d79a3808663342f7d3d4d3c924f..e159d92e6e1e7e1e9cfd8b24ea80070bf217a20f 100644 (file)
@@ -31,7 +31,7 @@ jobs:
         fetch-depth: 2
 
     - name: Install dependencies
-      run: sudo apt-get update && sudo apt-get install ninja-build elfutils libzstd-dev
+      run: sudo apt-get update && sudo apt-get install ninja-build elfutils libzstd-dev pkg-config libhiredis-dev
 
     - name: Initialize CodeQL
       uses: github/codeql-action/init@v1
index 74db15fb4e6dc481978e594ea9280439da606569..d8e547f35d87da46dfc0c4bf5a2be038dcf9a4fb 100644 (file)
@@ -1,6 +1,10 @@
 # Typical build directories
 /build*/
 
+# CLion
+/.idea/
+/cmake-build-*/
+
 # Downloaded tools
 misc/.clang-format-exe
 
index 7e59d6e6c85eb0ec0e4a7fa1170ebead1a9c50bb..568c89a0d4b4b528cc8d5b9488485605ddeef26b 100644 (file)
--- a/.mailmap
+++ b/.mailmap
@@ -25,6 +25,7 @@ Peter Budai <peterbudai@hotmail.com>
 Ramiro Polla <ramiro.polla@gmail.com>
 Ramiro Polla <ramiro.polla@gmail.com> <ramiro@mac-vbox-ubuntu910.(none)>
 Ryan Brown <ryb@ableton.com>
+Ryan Burns <52847440+r-burns@users.noreply.github.com>
 Thomas Otto <39962140+totph@users.noreply.github.com>
 Thomas Röfer <Thomas.Roefer@dfki.de>
 Tor Arne Vestbø <tor.arne.vestbo@qt.io> <torarnv@gmail.com>
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644 (file)
index 0000000..20292bf
--- /dev/null
@@ -0,0 +1,35 @@
+# Ccache architecture
+
+## Code structure
+
+### Top-level directories
+
+* `ci`: Utility scripts used in CI.
+* `cmake`: CMake scripts.
+* `doc`: Documentation.
+* `dockerfiles`: Dockerfiles that specify different environments of interest for
+  ccache.
+* `misc`: Miscellaneous utility scripts, example files, etc.
+* `src`: Source code. See below.
+* `test`: Integration test suite which tests the ccache binary in different
+  scenarios.
+* `unittest`: Unit test suite which typically tests individual functions.
+
+### Subdirectories of `src`
+
+This section describes the directory structure that the project aims to
+transform the `src` directory into in the long run to make the code base easier
+to understand and work with. In other words, this is work in progress.
+
+* `compiler`: Knowledge about things like compiler options, compiler behavior,
+  preprocessor output format, etc. Ideally this code should in the future be
+  refactored into compiler-specific frontends, such as GCC, Clang, NVCC, MSVC,
+  etc.
+* `compression`: Compression formats.
+* `core`: Everything not part of other directories.
+* `storage`: Storage backends.
+* `storage/primary`: Code for the primary storage backend.
+* `storage/secondary`: Code for secondary storage backends.
+* `third_party`: Bundled third party code.
+* `util`: Generic utility functionality that does not depend on ccache-specific
+  things.
index e186e8128bc4d152f247333420941941d016d284..4cf2e5f3e4effdcb096fed558364c75af0968b33 100644 (file)
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.4.3)
+cmake_minimum_required(VERSION 3.10)
 
 project(ccache LANGUAGES C CXX)
 if(MSVC)
@@ -8,8 +8,10 @@ else()
 endif()
 set(CMAKE_PROJECT_DESCRIPTION "a fast C/C++ compiler cache")
 
-if(NOT "${CMAKE_CXX_STANDARD}")
-  set(CMAKE_CXX_STANDARD 11)
+if(MSVC)
+  set(CMAKE_CXX_STANDARD 17) # Need support for std::filesystem
+else()
+  set(CMAKE_CXX_STANDARD 14)
 endif()
 set(CMAKE_CXX_STANDARD_REQUIRED YES)
 set(CMAKE_CXX_EXTENSIONS NO)
@@ -28,10 +30,8 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
 # C++ error messages)
 #
 
-# Clang 3.4 and AppleClang 6.0 fail to compile doctest.
-# GCC 4.8.4 is known to work OK but give warnings.
-if((CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 3.5)
-   OR (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.8.4)
+if((CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 3.8)
+   OR (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5)
    OR (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7.0))
   message(
     FATAL_ERROR
@@ -39,10 +39,8 @@ if((CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSIO
     "You need one listed here: https://ccache.dev/platform-compiler-language-support.html")
 endif()
 
-# All Clang problems / special handling ccache has are because of version 3.5.
-# All GCC problems / special handling ccache has are because of version 4.8.4.
-if((CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 3.6)
-    OR (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5.0))
+if((CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4)
+    OR (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 6))
   message(
     WARNING
     "The compiler you are using is rather old.\n"
@@ -58,28 +56,25 @@ endif()
 #
 include(CcacheVersion)
 
-if("${CCACHE_VERSION_ORIGIN}" STREQUAL git OR DEFINED ENV{CI})
-  set(CCACHE_DEV_MODE ON)
-else()
-  set(CCACHE_DEV_MODE OFF)
+if(NOT DEFINED CCACHE_DEV_MODE)
+  if("${CCACHE_VERSION_ORIGIN}" STREQUAL git OR DEFINED ENV{CI})
+    set(CCACHE_DEV_MODE ON)
+  else()
+    set(CCACHE_DEV_MODE OFF)
+  endif()
 endif()
 message(STATUS "Ccache dev mode: ${CCACHE_DEV_MODE}")
 
 include(UseCcache)
-if(NOT MSVC)
-  include(UseFastestLinker)
-endif()
+include(UseFastestLinker)
 include(StandardSettings)
 include(StandardWarnings)
 include(CIBuildType)
 include(DefaultBuildType)
 
-if(NOT ${CMAKE_VERSION} VERSION_LESS "3.9")
-  cmake_policy(SET CMP0069 NEW)
-  option(ENABLE_IPO "Enable interprocedural (link time, LTO) optimization" OFF)
-  if(ENABLE_IPO)
-    set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
-  endif()
+option(ENABLE_IPO "Enable interprocedural (link time, LTO) optimization" OFF)
+if(ENABLE_IPO)
+  set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
 endif()
 
 #
@@ -89,23 +84,27 @@ include(GNUInstallDirs)
 include(GenerateConfigurationFile)
 include(GenerateVersionFile)
 
-if(HAVE_SYS_MMAN_H AND HAVE_PTHREAD_MUTEXATTR_SETPSHARED)
-  set(INODE_CACHE_SUPPORTED 1)
-endif()
-
 #
 # Third party
 #
+set(HIREDIS_FROM_INTERNET_DEFAULT OFF)
 set(ZSTD_FROM_INTERNET_DEFAULT OFF)
 
 # Default to downloading deps for Visual Studio, unless using a package manager.
 if(MSVC AND NOT CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg|conan")
+  set(HIREDIS_FROM_INTERNET_DEFAULT ON)
   set(ZSTD_FROM_INTERNET_DEFAULT ON)
 endif()
 
 option(ZSTD_FROM_INTERNET "Download and use libzstd from the Internet" ${ZSTD_FROM_INTERNET_DEFAULT})
 find_package(zstd 1.1.2 REQUIRED)
 
+option(REDIS_STORAGE_BACKEND "Enable Redis secondary storage" ON)
+if(REDIS_STORAGE_BACKEND)
+  option(HIREDIS_FROM_INTERNET "Download and use libhiredis from the Internet" ${HIREDIS_FROM_INTERNET_DEFAULT})
+  find_package(hiredis 0.13.3 REQUIRED)
+endif()
+
 #
 # Special flags
 #
@@ -128,7 +127,7 @@ add_subdirectory(src)
 # ccache executable
 #
 add_executable(ccache src/main.cpp)
-target_link_libraries(ccache PRIVATE standard_settings standard_warnings ccache_lib)
+target_link_libraries(ccache PRIVATE standard_settings standard_warnings ccache_framework)
 
 #
 # Documentation
index ad4b8aa85d248737a51ff1ebd5084cbc9f88273a..a285535a97c9ebcc861f1a2edfa040546d5828bb 100644 (file)
@@ -33,6 +33,7 @@ proposal(s) on [GitHub](https://github.com/ccache/ccache).
 
 Here are some hints to make the process smoother:
 
+* Have a look in `ARCHITECTURE.md` for an overview of the source code tree.
 * If you plan to implement major changes it is wise to open an issue on GitHub
   (or ask in the Gitter room, or send a mail to the mailing list) asking for
   comments on your plans before doing the bulk of the work. That way you can
@@ -42,17 +43,42 @@ Here are some hints to make the process smoother:
   for merging yet but you want early comments and CI test results? Then create a
   draft pull request as described in [this Github blog
   post](https://github.blog/2019-02-14-introducing-draft-pull-requests/).
+* Please add test cases for your new changes if applicable.
 * Please follow the ccache's code style (see the section below).
-* Consider [A Note About Git Commit
-  Messages](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
-  when writing commit messages.
 
-## Code style
+## Commit message conventions
+
+It is preferable, but not mandatory, to format commit messages in the spirit of
+[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). The
+commit message subject should look like this:
+
+        <type>: <description>
+        <type>(<scope>): <description>
+
+`<description>` is a succinct description of the change:
 
-Ccache was written in C99 until 2019 when it started being converted to C++11.
-The conversion is a slow work in progress, which is why there is some C-style
-code left. Please refrain from doing large C to C++ conversions; do it little by
-little.
+* Use the imperative, present tense: "Change", not "Changed" nor "Changes".
+* Capitalize the first letter.
+* No dot (`.`) at the end.
+
+Here is a summary of types used for ccache:
+
+| Type         | Explanation |
+| ------------ | ----------- |
+| **build**    | A change of the build system or build configuration. |
+| **bump**     | An increase of the version of an external dependency or an update of a bundled third party package. |
+| **chore**    | A change that doesn't match any other type. |
+| **ci**       | A change of CI scripts or configuration. |
+| **docs**     | A change of documentation only. |
+| **enhance**  | An enhancement of the code without adding a user-visible feature, for example adding a new utility class to be used by a future feature or refactoring. |
+| **feat**     | An addition or improvement of a user-visible feature. |
+| **fix**      | A bug fix (not necessarily user-visible). |
+| **perf**     | A performance improvement. |
+| **refactor** | A restructuring of the existing code without changing its external behavior. |
+| **style**    | A change of code style. |
+| **test**     | An addition or modification of tests or test framework. |
+
+## Code style
 
 Source code formatting is defined by `.clang-format` in the root directory. The
 format is loosely based on [LLVM's code formatting
@@ -65,10 +91,11 @@ fine.
 
 Please follow these conventions:
 
-* Use `UpperCamelCase` for types (e.g. classes and structs) and namespaces.
-* Use `UPPER_CASE` names for macros and (non-class )enum values.
-* Use `snake_case` for other names (functions, variables, enum class values,
-  etc.).
+* Use `UpperCamelCase` for types (e.g. classes and structs).
+* Use `UPPER_CASE` names for macros and (non-class) enum values.
+* Use `snake_case` for other names (namespaces, functions, variables, enum class
+  values, etc.). (Namespaces used to be in `UpperCamelCase`; transition is work
+  in progress.)
 * Use an `m_` prefix for non-public member variables.
 * Use a `g_` prefix for global mutable variables.
 * Use a `k_` prefix for global constants.
index 4c6df86f88a1b40b3f64bfd35eddaed585448771..361fc99de7110add6934370d967e71d5380cde23 100644 (file)
@@ -1,12 +1,10 @@
-Ccache copyright and license
-============================
+= Ccache copyright and license
 
-Overall license
----------------
+== Overall license
 
 The license for ccache as a whole is as follows:
 
--------------------------------------------------------------------------------
+----
 This program is free software; you can redistribute it and/or modify it under
 the terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3 of the License, or (at your option) any later
@@ -19,14 +17,13 @@ PARTICULAR PURPOSE. See the GNU General Public License for more details.
 You should have received a copy of the GNU General Public License along with
 this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
 Street, Fifth Floor, Boston, MA 02110-1301 USA
--------------------------------------------------------------------------------
+----
 
 The full license text can be found in GPL-3.0.txt and at
 https://www.gnu.org/licenses/gpl-3.0.html.
 
 
-Copyright and authors
----------------------
+== Copyright and authors
 
 Ccache is a collective work with contributions from many people, listed in
 AUTHORS.adoc and at https://ccache.dev/credits.html. Subsequent additions by
@@ -36,14 +33,13 @@ their portions of the work.
 
 The copyright for ccache as a whole is as follows:
 
--------------------------------------------------------------------------------
+----
 Copyright (C) 2002-2007 Andrew Tridgell
 Copyright (C) 2009-2021 Joel Rosdahl and other contributors
--------------------------------------------------------------------------------
+----
 
 
-Files derived from other sources
---------------------------------
+== Files derived from other sources
 
 The ccache distribution contain some files from other sources and some have
 been modified for use in ccache. These files all carry attribution notices, and
@@ -52,13 +48,12 @@ the GPL: that is, if separated from the ccache sources, they may be usable
 under less restrictive terms.
 
 
-src/third_party/base32hex.*
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== src/third_party/base32hex.*
 
 This base32hex implementation comes from
 <https://github.com/pmconrad/tinydnssec>.
 
--------------------------------------------------------------------------------
+----
 (C) 2012 Peter Conrad <conrad@quisquis.de>
 
 This program is free software: you can redistribute it and/or modify
@@ -72,20 +67,18 @@ GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
--------------------------------------------------------------------------------
+----
 
 
-src/third_party/blake3/blake3_*
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== src/third_party/blake3/blake3_*
 
-This is a subset of https://github.com/BLAKE3-team/BLAKE3[BLAKE3] 0.3.7 with
+This is a subset of https://github.com/BLAKE3-team/BLAKE3[BLAKE3] 1.0.0 with
 the following license:
 
--------------------------------------------------------------------------------
+----
 This work is released into the public domain with CC0 1.0. Alternatively, it is
 licensed under the Apache License 2.0.
 
--------------------------------------------------------------------------------
 -------------------------------------------------------------------------------
 
 Creative Commons Legal Code
@@ -210,7 +203,6 @@ express Statement of Purpose.
     party to this document and has no duty or obligation with respect to
     this CC0 or use of the Work.
 
--------------------------------------------------------------------------------
 -------------------------------------------------------------------------------
 
                                  Apache License
@@ -414,16 +406,15 @@ express Statement of Purpose.
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
--------------------------------------------------------------------------------
+----
 
 
-src/third_party/doctest.h
-~~~~~~~~~~~~~~~~~~~~~~~~~
+=== src/third_party/doctest.h
 
 This is the single header version of https://github.com/onqtam/doctest[doctest]
 2.4.6 with the following license:
 
--------------------------------------------------------------------------------
+----
 The MIT License (MIT)
 
 Copyright (c) 2016-2021 Viktor Kirilov
@@ -445,15 +436,14 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
--------------------------------------------------------------------------------
+----
 
 
-src/third_party/fmt/*.h and src/third_party/format.cpp
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== src/third_party/fmt/*.h and src/third_party/format.cpp
 
 This is a subset of https://fmt.dev[fmt] 7.1.3 with the following license:
 
--------------------------------------------------------------------------------
+----
 Formatting library for C++
 
 Copyright (c) 2012 - present, Victor Zverovich
@@ -482,16 +472,15 @@ As an exception, if, as a result of your compiling your source code, portions
 of this Software are embedded into a machine-executable object form of such
 source code, you may redistribute such embedded portions in such object form
 without including the above copyright and permission notices.
--------------------------------------------------------------------------------
+----
 
 
-src/third_party/getopt_long.*
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== src/third_party/getopt_long.*
 
 This implementation of `getopt_long()` was copied from
 https://www.postgresql.org[PostgreSQL] and has the following license text:
 
--------------------------------------------------------------------------------
+----
 Portions Copyright (c) 1987, 1993, 1994
 The Regents of the University of California.  All rights reserved.
 
@@ -521,16 +510,46 @@ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 SUCH DAMAGE.
--------------------------------------------------------------------------------
+----
+
+
+=== src/third_party/httplib.*
+
+cpp-httplib - A C++11 cross-platform HTTP/HTTPS library. Copied from
+https://github.com/yhirose/cpp-httplib[cpp-httplib] commit
+469c6bc2b611ec5d212275e559e58e4da256019d. The library has the following license:
+
+----
+The MIT License (MIT)
+
+Copyright (c) 2021 yhirose
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+----
 
 
-src/third_party/minitrace.*
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== src/third_party/minitrace.*
 
 A library for producing JSON traces suitable for Chrome's built-in trace viewer
 (chrome://tracing). Downloaded from <https://github.com/hrydgard/minitrace>.
 
--------------------------------------------------------------------------------
+----
 The MIT License (MIT)
 
 Copyright (c) 2014 Henrik Rydgård
@@ -552,17 +571,51 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
--------------------------------------------------------------------------------
+----
+
+
+=== src/third_party/nonstd/expected.hpp
+
+This is the single header version of
+https://github.com/martinmoene/expected-lite[expected-lite] 0.5.0 with the
+following license:
+
+----
+Copyright (c) 2016-2018 Martin Moene
+
+Boost Software License - Version 1.0 - August 17th, 2003
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third-parties to whom the Software is furnished to
+do so, all subject to the following:
 
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works of the Software, unless such copies or derivative
+works are solely in the form of machine-executable object code generated by
+a source language processor.
 
-src/third_party/nonstd/optional.hpp
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+----
+
+
+=== src/third_party/nonstd/optional.hpp
 
 This is the single header version of
 https://github.com/martinmoene/optional-lite[optional-lite] 3.4.0 with the
 following license:
 
--------------------------------------------------------------------------------
+----
 Copyright (c) 2014-2018 Martin Moene
 
 Boost Software License - Version 1.0 - August 17th, 2003
@@ -588,17 +641,16 @@ SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
+----
 
 
-src/third_party/nonstd/string_view.hpp
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== src/third_party/nonstd/string_view.hpp
 
 This alternative implementation of `std::string_view` was downloaded from
 <https://github.com/martinmoene/string-view-lite> and has the following license
 text:
 
--------------------------------------------------------------------------------
+----
 Copyright 2017-2020 by Martin Moene
 
 Boost Software License - Version 1.0 - August 17th, 2003
@@ -624,11 +676,10 @@ SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
+----
 
 
-src/third_party/win32/getopt.*
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== src/third_party/win32/getopt.*
 
 This implementation of `getopt_long()` for Win32 was taken from
 https://www.codeproject.com/Articles/157001/Full-getopt-Port-for-Unicode-and-Multibyte-Microso
@@ -638,14 +689,13 @@ The full license text can be found in LGPL-3.0.txt and at
 https://www.gnu.org/licenses/lgpl-3.0.html.
 
 
-src/third_party/win32/mktemp.*
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== src/third_party/win32/mktemp.*
 
 This implementation of `mkstemp()` for Win32 was adapted from
 <https://github.com/openbsd/src/blob/99b791d14c0f1858d87a0c33b55880fb9b00be66/lib/libc/stdio/mktemp.c>
-and has the folowing license text:
+and has the following license text:
 
--------------------------------------------------------------------------------
+----
 Copyright (c) 1996-1998, 2008 Theo de Raadt
 Copyright (c) 1997, 2008-2009 Todd C. Miller
 
@@ -660,16 +710,46 @@ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--------------------------------------------------------------------------------
+----
+
+
+=== src/third_party/url.*
+
+CxxUrl - A simple C++ URL class. Copied from CxxUrl v0.2 downloaded from
+<https://github.com/chmike/CxxUrl>. It has the following license text:
 
-src/third_party/win32/winerror_to_errno.h
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+----
+The MIT License (MIT)
+
+Copyright (c) 2015 Christophe Meessen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+----
+
+
+=== src/third_party/win32/winerror_to_errno.h
 
 The implementation of `winerror_to_errno()` was adapted from
 <https://github.com/python/cpython/blob/1a79785e3e8fea80bcf6a800b45a04e06c787480/PC/errmap.h>
-and has the folowing license text:
+and has the following license text:
 
--------------------------------------------------------------------------------
+----
 PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
 
 1. This LICENSE AGREEMENT is between the Python Software Foundation
@@ -717,15 +797,15 @@ products or services of Licensee, or any third party.
 8. By copying, installing or otherwise using Python, Licensee
 agrees to be bound by the terms and conditions of this License
 Agreement.
--------------------------------------------------------------------------------
+----
 
-src/third_party/xxh*
-~~~~~~~~~~~~~~~~~~~~
+
+=== src/third_party/xxh*
 
 xxHash - Extremely Fast Hash algorithm. Copied from xxHash v0.8.0 downloaded
 from <https://github.com/Cyan4973/xxHash/releases>.
 
--------------------------------------------------------------------------------
+----
 Copyright (c) 2012-2020 Yann Collet
 All rights reserved.
 
@@ -753,4 +833,4 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
+----
index 57ae2ca3e0d8151a769c40be1d100ed9dfd46ba2..9df170ea4992bacf32945fd011509949c6838b34 100644 (file)
--- a/README.md
+++ b/README.md
@@ -4,6 +4,7 @@ Ccache – a fast compiler cache
 [![Build Status](https://github.com/ccache/ccache/workflows/Build/badge.svg)](https://github.com/ccache/ccache/actions?query=workflow%3A%22Build%22)
 [![Code Quality: Cpp](https://img.shields.io/lgtm/grade/cpp/g/ccache/ccache.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ccache/ccache/context:cpp)
 [![Total Alerts](https://img.shields.io/lgtm/alerts/g/ccache/ccache.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ccache/ccache/alerts)
+[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5057/badge)](https://bestpractices.coreinfrastructure.org/projects/5057)
 [![Gitter](https://img.shields.io/gitter/room/ccache/ccache.svg)](https://gitter.im/ccache/ccache)
 
 Ccache (or “ccache”) is a compiler cache. It [speeds up
index 21a3971f91ff13d11ca809a6d2dbbb53f14fed22..fa08bdd22b3e7f9bd022c9cf687df0b7f03a9229 100755 (executable)
@@ -6,7 +6,7 @@ elif [ -d build/testdir ]; then
     testdir=build/testdir
 else
     echo "No testdir found" >&2
-    exit 1
+    exit 0
 fi
 
 tar -caf testdir.tar.xz $testdir
index a35949da5224d8240d13193ff5c8ab15eb65b43e..c41aeca998c618f59fcdc7b12f3a37746ac9b721 100644 (file)
@@ -1,10 +1,6 @@
 # Note: This is part of CMakeLists.txt file, not to be confused with
 # CPackConfig.cmake.
 
-if(${CMAKE_VERSION} VERSION_LESS "3.9")
-  set(CPACK_PACKAGE_DESCRIPTION "${CMAKE_PROJECT_DESCRIPTION}")
-endif()
-
 # From CcacheVersion.cmake.
 set(CPACK_PACKAGE_VERSION ${CCACHE_VERSION})
 
index c0cbd91094a39ff46d3124c5e8ecf1e04dfb22d1..fbd2e2ded1dee31973e87554081a4e3374825913 100644 (file)
@@ -22,7 +22,7 @@
 # CCACHE_VERSION_ORIGIN is set to "archive" in scenario 1 and "git" in scenario
 # 3.
 
-set(version_info "2c13bce2609a02b70c92e84cdd177a6072f44d5e HEAD, tag: v4.3, origin/master, origin/HEAD, master")
+set(version_info "6ac58b50ec7ad402395acba9b1a5fdcbeabf347d HEAD, tag: v4.4, origin/master, origin/HEAD, master")
 
 if(version_info MATCHES "^([0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f])[0-9a-f]* (.*)")
   # Scenario 1.
@@ -48,7 +48,7 @@ elseif(EXISTS "${CMAKE_SOURCE_DIR}/.git")
   else()
     macro(git)
       execute_process(
-        COMMAND "${GIT_EXECUTABLE}" ${ARGN}
+        COMMAND "${GIT_EXECUTABLE}" -C "${CMAKE_SOURCE_DIR}" ${ARGN}
         OUTPUT_VARIABLE git_stdout OUTPUT_STRIP_TRAILING_WHITESPACE
         ERROR_VARIABLE git_stderr ERROR_STRIP_TRAILING_WHITESPACE)
     endmacro()
index 4e639a78629ed48e248dfba6ad57bacea1db317d..0d3cf024a6fb3465d5f703a18f1acf093daf84da 100644 (file)
@@ -1,38 +1,30 @@
 option(ENABLE_CPPCHECK "Enable static analysis with Cppcheck" OFF)
 if(ENABLE_CPPCHECK)
-  if(${CMAKE_VERSION} VERSION_LESS "3.10")
-    message(WARNING "Cppcheck requires CMake 3.10")
+  find_program(CPPCHECK_EXE cppcheck)
+  mark_as_advanced(CPPCHECK_EXE) # Don't show in CMake UIs
+  if(CPPCHECK_EXE)
+    set(CMAKE_CXX_CPPCHECK
+        ${CPPCHECK_EXE}
+        --suppressions-list=${CMAKE_SOURCE_DIR}/misc/cppcheck-suppressions.txt
+        --inline-suppr
+        -q
+        --enable=all
+        --force
+        --std=c++14
+        -I ${CMAKE_SOURCE_DIR}
+        --template="cppcheck: warning: {id}:{file}:{line}: {message}"
+        -i src/third_party)
   else()
-    find_program(CPPCHECK_EXE cppcheck)
-    mark_as_advanced(CPPCHECK_EXE) # Don't show in CMake UIs
-    if(CPPCHECK_EXE)
-      set(CMAKE_CXX_CPPCHECK
-          ${CPPCHECK_EXE}
-          --suppressions-list=${CMAKE_SOURCE_DIR}/misc/cppcheck-suppressions.txt
-          --inline-suppr
-          -q
-          --enable=all
-          --force
-          --std=c++11
-          -I ${CMAKE_SOURCE_DIR}
-          --template="cppcheck: warning: {id}:{file}:{line}: {message}"
-          -i src/third_party)
-    else()
-      message(WARNING "Cppcheck requested but executable not found")
-    endif()
+    message(WARNING "Cppcheck requested but executable not found")
   endif()
 endif()
 
 option(ENABLE_CLANG_TIDY "Enable static analysis with Clang-Tidy" OFF)
 if(ENABLE_CLANG_TIDY)
-  if(${CMAKE_VERSION} VERSION_LESS "3.6")
-    message(WARNING "Clang-Tidy requires CMake 3.6")
+  find_program(CLANGTIDY clang-tidy)
+  if(CLANGTIDY)
+    set(CMAKE_CXX_CLANG_TIDY ${CLANGTIDY})
   else()
-    find_program(CLANGTIDY clang-tidy)
-    if(CLANGTIDY)
-      set(CMAKE_CXX_CLANG_TIDY ${CLANGTIDY})
-    else()
-      message(SEND_ERROR "Clang-Tidy requested but executable not found")
-    endif()
+    message(SEND_ERROR "Clang-Tidy requested but executable not found")
   endif()
 endif()
index 7ff5411b89acd3ff7f47026b557c8d758f27323b..ee464763c5b0219cfe3234cdcbf9d1e21ae4cf9a 100644 (file)
@@ -82,8 +82,6 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
     CCACHE_COMPILER_WARNINGS "-Wno-zero-as-null-pointer-constant")
   add_compile_flag_if_supported(
     CCACHE_COMPILER_WARNINGS "-Wno-undefined-func-template")
-  add_compile_flag_if_supported(
-    CCACHE_COMPILER_WARNINGS "-Wno-return-std-move-in-c++11")
 elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
   list(
     APPEND
diff --git a/cmake/Findhiredis.cmake b/cmake/Findhiredis.cmake
new file mode 100644 (file)
index 0000000..1369637
--- /dev/null
@@ -0,0 +1,84 @@
+if(HIREDIS_FROM_INTERNET)
+  set(hiredis_version "1.0.0")
+  set(hiredis_url https://github.com/redis/hiredis/archive/v${hiredis_version}.tar.gz)
+
+  set(hiredis_dir ${CMAKE_BINARY_DIR}/hiredis-${hiredis_version})
+  set(hiredis_build ${CMAKE_BINARY_DIR}/hiredis-build)
+
+  if(NOT EXISTS "${hiredis_dir}.tar.gz")
+    file(DOWNLOAD "${hiredis_url}" "${hiredis_dir}.tar.gz" STATUS download_status)
+    list(GET download_status 0 error_code)
+    if(error_code)
+      file(REMOVE "${hiredis_dir}.tar.gz")
+      list(GET download_status 1 error_message)
+      message(FATAL "Failed to download hiredis: ${error_message}")
+    endif()
+  endif()
+
+  execute_process(
+    COMMAND tar xf "${hiredis_dir}.tar.gz"
+    WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
+    RESULT_VARIABLE tar_error)
+  if(NOT tar_error EQUAL 0)
+    message(FATAL "extracting ${hiredis_dir}.tar.gz failed")
+  endif()
+
+  set(
+    hiredis_sources
+    "${hiredis_dir}/alloc.c"
+    "${hiredis_dir}/async.c"
+    "${hiredis_dir}/dict.c"
+    "${hiredis_dir}/hiredis.c"
+    "${hiredis_dir}/net.c"
+    "${hiredis_dir}/read.c"
+    "${hiredis_dir}/sds.c"
+    "${hiredis_dir}/sockcompat.c"
+  )
+  add_library(libhiredis_static STATIC EXCLUDE_FROM_ALL ${hiredis_sources})
+  add_library(HIREDIS::HIREDIS ALIAS libhiredis_static)
+  if(WIN32)
+    target_compile_definitions(libhiredis_static PRIVATE _CRT_SECURE_NO_WARNINGS)
+    target_link_libraries(libhiredis_static PUBLIC ws2_32)
+  endif()
+
+  make_directory("${hiredis_dir}/include")
+  make_directory("${hiredis_dir}/include/hiredis")
+  file(GLOB hiredis_headers "${hiredis_dir}/*.h")
+  file(COPY ${hiredis_headers} DESTINATION "${hiredis_dir}/include/hiredis")
+  set_target_properties(
+    libhiredis_static
+    PROPERTIES
+    INTERFACE_INCLUDE_DIRECTORIES "$<BUILD_INTERFACE:${hiredis_dir}/include>")
+else()
+  find_package(PkgConfig)
+  if(PKG_CONFIG_FOUND)
+    pkg_check_modules(HIREDIS hiredis>=${hiredis_FIND_VERSION})
+    find_library(HIREDIS_LIBRARY ${HIREDIS_LIBRARIES} HINTS ${HIREDIS_LIBDIR})
+    find_path(HIREDIS_INCLUDE_DIR hiredis/hiredis.h HINTS ${HIREDIS_PREFIX}/include)
+  else()
+    find_library(HIREDIS_LIBRARY hiredis)
+    find_path(HIREDIS_INCLUDE_DIR hiredis/hiredis.h)
+  endif()
+
+  include(FindPackageHandleStandardArgs)
+  find_package_handle_standard_args(
+    hiredis
+    "please install libhiredis or use -DHIREDIS_FROM_INTERNET=ON or disable with -DREDIS_STORAGE_BACKEND=OFF"
+    HIREDIS_INCLUDE_DIR HIREDIS_LIBRARY)
+  mark_as_advanced(HIREDIS_INCLUDE_DIR HIREDIS_LIBRARY)
+
+  add_library(HIREDIS::HIREDIS UNKNOWN IMPORTED)
+  set_target_properties(
+    HIREDIS::HIREDIS
+    PROPERTIES
+    IMPORTED_LOCATION "${HIREDIS_LIBRARY}"
+    INTERFACE_COMPILE_OPTIONS "${HIREDIS_CFLAGS_OTHER}"
+    INTERFACE_INCLUDE_DIRECTORIES "${HIREDIS_INCLUDE_DIR}")
+endif()
+
+include(FeatureSummary)
+set_package_properties(
+  hiredis
+  PROPERTIES
+  URL "https://github.com/redis/hiredis"
+  DESCRIPTION "Hiredis is a minimalistic C client library for the Redis database")
index e889a680923091d34be6b77d0388d0a0b759eb93..e5fc8cdee7d9387684e2c1aeea6bc1ab6997a3ed 100644 (file)
@@ -6,7 +6,7 @@ if(ZSTD_FROM_INTERNET)
   # Although ${zstd_FIND_VERSION} was requested, let's download a newer version.
   # Note: The directory structure has changed in 1.3.0; we only support 1.3.0
   # and newer.
-  set(zstd_version "1.4.9")
+  set(zstd_version "1.5.0")
   set(zstd_url https://github.com/facebook/zstd/archive/v${zstd_version}.tar.gz)
 
   set(zstd_dir ${CMAKE_BINARY_DIR}/zstd-${zstd_version})
index a4605293a1bdc29af2618ab5cc23a5f588d42dd8..ee6716bea31caa0b66546fd7dda8aba8364796ba 100644 (file)
@@ -43,9 +43,9 @@ foreach(func IN ITEMS ${functions})
   check_function_exists(${func} ${func_var})
 endforeach()
 
-include(CheckCSourceCompiles)
+include(CheckCXXSourceCompiles)
 set(CMAKE_REQUIRED_FLAGS -pthread)
-check_c_source_compiles(
+check_cxx_source_compiles(
   [=[
     #include <pthread.h>
     int main()
@@ -60,12 +60,14 @@ check_function_exists(pthread_mutexattr_setpshared HAVE_PTHREAD_MUTEXATTR_SETPSH
 set(CMAKE_REQUIRED_FLAGS)
 
 include(CheckStructHasMember)
+check_struct_has_member("struct stat" st_atim sys/stat.h
+                        HAVE_STRUCT_STAT_ST_ATIM LANGUAGE CXX)
 check_struct_has_member("struct stat" st_ctim sys/stat.h
-                        HAVE_STRUCT_STAT_ST_CTIM)
+                        HAVE_STRUCT_STAT_ST_CTIM LANGUAGE CXX)
 check_struct_has_member("struct stat" st_mtim sys/stat.h
-                        HAVE_STRUCT_STAT_ST_MTIM)
+                        HAVE_STRUCT_STAT_ST_MTIM LANGUAGE CXX)
 check_struct_has_member("struct statfs" f_fstypename sys/mount.h
-                        HAVE_STRUCT_STATFS_F_FSTYPENAME)
+                        HAVE_STRUCT_STATFS_F_FSTYPENAME LANGUAGE CXX)
 
 include(CheckCXXSourceCompiles)
 check_cxx_source_compiles(
@@ -98,5 +100,9 @@ endif()
 # alias
 set(MTR_ENABLED "${ENABLE_TRACING}")
 
+if(HAVE_SYS_MMAN_H AND HAVE_PTHREAD_MUTEXATTR_SETPSHARED)
+  set(INODE_CACHE_SUPPORTED 1)
+endif()
+
 configure_file(${CMAKE_SOURCE_DIR}/cmake/config.h.in
                ${CMAKE_BINARY_DIR}/config.h @ONLY)
index a0f0189c7d557154ae3fdf319f9f25740e8f12db..4dfc7e1d35ef0d53d96e4b0ebd8e4895ceeeb647 100644 (file)
@@ -3,10 +3,12 @@
 
 add_library(standard_settings INTERFACE)
 
-# Not supported in CMake 3.4: target_compile_features(project_options INTERFACE
-# c_std_11 cxx_std_11)
-
 if(CMAKE_CXX_COMPILER_ID MATCHES "^GNU|(Apple)?Clang$")
+  target_compile_options(
+    standard_settings
+    INTERFACE -include ${CMAKE_BINARY_DIR}/config.h
+  )
+
   option(ENABLE_COVERAGE "Enable coverage reporting for GCC/Clang" FALSE)
   if(ENABLE_COVERAGE)
     target_compile_options(standard_settings INTERFACE --coverage -O0 -g)
@@ -50,5 +52,17 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "^GNU|(Apple)?Clang$")
   include(StdAtomic)
 
 elseif(MSVC)
-  target_compile_options(standard_settings INTERFACE /std:c++latest /Zc:preprocessor /Zc:__cplusplus /D_CRT_SECURE_NO_WARNINGS)
+  target_compile_options(standard_settings INTERFACE "/FI${CMAKE_BINARY_DIR}/config.h")
+
+  target_compile_options(
+    standard_settings
+    INTERFACE /Zc:preprocessor /Zc:__cplusplus /D_CRT_SECURE_NO_WARNINGS
+  )
+endif()
+
+if(WIN32)
+  target_compile_definitions(
+    standard_settings
+    INTERFACE WIN32_LEAN_AND_MEAN
+  )
 endif()
index ffc26ea29054074ae2f8b75c963543dce7db64de..9b4fdd6abe0a44061c2f2bf734d853bc69e26800 100644 (file)
@@ -1,3 +1,23 @@
+if(NOT CCACHE_DEV_MODE)
+  # For ccache, using a faster linker is in practice only relevant to reduce the
+  # compile-link-test cycle for developers, so use the standard linker for
+  # non-developer builds.
+  return()
+endif()
+
+if(MSVC)
+  message(STATUS "Using standard linker for MSVC")
+  return()
+endif()
+
+if(NOT CMAKE_SYSTEM_PROCESSOR STREQUAL x86_64)
+  # Be conservative and only probe for a faster linker on platforms that likely
+  # don't have toolchain bugs. See for example
+  # <https://www.sourceware.org/bugzilla/show_bug.cgi?id=22838>.
+  message(STATUS "Not probing for faster linker on ${CMAKE_SYSTEM_PROCESSOR}")
+  return()
+endif()
+
 function(check_linker linker)
   string(TOUPPER ${linker} upper_linker)
   file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/CMakefiles/CMakeTmp/main.c" "int main() { return 0; }")
index d10e16417f8f05bb42f8fb05897ee3c7c300dcf3..729a1d4af618242aeba9c790eff75c061630245f 100644 (file)
@@ -1,3 +1,6 @@
+// This file is included by all compilation units, including those in
+// src/third_party. It should only contain macros and typedefs.
+
 #pragma once
 #ifdef __clang__
 #  pragma clang diagnostic push
@@ -6,6 +9,11 @@
 #  endif
 #endif
 
+#ifdef __MINGW32__
+#  define __USE_MINGW_ANSI_STDIO 1
+#  define __STDC_FORMAT_MACROS 1
+#endif
+
 // For example for vasprintf under i686-w64-mingw32-g++-posix. The later
 // definition of _XOPEN_SOURCE disables certain features on Linux, so we need
 // _GNU_SOURCE to re-enable them (makedev, tm_zone).
@@ -56,8 +64,6 @@
 #cmakedefine _WIN32_WINNT @_WIN32_WINNT@
 // clang-format on
 
-#define SYSCONFDIR "@CMAKE_INSTALL_FULL_SYSCONFDIR@"
-
 #ifdef __clang__
 #  pragma clang diagnostic pop
 #endif
 // Define if "f_fstypename" is a member of "struct statfs".
 #cmakedefine HAVE_STRUCT_STATFS_F_FSTYPENAME
 
+// Define if "st_atim" is a member of "struct stat".
+#cmakedefine HAVE_STRUCT_STAT_ST_ATIM
+
 // Define if "st_ctim" is a member of "struct stat".
 #cmakedefine HAVE_STRUCT_STAT_ST_CTIM
 
 #  undef HAVE_STRUCT_STAT_ST_CTIM
 #  undef HAVE_STRUCT_STAT_ST_MTIM
 #endif
+
+// Typedefs that make it possible to use common types in ccache header files
+// without including core/wincompat.hpp.
+#ifdef _WIN32
+#  ifdef _MSC_VER
+typedef unsigned __int32 mode_t;
+typedef int pid_t;
+#  endif // _MSC_VER
+#endif   // _WIN32
+
+// GCC version of a couple of standard C++ attributes.
+#ifdef __GNUC__
+#  define nodiscard gnu::warn_unused_result
+#  define maybe_unused gnu::unused
+#endif
+
+// O_BINARY is needed when reading binary data on Windows, so use it everywhere
+// with a compatibility define for Unix platforms.
+#if !defined(_WIN32) && !defined(O_BINARY)
+#  define O_BINARY 0
+#endif
+
+#ifndef ESTALE
+#  define ESTALE -1
+#endif
+
+#define SYSCONFDIR "@CMAKE_INSTALL_FULL_SYSCONFDIR@"
+
+#cmakedefine INODE_CACHE_SUPPORTED
+
+// Buffer size for I/O operations. Should be a multiple of 4 KiB.
+#define CCACHE_READ_BUFFER_SIZE 65536
index 2a8363a964809cc4b67d8bb0d6c67aa8a228d1dc..2f8f181d5901ecc99d68c7787542f404ee822a73 100644 (file)
@@ -1,5 +1,4 @@
-Ccache authors
-==============
+= Ccache authors
 
 Ccache was originally written by Andrew Tridgell and is currently developed and
 maintained by Joel Rosdahl.
@@ -80,6 +79,7 @@ Ccache is a collective work with contributions from many people, including:
 * Matthias Kretz
 * Matt Whitlock
 * Melven Roehrig-Zoellner
+* Michael Kruse
 * Michael Marineau
 * Michael Meeks
 * Michał Mirosław
@@ -117,8 +117,10 @@ Ccache is a collective work with contributions from many people, including:
 * Robert Yang
 * Robin H. Johnson
 * Rolf Bjarne Kvinge
+* R. Voggenauer
 * RW
 * Ryan Brown
+* Ryan Burns
 * Ryan Egesdahl
 * Sam Gross
 * Sergei Trofimovich
@@ -131,6 +133,7 @@ Ccache is a collective work with contributions from many people, including:
 * Tim Potter
 * Tomasz Miąsko
 * Tom Hughes
+* Tom Stellard
 * Tor Arne Vestbø
 * Vadim Petrochenkov
 * Ville Skyttä
index c5ce224d9e99ff317e45f9ccf627107169da3fa6..997e49d208c804d9a12d02e0ffb6d6e663a407e3 100644 (file)
@@ -1,70 +1,54 @@
-find_program(ASCIIDOC_EXE asciidoc)
-mark_as_advanced(ASCIIDOC_EXE) # Don't show in CMake UIs
+find_program(ASCIIDOCTOR_EXE asciidoctor)
+mark_as_advanced(ASCIIDOCTOR_EXE) # Don't show in CMake UIs
 
-if(NOT ASCIIDOC_EXE)
-  message(WARNING "Could not find asciidoc; documentation will not be generated")
+if(NOT ASCIIDOCTOR_EXE)
+  message(WARNING "Could not find asciidoctor; documentation will not be generated")
 else()
-  #
-  # HTML documentation
-  #
-  function(generate_html adoc_file)
+  function(generate_doc backend adoc_file output_file)
     get_filename_component(base_name "${adoc_file}" NAME_WE)
-    set(html_file "${base_name}.html")
     add_custom_command(
-      OUTPUT "${html_file}"
+      OUTPUT "${output_file}"
       COMMAND
-        ${ASCIIDOC_EXE}
-          -o "${html_file}"
+        ${ASCIIDOCTOR_EXE}
+          -o "${output_file}"
           -a revnumber="${CCACHE_VERSION}"
-          -a toc
-          -b xhtml11
+          -a icons=font
+          -a toc=left
+          -a sectanchors
+          -a stylesheet="${CMAKE_CURRENT_SOURCE_DIR}/ccache-doc.css"
+          -b "${backend}"
           "${CMAKE_SOURCE_DIR}/${adoc_file}"
       MAIN_DEPENDENCY "${CMAKE_SOURCE_DIR}/${adoc_file}"
+      DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/ccache-doc.css"
     )
-    set(html_files "${html_files}" "${html_file}" PARENT_SCOPE)
+    set(doc_files "${doc_files}" "${output_file}" PARENT_SCOPE)
   endfunction()
 
-  generate_html(LICENSE.adoc)
-  generate_html(doc/AUTHORS.adoc)
-  generate_html(doc/MANUAL.adoc)
-  generate_html(doc/NEWS.adoc)
-
-  add_custom_target(doc-html DEPENDS "${html_files}")
-  set(doc_files "${html_files}")
+  #
+  # HTML documentation
+  #
+  generate_doc(html    LICENSE.adoc     LICENSE.html)
+  generate_doc(html    doc/AUTHORS.adoc AUTHORS.html)
+  generate_doc(html    doc/MANUAL.adoc  MANUAL.html)
+  generate_doc(html    doc/NEWS.adoc    NEWS.html)
+  add_custom_target(doc-html DEPENDS "${doc_files}")
 
   #
   # Man page
   #
-  find_program(A2X_EXE a2x)
-  mark_as_advanced(A2X_EXE) # Don't show in CMake UIs
-  if(NOT A2X_EXE)
-    message(WARNING "Could not find a2x; man page will not be generated")
-  else()
-    # MANUAL.adoc -> MANUAL.xml -> man page
-    add_custom_command(
-      OUTPUT MANUAL.xml
-      COMMAND
-        ${ASCIIDOC_EXE}
-          -o -
-          -a revnumber=${CCACHE_VERSION}
-          -d manpage
-          -b docbook "${CMAKE_SOURCE_DIR}/doc/MANUAL.adoc"
-        | perl -pe 's!<literal>\(.*?\)</literal>!<emphasis role="strong">\\1</emphasis>!g'
-            >MANUAL.xml
-      MAIN_DEPENDENCY "${CMAKE_SOURCE_DIR}/doc/MANUAL.adoc"
-    )
-    add_custom_command(
-      OUTPUT ccache.1
-      COMMAND ${A2X_EXE} --doctype manpage --format manpage MANUAL.xml
-      MAIN_DEPENDENCY MANUAL.xml
-    )
-    add_custom_target(doc-man-page DEPENDS ccache.1)
-    install(
-      FILES "${CMAKE_CURRENT_BINARY_DIR}/ccache.1"
-      DESTINATION "${CMAKE_INSTALL_MANDIR}/man1"
-    )
-    set(doc_files "${doc_files}" ccache.1)
-  endif()
+  generate_doc(manpage doc/MANUAL.adoc ccache.1.tmp)
+  add_custom_command(
+    OUTPUT ccache.1
+    # Convert monospace to bold since that's typically rendered better when
+    # viewing the man page.
+    COMMAND perl -pe "'s!\\\\f\\(CR(.*?)\\\\fP!\\\\fB\\1\\\\fP!g'" ccache.1.tmp >ccache.1
+    MAIN_DEPENDENCY ccache.1.tmp
+  )
+  install(
+    FILES "${CMAKE_CURRENT_BINARY_DIR}/ccache.1"
+    DESTINATION "${CMAKE_INSTALL_MANDIR}/man1"
+  )
+  add_custom_target(doc-man-page DEPENDS ccache.1)
 
-  add_custom_target(doc ALL DEPENDS "${doc_files}")
+  add_custom_target(doc ALL DEPENDS doc-html doc-man-page)
 endif()
index a90a24f92a7a22930ba39bb5eb59a5031fb5f93d..27e928c23dd365f26c083f3f5ad925aa240b26bc 100644 (file)
@@ -6,14 +6,15 @@ Prerequisites
 
 To build ccache you need:
 
-- CMake 3.4.3 or newer.
-- A C++11 compiler. See [Supported platforms, compilers and
+- CMake 3.10 or newer.
+- A C++14 compiler. See [Supported platforms, compilers and
   languages](https://ccache.dev/platform-compiler-language-support.html) for
   details.
 - A C99 compiler.
 - [libzstd](http://www.zstd.net). If you don't have libzstd installed and
   can't or don't want to install it in a standard system location, there are
   two options:
+
     1. Install zstd in a custom path and set `CMAKE_PREFIX_PATH` to it, e.g.
        by passing `-DCMAKE_PREFIX_PATH=/some/custom/path` to `cmake`, or
     2. Pass `-DZSTD_FROM_INTERNET=ON` to `cmake` to make it download libzstd
@@ -24,13 +25,20 @@ To build ccache you need:
 
 Optional:
 
+- [hiredis](https://github.com/redis/hiredis) for the Redis storage backend. If
+  you don't have libhiredis installed and can't or don't want to install it in a
+  standard system location, there are two options:
+
+    1. Install hiredis in a custom path and set `CMAKE_PREFIX_PATH` to it, e.g.
+       by passing `-DCMAKE_PREFIX_PATH=/some/custom/path` to `cmake`, or
+    2. Pass `-DHIREDIS_FROM_INTERNET=ON` to `cmake` to make it download hiredis
+       from the Internet and unpack it in the local binary tree. Ccache will
+       then be linked statically to the locally built libhiredis.
+
+  To link libhiredis statically you can use
+  `-DHIREDIS_LIBRARY=/path/to/libhiredis.a`.
 - GNU Bourne Again SHell (bash) for tests.
-- [AsciiDoc](https://www.methods.co.nz/asciidoc/) to build the HTML
-  documentation.
-  - Tip: On Debian-based systems (e.g. Ubuntu), install the `docbook-xml` and
-    `docbook-xsl` packages in addition to `asciidoc`. Without the former the
-    man page generation will be very slow.
-- [xsltproc](http://xmlsoft.org/XSLT/xsltproc2.html) to build the man page.
+- [Asciidoctor](https://asciidoctor.org) to build the HTML documentation.
 - [Python](https://www.python.org) to debug and run the performance test suite.
 
 
index 25c34c0dad7c07f0e0d3873f06c534631f23b13f..e29d0a2015179acc7e001ccd950cd40aa2b11509 100644 (file)
@@ -1,18 +1,12 @@
-CCACHE(1)
-=========
-:man source:  ccache
-:man version: {revnumber}
-:man manual:  ccache Manual
+= ccache(1)
+:mansource: Ccache {revnumber}
 
-
-Name
-----
+== Name
 
 ccache - a fast C/C++ compiler cache
 
 
-Synopsis
---------
+== Synopsis
 
 [verse]
 *ccache* [_options_]
@@ -20,8 +14,7 @@ Synopsis
 _compiler_ [_compiler options_]                   (via symbolic link)
 
 
-Description
------------
+== Description
 
 Ccache is a compiler cache. It speeds up recompilation by caching the result of
 previous compilations and detecting when the same compilation is being done
@@ -30,21 +23,20 @@ again.
 Ccache has been carefully written to always produce exactly the same compiler
 output that you would get without the cache. The only way you should be able to
 tell that you are using ccache is the speed. Currently known exceptions to this
-goal are listed under <<_caveats,CAVEATS>>. If you discover an undocumented case
-where ccache changes the output of your compiler, please let us know.
+goal are listed under _<<Caveats>>_. If you discover an undocumented case where
+ccache changes the output of your compiler, please let us know.
 
 
-Run modes
----------
+== Run modes
 
 There are two ways to use ccache. You can either prefix your compilation
-commands with *ccache* or you can let ccache masquerade as the compiler by
+commands with `ccache` or you can let ccache masquerade as the compiler by
 creating a symbolic link (named as the compiler) to ccache. The first method is
 most convenient if you just want to try out ccache or wish to use it for some
 specific projects. The second method is most useful for when you wish to use
 ccache for all your compilations.
 
-To use the first method, just make sure that *ccache* is in your *PATH*.
+To use the first method, just make sure that `ccache` is in your `PATH`.
 
 To use the symlinks method, do something like this:
 
@@ -58,29 +50,27 @@ ln -s ccache /usr/local/bin/c++
 
 And so forth. This will work as long as the directory with symlinks comes
 before the path to the compiler (which is usually in `/usr/bin`). After
-installing you may wish to run ``which gcc'' to make sure that the correct link
+installing you may wish to run "`which gcc`" to make sure that the correct link
 is being used.
 
 WARNING: The technique of letting ccache masquerade as the compiler works well,
-but currently doesn't interact well with other tools that do the same thing.
-See _<<_using_ccache_with_other_compiler_wrappers,Using ccache with other
-compiler wrappers>>_.
+but currently doesn't interact well with other tools that do the same thing. See
+_<<Using ccache with other compiler wrappers>>_.
 
 WARNING: Use a symbolic links for masquerading, not hard links.
 
-Command line options
---------------------
 
-These command line options only apply when you invoke ccache as ``ccache''.
+== Command line options
+
+These command line options only apply when you invoke ccache as "`ccache`".
 When invoked as a compiler (via a symlink as described in the previous
 section), the normal compiler options apply and you should refer to the
 compiler's documentation.
 
 
-Common options
-~~~~~~~~~~~~~~
+=== Common options
 
-*`-c`*, *`--cleanup`*::
+*-c*, *--cleanup*::
 
     Clean up the cache by removing old cached files until the specified file
     number and cache size limits are not exceeded. This also recalculates the
@@ -90,41 +80,41 @@ Common options
     cleanup is mostly useful if you manually modify the cache contents or
     believe that the cache size statistics may be inaccurate.
 
-*`-C`*, *`--clear`*::
+*-C*, *--clear*::
 
     Clear the entire cache, removing all cached files, but keeping the
     configuration file.
 
-*`--config-path`* _PATH_::
+*--config-path* _PATH_::
 
-    Let the subsequent command line options operate on configuration file
-    _PATH_ instead of the default. Using this option has the same effect as
-    setting the environment variable `CCACHE_CONFIGPATH` temporarily.
+    Let the command line options operate on configuration file _PATH_ instead of
+    the default. Using this option has the same effect as setting the
+    environment variable `CCACHE_CONFIGPATH` temporarily.
 
-*`-d`*, *`--directory`* _PATH_::
+*-d*, *--dir* _PATH_::
 
-    Let the subsequent command line options operate on cache directory _PATH_
-    instead of the default. For example, to show statistics for a cache
-    directory at `/shared/ccache` you can run `ccache -d /shared/ccache -s`.
-    Using this option has the same effect as setting the environment variable
-    `CCACHE_DIR` temporarily.
+    Let the command line options operate on cache directory _PATH_ instead of
+    the default. For example, to show statistics for a cache directory at
+    `/shared/ccache` you can run `ccache -d /shared/ccache -s`. Using this option
+    has the same effect as setting the environment variable `CCACHE_DIR`
+    temporarily.
 
-*`--evict-older-than`* _AGE_::
+*--evict-older-than* _AGE_::
 
     Remove files older than _AGE_ from the cache. _AGE_ should be an unsigned
     integer with a `d` (days) or `s` (seconds) suffix.
 
-*`-h`*, *`--help`*::
+*-h*, *--help*::
 
     Print a summary of command line options.
 
-*`-F`* _NUM_, *`--max-files`* _NUM_::
+*-F* _NUM_, *--max-files* _NUM_::
 
     Set the maximum number of files allowed in the cache to _NUM_. Use 0 for no
     limit. The value is stored in a configuration file in the cache directory
     and applies to all future compilations.
 
-*`-M`* _SIZE_, *`--max-size`* _SIZE_::
+*-M* _SIZE_, *--max-size* _SIZE_::
 
     Set the maximum size of the files stored in the cache. _SIZE_ should be a
     number followed by an optional suffix: k, M, G, T (decimal), Ki, Mi, Gi or
@@ -132,97 +122,136 @@ Common options
     stored in a configuration file in the cache directory and applies to all
     future compilations.
 
-*`-X`* _LEVEL_, *`--recompress`* _LEVEL_::
+*-X* _LEVEL_, *--recompress* _LEVEL_::
 
     Recompress the cache to level _LEVEL_ using the Zstandard algorithm. The
     level can be an integer, with the same semantics as the
     <<config_compression_level,*compression_level*>> configuration option), or
     the special value *uncompressed* for no compression. See
-    _<<_cache_compression,Cache compression>>_ for more information. This can
-    potentionally take a long time since all files in the cache need to be
-    visited. Only files that are currently compressed with a different level
-    than _LEVEL_ will be recompressed.
+    _<<Cache compression>>_ for more information. This can potentionally take a
+    long time since all files in the cache need to be visited. Only files that
+    are currently compressed with a different level than _LEVEL_ will be
+    recompressed.
 
-*`-o`* _KEY=VALUE_, *`--set-config`* _KEY_=_VALUE_::
+*-o* _KEY=VALUE_, *--set-config* _KEY_=_VALUE_::
 
-    Set configuration option _KEY_ to _VALUE_. See
-    _<<_configuration,Configuration>>_ for more information.
+    Set configuration option _KEY_ to _VALUE_. See _<<Configuration>>_ for more
+    information.
 
-*`-x`*, *`--show-compression`*::
+*-x*, *--show-compression*::
 
-    Print cache compression statistics. See _<<_cache_compression,Cache
-    compression>>_ for more information. This can potentionally take a long
-    time since all files in the cache need to be visited.
+    Print cache compression statistics. See _<<Cache compression>>_ for more
+    information. This can potentionally take a long time since all files in the
+    cache need to be visited.
 
-*`-p`*, *`--show-config`*::
+*-p*, *--show-config*::
 
     Print current configuration options and from where they originate
     (environment variable, configuration file or compile-time default) in
     human-readable format.
 
-*`-s`*, *`--show-stats`*::
+*--show-log-stats*::
+
+    Print statistics counters from the stats log in human-readable format. See
+    <<config_stats_log,*stats_log*>>. Use `-v`/`--verbose` once or twice for
+    more details.
+
+*-s*, *--show-stats*::
 
     Print a summary of configuration and statistics counters in human-readable
-    format.
+    format. Use `-v`/`--verbose` once or twice for more details.
+
+*-v*, *--verbose*::
 
-*`-V`*, *`--version`*::
+    Increase verbosity. The option can be given multiple times.
+
+*-V*, *--version*::
 
     Print version and copyright information.
 
-*`-z`*, *`--zero-stats`*::
+*-z*, *--zero-stats*::
 
     Zero the cache statistics (but not the configuration options).
 
 
-Options for scripting or debugging
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Options for secondary storage
+
+*--trim-dir* _PATH_::
+
+   Remove old files from directory _PATH_ until it is at most the size specified
+   by `--trim-max-size`.
++
+WARNING: Don't use this option to trim the primary cache. To trim the primary
+cache directory to a certain size, use `CCACHE_MAXSIZE=_SIZE_ ccache -c`.
+
+*--trim-max-size* _SIZE_::
+
+   Specify the maximum size for `--trim-dir`. _SIZE_ should be a number followed
+   by an optional suffix: k, M, G, T (decimal), Ki, Mi, Gi or Ti (binary). The
+   default suffix is G.
+
+*--trim-method* _METHOD_::
+
+    Specify the method to trim a directory with `--trim-dir`. Possible values
+    are:
++
+--
+*atime*::
+    LRU (least recently used) using the file access timestamp. This is the
+    default.
+*mtime*::
+    LRU (least recently used) using the file modification timestamp.
+--
+
+=== Options for scripting or debugging
 
-*`--checksum-file`* _PATH_::
+*--checksum-file* _PATH_::
 
-    Print the checksum (64 bit XXH3) of the file at _PATH_.
+    Print the checksum (64 bit XXH3) of the file at _PATH_ (`-` for standard
+    input).
 
-*`--dump-manifest`* _PATH_::
+*--dump-manifest* _PATH_::
 
-    Dump manifest file at _PATH_ in text format to standard output. This is
-    only useful when debugging ccache and its behavior.
+    Dump manifest file at _PATH_ (`-` for standard input) in text format to
+    standard output. This is only useful when debugging ccache and its behavior.
 
-*`--dump-result`* _PATH_::
+*--dump-result* _PATH_::
 
-    Dump result file at _PATH_ in text format to standard output. This is only
-    useful when debugging ccache and its behavior.
+    Dump result file at _PATH_ (`-` for standard input) in text format to
+    standard output. This is only useful when debugging ccache and its behavior.
 
-*`--extract-result`* _PATH_::
+*--extract-result* _PATH_::
 
-    Extract data stored in the result file at _PATH_. The data will be written
-    to *ccache-result.** files in to the current working directory. This is
-    only useful when debugging ccache and its behavior.
+    Extract data stored in the result file at _PATH_ (`-` for standard input).
+    The data will be written to `ccache-result.*` files in to the current
+    working directory. This is only useful when debugging ccache and its
+    behavior.
 
-*`-k`* _KEY_, *`--get-config`* _KEY_::
+*-k* _KEY_, *--get-config* _KEY_::
 
-    Print the value of configuration option _KEY_. See
-    _<<_configuration,Configuration>>_ for more information.
+    Print the value of configuration option _KEY_. See _<<Configuration>>_ for
+    more information.
 
-*`--hash-file`* _PATH_::
+*--hash-file* _PATH_::
 
-    Print the hash (160 bit BLAKE3) of the file at _PATH_. This is only useful
-    when debugging ccache and its behavior.
+    Print the hash (160 bit BLAKE3) of the file at _PATH_ (`-` for standard
+    input). This is only useful when debugging ccache and its behavior.
 
-*`--print-stats`*::
+*--print-stats*::
 
     Print statistics counter IDs and corresponding values in machine-parsable
     (tab-separated) format.
 
 
 
-Extra options
-~~~~~~~~~~~~~
+=== Extra options
 
 When run as a compiler, ccache usually just takes the same command line options
 as the compiler you are using. The only exception to this is the option
-*--ccache-skip*. That option can be used to tell ccache to avoid interpreting
+`--ccache-skip`. That option can be used to tell ccache to avoid interpreting
 the next option in any way and to pass it along to the compiler as-is.
 
-NOTE: *--ccache-skip* currently only tells ccache not to interpret the next
+NOTE: `--ccache-skip` currently only tells ccache not to interpret the next
 option as a special compiler option -- the option will still be included in the
 direct mode hash.
 
@@ -231,20 +260,19 @@ line and determine what is an input filename and what is a compiler option, as
 it needs the input filename to determine the name of the resulting object file
 (among other things). The heuristic ccache uses when parsing the command line
 is that any argument that exists as a file is treated as an input file name. By
-using *--ccache-skip* you can force an option to not be treated as an input
+using `--ccache-skip` you can force an option to not be treated as an input
 file name and instead be passed along to the compiler as a command line option.
 
-Another case where *--ccache-skip* can be useful is if ccache interprets an
+Another case where `--ccache-skip` can be useful is if ccache interprets an
 option specially but shouldn't, since the option has another meaning for your
 compiler than what ccache thinks.
 
 
-Configuration
--------------
+== Configuration
 
-ccache's default behavior can be overridden by options in configuration files,
+Ccache's default behavior can be overridden by options in configuration files,
 which in turn can be overridden by environment variables with names starting
-with *CCACHE_*. Ccache normally reads configuration from two files: first a
+with `CCACHE_`. Ccache normally reads configuration from two files: first a
 system-level configuration file and secondly a cache-specific configuration
 file. The priorities of configuration options are as follows (where 1 is
 highest):
@@ -252,39 +280,37 @@ highest):
 1. Environment variables.
 2. The primary (cache-specific) configuration file (see below).
 3. The secondary (system-wide read-only) configuration file
-   *_<sysconfdir>_/ccache.conf* (typically */etc/ccache.conf* or
-    */usr/local/etc/ccache.conf*).
+   `<sysconfdir>/ccache.conf` (typically `/etc/ccache.conf` or
+   `/usr/local/etc/ccache.conf`).
 4. Compile-time defaults.
 
-As a special case, if the the environment variable *CCACHE_CONFIGPATH* is set
+As a special case, if the the environment variable `CCACHE_CONFIGPATH` is set
 it specifies the primary configuration file and the secondary (system-wide)
 configuration file won't be read.
 
 
-Location of the primary configuration file
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Location of the primary configuration file
 
 The location of the primary (cache-specific) configuration is determined like
 this:
 
-1. If *CCACHE_CONFIGPATH* is set, use that path.
-2. Otherwise, if the environment variable *CCACHE_DIR* is set then use
-   *$CCACHE_DIR/ccache.conf*.
+1. If `CCACHE_CONFIGPATH` is set, use that path.
+2. Otherwise, if the environment variable `CCACHE_DIR` is set then use
+   `$CCACHE_DIR/ccache.conf`.
 3. Otherwise, if <<config_cache_dir,*cache_dir*>> is set in the secondary
-   (system-wide) configuration file then use *<cache_dir>/ccache.conf*.
-4. Otherwise, if there is a legacy *$HOME/.ccache* directory then use
-   *$HOME/.ccache/ccache.conf*.
-5. Otherwise, if *XDG_CONFIG_HOME* is set then use
-   *$XDG_CONFIG_HOME/ccache/ccache.conf*.
-6. Otherwise, use *%APPDATA%/ccache/ccache.conf* (Windows),
-   *$HOME/Library/Preferences/ccache/ccache.conf* (macOS) or
-   *$HOME/.config/ccache/ccache.conf* (other systems).
+   (system-wide) configuration file then use `<cache_dir>/ccache.conf`.
+4. Otherwise, if there is a legacy `$HOME/.ccache` directory then use
+   `$HOME/.ccache/ccache.conf`.
+5. Otherwise, if `XDG_CONFIG_HOME` is set then use
+   `$XDG_CONFIG_HOME/ccache/ccache.conf`.
+6. Otherwise, use `%APPDATA%/ccache/ccache.conf` (Windows),
+   `$HOME/Library/Preferences/ccache/ccache.conf` (macOS) or
+   `$HOME/.config/ccache/ccache.conf` (other systems).
 
 
-Configuration file syntax
-~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Configuration file syntax
 
-Configuration files are in a simple ``key = value'' format, one option per
+Configuration files are in a simple "`key = value`" format, one option per
 line. Lines starting with a hash sign are comments. Blank lines are ignored, as
 is whitespace surrounding keys and values. Example:
 
@@ -293,31 +319,31 @@ is whitespace surrounding keys and values. Example:
 max_size = 10G
 -------------------------------------------------------------------------------
 
-Boolean values
-~~~~~~~~~~~~~~
+
+=== Boolean values
 
 Some configuration options are boolean values (i.e. truth values). In a
 configuration file, such values must be set to the string *true* or *false*.
 For the corresponding environment variables, the semantics are a bit different:
 
-* A set environment variable means ``true'' (even if set to the empty string).
+* A set environment variable means "`true`" (even if set to the empty string).
 * The following case-insensitive negative values are considered an error
   (instead of surprising the user): *0*, *false*, *disable* and *no*.
-* An unset environment variable means ``false''.
+* An unset environment variable means "`false`".
 
 Each boolean environment variable also has a negated form starting with
-*CCACHE_NO*. For example, *CCACHE_COMPRESS* can be set to force compression and
-*CCACHE_NOCOMPRESS* can be set to force no compression.
+`CCACHE_NO`. For example, `CCACHE_COMPRESS` can be set to force compression and
+`CCACHE_NOCOMPRESS` can be set to force no compression.
 
 
-Configuration options
-~~~~~~~~~~~~~~~~~~~~~~
+=== Configuration options
 
 Below is a list of available configuration options. The corresponding
 environment variable name is indicated in parentheses after each configuration
 option key.
 
-[[config_absolute_paths_in_stderr]] *absolute_paths_in_stderr* (*CCACHE_ABSSTDERR*)::
+[#config_absolute_paths_in_stderr]
+*absolute_paths_in_stderr* (*CCACHE_ABSSTDERR*)::
 
     This option specifies whether ccache should rewrite relative paths in the
     compiler's standard error output to absolute paths. This can be useful if
@@ -326,16 +352,16 @@ option key.
     working directory, which makes relative paths in compiler errors or
     warnings incorrect. The default is false.
 
-[[config_base_dir]] *base_dir* (*CCACHE_BASEDIR*)::
+[#config_base_dir]
+*base_dir* (*CCACHE_BASEDIR*)::
 
     This option should be an absolute path to a directory. If set, ccache will
-    rewrite absolute paths into paths relative to the current working
-    directory, but only absolute paths that begin with *base_dir*. Cache
-    results can then be shared for compilations in different directories even
-    if the project uses absolute paths in the compiler command line. See also
-    the discussion under _<<_compiling_in_different_directories,Compiling in
-    different directories>>_. If set to the empty string (which is the
-    default), no rewriting is done.
+    rewrite absolute paths into paths relative to the current working directory,
+    but only absolute paths that begin with *base_dir*. Cache results can then
+    be shared for compilations in different directories even if the project uses
+    absolute paths in the compiler command line. See also the discussion under
+    _<<Compiling in different directories>>_. If set to the empty string (which
+    is the default), no rewriting is done.
 +
 A typical path to use as *base_dir* is your home directory or another directory
 that is a parent of your project directories. Don't use `/` as the base
@@ -382,28 +408,30 @@ relative path to `/usr/include/example` will be different. With *base_dir* set
 to `/home/bob/stuff/project1` there will a cache miss since the path to
 project2 will be a different absolute path.
 
-[[config_cache_dir]] *cache_dir* (*CCACHE_DIR*)::
+[#config_cache_dir]
+*cache_dir* (*CCACHE_DIR*)::
 
     This option specifies where ccache will keep its cached compiler outputs.
-    The default is *$XDG_CACHE_HOME/ccache* if *XDG_CACHE_HOME* is set,
-    otherwise *$HOME/.cache/ccache*. Exception: If the legacy directory
-    *$HOME/.ccache* exists then that directory is the default.
+    The default is `$XDG_CACHE_HOME/ccache` if `XDG_CACHE_HOME` is set,
+    otherwise `$HOME/.cache/ccache`. Exception: If the legacy directory
+    `$HOME/.ccache` exists then that directory is the default.
 +
-See also _<<_location_of_the_primary_configuration_file,Location of the primary
-configuration file>>_.
+See also _<<Location of the primary configuration file>>_.
 +
-If you want to use another *CCACHE_DIR* value temporarily for one ccache
-invocation you can use the `-d/--directory` command line option instead.
+If you want to use another `CCACHE_DIR` value temporarily for one ccache
+invocation you can use the `-d`/`--dir` command line option instead.
 
-[[config_compiler]] *compiler* (*CCACHE_COMPILER* or (deprecated) *CCACHE_CC*)::
+[#config_compiler]
+*compiler* (*CCACHE_COMPILER* or (deprecated) *CCACHE_CC*)::
 
     This option can be used to force the name of the compiler to use. If set to
     the empty string (which is the default), ccache works it out from the
     command line.
 
-[[config_compiler_check]] *compiler_check* (*CCACHE_COMPILERCHECK*)::
+[#config_compiler_check]
+*compiler_check* (*CCACHE_COMPILERCHECK*)::
 
-    By default, ccache includes the modification time (``mtime'') and size of
+    By default, ccache includes the modification time ("`mtime`") and size of
     the compiler in the hash to ensure that results retrieved from the cache
     are accurate. This option can be used to select another strategy. Possible
     values are:
@@ -433,35 +461,32 @@ _a command string_::
     path to the compiler. Several commands can be specified with semicolon as
     separator. Examples:
 +
---
-
 ----
 %compiler% -v
 ----
-
++
 ----
 %compiler% -dumpmachine; %compiler% -dumpversion
 ----
-
++
 You should make sure that the specified command is as fast as possible since it
 will be run once for each ccache invocation.
-
++
 Identifying the compiler using a command is useful if you want to avoid cache
 misses when the compiler has been rebuilt but not changed.
-
++
 Another case is when the compiler (as seen by ccache) actually isn't the real
 compiler but another compiler wrapper -- in that case, the default *mtime*
 method will hash the mtime and size of the other compiler wrapper, which means
-that ccache won't be able to detect a compiler upgrade. Using a suitable
-command to identify the compiler is thus safer, but it's also slower, so you
-should consider continue using the *mtime* method in combination with the
+that ccache won't be able to detect a compiler upgrade. Using a suitable command
+to identify the compiler is thus safer, but it's also slower, so you should
+consider continue using the *mtime* method in combination with the
 *prefix_command* option if possible. See
-_<<_using_ccache_with_other_compiler_wrappers,Using ccache with other compiler
-wrappers>>_.
---
+_<<Using ccache with other compiler wrappers>>_.
 --
 
-[[config_compiler_type]] *compiler_type* (*CCACHE_COMPILERTYPE*)::
+[#config_compiler_type]
+*compiler_type* (*CCACHE_COMPILERTYPE*)::
 
     Ccache normally guesses the compiler type based on the compiler name. The
     *compiler_type* option lets you force a compiler type. This can be useful
@@ -481,10 +506,11 @@ wrappers>>_.
 *other*::
     Any compiler other than the known types.
 *pump*::
-    distcc's "pump" script.
+    distcc's "`pump`" script.
 --
 
-[[config_compression]] *compression* (*CCACHE_COMPRESS* or *CCACHE_NOCOMPRESS*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_compression]
+*compression* (*CCACHE_COMPRESS* or *CCACHE_NOCOMPRESS*, see _<<Boolean values>>_ above)::
 
     If true, ccache will compress data it puts in the cache. However, this
     option has no effect on how files are retrieved from the cache; compressed
@@ -500,7 +526,8 @@ Compression will be disabled if file cloning (the
 <<config_file_clone,*file_clone*>> option) or hard linking (the
 <<config_hard_link,*hard_link*>> option) is enabled.
 
-[[config_compression_level]] *compression_level* (*CCACHE_COMPRESSLEVEL*)::
+[#config_compression_level]
+*compression_level* (*CCACHE_COMPRESSLEVEL*)::
 
     This option determines the level at which ccache will compress object files
     using the real-time compression algorithm Zstandard. It only has effect if
@@ -520,11 +547,11 @@ Semantics of *compression_level*:
     essentially the same for all levels. As a rule of thumb, use level 5 or
     lower since higher levels may slow down compilations noticeably. Higher
     levels are however useful when recompressing the cache with command line
-    option *-X/--recompress*.
+    option `-X`/`--recompress`.
 *< 0*::
-    A negative value corresponds to Zstandard's ``ultra-fast'' compression
+    A negative value corresponds to Zstandard's "`ultra-fast`" compression
     levels, which are even faster than level 1 but with less good compression
-    ratios. For instance, level *-3* corresponds to ``--fast=3'' for the *zstd*
+    ratios. For instance, level *-3* corresponds to `--fast=3` for the `zstd`
     command line tool. In practice, there is little use for levels lower than
     *-5* or so.
 *0* (default)::
@@ -534,26 +561,28 @@ Semantics of *compression_level*:
 +
 See the http://zstd.net[Zstandard documentation] for more information.
 
-[[config_cpp_extension]] *cpp_extension* (*CCACHE_EXTENSION*)::
+[#config_cpp_extension]
+*cpp_extension* (*CCACHE_EXTENSION*)::
 
     This option can be used to force a certain extension for the intermediate
     preprocessed file. The default is to automatically determine the extension
     to use for intermediate preprocessor files based on the type of file being
     compiled, but that sometimes doesn't work. For example, when using the
-    ``aCC'' compiler on HP-UX, set the cpp extension to *i*.
+    "`aCC`" compiler on HP-UX, set the cpp extension to *i*.
 
-[[config_debug]] *debug* (*CCACHE_DEBUG* or *CCACHE_NODEBUG*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_debug]
+*debug* (*CCACHE_DEBUG* or *CCACHE_NODEBUG*, see _<<Boolean values>>_ above)::
 
     If true, enable the debug mode. The debug mode creates per-object debug
     files that are helpful when debugging unexpected cache misses. Note however
-    that ccache performance will be reduced slightly. See
-    _<<_cache_debugging,Cache debugging>>_ for more information. The default is
-    false.
+    that ccache performance will be reduced slightly. See _<<Cache debugging>>_
+    for more information. The default is false.
 
-[[config_debug_dir]] *debug_dir* (*CCACHE_DEBUGDIR*)::
+[#config_debug_dir]
+*debug_dir* (*CCACHE_DEBUGDIR*)::
 
-    Specifies where to write per-object debug files if the _<<config_debug,debug
-    mode>>_ is enabled. If set to the empty string, the files will be written
+    Specifies where to write per-object debug files if the <<config_debug,debug
+    mode>> is enabled. If set to the empty string, the files will be written
     next to the object file. If set to a directory, the debug files will be
     written with full absolute paths in that directory, creating it if needed.
     The default is the empty string.
@@ -561,35 +590,40 @@ See the http://zstd.net[Zstandard documentation] for more information.
 For example, if *debug_dir* is set to `/example`, the current working directory
 is `/home/user` and the object file is `build/output.o` then the debug log will
 be written to `/example/home/user/build/output.o.ccache-log`. See also
-_<<_cache_debugging,Cache debugging>>_.
+_<<Cache debugging>>_.
 
-[[config_depend_mode]] *depend_mode* (*CCACHE_DEPEND* or *CCACHE_NODEPEND*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_depend_mode]
+*depend_mode* (*CCACHE_DEPEND* or *CCACHE_NODEPEND*, see _<<Boolean values>>_ above)::
 
     If true, the depend mode will be used. The default is false. See
-    _<<_the_depend_mode,The depend mode>>_.
+    _<<The depend mode>>_.
 
-[[config_direct_mode]] *direct_mode* (*CCACHE_DIRECT* or *CCACHE_NODIRECT*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_direct_mode]
+*direct_mode* (*CCACHE_DIRECT* or *CCACHE_NODIRECT*, see _<<Boolean values>>_ above)::
 
     If true, the direct mode will be used. The default is true. See
-    _<<_the_direct_mode,The direct mode>>_.
+    _<<The direct mode>>_.
 
-[[config_disable]] *disable* (*CCACHE_DISABLE* or *CCACHE_NODISABLE*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_disable]
+*disable* (*CCACHE_DISABLE* or *CCACHE_NODISABLE*, see _<<Boolean values>>_ above)::
 
     When true, ccache will just call the real compiler, bypassing the cache
     completely. The default is false.
 
-[[config_extra_files_to_hash]] *extra_files_to_hash* (*CCACHE_EXTRAFILES*)::
+[#config_extra_files_to_hash]
+*extra_files_to_hash* (*CCACHE_EXTRAFILES*)::
 
     This option is a list of paths to files that ccache will include in the the
     hash sum that identifies the build. The list separator is semicolon on
     Windows systems and colon on other systems.
 
-[[config_file_clone]] *file_clone* (*CCACHE_FILECLONE* or *CCACHE_NOFILECLONE*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_file_clone]
+*file_clone* (*CCACHE_FILECLONE* or *CCACHE_NOFILECLONE*, see _<<Boolean values>>_ above)::
 
-    If true, ccache will attempt to use file cloning (also known as ``copy on
-    write'', ``CoW'' or ``reflinks'') to store and fetch cached compiler results.
-    *file_clone* has priority over <<config_hard_link,*hard_link*>>. The
-    default is false.
+    If true, ccache will attempt to use file cloning (also known as "`copy on
+    write`", "`CoW`" or "`reflinks`") to store and fetch cached compiler
+    results. *file_clone* has priority over <<config_hard_link,*hard_link*>>.
+    The default is false.
 +
 Files stored by cloning cannot be compressed, so the cache size will likely be
 significantly larger if this option is enabled. However, performance may be
@@ -600,7 +634,8 @@ safe to use, but not all file systems support the feature. For such file
 systems, ccache will fall back to use plain copying (or hard links if
 <<config_hard_link,*hard_link*>> is enabled).
 
-[[config_hard_link]] *hard_link* (*CCACHE_HARDLINK* or *CCACHE_NOHARDLINK*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_hard_link]
+*hard_link* (*CCACHE_HARDLINK* or *CCACHE_NOHARDLINK*, see _<<Boolean values>>_ above)::
 
     If true, ccache will attempt to use hard links to store and fetch cached
     object files. The default is false.
@@ -621,25 +656,25 @@ WARNING: Do not enable this option unless you are aware of these caveats:
   file size will not.
 * Programs that don't expect that files from two different identical
   compilations are hard links to each other can fail.
-* Programs that rely on modification times (like ``make'') can be confused if
+* Programs that rely on modification times (like `make`) can be confused if
   several users (or one user with several build trees) use the same cache
   directory. The reason for this is that the object files share i-nodes and
-  therefore modification times. If *file.o* is in build tree A (hard-linked
-  from the cache) and *file.o* then is produced by ccache in build tree B by
+  therefore modification times. If `file.o` is in build tree *A* (hard-linked
+  from the cache) and `file.o` then is produced by ccache in build tree *B* by
   hard-linking from the cache, the modification timestamp will be updated for
-  *file.o* in build tree A as well. This can retrigger relinking in build tree
-  A even though nothing really has changed.
+  `file.o` in build tree *A* as well. This can retrigger relinking in build tree
+  *A* even though nothing really has changed.
 
-[[config_hash_dir]] *hash_dir* (*CCACHE_HASHDIR* or *CCACHE_NOHASHDIR*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_hash_dir]
+*hash_dir* (*CCACHE_HASHDIR* or *CCACHE_NOHASHDIR*, see _<<Boolean values>>_ above)::
 
     If true (which is the default), ccache will include the current working
     directory (CWD) in the hash that is used to distinguish two compilations
-    when generating debug info (compiler option *-g* with variations).
+    when generating debug info (compiler option `-g` with variations).
     Exception: The CWD will not be included in the hash if
     <<config_base_dir,*base_dir*>> is set (and matches the CWD) and the
-    compiler option *-fdebug-prefix-map* is used. See also the discussion under
-    _<<_compiling_in_different_directories,Compiling in different
-    directories>>_.
+    compiler option `-fdebug-prefix-map` is used. See also the discussion under
+    _<<Compiling in different directories>>_.
 +
 The reason for including the CWD in the hash by default is to prevent a problem
 with the storage of the current working directory in the debug info of an
@@ -650,7 +685,8 @@ You can disable this option to get cache hits when compiling the same source
 code in different directories if you don't mind that CWD in the debug info
 might be incorrect.
 
-[[config_ignore_headers_in_manifest]] *ignore_headers_in_manifest* (*CCACHE_IGNOREHEADERS*)::
+[#config_ignore_headers_in_manifest]
+*ignore_headers_in_manifest* (*CCACHE_IGNOREHEADERS*)::
 
     This option is a list of paths to files (or directories with headers) that
     ccache will *not* include in the manifest list that makes up the direct
@@ -658,17 +694,19 @@ might be incorrect.
     change. The list separator is semicolon on Windows systems and colon on
     other systems.
 
-[[config_ignore_options]] *ignore_options* (*CCACHE_IGNOREOPTIONS*)::
+[#config_ignore_options]
+*ignore_options* (*CCACHE_IGNOREOPTIONS*)::
 
     This option is a space-delimited list of compiler options that ccache will
     exclude from the hash. Excluding a compiler option from the hash can be
     useful when you know it doesn't affect the result (but ccache doesn't know
     that), or when it does and you don't care. If a compiler option in the list
     is suffixed with an asterisk (`*`) it will be matched as a prefix. For
-    example, `-fmessage-length=*` will match both `-fmessage-length=20` and
+    example, `+-fmessage-length=*+` will match both `-fmessage-length=20` and
     `-fmessage-length=70`.
 
-[[config_inode_cache]] *inode_cache* (*CCACHE_INODECACHE* or *CCACHE_NOINODECACHE*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_inode_cache]
+*inode_cache* (*CCACHE_INODECACHE* or *CCACHE_NOINODECACHE*, see _<<Boolean values>>_ above)::
 
     If true, enables caching of source file hashes based on device, inode and
     timestamps. This will reduce the time spent on hashing included files as
@@ -679,18 +717,21 @@ available on Windows.
 +
 The feature requires *temporary_dir* to be located on a local filesystem.
 
-[[config_keep_comments_cpp]] *keep_comments_cpp* (*CCACHE_COMMENTS* or *CCACHE_NOCOMMENTS*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_keep_comments_cpp]
+*keep_comments_cpp* (*CCACHE_COMMENTS* or *CCACHE_NOCOMMENTS*, see _<<Boolean values>>_ above)::
 
     If true, ccache will not discard the comments before hashing preprocessor
-    output. This can be used to check documentation with *-Wdocumentation*.
+    output. This can be used to check documentation with `-Wdocumentation`.
 
-[[config_limit_multiple]] *limit_multiple* (*CCACHE_LIMIT_MULTIPLE*)::
+[#config_limit_multiple]
+*limit_multiple* (*CCACHE_LIMIT_MULTIPLE*)::
 
     Sets the limit when cleaning up. Files are deleted (in LRU order) until the
     levels are below the limit. The default is 0.8 (= 80%). See
-    _<<_automatic_cleanup,Automatic cleanup>>_ for more information.
+    _<<Automatic cleanup>>_ for more information.
 
-[[config_log_file]] *log_file* (*CCACHE_LOGFILE*)::
+[#config_log_file]
+*log_file* (*CCACHE_LOGFILE*)::
 
     If set to a file path, ccache will write information on what it is doing to
     the specified file. This is useful for tracking down problems.
@@ -706,76 +747,91 @@ file in `/etc/rsyslog.d`:
 & ~
 -------------------------------------------------------------------------------
 
-[[config_max_files]] *max_files* (*CCACHE_MAXFILES*)::
+[#config_max_files]
+*max_files* (*CCACHE_MAXFILES*)::
 
     This option specifies the maximum number of files to keep in the cache. Use
-    0 for no limit (which is the default). See also
-    _<<_cache_size_management,Cache size management>>_.
+    0 for no limit (which is the default). See also _<<Cache size management>>_.
 
-[[config_max_size]] *max_size* (*CCACHE_MAXSIZE*)::
+[#config_max_size]
+*max_size* (*CCACHE_MAXSIZE*)::
 
-    This option specifies the maximum size of the cache. Use 0 for no limit.
-    The default value is 5G. Available suffixes: k, M, G, T (decimal) and Ki,
-    Mi, Gi, Ti (binary). The default suffix is G. See also
-    _<<_cache_size_management,Cache size management>>_.
+    This option specifies the maximum size of the cache. Use 0 for no limit. The
+    default value is 5G. Available suffixes: k, M, G, T (decimal) and Ki, Mi,
+    Gi, Ti (binary). The default suffix is G. See also
+    _<<Cache size management>>_.
 
-[[config_path]] *path* (*CCACHE_PATH*)::
+[#config_path]
+*path* (*CCACHE_PATH*)::
 
     If set, ccache will search directories in this list when looking for the
     real compiler. The list separator is semicolon on Windows systems and colon
     on other systems. If not set, ccache will look for the first executable
-    matching the compiler name in the normal *PATH* that isn't a symbolic link
+    matching the compiler name in the normal `PATH` that isn't a symbolic link
     to ccache itself.
 
-[[config_pch_external_checksum]] *pch_external_checksum* (*CCACHE_PCH_EXTSUM* or *CCACHE_NOPCH_EXTSUM*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_pch_external_checksum]
+*pch_external_checksum* (*CCACHE_PCH_EXTSUM* or *CCACHE_NOPCH_EXTSUM*, see _<<Boolean values>>_ above)::
 
     When this option is set, and ccache finds a precompiled header file,
-    ccache will look for a file with the extension ``.sum'' added
-    (e.g. ``pre.h.gch.sum''), and if found, it will hash this file instead
+    ccache will look for a file with the extension "`.sum`" added
+    (e.g. "`pre.h.gch.sum`"), and if found, it will hash this file instead
     of the precompiled header itself to work around the performance
     penalty of hashing very large files.
 
-[[config_prefix_command]] *prefix_command* (*CCACHE_PREFIX*)::
+[#config_prefix_command]
+*prefix_command* (*CCACHE_PREFIX*)::
 
-    This option adds a list of prefixes (separated by space) to the command
-    line that ccache uses when invoking the compiler. See also
-    _<<_using_ccache_with_other_compiler_wrappers,Using ccache with other
-    compiler wrappers>>_.
+    This option adds a list of prefixes (separated by space) to the command line
+    that ccache uses when invoking the compiler. See also
+    _<<Using ccache with other compiler wrappers>>_.
 
-[[config_prefix_command_cpp]] *prefix_command_cpp* (*CCACHE_PREFIX_CPP*)::
+[#config_prefix_command_cpp]
+*prefix_command_cpp* (*CCACHE_PREFIX_CPP*)::
 
     This option adds a list of prefixes (separated by space) to the command
     line that ccache uses when invoking the preprocessor.
 
-[[config_read_only]] *read_only* (*CCACHE_READONLY* or *CCACHE_NOREADONLY*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_read_only]
+*read_only* (*CCACHE_READONLY* or *CCACHE_NOREADONLY*, see _<<Boolean values>>_ above)::
 
     If true, ccache will attempt to use existing cached results, but it will not
-    add new results to the cache. Statistics counters will still be updated,
-    though, unless the <<config_stats,*stats*>> option is set to *false*.
+    add new results to any cache backend. Statistics counters will still be
+    updated, though, unless the <<config_stats,*stats*>> option is set to
+    *false*.
 +
 If you are using this because your ccache directory is read-only, you need to
 set <<config_temporary_dir,*temporary_dir*>> since ccache will fail to create
 temporary files otherwise. You may also want to set <<config_stats,*stats*>> to
 *false* make ccache not even try to update stats files.
 
-[[config_read_only_direct]] *read_only_direct* (*CCACHE_READONLY_DIRECT* or *CCACHE_NOREADONLY_DIRECT*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_read_only_direct]
+*read_only_direct* (*CCACHE_READONLY_DIRECT* or *CCACHE_NOREADONLY_DIRECT*, see _<<Boolean values>>_ above)::
 
     Just like <<config_read_only,*read_only*>> except that ccache will only try
     to retrieve results from the cache using the direct mode, not the
     preprocessor mode. See documentation for <<config_read_only,*read_only*>>
     regarding using a read-only ccache directory.
 
-[[config_recache]] *recache* (*CCACHE_RECACHE* or *CCACHE_NORECACHE*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_recache]
+*recache* (*CCACHE_RECACHE* or *CCACHE_NORECACHE*, see _<<Boolean values>>_ above)::
 
     If true, ccache will not use any previously stored result. New results will
     still be cached, possibly overwriting any pre-existing results.
 
-[[config_run_second_cpp]] *run_second_cpp* (*CCACHE_CPP2* or *CCACHE_NOCPP2*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_reshare]
+*reshare* (*CCACHE_RESHARE* or *CCACHE_NORESHARE*, see _<<Boolean values>>_ above)::
+
+    If true, ccache will write results to secondary storage even for primary
+    storage cache hits. The default is false.
+
+[#config_run_second_cpp]
+*run_second_cpp* (*CCACHE_CPP2* or *CCACHE_NOCPP2*, see _<<Boolean values>>_ above)::
 
     If true, ccache will first run the preprocessor to preprocess the source
-    code (see _<<_the_preprocessor_mode,The preprocessor mode>>_) and then on a
-    cache miss run the compiler on the source code to get hold of the object
-    file. This is the default.
+    code (see _<<The preprocessor mode>>_) and then on a cache miss run the
+    compiler on the source code to get hold of the object file. This is the
+    default.
 +
 If false, ccache will first run preprocessor to preprocess the source code and
 then on a cache miss run the compiler on the _preprocessed source code_ instead
@@ -785,13 +841,29 @@ compilers won't produce the same result (for instance diagnostics warnings)
 when compiling preprocessed source code.
 +
 A solution to the above mentioned downside is to set *run_second_cpp* to false
-and pass *-fdirectives-only* (for GCC) or *-frewrite-includes* (for Clang) to
+and pass `-fdirectives-only` (for GCC) or `-frewrite-includes` (for Clang) to
 the compiler. This will cause the compiler to leave the macros and other
 preprocessor information, and only process the *#include* directives. When run
 in this way, the preprocessor arguments will be passed to the compiler since it
 still has to do _some_ preprocessing (like macros).
 
-[[config_sloppiness]] *sloppiness* (*CCACHE_SLOPPINESS*)::
+[#config_secondary_storage]
+*secondary_storage* (*CCACHE_SECONDARY_STORAGE*)::
+
+    This option specifies one or several storage backends (separated by space)
+    to query after the primary cache storage. See
+    _<<Secondary storage backends>>_ for documentation of syntax and available
+    backends.
++
+Examples:
++
+* `+file:/shared/nfs/directory+`
+* `+file:///shared/nfs/one|read-only file:///shared/nfs/two+`
+* `+http://example.com/cache+`
+* `+redis://example.com+`
+
+[#config_sloppiness]
+*sloppiness* (*CCACHE_SLOPPINESS*)::
 
     By default, ccache tries to give as few false cache hits as possible.
     However, in certain situations it's possible that you know things that
@@ -802,7 +874,7 @@ still has to do _some_ preprocessing (like macros).
 +
 --
 *clang_index_store*::
-    Ignore the Clang compiler option *-index-store-path* and its argument when
+    Ignore the Clang compiler option `-index-store-path` and its argument when
     computing the manifest hash. This is useful if you use Xcode, which uses an
     index store path derived from the local project path. Note that the index
     store won't be updated correctly on cache hits if you enable this
@@ -817,77 +889,258 @@ still has to do _some_ preprocessing (like macros).
 *include_file_ctime*::
     By default, ccache will not cache a file if it includes a header whose ctime
     is too new. This sloppiness disables that check. See also
-    _<<_handling_of_newly_created_header_files,Handling of newly created header
-    files>>_.
+    _<<Handling of newly created header files>>_.
 *include_file_mtime*::
-    By default, ccache will not cache a file if it includes a header whose
-    mtime is too new. This sloppiness disables that check. See also
-    _<<_handling_of_newly_created_header_files,Handling of newly created header
-    files>>_.
+    By default, ccache will not cache a file if it includes a header whose mtime
+    is too new. This sloppiness disables that check. See also
+    _<<Handling of newly created header files>>_.
 *ivfsoverlay*::
-    Ignore the Clang compiler option *-ivfsoverlay* and its argument. This is
+    Ignore the Clang compiler option `-ivfsoverlay` and its argument. This is
     useful if you use Xcode, which uses a virtual file system (VFS) for things
     like combining Objective-C and Swift code.
 *locale*::
-    Ccache includes the environment variables *LANG*, *LC_ALL*, *LC_CTYPE* and
-    *LC_MESSAGES* in the hash by default since they may affect localization of
+    Ccache includes the environment variables `LANG`, `LC_ALL`, `LC_CTYPE` and
+    `LC_MESSAGES` in the hash by default since they may affect localization of
     compiler warning messages. Set this sloppiness to tell ccache not to do
     that.
 *pch_defines*::
-    Be sloppy about **#define**s when precompiling a header file. See
-    _<<_precompiled_headers,Precompiled headers>>_ for more information.
+    Be sloppy about `#define` directives when precompiling a header file. See
+    _<<Precompiled headers>>_ for more information.
 *modules*::
-    By default, ccache will not cache compilations if *-fmodules* is used since
+    By default, ccache will not cache compilations if `-fmodules` is used since
     it cannot hash the state of compiler's internal representation of relevant
     modules. This sloppiness allows caching in such a case. See
-    _<<_c_modules,C++ modules>>_ for more information.
+    _<<C++ modules>>_ for more information.
 *system_headers*::
     By default, ccache will also include all system headers in the manifest.
     With this sloppiness set, ccache will only include system headers in the
     hash but not add the system header files to the list of include files.
 *time_macros*::
-    Ignore `__DATE__`, `__TIME__` and `__TIMESTAMP__` being present in the
+    Ignore `+__DATE__+`, `+__TIME__+` and `+__TIMESTAMP__+` being present in the
     source code.
 --
 +
-See the discussion under _<<_troubleshooting,Troubleshooting>>_ for more
-information.
+See the discussion under _<<Troubleshooting>>_ for more information.
 
-[[config_stats]] *stats* (*CCACHE_STATS* or *CCACHE_NOSTATS*, see _<<_boolean_values,Boolean values>>_ above)::
+[#config_stats]
+*stats* (*CCACHE_STATS* or *CCACHE_NOSTATS*, see _<<Boolean values>>_ above)::
 
     If true, ccache will update the statistics counters on each compilation.
     The default is true.
 
-[[config_temporary_dir]] *temporary_dir* (*CCACHE_TEMPDIR*)::
+[#config_stats_log]
+*stats_log* (*CCACHE_STATSLOG*)::
+
+    If set to a file path, ccache will write statistics counter updates to the
+    specified file. This is useful for getting statistics for individual builds.
+    To show a summary of the current stats log, use `ccache --show-log-stats`.
++
+NOTE: Lines in the stats log starting with a hash sign (`#`) are comments.
+
+[#config_temporary_dir]
+*temporary_dir* (*CCACHE_TEMPDIR*)::
 
     This option specifies where ccache will put temporary files. The default is
-    */run/user/<UID>/ccache-tmp* if */run/user/<UID>* exists, otherwise
-    *<cache_dir>/tmp*.
+    `/run/user/<UID>/ccache-tmp` if `/run/user/<UID>` exists, otherwise
+    `<cache_dir>/tmp`.
 +
 NOTE: In previous versions of ccache, *CCACHE_TEMPDIR* had to be on the same
-filesystem as the *CCACHE_DIR* path, but this requirement has been relaxed.)
+filesystem as the `CCACHE_DIR` path, but this requirement has been relaxed.
+
+[#config_umask]
+*umask* (*CCACHE_UMASK*)::
+
+    This option (an octal integer) specifies the umask for files and directories
+    in the cache directory. This is mostly useful when you wish to share your
+    cache with other users.
+
+
+== Secondary storage backends
+
+The <<config_secondary_storage,*secondary_storage*>> option lets you configure
+ccache to use one or several other storage backends in addition to the primary
+cache storage located in <<config_cache_dir,*cache_dir*>>. Note that cache
+statistics counters will still be kept in the primary cache directory --
+secondary storage backends only store cache results and manifests.
+
+A secondary storage backend is specified with a URL, optionally followed by a
+pipe (`|`) and a pipe-separated list of attributes. An attribute is
+_key_=_value_ or just _key_ as a short form of _key_=*true*. Attribute values
+must be https://en.wikipedia.org/wiki/Percent-encoding[percent-encoded] if they
+contain percent, pipe or space characters.
+
+=== Attributes for all backends
+
+These optional attributes are available for all secondary storage backends:
+
+* *read-only*: If *true*, only read from this backend, don't write. The default
+  is *false*.
+* *shards*: A comma-separated list of names for sharding (partitioning) the
+  cache entries using
+  https://en.wikipedia.org/wiki/Rendezvous_hashing[Rendezvous hashing],
+  typically to spread the cache over a server cluster. When set, the storage URL
+  must contain an asterisk (`+*+`), which will be replaced by one of the shard
+  names to form a real URL. A shard name can optionally have an appended weight
+  within parentheses to indicate how much of the key space should be associated
+  with that shard. A shard with weight *w* will contain *w*/*S* of the cache,
+  where *S* is the sum of all shard weights. A weight could for instance be set
+  to represent the available memory for a memory cache on a specific server. The
+  default weight is *1*.
++
+Examples:
++
+--
+* `+redis://cache-*.example.com|shards=a(3),b(1),c(1.5)+` will put 55% (3/5.5)
+  of the cache on `+redis://cache-a.example.com+`, 18% (1/5.5) on
+  `+redis://cache-b.example.com+` and 27% (1.5/5.5) on
+  `+redis://cache-c.example.com+`.
+* `+http://example.com/*|shards=alpha,beta+` will put 50% of the cache on
+  `+http://example.com/alpha+` and 50% on `+http://example.com/beta+`.
+--
+* *share-hits*: If *true*, write hits for this backend to primary storage. The
+  default is *true*.
+
+
+=== Storage interaction
+
+The table below describes the interaction between primary and secondary storage
+on cache hits and misses:
+
+[options="header",cols="20%,20%,60%"]
+|==============================================================================
+| *Primary storage* | *Secondary storage* | *What happens*
+
+| miss | miss | Compile, write to primary, write to secondary^[1]^
+| miss | hit  | Read from secondary, write to primary^[2]^
+| hit  | -    | Read from primary, don't write to secondary^[3]^
+
+|==============================================================================
+
+^[1]^ Unless secondary storage has attribute `read-only=true`. +
+^[2]^ Unless secondary storage has attribute `share-hits=false`. +
+^[3]^ Unless primary storage is set to share its cache hits with the
+<<config_reshare,*reshare*>> option.
+
+
+
+=== File storage backend
+
+URL format: `+file:DIRECTORY+` or `+file://DIRECTORY+`
+
+This backend stores data as separate files in a directory structure below
+*DIRECTORY* (an absolute path), similar (but not identical) to the primary cache
+storage. A typical use case for this backend would be sharing a cache on an NFS
+directory.
+
+IMPORTANT: ccache will not perform any cleanup of the storage -- that has to be
+done by other means, for instance by running `ccache --trim-dir` periodically.
+
+Examples:
+
+* `+file:/shared/nfs/directory+`
+* `+file:///shared/nfs/directory|umask=002|update-mtime=true+`
+
+Optional attributes:
 
-[[config_umask]] *umask* (*CCACHE_UMASK*)::
+* *layout*: How to store file under the cache directory. Available values:
++
+--
+* *flat*: Store all files directly under the cache directory.
+* *subdirs*: Store files in 256 subdirectories of the cache directory.
+--
++
+The default is *subdirs*.
+* *umask*: This attribute (an octal integer) overrides the umask to use for
+  files and directories in the cache directory.
+* *update-mtime*: If *true*, update the modification time (mtime) of cache
+  entries that are read. The default is *false*.
+
+
+=== HTTP storage backend
+
+URL format: `+http://HOST[:PORT][/PATH]+`
+
+This backend stores data in an HTTP-compatible server. The required HTTP methods
+are `GET`, `PUT` and `DELETE`.
+
+IMPORTANT: ccache will not perform any cleanup of the storage -- that has to be
+done by other means, for instance by running `ccache --trim-dir` periodically.
+
+NOTE: HTTPS is not supported.
+
+TIP: See https://ccache.dev/howto/http-storage.html[How to set up HTTP storage]
+for hints on how to set up an HTTP server for use with ccache.
+
+Examples:
+
+* `+http://localhost+`
+* `+http://someusername:p4ssw0rd@example.com/cache/+`
+* `+http://localhost:8080|layout=bazel|connect-timeout=50+`
+
+Optional attributes:
+
+* *connect-timeout*: Timeout (in ms) for network connection. The default is 100.
+* *layout*: How to map key names to the path part of the URL. Available values:
++
+--
+* *bazel*: Store values in a format compatible with the Bazel HTTP caching
+   protocol. More specifically, the entries will be stored as 64 hex digits
+   under the `/ac/` part of the cache.
++
+NOTE: You may have to disable verification of action cache values in the server
+for this to work since ccache entries are not valid action result metadata
+values.
+* *flat*: Append the key directly to the path part of the URL (with a leading
+   slash if needed).
+* *subdirs*: Append the first two characters of the key to the URL (with a
+  leading slash if needed), followed by a slash and the rest of the key. This
+  divides the entries into 256 buckets.
+--
++
+The default is *subdirs*.
+* *operation-timeout*: Timeout (in ms) for HTTP requests. The default is 10000.
+
+
+=== Redis storage backend
+
+URL format: `+redis://[[USERNAME:]PASSWORD@]HOST[:PORT][/DBNUMBER]+`
+
+This backend stores data in a https://redis.io[Redis] (or Redis-compatible)
+server. There are implementations for both memory-based and disk-based storage.
+*PORT* defaults to *6379* and *DBNUMBER* defaults to *0*.
 
-    This option specifies the umask for files and directories in the cache
-    directory. This is mostly useful when you wish to share your cache with
-    other users.
+NOTE: ccache will not perform any cleanup of the Redis storage, but you can
+https://redis.io/topics/lru-cache[configure LRU eviction].
 
+TIP: See https://ccache.dev/howto/redis-storage.html[How to set up Redis
+storage] for hints on setting up a Redis server for use with ccache.
 
-Cache size management
----------------------
+TIP: You can set up a cluster of Redis servers using the `shards` attribute
+described in _<<Secondary storage backends>>_.
+
+Examples:
+
+* `+redis://localhost+`
+* `+redis://p4ssw0rd@cache.example.com:6379/0|connect-timeout=50+`
+
+Optional attributes:
+
+* *connect-timeout*: Timeout (in ms) for network connection. The default is 100.
+* *operation-timeout*: Timeout (in ms) for Redis commands. The default is 10000.
+
+
+== Cache size management
 
 By default, ccache has a 5 GB limit on the total size of files in the cache and
 no limit on the number of files. You can set different limits using the command
-line options *-M*/*--max-size* and *-F*/*--max-files*. Use *ccache
--s/--show-stats* to see the cache size and the currently configured limits (in
-addition to other various statistics).
+line options `-M`/`--max-size` and `-F`/`--max-files`. Use the
+`-s`/`--show-stats` option to see the cache size and the currently configured
+limits (in addition to other various statistics).
 
 Cleanup can be triggered in two different ways: automatic and manual.
 
 
-Automatic cleanup
-~~~~~~~~~~~~~~~~~
+=== Automatic cleanup
 
 Ccache maintains counters for various statistics about the cache, including the
 size and number of all cached files. In order to improve performance and reduce
@@ -916,10 +1169,9 @@ limits is that a cleanup is a fairly slow operation, so it would not be a good
 idea to trigger it often, like after each cache miss.
 
 
-Manual cleanup
-~~~~~~~~~~~~~~
+=== Manual cleanup
 
-You can run *ccache -c/--cleanup* to force cleanup of the whole cache, i.e. all
+You can run `ccache -c/--cleanup` to force cleanup of the whole cache, i.e. all
 of the sixteen subdirectories. This will recalculate the statistics counters
 and make sure that the configuration options *max_size* and
 <<config_max_files,*max_files*>> are not exceeded. Note that
@@ -927,8 +1179,7 @@ and make sure that the configuration options *max_size* and
 cleanup.
 
 
-Cache compression
------------------
+== Cache compression
 
 Ccache will by default compress all data it puts into the cache using the
 compression algorithm http://zstd.net[Zstandard] (zstd) using compression level
@@ -939,24 +1190,24 @@ course is redundant. See the documentation for the configuration options
 <<config_compression,*compression*>> and
 <<config_compression_level,*compression_level*>> for more information.
 
-You can use the command line option *-x/--show-compression* to print
+You can use the command line option `-x`/`--show-compression` to print
 information related to compression. Example:
 
 -------------------------------------------------------------------------------
-Total data:              14.8 GB (16.0 GB disk blocks)
-Compressed data:         11.3 GB (30.6% of original size)
-  - Original data:       36.9 GB
-  - Compression ratio:  3.267 x  (69.4% space savings)
-Incompressible data:      3.5 GB
+Total data:           14.8 GB (16.0 GB disk blocks)
+Compressed data:      11.3 GB (30.6% of original size)
+  Original size:      36.9 GB
+  Compression ratio: 3.267 x  (69.4% space savings)
+Incompressible data:   3.5 GB
 -------------------------------------------------------------------------------
 
 Notes:
 
-* The ``disk blocks'' size is the cache size when taking disk block size into
-  account. This value should match the ``cache size'' value from ``ccache
-  --show-stats''. The other size numbers refer to actual content sizes.
-* ``Compressed data'' refers to result and manifest files stored in the cache.
-* ``Incompressible data'' refers to files that are always stored uncompressed
+* The "`disk blocks`" size is the cache size when taking disk block size into
+  account. This value should match the "`Cache size`" value from "`ccache
+  --show-stats`". The other size numbers refer to actual content sizes.
+* "`Compressed data`" refers to result and manifest files stored in the cache.
+* "`Incompressible data`" refers to files that are always stored uncompressed
   (triggered by enabling <<config_file_clone,*file_clone*>> or
   <<config_hard_link,*hard_link*>>) or unknown files (for instance files
   created by older ccache versions).
@@ -964,7 +1215,7 @@ Notes:
   <<config_compression_level,*compression_level*>>.
 
 The cache data can also be recompressed to another compression level (or made
-uncompressed) with the command line option *-X/--recompress*. If you choose to
+uncompressed) with the command line option `-X`/`--recompress`. If you choose to
 disable compression by default or to use a low compression level, you can
 (re)compress newly cached data with a higher compression level after the build
 or at another time when there are more CPU cycles available, for instance every
@@ -973,130 +1224,114 @@ are currently compressed with a different level than the target level will be
 recompressed.
 
 
-Cache statistics
-----------------
+== Cache statistics
 
-*ccache -s/--show-stats* can show the following statistics:
+`ccache --show-stats` shows a summary of statistics, including cache size,
+cleanups (number of performed cleanups, either implicitly due to a cache size
+limit being reached or due to explicit `ccache -c` calls), overall hit rate, hit
+rate for <<The direct mode,direct>>/<<The preprocessor mode,preprocessed>> modes
+and hit rate for primary and <<config_secondary_storage,secondary>> storage.
+
+The summary also includes counters called "`Errors`" and "`Uncacheable`", which
+are sums of more detailed counters. To see those detailed counters, use the
+`-v`/`--verbose` flag. The verbose mode can show the following counters:
 
 [options="header",cols="30%,70%"]
 |==============================================================================
-|Name | Description
-| autoconf compile/link |
-Uncachable compilation or linking by an autoconf test.
+| *Counter* | *Description*
+
+| Autoconf compile/link |
+Uncacheable compilation or linking by an Autoconf test.
 
-| bad compiler arguments |
+| Bad compiler arguments |
 Malformed compiler argument, e.g. missing a value for a compiler option that
 requires an argument or failure to read a file specified by a compiler option
 argument.
 
-| cache file missing |
-A file was unexpectedly missing from the cache. This only happens in rare
-situations, e.g. if one ccache instance is about to get a file from the cache
-while another instance removed the file as part of cache cleanup.
-
-| cache hit (direct) |
-A result was successfully found using <<_the_direct_mode,the direct mode>>.
-
-| cache hit (preprocessed) |
-A result was successfully found using <<_the_preprocessor_mode,the preprocessor
-mode>>.
-
-| cache miss |
-No result was found.
-
-| cache size |
-Current size of the cache.
-
-| called for link |
+| Called for linking |
 The compiler was called for linking, not compiling. Ccache only supports
-compilation of a single file, i.e. calling the compiler with the *-c* option to
+compilation of a single file, i.e. calling the compiler with the `-c` option to
 produce a single object file from a single source file.
 
-| called for preprocessing |
+| Called for preprocessing |
 The compiler was called for preprocessing, not compiling.
 
-| can't use precompiled header |
-Preconditions for using <<_precompiled_headers,precompiled headers>> were not
-fulfilled.
-
-| can't use modules |
-Preconditions for using <<_c_modules,C++ modules>> were not fulfilled.
+| Could not use modules |
+Preconditions for using <<C++ modules>> were not fulfilled.
 
-| ccache internal error |
-Unexpected failure, e.g. due to problems reading/writing the cache.
+| Could not use precompiled header |
+Preconditions for using <<Precompiled headers,precompiled headers>> were not
+fulfilled.
 
-| cleanups performed |
-Number of cleanups performed, either implicitly due to the cache size limit
-being reached or due to explicit *ccache -c/--cleanup* calls.
+| Could not write to output file |
+The output path specified with `-o` is not a file (e.g. a directory or a device
+node).
 
-| compile failed |
+| Compilation failed |
 The compilation failed. No result stored in the cache.
 
-| compiler check failed |
+| Compiler check failed |
 A compiler check program specified by
 <<config_compiler_check,*compiler_check*>> (*CCACHE_COMPILERCHECK*) failed.
 
-| compiler produced empty output |
+| Compiler produced empty output |
 The compiler's output file (typically an object file) was empty after
 compilation.
 
-| compiler produced no output |
+| Compiler produced no output |
 The compiler's output file (typically an object file) was missing after
 compilation.
 
-| compiler produced stdout |
+| Compiler produced stdout |
 The compiler wrote data to standard output. This is something that compilers
 normally never do, so ccache is not designed to store such output in the cache.
 
-| couldn't find the compiler |
+| Could not find the compiler |
 The compiler to execute could not be found.
 
-| error hashing extra file |
+| Error hashing extra file |
 Failure reading a file specified by
 <<config_extra_files_to_hash,*extra_files_to_hash*>> (*CCACHE_EXTRAFILES*).
 
-| files in cache |
-Current number of files in the cache.
+| Forced recache |
+<<config_recache,*CCACHE_RECACHE*>> was used to overwrite an existing result.
+
+| Internal error |
+Unexpected failure, e.g. due to problems reading/writing the cache.
+
+| Missing cache file |
+A file was unexpectedly missing from the cache. This only happens in rare
+situations, e.g. if one ccache instance is about to get a file from the cache
+while another instance removed the file as part of cache cleanup.
 
-| multiple source files |
+| Multiple source files |
 The compiler was called to compile multiple source files in one go. This is not
 supported by ccache.
 
-| no input file |
+| No input file |
 No input file was specified to the compiler.
 
-| output to a non-regular file |
-The output path specified with *-o* is not a file (e.g. a directory or a device
-node).
-
-| output to stdout |
-The compiler was instructed to write its output to standard output using *-o
--*. This is not supported by ccache.
-
-| preprocessor error |
-Preprocessing the source code using the compiler's *-E* option failed.
+| Output to stdout |
+The compiler was instructed to write its output to standard output using `-o -`.
+This is not supported by ccache.
 
-| stats updated |
-When statistics were updated the last time.
+| Preprocessing failed |
+Preprocessing the source code using the compiler's `-E` option failed.
 
-| stats zeroed |
-When *ccache -z* was called the last time.
-
-| unsupported code directive |
-Code like the assembler *.incbin* directive was found. This is not supported
+| Unsupported code directive |
+Code like the assembler `.incbin` directive was found. This is not supported
 by ccache.
 
-| unsupported compiler option |
+| Unsupported compiler option |
 A compiler option not supported by ccache was found.
 
-| unsupported source language |
-A source language e.g. specified with *-x* was unsupported by ccache.
+| Unsupported source language |
+A source language e.g. specified with `-x` was unsupported by ccache.
 
 |==============================================================================
 
 
-How ccache works
-----------------
+== How ccache works
 
 The basic idea is to detect when you are compiling exactly the same code a
 second time and reuse the previously produced output. The detection is done by
@@ -1119,20 +1354,18 @@ cache:
 The direct mode is generally faster since running the preprocessor has some
 overhead.
 
-If no previous result is detected (i.e., there is a cache miss) using the
-direct mode, ccache will fall back to the preprocessor mode unless the *depend
-mode* is enabled. In the depend mode, ccache never runs the preprocessor, not
-even on cache misses. Read more in _<<_the_depend_mode,The depend mode>>_
-below.
+If no previous result is detected (i.e., there is a cache miss) using the direct
+mode, ccache will fall back to the preprocessor mode unless the *depend mode* is
+enabled. In the depend mode, ccache never runs the preprocessor, not even on
+cache misses. Read more in _<<The depend mode>>_ below.
 
 
-Common hashed information
-~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Common hashed information
 
 The following information is always included in the hash:
 
 * the extension used by the compiler for a file with preprocessor output
-  (normally *.i* for C code and *.ii* for C++ code)
+  (normally `.i` for C code and `.ii` for C++ code)
 * the compiler's size and modification time (or other compiler-specific
   information specified by <<config_compiler_check,*compiler_check*>>)
 * the name of the compiler
@@ -1141,14 +1374,13 @@ The following information is always included in the hash:
   <<config_extra_files_to_hash,*extra_files_to_hash*>> (if any)
 
 
-The preprocessor mode
-~~~~~~~~~~~~~~~~~~~~~
+=== The preprocessor mode
 
 In the preprocessor mode, the hash is formed of the common information and:
 
-* the preprocessor output from running the compiler with *-E*
-* the command line options except those that affect include files (*-I*,
-  *-include*, *-D*, etc; the theory is that these command line options will
+* the preprocessor output from running the compiler with `-E`
+* the command line options except those that affect include files (`-I`,
+  `-include`, `-D`, etc; the theory is that these command line options will
   change the preprocessor output if they have any effect at all)
 * any standard error output generated by the preprocessor
 
@@ -1156,15 +1388,14 @@ Based on the hash, the cached compilation result can be looked up directly in
 the cache.
 
 
-The direct mode
-~~~~~~~~~~~~~~~
+=== The direct mode
 
 In the direct mode, the hash is formed of the common information and:
 
 * the input source file
 * the compiler options
 
-Based on the hash, a data structure called ``manifest'' is looked up in the
+Based on the hash, a data structure called "`manifest`" is looked up in the
 cache. The manifest contains:
 
 * references to cached compilation results (object file, dependency file, etc)
@@ -1195,19 +1426,18 @@ The direct mode will be disabled if any of the following holds:
 * a modification time of one of the include files is too new (needed to avoid a
   race condition)
 * a compiler option not supported by the direct mode is used:
-** a *-Wp,_X_* compiler option other than *-Wp,-MD,_path_*,
-   *-Wp,-MMD,_path_* and *-Wp,-D_define_*
-** *-Xpreprocessor*
-* the string `__TIME__` is present in the source code
+** a `-Wp,++*++` compiler option other than `-Wp,-MD,<path>`, `-Wp,-MMD,<path>`
+   and `-Wp,-D<define>`
+** `-Xpreprocessor`
+* the string `+__TIME__+` is present in the source code
 
 
-The depend mode
-~~~~~~~~~~~~~~~
+=== The depend mode
 
 If the depend mode is enabled, ccache will not use the preprocessor at all. The
 hash used to identify results in the cache will be based on the direct mode
 hash described above plus information about include files read from the
-dependency file generated by the compiler with *-MD* or *-MMD*.
+dependency file generated by the compiler with `-MD` or `-MMD`.
 
 Advantages:
 
@@ -1232,15 +1462,14 @@ The depend mode will be disabled if any of the following holds:
 
 * <<config_depend_mode,*depend_mode*>> is false.
 * <<config_run_second_cpp,*run_second_cpp*>> is false.
-* The compiler is not generating dependencies using *-MD* or *-MMD*.
+* The compiler is not generating dependencies using `-MD` or `-MMD`.
 
 
-Handling of newly created header files
---------------------------------------
+== Handling of newly created header files
 
 If modification time (mtime) or status change time (ctime) of one of the include
 files is the same second as the time compilation is being done, ccache disables
-the direct mode (or, in the case of a <<_precompiled_headers,precompiled
+the direct mode (or, in the case of a <<Precompiled headers,precompiled
 header>>, disables caching completely). This done as a safety measure to avoid a
 race condition (see below).
 
@@ -1263,12 +1492,11 @@ For reference, the race condition mentioned above consists of these events:
 5. The wrong object file is stored in the cache.
 
 
-Cache debugging
----------------
+== Cache debugging
 
 To find out what information ccache actually is hashing, you can enable the
 debug mode via the configuration option <<config_debug,*debug*>> or by setting
-*CCACHE_DEBUG* in the environment. This can be useful if you are investigating
+`CCACHE_DEBUG` in the environment. This can be useful if you are investigating
 why you don't get cache hits. Note that performance will be reduced slightly.
 
 When the debug mode is enabled, ccache will create up to five additional files
@@ -1276,61 +1504,62 @@ next to the object file:
 
 [options="header",cols="30%,70%"]
 |==============================================================================
-|Filename | Description
-| *<objectfile>.ccache-input-c* |
+| *Filename* | *Description*
+
+| `<objectfile>.ccache-input-c` |
 Binary input hashed by both the direct mode and the preprocessor mode.
 
-| *<objectfile>.ccache-input-d* |
+| `<objectfile>.ccache-input-d` |
 Binary input only hashed by the direct mode.
 
-| *<objectfile>.ccache-input-p* |
+| `<objectfile>.ccache-input-p` |
 Binary input only hashed by the preprocessor mode.
 
-| *<objectfile>.ccache-input-text* |
+| `<objectfile>.ccache-input-text` |
 Human-readable combined diffable text version of the three files above.
 
-| *<objectfile>.ccache-log* |
+| `<objectfile>.ccache-log` |
 Log for this object file.
 
 |==============================================================================
 
-If <<config_debug_dir,*config_dir*>> (environment variable *CCACHE_DEBUGDIR*) is
+If <<config_debug_dir,*config_dir*>> (environment variable `CCACHE_DEBUGDIR`) is
 set, the files above will be written to that directory with full absolute paths
 instead of next to the object file.
 
 In the direct mode, ccache uses the 160 bit BLAKE3 hash of the
-*ccache-input-c* + *ccache-input-d* data (where *+* means concatenation), while
-the *ccache-input-c* + *ccache-input-p* data is used in the preprocessor mode.
+"`ccache-input-c`" + "`ccache-input-d`" data (where *+* means concatenation),
+while the "`ccache-input-c`" + "`ccache-input-p`" data is used in the
+preprocessor mode.
 
-The *ccache-input-text* file is a combined text version of the three
-binary input files. It has three sections (``COMMON'', ``DIRECT MODE'' and
-``PREPROCESSOR MODE''), which is turn contain annotations that say what kind of
+The "`ccache-input-text`" file is a combined text version of the three binary
+input files. It has three sections ("`COMMON`", "`DIRECT MODE`" and
+"`PREPROCESSOR MODE`"), which is turn contain annotations that say what kind of
 data comes next.
 
 To debug why you don't get an expected cache hit for an object file, you can do
 something like this:
 
 1. Build with debug mode enabled.
-2. Save the *<objectfile>.ccache-&#42;* files.
+2. Save the `<objectfile>.ccache-++*++` files.
 3. Build again with debug mode enabled.
-4. Compare *<objectfile>.ccache-input-text* for the two builds. This together
-   with the *<objectfile>.ccache-log* files should give you some clues about
+4. Compare `<objectfile>.ccache-input-text` for the two builds. This together
+   with the `<objectfile>.ccache-log` files should give you some clues about
    what is happening.
 
 
-Compiling in different directories
-----------------------------------
+== Compiling in different directories
 
 Some information included in the hash that identifies a unique compilation can
 contain absolute paths:
 
 * The preprocessed source code may contain absolute paths to include files if
-  the compiler option *-g* is used or if absolute paths are given to *-I* and
+  the compiler option `-g` is used or if absolute paths are given to `-I` and
   similar compiler options.
-* Paths specified by compiler options (such as *-I*, *-MF*, etc) on the command
+* Paths specified by compiler options (such as `-I`, `-MF`, etc) on the command
   line may be absolute.
 * The source code file path may be absolute, and that path may substituted for
-  `__FILE__` macros in the source code or included in warnings emitted to
+  `+__FILE__+` macros in the source code or included in warnings emitted to
   standard error by the preprocessor.
 
 This means that if you compile the same code in different locations, you can't
@@ -1341,73 +1570,66 @@ hash.
 Here's what can be done to enable cache hits between different build
 directories:
 
-* If you build with *-g* (or similar) to add debug information to the object
+* If you build with `-g` (or similar) to add debug information to the object
   file, you must either:
-+
---
-** use the compiler option *-fdebug-prefix-map=_old_=_new_* for relocating
-   debug info to a common prefix (e.g. *-fdebug-prefix-map=$PWD=.*); or
+** use the compiler option `-fdebug-prefix-map=<old>=<new>` for relocating
+   debug info to a common prefix (e.g. `-fdebug-prefix-map=$PWD=.`); or
 ** set *hash_dir = false*.
---
 * If you use absolute paths anywhere on the command line (e.g. the source code
-  file path or an argument to compiler options like *-I* and *-MF*), you must
-  set <<config_base_dir,*base_dir*>> to an absolute path to a ``base
-  directory''. Ccache will then rewrite absolute paths under that directory to
+  file path or an argument to compiler options like `-I` and `-MF`), you must
+  set <<config_base_dir,*base_dir*>> to an absolute path to a "`base
+  directory`". Ccache will then rewrite absolute paths under that directory to
   relative before computing the hash.
 
 
-Precompiled headers
--------------------
+== Precompiled headers
 
 Ccache has support for GCC's precompiled headers. However, you have to do some
 things to make it work properly:
 
 * You must set <<config_sloppiness,*sloppiness*>> to *pch_defines,time_macros*.
-  The reason is that ccache can't tell whether `__TIME__`, `__DATE__` or
-  `__TIMESTAMP__` is used when using a precompiled header. Further, it can't
-  detect changes in **#define**s in the source code because of how
-  preprocessing works in combination with precompiled headers.
+  The reason is that ccache can't tell whether `+__TIME__+`, `+__DATE__+` or
+  `+__TIMESTAMP__+` is used when using a precompiled header. Further, it can't
+  detect changes in ``#define``s in the source code because of how preprocessing
+  works in combination with precompiled headers.
 * You may also want to include *include_file_mtime,include_file_ctime* in
   <<config_sloppiness,*sloppiness*>>. See
-  _<<_handling_of_newly_created_header_files,Handling of newly created header
-  files>>_.
+  _<<Handling of newly created header files>>_.
 * You must either:
 +
 --
-** use the compiler option *-include* to include the precompiled header (i.e.,
-   don't use *#include* in the source code to include the header; the filename
-   itself must be sufficient to find the header, i.e. *-I* paths are not
+* use the compiler option `-include` to include the precompiled header (i.e.,
+   don't use `#include` in the source code to include the header; the filename
+   itself must be sufficient to find the header, i.e. `-I` paths are not
    searched); or
-** (for the Clang compiler) use the compiler option *-include-pch* to include
+* (for the Clang compiler) use the compiler option `-include-pch` to include
    the PCH file generated from the precompiled header; or
-** (for the GCC compiler) add the compiler option *-fpch-preprocess* when
+* (for the GCC compiler) add the compiler option `-fpch-preprocess` when
    compiling.
-
-If you don't do this, either the non-precompiled version of the header file
-will be used (if available) or ccache will fall back to running the real
-compiler and increase the statistics counter ``preprocessor error'' (if the
-non-precompiled header file is not available).
 --
++
+If you don't do this, either the non-precompiled version of the header file will
+be used (if available) or ccache will fall back to running the real compiler and
+increase the statistics counter "`Preprocessing failed`" (if the non-precompiled
+header file is not available).
 
 
-C++ modules
------------
+== C++ modules
 
-Ccache has support for Clang's *-fmodules* option. In practice ccache only
-additionally hashes *module.modulemap* files; it does not know how Clang
+Ccache has support for Clang's `-fmodules` option. In practice ccache only
+additionally hashes `module.modulemap` files; it does not know how Clang
 handles its cached binary form of modules so those are ignored. This should not
-matter in practice: as long as everything else (including *module.modulemap*
+matter in practice: as long as everything else (including `module.modulemap`
 files) is the same the cached result should work. Still, you must set
 <<config_sloppiness,*sloppiness*>> to *modules* to allow caching.
 
-You must use both <<_the_direct_mode,*direct mode*>> and
-<<_the_depend_mode,*depend mode*>>. When using <<_the_preprocessor_mode,the
-preprocessor mode>> Clang does not provide enough information to allow hashing
-of *module.modulemap* files.
+You must use both <<The direct mode,*direct mode*>> and
+<<The depend mode,*depend mode*>>. When using
+<<The preprocessor mode,the preprocessor mode>> Clang does not provide enough
+information to allow hashing of `module.modulemap` files.
 
 
-Sharing a cache
----------------
+== Sharing a cache
 
 A group of developers can increase the cache hit rate by sharing a cache
 directory. To share a cache without unpleasant side effects, the following
@@ -1417,7 +1639,7 @@ conditions should to be met:
 * Make sure that the configuration option <<config_hard_link,*hard_link*>> is
   false (which is the default).
 * Make sure that all users are in the same group.
-* Set the configuration option <<config_umask,*umask*>> to 002. This ensures
+* Set the configuration option <<config_umask,*umask*>> to *002*. This ensures
   that cached files are accessible to everyone in the group.
 * Make sure that all users have write permission in the entire cache directory
   (and that you trust all users of the shared cache).
@@ -1425,11 +1647,9 @@ conditions should to be met:
   tells the filesystem to inherit group ownership for new directories. The
   following command might be useful for this:
 +
---
 ----
 find $CCACHE_DIR -type d | xargs chmod g+s
 ----
---
 
 The reason to avoid the hard link mode is that the hard links cause unwanted
 side effects, as all links to a cached file share the file's modification
@@ -1442,8 +1662,7 @@ You may also want to make sure that a base directory is set appropriately, as
 discussed in a previous section.
 
 
-Sharing a cache on NFS
-----------------------
+== Sharing a cache on NFS
 
 It is possible to put the cache directory on an NFS filesystem (or similar
 filesystems), but keep in mind that:
@@ -1462,26 +1681,27 @@ systems. One way of improving cache hit rate in that case is to set
 <<config_sloppiness,*sloppiness*>> to *system_headers* to ignore system
 headers.
 
+An alternative to putting the main cache directory on NFS is to set up a
+<<config_secondary_storage,secondary storage>> file cache.
+
 
-Using ccache with other compiler wrappers
------------------------------------------
+== Using ccache with other compiler wrappers
 
 The recommended way of combining ccache with another compiler wrapper (such as
-``distcc'') is by letting ccache execute the compiler wrapper. This is
+"`distcc`") is by letting ccache execute the compiler wrapper. This is
 accomplished by defining <<config_prefix_command,*prefix_command*>>, for
-example by setting the environment variable *CCACHE_PREFIX* to the name of the
-wrapper (e.g. *distcc*). Ccache will then prefix the command line with the
+example by setting the environment variable `CCACHE_PREFIX` to the name of the
+wrapper (e.g. `distcc`). Ccache will then prefix the command line with the
 specified command when running the compiler. To specify several prefix
 commands, set <<config_prefix_command,*prefix_command*>> to a colon-separated
 list of commands.
 
 Unless you set <<config_compiler_check,*compiler_check*>> to a suitable command
-(see the description of that configuration option), it is not recommended to
-use the form *ccache anotherwrapper compiler args* as the compilation command.
-It's also not recommended to use the masquerading technique for the other
-compiler wrapper. The reason is that by default, ccache will in both cases hash
-the mtime and size of the other wrapper instead of the real compiler, which
-means that:
+(see the description of that configuration option), it is not recommended to use
+the form `ccache anotherwrapper compiler args` as the compilation command. It's
+also not recommended to use the masquerading technique for the other compiler
+wrapper. The reason is that by default, ccache will in both cases hash the mtime
+and size of the other wrapper instead of the real compiler, which means that:
 
 * Compiler upgrades will not be detected properly.
 * The cached results will not be shared between compilations with and without
@@ -1491,31 +1711,27 @@ Another minor thing is that if <<config_prefix_command,*prefix_command*>> is
 used, ccache will not invoke the other wrapper when running the preprocessor,
 which increases performance. You can use
 <<config_prefix_command_cpp,*prefix_command_cpp*>> if you also want to invoke
-the other wrapper when doing preprocessing (normally by adding *-E*).
+the other wrapper when doing preprocessing (normally by adding `-E`).
 
 
-Caveats
--------
+== Caveats
 
 * The direct mode fails to pick up new header files in some rare scenarios. See
-  _<<_the_direct_mode,The direct mode>>_ above.
+  _<<The direct mode>>_ above.
 
 
-Troubleshooting
----------------
+== Troubleshooting
 
-General
-~~~~~~~
+=== General
 
 A general tip for getting information about what ccache is doing is to enable
 debug logging by setting the configuration option <<config_debug,*debug*>> (or
-the environment variable *CCACHE_DEBUG*); see _<<_cache_debugging,Cache
-debugging>>_ for more information. Another way of keeping track of what is
+the environment variable *CCACHE_DEBUG*); see _<<Cache debugging>>_
+for more information. Another way of keeping track of what is
 happening is to check the output of *ccache -s*.
 
 
-Performance
-~~~~~~~~~~~
+=== Performance
 
 Ccache has been written to perform well out of the box, but sometimes you may
 have to do some adjustments of how you use the compiler and ccache in order to
@@ -1525,77 +1741,74 @@ Since ccache works best when I/O is fast, put the cache directory on a fast
 storage device if possible. Having lots of free memory so that files in the
 cache directory stay in the disk cache is also preferable.
 
-A good way of monitoring how well ccache works is to run *ccache -s* before and
+A good way of monitoring how well ccache works is to run `ccache -s` before and
 after your build and then compare the statistics counters. Here are some common
 problems and what may be done to increase the hit rate:
 
-* If ``cache hit (preprocessed)'' has been incremented instead of ``cache hit
-  (direct)'', ccache has fallen back to preprocessor mode, which is generally
-  slower. Some possible reasons are:
+* If the counter for preprocessed cache hits has been incremented instead of the
+  one for direct cache hits, ccache has fallen back to preprocessor mode, which
+  is generally slower. Some possible reasons are:
 ** The source code has been modified in such a way that the preprocessor output
    is not affected.
 ** Compiler arguments that are hashed in the direct mode but not in the
-   preprocessor mode have changed (*-I*, *-include*, *-D*, etc) and they didn't
+   preprocessor mode have changed (`-I`, `-include`, `-D`, etc) and they didn't
    affect the preprocessor output.
-** The compiler option *-Xpreprocessor* or *-Wp,_X_* (except *-Wp,-MD,_path_*,
-   *-Wp,-MMD,_path_*, and *-Wp,-D_define_*) is used.
+** The compiler option `-Xpreprocessor` or `-Wp,++*++` (except `-Wp,-MD,<path>`,
+   `-Wp,-MMD,<path>`, and `-Wp,-D<define>`) is used.
 ** This was the first compilation with a new value of the
    <<config_base_dir,base directory>>.
 ** A modification or status change time of one of the include files is too new
    (created the same second as the compilation is being done). See
-   _<<_handling_of_newly_created_header_files,Handling of newly created header
-   files>>_.
-** The `__TIME__` preprocessor macro is (potentially) being used. Ccache turns
-   off direct mode if `__TIME__` is present in the source code. This is done as
-   a safety measure since the string indicates that a `__TIME__` macro _may_
-   affect the output. (To be sure, ccache would have to run the preprocessor,
-   but the sole point of the direct mode is to avoid that.) If you know that
-   `__TIME__` isn't used in practise, or don't care if ccache produces objects
-   where `__TIME__` is expanded to something in the past, you can set
-   <<config_sloppiness,*sloppiness*>> to *time_macros*.
-** The `__DATE__` preprocessor macro is (potentially) being used and the date
-   has changed. This is similar to how `__TIME__` is handled. If `__DATE__` is
-   present in the source code, ccache hashes the current date in order to be
-   able to produce the correct object file if the `__DATE__` macro affects the
-   output. If you know that `__DATE__` isn't used in practise, or don't care if
-   ccache produces objects where `__DATE__` is expanded to something in the
+   _<<Handling of newly created header files>>_.
+** The `+__TIME__+` preprocessor macro is (potentially) being used. Ccache turns
+   off direct mode if `+__TIME__+` is present in the source code. This is done
+   as a safety measure since the string indicates that a `+__TIME__+` macro
+   _may_ affect the output. (To be sure, ccache would have to run the
+   preprocessor, but the sole point of the direct mode is to avoid that.) If you
+   know that `+__TIME__+` isn't used in practise, or don't care if ccache
+   produces objects where `+__TIME__+` is expanded to something in the past, you
+   can set <<config_sloppiness,*sloppiness*>> to *time_macros*.
+** The `+__DATE__+` preprocessor macro is (potentially) being used and the date
+   has changed. This is similar to how `+__TIME__+` is handled. If `+__DATE__+`
+   is present in the source code, ccache hashes the current date in order to be
+   able to produce the correct object file if the `+__DATE__+` macro affects the
+   output. If you know that `+__DATE__+` isn't used in practise, or don't care
+   if ccache produces objects where `+__DATE__+` is expanded to something in the
    past, you can set <<config_sloppiness,*sloppiness*>> to *time_macros*.
-** The `__TIMESTAMP__` preprocessor macro is (potentially) being used and the
+** The `+__TIMESTAMP__+` preprocessor macro is (potentially) being used and the
    source file's modification time has changed. This is similar to how
-   `__TIME__` is handled. If `__TIMESTAMP__` is present in the source code,
+   `+__TIME__+` is handled. If `+__TIMESTAMP__+` is present in the source code,
    ccache hashes the string representation of the source file's modification
    time in order to be able to produce the correct object file if the
-   `__TIMESTAMP__` macro affects the output. If you know that `__TIMESTAMP__`
-   isn't used in practise, or don't care if ccache produces objects where
-   `__TIMESTAMP__` is expanded to something in the past, you can set
-   <<config_sloppiness,*sloppiness*>> to *time_macros*.
+   `+__TIMESTAMP__+` macro affects the output. If you know that
+   `+__TIMESTAMP__+` isn't used in practise, or don't care if ccache produces
+   objects where `+__TIMESTAMP__+` is expanded to something in the past, you can
+   set <<config_sloppiness,*sloppiness*>> to *time_macros*.
 ** The input file path has changed. Ccache includes the input file path in the
    direct mode hash to be able to take relative include files into account and
-   to produce a correct object file if the source code includes a `__FILE__`
+   to produce a correct object file if the source code includes a `+__FILE__+`
    macro.
-* If ``cache miss'' has been incremented even though the same code has been
+* If a cache hit counter was not incremented even though the same code has been
   compiled and cached before, ccache has either detected that something has
   changed anyway or a cleanup has been performed (either explicitly or
-  implicitly when a cache limit has been reached). Some perhaps unobvious
-  things that may result in a cache miss are usage of `__TIME__`, `__DATE__` or
-  `__TIMESTAMP__` macros, or use of automatically generated code that contains
+  implicitly when a cache limit has been reached). Some perhaps unobvious things
+  that may result in a cache miss are usage of `+__TIME__+`, `+__DATE__+` or
+  `+__TIMESTAMP__+` macros, or use of automatically generated code that contains
   a timestamp, build counter or other volatile information.
-* If ``multiple source files'' has been incremented, it's an indication that
-  the compiler has been invoked on several source code files at once. Ccache
-  doesn't support that. Compile the source code files separately if possible.
-* If ``unsupported compiler option'' has been incremented, enable debug logging
+* If "`Multiple source files`" has been incremented, it's an indication that the
+  compiler has been invoked on several source code files at once. Ccache doesn't
+  support that. Compile the source code files separately if possible.
+* If "`Unsupported compiler option`" has been incremented, enable debug logging
   and check which compiler option was rejected.
-* If ``preprocessor error'' has been incremented, one possible reason is that
-  precompiled headers are being used. See _<<_precompiled_headers,Precompiled
-  headers>>_ for how to remedy this.
-* If ``can't use precompiled header'' has been incremented, see
-  _<<_precompiled_headers,Precompiled headers>>_.
-* If ``can't use modules'' has been incremented, see _<<_c_modules,C++
-  modules>>_.
+* If "`Preprocessing failed`" has been incremented, one possible reason is that
+  precompiled headers are being used. See _<<Precompiled headers>>_ for how to
+  remedy this.
+* If "`Could not use precompiled header`" has been incremented, see
+  _<<Precompiled headers>>_.
+* If "`Could not use modules`" has been incremented, see _<<C++ modules>>_.
 
 
-Corrupt object files
-~~~~~~~~~~~~~~~~~~~~
+=== Corrupt object files
 
 It should be noted that ccache is susceptible to general storage problems. If a
 bad object file sneaks into the cache for some reason, it will of course stay
@@ -1606,9 +1819,9 @@ happens, the easiest way of fixing it is this:
 
 1. Build so that the bad object file ends up in the build tree.
 2. Remove the bad object file from the build tree.
-3. Rebuild with *CCACHE_RECACHE* set.
+3. Rebuild with `CCACHE_RECACHE` set.
 
-An alternative is to clear the whole cache with *ccache -C* if you don't mind
+An alternative is to clear the whole cache with `ccache -C` if you don't mind
 losing other cached results.
 
 There are no reported issues about ccache producing broken object files
@@ -1616,15 +1829,13 @@ reproducibly. That doesn't mean it can't happen, so if you find a repeatable
 case, please report it.
 
 
-More information
-----------------
+== More information
 
 Credits, mailing list information, bug reporting instructions, source code,
 etc, can be found on ccache's web site: <https://ccache.dev>.
 
 
-Author
-------
+== Author
 
 Ccache was originally written by Andrew Tridgell and is currently developed and
 maintained by Joel Rosdahl. See AUTHORS.txt or AUTHORS.html and
index 7d69bd49042564c3d896915de15ecbcb35d45631..9d54182d06a7585960f064297ed91619be00de21 100644 (file)
-Ccache news
-===========
+= Ccache news
+
+== Ccache 4.4
+
+Release date: 2021-08-19
+
+
+=== New features
+
+- Made it possible to share a cache over network or on a local filesystem. The
+  configuration option `secondary_storage`/`CCACHE_SECONDARY_STORAGE` specifies
+  one or several storage backends to query after the primary local cache
+  storage. It is also possible to configure sharding (partitioning) of the cache
+  to spread it over a server cluster using
+  https://en.wikipedia.org/wiki/Rendezvous_hashing[Rendezvous hashing]. See the
+  _https://ccache.dev/manual/4.4.html#_secondary_storage_backends[Secondary
+  storage backends]_ chapter in the manual for details. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Added an HTTP backend for secondary storage on any HTTP server that supports
+  GET/PUT/DELETE methods. See https://ccache.dev/howto/http-storage.html[How to
+  set up HTTP storage] for hints on how to set up an HTTP server for use with
+  ccache. +
+  [small]#_[contributed by Gregor Jasny]_#
+
+- Added a Redis backend for secondary storage on any server that supports the
+  Redis protocol. See https://ccache.dev/howto/redis-storage.html[How to set up
+  Redis storage] for hints on how to set up a Redis server for use with
+  ccache. +
+  [small]#_[contributed by Anders F Björklund]_#
+
+- Added a filesystem backend for secondary storage. It can for instance be used
+  for a shared cache over networked filesystems such as NFS, or for mounting a
+  secondary read-only cache layer into a build container. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Added `--trim-dir`, `--trim-max-size` and `--trim-method` options that can be
+  used to trim a secondary storage directory to a certain size, e.g. via
+  cron. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Added a configuration option `reshare`/`CCACHE_RESHARE` which makes ccache
+  send results to secondary storage even for primary storage cache hits. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Added new statistics counters for direct/preprocessed cache misses, primary
+  storage hits/misses, secondary storage hits/misses/errors/timeouts and forced
+  recaches. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Improved statistics summary. The `-s`/`--show-stats` option now prints a more
+  condensed overview where the counters representing "`uncacheable calls`" are
+  summed as uncacheable and errors counters. The summary shows hit rate for
+  direct/preprocessed hits/misses, as well as primary/secondary storage
+  hits/misses. More details are shown with `-v`/`--verbose`. Note: Scripts
+  should use `--print-stats` (available since ccache 3.7) instead of trying to
+  parse the output of `--show-stats`. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Added a "`stats log`" feature (configuration option
+  `stats_log`/`CCACHE_STATSLOG`), which tells ccache to store statistics in a
+  separate log file specified by the user. It can for instance be used to
+  collect statistics for a single build without interference from other
+  concurrent builds. Statistics from the log file can then be viewed with
+  `ccache --show-log-stats`. +
+  [small]#_[contributed by Anders F Björklund]_#
+
+- Added support for clang's `--config` option. +
+  [small]#_[contributed by Tom Stellard]_#
+
+- Added support for one `-Xarch_*` option that matches a corresponding `-arch`
+  option. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Renamed the `--directory` option to `--dir` for consistency with other
+  options. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Made the `--config-path` and `--dir` options affect whole command line so that
+  they don't have to be put before `-s`/`--show-stats`. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Made `--dump-manifest` and `--dump-result` accept filename `-` for reading
+  from standard input. +
+  [small]#_[contributed by Anders F Björklund]_#
+
+- Made the output of `--print-stats` sorted. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Added more internal trace points. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+
+=== Bug fixes
+
+- Fixed a crash if using `base_dir` and `$PWD` is set to a relative path. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Fixed a bug with `-fprofile-generate` where ccache could give false positive
+  cache hits when compiling with relative paths in another directory. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Fixed a bug in `debug_dir`/`CCACHE_DEBUGDIR`. The absolute path to the object
+  file was not created correctly if the object file didn't already exist. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Disabled preprocessor hits for pre-compiled headers with Clang again. +
+  [small]#_[contributed by Arne Hasselbring]_#
+
+- Fixed a problem when using the Gold linker on MIPS by only probing for a
+  faster linker in dev build mode and on x86_64. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Made the `-DENABLE_TRACING=1` mode work again. +
+  [small]#_[contributed by Anders F Björklund]_#
+
+
+=== Changed tooling
+
+- A C++14 compiler or newer is now required to build ccache. For GCC, this means
+  version 6 or newer in practice.
+
+- CMake 3.10 or newer is now required to build ccache.
+
+- https://asciidoctor.org[Asciidoctor] is now required to build ccache
+  documentation.
+
+
+=== Build/test/documentation improvements
+
+- Fixed an issue in the modules test suite that showed up when running the
+  ccache test suite with the clang wrapper provided by Nixpkgs. +
+  [small]#_[contributed by Ryan Burns]_#
+
+- Made the nvcc_ldir test suite require a working NVCC. +
+  [small]#_[contributed by Michael Kruse]_#
+
+- Made the ivfsoverlay test suite more robust. +
+  [small]#_[contributed by Michael Kruse]_#
+
+- Fixed issue with usage of `/FI` when building ccache with MSVC. +
+  [small]#_[contributed by Michael Kruse]_#
+
+- Fixed Apple Clang detection in the integration test suite. +
+  [small]#_[contributed by Gregor Jasny]_#
+
+- Made clang the default compiler when running the test suite on macOS. +
+  [small]#_[contributed by Gregor Jasny]_#
+
+- Silenced stray printout from "-P -c" test case. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Fixed selection of the ccache binary to use when running the test suite with
+  multi-config generators like Xcode. +
+  [small]#_[contributed by Gregor Jasny]_#
+
+- Fixed CMake feature detection for `ctim`/`mtim` fields in `struct stat`. +
+  [small]#_[contributed by Gregor Jasny]_#
+
+- Fixed issue with not linking to .lib correctly on Windows. +
+  [small]#_[contributed by R. Voggenauer]_#
+
+- Made it possible to override `CCACHE_DEV_MODE` on the command line. +
+  [small]#_[contributed by Joel Rosdahl]_#
+
+- Improved HTML documentation style. +
+  [small]#_[contributed by Joel Rosdahl with minor fixes by Orgad Shaneh]_#
+
+
+== Ccache 4.3
 
-Ccache 4.3
-----------
 Release date: 2021-05-09
 
-New features
-~~~~~~~~~~~~
+
+=== New features
 
 - Ccache now ignores the Clang compiler option `-ivfsoverlay` and its argument
-  if you opt in to ``ivfsoverlay sloppiness''. This is useful if you use Xcode,
+  if you opt in to "`ivfsoverlay sloppiness`". This is useful if you use Xcode,
   which uses a virtual file system (VFS) for things like combining Objective-C
   and Swift code.
 
-- When using `-P` in combination with `-E`, ccache now reports this as ``called
-  for preprocessing'' instead of ``unsupported compiler option''.
+- When using `-P` in combination with `-E`, ccache now reports this as "`called
+  for preprocessing`" instead of "`unsupported compiler option`".
 
 - Added support for `-specs file.specs` and `--specs file.specs` without an
   equal sign between the arguments.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
-- "Split dwarf" code paths are now disabled when outputting to `/dev/null`. This
+- "`Split dwarf`" code paths are now disabled when outputting to `/dev/null`. This
   avoids an attempt to delete `/dev/null.dwo`.
 
 - Made the `stat`/`lstat` wrapper function for Windows treat pending deletes as
@@ -32,13 +197,12 @@ Bug fixes
 - Fixed a bug that made ccache process header files redundantly for some
   relative headers when using Clang.
 
-- The object path in now included in the input hash when using `-fprofile-arcs`
+- The object path is now included in the input hash when using `-fprofile-arcs`
   (or `--coverage`) since the object file embeds a `.gcda` path based on the
   object file path.
 
 
-Build improvements
-~~~~~~~~~~~~~~~~~~
+=== Build improvements
 
 - Added an `ENABLE_DOCUMENTATION` build option (default: true) that can be used
   to disable the build of documentation.
@@ -50,12 +214,12 @@ Build improvements
   expands to the empty string.
 
 
-Ccache 4.2.1
-------------
+== Ccache 4.2.1
+
 Release date: 2021-03-27
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - Ccache now only duplicates the stderr file descriptor into `$UNCACHED_ERR_FD`
   for calls to the preprocessor/compiler. This works around a complex bug in
@@ -87,8 +251,7 @@ Bug fixes
 - Fixed handling of long command lines on Windows.
 
 
-Portability and build improvements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Portability and build improvements
 
 - Build configuration scripts now probe for atomic increment as well. This fixes
   a linking error on Sparc.
@@ -98,29 +261,28 @@ Portability and build improvements
 
 - Added support for building ccache with xlclang++ on AIX 7.2.
 
-- Fixed assertion in the "Debug option" test.
+- Fixed assertion in the "`Debug option`" test.
 
 - Made build configuration skip using ccache when building with MSVC.
 
 - Upgraded to doctest 2.4.6. This fixes a build error with glibc >= 2.34.
 
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
-- Fixed markup of `compiler_type` value "other".
+- Fixed markup of `compiler_type` value `other`.
 
 - Fixed markup of `debug_dir` documentation.
 
 - Fixed references to the `extra_files_to_hash` configuration option.
 
 
-Ccache 4.2
-----------
+== Ccache 4.2
+
 Release date: 2021-02-02
 
-New features
-~~~~~~~~~~~~
+
+=== New features
 
 - Improved calculation of relative paths when using `base_dir` to also consider
   canonical paths (i.e. paths with dereferenced symlinks) as candidates.
@@ -137,8 +299,7 @@ New features
   `SOURCE_DATE_EPOCH`.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Fixed a bug where a non-Clang compiler would silently accept the
   Clang-specific `-f(no-)color-diagnostics` option when run via ccache. This
@@ -165,8 +326,7 @@ Bug fixes
 - Fixed retrieval of the object file the destination is `/dev/null`.
 
 
-Portability and build improvements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Portability and build improvements
 
 - Additional compiler flags like `-Wextra -Werror` are now only added when
   building ccache in developer mode.
@@ -207,8 +367,7 @@ Portability and build improvements
 - Took steps towards being able to run the test suite on Windows.
 
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
 - Improved wording of `compiler_check` string values.
 
@@ -225,16 +384,16 @@ Documentation
 - Mention that ccache requires the `-c` compiler option.
 
 
-Ccache 4.1
-----------
+== Ccache 4.1
+
 Release date: 2020-11-22
 
-New features
-~~~~~~~~~~~~
+
+=== New features
 
 - Symlinks are now followed when guessing the compiler. This makes ccache able
-  to guess compiler type “GCC” for a common symlink chain like this:
-  `/usr/bin/cc` → `/etc/alternatives/cc` → `/usr/bin/gcc` → `gcc-9` →
+  to guess compiler type "`GCC`" for a common symlink chain like this:
+  `/usr/bin/cc` -> `/etc/alternatives/cc` -> `/usr/bin/gcc` -> `gcc-9` ->
   `x86_64-linux-gnu-gcc-9`.
 
 - Added a new `compiler_type` (`CCACHE_COMPILERTYPE`) configuration option that
@@ -247,20 +406,19 @@ New features
   `CCACHE_CONFIGPATH` temporarily.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - The original color diagnostics options are now retained when forcing colored
   output. This fixes a bug where feature detection of the `-fcolor-diagnostics`
   option would succeed when run via ccache even though the actual compiler
-  doesnt support it (e.g. GCC <4.9).
+  doesn't support it (e.g. GCC <4.9).
 
 - Fixed a bug related to umask when using the `umask` (`CCACHE_UMASK`)
   configuration option.
 
 - Allow `ccache ccache compiler ...` (repeated `ccache`) again.
 
-- Fixed parsing of dependency file in the “depend mode” so that filenames with
+- Fixed parsing of dependency file in the "`depend mode`" so that filenames with
   space or other special characters are handled correctly.
 
 - Fixed rewriting of the dependency file content when the object filename
@@ -273,16 +431,15 @@ Bug fixes
   found out at runtime that file cloning is unsupported by the OS.
 
 
-Portability and build fixes
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Portability and build fixes
 
 - The ccache binary is now linked with `libatomic` if needed. This fixes build
   problems with GCC on ARM and PowerPC.
 
 - Fixed build of BLAKE3 code with Clang 3.4 and 3.5.
 
-- Fixed “use of undeclared identifier 'CLONE_NOOWNERCOPY'” build error on macOS
-  10.12.
+- Fixed "`use of undeclared identifier 'CLONE_NOOWNERCOPY'`" build error on
+  macOS 10.12.
 
 - Fixed build problems related to missing AVX2 and AVX512 support on older
   macOS versions.
@@ -290,7 +447,7 @@ Portability and build fixes
 - Fixed static linkage with libgcc and libstdc++ for MinGW and made it
   optional.
 
-- Fixed conditional compilation of “robust mutex” code for the inode cache
+- Fixed conditional compilation of "`robust mutex`" code for the inode cache
   routines.
 
 - Fixed badly named man page filename (`Ccache.1` instead of `ccache.1`).
@@ -298,8 +455,7 @@ Portability and build fixes
 - Disabled some tests on ancient Clang versions.
 
 
-Other improvements and fixes
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Other improvements and fixes
 
 - The man page is now built by default if the required tools are available.
 
@@ -307,20 +463,19 @@ Other improvements and fixes
 
 - Improved build errors when building ccache with very old compiler versions.
 
-- Fall back to version “unknown” when Git is not installed.
+- Fall back to version "`unknown`" when Git is not installed.
 
 - Documented the relationship between `CCACHE_DIR` and `-d/--directory`.
 
 - Fixed incorrect reference and bad markup in the manual.
 
 
-Ccache 4.0
-----------
+== Ccache 4.0
+
 Release date: 2020-10-18
 
 
-Summary of major changes
-~~~~~~~~~~~~~~~~~~~~~~~~
+=== Summary of major changes
 
 - Changed the default cache directory location to follow the XDG base directory
   specification.
@@ -337,31 +492,29 @@ Summary of major changes
 
 - Improved cache directory structure.
 
-- Added support for using file cloning (AKA “reflinks”).
+- Added support for using file cloning (AKA "`reflinks`").
 
-- Added an experimental “inode cache” for file hashes.
+- Added an experimental "`inode cache`" for file hashes.
 
 
-Compatibility notes
-~~~~~~~~~~~~~~~~~~~
+=== Compatibility notes
 
 - The default location of the cache directory has changed to follow the XDG
-  base directory specification (<<_detailed_functional_changes,more details
+  base directory specification (<<Detailed functional changes,more details
   below>>). This means that scripts can no longer assume that the cache
   directory is `~/.ccache` by default. The `CCACHE_DIR` environment variable
   still overrides the default location just like before.
 
 - The cache directory structure has changed compared to previous versions
-  (<<_detailed_functional_changes,more details below>>). This means that ccache
+  (<<Detailed functional changes,more details below>>). This means that ccache
   4.0 will not share cache results with earlier versions. It is however safe to
   run ccache 4.0 and earlier versions against the same cache directory: cache
   bookkeeping, statistics and cleanup are backward compatible, with the minor
-  exception that some statistics counters incremented by ccache 4.0 wont be
+  exception that some statistics counters incremented by ccache 4.0 won't be
   visible when running `ccache -s` with an older version.
 
 
-Changed tooling
-~~~~~~~~~~~~~~~
+=== Changed tooling
 
 - CMake is now used instead of Autoconf for configuration and building.
 
@@ -371,10 +524,9 @@ Changed tooling
 - Ccache can now be built using Microsoft Visual C++.
 
 
-Detailed functional changes
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== Detailed functional changes
 
-- All data of a cached result is now stored in a single file called “result”
+- All data of a cached result is now stored in a single file called "`result`"
   instead of up to seven files. This reduces inode usage and improves data
   locality.
 
@@ -410,12 +562,12 @@ Detailed functional changes
   The `cache_dir_levels` (`CCACHE_NLEVELS`) configuration option has therefore
   been removed.
 
-- Added an experimental “inode cache” for file hashes, allowing computed hash
+- Added an experimental "`inode cache`" for file hashes, allowing computed hash
   values to be reused both within and between builds. The inode cache is off by
   default but can be enabled by setting `inode_cache` (`CCACHE_INODECACHE`) to
   `true`.
 
-- Added support for using file cloning (AKA “reflinks”) on Btrfs, XFS and APFS
+- Added support for using file cloning (AKA "`reflinks`") on Btrfs, XFS and APFS
   to copy data to and from the cache very efficiently.
 
 - Two measures have been implemented to make the hard link mode safer:
@@ -465,8 +617,8 @@ Detailed functional changes
 - Added optional logging to syslog if `log_file` (`CCACHE_LOGFILE`) is set to
   `syslog`.
 
-- The compiler option `-fmodules` is now handled in the “depend mode”. If
-  “depend mode” is disabled the option is still considered too hard and ccache
+- The compiler option `-fmodules` is now handled in the "`depend mode`". If
+  "`depend mode`" is disabled the option is still considered too hard and ccache
   will fall back to running the compiler.
 
 - Ccache can now cache compilations with coverage notes (`.gcno` files)
@@ -491,12 +643,12 @@ Detailed functional changes
 - Ccache is now able to share cache entries for different object file names
   when using `-MD` or `-MMD`.
 
-- Clangs `-Xclang` (used by CMake for precompiled headers),
+- Clang's `-Xclang` (used by CMake for precompiled headers),
   `-fno-pch-timestamp`, `-emit-pch`, `-emit-pth` and `-include-pth` options are
   now understood.
 
-- Added support for the HIP (C++ Heterogeneous-Compute Interface for
-  Portability) language.
+- Added support for the HIP ("`C++ Heterogeneous-Compute Interface for
+  Portability`") language.
 
 - The manifest format now allows for header files larger than 4 GiB.
 
@@ -513,8 +665,8 @@ Detailed functional changes
 - Made handling of `.dwo` files and interaction between `-gsplit-dwarf` and
   other `-g*` options more robust.
 
-- The “couldn't find compiler” statistics counter is no longer incremented when
-  ccache exits with a fatal error.
+- The "`couldn't find compiler`" statistics counter is no longer incremented
+  when ccache exits with a fatal error.
 
 - Failure to run a `compiler_check` command is no longer a fatal error.
 
@@ -537,8 +689,7 @@ Detailed functional changes
   older than 3.1 (released 2010).
 
 
-Other improvements
-~~~~~~~~~~~~~~~~~~
+=== Other improvements
 
 - Improved help text and documentation of command line options.
 
@@ -549,7 +700,7 @@ Other improvements
 - Added HTML anchors to configuration options in the manual so that it is
   possible link to a specific option.
 
-- Tweaked placement of “(readonly)” in output of `ccache -s`.
+- Tweaked placement of "`(readonly)`" in output of `ccache -s`.
 
 - Improved visibility of color output from the test suite.
 
@@ -563,13 +714,12 @@ Other improvements
 - Disabled read-only tests on file systems that lack such support.
 
 
-ccache 3.7.12
--------------
+== Ccache 3.7.12
 
 Release date: 2020-10-01
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - Coverage files (`.gcno`) produced by GCC 9+ when using `-fprofile-dir=dir`
   are now handled gracefully by falling back to running the compiler.
@@ -578,8 +728,7 @@ Bug fixes
   32-bit mode.
 
 
-Other
-~~~~~
+=== Other
 
 - Improved documentation about sharing a cache on NFS.
 
@@ -588,35 +737,33 @@ Other
 - Fixed test case failures with GCC 4.4.
 
 
-ccache 3.7.11
--------------
+== Ccache 3.7.11
 
 Release date: 2020-07-21
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - Added knowledge about `-fprofile-{correction,reorder-functions,values}`.
 
 - ccache now handles the Intel compiler option `-xCODE` (where `CODE` is a
   processor feature code) correctly.
 
-- Added support for NVCCs `-Werror` and `--Werror` options.
+- Added support for NVCC's `-Werror` and `--Werror` options.
 
 
-Other
-~~~~~
+=== Other
 
-- ccache’s “Directory is not hashed if using -gz[=zlib]” tests are now skipped
+- ccache's "`Directory is not hashed if using -gz[=zlib]`" tests are now skipped
   for GCC 6.
 
 
-ccache 3.7.10
-------------
+== Ccache 3.7.10
+
 Release date: 2020-06-22
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - Improved handling of profiling options. ccache should now work correctly for
   profiling options like `-fprofile-{generate,use}[=path]` for GCC ≥ 9 and
@@ -626,11 +773,11 @@ Bug fixes
 
 - ccache now copies files directly from the cache to the destination file
   instead of via a temporary file. This avoids problems when using filenames
-  long enough to be near the file systems filename max limit.
+  long enough to be near the file system's filename max limit.
 
 - When the hard-link mode is enabled, ccache now only uses hard links for
   object files, not other files like dependency files. This is because
-  compilers unlink object files before writing to them but they dont do that
+  compilers unlink object files before writing to them but they don't do that
   for dependency files, so the latter can become overwritten and therefore
   corrupted in the cache.
 
@@ -641,38 +788,37 @@ Bug fixes
 - Temporary files are now deleted immediately on signals like SIGTERM and
   SIGINT instead of some time later in a cleanup phase.
 
-- Fixed a bug that affected ccaches `-o/--set-config` option for the
+- Fixed a bug that affected ccache's `-o/--set-config` option for the
   `base_dir` and `cache_dir_levels` keys.
 
 
-ccache 3.7.9
-------------
+== Ccache 3.7.9
+
 Release date: 2020-03-29
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - Fixed replacing of /dev/null when building as root with hard link mode
   enabled and using `-o /dev/null`.
 
-- Removed incorrect assertion resulting in ccache: error: Internal error in
-  format when using `-fdebug-prefix-map=X=` with X equal to `$PWD`.
+- Removed incorrect assertion resulting in "`ccache: error: Internal error in
+  format`" when using `-fdebug-prefix-map=X=` with X equal to `$PWD`.
 
 
-Other
-~~~~~
+=== Other
 
 - Improved CUDA/NVCC support: Recognize `-dc` and `-x cu` options.
 
 - Improved name of temporary file used in NFS-safe unlink.
 
 
-ccache 3.7.8
-------------
+== Ccache 3.7.8
+
 Release date: 2020-03-16
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - Use `$PWD` instead of the real CWD (current working directory) when checking
   for CWD in preprocessed output. This fixes a problem when `$PWD` includes a
@@ -684,8 +830,7 @@ Bug fixes
 - If `localtime_r` fails the epoch time is now logged instead of garbage.
 
 
-Other
-~~~~~
+=== Other
 
 - Improved error message when a boolean environment variable has an invalid
   value.
@@ -693,28 +838,28 @@ Other
 - Improved the regression fix in ccache 3.7.5 related to not passing
   compilation-only options to the preprocessor.
 
-- ccaches PCH test suite now skips running the tests if it detects broken PCH
+- ccache's PCH test suite now skips running the tests if it detects broken PCH
   compiler support.
 
 - Fixed unit test failure on Windows.
 
-- Fixed “stringop-truncation” build warning on Windows.
+- Fixed "`stringop-truncation`" build warning on Windows.
 
-- Improved “x_rename” implementation on Windows.
+- Improved "`x_rename`" implementation on Windows.
 
 - Improved removal of temporary file when rewriting absolute paths to relative
   in the dependency file.
 
-- Clarified “include_file_ctime sloppiness” in the Performance section in the
+- Clarified "`include_file_ctime sloppiness`" in the Performance section in the
   manual.
 
 
-ccache 3.7.7
-------------
+== Ccache 3.7.7
+
 Release date: 2020-01-05
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - Fixed a bug related to object file location in the dependency file (if using
   `-MD` or `-MMD` but not `-MF` and the build directory is not the same as the
@@ -724,32 +869,32 @@ Bug fixes
   compilers again. (A better fix for this is planned for ccache 4.0.)
 
 - Removed the unify mode since it has bugs and shortcomings that are non-trivial
-  or impossible to fix: it doesn’t work with the direct mode, it doesn’t handle
+  or impossible to fix: it doesn't work with the direct mode, it doesn't handle
   C++ raw strings correctly, it can give false cache hits for `.incbin`
-  directives, its turned off when using `-g` and it can make line numbers in
+  directives, it's turned off when using `-g` and it can make line numbers in
   warning messages and `__LINE__` macros incorrect.
 
 - mtime and ctime values are now stored in the manifest files only when
   sloppy_file_stat is set. This avoids adding superfluous manifest file entries
   on direct mode cache misses.
 
-- A “Result:” line is now always printed to the log.
+- A "`Result:`" line is now always printed to the log.
 
-- The “cache miss” statistics counter will now be updated for read-only cache
+- The "`cache miss`" statistics counter will now be updated for read-only cache
   misses, making it consistent with the cache hit case.
 
 
-ccache 3.7.6
-------------
+== Ccache 3.7.6
+
 Release date: 2019-11-17
 
-Bug fixes
-~~~~~~~~~
 
-- The opt-in “file_macro sloppiness” mode has been removed so that the input
+=== Bug fixes
+
+- The opt-in "`file_macro sloppiness`" mode has been removed so that the input
   file path now is always included in the direct mode hash. This fixes a bug
-  that could result in false cache hits in an edge case when file_macro
-  sloppiness is enabled and several identical source files include a relative
+  that could result in false cache hits in an edge case when "`file_macro
+  sloppiness`" is enabled and several identical source files include a relative
   header file with the same name but in different directories.
 
 - Statistics files are no longer lost when the filesystem of the cache is full.
@@ -760,26 +905,25 @@ Bug fixes
 - Properly handle color diagnostics in the depend mode as well.
 
 
-ccache 3.7.5
-------------
+== Ccache 3.7.5
+
 Release date: 2019-10-22
 
-New features
-~~~~~~~~~~~~
+
+=== New features
 
 - Added support for `-MF=arg` (with an extra equal sign) as understood by
   EDG-based compilers.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Fixed a regression in 3.7.2 that could result in a warning message instead of
-  an error in an edge case related to usage of “-Werror”.
+  an error in an edge case related to usage of "`-Werror`".
 
 - An implicit `-MQ` is now passed to the preprocessor only if the object file
   extension is non-standard. This will make it easier to use EDG-based
-  compilers (e.g. GHS) which dont understand `-MQ`. (This is a bug fix of the
+  compilers (e.g. GHS) which don't understand `-MQ`. (This is a bug fix of the
   corresponding improvement implemented in ccache 3.4.)
 
 - ccache now falls back to running the real compiler instead of failing fataly
@@ -792,45 +936,45 @@ Bug fixes
 - Fixed warning during configure in out-of-tree build in developer mode.
 
 
-ccache 3.7.4
-------------
+== Ccache 3.7.4
+
 Release date: 2019-09-12
 
-Improvements
-~~~~~~~~~~~~
+
+=== Improvements
 
 - Added support for the `-gz[=type]` compiler option (previously ccache would
-  think that “-gz” alone would enable debug information, thus potentially
+  think that "`-gz`" alone would enable debug information, thus potentially
   including the current directory in the hash).
 
-- Added support for converting paths like “/c/users/...” into relative paths on
-  Windows.
+- Added support for converting paths like "`/c/users/...`" into relative paths
+  on Windows.
+
 
+== Ccache 3.7.3
 
-ccache 3.7.3
-------------
 Release date: 2019-08-17
 
-Bug fixes
-~~~~~~~~~
 
-- The cache size (which is counted in “used disk blocks”) is now correct on
+=== Bug fixes
+
+- The cache size (which is counted in "`used disk blocks`") is now correct on
   filesystems that use more or less disk blocks than conventional filesystems,
   e.g. ecryptfs or btrfs/zfs with transparent compression. This also fixes a
-  related problem with ccache’s own test suite when run on such file systems.
+  related problem with ccache's own test suite when run on such file systems.
+
+- Fixed a regression in 3.7.2 when using the compiler option "`-Werror`" and
+  then "`-Wno-error`" later on the command line.
 
-- Fixed a regression in 3.7.2 when using the compiler option “-Werror” and then
-  “-Wno-error” later on the command line.
 
+== Ccache 3.7.2
 
-ccache 3.7.2
-------------
 Release date: 2019-07-19
 
-Bug fixes
-~~~~~~~~~
 
-- The compiler option `-gdwarf*` no longer forces “run_second_cpp = true”.
+=== Bug fixes
+
+- The compiler option `-gdwarf*` no longer forces "`run_second_cpp = true`".
 
 - Added verification that the value passed to the `-o/--set-config` option is
   valid.
@@ -847,11 +991,10 @@ Bug fixes
 
 - Unknown manifest versions are now handled gracefully in `--dump-manifest`.
 
-- Fixed `make check` with “funny” locales.
+- Fixed `make check` with "`funny`" locales.
 
 
-Documentation
-~~~~~~~~~~~~~
+=== Documentation
 
 - Added a hint about not running `autogen.sh` when building from a release
   archive.
@@ -859,28 +1002,28 @@ Documentation
 - Mention that `xsltproc` is needed when building from the source repository.
 
 
-ccache 3.7.1
-------------
+== Ccache 3.7.1
+
 Release date: 2019-05-01
 
-Changes
-~~~~~~~
+
+=== Changes
 
 - Fixed a problem when using the compiler option `-MF /dev/null`.
 
 - Long commandlines are now handled gracefully on Windows by using the `@file`
   syntax to avoid hitting the commandline size limit.
 
-- Fixed complaint from GCC 9s `-Werror=format-overflow` when compiling ccache
+- Fixed complaint from GCC 9's `-Werror=format-overflow` when compiling ccache
   itself.
 
 
-ccache 3.7
-----------
+== Ccache 3.7
+
 Release date: 2019-04-23
 
-Changes
-~~~~~~~
+
+=== Changes
 
 - Fixed crash when the debug mode is enabled and the output file is in a
   non-writable directory, e.g. when the output file is `/dev/null`.
@@ -924,12 +1067,12 @@ Changes
   machine-parsable (tab-separated) format.
 
 - ccache no longer creates a missing output directory, thus mimicking the
-  compiler behavior for `-o out/obj.o` when “out” doesn’t exist.
+  compiler behavior for `-o out/obj.o` when `out` doesn't exist.
 
-- `-fdebug-prefix-map=ARG`, `-ffile-prefix-map=ARG` and
-  `-fmacro-prefix-map=ARG` are now included in the hash, but only the part
-  before “ARG”. This fixes a bug where compiler feature detection of said flags
-  would not work correctly with ccache.
+- `-fdebug-prefix-map=ARG`, `-ffile-prefix-map=ARG` and `-fmacro-prefix-map=ARG`
+  are now included in the hash, but only the part before "`ARG`". This fixes a
+  bug where compiler feature detection of said flags would not work correctly
+  with ccache.
 
 - Bail out on too hard compiler option `-gtoggle`.
 
@@ -952,19 +1095,19 @@ Changes
   involved.
 
 
-ccache 3.6
-----------
+== Ccache 3.6
+
 Release date: 2019-01-14
 
-Changes
-~~~~~~~
 
-- ccache now has an opt-in “depend mode”. When enabled, ccache never executes
+=== Changes
+
+- ccache now has an opt-in "`depend mode`". When enabled, ccache never executes
   the preprocessor, which results in much lower cache miss overhead at the
   expense of a lower potential cache hit rate. The depend mode is only possible
   to use when the compiler option `-MD` or `-MMD` is used.
 
-- Added support for GCCs `-ffile-prefix-map` option. The `-fmacro-prefix-map`
+- Added support for GCC's `-ffile-prefix-map` option. The `-fmacro-prefix-map`
   option is now also skipped from the hash.
 
 - Added support for multiple `-fsanitize-blacklist` arguments.
@@ -976,7 +1119,7 @@ Changes
 - Fixed a problem due to Clang overwriting the output file when compiling an
   assembler file.
 
-- Clarified the manual to explain the reasoning behind the “file_macro”
+- Clarified the manual to explain the reasoning behind the "`file_macro`"
   sloppiness setting in a better way.
 
 - ccache now handles several levels of nonexistent directories when rewriting
@@ -985,27 +1128,27 @@ Changes
 - A new sloppiness setting `clang_index_store` makes ccache skip the Clang
   compiler option `-index-store-path` and its argument when computing the
   manifest hash. This is useful if you use Xcode, which uses an index store
-  path derived from the local project path. Note that the index store wont be
+  path derived from the local project path. Note that the index store won't be
   updated correctly on cache hits if you enable this option.
 
 - Rename sloppiness `no_system_headers` to `system_headers` for consistency
   with other options. `no_system_headers` can still be used as an
   (undocumented) alias.
 
-- The GCC variables “DEPENDENCIES_OUTPUT” and “SUNPRO_DEPENDENCIES” are now
+- The GCC variables "`DEPENDENCIES_OUTPUT`" and "`SUNPRO_DEPENDENCIES`" are now
   supported correctly.
 
 - The algorithm that scans for `__DATE_` and `__TIME__` tokens in the hashed
-  source code now doesnt produce false positives for tokens where `__DATE__`
+  source code now doesn't produce false positives for tokens where `__DATE__`
   or `__TIME__` is a substring.
 
 
-ccache 3.5.1
-------------
+== Ccache 3.5.1
+
 Release date: 2019-01-02
 
-Changes
-~~~~~~~
+
+=== Changes
 
 - Added missing getopt_long.c source file to release archive.
 
@@ -1016,17 +1159,17 @@ Changes
 - Improved development mode build flags.
 
 
-ccache 3.5
-----------
+== Ccache 3.5
+
 Release date: 2018-10-15
 
-Changes
-~~~~~~~
+
+=== Changes
 
 - Added a boolean `debug` (`CCACHE_DEBUG`) configuration option. When enabled,
-  ccache will create per-object debug files that are helpful e.g. when
-  debugging unexpected cache misses. See also the new “Cache debugging” section
-  in the manual.
+  ccache will create per-object debug files that are helpful e.g. when debugging
+  unexpected cache misses. See also the new "`Cache debugging`" section in the
+  manual.
 
 - Renamed `CCACHE_CC` to `CCACHE_COMPILER` (keeping the former as a deprecated
   alias).
@@ -1042,36 +1185,36 @@ Changes
 - Improved performance substantially when using `hash_dir = false` on platforms
   like macOS where `getcwd()` is slow.
 
-- Added “stats updated” timestamp in `ccache -s` output. This can be useful if
+- Added "`stats updated`" timestamp in `ccache -s` output. This can be useful if
   you wonder whether ccache actually was used for your last build.
 
-- Renamed “stats zero time” to “stats zeroed” and documented it. The counter is
-  also now only present in `ccache -s` output when `ccache -z` actually has
+- Renamed "`stats zero time`" to "`stats zeroed`" and documented it. The counter
+  is also now only present in `ccache -s` output when `ccache -z` actually has
   been called.
 
 - The content of the `-fsanitize-blacklist` file is now included in the hash,
   so updates to the file will now correctly result in separate cache entries.
 
-- Its now possible to opt out of building and installing man pages when
+- It's now possible to opt out of building and installing man pages when
   running `make install` in the source repository.
 
-- If the compiler type cant be detected (e.g. if it is named `cc`), use safer
-  defaults that wont trip up Clang.
+- If the compiler type can't be detected (e.g. if it is named `cc`), use safer
+  defaults that won't trip up Clang.
 
 - Made the ccache test suite work on FreeBSD.
 
 - Added `file_stat_matches_ctime` option to disable ctime check if
   `file_stat_matches` is enabled.
 
-- Made “./configure --without-bundled-zlib” do what’s intended.
+- Made "`./configure --without-bundled-zlib`" do what's intended.
+
 
+== Ccache 3.4.3
 
-ccache 3.4.3
------------
 Release date: 2018-09-02
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - Fixed a race condition when creating the initial config file in the cache
   directory.
@@ -1085,12 +1228,12 @@ Bug fixes
 - Upgraded bundled zlib to version 1.2.11.
 
 
-ccache 3.4.2
-------------
+== Ccache 3.4.2
+
 Release date: 2018-03-25
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - The cleanup algorithm has been fixed to not misbehave when files are removed
   by another process while the cleanup process is running. Previously, too many
@@ -1098,42 +1241,42 @@ Bug fixes
   triggered at the same time, in extreme cases trimming the cache to a much
   smaller size than the configured limits.
 
-- Correctly hash preprocessed headers located in a “.gch directory”.
+- Correctly hash preprocessed headers located in a "`.gch directory`".
   Previously, ccache would not pick up changes to such precompiled headers,
   risking false positive cache hits.
 
 - Fixed build failure when using the bundled zlib sources.
 
 - ccache 3.3.5 added a workaround for not triggering Clang errors when a
-  precompiled headers dependency has an updated timestamp (but identical
+  precompiled header's dependency has an updated timestamp (but identical
   content). That workaround is now only applied when the compiler is Clang.
 
 - Made it possible to perform out-of-source builds in dev mode again.
 
 
-ccache 3.4.1
-------------
+== Ccache 3.4.1
+
 Release date: 2018-02-11
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - Fixed printing of version number in `ccache --version`.
 
 
-ccache 3.4
-----------
+== Ccache 3.4
+
 Release date: 2018-02-11
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+=== New features and enhancements
 
 - The compiler option form `--sysroot arg` is now handled like the documented
   `--sysroot=arg` form.
 
 - Added support for caching `.su` files generated by GCC flag `-fstack-usage`.
 
-- ccache should now work with distcc’s “pump” wrapper.
+- ccache should now work with distcc's "`pump`" wrapper.
 
 - The optional unifier is no longer disabled when the direct mode is enabled.
 
@@ -1142,7 +1285,7 @@ New features and enhancements
 
 - Boolean environment variable settings no longer accept the following
   (case-insensitive) values: `0`, `false`, `disable` and `no`. All other values
-  are accepted and taken to mean “true”. This is to stop users from setting
+  are accepted and taken to mean "`true`". This is to stop users from setting
   e.g. `CCACHE_DISABLE=0` and then expect the cache to be used.
 
 - Improved support for `run_second_cpp = false`: If combined with passing
@@ -1151,7 +1294,7 @@ New features and enhancements
 
 - An implicit `-MQ` is now passed to the preprocessor only if the object file
   extension is non-standard. This should make it easier to use EDG-based
-  compilers (e.g. GHS) which dont understand `-MQ`.
+  compilers (e.g. GHS) which don't understand `-MQ`.
 
 - ccache now treats an unreadable configuration file just like a missing
   configuration file.
@@ -1161,8 +1304,7 @@ New features and enhancements
 - Documented caveats related to colored warnings from compilers.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - File size and number counters are now updated correctly when files are
   overwritten in the cache, e.g. when using `CCACHE_RECACHE`.
@@ -1172,39 +1314,35 @@ Bug fixes
 - Fixed how the NVCC options `-optf` and `-odir` are handled.
 
 
-ccache 3.3.6
-------------
+== Ccache 3.3.6
+
 Release date: 2018-01-28
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
 - Improved instructions on how to get cache hits between different working
   directories.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Fixed regression in ccache 3.3.5 related to the `UNCACHED_ERR_FD` feature.
 
 
-ccache 3.3.5
-------------
+== Ccache 3.3.5
+
 Release date: 2018-01-13
 
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
 - Documented how automatic cache cleanup works.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Fixed a regression where the original order of debug options could be lost.
-  This reverts the “Improved parsing of `-g*` options” feature in ccache 3.3.
+  This reverts the "`Improved parsing of `-g*` options`" feature in ccache 3.3.
 
 - Multiple `-fdebug-prefix-map` options should now be handled correctly.
 
@@ -1222,9 +1360,9 @@ Bug fixes
 
 - `ccache -c/--cleanup` now works like documented: it just recalculates size
   counters and trims the cache to not exceed the max size and file number
-  limits. Previously, the forced cleanup took “limit_multiple” into account, so
-  that `ccache -c/--cleanup` by default would trim the cache to 80% of the max
-  limit.
+  limits. Previously, the forced cleanup took "`limit_multiple`" into account,
+  so that `ccache -c/--cleanup` by default would trim the cache to 80% of the
+  max limit.
 
 - ccache no longer ignores linker arguments for Clang since Clang warns about
   them.
@@ -1236,46 +1374,44 @@ Bug fixes
   `-remap` or `-trigraphs` option in preprocessor mode.
 
 
-ccache 3.3.4
-------------
+== Ccache 3.3.4
+
 Release date: 2017-02-17
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
 - Documented the different cache statistics counters.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Fixed a regression in ccache 3.3 related to potentially bad content of
   dependency files when compiling identical source code but with different
   source paths. This was only partially fixed in 3.3.2 and reverts the new
-  “Names of included files are no longer included in the hash of the compiler’s
-  preprocessed output feature in 3.3.
+  "`Names of included files are no longer included in the hash of the compiler's
+  preprocessed output`" feature in 3.3.
 
 - Corrected statistics counter for `-optf`/`--options-file` failure.
 
 - Fixed undefined behavior warnings in ccache found by `-fsanitize=undefined`.
 
-ccache 3.3.3
-------------
+== Ccache 3.3.3
+
 Release date: 2016-10-26
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - ccache now detects usage of `.incbin` assembler directives in the source code
   and avoids caching such compilations.
 
 
-ccache 3.3.2
-------------
+== Ccache 3.3.2
+
 Release date: 2016-09-28
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - Fixed a regression in ccache 3.3 related to potentially bad content of
   dependency files when compiling identical source code but with different
@@ -1286,14 +1422,14 @@ Bug fixes
   resulting in missing dependency files from direct mode cache hits.
 
 
-ccache 3.3.1
-------------
+== Ccache 3.3.1
+
 Release date: 2016-09-07
 
-Bug fixes
-~~~~~~~~~
 
-- Fixed a problem in the “multiple `-arch` options” support introduced in 3.3.
+=== Bug fixes
+
+- Fixed a problem in the "`multiple `-arch` options`" support introduced in 3.3.
   When using the direct mode (the default), different combinations of `-arch`
   options were not detected properly.
 
@@ -1302,22 +1438,20 @@ Bug fixes
   (`CCACHE_CPP2`) is enabled.
 
 
-ccache 3.3
-----------
+== Ccache 3.3
+
 Release date: 2016-08-27
 
-Notes
-~~~~~
+=== Notes
 
 - A C99-compatible compiler is now required to build ccache.
 
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
 - The configuration option `run_second_cpp` (`CCACHE_CPP2`) now defaults to
-  true. This improves ccaches out-of-the-box experience for compilers that
-  cant compile their own preprocessed output with the same outcome as if they
+  true. This improves ccache's out-of-the-box experience for compilers that
+  can't compile their own preprocessed output with the same outcome as if they
   compiled the real source code directly, e.g. newer versions of GCC and Clang.
 
 - The configuration option `hash_dir` (`CCACHE_HASHDIR`) now defaults to true.
@@ -1341,22 +1475,22 @@ New features and enhancements
 
 - Added a new statistics counter that tracks the number of performed cleanups
   due to the cache size being over the limit. The value is shown in the output
-  of “ccache -s”.
+  of "`ccache -s`".
 
 - Added support for relocating debug info directory using `-fdebug-prefix-map`.
   This allows for cache hits even when `hash_dir` is used in combination with
   `base_dir`.
 
-- Added a new “cache hit rate” field to the output of “ccache -s”.
+- Added a new "`cache hit rate`" field to the output of "`ccache -s`".
 
-- Added support for caching compilation of assembler code produced by e.g. gcc
-  -S file.c.
+- Added support for caching compilation of assembler code produced by e.g. "`gcc
+  -S file.c`".
 
 - Added support for cuda including the -optf/--options-file option.
 
 - Added support for Fortran 77.
 
-- Added support for multiple `-arch` options to produce “fat binaries”.
+- Added support for multiple `-arch` options to produce "`fat binaries`".
 
 - Multiple identical `-arch` arguments are now handled without bailing.
 
@@ -1378,7 +1512,7 @@ New features and enhancements
 - ccache now understands the undocumented `-coverage` (only one dash) GCC
   option.
 
-- Names of included files are no longer included in the hash of the compilers
+- Names of included files are no longer included in the hash of the compiler's
   preprocessed output. This leads to more potential cache hits when not using
   the direct mode.
 
@@ -1386,8 +1520,7 @@ New features and enhancements
   slightly.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Bail out on too hard compiler option `-P`.
 
@@ -1396,24 +1529,24 @@ Bug fixes
 - Fixed build and test for MinGW32 and Windows.
 
 
-ccache 3.2.9
-------------
+== Ccache 3.2.9
+
 Release date: 2016-09-28
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - Fixed a regression in ccache 3.2.8: ccache could get confused when using the
   compiler option `-Wp,` to pass multiple options to the preprocessor,
   resulting in missing dependency files from direct mode cache hits.
 
 
-ccache 3.2.8
-------------
+== Ccache 3.2.8
+
 Release date: 2016-09-07
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - Fixed an issue when compiler option `-Wp,-MT,path` is used instead of `-MT
   path` (and similar for `-MF`, `-MP` and `-MQ`) and `run_second_cpp`
@@ -1423,86 +1556,83 @@ Bug fixes
   option.
 
 
-ccache 3.2.7
-------------
+== Ccache 3.2.7
+
 Release date: 2016-07-20
 
-Bug fixes
-~~~~~~~~~
+
+=== Bug fixes
 
 - Fixed a bug which could lead to false cache hits for compiler command lines
   with a missing argument to an option that takes an argument.
 
-- ccache now knows how to work around a glitch in the output of GCC 6s
+- ccache now knows how to work around a glitch in the output of GCC 6's
   preprocessor.
 
 
-ccache 3.2.6
-------------
+== Ccache 3.2.6
+
 Release date: 2016-07-12
 
-Bug fixes
-~~~~~~~~~
 
-- Fixed build problem on QNX, which lacks “SA_RESTART”.
+=== Bug fixes
+
+- Fixed build problem on QNX, which lacks "`SA_RESTART`".
 
 - Bail out on compiler option `-fstack-usage` since it creates a `.su` file
-  which ccache currently doesnt handle.
+  which ccache currently doesn't handle.
 
 - Fixed a bug where (due to ccache rewriting paths) the compiler could choose
   incorrect include files if `CCACHE_BASEDIR` is used and the source file path
   is absolute and is a symlink.
 
 
-ccache 3.2.5
-------------
+== Ccache 3.2.5
+
 Release date: 2016-04-17
 
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
 - Only pass Clang-specific `-stdlib=` to the preprocessor.
 
 - Improved handling of stale NFS handles.
 
-- Made it harder to misinterpret documentation of boolean environment settings
+- Made it harder to misinterpret documentation of boolean environment settings'
   semantics.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Include m4 files used by configure.ac in the source dist archives.
 
-- Corrected “Performance” section in the manual regarding `__DATE_`, `__TIME__`
-  and `__FILE__` macros.
+- Corrected "`Performance`" section in the manual regarding `__DATE_`,
+  `__TIME__` and `__FILE__` macros.
 
 - Fixed build on Solaris 10+ and AIX 7.
 
 - Fixed failure to create directories on QNX.
 
-- Don’t (try to) update manifest file in “read-only” and “read-only direct”
+- Don't (try to) update manifest file in "`read-only`" and "`read-only direct`"
   modes.
 
-- Fixed a bug in caching of `stat` system calls in file_stat_matches
-  sloppiness mode.
+- Fixed a bug in caching of `stat` system calls in "`file_stat_matches
+  sloppiness mode`".
 
 - Fixed bug in hashing of Clang plugins, leading to unnecessary cache misses.
 
-- Fixed --print-config to show “pch_defines sloppiness”.
+- Fixed --print-config to show "`pch_defines sloppiness`".
 
-- The man page is now built when running “make install” from Git repository
+- The man page is now built when running "`make install`" from Git repository
   sources.
 
 
-ccache 3.2.4
-------------
+== Ccache 3.2.4
+
 Release date: 2015-10-08
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Fixed build error related to zlib on systems with older make versions
   (regression in ccache 3.2.3).
@@ -1521,19 +1651,17 @@ Bug fixes
   64 GiB on 32-bit systems.
 
 
-ccache 3.2.3
-------------
+== Ccache 3.2.3
+
 Release date: 2015-08-16
 
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
 - Added support for compiler option `-gsplit-dwarf`.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Support external zlib in nonstandard directory.
 
@@ -1543,34 +1671,32 @@ Bug fixes
 
 - Bail out on compiler option `--save-temps` in addition to `-save-temps`.
 
-- Only log “Disabling direct mode” once when failing to read potential include
+- Only log "`Disabling direct mode`" once when failing to read potential include
   files.
 
 
-ccache 3.2.2
-------------
+== Ccache 3.2.2
+
 Release date: 2015-05-10
 
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
 - Added support for `CCACHE_COMPILERCHECK=string:<value>`. This is a faster
-  alternative to `CCACHE_COMPILERCHECK=<command>` if the commands output can
+  alternative to `CCACHE_COMPILERCHECK=<command>` if the command's output can
   be precalculated by the build system.
 
 - Add support for caching code coverage results (compiling for gcov).
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Made hash of cached result created with and without `CCACHE_CPP2` different.
   This makes it possible to rebuild with `CCACHE_CPP2` set without having to
   clear the cache to get new results.
 
-- Don’t try to reset a nonexistent stats file. This avoids “No such file or
-  directory” messages in the ccache log when the cache directory doesn’t exist.
+- Don't try to reset a nonexistent stats file. This avoids "`No such file or
+  directory`" messages in the ccache log when the cache directory doesn't exist.
 
 - Fixed a bug where ccache deleted Clang diagnostics after compiler failures.
 
@@ -1592,13 +1718,12 @@ Bug fixes
 - Fixed build error when compiling ccache with recent Clang versions.
 
 
-ccache 3.2.1
-------------
+== Ccache 3.2.1
+
 Release date: 2014-12-10
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Fixed regression in temporary file handling, which lead to incorrect
   permissions for stats, manifest and ccache.conf files in the cache.
@@ -1616,13 +1741,12 @@ Bug fixes
   options.
 
 
-ccache 3.2
-----------
+== Ccache 3.2
+
 Release date: 2014-11-17
 
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
 - Added support for configuring ccache via one or several configuration files
   instead of via environment variables. Environment variables still have
@@ -1640,7 +1764,7 @@ New features and enhancements
 - Added support for several binaries (separated by space) in `CCACHE_PREFIX`.
 
 - The `-c` option is no longer passed to the preprocessor. This fixes problems
-  with Clang and Solariss C++ compiler.
+  with Clang and Solaris's C++ compiler.
 
 - ccache no longer passes preprocessor options like `-D` and `-I` to the
   compiler when compiling preprocessed output. This fixes warnings emitted by
@@ -1649,7 +1773,7 @@ New features and enhancements
 - Compiler options `-fprofile-generate`, `-fprofile-arcs`, `-fprofile-use` and
   `-fbranch-probabilities` are now handled without bailing.
 
-- Added support for Clangs `--serialize-diagnostic` option, storing the
+- Added support for Clang's `--serialize-diagnostic` option, storing the
   diagnostic file (`.dia`) in the cache.
 
 - Added support for precompiled headers when using Clang.
@@ -1664,19 +1788,19 @@ New features and enhancements
   the other way around. This is needed to support compiler options like
   `-fprofile-arcs` and `--serialize-diagnostics`.
 
-- ccache now checks that included files’ ctimes aren’t too new. This check can
-  be turned off by adding `include_file_ctime` to the “ccache sloppiness”
+- ccache now checks that included files' ctimes aren't too new. This check can
+  be turned off by adding `include_file_ctime` to the "`ccache sloppiness`"
   setting.
 
 - Added possibility to get cache hits based on filename, size, mtime and ctime
   only. On other words, source code files are not even read, only stat-ed. This
-  operation mode is opt-in by adding `file_stat_matches` to the ccache
-  sloppiness setting.
+  operation mode is opt-in by adding `file_stat_matches` to the "`ccache
+  sloppiness`" setting.
 
 - The filename part of options like `-Wp,-MDfilename` is no longer included in
-  the hash since the filename doesnt have any bearing on the result.
+  the hash since the filename doesn't have any bearing on the result.
 
-- Added a “read-only direct” configuration setting, which is like the ordinary
+- Added a "`read-only direct`" configuration setting, which is like the ordinary
   read-only setting except that ccache will only try to retrieve results from
   the cache using the direct mode, not the preprocessor mode.
 
@@ -1691,7 +1815,7 @@ New features and enhancements
 - Added support for `@file` and `-@file` arguments (reading options from a
   file).
 
-- `-Wl,` options are no longer included in the hash since they dont affect
+- `-Wl,` options are no longer included in the hash since they don't affect
   compilation.
 
 - Bail out on too hard compiler option `-Wp,-P`.
@@ -1715,8 +1839,7 @@ New features and enhancements
 - Various other improvements of the test suite.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Any previous `.stderr` is now removed from the cache when recaching.
 
@@ -1727,26 +1850,24 @@ Bug fixes
 - Fixed test suite failures when `CC` is a ccache-wrapped compiler.
 
 
-ccache 3.1.12
--------------
+== Ccache 3.1.12
+
 Release date: 2016-07-12
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Fixed a bug where (due to ccache rewriting paths) the compiler could choose
   incorrect include files if `CCACHE_BASEDIR` is used and the source file path
   is absolute and is a symlink.
 
 
-ccache 3.1.11
--------------
+== Ccache 3.1.11
+
 Release date: 2015-03-07
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Fixed bug which could result in false cache hits when source code contains
   `'"'` followed by `" /*"` or `" //"` (with variations).
@@ -1755,17 +1876,16 @@ Bug fixes
   This makes it possible to rebuild with `CCACHE_CPP2` set without having to
   clear the cache to get new results.
 
-- Don’t try to reset a nonexistent stats file. This avoids “No such file or
-  directory” messages in the ccache log when the cache directory doesn’t exist.
+- Don't try to reset a nonexistent stats file. This avoids "`No such file or
+  directory`" messages in the ccache log when the cache directory doesn't exist.
+
 
+== Ccache 3.1.10
 
-ccache 3.1.10
--------------
 Release date: 2014-10-19
 
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
 - Added support for the `-Xclang` compiler option.
 
@@ -1780,21 +1900,20 @@ New features and enhancements
   `CCACHE_BASEDIR` to reuse results across different directories.)
 
 - Added note in documentation that `--ccache-skip` currently does not mean
-  “don’t hash the following option”.
+  "`don't hash the following option`".
 
 - To enable support for precompiled headers (PCH), `CCACHE_SLOPPINESS` now also
   needs to include the new `pch_defines` sloppiness. This is because ccache
-  cant detect changes in the source code when only defined macros have been
+  can't detect changes in the source code when only defined macros have been
   changed.
 
 - Stale files in the internal temporary directory (`<ccache_dir>/tmp`) are now
   cleaned up if they are older than one hour.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
-- Fixed path canonicalization in `make_relative_path()` when path doesnt
+- Fixed path canonicalization in `make_relative_path()` when path doesn't
   exist.
 
 - Fixed bug in `common_dir_prefix_length()`. This corrects the `CCACHE_BASEDIR`
@@ -1809,13 +1928,12 @@ Bug fixes
 - Fixed problem with logging of current working directory.
 
 
-ccache 3.1.9
-------------
+== Ccache 3.1.9
+
 Release date: 2013-01-06
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - The EAGAIN signal is now handled correctly when emitting cached stderr
   output. This fixes a problem triggered by large error outputs from the
@@ -1823,7 +1941,7 @@ Bug fixes
 
 - Subdirectories in the cache are no longer created in read-only mode.
 
-- Fixed so that ccaches log file descriptor is not made available to the
+- Fixed so that ccache's log file descriptor is not made available to the
   compiler.
 
 - Improved error reporting when failing to create temporary stdout/stderr files
@@ -1832,19 +1950,17 @@ Bug fixes
 - Disappearing temporary stdout/stderr files are now handled gracefully.
 
 
-Other
-~~~~~
+=== Other
 
 - Fixed test suite to work on ecryptfs.
 
 
-ccache 3.1.8
-------------
+== Ccache 3.1.8
+
 Release date: 2012-08-11
 
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
 - Made paths to dependency files relative in order to increase cache hits.
 
@@ -1854,8 +1970,7 @@ New features and enhancements
 - Clang plugins are now hashed to catch plugin upgrades.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Fixed crash when the current working directory has been removed.
 
@@ -1868,28 +1983,26 @@ Bug fixes
   base directory.
 
 
-Other
-~~~~~
+=== Other
 
 - Made git version macro work when compiling outside of the source directory.
 
 - Fixed `static_assert` macro definition clash with GCC 4.7.
 
 
-ccache 3.1.7
-------------
+== Ccache 3.1.7
+
 Release date: 2012-01-08
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Non-writable `CCACHE_DIR` is now handled gracefully when `CCACHE_READONLY` is
   set.
 
 - Made failure to create files (typically due to bad directory permissions) in
   the cache directory fatal. Previously, such failures were silently and
-  erroneously flagged as “compiler produced stdout”.
+  erroneously flagged as "`compiler produced stdout`".
 
 - Both the `-specs=file` and `--specs=file` forms are now recognized.
 
@@ -1908,8 +2021,7 @@ Bug fixes
   versions.)
 
 
-Other
-~~~~~
+=== Other
 
 - Corrected license header for `mdfour.c`.
 
@@ -1917,34 +2029,31 @@ Other
 
 
 
-ccache 3.1.6
-------------
+== Ccache 3.1.6
+
 Release date: 2011-08-21
 
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
 - Rewrite argument to `--sysroot` if `CCACHE_BASEDIR` is used.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
-- Dont crash if `getcwd()` fails.
+- Don't crash if `getcwd()` fails.
 
-- Fixed alignment of “called for preprocessing” counter.
+- Fixed alignment of "`called for preprocessing`" counter.
 
 
-ccache 3.1.5
-------------
+== Ccache 3.1.5
+
 Release date: 2011-05-29
 
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
-- Added a new statistics counter named “called for preprocessing”.
+- Added a new statistics counter named "`called for preprocessing`".
 
 - The original command line is now logged to the file specified with
   `CCACHE_LOGFILE`.
@@ -1957,8 +2066,7 @@ New features and enhancements
 - Improved order of statistics counters in `ccache -s` output.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - The `-MF`/`-MT`/`-MQ` options with concatenated argument are now handled
   correctly when they are last on the command line.
@@ -1968,53 +2076,49 @@ Bug fixes
 
 - Fixed a minor memory leak.
 
-- Systems that lack (and don’t need to be linked with) libm are now supported.
+- Systems that lack (and don't need to be linked with) libm are now supported.
+
 
+== Ccache 3.1.4
 
-ccache 3.1.4
-------------
 Release date: 2011-01-09
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Made a work-around for a bug in `gzputc()` in zlib 1.2.5.
 
-- Corrupt manifest files are now removed so that they wont block direct mode
+- Corrupt manifest files are now removed so that they won't block direct mode
   hits.
 
-- ccache now copes with file systems that dont know about symbolic links.
+- ccache now copes with file systems that don't know about symbolic links.
 
-- The file handle in now correctly closed on write error when trying to create
+- The file handle is now correctly closed on write error when trying to create
   a cache dir tag.
 
 
-ccache 3.1.3
-------------
+== Ccache 3.1.3
+
 Release date: 2010-11-28
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - The -MFarg, -MTarg and -MQarg compiler options (i.e, without space between
   option and argument) are now handled correctly.
 
 
-Other
-~~~~~
+=== Other
 
 - Portability fixes for HP-UX 11.00 and other esoteric systems.
 
 
-ccache 3.1.2
-------------
+== Ccache 3.1.2
+
 Release date: 2010-11-21
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Bail out on too hard compiler options `-fdump-*`.
 
@@ -2024,24 +2128,22 @@ Bug fixes
 - Fixed issue when parsing precompiler output on AIX.
 
 
-Other
-~~~~~
+=== Other
 
 - Improved documentation on which information is included in the hash sum.
 
-- Made the “too new header file” test case work on file systems with
+- Made the "`too new header file`" test case work on file systems with
   unsynchronized clocks.
 
 - The test suite now also works on systems that lack a /dev/zero.
 
 
-ccache 3.1.1
-------------
+== Ccache 3.1.1
+
 Release date: 2010-11-07
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - ccache now falls back to preprocessor mode when a non-regular include file
   (device, socket, etc) has been detected so that potential hanging due to
@@ -2056,27 +2158,25 @@ Bug fixes
 - Fixed configure detection of ar.
 
 - ccache development version (set by dev.mk) now works with gits whose
-  `describe` command doesnt understand `--dirty`.
+  `describe` command doesn't understand `--dirty`.
 
 
-Other
-~~~~~
+=== Other
 
 - Minor debug log message improvements.
 
 
-ccache 3.1
-----------
+== Ccache 3.1
+
 Release date: 2010-09-16
 
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
 - Added support for hashing the output of a custom command (e.g. `%compiler%
   --version`) to identify the compiler instead of stat-ing or hashing the
   compiler binary. This can improve robustness when the compiler (as seen by
-  ccache) actually isnt the real compiler but another compiler wrapper.
+  ccache) actually isn't the real compiler but another compiler wrapper.
 
 - Added support for caching compilations that use precompiled headers. (See the
   manual for important instructions regarding this.)
@@ -2095,9 +2195,9 @@ New features and enhancements
 - Reading and writing of statistics counters has been made forward-compatible
   (unknown counters are retained).
 
-- Files are now read without using `mmap()`. This has two benefits: its more
+- Files are now read without using `mmap()`. This has two benefits: it's more
   robust against file changes during reading and it improves performance on
-  poor systems where `mmap()` doesnt use the disk cache.
+  poor systems where `mmap()` doesn't use the disk cache.
 
 - Added `.cp` and `.CP` as known C++ suffixes.
 
@@ -2107,8 +2207,7 @@ New features and enhancements
   statistics when using the Darwin linker.)
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Non-fatal error messages are now never printed to stderr but logged instead.
 
@@ -2119,8 +2218,7 @@ Bug fixes
 - EINTR is now handled correctly.
 
 
-Other
-~~~~~
+=== Other
 
 - Work on porting ccache to win32 (native), mostly done by Ramiro Polla. The
   port is not yet finished, but will hopefully be complete in some subsequent
@@ -2146,22 +2244,21 @@ Other
 - New `HACKING.txt` file with some notes about ccache code conventions.
 
 
-ccache 3.0.1
-------------
+== Ccache 3.0.1
+
 Release date: 2010-07-15
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
-- The statistics counter “called for link” is now correctly updated when
+- The statistics counter "`called for link`" is now correctly updated when
   linking with a single object file.
 
 - Fixed a problem with out-of-source builds.
 
 
-ccache 3.0
-----------
+== Ccache 3.0
+
 Release date: 2010-06-20
 
 
@@ -2172,19 +2269,17 @@ General
   or later.
 
 
-Upgrade notes
-~~~~~~~~~~~~~
+=== Upgrade notes
 
-- The way the hashes are calculated has changed, so you wont get cache hits
+- The way the hashes are calculated has changed, so you won't get cache hits
   for compilation results stored by older ccache versions. Because of this, you
   might as well clear the old cache directory with `ccache --clear` if you
   want, unless you plan to keep using an older ccache version.
 
 
-New features and enhancements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+=== New features and enhancements
 
-- ccache now has a “direct mode” where it computes a hash of the source code
+- ccache now has a "`direct mode`" where it computes a hash of the source code
   (including all included files) and compiler options without running the
   preprocessor. By not running the preprocessor, CPU usage is reduced; the
   speed is somewhere between 1 and 5 times that of ccache running in
@@ -2202,19 +2297,19 @@ New features and enhancements
 - Object files are now optionally stored compressed in the cache. The runtime
   cost is negligible, and more files will fit in the ccache directory and in
   the disk cache. Set `CCACHE_COMPRESS` to enable object file compression. Note
-  that you cant use compression in combination with the hard link feature.
+  that you can't use compression in combination with the hard link feature.
 
 - A `CCACHE_COMPILERCHECK` option has been added. This option tells ccache what
   compiler-identifying information to hash to ensure that results retrieved
-  from the cache are accurate. Possible values are: none (dont hash anything),
-  mtime (hash the compilers mtime and size) and content (hash the content of
+  from the cache are accurate. Possible values are: none (don't hash anything),
+  mtime (hash the compiler's mtime and size) and content (hash the content of
   the compiler binary). The default is mtime.
 
 - It is now possible to specify extra files whose contents should be included
   in the hash sum by setting the `CCACHE_EXTRAFILES` option.
 
 - Added support for Objective-C and Objective-C\+\+. The statistics counter
-  “not a C/C++ file” has been renamed to “unsupported source language”.
+  "`not a C/C++ file`" has been renamed to "`unsupported source language`".
 
 - Added support for the `-x` compiler option.
 
@@ -2237,13 +2332,13 @@ New features and enhancements
 
 - Temporary files that later will be moved into the cache are now created in
   the cache directory they will end up in. This makes ccache more friendly to
-  Linuxs directory layout.
+  Linux's directory layout.
 
 - Improved the test suite and added tests for most of the new functionality.
-  Its now also possible to specify a subset of tests to run.
+  It's now also possible to specify a subset of tests to run.
 
 - Standard error output from the compiler is now only stored in the cache if
-  its non-empty.
+  it's non-empty.
 
 - If the compiler produces no object file or an empty object file, but gives a
   zero exit status (could be due to a file system problem, a buggy program
@@ -2251,7 +2346,7 @@ New features and enhancements
 
 - Added `installcheck` and `distcheck` make targets.
 
-- Clarified cache size limit options and cleanup semantics.
+- Clarified cache size limit options' and cleanup semantics.
 
 - Improved display of cache max size values.
 
@@ -2260,8 +2355,7 @@ New features and enhancements
   `-iwithprefixbefore`, `-nostdinc`, `-nostdinc++` and `-U`.
 
 
-Bug fixes
-~~~~~~~~~
+=== Bug fixes
 
 - Various portability improvements.
 
@@ -2280,7 +2374,7 @@ Bug fixes
   `-save-temps`. Also bail out on `@file` style options.
 
 - Errors when using multiple `-arch` compiler options are now noted as
-  “unsupported compiler option”.
+  "`unsupported compiler option`".
 
 - `-MD`/`-MMD` options without `-MT`/`-MF` are now handled correctly.
 
diff --git a/doc/ccache-doc.css b/doc/ccache-doc.css
new file mode 100644 (file)
index 0000000..86a0ed9
--- /dev/null
@@ -0,0 +1,50 @@
+@import url(https://fonts.googleapis.com/css?family=Montserrat|Open+Sans);
+@import url(https://cdn.jsdelivr.net/gh/asciidoctor/asciidoctor@2.0/data/stylesheets/asciidoctor-default.css); /* Default asciidoc style framework - important */
+
+:root{
+--maincolor:#ffffff;
+--primarycolor:#294172;
+--secondarycolor:#5f646c;
+--tertiarycolor:#cccccc;
+--highlightcolor:#f5f5f5;
+--sidebarbackground:#f5f5f5;
+--linkcolor:#02a;
+--linkcoloralternate:#db3279;
+--white:#ffffff;
+--black:#000000;
+}
+
+/* Text styles */
+
+body{font-family: "Open Sans", sans-serif;background-color: var(--maincolor);color:var(--black);}
+
+h1{color:var(--primarycolor) !important;font-family:"Montserrat",sans-serif;}
+h2,h3,h4,h5,h6{color:var(--secondarycolor) !important;font-family:"Montserrat",sans-serif;}
+.title{color:var(--black) !important;font-family:"Open Sans",sans-serif;font-style: normal; font-weight: normal;}
+a{text-decoration: none;}
+a:hover{color: #3958da; text-decoration: underline;}
+p{font-family: "Open Sans",sans-serif ! important}
+#toc.toc2 a:link{color:var(--linkcolor);}
+blockquote{color:var(--linkcoloralternate) !important}
+.quoteblock blockquote:before{color:var(--linkcoloralternate)}
+code{color:var(--black);background-color: var(--highlightcolor) !important}
+mark{background-color: var(--highlightcolor)} /* Text highlighting color */
+
+/* Table styles */
+th{background-color: var(--maincolor);color:var(--black) !important;}
+td{background-color: var(--maincolor);color: var(--black) !important}
+
+
+#toc.toc2{background-color:var(--sidebarbackground);}
+#toctitle{color:var(--black);}
+
+/* Responsiveness fixes */
+video {
+  max-width: 100%;
+}
+
+@media all and (max-width: 600px) {
+table {
+  width: 55vw!important;
+  font-size: 3vw;
+}
index c7923984d1ffcd148f4112ad611a0e9517a8b4cc..d1de6859b48ec7972e7874f88b71013c61425d27 100644 (file)
@@ -3,7 +3,7 @@ different build environments.
 
 For instance, run something like this to build ccache in Ubuntu 20.04:
 
-    misc/build-in-docker ubuntu-20-focal
+    misc/build-in-docker ubuntu-20.04
 
 The above command will first build the Ubuntu 20.04 Docker image if needed and
 finally build ccache and run the ccache test suite.
diff --git a/dockerfiles/alpine-3.12/Dockerfile b/dockerfiles/alpine-3.12/Dockerfile
deleted file mode 100644 (file)
index 324921a..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-FROM alpine:3.12
-
-RUN apk add --no-cache \
-        bash \
-        ccache \
-        clang \
-        cmake \
-        elfutils \
-        g++ \
-        gcc \
-        libc-dev \
-        make \
-        perl \
-        zstd-dev
-
-# Redirect all compilers to ccache.
-RUN for t in gcc g++ cc c++ clang clang++; do ln -vs /usr/bin/ccache /usr/local/bin/$t; done
diff --git a/dockerfiles/alpine-3.14/Dockerfile b/dockerfiles/alpine-3.14/Dockerfile
new file mode 100644 (file)
index 0000000..7b79512
--- /dev/null
@@ -0,0 +1,20 @@
+FROM alpine:3.14
+
+RUN apk add --no-cache \
+        bash \
+        ccache \
+        clang \
+        cmake \
+        elfutils \
+        g++ \
+        gcc \
+        hiredis-dev \
+        libc-dev \
+        make \
+        perl \
+        python3 \
+        redis \
+        zstd-dev
+
+# Redirect all compilers to ccache.
+RUN for t in gcc g++ cc c++ clang clang++; do ln -vs /usr/bin/ccache /usr/local/bin/$t; done
diff --git a/dockerfiles/alpine-3.4/Dockerfile b/dockerfiles/alpine-3.4/Dockerfile
deleted file mode 100644 (file)
index ad42a00..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-# Released 2016, this is the first release to contain cmake >= 3.4.3
-
-FROM alpine:3.4
-
-RUN apk add --no-cache \
-        bash \
-        ccache \
-        cmake \
-        g++ \
-        gcc \
-        libc-dev \
-        make \
-        perl
-
-# Redirect all compilers to ccache.
-RUN for t in gcc g++ cc c++ clang clang++; do ln -vs /usr/bin/ccache /usr/local/bin/$t; done
diff --git a/dockerfiles/alpine-3.8/Dockerfile b/dockerfiles/alpine-3.8/Dockerfile
new file mode 100644 (file)
index 0000000..2aab354
--- /dev/null
@@ -0,0 +1,20 @@
+FROM alpine:3.8
+
+RUN apk add --no-cache \
+        bash \
+        ccache \
+        clang \
+        cmake \
+        elfutils \
+        g++ \
+        gcc \
+        hiredis-dev \
+        libc-dev \
+        make \
+        perl \
+        python3 \
+        redis \
+        zstd-dev
+
+# Redirect all compilers to ccache.
+RUN for t in gcc g++ cc c++ clang clang++; do ln -vs /usr/bin/ccache /usr/local/bin/$t; done
index 89a122f0490fa404930c34b10986751847d432ca..d5c4e4897cf05811f89b2f2e04529a884c183f06 100644 (file)
@@ -1,21 +1,24 @@
 FROM centos:7
 
 RUN yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \
+ && yum install -y centos-release-scl \
  && yum install -y \
-        asciidoc \
+        asciidoctor \
         autoconf \
         bash \
         ccache \
         clang \
         cmake3 \
+        devtoolset-8 \
         elfutils \
         gcc \
         gcc-c++ \
         libzstd-devel \
         make \
-# Remove superfluous dependencies brought in by asciidoc:
- && rpm -e --nodeps graphviz \
+        python3 \
  && yum autoremove -y \
  && yum clean all \
  && cp /usr/bin/cmake3 /usr/bin/cmake \
  && cp /usr/bin/ctest3 /usr/bin/ctest
+
+ENTRYPOINT ["scl", "enable", "devtoolset-8", "--"]
index b7ab38fd6e48c1ee114a84ff6f3ae4ef8fbe7b28..e61c576c2e507ecf2c426ba40f1ed848730e2660 100644 (file)
@@ -1,8 +1,10 @@
 FROM centos:8
 
+# also run update due to https://bugs.centos.org/view.php?id=18212
 RUN dnf install -y epel-release \
+ && dnf update -y \
  && dnf install -y \
-        asciidoc \
+        asciidoctor \
         autoconf \
         bash \
         ccache \
@@ -12,6 +14,9 @@ RUN dnf install -y epel-release \
         elfutils \
         gcc \
         gcc-c++ \
+        hiredis-devel \
         libzstd-devel \
         make \
+        python3 \
+        redis \
  && dnf clean all
index 703bdc9307c31bf79d7324ffee04e8e0a391b3ee..54bd44395029718ecbce049f22502697ca675bbd 100644 (file)
@@ -9,8 +9,11 @@ RUN apt-get update \
         cmake \
         elfutils \
         gcc-multilib \
+        libhiredis-dev \
         libzstd-dev \
-        xsltproc \
+        python3 \
+        redis-server \
+        redis-tools \
  && rm -rf /var/lib/apt/lists/*
 
 # Redirect all compilers to ccache.
diff --git a/dockerfiles/debian-11/Dockerfile b/dockerfiles/debian-11/Dockerfile
new file mode 100644 (file)
index 0000000..ce7db7b
--- /dev/null
@@ -0,0 +1,20 @@
+FROM debian:11
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+        bash \
+        build-essential \
+        ccache \
+        clang \
+        cmake \
+        elfutils \
+        gcc-multilib \
+        libhiredis-dev \
+        libzstd-dev \
+        python3 \
+        redis-server \
+        redis-tools \
+ && rm -rf /var/lib/apt/lists/*
+
+# Redirect all compilers to ccache.
+RUN for t in gcc g++ cc c++ clang clang++; do ln -vs /usr/bin/ccache /usr/local/bin/$t; done
diff --git a/dockerfiles/debian-9/Dockerfile b/dockerfiles/debian-9/Dockerfile
deleted file mode 100644 (file)
index b4c6585..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-FROM debian:9
-
-RUN apt-get update \
- && apt-get install -y --no-install-recommends \
-        bash \
-        build-essential \
-        ccache \
-        clang \
-        cmake \
-        elfutils \
-        gcc-multilib \
-        libzstd-dev \
-        xsltproc \
- && rm -rf /var/lib/apt/lists/*
-
-# Redirect all compilers to ccache.
-RUN for t in gcc g++ cc c++ clang clang++; do ln -vs /usr/bin/ccache /usr/local/bin/$t; done
index 65c883d584a0fec721e8f94a8aba2685253f738b..c4431a7dcca284f06afc1154e86f2311c9ee2d59 100644 (file)
@@ -11,6 +11,9 @@ RUN dnf install -y \
         findutils \
         gcc \
         gcc-c++ \
+        hiredis-devel \
         libzstd-devel \
         make \
+        python3 \
+        redis \
  && dnf clean all
diff --git a/dockerfiles/ubuntu-14.04/Dockerfile b/dockerfiles/ubuntu-14.04/Dockerfile
deleted file mode 100644 (file)
index 5b3d1db..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-FROM ubuntu:14.04
-
-RUN apt-get update \
- && apt-get install -y --no-install-recommends \
-        asciidoc \
-        bash \
-        build-essential \
-        ccache \
-        clang-3.4 \
-        curl \
-        docbook-xml \
-        docbook-xsl \
-        elfutils \
-        g++-multilib \
-        ninja-build \
-        wget \
-        xsltproc \
- && rm -rf /var/lib/apt/lists/*
-
-# Redirect all compilers to ccache.
-RUN for t in gcc g++ cc c++ clang clang++; do ln -vs /usr/bin/ccache /usr/local/bin/$t; done
-
-# The distribution's CMake it too old (2.8.12.2).
-RUN curl -sSL https://cmake.org/files/v3.5/cmake-3.5.2-Linux-x86_64.tar.gz | sudo tar -xzC /opt \
- && cp -a /opt/cmake-3.5.2-Linux-x86_64/bin /usr/local \
- && cp -a /opt/cmake-3.5.2-Linux-x86_64/share /usr/local
diff --git a/dockerfiles/ubuntu-16.04/Dockerfile b/dockerfiles/ubuntu-16.04/Dockerfile
deleted file mode 100644 (file)
index 7c9ddbc..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-FROM ubuntu:16.04
-
-RUN apt-get update \
- && apt-get install -y --no-install-recommends \
-        asciidoc \
-        bash \
-        build-essential \
-        ccache \
-        clang \
-        cmake \
-        docbook-xml \
-        docbook-xsl \
-        elfutils \
-        gcc-multilib \
-        libzstd1-dev \
-        xsltproc \
- && rm -rf /var/lib/apt/lists/*
-
-# Redirect all compilers to ccache.
-RUN for t in gcc g++ cc c++ clang clang++; do ln -vs /usr/bin/ccache /usr/local/bin/$t; done
index 4ab985b528c070c1d5360d6f8a211d613d0da580..409f6dc3c2c6caa5aa33c9ee532781001ad92283 100644 (file)
@@ -2,7 +2,7 @@ FROM ubuntu:18.04
 
 RUN apt-get update \
  && apt-get install -y --no-install-recommends \
-        asciidoc \
+        asciidoctor \
         bash \
         build-essential \
         ccache \
@@ -12,8 +12,11 @@ RUN apt-get update \
         docbook-xsl \
         elfutils \
         gcc-multilib \
+        libhiredis-dev \
         libzstd-dev \
-        xsltproc \
+        python3 \
+        redis-server \
+        redis-tools \
  && rm -rf /var/lib/apt/lists/*
 
 # Redirect all compilers to ccache.
index f2be356d057739ce72c750198a230de73ec04b11..46f4135e1ab13bac6e52356858429d01e62b1f39 100644 (file)
@@ -3,7 +3,7 @@ FROM ubuntu:20.04
 # Non-interactive: do not set up timezone settings.
 RUN apt-get update \
  && DEBIAN_FRONTEND="noninteractive" apt-get install -y --no-install-recommends \
-        asciidoc \
+        asciidoctor \
         bash \
         build-essential \
         ccache \
@@ -13,8 +13,11 @@ RUN apt-get update \
         docbook-xsl \
         elfutils \
         gcc-multilib \
+        libhiredis-dev \
         libzstd-dev \
-        xsltproc \
+        python3 \
+        redis-server \
+        redis-tools \
  && rm -rf /var/lib/apt/lists/*
 
 # Redirect all compilers to ccache.
diff --git a/misc/ccache.magic b/misc/ccache.magic
new file mode 100644 (file)
index 0000000..ea5a143
--- /dev/null
@@ -0,0 +1,7 @@
+# ccache manifest
+0       string          cCmF            ccache manifest
+>4      ubyte           x               \b, version %d
+
+# ccache result
+0       string          cCrS            ccache result
+>4      ubyte           x               \b, version %d
index 60f3e5c72fec6d4d4746ed5072e0492fb5e8d20c..952724dfafd0a190f9beacb6bab24b552ebd4aa1 100644 (file)
@@ -1,6 +1,7 @@
 copyable
 creat
 files'
+hda
 pase
 seve
 stoll
index d33ad646f5c87d3d83992530d0aacfee48a80c1e..93cb2f4f2aa807f6c29d7c8abbf165126d0a07e7 100755 (executable)
@@ -263,9 +263,7 @@ def main(argv):
     op.add_option(
         "--no-compression", help="disable compression", action="store_true"
     )
-    op.add_option(
-        "--compression-level", help="set compression level", type=int
-    )
+    op.add_option("--compression-level", help="set compression level", type=int)
     op.add_option(
         "-d",
         "--directory",
index a95c39210a1c4cf13112548ea1b3d01ae22f8d4a..3ab7d855ecb31d8fa1ea41114453d85f20735065 100755 (executable)
@@ -20,17 +20,11 @@ build() {
 
 #     NAME         CC    CXX     TEST_CC CMAKE_PARAMS
 
-build debian-9     gcc   g++     gcc
-build debian-9     clang clang++ clang
-
 build debian-10    gcc   g++     gcc
 build debian-10    clang clang++ clang
 
-build ubuntu-14.04 gcc   g++     gcc     -DZSTD_FROM_INTERNET=ON
-build ubuntu-14.04 gcc   g++     clang   -DZSTD_FROM_INTERNET=ON
-
-build ubuntu-16.04 gcc   g++     gcc
-build ubuntu-16.04 clang clang++ clang
+build debian-11    gcc   g++     gcc
+build debian-11    clang clang++ clang
 
 build ubuntu-18.04 gcc   g++     gcc
 build ubuntu-18.04 clang clang++ clang
@@ -38,8 +32,8 @@ build ubuntu-18.04 clang clang++ clang
 build ubuntu-20.04 gcc   g++     gcc
 build ubuntu-20.04 clang clang++ clang
 
-build centos-7     gcc   g++     gcc     -DWARNINGS_AS_ERRORS=false
-build centos-7     gcc   g++     clang   -DWARNINGS_AS_ERRORS=false
+build centos-7     gcc   g++     gcc     -DWARNINGS_AS_ERRORS=false -DREDIS_STORAGE_BACKEND=OFF
+build centos-7     gcc   g++     clang   -DWARNINGS_AS_ERRORS=false -DREDIS_STORAGE_BACKEND=OFF
 
 build centos-8     gcc   g++     gcc
 build centos-8     clang clang++ clang
@@ -47,8 +41,8 @@ build centos-8     clang clang++ clang
 build fedora-32    gcc   g++     gcc
 build fedora-32    clang clang++ clang
 
-build alpine-3.4   gcc   g++     gcc     -DZSTD_FROM_INTERNET=ON
-build alpine-3.4   gcc   g++     clang   -DZSTD_FROM_INTERNET=ON
+build alpine-3.8   gcc   g++     gcc
+build alpine-3.8   gcc   g++     clang
 
-build alpine-3.12  gcc   g++     gcc
-build alpine-3.12  clang clang++ clang
+build alpine-3.14  gcc   g++     gcc
+build alpine-3.14  clang clang++ clang
diff --git a/misc/upload-redis b/misc/upload-redis
new file mode 100755 (executable)
index 0000000..c91ade3
--- /dev/null
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+
+# This script uploads the contents of the cache from primary storage to a Redis
+# secondary storage.
+
+import redis
+import os
+
+config = os.getenv("REDIS_CONF", "localhost")
+if ":" in config:
+    host, port = config.rsplit(":", 1)
+    sock = None
+elif config.startswith("/"):
+    host, port, sock = None, None, config
+else:
+    host, port, sock = config, 6379, None
+username = os.getenv("REDIS_USERNAME")
+password = os.getenv("REDIS_PASSWORD")
+context = redis.Redis(
+    host=host, port=port, unix_socket_path=sock, password=password
+)
+
+CCACHE_MANIFEST = b"cCmF"
+CCACHE_RESULT = b"cCrS"
+
+ccache = os.getenv("CCACHE_DIR", os.path.expanduser("~/.cache/ccache"))
+filelist = []
+for dirpath, dirnames, filenames in os.walk(ccache):
+    # sort by modification time, most recently used last
+    for filename in filenames:
+        if filename.endswith(".lock"):
+            continue
+        stat = os.stat(os.path.join(dirpath, filename))
+        filelist.append((stat.st_mtime, dirpath, filename))
+filelist.sort()
+files = result = manifest = objects = 0
+for mtime, dirpath, filename in filelist:
+    dirname = dirpath.replace(ccache + os.path.sep, "")
+    if dirname == "tmp":
+        continue
+    elif filename == "CACHEDIR.TAG" or filename == "stats":
+        # ignore these
+        files = files + 1
+    else:
+        (base, ext) = filename[:-1], filename[-1:]
+        if ext == "R" or ext == "M":
+            if ext == "R":
+                result = result + 1
+            if ext == "M":
+                manifest = manifest + 1
+            key = "ccache:" + "".join(list(os.path.split(dirname)) + [base])
+            val = open(os.path.join(dirpath, filename), "rb").read() or None
+            if val:
+                print("%s: %s %d" % (key, ext, len(val)))
+                magic = val[0:4]
+                if ext == "M":
+                    assert magic == CCACHE_MANIFEST
+                if ext == "R":
+                    assert magic == CCACHE_RESULT
+                context.set(key, val)
+                objects = objects + 1
+        files = files + 1
+print(
+    "%d files, %d result (%d manifest) = %d objects"
+    % (files, result, manifest, objects)
+)
index f30529dc0d62a5f0803e554b59d9a2813cdbec88..3781fd60182622f092ae2eeb83ef7a89afaaedcb 100644 (file)
@@ -69,11 +69,11 @@ CheckOptions:
 
   # If you hit a limit, please consider changing the code instead of the limit.
   - key:             readability-function-size.LineThreshold
-    value:           700
+    value:           999999
   - key:             readability-function-size.StatementThreshold
     value:           999999
   - key:             readability-function-size.BranchThreshold
-    value:           170
+    value:           999999
   - key:             readability-function-size.ParameterThreshold
     value:           6
   - key:             readability-function-size.NestingThreshold
index 5270989d7dadf80ce94b01e867db432f0c092143..e918c6a481823c5662f2c31274abde9a83cf99bc 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -20,6 +20,9 @@
 
 #include "Util.hpp"
 
+#include <core/exceptions.hpp>
+#include <util/string.hpp>
+
 using nonstd::nullopt;
 using nonstd::optional;
 using nonstd::string_view;
@@ -52,7 +55,7 @@ Args::from_gcc_atfile(const std::string& filename)
   std::string argtext;
   try {
     argtext = Util::read_file(filename);
-  } catch (Error&) {
+  } catch (core::Error&) {
     return nullopt;
   }
 
@@ -169,8 +172,8 @@ Args::erase_with_prefix(string_view prefix)
 {
   m_args.erase(std::remove_if(m_args.begin(),
                               m_args.end(),
-                              [&prefix](const std::string& s) {
-                                return Util::starts_with(s, prefix);
+                              [&prefix](const auto& s) {
+                                return util::starts_with(s, prefix);
                               }),
                m_args.end());
 }
index 48fb77df5cdd3e2a2857d2a2c9b5164c9749bda6..a93d1881bb0cd12db9d63f83b3cf0b5d86d6b17c 100644 (file)
@@ -18,8 +18,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 #include "NonCopyable.hpp"
 #include "Util.hpp"
 
index 4d9eaf3942fff7bbc65027d9d9f62de47311b9d3..6e9ea823b1f62bda816aa11747476feb2a5ffb85 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,8 +18,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 #include "Args.hpp"
 
 #include <string>
index d02679b40f5c3585a845999f086280948822fdaa..f85957379c2d4ad6a1301c836e52f1b3439a4253 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -21,7 +21,8 @@
 #include "TemporaryFile.hpp"
 #include "Util.hpp"
 #include "assertions.hpp"
-#include "exceptions.hpp"
+
+#include <core/exceptions.hpp>
 
 AtomicFile::AtomicFile(const std::string& path, Mode mode) : m_path(path)
 {
@@ -43,7 +44,8 @@ void
 AtomicFile::write(const std::string& data)
 {
   if (fwrite(data.data(), data.size(), 1, m_stream) != 1) {
-    throw Error("failed to write data to {}: {}", m_path, strerror(errno));
+    throw core::Error(
+      "failed to write data to {}: {}", m_path, strerror(errno));
   }
 }
 
@@ -51,7 +53,8 @@ void
 AtomicFile::write(const std::vector<uint8_t>& data)
 {
   if (fwrite(data.data(), data.size(), 1, m_stream) != 1) {
-    throw Error("failed to write data to {}: {}", m_path, strerror(errno));
+    throw core::Error(
+      "failed to write data to {}: {}", m_path, strerror(errno));
   }
 }
 
@@ -63,7 +66,8 @@ AtomicFile::commit()
   m_stream = nullptr;
   if (result == EOF) {
     Util::unlink_tmp(m_tmp_path);
-    throw Error("failed to write data to {}: {}", m_path, strerror(errno));
+    throw core::Error(
+      "failed to write data to {}: {}", m_path, strerror(errno));
   }
   Util::rename(m_tmp_path, m_path);
 }
index 118c310b4c20d910e22109a6f25c02a6a89e60ed..9dba4181f60d8048db2f8e28fe959cefd91429bf 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,8 +18,8 @@
 
 #pragma once
 
-#include "system.hpp"
-
+#include <cstdint>
+#include <cstdio>
 #include <string>
 #include <vector>
 
index beefd81f1fc2a1efc36bc581a53c5ba8278ec6d1..e480cf8c57cc8a57f03fddcd715d66f4b61ff5d7 100644 (file)
@@ -4,21 +4,14 @@ set(
   AtomicFile.cpp
   CacheEntryReader.cpp
   CacheEntryWriter.cpp
-  CacheFile.cpp
-  Compression.cpp
-  Compressor.cpp
   Config.cpp
   Context.cpp
-  Counters.cpp
-  Decompressor.cpp
   Depfile.cpp
+  Fd.cpp
   Hash.cpp
   Lockfile.cpp
   Logging.cpp
   Manifest.cpp
-  MiniTrace.cpp
-  NullCompressor.cpp
-  NullDecompressor.cpp
   ProgressBar.cpp
   Result.cpp
   ResultDumper.cpp
@@ -26,57 +19,69 @@ set(
   ResultRetriever.cpp
   SignalHandler.cpp
   Stat.cpp
-  Statistics.cpp
   TemporaryFile.cpp
   ThreadPool.cpp
   Util.cpp
-  ZstdCompressor.cpp
-  ZstdDecompressor.cpp
   argprocessing.cpp
   assertions.cpp
   ccache.cpp
-  cleanup.cpp
   compopt.cpp
-  compress.cpp
   execute.cpp
   hashutil.cpp
   language.cpp
-  version.cpp)
+  version.cpp
+)
 
 if(INODE_CACHE_SUPPORTED)
   list(APPEND source_files InodeCache.cpp)
 endif()
 
+if(MTR_ENABLED)
+  list(APPEND source_files MiniTrace.cpp)
+endif()
+
 if(WIN32)
   list(APPEND source_files Win32Util.cpp)
 endif()
 
-add_library(ccache_lib STATIC ${source_files})
+add_library(ccache_framework STATIC ${source_files})
 target_compile_definitions(
-  ccache_lib PUBLIC -Dnssv_CONFIG_SELECT_STRING_VIEW=nssv_STRING_VIEW_NONSTD
+  ccache_framework PUBLIC -Dnssv_CONFIG_SELECT_STRING_VIEW=nssv_STRING_VIEW_NONSTD
 )
 
 if(WIN32)
-  target_link_libraries(ccache_lib PRIVATE ws2_32 "psapi")
+  target_link_libraries(ccache_framework PRIVATE "psapi")
 
   if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
     if(STATIC_LINK)
-      target_link_libraries(ccache_lib PRIVATE -static-libgcc -static-libstdc++ -static winpthread -dynamic)
+      target_link_libraries(ccache_framework PRIVATE -static-libgcc -static-libstdc++ -static winpthread -dynamic)
     else()
-      target_link_libraries(ccache_lib PRIVATE winpthread)
+      target_link_libraries(ccache_framework PRIVATE winpthread)
     endif()
   elseif(STATIC_LINK AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
-    target_link_libraries(ccache_lib PRIVATE -static c++ -dynamic)
+    target_link_libraries(ccache_framework PRIVATE -static c++ -dynamic)
   endif()
 endif()
 
 set(THREADS_PREFER_PTHREAD_FLAG ON)
 find_package(Threads REQUIRED)
 target_link_libraries(
-  ccache_lib
-  PRIVATE standard_settings standard_warnings ZSTD::ZSTD
-          Threads::Threads third_party_lib)
+  ccache_framework
+  PRIVATE standard_settings standard_warnings ZSTD::ZSTD Threads::Threads third_party
+)
+
+target_include_directories(ccache_framework PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
 
-target_include_directories(ccache_lib PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
+if(REDIS_STORAGE_BACKEND)
+  target_compile_definitions(ccache_framework PRIVATE -DHAVE_REDIS_STORAGE_BACKEND)
+  target_link_libraries(
+    ccache_framework
+    PUBLIC standard_settings standard_warnings HIREDIS::HIREDIS third_party
+  )
+endif()
 
+add_subdirectory(compression)
+add_subdirectory(core)
+add_subdirectory(storage)
 add_subdirectory(third_party)
+add_subdirectory(util)
index 36dd3931ee210d9d62438d6b50c7a1f913ea3d0c..79271c7c5d2ae3fe2657ec2731fe59d046e09fad 100644 (file)
 
 #include "CacheEntryReader.hpp"
 
-#include "Compressor.hpp"
-#include "exceptions.hpp"
 #include "fmtmacros.hpp"
 
+#include <compression/Compressor.hpp>
+#include <core/exceptions.hpp>
+
 #include "third_party/fmt/core.h"
 
 CacheEntryReader::CacheEntryReader(FILE* stream,
@@ -30,29 +31,30 @@ CacheEntryReader::CacheEntryReader(FILE* stream,
 {
   uint8_t header_bytes[15];
   if (fread(header_bytes, sizeof(header_bytes), 1, stream) != 1) {
-    throw Error("Error reading header");
+    throw core::Error("Error reading header");
   }
 
   memcpy(m_magic, header_bytes, sizeof(m_magic));
   m_version = header_bytes[4];
-  m_compression_type = Compression::type_from_int(header_bytes[5]);
+  m_compression_type = compression::type_from_int(header_bytes[5]);
   m_compression_level = header_bytes[6];
   Util::big_endian_to_int(header_bytes + 7, m_content_size);
 
   if (memcmp(m_magic, expected_magic, sizeof(m_magic)) != 0) {
-    throw Error("Bad magic value 0x{:02x}{:02x}{:02x}{:02x}",
-                m_magic[0],
-                m_magic[1],
-                m_magic[2],
-                m_magic[3]);
+    throw core::Error("Bad magic value 0x{:02x}{:02x}{:02x}{:02x}",
+                      m_magic[0],
+                      m_magic[1],
+                      m_magic[2],
+                      m_magic[3]);
   }
   if (m_version != expected_version) {
-    throw Error(
+    throw core::Error(
       "Unknown version (actual {}, expected {})", m_version, expected_version);
   }
 
   m_checksum.update(header_bytes, sizeof(header_bytes));
-  m_decompressor = Decompressor::create_from_type(m_compression_type, stream);
+  m_decompressor =
+    compression::Decompressor::create_from_type(m_compression_type, stream);
 }
 
 void
@@ -62,7 +64,7 @@ CacheEntryReader::dump_header(FILE* dump_stream)
   PRINT(dump_stream, "Version: {}\n", m_version);
   PRINT(dump_stream,
         "Compression type: {}\n",
-        Compression::type_to_string(m_compression_type));
+        compression::type_to_string(m_compression_type));
   PRINT(dump_stream, "Compression level: {}\n", m_compression_level);
   PRINT(dump_stream, "Content size: {}\n", m_content_size);
 }
@@ -85,9 +87,10 @@ CacheEntryReader::finalize()
   Util::big_endian_to_int(buffer, expected_digest);
 
   if (actual_digest != expected_digest) {
-    throw Error("Incorrect checksum (actual 0x{:016x}, expected 0x{:016x})",
-                actual_digest,
-                expected_digest);
+    throw core::Error(
+      "Incorrect checksum (actual 0x{:016x}, expected 0x{:016x})",
+      actual_digest,
+      expected_digest);
   }
 
   m_decompressor->finalize();
index 37db80f5e62ba54f98917d57f0e27ab26900e524..f79bca2d1580226366c3d32156989996202167a7 100644 (file)
 
 #pragma once
 
-#include "system.hpp"
-
 #include "Checksum.hpp"
-#include "Decompressor.hpp"
 #include "Util.hpp"
 
+#include <compression/Decompressor.hpp>
+
+#include <cstdint>
+#include <cstdio>
 #include <memory>
 
 // This class knows how to read a cache entry with a common header and a
@@ -82,7 +83,7 @@ public:
   uint8_t version() const;
 
   // Get compression type.
-  Compression::Type compression_type() const;
+  compression::Type compression_type() const;
 
   // Get compression level.
   int8_t compression_level() const;
@@ -91,11 +92,11 @@ public:
   uint64_t content_size() const;
 
 private:
-  std::unique_ptr<Decompressor> m_decompressor;
+  std::unique_ptr<compression::Decompressor> m_decompressor;
   Checksum m_checksum;
   uint8_t m_magic[4];
   uint8_t m_version;
-  Compression::Type m_compression_type;
+  compression::Type m_compression_type;
   int8_t m_compression_level;
   uint64_t m_content_size;
 };
@@ -121,7 +122,7 @@ CacheEntryReader::version() const
   return m_version;
 }
 
-inline Compression::Type
+inline compression::Type
 CacheEntryReader::compression_type() const
 {
   return m_compression_type;
index b9369476731b9344700434edab04f836e0e9ebf8..e04312029a0d6589f39ecc8fa83ef424943838c7 100644 (file)
 
 #include "CacheEntryWriter.hpp"
 
+#include <core/exceptions.hpp>
+
 CacheEntryWriter::CacheEntryWriter(FILE* stream,
                                    const uint8_t* magic,
                                    uint8_t version,
-                                   Compression::Type compression_type,
+                                   compression::Type compression_type,
                                    int8_t compression_level,
                                    uint64_t payload_size)
   // clang-format off
-  : m_compressor(
-      Compressor::create_from_type(compression_type, stream, compression_level))
+  : m_compressor(compression::Compressor::create_from_type(
+                   compression_type, stream, compression_level))
 // clang-format on
 {
   uint8_t header_bytes[15];
@@ -37,7 +39,7 @@ CacheEntryWriter::CacheEntryWriter(FILE* stream,
   uint64_t content_size = 15 + payload_size + 8;
   Util::int_to_big_endian(content_size, header_bytes + 7);
   if (fwrite(header_bytes, sizeof(header_bytes), 1, stream) != 1) {
-    throw Error("Failed to write cache entry header");
+    throw core::Error("Failed to write cache entry header");
   }
   m_checksum.update(header_bytes, sizeof(header_bytes));
 }
index dc9226f61ac3452eaa33870c2782612d1abb8c0e..b0e3a8cada07ed69ba6675f9035659105729b4e9 100644 (file)
 
 #pragma once
 
-#include "system.hpp"
-
 #include "Checksum.hpp"
-#include "Compressor.hpp"
 #include "Util.hpp"
 
+#include <compression/Compressor.hpp>
+
+#include <cstdint>
+#include <cstdio>
 #include <memory>
 
 // This class knows how to write a cache entry with a common header and a
@@ -44,7 +45,7 @@ public:
   CacheEntryWriter(FILE* stream,
                    const uint8_t* magic,
                    uint8_t version,
-                   Compression::Type compression_type,
+                   compression::Type compression_type,
                    int8_t compression_level,
                    uint64_t payload_size);
 
@@ -72,7 +73,7 @@ public:
   void finalize();
 
 private:
-  std::unique_ptr<Compressor> m_compressor;
+  std::unique_ptr<compression::Compressor> m_compressor;
   Checksum m_checksum;
 };
 
diff --git a/src/CacheFile.cpp b/src/CacheFile.cpp
deleted file mode 100644 (file)
index 68072df..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "CacheFile.hpp"
-
-#include "Manifest.hpp"
-#include "Result.hpp"
-#include "Util.hpp"
-
-const Stat&
-CacheFile::lstat() const
-{
-  if (!m_stat) {
-    m_stat = Stat::lstat(m_path);
-  }
-
-  return *m_stat;
-}
-
-CacheFile::Type
-CacheFile::type() const
-{
-  if (Util::ends_with(m_path, Manifest::k_file_suffix)) {
-    return Type::manifest;
-  } else if (Util::ends_with(m_path, Result::k_file_suffix)) {
-    return Type::result;
-  } else {
-    return Type::unknown;
-  }
-}
diff --git a/src/CacheFile.hpp b/src/CacheFile.hpp
deleted file mode 100644 (file)
index 0541068..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include "system.hpp"
-
-#include "Stat.hpp"
-
-#include "third_party/nonstd/optional.hpp"
-
-#include <string>
-
-class CacheFile
-{
-public:
-  enum class Type { result, manifest, unknown };
-
-  explicit CacheFile(const std::string& path);
-
-  const Stat& lstat() const;
-  const std::string& path() const;
-  Type type() const;
-
-private:
-  std::string m_path;
-  mutable nonstd::optional<Stat> m_stat;
-};
-
-inline CacheFile::CacheFile(const std::string& path) : m_path(path)
-{
-}
-
-inline const std::string&
-CacheFile::path() const
-{
-  return m_path;
-}
index e2c6274f3a9ad70d04987c9701a8c39cb05019b7..ee8bcc23bc5e322bf13dc1739652afb2940d4f1c 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "system.hpp"
-
 #ifdef USE_XXH_DISPATCH
 #  include "third_party/xxh_x86dispatch.h"
 #else
 #  include "third_party/xxhash.h"
 #endif
 
+#include <cstdint>
+
 class Checksum
 {
 public:
diff --git a/src/Compression.cpp b/src/Compression.cpp
deleted file mode 100644 (file)
index aa2a182..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "Compression.hpp"
-
-#include "Config.hpp"
-#include "Context.hpp"
-#include "assertions.hpp"
-#include "exceptions.hpp"
-
-namespace Compression {
-
-int8_t
-level_from_config(const Config& config)
-{
-  return config.compression() ? config.compression_level() : 0;
-}
-
-Type
-type_from_config(const Config& config)
-{
-  return config.compression() ? Type::zstd : Type::none;
-}
-
-Type
-type_from_int(uint8_t type)
-{
-  switch (type) {
-  case static_cast<uint8_t>(Type::none):
-    return Type::none;
-
-  case static_cast<uint8_t>(Type::zstd):
-    return Type::zstd;
-  }
-
-  throw Error("Unknown type: {}", type);
-}
-
-std::string
-type_to_string(Type type)
-{
-  switch (type) {
-  case Type::none:
-    return "none";
-
-  case Type::zstd:
-    return "zstd";
-  }
-
-  ASSERT(false);
-}
-
-} // namespace Compression
diff --git a/src/Compression.hpp b/src/Compression.hpp
deleted file mode 100644 (file)
index 24e1746..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include "system.hpp"
-
-#include <string>
-
-class Config;
-
-namespace Compression {
-
-enum class Type : uint8_t {
-  none = 0,
-  zstd = 1,
-};
-
-int8_t level_from_config(const Config& config);
-
-Type type_from_config(const Config& config);
-
-Type type_from_int(uint8_t type);
-
-std::string type_to_string(Compression::Type type);
-
-} // namespace Compression
diff --git a/src/Compressor.cpp b/src/Compressor.cpp
deleted file mode 100644 (file)
index efb1257..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "Compressor.hpp"
-
-#include "NullCompressor.hpp"
-#include "StdMakeUnique.hpp"
-#include "ZstdCompressor.hpp"
-#include "assertions.hpp"
-
-std::unique_ptr<Compressor>
-Compressor::create_from_type(Compression::Type type,
-                             FILE* stream,
-                             int8_t compression_level)
-{
-  switch (type) {
-  case Compression::Type::none:
-    return std::make_unique<NullCompressor>(stream);
-
-  case Compression::Type::zstd:
-    return std::make_unique<ZstdCompressor>(stream, compression_level);
-  }
-
-  ASSERT(false);
-}
diff --git a/src/Compressor.hpp b/src/Compressor.hpp
deleted file mode 100644 (file)
index 3295874..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2019 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include "system.hpp"
-
-#include "Compression.hpp"
-
-#include <memory>
-
-class Compressor
-{
-public:
-  virtual ~Compressor() = default;
-
-  // Create a compressor for the specified type.
-  //
-  // Parameters:
-  // - type: The type.
-  // - stream: The stream to write to.
-  // - compression_level: Desired compression level.
-  static std::unique_ptr<Compressor> create_from_type(Compression::Type type,
-                                                      FILE* stream,
-                                                      int8_t compression_level);
-
-  // Get the actual compression level used for the compressed stream.
-  virtual int8_t actual_compression_level() const = 0;
-
-  // Write data from a buffer to the compressed stream.
-  //
-  // Parameters:
-  // - data: Data to write.
-  // - count: Size of data to write.
-  //
-  // Throws Error on failure.
-  virtual void write(const void* data, size_t count) = 0;
-
-  // Write an unsigned integer to the compressed stream.
-  //
-  // Parameters:
-  // - value: Value to write.
-  //
-  // Throws Error on failure.
-  template<typename T> void write(T value);
-
-  // Finalize compression.
-  //
-  // This method checks that the end state of the compressed stream is correct
-  // and throws Error if not.
-  virtual void finalize() = 0;
-};
index 9e7092360f887044e34b2c701efd1de52b4890ce..0ce230313e15da540d75b63b21b0db203636092f 100644 (file)
 #include "Config.hpp"
 
 #include "AtomicFile.hpp"
-#include "Compression.hpp"
-#include "Sloppiness.hpp"
+#include "MiniTrace.hpp"
 #include "Util.hpp"
 #include "assertions.hpp"
-#include "exceptions.hpp"
 #include "fmtmacros.hpp"
 
+#include <UmaskScope.hpp>
+#include <compression/types.hpp>
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+#include <util/expected.hpp>
+#include <util/path.hpp>
+#include <util/string.hpp>
+
 #include "third_party/fmt/core.h"
 
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
 #include <algorithm>
 #include <cassert>
 #include <fstream>
 using nonstd::nullopt;
 using nonstd::optional;
 
+#if defined(_MSC_VER)
+#  define DLLIMPORT __declspec(dllimport)
+#else
+#  define DLLIMPORT
+#endif
+
+DLLIMPORT extern char** environ;
+
 namespace {
 
 enum class ConfigItem {
@@ -75,9 +93,12 @@ enum class ConfigItem {
   read_only,
   read_only_direct,
   recache,
+  reshare,
   run_second_cpp,
+  secondary_storage,
   sloppiness,
   stats,
+  stats_log,
   temporary_dir,
   umask,
 };
@@ -116,9 +137,12 @@ const std::unordered_map<std::string, ConfigItem> k_config_key_table = {
   {"read_only", ConfigItem::read_only},
   {"read_only_direct", ConfigItem::read_only_direct},
   {"recache", ConfigItem::recache},
+  {"reshare", ConfigItem::reshare},
   {"run_second_cpp", ConfigItem::run_second_cpp},
+  {"secondary_storage", ConfigItem::secondary_storage},
   {"sloppiness", ConfigItem::sloppiness},
   {"stats", ConfigItem::stats},
+  {"stats_log", ConfigItem::stats_log},
   {"temporary_dir", ConfigItem::temporary_dir},
   {"umask", ConfigItem::umask},
 };
@@ -159,8 +183,11 @@ const std::unordered_map<std::string, std::string> k_env_variable_table = {
   {"READONLY", "read_only"},
   {"READONLY_DIRECT", "read_only_direct"},
   {"RECACHE", "recache"},
+  {"RESHARE", "reshare"},
+  {"SECONDARY_STORAGE", "secondary_storage"},
   {"SLOPPINESS", "sloppiness"},
   {"STATS", "stats"},
+  {"STATSLOG", "stats_log"},
   {"TEMPDIR", "temporary_dir"},
   {"UMASK", "umask"},
 };
@@ -181,7 +208,7 @@ parse_bool(const std::string& value,
     std::string lower_value = Util::to_lowercase(value);
     if (value == "0" || lower_value == "false" || lower_value == "disable"
         || lower_value == "no") {
-      throw Error(
+      throw core::Error(
         "invalid boolean environment variable value \"{}\" (did you mean to"
         " set \"CCACHE_{}{}=true\"?)",
         value,
@@ -194,7 +221,7 @@ parse_bool(const std::string& value,
   } else if (value == "false") {
     return false;
   } else {
-    throw Error("not a boolean value: \"{}\"", value);
+    throw core::Error("not a boolean value: \"{}\"", value);
   }
 }
 
@@ -204,22 +231,6 @@ format_bool(bool value)
   return value ? "true" : "false";
 }
 
-double
-parse_double(const std::string& value)
-{
-  size_t end;
-  double result;
-  try {
-    result = std::stod(value, &end);
-  } catch (std::exception& e) {
-    throw Error(e.what());
-  }
-  if (end != value.size()) {
-    throw Error("invalid floating point: \"{}\"", value);
-  }
-  return result;
-}
-
 std::string
 format_cache_size(uint64_t value)
 {
@@ -245,38 +256,38 @@ parse_compiler_type(const std::string& value)
   }
 }
 
-uint32_t
+core::Sloppiness
 parse_sloppiness(const std::string& value)
 {
   size_t start = 0;
   size_t end = 0;
-  uint32_t result = 0;
+  core::Sloppiness result;
   while (end != std::string::npos) {
     end = value.find_first_of(", ", start);
     std::string token =
-      Util::strip_whitespace(value.substr(start, end - start));
+      util::strip_whitespace(value.substr(start, end - start));
     if (token == "file_stat_matches") {
-      result |= SLOPPY_FILE_STAT_MATCHES;
+      result.enable(core::Sloppy::file_stat_matches);
     } else if (token == "file_stat_matches_ctime") {
-      result |= SLOPPY_FILE_STAT_MATCHES_CTIME;
+      result.enable(core::Sloppy::file_stat_matches_ctime);
     } else if (token == "include_file_ctime") {
-      result |= SLOPPY_INCLUDE_FILE_CTIME;
+      result.enable(core::Sloppy::include_file_ctime);
     } else if (token == "include_file_mtime") {
-      result |= SLOPPY_INCLUDE_FILE_MTIME;
+      result.enable(core::Sloppy::include_file_mtime);
     } else if (token == "system_headers" || token == "no_system_headers") {
-      result |= SLOPPY_SYSTEM_HEADERS;
+      result.enable(core::Sloppy::system_headers);
     } else if (token == "pch_defines") {
-      result |= SLOPPY_PCH_DEFINES;
+      result.enable(core::Sloppy::pch_defines);
     } else if (token == "time_macros") {
-      result |= SLOPPY_TIME_MACROS;
+      result.enable(core::Sloppy::time_macros);
     } else if (token == "clang_index_store") {
-      result |= SLOPPY_CLANG_INDEX_STORE;
+      result.enable(core::Sloppy::clang_index_store);
     } else if (token == "locale") {
-      result |= SLOPPY_LOCALE;
+      result.enable(core::Sloppy::locale);
     } else if (token == "modules") {
-      result |= SLOPPY_MODULES;
+      result.enable(core::Sloppy::modules);
     } else if (token == "ivfsoverlay") {
-      result |= SLOPPY_IVFSOVERLAY;
+      result.enable(core::Sloppy::ivfsoverlay);
     } // else: ignore unknown value for forward compatibility
     start = value.find_first_not_of(", ", end);
   }
@@ -284,40 +295,40 @@ parse_sloppiness(const std::string& value)
 }
 
 std::string
-format_sloppiness(uint32_t sloppiness)
+format_sloppiness(core::Sloppiness sloppiness)
 {
   std::string result;
-  if (sloppiness & SLOPPY_INCLUDE_FILE_MTIME) {
+  if (sloppiness.is_enabled(core::Sloppy::include_file_mtime)) {
     result += "include_file_mtime, ";
   }
-  if (sloppiness & SLOPPY_INCLUDE_FILE_CTIME) {
+  if (sloppiness.is_enabled(core::Sloppy::include_file_ctime)) {
     result += "include_file_ctime, ";
   }
-  if (sloppiness & SLOPPY_TIME_MACROS) {
+  if (sloppiness.is_enabled(core::Sloppy::time_macros)) {
     result += "time_macros, ";
   }
-  if (sloppiness & SLOPPY_PCH_DEFINES) {
+  if (sloppiness.is_enabled(core::Sloppy::pch_defines)) {
     result += "pch_defines, ";
   }
-  if (sloppiness & SLOPPY_FILE_STAT_MATCHES) {
+  if (sloppiness.is_enabled(core::Sloppy::file_stat_matches)) {
     result += "file_stat_matches, ";
   }
-  if (sloppiness & SLOPPY_FILE_STAT_MATCHES_CTIME) {
+  if (sloppiness.is_enabled(core::Sloppy::file_stat_matches_ctime)) {
     result += "file_stat_matches_ctime, ";
   }
-  if (sloppiness & SLOPPY_SYSTEM_HEADERS) {
+  if (sloppiness.is_enabled(core::Sloppy::system_headers)) {
     result += "system_headers, ";
   }
-  if (sloppiness & SLOPPY_CLANG_INDEX_STORE) {
+  if (sloppiness.is_enabled(core::Sloppy::clang_index_store)) {
     result += "clang_index_store, ";
   }
-  if (sloppiness & SLOPPY_LOCALE) {
+  if (sloppiness.is_enabled(core::Sloppy::locale)) {
     result += "locale, ";
   }
-  if (sloppiness & SLOPPY_MODULES) {
+  if (sloppiness.is_enabled(core::Sloppy::modules)) {
     result += "modules, ";
   }
-  if (sloppiness & SLOPPY_IVFSOVERLAY) {
+  if (sloppiness.is_enabled(core::Sloppy::ivfsoverlay)) {
     result += "ivfsoverlay, ";
   }
   if (!result.empty()) {
@@ -327,36 +338,21 @@ format_sloppiness(uint32_t sloppiness)
   return result;
 }
 
-uint32_t
-parse_umask(const std::string& value)
-{
-  if (value.empty()) {
-    return std::numeric_limits<uint32_t>::max();
-  }
-
-  size_t end;
-  uint32_t result = std::stoul(value, &end, 8);
-  if (end != value.size()) {
-    throw Error("not an octal integer: \"{}\"", value);
-  }
-  return result;
-}
-
 std::string
-format_umask(uint32_t umask)
+format_umask(nonstd::optional<mode_t> umask)
 {
-  if (umask == std::numeric_limits<uint32_t>::max()) {
-    return {};
+  if (umask) {
+    return FMT("{:03o}", *umask);
   } else {
-    return FMT("{:03o}", umask);
+    return {};
   }
 }
 
 void
 verify_absolute_path(const std::string& value)
 {
-  if (!Util::is_absolute_path(value)) {
-    throw Error("not an absolute path: \"{}\"", value);
+  if (!util::is_absolute_path(value)) {
+    throw core::Error("not an absolute path: \"{}\"", value);
   }
 }
 
@@ -366,7 +362,7 @@ parse_line(const std::string& line,
            std::string* value,
            std::string* error_message)
 {
-  std::string stripped_line = Util::strip_whitespace(line);
+  std::string stripped_line = util::strip_whitespace(line);
   if (stripped_line.empty() || stripped_line[0] == '#') {
     return true;
   }
@@ -377,8 +373,8 @@ parse_line(const std::string& line,
   }
   *key = stripped_line.substr(0, equal_pos);
   *value = stripped_line.substr(equal_pos + 1);
-  *key = Util::strip_whitespace(*key);
-  *value = Util::strip_whitespace(*value);
+  *key = util::strip_whitespace(*key);
+  *value = util::strip_whitespace(*value);
   return true;
 }
 
@@ -408,11 +404,11 @@ parse_config_file(const std::string& path,
       std::string value;
       std::string error_message;
       if (!parse_line(line, &key, &value, &error_message)) {
-        throw Error(error_message);
+        throw core::Error(error_message);
       }
       config_line_handler(line, key, value);
-    } catch (const Error& e) {
-      throw Error("{}:{}: {}", path, line_number, e.what());
+    } catch (const core::Error& e) {
+      throw core::Error("{}:{}: {}", path, line_number, e.what());
     }
   }
   return true;
@@ -420,6 +416,30 @@ parse_config_file(const std::string& path,
 
 } // namespace
 
+static std::string
+default_cache_dir(const std::string& home_dir)
+{
+#ifdef _WIN32
+  return home_dir + "/ccache";
+#elif defined(__APPLE__)
+  return home_dir + "/Library/Caches/ccache";
+#else
+  return home_dir + "/.cache/ccache";
+#endif
+}
+
+static std::string
+default_config_dir(const std::string& home_dir)
+{
+#ifdef _WIN32
+  return home_dir + "/ccache";
+#elif defined(__APPLE__)
+  return home_dir + "/Library/Preferences/ccache";
+#else
+  return home_dir + "/.config/ccache";
+#endif
+}
+
 std::string
 compiler_type_to_string(CompilerType compiler_type)
 {
@@ -442,6 +462,77 @@ compiler_type_to_string(CompilerType compiler_type)
   ASSERT(false);
 }
 
+void
+Config::read()
+{
+  const std::string home_dir = Util::get_home_directory();
+  const std::string legacy_ccache_dir = home_dir + "/.ccache";
+  const bool legacy_ccache_dir_exists =
+    Stat::stat(legacy_ccache_dir).is_directory();
+  const char* const env_xdg_cache_home = getenv("XDG_CACHE_HOME");
+  const char* const env_xdg_config_home = getenv("XDG_CONFIG_HOME");
+
+  const char* env_ccache_configpath = getenv("CCACHE_CONFIGPATH");
+  if (env_ccache_configpath) {
+    set_primary_config_path(env_ccache_configpath);
+  } else {
+    // Only used for ccache tests:
+    const char* const env_ccache_configpath2 = getenv("CCACHE_CONFIGPATH2");
+
+    set_secondary_config_path(env_ccache_configpath2
+                                ? env_ccache_configpath2
+                                : FMT("{}/ccache.conf", SYSCONFDIR));
+    MTR_BEGIN("config", "conf_read_secondary");
+    // A missing config file in SYSCONFDIR is OK so don't check return value.
+    update_from_file(secondary_config_path());
+    MTR_END("config", "conf_read_secondary");
+
+    const char* const env_ccache_dir = getenv("CCACHE_DIR");
+    std::string primary_config_dir;
+    if (env_ccache_dir && *env_ccache_dir) {
+      primary_config_dir = env_ccache_dir;
+    } else if (!cache_dir().empty() && !env_ccache_dir) {
+      primary_config_dir = cache_dir();
+    } else if (legacy_ccache_dir_exists) {
+      primary_config_dir = legacy_ccache_dir;
+    } else if (env_xdg_config_home) {
+      primary_config_dir = FMT("{}/ccache", env_xdg_config_home);
+    } else {
+      primary_config_dir = default_config_dir(home_dir);
+    }
+    set_primary_config_path(primary_config_dir + "/ccache.conf");
+  }
+
+  const std::string& cache_dir_before_primary_config = cache_dir();
+
+  MTR_BEGIN("config", "conf_read_primary");
+  update_from_file(primary_config_path());
+  MTR_END("config", "conf_read_primary");
+
+  // Ignore cache_dir set in primary
+  set_cache_dir(cache_dir_before_primary_config);
+
+  MTR_BEGIN("config", "conf_update_from_environment");
+  update_from_environment();
+  // (cache_dir is set above if CCACHE_DIR is set.)
+  MTR_END("config", "conf_update_from_environment");
+
+  if (cache_dir().empty()) {
+    if (legacy_ccache_dir_exists) {
+      set_cache_dir(legacy_ccache_dir);
+    } else if (env_xdg_cache_home) {
+      set_cache_dir(FMT("{}/ccache", env_xdg_cache_home));
+    } else {
+      set_cache_dir(default_cache_dir(home_dir));
+    }
+  }
+  // else: cache_dir was set explicitly via environment or via secondary
+  // config.
+
+  // We have now determined config.cache_dir and populated the rest of config
+  // in prio order (1. environment, 2. primary config, 3. secondary config).
+}
+
 const std::string&
 Config::primary_config_path() const
 {
@@ -469,14 +560,12 @@ Config::set_secondary_config_path(std::string path)
 bool
 Config::update_from_file(const std::string& path)
 {
-  return parse_config_file(path,
-                           [&](const std::string& /*line*/,
-                               const std::string& key,
-                               const std::string& value) {
-                             if (!key.empty()) {
-                               set_item(key, value, nullopt, false, path);
-                             }
-                           });
+  return parse_config_file(
+    path, [&](const auto& /*line*/, const auto& key, const auto& value) {
+      if (!key.empty()) {
+        this->set_item(key, value, nullopt, false, path);
+      }
+    });
 }
 
 void
@@ -485,7 +574,7 @@ Config::update_from_environment()
   for (char** env = environ; *env; ++env) {
     std::string setting = *env;
     const std::string prefix = "CCACHE_";
-    if (!Util::starts_with(setting, prefix)) {
+    if (!util::starts_with(setting, prefix)) {
       continue;
     }
     size_t equal_pos = setting.find('=');
@@ -495,7 +584,7 @@ Config::update_from_environment()
 
     std::string key = setting.substr(prefix.size(), equal_pos - prefix.size());
     std::string value = setting.substr(equal_pos + 1);
-    bool negate = Util::starts_with(key, "NO");
+    bool negate = util::starts_with(key, "NO");
     if (negate) {
       key = key.substr(2);
     }
@@ -509,8 +598,8 @@ Config::update_from_environment()
 
     try {
       set_item(config_key, value, key, negate, "environment");
-    } catch (const Error& e) {
-      throw Error("CCACHE_{}{}: {}", negate ? "NO" : "", key, e.what());
+    } catch (const core::Error& e) {
+      throw core::Error("CCACHE_{}{}: {}", negate ? "NO" : "", key, e.what());
     }
   }
 }
@@ -520,7 +609,7 @@ Config::get_string_value(const std::string& key) const
 {
   auto it = k_config_key_table.find(key);
   if (it == k_config_key_table.end()) {
-    throw Error("unknown configuration option \"{}\"", key);
+    throw core::Error("unknown configuration option \"{}\"", key);
   }
 
   switch (it->second) {
@@ -623,15 +712,24 @@ Config::get_string_value(const std::string& key) const
   case ConfigItem::recache:
     return format_bool(m_recache);
 
+  case ConfigItem::reshare:
+    return format_bool(m_reshare);
+
   case ConfigItem::run_second_cpp:
     return format_bool(m_run_second_cpp);
 
+  case ConfigItem::secondary_storage:
+    return m_secondary_storage;
+
   case ConfigItem::sloppiness:
     return format_sloppiness(m_sloppiness);
 
   case ConfigItem::stats:
     return format_bool(m_stats);
 
+  case ConfigItem::stats_log:
+    return m_stats_log;
+
   case ConfigItem::temporary_dir:
     return m_temporary_dir;
 
@@ -645,10 +743,12 @@ Config::get_string_value(const std::string& key) const
 void
 Config::set_value_in_file(const std::string& path,
                           const std::string& key,
-                          const std::string& value)
+                          const std::string& value) const
 {
+  UmaskScope umask_scope(m_umask);
+
   if (k_config_key_table.find(key) == k_config_key_table.end()) {
-    throw Error("unknown configuration option \"{}\"", key);
+    throw core::Error("unknown configuration option \"{}\"", key);
   }
 
   // Verify that the value is valid; set_item will throw if not.
@@ -661,26 +761,25 @@ Config::set_value_in_file(const std::string& path,
     Util::ensure_dir_exists(Util::dir_name(resolved_path));
     try {
       Util::write_file(resolved_path, "");
-    } catch (const Error& e) {
-      throw Error("failed to write to {}: {}", resolved_path, e.what());
+    } catch (const core::Error& e) {
+      throw core::Error("failed to write to {}: {}", resolved_path, e.what());
     }
   }
 
   AtomicFile output(resolved_path, AtomicFile::Mode::text);
   bool found = false;
 
-  if (!parse_config_file(path,
-                         [&](const std::string& c_line,
-                             const std::string& c_key,
-                             const std::string& /*c_value*/) {
-                           if (c_key == key) {
-                             output.write(FMT("{} = {}\n", key, value));
-                             found = true;
-                           } else {
-                             output.write(FMT("{}\n", c_line));
-                           }
-                         })) {
-    throw Error("failed to open {}: {}", path, strerror(errno));
+  if (!parse_config_file(
+        path,
+        [&](const auto& c_line, const auto& c_key, const auto& /*c_value*/) {
+          if (c_key == key) {
+            output.write(FMT("{} = {}\n", key, value));
+            found = true;
+          } else {
+            output.write(FMT("{}\n", c_line));
+          }
+        })) {
+    throw core::Error("failed to open {}: {}", path, strerror(errno));
   }
 
   if (!found) {
@@ -753,11 +852,10 @@ Config::set_item(const std::string& key,
     m_compression = parse_bool(value, env_var_key, negate);
     break;
 
-  case ConfigItem::compression_level: {
-    m_compression_level =
-      Util::parse_signed(value, INT8_MIN, INT8_MAX, "compression_level");
+  case ConfigItem::compression_level:
+    m_compression_level = util::value_or_throw<core::Error>(
+      util::parse_signed(value, INT8_MIN, INT8_MAX, "compression_level"));
     break;
-  }
 
   case ConfigItem::cpp_extension:
     m_cpp_extension = value;
@@ -816,7 +914,8 @@ Config::set_item(const std::string& key,
     break;
 
   case ConfigItem::limit_multiple:
-    m_limit_multiple = Util::clamp(parse_double(value), 0.0, 1.0);
+    m_limit_multiple = Util::clamp(
+      util::value_or_throw<core::Error>(util::parse_double(value)), 0.0, 1.0);
     break;
 
   case ConfigItem::log_file:
@@ -824,7 +923,8 @@ Config::set_item(const std::string& key,
     break;
 
   case ConfigItem::max_files:
-    m_max_files = Util::parse_unsigned(value, nullopt, nullopt, "max_files");
+    m_max_files = util::value_or_throw<core::Error>(
+      util::parse_unsigned(value, nullopt, nullopt, "max_files"));
     break;
 
   case ConfigItem::max_size:
@@ -859,10 +959,18 @@ Config::set_item(const std::string& key,
     m_recache = parse_bool(value, env_var_key, negate);
     break;
 
+  case ConfigItem::reshare:
+    m_reshare = parse_bool(value, env_var_key, negate);
+    break;
+
   case ConfigItem::run_second_cpp:
     m_run_second_cpp = parse_bool(value, env_var_key, negate);
     break;
 
+  case ConfigItem::secondary_storage:
+    m_secondary_storage = Util::expand_environment_variables(value);
+    break;
+
   case ConfigItem::sloppiness:
     m_sloppiness = parse_sloppiness(value);
     break;
@@ -871,13 +979,23 @@ Config::set_item(const std::string& key,
     m_stats = parse_bool(value, env_var_key, negate);
     break;
 
+  case ConfigItem::stats_log:
+    m_stats_log = Util::expand_environment_variables(value);
+    break;
+
   case ConfigItem::temporary_dir:
     m_temporary_dir = Util::expand_environment_variables(value);
     m_temporary_dir_configured_explicitly = true;
     break;
 
   case ConfigItem::umask:
-    m_umask = parse_umask(value);
+    if (!value.empty()) {
+      const auto umask = util::parse_umask(value);
+      if (!umask) {
+        throw core::Error(umask.error());
+      }
+      m_umask = *umask;
+    }
     break;
   }
 
@@ -889,7 +1007,7 @@ Config::check_key_tables_consistency()
 {
   for (const auto& item : k_env_variable_table) {
     if (k_config_key_table.find(item.second) == k_config_key_table.end()) {
-      throw Error(
+      throw core::Error(
         "env var {} mapped to {} which is missing from k_config_key_table",
         item.first,
         item.second);
index eb574dc8c45f0f45fc5c73afb0a6611f4b8eb7b2..acb09e990322b01ec419a67437e29a662af4e631 100644 (file)
 
 #pragma once
 
-#include "system.hpp"
-
 #include "NonCopyable.hpp"
 #include "Util.hpp"
 
+#include <core/Sloppiness.hpp>
+
 #include "third_party/nonstd/optional.hpp"
 
+#include <cstdint>
 #include <functional>
 #include <limits>
 #include <string>
@@ -34,12 +35,12 @@ enum class CompilerType { auto_guess, clang, gcc, nvcc, other, pump };
 
 std::string compiler_type_to_string(CompilerType compiler_type);
 
-class Config
+class Config : NonCopyable
 {
 public:
   Config() = default;
-  Config(Config&) = default;
-  Config& operator=(const Config&) = default;
+
+  void read();
 
   bool absolute_paths_in_stderr() const;
   const std::string& base_dir() const;
@@ -74,19 +75,22 @@ public:
   bool read_only() const;
   bool read_only_direct() const;
   bool recache() const;
+  bool reshare() const;
   bool run_second_cpp() const;
-  uint32_t sloppiness() const;
+  const std::string& secondary_storage() const;
+  core::Sloppiness sloppiness() const;
   bool stats() const;
+  const std::string& stats_log() const;
   const std::string& temporary_dir() const;
-  uint32_t umask() const;
+  nonstd::optional<mode_t> umask() const;
 
   void set_base_dir(const std::string& value);
   void set_cache_dir(const std::string& value);
-  void set_cpp_extension(const std::string& value);
   void set_compiler(const std::string& value);
   void set_compiler_type(CompilerType value);
-  void set_depend_mode(bool value);
+  void set_cpp_extension(const std::string& value);
   void set_debug(bool value);
+  void set_depend_mode(bool value);
   void set_direct_mode(bool value);
   void set_ignore_options(const std::string& value);
   void set_inode_cache(bool value);
@@ -122,9 +126,9 @@ public:
 
   void visit_items(const ItemVisitor& item_visitor) const;
 
-  static void set_value_in_file(const std::string& path,
-                                const std::string& key,
-                                const std::string& value);
+  void set_value_in_file(const std::string& path,
+                         const std::string& key,
+                         const std::string& value) const;
 
   // Called from unit tests.
   static void check_key_tables_consistency();
@@ -166,11 +170,14 @@ private:
   bool m_read_only = false;
   bool m_read_only_direct = false;
   bool m_recache = false;
+  bool m_reshare = false;
   bool m_run_second_cpp = true;
-  uint32_t m_sloppiness = 0;
+  std::string m_secondary_storage;
+  core::Sloppiness m_sloppiness;
   bool m_stats = true;
+  std::string m_stats_log;
   std::string m_temporary_dir;
-  uint32_t m_umask = std::numeric_limits<uint32_t>::max(); // Don't set umask
+  nonstd::optional<mode_t> m_umask;
 
   bool m_temporary_dir_configured_explicitly = false;
 
@@ -383,13 +390,25 @@ Config::recache() const
   return m_recache;
 }
 
+inline bool
+Config::reshare() const
+{
+  return m_reshare;
+}
+
 inline bool
 Config::run_second_cpp() const
 {
   return m_run_second_cpp;
 }
 
-inline uint32_t
+inline const std::string&
+Config::secondary_storage() const
+{
+  return m_secondary_storage;
+}
+
+inline core::Sloppiness
 Config::sloppiness() const
 {
   return m_sloppiness;
@@ -401,13 +420,19 @@ Config::stats() const
   return m_stats;
 }
 
+inline const std::string&
+Config::stats_log() const
+{
+  return m_stats_log;
+}
+
 inline const std::string&
 Config::temporary_dir() const
 {
   return m_temporary_dir;
 }
 
-inline uint32_t
+inline nonstd::optional<mode_t>
 Config::umask() const
 {
   return m_umask;
index a7ce450c5235fc6afbe5602d000c89e82775f48c..bcfff8c78a46d97b4bc21340510ebc7ed4f008bf 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #include "Context.hpp"
 
-#include "Counters.hpp"
 #include "Logging.hpp"
 #include "SignalHandler.hpp"
 #include "Util.hpp"
 #include "hashutil.hpp"
 
+#include <core/wincompat.hpp>
+#include <util/path.hpp>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
 #include <algorithm>
 #include <string>
 #include <vector>
@@ -32,7 +38,8 @@ using nonstd::string_view;
 
 Context::Context()
   : actual_cwd(Util::get_actual_cwd()),
-    apparent_cwd(Util::get_apparent_cwd(actual_cwd))
+    apparent_cwd(Util::get_apparent_cwd(actual_cwd)),
+    storage(config)
 #ifdef INODE_CACHE_SUPPORTED
     ,
     inode_cache(config)
@@ -40,6 +47,27 @@ Context::Context()
 {
 }
 
+void
+Context::initialize()
+{
+  config.read();
+  Logging::init(config);
+
+  ignore_header_paths =
+    util::split_path_list(config.ignore_headers_in_manifest());
+  set_ignore_options(Util::split_into_strings(config.ignore_options(), " "));
+
+  // Set default umask for all files created by ccache from now on (if
+  // configured to). This is intentionally done after calling Logging::init so
+  // that the log file won't be affected by the umask but before creating the
+  // initial configuration file. The intention is that all files and directories
+  // in the cache directory should be affected by the configured umask and that
+  // no other files and directories should.
+  if (config.umask()) {
+    original_umask = umask(*config.umask());
+  }
+}
+
 Context::~Context()
 {
   unlink_pending_tmp_files();
index c0211244c36a4ea39305712888837a13e72df6ba..547db9d3cf16d67d5f2f7909ee91c5a7e62c96e1 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "system.hpp"
-
 #include "Args.hpp"
 #include "ArgsInfo.hpp"
 #include "Config.hpp"
-#include "Counters.hpp"
 #include "Digest.hpp"
 #include "File.hpp"
 #include "MiniTrace.hpp"
 #include "NonCopyable.hpp"
-#include "Sloppiness.hpp"
 
 #ifdef INODE_CACHE_SUPPORTED
 #  include "InodeCache.hpp"
 #endif
 
+#include <storage/Storage.hpp>
+
 #include "third_party/nonstd/optional.hpp"
 #include "third_party/nonstd/string_view.hpp"
 
+#include <ctime>
 #include <string>
 #include <unordered_map>
 #include <vector>
@@ -49,6 +48,10 @@ public:
   Context();
   ~Context();
 
+  // Read configuration, initialize logging, etc. Typically not called from unit
+  // tests.
+  void initialize();
+
   ArgsInfo args_info;
   Config config;
 
@@ -61,20 +64,6 @@ public:
   // The original argument list.
   Args orig_args;
 
-  // Name (represented as a hash) of the file containing the manifest for the
-  // cached result.
-  const nonstd::optional<Digest>& manifest_name() const;
-
-  // Full path to the file containing the manifest (cachedir/a/b/cdef[...]M), if
-  // any.
-  const nonstd::optional<std::string>& manifest_path() const;
-
-  // Name (represented as a hash) of the file containing the cached result.
-  const nonstd::optional<Digest>& result_name() const;
-
-  // Full path to the file containing the result (cachedir/a/b/cdef[...]R).
-  const nonstd::optional<std::string>& result_path() const;
-
   // Time of compilation. Used to see if include files have changed after
   // compilation.
   time_t time_of_compilation = 0;
@@ -100,19 +89,14 @@ public:
   // Headers (or directories with headers) to ignore in manifest mode.
   std::vector<std::string> ignore_header_paths;
 
+  // Storage (fronting primary and secondary storage backends).
+  storage::Storage storage;
+
 #ifdef INODE_CACHE_SUPPORTED
   // InodeCache that caches source file hashes when enabled.
   mutable InodeCache inode_cache;
 #endif
 
-  // Statistics updates which get written into the statistics file belonging to
-  // the result.
-  Counters counter_updates;
-
-  // Statistics updates which get written into the statistics file belonging to
-  // the manifest.
-  Counters manifest_counter_updates;
-
   // PID of currently executing compiler that we have started, if any. 0 means
   // no ongoing compilation.
   pid_t compiler_pid = 0;
@@ -133,21 +117,10 @@ public:
   std::unique_ptr<MiniTrace> mini_trace;
 #endif
 
-  void set_manifest_name(const Digest& name);
-  void set_manifest_path(const std::string& path);
-  void set_result_name(const Digest& name);
-  void set_result_path(const std::string& path);
-
   // Register a temporary file to remove at program exit.
   void register_pending_tmp_file(const std::string& path);
 
 private:
-  nonstd::optional<Digest> m_manifest_name;
-  nonstd::optional<std::string> m_manifest_path;
-
-  nonstd::optional<Digest> m_result_name;
-  nonstd::optional<std::string> m_result_path;
-
   // Options to ignore for the hash.
   std::vector<std::string> m_ignore_options;
 
@@ -163,56 +136,8 @@ private:
   void unlink_pending_tmp_files_signal_safe(); // called from signal handler
 };
 
-inline const nonstd::optional<Digest>&
-Context::manifest_name() const
-{
-  return m_manifest_name;
-}
-
-inline const nonstd::optional<std::string>&
-Context::manifest_path() const
-{
-  return m_manifest_path;
-}
-
-inline const nonstd::optional<Digest>&
-Context::result_name() const
-{
-  return m_result_name;
-}
-
-inline const nonstd::optional<std::string>&
-Context::result_path() const
-{
-  return m_result_path;
-}
-
 inline const std::vector<std::string>&
 Context::ignore_options() const
 {
   return m_ignore_options;
 }
-
-inline void
-Context::set_manifest_name(const Digest& name)
-{
-  m_manifest_name = name;
-}
-
-inline void
-Context::set_manifest_path(const std::string& path)
-{
-  m_manifest_path = path;
-}
-
-inline void
-Context::set_result_name(const Digest& name)
-{
-  m_result_name = name;
-}
-
-inline void
-Context::set_result_path(const std::string& path)
-{
-  m_result_path = path;
-}
diff --git a/src/Counters.cpp b/src/Counters.cpp
deleted file mode 100644 (file)
index 1263d9d..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (C) 2010-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "Counters.hpp"
-
-#include "Statistic.hpp"
-#include "assertions.hpp"
-
-#include <algorithm>
-
-Counters::Counters() : m_counters(static_cast<size_t>(Statistic::END))
-{
-}
-
-uint64_t
-Counters::get(Statistic statistic) const
-{
-  const auto index = static_cast<size_t>(statistic);
-  ASSERT(index < static_cast<size_t>(Statistic::END));
-  return index < m_counters.size() ? m_counters[index] : 0;
-}
-
-void
-Counters::set(Statistic statistic, uint64_t value)
-{
-  const auto index = static_cast<size_t>(statistic);
-  ASSERT(index < static_cast<size_t>(Statistic::END));
-  m_counters[index] = value;
-}
-
-uint64_t
-Counters::get_raw(size_t index) const
-{
-  ASSERT(index < size());
-  return m_counters[index];
-}
-
-void
-Counters::set_raw(size_t index, uint64_t value)
-{
-  if (index >= m_counters.size()) {
-    m_counters.resize(index + 1);
-  }
-  m_counters[index] = value;
-}
-
-void
-Counters::increment(Statistic statistic, int64_t value)
-{
-  const auto i = static_cast<size_t>(statistic);
-  if (i >= m_counters.size()) {
-    m_counters.resize(i + 1);
-  }
-  auto& counter = m_counters[i];
-  counter =
-    std::max(static_cast<int64_t>(0), static_cast<int64_t>(counter + value));
-}
-
-void
-Counters::increment(const Counters& other)
-{
-  m_counters.resize(std::max(size(), other.size()));
-  for (size_t i = 0; i < other.size(); ++i) {
-    auto& counter = m_counters[i];
-    counter = std::max(static_cast<int64_t>(0),
-                       static_cast<int64_t>(counter + other.m_counters[i]));
-  }
-}
-
-size_t
-Counters::size() const
-{
-  return m_counters.size();
-}
-
-bool
-Counters::all_zero() const
-{
-  return !std::any_of(
-    m_counters.begin(), m_counters.end(), [](unsigned v) { return v != 0; });
-}
diff --git a/src/Counters.hpp b/src/Counters.hpp
deleted file mode 100644 (file)
index d19227b..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2010-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include "system.hpp"
-
-#include <vector>
-
-enum class Statistic;
-
-// A simple wrapper around a vector of integers used for the statistics
-// counters.
-class Counters
-{
-public:
-  Counters();
-
-  uint64_t get(Statistic statistic) const;
-  void set(Statistic statistic, uint64_t value);
-
-  uint64_t get_raw(size_t index) const;
-  void set_raw(size_t index, uint64_t value);
-
-  void increment(Statistic statistic, int64_t value = 1);
-  void increment(const Counters& other);
-
-  size_t size() const;
-
-  // Return true if all counters are zero, false otherwise.
-  bool all_zero() const;
-
-private:
-  std::vector<uint64_t> m_counters;
-};
diff --git a/src/Decompressor.cpp b/src/Decompressor.cpp
deleted file mode 100644 (file)
index b53cd95..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "Decompressor.hpp"
-
-#include "NullDecompressor.hpp"
-#include "StdMakeUnique.hpp"
-#include "ZstdDecompressor.hpp"
-#include "assertions.hpp"
-
-std::unique_ptr<Decompressor>
-Decompressor::create_from_type(Compression::Type type, FILE* stream)
-{
-  switch (type) {
-  case Compression::Type::none:
-    return std::make_unique<NullDecompressor>(stream);
-
-  case Compression::Type::zstd:
-    return std::make_unique<ZstdDecompressor>(stream);
-  }
-
-  ASSERT(false);
-}
diff --git a/src/Decompressor.hpp b/src/Decompressor.hpp
deleted file mode 100644 (file)
index 5955889..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2019 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include "system.hpp"
-
-#include "Compression.hpp"
-
-#include <memory>
-
-class Decompressor
-{
-public:
-  virtual ~Decompressor() = default;
-
-  // Create a decompressor for the specified type.
-  //
-  // Parameters:
-  // - type: The type.
-  // - stream: The stream to read from.
-  static std::unique_ptr<Decompressor> create_from_type(Compression::Type type,
-                                                        FILE* stream);
-
-  // Read data into a buffer from the compressed stream.
-  //
-  // Parameters:
-  // - data: Buffer to write decompressed data to.
-  // - count: How many bytes to write.
-  //
-  // Throws Error on failure.
-  virtual void read(void* data, size_t count) = 0;
-
-  // Finalize decompression.
-  //
-  // This method checks that the end state of the compressed stream is correct
-  // and throws Error if not.
-  virtual void finalize() = 0;
-};
index 13106ce7fb576cb7c092ba043de5c3bc5eac0b18..5a13cc220b4ec50e0fa1725891697682e55455d5 100644 (file)
@@ -23,6 +23,9 @@
 #include "Logging.hpp"
 #include "assertions.hpp"
 
+#include <core/exceptions.hpp>
+#include <util/path.hpp>
+
 static inline bool
 is_blank(const std::string& s)
 {
@@ -69,17 +72,18 @@ rewrite_paths(const Context& ctx, const std::string& file_content)
   adjusted_file_content.reserve(file_content.size());
 
   bool content_rewritten = false;
-  for (const auto& line : Util::split_into_views(file_content, "\n")) {
+  for (const auto line : util::Tokenizer(
+         file_content, "\n", util::Tokenizer::Mode::skip_last_empty)) {
     const auto tokens = Util::split_into_views(line, " \t");
     for (size_t i = 0; i < tokens.size(); ++i) {
-      DEBUG_ASSERT(line.length() > 0); // line.empty() -> no tokens
+      DEBUG_ASSERT(!line.empty()); // line.empty() -> no tokens
       if (i > 0 || line[0] == ' ' || line[0] == '\t') {
         adjusted_file_content.push_back(' ');
       }
 
       const auto& token = tokens[i];
       bool token_rewritten = false;
-      if (Util::is_absolute_path(token)) {
+      if (util::is_absolute_path(token)) {
         const auto new_path = Util::make_relative_path(ctx, token);
         if (new_path != token) {
           adjusted_file_content.append(new_path);
@@ -120,7 +124,7 @@ make_paths_relative_in_output_dep(const Context& ctx)
   std::string file_content;
   try {
     file_content = Util::read_file(output_dep);
-  } catch (const Error& e) {
+  } catch (const core::Error& e) {
     LOG("Cannot open dependency file {}: {}", output_dep, e.what());
     return;
   }
index 7250a4cb323f9363531fd899a9db8a31aee56169..0507006832fa7baae31d41c568c762fbbd10aa4b 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
index 6a7b53ecca3f22620356022b7a087dd911d84607..189b537692603d66090f676a25f63c54c903d024 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "system.hpp"
-
 #include "Util.hpp"
 
 #include "third_party/fmt/core.h"
 
+#include <cstdint>
 #include <string>
 
 // Digest represents the binary form of the final digest (AKA hash or checksum)
diff --git a/src/Fd.cpp b/src/Fd.cpp
new file mode 100644 (file)
index 0000000..0828258
--- /dev/null
@@ -0,0 +1,31 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "Fd.hpp"
+
+#include <core/wincompat.hpp>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
+bool
+Fd::close()
+{
+  return m_fd != -1 && ::close(release()) == 0;
+}
index 6316e45cedebdac0d48313755b84b106556708be..3a47278c03714b299576e548010d384c64f43567 100644 (file)
@@ -18,8 +18,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 #include "NonCopyable.hpp"
 #include "assertions.hpp"
 
@@ -87,12 +85,6 @@ Fd::operator=(Fd&& other_fd) noexcept
   return *this;
 }
 
-inline bool
-Fd::close()
-{
-  return m_fd != -1 && ::close(release()) == 0;
-}
-
 inline int
 Fd::release()
 {
index f8f6c0be48421ad6bfc83dbc70a77dabf5b2a8de..fe3206f487819e3b11c63633148bcba16d45cc7b 100644 (file)
 
 #pragma once
 
-#include "system.hpp"
-
 #include "NonCopyable.hpp"
 
+#include <cstdio>
 #include <string>
 
 class File : public NonCopyable
index 74f6b781fd816782e6a51b0f99e62e5ee9bc075a..c5d2033c1d45ab2b6aea7713516543e4d2bec29d 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,8 +18,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 #include <functional>
 
 class Finalizer
index 0770a901218194f5605ee0bdb44da493e89e7042..4834dc41e6f49088a714edaf269f8b0eea3c95e1 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,8 +18,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 #include "third_party/fmt/core.h"
 #include "third_party/nonstd/string_view.hpp"
 
index 61cc5a3e734b3fc59bcbb66b689d8e8144b211e5..2eb491dc966d7243951cd20eb700c9018f042b93 100644 (file)
 #include "Logging.hpp"
 #include "fmtmacros.hpp"
 
+#include <core/wincompat.hpp>
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
 using nonstd::string_view;
 
 const string_view HASH_DELIMITER("\000cCaChE\000", 8);
index d2686aff2cdbd81b0e5e2529438a5972fd696228..02c5a4d1227fd4d52cf90018f2cd3f3c998e0664 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "system.hpp"
-
 #include "Digest.hpp"
 
 #include "third_party/blake3/blake3.h"
 #include "third_party/nonstd/string_view.hpp"
 
+#include <cstdint>
+#include <cstdio>
+
 // This class represents a hash state.
 class Hash
 {
index 5e473ec7e8464f7e2c208ae67fe3b9ef20ab872a..815e6aba6bab56df6e5400de17e7c159ff326a24 100644 (file)
@@ -19,6 +19,7 @@
 #include "InodeCache.hpp"
 
 #include "Config.hpp"
+#include "Digest.hpp"
 #include "Fd.hpp"
 #include "Finalizer.hpp"
 #include "Hash.hpp"
 #include "Util.hpp"
 #include "fmtmacros.hpp"
 
-#include <atomic>
+#include <fcntl.h>
 #include <libgen.h>
 #include <sys/mman.h>
+#include <unistd.h>
+
+#include <atomic>
 #include <type_traits>
 
 // The inode cache resides on a file that is mapped into shared memory by
@@ -62,7 +66,7 @@ const uint32_t k_num_entries = 4;
 
 static_assert(Digest::size() == 20,
               "Increment version number if size of digest is changed.");
-static_assert(IS_TRIVIALLY_COPYABLE(Digest),
+static_assert(std::is_trivially_copyable<Digest>::value,
               "Digest is expected to be trivially copyable.");
 
 static_assert(
@@ -369,7 +373,7 @@ InodeCache::get(const std::string& path,
   }
 
   bool found = false;
-  const bool success = with_bucket(key_digest, [&](Bucket* const bucket) {
+  const bool success = with_bucket(key_digest, [&](const auto bucket) {
     for (uint32_t i = 0; i < k_num_entries; ++i) {
       if (bucket->entries[i].key_digest == key_digest) {
         if (i > 0) {
@@ -422,7 +426,7 @@ InodeCache::put(const std::string& path,
     return false;
   }
 
-  const bool success = with_bucket(key_digest, [&](Bucket* const bucket) {
+  const bool success = with_bucket(key_digest, [&](const auto bucket) {
     memmove(&bucket->entries[1],
             &bucket->entries[0],
             sizeof(Entry) * (k_num_entries - 1));
index f2d049abdf5a5da20df78eeb08e930d39998e120..a4a272972cb2086375965f266b71e4a6621734f3 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "system.hpp"
-
-#include "config.h"
-
+#include <cstdint>
 #include <functional>
 #include <string>
 
index fb9d5e409448e06913389b64a57b44bd8fd82774..b1487dab2a049c608db4df3383dac39a7b98d83b 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #include "Logging.hpp"
 #include "Util.hpp"
+#include "Win32Util.hpp"
 #include "fmtmacros.hpp"
 
-#ifdef _WIN32
-#  include "Win32Util.hpp"
-#endif
+#include <core/wincompat.hpp>
 
 #include "third_party/fmt/core.h"
 
+#include <thread>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
 #include <algorithm>
 #include <sstream>
 #include <thread>
@@ -109,7 +114,7 @@ do_acquire_posix(const std::string& lockfile, uint32_t staleness_limit)
       LOG("lockfile_acquire: failed to acquire {}; sleeping {} microseconds",
           lockfile,
           to_sleep);
-      usleep(to_sleep);
+      std::this_thread::sleep_for(std::chrono::microseconds(to_sleep));
       slept += to_sleep;
       to_sleep = std::min(max_to_sleep, 2 * to_sleep);
     } else if (content != initial_content) {
@@ -134,9 +139,9 @@ do_acquire_posix(const std::string& lockfile, uint32_t staleness_limit)
 HANDLE
 do_acquire_win32(const std::string& lockfile, uint32_t staleness_limit)
 {
-  unsigned to_sleep = 1000;      // Microseconds.
-  unsigned max_to_sleep = 10000; // Microseconds.
-  unsigned slept = 0;            // Microseconds.
+  const uint32_t max_to_sleep = 10000; // Microseconds.
+  uint32_t to_sleep = 1000;            // Microseconds.
+  uint32_t slept = 0;                  // Microseconds.
   HANDLE handle;
 
   while (true) {
@@ -181,7 +186,7 @@ do_acquire_win32(const std::string& lockfile, uint32_t staleness_limit)
     LOG("lockfile_acquire: failed to acquire {}; sleeping {} microseconds",
         lockfile,
         to_sleep);
-    usleep(to_sleep);
+    std::this_thread::sleep_for(std::chrono::microseconds(to_sleep));
     slept += to_sleep;
     to_sleep = std::min(max_to_sleep, 2 * to_sleep);
   }
@@ -221,3 +226,13 @@ Lockfile::~Lockfile()
 #endif
   }
 }
+
+bool
+Lockfile::acquired() const
+{
+#ifndef _WIN32
+  return m_acquired;
+#else
+  return m_handle != INVALID_HANDLE_VALUE;
+#endif
+}
index 78baed81e78e9afdba60b311fa6b6109cab43c1d..309c0c4aa39f521027196d552a5a1b83e54cfa82 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,8 +18,7 @@
 
 #pragma once
 
-#include "system.hpp"
-
+#include <cstdint>
 #include <string>
 
 class Lockfile
@@ -40,16 +39,6 @@ private:
 #ifndef _WIN32
   bool m_acquired = false;
 #else
-  HANDLE m_handle = nullptr;
+  void* m_handle = nullptr;
 #endif
 };
-
-inline bool
-Lockfile::acquired() const
-{
-#ifndef _WIN32
-  return m_acquired;
-#else
-  return m_handle != INVALID_HANDLE_VALUE;
-#endif
-}
index c6590d88e7856ea64091362c77ae8586fb53ed4e..5068c853c96f7a4709d13e337470a4264e0731cd 100644 (file)
 #include "Config.hpp"
 #include "File.hpp"
 #include "Util.hpp"
-#include "exceptions.hpp"
+#include "Win32Util.hpp"
 #include "execute.hpp"
 #include "fmtmacros.hpp"
 
+#include <core/wincompat.hpp>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
 #ifdef HAVE_SYSLOG_H
 #  include <syslog.h>
 #endif
 #  endif
 #endif
 
-#ifdef _WIN32
-#  include <psapi.h>
-#  include <sys/locking.h>
-#  include <tchar.h>
-#endif
-
 using nonstd::string_view;
 
 namespace {
@@ -54,7 +54,7 @@ std::string logfile_path;
 File logfile;
 
 // Whether to use syslog() instead.
-bool use_syslog;
+bool use_syslog = false;
 
 // Buffer used for logs in debug mode.
 std::string debug_log_buffer;
index 38553f25f0054374173d7d6697dd7b09611498a3..7ed70faefcc88bc5784fb384aaabfe7859afca49 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,8 +18,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 #include "FormatNonstdStringView.hpp"
 
 #include "third_party/fmt/core.h"
   } while (false)
 
 // Log a message (plus a newline character) described by a format string with at
-// least one placeholder. `format` is compile-time checked if CMAKE_CXX_STANDARD
-// >= 14.
+// least one placeholder. `format` is checked at compile time.
 #define LOG(format_, ...) LOG_RAW(fmt::format(FMT_STRING(format_), __VA_ARGS__))
 
 // Log a message (plus a newline character) described by a format string with at
 // least one placeholder without flushing and with a reused timestamp. `format`
-// is compile-time checked if CMAKE_CXX_STANDARD >= 14.
+// is checked at compile time.
 #define BULK_LOG(format_, ...)                                                 \
   do {                                                                         \
     if (Logging::enabled()) {                                                  \
index 38aec0c4d1d0e3a9cc180e22e824094021b3491c..d5e8a3b55f2babc908ed6550543e7965405873e1 100644 (file)
 #include "File.hpp"
 #include "Hash.hpp"
 #include "Logging.hpp"
-#include "Sloppiness.hpp"
-#include "StdMakeUnique.hpp"
 #include "fmtmacros.hpp"
 #include "hashutil.hpp"
 
+#include <core/exceptions.hpp>
+
+#include <memory>
+
 // Manifest data format
 // ====================
 //
@@ -163,14 +165,14 @@ struct ResultEntry
   // Indexes to file_infos.
   std::vector<uint32_t> file_info_indexes;
 
-  // Name of the result.
-  Digest name;
+  // Key of the result.
+  Digest key;
 };
 
 bool
 operator==(const ResultEntry& lhs, const ResultEntry& rhs)
 {
-  return lhs.file_info_indexes == rhs.file_info_indexes && lhs.name == rhs.name;
+  return lhs.file_info_indexes == rhs.file_info_indexes && lhs.key == rhs.key;
 }
 
 struct ManifestData
@@ -181,12 +183,12 @@ struct ManifestData
   // Information about referenced include files.
   std::vector<FileInfo> file_infos;
 
-  // Result names plus references to include file infos.
+  // Result keys plus references to include file infos.
   std::vector<ResultEntry> results;
 
   bool
   add_result_entry(
-    const Digest& result_digest,
+    const Digest& result_key,
     const std::unordered_map<std::string, Digest>& included_files,
     time_t time_of_compilation,
     bool save_timestamp)
@@ -213,7 +215,7 @@ struct ManifestData
                                                       save_timestamp));
     }
 
-    ResultEntry entry{std::move(file_info_indexes), result_digest};
+    ResultEntry entry{std::move(file_info_indexes), result_key};
     if (std::find(results.begin(), results.end(), entry) == results.end()) {
       results.push_back(std::move(entry));
       return true;
@@ -293,12 +295,19 @@ struct FileStats
 std::unique_ptr<ManifestData>
 read_manifest(const std::string& path, FILE* dump_stream = nullptr)
 {
-  File file(path, "rb");
-  if (!file) {
-    return {};
+  FILE* file_stream;
+  File file;
+  if (path == "-") {
+    file_stream = stdin;
+  } else {
+    file = File(path, "rb");
+    if (!file) {
+      return {};
+    }
+    file_stream = file.get();
   }
 
-  CacheEntryReader reader(file.get(), Manifest::k_magic, Manifest::k_version);
+  CacheEntryReader reader(file_stream, Manifest::k_magic, Manifest::k_version);
 
   if (dump_stream) {
     reader.dump_header(dump_stream);
@@ -342,7 +351,7 @@ read_manifest(const std::string& path, FILE* dump_stream = nullptr)
       reader.read(file_info_index);
       entry.file_info_indexes.push_back(file_info_index);
     }
-    reader.read(entry.name.bytes(), Digest::size());
+    reader.read(entry.key.bytes(), Digest::size());
   }
 
   reader.finalize();
@@ -372,8 +381,8 @@ write_manifest(const Config& config,
   CacheEntryWriter writer(atomic_manifest_file.stream(),
                           Manifest::k_magic,
                           Manifest::k_version,
-                          Compression::type_from_config(config),
-                          Compression::level_from_config(config),
+                          compression::type_from_config(config),
+                          compression::level_from_config(config),
                           payload_size);
   writer.write<uint32_t>(mf.files.size());
   for (const auto& file : mf.files) {
@@ -396,7 +405,7 @@ write_manifest(const Config& config,
     for (auto index : result.file_info_indexes) {
       writer.write(index);
     }
-    writer.write(result.name.bytes(), Digest::size());
+    writer.write(result.key.bytes(), Digest::size());
   }
 
   writer.finalize();
@@ -443,8 +452,9 @@ verify_result(const Context& ctx,
       return false;
     }
 
-    if (ctx.config.sloppiness() & SLOPPY_FILE_STAT_MATCHES) {
-      if (!(ctx.config.sloppiness() & SLOPPY_FILE_STAT_MATCHES_CTIME)) {
+    if (ctx.config.sloppiness().is_enabled(core::Sloppy::file_stat_matches)) {
+      if (!(ctx.config.sloppiness().is_enabled(
+            core::Sloppy::file_stat_matches_ctime))) {
         if (fi.mtime == fs.mtime && fi.ctime == fs.ctime) {
           LOG("mtime/ctime hit for {}", path);
           continue;
@@ -493,21 +503,18 @@ const std::string k_file_suffix = "M";
 const uint8_t k_magic[4] = {'c', 'C', 'm', 'F'};
 const uint8_t k_version = 2;
 
-// Try to get the result name from a manifest file. Returns nullopt on failure.
+// Try to get the result key from a manifest file. Returns nullopt on failure.
 optional<Digest>
 get(const Context& ctx, const std::string& path)
 {
   std::unique_ptr<ManifestData> mf;
   try {
     mf = read_manifest(path);
-    if (mf) {
-      // Update modification timestamp to save files from LRU cleanup.
-      Util::update_mtime(path);
-    } else {
+    if (!mf) {
       LOG_RAW("No such manifest file");
       return nullopt;
     }
-  } catch (const Error& e) {
+  } catch (const core::Error& e) {
     LOG("Error: {}", e.what());
     return nullopt;
   }
@@ -519,19 +526,19 @@ get(const Context& ctx, const std::string& path)
   for (uint32_t i = mf->results.size(); i > 0; i--) {
     if (verify_result(
           ctx, *mf, mf->results[i - 1], stated_files, hashed_files)) {
-      return mf->results[i - 1].name;
+      return mf->results[i - 1].key;
     }
   }
 
   return nullopt;
 }
 
-// Put the result name into a manifest file given a set of included files.
+// Put the result key into a manifest file given a set of included files.
 // Returns true on success, otherwise false.
 bool
 put(const Config& config,
     const std::string& path,
-    const Digest& result_name,
+    const Digest& result_key,
     const std::unordered_map<std::string, Digest>& included_files,
 
     time_t time_of_compilation,
@@ -548,7 +555,7 @@ put(const Config& config,
       // Manifest file didn't exist.
       mf = std::make_unique<ManifestData>();
     }
-  } catch (const Error& e) {
+  } catch (const core::Error& e) {
     LOG("Error: {}", e.what());
     // Manifest file was corrupt, ignore.
     mf = std::make_unique<ManifestData>();
@@ -578,13 +585,13 @@ put(const Config& config,
   }
 
   bool added = mf->add_result_entry(
-    result_name, included_files, time_of_compilation, save_timestamp);
+    result_key, included_files, time_of_compilation, save_timestamp);
 
   if (added) {
     try {
       write_manifest(config, path, *mf);
       return true;
-    } catch (const Error& e) {
+    } catch (const core::Error& e) {
       LOG("Error: {}", e.what());
     }
   } else {
@@ -599,7 +606,7 @@ dump(const std::string& path, FILE* stream)
   std::unique_ptr<ManifestData> mf;
   try {
     mf = read_manifest(path, stream);
-  } catch (const Error& e) {
+  } catch (const core::Error& e) {
     PRINT(stream, "Error: {}\n", e.what());
     return false;
   }
@@ -630,7 +637,7 @@ dump(const std::string& path, FILE* stream)
       PRINT(stream, " {}", file_info_index);
     }
     PRINT_RAW(stream, "\n");
-    PRINT(stream, "    Name: {}\n", mf->results[i].name.to_string());
+    PRINT(stream, "    Key: {}\n", mf->results[i].key.to_string());
   }
 
   return true;
index fda77588fb3dd7ee9aecc1619046bcc0a3e4cd53..0230c707a85b8fe9fb5e369dae95bdbaec71f8ec 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2009-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2009-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "system.hpp"
-
 #include "third_party/nonstd/optional.hpp"
 
+#include <cstdint>
+#include <cstdio>
+#include <ctime>
 #include <string>
 #include <unordered_map>
 
@@ -38,7 +39,7 @@ extern const uint8_t k_version;
 nonstd::optional<Digest> get(const Context& ctx, const std::string& path);
 bool put(const Config& config,
          const std::string& path,
-         const Digest& result_name,
+         const Digest& result_key,
          const std::unordered_map<std::string, Digest>& included_files,
          time_t time_of_compilation,
          bool save_timestamp);
index 0516562f1f0987fa5f6d35770cadd6f8e92c5ca2..07f495ad7b31a527d213b805fe3c07a3e7419ad6 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 // this program; if not, write to the Free Software Foundation, Inc., 51
 // Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-#include "system.hpp"
+#include "MiniTrace.hpp"
 
-#ifdef MTR_ENABLED
+#include "ArgsInfo.hpp"
+#include "TemporaryFile.hpp"
+#include "Util.hpp"
+#include "fmtmacros.hpp"
 
-#  include "ArgsInfo.hpp"
-#  include "MiniTrace.hpp"
-#  include "TemporaryFile.hpp"
-#  include "Util.hpp"
-#  include "fmtmacros.hpp"
+#include <core/wincompat.hpp>
 
-#  ifdef HAVE_SYS_TIME_H
-#    include <sys/time.h>
-#  endif
+#ifdef HAVE_SYS_TIME_H
+#  include <sys/time.h>
+#endif
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
 
 namespace {
 
 std::string
 get_system_tmp_dir()
 {
-#  ifndef _WIN32
+#ifndef _WIN32
   const char* tmpdir = getenv("TMPDIR");
   if (tmpdir) {
     return tmpdir;
   }
-#  else
+#else
   static char dirbuf[PATH_MAX];
   DWORD retval = GetTempPath(PATH_MAX, dirbuf);
   if (retval > 0 && retval < PATH_MAX) {
     return dirbuf;
   }
-#  endif
+#endif
   return "/tmp";
 }
 
 double
 time_seconds()
 {
-#  ifdef HAVE_GETTIMEOFDAY
+#ifdef HAVE_GETTIMEOFDAY
   struct timeval tv;
   gettimeofday(&tv, nullptr);
   return (double)tv.tv_sec + (double)tv.tv_usec / 1000000.0;
-#  else
+#else
   return (double)time(nullptr);
-#  endif
+#endif
 }
 
 } // namespace
@@ -72,7 +75,8 @@ MiniTrace::MiniTrace(const ArgsInfo& args_info)
   m_tmp_trace_file = tmp_file.path;
 
   mtr_init(m_tmp_trace_file.c_str());
-  MTR_INSTANT_C("", "", "time", FMT("{:f}", time_seconds()).c_str());
+  m_start_time = FMT("{:f}", time_seconds());
+  MTR_INSTANT_C("", "", "time", m_start_time.c_str());
   MTR_META_PROCESS_NAME("ccache");
   MTR_START("program", "ccache", m_trace_id);
 }
@@ -88,5 +92,3 @@ MiniTrace::~MiniTrace()
   }
   Util::unlink_tmp(m_tmp_trace_file);
 }
-
-#endif
index 3b44360d4e1b7e1bfb02c2b8eb8e60e4ea108cae..76aae38105051b107f409ccc134f453b3eaba4b7 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "system.hpp"
-
 #include "third_party/minitrace.h"
 
-#ifdef MTR_ENABLED
-
-#  include <string>
+#include <string>
 
 struct ArgsInfo;
 
@@ -38,6 +34,5 @@ private:
   const ArgsInfo& m_args_info;
   const void* const m_trace_id;
   std::string m_tmp_trace_file;
+  std::string m_start_time;
 };
-
-#endif
diff --git a/src/NullCompressor.cpp b/src/NullCompressor.cpp
deleted file mode 100644 (file)
index 13494fc..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "NullCompressor.hpp"
-
-#include "exceptions.hpp"
-
-NullCompressor::NullCompressor(FILE* stream) : m_stream(stream)
-{
-}
-
-int8_t
-NullCompressor::actual_compression_level() const
-{
-  return 0;
-}
-
-void
-NullCompressor::write(const void* data, size_t count)
-{
-  if (fwrite(data, 1, count, m_stream) != count) {
-    throw Error("failed to write to uncompressed stream");
-  }
-}
-
-void
-NullCompressor::finalize()
-{
-  if (fflush(m_stream) != 0) {
-    throw Error("failed to finalize uncompressed stream");
-  }
-}
diff --git a/src/NullCompressor.hpp b/src/NullCompressor.hpp
deleted file mode 100644 (file)
index 9a4fcd1..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2019 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include "system.hpp"
-
-#include "Compressor.hpp"
-#include "NonCopyable.hpp"
-
-// A compressor of an uncompressed stream.
-class NullCompressor : public Compressor, NonCopyable
-{
-public:
-  // Parameters:
-  // - stream: The file to write data to.
-  explicit NullCompressor(FILE* stream);
-
-  int8_t actual_compression_level() const override;
-  void write(const void* data, size_t count) override;
-  void finalize() override;
-
-private:
-  FILE* m_stream;
-};
diff --git a/src/NullDecompressor.cpp b/src/NullDecompressor.cpp
deleted file mode 100644 (file)
index 090bdf5..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2019 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "NullDecompressor.hpp"
-
-#include "exceptions.hpp"
-
-NullDecompressor::NullDecompressor(FILE* stream) : m_stream(stream)
-{
-}
-
-void
-NullDecompressor::read(void* data, size_t count)
-{
-  if (fread(data, count, 1, m_stream) != 1) {
-    throw Error("failed to read from uncompressed stream");
-  }
-}
-
-void
-NullDecompressor::finalize()
-{
-  if (fgetc(m_stream) != EOF) {
-    throw Error("garbage data at end of uncompressed stream");
-  }
-}
diff --git a/src/NullDecompressor.hpp b/src/NullDecompressor.hpp
deleted file mode 100644 (file)
index 514ed7d..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (C) 2019 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include "system.hpp"
-
-#include "Decompressor.hpp"
-#include "NonCopyable.hpp"
-
-// A decompressor of an uncompressed stream.
-class NullDecompressor : public Decompressor, NonCopyable
-{
-public:
-  // Parameters:
-  // - stream: The file to read data from.
-  explicit NullDecompressor(FILE* stream);
-
-  void read(void* data, size_t count) override;
-  void finalize() override;
-
-private:
-  FILE* m_stream;
-};
index c7b219e02de40781582658b473309a58c2f96a17..99a37a088cc1dc4ada238ce65290f36dbb7b72f9 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #include "fmtmacros.hpp"
 
+#include <core/wincompat.hpp>
+
 #include "third_party/fmt/core.h"
 
-#ifndef _WIN32
+#ifdef _WIN32
+#else
 #  include <sys/ioctl.h>
 #endif
 
 #  include <termios.h>
 #endif
 
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
 #include <algorithm>
 
 namespace {
index 8aefa897d4e6170d6419c8ca92dbdf16f918531d..1be38020aa645b28e6976576e66e59e8bc667a0f 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,8 +18,8 @@
 
 #pragma once
 
-#include "system.hpp"
-
+#include <cstddef>
+#include <cstdint>
 #include <string>
 
 class ProgressBar
index 8408d2a7b7fd3037e8caf45fd853795e8697f638..202ce820cbf7119d4229a171fc788b21d72d4e9b 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 #include "File.hpp"
 #include "Logging.hpp"
 #include "Stat.hpp"
-#include "Statistic.hpp"
 #include "Util.hpp"
-#include "exceptions.hpp"
 #include "fmtmacros.hpp"
 
+#include <core/Statistic.hpp>
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+#include <util/path.hpp>
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
 #include <algorithm>
 
 // Result data format
@@ -182,7 +193,7 @@ gcno_file_in_mangled_form(const Context& ctx)
 {
   const auto& output_obj = ctx.args_info.output_obj;
   const std::string abs_output_obj =
-    Util::is_absolute_path(output_obj)
+    util::is_absolute_path(output_obj)
       ? output_obj
       : FMT("{}/{}", ctx.apparent_cwd, output_obj);
   std::string hashified_obj = abs_output_obj;
@@ -196,6 +207,14 @@ gcno_file_in_unmangled_form(const Context& ctx)
   return Util::change_extension(ctx.args_info.output_obj, ".gcno");
 }
 
+FileSizeAndCountDiff&
+FileSizeAndCountDiff::operator+=(const FileSizeAndCountDiff& other)
+{
+  size_kibibyte += other.size_kibibyte;
+  count += other.count;
+  return *this;
+}
+
 Result::Reader::Reader(const std::string& result_path)
   : m_result_path(result_path)
 {
@@ -212,7 +231,7 @@ Result::Reader::read(Consumer& consumer)
     } else {
       return "No such result file";
     }
-  } catch (const Error& e) {
+  } catch (const core::Error& e) {
     return e.what();
   }
 }
@@ -220,13 +239,20 @@ Result::Reader::read(Consumer& consumer)
 bool
 Reader::read_result(Consumer& consumer)
 {
-  File file(m_result_path, "rb");
-  if (!file) {
-    // Cache miss.
-    return false;
+  FILE* file_stream;
+  File file;
+  if (m_result_path == "-") {
+    file_stream = stdin;
+  } else {
+    file = File(m_result_path, "rb");
+    if (!file) {
+      // Cache miss.
+      return false;
+    }
+    file_stream = file.get();
   }
 
-  CacheEntryReader cache_entry_reader(file.get(), k_magic, k_version);
+  CacheEntryReader cache_entry_reader(file_stream, k_magic, k_version);
 
   consumer.on_header(cache_entry_reader);
 
@@ -239,7 +265,7 @@ Reader::read_result(Consumer& consumer)
   }
 
   if (i != n_entries) {
-    throw Error("Too few entries (read {}, expected {})", i, n_entries);
+    throw core::Error("Too few entries (read {}, expected {})", i, n_entries);
   }
 
   cache_entry_reader.finalize();
@@ -260,7 +286,7 @@ Reader::read_entry(CacheEntryReader& cache_entry_reader,
     break;
 
   default:
-    throw Error("Unknown entry type: {}", marker);
+    throw core::Error("Unknown entry type: {}", marker);
   }
 
   UnderlyingFileTypeInt type;
@@ -273,7 +299,7 @@ Reader::read_entry(CacheEntryReader& cache_entry_reader,
   if (marker == k_embedded_file_marker) {
     consumer.on_entry_start(entry_number, file_type, file_len, nullopt);
 
-    uint8_t buf[READ_BUFFER_SIZE];
+    uint8_t buf[CCACHE_READ_BUFFER_SIZE];
     size_t remain = file_len;
     while (remain > 0) {
       size_t n = std::min(remain, sizeof(buf));
@@ -287,10 +313,11 @@ Reader::read_entry(CacheEntryReader& cache_entry_reader,
     auto raw_path = get_raw_file_path(m_result_path, entry_number);
     auto st = Stat::stat(raw_path, Stat::OnError::throw_error);
     if (st.size() != file_len) {
-      throw Error("Bad file size of {} (actual {} bytes, expected {} bytes)",
-                  raw_path,
-                  st.size(),
-                  file_len);
+      throw core::Error(
+        "Bad file size of {} (actual {} bytes, expected {} bytes)",
+        raw_path,
+        st.size(),
+        file_len);
     }
 
     consumer.on_entry_start(entry_number, file_type, file_len, raw_path);
@@ -311,20 +338,20 @@ Writer::write(FileType file_type, const std::string& file_path)
   m_entries_to_write.emplace_back(file_type, file_path);
 }
 
-optional<std::string>
+nonstd::expected<FileSizeAndCountDiff, std::string>
 Writer::finalize()
 {
   try {
-    do_finalize();
-    return nullopt;
-  } catch (const Error& e) {
-    return e.what();
+    return do_finalize();
+  } catch (const core::Error& e) {
+    return nonstd::make_unexpected(e.what());
   }
 }
 
-void
+FileSizeAndCountDiff
 Writer::do_finalize()
 {
+  FileSizeAndCountDiff file_size_and_count_diff{0, 0};
   uint64_t payload_size = 0;
   payload_size += 1; // n_entries
   for (const auto& pair : m_entries_to_write) {
@@ -341,8 +368,8 @@ Writer::do_finalize()
   CacheEntryWriter writer(atomic_result_file.stream(),
                           k_magic,
                           k_version,
-                          Compression::type_from_config(m_ctx.config),
-                          Compression::level_from_config(m_ctx.config),
+                          compression::type_from_config(m_ctx.config),
+                          compression::level_from_config(m_ctx.config),
                           payload_size);
 
   writer.write<uint8_t>(m_entries_to_write.size());
@@ -351,7 +378,7 @@ Writer::do_finalize()
   for (const auto& pair : m_entries_to_write) {
     const auto file_type = pair.first;
     const auto& path = pair.second;
-    LOG("Storing result {}", path);
+    LOG("Storing result file {}", path);
 
     const bool store_raw = should_store_raw_file(m_ctx.config, file_type);
     uint64_t file_size = Stat::stat(path, Stat::OnError::throw_error).size();
@@ -369,7 +396,7 @@ Writer::do_finalize()
     writer.write(file_size);
 
     if (store_raw) {
-      write_raw_file_entry(path, entry_number);
+      file_size_and_count_diff += write_raw_file_entry(path, entry_number);
     } else {
       write_embedded_file_entry(writer, path, file_size);
     }
@@ -379,6 +406,8 @@ Writer::do_finalize()
 
   writer.finalize();
   atomic_result_file.commit();
+
+  return file_size_and_count_diff;
 }
 
 void
@@ -388,29 +417,29 @@ Result::Writer::write_embedded_file_entry(CacheEntryWriter& writer,
 {
   Fd file(open(path.c_str(), O_RDONLY | O_BINARY));
   if (!file) {
-    throw Error("Failed to open {} for reading", path);
+    throw core::Error("Failed to open {} for reading", path);
   }
 
   uint64_t remain = file_size;
   while (remain > 0) {
-    uint8_t buf[READ_BUFFER_SIZE];
+    uint8_t buf[CCACHE_READ_BUFFER_SIZE];
     size_t n = std::min(remain, static_cast<uint64_t>(sizeof(buf)));
-    ssize_t bytes_read = read(*file, buf, n);
+    auto bytes_read = read(*file, buf, n);
     if (bytes_read == -1) {
       if (errno == EINTR) {
         continue;
       }
-      throw Error("Error reading from {}: {}", path, strerror(errno));
+      throw core::Error("Error reading from {}: {}", path, strerror(errno));
     }
     if (bytes_read == 0) {
-      throw Error("Error reading from {}: end of file", path);
+      throw core::Error("Error reading from {}: end of file", path);
     }
     writer.write(buf, bytes_read);
     remain -= bytes_read;
   }
 }
 
-void
+FileSizeAndCountDiff
 Result::Writer::write_raw_file_entry(const std::string& path,
                                      uint32_t entry_number)
 {
@@ -418,16 +447,15 @@ Result::Writer::write_raw_file_entry(const std::string& path,
   const auto old_stat = Stat::stat(raw_file);
   try {
     Util::clone_hard_link_or_copy_file(m_ctx, path, raw_file, true);
-  } catch (Error& e) {
-    throw Error(
+  } catch (core::Error& e) {
+    throw core::Error(
       "Failed to store {} as raw file {}: {}", path, raw_file, e.what());
   }
   const auto new_stat = Stat::stat(raw_file);
-  m_ctx.counter_updates.increment(
-    Statistic::cache_size_kibibyte,
-    Util::size_change_kibibyte(old_stat, new_stat));
-  m_ctx.counter_updates.increment(Statistic::files_in_cache,
-                                  (new_stat ? 1 : 0) - (old_stat ? 1 : 0));
+  return {
+    Util::size_change_kibibyte(old_stat, new_stat),
+    (new_stat ? 1 : 0) - (old_stat ? 1 : 0),
+  };
 }
 
 } // namespace Result
index 01068c9e6f06e25d3fccc249038b85c8df17177d..994322dd4cd709486ac88136a1b00f5f39fe6b9d 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "system.hpp"
-
+#include "third_party/nonstd/expected.hpp"
 #include "third_party/nonstd/optional.hpp"
 
+#include <cstdint>
 #include <map>
 #include <string>
 #include <vector>
@@ -64,7 +64,7 @@ enum class FileType : UnderlyingFileTypeInt {
   // Diagnostics output file specified by --serialize-diagnostics.
   diagnostic = 5,
 
-  // DWARF object file geenrated by -gsplit-dwarf, i.e. output file but with a
+  // DWARF object file generated by -gsplit-dwarf, i.e. output file but with a
   // .dwo extension.
   dwarf_object = 6,
 
@@ -79,6 +79,14 @@ const char* file_type_to_string(FileType type);
 std::string gcno_file_in_mangled_form(const Context& ctx);
 std::string gcno_file_in_unmangled_form(const Context& ctx);
 
+struct FileSizeAndCountDiff
+{
+  int64_t size_kibibyte;
+  int64_t count;
+
+  FileSizeAndCountDiff& operator+=(const FileSizeAndCountDiff& other);
+};
+
 // This class knows how to read a result cache entry.
 class Reader
 {
@@ -121,18 +129,19 @@ public:
   void write(FileType file_type, const std::string& file_path);
 
   // Write registered files to the result. Returns an error message on error.
-  nonstd::optional<std::string> finalize();
+  nonstd::expected<FileSizeAndCountDiff, std::string> finalize();
 
 private:
   Context& m_ctx;
   const std::string m_result_path;
   std::vector<std::pair<FileType, std::string>> m_entries_to_write;
 
-  void do_finalize();
+  FileSizeAndCountDiff do_finalize();
   static void write_embedded_file_entry(CacheEntryWriter& writer,
                                         const std::string& path,
                                         uint64_t file_size);
-  void write_raw_file_entry(const std::string& path, uint32_t entry_number);
+  FileSizeAndCountDiff write_raw_file_entry(const std::string& path,
+                                            uint32_t entry_number);
 };
 
 } // namespace Result
index 85602d61fcc79e2fd216a20d6ba10035cb43e41d..c48d2ab59c048da404c981f85501f91e9aa8ed72 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "system.hpp"
-
 #include "Result.hpp"
 
+#include <cstdint>
+#include <cstdio>
+
 // This class dumps information about the result entry to `stream`.
 class ResultDumper : public Result::Reader::Consumer
 {
index e18ae516018132adba68b9a212596e8c0dac3cc3..077f324426f084152e0bb1dfce69d9747b67478d 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 #include "Util.hpp"
 #include "fmtmacros.hpp"
 
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
 ResultExtractor::ResultExtractor(const std::string& directory)
   : m_directory(directory)
 {
@@ -51,14 +58,14 @@ ResultExtractor::on_entry_start(uint32_t /*entry_number*/,
     m_dest_fd = Fd(
       open(m_dest_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666));
     if (!m_dest_fd) {
-      throw Error(
+      throw core::Error(
         "Failed to open {} for writing: {}", m_dest_path, strerror(errno));
     }
   } else {
     try {
       Util::copy_file(*raw_file, m_dest_path, false);
-    } catch (Error& e) {
-      throw Error(
+    } catch (core::Error& e) {
+      throw core::Error(
         "Failed to copy {} to {}: {}", *raw_file, m_dest_path, e.what());
     }
   }
@@ -71,8 +78,8 @@ ResultExtractor::on_entry_data(const uint8_t* data, size_t size)
 
   try {
     Util::write_fd(*m_dest_fd, data, size);
-  } catch (Error& e) {
-    throw Error("Failed to write to {}: {}", m_dest_path, e.what());
+  } catch (core::Error& e) {
+    throw core::Error("Failed to write to {}: {}", m_dest_path, e.what());
   }
 }
 
index f4b8f3beda8ea44c23bc82f5b0205c547428e44f..0e801ff4a0e87aefcee4bc6c2f0a8a78e53976af 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,8 +18,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 #include "Fd.hpp"
 #include "Result.hpp"
 
index 0218877c3ab801e65e0f102712a16cc3bea638bf..06dab86866dfa7c888f8b6ce6a67b4b72b4262e4 100644 (file)
 #include "Depfile.hpp"
 #include "Logging.hpp"
 
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
 using Result::FileType;
 
 ResultRetriever::ResultRetriever(Context& ctx, bool rewrite_dependency_target)
@@ -119,7 +130,7 @@ ResultRetriever::on_entry_start(uint32_t entry_number,
     m_dest_fd = Fd(
       open(dest_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666));
     if (!m_dest_fd) {
-      throw Error(
+      throw core::Error(
         "Failed to open {} for writing: {}", dest_path, strerror(errno));
     }
     m_dest_path = dest_path;
@@ -137,8 +148,8 @@ ResultRetriever::on_entry_data(const uint8_t* data, size_t size)
   } else if (m_dest_fd) {
     try {
       Util::write_fd(*m_dest_fd, data, size);
-    } catch (Error& e) {
-      throw Error("Failed to write to {}: {}", m_dest_path, e.what());
+    } catch (core::Error& e) {
+      throw core::Error("Failed to write to {}: {}", m_dest_path, e.what());
     }
   }
 }
@@ -179,7 +190,7 @@ ResultRetriever::write_dependency_file()
     Util::write_fd(*m_dest_fd,
                    m_dest_data.data() + start_pos,
                    m_dest_data.length() - start_pos);
-  } catch (Error& e) {
-    throw Error("Failed to write to {}: {}", m_dest_path, e.what());
+  } catch (core::Error& e) {
+    throw core::Error("Failed to write to {}: {}", m_dest_path, e.what());
   }
 }
index 609f15f91a60f193937af31d703cb00d012c155f..4794afdef27364f67b6bdf280f7ae4bcf37adfab 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,8 +18,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 #include "Fd.hpp"
 #include "Result.hpp"
 
@@ -41,7 +39,7 @@ public:
 
 private:
   Context& m_ctx;
-  Result::FileType m_dest_file_type;
+  Result::FileType m_dest_file_type{};
   Fd m_dest_fd;
   std::string m_dest_path;
 
index 8a07cc7e1a12a11f55564fbc854a00189992f590..9051eeb0dffb432beb48ab98d7efe00b19b9c274 100644 (file)
@@ -24,6 +24,9 @@
 #  include "assertions.hpp"
 
 #  include <signal.h> // NOLINT: sigaddset et al are defined in signal.h
+#  include <sys/types.h>
+#  include <sys/wait.h>
+#  include <unistd.h>
 
 namespace {
 
index 3ef88479dcc1cb24d71065fda28fd14be069521e..603dbe213b60e2ce3879a4f4827659a490b1fd78 100644 (file)
@@ -18,8 +18,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 class Context;
 
 class SignalHandler
diff --git a/src/Sloppiness.hpp b/src/Sloppiness.hpp
deleted file mode 100644 (file)
index 62c0508..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2021 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-enum Sloppiness {
-  SLOPPY_INCLUDE_FILE_MTIME = 1 << 0,
-  SLOPPY_INCLUDE_FILE_CTIME = 1 << 1,
-  SLOPPY_TIME_MACROS = 1 << 2,
-  SLOPPY_PCH_DEFINES = 1 << 3,
-  // Allow us to match files based on their stats (size, mtime, ctime), without
-  // looking at their contents.
-  SLOPPY_FILE_STAT_MATCHES = 1 << 4,
-  // Allow us to not include any system headers in the manifest include files,
-  // similar to -MM versus -M for dependencies.
-  SLOPPY_SYSTEM_HEADERS = 1 << 5,
-  // Allow us to ignore ctimes when comparing file stats, so we can fake mtimes
-  // if we want to (it is much harder to fake ctimes, requires changing clock)
-  SLOPPY_FILE_STAT_MATCHES_CTIME = 1 << 6,
-  // Allow us to not include the -index-store-path option in the manifest hash.
-  SLOPPY_CLANG_INDEX_STORE = 1 << 7,
-  // Ignore locale settings.
-  SLOPPY_LOCALE = 1 << 8,
-  // Allow caching even if -fmodules is used.
-  SLOPPY_MODULES = 1 << 9,
-  // Ignore virtual file system (VFS) overlay file.
-  SLOPPY_IVFSOVERLAY = 1 << 10,
-};
index 244fd6f5a911f9c33308fecd63c357846b958fd7..6e8d44eb9acc46451a4bfc3fe4b4f6c96f2aa923 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #include "Stat.hpp"
 
-#ifdef _WIN32
-#  include "Win32Util.hpp"
-
-#  include "third_party/win32/winerror_to_errno.h"
-#endif
-
 #include "Finalizer.hpp"
 #include "Logging.hpp"
+#include "Win32Util.hpp"
+
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+
+#ifdef _WIN32
+#  include <third_party/win32/winerror_to_errno.h>
+#endif
 
 namespace {
 
@@ -210,7 +212,7 @@ Stat::Stat(StatFunction stat_function,
   } else {
     m_errno = errno;
     if (on_error == OnError::throw_error) {
-      throw Error("failed to stat {}: {}", path, strerror(errno));
+      throw core::Error("failed to stat {}: {}", path, strerror(errno));
     }
     if (on_error == OnError::log) {
       LOG("Failed to stat {}: {}", path, strerror(errno));
index 5a046d2ab82503ebddfa502d7639025a750c7149..2f56214a53d8df4b4ba52312fefab8c2a7a1f430 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "system.hpp"
+#include <core/wincompat.hpp>
 
-#include "exceptions.hpp"
+#include <sys/stat.h>
+#include <sys/types.h>
 
+#include <ctime>
 #include <string>
 
+#ifdef _WIN32
+#  ifndef S_IFIFO
+#    define S_IFIFO 0x1000
+#  endif
+#  ifndef S_IFBLK
+#    define S_IFBLK 0x6000
+#  endif
+#  ifndef S_IFLNK
+#    define S_IFLNK 0xA000
+#  endif
+#  ifndef S_ISREG
+#    define S_ISREG(m) (((m)&S_IFMT) == S_IFREG)
+#  endif
+#  ifndef S_ISDIR
+#    define S_ISDIR(m) (((m)&S_IFMT) == S_IFDIR)
+#  endif
+#  ifndef S_ISFIFO
+#    define S_ISFIFO(m) (((m)&S_IFMT) == S_IFIFO)
+#  endif
+#  ifndef S_ISCHR
+#    define S_ISCHR(m) (((m)&S_IFMT) == S_IFCHR)
+#  endif
+#  ifndef S_ISLNK
+#    define S_ISLNK(m) (((m)&S_IFMT) == S_IFLNK)
+#  endif
+#  ifndef S_ISBLK
+#    define S_ISBLK(m) (((m)&S_IFMT) == S_IFBLK)
+#  endif
+#endif
+
 class Stat
 {
 public:
@@ -94,6 +126,7 @@ public:
   dev_t device() const;
   ino_t inode() const;
   mode_t mode() const;
+  time_t atime() const;
   time_t ctime() const;
   time_t mtime() const;
   uint64_t size() const;
@@ -109,6 +142,7 @@ public:
   uint32_t reparse_tag() const;
 #endif
 
+  timespec atim() const;
   timespec ctim() const;
   timespec mtim() const;
 
@@ -164,6 +198,12 @@ Stat::mode() const
   return m_stat.st_mode;
 }
 
+inline time_t
+Stat::atime() const
+{
+  return atim().tv_sec;
+}
+
 inline time_t
 Stat::ctime() const
 {
@@ -224,6 +264,16 @@ Stat::reparse_tag() const
 }
 #endif
 
+inline timespec
+Stat::atim() const
+{
+#if defined(_WIN32) || defined(HAVE_STRUCT_STAT_ST_ATIM)
+  return m_stat.st_atim;
+#else
+  return {m_stat.st_atime, 0};
+#endif
+}
+
 inline timespec
 Stat::ctim() const
 {
diff --git a/src/Statistic.hpp b/src/Statistic.hpp
deleted file mode 100644 (file)
index cd6cda6..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (C) 2021 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-// Statistics fields in storage order.
-enum class Statistic {
-  none = 0,
-  compiler_produced_stdout = 1,
-  compile_failed = 2,
-  internal_error = 3,
-  cache_miss = 4,
-  preprocessor_error = 5,
-  could_not_find_compiler = 6,
-  missing_cache_file = 7,
-  preprocessed_cache_hit = 8,
-  bad_compiler_arguments = 9,
-  called_for_link = 10,
-  files_in_cache = 11,
-  cache_size_kibibyte = 12,
-  obsolete_max_files = 13,
-  obsolete_max_size = 14,
-  unsupported_source_language = 15,
-  bad_output_file = 16,
-  no_input_file = 17,
-  multiple_source_files = 18,
-  autoconf_test = 19,
-  unsupported_compiler_option = 20,
-  output_to_stdout = 21,
-  direct_cache_hit = 22,
-  compiler_produced_no_output = 23,
-  compiler_produced_empty_output = 24,
-  error_hashing_extra_file = 25,
-  compiler_check_failed = 26,
-  could_not_use_precompiled_header = 27,
-  called_for_preprocessing = 28,
-  cleanups_performed = 29,
-  unsupported_code_directive = 30,
-  stats_zeroed_timestamp = 31,
-  could_not_use_modules = 32,
-
-  END
-};
diff --git a/src/Statistics.cpp b/src/Statistics.cpp
deleted file mode 100644 (file)
index 08ad892..0000000
+++ /dev/null
@@ -1,355 +0,0 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "Statistics.hpp"
-
-#include "AtomicFile.hpp"
-#include "Config.hpp"
-#include "Lockfile.hpp"
-#include "Logging.hpp"
-#include "Util.hpp"
-#include "exceptions.hpp"
-#include "fmtmacros.hpp"
-
-const unsigned FLAG_NOZERO = 1; // don't zero with the -z option
-const unsigned FLAG_ALWAYS = 2; // always show, even if zero
-const unsigned FLAG_NEVER = 4;  // never show
-
-using nonstd::nullopt;
-using nonstd::optional;
-
-// Returns a formatted version of a statistics value, or the empty string if the
-// statistics line shouldn't be printed.
-using FormatFunction = std::string (*)(uint64_t value);
-
-static std::string
-format_size(uint64_t size)
-{
-  return FMT("{:>11}", Util::format_human_readable_size(size));
-}
-
-static std::string
-format_size_times_1024(uint64_t size)
-{
-  return format_size(size * 1024);
-}
-
-static std::string
-format_timestamp(uint64_t timestamp)
-{
-  if (timestamp > 0) {
-    const auto tm = Util::localtime(timestamp);
-    char buffer[100] = "?";
-    if (tm) {
-      strftime(buffer, sizeof(buffer), "%c", &*tm);
-    }
-    return std::string("    ") + buffer;
-  } else {
-    return {};
-  }
-}
-
-static double
-hit_rate(const Counters& counters)
-{
-  const uint64_t direct = counters.get(Statistic::direct_cache_hit);
-  const uint64_t preprocessed = counters.get(Statistic::preprocessed_cache_hit);
-  const uint64_t hit = direct + preprocessed;
-  const uint64_t miss = counters.get(Statistic::cache_miss);
-  const uint64_t total = hit + miss;
-  return total > 0 ? (100.0 * hit) / total : 0.0;
-}
-
-static void
-for_each_level_1_and_2_stats_file(
-  const std::string& cache_dir,
-  const std::function<void(const std::string& path)> function)
-{
-  for (size_t level_1 = 0; level_1 <= 0xF; ++level_1) {
-    function(FMT("{}/{:x}/stats", cache_dir, level_1));
-    for (size_t level_2 = 0; level_2 <= 0xF; ++level_2) {
-      function(FMT("{}/{:x}/{:x}/stats", cache_dir, level_1, level_2));
-    }
-  }
-}
-
-static std::pair<Counters, time_t>
-collect_counters(const Config& config)
-{
-  Counters counters;
-  uint64_t zero_timestamp = 0;
-  time_t last_updated = 0;
-
-  // Add up the stats in each directory.
-  for_each_level_1_and_2_stats_file(
-    config.cache_dir(), [&](const std::string& path) {
-      counters.set(Statistic::stats_zeroed_timestamp, 0); // Don't add
-      counters.increment(Statistics::read(path));
-      zero_timestamp = std::max(counters.get(Statistic::stats_zeroed_timestamp),
-                                zero_timestamp);
-      last_updated = std::max(last_updated, Stat::stat(path).mtime());
-    });
-
-  counters.set(Statistic::stats_zeroed_timestamp, zero_timestamp);
-  return std::make_pair(counters, last_updated);
-}
-
-namespace {
-
-struct StatisticsField
-{
-  StatisticsField(Statistic statistic_,
-                  const char* id_,
-                  const char* message_,
-                  unsigned flags_ = 0,
-                  FormatFunction format_ = nullptr)
-    : statistic(statistic_),
-      id(id_),
-      message(message_),
-      flags(flags_),
-      format(format_)
-  {
-  }
-
-  const Statistic statistic;
-  const char* const id;        // for --print-stats
-  const char* const message;   // for --show-stats
-  const unsigned flags;        // bitmask of FLAG_* values
-  const FormatFunction format; // nullptr -> use plain integer format
-};
-
-} // namespace
-
-#define STATISTICS_FIELD(id, ...)                                              \
-  {                                                                            \
-    Statistic::id, #id, __VA_ARGS__                                            \
-  }
-
-// Statistics fields in display order.
-const StatisticsField k_statistics_fields[] = {
-  STATISTICS_FIELD(
-    stats_zeroed_timestamp, "stats zeroed", FLAG_ALWAYS, format_timestamp),
-  STATISTICS_FIELD(direct_cache_hit, "cache hit (direct)", FLAG_ALWAYS),
-  STATISTICS_FIELD(
-    preprocessed_cache_hit, "cache hit (preprocessed)", FLAG_ALWAYS),
-  STATISTICS_FIELD(cache_miss, "cache miss", FLAG_ALWAYS),
-  STATISTICS_FIELD(called_for_link, "called for link"),
-  STATISTICS_FIELD(called_for_preprocessing, "called for preprocessing"),
-  STATISTICS_FIELD(multiple_source_files, "multiple source files"),
-  STATISTICS_FIELD(compiler_produced_stdout, "compiler produced stdout"),
-  STATISTICS_FIELD(compiler_produced_no_output, "compiler produced no output"),
-  STATISTICS_FIELD(compiler_produced_empty_output,
-                   "compiler produced empty output"),
-  STATISTICS_FIELD(compile_failed, "compile failed"),
-  STATISTICS_FIELD(internal_error, "ccache internal error"),
-  STATISTICS_FIELD(preprocessor_error, "preprocessor error"),
-  STATISTICS_FIELD(could_not_use_precompiled_header,
-                   "can't use precompiled header"),
-  STATISTICS_FIELD(could_not_use_modules, "can't use modules"),
-  STATISTICS_FIELD(could_not_find_compiler, "couldn't find the compiler"),
-  STATISTICS_FIELD(missing_cache_file, "cache file missing"),
-  STATISTICS_FIELD(bad_compiler_arguments, "bad compiler arguments"),
-  STATISTICS_FIELD(unsupported_source_language, "unsupported source language"),
-  STATISTICS_FIELD(compiler_check_failed, "compiler check failed"),
-  STATISTICS_FIELD(autoconf_test, "autoconf compile/link"),
-  STATISTICS_FIELD(unsupported_compiler_option, "unsupported compiler option"),
-  STATISTICS_FIELD(unsupported_code_directive, "unsupported code directive"),
-  STATISTICS_FIELD(output_to_stdout, "output to stdout"),
-  STATISTICS_FIELD(bad_output_file, "could not write to output file"),
-  STATISTICS_FIELD(no_input_file, "no input file"),
-  STATISTICS_FIELD(error_hashing_extra_file, "error hashing extra file"),
-  STATISTICS_FIELD(cleanups_performed, "cleanups performed", FLAG_ALWAYS),
-  STATISTICS_FIELD(files_in_cache, "files in cache", FLAG_NOZERO | FLAG_ALWAYS),
-  STATISTICS_FIELD(cache_size_kibibyte,
-                   "cache size",
-                   FLAG_NOZERO | FLAG_ALWAYS,
-                   format_size_times_1024),
-  STATISTICS_FIELD(obsolete_max_files, "OBSOLETE", FLAG_NOZERO | FLAG_NEVER),
-  STATISTICS_FIELD(obsolete_max_size, "OBSOLETE", FLAG_NOZERO | FLAG_NEVER),
-  STATISTICS_FIELD(none, nullptr),
-};
-
-namespace Statistics {
-
-Counters
-read(const std::string& path)
-{
-  Counters counters;
-
-  std::string data;
-  try {
-    data = Util::read_file(path);
-  } catch (const Error&) {
-    // Ignore.
-    return counters;
-  }
-
-  size_t i = 0;
-  const char* str = data.c_str();
-  while (true) {
-    char* end;
-    const uint64_t value = std::strtoull(str, &end, 10);
-    if (end == str) {
-      break;
-    }
-    counters.set_raw(i, value);
-    ++i;
-    str = end;
-  }
-
-  return counters;
-}
-
-optional<Counters>
-update(const std::string& path,
-       std::function<void(Counters& counters)> function)
-{
-  Lockfile lock(path);
-  if (!lock.acquired()) {
-    LOG("Failed to acquire lock for {}", path);
-    return nullopt;
-  }
-
-  auto counters = Statistics::read(path);
-  function(counters);
-
-  AtomicFile file(path, AtomicFile::Mode::text);
-  for (size_t i = 0; i < counters.size(); ++i) {
-    file.write(FMT("{}\n", counters.get_raw(i)));
-  }
-  try {
-    file.commit();
-  } catch (const Error& e) {
-    // Make failure to write a stats file a soft error since it's not
-    // important enough to fail whole the process and also because it is
-    // called in the Context destructor.
-    LOG("Error: {}", e.what());
-  }
-
-  return counters;
-}
-
-optional<std::string>
-get_result(const Counters& counters)
-{
-  for (const auto& field : k_statistics_fields) {
-    if (counters.get(field.statistic) != 0 && !(field.flags & FLAG_NOZERO)) {
-      return field.message;
-    }
-  }
-  return nullopt;
-}
-
-void
-zero_all_counters(const Config& config)
-{
-  const time_t timestamp = time(nullptr);
-
-  for_each_level_1_and_2_stats_file(
-    config.cache_dir(), [=](const std::string& path) {
-      Statistics::update(path, [=](Counters& cs) {
-        for (size_t i = 0; k_statistics_fields[i].message; ++i) {
-          if (!(k_statistics_fields[i].flags & FLAG_NOZERO)) {
-            cs.set(k_statistics_fields[i].statistic, 0);
-          }
-        }
-        cs.set(Statistic::stats_zeroed_timestamp, timestamp);
-      });
-    });
-}
-
-std::string
-format_human_readable(const Config& config)
-{
-  Counters counters;
-  time_t last_updated;
-  std::tie(counters, last_updated) = collect_counters(config);
-  std::string result;
-
-  result += FMT("{:36}{}\n", "cache directory", config.cache_dir());
-  result += FMT("{:36}{}\n", "primary config", config.primary_config_path());
-  result += FMT(
-    "{:36}{}\n", "secondary config (readonly)", config.secondary_config_path());
-  if (last_updated > 0) {
-    const auto tm = Util::localtime(last_updated);
-    char timestamp[100] = "?";
-    if (tm) {
-      strftime(timestamp, sizeof(timestamp), "%c", &*tm);
-    }
-    result += FMT("{:36}{}\n", "stats updated", timestamp);
-  }
-
-  // ...and display them.
-  for (size_t i = 0; k_statistics_fields[i].message; i++) {
-    const Statistic statistic = k_statistics_fields[i].statistic;
-
-    if (k_statistics_fields[i].flags & FLAG_NEVER) {
-      continue;
-    }
-    if (counters.get(statistic) == 0
-        && !(k_statistics_fields[i].flags & FLAG_ALWAYS)) {
-      continue;
-    }
-
-    const std::string value =
-      k_statistics_fields[i].format
-        ? k_statistics_fields[i].format(counters.get(statistic))
-        : FMT("{:8}", counters.get(statistic));
-    if (!value.empty()) {
-      result += FMT("{:32}{}\n", k_statistics_fields[i].message, value);
-    }
-
-    if (statistic == Statistic::cache_miss) {
-      double percent = hit_rate(counters);
-      result += FMT("{:34}{:6.2f} %\n", "cache hit rate", percent);
-    }
-  }
-
-  if (config.max_files() != 0) {
-    result += FMT("{:32}{:8}\n", "max files", config.max_files());
-  }
-  if (config.max_size() != 0) {
-    result +=
-      FMT("{:32}{}\n", "max cache size", format_size(config.max_size()));
-  }
-
-  return result;
-}
-
-std::string
-format_machine_readable(const Config& config)
-{
-  Counters counters;
-  time_t last_updated;
-  std::tie(counters, last_updated) = collect_counters(config);
-  std::string result;
-
-  result += FMT("stats_updated_timestamp\t{}\n", last_updated);
-
-  for (size_t i = 0; k_statistics_fields[i].message; i++) {
-    if (!(k_statistics_fields[i].flags & FLAG_NEVER)) {
-      result += FMT("{}\t{}\n",
-                    k_statistics_fields[i].id,
-                    counters.get(k_statistics_fields[i].statistic));
-    }
-  }
-
-  return result;
-}
-
-} // namespace Statistics
diff --git a/src/Statistics.hpp b/src/Statistics.hpp
deleted file mode 100644 (file)
index 34a9982..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include "system.hpp"
-
-#include "Counters.hpp"
-#include "Statistic.hpp" // Any reasonable use of Statistics requires the Statistic enum.
-
-#include "third_party/nonstd/optional.hpp"
-
-#include <functional>
-#include <string>
-
-class Config;
-
-namespace Statistics {
-
-// Read counters from `path`. No lock is acquired.
-Counters read(const std::string& path);
-
-// Acquire a lock, read counters from `path`, call `function` with the counters,
-// write the counters to `path` and release the lock. Returns the resulting
-// counters or nullopt on error (e.g. if the lock could not be acquired).
-nonstd::optional<Counters> update(const std::string& path,
-                                  std::function<void(Counters& counters)>);
-
-// Return a human-readable string representing the final ccache result, or
-// nullopt if there was no result.
-nonstd::optional<std::string> get_result(const Counters& counters);
-
-// Zero all statistics counters except those tracking cache size and number of
-// files in the cache.
-void zero_all_counters(const Config& config);
-
-// Format cache statistics in human-readable format.
-std::string format_human_readable(const Config& config);
-
-// Format cache statistics in machine-readable format.
-std::string format_machine_readable(const Config& config);
-
-} // namespace Statistics
diff --git a/src/StdMakeUnique.hpp b/src/StdMakeUnique.hpp
deleted file mode 100644 (file)
index 829d2d5..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2019 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-namespace std {
-
-#if __cplusplus < 201402L
-template<typename T, typename... TArgs>
-inline unique_ptr<T>
-make_unique(TArgs&&... args)
-{
-  return unique_ptr<T>(new T(std::forward<TArgs>(args)...));
-}
-#endif
-
-} // namespace std
index feaa5f18236c1d0cb9c7cd1e57e01ac93e01a1bc..8c9e3868d694a0cdc1d6ac3d9bdb5958ca198f9e 100644 (file)
@@ -20,6 +20,8 @@
 
 #include "Util.hpp"
 
+#include <core/exceptions.hpp>
+
 #ifdef _WIN32
 #  include "third_party/win32/mktemp.h"
 #endif
@@ -61,7 +63,7 @@ TemporaryFile::TemporaryFile(string_view path_prefix)
   fd = Fd(mkstemp(&path[0]));
 #endif
   if (!fd) {
-    throw Fatal(
+    throw core::Fatal(
       "Failed to create temporary file for {}: {}", path, strerror(errno));
   }
 
index 0538d9a40e8b246fc9aaa2db55a01fd4f11fe422..624d5937dcc32adbd0d929d2a77e5f8b8d6a130e 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
index 5eee3818210b89b3358941c5c9481ef2c79933fd..fa46538259cb4eb7066b80568afe3b020cde8b0f 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,8 +18,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 #include <condition_variable>
 #include <functional>
 #include <limits>
index d96f448e23fce2862da78523bf447d846430888b..2a2f85a74425591a339d5c4412663dc4bd8b9a5b 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "system.hpp"
-
 #include "third_party/nonstd/optional.hpp"
 
+#include <sys/stat.h>
+#include <sys/types.h>
+
 // This class sets a new (process-global) umask and restores the previous umask
 // when destructed.
 class UmaskScope
@@ -34,7 +35,7 @@ private:
   nonstd::optional<mode_t> m_saved_umask;
 };
 
-UmaskScope::UmaskScope(nonstd::optional<mode_t> new_umask)
+inline UmaskScope::UmaskScope(nonstd::optional<mode_t> new_umask)
 {
 #ifndef _WIN32
   if (new_umask) {
@@ -45,7 +46,7 @@ UmaskScope::UmaskScope(nonstd::optional<mode_t> new_umask)
 #endif
 }
 
-UmaskScope::~UmaskScope()
+inline UmaskScope::~UmaskScope()
 {
 #ifndef _WIN32
   if (m_saved_umask) {
index f49708fd4653d35352a3e54d776e82a73372e844..24ce1bbd921a50cc5cfd33353e42710ae8f2b706 100644 (file)
 #include "Config.hpp"
 #include "Context.hpp"
 #include "Fd.hpp"
-#include "FormatNonstdStringView.hpp"
 #include "Logging.hpp"
 #include "TemporaryFile.hpp"
+#include "Win32Util.hpp"
 #include "fmtmacros.hpp"
 
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+#include <util/path.hpp>
+#include <util/string.hpp>
+
 extern "C" {
 #include "third_party/base32hex.h"
 }
 
+#ifdef HAVE_DIRENT_H
+#  include <dirent.h>
+#endif
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
+#include <fcntl.h>
+
 #include <algorithm>
+#include <climits>
 #include <fstream>
 
 #ifndef HAVE_DIRENT_H
@@ -45,6 +61,12 @@ extern "C" {
 #  include <sys/time.h>
 #endif
 
+#ifdef HAVE_UTIME_H
+#  include <utime.h>
+#elif defined(HAVE_SYS_UTIME_H)
+#  include <sys/utime.h>
+#endif
+
 #ifdef HAVE_LINUX_FS_H
 #  include <linux/magic.h>
 #  include <sys/statfs.h>
@@ -53,10 +75,6 @@ extern "C" {
 #  include <sys/param.h>
 #endif
 
-#ifdef _WIN32
-#  include "Win32Util.hpp"
-#endif
-
 #ifdef __linux__
 #  ifdef HAVE_SYS_IOCTL_H
 #    include <sys/ioctl.h>
@@ -136,26 +154,14 @@ path_max(const std::string& path)
 
 template<typename T>
 std::vector<T>
-split_at(string_view input, const char* separators)
+split_into(string_view string,
+           const char* separators,
+           util::Tokenizer::Mode mode)
 {
-  ASSERT(separators != nullptr && separators[0] != '\0');
-
   std::vector<T> result;
-
-  size_t start = 0;
-  while (start < input.size()) {
-    size_t end = input.find_first_of(separators, start);
-
-    if (end == string_view::npos) {
-      result.emplace_back(input.data() + start, input.size() - start);
-      break;
-    } else if (start != end) {
-      result.emplace_back(input.data() + start, end - start);
-    }
-
-    start = end + 1;
+  for (const auto token : util::Tokenizer(string, separators, mode)) {
+    result.emplace_back(token);
   }
-
   return result;
 }
 
@@ -165,14 +171,15 @@ rewrite_stderr_to_absolute_paths(string_view text)
   static const std::string in_file_included_from = "In file included from ";
 
   std::string result;
-  for (auto line : Util::split_into_views(text, "\n")) {
+  for (auto line :
+       util::Tokenizer(text, "\n", util::Tokenizer::Mode::skip_last_empty)) {
     // Rewrite <path> to <absolute path> in the following two cases, where X may
     // be optional ANSI CSI sequences:
     //
     // In file included from X<path>X:1:
     // X<path>X:1:2: ...
 
-    if (Util::starts_with(line, in_file_included_from)) {
+    if (util::starts_with(line, in_file_included_from)) {
       result += in_file_included_from;
       line = line.substr(in_file_included_from.length());
     }
@@ -229,7 +236,7 @@ clone_file(const std::string& src, const std::string& dest, bool via_tmp_file)
 #  if defined(__linux__)
   Fd src_fd(open(src.c_str(), O_RDONLY));
   if (!src_fd) {
-    throw Error("{}: {}", src, strerror(errno));
+    throw core::Error("{}: {}", src, strerror(errno));
   }
 
   Fd dest_fd;
@@ -242,12 +249,12 @@ clone_file(const std::string& src, const std::string& dest, bool via_tmp_file)
     dest_fd =
       Fd(open(dest.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666));
     if (!dest_fd) {
-      throw Error("{}: {}", src, strerror(errno));
+      throw core::Error("{}: {}", src, strerror(errno));
     }
   }
 
   if (ioctl(*dest_fd, FICLONE, *src_fd) != 0) {
-    throw Error(strerror(errno));
+    throw core::Error(strerror(errno));
   }
 
   dest_fd.close();
@@ -259,13 +266,13 @@ clone_file(const std::string& src, const std::string& dest, bool via_tmp_file)
 #  elif defined(__APPLE__)
   (void)via_tmp_file;
   if (clonefile(src.c_str(), dest.c_str(), CLONE_NOOWNERCOPY) != 0) {
-    throw Error(strerror(errno));
+    throw core::Error(strerror(errno));
   }
 #  else
   (void)src;
   (void)dest;
   (void)via_tmp_file;
-  throw Error(strerror(EOPNOTSUPP));
+  throw core::Error(strerror(EOPNOTSUPP));
 #  endif
 }
 #endif // FILE_CLONING_SUPPORTED
@@ -282,7 +289,7 @@ clone_hard_link_or_copy_file(const Context& ctx,
     try {
       clone_file(source, dest, via_tmp_file);
       return;
-    } catch (Error& e) {
+    } catch (core::Error& e) {
       LOG("Failed to clone: {}", e.what());
     }
 #else
@@ -297,7 +304,7 @@ clone_hard_link_or_copy_file(const Context& ctx,
         LOG("Failed to chmod: {}", strerror(errno));
       }
       return;
-    } catch (const Error& e) {
+    } catch (const core::Error& e) {
       LOG_RAW(e.what());
       // Fall back to copying.
     }
@@ -349,7 +356,7 @@ copy_file(const std::string& src, const std::string& dest, bool via_tmp_file)
 {
   Fd src_fd(open(src.c_str(), O_RDONLY));
   if (!src_fd) {
-    throw Error("{}: {}", src, strerror(errno));
+    throw core::Error("{}: {}", src, strerror(errno));
   }
 
   Fd dest_fd;
@@ -362,7 +369,7 @@ copy_file(const std::string& src, const std::string& dest, bool via_tmp_file)
     dest_fd =
       Fd(open(dest.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666));
     if (!dest_fd) {
-      throw Error("{}: {}", dest, strerror(errno));
+      throw core::Error("{}: {}", dest, strerror(errno));
     }
   }
 
@@ -448,7 +455,7 @@ expand_environment_variables(const std::string& str)
         ++right;
       }
       if (curly && *right != '}') {
-        throw Error("syntax error: missing '}}' after \"{}\"", left);
+        throw core::Error("syntax error: missing '}}' after \"{}\"", left);
       }
       if (right == left) {
         // Special case: don't consider a single $ the left of a variable.
@@ -458,7 +465,7 @@ expand_environment_variables(const std::string& str)
         std::string name(left, right - left);
         const char* value = getenv(name.c_str());
         if (!value) {
-          throw Error("environment variable \"{}\" not set", name);
+          throw core::Error("environment variable \"{}\" not set", name);
         }
         result += value;
         if (!curly) {
@@ -498,7 +505,7 @@ fallocate(int fd, long new_size)
   int err = 0;
   try {
     write_fd(fd, buf, bytes_to_write);
-  } catch (Error&) {
+  } catch (core::Error&) {
     err = errno;
   }
   lseek(fd, saved_pos, SEEK_SET);
@@ -507,22 +514,6 @@ fallocate(int fd, long new_size)
 #endif
 }
 
-void
-for_each_level_1_subdir(const std::string& cache_dir,
-                        const SubdirVisitor& visitor,
-                        const ProgressReceiver& progress_receiver)
-{
-  for (int i = 0; i <= 0xF; i++) {
-    double progress = 1.0 * i / 16;
-    progress_receiver(progress);
-    std::string subdir_path = FMT("{}/{:x}", cache_dir, i);
-    visitor(subdir_path, [&](double inner_progress) {
-      progress_receiver(progress + inner_progress / 16);
-    });
-  }
-  progress_receiver(1.0);
-}
-
 std::string
 format_argv_for_logging(const char* const* argv)
 {
@@ -589,7 +580,8 @@ void
 ensure_dir_exists(nonstd::string_view dir)
 {
   if (!create_dir(dir)) {
-    throw Fatal("Failed to create directory {}: {}", dir, strerror(errno));
+    throw core::Fatal(
+      "Failed to create directory {}: {}", dir, strerror(errno));
   }
 }
 
@@ -617,7 +609,7 @@ get_apparent_cwd(const std::string& actual_cwd)
   return actual_cwd;
 #else
   auto pwd = getenv("PWD");
-  if (!pwd) {
+  if (!pwd || !util::is_absolute_path(pwd)) {
     return actual_cwd;
   }
 
@@ -654,37 +646,6 @@ get_extension(string_view path)
   }
 }
 
-std::vector<CacheFile>
-get_level_1_files(const std::string& dir,
-                  const ProgressReceiver& progress_receiver)
-{
-  std::vector<CacheFile> files;
-
-  if (!Stat::stat(dir)) {
-    return files;
-  }
-
-  size_t level_2_directories = 0;
-
-  Util::traverse(dir, [&](const std::string& path, bool is_dir) {
-    auto name = Util::base_name(path);
-    if (name == "CACHEDIR.TAG" || name == "stats" || name.starts_with(".nfs")) {
-      return;
-    }
-
-    if (!is_dir) {
-      files.emplace_back(path);
-    } else if (path != dir
-               && path.find('/', dir.size() + 1) == std::string::npos) {
-      ++level_2_directories;
-      progress_receiver(level_2_directories / 16.0);
-    }
-  });
-
-  progress_receiver(1.0);
-  return files;
-}
-
 std::string
 get_home_directory()
 {
@@ -706,7 +667,8 @@ get_home_directory()
     }
   }
 #endif
-  throw Fatal("Could not determine home directory from $HOME or getpwuid(3)");
+  throw core::Fatal(
+    "Could not determine home directory from $HOME or getpwuid(3)");
 }
 
 const char*
@@ -728,8 +690,8 @@ get_hostname()
 std::string
 get_relative_path(string_view dir, string_view path)
 {
-  ASSERT(Util::is_absolute_path(dir));
-  ASSERT(Util::is_absolute_path(path));
+  ASSERT(util::is_absolute_path(dir));
+  ASSERT(util::is_absolute_path(path));
 
 #ifdef _WIN32
   // Paths can be escaped by a slash for use with e.g. -isystem.
@@ -769,27 +731,6 @@ get_relative_path(string_view dir, string_view path)
   return result.empty() ? "." : result;
 }
 
-std::string
-get_path_in_cache(string_view cache_dir, uint8_t level, string_view name)
-{
-  ASSERT(level >= 1 && level <= 8);
-  ASSERT(name.length() >= level);
-
-  std::string path(cache_dir);
-  path.reserve(path.size() + level * 2 + 1 + name.length() - level);
-
-  for (uint8_t i = 0; i < level; ++i) {
-    path.push_back('/');
-    path.push_back(name.at(i));
-  }
-
-  path.push_back('/');
-  string_view name_remaining = name.substr(level);
-  path.append(name_remaining.data(), name_remaining.length());
-
-  return path;
-}
-
 void
 hard_link(const std::string& oldpath, const std::string& newpath)
 {
@@ -800,30 +741,18 @@ hard_link(const std::string& oldpath, const std::string& newpath)
 
 #ifndef _WIN32
   if (link(oldpath.c_str(), newpath.c_str()) != 0) {
-    throw Error(
+    throw core::Error(
       "failed to link {} to {}: {}", oldpath, newpath, strerror(errno));
   }
 #else
   if (!CreateHardLink(newpath.c_str(), oldpath.c_str(), nullptr)) {
     DWORD error = GetLastError();
-    throw Error("failed to link {} to {}: {}",
-                oldpath,
-                newpath,
-                Win32Util::error_message(error));
-  }
-#endif
-}
-
-bool
-is_absolute_path(string_view path)
-{
-#ifdef _WIN32
-  if (path.length() >= 2 && path[1] == ':'
-      && (path[2] == '/' || path[2] == '\\')) {
-    return true;
+    throw core::Error("failed to link {} to {}: {}",
+                      oldpath,
+                      newpath,
+                      Win32Util::error_message(error));
   }
 #endif
-  return !path.empty() && path[0] == '/';
 }
 
 #if defined(HAVE_LINUX_FS_H) || defined(HAVE_STRUCT_STATFS_F_FSTYPENAME)
@@ -875,7 +804,7 @@ make_relative_path(const std::string& base_dir,
                    const std::string& apparent_cwd,
                    nonstd::string_view path)
 {
-  if (base_dir.empty() || !Util::starts_with(path, base_dir)) {
+  if (base_dir.empty() || !util::starts_with(path, base_dir)) {
     return std::string(path);
   }
 
@@ -907,7 +836,7 @@ make_relative_path(const std::string& base_dir,
   const auto path_suffix = std::string(original_path.substr(path.length()));
   const auto real_path = Util::real_path(std::string(path));
 
-  const auto add_relpath_candidates = [&](nonstd::string_view path) {
+  const auto add_relpath_candidates = [&](auto path) {
     const std::string normalized_path = Util::normalize_absolute_path(path);
     relpath_candidates.push_back(
       Util::get_relative_path(actual_cwd, normalized_path));
@@ -924,7 +853,7 @@ make_relative_path(const std::string& base_dir,
   // Find best (i.e. shortest existing) match:
   std::sort(relpath_candidates.begin(),
             relpath_candidates.end(),
-            [](const std::string& path1, const std::string& path2) {
+            [](const auto& path1, const auto& path2) {
               return path1.length() < path2.length();
             });
   for (const auto& relpath : relpath_candidates) {
@@ -958,7 +887,7 @@ matches_dir_prefix_or_file(string_view dir_prefix_or_file, string_view path)
 std::string
 normalize_absolute_path(string_view path)
 {
-  if (!is_absolute_path(path)) {
+  if (!util::is_absolute_path(path)) {
     return std::string(path);
   }
 
@@ -1028,40 +957,17 @@ parse_duration(const std::string& duration)
     factor = 1;
     break;
   default:
-    throw Error("invalid suffix (supported: d (day) and s (second)): \"{}\"",
-                duration);
-  }
-
-  return factor * parse_unsigned(duration.substr(0, duration.length() - 1));
-}
-
-int64_t
-parse_signed(const std::string& value,
-             optional<int64_t> min_value,
-             optional<int64_t> max_value,
-             string_view description)
-{
-  std::string stripped_value = strip_whitespace(value);
-
-  size_t end = 0;
-  long long result = 0;
-  bool failed = false;
-  try {
-    // Note: sizeof(long long) is guaranteed to be >= sizeof(int64_t)
-    result = std::stoll(stripped_value, &end, 10);
-  } catch (std::exception&) {
-    failed = true;
-  }
-  if (failed || end != stripped_value.size()) {
-    throw Error("invalid integer: \"{}\"", stripped_value);
+    throw core::Error(
+      "invalid suffix (supported: d (day) and s (second)): \"{}\"", duration);
   }
 
-  int64_t min = min_value ? *min_value : INT64_MIN;
-  int64_t max = max_value ? *max_value : INT64_MAX;
-  if (result < min || result > max) {
-    throw Error("{} must be between {} and {}", description, min, max);
+  const auto value =
+    util::parse_unsigned(duration.substr(0, duration.length() - 1));
+  if (value) {
+    return factor * *value;
+  } else {
+    throw core::Error(value.error());
   }
-  return result;
 }
 
 uint64_t
@@ -1072,7 +978,7 @@ parse_size(const std::string& value)
   char* p;
   double result = strtod(value.c_str(), &p);
   if (errno != 0 || result < 0 || p == value.c_str() || value.empty()) {
-    throw Error("invalid size: \"{}\"", value);
+    throw core::Error("invalid size: \"{}\"", value);
   }
 
   while (isspace(*p)) {
@@ -1096,7 +1002,7 @@ parse_size(const std::string& value)
       result *= multiplier;
       break;
     default:
-      throw Error("invalid size: \"{}\"", value);
+      throw core::Error("invalid size: \"{}\"", value);
     }
   } else {
     // Default suffix: G.
@@ -1105,45 +1011,11 @@ parse_size(const std::string& value)
   return static_cast<uint64_t>(result);
 }
 
-uint64_t
-parse_unsigned(const std::string& value,
-               optional<uint64_t> min_value,
-               optional<uint64_t> max_value,
-               string_view description)
-{
-  std::string stripped_value = strip_whitespace(value);
-
-  size_t end = 0;
-  unsigned long long result = 0;
-  bool failed = false;
-  if (Util::starts_with(stripped_value, "-")) {
-    failed = true;
-  } else {
-    try {
-      // Note: sizeof(unsigned long long) is guaranteed to be >=
-      // sizeof(uint64_t)
-      result = std::stoull(stripped_value, &end, 10);
-    } catch (std::exception&) {
-      failed = true;
-    }
-  }
-  if (failed || end != stripped_value.size()) {
-    throw Error("invalid unsigned integer: \"{}\"", stripped_value);
-  }
-
-  uint64_t min = min_value ? *min_value : 0;
-  uint64_t max = max_value ? *max_value : UINT64_MAX;
-  if (result < min || result > max) {
-    throw Error("{} must be between {} and {}", description, min, max);
-  }
-  return result;
-}
-
 bool
 read_fd(int fd, DataReceiver data_receiver)
 {
-  ssize_t n;
-  char buffer[READ_BUFFER_SIZE];
+  int64_t n;
+  char buffer[CCACHE_READ_BUFFER_SIZE];
   while ((n = read(fd, buffer, sizeof(buffer))) != 0) {
     if (n == -1 && errno != EINTR) {
       break;
@@ -1161,7 +1033,7 @@ read_file(const std::string& path, size_t size_hint)
   if (size_hint == 0) {
     auto stat = Stat::stat(path);
     if (!stat) {
-      throw Error(strerror(errno));
+      throw core::Error(strerror(errno));
     }
     size_hint = stat.size();
   }
@@ -1171,10 +1043,10 @@ read_file(const std::string& path, size_t size_hint)
 
   Fd fd(open(path.c_str(), O_RDONLY | O_BINARY));
   if (!fd) {
-    throw Error(strerror(errno));
+    throw core::Error(strerror(errno));
   }
 
-  ssize_t ret = 0;
+  int64_t ret = 0;
   size_t pos = 0;
   std::string result;
   result.resize(size_hint);
@@ -1198,7 +1070,7 @@ read_file(const std::string& path, size_t size_hint)
 
   if (ret == -1) {
     LOG("Failed reading {}", path);
-    throw Error(strerror(errno));
+    throw core::Error(strerror(errno));
   }
 
   result.resize(pos);
@@ -1211,7 +1083,7 @@ read_link(const std::string& path)
 {
   size_t buffer_size = path_max(path);
   std::unique_ptr<char[]> buffer(new char[buffer_size]);
-  ssize_t len = readlink(path.c_str(), buffer.get(), buffer_size - 1);
+  const auto len = readlink(path.c_str(), buffer.get(), buffer_size - 1);
   if (len == -1) {
     return "";
   }
@@ -1272,7 +1144,7 @@ rename(const std::string& oldpath, const std::string& newpath)
 {
 #ifndef _WIN32
   if (::rename(oldpath.c_str(), newpath.c_str()) != 0) {
-    throw Error(
+    throw core::Error(
       "failed to rename {} to {}: {}", oldpath, newpath, strerror(errno));
   }
 #else
@@ -1281,10 +1153,10 @@ rename(const std::string& oldpath, const std::string& newpath)
   if (!MoveFileExA(
         oldpath.c_str(), newpath.c_str(), MOVEFILE_REPLACE_EXISTING)) {
     DWORD error = GetLastError();
-    throw Error("failed to rename {} to {}: {}",
-                oldpath,
-                newpath,
-                Win32Util::error_message(error));
+    throw core::Error("failed to rename {} to {}: {}",
+                      oldpath,
+                      newpath,
+                      Win32Util::error_message(error));
   }
 #endif
 }
@@ -1312,7 +1184,7 @@ send_to_stderr(const Context& ctx, const std::string& text)
     try {
       modified_text = strip_ansi_csi_seqs(text);
       text_to_send = &modified_text;
-    } catch (const Error&) {
+    } catch (const core::Error&) {
       // Fall through
     }
   }
@@ -1324,8 +1196,8 @@ send_to_stderr(const Context& ctx, const std::string& text)
 
   try {
     write_fd(STDERR_FILENO, text_to_send->data(), text_to_send->length());
-  } catch (Error& e) {
-    throw Error("Failed to write to stderr: {}", e.what());
+  } catch (core::Error& e) {
+    throw core::Error("Failed to write to stderr: {}", e.what());
   }
 }
 
@@ -1355,15 +1227,19 @@ setenv(const std::string& name, const std::string& value)
 }
 
 std::vector<string_view>
-split_into_views(string_view input, const char* separators)
+split_into_views(string_view string,
+                 const char* separators,
+                 util::Tokenizer::Mode mode)
 {
-  return split_at<string_view>(input, separators);
+  return split_into<string_view>(string, separators, mode);
 }
 
 std::vector<std::string>
-split_into_strings(string_view input, const char* separators)
+split_into_strings(string_view string,
+                   const char* separators,
+                   util::Tokenizer::Mode mode)
 {
-  return split_at<std::string>(input, separators);
+  return split_into<std::string>(string, separators, mode);
 }
 
 std::string
@@ -1388,15 +1264,6 @@ strip_ansi_csi_seqs(string_view string)
   return result;
 }
 
-std::string
-strip_whitespace(string_view string)
-{
-  auto is_space = [](int ch) { return std::isspace(ch); };
-  auto start = std::find_if_not(string.begin(), string.end(), is_space);
-  auto end = std::find_if_not(string.rbegin(), string.rend(), is_space).base();
-  return start < end ? std::string(start, end) : std::string();
-}
-
 std::string
 to_lowercase(string_view string)
 {
@@ -1433,9 +1300,9 @@ traverse(const std::string& path, const TraverseVisitor& visitor)
           if (stat.error_number() == ENOENT || stat.error_number() == ESTALE) {
             continue;
           }
-          throw Error("failed to lstat {}: {}",
-                      entry_path,
-                      strerror(stat.error_number()));
+          throw core::Error("failed to lstat {}: {}",
+                            entry_path,
+                            strerror(stat.error_number()));
         }
         is_dir = stat.is_directory();
       }
@@ -1450,7 +1317,7 @@ traverse(const std::string& path, const TraverseVisitor& visitor)
   } else if (errno == ENOTDIR) {
     visitor(path, false);
   } else {
-    throw Error("failed to open directory {}: {}", path, strerror(errno));
+    throw core::Error("failed to open directory {}: {}", path, strerror(errno));
   }
 }
 
@@ -1473,7 +1340,7 @@ traverse(const std::string& path, const TraverseVisitor& visitor)
   } else if (std::filesystem::exists(path)) {
     visitor(path, false);
   } else {
-    throw Error("failed to open directory {}: {}", path, strerror(errno));
+    throw core::Error("failed to open directory {}: {}", path, strerror(errno));
   }
 }
 
@@ -1492,7 +1359,7 @@ unlink_safe(const std::string& path, UnlinkLog unlink_log)
   bool success = true;
   try {
     Util::rename(path, tmp_name);
-  } catch (Error&) {
+  } catch (core::Error&) {
     success = false;
     saved_errno = errno;
   }
@@ -1562,10 +1429,10 @@ wipe_path(const std::string& path)
   traverse(path, [](const std::string& p, bool is_dir) {
     if (is_dir) {
       if (rmdir(p.c_str()) != 0 && errno != ENOENT && errno != ESTALE) {
-        throw Error("failed to rmdir {}: {}", p, strerror(errno));
+        throw core::Error("failed to rmdir {}: {}", p, strerror(errno));
       }
     } else if (unlink(p.c_str()) != 0 && errno != ENOENT && errno != ESTALE) {
-      throw Error("failed to unlink {}: {}", p, strerror(errno));
+      throw core::Error("failed to unlink {}: {}", p, strerror(errno));
     }
   });
 }
@@ -1573,13 +1440,13 @@ wipe_path(const std::string& path)
 void
 write_fd(int fd, const void* data, size_t size)
 {
-  ssize_t written = 0;
+  int64_t written = 0;
   do {
-    ssize_t count =
+    const auto count =
       write(fd, static_cast<const uint8_t*>(data) + written, size - written);
     if (count == -1) {
       if (errno != EAGAIN && errno != EINTR) {
-        throw Error(strerror(errno));
+        throw core::Error(strerror(errno));
       }
     } else {
       written += count;
@@ -1593,13 +1460,13 @@ write_file(const std::string& path,
            std::ios_base::openmode open_mode)
 {
   if (path.empty()) {
-    throw Error("No such file or directory");
+    throw core::Error("No such file or directory");
   }
 
   open_mode |= std::ios::out;
   std::ofstream file(path, open_mode);
   if (!file) {
-    throw Error(strerror(errno));
+    throw core::Error(strerror(errno));
   }
   file << data;
 }
index 7db8d95af189663ab358f45167ca3b01133571d0..7a3d116e27f90decb775866062aa9e034bcecb02 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "system.hpp"
-
-#include "CacheFile.hpp"
+#include <Stat.hpp>
+#include <util/Tokenizer.hpp>
 
 #include "third_party/nonstd/optional.hpp"
 #include "third_party/nonstd/string_view.hpp"
 
 #include <algorithm>
+#include <cstdint>
 #include <functional>
 #include <ios>
 #include <memory>
@@ -38,9 +38,6 @@ class Context;
 namespace Util {
 
 using DataReceiver = std::function<void(const void* data, size_t size)>;
-using ProgressReceiver = std::function<void(double progress)>;
-using SubdirVisitor = std::function<void(
-  const std::string& dir_path, const ProgressReceiver& progress_receiver)>;
 using TraverseVisitor =
   std::function<void(const std::string& path, bool is_dir)>;
 
@@ -93,14 +90,15 @@ clamp(T value, T min, T max)
 }
 
 // Clone a file from `src` to `dest`. If `via_tmp_file` is true, `src` is cloned
-// to a temporary file and then renamed to `dest`. Throws `Error` on error.
+// to a temporary file and then renamed to `dest`. Throws `core::Error` on
+// error.
 void clone_file(const std::string& src,
                 const std::string& dest,
                 bool via_tmp_file = false);
 
 // Clone, hard link or copy a file from `source` to `dest` depending on settings
 // in `ctx`. If cloning or hard linking cannot and should not be done the file
-// will be copied instead. Throws `Error` on error.
+// will be copied instead. Throws `core::Error` on error.
 void clone_hard_link_or_copy_file(const Context& ctx,
                                   const std::string& source,
                                   const std::string& dest,
@@ -111,11 +109,11 @@ void clone_hard_link_or_copy_file(const Context& ctx,
 size_t common_dir_prefix_length(nonstd::string_view dir,
                                 nonstd::string_view path);
 
-// Copy all data from `fd_in` to `fd_out`. Throws `Error` on error.
+// Copy all data from `fd_in` to `fd_out`. Throws `core::Error` on error.
 void copy_fd(int fd_in, int fd_out);
 
 // Copy a file from `src` to `dest`. If via_tmp_file is true, `src` is copied to
-// a temporary file and then renamed to dest. Throws `Error` on error.
+// a temporary file and then renamed to dest. Throws `core::Error` on error.
 void copy_file(const std::string& src,
                const std::string& dest,
                bool via_tmp_file = false);
@@ -128,18 +126,11 @@ bool create_dir(nonstd::string_view dir);
 // Get directory name of path.
 nonstd::string_view dir_name(nonstd::string_view path);
 
-// Return true if `suffix` is a suffix of `string`.
-inline bool
-ends_with(nonstd::string_view string, nonstd::string_view suffix)
-{
-  return string.ends_with(suffix);
-}
-
 // Like create_dir but throws Fatal on error.
 void ensure_dir_exists(nonstd::string_view dir);
 
 // Expand all instances of $VAR or ${VAR}, where VAR is an environment variable,
-// in `str`. Throws `Error` if one of the environment variables.
+// in `str`. Throws `core::Error` if one of the environment variables.
 [[nodiscard]] std::string expand_environment_variables(const std::string& str);
 
 // Extends file size to at least new_size by calling posix_fallocate() if
@@ -151,17 +142,6 @@ void ensure_dir_exists(nonstd::string_view dir);
 // Returns 0 on success, an error number otherwise.
 int fallocate(int fd, long new_size);
 
-// Call a function for each subdir (0-9a-f) in the cache.
-//
-// Parameters:
-// - cache_dir: Path to the cache directory.
-// - visitor: Function to call with directory path and progress_receiver as
-//   arguments.
-// - progress_receiver: Function that will be called for progress updates.
-void for_each_level_1_subdir(const std::string& cache_dir,
-                             const SubdirVisitor& visitor,
-                             const ProgressReceiver& progress_receiver);
-
 // Format `argv` as a simple string for logging purposes. That is, the result is
 // not intended to be machine parsable. `argv` must be terminated by a nullptr.
 std::string format_argv_for_logging(const char* const* argv);
@@ -194,24 +174,6 @@ std::string get_apparent_cwd(const std::string& actual_cwd);
 // `path` has no file extension, an empty string_view is returned.
 nonstd::string_view get_extension(nonstd::string_view path);
 
-// Get a list of files in a level 1 subdirectory of the cache.
-//
-// The function works under the assumption that directory entries with one
-// character names (except ".") are subdirectories and that there are no other
-// subdirectories.
-//
-// Files ignored:
-// - CACHEDIR.TAG
-// - stats
-// - .nfs* (temporary NFS files that may be left for open but deleted files).
-//
-// Parameters:
-// - dir: The directory to traverse recursively.
-// - progress_receiver: Function that will be called for progress updates.
-std::vector<CacheFile>
-get_level_1_files(const std::string& dir,
-                  const ProgressReceiver& progress_receiver);
-
 // Return the current user's home directory, or throw `Fatal` if it can't
 // be determined.
 std::string get_home_directory();
@@ -226,14 +188,7 @@ const char* get_hostname();
 std::string get_relative_path(nonstd::string_view dir,
                               nonstd::string_view path);
 
-// Join `cache_dir`, a '/' and `name` into a single path and return it.
-// Additionally, `level` single-character, '/'-separated subpaths are split from
-// the beginning of `name` before joining them all.
-std::string get_path_in_cache(nonstd::string_view cache_dir,
-                              uint8_t level,
-                              nonstd::string_view name);
-
-// Hard-link `oldpath` to `newpath`. Throws `Error` on error.
+// Hard-link `oldpath` to `newpath`. Throws `core::Error` on error.
 void hard_link(const std::string& oldpath, const std::string& newpath);
 
 // Write bytes in big endian order from an integer value.
@@ -265,9 +220,6 @@ int_to_big_endian(int8_t value, uint8_t* buffer)
   buffer[0] = value;
 }
 
-// Return whether `path` is absolute.
-bool is_absolute_path(nonstd::string_view path);
-
 // Test if a file is on nfs.
 //
 // Sets is_nfs to the result if fstatfs is available and no error occurred.
@@ -288,18 +240,6 @@ is_dir_separator(char ch)
     ;
 }
 
-// Return whether `path` is a full path.
-inline bool
-is_full_path(nonstd::string_view path)
-{
-#ifdef _WIN32
-  if (path.find('\\') != nonstd::string_view::npos) {
-    return true;
-  }
-#endif
-  return path.find('/') != nonstd::string_view::npos;
-}
-
 // Return whether `path` represents a precompiled header (see "Precompiled
 // Headers" in GCC docs).
 bool is_precompiled_header(nonstd::string_view path);
@@ -333,36 +273,14 @@ bool matches_dir_prefix_or_file(nonstd::string_view dir_prefix_or_file,
 std::string normalize_absolute_path(nonstd::string_view path);
 
 // Parse `duration`, an unsigned integer with d (days) or s (seconds) suffix,
-// into seconds. Throws `Error` on error.
+// into seconds. Throws `core::Error` on error.
 uint64_t parse_duration(const std::string& duration);
 
-// Parse a string into a signed integer.
-//
-// Throws `Error` if `value` cannot be parsed as an int64_t or if the value
-// falls out of the range [`min_value`, `max_value`]. `min_value` and
-// `max_value` default to min and max values of int64_t. `description` is
-// included in the error message for range violations.
-int64_t parse_signed(const std::string& value,
-                     nonstd::optional<int64_t> min_value = nonstd::nullopt,
-                     nonstd::optional<int64_t> max_value = nonstd::nullopt,
-                     nonstd::string_view description = "integer");
-
 // Parse a "size value", i.e. a string that can end in k, M, G, T (10-based
 // suffixes) or Ki, Mi, Gi, Ti (2-based suffixes). For backward compatibility, K
-// is also recognized as a synonym of k. Throws `Error` on parse error.
+// is also recognized as a synonym of k. Throws `core::Error` on parse error.
 uint64_t parse_size(const std::string& value);
 
-// Parse a string into an unsigned integer.
-//
-// Throws `Error` if `value` cannot be parsed as an uint64_t or if the value
-// falls out of the range [`min_value`, `max_value`]. `min_value` and
-// `max_value` default to min and max values of uint64_t. `description` is
-// included in the error message for range violations.
-uint64_t parse_unsigned(const std::string& value,
-                        nonstd::optional<uint64_t> min_value = nonstd::nullopt,
-                        nonstd::optional<uint64_t> max_value = nonstd::nullopt,
-                        nonstd::string_view description = "integer");
-
 // Read data from `fd` until end of file and call `data_receiver` with the read
 // data. Returns whether reading was successful, i.e. whether the read(2) call
 // did not return -1.
@@ -371,8 +289,8 @@ bool read_fd(int fd, DataReceiver data_receiver);
 // Return `path`'s content as a string. If `size_hint` is not 0 then assume that
 // `path` has this size (this saves system calls).
 //
-// Throws `Error` on error. The description contains the error message without
-// the path.
+// Throws `core::Error` on error. The description contains the error message
+// without the path.
 std::string read_file(const std::string& path, size_t size_hint = 0);
 
 #ifndef _WIN32
@@ -390,7 +308,8 @@ std::string real_path(const std::string& path,
 // extension as determined by `get_extension()`.
 nonstd::string_view remove_extension(nonstd::string_view path);
 
-// Rename `oldpath` to `newpath` (deleting `newpath`). Throws `Error` on error.
+// Rename `oldpath` to `newpath` (deleting `newpath`). Throws `core::Error` on
+// error.
 void rename(const std::string& oldpath, const std::string& newpath);
 
 // Detmine if `program_name` is equal to `canonical_program_name`. On Windows,
@@ -401,8 +320,8 @@ bool same_program_name(nonstd::string_view program_name,
 
 // Send `text` to STDERR_FILENO, optionally stripping ANSI color sequences if
 // `ctx.args_info.strip_diagnostics_colors` is true and rewriting paths to
-// absolute if `ctx.config.absolute_paths_in_stderr` is true. Throws `Error` on
-// error.
+// absolute if `ctx.config.absolute_paths_in_stderr` is true. Throws
+// `core::Error` on error.
 void send_to_stderr(const Context& ctx, const std::string& text);
 
 // Set the FD_CLOEXEC on file descriptor `fd`. This is a NOP on Windows.
@@ -420,51 +339,36 @@ size_change_kibibyte(const Stat& old_stat, const Stat& new_stat)
          / 1024;
 }
 
-// Split `input` into words at any of the characters listed in `separators`.
-// These words are a view into `input`; empty words are omitted. `separators`
-// must neither be the empty string nor a nullptr.
-std::vector<nonstd::string_view> split_into_views(nonstd::string_view input,
-                                                  const char* separators);
-
-// Same as `split_into_views` but the words are copied from `input`.
-std::vector<std::string> split_into_strings(nonstd::string_view input,
-                                            const char* separators);
+// Split `string` into tokens at any of the characters in `separators`. These
+// tokens are views into `string`. `separators` must neither be the empty string
+// nor a nullptr.
+std::vector<nonstd::string_view> split_into_views(
+  nonstd::string_view string,
+  const char* separators,
+  util::Tokenizer::Mode mode = util::Tokenizer::Mode::skip_empty);
 
-// Return true if `prefix` is a prefix of `string`.
-inline bool
-starts_with(const char* string, nonstd::string_view prefix)
-{
-  // Optimized version of starts_with(string_view, string_view): avoid computing
-  // the length of the string argument.
-  return strncmp(string, prefix.data(), prefix.length()) == 0;
-}
-
-// Return true if `prefix` is a prefix of `string`.
-inline bool
-starts_with(nonstd::string_view string, nonstd::string_view prefix)
-{
-  return string.starts_with(prefix);
-}
+// Same as `split_into_views` but the tokens are copied from `string`.
+std::vector<std::string> split_into_strings(
+  nonstd::string_view string,
+  const char* separators,
+  util::Tokenizer::Mode mode = util::Tokenizer::Mode::skip_empty);
 
 // Returns a copy of string with the specified ANSI CSI sequences removed.
 [[nodiscard]] std::string strip_ansi_csi_seqs(nonstd::string_view string);
 
-// Strip whitespace from left and right side of a string.
-[[nodiscard]] std::string strip_whitespace(nonstd::string_view string);
-
 // Convert a string to lowercase.
 [[nodiscard]] std::string to_lowercase(nonstd::string_view string);
 
 // Traverse `path` recursively (postorder, i.e. files are visited before their
 // parent directory).
 //
-// Throws Error on error.
+// Throws core::Error on error.
 void traverse(const std::string& path, const TraverseVisitor& visitor);
 
 // Remove `path` (non-directory), NFS safe. Logs according to `unlink_log`.
 //
-// Returns whether removal was successful. A nonexistent `path` is considered
-// successful.
+// Returns whether removal was successful. A nonexistent `path` is considered a
+// failure.
 bool unlink_safe(const std::string& path,
                  UnlinkLog unlink_log = UnlinkLog::log_failure);
 
@@ -485,18 +389,18 @@ void update_mtime(const std::string& path);
 // Remove `path` (and its contents if it's a directory). A nonexistent path is
 // not considered an error.
 //
-// Throws Error on error.
+// Throws core::Error on error.
 void wipe_path(const std::string& path);
 
-// Write `size` bytes from `data` to `fd`. Throws `Error` on error.
+// Write `size` bytes from `data` to `fd`. Throws `core::Error` on error.
 void write_fd(int fd, const void* data, size_t size);
 
 // Write `data` to `path`. The file will be opened according to `open_mode`,
 // which always will include `std::ios::out` even if not specified at the call
 // site.
 //
-// Throws `Error` on error. The description contains the error message without
-// the path.
+// Throws `core::Error` on error. The description contains the error message
+// without the path.
 void write_file(const std::string& path,
                 const std::string& data,
                 std::ios_base::openmode open_mode = std::ios::binary);
index 1a9de6a0b71ca88f066db8069c6e0cc8a9fb5417..14e3e5b89fe4fdffb10069c7d2604f911397c6ed 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -143,12 +143,6 @@ gettimeofday(struct timeval* tp, struct timezone* /*tzp*/)
 }
 #endif
 
-void
-usleep(int64_t usec)
-{
-  std::this_thread::sleep_for(std::chrono::microseconds(usec));
-}
-
 struct tm*
 localtime_r(time_t* _clock, struct tm* _result)
 {
index 0a2f1a133c30ed470d4e23abd7613d6858e7ab57..c805dc7d8f41d0d3fdf01dbafc8999e02dac854a 100644 (file)
 
 #pragma once
 
-#include "system.hpp"
+#ifdef _WIN32
 
-#include <string>
+#  include <core/wincompat.hpp>
+
+#  include <string>
+
+struct tm* localtime_r(time_t* _clock, struct tm* _result);
+
+#  ifdef _MSC_VER
+int gettimeofday(struct timeval* tp, struct timezone* tzp);
+int asprintf(char** strp, const char* fmt, ...);
+#  endif
 
 namespace Win32Util {
 
@@ -44,3 +53,5 @@ std::string error_message(DWORD error_code);
 NTSTATUS get_last_ntstatus();
 
 } // namespace Win32Util
+
+#endif
diff --git a/src/ZstdCompressor.cpp b/src/ZstdCompressor.cpp
deleted file mode 100644 (file)
index 39f6bb2..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "ZstdCompressor.hpp"
-
-#include "Logging.hpp"
-#include "assertions.hpp"
-#include "exceptions.hpp"
-
-#include <algorithm>
-
-ZstdCompressor::ZstdCompressor(FILE* stream, int8_t compression_level)
-  : m_stream(stream),
-    m_zstd_stream(ZSTD_createCStream())
-{
-  if (compression_level == 0) {
-    compression_level = default_compression_level;
-    LOG("Using default compression level {}", compression_level);
-  }
-
-  // libzstd 1.3.4 and newer support negative levels. However, the query
-  // function ZSTD_minCLevel did not appear until 1.3.6, so perform detection
-  // based on version instead.
-  if (ZSTD_versionNumber() < 10304 && compression_level < 1) {
-    LOG(
-      "Using compression level 1 (minimum level supported by libzstd) instead"
-      " of {}",
-      compression_level);
-    compression_level = 1;
-  }
-
-  m_compression_level = std::min<int>(compression_level, ZSTD_maxCLevel());
-  if (m_compression_level != compression_level) {
-    LOG("Using compression level {} (max libzstd level) instead of {}",
-        m_compression_level,
-        compression_level);
-  }
-
-  size_t ret = ZSTD_initCStream(m_zstd_stream, m_compression_level);
-  if (ZSTD_isError(ret)) {
-    ZSTD_freeCStream(m_zstd_stream);
-    throw Error("error initializing zstd compression stream");
-  }
-}
-
-ZstdCompressor::~ZstdCompressor()
-{
-  ZSTD_freeCStream(m_zstd_stream);
-}
-
-int8_t
-ZstdCompressor::actual_compression_level() const
-{
-  return m_compression_level;
-}
-
-void
-ZstdCompressor::write(const void* data, size_t count)
-{
-  m_zstd_in.src = data;
-  m_zstd_in.size = count;
-  m_zstd_in.pos = 0;
-
-  int flush = data ? 0 : 1;
-
-  size_t ret;
-  while (m_zstd_in.pos < m_zstd_in.size) {
-    uint8_t buffer[READ_BUFFER_SIZE];
-    m_zstd_out.dst = buffer;
-    m_zstd_out.size = sizeof(buffer);
-    m_zstd_out.pos = 0;
-    ret = ZSTD_compressStream(m_zstd_stream, &m_zstd_out, &m_zstd_in);
-    ASSERT(!(ZSTD_isError(ret)));
-    size_t compressed_bytes = m_zstd_out.pos;
-    if (fwrite(buffer, 1, compressed_bytes, m_stream) != compressed_bytes
-        || ferror(m_stream)) {
-      throw Error("failed to write to zstd output stream ");
-    }
-  }
-  ret = flush;
-  while (ret > 0) {
-    uint8_t buffer[READ_BUFFER_SIZE];
-    m_zstd_out.dst = buffer;
-    m_zstd_out.size = sizeof(buffer);
-    m_zstd_out.pos = 0;
-    ret = ZSTD_endStream(m_zstd_stream, &m_zstd_out);
-    size_t compressed_bytes = m_zstd_out.pos;
-    if (fwrite(buffer, 1, compressed_bytes, m_stream) != compressed_bytes
-        || ferror(m_stream)) {
-      throw Error("failed to write to zstd output stream");
-    }
-  }
-}
-
-void
-ZstdCompressor::finalize()
-{
-  write(nullptr, 0);
-}
diff --git a/src/ZstdCompressor.hpp b/src/ZstdCompressor.hpp
deleted file mode 100644 (file)
index 58ca453..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include "system.hpp"
-
-#include "Compressor.hpp"
-#include "NonCopyable.hpp"
-
-#include <zstd.h>
-
-// A compressor of a Zstandard stream.
-class ZstdCompressor : public Compressor, NonCopyable
-{
-public:
-  // Parameters:
-  // - stream: The file to write data to.
-  // - compression_level: Desired compression level.
-  ZstdCompressor(FILE* stream, int8_t compression_level);
-
-  ~ZstdCompressor() override;
-
-  int8_t actual_compression_level() const override;
-  void write(const void* data, size_t count) override;
-  void finalize() override;
-
-  constexpr static uint8_t default_compression_level = 1;
-
-private:
-  FILE* m_stream;
-  ZSTD_CStream* m_zstd_stream;
-  ZSTD_inBuffer m_zstd_in;
-  ZSTD_outBuffer m_zstd_out;
-  int8_t m_compression_level;
-};
diff --git a/src/ZstdDecompressor.cpp b/src/ZstdDecompressor.cpp
deleted file mode 100644 (file)
index c08d0f9..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "ZstdDecompressor.hpp"
-
-#include "assertions.hpp"
-#include "exceptions.hpp"
-
-ZstdDecompressor::ZstdDecompressor(FILE* stream)
-  : m_stream(stream),
-    m_input_size(0),
-    m_input_consumed(0),
-    m_zstd_stream(ZSTD_createDStream()),
-    m_reached_stream_end(false)
-{
-  size_t ret = ZSTD_initDStream(m_zstd_stream);
-  if (ZSTD_isError(ret)) {
-    ZSTD_freeDStream(m_zstd_stream);
-    throw Error("failed to initialize zstd decompression stream");
-  }
-}
-
-ZstdDecompressor::~ZstdDecompressor()
-{
-  ZSTD_freeDStream(m_zstd_stream);
-}
-
-void
-ZstdDecompressor::read(void* data, size_t count)
-{
-  size_t bytes_read = 0;
-  while (bytes_read < count) {
-    ASSERT(m_input_size >= m_input_consumed);
-    if (m_input_size == m_input_consumed) {
-      m_input_size = fread(m_input_buffer, 1, sizeof(m_input_buffer), m_stream);
-      if (m_input_size == 0) {
-        throw Error("failed to read from zstd input stream");
-      }
-      m_input_consumed = 0;
-    }
-
-    m_zstd_in.src = (m_input_buffer + m_input_consumed);
-    m_zstd_in.size = m_input_size - m_input_consumed;
-    m_zstd_in.pos = 0;
-
-    m_zstd_out.dst = static_cast<uint8_t*>(data) + bytes_read;
-    m_zstd_out.size = count - bytes_read;
-    m_zstd_out.pos = 0;
-    size_t ret = ZSTD_decompressStream(m_zstd_stream, &m_zstd_out, &m_zstd_in);
-    if (ZSTD_isError(ret)) {
-      throw Error("failed to read from zstd input stream");
-    }
-    if (ret == 0) {
-      m_reached_stream_end = true;
-      break;
-    }
-    bytes_read += m_zstd_out.pos;
-    m_input_consumed += m_zstd_in.pos;
-  }
-}
-
-void
-ZstdDecompressor::finalize()
-{
-  if (!m_reached_stream_end) {
-    throw Error("garbage data at end of zstd input stream");
-  }
-}
diff --git a/src/ZstdDecompressor.hpp b/src/ZstdDecompressor.hpp
deleted file mode 100644 (file)
index 8f85ce6..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include "system.hpp"
-
-#include "Decompressor.hpp"
-
-#include <fstream>
-#include <zstd.h>
-
-// A decompressor of a Zstandard stream.
-class ZstdDecompressor : public Decompressor
-{
-public:
-  // Parameters:
-  // - stream: The file to read data from.
-  explicit ZstdDecompressor(FILE* stream);
-
-  ~ZstdDecompressor() override;
-
-  void read(void* data, size_t count) override;
-  void finalize() override;
-
-private:
-  FILE* m_stream;
-  char m_input_buffer[READ_BUFFER_SIZE];
-  size_t m_input_size;
-  size_t m_input_consumed;
-  ZSTD_DStream* m_zstd_stream;
-  ZSTD_inBuffer m_zstd_in;
-  ZSTD_outBuffer m_zstd_out;
-  bool m_reached_stream_end;
-};
index 92b929e09b29f3e3eb3bf4c62160e623503f0222..c77dd1125031e2c4357f4eae7c3eb1668d1810ab 100644 (file)
 #include "argprocessing.hpp"
 
 #include "Context.hpp"
-#include "FormatNonstdStringView.hpp"
 #include "Logging.hpp"
 #include "assertions.hpp"
 #include "compopt.hpp"
 #include "fmtmacros.hpp"
 #include "language.hpp"
 
+#include <core/wincompat.hpp>
+#include <util/string.hpp>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
 #include <cassert>
 
+using core::Statistic;
 using nonstd::nullopt;
 using nonstd::optional;
 using nonstd::string_view;
@@ -46,6 +53,7 @@ struct ArgumentProcessingState
   ColorDiagnostics color_diagnostics = ColorDiagnostics::automatic;
   bool found_directives_only = false;
   bool found_rewrite_includes = false;
+  nonstd::optional<std::string> found_xarch_arch;
 
   std::string explicit_language;    // As specified with -x.
   std::string input_charset_option; // -finput-charset=...
@@ -93,7 +101,8 @@ bool
 color_output_possible()
 {
   const char* term_env = getenv("TERM");
-  return isatty(STDERR_FILENO) && term_env && strcasecmp(term_env, "DUMB") != 0;
+  return isatty(STDERR_FILENO) && term_env
+         && Util::to_lowercase(term_env) != "dumb";
 }
 
 bool
@@ -156,7 +165,7 @@ process_profiling_option(Context& ctx, const std::string& arg)
   std::string new_profile_path;
   bool new_profile_use = false;
 
-  if (Util::starts_with(arg, "-fprofile-dir=")) {
+  if (util::starts_with(arg, "-fprofile-dir=")) {
     new_profile_path = arg.substr(arg.find('=') + 1);
   } else if (arg == "-fprofile-generate" || arg == "-fprofile-instr-generate") {
     ctx.args_info.profile_generate = true;
@@ -166,8 +175,8 @@ process_profiling_option(Context& ctx, const std::string& arg)
       // GCC uses $PWD/$(basename $obj).
       new_profile_path = ctx.apparent_cwd;
     }
-  } else if (Util::starts_with(arg, "-fprofile-generate=")
-             || Util::starts_with(arg, "-fprofile-instr-generate=")) {
+  } else if (util::starts_with(arg, "-fprofile-generate=")
+             || util::starts_with(arg, "-fprofile-instr-generate=")) {
     ctx.args_info.profile_generate = true;
     new_profile_path = arg.substr(arg.find('=') + 1);
   } else if (arg == "-fprofile-use" || arg == "-fprofile-instr-use"
@@ -177,10 +186,10 @@ process_profiling_option(Context& ctx, const std::string& arg)
     if (ctx.args_info.profile_path.empty()) {
       new_profile_path = ".";
     }
-  } else if (Util::starts_with(arg, "-fprofile-use=")
-             || Util::starts_with(arg, "-fprofile-instr-use=")
-             || Util::starts_with(arg, "-fprofile-sample-use=")
-             || Util::starts_with(arg, "-fauto-profile=")) {
+  } else if (util::starts_with(arg, "-fprofile-use=")
+             || util::starts_with(arg, "-fprofile-instr-use=")
+             || util::starts_with(arg, "-fprofile-sample-use=")
+             || util::starts_with(arg, "-fauto-profile=")) {
     new_profile_use = true;
     new_profile_path = arg.substr(arg.find('=') + 1);
   } else {
@@ -234,7 +243,7 @@ process_arg(Context& ctx,
 
   // Ignore clang -ivfsoverlay <arg> to not detect multiple input files.
   if (args[i] == "-ivfsoverlay"
-      && !(config.sloppiness() & SLOPPY_IVFSOVERLAY)) {
+      && !(config.sloppiness().is_enabled(core::Sloppy::ivfsoverlay))) {
     LOG_RAW(
       "You have to specify \"ivfsoverlay\" sloppiness when using"
       " -ivfsoverlay to get hits");
@@ -247,7 +256,7 @@ process_arg(Context& ctx,
   }
 
   // Handle "@file" argument.
-  if (Util::starts_with(args[i], "@") || Util::starts_with(args[i], "-@")) {
+  if (util::starts_with(args[i], "@") || util::starts_with(args[i], "-@")) {
     const char* argpath = args[i].c_str() + 1;
 
     if (argpath[-1] == '-') {
@@ -289,8 +298,8 @@ process_arg(Context& ctx,
   }
 
   // These are always too hard.
-  if (compopt_too_hard(args[i]) || Util::starts_with(args[i], "-fdump-")
-      || Util::starts_with(args[i], "-MJ")) {
+  if (compopt_too_hard(args[i]) || util::starts_with(args[i], "-fdump-")
+      || util::starts_with(args[i], "-MJ")) {
     LOG("Compiler option {} is unsupported", args[i]);
     return Statistic::unsupported_compiler_option;
   }
@@ -302,9 +311,21 @@ process_arg(Context& ctx,
   }
 
   // -Xarch_* options are too hard.
-  if (Util::starts_with(args[i], "-Xarch_")) {
-    LOG("Unsupported compiler option: {}", args[i]);
-    return Statistic::unsupported_compiler_option;
+  if (util::starts_with(args[i], "-Xarch_")) {
+    if (i == args.size() - 1) {
+      LOG("Missing argument to {}", args[i]);
+      return Statistic::bad_compiler_arguments;
+    }
+    const auto arch = args[i].substr(7);
+    if (!state.found_xarch_arch) {
+      state.found_xarch_arch = arch;
+    } else if (*state.found_xarch_arch != arch) {
+      LOG_RAW("Multiple different -Xarch_* options not supported");
+      return Statistic::unsupported_compiler_option;
+    }
+    state.common_args.push_back(args[i]);
+    state.common_args.push_back(args[i + 1]);
+    return nullopt;
   }
 
   // Handle -arch options.
@@ -320,7 +341,7 @@ process_arg(Context& ctx,
   // Some arguments that clang passes directly to cc1 (related to precompiled
   // headers) need the usual ccache handling. In those cases, the -Xclang
   // prefix is skipped and the cc1 argument is handled instead.
-  if (args[i] == "-Xclang" && i < args.size() - 1
+  if (args[i] == "-Xclang" && i + 1 < args.size()
       && (args[i + 1] == "-emit-pch" || args[i + 1] == "-emit-pth"
           || args[i + 1] == "-include-pch" || args[i + 1] == "-include-pth"
           || args[i + 1] == "-fno-pch-timestamp")) {
@@ -367,7 +388,7 @@ process_arg(Context& ctx,
       LOG("Compiler option {} is unsupported without direct depend mode",
           args[i]);
       return Statistic::could_not_use_modules;
-    } else if (!(config.sloppiness() & SLOPPY_MODULES)) {
+    } else if (!(config.sloppiness().is_enabled(core::Sloppy::modules))) {
       LOG_RAW(
         "You have to specify \"modules\" sloppiness when using"
         " -fmodules to get hits");
@@ -395,7 +416,7 @@ process_arg(Context& ctx,
     return nullopt;
   }
 
-  if (Util::starts_with(args[i], "-x")) {
+  if (util::starts_with(args[i], "-x")) {
     if (args[i].length() >= 3 && !islower(args[i][2])) {
       // -xCODE (where CODE can be e.g. Host or CORE-AVX2, always starting with
       // an uppercase letter) is an ordinary Intel compiler option, not a
@@ -438,15 +459,15 @@ process_arg(Context& ctx,
   }
 
   // Alternate form of -o with no space. Nvcc does not support this.
-  if (Util::starts_with(args[i], "-o")
+  if (util::starts_with(args[i], "-o")
       && config.compiler_type() != CompilerType::nvcc) {
     args_info.output_obj =
       Util::make_relative_path(ctx, string_view(args[i]).substr(2));
     return nullopt;
   }
 
-  if (Util::starts_with(args[i], "-fdebug-prefix-map=")
-      || Util::starts_with(args[i], "-ffile-prefix-map=")) {
+  if (util::starts_with(args[i], "-fdebug-prefix-map=")
+      || util::starts_with(args[i], "-ffile-prefix-map=")) {
     std::string map = args[i].substr(args[i].find('=') + 1);
     args_info.debug_prefix_maps.push_back(map);
     state.common_args.push_back(args[i]);
@@ -455,17 +476,17 @@ process_arg(Context& ctx,
 
   // Debugging is handled specially, so that we know if we can strip line
   // number info.
-  if (Util::starts_with(args[i], "-g")) {
+  if (util::starts_with(args[i], "-g")) {
     state.common_args.push_back(args[i]);
 
-    if (Util::starts_with(args[i], "-gdwarf")) {
+    if (util::starts_with(args[i], "-gdwarf")) {
       // Selection of DWARF format (-gdwarf or -gdwarf-<version>) enables
       // debug info on level 2.
       args_info.generating_debuginfo = true;
       return nullopt;
     }
 
-    if (Util::starts_with(args[i], "-gz")) {
+    if (util::starts_with(args[i], "-gz")) {
       // -gz[=type] neither disables nor enables debug info.
       return nullopt;
     }
@@ -496,7 +517,7 @@ process_arg(Context& ctx,
     return nullopt;
   }
 
-  if (Util::starts_with(args[i], "-MF")) {
+  if (util::starts_with(args[i], "-MF")) {
     state.dependency_filename_specified = true;
 
     std::string dep_file;
@@ -524,7 +545,7 @@ process_arg(Context& ctx,
     return nullopt;
   }
 
-  if (Util::starts_with(args[i], "-MQ") || Util::starts_with(args[i], "-MT")) {
+  if (util::starts_with(args[i], "-MQ") || util::starts_with(args[i], "-MT")) {
     ctx.args_info.dependency_target_specified = true;
 
     if (args[i].size() == 3) {
@@ -578,8 +599,8 @@ process_arg(Context& ctx,
     return nullopt;
   }
 
-  if (Util::starts_with(args[i], "-fprofile-")
-      || Util::starts_with(args[i], "-fauto-profile")
+  if (util::starts_with(args[i], "-fprofile-")
+      || util::starts_with(args[i], "-fauto-profile")
       || args[i] == "-fbranch-probabilities") {
     if (!process_profiling_option(ctx, args[i])) {
       // The failure is logged by process_profiling_option.
@@ -589,13 +610,13 @@ process_arg(Context& ctx,
     return nullopt;
   }
 
-  if (Util::starts_with(args[i], "-fsanitize-blacklist=")) {
+  if (util::starts_with(args[i], "-fsanitize-blacklist=")) {
     args_info.sanitize_blacklists.emplace_back(args[i].substr(21));
     state.common_args.push_back(args[i]);
     return nullopt;
   }
 
-  if (Util::starts_with(args[i], "--sysroot=")) {
+  if (util::starts_with(args[i], "--sysroot=")) {
     auto path = string_view(args[i]).substr(10);
     auto relpath = Util::make_relative_path(ctx, path);
     state.common_args.push_back("--sysroot=" + relpath);
@@ -636,12 +657,12 @@ process_arg(Context& ctx,
     return nullopt;
   }
 
-  if (Util::starts_with(args[i], "-Wp,")) {
+  if (util::starts_with(args[i], "-Wp,")) {
     if (args[i].find(",-P,") != std::string::npos
-        || Util::ends_with(args[i], ",-P")) {
+        || util::ends_with(args[i], ",-P")) {
       // -P together with other preprocessor options is just too hard.
       return Statistic::unsupported_compiler_option;
-    } else if (Util::starts_with(args[i], "-Wp,-MD,")
+    } else if (util::starts_with(args[i], "-Wp,-MD,")
                && args[i].find(',', 8) == std::string::npos) {
       args_info.generating_dependencies = true;
       state.dependency_filename_specified = true;
@@ -649,7 +670,7 @@ process_arg(Context& ctx,
         Util::make_relative_path(ctx, string_view(args[i]).substr(8));
       state.dep_args.push_back(args[i]);
       return nullopt;
-    } else if (Util::starts_with(args[i], "-Wp,-MMD,")
+    } else if (util::starts_with(args[i], "-Wp,-MMD,")
                && args[i].find(',', 9) == std::string::npos) {
       args_info.generating_dependencies = true;
       state.dependency_filename_specified = true;
@@ -657,13 +678,13 @@ process_arg(Context& ctx,
         Util::make_relative_path(ctx, string_view(args[i]).substr(9));
       state.dep_args.push_back(args[i]);
       return nullopt;
-    } else if (Util::starts_with(args[i], "-Wp,-D")
+    } else if (util::starts_with(args[i], "-Wp,-D")
                && args[i].find(',', 6) == std::string::npos) {
       // Treat it like -D.
       state.cpp_args.push_back(args[i].substr(4));
       return nullopt;
     } else if (args[i] == "-Wp,-MP"
-               || (args[i].size() > 8 && Util::starts_with(args[i], "-Wp,-M")
+               || (args[i].size() > 8 && util::starts_with(args[i], "-Wp,-M")
                    && args[i][7] == ','
                    && (args[i][6] == 'F' || args[i][6] == 'Q'
                        || args[i][6] == 'T')
@@ -689,7 +710,7 @@ process_arg(Context& ctx,
   }
 
   // Input charset needs to be handled specially.
-  if (Util::starts_with(args[i], "-finput-charset=")) {
+  if (util::starts_with(args[i], "-finput-charset=")) {
     state.input_charset_option = args[i];
     return nullopt;
   }
@@ -720,7 +741,7 @@ process_arg(Context& ctx,
 
   // In the "-Xclang -fcolor-diagnostics" form, -Xclang is skipped and the
   // -fcolor-diagnostics argument which is passed to cc1 is handled below.
-  if (args[i] == "-Xclang" && i < args.size() - 1
+  if (args[i] == "-Xclang" && i + 1 < args.size()
       && args[i + 1] == "-fcolor-diagnostics") {
     state.compiler_only_args_no_hash.push_back(args[i]);
     ++i;
@@ -768,7 +789,7 @@ process_arg(Context& ctx,
     return nullopt;
   }
 
-  if (config.sloppiness() & SLOPPY_CLANG_INDEX_STORE
+  if (config.sloppiness().is_enabled(core::Sloppy::clang_index_store)
       && args[i] == "-index-store-path") {
     // Xcode 9 or later calls Clang with this option. The given path includes a
     // UUID that might lead to cache misses, especially when cache is shared
@@ -1039,7 +1060,7 @@ process_args(Context& ctx)
 
   if (state.found_pch || state.found_fpch_preprocess) {
     args_info.using_precompiled_header = true;
-    if (!(config.sloppiness() & SLOPPY_TIME_MACROS)) {
+    if (!(config.sloppiness().is_enabled(core::Sloppy::time_macros))) {
       LOG_RAW(
         "You have to specify \"time_macros\" sloppiness when using"
         " precompiled headers to get direct hits");
@@ -1075,7 +1096,7 @@ process_args(Context& ctx)
   }
 
   if (args_info.output_is_precompiled_header
-      && !(config.sloppiness() & SLOPPY_PCH_DEFINES)) {
+      && !(config.sloppiness().is_enabled(core::Sloppy::pch_defines))) {
     LOG_RAW(
       "You have to specify \"pch_defines,time_macros\" sloppiness when"
       " creating precompiled headers");
@@ -1254,6 +1275,17 @@ process_args(Context& ctx)
     compiler_args.push_back("-dc");
   }
 
+  if (state.found_xarch_arch && !args_info.arch_args.empty()) {
+    if (args_info.arch_args.size() > 1) {
+      LOG_RAW(
+        "Multiple -arch options in combination with -Xarch_* not supported");
+      return Statistic::unsupported_compiler_option;
+    } else if (args_info.arch_args[0] != *state.found_xarch_arch) {
+      LOG_RAW("-arch option not matching -Xarch_* option not supported");
+      return Statistic::unsupported_compiler_option;
+    }
+  }
+
   for (const auto& arch : args_info.arch_args) {
     compiler_args.push_back("-arch");
     compiler_args.push_back(arch);
index a8e8f3aee358839c789dc4ed3674f76d6a1b92aa..54671e8f944c0eb25641a1e25de075feed0060ea 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -19,7 +19,8 @@
 #pragma once
 
 #include "Args.hpp"
-#include "Statistic.hpp"
+
+#include <core/Statistic.hpp>
 
 #include "third_party/nonstd/optional.hpp"
 
@@ -27,14 +28,14 @@ class Context;
 
 struct ProcessArgsResult
 {
-  ProcessArgsResult(Statistic error_);
+  ProcessArgsResult(core::Statistic error_);
   ProcessArgsResult(const Args& preprocessor_args_,
                     const Args& extra_args_to_hash_,
                     const Args& compiler_args_);
 
   // nullopt on success, otherwise the statistics counter that should be
   // incremented.
-  nonstd::optional<Statistic> error;
+  nonstd::optional<core::Statistic> error;
 
   // Arguments (except -E) to send to the preprocessor.
   Args preprocessor_args;
@@ -46,7 +47,8 @@ struct ProcessArgsResult
   Args compiler_args;
 };
 
-inline ProcessArgsResult::ProcessArgsResult(Statistic error_) : error(error_)
+inline ProcessArgsResult::ProcessArgsResult(core::Statistic error_)
+  : error(error_)
 {
 }
 
index 040c3a5f63c93f95507f801cf45e00e47e2e1a14..d88cb10f5675b6a3a15d6fafe7b4a584bc10f0d6 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,7 +18,7 @@
 
 #pragma once
 
-#include "system.hpp"
+#include <cstddef>
 
 #ifdef _MSC_VER
 #  define CCACHE_FUNCTION __func__
index 761b9ab437a2f75e39132428ad1bec81663946bd..1d73e4dc0d6162c1fb9815b25e01b45672c8fc15 100644 (file)
@@ -21,8 +21,6 @@
 
 #include "Args.hpp"
 #include "ArgsInfo.hpp"
-#include "Checksum.hpp"
-#include "Compression.hpp"
 #include "Context.hpp"
 #include "Depfile.hpp"
 #include "Fd.hpp"
 #include "Logging.hpp"
 #include "Manifest.hpp"
 #include "MiniTrace.hpp"
-#include "ProgressBar.hpp"
 #include "Result.hpp"
-#include "ResultDumper.hpp"
-#include "ResultExtractor.hpp"
 #include "ResultRetriever.hpp"
 #include "SignalHandler.hpp"
-#include "Statistics.hpp"
-#include "StdMakeUnique.hpp"
 #include "TemporaryFile.hpp"
 #include "UmaskScope.hpp"
 #include "Util.hpp"
+#include "Win32Util.hpp"
 #include "argprocessing.hpp"
-#include "cleanup.hpp"
 #include "compopt.hpp"
-#include "compress.hpp"
-#include "exceptions.hpp"
 #include "execute.hpp"
 #include "fmtmacros.hpp"
 #include "hashutil.hpp"
 #include "language.hpp"
 
+#include <compression/types.hpp>
+#include <core/Statistics.hpp>
+#include <core/StatsLog.hpp>
+#include <core/exceptions.hpp>
+#include <core/mainoptions.hpp>
+#include <core/types.hpp>
+#include <core/wincompat.hpp>
+#include <storage/Storage.hpp>
+#include <util/expected.hpp>
+#include <util/path.hpp>
+#include <util/string.hpp>
+
 #include "third_party/fmt/core.h"
 #include "third_party/nonstd/optional.hpp"
 #include "third_party/nonstd/string_view.hpp"
 
-#ifdef HAVE_GETOPT_LONG
-#  include <getopt.h>
-#elif defined(_WIN32)
-#  include "third_party/win32/getopt.h"
-#else
-extern "C" {
-#  include "third_party/getopt_long.h"
-}
-#endif
+#include <fcntl.h>
 
-#ifdef _WIN32
-#  include "Win32Util.hpp"
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
 #endif
 
 #include <algorithm>
 #include <cmath>
 #include <limits>
+#include <memory>
 
 #ifndef MYNAME
 #  define MYNAME "ccache"
 #endif
 const char CCACHE_NAME[] = MYNAME;
 
+using core::Statistic;
 using nonstd::nullopt;
 using nonstd::optional;
 using nonstd::string_view;
 
-constexpr const char VERSION_TEXT[] =
-  R"({} version {}
-
-Copyright (C) 2002-2007 Andrew Tridgell
-Copyright (C) 2009-2021 Joel Rosdahl and other contributors
-
-See <https://ccache.dev/credits.html> for a complete list of contributors.
-
-This program is free software; you can redistribute it and/or modify it under
-the terms of the GNU General Public License as published by the Free Software
-Foundation; either version 3 of the License, or (at your option) any later
-version.
-)";
-
-constexpr const char USAGE_TEXT[] =
-  R"(Usage:
-    {} [options]
-    {} compiler [compiler options]
-    compiler [compiler options]          (via symbolic link)
-
-Common options:
-    -c, --cleanup              delete old files and recalculate size counters
-                               (normally not needed as this is done
-                               automatically)
-    -C, --clear                clear the cache completely (except configuration)
-        --config-path PATH     operate on configuration file PATH instead of the
-                               default
-    -d, --directory PATH       operate on cache directory PATH instead of the
-                               default
-        --evict-older-than AGE remove files older than AGE (unsigned integer
-                               with a d (days) or s (seconds) suffix)
-    -F, --max-files NUM        set maximum number of files in cache to NUM (use
-                               0 for no limit)
-    -M, --max-size SIZE        set maximum size of cache to SIZE (use 0 for no
-                               limit); available suffixes: k, M, G, T (decimal)
-                               and Ki, Mi, Gi, Ti (binary); default suffix: G
-    -X, --recompress LEVEL     recompress the cache to level LEVEL (integer or
-                               "uncompressed") using the Zstandard algorithm;
-                               see "Cache compression" in the manual for details
-    -o, --set-config KEY=VAL   set configuration item KEY to value VAL
-    -x, --show-compression     show compression statistics
-    -p, --show-config          show current configuration options in
-                               human-readable format
-    -s, --show-stats           show summary of configuration and statistics
-                               counters in human-readable format
-    -z, --zero-stats           zero statistics counters
-
-    -h, --help                 print this help text
-    -V, --version              print version and copyright information
-
-Options for scripting or debugging:
-        --checksum-file PATH   print the checksum (64 bit XXH3) of the file at
-                               PATH
-        --dump-manifest PATH   dump manifest file at PATH in text format
-        --dump-result PATH     dump result file at PATH in text format
-        --extract-result PATH  extract data stored in result file at PATH to the
-                               current working directory
-    -k, --get-config KEY       print the value of configuration key KEY
-        --hash-file PATH       print the hash (160 bit BLAKE3) of the file at
-                               PATH
-        --print-stats          print statistics counter IDs and corresponding
-                               values in machine-parsable format
-
-See also the manual on <https://ccache.dev/documentation.html>.
-)";
-
-// How often (in seconds) to scan $CCACHE_DIR/tmp for left-over temporary
-// files.
-const int k_tempdir_cleanup_interval = 2 * 24 * 60 * 60; // 2 days
-
-// Maximum files per cache directory. This constant is somewhat arbitrarily
-// chosen to be large enough to avoid unnecessary cache levels but small enough
-// not to make esoteric file systems (with bad performance for large
-// directories) too slow. It could be made configurable, but hopefully there
-// will be no need to do that.
-const uint64_t k_max_cache_files_per_directory = 2000;
-
-// Minimum number of cache levels ($CCACHE_DIR/1/2/stored_file).
-const uint8_t k_min_cache_levels = 2;
-
-// Maximum number of cache levels ($CCACHE_DIR/1/2/3/stored_file).
-//
-// On a cache miss, (k_max_cache_levels - k_min_cache_levels + 1) cache lookups
-// (i.e. stat system calls) will be performed for a cache entry.
-//
-// An assumption made here is that if a cache is so large that it holds more
-// than 16^4 * k_max_cache_files_per_directory files then we can assume that the
-// file system is sane enough to handle more than
-// k_max_cache_files_per_directory.
-const uint8_t k_max_cache_levels = 4;
-
 // This is a string that identifies the current "version" of the hash sum
 // computed by ccache. If, for any reason, we want to force the hash sum to be
 // different for the same input in a new ccache version, we can just change
@@ -186,28 +92,38 @@ const char HASH_PREFIX[] = "3";
 
 namespace {
 
-// Throw a Failure if ccache did not succeed in getting or putting a result in
-// the cache. If `exit_code` is set, just exit with that code directly,
-// otherwise execute the real compiler and exit with its exit code. Also updates
-// statistics counter `statistic` if it's not `Statistic::none`.
-class Failure : public std::exception
+// Return nonstd::make_unexpected<Failure> if ccache did not succeed in getting
+// or putting a result in the cache. If `exit_code` is set, ccache will just
+// exit with that code directly, otherwise execute the real compiler and exit
+// with its exit code. Statistics counters will also be incremented.
+class Failure
 {
 public:
-  Failure(Statistic statistic,
-          nonstd::optional<int> exit_code = nonstd::nullopt);
+  Failure(Statistic statistic);
+  Failure(std::initializer_list<Statistic> statistics);
 
+  const core::StatisticsCounters& counters() const;
   nonstd::optional<int> exit_code() const;
-  Statistic statistic() const;
+  void set_exit_code(int exit_code);
 
 private:
-  Statistic m_statistic;
+  core::StatisticsCounters m_counters;
   nonstd::optional<int> m_exit_code;
 };
 
-inline Failure::Failure(Statistic statistic, nonstd::optional<int> exit_code)
-  : m_statistic(statistic),
-    m_exit_code(exit_code)
+inline Failure::Failure(const Statistic statistic) : m_counters({statistic})
+{
+}
+
+inline Failure::Failure(const std::initializer_list<Statistic> statistics)
+  : m_counters(statistics)
+{
+}
+
+inline const core::StatisticsCounters&
+Failure::counters() const
 {
+  return m_counters;
 }
 
 inline nonstd::optional<int>
@@ -216,10 +132,10 @@ Failure::exit_code() const
   return m_exit_code;
 }
 
-inline Statistic
-Failure::statistic() const
+inline void
+Failure::set_exit_code(const int exit_code)
 {
-  return m_statistic;
+  m_exit_code = exit_code;
 }
 
 } // namespace
@@ -235,7 +151,7 @@ add_prefix(const Context& ctx, Args& args, const std::string& prefix_command)
   for (const auto& word : Util::split_into_strings(prefix_command, " ")) {
     std::string path = find_executable(ctx, word, CCACHE_NAME);
     if (path.empty()) {
-      throw Fatal("{}: {}", word, strerror(errno));
+      throw core::Fatal("{}: {}", word, strerror(errno));
     }
 
     prefix.push_back(path);
@@ -247,44 +163,20 @@ add_prefix(const Context& ctx, Args& args, const std::string& prefix_command)
   }
 }
 
-static void
-clean_up_internal_tempdir(const Config& config)
-{
-  time_t now = time(nullptr);
-  auto dir_st = Stat::stat(config.cache_dir(), Stat::OnError::log);
-  if (!dir_st || dir_st.mtime() + k_tempdir_cleanup_interval >= now) {
-    // No cleanup needed.
-    return;
-  }
-
-  Util::update_mtime(config.cache_dir());
-
-  const std::string& temp_dir = config.temporary_dir();
-  if (!Stat::lstat(temp_dir)) {
-    return;
-  }
-
-  Util::traverse(temp_dir, [now](const std::string& path, bool is_dir) {
-    if (is_dir) {
-      return;
-    }
-    auto st = Stat::lstat(path, Stat::OnError::log);
-    if (st && st.mtime() + k_tempdir_cleanup_interval < now) {
-      Util::unlink_tmp(path);
-    }
-  });
-}
-
 static std::string
 prepare_debug_path(const std::string& debug_dir,
                    const std::string& output_obj,
                    string_view suffix)
 {
-  const std::string prefix =
-    debug_dir.empty() ? output_obj : debug_dir + Util::real_path(output_obj);
+  auto prefix = debug_dir.empty()
+                  ? output_obj
+                  : debug_dir + util::to_absolute_path(output_obj);
+#ifdef _WIN32
+  prefix.erase(std::remove(prefix.begin(), prefix.end(), ':'), prefix.end());
+#endif
   try {
     Util::ensure_dir_exists(Util::dir_name(prefix));
-  } catch (Error&) {
+  } catch (core::Error&) {
     // Ignore since we can't handle an error in another way in this context. The
     // caller takes care of logging when trying to open the path for writing.
   }
@@ -326,7 +218,7 @@ guess_compiler(string_view path)
     if (symlink_value.empty()) {
       break;
     }
-    if (Util::is_absolute_path(symlink_value)) {
+    if (util::is_absolute_path(symlink_value)) {
       compiler_path = symlink_value;
     } else {
       compiler_path =
@@ -358,14 +250,14 @@ include_file_too_new(const Context& ctx,
   // The comparison using >= is intentional, due to a possible race between
   // starting compilation and writing the include file. See also the notes under
   // "Performance" in doc/MANUAL.adoc.
-  if (!(ctx.config.sloppiness() & SLOPPY_INCLUDE_FILE_MTIME)
+  if (!(ctx.config.sloppiness().is_enabled(core::Sloppy::include_file_mtime))
       && path_stat.mtime() >= ctx.time_of_compilation) {
     LOG("Include file {} too new", path);
     return true;
   }
 
   // The same >= logic as above applies to the change time of the file.
-  if (!(ctx.config.sloppiness() & SLOPPY_INCLUDE_FILE_CTIME)
+  if (!(ctx.config.sloppiness().is_enabled(core::Sloppy::include_file_ctime))
       && path_stat.ctime() >= ctx.time_of_compilation) {
     LOG("Include file {} ctime too new", path);
     return true;
@@ -394,13 +286,14 @@ do_remember_include_file(Context& ctx,
     return true;
   }
 
-  if (system && (ctx.config.sloppiness() & SLOPPY_SYSTEM_HEADERS)) {
+  if (system
+      && (ctx.config.sloppiness().is_enabled(core::Sloppy::system_headers))) {
     // Don't remember this system header.
     return true;
   }
 
   // Canonicalize path for comparison; Clang uses ./header.h.
-  if (Util::starts_with(path, "./")) {
+  if (util::starts_with(path, "./")) {
     path.erase(0, 2);
   }
 
@@ -543,10 +436,7 @@ print_included_files(const Context& ctx, FILE* fp)
 // - Makes include file paths for which the base directory is a prefix relative
 //   when computing the hash sum.
 // - Stores the paths and hashes of included files in ctx.included_files.
-//
-// Returns Statistic::none on success, otherwise a statistics counter to be
-// incremented.
-static Statistic
+static nonstd::expected<void, Failure>
 process_preprocessed_file(Context& ctx,
                           Hash& hash,
                           const std::string& path,
@@ -555,8 +445,8 @@ process_preprocessed_file(Context& ctx,
   std::string data;
   try {
     data = Util::read_file(path);
-  } catch (Error&) {
-    return Statistic::internal_error;
+  } catch (core::Error&) {
+    return nonstd::make_unexpected(Statistic::internal_error);
   }
 
   // Bytes between p and q are pending to be hashed.
@@ -598,14 +488,14 @@ process_preprocessed_file(Context& ctx,
         // GCC:
         && ((q[1] == ' ' && q[2] >= '0' && q[2] <= '9')
             // GCC precompiled header:
-            || Util::starts_with(&q[1], pragma_gcc_pch_preprocess)
+            || util::starts_with(&q[1], pragma_gcc_pch_preprocess)
             // HP/AIX:
             || (q[1] == 'l' && q[2] == 'i' && q[3] == 'n' && q[4] == 'e'
                 && q[5] == ' '))
         && (q == data.data() || q[-1] == '\n')) {
       // Workarounds for preprocessor linemarker bugs in GCC version 6.
       if (q[2] == '3') {
-        if (Util::starts_with(q, hash_31_command_line_newline)) {
+        if (util::starts_with(q, hash_31_command_line_newline)) {
           // Bogus extra line with #31, after the regular #1: Ignore the whole
           // line, and continue parsing.
           hash.hash(p, q - p);
@@ -615,7 +505,7 @@ process_preprocessed_file(Context& ctx,
           q++;
           p = q;
           continue;
-        } else if (Util::starts_with(q, hash_32_command_line_2_newline)) {
+        } else if (util::starts_with(q, hash_32_command_line_2_newline)) {
           // Bogus wrong line with #32, instead of regular #1: Replace the line
           // number with the usual one.
           hash.hash(p, q - p);
@@ -637,7 +527,7 @@ process_preprocessed_file(Context& ctx,
       q++;
       if (q >= end) {
         LOG_RAW("Failed to parse included file path");
-        return Statistic::internal_error;
+        return nonstd::make_unexpected(Statistic::internal_error);
       }
       // q points to the beginning of an include file path
       hash.hash(p, q - p);
@@ -657,14 +547,14 @@ process_preprocessed_file(Context& ctx,
       // p and q span the include file path.
       std::string inc_path(p, q - p);
       if (!ctx.has_absolute_include_headers) {
-        ctx.has_absolute_include_headers = Util::is_absolute_path(inc_path);
+        ctx.has_absolute_include_headers = util::is_absolute_path(inc_path);
       }
       inc_path = Util::make_relative_path(ctx, inc_path);
 
       bool should_hash_inc_path = true;
       if (!ctx.config.hash_dir()) {
-        if (Util::starts_with(inc_path, ctx.apparent_cwd)
-            && Util::ends_with(inc_path, "//")) {
+        if (util::starts_with(inc_path, ctx.apparent_cwd)
+            && util::ends_with(inc_path, "//")) {
           // When compiling with -g or similar, GCC adds the absolute path to
           // CWD like this:
           //
@@ -681,7 +571,8 @@ process_preprocessed_file(Context& ctx,
 
       if (remember_include_file(ctx, inc_path, hash, system, nullptr)
           == RememberIncludeFileResult::cannot_use_pch) {
-        return Statistic::could_not_use_precompiled_header;
+        return nonstd::make_unexpected(
+          Statistic::could_not_use_precompiled_header);
       }
       p = q; // Everything of interest between p and q has been hashed now.
     } else if (q[0] == '.' && q[1] == 'i' && q[2] == 'n' && q[3] == 'c'
@@ -693,7 +584,8 @@ process_preprocessed_file(Context& ctx,
       LOG_RAW(
         "Found unsupported .inc"
         "bin directive in source code");
-      throw Failure(Statistic::unsupported_code_directive);
+      return nonstd::make_unexpected(
+        Failure(Statistic::unsupported_code_directive));
     } else if (pump && strncmp(q, "_________", 9) == 0) {
       // Unfortunately the distcc-pump wrapper outputs standard output lines:
       // __________Using distcc-pump from /usr/bin
@@ -727,18 +619,18 @@ process_preprocessed_file(Context& ctx,
     print_included_files(ctx, stdout);
   }
 
-  return Statistic::none;
+  return {};
 }
 
 // Extract the used includes from the dependency file. Note that we cannot
 // distinguish system headers from other includes here.
 static optional<Digest>
-result_name_from_depfile(Context& ctx, Hash& hash)
+result_key_from_depfile(Context& ctx, Hash& hash)
 {
   std::string file_content;
   try {
     file_content = Util::read_file(ctx.args_info.output_dep);
-  } catch (const Error& e) {
+  } catch (const core::Error& e) {
     LOG(
       "Cannot open dependency file {}: {}", ctx.args_info.output_dep, e.what());
     return nullopt;
@@ -749,7 +641,7 @@ result_name_from_depfile(Context& ctx, Hash& hash)
       continue;
     }
     if (!ctx.has_absolute_include_headers) {
-      ctx.has_absolute_include_headers = Util::is_absolute_path(token);
+      ctx.has_absolute_include_headers = util::is_absolute_path(token);
     }
     std::string path = Util::make_relative_path(ctx, token);
     remember_include_file(ctx, path, hash, false, &hash);
@@ -772,7 +664,7 @@ result_name_from_depfile(Context& ctx, Hash& hash)
 }
 // Execute the compiler/preprocessor, with logic to retry without requesting
 // colored diagnostics messages if that fails.
-static int
+static nonstd::expected<int, Failure>
 do_execute(Context& ctx,
            Args& args,
            TemporaryFile&& tmp_stdout,
@@ -806,14 +698,14 @@ do_execute(Context& ctx,
         tmp_stdout.path.c_str(), O_RDWR | O_CREAT | O_TRUNC | O_BINARY, 0600));
       if (!tmp_stdout.fd) {
         LOG("Failed to truncate {}: {}", tmp_stdout.path, strerror(errno));
-        throw Failure(Statistic::internal_error);
+        return nonstd::make_unexpected(Statistic::internal_error);
       }
 
       tmp_stderr.fd = Fd(open(
         tmp_stderr.path.c_str(), O_RDWR | O_CREAT | O_TRUNC | O_BINARY, 0600));
       if (!tmp_stderr.fd) {
         LOG("Failed to truncate {}: {}", tmp_stderr.path, strerror(errno));
-        throw Failure(Statistic::internal_error);
+        return nonstd::make_unexpected(Statistic::internal_error);
       }
 
       ctx.diagnostics_color_failed = true;
@@ -824,96 +716,34 @@ do_execute(Context& ctx,
   return status;
 }
 
-struct LookUpCacheFileResult
-{
-  std::string path;
-  Stat stat;
-  uint8_t level;
-};
-
-static LookUpCacheFileResult
-look_up_cache_file(const std::string& cache_dir,
-                   const Digest& name,
-                   nonstd::string_view suffix)
-{
-  const auto name_string = FMT("{}{}", name.to_string(), suffix);
-
-  for (uint8_t level = k_min_cache_levels; level <= k_max_cache_levels;
-       ++level) {
-    const auto path = Util::get_path_in_cache(cache_dir, level, name_string);
-    const auto stat = Stat::stat(path);
-    if (stat) {
-      return {path, stat, level};
-    }
-  }
-
-  const auto shallowest_path =
-    Util::get_path_in_cache(cache_dir, k_min_cache_levels, name_string);
-  return {shallowest_path, Stat(), k_min_cache_levels};
-}
-
 // Create or update the manifest file.
 static void
-update_manifest_file(Context& ctx)
+update_manifest_file(Context& ctx,
+                     const Digest& manifest_key,
+                     const Digest& result_key)
 {
-  if (!ctx.config.direct_mode() || ctx.config.read_only()
-      || ctx.config.read_only_direct()) {
+  if (ctx.config.read_only() || ctx.config.read_only_direct()) {
     return;
   }
 
-  ASSERT(ctx.manifest_path());
-  ASSERT(ctx.result_path());
-
-  MTR_BEGIN("manifest", "manifest_put");
-
-  const auto old_stat = Stat::stat(*ctx.manifest_path());
+  MTR_SCOPE("manifest", "manifest_put");
 
   // See comment in get_file_hash_index for why saving of timestamps is forced
   // for precompiled headers.
   const bool save_timestamp =
-    (ctx.config.sloppiness() & SLOPPY_FILE_STAT_MATCHES)
+    (ctx.config.sloppiness().is_enabled(core::Sloppy::file_stat_matches))
     || ctx.args_info.output_is_precompiled_header;
 
-  LOG("Adding result name to {}", *ctx.manifest_path());
-  if (!Manifest::put(ctx.config,
-                     *ctx.manifest_path(),
-                     *ctx.result_name(),
-                     ctx.included_files,
-                     ctx.time_of_compilation,
-                     save_timestamp)) {
-    LOG("Failed to add result name to {}", *ctx.manifest_path());
-  } else {
-    const auto new_stat = Stat::stat(*ctx.manifest_path(), Stat::OnError::log);
-    ctx.manifest_counter_updates.increment(
-      Statistic::cache_size_kibibyte,
-      Util::size_change_kibibyte(old_stat, new_stat));
-    ctx.manifest_counter_updates.increment(Statistic::files_in_cache,
-                                           !old_stat && new_stat ? 1 : 0);
-  }
-  MTR_END("manifest", "manifest_put");
-}
-
-static void
-create_cachedir_tag(const Context& ctx)
-{
-  constexpr char cachedir_tag[] =
-    "Signature: 8a477f597d28d172789f06886806bc55\n"
-    "# This file is a cache directory tag created by ccache.\n"
-    "# For information about cache directory tags, see:\n"
-    "#\thttp://www.brynosaurus.com/cachedir/\n";
-
-  const std::string path = FMT("{}/{}/CACHEDIR.TAG",
-                               ctx.config.cache_dir(),
-                               ctx.result_name()->to_string()[0]);
-  const auto stat = Stat::stat(path);
-  if (stat) {
-    return;
-  }
-  try {
-    Util::write_file(path, cachedir_tag);
-  } catch (const Error& e) {
-    LOG("Failed to create {}: {}", path, e.what());
-  }
+  ctx.storage.put(
+    manifest_key, core::CacheEntryType::manifest, [&](const auto& path) {
+      LOG("Adding result key to {}", path);
+      return Manifest::put(ctx.config,
+                           path,
+                           result_key,
+                           ctx.included_files,
+                           ctx.time_of_compilation,
+                           save_timestamp);
+    });
 }
 
 struct FindCoverageFileResult
@@ -954,10 +784,70 @@ find_coverage_file(const Context& ctx)
   return {true, found_file, found_file == mangled_form};
 }
 
-// Run the real compiler and put the result in cache.
-static void
+static bool
+write_result(Context& ctx,
+             const std::string& result_path,
+             const Stat& obj_stat,
+             const std::string& stderr_path)
+{
+  Result::Writer result_writer(ctx, result_path);
+
+  const auto stderr_stat = Stat::stat(stderr_path, Stat::OnError::log);
+  if (!stderr_stat) {
+    return false;
+  }
+
+  if (stderr_stat.size() > 0) {
+    result_writer.write(Result::FileType::stderr_output, stderr_path);
+  }
+  if (obj_stat) {
+    result_writer.write(Result::FileType::object, ctx.args_info.output_obj);
+  }
+  if (ctx.args_info.generating_dependencies) {
+    result_writer.write(Result::FileType::dependency, ctx.args_info.output_dep);
+  }
+  if (ctx.args_info.generating_coverage) {
+    const auto coverage_file = find_coverage_file(ctx);
+    if (!coverage_file.found) {
+      return false;
+    }
+    result_writer.write(coverage_file.mangled
+                          ? Result::FileType::coverage_mangled
+                          : Result::FileType::coverage_unmangled,
+                        coverage_file.path);
+  }
+  if (ctx.args_info.generating_stackusage) {
+    result_writer.write(Result::FileType::stackusage, ctx.args_info.output_su);
+  }
+  if (ctx.args_info.generating_diagnostics) {
+    result_writer.write(Result::FileType::diagnostic, ctx.args_info.output_dia);
+  }
+  if (ctx.args_info.seen_split_dwarf && Stat::stat(ctx.args_info.output_dwo)) {
+    // Only store .dwo file if it was created by the compiler (GCC and Clang
+    // behave differently e.g. for "-gsplit-dwarf -g1").
+    result_writer.write(Result::FileType::dwarf_object,
+                        ctx.args_info.output_dwo);
+  }
+
+  const auto file_size_and_count_diff = result_writer.finalize();
+  if (file_size_and_count_diff) {
+    ctx.storage.primary.increment_statistic(
+      Statistic::cache_size_kibibyte, file_size_and_count_diff->size_kibibyte);
+    ctx.storage.primary.increment_statistic(Statistic::files_in_cache,
+                                            file_size_and_count_diff->count);
+  } else {
+    LOG("Error: {}", file_size_and_count_diff.error());
+    return false;
+  }
+
+  return true;
+}
+
+// Run the real compiler and put the result in cache. Returns the result key.
+static nonstd::expected<Digest, Failure>
 to_cache(Context& ctx,
          Args& args,
+         nonstd::optional<Digest> result_key,
          const Args& depend_extra_args,
          Hash* depend_mode_hash)
 {
@@ -997,7 +887,7 @@ to_cache(Context& ctx,
     if (unlink(ctx.args_info.output_dwo.c_str()) != 0 && errno != ENOENT
         && errno != ESTALE) {
       LOG("Failed to unlink {}: {}", ctx.args_info.output_dwo, strerror(errno));
-      throw Failure(Statistic::bad_output_file);
+      return nonstd::make_unexpected(Statistic::bad_output_file);
     }
   }
 
@@ -1012,7 +902,7 @@ to_cache(Context& ctx,
   ctx.register_pending_tmp_file(tmp_stderr.path);
   std::string tmp_stderr_path = tmp_stderr.path;
 
-  int status;
+  nonstd::expected<int, Failure> status;
   if (!ctx.config.depend_mode()) {
     status =
       do_execute(ctx, args, std::move(tmp_stdout), std::move(tmp_stderr));
@@ -1031,17 +921,21 @@ to_cache(Context& ctx,
   }
   MTR_END("execute", "compiler");
 
+  if (!status) {
+    return nonstd::make_unexpected(status.error());
+  }
+
   auto st = Stat::stat(tmp_stdout_path, Stat::OnError::log);
   if (!st) {
     // The stdout file was removed - cleanup in progress? Better bail out.
-    throw Failure(Statistic::missing_cache_file);
+    return nonstd::make_unexpected(Statistic::missing_cache_file);
   }
 
   // distcc-pump outputs lines like this:
   // __________Using # distcc servers in pump mode
   if (st.size() != 0 && ctx.config.compiler_type() != CompilerType::pump) {
     LOG_RAW("Compiler produced stdout");
-    throw Failure(Statistic::compiler_produced_stdout);
+    return nonstd::make_unexpected(Statistic::compiler_produced_stdout);
   }
 
   // Merge stderr from the preprocessor (if any) and stderr from the real
@@ -1053,23 +947,26 @@ to_cache(Context& ctx,
   }
 
   if (status != 0) {
-    LOG("Compiler gave exit status {}", status);
+    LOG("Compiler gave exit status {}", *status);
 
     // We can output stderr immediately instead of rerunning the compiler.
     Util::send_to_stderr(ctx, Util::read_file(tmp_stderr_path));
 
-    throw Failure(Statistic::compile_failed, status);
+    auto failure = Failure(Statistic::compile_failed);
+    failure.set_exit_code(*status);
+    return nonstd::make_unexpected(failure);
   }
 
   if (ctx.config.depend_mode()) {
     ASSERT(depend_mode_hash);
-    auto result_name = result_name_from_depfile(ctx, *depend_mode_hash);
-    if (!result_name) {
-      throw Failure(Statistic::internal_error);
+    result_key = result_key_from_depfile(ctx, *depend_mode_hash);
+    if (!result_key) {
+      return nonstd::make_unexpected(Statistic::internal_error);
     }
-    ctx.set_result_name(*result_name);
   }
 
+  ASSERT(result_key);
+
   bool produce_dep_file = ctx.args_info.generating_dependencies
                           && ctx.args_info.output_dep != "/dev/null";
 
@@ -1081,100 +978,44 @@ to_cache(Context& ctx,
   if (!obj_stat) {
     if (ctx.args_info.expect_output_obj) {
       LOG_RAW("Compiler didn't produce an object file (unexpected)");
-      throw Failure(Statistic::compiler_produced_no_output);
+      return nonstd::make_unexpected(Statistic::compiler_produced_no_output);
     } else {
       LOG_RAW("Compiler didn't produce an object file (expected)");
     }
   } else if (obj_stat.size() == 0) {
     LOG_RAW("Compiler produced an empty object file");
-    throw Failure(Statistic::compiler_produced_empty_output);
-  }
-
-  const auto stderr_stat = Stat::stat(tmp_stderr_path, Stat::OnError::log);
-  if (!stderr_stat) {
-    throw Failure(Statistic::internal_error);
-  }
-
-  const auto result_file = look_up_cache_file(
-    ctx.config.cache_dir(), *ctx.result_name(), Result::k_file_suffix);
-  ctx.set_result_path(result_file.path);
-  Result::Writer result_writer(ctx, result_file.path);
-
-  if (stderr_stat.size() > 0) {
-    result_writer.write(Result::FileType::stderr_output, tmp_stderr_path);
-  }
-  if (obj_stat) {
-    result_writer.write(Result::FileType::object, ctx.args_info.output_obj);
-  }
-  if (ctx.args_info.generating_dependencies) {
-    result_writer.write(Result::FileType::dependency, ctx.args_info.output_dep);
-  }
-  if (ctx.args_info.generating_coverage) {
-    const auto coverage_file = find_coverage_file(ctx);
-    if (!coverage_file.found) {
-      throw Failure(Statistic::internal_error);
-    }
-    result_writer.write(coverage_file.mangled
-                          ? Result::FileType::coverage_mangled
-                          : Result::FileType::coverage_unmangled,
-                        coverage_file.path);
-  }
-  if (ctx.args_info.generating_stackusage) {
-    result_writer.write(Result::FileType::stackusage, ctx.args_info.output_su);
-  }
-  if (ctx.args_info.generating_diagnostics) {
-    result_writer.write(Result::FileType::diagnostic, ctx.args_info.output_dia);
-  }
-  if (ctx.args_info.seen_split_dwarf && Stat::stat(ctx.args_info.output_dwo)) {
-    // Only store .dwo file if it was created by the compiler (GCC and Clang
-    // behave differently e.g. for "-gsplit-dwarf -g1").
-    result_writer.write(Result::FileType::dwarf_object,
-                        ctx.args_info.output_dwo);
+    return nonstd::make_unexpected(Statistic::compiler_produced_empty_output);
   }
 
-  auto error = result_writer.finalize();
-  if (error) {
-    LOG("Error: {}", *error);
-  } else {
-    LOG("Stored in cache: {}", result_file.path);
-  }
-
-  auto new_result_stat = Stat::stat(result_file.path, Stat::OnError::log);
-  if (!new_result_stat) {
-    throw Failure(Statistic::internal_error);
+  MTR_BEGIN("result", "result_put");
+  const bool added = ctx.storage.put(
+    *result_key, core::CacheEntryType::result, [&](const auto& path) {
+      return write_result(ctx, path, obj_stat, tmp_stderr_path);
+    });
+  MTR_END("result", "result_put");
+  if (!added) {
+    return nonstd::make_unexpected(Statistic::internal_error);
   }
-  ctx.counter_updates.increment(
-    Statistic::cache_size_kibibyte,
-    Util::size_change_kibibyte(result_file.stat, new_result_stat));
-  ctx.counter_updates.increment(Statistic::files_in_cache,
-                                result_file.stat ? 0 : 1);
-
-  MTR_END("file", "file_put");
-
-  // Make sure we have a CACHEDIR.TAG in the cache part of cache_dir. This can
-  // be done almost anywhere, but we might as well do it near the end as we save
-  // the stat call if we exit early.
-  create_cachedir_tag(ctx);
 
   // Everything OK.
   Util::send_to_stderr(ctx, Util::read_file(tmp_stderr_path));
+
+  return *result_key;
 }
 
-// Find the result name by running the compiler in preprocessor mode and
+// Find the result key by running the compiler in preprocessor mode and
 // hashing the result.
-static Digest
-get_result_name_from_cpp(Context& ctx, Args& args, Hash& hash)
+static nonstd::expected<Digest, Failure>
+get_result_key_from_cpp(Context& ctx, Args& args, Hash& hash)
 {
   ctx.time_of_compilation = time(nullptr);
 
   std::string stderr_path;
   std::string stdout_path;
-  int status;
   if (ctx.args_info.direct_i_file) {
     // We are compiling a .i or .ii file - that means we can skip the cpp stage
     // and directly form the correct i_tmpfile.
     stdout_path = ctx.args_info.input_file;
-    status = 0;
   } else {
     // Run cpp on the input file to obtain the .i.
 
@@ -1208,30 +1049,28 @@ get_result_name_from_cpp(Context& ctx, Args& args, Hash& hash)
     add_prefix(ctx, args, ctx.config.prefix_command_cpp());
     LOG_RAW("Running preprocessor");
     MTR_BEGIN("execute", "preprocessor");
-    status =
+    const auto status =
       do_execute(ctx, args, std::move(tmp_stdout), std::move(tmp_stderr));
     MTR_END("execute", "preprocessor");
     args.pop_back(args_added);
-  }
 
-  if (status != 0) {
-    LOG("Preprocessor gave exit status {}", status);
-    throw Failure(Statistic::preprocessor_error);
+    if (!status) {
+      return nonstd::make_unexpected(status.error());
+    } else if (*status != 0) {
+      LOG("Preprocessor gave exit status {}", *status);
+      return nonstd::make_unexpected(Statistic::preprocessor_error);
+    }
   }
 
   hash.hash_delimiter("cpp");
   const bool is_pump = ctx.config.compiler_type() == CompilerType::pump;
-  const Statistic error =
-    process_preprocessed_file(ctx, hash, stdout_path, is_pump);
-  if (error != Statistic::none) {
-    throw Failure(error);
-  }
+  TRY(process_preprocessed_file(ctx, hash, stdout_path, is_pump));
 
   hash.hash_delimiter("cppstderr");
   if (!ctx.args_info.direct_i_file && !hash.hash_file(stderr_path)) {
     // Somebody removed the temporary file?
     LOG("Failed to open {}: {}", stderr_path, strerror(errno));
-    throw Failure(Statistic::internal_error);
+    return nonstd::make_unexpected(Statistic::internal_error);
   }
 
   if (ctx.args_info.direct_i_file) {
@@ -1253,7 +1092,7 @@ get_result_name_from_cpp(Context& ctx, Args& args, Hash& hash)
 
 // Hash mtime or content of a file, or the output of a command, according to
 // the CCACHE_COMPILERCHECK setting.
-static void
+static nonstd::expected<void, Failure>
 hash_compiler(const Context& ctx,
               Hash& hash,
               const Stat& st,
@@ -1266,7 +1105,7 @@ hash_compiler(const Context& ctx,
     hash.hash_delimiter("cc_mtime");
     hash.hash(st.size());
     hash.hash(st.mtime());
-  } else if (Util::starts_with(ctx.config.compiler_check(), "string:")) {
+  } else if (util::starts_with(ctx.config.compiler_check(), "string:")) {
     hash.hash_delimiter("cc_hash");
     hash.hash(&ctx.config.compiler_check()[7]);
   } else if (ctx.config.compiler_check() == "content" || !allow_command) {
@@ -1277,9 +1116,10 @@ hash_compiler(const Context& ctx,
           hash, ctx.config.compiler_check(), ctx.orig_args[0])) {
       LOG("Failure running compiler check command: {}",
           ctx.config.compiler_check());
-      throw Failure(Statistic::compiler_check_failed);
+      return nonstd::make_unexpected(Statistic::compiler_check_failed);
     }
   }
+  return {};
 }
 
 // Hash the host compiler(s) invoked by nvcc.
@@ -1287,7 +1127,7 @@ hash_compiler(const Context& ctx,
 // If `ccbin_st` and `ccbin` are set, they refer to a directory or compiler set
 // with -ccbin/--compiler-bindir. If `ccbin_st` is nullptr or `ccbin` is the
 // empty string, the compilers are looked up in PATH instead.
-static void
+static nonstd::expected<void, Failure>
 hash_nvcc_host_compiler(const Context& ctx,
                         Hash& hash,
                         const Stat* ccbin_st = nullptr,
@@ -1318,19 +1158,21 @@ hash_nvcc_host_compiler(const Context& ctx,
         std::string path = FMT("{}/{}", ccbin, compiler);
         auto st = Stat::stat(path);
         if (st) {
-          hash_compiler(ctx, hash, st, path, false);
+          TRY(hash_compiler(ctx, hash, st, path, false));
         }
       } else {
         std::string path = find_executable(ctx, compiler, CCACHE_NAME);
         if (!path.empty()) {
           auto st = Stat::stat(path, Stat::OnError::log);
-          hash_compiler(ctx, hash, st, ccbin, false);
+          TRY(hash_compiler(ctx, hash, st, ccbin, false));
         }
       }
     }
   } else {
-    hash_compiler(ctx, hash, *ccbin_st, ccbin, false);
+    TRY(hash_compiler(ctx, hash, *ccbin_st, ccbin, false));
   }
+
+  return {};
 }
 
 static bool
@@ -1340,7 +1182,7 @@ should_rewrite_dependency_target(const ArgsInfo& args_info)
 }
 
 // update a hash with information common for the direct and preprocessor modes.
-static void
+static nonstd::expected<void, Failure>
 hash_common_info(const Context& ctx,
                  const Args& args,
                  Hash& hash,
@@ -1361,11 +1203,11 @@ hash_common_info(const Context& ctx,
 
   auto st = Stat::stat(compiler_path, Stat::OnError::log);
   if (!st) {
-    throw Failure(Statistic::could_not_find_compiler);
+    return nonstd::make_unexpected(Statistic::could_not_find_compiler);
   }
 
   // Hash information about the compiler.
-  hash_compiler(ctx, hash, st, compiler_path, true);
+  TRY(hash_compiler(ctx, hash, st, compiler_path, true));
 
   // Also hash the compiler name as some compilers use hard links and behave
   // differently depending on the real name.
@@ -1388,7 +1230,7 @@ hash_common_info(const Context& ctx,
     }
   }
 
-  if (!(ctx.config.sloppiness() & SLOPPY_LOCALE)) {
+  if (!(ctx.config.sloppiness().is_enabled(core::Sloppy::locale))) {
     // Hash environment variables that may affect localization of compiler
     // warning messages.
     const char* envvars[] = {
@@ -1414,7 +1256,7 @@ hash_common_info(const Context& ctx,
             old_path,
             new_path,
             ctx.apparent_cwd);
-        if (Util::starts_with(ctx.apparent_cwd, old_path)) {
+        if (util::starts_with(ctx.apparent_cwd, old_path)) {
           dir_to_hash = new_path + ctx.apparent_cwd.substr(old_path.size());
         }
       }
@@ -1463,17 +1305,17 @@ hash_common_info(const Context& ctx,
     LOG("Hashing sanitize blacklist {}", sanitize_blacklist);
     hash.hash("sanitizeblacklist");
     if (!hash_binary_file(ctx, hash, sanitize_blacklist)) {
-      throw Failure(Statistic::error_hashing_extra_file);
+      return nonstd::make_unexpected(Statistic::error_hashing_extra_file);
     }
   }
 
   if (!ctx.config.extra_files_to_hash().empty()) {
-    for (const std::string& path : Util::split_into_strings(
-           ctx.config.extra_files_to_hash(), PATH_DELIM)) {
+    for (const std::string& path :
+         util::split_path_list(ctx.config.extra_files_to_hash())) {
       LOG("Hashing extra file {}", path);
       hash.hash_delimiter("extrafile");
       if (!hash_binary_file(ctx, hash, path)) {
-        throw Failure(Statistic::error_hashing_extra_file);
+        return nonstd::make_unexpected(Statistic::error_hashing_extra_file);
       }
     }
   }
@@ -1486,6 +1328,8 @@ hash_common_info(const Context& ctx,
       hash.hash(gcc_colors);
     }
   }
+
+  return {};
 }
 
 static bool
@@ -1530,23 +1374,25 @@ option_should_be_ignored(const std::string& arg,
                          const std::vector<std::string>& patterns)
 {
   return std::any_of(
-    patterns.cbegin(), patterns.cend(), [&arg](const std::string& pattern) {
+    patterns.cbegin(), patterns.cend(), [&arg](const auto& pattern) {
       const auto& prefix = string_view(pattern).substr(0, pattern.length() - 1);
       return (
         pattern == arg
-        || (Util::ends_with(pattern, "*") && Util::starts_with(arg, prefix)));
+        || (util::ends_with(pattern, "*") && util::starts_with(arg, prefix)));
     });
 }
 
 // Update a hash sum with information specific to the direct and preprocessor
-// modes and calculate the result name. Returns the result name on success,
-// otherwise nullopt.
-static optional<Digest>
-calculate_result_name(Context& ctx,
-                      const Args& args,
-                      Args& preprocessor_args,
-                      Hash& hash,
-                      bool direct_mode)
+// modes and calculate the result key. Returns the result key on success, and
+// if direct_mode is true also the manifest key.
+static nonstd::expected<
+  std::pair<nonstd::optional<Digest>, nonstd::optional<Digest>>,
+  Failure>
+calculate_result_and_manifest_key(Context& ctx,
+                                  const Args& args,
+                                  Args& preprocessor_args,
+                                  Hash& hash,
+                                  bool direct_mode)
 {
   bool found_ccbin = false;
 
@@ -1580,12 +1426,12 @@ calculate_result_name(Context& ctx,
       i++;
       continue;
     }
-    if (Util::starts_with(args[i], "-L") && !is_clang) {
+    if (util::starts_with(args[i], "-L") && !is_clang) {
       continue;
     }
 
     // -Wl,... doesn't affect compilation (except for clang).
-    if (Util::starts_with(args[i], "-Wl,") && !is_clang) {
+    if (util::starts_with(args[i], "-Wl,") && !is_clang) {
       continue;
     }
 
@@ -1593,17 +1439,17 @@ calculate_result_name(Context& ctx,
     // CCACHE_BASEDIR to reuse results across different directories. Skip using
     // the value of the option from hashing but still hash the existence of the
     // option.
-    if (Util::starts_with(args[i], "-fdebug-prefix-map=")) {
+    if (util::starts_with(args[i], "-fdebug-prefix-map=")) {
       hash.hash_delimiter("arg");
       hash.hash("-fdebug-prefix-map=");
       continue;
     }
-    if (Util::starts_with(args[i], "-ffile-prefix-map=")) {
+    if (util::starts_with(args[i], "-ffile-prefix-map=")) {
       hash.hash_delimiter("arg");
       hash.hash("-ffile-prefix-map=");
       continue;
     }
-    if (Util::starts_with(args[i], "-fmacro-prefix-map=")) {
+    if (util::starts_with(args[i], "-fmacro-prefix-map=")) {
       hash.hash_delimiter("arg");
       hash.hash("-fmacro-prefix-map=");
       continue;
@@ -1629,17 +1475,17 @@ calculate_result_name(Context& ctx,
     // If we're generating dependencies, we make sure to skip the filename of
     // the dependency file, since it doesn't impact the output.
     if (ctx.args_info.generating_dependencies) {
-      if (Util::starts_with(args[i], "-Wp,")) {
-        if (Util::starts_with(args[i], "-Wp,-MD,")
+      if (util::starts_with(args[i], "-Wp,")) {
+        if (util::starts_with(args[i], "-Wp,-MD,")
             && args[i].find(',', 8) == std::string::npos) {
           hash.hash(args[i].data(), 8);
           continue;
-        } else if (Util::starts_with(args[i], "-Wp,-MMD,")
+        } else if (util::starts_with(args[i], "-Wp,-MMD,")
                    && args[i].find(',', 9) == std::string::npos) {
           hash.hash(args[i].data(), 9);
           continue;
         }
-      } else if (Util::starts_with(args[i], "-MF")) {
+      } else if (util::starts_with(args[i], "-MF")) {
         // In either case, hash the "-MF" part.
         hash.hash_delimiter("arg");
         hash.hash(args[i].data(), 3);
@@ -1655,15 +1501,16 @@ calculate_result_name(Context& ctx,
       }
     }
 
-    if (Util::starts_with(args[i], "-specs=")
-        || Util::starts_with(args[i], "--specs=")
-        || (args[i] == "-specs" || args[i] == "--specs")) {
+    if (util::starts_with(args[i], "-specs=")
+        || util::starts_with(args[i], "--specs=")
+        || (args[i] == "-specs" || args[i] == "--specs")
+        || args[i] == "--config") {
       std::string path;
       size_t eq_pos = args[i].find('=');
       if (eq_pos == std::string::npos) {
         if (i + 1 >= args.size()) {
           LOG("missing argument for \"{}\"", args[i]);
-          throw Failure(Statistic::bad_compiler_arguments);
+          return nonstd::make_unexpected(Statistic::bad_compiler_arguments);
         }
         path = args[i + 1];
         i++;
@@ -1675,16 +1522,16 @@ calculate_result_name(Context& ctx,
         // If given an explicit specs file, then hash that file, but don't
         // include the path to it in the hash.
         hash.hash_delimiter("specs");
-        hash_compiler(ctx, hash, st, path, false);
+        TRY(hash_compiler(ctx, hash, st, path, false));
         continue;
       }
     }
 
-    if (Util::starts_with(args[i], "-fplugin=")) {
+    if (util::starts_with(args[i], "-fplugin=")) {
       auto st = Stat::stat(&args[i][9], Stat::OnError::log);
       if (st) {
         hash.hash_delimiter("plugin");
-        hash_compiler(ctx, hash, st, &args[i][9], false);
+        TRY(hash_compiler(ctx, hash, st, &args[i][9], false));
         continue;
       }
     }
@@ -1694,7 +1541,7 @@ calculate_result_name(Context& ctx,
       auto st = Stat::stat(args[i + 3], Stat::OnError::log);
       if (st) {
         hash.hash_delimiter("plugin");
-        hash_compiler(ctx, hash, st, args[i + 3], false);
+        TRY(hash_compiler(ctx, hash, st, args[i + 3], false));
         i += 3;
         continue;
       }
@@ -1706,7 +1553,7 @@ calculate_result_name(Context& ctx,
       if (st) {
         found_ccbin = true;
         hash.hash_delimiter("ccbin");
-        hash_nvcc_host_compiler(ctx, hash, &st, args[i + 1]);
+        TRY(hash_nvcc_host_compiler(ctx, hash, &st, args[i + 1]));
         i++;
         continue;
       }
@@ -1730,7 +1577,7 @@ calculate_result_name(Context& ctx,
   }
 
   if (!found_ccbin && ctx.args_info.actual_language == "cu") {
-    hash_nvcc_host_compiler(ctx, hash);
+    TRY(hash_nvcc_host_compiler(ctx, hash));
   }
 
   // For profile generation (-fprofile(-instr)-generate[=path])
@@ -1748,14 +1595,22 @@ calculate_result_name(Context& ctx,
 
   if (ctx.args_info.profile_generate) {
     ASSERT(!ctx.args_info.profile_path.empty());
-    LOG("Adding profile directory {} to our hash", ctx.args_info.profile_path);
+
+    // For a relative profile directory D the compiler stores $PWD/D as part of
+    // the profile filename so we need to include the same information in the
+    // hash.
+    const std::string profile_path =
+      util::is_absolute_path(ctx.args_info.profile_path)
+        ? ctx.args_info.profile_path
+        : FMT("{}/{}", ctx.apparent_cwd, ctx.args_info.profile_path);
+    LOG("Adding profile directory {} to our hash", profile_path);
     hash.hash_delimiter("-fprofile-dir");
-    hash.hash(ctx.args_info.profile_path);
+    hash.hash(profile_path);
   }
 
   if (ctx.args_info.profile_use && !hash_profile_data_file(ctx, hash)) {
     LOG_RAW("No profile data file found");
-    throw Failure(Statistic::no_input_file);
+    return nonstd::make_unexpected(Statistic::no_input_file);
   }
 
   // Adding -arch to hash since cpp output is affected.
@@ -1764,7 +1619,9 @@ calculate_result_name(Context& ctx,
     hash.hash(arch);
   }
 
-  optional<Digest> result_name;
+  nonstd::optional<Digest> result_key;
+  nonstd::optional<Digest> manifest_key;
+
   if (direct_mode) {
     // Hash environment variables that affect the preprocessor output.
     const char* envvars[] = {"CPATH",
@@ -1798,68 +1655,70 @@ calculate_result_name(Context& ctx,
     hash.hash_delimiter("sourcecode");
     int result = hash_source_code_file(ctx, hash, ctx.args_info.input_file);
     if (result & HASH_SOURCE_CODE_ERROR) {
-      throw Failure(Statistic::internal_error);
+      return nonstd::make_unexpected(Statistic::internal_error);
     }
     if (result & HASH_SOURCE_CODE_FOUND_TIME) {
       LOG_RAW("Disabling direct mode");
       ctx.config.set_direct_mode(false);
-      return nullopt;
+      return std::make_pair(nullopt, nullopt);
     }
 
-    const auto manifest_name = hash.digest();
-    ctx.set_manifest_name(manifest_name);
+    manifest_key = hash.digest();
 
-    const auto manifest_file = look_up_cache_file(
-      ctx.config.cache_dir(), manifest_name, Manifest::k_file_suffix);
-    ctx.set_manifest_path(manifest_file.path);
+    const auto manifest_path =
+      ctx.storage.get(*manifest_key, core::CacheEntryType::manifest);
 
-    if (manifest_file.stat) {
-      LOG("Looking for result name in {}", manifest_file.path);
+    if (manifest_path) {
+      LOG("Looking for result key in {}", *manifest_path);
       MTR_BEGIN("manifest", "manifest_get");
-      result_name = Manifest::get(ctx, manifest_file.path);
+      result_key = Manifest::get(ctx, *manifest_path);
       MTR_END("manifest", "manifest_get");
-      if (result_name) {
-        LOG_RAW("Got result name from manifest");
+      if (result_key) {
+        LOG_RAW("Got result key from manifest");
       } else {
-        LOG_RAW("Did not find result name in manifest");
+        LOG_RAW("Did not find result key in manifest");
       }
-    } else {
-      LOG("No manifest with name {} in the cache", manifest_name.to_string());
     }
+  } else if (ctx.args_info.arch_args.empty()) {
+    const auto digest = get_result_key_from_cpp(ctx, preprocessor_args, hash);
+    if (!digest) {
+      return nonstd::make_unexpected(digest.error());
+    }
+    result_key = *digest;
+    LOG_RAW("Got result key from preprocessor");
   } else {
-    if (ctx.args_info.arch_args.empty()) {
-      result_name = get_result_name_from_cpp(ctx, preprocessor_args, hash);
-      LOG_RAW("Got result name from preprocessor");
-    } else {
-      preprocessor_args.push_back("-arch");
-      for (size_t i = 0; i < ctx.args_info.arch_args.size(); ++i) {
-        preprocessor_args.push_back(ctx.args_info.arch_args[i]);
-        result_name = get_result_name_from_cpp(ctx, preprocessor_args, hash);
-        LOG("Got result name from preprocessor with -arch {}",
-            ctx.args_info.arch_args[i]);
-        if (i != ctx.args_info.arch_args.size() - 1) {
-          result_name = nullopt;
-        }
-        preprocessor_args.pop_back();
+    preprocessor_args.push_back("-arch");
+    for (size_t i = 0; i < ctx.args_info.arch_args.size(); ++i) {
+      preprocessor_args.push_back(ctx.args_info.arch_args[i]);
+      const auto digest = get_result_key_from_cpp(ctx, preprocessor_args, hash);
+      if (!digest) {
+        return nonstd::make_unexpected(digest.error());
+      }
+      result_key = *digest;
+      LOG("Got result key from preprocessor with -arch {}",
+          ctx.args_info.arch_args[i]);
+      if (i != ctx.args_info.arch_args.size() - 1) {
+        result_key = nullopt;
       }
       preprocessor_args.pop_back();
     }
+    preprocessor_args.pop_back();
   }
 
-  return result_name;
+  return std::make_pair(result_key, manifest_key);
 }
 
 enum class FromCacheCallMode { direct, cpp };
 
 // Try to return the compile result from cache.
-static optional<Statistic>
-from_cache(Context& ctx, FromCacheCallMode mode)
+static bool
+from_cache(Context& ctx, FromCacheCallMode mode, const Digest& result_key)
 {
   UmaskScope umask_scope(ctx.original_umask);
 
   // The user might be disabling cache hits.
   if (ctx.config.recache()) {
-    return nullopt;
+    return false;
   }
 
   // If we're using Clang, we can't trust a precompiled header object based on
@@ -1872,39 +1731,32 @@ from_cache(Context& ctx, FromCacheCallMode mode)
   if ((ctx.config.compiler_type() == CompilerType::clang
        || ctx.config.compiler_type() == CompilerType::other)
       && ctx.args_info.output_is_precompiled_header
-      && !ctx.args_info.fno_pch_timestamp && mode == FromCacheCallMode::cpp) {
+      && mode == FromCacheCallMode::cpp) {
     LOG_RAW("Not considering cached precompiled header in preprocessor mode");
-    return nullopt;
+    return false;
   }
 
-  MTR_BEGIN("cache", "from_cache");
+  MTR_SCOPE("cache", "from_cache");
 
   // Get result from cache.
-  const auto result_file = look_up_cache_file(
-    ctx.config.cache_dir(), *ctx.result_name(), Result::k_file_suffix);
-  if (!result_file.stat) {
-    LOG("No result with name {} in the cache", ctx.result_name()->to_string());
-    return nullopt;
+  const auto result_path =
+    ctx.storage.get(result_key, core::CacheEntryType::result);
+  if (!result_path) {
+    return false;
   }
-  ctx.set_result_path(result_file.path);
-  Result::Reader result_reader(result_file.path);
+
+  Result::Reader result_reader(*result_path);
   ResultRetriever result_retriever(
     ctx, should_rewrite_dependency_target(ctx.args_info));
 
   auto error = result_reader.read(result_retriever);
-  MTR_END("cache", "from_cache");
   if (error) {
     LOG("Failed to get result from cache: {}", *error);
-    return nullopt;
+    return false;
   }
 
-  // Update modification timestamp to save file from LRU cleanup.
-  Util::update_mtime(*ctx.result_path());
-
   LOG_RAW("Succeeded getting cached result");
-
-  return mode == FromCacheCallMode::direct ? Statistic::direct_cache_hit
-                                           : Statistic::preprocessed_cache_hit;
+  return true;
 }
 
 // Find the real compiler and put it into ctx.orig_args[0]. We just search the
@@ -1934,17 +1786,17 @@ find_compiler(Context& ctx,
                            : ctx.orig_args[compiler_pos]);
 
   const std::string resolved_compiler =
-    Util::is_full_path(compiler)
+    util::is_full_path(compiler)
       ? compiler
       : find_executable_function(ctx, compiler, CCACHE_NAME);
 
   if (resolved_compiler.empty()) {
-    throw Fatal("Could not find compiler \"{}\" in PATH", compiler);
+    throw core::Fatal("Could not find compiler \"{}\" in PATH", compiler);
   }
 
   if (Util::same_program_name(Util::base_name(resolved_compiler),
                               CCACHE_NAME)) {
-    throw Fatal(
+    throw core::Fatal(
       "Recursive invocation (the name of the ccache binary must be \"{}\")",
       CCACHE_NAME);
   }
@@ -1953,129 +1805,12 @@ find_compiler(Context& ctx,
   ctx.orig_args[0] = resolved_compiler;
 }
 
-static std::string
-default_cache_dir(const std::string& home_dir)
-{
-#ifdef _WIN32
-  return home_dir + "/ccache";
-#elif defined(__APPLE__)
-  return home_dir + "/Library/Caches/ccache";
-#else
-  return home_dir + "/.cache/ccache";
-#endif
-}
-
-static std::string
-default_config_dir(const std::string& home_dir)
-{
-#ifdef _WIN32
-  return home_dir + "/ccache";
-#elif defined(__APPLE__)
-  return home_dir + "/Library/Preferences/ccache";
-#else
-  return home_dir + "/.config/ccache";
-#endif
-}
-
-// Read config file(s), populate variables, create configuration file in cache
-// directory if missing, etc.
-static void
-set_up_config(Config& config)
-{
-  const std::string home_dir = Util::get_home_directory();
-  const std::string legacy_ccache_dir = home_dir + "/.ccache";
-  const bool legacy_ccache_dir_exists =
-    Stat::stat(legacy_ccache_dir).is_directory();
-  const char* const env_xdg_cache_home = getenv("XDG_CACHE_HOME");
-  const char* const env_xdg_config_home = getenv("XDG_CONFIG_HOME");
-
-  const char* env_ccache_configpath = getenv("CCACHE_CONFIGPATH");
-  if (env_ccache_configpath) {
-    config.set_primary_config_path(env_ccache_configpath);
-  } else {
-    // Only used for ccache tests:
-    const char* const env_ccache_configpath2 = getenv("CCACHE_CONFIGPATH2");
-
-    config.set_secondary_config_path(env_ccache_configpath2
-                                       ? env_ccache_configpath2
-                                       : FMT("{}/ccache.conf", SYSCONFDIR));
-    MTR_BEGIN("config", "conf_read_secondary");
-    // A missing config file in SYSCONFDIR is OK so don't check return value.
-    config.update_from_file(config.secondary_config_path());
-    MTR_END("config", "conf_read_secondary");
-
-    const char* const env_ccache_dir = getenv("CCACHE_DIR");
-    std::string primary_config_dir;
-    if (env_ccache_dir && *env_ccache_dir) {
-      primary_config_dir = env_ccache_dir;
-    } else if (!config.cache_dir().empty() && !env_ccache_dir) {
-      primary_config_dir = config.cache_dir();
-    } else if (legacy_ccache_dir_exists) {
-      primary_config_dir = legacy_ccache_dir;
-    } else if (env_xdg_config_home) {
-      primary_config_dir = FMT("{}/ccache", env_xdg_config_home);
-    } else {
-      primary_config_dir = default_config_dir(home_dir);
-    }
-    config.set_primary_config_path(primary_config_dir + "/ccache.conf");
-  }
-
-  const std::string& cache_dir_before_primary_config = config.cache_dir();
-
-  MTR_BEGIN("config", "conf_read_primary");
-  config.update_from_file(config.primary_config_path());
-  MTR_END("config", "conf_read_primary");
-
-  // Ignore cache_dir set in primary config.
-  config.set_cache_dir(cache_dir_before_primary_config);
-
-  MTR_BEGIN("config", "conf_update_from_environment");
-  config.update_from_environment();
-  // (config.cache_dir is set above if CCACHE_DIR is set.)
-  MTR_END("config", "conf_update_from_environment");
-
-  if (config.cache_dir().empty()) {
-    if (legacy_ccache_dir_exists) {
-      config.set_cache_dir(legacy_ccache_dir);
-    } else if (env_xdg_cache_home) {
-      config.set_cache_dir(FMT("{}/ccache", env_xdg_cache_home));
-    } else {
-      config.set_cache_dir(default_cache_dir(home_dir));
-    }
-  }
-  // else: cache_dir was set explicitly via environment or via secondary config.
-
-  // We have now determined config.cache_dir and populated the rest of config in
-  // prio order (1. environment, 2. primary config, 3. secondary config).
-}
-
-static void
-set_up_context(Context& ctx, int argc, const char* const* argv)
-{
-  ctx.orig_args = Args::from_argv(argc, argv);
-  ctx.ignore_header_paths = Util::split_into_strings(
-    ctx.config.ignore_headers_in_manifest(), PATH_DELIM);
-  ctx.set_ignore_options(
-    Util::split_into_strings(ctx.config.ignore_options(), " "));
-}
-
-// Initialize ccache, must be called once before anything else is run.
+// Initialize ccache. Must be called once before anything else is run.
 static void
 initialize(Context& ctx, int argc, const char* const* argv)
 {
-  set_up_config(ctx.config);
-  set_up_context(ctx, argc, argv);
-  Logging::init(ctx.config);
-
-  // Set default umask for all files created by ccache from now on (if
-  // configured to). This is intentionally done after calling init_log so that
-  // the log file won't be affected by the umask but before creating the initial
-  // configuration file. The intention is that all files and directories in the
-  // cache directory should be affected by the configured umask and that no
-  // other files and directories should.
-  if (ctx.config.umask() != std::numeric_limits<uint32_t>::max()) {
-    ctx.original_umask = umask(ctx.config.umask());
-  }
+  ctx.orig_args = Args::from_argv(argc, argv);
+  ctx.storage.initialize();
 
   LOG("=== CCACHE {} STARTED =========================================",
       CCACHE_VERSION);
@@ -2091,197 +1826,70 @@ initialize(Context& ctx, int argc, const char* const* argv)
 
 // Make a copy of stderr that will not be cached, so things like distcc can
 // send networking errors to it.
-static void
+static nonstd::expected<void, Failure>
 set_up_uncached_err()
 {
   int uncached_fd =
     dup(STDERR_FILENO); // The file descriptor is intentionally leaked.
   if (uncached_fd == -1) {
     LOG("dup(2) failed: {}", strerror(errno));
-    throw Failure(Statistic::internal_error);
+    return nonstd::make_unexpected(Statistic::internal_error);
   }
 
   Util::setenv("UNCACHED_ERR_FD", FMT("{}", uncached_fd));
-}
-
-static void
-configuration_logger(const std::string& key,
-                     const std::string& value,
-                     const std::string& origin)
-{
-  BULK_LOG("Config: ({}) {} = {}", origin, key, value);
-}
-
-static void
-configuration_printer(const std::string& key,
-                      const std::string& value,
-                      const std::string& origin)
-{
-  PRINT(stdout, "({}) {} = {}\n", origin, key, value);
+  return {};
 }
 
 static int cache_compilation(int argc, const char* const* argv);
-static Statistic do_cache_compilation(Context& ctx, const char* const* argv);
 
-static uint8_t
-calculate_wanted_cache_level(uint64_t files_in_level_1)
-{
-  uint64_t files_per_directory = files_in_level_1 / 16;
-  for (uint8_t i = k_min_cache_levels; i <= k_max_cache_levels; ++i) {
-    if (files_per_directory < k_max_cache_files_per_directory) {
-      return i;
-    }
-    files_per_directory /= 16;
-  }
-  return k_max_cache_levels;
-}
+static nonstd::expected<core::StatisticsCounters, Failure>
+do_cache_compilation(Context& ctx, const char* const* argv);
 
-static optional<Counters>
-update_stats_and_maybe_move_cache_file(const Context& ctx,
-                                       const Digest& name,
-                                       const std::string& current_path,
-                                       const Counters& counter_updates,
-                                       const std::string& file_suffix)
+static void
+log_result_to_debug_log(Context& ctx)
 {
-  if (counter_updates.all_zero()) {
-    return nullopt;
-  }
-
-  // Use stats file in the level one subdirectory for cache bookkeeping counters
-  // since cleanup is performed on level one. Use stats file in the level two
-  // subdirectory for other counters to reduce lock contention.
-  const bool use_stats_on_level_1 =
-    counter_updates.get(Statistic::cache_size_kibibyte) != 0
-    || counter_updates.get(Statistic::files_in_cache) != 0;
-  std::string level_string = FMT("{:x}", name.bytes()[0] >> 4);
-  if (!use_stats_on_level_1) {
-    level_string += FMT("/{:x}", name.bytes()[0] & 0xF);
-  }
-  const auto stats_file =
-    FMT("{}/{}/stats", ctx.config.cache_dir(), level_string);
-
-  auto counters =
-    Statistics::update(stats_file, [&counter_updates](Counters& cs) {
-      cs.increment(counter_updates);
-    });
-  if (!counters) {
-    return nullopt;
+  if (ctx.config.log_file().empty() && !ctx.config.debug()) {
+    return;
   }
 
-  if (use_stats_on_level_1) {
-    // Only consider moving the cache file to another level when we have read
-    // the level 1 stats file since it's only then we know the proper
-    // files_in_cache value.
-    const auto wanted_level =
-      calculate_wanted_cache_level(counters->get(Statistic::files_in_cache));
-    const auto wanted_path = Util::get_path_in_cache(
-      ctx.config.cache_dir(), wanted_level, name.to_string() + file_suffix);
-    if (current_path != wanted_path) {
-      Util::ensure_dir_exists(Util::dir_name(wanted_path));
-      LOG("Moving {} to {}", current_path, wanted_path);
-      try {
-        Util::rename(current_path, wanted_path);
-      } catch (const Error&) {
-        // Two ccache processes may move the file at the same time, so failure
-        // to rename is OK.
-      }
-    }
+  core::Statistics statistics(ctx.storage.primary.get_statistics_updates());
+  for (const auto& message : statistics.get_statistics_ids()) {
+    LOG("Result: {}", message);
   }
-  return counters;
 }
 
 static void
-finalize_stats_and_trigger_cleanup(Context& ctx)
+log_result_to_stats_log(Context& ctx)
 {
-  const auto& config = ctx.config;
-
-  if (config.disable()) {
-    // Just log result, don't update statistics.
-    LOG_RAW("Result: disabled");
-    return;
-  }
-
-  if (!config.log_file().empty() || config.debug()) {
-    const auto result = Statistics::get_result(ctx.counter_updates);
-    if (result) {
-      LOG("Result: {}", *result);
-    }
-  }
-
-  if (!config.stats()) {
+  if (ctx.config.stats_log().empty()) {
     return;
   }
 
-  if (!ctx.result_path()) {
-    ASSERT(ctx.counter_updates.get(Statistic::cache_size_kibibyte) == 0);
-    ASSERT(ctx.counter_updates.get(Statistic::files_in_cache) == 0);
-
-    // Context::set_result_path hasn't been called yet, so we just choose one of
-    // the stats files in the 256 level 2 directories.
-    const auto bucket = getpid() % 256;
-    const auto stats_file =
-      FMT("{}/{:x}/{:x}/stats", config.cache_dir(), bucket / 16, bucket % 16);
-    Statistics::update(
-      stats_file, [&ctx](Counters& cs) { cs.increment(ctx.counter_updates); });
-    return;
-  }
-
-  if (ctx.manifest_path()) {
-    update_stats_and_maybe_move_cache_file(ctx,
-                                           *ctx.manifest_name(),
-                                           *ctx.manifest_path(),
-                                           ctx.manifest_counter_updates,
-                                           Manifest::k_file_suffix);
-  }
-
-  const auto counters =
-    update_stats_and_maybe_move_cache_file(ctx,
-                                           *ctx.result_name(),
-                                           *ctx.result_path(),
-                                           ctx.counter_updates,
-                                           Result::k_file_suffix);
-  if (!counters) {
+  core::Statistics statistics(ctx.storage.primary.get_statistics_updates());
+  const auto ids = statistics.get_statistics_ids();
+  if (ids.empty()) {
     return;
   }
 
-  const auto subdir =
-    FMT("{}/{:x}", config.cache_dir(), ctx.result_name()->bytes()[0] >> 4);
-  bool need_cleanup = false;
-
-  if (config.max_files() != 0
-      && counters->get(Statistic::files_in_cache) > config.max_files() / 16) {
-    LOG("Need to clean up {} since it holds {} files (limit: {} files)",
-        subdir,
-        counters->get(Statistic::files_in_cache),
-        config.max_files() / 16);
-    need_cleanup = true;
-  }
-  if (config.max_size() != 0
-      && counters->get(Statistic::cache_size_kibibyte)
-           > config.max_size() / 1024 / 16) {
-    LOG("Need to clean up {} since it holds {} KiB (limit: {} KiB)",
-        subdir,
-        counters->get(Statistic::cache_size_kibibyte),
-        config.max_size() / 1024 / 16);
-    need_cleanup = true;
-  }
-
-  if (need_cleanup) {
-    const double factor = config.limit_multiple() / 16;
-    const uint64_t max_size = round(config.max_size() * factor);
-    const uint32_t max_files = round(config.max_files() * factor);
-    const time_t max_age = 0;
-    clean_up_dir(
-      subdir, max_size, max_files, max_age, [](double /*progress*/) {});
-  }
+  core::StatsLog(ctx.config.stats_log())
+    .log_result(ctx.args_info.input_file, ids);
 }
 
 static void
 finalize_at_exit(Context& ctx)
 {
   try {
-    finalize_stats_and_trigger_cleanup(ctx);
-  } catch (const ErrorBase& e) {
+    if (ctx.config.disable()) {
+      // Just log result, don't update statistics.
+      LOG_RAW("Result: disabled");
+      return;
+    }
+
+    log_result_to_debug_log(ctx);
+    log_result_to_stats_log(ctx);
+
+    ctx.storage.finalize();
+  } catch (const core::ErrorBase& e) {
     // finalize_at_exit must not throw since it's called by a destructor.
     LOG("Error while finalizing stats: {}", e.what());
   }
@@ -2301,11 +1909,12 @@ cache_compilation(int argc, const char* const* argv)
 
   bool fall_back_to_original_compiler = false;
   Args saved_orig_args;
-  nonstd::optional<mode_t> original_umask;
+  nonstd::optional<uint32_t> original_umask;
   std::string saved_temp_dir;
 
   {
     Context ctx;
+    ctx.initialize();
     SignalHandler signal_handler(ctx);
     Finalizer finalizer([&ctx] { finalize_at_exit(ctx); });
 
@@ -2315,16 +1924,12 @@ cache_compilation(int argc, const char* const* argv)
     find_compiler(ctx, &find_executable);
     MTR_END("main", "find_compiler");
 
-    try {
-      Statistic statistic = do_cache_compilation(ctx, argv);
-      ctx.counter_updates.increment(statistic);
-    } catch (const Failure& e) {
-      if (e.statistic() != Statistic::none) {
-        ctx.counter_updates.increment(e.statistic());
-      }
-
-      if (e.exit_code()) {
-        return *e.exit_code();
+    const auto result = do_cache_compilation(ctx, argv);
+    const auto& counters = result ? *result : result.error().counters();
+    ctx.storage.primary.increment_statistics(counters);
+    if (!result) {
+      if (result.error().exit_code()) {
+        return *result.error().exit_code();
       }
       // Else: Fall back to running the real compiler.
       fall_back_to_original_compiler = true;
@@ -2353,33 +1958,36 @@ cache_compilation(int argc, const char* const* argv)
     }
     auto execv_argv = saved_orig_args.to_argv();
     execute_noreturn(execv_argv.data(), saved_temp_dir);
-    throw Fatal(
+    throw core::Fatal(
       "execute_noreturn of {} failed: {}", execv_argv[0], strerror(errno));
   }
 
   return EXIT_SUCCESS;
 }
 
-static Statistic
+static nonstd::expected<core::StatisticsCounters, Failure>
 do_cache_compilation(Context& ctx, const char* const* argv)
 {
   if (ctx.actual_cwd.empty()) {
     LOG("Unable to determine current working directory: {}", strerror(errno));
-    throw Failure(Statistic::internal_error);
-  }
-
-  MTR_BEGIN("main", "clean_up_internal_tempdir");
-  if (ctx.config.temporary_dir() == ctx.config.cache_dir() + "/tmp") {
-    clean_up_internal_tempdir(ctx.config);
+    return nonstd::make_unexpected(Statistic::internal_error);
   }
-  MTR_END("main", "clean_up_internal_tempdir");
 
   if (!ctx.config.log_file().empty() || ctx.config.debug()) {
-    ctx.config.visit_items(configuration_logger);
+    ctx.config.visit_items([&ctx](const std::string& key,
+                                  const std::string& value,
+                                  const std::string& origin) {
+      const auto& log_value =
+        key == "secondary_storage"
+          ? ctx.storage.get_secondary_storage_config_for_logging()
+          : value;
+      BULK_LOG("Config: ({}) {} = {}", origin, key, log_value);
+    });
   }
 
   // Guess compiler after logging the config value in order to be able to
-  // display "compiler_type = auto" before overwriting the value with the guess.
+  // display "compiler_type = auto" before overwriting the value with the
+  // guess.
   if (ctx.config.compiler_type() == CompilerType::auto_guess) {
     ctx.config.set_compiler_type(guess_compiler(ctx.orig_args[0]));
   }
@@ -2387,8 +1995,7 @@ do_cache_compilation(Context& ctx, const char* const* argv)
 
   if (ctx.config.disable()) {
     LOG_RAW("ccache is disabled");
-    // Statistic::cache_miss is a dummy to trigger stats_flush.
-    throw Failure(Statistic::cache_miss);
+    return nonstd::make_unexpected(Statistic::none);
   }
 
   LOG("Command line: {}", Util::format_argv_for_logging(argv));
@@ -2405,10 +2012,10 @@ do_cache_compilation(Context& ctx, const char* const* argv)
   MTR_END("main", "process_args");
 
   if (processed.error) {
-    throw Failure(*processed.error);
+    return nonstd::make_unexpected(*processed.error);
   }
 
-  set_up_uncached_err();
+  TRY(set_up_uncached_err());
 
   if (ctx.config.depend_mode()
       && (!ctx.args_info.generating_dependencies
@@ -2456,10 +2063,11 @@ do_cache_compilation(Context& ctx, const char* const* argv)
   Hash common_hash;
   init_hash_debug(ctx, common_hash, 'c', "COMMON", debug_text_file);
 
-  MTR_BEGIN("hash", "common_hash");
-  hash_common_info(
-    ctx, processed.preprocessor_args, common_hash, ctx.args_info);
-  MTR_END("hash", "common_hash");
+  {
+    MTR_SCOPE("hash", "common_hash");
+    TRY(hash_common_info(
+      ctx, processed.preprocessor_args, common_hash, ctx.args_info));
+  }
 
   // Try to find the hash using the manifest.
   Hash direct_hash = common_hash;
@@ -2469,38 +2077,45 @@ do_cache_compilation(Context& ctx, const char* const* argv)
   args_to_hash.push_back(processed.extra_args_to_hash);
 
   bool put_result_in_manifest = false;
-  optional<Digest> result_name;
-  optional<Digest> result_name_from_manifest;
+  optional<Digest> result_key;
+  optional<Digest> result_key_from_manifest;
+  optional<Digest> manifest_key;
+
   if (ctx.config.direct_mode()) {
     LOG_RAW("Trying direct lookup");
-    MTR_BEGIN("hash", "direct_hash");
     Args dummy_args;
-    result_name =
-      calculate_result_name(ctx, args_to_hash, dummy_args, direct_hash, true);
+    MTR_BEGIN("hash", "direct_hash");
+    const auto result_and_manifest_key = calculate_result_and_manifest_key(
+      ctx, args_to_hash, dummy_args, direct_hash, true);
     MTR_END("hash", "direct_hash");
-    if (result_name) {
-      ctx.set_result_name(*result_name);
-
+    if (!result_and_manifest_key) {
+      return nonstd::make_unexpected(result_and_manifest_key.error());
+    }
+    std::tie(result_key, manifest_key) = *result_and_manifest_key;
+    if (result_key) {
       // If we can return from cache at this point then do so.
-      auto result = from_cache(ctx, FromCacheCallMode::direct);
-      if (result) {
-        return *result;
+      const bool found =
+        from_cache(ctx, FromCacheCallMode::direct, *result_key);
+      if (found) {
+        return Statistic::direct_cache_hit;
       }
 
       // Wasn't able to return from cache at this point. However, the result
       // was already found in manifest, so don't re-add it later.
       put_result_in_manifest = false;
 
-      result_name_from_manifest = result_name;
+      result_key_from_manifest = result_key;
     } else {
       // Add result to manifest later.
       put_result_in_manifest = true;
     }
+
+    ctx.storage.primary.increment_statistic(Statistic::direct_cache_miss);
   }
 
   if (ctx.config.read_only_direct()) {
     LOG_RAW("Read-only direct mode; running real compiler");
-    throw Failure(Statistic::cache_miss);
+    return nonstd::make_unexpected(Statistic::cache_miss);
   }
 
   if (!ctx.config.depend_mode()) {
@@ -2510,21 +2125,19 @@ do_cache_compilation(Context& ctx, const char* const* argv)
     init_hash_debug(ctx, cpp_hash, 'p', "PREPROCESSOR MODE", debug_text_file);
 
     MTR_BEGIN("hash", "cpp_hash");
-    result_name = calculate_result_name(
+    const auto result_and_manifest_key = calculate_result_and_manifest_key(
       ctx, args_to_hash, processed.preprocessor_args, cpp_hash, false);
     MTR_END("hash", "cpp_hash");
+    if (!result_and_manifest_key) {
+      return nonstd::make_unexpected(result_and_manifest_key.error());
+    }
+    result_key = result_and_manifest_key->first;
 
-    // calculate_result_name does not return nullopt if the last (direct_mode)
-    // argument is false.
-    ASSERT(result_name);
-    ctx.set_result_name(*result_name);
-
-    if (result_name_from_manifest && result_name_from_manifest != result_name) {
-      // manifest_path is guaranteed to be set when calculate_result_name
-      // returns a non-nullopt result in direct mode, i.e. when
-      // result_name_from_manifest is set.
-      ASSERT(ctx.manifest_path());
+    // calculate_result_and_manifest_key always returns a non-nullopt result_key
+    // if the last argument (direct_mode) is false.
+    ASSERT(result_key);
 
+    if (result_key_from_manifest && result_key_from_manifest != result_key) {
       // The hash from manifest differs from the hash of the preprocessor
       // output. This could be because:
       //
@@ -2540,24 +2153,26 @@ do_cache_compilation(Context& ctx, const char* const* argv)
       LOG_RAW("Hash from manifest doesn't match preprocessor output");
       LOG_RAW("Likely reason: different CCACHE_BASEDIRs used");
       LOG_RAW("Removing manifest as a safety measure");
-      Util::unlink_safe(*ctx.manifest_path());
+      ctx.storage.remove(*result_key, core::CacheEntryType::result);
 
       put_result_in_manifest = true;
     }
 
     // If we can return from cache at this point then do.
-    auto result = from_cache(ctx, FromCacheCallMode::cpp);
-    if (result) {
-      if (put_result_in_manifest) {
-        update_manifest_file(ctx);
+    const auto found = from_cache(ctx, FromCacheCallMode::cpp, *result_key);
+    if (found) {
+      if (manifest_key && put_result_in_manifest) {
+        update_manifest_file(ctx, *manifest_key, *result_key);
       }
-      return *result;
+      return Statistic::preprocessed_cache_hit;
     }
+
+    ctx.storage.primary.increment_statistic(Statistic::preprocessed_cache_miss);
   }
 
   if (ctx.config.read_only()) {
     LOG_RAW("Read-only mode; running real compiler");
-    throw Failure(Statistic::cache_miss);
+    return nonstd::make_unexpected(Statistic::cache_miss);
   }
 
   add_prefix(ctx, processed.compiler_args, ctx.config.prefix_command());
@@ -2567,261 +2182,25 @@ do_cache_compilation(Context& ctx, const char* const* argv)
 
   // Run real compiler, sending output to cache.
   MTR_BEGIN("cache", "to_cache");
-  to_cache(ctx,
-           processed.compiler_args,
-           ctx.args_info.depend_extra_args,
-           depend_mode_hash);
-  update_manifest_file(ctx);
+  const auto digest = to_cache(ctx,
+                               processed.compiler_args,
+                               result_key,
+                               ctx.args_info.depend_extra_args,
+                               depend_mode_hash);
   MTR_END("cache", "to_cache");
-
-  return Statistic::cache_miss;
-}
-
-// The main program when not doing a compile.
-static int
-handle_main_options(int argc, const char* const* argv)
-{
-  enum longopts {
-    CHECKSUM_FILE,
-    CONFIG_PATH,
-    DUMP_MANIFEST,
-    DUMP_RESULT,
-    EVICT_OLDER_THAN,
-    EXTRACT_RESULT,
-    HASH_FILE,
-    PRINT_STATS,
-  };
-  static const struct option options[] = {
-    {"checksum-file", required_argument, nullptr, CHECKSUM_FILE},
-    {"cleanup", no_argument, nullptr, 'c'},
-    {"clear", no_argument, nullptr, 'C'},
-    {"config-path", required_argument, nullptr, CONFIG_PATH},
-    {"directory", required_argument, nullptr, 'd'},
-    {"dump-manifest", required_argument, nullptr, DUMP_MANIFEST},
-    {"dump-result", required_argument, nullptr, DUMP_RESULT},
-    {"evict-older-than", required_argument, nullptr, EVICT_OLDER_THAN},
-    {"extract-result", required_argument, nullptr, EXTRACT_RESULT},
-    {"get-config", required_argument, nullptr, 'k'},
-    {"hash-file", required_argument, nullptr, HASH_FILE},
-    {"help", no_argument, nullptr, 'h'},
-    {"max-files", required_argument, nullptr, 'F'},
-    {"max-size", required_argument, nullptr, 'M'},
-    {"print-stats", no_argument, nullptr, PRINT_STATS},
-    {"recompress", required_argument, nullptr, 'X'},
-    {"set-config", required_argument, nullptr, 'o'},
-    {"show-compression", no_argument, nullptr, 'x'},
-    {"show-config", no_argument, nullptr, 'p'},
-    {"show-stats", no_argument, nullptr, 's'},
-    {"version", no_argument, nullptr, 'V'},
-    {"zero-stats", no_argument, nullptr, 'z'},
-    {nullptr, 0, nullptr, 0}};
-
-  Context ctx;
-  initialize(ctx, argc, argv);
-
-  int c;
-  while ((c = getopt_long(argc,
-                          const_cast<char* const*>(argv),
-                          "cCd:k:hF:M:po:sVxX:z",
-                          options,
-                          nullptr))
-         != -1) {
-    std::string arg = optarg ? optarg : std::string();
-
-    switch (c) {
-    case CHECKSUM_FILE: {
-      Checksum checksum;
-      Fd fd(arg == "-" ? STDIN_FILENO : open(arg.c_str(), O_RDONLY));
-      Util::read_fd(*fd, [&checksum](const void* data, size_t size) {
-        checksum.update(data, size);
-      });
-      PRINT(stdout, "{:016x}\n", checksum.digest());
-      break;
-    }
-
-    case CONFIG_PATH:
-      Util::setenv("CCACHE_CONFIGPATH", arg);
-      break;
-
-    case DUMP_MANIFEST:
-      return Manifest::dump(arg, stdout) ? 0 : 1;
-
-    case DUMP_RESULT: {
-      ResultDumper result_dumper(stdout);
-      Result::Reader result_reader(arg);
-      auto error = result_reader.read(result_dumper);
-      if (error) {
-        PRINT(stderr, "Error: {}\n", *error);
-      }
-      return error ? EXIT_FAILURE : EXIT_SUCCESS;
-    }
-
-    case EVICT_OLDER_THAN: {
-      auto seconds = Util::parse_duration(arg);
-      ProgressBar progress_bar("Evicting...");
-      clean_old(
-        ctx, [&](double progress) { progress_bar.update(progress); }, seconds);
-      if (isatty(STDOUT_FILENO)) {
-        PRINT_RAW(stdout, "\n");
-      }
-      break;
-    }
-
-    case EXTRACT_RESULT: {
-      ResultExtractor result_extractor(".");
-      Result::Reader result_reader(arg);
-      auto error = result_reader.read(result_extractor);
-      if (error) {
-        PRINT(stderr, "Error: {}\n", *error);
-      }
-      return error ? EXIT_FAILURE : EXIT_SUCCESS;
-    }
-
-    case HASH_FILE: {
-      Hash hash;
-      if (arg == "-") {
-        hash.hash_fd(STDIN_FILENO);
-      } else {
-        hash.hash_file(arg);
-      }
-      PRINT(stdout, "{}\n", hash.digest().to_string());
-      break;
-    }
-
-    case PRINT_STATS:
-      PRINT_RAW(stdout, Statistics::format_machine_readable(ctx.config));
-      break;
-
-    case 'c': // --cleanup
-    {
-      ProgressBar progress_bar("Cleaning...");
-      clean_up_all(ctx.config,
-                   [&](double progress) { progress_bar.update(progress); });
-      if (isatty(STDOUT_FILENO)) {
-        PRINT_RAW(stdout, "\n");
-      }
-      break;
-    }
-
-    case 'C': // --clear
-    {
-      ProgressBar progress_bar("Clearing...");
-      wipe_all(ctx, [&](double progress) { progress_bar.update(progress); });
-      if (isatty(STDOUT_FILENO)) {
-        PRINT_RAW(stdout, "\n");
-      }
-      break;
-    }
-
-    case 'd': // --directory
-      Util::setenv("CCACHE_DIR", arg);
-      break;
-
-    case 'h': // --help
-      PRINT(stdout, USAGE_TEXT, CCACHE_NAME, CCACHE_NAME);
-      exit(EXIT_SUCCESS);
-
-    case 'k': // --get-config
-      PRINT(stdout, "{}\n", ctx.config.get_string_value(arg));
-      break;
-
-    case 'F': { // --max-files
-      auto files = Util::parse_unsigned(arg);
-      Config::set_value_in_file(
-        ctx.config.primary_config_path(), "max_files", arg);
-      if (files == 0) {
-        PRINT_RAW(stdout, "Unset cache file limit\n");
-      } else {
-        PRINT(stdout, "Set cache file limit to {}\n", files);
-      }
-      break;
-    }
-
-    case 'M': { // --max-size
-      uint64_t size = Util::parse_size(arg);
-      Config::set_value_in_file(
-        ctx.config.primary_config_path(), "max_size", arg);
-      if (size == 0) {
-        PRINT_RAW(stdout, "Unset cache size limit\n");
-      } else {
-        PRINT(stdout,
-              "Set cache size limit to {}\n",
-              Util::format_human_readable_size(size));
-      }
-      break;
-    }
-
-    case 'o': { // --set-config
-      // Start searching for equal sign at position 1 to improve error message
-      // for the -o=K=V case (key "=K" and value "V").
-      size_t eq_pos = arg.find('=', 1);
-      if (eq_pos == std::string::npos) {
-        throw Error("missing equal sign in \"{}\"", arg);
-      }
-      std::string key = arg.substr(0, eq_pos);
-      std::string value = arg.substr(eq_pos + 1);
-      Config::set_value_in_file(ctx.config.primary_config_path(), key, value);
-      break;
-    }
-
-    case 'p': // --show-config
-      ctx.config.visit_items(configuration_printer);
-      break;
-
-    case 's': // --show-stats
-      PRINT_RAW(stdout, Statistics::format_human_readable(ctx.config));
-      break;
-
-    case 'V': // --version
-      PRINT(VERSION_TEXT, CCACHE_NAME, CCACHE_VERSION);
-      exit(EXIT_SUCCESS);
-
-    case 'x': // --show-compression
-    {
-      ProgressBar progress_bar("Scanning...");
-      compress_stats(ctx.config,
-                     [&](double progress) { progress_bar.update(progress); });
-      break;
-    }
-
-    case 'X': // --recompress
-    {
-      optional<int8_t> wanted_level;
-      if (arg == "uncompressed") {
-        wanted_level = nullopt;
-      } else {
-        wanted_level =
-          Util::parse_signed(arg, INT8_MIN, INT8_MAX, "compression level");
-      }
-
-      ProgressBar progress_bar("Recompressing...");
-      compress_recompress(ctx, wanted_level, [&](double progress) {
-        progress_bar.update(progress);
-      });
-      break;
-    }
-
-    case 'z': // --zero-stats
-      Statistics::zero_all_counters(ctx.config);
-      PRINT_RAW(stdout, "Statistics zeroed\n");
-      break;
-
-    default:
-      PRINT(stderr, USAGE_TEXT, CCACHE_NAME, CCACHE_NAME);
-      exit(EXIT_FAILURE);
-    }
-
-    // Some of the above switches might have changed config settings, so run the
-    // setup again.
-    ctx.config = Config();
-    set_up_config(ctx.config);
+  if (!digest) {
+    return nonstd::make_unexpected(digest.error());
+  }
+  result_key = *digest;
+  if (ctx.config.direct_mode()) {
+    ASSERT(manifest_key);
+    MTR_SCOPE("cache", "update_manifest");
+    update_manifest_file(ctx, *manifest_key, *result_key);
   }
 
-  return 0;
+  return ctx.config.recache() ? Statistic::recache : Statistic::cache_miss;
 }
 
-int ccache_main(int argc, const char* const* argv);
-
 int
 ccache_main(int argc, const char* const* argv)
 {
@@ -2830,18 +2209,18 @@ ccache_main(int argc, const char* const* argv)
     std::string program_name(Util::base_name(argv[0]));
     if (Util::same_program_name(program_name, CCACHE_NAME)) {
       if (argc < 2) {
-        PRINT(stderr, USAGE_TEXT, CCACHE_NAME, CCACHE_NAME);
+        PRINT_RAW(stderr, core::get_usage_text());
         exit(EXIT_FAILURE);
       }
-      // If the first argument isn't an option, then assume we are being passed
-      // a compiler name and options.
+      // If the first argument isn't an option, then assume we are being
+      // passed a compiler name and options.
       if (argv[1][0] == '-') {
-        return handle_main_options(argc, argv);
+        return core::process_main_options(argc, argv);
       }
     }
 
     return cache_compilation(argc, argv);
-  } catch (const ErrorBase& e) {
+  } catch (const core::ErrorBase& e) {
     PRINT(stderr, "ccache: error: {}\n", e.what());
     return EXIT_FAILURE;
   }
index bf34cb0eb34e80ce34bdfc8ac365f1f5c6936190..2b799fe8e12dceb04ce2996947f90ad4bc3a4029 100644 (file)
@@ -1,5 +1,5 @@
 // Copyright (C) 2002-2007 Andrew Tridgell
-// Copyright (C) 2009-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2009-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -19,8 +19,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 #include "Config.hpp"
 
 #include "third_party/nonstd/string_view.hpp"
@@ -30,6 +28,7 @@
 
 class Context;
 
+extern const char CCACHE_NAME[];
 extern const char CCACHE_VERSION[];
 
 using FindExecutableFunction =
@@ -37,6 +36,8 @@ using FindExecutableFunction =
                             const std::string& name,
                             const std::string& exclude_name)>;
 
+int ccache_main(int argc, const char* const* argv);
+
 // Tested by unit tests.
 void find_compiler(Context& ctx,
                    const FindExecutableFunction& find_executable_function);
diff --git a/src/cleanup.cpp b/src/cleanup.cpp
deleted file mode 100644 (file)
index 5c76ebb..0000000
+++ /dev/null
@@ -1,231 +0,0 @@
-// Copyright (C) 2002-2006 Andrew Tridgell
-// Copyright (C) 2009-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "cleanup.hpp"
-
-#include "CacheFile.hpp"
-#include "Config.hpp"
-#include "Context.hpp"
-#include "Logging.hpp"
-#include "Statistics.hpp"
-#include "Util.hpp"
-
-#ifdef INODE_CACHE_SUPPORTED
-#  include "InodeCache.hpp"
-#endif
-
-#include <algorithm>
-
-static void
-delete_file(const std::string& path,
-            uint64_t size,
-            uint64_t* cache_size,
-            uint64_t* files_in_cache)
-{
-  bool deleted = Util::unlink_safe(path, Util::UnlinkLog::ignore_failure);
-  if (!deleted && errno != ENOENT && errno != ESTALE) {
-    LOG("Failed to unlink {} ({})", path, strerror(errno));
-  } else if (cache_size && files_in_cache) {
-    // The counters are intentionally subtracted even if there was no file to
-    // delete since the final cache size calculation will be incorrect if they
-    // aren't. (This can happen when there are several parallel ongoing
-    // cleanups of the same directory.)
-    *cache_size -= size;
-    --*files_in_cache;
-  }
-}
-
-static void
-update_counters(const std::string& dir,
-                uint64_t files_in_cache,
-                uint64_t cache_size,
-                bool cleanup_performed)
-{
-  const std::string stats_file = dir + "/stats";
-  Statistics::update(stats_file, [=](Counters& cs) {
-    if (cleanup_performed) {
-      cs.increment(Statistic::cleanups_performed);
-    }
-    cs.set(Statistic::files_in_cache, files_in_cache);
-    cs.set(Statistic::cache_size_kibibyte, cache_size / 1024);
-  });
-}
-
-void
-clean_old(const Context& ctx,
-          const Util::ProgressReceiver& progress_receiver,
-          uint64_t max_age)
-{
-  Util::for_each_level_1_subdir(
-    ctx.config.cache_dir(),
-    [&](const std::string& subdir,
-        const Util::ProgressReceiver& sub_progress_receiver) {
-      clean_up_dir(subdir, 0, 0, max_age, sub_progress_receiver);
-    },
-    progress_receiver);
-}
-
-// Clean up one cache subdirectory.
-void
-clean_up_dir(const std::string& subdir,
-             uint64_t max_size,
-             uint64_t max_files,
-             uint64_t max_age,
-             const Util::ProgressReceiver& progress_receiver)
-{
-  LOG("Cleaning up cache directory {}", subdir);
-
-  std::vector<CacheFile> files = Util::get_level_1_files(
-    subdir, [&](double progress) { progress_receiver(progress / 3); });
-
-  uint64_t cache_size = 0;
-  uint64_t files_in_cache = 0;
-  time_t current_time = time(nullptr);
-
-  for (size_t i = 0; i < files.size();
-       ++i, progress_receiver(1.0 / 3 + 1.0 * i / files.size() / 3)) {
-    const auto& file = files[i];
-
-    if (!file.lstat().is_regular()) {
-      // Not a file or missing file.
-      continue;
-    }
-
-    // Delete any tmp files older than 1 hour right away.
-    if (file.lstat().mtime() + 3600 < current_time
-        && Util::base_name(file.path()).find(".tmp.") != std::string::npos) {
-      Util::unlink_tmp(file.path());
-      continue;
-    }
-
-    cache_size += file.lstat().size_on_disk();
-    files_in_cache += 1;
-  }
-
-  // Sort according to modification time, oldest first.
-  std::sort(
-    files.begin(), files.end(), [](const CacheFile& f1, const CacheFile& f2) {
-      return f1.lstat().mtime() < f2.lstat().mtime();
-    });
-
-  LOG("Before cleanup: {:.0f} KiB, {:.0f} files",
-      static_cast<double>(cache_size) / 1024,
-      static_cast<double>(files_in_cache));
-
-  bool cleaned = false;
-  for (size_t i = 0; i < files.size();
-       ++i, progress_receiver(2.0 / 3 + 1.0 * i / files.size() / 3)) {
-    const auto& file = files[i];
-
-    if (!file.lstat() || file.lstat().is_directory()) {
-      continue;
-    }
-
-    if ((max_size == 0 || cache_size <= max_size)
-        && (max_files == 0 || files_in_cache <= max_files)
-        && (max_age == 0
-            || file.lstat().mtime()
-                 > (current_time - static_cast<int64_t>(max_age)))) {
-      break;
-    }
-
-    if (Util::ends_with(file.path(), ".stderr")) {
-      // In order to be nice to legacy ccache versions, make sure that the .o
-      // file is deleted before .stderr, because if the ccache process gets
-      // killed after deleting the .stderr but before deleting the .o, the
-      // cached result will be inconsistent. (.stderr is the only file that is
-      // optional for legacy ccache versions; any other file missing from the
-      // cache will be detected.)
-      std::string o_file = file.path().substr(0, file.path().size() - 6) + "o";
-
-      // Don't subtract this extra deletion from the cache size; that
-      // bookkeeping will be done when the loop reaches the .o file. If the
-      // loop doesn't reach the .o file since the target limits have been
-      // reached, the bookkeeping won't happen, but that small counter
-      // discrepancy won't do much harm and it will correct itself in the next
-      // cleanup.
-      delete_file(o_file, 0, nullptr, nullptr);
-    }
-
-    delete_file(
-      file.path(), file.lstat().size_on_disk(), &cache_size, &files_in_cache);
-    cleaned = true;
-  }
-
-  LOG("After cleanup: {:.0f} KiB, {:.0f} files",
-      static_cast<double>(cache_size) / 1024,
-      static_cast<double>(files_in_cache));
-
-  if (cleaned) {
-    LOG("Cleaned up cache directory {}", subdir);
-  }
-
-  update_counters(subdir, files_in_cache, cache_size, cleaned);
-}
-
-// Clean up all cache subdirectories.
-void
-clean_up_all(const Config& config,
-             const Util::ProgressReceiver& progress_receiver)
-{
-  Util::for_each_level_1_subdir(
-    config.cache_dir(),
-    [&](const std::string& subdir,
-        const Util::ProgressReceiver& sub_progress_receiver) {
-      clean_up_dir(subdir,
-                   config.max_size() / 16,
-                   config.max_files() / 16,
-                   0,
-                   sub_progress_receiver);
-    },
-    progress_receiver);
-}
-
-// Wipe one cache subdirectory.
-static void
-wipe_dir(const std::string& subdir,
-         const Util::ProgressReceiver& progress_receiver)
-{
-  LOG("Clearing out cache directory {}", subdir);
-
-  const std::vector<CacheFile> files = Util::get_level_1_files(
-    subdir, [&](double progress) { progress_receiver(progress / 2); });
-
-  for (size_t i = 0; i < files.size(); ++i) {
-    Util::unlink_safe(files[i].path());
-    progress_receiver(0.5 + 0.5 * i / files.size());
-  }
-
-  const bool cleared = !files.empty();
-  if (cleared) {
-    LOG("Cleared out cache directory {}", subdir);
-  }
-  update_counters(subdir, 0, 0, cleared);
-}
-
-// Wipe all cached files in all subdirectories.
-void
-wipe_all(const Context& ctx, const Util::ProgressReceiver& progress_receiver)
-{
-  Util::for_each_level_1_subdir(
-    ctx.config.cache_dir(), wipe_dir, progress_receiver);
-#ifdef INODE_CACHE_SUPPORTED
-  ctx.inode_cache.drop();
-#endif
-}
diff --git a/src/cleanup.hpp b/src/cleanup.hpp
deleted file mode 100644 (file)
index 053eb53..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include "system.hpp"
-
-#include "Util.hpp"
-
-#include <string>
-
-class Config;
-class Context;
-
-void clean_old(const Context& ctx,
-               const Util::ProgressReceiver& progress_receiver,
-               uint64_t max_age);
-
-void clean_up_dir(const std::string& subdir,
-                  uint64_t max_size,
-                  uint64_t max_files,
-                  uint64_t max_age,
-                  const Util::ProgressReceiver& progress_receiver);
-
-void clean_up_all(const Config& config,
-                  const Util::ProgressReceiver& progress_receiver);
-
-void wipe_all(const Context& ctx,
-              const Util::ProgressReceiver& progress_receiver);
index 3ed4f5b5b8acb0ec44558407834cd1b79af3bc41..8cc516e0b042fb5d4d728a3e0dfede854a64dc32 100644 (file)
@@ -45,6 +45,8 @@
 // The option only affects compilation; not passed to the preprocessor.
 #define AFFECTS_COMP (1 << 6)
 
+#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
+
 struct CompOpt
 {
   const char* name;
@@ -55,6 +57,7 @@ const CompOpt compopts[] = {
   {"--Werror", TAKES_ARG},                            // nvcc
   {"--analyze", TOO_HARD},                            // Clang
   {"--compiler-bindir", AFFECTS_CPP | TAKES_ARG},     // nvcc
+  {"--config", TAKES_ARG},                            // Clang
   {"--libdevice-directory", AFFECTS_CPP | TAKES_ARG}, // nvcc
   {"--output-directory", AFFECTS_CPP | TAKES_ARG},    // nvcc
   {"--param", TAKES_ARG},
index a0169280a1749d335c2134ebadb9fbee1a3c96e6..29a3354b0061123c84f7004f4a0650c8244387aa 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2010-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2010-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,8 +18,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 #include <string>
 
 bool compopt_short(bool (*fn)(const std::string& option),
diff --git a/src/compress.cpp b/src/compress.cpp
deleted file mode 100644 (file)
index 1164b79..0000000
+++ /dev/null
@@ -1,372 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "compress.hpp"
-
-#include "AtomicFile.hpp"
-#include "CacheEntryReader.hpp"
-#include "CacheEntryWriter.hpp"
-#include "Context.hpp"
-#include "File.hpp"
-#include "Logging.hpp"
-#include "Manifest.hpp"
-#include "Result.hpp"
-#include "Statistics.hpp"
-#include "StdMakeUnique.hpp"
-#include "ThreadPool.hpp"
-#include "ZstdCompressor.hpp"
-#include "assertions.hpp"
-#include "fmtmacros.hpp"
-
-#include "third_party/fmt/core.h"
-
-#include <string>
-#include <thread>
-
-using nonstd::optional;
-
-namespace {
-
-class RecompressionStatistics
-{
-public:
-  void update(uint64_t content_size,
-              uint64_t old_size,
-              uint64_t new_size,
-              uint64_t incompressible_size);
-  uint64_t content_size() const;
-  uint64_t old_size() const;
-  uint64_t new_size() const;
-  uint64_t incompressible_size() const;
-
-private:
-  mutable std::mutex m_mutex;
-  uint64_t m_content_size = 0;
-  uint64_t m_old_size = 0;
-  uint64_t m_new_size = 0;
-  uint64_t m_incompressible_size = 0;
-};
-
-void
-RecompressionStatistics::update(uint64_t content_size,
-                                uint64_t old_size,
-                                uint64_t new_size,
-                                uint64_t incompressible_size)
-{
-  std::unique_lock<std::mutex> lock(m_mutex);
-  m_incompressible_size += incompressible_size;
-  m_content_size += content_size;
-  m_old_size += old_size;
-  m_new_size += new_size;
-}
-
-uint64_t
-RecompressionStatistics::content_size() const
-{
-  std::unique_lock<std::mutex> lock(m_mutex);
-  return m_content_size;
-}
-
-uint64_t
-RecompressionStatistics::old_size() const
-{
-  std::unique_lock<std::mutex> lock(m_mutex);
-  return m_old_size;
-}
-
-uint64_t
-RecompressionStatistics::new_size() const
-{
-  std::unique_lock<std::mutex> lock(m_mutex);
-  return m_new_size;
-}
-
-uint64_t
-RecompressionStatistics::incompressible_size() const
-{
-  std::unique_lock<std::mutex> lock(m_mutex);
-  return m_incompressible_size;
-}
-
-File
-open_file(const std::string& path, const char* mode)
-{
-  File f(path, mode);
-  if (!f) {
-    throw Error("failed to open {} for reading: {}", path, strerror(errno));
-  }
-  return f;
-}
-
-std::unique_ptr<CacheEntryReader>
-create_reader(const CacheFile& cache_file, FILE* stream)
-{
-  if (cache_file.type() == CacheFile::Type::unknown) {
-    throw Error("unknown file type for {}", cache_file.path());
-  }
-
-  switch (cache_file.type()) {
-  case CacheFile::Type::result:
-    return std::make_unique<CacheEntryReader>(
-      stream, Result::k_magic, Result::k_version);
-
-  case CacheFile::Type::manifest:
-    return std::make_unique<CacheEntryReader>(
-      stream, Manifest::k_magic, Manifest::k_version);
-
-  case CacheFile::Type::unknown:
-    ASSERT(false); // Handled at function entry.
-  }
-
-  ASSERT(false);
-}
-
-std::unique_ptr<CacheEntryWriter>
-create_writer(FILE* stream,
-              const CacheEntryReader& reader,
-              Compression::Type compression_type,
-              int8_t compression_level)
-{
-  return std::make_unique<CacheEntryWriter>(stream,
-                                            reader.magic(),
-                                            reader.version(),
-                                            compression_type,
-                                            compression_level,
-                                            reader.payload_size());
-}
-
-void
-recompress_file(RecompressionStatistics& statistics,
-                const std::string& stats_file,
-                const CacheFile& cache_file,
-                optional<int8_t> level)
-{
-  auto file = open_file(cache_file.path(), "rb");
-  auto reader = create_reader(cache_file, file.get());
-
-  auto old_stat = Stat::stat(cache_file.path(), Stat::OnError::log);
-  uint64_t content_size = reader->content_size();
-  int8_t wanted_level =
-    level ? (*level == 0 ? ZstdCompressor::default_compression_level : *level)
-          : 0;
-
-  if (reader->compression_level() == wanted_level) {
-    statistics.update(content_size, old_stat.size(), old_stat.size(), 0);
-    return;
-  }
-
-  LOG("Recompressing {} to {}",
-      cache_file.path(),
-      level ? FMT("level {}", wanted_level) : "uncompressed");
-  AtomicFile atomic_new_file(cache_file.path(), AtomicFile::Mode::binary);
-  auto writer =
-    create_writer(atomic_new_file.stream(),
-                  *reader,
-                  level ? Compression::Type::zstd : Compression::Type::none,
-                  wanted_level);
-
-  char buffer[READ_BUFFER_SIZE];
-  size_t bytes_left = reader->payload_size();
-  while (bytes_left > 0) {
-    size_t bytes_to_read = std::min(bytes_left, sizeof(buffer));
-    reader->read(buffer, bytes_to_read);
-    writer->write(buffer, bytes_to_read);
-    bytes_left -= bytes_to_read;
-  }
-  reader->finalize();
-  writer->finalize();
-
-  file.close();
-
-  atomic_new_file.commit();
-  auto new_stat = Stat::stat(cache_file.path(), Stat::OnError::log);
-
-  Statistics::update(stats_file, [=](Counters& cs) {
-    cs.increment(Statistic::cache_size_kibibyte,
-                 Util::size_change_kibibyte(old_stat, new_stat));
-  });
-
-  statistics.update(content_size, old_stat.size(), new_stat.size(), 0);
-
-  LOG("Recompression of {} done", cache_file.path());
-}
-
-} // namespace
-
-void
-compress_stats(const Config& config,
-               const Util::ProgressReceiver& progress_receiver)
-{
-  uint64_t on_disk_size = 0;
-  uint64_t compr_size = 0;
-  uint64_t content_size = 0;
-  uint64_t incompr_size = 0;
-
-  Util::for_each_level_1_subdir(
-    config.cache_dir(),
-    [&](const std::string& subdir,
-        const Util::ProgressReceiver& sub_progress_receiver) {
-      const std::vector<CacheFile> files = Util::get_level_1_files(
-        subdir, [&](double progress) { sub_progress_receiver(progress / 2); });
-
-      for (size_t i = 0; i < files.size(); ++i) {
-        const auto& cache_file = files[i];
-        on_disk_size += cache_file.lstat().size_on_disk();
-
-        try {
-          auto file = open_file(cache_file.path(), "rb");
-          auto reader = create_reader(cache_file, file.get());
-          compr_size += cache_file.lstat().size();
-          content_size += reader->content_size();
-        } catch (Error&) {
-          incompr_size += cache_file.lstat().size();
-        }
-
-        sub_progress_receiver(1.0 / 2 + 1.0 * i / files.size() / 2);
-      }
-    },
-    progress_receiver);
-
-  if (isatty(STDOUT_FILENO)) {
-    PRINT_RAW(stdout, "\n\n");
-  }
-
-  double ratio =
-    compr_size > 0 ? static_cast<double>(content_size) / compr_size : 0.0;
-  double savings = ratio > 0.0 ? 100.0 - (100.0 / ratio) : 0.0;
-
-  std::string on_disk_size_str = Util::format_human_readable_size(on_disk_size);
-  std::string cache_size_str =
-    Util::format_human_readable_size(compr_size + incompr_size);
-  std::string compr_size_str = Util::format_human_readable_size(compr_size);
-  std::string content_size_str = Util::format_human_readable_size(content_size);
-  std::string incompr_size_str = Util::format_human_readable_size(incompr_size);
-
-  PRINT(stdout,
-        "Total data:            {:>8s} ({} disk blocks)\n",
-        cache_size_str,
-        on_disk_size_str);
-  PRINT(stdout,
-        "Compressed data:       {:>8s} ({:.1f}% of original size)\n",
-        compr_size_str,
-        100.0 - savings);
-  PRINT(stdout, "  - Original data:     {:>8s}\n", content_size_str);
-  PRINT(stdout,
-        "  - Compression ratio: {:>5.3f} x  ({:.1f}% space savings)\n",
-        ratio,
-        savings);
-  PRINT(stdout, "Incompressible data:   {:>8s}\n", incompr_size_str);
-}
-
-void
-compress_recompress(Context& ctx,
-                    optional<int8_t> level,
-                    const Util::ProgressReceiver& progress_receiver)
-{
-  const size_t threads = std::thread::hardware_concurrency();
-  const size_t read_ahead = 2 * threads;
-  ThreadPool thread_pool(threads, read_ahead);
-  RecompressionStatistics statistics;
-
-  Util::for_each_level_1_subdir(
-    ctx.config.cache_dir(),
-    [&](const std::string& subdir,
-        const Util::ProgressReceiver& sub_progress_receiver) {
-      std::vector<CacheFile> files =
-        Util::get_level_1_files(subdir, [&](double progress) {
-          sub_progress_receiver(0.1 * progress);
-        });
-
-      auto stats_file = subdir + "/stats";
-
-      for (size_t i = 0; i < files.size(); ++i) {
-        const auto& file = files[i];
-
-        if (file.type() != CacheFile::Type::unknown) {
-          thread_pool.enqueue([&statistics, stats_file, file, level] {
-            try {
-              recompress_file(statistics, stats_file, file, level);
-            } catch (Error&) {
-              // Ignore for now.
-            }
-          });
-        } else {
-          statistics.update(0, 0, 0, file.lstat().size());
-        }
-
-        sub_progress_receiver(0.1 + 0.9 * i / files.size());
-      }
-
-      if (Util::ends_with(subdir, "f")) {
-        // Wait here instead of after Util::for_each_level_1_subdir to avoid
-        // updating the progress bar to 100% before all work is done.
-        thread_pool.shut_down();
-      }
-    },
-    progress_receiver);
-
-  if (isatty(STDOUT_FILENO)) {
-    PRINT_RAW(stdout, "\n\n");
-  }
-
-  double old_ratio =
-    statistics.old_size() > 0
-      ? static_cast<double>(statistics.content_size()) / statistics.old_size()
-      : 0.0;
-  double old_savings = old_ratio > 0.0 ? 100.0 - (100.0 / old_ratio) : 0.0;
-  double new_ratio =
-    statistics.new_size() > 0
-      ? static_cast<double>(statistics.content_size()) / statistics.new_size()
-      : 0.0;
-  double new_savings = new_ratio > 0.0 ? 100.0 - (100.0 / new_ratio) : 0.0;
-  int64_t size_difference = static_cast<int64_t>(statistics.new_size())
-                            - static_cast<int64_t>(statistics.old_size());
-
-  std::string old_compr_size_str =
-    Util::format_human_readable_size(statistics.old_size());
-  std::string new_compr_size_str =
-    Util::format_human_readable_size(statistics.new_size());
-  std::string content_size_str =
-    Util::format_human_readable_size(statistics.content_size());
-  std::string incompr_size_str =
-    Util::format_human_readable_size(statistics.incompressible_size());
-  std::string size_difference_str =
-    FMT("{}{}",
-        size_difference < 0 ? "-" : (size_difference > 0 ? "+" : " "),
-        Util::format_human_readable_size(
-          size_difference < 0 ? -size_difference : size_difference));
-
-  PRINT(stdout, "Original data:         {:>8s}\n", content_size_str);
-  PRINT(stdout,
-        "Old compressed data:   {:>8s} ({:.1f}% of original size)\n",
-        old_compr_size_str,
-        100.0 - old_savings);
-  PRINT(stdout,
-        "  - Compression ratio: {:>5.3f} x  ({:.1f}% space savings)\n",
-        old_ratio,
-        old_savings);
-  PRINT(stdout,
-        "New compressed data:   {:>8s} ({:.1f}% of original size)\n",
-        new_compr_size_str,
-        100.0 - new_savings);
-  PRINT(stdout,
-        "  - Compression ratio: {:>5.3f} x  ({:.1f}% space savings)\n",
-        new_ratio,
-        new_savings);
-  PRINT(stdout, "Size change:          {:>9s}\n", size_difference_str);
-}
diff --git a/src/compress.hpp b/src/compress.hpp
deleted file mode 100644 (file)
index 91eb04d..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include "system.hpp"
-
-#include "Util.hpp"
-
-#include "third_party/nonstd/optional.hpp"
-
-class Config;
-class Context;
-
-void compress_stats(const Config& config,
-                    const Util::ProgressReceiver& progress_receiver);
-
-// Recompress the cache.
-//
-// Arguments:
-// - ctx: The context.
-// - level: Target compression level (positive or negative value for actual
-//   level, 0 for default level and nonstd::nullopt for no compression).
-// - progress_receiver: Function that will be called for progress updates.
-void compress_recompress(Context& ctx,
-                         nonstd::optional<int8_t> level,
-                         const Util::ProgressReceiver& progress_receiver);
diff --git a/src/compression/CMakeLists.txt b/src/compression/CMakeLists.txt
new file mode 100644 (file)
index 0000000..042a0b4
--- /dev/null
@@ -0,0 +1,12 @@
+set(
+  sources
+  ${CMAKE_CURRENT_SOURCE_DIR}/Compressor.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/Decompressor.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/NullCompressor.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/NullDecompressor.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/ZstdCompressor.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/ZstdDecompressor.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/types.cpp
+)
+
+target_sources(ccache_framework PRIVATE ${sources})
diff --git a/src/compression/Compressor.cpp b/src/compression/Compressor.cpp
new file mode 100644 (file)
index 0000000..cd695be
--- /dev/null
@@ -0,0 +1,45 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "Compressor.hpp"
+
+#include "NullCompressor.hpp"
+#include "ZstdCompressor.hpp"
+#include "assertions.hpp"
+
+#include <memory>
+
+namespace compression {
+
+std::unique_ptr<Compressor>
+Compressor::create_from_type(const Type type,
+                             FILE* const stream,
+                             const int8_t compression_level)
+{
+  switch (type) {
+  case compression::Type::none:
+    return std::make_unique<NullCompressor>(stream);
+
+  case compression::Type::zstd:
+    return std::make_unique<ZstdCompressor>(stream, compression_level);
+  }
+
+  ASSERT(false);
+}
+
+} // namespace compression
diff --git a/src/compression/Compressor.hpp b/src/compression/Compressor.hpp
new file mode 100644 (file)
index 0000000..9c8e563
--- /dev/null
@@ -0,0 +1,70 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <compression/types.hpp>
+
+#include <cstdint>
+#include <cstdio>
+#include <memory>
+
+namespace compression {
+
+class Compressor
+{
+public:
+  virtual ~Compressor() = default;
+
+  // Create a compressor for the specified type.
+  //
+  // Parameters:
+  // - type: The type.
+  // - stream: The stream to write to.
+  // - compression_level: Desired compression level.
+  static std::unique_ptr<Compressor>
+  create_from_type(Type type, FILE* stream, int8_t compression_level);
+
+  // Get the actual compression level used for the compressed stream.
+  virtual int8_t actual_compression_level() const = 0;
+
+  // Write data from a buffer to the compressed stream.
+  //
+  // Parameters:
+  // - data: Data to write.
+  // - count: Size of data to write.
+  //
+  // Throws Error on failure.
+  virtual void write(const void* data, size_t count) = 0;
+
+  // Write an unsigned integer to the compressed stream.
+  //
+  // Parameters:
+  // - value: Value to write.
+  //
+  // Throws Error on failure.
+  template<typename T> void write(T value);
+
+  // Finalize compression.
+  //
+  // This method checks that the end state of the compressed stream is correct
+  // and throws Error if not.
+  virtual void finalize() = 0;
+};
+
+} // namespace compression
diff --git a/src/compression/Decompressor.cpp b/src/compression/Decompressor.cpp
new file mode 100644 (file)
index 0000000..3a76b16
--- /dev/null
@@ -0,0 +1,41 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "Decompressor.hpp"
+
+#include "NullDecompressor.hpp"
+#include "ZstdDecompressor.hpp"
+#include "assertions.hpp"
+
+namespace compression {
+
+std::unique_ptr<Decompressor>
+Decompressor::create_from_type(Type type, FILE* stream)
+{
+  switch (type) {
+  case compression::Type::none:
+    return std::make_unique<NullDecompressor>(stream);
+
+  case compression::Type::zstd:
+    return std::make_unique<ZstdDecompressor>(stream);
+  }
+
+  ASSERT(false);
+}
+
+} // namespace compression
diff --git a/src/compression/Decompressor.hpp b/src/compression/Decompressor.hpp
new file mode 100644 (file)
index 0000000..8d6b173
--- /dev/null
@@ -0,0 +1,57 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <compression/types.hpp>
+
+#include <cstdio>
+#include <memory>
+
+namespace compression {
+
+class Decompressor
+{
+public:
+  virtual ~Decompressor() = default;
+
+  // Create a decompressor for the specified type.
+  //
+  // Parameters:
+  // - type: The type.
+  // - stream: The stream to read from.
+  static std::unique_ptr<Decompressor> create_from_type(Type type,
+                                                        FILE* stream);
+
+  // Read data into a buffer from the compressed stream.
+  //
+  // Parameters:
+  // - data: Buffer to write decompressed data to.
+  // - count: How many bytes to write.
+  //
+  // Throws Error on failure.
+  virtual void read(void* data, size_t count) = 0;
+
+  // Finalize decompression.
+  //
+  // This method checks that the end state of the compressed stream is correct
+  // and throws Error if not.
+  virtual void finalize() = 0;
+};
+
+} // namespace compression
diff --git a/src/compression/NullCompressor.cpp b/src/compression/NullCompressor.cpp
new file mode 100644 (file)
index 0000000..4d5ee67
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "NullCompressor.hpp"
+
+#include <core/exceptions.hpp>
+
+namespace compression {
+
+NullCompressor::NullCompressor(FILE* const stream) : m_stream(stream)
+{
+}
+
+int8_t
+NullCompressor::actual_compression_level() const
+{
+  return 0;
+}
+
+void
+NullCompressor::write(const void* const data, const size_t count)
+{
+  if (fwrite(data, 1, count, m_stream) != count) {
+    throw core::Error("failed to write to uncompressed stream");
+  }
+}
+
+void
+NullCompressor::finalize()
+{
+  if (fflush(m_stream) != 0) {
+    throw core::Error("failed to finalize uncompressed stream");
+  }
+}
+
+} // namespace compression
diff --git a/src/compression/NullCompressor.hpp b/src/compression/NullCompressor.hpp
new file mode 100644 (file)
index 0000000..2f26c7b
--- /dev/null
@@ -0,0 +1,45 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include "Compressor.hpp"
+
+#include <NonCopyable.hpp>
+
+#include <cstdio>
+
+namespace compression {
+
+// A compressor of an uncompressed stream.
+class NullCompressor : public Compressor, NonCopyable
+{
+public:
+  // Parameters:
+  // - stream: The file to write data to.
+  explicit NullCompressor(FILE* stream);
+
+  int8_t actual_compression_level() const override;
+  void write(const void* data, size_t count) override;
+  void finalize() override;
+
+private:
+  FILE* m_stream;
+};
+
+} // namespace compression
diff --git a/src/compression/NullDecompressor.cpp b/src/compression/NullDecompressor.cpp
new file mode 100644 (file)
index 0000000..f25da63
--- /dev/null
@@ -0,0 +1,45 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "NullDecompressor.hpp"
+
+#include <core/exceptions.hpp>
+
+namespace compression {
+
+NullDecompressor::NullDecompressor(FILE* const stream) : m_stream(stream)
+{
+}
+
+void
+NullDecompressor::read(void* const data, const size_t count)
+{
+  if (fread(data, count, 1, m_stream) != 1) {
+    throw core::Error("failed to read from uncompressed stream");
+  }
+}
+
+void
+NullDecompressor::finalize()
+{
+  if (fgetc(m_stream) != EOF) {
+    throw core::Error("garbage data at end of uncompressed stream");
+  }
+}
+
+} // namespace compression
diff --git a/src/compression/NullDecompressor.hpp b/src/compression/NullDecompressor.hpp
new file mode 100644 (file)
index 0000000..722319d
--- /dev/null
@@ -0,0 +1,44 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include "Decompressor.hpp"
+
+#include <NonCopyable.hpp>
+
+#include <cstdio>
+
+namespace compression {
+
+// A decompressor of an uncompressed stream.
+class NullDecompressor : public Decompressor, NonCopyable
+{
+public:
+  // Parameters:
+  // - stream: The file to read data from.
+  explicit NullDecompressor(FILE* stream);
+
+  void read(void* data, size_t count) override;
+  void finalize() override;
+
+private:
+  FILE* m_stream;
+};
+
+} // namespace compression
diff --git a/src/compression/ZstdCompressor.cpp b/src/compression/ZstdCompressor.cpp
new file mode 100644 (file)
index 0000000..fffefd1
--- /dev/null
@@ -0,0 +1,123 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "ZstdCompressor.hpp"
+
+#include "Logging.hpp"
+#include "assertions.hpp"
+
+#include <core/exceptions.hpp>
+
+#include <zstd.h>
+
+#include <algorithm>
+
+namespace compression {
+
+ZstdCompressor::ZstdCompressor(FILE* const stream, int8_t compression_level)
+  : m_stream(stream),
+    m_zstd_stream(ZSTD_createCStream()),
+    m_zstd_in(std::make_unique<ZSTD_inBuffer_s>()),
+    m_zstd_out(std::make_unique<ZSTD_outBuffer_s>())
+{
+  if (compression_level == 0) {
+    compression_level = default_compression_level;
+    LOG("Using default compression level {}", compression_level);
+  }
+
+  // libzstd 1.3.4 and newer support negative levels. However, the query
+  // function ZSTD_minCLevel did not appear until 1.3.6, so perform detection
+  // based on version instead.
+  if (ZSTD_versionNumber() < 10304 && compression_level < 1) {
+    LOG(
+      "Using compression level 1 (minimum level supported by libzstd) instead"
+      " of {}",
+      compression_level);
+    compression_level = 1;
+  }
+
+  m_compression_level = std::min<int>(compression_level, ZSTD_maxCLevel());
+  if (m_compression_level != compression_level) {
+    LOG("Using compression level {} (max libzstd level) instead of {}",
+        m_compression_level,
+        compression_level);
+  }
+
+  size_t ret = ZSTD_initCStream(m_zstd_stream, m_compression_level);
+  if (ZSTD_isError(ret)) {
+    ZSTD_freeCStream(m_zstd_stream);
+    throw core::Error("error initializing zstd compression stream");
+  }
+}
+
+ZstdCompressor::~ZstdCompressor()
+{
+  ZSTD_freeCStream(m_zstd_stream);
+}
+
+int8_t
+ZstdCompressor::actual_compression_level() const
+{
+  return m_compression_level;
+}
+
+void
+ZstdCompressor::write(const void* const data, const size_t count)
+{
+  m_zstd_in->src = data;
+  m_zstd_in->size = count;
+  m_zstd_in->pos = 0;
+
+  int flush = data ? 0 : 1;
+
+  size_t ret;
+  while (m_zstd_in->pos < m_zstd_in->size) {
+    uint8_t buffer[CCACHE_READ_BUFFER_SIZE];
+    m_zstd_out->dst = buffer;
+    m_zstd_out->size = sizeof(buffer);
+    m_zstd_out->pos = 0;
+    ret = ZSTD_compressStream(m_zstd_stream, m_zstd_out.get(), m_zstd_in.get());
+    ASSERT(!(ZSTD_isError(ret)));
+    const size_t compressed_bytes = m_zstd_out->pos;
+    if (fwrite(buffer, 1, compressed_bytes, m_stream) != compressed_bytes
+        || ferror(m_stream)) {
+      throw core::Error("failed to write to zstd output stream ");
+    }
+  }
+  ret = flush;
+  while (ret > 0) {
+    uint8_t buffer[CCACHE_READ_BUFFER_SIZE];
+    m_zstd_out->dst = buffer;
+    m_zstd_out->size = sizeof(buffer);
+    m_zstd_out->pos = 0;
+    ret = ZSTD_endStream(m_zstd_stream, m_zstd_out.get());
+    const size_t compressed_bytes = m_zstd_out->pos;
+    if (fwrite(buffer, 1, compressed_bytes, m_stream) != compressed_bytes
+        || ferror(m_stream)) {
+      throw core::Error("failed to write to zstd output stream");
+    }
+  }
+}
+
+void
+ZstdCompressor::finalize()
+{
+  write(nullptr, 0);
+}
+
+} // namespace compression
diff --git a/src/compression/ZstdCompressor.hpp b/src/compression/ZstdCompressor.hpp
new file mode 100644 (file)
index 0000000..3a1c18e
--- /dev/null
@@ -0,0 +1,59 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include "Compressor.hpp"
+
+#include <NonCopyable.hpp>
+
+#include <cstdint>
+#include <memory>
+
+struct ZSTD_CCtx_s;
+struct ZSTD_inBuffer_s;
+struct ZSTD_outBuffer_s;
+
+namespace compression {
+
+// A compressor of a Zstandard stream.
+class ZstdCompressor : public Compressor, NonCopyable
+{
+public:
+  // Parameters:
+  // - stream: The file to write data to.
+  // - compression_level: Desired compression level.
+  ZstdCompressor(FILE* stream, int8_t compression_level);
+
+  ~ZstdCompressor() override;
+
+  int8_t actual_compression_level() const override;
+  void write(const void* data, size_t count) override;
+  void finalize() override;
+
+  constexpr static uint8_t default_compression_level = 1;
+
+private:
+  FILE* m_stream;
+  ZSTD_CCtx_s* m_zstd_stream;
+  std::unique_ptr<ZSTD_inBuffer_s> m_zstd_in;
+  std::unique_ptr<ZSTD_outBuffer_s> m_zstd_out;
+  int8_t m_compression_level;
+};
+
+} // namespace compression
diff --git a/src/compression/ZstdDecompressor.cpp b/src/compression/ZstdDecompressor.cpp
new file mode 100644 (file)
index 0000000..916a6e8
--- /dev/null
@@ -0,0 +1,89 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "ZstdDecompressor.hpp"
+
+#include "assertions.hpp"
+
+#include <core/exceptions.hpp>
+
+namespace compression {
+
+ZstdDecompressor::ZstdDecompressor(FILE* const stream)
+  : m_stream(stream),
+    m_input_size(0),
+    m_input_consumed(0),
+    m_zstd_stream(ZSTD_createDStream()),
+    m_reached_stream_end(false)
+{
+  const size_t ret = ZSTD_initDStream(m_zstd_stream);
+  if (ZSTD_isError(ret)) {
+    ZSTD_freeDStream(m_zstd_stream);
+    throw core::Error("failed to initialize zstd decompression stream");
+  }
+}
+
+ZstdDecompressor::~ZstdDecompressor()
+{
+  ZSTD_freeDStream(m_zstd_stream);
+}
+
+void
+ZstdDecompressor::read(void* const data, const size_t count)
+{
+  size_t bytes_read = 0;
+  while (bytes_read < count) {
+    ASSERT(m_input_size >= m_input_consumed);
+    if (m_input_size == m_input_consumed) {
+      m_input_size = fread(m_input_buffer, 1, sizeof(m_input_buffer), m_stream);
+      if (m_input_size == 0) {
+        throw core::Error("failed to read from zstd input stream");
+      }
+      m_input_consumed = 0;
+    }
+
+    m_zstd_in.src = (m_input_buffer + m_input_consumed);
+    m_zstd_in.size = m_input_size - m_input_consumed;
+    m_zstd_in.pos = 0;
+
+    m_zstd_out.dst = static_cast<uint8_t*>(data) + bytes_read;
+    m_zstd_out.size = count - bytes_read;
+    m_zstd_out.pos = 0;
+    const size_t ret =
+      ZSTD_decompressStream(m_zstd_stream, &m_zstd_out, &m_zstd_in);
+    if (ZSTD_isError(ret)) {
+      throw core::Error("failed to read from zstd input stream");
+    }
+    if (ret == 0) {
+      m_reached_stream_end = true;
+      break;
+    }
+    bytes_read += m_zstd_out.pos;
+    m_input_consumed += m_zstd_in.pos;
+  }
+}
+
+void
+ZstdDecompressor::finalize()
+{
+  if (!m_reached_stream_end) {
+    throw core::Error("garbage data at end of zstd input stream");
+  }
+}
+
+} // namespace compression
diff --git a/src/compression/ZstdDecompressor.hpp b/src/compression/ZstdDecompressor.hpp
new file mode 100644 (file)
index 0000000..aee51b6
--- /dev/null
@@ -0,0 +1,53 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include "Decompressor.hpp"
+
+#include <zstd.h>
+
+#include <cstdint>
+
+namespace compression {
+
+// A decompressor of a Zstandard stream.
+class ZstdDecompressor : public Decompressor
+{
+public:
+  // Parameters:
+  // - stream: The file to read data from.
+  explicit ZstdDecompressor(FILE* stream);
+
+  ~ZstdDecompressor() override;
+
+  void read(void* data, size_t count) override;
+  void finalize() override;
+
+private:
+  FILE* m_stream;
+  char m_input_buffer[CCACHE_READ_BUFFER_SIZE];
+  size_t m_input_size;
+  size_t m_input_consumed;
+  ZSTD_DStream* m_zstd_stream;
+  ZSTD_inBuffer m_zstd_in;
+  ZSTD_outBuffer m_zstd_out;
+  bool m_reached_stream_end;
+};
+
+} // namespace compression
diff --git a/src/compression/types.cpp b/src/compression/types.cpp
new file mode 100644 (file)
index 0000000..d0e91f6
--- /dev/null
@@ -0,0 +1,68 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "types.hpp"
+
+#include <Config.hpp>
+#include <Context.hpp>
+#include <assertions.hpp>
+#include <core/exceptions.hpp>
+
+namespace compression {
+
+int8_t
+level_from_config(const Config& config)
+{
+  return config.compression() ? config.compression_level() : 0;
+}
+
+Type
+type_from_config(const Config& config)
+{
+  return config.compression() ? Type::zstd : Type::none;
+}
+
+Type
+type_from_int(const uint8_t type)
+{
+  switch (type) {
+  case static_cast<uint8_t>(Type::none):
+    return Type::none;
+
+  case static_cast<uint8_t>(Type::zstd):
+    return Type::zstd;
+  }
+
+  throw core::Error("Unknown type: {}", type);
+}
+
+std::string
+type_to_string(const Type type)
+{
+  switch (type) {
+  case Type::none:
+    return "none";
+
+  case Type::zstd:
+    return "zstd";
+  }
+
+  ASSERT(false);
+}
+
+} // namespace compression
diff --git a/src/compression/types.hpp b/src/compression/types.hpp
new file mode 100644 (file)
index 0000000..4b0e35e
--- /dev/null
@@ -0,0 +1,41 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <cstdint>
+#include <string>
+
+class Config;
+
+namespace compression {
+
+enum class Type : uint8_t {
+  none = 0,
+  zstd = 1,
+};
+
+int8_t level_from_config(const Config& config);
+
+Type type_from_config(const Config& config);
+
+Type type_from_int(uint8_t type);
+
+std::string type_to_string(Type type);
+
+} // namespace compression
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
new file mode 100644 (file)
index 0000000..60d4297
--- /dev/null
@@ -0,0 +1,9 @@
+set(
+  sources
+  ${CMAKE_CURRENT_SOURCE_DIR}/Statistics.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/StatisticsCounters.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/StatsLog.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/mainoptions.cpp
+)
+
+target_sources(ccache_framework PRIVATE ${sources})
diff --git a/src/core/Sloppiness.hpp b/src/core/Sloppiness.hpp
new file mode 100644 (file)
index 0000000..917526b
--- /dev/null
@@ -0,0 +1,95 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <string>
+
+namespace core {
+
+enum class Sloppy : uint32_t {
+  none = 0U,
+
+  include_file_mtime = 1U << 0,
+  include_file_ctime = 1U << 1,
+  time_macros = 1U << 2,
+  pch_defines = 1U << 3,
+  // Allow us to match files based on their stats (size, mtime, ctime), without
+  // looking at their contents.
+  file_stat_matches = 1U << 4,
+  // Allow us to not include any system headers in the manifest include files,
+  // similar to -MM versus -M for dependencies.
+  system_headers = 1U << 5,
+  // Allow us to ignore ctimes when comparing file stats, so we can fake mtimes
+  // if we want to (it is much harder to fake ctimes, requires changing clock)
+  file_stat_matches_ctime = 1U << 6,
+  // Allow us to not include the -index-store-path option in the manifest hash.
+  clang_index_store = 1U << 7,
+  // Ignore locale settings.
+  locale = 1U << 8,
+  // Allow caching even if -fmodules is used.
+  modules = 1U << 9,
+  // Ignore virtual file system (VFS) overlay file.
+  ivfsoverlay = 1U << 10,
+};
+
+class Sloppiness
+{
+public:
+  Sloppiness(Sloppy value = Sloppy::none);
+  explicit Sloppiness(uint32_t value);
+
+  void enable(Sloppy value);
+  bool is_enabled(Sloppy value) const;
+  uint32_t to_bitmask() const;
+
+private:
+  Sloppy m_sloppiness = Sloppy::none;
+};
+
+// --- Inline implementations ---
+
+inline Sloppiness::Sloppiness(Sloppy value) : m_sloppiness(value)
+{
+}
+
+inline Sloppiness::Sloppiness(uint32_t value)
+  : m_sloppiness(static_cast<Sloppy>(value))
+{
+}
+
+inline void
+Sloppiness::enable(Sloppy value)
+{
+  m_sloppiness = static_cast<Sloppy>(static_cast<uint32_t>(m_sloppiness)
+                                     | static_cast<uint32_t>(value));
+}
+
+inline bool
+Sloppiness::is_enabled(Sloppy value) const
+{
+  return static_cast<uint32_t>(m_sloppiness) & static_cast<uint32_t>(value);
+}
+
+inline uint32_t
+Sloppiness::to_bitmask() const
+{
+  return static_cast<uint32_t>(m_sloppiness);
+}
+
+} // namespace core
diff --git a/src/core/Statistic.hpp b/src/core/Statistic.hpp
new file mode 100644 (file)
index 0000000..dddfca9
--- /dev/null
@@ -0,0 +1,71 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+namespace core {
+
+// Statistics fields in storage order.
+enum class Statistic {
+  none = 0,
+  compiler_produced_stdout = 1,
+  compile_failed = 2,
+  internal_error = 3,
+  cache_miss = 4,
+  preprocessor_error = 5,
+  could_not_find_compiler = 6,
+  missing_cache_file = 7,
+  preprocessed_cache_hit = 8,
+  bad_compiler_arguments = 9,
+  called_for_link = 10,
+  files_in_cache = 11,
+  cache_size_kibibyte = 12,
+  obsolete_max_files = 13,
+  obsolete_max_size = 14,
+  unsupported_source_language = 15,
+  bad_output_file = 16,
+  no_input_file = 17,
+  multiple_source_files = 18,
+  autoconf_test = 19,
+  unsupported_compiler_option = 20,
+  output_to_stdout = 21,
+  direct_cache_hit = 22,
+  compiler_produced_no_output = 23,
+  compiler_produced_empty_output = 24,
+  error_hashing_extra_file = 25,
+  compiler_check_failed = 26,
+  could_not_use_precompiled_header = 27,
+  called_for_preprocessing = 28,
+  cleanups_performed = 29,
+  unsupported_code_directive = 30,
+  stats_zeroed_timestamp = 31,
+  could_not_use_modules = 32,
+  direct_cache_miss = 33,
+  preprocessed_cache_miss = 34,
+  primary_storage_hit = 35,
+  primary_storage_miss = 36,
+  secondary_storage_hit = 37,
+  secondary_storage_miss = 38,
+  secondary_storage_error = 39,
+  secondary_storage_timeout = 40,
+  recache = 41,
+
+  END
+};
+
+} // namespace core
diff --git a/src/core/Statistics.cpp b/src/core/Statistics.cpp
new file mode 100644 (file)
index 0000000..3417424
--- /dev/null
@@ -0,0 +1,382 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "Statistics.hpp"
+
+#include <Config.hpp>
+#include <Logging.hpp>
+#include <Util.hpp>
+#include <fmtmacros.hpp>
+#include <util/TextTable.hpp>
+#include <util/string.hpp>
+
+namespace core {
+
+using core::Statistic;
+
+const unsigned FLAG_NOZERO = 1U << 0;      // don't zero with --zero-stats
+const unsigned FLAG_NEVER = 1U << 1;       // don't include in --print-stats
+const unsigned FLAG_ERROR = 1U << 2;       // include in error count
+const unsigned FLAG_UNCACHEABLE = 1U << 3; // include in uncacheable count
+
+namespace {
+
+struct StatisticsField
+{
+  StatisticsField(const Statistic statistic_,
+                  const char* const id_,
+                  const char* const description_,
+                  const unsigned flags_ = 0)
+    : statistic(statistic_),
+      id(id_),
+      description(description_),
+      flags(flags_)
+  {
+  }
+
+  const Statistic statistic;
+  const char* const id;          // for --print-stats
+  const char* const description; // for --show-stats --verbose
+  const unsigned flags;          // bitmask of FLAG_* values
+};
+
+} // namespace
+
+#define FIELD(id, ...)                                                         \
+  {                                                                            \
+    Statistic::id, #id, __VA_ARGS__                                            \
+  }
+
+const StatisticsField k_statistics_fields[] = {
+  // Field "none" intentionally omitted.
+  FIELD(autoconf_test, "Autoconf compile/link", FLAG_UNCACHEABLE),
+  FIELD(bad_compiler_arguments, "Bad compiler arguments", FLAG_UNCACHEABLE),
+  FIELD(bad_output_file, "Could not write to output file", FLAG_ERROR),
+  FIELD(cache_miss, nullptr),
+  FIELD(cache_size_kibibyte, nullptr, FLAG_NOZERO),
+  FIELD(called_for_link, "Called for linking", FLAG_UNCACHEABLE),
+  FIELD(called_for_preprocessing, "Called for preprocessing", FLAG_UNCACHEABLE),
+  FIELD(cleanups_performed, nullptr),
+  FIELD(compile_failed, "Compilation failed", FLAG_UNCACHEABLE),
+  FIELD(compiler_check_failed, "Compiler check failed", FLAG_ERROR),
+  FIELD(compiler_produced_empty_output,
+        "Compiler produced empty output",
+        FLAG_UNCACHEABLE),
+  FIELD(compiler_produced_no_output,
+        "Compiler produced no output",
+        FLAG_UNCACHEABLE),
+  FIELD(compiler_produced_stdout, "Compiler produced stdout", FLAG_UNCACHEABLE),
+  FIELD(could_not_find_compiler, "Could not find compiler", FLAG_ERROR),
+  FIELD(could_not_use_modules, "Could not use modules", FLAG_UNCACHEABLE),
+  FIELD(could_not_use_precompiled_header,
+        "Could not use precompiled header",
+        FLAG_UNCACHEABLE),
+  FIELD(direct_cache_hit, nullptr),
+  FIELD(direct_cache_miss, nullptr),
+  FIELD(error_hashing_extra_file, "Error hashing extra file", FLAG_ERROR),
+  FIELD(files_in_cache, nullptr, FLAG_NOZERO),
+  FIELD(internal_error, "Internal error", FLAG_ERROR),
+  FIELD(missing_cache_file, "Missing cache file", FLAG_ERROR),
+  FIELD(multiple_source_files, "Multiple source files", FLAG_UNCACHEABLE),
+  FIELD(no_input_file, "No input file", FLAG_UNCACHEABLE),
+  FIELD(obsolete_max_files, nullptr, FLAG_NOZERO | FLAG_NEVER),
+  FIELD(obsolete_max_size, nullptr, FLAG_NOZERO | FLAG_NEVER),
+  FIELD(output_to_stdout, "Output to stdout", FLAG_UNCACHEABLE),
+  FIELD(preprocessed_cache_hit, nullptr),
+  FIELD(preprocessed_cache_miss, nullptr),
+  FIELD(preprocessor_error, "Preprocessing failed", FLAG_UNCACHEABLE),
+  FIELD(primary_storage_hit, nullptr),
+  FIELD(primary_storage_miss, nullptr),
+  FIELD(recache, "Forced recache", FLAG_UNCACHEABLE),
+  FIELD(secondary_storage_error, nullptr),
+  FIELD(secondary_storage_hit, nullptr),
+  FIELD(secondary_storage_miss, nullptr),
+  FIELD(secondary_storage_timeout, nullptr),
+  FIELD(stats_zeroed_timestamp, nullptr),
+  FIELD(
+    unsupported_code_directive, "Unsupported code directive", FLAG_UNCACHEABLE),
+  FIELD(unsupported_compiler_option,
+        "Unsupported compiler option",
+        FLAG_UNCACHEABLE),
+  FIELD(unsupported_source_language,
+        "Unsupported source language",
+        FLAG_UNCACHEABLE),
+};
+
+static_assert(sizeof(k_statistics_fields) / sizeof(k_statistics_fields[0])
+                == static_cast<size_t>(Statistic::END) - 1,
+              "incorrect number of fields");
+
+static std::string
+format_timestamp(const uint64_t value)
+{
+  if (value == 0) {
+    return "never";
+  } else {
+    const auto tm = Util::localtime(value);
+    char buffer[100] = "?";
+    if (tm) {
+      strftime(buffer, sizeof(buffer), "%c", &*tm);
+    }
+    return buffer;
+  }
+}
+
+static std::string
+percent(const uint64_t nominator, const uint64_t denominator)
+{
+  if (denominator == 0) {
+    return "";
+  } else if (nominator >= denominator) {
+    return FMT("({:.1f} %)", (100.0 * nominator) / denominator);
+  } else {
+    return FMT("({:.2f} %)", (100.0 * nominator) / denominator);
+  }
+}
+
+Statistics::Statistics(const StatisticsCounters& counters)
+  : m_counters(counters)
+{
+}
+
+std::vector<std::string>
+Statistics::get_statistics_ids() const
+{
+  std::vector<std::string> result;
+  for (const auto& field : k_statistics_fields) {
+    if (m_counters.get(field.statistic) != 0 && !(field.flags & FLAG_NOZERO)) {
+      result.emplace_back(field.id);
+    }
+  }
+  std::sort(result.begin(), result.end());
+  return result;
+}
+
+uint64_t
+Statistics::count_stats(const unsigned flags) const
+{
+  uint64_t sum = 0;
+  for (const auto& field : k_statistics_fields) {
+    if (field.flags & flags) {
+      sum += m_counters.get(field.statistic);
+    }
+  }
+  return sum;
+}
+
+std::vector<std::pair<std::string, uint64_t>>
+Statistics::get_stats(unsigned flags, const bool all) const
+{
+  std::vector<std::pair<std::string, uint64_t>> result;
+  for (const auto& field : k_statistics_fields) {
+    const auto count = m_counters.get(field.statistic);
+    if ((field.flags & flags) && (all || count > 0)) {
+      result.emplace_back(field.description, count);
+    }
+  }
+  return result;
+}
+
+std::string
+Statistics::format_human_readable(const Config& config,
+                                  const time_t last_updated,
+                                  const uint8_t verbosity,
+                                  const bool from_log) const
+{
+  util::TextTable table;
+  using C = util::TextTable::Cell;
+
+#define S(x_) m_counters.get(Statistic::x_)
+
+  const uint64_t d_hits = S(direct_cache_hit);
+  const uint64_t d_misses = S(direct_cache_miss);
+  const uint64_t p_hits = S(preprocessed_cache_hit);
+  const uint64_t p_misses = S(preprocessed_cache_miss);
+  const uint64_t hits = d_hits + p_hits;
+  const uint64_t misses = S(cache_miss);
+
+  table.add_heading("Summary:");
+  if (verbosity > 0 && !from_log) {
+    table.add_row({"  Cache directory:", C(config.cache_dir()).colspan(4)});
+    table.add_row(
+      {"  Primary config:", C(config.primary_config_path()).colspan(4)});
+    table.add_row(
+      {"  Secondary config:", C(config.secondary_config_path()).colspan(4)});
+    table.add_row(
+      {"  Stats updated:", C(format_timestamp(last_updated)).colspan(4)});
+    if (verbosity > 1) {
+      const uint64_t last_zeroed = S(stats_zeroed_timestamp);
+      table.add_row(
+        {"  Stats zeroed:", C(format_timestamp(last_zeroed)).colspan(4)});
+    }
+  }
+  table.add_row({
+    "  Hits:",
+    hits,
+    "/",
+    hits + misses,
+    percent(hits, hits + misses),
+  });
+  table.add_row({
+    "    Direct:",
+    d_hits,
+    "/",
+    d_hits + d_misses,
+    percent(d_hits, d_hits + d_misses),
+  });
+  table.add_row({
+    "    Preprocessed:",
+    p_hits,
+    "/",
+    p_hits + p_misses,
+    percent(p_hits, p_hits + p_misses),
+  });
+
+  const auto errors = count_stats(FLAG_ERROR);
+  const auto uncacheable = count_stats(FLAG_UNCACHEABLE);
+  if (verbosity > 1 || errors > 0) {
+    table.add_row({"  Errors:", errors});
+  }
+  if (verbosity > 1 || uncacheable > 0) {
+    table.add_row({"  Uncacheable:", uncacheable});
+  }
+
+  const uint64_t g = 1'000'000'000;
+  const uint64_t pri_hits = S(primary_storage_hit);
+  const uint64_t pri_misses = S(primary_storage_miss);
+  const uint64_t pri_size = S(cache_size_kibibyte) * 1024;
+  const uint64_t cleanups = S(cleanups_performed);
+  table.add_heading("Primary storage:");
+  table.add_row({
+    "  Hits:",
+    pri_hits,
+    "/",
+    pri_hits + pri_misses,
+    percent(pri_hits, pri_hits + pri_misses),
+  });
+  if (!from_log) {
+    table.add_row({
+      "  Cache size (GB):",
+      C(FMT("{:.2f}", static_cast<double>(pri_size) / g)).right_align(),
+      "/",
+      C(FMT("{:.2f}", static_cast<double>(config.max_size()) / g))
+        .right_align(),
+      percent(pri_size, config.max_size()),
+    });
+    if (verbosity > 0) {
+      std::vector<C> cells{"  Files:", S(files_in_cache)};
+      if (config.max_files() > 0) {
+        cells.emplace_back("/");
+        cells.emplace_back(config.max_files());
+        cells.emplace_back(percent(S(files_in_cache), config.max_files()));
+      }
+      table.add_row(cells);
+    }
+    if (cleanups > 0) {
+      table.add_row({"  Cleanups:", cleanups});
+    }
+  }
+
+  const uint64_t sec_hits = S(secondary_storage_hit);
+  const uint64_t sec_misses = S(secondary_storage_miss);
+  const uint64_t sec_errors = S(secondary_storage_error);
+  const uint64_t sec_timeouts = S(secondary_storage_timeout);
+
+  if (verbosity > 0 || sec_hits + sec_misses + sec_errors + sec_timeouts > 0) {
+    table.add_heading("Secondary storage:");
+    table.add_row({
+      "  Hits:",
+      sec_hits,
+      "/",
+      sec_hits + sec_misses,
+      percent(sec_hits, sec_hits + pri_misses),
+    });
+    if (verbosity > 1 || sec_errors > 0) {
+      table.add_row({"  Errors:", sec_errors});
+    }
+    if (verbosity > 1 || sec_timeouts > 0) {
+      table.add_row({"  Timeouts:", sec_timeouts});
+    }
+  }
+
+  auto cmp_fn = [](const auto& e1, const auto& e2) {
+    return e1.first.compare(e2.first) < 0;
+  };
+
+  if (verbosity > 1 || (verbosity == 1 && errors > 0)) {
+    auto error_stats = get_stats(FLAG_ERROR, verbosity > 1);
+    std::sort(error_stats.begin(), error_stats.end(), cmp_fn);
+    table.add_heading("Errors:");
+    for (const auto& descr_count : error_stats) {
+      table.add_row({FMT("  {}:", descr_count.first), descr_count.second});
+    }
+  }
+
+  if (verbosity > 1 || (verbosity == 1 && uncacheable > 0)) {
+    auto uncacheable_stats = get_stats(FLAG_UNCACHEABLE, verbosity > 1);
+    std::sort(uncacheable_stats.begin(), uncacheable_stats.end(), cmp_fn);
+    table.add_heading("Uncacheable:");
+    for (const auto& descr_count : uncacheable_stats) {
+      table.add_row({FMT("  {}:", descr_count.first), descr_count.second});
+    }
+  }
+
+  return table.render();
+}
+
+std::string
+Statistics::format_machine_readable(const time_t last_updated) const
+{
+  std::vector<std::string> lines;
+
+  lines.push_back(FMT("stats_updated_timestamp\t{}\n", last_updated));
+
+  for (const auto& field : k_statistics_fields) {
+    if (!(field.flags & FLAG_NEVER)) {
+      lines.push_back(
+        FMT("{}\t{}\n", field.id, m_counters.get(field.statistic)));
+    }
+  }
+
+  std::sort(lines.begin(), lines.end());
+  return util::join(lines, "");
+}
+
+std::unordered_map<std::string, Statistic>
+Statistics::get_id_map()
+{
+  std::unordered_map<std::string, Statistic> result;
+  for (const auto& field : k_statistics_fields) {
+    result[field.id] = field.statistic;
+  }
+  return result;
+}
+
+std::vector<Statistic>
+Statistics::get_zeroable_fields()
+{
+  std::vector<Statistic> result;
+  for (const auto& field : k_statistics_fields) {
+    if (!(field.flags & FLAG_NOZERO)) {
+      result.push_back(field.statistic);
+    }
+  }
+  return result;
+}
+
+} // namespace core
diff --git a/src/core/Statistics.hpp b/src/core/Statistics.hpp
new file mode 100644 (file)
index 0000000..3e9ed81
--- /dev/null
@@ -0,0 +1,70 @@
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <core/StatisticsCounters.hpp>
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+class Config;
+
+namespace core {
+
+class Statistics
+{
+public:
+  Statistics(const StatisticsCounters& counters);
+
+  // Return machine-readable strings representing the statistics counters.
+  std::vector<std::string> get_statistics_ids() const;
+
+  // Format cache statistics in human-readable format.
+  std::string format_human_readable(const Config& config,
+                                    time_t last_updated,
+                                    uint8_t verbosity,
+                                    bool from_log) const;
+
+  // Format cache statistics in machine-readable format.
+  std::string format_machine_readable(time_t last_updated) const;
+
+  const StatisticsCounters& counters() const;
+
+  static std::unordered_map<std::string, Statistic> get_id_map();
+
+  static std::vector<Statistic> get_zeroable_fields();
+
+private:
+  const StatisticsCounters m_counters;
+
+  uint64_t count_stats(unsigned flags) const;
+  std::vector<std::pair<std::string, uint64_t>> get_stats(unsigned flags,
+                                                          bool all) const;
+};
+
+// --- Inline implementations ---
+
+inline const StatisticsCounters&
+Statistics::counters() const
+{
+  return m_counters;
+}
+
+} // namespace core
diff --git a/src/core/StatisticsCounters.cpp b/src/core/StatisticsCounters.cpp
new file mode 100644 (file)
index 0000000..5bef843
--- /dev/null
@@ -0,0 +1,114 @@
+// Copyright (C) 2010-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "StatisticsCounters.hpp"
+
+#include <assertions.hpp>
+
+#include <algorithm>
+
+namespace core {
+
+StatisticsCounters::StatisticsCounters()
+  : m_counters(static_cast<size_t>(Statistic::END))
+{
+}
+
+StatisticsCounters::StatisticsCounters(const Statistic statistic)
+  : StatisticsCounters({statistic})
+{
+}
+
+StatisticsCounters::StatisticsCounters(
+  const std::initializer_list<Statistic> statistics)
+  : StatisticsCounters()
+{
+  for (auto st : statistics) {
+    increment(st);
+  }
+}
+
+uint64_t
+StatisticsCounters::get(Statistic statistic) const
+{
+  const auto index = static_cast<size_t>(statistic);
+  ASSERT(index < static_cast<size_t>(Statistic::END));
+  return index < m_counters.size() ? m_counters[index] : 0;
+}
+
+void
+StatisticsCounters::set(Statistic statistic, uint64_t value)
+{
+  const auto index = static_cast<size_t>(statistic);
+  ASSERT(index < static_cast<size_t>(Statistic::END));
+  m_counters[index] = value;
+}
+
+uint64_t
+StatisticsCounters::get_raw(size_t index) const
+{
+  ASSERT(index < size());
+  return m_counters[index];
+}
+
+void
+StatisticsCounters::set_raw(size_t index, uint64_t value)
+{
+  if (index >= m_counters.size()) {
+    m_counters.resize(index + 1);
+  }
+  m_counters[index] = value;
+}
+
+void
+StatisticsCounters::increment(Statistic statistic, int64_t value)
+{
+  const auto i = static_cast<size_t>(statistic);
+  if (i >= m_counters.size()) {
+    m_counters.resize(i + 1);
+  }
+  auto& counter = m_counters[i];
+  counter =
+    std::max(static_cast<int64_t>(0), static_cast<int64_t>(counter + value));
+}
+
+void
+StatisticsCounters::increment(const StatisticsCounters& other)
+{
+  m_counters.resize(std::max(size(), other.size()));
+  for (size_t i = 0; i < other.size(); ++i) {
+    auto& counter = m_counters[i];
+    counter = std::max(static_cast<int64_t>(0),
+                       static_cast<int64_t>(counter + other.m_counters[i]));
+  }
+}
+
+size_t
+StatisticsCounters::size() const
+{
+  return m_counters.size();
+}
+
+bool
+StatisticsCounters::all_zero() const
+{
+  return !std::any_of(
+    m_counters.begin(), m_counters.end(), [](unsigned v) { return v != 0; });
+}
+
+} // namespace core
diff --git a/src/core/StatisticsCounters.hpp b/src/core/StatisticsCounters.hpp
new file mode 100644 (file)
index 0000000..21bb1b3
--- /dev/null
@@ -0,0 +1,57 @@
+// Copyright (C) 2010-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include "Statistic.hpp"
+
+#include <cstddef>
+#include <cstdint>
+#include <initializer_list>
+#include <vector>
+
+namespace core {
+
+// A simple wrapper around a vector of integers used for the statistics
+// counters.
+class StatisticsCounters
+{
+public:
+  StatisticsCounters();
+  StatisticsCounters(Statistic statistic);
+  StatisticsCounters(std::initializer_list<Statistic> statistics);
+
+  uint64_t get(Statistic statistic) const;
+  void set(Statistic statistic, uint64_t value);
+
+  uint64_t get_raw(size_t index) const;
+  void set_raw(size_t index, uint64_t value);
+
+  void increment(Statistic statistic, int64_t value = 1);
+  void increment(const StatisticsCounters& other);
+
+  size_t size() const;
+
+  // Return true if all counters are zero, false otherwise.
+  bool all_zero() const;
+
+private:
+  std::vector<uint64_t> m_counters;
+};
+
+} // namespace core
diff --git a/src/core/StatsLog.cpp b/src/core/StatsLog.cpp
new file mode 100644 (file)
index 0000000..3202d2c
--- /dev/null
@@ -0,0 +1,71 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "StatsLog.hpp"
+
+#include <File.hpp>
+#include <Logging.hpp>
+#include <core/Statistics.hpp>
+#include <fmtmacros.hpp>
+
+#include <fstream>
+
+namespace core {
+
+StatisticsCounters
+StatsLog::read() const
+{
+  core::StatisticsCounters counters;
+
+  const auto id_map = Statistics::get_id_map();
+
+  std::ifstream in(m_path);
+  std::string line;
+  while (std::getline(in, line, '\n')) {
+    if (line[0] == '#') {
+      continue;
+    }
+    const auto entry = id_map.find(line);
+    if (entry != id_map.end()) {
+      Statistic statistic = entry->second;
+      counters.increment(statistic, 1);
+    } else {
+      LOG("Unknown statistic: {}", line);
+    }
+  }
+
+  return counters;
+}
+
+void
+StatsLog::log_result(const std::string& input_file,
+                     const std::vector<std::string>& result_ids)
+{
+  File file(m_path, "ab");
+  if (!file) {
+    LOG("Failed to open {}: {}", m_path, strerror(errno));
+    return;
+  }
+
+  PRINT(*file, "# {}\n", input_file);
+  for (const auto& id : result_ids) {
+    PRINT(*file, "{}\n", id);
+  }
+}
+
+} // namespace core
diff --git a/src/core/StatsLog.hpp b/src/core/StatsLog.hpp
new file mode 100644 (file)
index 0000000..19284de
--- /dev/null
@@ -0,0 +1,47 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include "StatisticsCounters.hpp"
+
+#include <string>
+#include <vector>
+
+namespace core {
+
+class StatsLog
+{
+public:
+  StatsLog(const std::string& path);
+
+  StatisticsCounters read() const;
+  void log_result(const std::string& input_file,
+                  const std::vector<std::string>& result_ids);
+
+private:
+  const std::string m_path;
+};
+
+// --- Inline implementations ---
+
+inline StatsLog::StatsLog(const std::string& path) : m_path(path)
+{
+}
+
+} // namespace core
diff --git a/src/core/exceptions.hpp b/src/core/exceptions.hpp
new file mode 100644 (file)
index 0000000..aae4b26
--- /dev/null
@@ -0,0 +1,83 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <FormatNonstdStringView.hpp>
+
+#include <third_party/fmt/core.h>
+#include <third_party/nonstd/optional.hpp>
+
+#include <stdexcept>
+#include <string>
+#include <utility>
+
+namespace core {
+
+// Don't throw or catch ErrorBase directly, use a subclass.
+class ErrorBase : public std::runtime_error
+{
+  using std::runtime_error::runtime_error;
+};
+
+// Throw an Error to indicate a potentially non-fatal error that may be caught
+// and handled by callers. An uncaught Error that reaches the top level will be
+// treated similar to Fatal.
+class Error : public ErrorBase
+{
+public:
+  // Special case: If given only one string, don't parse it as a format string.
+  Error(const std::string& message);
+
+  // `args` are forwarded to `fmt::format`.
+  template<typename... T> inline Error(T&&... args);
+};
+
+inline Error::Error(const std::string& message) : ErrorBase(message)
+{
+}
+
+template<typename... T>
+inline Error::Error(T&&... args)
+  : ErrorBase(fmt::format(std::forward<T>(args)...))
+{
+}
+
+// Throw a Fatal to make ccache print the error message to stderr and exit
+// with a non-zero exit code.
+class Fatal : public ErrorBase
+{
+public:
+  // Special case: If given only one string, don't parse it as a format string.
+  Fatal(const std::string& message);
+
+  // `args` are forwarded to `fmt::format`.
+  template<typename... T> inline Fatal(T&&... args);
+};
+
+inline Fatal::Fatal(const std::string& message) : ErrorBase(message)
+{
+}
+
+template<typename... T>
+inline Fatal::Fatal(T&&... args)
+  : ErrorBase(fmt::format(std::forward<T>(args)...))
+{
+}
+
+} // namespace core
diff --git a/src/core/mainoptions.cpp b/src/core/mainoptions.cpp
new file mode 100644 (file)
index 0000000..6d43c0e
--- /dev/null
@@ -0,0 +1,595 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "mainoptions.hpp"
+
+#include <Checksum.hpp>
+#include <Config.hpp>
+#include <Fd.hpp>
+#include <Hash.hpp>
+#include <InodeCache.hpp>
+#include <Manifest.hpp>
+#include <ProgressBar.hpp>
+#include <ResultDumper.hpp>
+#include <ResultExtractor.hpp>
+#include <ccache.hpp>
+#include <core/Statistics.hpp>
+#include <core/StatsLog.hpp>
+#include <core/exceptions.hpp>
+#include <fmtmacros.hpp>
+#include <storage/Storage.hpp>
+#include <storage/primary/PrimaryStorage.hpp>
+#include <util/TextTable.hpp>
+#include <util/expected.hpp>
+#include <util/string.hpp>
+
+#include <third_party/nonstd/optional.hpp>
+
+#include <fcntl.h>
+
+#include <string>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
+#ifdef HAVE_GETOPT_LONG
+#  include <getopt.h>
+#elif defined(_WIN32)
+#  include <third_party/win32/getopt.h>
+#else
+extern "C" {
+#  include <third_party/getopt_long.h>
+}
+#endif
+
+namespace core {
+
+constexpr const char VERSION_TEXT[] =
+  R"({0} version {1}
+Features: {2}
+
+Copyright (C) 2002-2007 Andrew Tridgell
+Copyright (C) 2009-2021 Joel Rosdahl and other contributors
+
+See <https://ccache.dev/credits.html> for a complete list of contributors.
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free Software
+Foundation; either version 3 of the License, or (at your option) any later
+version.
+)";
+
+constexpr const char USAGE_TEXT[] =
+  R"(Usage:
+    {0} [options]
+    {0} compiler [compiler options]
+    compiler [compiler options]          (via symbolic link)
+
+Common options:
+    -c, --cleanup              delete old files and recalculate size counters
+                               (normally not needed as this is done
+                               automatically)
+    -C, --clear                clear the cache completely (except configuration)
+        --config-path PATH     operate on configuration file PATH instead of the
+                               default
+    -d, --dir PATH             operate on cache directory PATH instead of the
+                               default
+        --evict-older-than AGE remove files older than AGE (unsigned integer
+                               with a d (days) or s (seconds) suffix)
+    -F, --max-files NUM        set maximum number of files in cache to NUM (use
+                               0 for no limit)
+    -M, --max-size SIZE        set maximum size of cache to SIZE (use 0 for no
+                               limit); available suffixes: k, M, G, T (decimal)
+                               and Ki, Mi, Gi, Ti (binary); default suffix: G
+    -X, --recompress LEVEL     recompress the cache to level LEVEL (integer or
+                               "uncompressed") using the Zstandard algorithm;
+                               see "Cache compression" in the manual for details
+    -o, --set-config KEY=VAL   set configuration item KEY to value VAL
+    -x, --show-compression     show compression statistics
+    -p, --show-config          show current configuration options in
+                               human-readable format
+        --show-log-stats       print statistics counters from the stats log
+                               in human-readable format
+    -s, --show-stats           show summary of configuration and statistics
+                               counters in human-readable format
+    -v, --verbose              increase verbosity
+    -z, --zero-stats           zero statistics counters
+
+    -h, --help                 print this help text
+    -V, --version              print version and copyright information
+
+Options for secondary storage:
+        --trim-dir PATH        remove old files from directory _PATH_ until it
+                               is at most the size specified by --trim-max-size
+                               (note: don't use this option to trim the primary
+                               cache)
+        --trim-max-size SIZE   specify the maximum size for --trim-dir;
+                               available suffixes: k, M, G, T (decimal) and Ki,
+                               Mi, Gi, Ti (binary); default suffix: G
+        --trim-method METHOD   specify the method (atime or mtime) for
+                               --trim-dir; default: atime
+
+Options for scripting or debugging:
+        --checksum-file PATH   print the checksum (64 bit XXH3) of the file at
+                               PATH
+        --dump-manifest PATH   dump manifest file at PATH in text format
+        --dump-result PATH     dump result file at PATH in text format
+        --extract-result PATH  extract data stored in result file at PATH to the
+                               current working directory
+    -k, --get-config KEY       print the value of configuration key KEY
+        --hash-file PATH       print the hash (160 bit BLAKE3) of the file at
+                               PATH
+        --print-stats          print statistics counter IDs and corresponding
+                               values in machine-parsable format
+
+See also the manual on <https://ccache.dev/documentation.html>.
+)";
+
+static void
+configuration_printer(const std::string& key,
+                      const std::string& value,
+                      const std::string& origin)
+{
+  PRINT(stdout, "({}) {} = {}\n", origin, key, value);
+}
+
+static void
+print_compression_statistics(const storage::primary::CompressionStatistics& cs)
+{
+  const double ratio = cs.compr_size > 0
+                         ? static_cast<double>(cs.content_size) / cs.compr_size
+                         : 0.0;
+  const double savings = ratio > 0.0 ? 100.0 - (100.0 / ratio) : 0.0;
+
+  using C = util::TextTable::Cell;
+  auto human_readable = Util::format_human_readable_size;
+  util::TextTable table;
+
+  table.add_row({
+    "Total data:",
+    C(human_readable(cs.compr_size + cs.incompr_size)).right_align(),
+    FMT("({} disk blocks)", human_readable(cs.on_disk_size)),
+  });
+  table.add_row({
+    "Compressed data:",
+    C(human_readable(cs.compr_size)).right_align(),
+    FMT("({:.1f}% of original size)", 100.0 - savings),
+  });
+  table.add_row({
+    "  Original size:",
+    C(human_readable(cs.content_size)).right_align(),
+  });
+  table.add_row({
+    "  Compression ratio:",
+    C(FMT("{:.3f} x ", ratio)).right_align(),
+    FMT("({:.1f}% space savings)", savings),
+  });
+  table.add_row({
+    "Incompressible data:",
+    C(human_readable(cs.incompr_size)).right_align(),
+  });
+
+  PRINT_RAW(stdout, table.render());
+}
+
+static void
+trim_dir(const std::string& dir,
+         const uint64_t trim_max_size,
+         const bool trim_lru_mtime)
+{
+  struct File
+  {
+    std::string path;
+    Stat stat;
+  };
+  std::vector<File> files;
+  uint64_t size_before = 0;
+
+  Util::traverse(dir, [&](const std::string& path, const bool is_dir) {
+    const auto stat = Stat::lstat(path);
+    if (!stat) {
+      // Probably some race, ignore.
+      return;
+    }
+    size_before += stat.size_on_disk();
+    if (!is_dir) {
+      const auto name = Util::base_name(path);
+      if (name == "ccache.conf" || name == "stats") {
+        throw Fatal("this looks like a primary cache directory (found {})",
+                    path);
+      }
+      files.push_back({path, stat});
+    }
+  });
+
+  std::sort(files.begin(), files.end(), [&](const auto& f1, const auto& f2) {
+    const auto ts_1 = trim_lru_mtime ? f1.stat.mtim() : f1.stat.atim();
+    const auto ts_2 = trim_lru_mtime ? f2.stat.mtim() : f2.stat.atim();
+    const auto ns_1 = 1'000'000'000ULL * ts_1.tv_sec + ts_1.tv_nsec;
+    const auto ns_2 = 1'000'000'000ULL * ts_2.tv_sec + ts_2.tv_nsec;
+    return ns_1 < ns_2;
+  });
+
+  uint64_t size_after = size_before;
+
+  for (const auto& file : files) {
+    if (size_after <= trim_max_size) {
+      break;
+    }
+    Util::unlink_tmp(file.path);
+    size_after -= file.stat.size();
+  }
+
+  PRINT(stdout,
+        "Removed {} ({} -> {})\n",
+        Util::format_human_readable_size(size_before - size_after),
+        Util::format_human_readable_size(size_before),
+        Util::format_human_readable_size(size_after));
+}
+
+static std::string
+get_version_text()
+{
+  return FMT(
+    VERSION_TEXT, CCACHE_NAME, CCACHE_VERSION, storage::get_features());
+}
+
+std::string
+get_usage_text()
+{
+  return FMT(USAGE_TEXT, CCACHE_NAME);
+}
+
+enum {
+  CHECKSUM_FILE,
+  CONFIG_PATH,
+  DUMP_MANIFEST,
+  DUMP_RESULT,
+  EVICT_OLDER_THAN,
+  EXTRACT_RESULT,
+  HASH_FILE,
+  PRINT_STATS,
+  SHOW_LOG_STATS,
+  TRIM_DIR,
+  TRIM_MAX_SIZE,
+  TRIM_METHOD,
+};
+
+const char options_string[] = "cCd:k:hF:M:po:svVxX:z";
+const option long_options[] = {
+  {"checksum-file", required_argument, nullptr, CHECKSUM_FILE},
+  {"cleanup", no_argument, nullptr, 'c'},
+  {"clear", no_argument, nullptr, 'C'},
+  {"config-path", required_argument, nullptr, CONFIG_PATH},
+  {"dir", required_argument, nullptr, 'd'},
+  {"directory", required_argument, nullptr, 'd'}, // backward compatibility
+  {"dump-manifest", required_argument, nullptr, DUMP_MANIFEST},
+  {"dump-result", required_argument, nullptr, DUMP_RESULT},
+  {"evict-older-than", required_argument, nullptr, EVICT_OLDER_THAN},
+  {"extract-result", required_argument, nullptr, EXTRACT_RESULT},
+  {"get-config", required_argument, nullptr, 'k'},
+  {"hash-file", required_argument, nullptr, HASH_FILE},
+  {"help", no_argument, nullptr, 'h'},
+  {"max-files", required_argument, nullptr, 'F'},
+  {"max-size", required_argument, nullptr, 'M'},
+  {"print-stats", no_argument, nullptr, PRINT_STATS},
+  {"recompress", required_argument, nullptr, 'X'},
+  {"set-config", required_argument, nullptr, 'o'},
+  {"show-compression", no_argument, nullptr, 'x'},
+  {"show-config", no_argument, nullptr, 'p'},
+  {"show-log-stats", no_argument, nullptr, SHOW_LOG_STATS},
+  {"show-stats", no_argument, nullptr, 's'},
+  {"trim-dir", required_argument, nullptr, TRIM_DIR},
+  {"trim-max-size", required_argument, nullptr, TRIM_MAX_SIZE},
+  {"trim-method", required_argument, nullptr, TRIM_METHOD},
+  {"verbose", no_argument, nullptr, 'v'},
+  {"version", no_argument, nullptr, 'V'},
+  {"zero-stats", no_argument, nullptr, 'z'},
+  {nullptr, 0, nullptr, 0}};
+
+int
+process_main_options(int argc, const char* const* argv)
+{
+  int c;
+  nonstd::optional<uint64_t> trim_max_size;
+  bool trim_lru_mtime = false;
+  uint8_t verbosity = 0;
+
+  // First pass: Handle non-command options that affect command options.
+  while ((c = getopt_long(argc,
+                          const_cast<char* const*>(argv),
+                          options_string,
+                          long_options,
+                          nullptr))
+         != -1) {
+    const std::string arg = optarg ? optarg : std::string();
+
+    switch (c) {
+    case 'd': // --dir
+      Util::setenv("CCACHE_DIR", arg);
+      break;
+
+    case CONFIG_PATH:
+      Util::setenv("CCACHE_CONFIGPATH", arg);
+      break;
+
+    case TRIM_MAX_SIZE:
+      trim_max_size = Util::parse_size(arg);
+      break;
+
+    case TRIM_METHOD:
+      trim_lru_mtime = (arg == "ctime");
+      break;
+
+    case 'v': // --verbose
+      ++verbosity;
+      break;
+
+    case '?': // unknown option
+      return EXIT_FAILURE;
+    }
+  }
+
+  // Second pass: Handle command options in order.
+  optind = 1;
+  while ((c = getopt_long(argc,
+                          const_cast<char* const*>(argv),
+                          options_string,
+                          long_options,
+                          nullptr))
+         != -1) {
+    Config config;
+    config.read();
+
+    const std::string arg = optarg ? optarg : std::string();
+
+    switch (c) {
+    case CONFIG_PATH:
+    case 'd': // --dir
+    case TRIM_MAX_SIZE:
+    case TRIM_METHOD:
+    case 'v': // --verbose
+      // Already handled in the first pass.
+      break;
+
+    case CHECKSUM_FILE: {
+      Checksum checksum;
+      Fd fd(arg == "-" ? STDIN_FILENO : open(arg.c_str(), O_RDONLY));
+      Util::read_fd(*fd, [&checksum](const void* data, size_t size) {
+        checksum.update(data, size);
+      });
+      PRINT(stdout, "{:016x}\n", checksum.digest());
+      break;
+    }
+
+    case DUMP_MANIFEST:
+      return Manifest::dump(arg, stdout) ? 0 : 1;
+
+    case DUMP_RESULT: {
+      ResultDumper result_dumper(stdout);
+      Result::Reader result_reader(arg);
+      auto error = result_reader.read(result_dumper);
+      if (error) {
+        PRINT(stderr, "Error: {}\n", *error);
+      }
+      return error ? EXIT_FAILURE : EXIT_SUCCESS;
+    }
+
+    case EVICT_OLDER_THAN: {
+      auto seconds = Util::parse_duration(arg);
+      ProgressBar progress_bar("Evicting...");
+      storage::primary::PrimaryStorage(config).clean_old(
+        [&](double progress) { progress_bar.update(progress); }, seconds);
+      if (isatty(STDOUT_FILENO)) {
+        PRINT_RAW(stdout, "\n");
+      }
+      break;
+    }
+
+    case EXTRACT_RESULT: {
+      ResultExtractor result_extractor(".");
+      Result::Reader result_reader(arg);
+      auto error = result_reader.read(result_extractor);
+      if (error) {
+        PRINT(stderr, "Error: {}\n", *error);
+      }
+      return error ? EXIT_FAILURE : EXIT_SUCCESS;
+    }
+
+    case HASH_FILE: {
+      Hash hash;
+      if (arg == "-") {
+        hash.hash_fd(STDIN_FILENO);
+      } else {
+        hash.hash_file(arg);
+      }
+      PRINT(stdout, "{}\n", hash.digest().to_string());
+      break;
+    }
+
+    case PRINT_STATS: {
+      StatisticsCounters counters;
+      time_t last_updated;
+      std::tie(counters, last_updated) =
+        storage::primary::PrimaryStorage(config).get_all_statistics();
+      Statistics statistics(counters);
+      PRINT_RAW(stdout, statistics.format_machine_readable(last_updated));
+      break;
+    }
+
+    case 'c': // --cleanup
+    {
+      ProgressBar progress_bar("Cleaning...");
+      storage::primary::PrimaryStorage(config).clean_all(
+        [&](double progress) { progress_bar.update(progress); });
+      if (isatty(STDOUT_FILENO)) {
+        PRINT_RAW(stdout, "\n");
+      }
+      break;
+    }
+
+    case 'C': // --clear
+    {
+      ProgressBar progress_bar("Clearing...");
+      storage::primary::PrimaryStorage(config).wipe_all(
+        [&](double progress) { progress_bar.update(progress); });
+      if (isatty(STDOUT_FILENO)) {
+        PRINT_RAW(stdout, "\n");
+      }
+#ifdef INODE_CACHE_SUPPORTED
+      InodeCache(config).drop();
+#endif
+      break;
+    }
+
+    case 'h': // --help
+      PRINT(stdout, USAGE_TEXT, CCACHE_NAME, CCACHE_NAME);
+      return EXIT_SUCCESS;
+
+    case 'k': // --get-config
+      PRINT(stdout, "{}\n", config.get_string_value(arg));
+      break;
+
+    case 'F': { // --max-files
+      auto files = util::value_or_throw<Error>(util::parse_unsigned(arg));
+      config.set_value_in_file(config.primary_config_path(), "max_files", arg);
+      if (files == 0) {
+        PRINT_RAW(stdout, "Unset cache file limit\n");
+      } else {
+        PRINT(stdout, "Set cache file limit to {}\n", files);
+      }
+      break;
+    }
+
+    case 'M': { // --max-size
+      uint64_t size = Util::parse_size(arg);
+      config.set_value_in_file(config.primary_config_path(), "max_size", arg);
+      if (size == 0) {
+        PRINT_RAW(stdout, "Unset cache size limit\n");
+      } else {
+        PRINT(stdout,
+              "Set cache size limit to {}\n",
+              Util::format_human_readable_size(size));
+      }
+      break;
+    }
+
+    case 'o': { // --set-config
+      // Start searching for equal sign at position 1 to improve error message
+      // for the -o=K=V case (key "=K" and value "V").
+      size_t eq_pos = arg.find('=', 1);
+      if (eq_pos == std::string::npos) {
+        throw Error("missing equal sign in \"{}\"", arg);
+      }
+      std::string key = arg.substr(0, eq_pos);
+      std::string value = arg.substr(eq_pos + 1);
+      config.set_value_in_file(config.primary_config_path(), key, value);
+      break;
+    }
+
+    case 'p': // --show-config
+      config.visit_items(configuration_printer);
+      break;
+
+    case SHOW_LOG_STATS: {
+      if (config.stats_log().empty()) {
+        throw Fatal("No stats log has been configured");
+      }
+      Statistics statistics(StatsLog(config.stats_log()).read());
+      const auto timestamp =
+        Stat::stat(config.stats_log(), Stat::OnError::log).mtime();
+      PRINT_RAW(
+        stdout,
+        statistics.format_human_readable(config, timestamp, verbosity, true));
+      if (verbosity == 0) {
+        PRINT_RAW(stdout, "\nUse the -v/--verbose option for more details.\n");
+      }
+      break;
+    }
+
+    case 's': { // --show-stats
+      StatisticsCounters counters;
+      time_t last_updated;
+      std::tie(counters, last_updated) =
+        storage::primary::PrimaryStorage(config).get_all_statistics();
+      Statistics statistics(counters);
+      PRINT_RAW(stdout,
+                statistics.format_human_readable(
+                  config, last_updated, verbosity, false));
+      if (verbosity == 0) {
+        PRINT_RAW(stdout, "\nUse the -v/--verbose option for more details.\n");
+      }
+      break;
+    }
+
+    case TRIM_DIR:
+      if (!trim_max_size) {
+        throw Error("please specify --trim-max-size when using --trim-dir");
+      }
+      trim_dir(arg, *trim_max_size, trim_lru_mtime);
+      break;
+
+    case 'V': // --version
+      PRINT_RAW(stdout, get_version_text());
+      break;
+
+    case 'x': // --show-compression
+    {
+      ProgressBar progress_bar("Scanning...");
+      const auto compression_statistics =
+        storage::primary::PrimaryStorage(config).get_compression_statistics(
+          [&](double progress) { progress_bar.update(progress); });
+      if (isatty(STDOUT_FILENO)) {
+        PRINT_RAW(stdout, "\n\n");
+      }
+      print_compression_statistics(compression_statistics);
+      break;
+    }
+
+    case 'X': // --recompress
+    {
+      nonstd::optional<int8_t> wanted_level;
+      if (arg == "uncompressed") {
+        wanted_level = nonstd::nullopt;
+      } else {
+        wanted_level = util::value_or_throw<Error>(
+          util::parse_signed(arg, INT8_MIN, INT8_MAX, "compression level"));
+      }
+
+      ProgressBar progress_bar("Recompressing...");
+      storage::primary::PrimaryStorage(config).recompress(
+        wanted_level, [&](double progress) { progress_bar.update(progress); });
+      break;
+    }
+
+    case 'z': // --zero-stats
+      storage::primary::PrimaryStorage(config).zero_all_statistics();
+      PRINT_RAW(stdout, "Statistics zeroed\n");
+      break;
+
+    default:
+      PRINT(stderr, USAGE_TEXT, CCACHE_NAME, CCACHE_NAME);
+      return EXIT_FAILURE;
+    }
+  }
+
+  return EXIT_SUCCESS;
+}
+
+} // namespace core
diff --git a/src/core/mainoptions.hpp b/src/core/mainoptions.hpp
new file mode 100644 (file)
index 0000000..e0f33e6
--- /dev/null
@@ -0,0 +1,30 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <string>
+
+namespace core {
+
+// The main program when not doing a compile.
+int process_main_options(int argc, const char* const* argv);
+
+std::string get_usage_text();
+
+} // namespace core
diff --git a/src/core/types.hpp b/src/core/types.hpp
new file mode 100644 (file)
index 0000000..d4fbf66
--- /dev/null
@@ -0,0 +1,25 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+namespace core {
+
+enum class CacheEntryType { result, manifest };
+
+} // namespace core
diff --git a/src/core/wincompat.hpp b/src/core/wincompat.hpp
new file mode 100644 (file)
index 0000000..f983bec
--- /dev/null
@@ -0,0 +1,107 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#ifdef _WIN32
+#  include <sys/stat.h>
+
+#  define NOMINMAX 1
+#  define STDIN_FILENO 0
+#  define STDOUT_FILENO 1
+#  define STDERR_FILENO 2
+
+#  ifdef _MSC_VER
+#    define PATH_MAX MAX_PATH
+#  endif // _MSC_VER
+
+// From:
+// http://mesos.apache.org/api/latest/c++/3rdparty_2stout_2include_2stout_2windows_8hpp_source.html
+#  ifdef _MSC_VER
+const mode_t S_IRUSR = mode_t(_S_IREAD);
+const mode_t S_IWUSR = mode_t(_S_IWRITE);
+#  endif
+
+#  ifndef S_IFIFO
+#    define S_IFIFO 0x1000
+#  endif
+
+#  ifndef S_IFBLK
+#    define S_IFBLK 0x6000
+#  endif
+
+#  ifndef S_IFLNK
+#    define S_IFLNK 0xA000
+#  endif
+
+#  ifndef S_ISREG
+#    define S_ISREG(m) (((m)&S_IFMT) == S_IFREG)
+#  endif
+#  ifndef S_ISDIR
+#    define S_ISDIR(m) (((m)&S_IFMT) == S_IFDIR)
+#  endif
+#  ifndef S_ISFIFO
+#    define S_ISFIFO(m) (((m)&S_IFMT) == S_IFIFO)
+#  endif
+#  ifndef S_ISCHR
+#    define S_ISCHR(m) (((m)&S_IFMT) == S_IFCHR)
+#  endif
+#  ifndef S_ISLNK
+#    define S_ISLNK(m) (((m)&S_IFMT) == S_IFLNK)
+#  endif
+#  ifndef S_ISBLK
+#    define S_ISBLK(m) (((m)&S_IFMT) == S_IFBLK)
+#  endif
+
+#  include <direct.h>
+#  include <fcntl.h>
+#  include <io.h>
+#  include <process.h>
+#  define NOMINMAX 1
+#  define WIN32_NO_STATUS
+// clang-format off
+#  include <windows.h>
+#  include <bcrypt.h> // NTSTATUS
+#  include <winsock2.h> // struct timeval
+// clang-format on
+#  undef WIN32_NO_STATUS
+#  include <ntstatus.h>
+#  define mkdir(a, b) _mkdir(a)
+
+// Protect against incidental use of MinGW execv.
+#  define execv(a, b) do_not_call_execv_on_windows
+
+#  ifdef _MSC_VER
+#    define PATH_MAX MAX_PATH
+#  endif
+
+#  ifdef _MSC_VER
+#    define DLLIMPORT __declspec(dllimport)
+#  else
+#    define DLLIMPORT
+#  endif
+
+#  define STDIN_FILENO 0
+#  define STDOUT_FILENO 1
+#  define STDERR_FILENO 2
+
+#  ifndef O_BINARY
+#    define O_BINARY 0
+#  endif
+
+#endif // _WIN32
diff --git a/src/exceptions.hpp b/src/exceptions.hpp
deleted file mode 100644 (file)
index f35f50c..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include "system.hpp"
-
-#include "FormatNonstdStringView.hpp"
-
-#include "third_party/fmt/core.h"
-#include "third_party/nonstd/optional.hpp"
-
-#include <stdexcept>
-#include <string>
-#include <utility>
-
-// Don't throw or catch ErrorBase directly, use a subclass.
-class ErrorBase : public std::runtime_error
-{
-  using std::runtime_error::runtime_error;
-};
-
-// Throw an Error to indicate a potentially non-fatal error that may be caught
-// and handled by callers. An uncaught Error that reaches the top level will be
-// treated similar to Fatal.
-class Error : public ErrorBase
-{
-public:
-  // Special case: If given only one string, don't parse it as a format string.
-  Error(const std::string& message);
-
-  // `args` are forwarded to `fmt::format`.
-  template<typename... T> inline Error(T&&... args);
-};
-
-inline Error::Error(const std::string& message) : ErrorBase(message)
-{
-}
-
-template<typename... T>
-inline Error::Error(T&&... args)
-  : ErrorBase(fmt::format(std::forward<T>(args)...))
-{
-}
-
-// Throw a Fatal to make ccache print the error message to stderr and exit
-// with a non-zero exit code.
-class Fatal : public ErrorBase
-{
-public:
-  // Special case: If given only one string, don't parse it as a format string.
-  Fatal(const std::string& message);
-
-  // `args` are forwarded to `fmt::format`.
-  template<typename... T> inline Fatal(T&&... args);
-};
-
-inline Fatal::Fatal(const std::string& message) : ErrorBase(message)
-{
-}
-
-template<typename... T>
-inline Fatal::Fatal(T&&... args)
-  : ErrorBase(fmt::format(std::forward<T>(args)...))
-{
-}
index ebcf2becbeab1c6372e0a98e28a9126845e7e962..8f4e787a577dc24a590339a64cac1c82c673bbf2 100644 (file)
 #include "Stat.hpp"
 #include "TemporaryFile.hpp"
 #include "Util.hpp"
+#include "Win32Util.hpp"
 #include "fmtmacros.hpp"
 
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+#include <util/path.hpp>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
+#ifdef HAVE_SYS_WAIT_H
+#  include <sys/wait.h>
+#endif
+
 #ifdef _WIN32
 #  include "Finalizer.hpp"
-#  include "Win32Util.hpp"
 #endif
 
 using nonstd::string_view;
@@ -192,7 +204,7 @@ execute(Context& ctx, const char* const* argv, Fd&& fd_out, Fd&& fd_err)
   }
 
   if (ctx.compiler_pid == -1) {
-    throw Fatal("Failed to fork: {}", strerror(errno));
+    throw core::Fatal("Failed to fork: {}", strerror(errno));
   }
 
   if (ctx.compiler_pid == 0) {
@@ -214,7 +226,7 @@ execute(Context& ctx, const char* const* argv, Fd&& fd_out, Fd&& fd_err)
     if (result == -1 && errno == EINTR) {
       continue;
     }
-    throw Fatal("waitpid failed: {}", strerror(errno));
+    throw core::Fatal("waitpid failed: {}", strerror(errno));
   }
 
   {
@@ -241,7 +253,7 @@ find_executable(const Context& ctx,
                 const std::string& name,
                 const std::string& exclude_name)
 {
-  if (Util::is_absolute_path(name)) {
+  if (util::is_absolute_path(name)) {
     return name;
   }
 
@@ -268,7 +280,7 @@ find_executable_in_path(const std::string& name,
 
   // Search the path looking for the first compiler of the right name that isn't
   // us.
-  for (const std::string& dir : Util::split_into_strings(path, PATH_DELIM)) {
+  for (const std::string& dir : util::split_path_list(path)) {
 #ifdef _WIN32
     char namebuf[MAX_PATH];
     int ret = SearchPath(
index 628ceb32753e529a01665474a50a146ef654c84e..8e492263981d002c3b1e583112da8c8283f26fbe 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,8 +18,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 #include "Fd.hpp"
 
 #include <string>
index 9a017fae9bff54dc2422763928eef27f41ce7248..3b7cf5d3118e9744805e2e4555cddfdba5ff5030 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "third_party/fmt/core.h"
-#include "third_party/fmt/format.h"
+#include <FormatNonstdStringView.hpp>
+
+#include <third_party/fmt/core.h>
+#include <third_party/fmt/format.h>
 
 // Convenience macro for calling `fmt::format` with `FMT_STRING` around the
 // format string literal.
index 7378c02fa980ba4a3975e80f8b282f4b8fb4d67d..9e30cd019ea9e163cd818e493a7ee5357a3b6a3e 100644 (file)
 #include "Context.hpp"
 #include "Hash.hpp"
 #include "Logging.hpp"
-#include "Sloppiness.hpp"
 #include "Stat.hpp"
+#include "Util.hpp"
+#include "Win32Util.hpp"
 #include "execute.hpp"
 #include "fmtmacros.hpp"
 #include "macroskip.hpp"
 
-#include "third_party/blake3/blake3_cpu_supports_avx2.h"
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+#include <util/string.hpp>
 
 #ifdef INODE_CACHE_SUPPORTED
 #  include "InodeCache.hpp"
 #endif
 
-#ifdef _WIN32
-#  include "Win32Util.hpp"
+#include "third_party/blake3/blake3_cpu_supports_avx2.h"
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
+#ifdef HAVE_SYS_WAIT_H
+#  include <sys/wait.h>
 #endif
 
 #ifdef HAVE_AVX2
@@ -180,7 +189,7 @@ hash_source_code_file_nocache(const Context& ctx,
     std::string data;
     try {
       data = Util::read_file(path, size_hint);
-    } catch (Error&) {
+    } catch (core::Error&) {
       return HASH_SOURCE_CODE_ERROR;
     }
     int result = hash_source_code_string(ctx, hash, data, path);
@@ -195,7 +204,7 @@ get_content_type(const Config& config, const std::string& path)
   if (Util::is_precompiled_header(path)) {
     return InodeCache::ContentType::precompiled_header;
   }
-  if (config.sloppiness() & SLOPPY_TIME_MACROS) {
+  if (config.sloppiness().is_enabled(core::Sloppy::time_macros)) {
     return InodeCache::ContentType::code_with_sloppy_time_macros;
   }
   return InodeCache::ContentType::code;
@@ -225,7 +234,7 @@ hash_source_code_string(const Context& ctx,
 
   // Check for __DATE__, __TIME__ and __TIMESTAMP__if the sloppiness
   // configuration tells us we should.
-  if (!(ctx.config.sloppiness() & SLOPPY_TIME_MACROS)) {
+  if (!(ctx.config.sloppiness().is_enabled(core::Sloppy::time_macros))) {
     result |= check_for_temporal_macros(str);
   }
 
@@ -367,14 +376,14 @@ hash_command_output(Hash& hash,
                     const std::string& compiler)
 {
 #ifdef _WIN32
-  std::string adjusted_command = Util::strip_whitespace(command);
+  std::string adjusted_command = util::strip_whitespace(command);
 
   // Add "echo" command.
   bool using_cmd_exe;
-  if (Util::starts_with(adjusted_command, "echo")) {
+  if (util::starts_with(adjusted_command, "echo")) {
     adjusted_command = FMT("cmd.exe /c \"{}\"", adjusted_command);
     using_cmd_exe = true;
-  } else if (Util::starts_with(adjusted_command, "%compiler%")
+  } else if (util::starts_with(adjusted_command, "%compiler%")
              && compiler == "echo") {
     adjusted_command =
       FMT("cmd.exe /c \"{}{}\"", compiler, adjusted_command.substr(10));
@@ -462,12 +471,12 @@ hash_command_output(Hash& hash,
 #else
   int pipefd[2];
   if (pipe(pipefd) == -1) {
-    throw Fatal("pipe failed: {}", strerror(errno));
+    throw core::Fatal("pipe failed: {}", strerror(errno));
   }
 
   pid_t pid = fork();
   if (pid == -1) {
-    throw Fatal("fork failed: {}", strerror(errno));
+    throw core::Fatal("fork failed: {}", strerror(errno));
   }
 
   if (pid == 0) {
index ae7bcb0cf4b961adf7f27a46522dbee97db3e4ff..b3f1756d7b7b0504d18a234b1f96c0c3d0bcf01a 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2009-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2009-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 
 #pragma once
 
-#include "system.hpp"
-
 #include "third_party/nonstd/string_view.hpp"
 
+#include <cstddef>
 #include <string>
 
 class Config;
index 99bf386e04665ee51816c6fe4043f202cc6beb2c..74be7aaf622c8d810fa5f85f0325092f5ecf04e4 100644 (file)
@@ -18,8 +18,6 @@
 
 #pragma once
 
-#include "system.hpp"
-
 #include "Config.hpp"
 
 #include <string>
index f15932346f604b5ed4946df0bc738de451ce560c..5cacba87f20e31532cfd3ecef2353d8c8d58cb49 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2010-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2010-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,7 +18,7 @@
 
 #pragma once
 
-#include "system.hpp"
+#include <cstdint>
 
 // A Boyer-Moore-Horspool skip table used for searching for the strings
 // "__TIME__", "__DATE__" and "__TIMESTAMP__".
diff --git a/src/storage/CMakeLists.txt b/src/storage/CMakeLists.txt
new file mode 100644 (file)
index 0000000..c107e8e
--- /dev/null
@@ -0,0 +1,9 @@
+add_subdirectory(primary)
+add_subdirectory(secondary)
+
+set(
+  sources
+  ${CMAKE_CURRENT_SOURCE_DIR}/Storage.cpp
+)
+
+target_sources(ccache_framework PRIVATE ${sources})
diff --git a/src/storage/Storage.cpp b/src/storage/Storage.cpp
new file mode 100644 (file)
index 0000000..7c2d0df
--- /dev/null
@@ -0,0 +1,563 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "Storage.hpp"
+
+#include <Checksum.hpp>
+#include <Config.hpp>
+#include <Logging.hpp>
+#include <MiniTrace.hpp>
+#include <TemporaryFile.hpp>
+#include <Util.hpp>
+#include <assertions.hpp>
+#include <core/Statistic.hpp>
+#include <core/exceptions.hpp>
+#include <fmtmacros.hpp>
+#include <storage/secondary/FileStorage.hpp>
+#include <storage/secondary/HttpStorage.hpp>
+#ifdef HAVE_REDIS_STORAGE_BACKEND
+#  include <storage/secondary/RedisStorage.hpp>
+#endif
+#include <util/Timer.hpp>
+#include <util/Tokenizer.hpp>
+#include <util/expected.hpp>
+#include <util/string.hpp>
+
+#include <third_party/url.hpp>
+
+#include <algorithm>
+#include <cmath>
+#include <unordered_map>
+#include <vector>
+
+namespace storage {
+
+const std::unordered_map<std::string /*scheme*/,
+                         std::shared_ptr<secondary::SecondaryStorage>>
+  k_secondary_storage_implementations = {
+    {"file", std::make_shared<secondary::FileStorage>()},
+    {"http", std::make_shared<secondary::HttpStorage>()},
+#ifdef HAVE_REDIS_STORAGE_BACKEND
+    {"redis", std::make_shared<secondary::RedisStorage>()},
+#endif
+};
+
+std::string
+get_features()
+{
+  std::vector<std::string> features;
+  features.reserve(k_secondary_storage_implementations.size());
+  std::transform(k_secondary_storage_implementations.begin(),
+                 k_secondary_storage_implementations.end(),
+                 std::back_inserter(features),
+                 [](auto& entry) { return FMT("{}-storage", entry.first); });
+  std::sort(features.begin(), features.end());
+  return util::join(features, " ");
+}
+
+struct SecondaryStorageShardConfig
+{
+  std::string name;
+  double weight;
+};
+
+struct SecondaryStorageConfig
+{
+  std::vector<SecondaryStorageShardConfig> shards;
+  secondary::SecondaryStorage::Backend::Params params;
+  bool read_only = false;
+  bool share_hits = true;
+};
+
+struct SecondaryStorageBackendEntry
+{
+  Url url;                     // With expanded "*".
+  std::string url_for_logging; // With expanded "*".
+  std::unique_ptr<secondary::SecondaryStorage::Backend> impl;
+  bool failed = false;
+};
+
+struct SecondaryStorageEntry
+{
+  SecondaryStorageConfig config;
+  std::string url_for_logging; // With unexpanded "*".
+  std::shared_ptr<secondary::SecondaryStorage> storage;
+  std::vector<SecondaryStorageBackendEntry> backends;
+};
+
+static std::string
+to_string(const SecondaryStorageConfig& entry)
+{
+  std::string result = entry.params.url.str();
+  for (const auto& attr : entry.params.attributes) {
+    result += FMT("|{}={}", attr.key, attr.raw_value);
+  }
+  return result;
+}
+
+static SecondaryStorageConfig
+parse_storage_config(const nonstd::string_view entry)
+{
+  const auto parts =
+    Util::split_into_views(entry, "|", util::Tokenizer::Mode::include_empty);
+
+  if (parts.empty() || parts.front().empty()) {
+    throw core::Error("secondary storage config must provide a URL: {}", entry);
+  }
+
+  SecondaryStorageConfig result;
+  result.params.url = std::string(parts[0]);
+  // The Url class is parsing the URL object lazily; check if successful.
+  try {
+    std::ignore = result.params.url.host();
+  } catch (const Url::parse_error& e) {
+    throw core::Error("Cannot parse URL: {}", e.what());
+  }
+
+  if (result.params.url.scheme().empty()) {
+    throw core::Error("URL scheme must not be empty: {}", entry);
+  }
+
+  for (size_t i = 1; i < parts.size(); ++i) {
+    if (parts[i].empty()) {
+      continue;
+    }
+    const auto kv_pair = util::split_once(parts[i], '=');
+    const auto& key = kv_pair.first;
+    const auto& raw_value = kv_pair.second.value_or("true");
+    const auto value =
+      util::value_or_throw<core::Error>(util::percent_decode(raw_value));
+    if (key == "read-only") {
+      result.read_only = (value == "true");
+    } else if (key == "shards") {
+      const auto url_str = result.params.url.str();
+      if (url_str.find('*') == std::string::npos) {
+        throw core::Error(R"(Missing "*" in URL when using shards: "{}")",
+                          url_str);
+      }
+      for (const auto& shard : util::Tokenizer(value, ",")) {
+        double weight = 1.0;
+        nonstd::string_view name;
+        const auto lp_pos = shard.find('(');
+        if (lp_pos != nonstd::string_view::npos) {
+          if (shard.back() != ')') {
+            throw core::Error("Invalid shard name: \"{}\"", shard);
+          }
+          weight =
+            util::value_or_throw<core::Error>(util::parse_double(std::string(
+              shard.substr(lp_pos + 1, shard.length() - lp_pos - 2))));
+          if (weight < 0.0) {
+            throw core::Error("Invalid shard weight: \"{}\"", weight);
+          }
+          name = shard.substr(0, lp_pos);
+        } else {
+          name = shard;
+        }
+
+        result.shards.push_back({std::string(name), weight});
+      }
+    } else if (key == "share-hits") {
+      result.share_hits = (value == "true");
+    }
+
+    result.params.attributes.push_back(
+      {std::string(key), value, std::string(raw_value)});
+  }
+
+  return result;
+}
+
+static std::vector<SecondaryStorageConfig>
+parse_storage_configs(const nonstd::string_view& configs)
+{
+  std::vector<SecondaryStorageConfig> result;
+  for (const auto& config : util::Tokenizer(configs, " ")) {
+    result.push_back(parse_storage_config(config));
+  }
+  return result;
+}
+
+static std::shared_ptr<secondary::SecondaryStorage>
+get_storage(const Url& url)
+{
+  const auto it = k_secondary_storage_implementations.find(url.scheme());
+  if (it != k_secondary_storage_implementations.end()) {
+    return it->second;
+  } else {
+    return {};
+  }
+}
+
+Storage::Storage(const Config& config) : primary(config), m_config(config)
+{
+}
+
+Storage::~Storage()
+{
+  for (const auto& tmp_file : m_tmp_files) {
+    Util::unlink_tmp(tmp_file);
+  }
+}
+
+void
+Storage::initialize()
+{
+  primary.initialize();
+  add_secondary_storages();
+}
+
+void
+Storage::finalize()
+{
+  primary.finalize();
+}
+
+nonstd::optional<std::string>
+Storage::get(const Digest& key, const core::CacheEntryType type)
+{
+  MTR_SCOPE("storage", "get");
+
+  const auto path = primary.get(key, type);
+  primary.increment_statistic(path ? core::Statistic::primary_storage_hit
+                                   : core::Statistic::primary_storage_miss);
+  if (path) {
+    if (m_config.reshare()) {
+      // Temporary optimization until primary storage API has been refactored to
+      // pass data via memory instead of files.
+      const bool should_put_in_secondary_storage =
+        std::any_of(m_secondary_storages.begin(),
+                    m_secondary_storages.end(),
+                    [](const auto& entry) { return !entry->config.read_only; });
+      if (should_put_in_secondary_storage) {
+        std::string value;
+        try {
+          value = Util::read_file(*path);
+        } catch (const core::Error& e) {
+          LOG("Failed to read {}: {}", *path, e.what());
+          return path; // Don't indicate failure since primary storage was OK.
+        }
+        put_in_secondary_storage(key, value, true);
+      }
+    }
+
+    return path;
+  }
+
+  const auto value_and_share_hits = get_from_secondary_storage(key);
+  if (!value_and_share_hits) {
+    return nonstd::nullopt;
+  }
+  const auto& value = value_and_share_hits->first;
+  const auto& share_hits = value_and_share_hits->second;
+
+  TemporaryFile tmp_file(FMT("{}/tmp.get", m_config.temporary_dir()));
+  m_tmp_files.push_back(tmp_file.path);
+  try {
+    Util::write_file(tmp_file.path, value);
+  } catch (const core::Error& e) {
+    throw core::Fatal("Error writing to {}: {}", tmp_file.path, e.what());
+  }
+
+  if (share_hits) {
+    primary.put(key, type, [&](const auto& path) {
+      try {
+        Util::copy_file(tmp_file.path, path);
+      } catch (const core::Error& e) {
+        LOG("Failed to copy {} to {}: {}", tmp_file.path, path, e.what());
+        // Don't indicate failure since get from primary storage was OK.
+      }
+      return true;
+    });
+  }
+
+  return tmp_file.path;
+}
+
+bool
+Storage::put(const Digest& key,
+             const core::CacheEntryType type,
+             const storage::EntryWriter& entry_writer)
+{
+  MTR_SCOPE("storage", "put");
+
+  const auto path = primary.put(key, type, entry_writer);
+  if (!path) {
+    return false;
+  }
+
+  // Temporary optimization until primary storage API has been refactored to
+  // pass data via memory instead of files.
+  const bool should_put_in_secondary_storage =
+    std::any_of(m_secondary_storages.begin(),
+                m_secondary_storages.end(),
+                [](const auto& entry) { return !entry->config.read_only; });
+  if (should_put_in_secondary_storage) {
+    std::string value;
+    try {
+      value = Util::read_file(*path);
+    } catch (const core::Error& e) {
+      LOG("Failed to read {}: {}", *path, e.what());
+      return true; // Don't indicate failure since primary storage was OK.
+    }
+    put_in_secondary_storage(key, value, false);
+  }
+
+  return true;
+}
+
+void
+Storage::remove(const Digest& key, const core::CacheEntryType type)
+{
+  MTR_SCOPE("storage", "remove");
+
+  primary.remove(key, type);
+  remove_from_secondary_storage(key);
+}
+
+std::string
+Storage::get_secondary_storage_config_for_logging() const
+{
+  auto configs = parse_storage_configs(m_config.secondary_storage());
+  for (auto& config : configs) {
+    const auto storage = get_storage(config.params.url);
+    if (storage) {
+      storage->redact_secrets(config.params);
+    }
+  }
+  return util::join(configs, " ");
+}
+
+void
+Storage::add_secondary_storages()
+{
+  const auto configs = parse_storage_configs(m_config.secondary_storage());
+  for (const auto& config : configs) {
+    auto url_for_logging = config.params.url;
+    url_for_logging.user_info("");
+    const auto storage = get_storage(config.params.url);
+    if (!storage) {
+      throw core::Error("unknown secondary storage URL: {}",
+                        url_for_logging.str());
+    }
+    m_secondary_storages.push_back(std::make_unique<SecondaryStorageEntry>(
+      SecondaryStorageEntry{config, url_for_logging.str(), storage, {}}));
+  }
+}
+
+void
+Storage::mark_backend_as_failed(
+  SecondaryStorageBackendEntry& backend_entry,
+  const secondary::SecondaryStorage::Backend::Failure failure)
+{
+  // The backend is expected to log details about the error.
+  backend_entry.failed = true;
+  primary.increment_statistic(
+    failure == secondary::SecondaryStorage::Backend::Failure::timeout
+      ? core::Statistic::secondary_storage_timeout
+      : core::Statistic::secondary_storage_error);
+}
+
+static double
+to_half_open_unit_interval(uint64_t value)
+{
+  constexpr uint8_t double_significand_bits = 53;
+  constexpr uint64_t denominator = 1ULL << double_significand_bits;
+  constexpr uint64_t mask = denominator - 1;
+  return static_cast<double>(value & mask) / denominator;
+}
+
+static Url
+get_shard_url(const Digest& key,
+              const std::string& url,
+              const std::vector<SecondaryStorageShardConfig>& shards)
+{
+  ASSERT(!shards.empty());
+
+  // This is the "weighted rendezvous hashing" algorithm.
+  double highest_score = -1.0;
+  std::string best_shard;
+  for (const auto& shard_config : shards) {
+    Checksum checksum;
+    checksum.update(key.bytes(), key.size());
+    checksum.update(shard_config.name.data(), shard_config.name.length());
+    const double score = to_half_open_unit_interval(checksum.digest());
+    ASSERT(score >= 0.0 && score < 1.0);
+    const double weighted_score =
+      score == 0.0 ? 0.0 : shard_config.weight / -std::log(score);
+    if (weighted_score > highest_score) {
+      best_shard = shard_config.name;
+      highest_score = weighted_score;
+    }
+  }
+
+  return util::replace_first(url, "*", best_shard);
+}
+
+SecondaryStorageBackendEntry*
+Storage::get_backend(SecondaryStorageEntry& entry,
+                     const Digest& key,
+                     const nonstd::string_view operation_description,
+                     const bool for_writing)
+{
+  if (for_writing && entry.config.read_only) {
+    LOG("Not {} {} since it is read-only",
+        operation_description,
+        entry.url_for_logging);
+    return nullptr;
+  }
+
+  const auto shard_url =
+    entry.config.shards.empty()
+      ? entry.config.params.url
+      : get_shard_url(key, entry.config.params.url.str(), entry.config.shards);
+  auto backend =
+    std::find_if(entry.backends.begin(),
+                 entry.backends.end(),
+                 [&](const auto& x) { return x.url.str() == shard_url.str(); });
+
+  if (backend == entry.backends.end()) {
+    auto shard_url_for_logging = shard_url;
+    shard_url_for_logging.user_info("");
+    entry.backends.push_back(
+      {shard_url, shard_url_for_logging.str(), {}, false});
+    auto shard_params = entry.config.params;
+    shard_params.url = shard_url;
+    try {
+      entry.backends.back().impl = entry.storage->create_backend(shard_params);
+    } catch (const secondary::SecondaryStorage::Backend::Failed& e) {
+      LOG("Failed to construct backend for {}{}",
+          entry.url_for_logging,
+          nonstd::string_view(e.what()).empty() ? "" : FMT(": {}", e.what()));
+      mark_backend_as_failed(entry.backends.back(), e.failure());
+    }
+    return &entry.backends.back();
+  } else if (backend->failed) {
+    LOG("Not {} {} since it failed earlier",
+        operation_description,
+        entry.url_for_logging);
+    return nullptr;
+  } else {
+    return &*backend;
+  }
+}
+
+nonstd::optional<std::pair<std::string, bool>>
+Storage::get_from_secondary_storage(const Digest& key)
+{
+  MTR_SCOPE("secondary_storage", "get");
+
+  for (const auto& entry : m_secondary_storages) {
+    auto backend = get_backend(*entry, key, "getting from", false);
+    if (!backend) {
+      continue;
+    }
+
+    Timer timer;
+    const auto result = backend->impl->get(key);
+    const auto ms = timer.measure_ms();
+    if (!result) {
+      mark_backend_as_failed(*backend, result.error());
+      continue;
+    }
+
+    const auto& value = *result;
+    if (value) {
+      LOG("Retrieved {} from {} ({:.2f} ms)",
+          key.to_string(),
+          backend->url_for_logging,
+          ms);
+      primary.increment_statistic(core::Statistic::secondary_storage_hit);
+      return std::make_pair(*value, entry->config.share_hits);
+    } else {
+      LOG("No {} in {} ({:.2f} ms)",
+          key.to_string(),
+          backend->url_for_logging,
+          ms);
+      primary.increment_statistic(core::Statistic::secondary_storage_miss);
+    }
+  }
+
+  return nonstd::nullopt;
+}
+
+void
+Storage::put_in_secondary_storage(const Digest& key,
+                                  const std::string& value,
+                                  bool only_if_missing)
+{
+  MTR_SCOPE("secondary_storage", "put");
+
+  for (const auto& entry : m_secondary_storages) {
+    auto backend = get_backend(*entry, key, "putting in", true);
+    if (!backend) {
+      continue;
+    }
+
+    Timer timer;
+    const auto result = backend->impl->put(key, value, only_if_missing);
+    const auto ms = timer.measure_ms();
+    if (!result) {
+      // The backend is expected to log details about the error.
+      mark_backend_as_failed(*backend, result.error());
+      continue;
+    }
+
+    const bool stored = *result;
+    LOG("{} {} in {} ({:.2f} ms)",
+        stored ? "Stored" : "Did not have to store",
+        key.to_string(),
+        entry->url_for_logging,
+        ms);
+  }
+}
+
+void
+Storage::remove_from_secondary_storage(const Digest& key)
+{
+  MTR_SCOPE("secondary_storage", "remove");
+
+  for (const auto& entry : m_secondary_storages) {
+    auto backend = get_backend(*entry, key, "removing from", true);
+    if (!backend) {
+      continue;
+    }
+
+    Timer timer;
+    const auto result = backend->impl->remove(key);
+    const auto ms = timer.measure_ms();
+    if (!result) {
+      mark_backend_as_failed(*backend, result.error());
+      continue;
+    }
+
+    const bool removed = *result;
+    if (removed) {
+      LOG("Removed {} from {} ({:.2f} ms)",
+          key.to_string(),
+          entry->url_for_logging,
+          ms);
+    } else {
+      LOG("No {} to remove from {} ({:.2f} ms)",
+          key.to_string(),
+          entry->url_for_logging,
+          ms);
+    }
+  }
+}
+
+} // namespace storage
diff --git a/src/storage/Storage.hpp b/src/storage/Storage.hpp
new file mode 100644 (file)
index 0000000..3e59674
--- /dev/null
@@ -0,0 +1,92 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <core/types.hpp>
+#include <storage/primary/PrimaryStorage.hpp>
+#include <storage/secondary/SecondaryStorage.hpp>
+#include <storage/types.hpp>
+
+#include <third_party/nonstd/optional.hpp>
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class Digest;
+
+namespace storage {
+
+std::string get_features();
+
+struct SecondaryStorageBackendEntry;
+struct SecondaryStorageEntry;
+
+class Storage
+{
+public:
+  Storage(const Config& config);
+  ~Storage();
+
+  void initialize();
+  void finalize();
+
+  primary::PrimaryStorage primary;
+
+  // Returns a path to a file containing the value.
+  nonstd::optional<std::string> get(const Digest& key,
+                                    core::CacheEntryType type);
+
+  bool put(const Digest& key,
+           core::CacheEntryType type,
+           const storage::EntryWriter& entry_writer);
+
+  void remove(const Digest& key, core::CacheEntryType type);
+
+  std::string get_secondary_storage_config_for_logging() const;
+
+private:
+  const Config& m_config;
+  std::vector<std::unique_ptr<SecondaryStorageEntry>> m_secondary_storages;
+  std::vector<std::string> m_tmp_files;
+
+  void add_secondary_storages();
+
+  void
+  mark_backend_as_failed(SecondaryStorageBackendEntry& backend_entry,
+                         secondary::SecondaryStorage::Backend::Failure failure);
+
+  SecondaryStorageBackendEntry*
+  get_backend(SecondaryStorageEntry& entry,
+              const Digest& key,
+              nonstd::string_view operation_description,
+              const bool for_writing);
+  nonstd::optional<std::pair<std::string, bool>>
+  get_from_secondary_storage(const Digest& key);
+
+  void put_in_secondary_storage(const Digest& key,
+                                const std::string& value,
+                                bool only_if_missing);
+
+  void remove_from_secondary_storage(const Digest& key);
+};
+
+} // namespace storage
diff --git a/src/storage/primary/CMakeLists.txt b/src/storage/primary/CMakeLists.txt
new file mode 100644 (file)
index 0000000..ab6ee4a
--- /dev/null
@@ -0,0 +1,12 @@
+set(
+  sources
+  ${CMAKE_CURRENT_SOURCE_DIR}/CacheFile.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/PrimaryStorage.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/PrimaryStorage_cleanup.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/PrimaryStorage_compress.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/PrimaryStorage_statistics.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/StatsFile.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/util.cpp
+)
+
+target_sources(ccache_framework PRIVATE ${sources})
diff --git a/src/storage/primary/CacheFile.cpp b/src/storage/primary/CacheFile.cpp
new file mode 100644 (file)
index 0000000..8ada17f
--- /dev/null
@@ -0,0 +1,45 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "CacheFile.hpp"
+
+#include <Manifest.hpp>
+#include <Result.hpp>
+#include <util/string.hpp>
+
+const Stat&
+CacheFile::lstat() const
+{
+  if (!m_stat) {
+    m_stat = Stat::lstat(m_path);
+  }
+
+  return *m_stat;
+}
+
+CacheFile::Type
+CacheFile::type() const
+{
+  if (util::ends_with(m_path, Manifest::k_file_suffix)) {
+    return Type::manifest;
+  } else if (util::ends_with(m_path, Result::k_file_suffix)) {
+    return Type::result;
+  } else {
+    return Type::unknown;
+  }
+}
diff --git a/src/storage/primary/CacheFile.hpp b/src/storage/primary/CacheFile.hpp
new file mode 100644 (file)
index 0000000..14064fe
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <Stat.hpp>
+
+#include <third_party/nonstd/optional.hpp>
+
+#include <string>
+
+class CacheFile
+{
+public:
+  enum class Type { result, manifest, unknown };
+
+  explicit CacheFile(const std::string& path);
+
+  const Stat& lstat() const;
+  const std::string& path() const;
+  Type type() const;
+
+private:
+  std::string m_path;
+  mutable nonstd::optional<Stat> m_stat;
+};
+
+inline CacheFile::CacheFile(const std::string& path) : m_path(path)
+{
+}
+
+inline const std::string&
+CacheFile::path() const
+{
+  return m_path;
+}
diff --git a/src/storage/primary/PrimaryStorage.cpp b/src/storage/primary/PrimaryStorage.cpp
new file mode 100644 (file)
index 0000000..bbe4d53
--- /dev/null
@@ -0,0 +1,405 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "PrimaryStorage.hpp"
+
+#include <Config.hpp>
+#include <Logging.hpp>
+#include <MiniTrace.hpp>
+#include <Util.hpp>
+#include <assertions.hpp>
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+#include <fmtmacros.hpp>
+#include <storage/primary/StatsFile.hpp>
+#include <util/file.hpp>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
+using core::Statistic;
+
+namespace storage {
+namespace primary {
+
+// How often (in seconds) to scan $CCACHE_DIR/tmp for left-over temporary
+// files.
+const int k_tempdir_cleanup_interval = 2 * 24 * 60 * 60; // 2 days
+
+// Maximum files per cache directory. This constant is somewhat arbitrarily
+// chosen to be large enough to avoid unnecessary cache levels but small enough
+// not to make esoteric file systems (with bad performance for large
+// directories) too slow. It could be made configurable, but hopefully there
+// will be no need to do that.
+const uint64_t k_max_cache_files_per_directory = 2000;
+
+// Minimum number of cache levels ($CCACHE_DIR/1/2/stored_file).
+const uint8_t k_min_cache_levels = 2;
+
+// Maximum number of cache levels ($CCACHE_DIR/1/2/3/stored_file).
+//
+// On a cache miss, (k_max_cache_levels - k_min_cache_levels + 1) cache lookups
+// (i.e. stat system calls) will be performed for a cache entry.
+//
+// An assumption made here is that if a cache is so large that it holds more
+// than 16^4 * k_max_cache_files_per_directory files then we can assume that the
+// file system is sane enough to handle more than
+// k_max_cache_files_per_directory.
+const uint8_t k_max_cache_levels = 4;
+
+static std::string
+suffix_from_type(const core::CacheEntryType type)
+{
+  switch (type) {
+  case core::CacheEntryType::manifest:
+    return "M";
+
+  case core::CacheEntryType::result:
+    return "R";
+  }
+
+  ASSERT(false);
+}
+
+static uint8_t
+calculate_wanted_cache_level(const uint64_t files_in_level_1)
+{
+  uint64_t files_per_directory = files_in_level_1 / 16;
+  for (uint8_t i = k_min_cache_levels; i <= k_max_cache_levels; ++i) {
+    if (files_per_directory < k_max_cache_files_per_directory) {
+      return i;
+    }
+    files_per_directory /= 16;
+  }
+  return k_max_cache_levels;
+}
+
+PrimaryStorage::PrimaryStorage(const Config& config) : m_config(config)
+{
+}
+
+void
+PrimaryStorage::initialize()
+{
+  MTR_SCOPE("primary_storage", "clean_internal_tempdir");
+
+  if (m_config.temporary_dir() == m_config.cache_dir() + "/tmp") {
+    clean_internal_tempdir();
+  }
+}
+
+void
+PrimaryStorage::finalize()
+{
+  if (!m_config.stats()) {
+    return;
+  }
+
+  if (m_manifest_key) {
+    // A manifest entry was written.
+    ASSERT(!m_manifest_path.empty());
+    update_stats_and_maybe_move_cache_file(*m_manifest_key,
+                                           m_manifest_path,
+                                           m_manifest_counter_updates,
+                                           core::CacheEntryType::manifest);
+  }
+
+  if (!m_result_key) {
+    // No result entry was written, so we just choose one of the stats files in
+    // the 256 level 2 directories.
+
+    ASSERT(m_result_counter_updates.get(Statistic::cache_size_kibibyte) == 0);
+    ASSERT(m_result_counter_updates.get(Statistic::files_in_cache) == 0);
+
+    const auto bucket = getpid() % 256;
+    const auto stats_file =
+      FMT("{}/{:x}/{:x}/stats", m_config.cache_dir(), bucket / 16, bucket % 16);
+    StatsFile(stats_file).update([&](auto& cs) {
+      cs.increment(m_result_counter_updates);
+    });
+    return;
+  }
+
+  ASSERT(!m_result_path.empty());
+
+  const auto counters =
+    update_stats_and_maybe_move_cache_file(*m_result_key,
+                                           m_result_path,
+                                           m_result_counter_updates,
+                                           core::CacheEntryType::result);
+  if (!counters) {
+    return;
+  }
+
+  const auto subdir =
+    FMT("{}/{:x}", m_config.cache_dir(), m_result_key->bytes()[0] >> 4);
+  bool need_cleanup = false;
+
+  if (m_config.max_files() != 0
+      && counters->get(Statistic::files_in_cache) > m_config.max_files() / 16) {
+    LOG("Need to clean up {} since it holds {} files (limit: {} files)",
+        subdir,
+        counters->get(Statistic::files_in_cache),
+        m_config.max_files() / 16);
+    need_cleanup = true;
+  }
+  if (m_config.max_size() != 0
+      && counters->get(Statistic::cache_size_kibibyte)
+           > m_config.max_size() / 1024 / 16) {
+    LOG("Need to clean up {} since it holds {} KiB (limit: {} KiB)",
+        subdir,
+        counters->get(Statistic::cache_size_kibibyte),
+        m_config.max_size() / 1024 / 16);
+    need_cleanup = true;
+  }
+
+  if (need_cleanup) {
+    const double factor = m_config.limit_multiple() / 16;
+    const uint64_t max_size = round(m_config.max_size() * factor);
+    const uint32_t max_files = round(m_config.max_files() * factor);
+    const time_t max_age = 0;
+    clean_dir(subdir, max_size, max_files, max_age, [](double /*progress*/) {});
+  }
+}
+
+nonstd::optional<std::string>
+PrimaryStorage::get(const Digest& key, const core::CacheEntryType type) const
+{
+  MTR_SCOPE("primary_storage", "get");
+
+  const auto cache_file = look_up_cache_file(key, type);
+  if (!cache_file.stat) {
+    LOG("No {} in primary storage", key.to_string());
+    return nonstd::nullopt;
+  }
+
+  LOG(
+    "Retrieved {} from primary storage ({})", key.to_string(), cache_file.path);
+
+  // Update modification timestamp to save file from LRU cleanup.
+  Util::update_mtime(cache_file.path);
+  return cache_file.path;
+}
+
+nonstd::optional<std::string>
+PrimaryStorage::put(const Digest& key,
+                    const core::CacheEntryType type,
+                    const storage::EntryWriter& entry_writer)
+{
+  MTR_SCOPE("primary_storage", "put");
+
+  const auto cache_file = look_up_cache_file(key, type);
+  switch (type) {
+  case core::CacheEntryType::manifest:
+    m_manifest_key = key;
+    m_manifest_path = cache_file.path;
+    break;
+
+  case core::CacheEntryType::result:
+    m_result_key = key;
+    m_result_path = cache_file.path;
+    break;
+  }
+
+  if (!entry_writer(cache_file.path)) {
+    LOG("Did not store {} in primary storage", key.to_string());
+    return nonstd::nullopt;
+  }
+
+  const auto new_stat = Stat::stat(cache_file.path, Stat::OnError::log);
+  if (!new_stat) {
+    LOG("Failed to stat {}: {}", cache_file.path, strerror(errno));
+    return nonstd::nullopt;
+  }
+
+  LOG("Stored {} in primary storage ({})", key.to_string(), cache_file.path);
+
+  auto& counter_updates = (type == core::CacheEntryType::manifest)
+                            ? m_manifest_counter_updates
+                            : m_result_counter_updates;
+  counter_updates.increment(
+    Statistic::cache_size_kibibyte,
+    Util::size_change_kibibyte(cache_file.stat, new_stat));
+  counter_updates.increment(Statistic::files_in_cache, cache_file.stat ? 0 : 1);
+
+  // Make sure we have a CACHEDIR.TAG in the cache part of cache_dir. This can
+  // be done almost anywhere, but we might as well do it near the end as we save
+  // the stat call if we exit early.
+  util::create_cachedir_tag(
+    FMT("{}/{}", m_config.cache_dir(), key.to_string()[0]));
+
+  return cache_file.path;
+}
+
+void
+PrimaryStorage::remove(const Digest& key, const core::CacheEntryType type)
+{
+  MTR_SCOPE("primary_storage", "remove");
+
+  const auto cache_file = look_up_cache_file(key, type);
+  if (cache_file.stat) {
+    Util::unlink_safe(cache_file.path);
+    LOG(
+      "Removed {} from primary storage ({})", key.to_string(), cache_file.path);
+  } else {
+    LOG("No {} to remove from primary storage", key.to_string());
+  }
+}
+
+void
+PrimaryStorage::increment_statistic(const Statistic statistic,
+                                    const int64_t value)
+{
+  m_result_counter_updates.increment(statistic, value);
+}
+
+void
+PrimaryStorage::increment_statistics(const core::StatisticsCounters& statistics)
+{
+  m_result_counter_updates.increment(statistics);
+}
+
+// Private methods
+
+PrimaryStorage::LookUpCacheFileResult
+PrimaryStorage::look_up_cache_file(const Digest& key,
+                                   const core::CacheEntryType type) const
+{
+  const auto key_string = FMT("{}{}", key.to_string(), suffix_from_type(type));
+
+  for (uint8_t level = k_min_cache_levels; level <= k_max_cache_levels;
+       ++level) {
+    const auto path = get_path_in_cache(level, key_string);
+    const auto stat = Stat::stat(path);
+    if (stat) {
+      return {path, stat, level};
+    }
+  }
+
+  const auto shallowest_path =
+    get_path_in_cache(k_min_cache_levels, key_string);
+  return {shallowest_path, Stat(), k_min_cache_levels};
+}
+
+void
+PrimaryStorage::clean_internal_tempdir()
+{
+  const time_t now = time(nullptr);
+  const auto dir_st = Stat::stat(m_config.cache_dir(), Stat::OnError::log);
+  if (!dir_st || dir_st.mtime() + k_tempdir_cleanup_interval >= now) {
+    // No cleanup needed.
+    return;
+  }
+
+  Util::update_mtime(m_config.cache_dir());
+
+  const std::string& temp_dir = m_config.temporary_dir();
+  if (!Stat::lstat(temp_dir)) {
+    return;
+  }
+
+  Util::traverse(temp_dir, [now](const std::string& path, bool is_dir) {
+    if (is_dir) {
+      return;
+    }
+    const auto st = Stat::lstat(path, Stat::OnError::log);
+    if (st && st.mtime() + k_tempdir_cleanup_interval < now) {
+      Util::unlink_tmp(path);
+    }
+  });
+}
+
+nonstd::optional<core::StatisticsCounters>
+PrimaryStorage::update_stats_and_maybe_move_cache_file(
+  const Digest& key,
+  const std::string& current_path,
+  const core::StatisticsCounters& counter_updates,
+  const core::CacheEntryType type)
+{
+  if (counter_updates.all_zero()) {
+    return nonstd::nullopt;
+  }
+
+  // Use stats file in the level one subdirectory for cache bookkeeping counters
+  // since cleanup is performed on level one. Use stats file in the level two
+  // subdirectory for other counters to reduce lock contention.
+  const bool use_stats_on_level_1 =
+    counter_updates.get(Statistic::cache_size_kibibyte) != 0
+    || counter_updates.get(Statistic::files_in_cache) != 0;
+  std::string level_string = FMT("{:x}", key.bytes()[0] >> 4);
+  if (!use_stats_on_level_1) {
+    level_string += FMT("/{:x}", key.bytes()[0] & 0xF);
+  }
+
+  const auto stats_file =
+    FMT("{}/{}/stats", m_config.cache_dir(), level_string);
+  const auto counters =
+    StatsFile(stats_file).update([&counter_updates](auto& cs) {
+      cs.increment(counter_updates);
+    });
+  if (!counters) {
+    return nonstd::nullopt;
+  }
+
+  if (use_stats_on_level_1) {
+    // Only consider moving the cache file to another level when we have read
+    // the level 1 stats file since it's only then we know the proper
+    // files_in_cache value.
+    const auto wanted_level =
+      calculate_wanted_cache_level(counters->get(Statistic::files_in_cache));
+    const auto wanted_path =
+      get_path_in_cache(wanted_level, key.to_string() + suffix_from_type(type));
+    if (current_path != wanted_path) {
+      Util::ensure_dir_exists(Util::dir_name(wanted_path));
+      LOG("Moving {} to {}", current_path, wanted_path);
+      try {
+        Util::rename(current_path, wanted_path);
+      } catch (const core::Error&) {
+        // Two ccache processes may move the file at the same time, so failure
+        // to rename is OK.
+      }
+    }
+  }
+  return counters;
+}
+
+std::string
+PrimaryStorage::get_path_in_cache(const uint8_t level,
+                                  const nonstd::string_view name) const
+{
+  ASSERT(level >= 1 && level <= 8);
+  ASSERT(name.length() >= level);
+
+  std::string path(m_config.cache_dir());
+  path.reserve(path.size() + level * 2 + 1 + name.length() - level);
+
+  for (uint8_t i = 0; i < level; ++i) {
+    path.push_back('/');
+    path.push_back(name.at(i));
+  }
+
+  path.push_back('/');
+  const nonstd::string_view name_remaining = name.substr(level);
+  path.append(name_remaining.data(), name_remaining.length());
+
+  return path;
+}
+
+} // namespace primary
+} // namespace storage
diff --git a/src/storage/primary/PrimaryStorage.hpp b/src/storage/primary/PrimaryStorage.hpp
new file mode 100644 (file)
index 0000000..99f47a5
--- /dev/null
@@ -0,0 +1,153 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <Digest.hpp>
+#include <core/StatisticsCounters.hpp>
+#include <core/types.hpp>
+#include <storage/primary/util.hpp>
+#include <storage/types.hpp>
+
+#include <third_party/nonstd/optional.hpp>
+
+#include <cstdint>
+
+class Config;
+
+namespace storage {
+namespace primary {
+
+struct CompressionStatistics
+{
+  uint64_t compr_size;
+  uint64_t content_size;
+  uint64_t incompr_size;
+  uint64_t on_disk_size;
+};
+
+class PrimaryStorage
+{
+public:
+  PrimaryStorage(const Config& config);
+
+  void initialize();
+  void finalize();
+
+  // --- Cache entry handling ---
+
+  // Returns a path to a file containing the value.
+  nonstd::optional<std::string> get(const Digest& key,
+                                    core::CacheEntryType type) const;
+
+  nonstd::optional<std::string> put(const Digest& key,
+                                    core::CacheEntryType type,
+                                    const storage::EntryWriter& entry_writer);
+
+  void remove(const Digest& key, core::CacheEntryType type);
+
+  // --- Statistics ---
+
+  void increment_statistic(core::Statistic statistic, int64_t value = 1);
+  void increment_statistics(const core::StatisticsCounters& statistics);
+
+  const core::StatisticsCounters& get_statistics_updates() const;
+
+  // Zero all statistics counters except those tracking cache size and number of
+  // files in the cache.
+  void zero_all_statistics();
+
+  // Get statistics and last time of update for the whole primary storage cache.
+  std::pair<core::StatisticsCounters, time_t> get_all_statistics() const;
+
+  // --- Cleanup ---
+
+  void clean_old(const ProgressReceiver& progress_receiver, uint64_t max_age);
+
+  void clean_all(const ProgressReceiver& progress_receiver);
+
+  void wipe_all(const ProgressReceiver& progress_receiver);
+
+  // --- Compression ---
+
+  CompressionStatistics
+  get_compression_statistics(const ProgressReceiver& progress_receiver) const;
+
+  void recompress(nonstd::optional<int8_t> level,
+                  const ProgressReceiver& progress_receiver);
+
+private:
+  const Config& m_config;
+
+  // Main statistics updates (result statistics and size/count change for result
+  // file) which get written into the statistics file belonging to the result
+  // file.
+  core::StatisticsCounters m_result_counter_updates;
+
+  // Statistics updates (only for manifest size/count change) which get written
+  // into the statistics file belonging to the manifest.
+  core::StatisticsCounters m_manifest_counter_updates;
+
+  // The manifest and result keys and paths are stored by put() so that
+  // finalize() can use them to move the files in place.
+  nonstd::optional<Digest> m_manifest_key;
+  nonstd::optional<Digest> m_result_key;
+  std::string m_manifest_path;
+  std::string m_result_path;
+
+  struct LookUpCacheFileResult
+  {
+    std::string path;
+    Stat stat;
+    uint8_t level;
+  };
+
+  LookUpCacheFileResult look_up_cache_file(const Digest& key,
+                                           core::CacheEntryType type) const;
+
+  void clean_internal_tempdir();
+
+  nonstd::optional<core::StatisticsCounters>
+  update_stats_and_maybe_move_cache_file(
+    const Digest& key,
+    const std::string& current_path,
+    const core::StatisticsCounters& counter_updates,
+    core::CacheEntryType type);
+
+  // Join the cache directory, a '/' and `name` into a single path and return
+  // it. Additionally, `level` single-character, '/'-separated subpaths are
+  // split from the beginning of `name` before joining them all.
+  std::string get_path_in_cache(uint8_t level, nonstd::string_view name) const;
+
+  static void clean_dir(const std::string& subdir,
+                        uint64_t max_size,
+                        uint64_t max_files,
+                        uint64_t max_age,
+                        const ProgressReceiver& progress_receiver);
+};
+
+// --- Inline implementations ---
+
+inline const core::StatisticsCounters&
+PrimaryStorage::get_statistics_updates() const
+{
+  return m_result_counter_updates;
+}
+
+} // namespace primary
+} // namespace storage
diff --git a/src/storage/primary/PrimaryStorage_cleanup.cpp b/src/storage/primary/PrimaryStorage_cleanup.cpp
new file mode 100644 (file)
index 0000000..6f3790f
--- /dev/null
@@ -0,0 +1,237 @@
+// Copyright (C) 2002-2006 Andrew Tridgell
+// Copyright (C) 2009-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "PrimaryStorage.hpp"
+
+#include <Config.hpp>
+#include <Context.hpp>
+#include <Logging.hpp>
+#include <Util.hpp>
+#include <storage/primary/CacheFile.hpp>
+#include <storage/primary/StatsFile.hpp>
+#include <storage/primary/util.hpp>
+#include <util/string.hpp>
+
+#ifdef INODE_CACHE_SUPPORTED
+#  include <InodeCache.hpp>
+#endif
+
+#include <algorithm>
+
+using core::Statistic;
+
+namespace storage {
+namespace primary {
+
+static void
+delete_file(const std::string& path,
+            const uint64_t size,
+            uint64_t* cache_size,
+            uint64_t* files_in_cache)
+{
+  const bool deleted = Util::unlink_safe(path, Util::UnlinkLog::ignore_failure);
+  if (!deleted && errno != ENOENT && errno != ESTALE) {
+    LOG("Failed to unlink {} ({})", path, strerror(errno));
+  } else if (cache_size && files_in_cache) {
+    // The counters are intentionally subtracted even if there was no file to
+    // delete since the final cache size calculation will be incorrect if they
+    // aren't. (This can happen when there are several parallel ongoing
+    // cleanups of the same directory.)
+    *cache_size -= size;
+    --*files_in_cache;
+  }
+}
+
+static void
+update_counters(const std::string& dir,
+                const uint64_t files_in_cache,
+                const uint64_t cache_size,
+                const bool cleanup_performed)
+{
+  const std::string stats_file = dir + "/stats";
+  StatsFile(stats_file).update([=](auto& cs) {
+    if (cleanup_performed) {
+      cs.increment(Statistic::cleanups_performed);
+    }
+    cs.set(Statistic::files_in_cache, files_in_cache);
+    cs.set(Statistic::cache_size_kibibyte, cache_size / 1024);
+  });
+}
+
+void
+PrimaryStorage::clean_old(const ProgressReceiver& progress_receiver,
+                          const uint64_t max_age)
+{
+  for_each_level_1_subdir(
+    m_config.cache_dir(),
+    [&](const std::string& subdir,
+        const ProgressReceiver& sub_progress_receiver) {
+      clean_dir(subdir, 0, 0, max_age, sub_progress_receiver);
+    },
+    progress_receiver);
+}
+
+// Clean up one cache subdirectory.
+void
+PrimaryStorage::clean_dir(const std::string& subdir,
+                          const uint64_t max_size,
+                          const uint64_t max_files,
+                          const uint64_t max_age,
+                          const ProgressReceiver& progress_receiver)
+{
+  LOG("Cleaning up cache directory {}", subdir);
+
+  std::vector<CacheFile> files = get_level_1_files(
+    subdir, [&](double progress) { progress_receiver(progress / 3); });
+
+  uint64_t cache_size = 0;
+  uint64_t files_in_cache = 0;
+  time_t current_time = time(nullptr);
+
+  for (size_t i = 0; i < files.size();
+       ++i, progress_receiver(1.0 / 3 + 1.0 * i / files.size() / 3)) {
+    const auto& file = files[i];
+
+    if (!file.lstat().is_regular()) {
+      // Not a file or missing file.
+      continue;
+    }
+
+    // Delete any tmp files older than 1 hour right away.
+    if (file.lstat().mtime() + 3600 < current_time
+        && Util::base_name(file.path()).find(".tmp.") != std::string::npos) {
+      Util::unlink_tmp(file.path());
+      continue;
+    }
+
+    cache_size += file.lstat().size_on_disk();
+    files_in_cache += 1;
+  }
+
+  // Sort according to modification time, oldest first.
+  std::sort(files.begin(), files.end(), [](const auto& f1, const auto& f2) {
+    const auto ts_1 = f1.lstat().mtim();
+    const auto ts_2 = f2.lstat().mtim();
+    const auto ns_1 = 1'000'000'000ULL * ts_1.tv_sec + ts_1.tv_nsec;
+    const auto ns_2 = 1'000'000'000ULL * ts_2.tv_sec + ts_2.tv_nsec;
+    return ns_1 < ns_2;
+  });
+
+  LOG("Before cleanup: {:.0f} KiB, {:.0f} files",
+      static_cast<double>(cache_size) / 1024,
+      static_cast<double>(files_in_cache));
+
+  bool cleaned = false;
+  for (size_t i = 0; i < files.size();
+       ++i, progress_receiver(2.0 / 3 + 1.0 * i / files.size() / 3)) {
+    const auto& file = files[i];
+
+    if (!file.lstat() || file.lstat().is_directory()) {
+      continue;
+    }
+
+    if ((max_size == 0 || cache_size <= max_size)
+        && (max_files == 0 || files_in_cache <= max_files)
+        && (max_age == 0
+            || file.lstat().mtime()
+                 > (current_time - static_cast<int64_t>(max_age)))) {
+      break;
+    }
+
+    if (util::ends_with(file.path(), ".stderr")) {
+      // In order to be nice to legacy ccache versions, make sure that the .o
+      // file is deleted before .stderr, because if the ccache process gets
+      // killed after deleting the .stderr but before deleting the .o, the
+      // cached result will be inconsistent. (.stderr is the only file that is
+      // optional for legacy ccache versions; any other file missing from the
+      // cache will be detected.)
+      std::string o_file = file.path().substr(0, file.path().size() - 6) + "o";
+
+      // Don't subtract this extra deletion from the cache size; that
+      // bookkeeping will be done when the loop reaches the .o file. If the
+      // loop doesn't reach the .o file since the target limits have been
+      // reached, the bookkeeping won't happen, but that small counter
+      // discrepancy won't do much harm and it will correct itself in the next
+      // cleanup.
+      delete_file(o_file, 0, nullptr, nullptr);
+    }
+
+    delete_file(
+      file.path(), file.lstat().size_on_disk(), &cache_size, &files_in_cache);
+    cleaned = true;
+  }
+
+  LOG("After cleanup: {:.0f} KiB, {:.0f} files",
+      static_cast<double>(cache_size) / 1024,
+      static_cast<double>(files_in_cache));
+
+  if (cleaned) {
+    LOG("Cleaned up cache directory {}", subdir);
+  }
+
+  update_counters(subdir, files_in_cache, cache_size, cleaned);
+}
+
+// Clean up all cache subdirectories.
+void
+PrimaryStorage::clean_all(const ProgressReceiver& progress_receiver)
+{
+  for_each_level_1_subdir(
+    m_config.cache_dir(),
+    [&](const std::string& subdir,
+        const ProgressReceiver& sub_progress_receiver) {
+      clean_dir(subdir,
+                m_config.max_size() / 16,
+                m_config.max_files() / 16,
+                0,
+                sub_progress_receiver);
+    },
+    progress_receiver);
+}
+
+// Wipe one cache subdirectory.
+static void
+wipe_dir(const std::string& subdir, const ProgressReceiver& progress_receiver)
+{
+  LOG("Clearing out cache directory {}", subdir);
+
+  const std::vector<CacheFile> files = get_level_1_files(
+    subdir, [&](double progress) { progress_receiver(progress / 2); });
+
+  for (size_t i = 0; i < files.size(); ++i) {
+    Util::unlink_safe(files[i].path());
+    progress_receiver(0.5 + 0.5 * i / files.size());
+  }
+
+  const bool cleared = !files.empty();
+  if (cleared) {
+    LOG("Cleared out cache directory {}", subdir);
+  }
+  update_counters(subdir, 0, 0, cleared);
+}
+
+// Wipe all cached files in all subdirectories.
+void
+PrimaryStorage::wipe_all(const ProgressReceiver& progress_receiver)
+{
+  for_each_level_1_subdir(m_config.cache_dir(), wipe_dir, progress_receiver);
+}
+
+} // namespace primary
+} // namespace storage
diff --git a/src/storage/primary/PrimaryStorage_compress.cpp b/src/storage/primary/PrimaryStorage_compress.cpp
new file mode 100644 (file)
index 0000000..892a27b
--- /dev/null
@@ -0,0 +1,354 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "PrimaryStorage.hpp"
+
+#include <AtomicFile.hpp>
+#include <CacheEntryReader.hpp>
+#include <CacheEntryWriter.hpp>
+#include <Context.hpp>
+#include <File.hpp>
+#include <Logging.hpp>
+#include <Manifest.hpp>
+#include <Result.hpp>
+#include <ThreadPool.hpp>
+#include <assertions.hpp>
+#include <compression/ZstdCompressor.hpp>
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+#include <fmtmacros.hpp>
+#include <storage/primary/StatsFile.hpp>
+#include <util/string.hpp>
+
+#include <third_party/fmt/core.h>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
+#include <memory>
+#include <string>
+#include <thread>
+
+namespace storage {
+namespace primary {
+
+namespace {
+
+class RecompressionStatistics
+{
+public:
+  void update(uint64_t content_size,
+              uint64_t old_size,
+              uint64_t new_size,
+              uint64_t incompressible_size);
+  uint64_t content_size() const;
+  uint64_t old_size() const;
+  uint64_t new_size() const;
+  uint64_t incompressible_size() const;
+
+private:
+  mutable std::mutex m_mutex;
+  uint64_t m_content_size = 0;
+  uint64_t m_old_size = 0;
+  uint64_t m_new_size = 0;
+  uint64_t m_incompressible_size = 0;
+};
+
+void
+RecompressionStatistics::update(const uint64_t content_size,
+                                const uint64_t old_size,
+                                const uint64_t new_size,
+                                const uint64_t incompressible_size)
+{
+  std::unique_lock<std::mutex> lock(m_mutex);
+  m_incompressible_size += incompressible_size;
+  m_content_size += content_size;
+  m_old_size += old_size;
+  m_new_size += new_size;
+}
+
+uint64_t
+RecompressionStatistics::content_size() const
+{
+  std::unique_lock<std::mutex> lock(m_mutex);
+  return m_content_size;
+}
+
+uint64_t
+RecompressionStatistics::old_size() const
+{
+  std::unique_lock<std::mutex> lock(m_mutex);
+  return m_old_size;
+}
+
+uint64_t
+RecompressionStatistics::new_size() const
+{
+  std::unique_lock<std::mutex> lock(m_mutex);
+  return m_new_size;
+}
+
+uint64_t
+RecompressionStatistics::incompressible_size() const
+{
+  std::unique_lock<std::mutex> lock(m_mutex);
+  return m_incompressible_size;
+}
+
+} // namespace
+
+static File
+open_file(const std::string& path, const char* const mode)
+{
+  File f(path, mode);
+  if (!f) {
+    throw core::Error(
+      "failed to open {} for reading: {}", path, strerror(errno));
+  }
+  return f;
+}
+
+static std::unique_ptr<CacheEntryReader>
+create_reader(const CacheFile& cache_file, FILE* const stream)
+{
+  if (cache_file.type() == CacheFile::Type::unknown) {
+    throw core::Error("unknown file type for {}", cache_file.path());
+  }
+
+  switch (cache_file.type()) {
+  case CacheFile::Type::result:
+    return std::make_unique<CacheEntryReader>(
+      stream, Result::k_magic, Result::k_version);
+
+  case CacheFile::Type::manifest:
+    return std::make_unique<CacheEntryReader>(
+      stream, Manifest::k_magic, Manifest::k_version);
+
+  case CacheFile::Type::unknown:
+    ASSERT(false); // Handled at function entry.
+  }
+
+  ASSERT(false);
+}
+
+static std::unique_ptr<CacheEntryWriter>
+create_writer(FILE* const stream,
+              const CacheEntryReader& reader,
+              const compression::Type compression_type,
+              const int8_t compression_level)
+{
+  return std::make_unique<CacheEntryWriter>(stream,
+                                            reader.magic(),
+                                            reader.version(),
+                                            compression_type,
+                                            compression_level,
+                                            reader.payload_size());
+}
+
+static void
+recompress_file(RecompressionStatistics& statistics,
+                const std::string& stats_file,
+                const CacheFile& cache_file,
+                const nonstd::optional<int8_t> level)
+{
+  auto file = open_file(cache_file.path(), "rb");
+  auto reader = create_reader(cache_file, file.get());
+
+  const auto old_stat = Stat::stat(cache_file.path(), Stat::OnError::log);
+  const uint64_t content_size = reader->content_size();
+  const int8_t wanted_level =
+    level
+      ? (*level == 0 ? compression::ZstdCompressor::default_compression_level
+                     : *level)
+      : 0;
+
+  if (reader->compression_level() == wanted_level) {
+    statistics.update(content_size, old_stat.size(), old_stat.size(), 0);
+    return;
+  }
+
+  LOG("Recompressing {} to {}",
+      cache_file.path(),
+      level ? FMT("level {}", wanted_level) : "uncompressed");
+  AtomicFile atomic_new_file(cache_file.path(), AtomicFile::Mode::binary);
+  auto writer =
+    create_writer(atomic_new_file.stream(),
+                  *reader,
+                  level ? compression::Type::zstd : compression::Type::none,
+                  wanted_level);
+
+  char buffer[CCACHE_READ_BUFFER_SIZE];
+  size_t bytes_left = reader->payload_size();
+  while (bytes_left > 0) {
+    size_t bytes_to_read = std::min(bytes_left, sizeof(buffer));
+    reader->read(buffer, bytes_to_read);
+    writer->write(buffer, bytes_to_read);
+    bytes_left -= bytes_to_read;
+  }
+  reader->finalize();
+  writer->finalize();
+
+  file.close();
+
+  atomic_new_file.commit();
+  const auto new_stat = Stat::stat(cache_file.path(), Stat::OnError::log);
+
+  StatsFile(stats_file).update([=](auto& cs) {
+    cs.increment(core::Statistic::cache_size_kibibyte,
+                 Util::size_change_kibibyte(old_stat, new_stat));
+  });
+
+  statistics.update(content_size, old_stat.size(), new_stat.size(), 0);
+
+  LOG("Recompression of {} done", cache_file.path());
+}
+
+CompressionStatistics
+PrimaryStorage::get_compression_statistics(
+  const ProgressReceiver& progress_receiver) const
+{
+  CompressionStatistics cs{};
+
+  for_each_level_1_subdir(
+    m_config.cache_dir(),
+    [&](const auto& subdir, const auto& sub_progress_receiver) {
+      const std::vector<CacheFile> files = get_level_1_files(
+        subdir, [&](double progress) { sub_progress_receiver(progress / 2); });
+
+      for (size_t i = 0; i < files.size(); ++i) {
+        const auto& cache_file = files[i];
+        cs.on_disk_size += cache_file.lstat().size_on_disk();
+
+        try {
+          auto file = open_file(cache_file.path(), "rb");
+          auto reader = create_reader(cache_file, file.get());
+          cs.compr_size += cache_file.lstat().size();
+          cs.content_size += reader->content_size();
+        } catch (core::Error&) {
+          cs.incompr_size += cache_file.lstat().size();
+        }
+
+        sub_progress_receiver(1.0 / 2 + 1.0 * i / files.size() / 2);
+      }
+    },
+    progress_receiver);
+
+  return cs;
+}
+
+void
+PrimaryStorage::recompress(const nonstd::optional<int8_t> level,
+                           const ProgressReceiver& progress_receiver)
+{
+  const size_t threads = std::thread::hardware_concurrency();
+  const size_t read_ahead = 2 * threads;
+  ThreadPool thread_pool(threads, read_ahead);
+  RecompressionStatistics statistics;
+
+  for_each_level_1_subdir(
+    m_config.cache_dir(),
+    [&](const auto& subdir, const auto& sub_progress_receiver) {
+      std::vector<CacheFile> files =
+        get_level_1_files(subdir, [&](double progress) {
+          sub_progress_receiver(0.1 * progress);
+        });
+
+      auto stats_file = subdir + "/stats";
+
+      for (size_t i = 0; i < files.size(); ++i) {
+        const auto& file = files[i];
+
+        if (file.type() != CacheFile::Type::unknown) {
+          thread_pool.enqueue([&statistics, stats_file, file, level] {
+            try {
+              recompress_file(statistics, stats_file, file, level);
+            } catch (core::Error&) {
+              // Ignore for now.
+            }
+          });
+        } else {
+          statistics.update(0, 0, 0, file.lstat().size());
+        }
+
+        sub_progress_receiver(0.1 + 0.9 * i / files.size());
+      }
+
+      if (util::ends_with(subdir, "f")) {
+        // Wait here instead of after for_each_level_1_subdir to avoid
+        // updating the progress bar to 100% before all work is done.
+        thread_pool.shut_down();
+      }
+    },
+    progress_receiver);
+
+  if (isatty(STDOUT_FILENO)) {
+    PRINT_RAW(stdout, "\n\n");
+  }
+
+  const double old_ratio =
+    statistics.old_size() > 0
+      ? static_cast<double>(statistics.content_size()) / statistics.old_size()
+      : 0.0;
+  const double old_savings =
+    old_ratio > 0.0 ? 100.0 - (100.0 / old_ratio) : 0.0;
+  const double new_ratio =
+    statistics.new_size() > 0
+      ? static_cast<double>(statistics.content_size()) / statistics.new_size()
+      : 0.0;
+  const double new_savings =
+    new_ratio > 0.0 ? 100.0 - (100.0 / new_ratio) : 0.0;
+  const int64_t size_difference = static_cast<int64_t>(statistics.new_size())
+                                  - static_cast<int64_t>(statistics.old_size());
+
+  const std::string old_compr_size_str =
+    Util::format_human_readable_size(statistics.old_size());
+  const std::string new_compr_size_str =
+    Util::format_human_readable_size(statistics.new_size());
+  const std::string content_size_str =
+    Util::format_human_readable_size(statistics.content_size());
+  const std::string incompr_size_str =
+    Util::format_human_readable_size(statistics.incompressible_size());
+  const std::string size_difference_str =
+    FMT("{}{}",
+        size_difference < 0 ? "-" : (size_difference > 0 ? "+" : " "),
+        Util::format_human_readable_size(
+          size_difference < 0 ? -size_difference : size_difference));
+
+  PRINT(stdout, "Original data:         {:>8s}\n", content_size_str);
+  PRINT(stdout,
+        "Old compressed data:   {:>8s} ({:.1f}% of original size)\n",
+        old_compr_size_str,
+        100.0 - old_savings);
+  PRINT(stdout,
+        "  - Compression ratio: {:>5.3f} x  ({:.1f}% space savings)\n",
+        old_ratio,
+        old_savings);
+  PRINT(stdout,
+        "New compressed data:   {:>8s} ({:.1f}% of original size)\n",
+        new_compr_size_str,
+        100.0 - new_savings);
+  PRINT(stdout,
+        "  - Compression ratio: {:>5.3f} x  ({:.1f}% space savings)\n",
+        new_ratio,
+        new_savings);
+  PRINT(stdout, "Size change:          {:>9s}\n", size_difference_str);
+}
+
+} // namespace primary
+} // namespace storage
diff --git a/src/storage/primary/PrimaryStorage_statistics.cpp b/src/storage/primary/PrimaryStorage_statistics.cpp
new file mode 100644 (file)
index 0000000..99cc4fc
--- /dev/null
@@ -0,0 +1,86 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "PrimaryStorage.hpp"
+
+#include <Config.hpp>
+#include <core/Statistics.hpp>
+#include <fmtmacros.hpp>
+#include <storage/primary/StatsFile.hpp>
+
+#include <algorithm>
+
+namespace storage {
+namespace primary {
+
+static void
+for_each_level_1_and_2_stats_file(
+  const std::string& cache_dir,
+  const std::function<void(const std::string& path)> function)
+{
+  for (size_t level_1 = 0; level_1 <= 0xF; ++level_1) {
+    function(FMT("{}/{:x}/stats", cache_dir, level_1));
+    for (size_t level_2 = 0; level_2 <= 0xF; ++level_2) {
+      function(FMT("{}/{:x}/{:x}/stats", cache_dir, level_1, level_2));
+    }
+  }
+}
+
+// Zero all statistics counters except those tracking cache size and number of
+// files in the cache.
+void
+PrimaryStorage::zero_all_statistics()
+{
+  const time_t timestamp = time(nullptr);
+  const auto zeroable_fields = core::Statistics::get_zeroable_fields();
+
+  for_each_level_1_and_2_stats_file(
+    m_config.cache_dir(), [=](const std::string& path) {
+      StatsFile(path).update([=](auto& cs) {
+        for (const auto statistic : zeroable_fields) {
+          cs.set(statistic, 0);
+        }
+        cs.set(core::Statistic::stats_zeroed_timestamp, timestamp);
+      });
+    });
+}
+
+// Get statistics and last time of update for the whole primary storage cache.
+std::pair<core::StatisticsCounters, time_t>
+PrimaryStorage::get_all_statistics() const
+{
+  core::StatisticsCounters counters;
+  uint64_t zero_timestamp = 0;
+  time_t last_updated = 0;
+
+  // Add up the stats in each directory.
+  for_each_level_1_and_2_stats_file(
+    m_config.cache_dir(), [&](const auto& path) {
+      counters.set(core::Statistic::stats_zeroed_timestamp, 0); // Don't add
+      counters.increment(StatsFile(path).read());
+      zero_timestamp = std::max(
+        counters.get(core::Statistic::stats_zeroed_timestamp), zero_timestamp);
+      last_updated = std::max(last_updated, Stat::stat(path).mtime());
+    });
+
+  counters.set(core::Statistic::stats_zeroed_timestamp, zero_timestamp);
+  return std::make_pair(counters, last_updated);
+}
+
+} // namespace primary
+} // namespace storage
diff --git a/src/storage/primary/StatsFile.cpp b/src/storage/primary/StatsFile.cpp
new file mode 100644 (file)
index 0000000..87f0f88
--- /dev/null
@@ -0,0 +1,94 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "StatsFile.hpp"
+
+#include <AtomicFile.hpp>
+#include <Lockfile.hpp>
+#include <Logging.hpp>
+#include <Util.hpp>
+#include <core/exceptions.hpp>
+#include <fmtmacros.hpp>
+
+namespace storage {
+namespace primary {
+
+StatsFile::StatsFile(const std::string& path) : m_path(path)
+{
+}
+
+core::StatisticsCounters
+StatsFile::read() const
+{
+  core::StatisticsCounters counters;
+
+  std::string data;
+  try {
+    data = Util::read_file(m_path);
+  } catch (const core::Error&) {
+    // Ignore.
+    return counters;
+  }
+
+  size_t i = 0;
+  const char* str = data.c_str();
+  while (true) {
+    char* end;
+    const uint64_t value = std::strtoull(str, &end, 10);
+    if (end == str) {
+      break;
+    }
+    counters.set_raw(i, value);
+    ++i;
+    str = end;
+  }
+
+  return counters;
+}
+
+nonstd::optional<core::StatisticsCounters>
+StatsFile::update(
+  std::function<void(core::StatisticsCounters& counters)> function) const
+{
+  Lockfile lock(m_path);
+  if (!lock.acquired()) {
+    LOG("Failed to acquire lock for {}", m_path);
+    return nonstd::nullopt;
+  }
+
+  auto counters = read();
+  function(counters);
+
+  AtomicFile file(m_path, AtomicFile::Mode::text);
+  for (size_t i = 0; i < counters.size(); ++i) {
+    file.write(FMT("{}\n", counters.get_raw(i)));
+  }
+  try {
+    file.commit();
+  } catch (const core::Error& e) {
+    // Make failure to write a stats file a soft error since it's not important
+    // enough to fail whole the process and also because it is called in the
+    // Context destructor.
+    LOG("Error: {}", e.what());
+  }
+
+  return counters;
+}
+
+} // namespace primary
+} // namespace storage
diff --git a/src/storage/primary/StatsFile.hpp b/src/storage/primary/StatsFile.hpp
new file mode 100644 (file)
index 0000000..0b8b96d
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <core/StatisticsCounters.hpp>
+
+#include <third_party/nonstd/optional.hpp>
+
+#include <functional>
+#include <string>
+
+namespace storage {
+namespace primary {
+
+class StatsFile
+{
+public:
+  StatsFile(const std::string& path);
+
+  // Read counters. No lock is acquired. If the file doesn't exist all returned
+  // counters will be zero.
+  core::StatisticsCounters read() const;
+
+  // Acquire a lock, read counters, call `function` with the counters, write the
+  // counters and release the lock. Returns the resulting counters or nullopt on
+  // error (e.g. if the lock could not be acquired).
+  nonstd::optional<core::StatisticsCounters>
+    update(std::function<void(core::StatisticsCounters& counters)>) const;
+
+private:
+  const std::string m_path;
+};
+
+} // namespace primary
+} // namespace storage
diff --git a/src/storage/primary/util.cpp b/src/storage/primary/util.cpp
new file mode 100644 (file)
index 0000000..f50b4de
--- /dev/null
@@ -0,0 +1,75 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "util.hpp"
+
+#include <Util.hpp>
+#include <fmtmacros.hpp>
+
+namespace storage {
+namespace primary {
+
+void
+for_each_level_1_subdir(const std::string& cache_dir,
+                        const SubdirVisitor& visitor,
+                        const ProgressReceiver& progress_receiver)
+{
+  for (int i = 0; i <= 0xF; i++) {
+    double progress = 1.0 * i / 16;
+    progress_receiver(progress);
+    std::string subdir_path = FMT("{}/{:x}", cache_dir, i);
+    visitor(subdir_path, [&](double inner_progress) {
+      progress_receiver(progress + inner_progress / 16);
+    });
+  }
+  progress_receiver(1.0);
+}
+
+std::vector<CacheFile>
+get_level_1_files(const std::string& dir,
+                  const ProgressReceiver& progress_receiver)
+{
+  std::vector<CacheFile> files;
+
+  if (!Stat::stat(dir)) {
+    return files;
+  }
+
+  size_t level_2_directories = 0;
+
+  Util::traverse(dir, [&](const std::string& path, bool is_dir) {
+    auto name = Util::base_name(path);
+    if (name == "CACHEDIR.TAG" || name == "stats" || name.starts_with(".nfs")) {
+      return;
+    }
+
+    if (!is_dir) {
+      files.emplace_back(path);
+    } else if (path != dir
+               && path.find('/', dir.size() + 1) == std::string::npos) {
+      ++level_2_directories;
+      progress_receiver(level_2_directories / 16.0);
+    }
+  });
+
+  progress_receiver(1.0);
+  return files;
+}
+
+} // namespace primary
+} // namespace storage
diff --git a/src/storage/primary/util.hpp b/src/storage/primary/util.hpp
new file mode 100644 (file)
index 0000000..f4e1201
--- /dev/null
@@ -0,0 +1,64 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <storage/primary/CacheFile.hpp>
+
+#include <functional>
+#include <string>
+#include <vector>
+
+namespace storage {
+namespace primary {
+
+using ProgressReceiver = std::function<void(double progress)>;
+using SubdirVisitor = std::function<void(
+  const std::string& dir_path, const ProgressReceiver& progress_receiver)>;
+
+// Call a function for each subdir (0-9a-f) in the cache.
+//
+// Parameters:
+// - cache_dir: Path to the cache directory.
+// - visitor: Function to call with directory path and progress_receiver as
+//   arguments.
+// - progress_receiver: Function that will be called for progress updates.
+void for_each_level_1_subdir(const std::string& cache_dir,
+                             const SubdirVisitor& visitor,
+                             const ProgressReceiver& progress_receiver);
+
+// Get a list of files in a level 1 subdirectory of the cache.
+//
+// The function works under the assumption that directory entries with one
+// character names (except ".") are subdirectories and that there are no other
+// subdirectories.
+//
+// Files ignored:
+// - CACHEDIR.TAG
+// - stats
+// - .nfs* (temporary NFS files that may be left for open but deleted files).
+//
+// Parameters:
+// - dir: The directory to traverse recursively.
+// - progress_receiver: Function that will be called for progress updates.
+std::vector<CacheFile>
+get_level_1_files(const std::string& dir,
+                  const ProgressReceiver& progress_receiver);
+
+} // namespace primary
+} // namespace storage
diff --git a/src/storage/secondary/CMakeLists.txt b/src/storage/secondary/CMakeLists.txt
new file mode 100644 (file)
index 0000000..0ea7551
--- /dev/null
@@ -0,0 +1,12 @@
+set(
+  sources
+  ${CMAKE_CURRENT_SOURCE_DIR}/FileStorage.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/HttpStorage.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/SecondaryStorage.cpp
+)
+
+if(REDIS_STORAGE_BACKEND)
+  list(APPEND sources ${CMAKE_CURRENT_SOURCE_DIR}/RedisStorage.cpp)
+endif()
+
+target_sources(ccache_framework PRIVATE ${sources})
diff --git a/src/storage/secondary/FileStorage.cpp b/src/storage/secondary/FileStorage.cpp
new file mode 100644 (file)
index 0000000..7312c62
--- /dev/null
@@ -0,0 +1,194 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "FileStorage.hpp"
+
+#include <AtomicFile.hpp>
+#include <Digest.hpp>
+#include <Logging.hpp>
+#include <UmaskScope.hpp>
+#include <Util.hpp>
+#include <assertions.hpp>
+#include <core/exceptions.hpp>
+#include <fmtmacros.hpp>
+#include <util/expected.hpp>
+#include <util/file.hpp>
+#include <util/string.hpp>
+
+#include <third_party/nonstd/string_view.hpp>
+
+#include <sys/stat.h> // for mode_t
+
+namespace storage {
+namespace secondary {
+
+namespace {
+
+class FileStorageBackend : public SecondaryStorage::Backend
+{
+public:
+  FileStorageBackend(const Params& params);
+
+  nonstd::expected<nonstd::optional<std::string>, Failure>
+  get(const Digest& key) override;
+
+  nonstd::expected<bool, Failure> put(const Digest& key,
+                                      const std::string& value,
+                                      bool only_if_missing) override;
+
+  nonstd::expected<bool, Failure> remove(const Digest& key) override;
+
+private:
+  enum class Layout { flat, subdirs };
+
+  const std::string m_dir;
+  nonstd::optional<mode_t> m_umask;
+  bool m_update_mtime = false;
+  Layout m_layout = Layout::subdirs;
+
+  std::string get_entry_path(const Digest& key) const;
+};
+
+FileStorageBackend::FileStorageBackend(const Params& params)
+  : m_dir(params.url.path())
+{
+  ASSERT(params.url.scheme() == "file");
+  if (!params.url.host().empty()) {
+    throw core::Fatal(FMT(
+      "invalid file path \"{}\":  specifying a host (\"{}\") is not supported",
+      params.url.str(),
+      params.url.host()));
+  }
+
+  for (const auto& attr : params.attributes) {
+    if (attr.key == "layout") {
+      if (attr.value == "flat") {
+        m_layout = Layout::flat;
+      } else if (attr.value == "subdirs") {
+        m_layout = Layout::subdirs;
+      } else {
+        LOG("Unknown layout: {}", attr.value);
+      }
+    } else if (attr.key == "umask") {
+      m_umask =
+        util::value_or_throw<core::Fatal>(util::parse_umask(attr.value));
+    } else if (attr.key == "update-mtime") {
+      m_update_mtime = attr.value == "true";
+    } else if (!is_framework_attribute(attr.key)) {
+      LOG("Unknown attribute: {}", attr.key);
+    }
+  }
+}
+
+nonstd::expected<nonstd::optional<std::string>,
+                 SecondaryStorage::Backend::Failure>
+FileStorageBackend::get(const Digest& key)
+{
+  const auto path = get_entry_path(key);
+  const bool exists = Stat::stat(path);
+
+  if (!exists) {
+    // Don't log failure if the entry doesn't exist.
+    return nonstd::nullopt;
+  }
+
+  if (m_update_mtime) {
+    // Update modification timestamp for potential LRU cleanup by some external
+    // mechanism.
+    Util::update_mtime(path);
+  }
+
+  try {
+    LOG("Reading {}", path);
+    return Util::read_file(path);
+  } catch (const core::Error& e) {
+    LOG("Failed to read {}: {}", path, e.what());
+    return nonstd::make_unexpected(Failure::error);
+  }
+}
+
+nonstd::expected<bool, SecondaryStorage::Backend::Failure>
+FileStorageBackend::put(const Digest& key,
+                        const std::string& value,
+                        const bool only_if_missing)
+{
+  const auto path = get_entry_path(key);
+
+  if (only_if_missing && Stat::stat(path)) {
+    LOG("{} already in cache", path);
+    return false;
+  }
+
+  {
+    UmaskScope umask_scope(m_umask);
+
+    const auto dir = Util::dir_name(path);
+    if (!Util::create_dir(dir)) {
+      LOG("Failed to create directory {}: {}", dir, strerror(errno));
+      return nonstd::make_unexpected(Failure::error);
+    }
+
+    util::create_cachedir_tag(m_dir);
+
+    LOG("Writing {}", path);
+    try {
+      AtomicFile file(path, AtomicFile::Mode::binary);
+      file.write(value);
+      file.commit();
+      return true;
+    } catch (const core::Error& e) {
+      LOG("Failed to write {}: {}", path, e.what());
+      return nonstd::make_unexpected(Failure::error);
+    }
+  }
+}
+
+nonstd::expected<bool, SecondaryStorage::Backend::Failure>
+FileStorageBackend::remove(const Digest& key)
+{
+  return Util::unlink_safe(get_entry_path(key));
+}
+
+std::string
+FileStorageBackend::get_entry_path(const Digest& key) const
+{
+  switch (m_layout) {
+  case Layout::flat:
+    return FMT("{}/{}", m_dir, key.to_string());
+
+  case Layout::subdirs: {
+    const auto key_str = key.to_string();
+    const uint8_t digits = 2;
+    ASSERT(key_str.length() > digits);
+    return FMT("{}/{:.{}}/{}", m_dir, key_str, digits, &key_str[digits]);
+  }
+  }
+
+  ASSERT(false);
+}
+
+} // namespace
+
+std::unique_ptr<SecondaryStorage::Backend>
+FileStorage::create_backend(const Backend::Params& params) const
+{
+  return std::make_unique<FileStorageBackend>(params);
+}
+
+} // namespace secondary
+} // namespace storage
diff --git a/src/storage/secondary/FileStorage.hpp b/src/storage/secondary/FileStorage.hpp
new file mode 100644 (file)
index 0000000..771941f
--- /dev/null
@@ -0,0 +1,34 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <storage/secondary/SecondaryStorage.hpp>
+
+namespace storage {
+namespace secondary {
+
+class FileStorage : public SecondaryStorage
+{
+public:
+  std::unique_ptr<Backend>
+  create_backend(const Backend::Params& params) const override;
+};
+
+} // namespace secondary
+} // namespace storage
diff --git a/src/storage/secondary/HttpStorage.cpp b/src/storage/secondary/HttpStorage.cpp
new file mode 100644 (file)
index 0000000..6b3d002
--- /dev/null
@@ -0,0 +1,285 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "HttpStorage.hpp"
+
+#include <Digest.hpp>
+#include <Logging.hpp>
+#include <ccache.hpp>
+#include <core/exceptions.hpp>
+#include <fmtmacros.hpp>
+#include <util/expected.hpp>
+#include <util/string.hpp>
+
+#include <third_party/httplib.h>
+#include <third_party/nonstd/string_view.hpp>
+#include <third_party/url.hpp>
+
+namespace storage {
+namespace secondary {
+
+namespace {
+
+class HttpStorageBackend : public SecondaryStorage::Backend
+{
+public:
+  HttpStorageBackend(const Params& params);
+
+  nonstd::expected<nonstd::optional<std::string>, Failure>
+  get(const Digest& key) override;
+
+  nonstd::expected<bool, Failure> put(const Digest& key,
+                                      const std::string& value,
+                                      bool only_if_missing) override;
+
+  nonstd::expected<bool, Failure> remove(const Digest& key) override;
+
+private:
+  enum class Layout { bazel, flat, subdirs };
+
+  const std::string m_url_path;
+  httplib::Client m_http_client;
+  Layout m_layout = Layout::subdirs;
+
+  std::string get_entry_path(const Digest& key) const;
+};
+
+std::string
+get_url_path(const Url& url)
+{
+  auto path = url.path();
+  if (path.empty() || path.back() != '/') {
+    path += '/';
+  }
+  return path;
+}
+
+Url
+get_partial_url(const Url& from_url)
+{
+  Url url;
+  url.scheme(from_url.scheme());
+  url.host(from_url.host(), from_url.ip_version());
+  if (!from_url.port().empty()) {
+    url.port(from_url.port());
+  }
+  return url;
+}
+
+std::string
+get_url(const Url& url)
+{
+  if (url.host().empty()) {
+    throw core::Fatal("A host is required in HTTP storage URL \"{}\"",
+                      url.str());
+  }
+
+  // httplib requires a partial URL with just scheme, host and port.
+  return get_partial_url(url).str();
+}
+
+HttpStorageBackend::HttpStorageBackend(const Params& params)
+  : m_url_path(get_url_path(params.url)),
+    m_http_client(get_url(params.url))
+{
+  if (!params.url.user_info().empty()) {
+    const auto pair = util::split_once(params.url.user_info(), ':');
+    if (!pair.second) {
+      throw core::Fatal("Expected username:password in URL but got \"{}\"",
+                        params.url.user_info());
+    }
+    m_http_client.set_basic_auth(std::string(pair.first).c_str(),
+                                 std::string(*pair.second).c_str());
+  }
+
+  m_http_client.set_default_headers({
+    {"User-Agent", FMT("{}/{}", CCACHE_NAME, CCACHE_VERSION)},
+  });
+  m_http_client.set_keep_alive(true);
+
+  auto connect_timeout = k_default_connect_timeout;
+  auto operation_timeout = k_default_operation_timeout;
+
+  for (const auto& attr : params.attributes) {
+    if (attr.key == "connect-timeout") {
+      connect_timeout = parse_timeout_attribute(attr.value);
+    } else if (attr.key == "layout") {
+      if (attr.value == "bazel") {
+        m_layout = Layout::bazel;
+      } else if (attr.value == "flat") {
+        m_layout = Layout::flat;
+      } else if (attr.value == "subdirs") {
+        m_layout = Layout::subdirs;
+      } else {
+        LOG("Unknown layout: {}", attr.value);
+      }
+    } else if (attr.key == "operation-timeout") {
+      operation_timeout = parse_timeout_attribute(attr.value);
+    } else if (!is_framework_attribute(attr.key)) {
+      LOG("Unknown attribute: {}", attr.key);
+    }
+  }
+
+  m_http_client.set_connection_timeout(connect_timeout);
+  m_http_client.set_read_timeout(operation_timeout);
+  m_http_client.set_write_timeout(operation_timeout);
+}
+
+nonstd::expected<nonstd::optional<std::string>,
+                 SecondaryStorage::Backend::Failure>
+HttpStorageBackend::get(const Digest& key)
+{
+  const auto url_path = get_entry_path(key);
+  const auto result = m_http_client.Get(url_path.c_str());
+
+  if (result.error() != httplib::Error::Success || !result) {
+    LOG("Failed to get {} from http storage: {} ({})",
+        url_path,
+        to_string(result.error()),
+        result.error());
+    return nonstd::make_unexpected(Failure::error);
+  }
+
+  if (result->status < 200 || result->status >= 300) {
+    // Don't log failure if the entry doesn't exist.
+    return nonstd::nullopt;
+  }
+
+  return result->body;
+}
+
+nonstd::expected<bool, SecondaryStorage::Backend::Failure>
+HttpStorageBackend::put(const Digest& key,
+                        const std::string& value,
+                        const bool only_if_missing)
+{
+  const auto url_path = get_entry_path(key);
+
+  if (only_if_missing) {
+    const auto result = m_http_client.Head(url_path.c_str());
+
+    if (result.error() != httplib::Error::Success || !result) {
+      LOG("Failed to check for {} in http storage: {} ({})",
+          url_path,
+          to_string(result.error()),
+          result.error());
+      return nonstd::make_unexpected(Failure::error);
+    }
+
+    if (result->status >= 200 && result->status < 300) {
+      LOG("Found entry {} already within http storage: status code: {}",
+          url_path,
+          result->status);
+      return false;
+    }
+  }
+
+  static const auto content_type = "application/octet-stream";
+  const auto result = m_http_client.Put(
+    url_path.c_str(), value.data(), value.size(), content_type);
+
+  if (result.error() != httplib::Error::Success || !result) {
+    LOG("Failed to put {} to http storage: {} ({})",
+        url_path,
+        to_string(result.error()),
+        result.error());
+    return nonstd::make_unexpected(Failure::error);
+  }
+
+  if (result->status < 200 || result->status >= 300) {
+    LOG("Failed to put {} to http storage: status code: {}",
+        url_path,
+        result->status);
+    return nonstd::make_unexpected(Failure::error);
+  }
+
+  return true;
+}
+
+nonstd::expected<bool, SecondaryStorage::Backend::Failure>
+HttpStorageBackend::remove(const Digest& key)
+{
+  const auto url_path = get_entry_path(key);
+  const auto result = m_http_client.Delete(url_path.c_str());
+
+  if (result.error() != httplib::Error::Success || !result) {
+    LOG("Failed to delete {} from http storage: {} ({})",
+        url_path,
+        to_string(result.error()),
+        result.error());
+    return nonstd::make_unexpected(Failure::error);
+  }
+
+  if (result->status < 200 || result->status >= 300) {
+    LOG("Failed to delete {} from http storage: status code: {}",
+        url_path,
+        result->status);
+    return nonstd::make_unexpected(Failure::error);
+  }
+
+  return true;
+}
+
+std::string
+HttpStorageBackend::get_entry_path(const Digest& key) const
+{
+  switch (m_layout) {
+  case Layout::bazel: {
+    // Mimic hex representation of a SHA256 hash value.
+    const auto sha256_hex_size = 64;
+    static_assert(Digest::size() == 20, "Update below if digest size changes");
+    std::string hex_digits = Util::format_base16(key.bytes(), key.size());
+    hex_digits.append(hex_digits.data(), sha256_hex_size - hex_digits.size());
+    LOG("Translated key {} to Bazel layout ac/{}", key.to_string(), hex_digits);
+    return FMT("{}ac/{}", m_url_path, hex_digits);
+  }
+
+  case Layout::flat:
+    return m_url_path + key.to_string();
+
+  case Layout::subdirs: {
+    const auto key_str = key.to_string();
+    const uint8_t digits = 2;
+    ASSERT(key_str.length() > digits);
+    return FMT("{}/{:.{}}/{}", m_url_path, key_str, digits, &key_str[digits]);
+  }
+  }
+
+  ASSERT(false);
+}
+
+} // namespace
+
+std::unique_ptr<SecondaryStorage::Backend>
+HttpStorage::create_backend(const Backend::Params& params) const
+{
+  return std::make_unique<HttpStorageBackend>(params);
+}
+
+void
+HttpStorage::redact_secrets(Backend::Params& params) const
+{
+  auto& url = params.url;
+  const auto user_info = util::split_once(url.user_info(), ':');
+  if (user_info.second) {
+    url.user_info(FMT("{}:{}", user_info.first, k_redacted_password));
+  }
+}
+
+} // namespace secondary
+} // namespace storage
diff --git a/src/storage/secondary/HttpStorage.hpp b/src/storage/secondary/HttpStorage.hpp
new file mode 100644 (file)
index 0000000..60c1354
--- /dev/null
@@ -0,0 +1,36 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <storage/secondary/SecondaryStorage.hpp>
+
+namespace storage {
+namespace secondary {
+
+class HttpStorage : public SecondaryStorage
+{
+public:
+  std::unique_ptr<Backend>
+  create_backend(const Backend::Params& params) const override;
+
+  void redact_secrets(Backend::Params& params) const override;
+};
+
+} // namespace secondary
+} // namespace storage
diff --git a/src/storage/secondary/RedisStorage.cpp b/src/storage/secondary/RedisStorage.cpp
new file mode 100644 (file)
index 0000000..c08fc06
--- /dev/null
@@ -0,0 +1,344 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "RedisStorage.hpp"
+
+#include <Digest.hpp>
+#include <Logging.hpp>
+#include <core/exceptions.hpp>
+#include <fmtmacros.hpp>
+#include <util/expected.hpp>
+#include <util/string.hpp>
+
+// Ignore "ISO C++ forbids flexible array member ‘buf’" warning from -Wpedantic.
+#ifdef __GNUC__
+#  pragma GCC diagnostic push
+#  pragma GCC diagnostic ignored "-Wpedantic"
+#endif
+#ifdef _MSC_VER
+#  pragma warning(push)
+#  pragma warning(disable : 4200)
+#endif
+#include <hiredis/hiredis.h>
+#ifdef _MSC_VER
+#  pragma warning(pop)
+#endif
+#ifdef __GNUC__
+#  pragma GCC diagnostic pop
+#endif
+
+#include <cstdarg>
+#include <memory>
+
+namespace storage {
+namespace secondary {
+
+namespace {
+
+using RedisContext = std::unique_ptr<redisContext, decltype(&redisFree)>;
+using RedisReply = std::unique_ptr<redisReply, decltype(&freeReplyObject)>;
+
+const uint32_t DEFAULT_PORT = 6379;
+
+class RedisStorageBackend : public SecondaryStorage::Backend
+{
+public:
+  RedisStorageBackend(const SecondaryStorage::Backend::Params& params);
+
+  nonstd::expected<nonstd::optional<std::string>, Failure>
+  get(const Digest& key) override;
+
+  nonstd::expected<bool, Failure> put(const Digest& key,
+                                      const std::string& value,
+                                      bool only_if_missing) override;
+
+  nonstd::expected<bool, Failure> remove(const Digest& key) override;
+
+private:
+  const std::string m_prefix;
+  RedisContext m_context;
+
+  void
+  connect(const Url& url, uint32_t connect_timeout, uint32_t operation_timeout);
+  void select_database(const Url& url);
+  void authenticate(const Url& url);
+  nonstd::expected<RedisReply, Failure> redis_command(const char* format, ...);
+  std::string get_key_string(const Digest& digest) const;
+};
+
+timeval
+to_timeval(const uint32_t ms)
+{
+  timeval tv;
+  tv.tv_sec = ms / 1000;
+  tv.tv_usec = (ms % 1000) * 1000;
+  return tv;
+}
+
+std::pair<nonstd::optional<std::string>, nonstd::optional<std::string>>
+split_user_info(const std::string& user_info)
+{
+  const auto pair = util::split_once(user_info, ':');
+  if (pair.first.empty()) {
+    // redis://HOST
+    return {nonstd::nullopt, nonstd::nullopt};
+  } else if (pair.second) {
+    // redis://USERNAME:PASSWORD@HOST
+    return {std::string(*pair.second), std::string(pair.first)};
+  } else {
+    // redis://PASSWORD@HOST
+    return {std::string(pair.first), nonstd::nullopt};
+  }
+}
+
+RedisStorageBackend::RedisStorageBackend(const Params& params)
+  : m_prefix("ccache"), // TODO: attribute
+    m_context(nullptr, redisFree)
+{
+  const auto& url = params.url;
+  ASSERT(url.scheme() == "redis");
+
+  auto connect_timeout = k_default_connect_timeout;
+  auto operation_timeout = k_default_operation_timeout;
+
+  for (const auto& attr : params.attributes) {
+    if (attr.key == "connect-timeout") {
+      connect_timeout = parse_timeout_attribute(attr.value);
+    } else if (attr.key == "operation-timeout") {
+      operation_timeout = parse_timeout_attribute(attr.value);
+    } else if (!is_framework_attribute(attr.key)) {
+      LOG("Unknown attribute: {}", attr.key);
+    }
+  }
+
+  connect(url, connect_timeout.count(), operation_timeout.count());
+  select_database(url);
+  authenticate(url);
+}
+
+inline bool
+is_error(int err)
+{
+  return err != REDIS_OK;
+}
+
+inline bool
+is_timeout(int err)
+{
+#ifdef REDIS_ERR_TIMEOUT
+  // Only returned for hiredis version 1.0.0 and above
+  return err == REDIS_ERR_TIMEOUT;
+#else
+  (void)err;
+  return false;
+#endif
+}
+
+nonstd::expected<nonstd::optional<std::string>,
+                 SecondaryStorage::Backend::Failure>
+RedisStorageBackend::get(const Digest& key)
+{
+  const auto key_string = get_key_string(key);
+  LOG("Redis GET {}", key_string);
+  const auto reply = redis_command("GET %s", key_string.c_str());
+  if (!reply) {
+    return nonstd::make_unexpected(reply.error());
+  } else if ((*reply)->type == REDIS_REPLY_STRING) {
+    return std::string((*reply)->str, (*reply)->len);
+  } else if ((*reply)->type == REDIS_REPLY_NIL) {
+    return nonstd::nullopt;
+  } else {
+    LOG("Unknown reply type: {}", (*reply)->type);
+    return nonstd::make_unexpected(Failure::error);
+  }
+}
+
+nonstd::expected<bool, SecondaryStorage::Backend::Failure>
+RedisStorageBackend::put(const Digest& key,
+                         const std::string& value,
+                         bool only_if_missing)
+{
+  const auto key_string = get_key_string(key);
+
+  if (only_if_missing) {
+    LOG("Redis EXISTS {}", key_string);
+    const auto reply = redis_command("EXISTS %s", key_string.c_str());
+    if (!reply) {
+      return nonstd::make_unexpected(reply.error());
+    } else if ((*reply)->type == REDIS_REPLY_INTEGER && (*reply)->integer > 0) {
+      LOG("Entry {} already in Redis", key_string);
+      return false;
+    } else {
+      LOG("Unknown reply type: {}", (*reply)->type);
+    }
+  }
+
+  LOG("Redis SET {} [{} bytes]", key_string, value.size());
+  const auto reply =
+    redis_command("SET %s %b", key_string.c_str(), value.data(), value.size());
+  if (!reply) {
+    return nonstd::make_unexpected(reply.error());
+  } else if ((*reply)->type == REDIS_REPLY_STATUS) {
+    return true;
+  } else {
+    LOG("Unknown reply type: {}", (*reply)->type);
+    return nonstd::make_unexpected(Failure::error);
+  }
+}
+
+nonstd::expected<bool, SecondaryStorage::Backend::Failure>
+RedisStorageBackend::remove(const Digest& key)
+{
+  const auto key_string = get_key_string(key);
+  LOG("Redis DEL {}", key_string);
+  const auto reply = redis_command("DEL %s", key_string.c_str());
+  if (!reply) {
+    return nonstd::make_unexpected(reply.error());
+  } else if ((*reply)->type == REDIS_REPLY_INTEGER) {
+    return (*reply)->integer > 0;
+  } else {
+    LOG("Unknown reply type: {}", (*reply)->type);
+    return nonstd::make_unexpected(Failure::error);
+  }
+}
+
+void
+RedisStorageBackend::connect(const Url& url,
+                             const uint32_t connect_timeout,
+                             const uint32_t operation_timeout)
+{
+  const std::string host = url.host().empty() ? "localhost" : url.host();
+  const uint32_t port = url.port().empty()
+                          ? DEFAULT_PORT
+                          : util::value_or_throw<core::Fatal>(
+                            util::parse_unsigned(url.port(), 1, 65535, "port"));
+  ASSERT(url.path().empty() || url.path()[0] == '/');
+
+  LOG("Redis connecting to {}:{} (connect timeout {} ms)",
+      url.host(),
+      port,
+      connect_timeout);
+  m_context.reset(redisConnectWithTimeout(
+    url.host().c_str(), port, to_timeval(connect_timeout)));
+
+  if (!m_context) {
+    throw Failed("Redis context construction error");
+  }
+  if (is_timeout(m_context->err)) {
+    throw Failed(FMT("Redis connection timeout: {}", m_context->errstr),
+                 Failure::timeout);
+  }
+  if (is_error(m_context->err)) {
+    throw Failed(FMT("Redis connection error: {}", m_context->errstr));
+  }
+
+  LOG("Redis operation timeout set to {} ms", operation_timeout);
+  if (redisSetTimeout(m_context.get(), to_timeval(operation_timeout))
+      != REDIS_OK) {
+    throw Failed("Failed to set operation timeout");
+  }
+
+  LOG_RAW("Redis connection OK");
+}
+
+void
+RedisStorageBackend::select_database(const Url& url)
+{
+  const uint32_t db_number =
+    url.path().empty() ? 0
+                       : util::value_or_throw<core::Fatal>(util::parse_unsigned(
+                         url.path().substr(1),
+                         0,
+                         std::numeric_limits<uint32_t>::max(),
+                         "db number"));
+
+  if (db_number != 0) {
+    LOG("Redis SELECT {}", db_number);
+    const auto reply =
+      util::value_or_throw<Failed>(redis_command("SELECT %d", db_number));
+  }
+}
+
+void
+RedisStorageBackend::authenticate(const Url& url)
+{
+  const auto password_username_pair = split_user_info(url.user_info());
+  const auto& password = password_username_pair.first;
+  if (password) {
+    const auto& username = password_username_pair.second;
+    if (username) {
+      LOG("Redis AUTH {} {}", *username, k_redacted_password);
+      util::value_or_throw<Failed>(
+        redis_command("AUTH %s %s", username->c_str(), password->c_str()));
+    } else {
+      LOG("Redis AUTH {}", k_redacted_password);
+      util::value_or_throw<Failed>(redis_command("AUTH %s", password->c_str()));
+    }
+  }
+}
+
+nonstd::expected<RedisReply, SecondaryStorage::Backend::Failure>
+RedisStorageBackend::redis_command(const char* format, ...)
+{
+  va_list ap;
+  va_start(ap, format);
+  auto reply =
+    static_cast<redisReply*>(redisvCommand(m_context.get(), format, ap));
+  va_end(ap);
+  if (!reply) {
+    LOG("Redis command failed: {}", m_context->errstr);
+    return nonstd::make_unexpected(is_timeout(m_context->err) ? Failure::timeout
+                                                              : Failure::error);
+  } else if (reply->type == REDIS_REPLY_ERROR) {
+    LOG("Redis command failed: {}", reply->str);
+    return nonstd::make_unexpected(Failure::error);
+  } else {
+    return RedisReply(reply, freeReplyObject);
+  }
+}
+
+std::string
+RedisStorageBackend::get_key_string(const Digest& digest) const
+{
+  return FMT("{}:{}", m_prefix, digest.to_string());
+}
+
+} // namespace
+
+std::unique_ptr<SecondaryStorage::Backend>
+RedisStorage::create_backend(const Backend::Params& params) const
+{
+  return std::make_unique<RedisStorageBackend>(params);
+}
+
+void
+RedisStorage::redact_secrets(Backend::Params& params) const
+{
+  auto& url = params.url;
+  const auto user_info = util::split_once(url.user_info(), ':');
+  if (user_info.second) {
+    // redis://username:password@host
+    url.user_info(FMT("{}:{}", user_info.first, k_redacted_password));
+  } else if (!user_info.first.empty()) {
+    // redis://password@host
+    url.user_info(k_redacted_password);
+  }
+}
+
+} // namespace secondary
+} // namespace storage
diff --git a/src/storage/secondary/RedisStorage.hpp b/src/storage/secondary/RedisStorage.hpp
new file mode 100644 (file)
index 0000000..98794fa
--- /dev/null
@@ -0,0 +1,36 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <storage/secondary/SecondaryStorage.hpp>
+
+namespace storage {
+namespace secondary {
+
+class RedisStorage : public SecondaryStorage
+{
+public:
+  std::unique_ptr<Backend>
+  create_backend(const Backend::Params& params) const override;
+
+  void redact_secrets(Backend::Params& params) const override;
+};
+
+} // namespace secondary
+} // namespace storage
diff --git a/src/storage/secondary/SecondaryStorage.cpp b/src/storage/secondary/SecondaryStorage.cpp
new file mode 100644 (file)
index 0000000..c73f19d
--- /dev/null
@@ -0,0 +1,41 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "SecondaryStorage.hpp"
+
+#include <util/expected.hpp>
+#include <util/string.hpp>
+
+namespace storage {
+namespace secondary {
+
+bool
+SecondaryStorage::Backend::is_framework_attribute(const std::string& name)
+{
+  return name == "read-only" || name == "shards" || name == "share-hits";
+}
+
+std::chrono::milliseconds
+SecondaryStorage::Backend::parse_timeout_attribute(const std::string& value)
+{
+  return std::chrono::milliseconds(util::value_or_throw<Failed>(
+    util::parse_unsigned(value, 1, 60 * 1000, "timeout")));
+}
+
+} // namespace secondary
+} // namespace storage
diff --git a/src/storage/secondary/SecondaryStorage.hpp b/src/storage/secondary/SecondaryStorage.hpp
new file mode 100644 (file)
index 0000000..628a9a7
--- /dev/null
@@ -0,0 +1,147 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <storage/types.hpp>
+
+#include <third_party/nonstd/expected.hpp>
+#include <third_party/nonstd/optional.hpp>
+#include <third_party/url.hpp>
+
+#include <chrono>
+#include <memory>
+#include <string>
+#include <vector>
+
+class Digest;
+
+namespace storage {
+namespace secondary {
+
+constexpr auto k_redacted_password = "********";
+const auto k_default_connect_timeout = std::chrono::milliseconds{100};
+const auto k_default_operation_timeout = std::chrono::milliseconds{10000};
+
+// This class defines the API that a secondary storage must implement.
+class SecondaryStorage
+{
+public:
+  class Backend
+  {
+  public:
+    struct Attribute
+    {
+      std::string key;       // Key part.
+      std::string value;     // Value part, percent-decoded.
+      std::string raw_value; // Value part, not percent-decoded.
+    };
+
+    struct Params
+    {
+      Url url;
+      std::vector<Attribute> attributes;
+    };
+
+    enum class Failure {
+      error,   // Operation error, e.g. bad parameters or failed connection.
+      timeout, // Timeout, e.g. due to slow network or server.
+    };
+
+    class Failed : public std::runtime_error
+    {
+    public:
+      Failed(Failure failure);
+      Failed(const std::string& message, Failure failure = Failure::error);
+
+      Failure failure() const;
+
+    private:
+      Failure m_failure;
+    };
+
+    virtual ~Backend() = default;
+
+    // Get the value associated with `key`. Returns the value on success or
+    // nonstd::nullopt if the entry is not present.
+    virtual nonstd::expected<nonstd::optional<std::string>, Failure>
+    get(const Digest& key) = 0;
+
+    // Put `value` associated to `key` in the storage. A true `only_if_missing`
+    // is a hint that the value does not have to be set if already present.
+    // Returns true if the entry was stored, otherwise false.
+    virtual nonstd::expected<bool, Failure>
+    put(const Digest& key,
+        const std::string& value,
+        bool only_if_missing = false) = 0;
+
+    // Remove `key` and its associated value. Returns true if the entry was
+    // removed, otherwise false.
+    virtual nonstd::expected<bool, Failure> remove(const Digest& key) = 0;
+
+    // Determine whether an attribute is handled by the secondary storage
+    // framework itself.
+    static bool is_framework_attribute(const std::string& name);
+
+    // Parse a timeout `value`, throwing `Failed` on error.
+    static std::chrono::milliseconds
+    parse_timeout_attribute(const std::string& value);
+  };
+
+  virtual ~SecondaryStorage() = default;
+
+  // Create an instance of the backend. The instance is created just before the
+  // first call to a backend method, so the backend constructor can open a
+  // connection or similar right away if wanted. The method should throw
+  // `core::Fatal` on fatal configuration error or `Backend::Failed` on
+  // connection error or timeout.
+  virtual std::unique_ptr<Backend>
+  create_backend(const Backend::Params& parameters) const = 0;
+
+  // Redact secrets in backend parameters, if any.
+  virtual void redact_secrets(Backend::Params& parameters) const;
+};
+
+// --- Inline implementations ---
+
+inline void
+SecondaryStorage::redact_secrets(
+  SecondaryStorage::Backend::Params& /*config*/) const
+{
+}
+
+inline SecondaryStorage::Backend::Failed::Failed(Failure failure)
+  : Failed("", failure)
+{
+}
+
+inline SecondaryStorage::Backend::Failed::Failed(const std::string& message,
+                                                 Failure failure)
+  : std::runtime_error::runtime_error(message),
+    m_failure(failure)
+{
+}
+
+inline SecondaryStorage::Backend::Failure
+SecondaryStorage::Backend::Failed::failure() const
+{
+  return m_failure;
+}
+
+} // namespace secondary
+} // namespace storage
diff --git a/src/storage/types.hpp b/src/storage/types.hpp
new file mode 100644 (file)
index 0000000..16e70cd
--- /dev/null
@@ -0,0 +1,28 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <functional>
+#include <string>
+
+namespace storage {
+
+using EntryWriter = std::function<bool(const std::string& path)>;
+
+} // namespace storage
diff --git a/src/system.hpp b/src/system.hpp
deleted file mode 100644 (file)
index fa4bbf3..0000000
+++ /dev/null
@@ -1,215 +0,0 @@
-// Copyright (C) 2010-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#ifdef __MINGW32__
-#  define __USE_MINGW_ANSI_STDIO 1
-#  define __STDC_FORMAT_MACROS 1
-#endif
-
-#include "config.h"
-
-#ifdef HAVE_SYS_FILE_H
-#  include <sys/file.h>
-#endif
-
-#ifdef HAVE_SYS_MMAN_H
-#  include <sys/mman.h>
-#endif
-#include <sys/stat.h>
-#include <sys/types.h>
-#ifdef HAVE_SYS_WAIT_H
-#  include <sys/wait.h>
-#endif
-
-#include <cassert>
-#include <cctype>
-#include <cerrno>
-#include <cinttypes>
-#include <climits>
-#include <csignal>
-#include <cstdarg>
-#include <cstddef>
-#include <cstdint>
-#include <cstdio>
-#include <cstdlib>
-#include <cstring>
-#include <ctime>
-
-#ifdef HAVE_DIRENT_H
-#  include <dirent.h>
-#endif
-
-#include <fcntl.h>
-
-#ifdef HAVE_STRINGS_H
-#  include <strings.h>
-#endif
-
-#ifdef HAVE_UNISTD_H
-#  include <unistd.h>
-#endif
-
-#ifdef HAVE_UTIME_H
-#  include <utime.h>
-#elif defined(HAVE_SYS_UTIME_H)
-#  include <sys/utime.h>
-#endif
-
-#ifdef HAVE_VARARGS_H
-#  include <varargs.h>
-#endif
-
-// AIX/PASE does not properly define usleep within its headers. However, the
-// function is available in libc.a. This extern define ensures that it is
-// usable within the ccache code base.
-#ifdef _AIX
-extern "C" int usleep(useconds_t);
-#endif
-
-#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
-
-// Buffer size for I/O operations. Should be a multiple of 4 KiB.
-const size_t READ_BUFFER_SIZE = 65536;
-
-#ifndef ESTALE
-#  define ESTALE -1
-#endif
-
-#ifdef _WIN32
-#  ifndef _WIN32_WINNT
-// _WIN32_WINNT is set in the generated header config.h
-#    error _WIN32_WINNT is undefined
-#  endif
-
-#  ifdef _MSC_VER
-typedef int mode_t;
-typedef int pid_t;
-#  endif
-
-#  ifndef __MINGW32__
-typedef int64_t ssize_t;
-#  endif
-
-// Defined in Win32Util.cpp
-void usleep(int64_t usec);
-struct tm* localtime_r(time_t* _clock, struct tm* _result);
-
-#  ifdef _MSC_VER
-int gettimeofday(struct timeval* tp, struct timezone* tzp);
-int asprintf(char** strp, const char* fmt, ...);
-#  endif
-
-// From:
-// http://mesos.apache.org/api/latest/c++/3rdparty_2stout_2include_2stout_2windows_8hpp_source.html
-#  ifdef _MSC_VER
-const mode_t S_IRUSR = mode_t(_S_IREAD);
-const mode_t S_IWUSR = mode_t(_S_IWRITE);
-#  endif
-
-#  ifndef S_IFIFO
-#    define S_IFIFO 0x1000
-#  endif
-
-#  ifndef S_IFBLK
-#    define S_IFBLK 0x6000
-#  endif
-
-#  ifndef S_IFLNK
-#    define S_IFLNK 0xA000
-#  endif
-
-#  ifndef S_ISREG
-#    define S_ISREG(m) (((m)&S_IFMT) == S_IFREG)
-#  endif
-#  ifndef S_ISDIR
-#    define S_ISDIR(m) (((m)&S_IFMT) == S_IFDIR)
-#  endif
-#  ifndef S_ISFIFO
-#    define S_ISFIFO(m) (((m)&S_IFMT) == S_IFIFO)
-#  endif
-#  ifndef S_ISCHR
-#    define S_ISCHR(m) (((m)&S_IFMT) == S_IFCHR)
-#  endif
-#  ifndef S_ISLNK
-#    define S_ISLNK(m) (((m)&S_IFMT) == S_IFLNK)
-#  endif
-#  ifndef S_ISBLK
-#    define S_ISBLK(m) (((m)&S_IFMT) == S_IFBLK)
-#  endif
-
-#  include <direct.h>
-#  include <io.h>
-#  include <process.h>
-#  define NOMINMAX 1
-#  define WIN32_NO_STATUS
-#  include <windows.h>
-#  undef WIN32_NO_STATUS
-#  include <ntstatus.h>
-#  define mkdir(a, b) _mkdir(a)
-#  define execv(a, b)                                                          \
-    do_not_call_execv_on_windows // to protect against incidental use of MinGW
-                                 // execv
-#  define strncasecmp _strnicmp
-#  define strcasecmp _stricmp
-
-#  ifdef _MSC_VER
-#    define PATH_MAX MAX_PATH
-#  endif
-
-#  ifdef _MSC_VER
-#    define DLLIMPORT __declspec(dllimport)
-#  else
-#    define DLLIMPORT
-#  endif
-
-#  define STDIN_FILENO 0
-#  define STDOUT_FILENO 1
-#  define STDERR_FILENO 2
-#  define DIR_DELIM_CH '\\'
-#  define PATH_DELIM ";"
-#else
-#  define DLLIMPORT
-#  define DIR_DELIM_CH '/'
-#  define PATH_DELIM ":"
-#endif
-
-DLLIMPORT extern char** environ;
-
-// Work with silly DOS binary open.
-#ifndef O_BINARY
-#  define O_BINARY 0
-#endif
-
-#if defined(HAVE_SYS_MMAN_H) && defined(HAVE_PTHREAD_MUTEXATTR_SETPSHARED)
-#  define INODE_CACHE_SUPPORTED
-#endif
-
-// Workaround for missing std::is_trivially_copyable in GCC < 5.
-#if __GNUG__ && __GNUC__ < 5
-#  define IS_TRIVIALLY_COPYABLE(T) __has_trivial_copy(T)
-#else
-#  define IS_TRIVIALLY_COPYABLE(T) std::is_trivially_copyable<T>::value
-#endif
-
-// GCC version of a couple of standard C++ attributes
-#ifdef __GNUC__
-#  define nodiscard gnu::warn_unused_result
-#  define maybe_unused gnu::unused
-#endif
index ed0ff9e84801d6c4117b97d79295047f971ebb03..fe43d594b987b6f93002258dfc4800f2f6357594 100644 (file)
@@ -1,17 +1,17 @@
-add_library(third_party_lib STATIC base32hex.c format.cpp xxhash.c)
+add_library(third_party STATIC base32hex.c format.cpp httplib.cpp url.cpp xxhash.c)
 if(NOT MSVC)
-  target_sources(third_party_lib PRIVATE getopt_long.c)
+  target_sources(third_party PRIVATE getopt_long.c)
 else()
-  target_sources(third_party_lib PRIVATE win32/getopt.c)
-  target_compile_definitions(third_party_lib PUBLIC -DSTATIC_GETOPT)
+  target_sources(third_party PRIVATE win32/getopt.c)
+  target_compile_definitions(third_party PUBLIC -DSTATIC_GETOPT)
 endif()
 
 if(WIN32)
-  target_sources(third_party_lib PRIVATE win32/mktemp.c)
+  target_sources(third_party PRIVATE win32/mktemp.c)
 endif ()
 
 if(ENABLE_TRACING)
-  target_sources(third_party_lib PRIVATE minitrace.c)
+  target_sources(third_party PRIVATE minitrace.c)
 endif()
 
 set(xxhdispatchtest [=[
@@ -31,47 +31,28 @@ try_compile(USE_XXH_DISPATCH ${CMAKE_CURRENT_BINARY_DIR}
   CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${CMAKE_CURRENT_SOURCE_DIR}"
   COMPILE_DEFINITIONS "-DXXH_STATIC_LINKING_ONLY")
 
-target_compile_definitions(third_party_lib INTERFACE "-DXXH_STATIC_LINKING_ONLY")
+target_compile_definitions(third_party INTERFACE "-DXXH_STATIC_LINKING_ONLY")
 if(USE_XXH_DISPATCH)
-  target_sources(third_party_lib PRIVATE xxh_x86dispatch.c)
-  target_compile_definitions(third_party_lib INTERFACE "-DUSE_XXH_DISPATCH")
+  target_sources(third_party PRIVATE xxh_x86dispatch.c)
+  target_compile_definitions(third_party INTERFACE "-DUSE_XXH_DISPATCH")
 endif()
 
 # Treat third party headers as system files (no warning for those headers).
 target_include_directories(
-  third_party_lib
-  PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} SYSTEM)
+  third_party
+  PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} SYSTEM
+)
 
-target_link_libraries(third_party_lib PRIVATE standard_settings)
-target_link_libraries(third_party_lib INTERFACE blake3)
+target_link_libraries(third_party PRIVATE standard_settings)
+target_link_libraries(third_party INTERFACE blake3)
 
-# These warnings are enabled by default even without e.g. -Wall, but we don't
-# want them in third_party.
-if(CMAKE_CXX_COMPILER_ID MATCHES "^GNU|Clang$")
-  target_compile_options(
-    third_party_lib
-    PRIVATE
-      $<$<COMPILE_LANGUAGE:C>:-Wno-implicit-function-declaration
-      -Wno-int-conversion>)
-endif()
-
-if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
-  target_compile_options(
-    third_party_lib
-    PRIVATE $<$<COMPILE_LANGUAGE:C>:-Wno-attributes>)
+if(WIN32)
+  target_link_libraries(third_party PRIVATE ws2_32)
 endif()
 
 # Silence warning from winbase.h due to /Zc:preprocessor.
 if(MSVC)
-  target_compile_options(
-    third_party_lib
-    PRIVATE /wd5105)
-endif()
-
-# The headers are included from the rest of the project, so turn off warnings as
-# required.
-if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
-  target_compile_options(third_party_lib INTERFACE -Wno-shadow)
+  target_compile_options(third_party PRIVATE /wd5105)
 endif()
 
 add_subdirectory(blake3)
index 581ee816e41d9a3eb64fe624468fb950f480cb66..14583773559c67a8a8ce56eb3509993409e31a4e 100644 (file)
@@ -17,17 +17,17 @@ function(add_source_if_enabled feature msvc_flags others_flags intrinsic)
     set(compile_flags "${others_flags}")
   endif()
 
+  if(MSVC)
+    set(suffix "_x86-64_windows_msvc.asm")
+  elseif(WIN32)
+    set(suffix "_x86-64_windows_gnu.S")
+  else()
+    set(suffix "_x86-64_unix.S")
+  endif()
+
   # First check if it's possible to use the assembler variant for the feature.
   string(TOUPPER "have_asm_${feature}" have_feature)
   if(NOT DEFINED "${have_feature}" AND CMAKE_SIZEOF_VOID_P EQUAL 8)
-    if(MSVC)
-      set(suffix "_x86-64_windows_msvc.asm")
-    elseif(WIN32)
-      set(suffix "_x86-64_windows_gnu.S")
-    else()
-      set(suffix "_x86-64_unix.S")
-    endif()
-
     if(NOT CMAKE_REQUIRED_QUIET)
       message(STATUS "Performing Test ${have_feature}")
     endif()
index 7abf5324ecd0538f60c05aeb03b0ebefaa1445cd..9998f75c79fad7c20fd8180693e53dd937c25339 100644 (file)
@@ -5,9 +5,7 @@
 #include "blake3.h"
 #include "blake3_impl.h"
 
-const char * blake3_version(void) {
-  return BLAKE3_VERSION_STRING;
-}
+const char *blake3_version(void) { return BLAKE3_VERSION_STRING; }
 
 INLINE void chunk_state_init(blake3_chunk_state *self, const uint32_t key[8],
                              uint8_t flags) {
index 57ebd5adc2c35109ab1a0556a562be471e4d8447..5eacf180140d0c062ab8e796891a800cf7065c6b 100644 (file)
@@ -8,13 +8,12 @@
 extern "C" {
 #endif
 
-#define BLAKE3_VERSION_STRING "0.3.7"
+#define BLAKE3_VERSION_STRING "1.0.0"
 #define BLAKE3_KEY_LEN 32
 #define BLAKE3_OUT_LEN 32
 #define BLAKE3_BLOCK_LEN 64
 #define BLAKE3_CHUNK_LEN 1024
 #define BLAKE3_MAX_DEPTH 54
-#define BLAKE3_MAX_SIMD_DEGREE 16
 
 // This struct is a private implementation detail. It has to be here because
 // it's part of blake3_hasher below.
@@ -39,12 +38,12 @@ typedef struct {
   uint8_t cv_stack[(BLAKE3_MAX_DEPTH + 1) * BLAKE3_OUT_LEN];
 } blake3_hasher;
 
-const char * blake3_version(void);
+const char *blake3_version(void);
 void blake3_hasher_init(blake3_hasher *self);
 void blake3_hasher_init_keyed(blake3_hasher *self,
                               const uint8_t key[BLAKE3_KEY_LEN]);
 void blake3_hasher_init_derive_key(blake3_hasher *self, const char *context);
-void blake3_hasher_init_derive_key_raw(blake3_hasher *self, const void *context, 
+void blake3_hasher_init_derive_key_raw(blake3_hasher *self, const void *context,
                                        size_t context_len);
 void blake3_hasher_update(blake3_hasher *self, const void *input,
                           size_t input_len);
index 97a726874c72bb075f4f1124fa7fa5cd9e2e41ba..b19efbaaeb362f6237c8bd220a4068e5ea1005ba 100644 (file)
@@ -2421,8 +2421,8 @@ _blake3_compress_in_place_avx512 PROC
         movzx   r8d, r8b
         shl     rax, 32
         add     r8, rax
-        vmovd   xmm3, r9
-        vmovd   xmm4, r8
+        vmovq   xmm3, r9
+        vmovq   xmm4, r8
         vpunpcklqdq xmm3, xmm3, xmm4
         vmovaps xmm2, xmmword ptr [BLAKE3_IV]
         vmovups xmm8, xmmword ptr [rdx]
@@ -2516,8 +2516,8 @@ _blake3_compress_xof_avx512 PROC
         mov     r10, qword ptr [rsp+78H]
         shl     rax, 32
         add     r8, rax
-        vmovd   xmm3, r9
-        vmovd   xmm4, r8
+        vmovq   xmm3, r9
+        vmovq   xmm4, r8
         vpunpcklqdq xmm3, xmm3, xmm4
         vmovaps xmm2, xmmword ptr [BLAKE3_IV]
         vmovups xmm8, xmmword ptr [rdx]
index d144046abe21cb1977e70a17ab3f259563758d45..99f033fefb41ded0abbc5b5c51012d11e511fc56 100644 (file)
@@ -1704,7 +1704,7 @@ blake3_hash_many_sse2:
         pshufd  xmm15, xmm11, 0x93
         shl     rax, 0x20
         or      rax, 0x40
-        movd    xmm3, rax
+        movq    xmm3, rax
         movdqa  xmmword ptr [rsp+0x20], xmm3
         movaps  xmm3, xmmword ptr [rsp]
         movaps  xmm11, xmmword ptr [rsp+0x10]
@@ -1917,7 +1917,7 @@ blake3_hash_many_sse2:
         movaps  xmm2, xmmword ptr [BLAKE3_IV+rip]
         shl     rax, 32
         or      rax, 64
-        movd    xmm12, rax
+        movq    xmm12, rax
         movdqa  xmm3, xmm13
         punpcklqdq xmm3, xmm12
         movups  xmm4, xmmword ptr [r8+rdx-0x40]
index 494c0c6fd8b390f0350a0e9d769390ded241e330..424b4f85e2556f91a97f81dcd494a90ec41cf95e 100644 (file)
@@ -1715,7 +1715,7 @@ blake3_hash_many_sse2:
         pshufd  xmm15, xmm11, 0x93
         shl     rax, 0x20
         or      rax, 0x40
-        movd    xmm3, rax
+        movq    xmm3, rax
         movdqa  xmmword ptr [rsp+0x20], xmm3
         movaps  xmm3, xmmword ptr [rsp]
         movaps  xmm11, xmmword ptr [rsp+0x10]
@@ -1928,7 +1928,7 @@ blake3_hash_many_sse2:
         movaps  xmm2, xmmword ptr [BLAKE3_IV+rip]
         shl     rax, 32
         or      rax, 64
-        movd    xmm12, rax
+        movq    xmm12, rax
         movdqa  xmm3, xmm13
         punpcklqdq xmm3, xmm12
         movups  xmm4, xmmword ptr [r8+rdx-0x40]
diff --git a/src/third_party/httplib.cpp b/src/third_party/httplib.cpp
new file mode 100644 (file)
index 0000000..34a71ca
--- /dev/null
@@ -0,0 +1,6033 @@
+#include "httplib.h"
+namespace httplib {
+
+/*
+ * Implementation that will be part of the .cc file if split into .h + .cc.
+ */
+
+namespace detail {
+
+bool is_hex(char c, int &v) {
+  if (0x20 <= c && isdigit(c)) {
+    v = c - '0';
+    return true;
+  } else if ('A' <= c && c <= 'F') {
+    v = c - 'A' + 10;
+    return true;
+  } else if ('a' <= c && c <= 'f') {
+    v = c - 'a' + 10;
+    return true;
+  }
+  return false;
+}
+
+bool from_hex_to_i(const std::string &s, size_t i, size_t cnt,
+                          int &val) {
+  if (i >= s.size()) { return false; }
+
+  val = 0;
+  for (; cnt; i++, cnt--) {
+    if (!s[i]) { return false; }
+    int v = 0;
+    if (is_hex(s[i], v)) {
+      val = val * 16 + v;
+    } else {
+      return false;
+    }
+  }
+  return true;
+}
+
+std::string from_i_to_hex(size_t n) {
+  const char *charset = "0123456789abcdef";
+  std::string ret;
+  do {
+    ret = charset[n & 15] + ret;
+    n >>= 4;
+  } while (n > 0);
+  return ret;
+}
+
+size_t to_utf8(int code, char *buff) {
+  if (code < 0x0080) {
+    buff[0] = (code & 0x7F);
+    return 1;
+  } else if (code < 0x0800) {
+    buff[0] = static_cast<char>(0xC0 | ((code >> 6) & 0x1F));
+    buff[1] = static_cast<char>(0x80 | (code & 0x3F));
+    return 2;
+  } else if (code < 0xD800) {
+    buff[0] = static_cast<char>(0xE0 | ((code >> 12) & 0xF));
+    buff[1] = static_cast<char>(0x80 | ((code >> 6) & 0x3F));
+    buff[2] = static_cast<char>(0x80 | (code & 0x3F));
+    return 3;
+  } else if (code < 0xE000) { // D800 - DFFF is invalid...
+    return 0;
+  } else if (code < 0x10000) {
+    buff[0] = static_cast<char>(0xE0 | ((code >> 12) & 0xF));
+    buff[1] = static_cast<char>(0x80 | ((code >> 6) & 0x3F));
+    buff[2] = static_cast<char>(0x80 | (code & 0x3F));
+    return 3;
+  } else if (code < 0x110000) {
+    buff[0] = static_cast<char>(0xF0 | ((code >> 18) & 0x7));
+    buff[1] = static_cast<char>(0x80 | ((code >> 12) & 0x3F));
+    buff[2] = static_cast<char>(0x80 | ((code >> 6) & 0x3F));
+    buff[3] = static_cast<char>(0x80 | (code & 0x3F));
+    return 4;
+  }
+
+  // NOTREACHED
+  return 0;
+}
+
+// NOTE: This code came up with the following stackoverflow post:
+// https://stackoverflow.com/questions/180947/base64-decode-snippet-in-c
+std::string base64_encode(const std::string &in) {
+  static const auto lookup =
+      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+  std::string out;
+  out.reserve(in.size());
+
+  int val = 0;
+  int valb = -6;
+
+  for (auto c : in) {
+    val = (val << 8) + static_cast<uint8_t>(c);
+    valb += 8;
+    while (valb >= 0) {
+      out.push_back(lookup[(val >> valb) & 0x3F]);
+      valb -= 6;
+    }
+  }
+
+  if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); }
+
+  while (out.size() % 4) {
+    out.push_back('=');
+  }
+
+  return out;
+}
+
+bool is_file(const std::string &path) {
+  struct stat st;
+  return stat(path.c_str(), &st) >= 0 && S_ISREG(st.st_mode);
+}
+
+bool is_dir(const std::string &path) {
+  struct stat st;
+  return stat(path.c_str(), &st) >= 0 && S_ISDIR(st.st_mode);
+}
+
+bool is_valid_path(const std::string &path) {
+  size_t level = 0;
+  size_t i = 0;
+
+  // Skip slash
+  while (i < path.size() && path[i] == '/') {
+    i++;
+  }
+
+  while (i < path.size()) {
+    // Read component
+    auto beg = i;
+    while (i < path.size() && path[i] != '/') {
+      i++;
+    }
+
+    auto len = i - beg;
+    assert(len > 0);
+
+    if (!path.compare(beg, len, ".")) {
+      ;
+    } else if (!path.compare(beg, len, "..")) {
+      if (level == 0) { return false; }
+      level--;
+    } else {
+      level++;
+    }
+
+    // Skip slash
+    while (i < path.size() && path[i] == '/') {
+      i++;
+    }
+  }
+
+  return true;
+}
+
+std::string encode_query_param(const std::string &value) {
+  std::ostringstream escaped;
+  escaped.fill('0');
+  escaped << std::hex;
+
+  for (auto c : value) {
+    if (std::isalnum(static_cast<uint8_t>(c)) || c == '-' || c == '_' ||
+        c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' ||
+        c == ')') {
+      escaped << c;
+    } else {
+      escaped << std::uppercase;
+      escaped << '%' << std::setw(2)
+              << static_cast<int>(static_cast<unsigned char>(c));
+      escaped << std::nouppercase;
+    }
+  }
+
+  return escaped.str();
+}
+
+std::string encode_url(const std::string &s) {
+  std::string result;
+  result.reserve(s.size());
+
+  for (size_t i = 0; s[i]; i++) {
+    switch (s[i]) {
+    case ' ': result += "%20"; break;
+    case '+': result += "%2B"; break;
+    case '\r': result += "%0D"; break;
+    case '\n': result += "%0A"; break;
+    case '\'': result += "%27"; break;
+    case ',': result += "%2C"; break;
+    // case ':': result += "%3A"; break; // ok? probably...
+    case ';': result += "%3B"; break;
+    default:
+      auto c = static_cast<uint8_t>(s[i]);
+      if (c >= 0x80) {
+        result += '%';
+        char hex[4];
+        auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c);
+        assert(len == 2);
+        result.append(hex, static_cast<size_t>(len));
+      } else {
+        result += s[i];
+      }
+      break;
+    }
+  }
+
+  return result;
+}
+
+std::string decode_url(const std::string &s,
+                              bool convert_plus_to_space) {
+  std::string result;
+
+  for (size_t i = 0; i < s.size(); i++) {
+    if (s[i] == '%' && i + 1 < s.size()) {
+      if (s[i + 1] == 'u') {
+        int val = 0;
+        if (from_hex_to_i(s, i + 2, 4, val)) {
+          // 4 digits Unicode codes
+          char buff[4];
+          size_t len = to_utf8(val, buff);
+          if (len > 0) { result.append(buff, len); }
+          i += 5; // 'u0000'
+        } else {
+          result += s[i];
+        }
+      } else {
+        int val = 0;
+        if (from_hex_to_i(s, i + 1, 2, val)) {
+          // 2 digits hex codes
+          result += static_cast<char>(val);
+          i += 2; // '00'
+        } else {
+          result += s[i];
+        }
+      }
+    } else if (convert_plus_to_space && s[i] == '+') {
+      result += ' ';
+    } else {
+      result += s[i];
+    }
+  }
+
+  return result;
+}
+
+void read_file(const std::string &path, std::string &out) {
+  std::ifstream fs(path, std::ios_base::binary);
+  fs.seekg(0, std::ios_base::end);
+  auto size = fs.tellg();
+  fs.seekg(0);
+  out.resize(static_cast<size_t>(size));
+  fs.read(&out[0], static_cast<std::streamsize>(size));
+}
+
+std::string file_extension(const std::string &path) {
+  std::smatch m;
+  static auto re = std::regex("\\.([a-zA-Z0-9]+)$");
+  if (std::regex_search(path, m, re)) { return m[1].str(); }
+  return std::string();
+}
+
+bool is_space_or_tab(char c) { return c == ' ' || c == '\t'; }
+
+std::pair<size_t, size_t> trim(const char *b, const char *e, size_t left,
+                                      size_t right) {
+  while (b + left < e && is_space_or_tab(b[left])) {
+    left++;
+  }
+  while (right > 0 && is_space_or_tab(b[right - 1])) {
+    right--;
+  }
+  return std::make_pair(left, right);
+}
+
+std::string trim_copy(const std::string &s) {
+  auto r = trim(s.data(), s.data() + s.size(), 0, s.size());
+  return s.substr(r.first, r.second - r.first);
+}
+
+void split(const char *b, const char *e, char d,
+                  std::function<void(const char *, const char *)> fn) {
+  size_t i = 0;
+  size_t beg = 0;
+
+  while (e ? (b + i < e) : (b[i] != '\0')) {
+    if (b[i] == d) {
+      auto r = trim(b, e, beg, i);
+      if (r.first < r.second) { fn(&b[r.first], &b[r.second]); }
+      beg = i + 1;
+    }
+    i++;
+  }
+
+  if (i) {
+    auto r = trim(b, e, beg, i);
+    if (r.first < r.second) { fn(&b[r.first], &b[r.second]); }
+  }
+}
+
+stream_line_reader::stream_line_reader(Stream &strm, char *fixed_buffer,
+                                              size_t fixed_buffer_size)
+    : strm_(strm), fixed_buffer_(fixed_buffer),
+      fixed_buffer_size_(fixed_buffer_size) {}
+
+const char *stream_line_reader::ptr() const {
+  if (glowable_buffer_.empty()) {
+    return fixed_buffer_;
+  } else {
+    return glowable_buffer_.data();
+  }
+}
+
+size_t stream_line_reader::size() const {
+  if (glowable_buffer_.empty()) {
+    return fixed_buffer_used_size_;
+  } else {
+    return glowable_buffer_.size();
+  }
+}
+
+bool stream_line_reader::end_with_crlf() const {
+  auto end = ptr() + size();
+  return size() >= 2 && end[-2] == '\r' && end[-1] == '\n';
+}
+
+bool stream_line_reader::getline() {
+  fixed_buffer_used_size_ = 0;
+  glowable_buffer_.clear();
+
+  for (size_t i = 0;; i++) {
+    char byte;
+    auto n = strm_.read(&byte, 1);
+
+    if (n < 0) {
+      return false;
+    } else if (n == 0) {
+      if (i == 0) {
+        return false;
+      } else {
+        break;
+      }
+    }
+
+    append(byte);
+
+    if (byte == '\n') { break; }
+  }
+
+  return true;
+}
+
+void stream_line_reader::append(char c) {
+  if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) {
+    fixed_buffer_[fixed_buffer_used_size_++] = c;
+    fixed_buffer_[fixed_buffer_used_size_] = '\0';
+  } else {
+    if (glowable_buffer_.empty()) {
+      assert(fixed_buffer_[fixed_buffer_used_size_] == '\0');
+      glowable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_);
+    }
+    glowable_buffer_ += c;
+  }
+}
+
+int close_socket(socket_t sock) {
+#ifdef _WIN32
+  return closesocket(sock);
+#else
+  return close(sock);
+#endif
+}
+
+template <typename T> ssize_t handle_EINTR(T fn) {
+  ssize_t res = false;
+  while (true) {
+    res = fn();
+    if (res < 0 && errno == EINTR) { continue; }
+    break;
+  }
+  return res;
+}
+
+ssize_t select_read(socket_t sock, time_t sec, time_t usec) {
+#ifdef CPPHTTPLIB_USE_POLL
+  struct pollfd pfd_read;
+  pfd_read.fd = sock;
+  pfd_read.events = POLLIN;
+
+  auto timeout = static_cast<int>(sec * 1000 + usec / 1000);
+
+  return handle_EINTR([&]() { return poll(&pfd_read, 1, timeout); });
+#else
+#ifndef _WIN32
+  if (sock >= FD_SETSIZE) { return 1; }
+#endif
+
+  fd_set fds;
+  FD_ZERO(&fds);
+  FD_SET(sock, &fds);
+
+  timeval tv;
+  tv.tv_sec = static_cast<long>(sec);
+  tv.tv_usec = static_cast<decltype(tv.tv_usec)>(usec);
+
+  return handle_EINTR([&]() {
+    return select(static_cast<int>(sock + 1), &fds, nullptr, nullptr, &tv);
+  });
+#endif
+}
+
+ssize_t select_write(socket_t sock, time_t sec, time_t usec) {
+#ifdef CPPHTTPLIB_USE_POLL
+  struct pollfd pfd_read;
+  pfd_read.fd = sock;
+  pfd_read.events = POLLOUT;
+
+  auto timeout = static_cast<int>(sec * 1000 + usec / 1000);
+
+  return handle_EINTR([&]() { return poll(&pfd_read, 1, timeout); });
+#else
+#ifndef _WIN32
+  if (sock >= FD_SETSIZE) { return 1; }
+#endif
+
+  fd_set fds;
+  FD_ZERO(&fds);
+  FD_SET(sock, &fds);
+
+  timeval tv;
+  tv.tv_sec = static_cast<long>(sec);
+  tv.tv_usec = static_cast<decltype(tv.tv_usec)>(usec);
+
+  return handle_EINTR([&]() {
+    return select(static_cast<int>(sock + 1), nullptr, &fds, nullptr, &tv);
+  });
+#endif
+}
+
+bool wait_until_socket_is_ready(socket_t sock, time_t sec, time_t usec) {
+#ifdef CPPHTTPLIB_USE_POLL
+  struct pollfd pfd_read;
+  pfd_read.fd = sock;
+  pfd_read.events = POLLIN | POLLOUT;
+
+  auto timeout = static_cast<int>(sec * 1000 + usec / 1000);
+
+  auto poll_res = handle_EINTR([&]() { return poll(&pfd_read, 1, timeout); });
+
+  if (poll_res > 0 && pfd_read.revents & (POLLIN | POLLOUT)) {
+    int error = 0;
+    socklen_t len = sizeof(error);
+    auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR,
+                          reinterpret_cast<char *>(&error), &len);
+    return res >= 0 && !error;
+  }
+  return false;
+#else
+#ifndef _WIN32
+  if (sock >= FD_SETSIZE) { return false; }
+#endif
+
+  fd_set fdsr;
+  FD_ZERO(&fdsr);
+  FD_SET(sock, &fdsr);
+
+  auto fdsw = fdsr;
+  auto fdse = fdsr;
+
+  timeval tv;
+  tv.tv_sec = static_cast<long>(sec);
+  tv.tv_usec = static_cast<decltype(tv.tv_usec)>(usec);
+
+  auto ret = handle_EINTR([&]() {
+    return select(static_cast<int>(sock + 1), &fdsr, &fdsw, &fdse, &tv);
+  });
+
+  if (ret > 0 && (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw))) {
+    int error = 0;
+    socklen_t len = sizeof(error);
+    return getsockopt(sock, SOL_SOCKET, SO_ERROR,
+                      reinterpret_cast<char *>(&error), &len) >= 0 &&
+           !error;
+  }
+  return false;
+#endif
+}
+
+class SocketStream : public Stream {
+public:
+  SocketStream(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec,
+               time_t write_timeout_sec, time_t write_timeout_usec);
+  ~SocketStream() override;
+
+  bool is_readable() const override;
+  bool is_writable() const override;
+  ssize_t read(char *ptr, size_t size) override;
+  ssize_t write(const char *ptr, size_t size) override;
+  void get_remote_ip_and_port(std::string &ip, int &port) const override;
+  socket_t socket() const override;
+
+private:
+  socket_t sock_;
+  time_t read_timeout_sec_;
+  time_t read_timeout_usec_;
+  time_t write_timeout_sec_;
+  time_t write_timeout_usec_;
+};
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+class SSLSocketStream : public Stream {
+public:
+  SSLSocketStream(socket_t sock, SSL *ssl, time_t read_timeout_sec,
+                  time_t read_timeout_usec, time_t write_timeout_sec,
+                  time_t write_timeout_usec);
+  ~SSLSocketStream() override;
+
+  bool is_readable() const override;
+  bool is_writable() const override;
+  ssize_t read(char *ptr, size_t size) override;
+  ssize_t write(const char *ptr, size_t size) override;
+  void get_remote_ip_and_port(std::string &ip, int &port) const override;
+  socket_t socket() const override;
+
+private:
+  socket_t sock_;
+  SSL *ssl_;
+  time_t read_timeout_sec_;
+  time_t read_timeout_usec_;
+  time_t write_timeout_sec_;
+  time_t write_timeout_usec_;
+};
+#endif
+
+bool keep_alive(socket_t sock, time_t keep_alive_timeout_sec) {
+  using namespace std::chrono;
+  auto start = steady_clock::now();
+  while (true) {
+    auto val = select_read(sock, 0, 10000);
+    if (val < 0) {
+      return false;
+    } else if (val == 0) {
+      auto current = steady_clock::now();
+      auto duration = duration_cast<milliseconds>(current - start);
+      auto timeout = keep_alive_timeout_sec * 1000;
+      if (duration.count() > timeout) { return false; }
+      std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    } else {
+      return true;
+    }
+  }
+}
+
+template <typename T>
+bool
+process_server_socket_core(socket_t sock, size_t keep_alive_max_count,
+                           time_t keep_alive_timeout_sec, T callback) {
+  assert(keep_alive_max_count > 0);
+  auto ret = false;
+  auto count = keep_alive_max_count;
+  while (count > 0 && keep_alive(sock, keep_alive_timeout_sec)) {
+    auto close_connection = count == 1;
+    auto connection_closed = false;
+    ret = callback(close_connection, connection_closed);
+    if (!ret || connection_closed) { break; }
+    count--;
+  }
+  return ret;
+}
+
+template <typename T>
+bool
+process_server_socket(socket_t sock, size_t keep_alive_max_count,
+                      time_t keep_alive_timeout_sec, time_t read_timeout_sec,
+                      time_t read_timeout_usec, time_t write_timeout_sec,
+                      time_t write_timeout_usec, T callback) {
+  return process_server_socket_core(
+      sock, keep_alive_max_count, keep_alive_timeout_sec,
+      [&](bool close_connection, bool &connection_closed) {
+        SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
+                          write_timeout_sec, write_timeout_usec);
+        return callback(strm, close_connection, connection_closed);
+      });
+}
+
+bool process_client_socket(socket_t sock, time_t read_timeout_sec,
+                                  time_t read_timeout_usec,
+                                  time_t write_timeout_sec,
+                                  time_t write_timeout_usec,
+                                  std::function<bool(Stream &)> callback) {
+  SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
+                    write_timeout_sec, write_timeout_usec);
+  return callback(strm);
+}
+
+int shutdown_socket(socket_t sock) {
+#ifdef _WIN32
+  return shutdown(sock, SD_BOTH);
+#else
+  return shutdown(sock, SHUT_RDWR);
+#endif
+}
+
+template <typename BindOrConnect>
+socket_t create_socket(const char *host, int port, int address_family,
+                       int socket_flags, bool tcp_nodelay,
+                       SocketOptions socket_options,
+                       BindOrConnect bind_or_connect) {
+  // Get address info
+  struct addrinfo hints;
+  struct addrinfo *result;
+
+  memset(&hints, 0, sizeof(struct addrinfo));
+  hints.ai_family = address_family;
+  hints.ai_socktype = SOCK_STREAM;
+  hints.ai_flags = socket_flags;
+  hints.ai_protocol = 0;
+
+  auto service = std::to_string(port);
+
+  if (getaddrinfo(host, service.c_str(), &hints, &result)) {
+#ifdef __linux__
+    res_init();
+#endif
+    return INVALID_SOCKET;
+  }
+
+  for (auto rp = result; rp; rp = rp->ai_next) {
+    // Create a socket
+#ifdef _WIN32
+    auto sock =
+        WSASocketW(rp->ai_family, rp->ai_socktype, rp->ai_protocol, nullptr, 0,
+                   WSA_FLAG_NO_HANDLE_INHERIT | WSA_FLAG_OVERLAPPED);
+    /**
+     * Since the WSA_FLAG_NO_HANDLE_INHERIT is only supported on Windows 7 SP1
+     * and above the socket creation fails on older Windows Systems.
+     *
+     * Let's try to create a socket the old way in this case.
+     *
+     * Reference:
+     * https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa
+     *
+     * WSA_FLAG_NO_HANDLE_INHERIT:
+     * This flag is supported on Windows 7 with SP1, Windows Server 2008 R2 with
+     * SP1, and later
+     *
+     */
+    if (sock == INVALID_SOCKET) {
+      sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+    }
+#else
+    auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+#endif
+    if (sock == INVALID_SOCKET) { continue; }
+
+#ifndef _WIN32
+    if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) { continue; }
+#endif
+
+    if (tcp_nodelay) {
+      int yes = 1;
+      setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<char *>(&yes),
+                 sizeof(yes));
+    }
+
+    if (socket_options) { socket_options(sock); }
+
+    if (rp->ai_family == AF_INET6) {
+      int no = 0;
+      setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<char *>(&no),
+                 sizeof(no));
+    }
+
+    // bind or connect
+    if (bind_or_connect(sock, *rp)) {
+      freeaddrinfo(result);
+      return sock;
+    }
+
+    close_socket(sock);
+  }
+
+  freeaddrinfo(result);
+  return INVALID_SOCKET;
+}
+
+void set_nonblocking(socket_t sock, bool nonblocking) {
+#ifdef _WIN32
+  auto flags = nonblocking ? 1UL : 0UL;
+  ioctlsocket(sock, FIONBIO, &flags);
+#else
+  auto flags = fcntl(sock, F_GETFL, 0);
+  fcntl(sock, F_SETFL,
+        nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK)));
+#endif
+}
+
+bool is_connection_error() {
+#ifdef _WIN32
+  return WSAGetLastError() != WSAEWOULDBLOCK;
+#else
+  return errno != EINPROGRESS;
+#endif
+}
+
+bool bind_ip_address(socket_t sock, const char *host) {
+  struct addrinfo hints;
+  struct addrinfo *result;
+
+  memset(&hints, 0, sizeof(struct addrinfo));
+  hints.ai_family = AF_UNSPEC;
+  hints.ai_socktype = SOCK_STREAM;
+  hints.ai_protocol = 0;
+
+  if (getaddrinfo(host, "0", &hints, &result)) { return false; }
+
+  auto ret = false;
+  for (auto rp = result; rp; rp = rp->ai_next) {
+    const auto &ai = *rp;
+    if (!::bind(sock, ai.ai_addr, static_cast<socklen_t>(ai.ai_addrlen))) {
+      ret = true;
+      break;
+    }
+  }
+
+  freeaddrinfo(result);
+  return ret;
+}
+
+#if !defined _WIN32 && !defined ANDROID
+#define USE_IF2IP
+#endif
+
+#ifdef USE_IF2IP
+std::string if2ip(const std::string &ifn) {
+  struct ifaddrs *ifap;
+  getifaddrs(&ifap);
+  for (auto ifa = ifap; ifa; ifa = ifa->ifa_next) {
+    if (ifa->ifa_addr && ifn == ifa->ifa_name) {
+      if (ifa->ifa_addr->sa_family == AF_INET) {
+        auto sa = reinterpret_cast<struct sockaddr_in *>(ifa->ifa_addr);
+        char buf[INET_ADDRSTRLEN];
+        if (inet_ntop(AF_INET, &sa->sin_addr, buf, INET_ADDRSTRLEN)) {
+          freeifaddrs(ifap);
+          return std::string(buf, INET_ADDRSTRLEN);
+        }
+      }
+    }
+  }
+  freeifaddrs(ifap);
+  return std::string();
+}
+#endif
+
+socket_t create_client_socket(
+    const char *host, int port, int address_family, bool tcp_nodelay,
+    SocketOptions socket_options, time_t connection_timeout_sec,
+    time_t connection_timeout_usec, time_t read_timeout_sec,
+    time_t read_timeout_usec, time_t write_timeout_sec,
+    time_t write_timeout_usec, const std::string &intf, Error &error) {
+  auto sock = create_socket(
+      host, port, address_family, 0, tcp_nodelay, std::move(socket_options),
+      [&](socket_t sock2, struct addrinfo &ai) -> bool {
+        if (!intf.empty()) {
+#ifdef USE_IF2IP
+          auto ip = if2ip(intf);
+          if (ip.empty()) { ip = intf; }
+          if (!bind_ip_address(sock2, ip.c_str())) {
+            error = Error::BindIPAddress;
+            return false;
+          }
+#endif
+        }
+
+        set_nonblocking(sock2, true);
+
+        auto ret =
+            ::connect(sock2, ai.ai_addr, static_cast<socklen_t>(ai.ai_addrlen));
+
+        if (ret < 0) {
+          if (is_connection_error() ||
+              !wait_until_socket_is_ready(sock2, connection_timeout_sec,
+                                          connection_timeout_usec)) {
+            error = Error::Connection;
+            return false;
+          }
+        }
+
+        set_nonblocking(sock2, false);
+
+        {
+          timeval tv;
+          tv.tv_sec = static_cast<long>(read_timeout_sec);
+          tv.tv_usec = static_cast<decltype(tv.tv_usec)>(read_timeout_usec);
+          setsockopt(sock2, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv));
+        }
+        {
+          timeval tv;
+          tv.tv_sec = static_cast<long>(write_timeout_sec);
+          tv.tv_usec = static_cast<decltype(tv.tv_usec)>(write_timeout_usec);
+          setsockopt(sock2, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv));
+        }
+
+        error = Error::Success;
+        return true;
+      });
+
+  if (sock != INVALID_SOCKET) {
+    error = Error::Success;
+  } else {
+    if (error == Error::Success) { error = Error::Connection; }
+  }
+
+  return sock;
+}
+
+void get_remote_ip_and_port(const struct sockaddr_storage &addr,
+                                   socklen_t addr_len, std::string &ip,
+                                   int &port) {
+  if (addr.ss_family == AF_INET) {
+    port = ntohs(reinterpret_cast<const struct sockaddr_in *>(&addr)->sin_port);
+  } else if (addr.ss_family == AF_INET6) {
+    port =
+        ntohs(reinterpret_cast<const struct sockaddr_in6 *>(&addr)->sin6_port);
+  }
+
+  std::array<char, NI_MAXHOST> ipstr{};
+  if (!getnameinfo(reinterpret_cast<const struct sockaddr *>(&addr), addr_len,
+                   ipstr.data(), static_cast<socklen_t>(ipstr.size()), nullptr,
+                   0, NI_NUMERICHOST)) {
+    ip = ipstr.data();
+  }
+}
+
+void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) {
+  struct sockaddr_storage addr;
+  socklen_t addr_len = sizeof(addr);
+
+  if (!getpeername(sock, reinterpret_cast<struct sockaddr *>(&addr),
+                   &addr_len)) {
+    get_remote_ip_and_port(addr, addr_len, ip, port);
+  }
+}
+
+constexpr unsigned int str2tag_core(const char *s, size_t l,
+                                           unsigned int h) {
+  return (l == 0) ? h
+                  : str2tag_core(s + 1, l - 1,
+                                 (h * 33) ^ static_cast<unsigned char>(*s));
+}
+
+unsigned int str2tag(const std::string &s) {
+  return str2tag_core(s.data(), s.size(), 0);
+}
+
+namespace udl {
+
+constexpr unsigned int operator"" _t(const char *s, size_t l) {
+  return str2tag_core(s, l, 0);
+}
+
+} // namespace udl
+
+const char *
+find_content_type(const std::string &path,
+                  const std::map<std::string, std::string> &user_data) {
+  auto ext = file_extension(path);
+
+  auto it = user_data.find(ext);
+  if (it != user_data.end()) { return it->second.c_str(); }
+
+  using udl::operator""_t;
+
+  switch (str2tag(ext)) {
+  default: return nullptr;
+  case "css"_t: return "text/css";
+  case "csv"_t: return "text/csv";
+  case "txt"_t: return "text/plain";
+  case "vtt"_t: return "text/vtt";
+  case "htm"_t:
+  case "html"_t: return "text/html";
+
+  case "apng"_t: return "image/apng";
+  case "avif"_t: return "image/avif";
+  case "bmp"_t: return "image/bmp";
+  case "gif"_t: return "image/gif";
+  case "png"_t: return "image/png";
+  case "svg"_t: return "image/svg+xml";
+  case "webp"_t: return "image/webp";
+  case "ico"_t: return "image/x-icon";
+  case "tif"_t: return "image/tiff";
+  case "tiff"_t: return "image/tiff";
+  case "jpg"_t:
+  case "jpeg"_t: return "image/jpeg";
+
+  case "mp4"_t: return "video/mp4";
+  case "mpeg"_t: return "video/mpeg";
+  case "webm"_t: return "video/webm";
+
+  case "mp3"_t: return "audio/mp3";
+  case "mpga"_t: return "audio/mpeg";
+  case "weba"_t: return "audio/webm";
+  case "wav"_t: return "audio/wave";
+
+  case "otf"_t: return "font/otf";
+  case "ttf"_t: return "font/ttf";
+  case "woff"_t: return "font/woff";
+  case "woff2"_t: return "font/woff2";
+
+  case "7z"_t: return "application/x-7z-compressed";
+  case "atom"_t: return "application/atom+xml";
+  case "pdf"_t: return "application/pdf";
+  case "js"_t:
+  case "mjs"_t: return "application/javascript";
+  case "json"_t: return "application/json";
+  case "rss"_t: return "application/rss+xml";
+  case "tar"_t: return "application/x-tar";
+  case "xht"_t:
+  case "xhtml"_t: return "application/xhtml+xml";
+  case "xslt"_t: return "application/xslt+xml";
+  case "xml"_t: return "application/xml";
+  case "gz"_t: return "application/gzip";
+  case "zip"_t: return "application/zip";
+  case "wasm"_t: return "application/wasm";
+  }
+}
+
+const char *status_message(int status) {
+  switch (status) {
+  case 100: return "Continue";
+  case 101: return "Switching Protocol";
+  case 102: return "Processing";
+  case 103: return "Early Hints";
+  case 200: return "OK";
+  case 201: return "Created";
+  case 202: return "Accepted";
+  case 203: return "Non-Authoritative Information";
+  case 204: return "No Content";
+  case 205: return "Reset Content";
+  case 206: return "Partial Content";
+  case 207: return "Multi-Status";
+  case 208: return "Already Reported";
+  case 226: return "IM Used";
+  case 300: return "Multiple Choice";
+  case 301: return "Moved Permanently";
+  case 302: return "Found";
+  case 303: return "See Other";
+  case 304: return "Not Modified";
+  case 305: return "Use Proxy";
+  case 306: return "unused";
+  case 307: return "Temporary Redirect";
+  case 308: return "Permanent Redirect";
+  case 400: return "Bad Request";
+  case 401: return "Unauthorized";
+  case 402: return "Payment Required";
+  case 403: return "Forbidden";
+  case 404: return "Not Found";
+  case 405: return "Method Not Allowed";
+  case 406: return "Not Acceptable";
+  case 407: return "Proxy Authentication Required";
+  case 408: return "Request Timeout";
+  case 409: return "Conflict";
+  case 410: return "Gone";
+  case 411: return "Length Required";
+  case 412: return "Precondition Failed";
+  case 413: return "Payload Too Large";
+  case 414: return "URI Too Long";
+  case 415: return "Unsupported Media Type";
+  case 416: return "Range Not Satisfiable";
+  case 417: return "Expectation Failed";
+  case 418: return "I'm a teapot";
+  case 421: return "Misdirected Request";
+  case 422: return "Unprocessable Entity";
+  case 423: return "Locked";
+  case 424: return "Failed Dependency";
+  case 425: return "Too Early";
+  case 426: return "Upgrade Required";
+  case 428: return "Precondition Required";
+  case 429: return "Too Many Requests";
+  case 431: return "Request Header Fields Too Large";
+  case 451: return "Unavailable For Legal Reasons";
+  case 501: return "Not Implemented";
+  case 502: return "Bad Gateway";
+  case 503: return "Service Unavailable";
+  case 504: return "Gateway Timeout";
+  case 505: return "HTTP Version Not Supported";
+  case 506: return "Variant Also Negotiates";
+  case 507: return "Insufficient Storage";
+  case 508: return "Loop Detected";
+  case 510: return "Not Extended";
+  case 511: return "Network Authentication Required";
+
+  default:
+  case 500: return "Internal Server Error";
+  }
+}
+
+bool can_compress_content_type(const std::string &content_type) {
+  return (!content_type.find("text/") && content_type != "text/event-stream") ||
+         content_type == "image/svg+xml" ||
+         content_type == "application/javascript" ||
+         content_type == "application/json" ||
+         content_type == "application/xml" ||
+         content_type == "application/xhtml+xml";
+}
+
+EncodingType encoding_type(const Request &req, const Response &res) {
+  auto ret =
+      detail::can_compress_content_type(res.get_header_value("Content-Type"));
+  if (!ret) { return EncodingType::None; }
+
+  const auto &s = req.get_header_value("Accept-Encoding");
+  (void)(s);
+
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+  // TODO: 'Accept-Encoding' has br, not br;q=0
+  ret = s.find("br") != std::string::npos;
+  if (ret) { return EncodingType::Brotli; }
+#endif
+
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+  // TODO: 'Accept-Encoding' has gzip, not gzip;q=0
+  ret = s.find("gzip") != std::string::npos;
+  if (ret) { return EncodingType::Gzip; }
+#endif
+
+  return EncodingType::None;
+}
+
+bool nocompressor::compress(const char *data, size_t data_length,
+                                   bool /*last*/, Callback callback) {
+  if (!data_length) { return true; }
+  return callback(data, data_length);
+}
+
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+gzip_compressor::gzip_compressor() {
+  std::memset(&strm_, 0, sizeof(strm_));
+  strm_.zalloc = Z_NULL;
+  strm_.zfree = Z_NULL;
+  strm_.opaque = Z_NULL;
+
+  is_valid_ = deflateInit2(&strm_, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8,
+                           Z_DEFAULT_STRATEGY) == Z_OK;
+}
+
+gzip_compressor::~gzip_compressor() { deflateEnd(&strm_); }
+
+bool gzip_compressor::compress(const char *data, size_t data_length,
+                                      bool last, Callback callback) {
+  assert(is_valid_);
+
+  do {
+    constexpr size_t max_avail_in =
+        std::numeric_limits<decltype(strm_.avail_in)>::max();
+
+    strm_.avail_in = static_cast<decltype(strm_.avail_in)>(
+        std::min(data_length, max_avail_in));
+    strm_.next_in = const_cast<Bytef *>(reinterpret_cast<const Bytef *>(data));
+
+    data_length -= strm_.avail_in;
+    data += strm_.avail_in;
+
+    auto flush = (last && data_length == 0) ? Z_FINISH : Z_NO_FLUSH;
+    int ret = Z_OK;
+
+    std::array<char, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};
+    do {
+      strm_.avail_out = static_cast<uInt>(buff.size());
+      strm_.next_out = reinterpret_cast<Bytef *>(buff.data());
+
+      ret = deflate(&strm_, flush);
+      if (ret == Z_STREAM_ERROR) { return false; }
+
+      if (!callback(buff.data(), buff.size() - strm_.avail_out)) {
+        return false;
+      }
+    } while (strm_.avail_out == 0);
+
+    assert((flush == Z_FINISH && ret == Z_STREAM_END) ||
+           (flush == Z_NO_FLUSH && ret == Z_OK));
+    assert(strm_.avail_in == 0);
+
+  } while (data_length > 0);
+
+  return true;
+}
+
+gzip_decompressor::gzip_decompressor() {
+  std::memset(&strm_, 0, sizeof(strm_));
+  strm_.zalloc = Z_NULL;
+  strm_.zfree = Z_NULL;
+  strm_.opaque = Z_NULL;
+
+  // 15 is the value of wbits, which should be at the maximum possible value
+  // to ensure that any gzip stream can be decoded. The offset of 32 specifies
+  // that the stream type should be automatically detected either gzip or
+  // deflate.
+  is_valid_ = inflateInit2(&strm_, 32 + 15) == Z_OK;
+}
+
+gzip_decompressor::~gzip_decompressor() { inflateEnd(&strm_); }
+
+bool gzip_decompressor::is_valid() const { return is_valid_; }
+
+bool gzip_decompressor::decompress(const char *data, size_t data_length,
+                                          Callback callback) {
+  assert(is_valid_);
+
+  int ret = Z_OK;
+
+  do {
+    constexpr size_t max_avail_in =
+        std::numeric_limits<decltype(strm_.avail_in)>::max();
+
+    strm_.avail_in = static_cast<decltype(strm_.avail_in)>(
+        std::min(data_length, max_avail_in));
+    strm_.next_in = const_cast<Bytef *>(reinterpret_cast<const Bytef *>(data));
+
+    data_length -= strm_.avail_in;
+    data += strm_.avail_in;
+
+    std::array<char, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};
+    while (strm_.avail_in > 0) {
+      strm_.avail_out = static_cast<uInt>(buff.size());
+      strm_.next_out = reinterpret_cast<Bytef *>(buff.data());
+
+      ret = inflate(&strm_, Z_NO_FLUSH);
+      assert(ret != Z_STREAM_ERROR);
+      switch (ret) {
+      case Z_NEED_DICT:
+      case Z_DATA_ERROR:
+      case Z_MEM_ERROR: inflateEnd(&strm_); return false;
+      }
+
+      if (!callback(buff.data(), buff.size() - strm_.avail_out)) {
+        return false;
+      }
+    }
+
+    if (ret != Z_OK && ret != Z_STREAM_END) return false;
+
+  } while (data_length > 0);
+
+  return true;
+}
+#endif
+
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+brotli_compressor::brotli_compressor() {
+  state_ = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr);
+}
+
+brotli_compressor::~brotli_compressor() {
+  BrotliEncoderDestroyInstance(state_);
+}
+
+bool brotli_compressor::compress(const char *data, size_t data_length,
+                                        bool last, Callback callback) {
+  std::array<uint8_t, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};
+
+  auto operation = last ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS;
+  auto available_in = data_length;
+  auto next_in = reinterpret_cast<const uint8_t *>(data);
+
+  for (;;) {
+    if (last) {
+      if (BrotliEncoderIsFinished(state_)) { break; }
+    } else {
+      if (!available_in) { break; }
+    }
+
+    auto available_out = buff.size();
+    auto next_out = buff.data();
+
+    if (!BrotliEncoderCompressStream(state_, operation, &available_in, &next_in,
+                                     &available_out, &next_out, nullptr)) {
+      return false;
+    }
+
+    auto output_bytes = buff.size() - available_out;
+    if (output_bytes) {
+      callback(reinterpret_cast<const char *>(buff.data()), output_bytes);
+    }
+  }
+
+  return true;
+}
+
+brotli_decompressor::brotli_decompressor() {
+  decoder_s = BrotliDecoderCreateInstance(0, 0, 0);
+  decoder_r = decoder_s ? BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT
+                        : BROTLI_DECODER_RESULT_ERROR;
+}
+
+brotli_decompressor::~brotli_decompressor() {
+  if (decoder_s) { BrotliDecoderDestroyInstance(decoder_s); }
+}
+
+bool brotli_decompressor::is_valid() const { return decoder_s; }
+
+bool brotli_decompressor::decompress(const char *data,
+                                            size_t data_length,
+                                            Callback callback) {
+  if (decoder_r == BROTLI_DECODER_RESULT_SUCCESS ||
+      decoder_r == BROTLI_DECODER_RESULT_ERROR) {
+    return 0;
+  }
+
+  const uint8_t *next_in = (const uint8_t *)data;
+  size_t avail_in = data_length;
+  size_t total_out;
+
+  decoder_r = BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT;
+
+  std::array<char, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};
+  while (decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) {
+    char *next_out = buff.data();
+    size_t avail_out = buff.size();
+
+    decoder_r = BrotliDecoderDecompressStream(
+        decoder_s, &avail_in, &next_in, &avail_out,
+        reinterpret_cast<uint8_t **>(&next_out), &total_out);
+
+    if (decoder_r == BROTLI_DECODER_RESULT_ERROR) { return false; }
+
+    if (!callback(buff.data(), buff.size() - avail_out)) { return false; }
+  }
+
+  return decoder_r == BROTLI_DECODER_RESULT_SUCCESS ||
+         decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT;
+}
+#endif
+
+bool has_header(const Headers &headers, const char *key) {
+  return headers.find(key) != headers.end();
+}
+
+const char *get_header_value(const Headers &headers, const char *key,
+                                    size_t id, const char *def) {
+  auto rng = headers.equal_range(key);
+  auto it = rng.first;
+  std::advance(it, static_cast<ssize_t>(id));
+  if (it != rng.second) { return it->second.c_str(); }
+  return def;
+}
+
+template <typename T>
+bool parse_header(const char *beg, const char *end, T fn) {
+  // Skip trailing spaces and tabs.
+  while (beg < end && is_space_or_tab(end[-1])) {
+    end--;
+  }
+
+  auto p = beg;
+  while (p < end && *p != ':') {
+    p++;
+  }
+
+  if (p == end) { return false; }
+
+  auto key_end = p;
+
+  if (*p++ != ':') { return false; }
+
+  while (p < end && is_space_or_tab(*p)) {
+    p++;
+  }
+
+  if (p < end) {
+    fn(std::string(beg, key_end), decode_url(std::string(p, end), false));
+    return true;
+  }
+
+  return false;
+}
+
+bool read_headers(Stream &strm, Headers &headers) {
+  const auto bufsiz = 2048;
+  char buf[bufsiz];
+  stream_line_reader line_reader(strm, buf, bufsiz);
+
+  for (;;) {
+    if (!line_reader.getline()) { return false; }
+
+    // Check if the line ends with CRLF.
+    if (line_reader.end_with_crlf()) {
+      // Blank line indicates end of headers.
+      if (line_reader.size() == 2) { break; }
+    } else {
+      continue; // Skip invalid line.
+    }
+
+    // Exclude CRLF
+    auto end = line_reader.ptr() + line_reader.size() - 2;
+
+    parse_header(line_reader.ptr(), end,
+                 [&](std::string &&key, std::string &&val) {
+                   headers.emplace(std::move(key), std::move(val));
+                 });
+  }
+
+  return true;
+}
+
+bool read_content_with_length(Stream &strm, uint64_t len,
+                                     Progress progress,
+                                     ContentReceiverWithProgress out) {
+  char buf[CPPHTTPLIB_RECV_BUFSIZ];
+
+  uint64_t r = 0;
+  while (r < len) {
+    auto read_len = static_cast<size_t>(len - r);
+    auto n = strm.read(buf, (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ));
+    if (n <= 0) { return false; }
+
+    if (!out(buf, static_cast<size_t>(n), r, len)) { return false; }
+    r += static_cast<uint64_t>(n);
+
+    if (progress) {
+      if (!progress(r, len)) { return false; }
+    }
+  }
+
+  return true;
+}
+
+void skip_content_with_length(Stream &strm, uint64_t len) {
+  char buf[CPPHTTPLIB_RECV_BUFSIZ];
+  uint64_t r = 0;
+  while (r < len) {
+    auto read_len = static_cast<size_t>(len - r);
+    auto n = strm.read(buf, (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ));
+    if (n <= 0) { return; }
+    r += static_cast<uint64_t>(n);
+  }
+}
+
+bool read_content_without_length(Stream &strm,
+                                        ContentReceiverWithProgress out) {
+  char buf[CPPHTTPLIB_RECV_BUFSIZ];
+  uint64_t r = 0;
+  for (;;) {
+    auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ);
+    if (n < 0) {
+      return false;
+    } else if (n == 0) {
+      return true;
+    }
+
+    if (!out(buf, static_cast<size_t>(n), r, 0)) { return false; }
+    r += static_cast<uint64_t>(n);
+  }
+
+  return true;
+}
+
+bool read_content_chunked(Stream &strm,
+                                 ContentReceiverWithProgress out) {
+  const auto bufsiz = 16;
+  char buf[bufsiz];
+
+  stream_line_reader line_reader(strm, buf, bufsiz);
+
+  if (!line_reader.getline()) { return false; }
+
+  unsigned long chunk_len;
+  while (true) {
+    char *end_ptr;
+
+    chunk_len = std::strtoul(line_reader.ptr(), &end_ptr, 16);
+
+    if (end_ptr == line_reader.ptr()) { return false; }
+    if (chunk_len == ULONG_MAX) { return false; }
+
+    if (chunk_len == 0) { break; }
+
+    if (!read_content_with_length(strm, chunk_len, nullptr, out)) {
+      return false;
+    }
+
+    if (!line_reader.getline()) { return false; }
+
+    if (strcmp(line_reader.ptr(), "\r\n")) { break; }
+
+    if (!line_reader.getline()) { return false; }
+  }
+
+  if (chunk_len == 0) {
+    // Reader terminator after chunks
+    if (!line_reader.getline() || strcmp(line_reader.ptr(), "\r\n"))
+      return false;
+  }
+
+  return true;
+}
+
+bool is_chunked_transfer_encoding(const Headers &headers) {
+  return !strcasecmp(get_header_value(headers, "Transfer-Encoding", 0, ""),
+                     "chunked");
+}
+
+template <typename T, typename U>
+bool prepare_content_receiver(T &x, int &status,
+                              ContentReceiverWithProgress receiver,
+                              bool decompress, U callback) {
+  if (decompress) {
+    std::string encoding = x.get_header_value("Content-Encoding");
+    std::unique_ptr<decompressor> decompressor;
+
+    if (encoding.find("gzip") != std::string::npos ||
+        encoding.find("deflate") != std::string::npos) {
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+      decompressor = detail::make_unique<gzip_decompressor>();
+#else
+      status = 415;
+      return false;
+#endif
+    } else if (encoding.find("br") != std::string::npos) {
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+      decompressor = detail::make_unique<brotli_decompressor>();
+#else
+      status = 415;
+      return false;
+#endif
+    }
+
+    if (decompressor) {
+      if (decompressor->is_valid()) {
+        ContentReceiverWithProgress out = [&](const char *buf, size_t n,
+                                              uint64_t off, uint64_t len) {
+          return decompressor->decompress(buf, n,
+                                          [&](const char *buf2, size_t n2) {
+                                            return receiver(buf2, n2, off, len);
+                                          });
+        };
+        return callback(std::move(out));
+      } else {
+        status = 500;
+        return false;
+      }
+    }
+  }
+
+  ContentReceiverWithProgress out = [&](const char *buf, size_t n, uint64_t off,
+                                        uint64_t len) {
+    return receiver(buf, n, off, len);
+  };
+  return callback(std::move(out));
+}
+
+template <typename T>
+bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
+                  Progress progress, ContentReceiverWithProgress receiver,
+                  bool decompress) {
+  return prepare_content_receiver(
+      x, status, std::move(receiver), decompress,
+      [&](const ContentReceiverWithProgress &out) {
+        auto ret = true;
+        auto exceed_payload_max_length = false;
+
+        if (is_chunked_transfer_encoding(x.headers)) {
+          ret = read_content_chunked(strm, out);
+        } else if (!has_header(x.headers, "Content-Length")) {
+          ret = read_content_without_length(strm, out);
+        } else {
+          auto len = get_header_value<uint64_t>(x.headers, "Content-Length");
+          if (len > payload_max_length) {
+            exceed_payload_max_length = true;
+            skip_content_with_length(strm, len);
+            ret = false;
+          } else if (len > 0) {
+            ret = read_content_with_length(strm, len, std::move(progress), out);
+          }
+        }
+
+        if (!ret) { status = exceed_payload_max_length ? 413 : 400; }
+        return ret;
+      });
+}
+
+ssize_t write_headers(Stream &strm, const Headers &headers) {
+  ssize_t write_len = 0;
+  for (const auto &x : headers) {
+    auto len =
+        strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str());
+    if (len < 0) { return len; }
+    write_len += len;
+  }
+  auto len = strm.write("\r\n");
+  if (len < 0) { return len; }
+  write_len += len;
+  return write_len;
+}
+
+bool write_data(Stream &strm, const char *d, size_t l) {
+  size_t offset = 0;
+  while (offset < l) {
+    auto length = strm.write(d + offset, l - offset);
+    if (length < 0) { return false; }
+    offset += static_cast<size_t>(length);
+  }
+  return true;
+}
+
+template <typename T>
+bool write_content(Stream &strm, const ContentProvider &content_provider,
+                          size_t offset, size_t length, T is_shutting_down,
+                          Error &error) {
+  size_t end_offset = offset + length;
+  auto ok = true;
+  DataSink data_sink;
+
+  data_sink.write = [&](const char *d, size_t l) -> bool {
+    if (ok) {
+      if (write_data(strm, d, l)) {
+        offset += l;
+      } else {
+        ok = false;
+      }
+    }
+    return ok;
+  };
+
+  data_sink.is_writable = [&](void) { return ok && strm.is_writable(); };
+
+  while (offset < end_offset && !is_shutting_down()) {
+    if (!content_provider(offset, end_offset - offset, data_sink)) {
+      error = Error::Canceled;
+      return false;
+    }
+    if (!ok) {
+      error = Error::Write;
+      return false;
+    }
+  }
+
+  error = Error::Success;
+  return true;
+}
+
+template <typename T>
+bool write_content(Stream &strm, const ContentProvider &content_provider,
+                          size_t offset, size_t length,
+                          const T &is_shutting_down) {
+  auto error = Error::Success;
+  return write_content(strm, content_provider, offset, length, is_shutting_down,
+                       error);
+}
+
+template <typename T>
+bool
+write_content_without_length(Stream &strm,
+                             const ContentProvider &content_provider,
+                             const T &is_shutting_down) {
+  size_t offset = 0;
+  auto data_available = true;
+  auto ok = true;
+  DataSink data_sink;
+
+  data_sink.write = [&](const char *d, size_t l) -> bool {
+    if (ok) {
+      offset += l;
+      if (!write_data(strm, d, l)) { ok = false; }
+    }
+    return ok;
+  };
+
+  data_sink.done = [&](void) { data_available = false; };
+
+  data_sink.is_writable = [&](void) { return ok && strm.is_writable(); };
+
+  while (data_available && !is_shutting_down()) {
+    if (!content_provider(offset, 0, data_sink)) { return false; }
+    if (!ok) { return false; }
+  }
+  return true;
+}
+
+template <typename T, typename U>
+bool
+write_content_chunked(Stream &strm, const ContentProvider &content_provider,
+                      const T &is_shutting_down, U &compressor, Error &error) {
+  size_t offset = 0;
+  auto data_available = true;
+  auto ok = true;
+  DataSink data_sink;
+
+  data_sink.write = [&](const char *d, size_t l) -> bool {
+    if (ok) {
+      data_available = l > 0;
+      offset += l;
+
+      std::string payload;
+      if (compressor.compress(d, l, false,
+                              [&](const char *data, size_t data_len) {
+                                payload.append(data, data_len);
+                                return true;
+                              })) {
+        if (!payload.empty()) {
+          // Emit chunked response header and footer for each chunk
+          auto chunk =
+              from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n";
+          if (!write_data(strm, chunk.data(), chunk.size())) { ok = false; }
+        }
+      } else {
+        ok = false;
+      }
+    }
+    return ok;
+  };
+
+  data_sink.done = [&](void) {
+    if (!ok) { return; }
+
+    data_available = false;
+
+    std::string payload;
+    if (!compressor.compress(nullptr, 0, true,
+                             [&](const char *data, size_t data_len) {
+                               payload.append(data, data_len);
+                               return true;
+                             })) {
+      ok = false;
+      return;
+    }
+
+    if (!payload.empty()) {
+      // Emit chunked response header and footer for each chunk
+      auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n";
+      if (!write_data(strm, chunk.data(), chunk.size())) {
+        ok = false;
+        return;
+      }
+    }
+
+    static const std::string done_marker("0\r\n\r\n");
+    if (!write_data(strm, done_marker.data(), done_marker.size())) {
+      ok = false;
+    }
+  };
+
+  data_sink.is_writable = [&](void) { return ok && strm.is_writable(); };
+
+  while (data_available && !is_shutting_down()) {
+    if (!content_provider(offset, 0, data_sink)) {
+      error = Error::Canceled;
+      return false;
+    }
+    if (!ok) {
+      error = Error::Write;
+      return false;
+    }
+  }
+
+  error = Error::Success;
+  return true;
+}
+
+template <typename T, typename U>
+bool write_content_chunked(Stream &strm,
+                                  const ContentProvider &content_provider,
+                                  const T &is_shutting_down, U &compressor) {
+  auto error = Error::Success;
+  return write_content_chunked(strm, content_provider, is_shutting_down,
+                               compressor, error);
+}
+
+template <typename T>
+bool redirect(T &cli, Request &req, Response &res,
+                     const std::string &path, const std::string &location,
+                     Error &error) {
+  Request new_req = req;
+  new_req.path = path;
+  new_req.redirect_count_ -= 1;
+
+  if (res.status == 303 && (req.method != "GET" && req.method != "HEAD")) {
+    new_req.method = "GET";
+    new_req.body.clear();
+    new_req.headers.clear();
+  }
+
+  Response new_res;
+
+  auto ret = cli.send(new_req, new_res, error);
+  if (ret) {
+    req = new_req;
+    res = new_res;
+    res.location = location;
+  }
+  return ret;
+}
+
+std::string params_to_query_str(const Params &params) {
+  std::string query;
+
+  for (auto it = params.begin(); it != params.end(); ++it) {
+    if (it != params.begin()) { query += "&"; }
+    query += it->first;
+    query += "=";
+    query += encode_query_param(it->second);
+  }
+  return query;
+}
+
+std::string append_query_params(const char *path, const Params &params) {
+  std::string path_with_query = path;
+  const static std::regex re("[^?]+\\?.*");
+  auto delm = std::regex_match(path, re) ? '&' : '?';
+  path_with_query += delm + params_to_query_str(params);
+  return path_with_query;
+}
+
+void parse_query_text(const std::string &s, Params &params) {
+  std::set<std::string> cache;
+  split(s.data(), s.data() + s.size(), '&', [&](const char *b, const char *e) {
+    std::string kv(b, e);
+    if (cache.find(kv) != cache.end()) { return; }
+    cache.insert(kv);
+
+    std::string key;
+    std::string val;
+    split(b, e, '=', [&](const char *b2, const char *e2) {
+      if (key.empty()) {
+        key.assign(b2, e2);
+      } else {
+        val.assign(b2, e2);
+      }
+    });
+
+    if (!key.empty()) {
+      params.emplace(decode_url(key, true), decode_url(val, true));
+    }
+  });
+}
+
+bool parse_multipart_boundary(const std::string &content_type,
+                                     std::string &boundary) {
+  auto pos = content_type.find("boundary=");
+  if (pos == std::string::npos) { return false; }
+  boundary = content_type.substr(pos + 9);
+  if (boundary.length() >= 2 && boundary.front() == '"' &&
+      boundary.back() == '"') {
+    boundary = boundary.substr(1, boundary.size() - 2);
+  }
+  return !boundary.empty();
+}
+
+bool parse_range_header(const std::string &s, Ranges &ranges) try {
+  static auto re_first_range = std::regex(R"(bytes=(\d*-\d*(?:,\s*\d*-\d*)*))");
+  std::smatch m;
+  if (std::regex_match(s, m, re_first_range)) {
+    auto pos = static_cast<size_t>(m.position(1));
+    auto len = static_cast<size_t>(m.length(1));
+    bool all_valid_ranges = true;
+    split(&s[pos], &s[pos + len], ',', [&](const char *b, const char *e) {
+      if (!all_valid_ranges) return;
+      static auto re_another_range = std::regex(R"(\s*(\d*)-(\d*))");
+      std::cmatch cm;
+      if (std::regex_match(b, e, cm, re_another_range)) {
+        ssize_t first = -1;
+        if (!cm.str(1).empty()) {
+          first = static_cast<ssize_t>(std::stoll(cm.str(1)));
+        }
+
+        ssize_t last = -1;
+        if (!cm.str(2).empty()) {
+          last = static_cast<ssize_t>(std::stoll(cm.str(2)));
+        }
+
+        if (first != -1 && last != -1 && first > last) {
+          all_valid_ranges = false;
+          return;
+        }
+        ranges.emplace_back(std::make_pair(first, last));
+      }
+    });
+    return all_valid_ranges;
+  }
+  return false;
+} catch (...) { return false; }
+
+class MultipartFormDataParser {
+public:
+  MultipartFormDataParser() = default;
+
+  void set_boundary(std::string &&boundary) { boundary_ = boundary; }
+
+  bool is_valid() const { return is_valid_; }
+
+  bool parse(const char *buf, size_t n, const ContentReceiver &content_callback,
+             const MultipartContentHeader &header_callback) {
+
+    static const std::regex re_content_disposition(
+        "^Content-Disposition:\\s*form-data;\\s*name=\"(.*?)\"(?:;\\s*filename="
+        "\"(.*?)\")?\\s*$",
+        std::regex_constants::icase);
+    static const std::string dash_ = "--";
+    static const std::string crlf_ = "\r\n";
+
+    buf_.append(buf, n); // TODO: performance improvement
+
+    while (!buf_.empty()) {
+      switch (state_) {
+      case 0: { // Initial boundary
+        auto pattern = dash_ + boundary_ + crlf_;
+        if (pattern.size() > buf_.size()) { return true; }
+        auto pos = buf_.find(pattern);
+        if (pos != 0) { return false; }
+        buf_.erase(0, pattern.size());
+        off_ += pattern.size();
+        state_ = 1;
+        break;
+      }
+      case 1: { // New entry
+        clear_file_info();
+        state_ = 2;
+        break;
+      }
+      case 2: { // Headers
+        auto pos = buf_.find(crlf_);
+        while (pos != std::string::npos) {
+          // Empty line
+          if (pos == 0) {
+            if (!header_callback(file_)) {
+              is_valid_ = false;
+              return false;
+            }
+            buf_.erase(0, crlf_.size());
+            off_ += crlf_.size();
+            state_ = 3;
+            break;
+          }
+
+          static const std::string header_name = "content-type:";
+          const auto header = buf_.substr(0, pos);
+          if (start_with_case_ignore(header, header_name)) {
+            file_.content_type = trim_copy(header.substr(header_name.size()));
+          } else {
+            std::smatch m;
+            if (std::regex_match(header, m, re_content_disposition)) {
+              file_.name = m[1];
+              file_.filename = m[2];
+            }
+          }
+
+          buf_.erase(0, pos + crlf_.size());
+          off_ += pos + crlf_.size();
+          pos = buf_.find(crlf_);
+        }
+        if (state_ != 3) { return true; }
+        break;
+      }
+      case 3: { // Body
+        {
+          auto pattern = crlf_ + dash_;
+          if (pattern.size() > buf_.size()) { return true; }
+
+          auto pos = find_string(buf_, pattern);
+
+          if (!content_callback(buf_.data(), pos)) {
+            is_valid_ = false;
+            return false;
+          }
+
+          off_ += pos;
+          buf_.erase(0, pos);
+        }
+        {
+          auto pattern = crlf_ + dash_ + boundary_;
+          if (pattern.size() > buf_.size()) { return true; }
+
+          auto pos = buf_.find(pattern);
+          if (pos != std::string::npos) {
+            if (!content_callback(buf_.data(), pos)) {
+              is_valid_ = false;
+              return false;
+            }
+
+            off_ += pos + pattern.size();
+            buf_.erase(0, pos + pattern.size());
+            state_ = 4;
+          } else {
+            if (!content_callback(buf_.data(), pattern.size())) {
+              is_valid_ = false;
+              return false;
+            }
+
+            off_ += pattern.size();
+            buf_.erase(0, pattern.size());
+          }
+        }
+        break;
+      }
+      case 4: { // Boundary
+        if (crlf_.size() > buf_.size()) { return true; }
+        if (buf_.compare(0, crlf_.size(), crlf_) == 0) {
+          buf_.erase(0, crlf_.size());
+          off_ += crlf_.size();
+          state_ = 1;
+        } else {
+          auto pattern = dash_ + crlf_;
+          if (pattern.size() > buf_.size()) { return true; }
+          if (buf_.compare(0, pattern.size(), pattern) == 0) {
+            buf_.erase(0, pattern.size());
+            off_ += pattern.size();
+            is_valid_ = true;
+            state_ = 5;
+          } else {
+            return true;
+          }
+        }
+        break;
+      }
+      case 5: { // Done
+        is_valid_ = false;
+        return false;
+      }
+      }
+    }
+
+    return true;
+  }
+
+private:
+  void clear_file_info() {
+    file_.name.clear();
+    file_.filename.clear();
+    file_.content_type.clear();
+  }
+
+  bool start_with_case_ignore(const std::string &a,
+                              const std::string &b) const {
+    if (a.size() < b.size()) { return false; }
+    for (size_t i = 0; i < b.size(); i++) {
+      if (::tolower(a[i]) != ::tolower(b[i])) { return false; }
+    }
+    return true;
+  }
+
+  bool start_with(const std::string &a, size_t off,
+                  const std::string &b) const {
+    if (a.size() - off < b.size()) { return false; }
+    for (size_t i = 0; i < b.size(); i++) {
+      if (a[i + off] != b[i]) { return false; }
+    }
+    return true;
+  }
+
+  size_t find_string(const std::string &s, const std::string &pattern) const {
+    auto c = pattern.front();
+
+    size_t off = 0;
+    while (off < s.size()) {
+      auto pos = s.find(c, off);
+      if (pos == std::string::npos) { return s.size(); }
+
+      auto rem = s.size() - pos;
+      if (pattern.size() > rem) { return pos; }
+
+      if (start_with(s, pos, pattern)) { return pos; }
+
+      off = pos + 1;
+    }
+
+    return s.size();
+  }
+
+  std::string boundary_;
+
+  std::string buf_;
+  size_t state_ = 0;
+  bool is_valid_ = false;
+  size_t off_ = 0;
+  MultipartFormData file_;
+};
+
+std::string to_lower(const char *beg, const char *end) {
+  std::string out;
+  auto it = beg;
+  while (it != end) {
+    out += static_cast<char>(::tolower(*it));
+    it++;
+  }
+  return out;
+}
+
+std::string make_multipart_data_boundary() {
+  static const char data[] =
+      "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
+  // std::random_device might actually be deterministic on some
+  // platforms, but due to lack of support in the c++ standard library,
+  // doing better requires either some ugly hacks or breaking portability.
+  std::random_device seed_gen;
+  // Request 128 bits of entropy for initialization
+  std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), seed_gen()};
+  std::mt19937 engine(seed_sequence);
+
+  std::string result = "--cpp-httplib-multipart-data-";
+
+  for (auto i = 0; i < 16; i++) {
+    result += data[engine() % (sizeof(data) - 1)];
+  }
+
+  return result;
+}
+
+std::pair<size_t, size_t>
+get_range_offset_and_length(const Request &req, size_t content_length,
+                            size_t index) {
+  auto r = req.ranges[index];
+
+  if (r.first == -1 && r.second == -1) {
+    return std::make_pair(0, content_length);
+  }
+
+  auto slen = static_cast<ssize_t>(content_length);
+
+  if (r.first == -1) {
+    r.first = (std::max)(static_cast<ssize_t>(0), slen - r.second);
+    r.second = slen - 1;
+  }
+
+  if (r.second == -1) { r.second = slen - 1; }
+  return std::make_pair(r.first, static_cast<size_t>(r.second - r.first) + 1);
+}
+
+std::string make_content_range_header_field(size_t offset, size_t length,
+                                                   size_t content_length) {
+  std::string field = "bytes ";
+  field += std::to_string(offset);
+  field += "-";
+  field += std::to_string(offset + length - 1);
+  field += "/";
+  field += std::to_string(content_length);
+  return field;
+}
+
+template <typename SToken, typename CToken, typename Content>
+bool process_multipart_ranges_data(const Request &req, Response &res,
+                                   const std::string &boundary,
+                                   const std::string &content_type,
+                                   SToken stoken, CToken ctoken,
+                                   Content content) {
+  for (size_t i = 0; i < req.ranges.size(); i++) {
+    ctoken("--");
+    stoken(boundary);
+    ctoken("\r\n");
+    if (!content_type.empty()) {
+      ctoken("Content-Type: ");
+      stoken(content_type);
+      ctoken("\r\n");
+    }
+
+    auto offsets = get_range_offset_and_length(req, res.body.size(), i);
+    auto offset = offsets.first;
+    auto length = offsets.second;
+
+    ctoken("Content-Range: ");
+    stoken(make_content_range_header_field(offset, length, res.body.size()));
+    ctoken("\r\n");
+    ctoken("\r\n");
+    if (!content(offset, length)) { return false; }
+    ctoken("\r\n");
+  }
+
+  ctoken("--");
+  stoken(boundary);
+  ctoken("--\r\n");
+
+  return true;
+}
+
+bool make_multipart_ranges_data(const Request &req, Response &res,
+                                       const std::string &boundary,
+                                       const std::string &content_type,
+                                       std::string &data) {
+  return process_multipart_ranges_data(
+      req, res, boundary, content_type,
+      [&](const std::string &token) { data += token; },
+      [&](const char *token) { data += token; },
+      [&](size_t offset, size_t length) {
+        if (offset < res.body.size()) {
+          data += res.body.substr(offset, length);
+          return true;
+        }
+        return false;
+      });
+}
+
+size_t
+get_multipart_ranges_data_length(const Request &req, Response &res,
+                                 const std::string &boundary,
+                                 const std::string &content_type) {
+  size_t data_length = 0;
+
+  process_multipart_ranges_data(
+      req, res, boundary, content_type,
+      [&](const std::string &token) { data_length += token.size(); },
+      [&](const char *token) { data_length += strlen(token); },
+      [&](size_t /*offset*/, size_t length) {
+        data_length += length;
+        return true;
+      });
+
+  return data_length;
+}
+
+template <typename T>
+bool write_multipart_ranges_data(Stream &strm, const Request &req,
+                                        Response &res,
+                                        const std::string &boundary,
+                                        const std::string &content_type,
+                                        const T &is_shutting_down) {
+  return process_multipart_ranges_data(
+      req, res, boundary, content_type,
+      [&](const std::string &token) { strm.write(token); },
+      [&](const char *token) { strm.write(token); },
+      [&](size_t offset, size_t length) {
+        return write_content(strm, res.content_provider_, offset, length,
+                             is_shutting_down);
+      });
+}
+
+std::pair<size_t, size_t>
+get_range_offset_and_length(const Request &req, const Response &res,
+                            size_t index) {
+  auto r = req.ranges[index];
+
+  if (r.second == -1) {
+    r.second = static_cast<ssize_t>(res.content_length_) - 1;
+  }
+
+  return std::make_pair(r.first, r.second - r.first + 1);
+}
+
+bool expect_content(const Request &req) {
+  if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" ||
+      req.method == "PRI" || req.method == "DELETE") {
+    return true;
+  }
+  // TODO: check if Content-Length is set
+  return false;
+}
+
+bool has_crlf(const char *s) {
+  auto p = s;
+  while (*p) {
+    if (*p == '\r' || *p == '\n') { return true; }
+    p++;
+  }
+  return false;
+}
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+template <typename CTX, typename Init, typename Update, typename Final>
+std::string message_digest(const std::string &s, Init init,
+                                  Update update, Final final,
+                                  size_t digest_length) {
+  using namespace std;
+
+  std::vector<unsigned char> md(digest_length, 0);
+  CTX ctx;
+  init(&ctx);
+  update(&ctx, s.data(), s.size());
+  final(md.data(), &ctx);
+
+  stringstream ss;
+  for (auto c : md) {
+    ss << setfill('0') << setw(2) << hex << (unsigned int)c;
+  }
+  return ss.str();
+}
+
+std::string MD5(const std::string &s) {
+  return message_digest<MD5_CTX>(s, MD5_Init, MD5_Update, MD5_Final,
+                                 MD5_DIGEST_LENGTH);
+}
+
+std::string SHA_256(const std::string &s) {
+  return message_digest<SHA256_CTX>(s, SHA256_Init, SHA256_Update, SHA256_Final,
+                                    SHA256_DIGEST_LENGTH);
+}
+
+std::string SHA_512(const std::string &s) {
+  return message_digest<SHA512_CTX>(s, SHA512_Init, SHA512_Update, SHA512_Final,
+                                    SHA512_DIGEST_LENGTH);
+}
+#endif
+
+#ifdef _WIN32
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+// NOTE: This code came up with the following stackoverflow post:
+// https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store
+bool load_system_certs_on_windows(X509_STORE *store) {
+  auto hStore = CertOpenSystemStoreW((HCRYPTPROV_LEGACY)NULL, L"ROOT");
+
+  if (!hStore) { return false; }
+
+  PCCERT_CONTEXT pContext = NULL;
+  while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) !=
+         nullptr) {
+    auto encoded_cert =
+        static_cast<const unsigned char *>(pContext->pbCertEncoded);
+
+    auto x509 = d2i_X509(NULL, &encoded_cert, pContext->cbCertEncoded);
+    if (x509) {
+      X509_STORE_add_cert(store, x509);
+      X509_free(x509);
+    }
+  }
+
+  CertFreeCertificateContext(pContext);
+  CertCloseStore(hStore, 0);
+
+  return true;
+}
+#endif
+
+class WSInit {
+public:
+  WSInit() {
+    WSADATA wsaData;
+    WSAStartup(0x0002, &wsaData);
+  }
+
+  ~WSInit() { WSACleanup(); }
+};
+
+static WSInit wsinit_;
+#endif
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+std::pair<std::string, std::string> make_digest_authentication_header(
+    const Request &req, const std::map<std::string, std::string> &auth,
+    size_t cnonce_count, const std::string &cnonce, const std::string &username,
+    const std::string &password, bool is_proxy = false) {
+  using namespace std;
+
+  string nc;
+  {
+    stringstream ss;
+    ss << setfill('0') << setw(8) << hex << cnonce_count;
+    nc = ss.str();
+  }
+
+  auto qop = auth.at("qop");
+  if (qop.find("auth-int") != std::string::npos) {
+    qop = "auth-int";
+  } else {
+    qop = "auth";
+  }
+
+  std::string algo = "MD5";
+  if (auth.find("algorithm") != auth.end()) { algo = auth.at("algorithm"); }
+
+  string response;
+  {
+    auto H = algo == "SHA-256"
+                 ? detail::SHA_256
+                 : algo == "SHA-512" ? detail::SHA_512 : detail::MD5;
+
+    auto A1 = username + ":" + auth.at("realm") + ":" + password;
+
+    auto A2 = req.method + ":" + req.path;
+    if (qop == "auth-int") { A2 += ":" + H(req.body); }
+
+    response = H(H(A1) + ":" + auth.at("nonce") + ":" + nc + ":" + cnonce +
+                 ":" + qop + ":" + H(A2));
+  }
+
+  auto field = "Digest username=\"" + username + "\", realm=\"" +
+               auth.at("realm") + "\", nonce=\"" + auth.at("nonce") +
+               "\", uri=\"" + req.path + "\", algorithm=" + algo +
+               ", qop=" + qop + ", nc=\"" + nc + "\", cnonce=\"" + cnonce +
+               "\", response=\"" + response + "\"";
+
+  auto key = is_proxy ? "Proxy-Authorization" : "Authorization";
+  return std::make_pair(key, field);
+}
+#endif
+
+bool parse_www_authenticate(const Response &res,
+                                   std::map<std::string, std::string> &auth,
+                                   bool is_proxy) {
+  auto auth_key = is_proxy ? "Proxy-Authenticate" : "WWW-Authenticate";
+  if (res.has_header(auth_key)) {
+    static auto re = std::regex(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~");
+    auto s = res.get_header_value(auth_key);
+    auto pos = s.find(' ');
+    if (pos != std::string::npos) {
+      auto type = s.substr(0, pos);
+      if (type == "Basic") {
+        return false;
+      } else if (type == "Digest") {
+        s = s.substr(pos + 1);
+        auto beg = std::sregex_iterator(s.begin(), s.end(), re);
+        for (auto i = beg; i != std::sregex_iterator(); ++i) {
+          auto m = *i;
+          auto key = s.substr(static_cast<size_t>(m.position(1)),
+                              static_cast<size_t>(m.length(1)));
+          auto val = m.length(2) > 0
+                         ? s.substr(static_cast<size_t>(m.position(2)),
+                                    static_cast<size_t>(m.length(2)))
+                         : s.substr(static_cast<size_t>(m.position(3)),
+                                    static_cast<size_t>(m.length(3)));
+          auth[key] = val;
+        }
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
+// https://stackoverflow.com/questions/440133/how-do-i-create-a-random-alpha-numeric-string-in-c/440240#answer-440240
+std::string random_string(size_t length) {
+  auto randchar = []() -> char {
+    const char charset[] = "0123456789"
+                           "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                           "abcdefghijklmnopqrstuvwxyz";
+    const size_t max_index = (sizeof(charset) - 1);
+    return charset[static_cast<size_t>(std::rand()) % max_index];
+  };
+  std::string str(length, 0);
+  std::generate_n(str.begin(), length, randchar);
+  return str;
+}
+
+class ContentProviderAdapter {
+public:
+  explicit ContentProviderAdapter(
+      ContentProviderWithoutLength &&content_provider)
+      : content_provider_(content_provider) {}
+
+  bool operator()(size_t offset, size_t, DataSink &sink) {
+    return content_provider_(offset, sink);
+  }
+
+private:
+  ContentProviderWithoutLength content_provider_;
+};
+
+} // namespace detail
+
+// Header utilities
+std::pair<std::string, std::string> make_range_header(Ranges ranges) {
+  std::string field = "bytes=";
+  auto i = 0;
+  for (auto r : ranges) {
+    if (i != 0) { field += ", "; }
+    if (r.first != -1) { field += std::to_string(r.first); }
+    field += '-';
+    if (r.second != -1) { field += std::to_string(r.second); }
+    i++;
+  }
+  return std::make_pair("Range", std::move(field));
+}
+
+std::pair<std::string, std::string>
+make_basic_authentication_header(const std::string &username,
+                                 const std::string &password, bool is_proxy) {
+  auto field = "Basic " + detail::base64_encode(username + ":" + password);
+  auto key = is_proxy ? "Proxy-Authorization" : "Authorization";
+  return std::make_pair(key, std::move(field));
+}
+
+std::pair<std::string, std::string>
+make_bearer_token_authentication_header(const std::string &token,
+                                        bool is_proxy = false) {
+  auto field = "Bearer " + token;
+  auto key = is_proxy ? "Proxy-Authorization" : "Authorization";
+  return std::make_pair(key, std::move(field));
+}
+
+// Request implementation
+bool Request::has_header(const char *key) const {
+  return detail::has_header(headers, key);
+}
+
+std::string Request::get_header_value(const char *key, size_t id) const {
+  return detail::get_header_value(headers, key, id, "");
+}
+
+size_t Request::get_header_value_count(const char *key) const {
+  auto r = headers.equal_range(key);
+  return static_cast<size_t>(std::distance(r.first, r.second));
+}
+
+void Request::set_header(const char *key, const char *val) {
+  if (!detail::has_crlf(key) && !detail::has_crlf(val)) {
+    headers.emplace(key, val);
+  }
+}
+
+void Request::set_header(const char *key, const std::string &val) {
+  if (!detail::has_crlf(key) && !detail::has_crlf(val.c_str())) {
+    headers.emplace(key, val);
+  }
+}
+
+bool Request::has_param(const char *key) const {
+  return params.find(key) != params.end();
+}
+
+std::string Request::get_param_value(const char *key, size_t id) const {
+  auto rng = params.equal_range(key);
+  auto it = rng.first;
+  std::advance(it, static_cast<ssize_t>(id));
+  if (it != rng.second) { return it->second; }
+  return std::string();
+}
+
+size_t Request::get_param_value_count(const char *key) const {
+  auto r = params.equal_range(key);
+  return static_cast<size_t>(std::distance(r.first, r.second));
+}
+
+bool Request::is_multipart_form_data() const {
+  const auto &content_type = get_header_value("Content-Type");
+  return !content_type.find("multipart/form-data");
+}
+
+bool Request::has_file(const char *key) const {
+  return files.find(key) != files.end();
+}
+
+MultipartFormData Request::get_file_value(const char *key) const {
+  auto it = files.find(key);
+  if (it != files.end()) { return it->second; }
+  return MultipartFormData();
+}
+
+// Response implementation
+bool Response::has_header(const char *key) const {
+  return headers.find(key) != headers.end();
+}
+
+std::string Response::get_header_value(const char *key,
+                                              size_t id) const {
+  return detail::get_header_value(headers, key, id, "");
+}
+
+size_t Response::get_header_value_count(const char *key) const {
+  auto r = headers.equal_range(key);
+  return static_cast<size_t>(std::distance(r.first, r.second));
+}
+
+void Response::set_header(const char *key, const char *val) {
+  if (!detail::has_crlf(key) && !detail::has_crlf(val)) {
+    headers.emplace(key, val);
+  }
+}
+
+void Response::set_header(const char *key, const std::string &val) {
+  if (!detail::has_crlf(key) && !detail::has_crlf(val.c_str())) {
+    headers.emplace(key, val);
+  }
+}
+
+void Response::set_redirect(const char *url, int stat) {
+  if (!detail::has_crlf(url)) {
+    set_header("Location", url);
+    if (300 <= stat && stat < 400) {
+      this->status = stat;
+    } else {
+      this->status = 302;
+    }
+  }
+}
+
+void Response::set_redirect(const std::string &url, int stat) {
+  set_redirect(url.c_str(), stat);
+}
+
+void Response::set_content(const char *s, size_t n,
+                                  const char *content_type) {
+  body.assign(s, n);
+
+  auto rng = headers.equal_range("Content-Type");
+  headers.erase(rng.first, rng.second);
+  set_header("Content-Type", content_type);
+}
+
+void Response::set_content(const std::string &s,
+                                  const char *content_type) {
+  set_content(s.data(), s.size(), content_type);
+}
+
+void Response::set_content_provider(
+    size_t in_length, const char *content_type, ContentProvider provider,
+    ContentProviderResourceReleaser resource_releaser) {
+  assert(in_length > 0);
+  set_header("Content-Type", content_type);
+  content_length_ = in_length;
+  content_provider_ = std::move(provider);
+  content_provider_resource_releaser_ = resource_releaser;
+  is_chunked_content_provider_ = false;
+}
+
+void Response::set_content_provider(
+    const char *content_type, ContentProviderWithoutLength provider,
+    ContentProviderResourceReleaser resource_releaser) {
+  set_header("Content-Type", content_type);
+  content_length_ = 0;
+  content_provider_ = detail::ContentProviderAdapter(std::move(provider));
+  content_provider_resource_releaser_ = resource_releaser;
+  is_chunked_content_provider_ = false;
+}
+
+void Response::set_chunked_content_provider(
+    const char *content_type, ContentProviderWithoutLength provider,
+    ContentProviderResourceReleaser resource_releaser) {
+  set_header("Content-Type", content_type);
+  content_length_ = 0;
+  content_provider_ = detail::ContentProviderAdapter(std::move(provider));
+  content_provider_resource_releaser_ = resource_releaser;
+  is_chunked_content_provider_ = true;
+}
+
+// Result implementation
+bool Result::has_request_header(const char *key) const {
+  return request_headers_.find(key) != request_headers_.end();
+}
+
+std::string Result::get_request_header_value(const char *key,
+                                                    size_t id) const {
+  return detail::get_header_value(request_headers_, key, id, "");
+}
+
+size_t Result::get_request_header_value_count(const char *key) const {
+  auto r = request_headers_.equal_range(key);
+  return static_cast<size_t>(std::distance(r.first, r.second));
+}
+
+// Stream implementation
+ssize_t Stream::write(const char *ptr) {
+  return write(ptr, strlen(ptr));
+}
+
+ssize_t Stream::write(const std::string &s) {
+  return write(s.data(), s.size());
+}
+
+namespace detail {
+
+// Socket stream implementation
+SocketStream::SocketStream(socket_t sock, time_t read_timeout_sec,
+                                  time_t read_timeout_usec,
+                                  time_t write_timeout_sec,
+                                  time_t write_timeout_usec)
+    : sock_(sock), read_timeout_sec_(read_timeout_sec),
+      read_timeout_usec_(read_timeout_usec),
+      write_timeout_sec_(write_timeout_sec),
+      write_timeout_usec_(write_timeout_usec) {}
+
+SocketStream::~SocketStream() {}
+
+bool SocketStream::is_readable() const {
+  return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0;
+}
+
+bool SocketStream::is_writable() const {
+  return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0;
+}
+
+ssize_t SocketStream::read(char *ptr, size_t size) {
+  if (!is_readable()) { return -1; }
+
+#ifdef _WIN32
+  if (size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
+    return -1;
+  }
+  return recv(sock_, ptr, static_cast<int>(size), CPPHTTPLIB_RECV_FLAGS);
+#else
+  return handle_EINTR(
+      [&]() { return recv(sock_, ptr, size, CPPHTTPLIB_RECV_FLAGS); });
+#endif
+}
+
+ssize_t SocketStream::write(const char *ptr, size_t size) {
+  if (!is_writable()) { return -1; }
+
+#ifdef _WIN32
+  if (size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
+    return -1;
+  }
+  return send(sock_, ptr, static_cast<int>(size), CPPHTTPLIB_SEND_FLAGS);
+#else
+  return handle_EINTR(
+      [&]() { return send(sock_, ptr, size, CPPHTTPLIB_SEND_FLAGS); });
+#endif
+}
+
+void SocketStream::get_remote_ip_and_port(std::string &ip,
+                                                 int &port) const {
+  return detail::get_remote_ip_and_port(sock_, ip, port);
+}
+
+socket_t SocketStream::socket() const { return sock_; }
+
+// Buffer stream implementation
+bool BufferStream::is_readable() const { return true; }
+
+bool BufferStream::is_writable() const { return true; }
+
+ssize_t BufferStream::read(char *ptr, size_t size) {
+#if defined(_MSC_VER) && _MSC_VER <= 1900
+  auto len_read = buffer._Copy_s(ptr, size, size, position);
+#else
+  auto len_read = buffer.copy(ptr, size, position);
+#endif
+  position += static_cast<size_t>(len_read);
+  return static_cast<ssize_t>(len_read);
+}
+
+ssize_t BufferStream::write(const char *ptr, size_t size) {
+  buffer.append(ptr, size);
+  return static_cast<ssize_t>(size);
+}
+
+void BufferStream::get_remote_ip_and_port(std::string & /*ip*/,
+                                                 int & /*port*/) const {}
+
+socket_t BufferStream::socket() const { return 0; }
+
+const std::string &BufferStream::get_buffer() const { return buffer; }
+
+} // namespace detail
+
+// HTTP server implementation
+Server::Server()
+    : new_task_queue(
+          [] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT); }),
+      svr_sock_(INVALID_SOCKET), is_running_(false) {
+#ifndef _WIN32
+  signal(SIGPIPE, SIG_IGN);
+#endif
+}
+
+Server::~Server() {}
+
+Server &Server::Get(const std::string &pattern, Handler handler) {
+  get_handlers_.push_back(
+      std::make_pair(std::regex(pattern), std::move(handler)));
+  return *this;
+}
+
+Server &Server::Post(const std::string &pattern, Handler handler) {
+  post_handlers_.push_back(
+      std::make_pair(std::regex(pattern), std::move(handler)));
+  return *this;
+}
+
+Server &Server::Post(const std::string &pattern,
+                            HandlerWithContentReader handler) {
+  post_handlers_for_content_reader_.push_back(
+      std::make_pair(std::regex(pattern), std::move(handler)));
+  return *this;
+}
+
+Server &Server::Put(const std::string &pattern, Handler handler) {
+  put_handlers_.push_back(
+      std::make_pair(std::regex(pattern), std::move(handler)));
+  return *this;
+}
+
+Server &Server::Put(const std::string &pattern,
+                           HandlerWithContentReader handler) {
+  put_handlers_for_content_reader_.push_back(
+      std::make_pair(std::regex(pattern), std::move(handler)));
+  return *this;
+}
+
+Server &Server::Patch(const std::string &pattern, Handler handler) {
+  patch_handlers_.push_back(
+      std::make_pair(std::regex(pattern), std::move(handler)));
+  return *this;
+}
+
+Server &Server::Patch(const std::string &pattern,
+                             HandlerWithContentReader handler) {
+  patch_handlers_for_content_reader_.push_back(
+      std::make_pair(std::regex(pattern), std::move(handler)));
+  return *this;
+}
+
+Server &Server::Delete(const std::string &pattern, Handler handler) {
+  delete_handlers_.push_back(
+      std::make_pair(std::regex(pattern), std::move(handler)));
+  return *this;
+}
+
+Server &Server::Delete(const std::string &pattern,
+                              HandlerWithContentReader handler) {
+  delete_handlers_for_content_reader_.push_back(
+      std::make_pair(std::regex(pattern), std::move(handler)));
+  return *this;
+}
+
+Server &Server::Options(const std::string &pattern, Handler handler) {
+  options_handlers_.push_back(
+      std::make_pair(std::regex(pattern), std::move(handler)));
+  return *this;
+}
+
+bool Server::set_base_dir(const std::string &dir,
+                                 const std::string &mount_point) {
+  return set_mount_point(mount_point, dir);
+}
+
+bool Server::set_mount_point(const std::string &mount_point,
+                                    const std::string &dir, Headers headers) {
+  if (detail::is_dir(dir)) {
+    std::string mnt = !mount_point.empty() ? mount_point : "/";
+    if (!mnt.empty() && mnt[0] == '/') {
+      base_dirs_.push_back({mnt, dir, std::move(headers)});
+      return true;
+    }
+  }
+  return false;
+}
+
+bool Server::remove_mount_point(const std::string &mount_point) {
+  for (auto it = base_dirs_.begin(); it != base_dirs_.end(); ++it) {
+    if (it->mount_point == mount_point) {
+      base_dirs_.erase(it);
+      return true;
+    }
+  }
+  return false;
+}
+
+Server &
+Server::set_file_extension_and_mimetype_mapping(const char *ext,
+                                                const char *mime) {
+  file_extension_and_mimetype_map_[ext] = mime;
+  return *this;
+}
+
+Server &Server::set_file_request_handler(Handler handler) {
+  file_request_handler_ = std::move(handler);
+  return *this;
+}
+
+Server &Server::set_error_handler(HandlerWithResponse handler) {
+  error_handler_ = std::move(handler);
+  return *this;
+}
+
+Server &Server::set_error_handler(Handler handler) {
+  error_handler_ = [handler](const Request &req, Response &res) {
+    handler(req, res);
+    return HandlerResponse::Handled;
+  };
+  return *this;
+}
+
+Server &Server::set_exception_handler(ExceptionHandler handler) {
+  exception_handler_ = std::move(handler);
+  return *this;
+}
+
+Server &Server::set_pre_routing_handler(HandlerWithResponse handler) {
+  pre_routing_handler_ = std::move(handler);
+  return *this;
+}
+
+Server &Server::set_post_routing_handler(Handler handler) {
+  post_routing_handler_ = std::move(handler);
+  return *this;
+}
+
+Server &Server::set_logger(Logger logger) {
+  logger_ = std::move(logger);
+  return *this;
+}
+
+Server &
+Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) {
+  expect_100_continue_handler_ = std::move(handler);
+
+  return *this;
+}
+
+Server &Server::set_address_family(int family) {
+  address_family_ = family;
+  return *this;
+}
+
+Server &Server::set_tcp_nodelay(bool on) {
+  tcp_nodelay_ = on;
+  return *this;
+}
+
+Server &Server::set_socket_options(SocketOptions socket_options) {
+  socket_options_ = std::move(socket_options);
+  return *this;
+}
+
+Server &Server::set_default_headers(Headers headers) {
+  default_headers_ = std::move(headers);
+  return *this;
+}
+
+Server &Server::set_keep_alive_max_count(size_t count) {
+  keep_alive_max_count_ = count;
+  return *this;
+}
+
+Server &Server::set_keep_alive_timeout(time_t sec) {
+  keep_alive_timeout_sec_ = sec;
+  return *this;
+}
+
+Server &Server::set_read_timeout(time_t sec, time_t usec) {
+  read_timeout_sec_ = sec;
+  read_timeout_usec_ = usec;
+  return *this;
+}
+
+Server &Server::set_write_timeout(time_t sec, time_t usec) {
+  write_timeout_sec_ = sec;
+  write_timeout_usec_ = usec;
+  return *this;
+}
+
+Server &Server::set_idle_interval(time_t sec, time_t usec) {
+  idle_interval_sec_ = sec;
+  idle_interval_usec_ = usec;
+  return *this;
+}
+
+Server &Server::set_payload_max_length(size_t length) {
+  payload_max_length_ = length;
+  return *this;
+}
+
+bool Server::bind_to_port(const char *host, int port, int socket_flags) {
+  if (bind_internal(host, port, socket_flags) < 0) return false;
+  return true;
+}
+int Server::bind_to_any_port(const char *host, int socket_flags) {
+  return bind_internal(host, 0, socket_flags);
+}
+
+bool Server::listen_after_bind() { return listen_internal(); }
+
+bool Server::listen(const char *host, int port, int socket_flags) {
+  return bind_to_port(host, port, socket_flags) && listen_internal();
+}
+
+bool Server::is_running() const { return is_running_; }
+
+void Server::stop() {
+  if (is_running_) {
+    assert(svr_sock_ != INVALID_SOCKET);
+    std::atomic<socket_t> sock(svr_sock_.exchange(INVALID_SOCKET));
+    detail::shutdown_socket(sock);
+    detail::close_socket(sock);
+  }
+}
+
+bool Server::parse_request_line(const char *s, Request &req) {
+  auto len = strlen(s);
+  if (len < 2 || s[len - 2] != '\r' || s[len - 1] != '\n') { return false; }
+  len -= 2;
+
+  {
+    size_t count = 0;
+
+    detail::split(s, s + len, ' ', [&](const char *b, const char *e) {
+      switch (count) {
+      case 0: req.method = std::string(b, e); break;
+      case 1: req.target = std::string(b, e); break;
+      case 2: req.version = std::string(b, e); break;
+      default: break;
+      }
+      count++;
+    });
+
+    if (count != 3) { return false; }
+  }
+
+  static const std::set<std::string> methods{
+      "GET",     "HEAD",    "POST",  "PUT",   "DELETE",
+      "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"};
+
+  if (methods.find(req.method) == methods.end()) { return false; }
+
+  if (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") { return false; }
+
+  {
+    size_t count = 0;
+
+    detail::split(req.target.data(), req.target.data() + req.target.size(), '?',
+                  [&](const char *b, const char *e) {
+                    switch (count) {
+                    case 0:
+                      req.path = detail::decode_url(std::string(b, e), false);
+                      break;
+                    case 1: {
+                      if (e - b > 0) {
+                        detail::parse_query_text(std::string(b, e), req.params);
+                      }
+                      break;
+                    }
+                    default: break;
+                    }
+                    count++;
+                  });
+
+    if (count > 2) { return false; }
+  }
+
+  return true;
+}
+
+bool Server::write_response(Stream &strm, bool close_connection,
+                                   const Request &req, Response &res) {
+  return write_response_core(strm, close_connection, req, res, false);
+}
+
+bool Server::write_response_with_content(Stream &strm,
+                                                bool close_connection,
+                                                const Request &req,
+                                                Response &res) {
+  return write_response_core(strm, close_connection, req, res, true);
+}
+
+bool Server::write_response_core(Stream &strm, bool close_connection,
+                                        const Request &req, Response &res,
+                                        bool need_apply_ranges) {
+  assert(res.status != -1);
+
+  if (400 <= res.status && error_handler_ &&
+      error_handler_(req, res) == HandlerResponse::Handled) {
+    need_apply_ranges = true;
+  }
+
+  std::string content_type;
+  std::string boundary;
+  if (need_apply_ranges) { apply_ranges(req, res, content_type, boundary); }
+
+  // Prepare additional headers
+  if (close_connection || req.get_header_value("Connection") == "close") {
+    res.set_header("Connection", "close");
+  } else {
+    std::stringstream ss;
+    ss << "timeout=" << keep_alive_timeout_sec_
+       << ", max=" << keep_alive_max_count_;
+    res.set_header("Keep-Alive", ss.str());
+  }
+
+  if (!res.has_header("Content-Type") &&
+      (!res.body.empty() || res.content_length_ > 0 || res.content_provider_)) {
+    res.set_header("Content-Type", "text/plain");
+  }
+
+  if (!res.has_header("Content-Length") && res.body.empty() &&
+      !res.content_length_ && !res.content_provider_) {
+    res.set_header("Content-Length", "0");
+  }
+
+  if (!res.has_header("Accept-Ranges") && req.method == "HEAD") {
+    res.set_header("Accept-Ranges", "bytes");
+  }
+
+  if (post_routing_handler_) { post_routing_handler_(req, res); }
+
+  // Response line and headers
+  {
+    detail::BufferStream bstrm;
+
+    if (!bstrm.write_format("HTTP/1.1 %d %s\r\n", res.status,
+                            detail::status_message(res.status))) {
+      return false;
+    }
+
+    if (!detail::write_headers(bstrm, res.headers)) { return false; }
+
+    // Flush buffer
+    auto &data = bstrm.get_buffer();
+    strm.write(data.data(), data.size());
+  }
+
+  // Body
+  auto ret = true;
+  if (req.method != "HEAD") {
+    if (!res.body.empty()) {
+      if (!strm.write(res.body)) { ret = false; }
+    } else if (res.content_provider_) {
+      if (write_content_with_provider(strm, req, res, boundary, content_type)) {
+        res.content_provider_success_ = true;
+      } else {
+        res.content_provider_success_ = false;
+        ret = false;
+      }
+    }
+  }
+
+  // Log
+  if (logger_) { logger_(req, res); }
+
+  return ret;
+}
+
+bool
+Server::write_content_with_provider(Stream &strm, const Request &req,
+                                    Response &res, const std::string &boundary,
+                                    const std::string &content_type) {
+  auto is_shutting_down = [this]() {
+    return this->svr_sock_ == INVALID_SOCKET;
+  };
+
+  if (res.content_length_ > 0) {
+    if (req.ranges.empty()) {
+      return detail::write_content(strm, res.content_provider_, 0,
+                                   res.content_length_, is_shutting_down);
+    } else if (req.ranges.size() == 1) {
+      auto offsets =
+          detail::get_range_offset_and_length(req, res.content_length_, 0);
+      auto offset = offsets.first;
+      auto length = offsets.second;
+      return detail::write_content(strm, res.content_provider_, offset, length,
+                                   is_shutting_down);
+    } else {
+      return detail::write_multipart_ranges_data(
+          strm, req, res, boundary, content_type, is_shutting_down);
+    }
+  } else {
+    if (res.is_chunked_content_provider_) {
+      auto type = detail::encoding_type(req, res);
+
+      std::unique_ptr<detail::compressor> compressor;
+      if (type == detail::EncodingType::Gzip) {
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+        compressor = detail::make_unique<detail::gzip_compressor>();
+#endif
+      } else if (type == detail::EncodingType::Brotli) {
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+        compressor = detail::make_unique<detail::brotli_compressor>();
+#endif
+      } else {
+        compressor = detail::make_unique<detail::nocompressor>();
+      }
+      assert(compressor != nullptr);
+
+      return detail::write_content_chunked(strm, res.content_provider_,
+                                           is_shutting_down, *compressor);
+    } else {
+      return detail::write_content_without_length(strm, res.content_provider_,
+                                                  is_shutting_down);
+    }
+  }
+}
+
+bool Server::read_content(Stream &strm, Request &req, Response &res) {
+  MultipartFormDataMap::iterator cur;
+  if (read_content_core(
+          strm, req, res,
+          // Regular
+          [&](const char *buf, size_t n) {
+            if (req.body.size() + n > req.body.max_size()) { return false; }
+            req.body.append(buf, n);
+            return true;
+          },
+          // Multipart
+          [&](const MultipartFormData &file) {
+            cur = req.files.emplace(file.name, file);
+            return true;
+          },
+          [&](const char *buf, size_t n) {
+            auto &content = cur->second.content;
+            if (content.size() + n > content.max_size()) { return false; }
+            content.append(buf, n);
+            return true;
+          })) {
+    const auto &content_type = req.get_header_value("Content-Type");
+    if (!content_type.find("application/x-www-form-urlencoded")) {
+      detail::parse_query_text(req.body, req.params);
+    }
+    return true;
+  }
+  return false;
+}
+
+bool Server::read_content_with_content_receiver(
+    Stream &strm, Request &req, Response &res, ContentReceiver receiver,
+    MultipartContentHeader multipart_header,
+    ContentReceiver multipart_receiver) {
+  return read_content_core(strm, req, res, std::move(receiver),
+                           std::move(multipart_header),
+                           std::move(multipart_receiver));
+}
+
+bool Server::read_content_core(Stream &strm, Request &req, Response &res,
+                                      ContentReceiver receiver,
+                                      MultipartContentHeader mulitpart_header,
+                                      ContentReceiver multipart_receiver) {
+  detail::MultipartFormDataParser multipart_form_data_parser;
+  ContentReceiverWithProgress out;
+
+  if (req.is_multipart_form_data()) {
+    const auto &content_type = req.get_header_value("Content-Type");
+    std::string boundary;
+    if (!detail::parse_multipart_boundary(content_type, boundary)) {
+      res.status = 400;
+      return false;
+    }
+
+    multipart_form_data_parser.set_boundary(std::move(boundary));
+    out = [&](const char *buf, size_t n, uint64_t /*off*/, uint64_t /*len*/) {
+      /* For debug
+      size_t pos = 0;
+      while (pos < n) {
+        auto read_size = std::min<size_t>(1, n - pos);
+        auto ret = multipart_form_data_parser.parse(
+            buf + pos, read_size, multipart_receiver, mulitpart_header);
+        if (!ret) { return false; }
+        pos += read_size;
+      }
+      return true;
+      */
+      return multipart_form_data_parser.parse(buf, n, multipart_receiver,
+                                              mulitpart_header);
+    };
+  } else {
+    out = [receiver](const char *buf, size_t n, uint64_t /*off*/,
+                     uint64_t /*len*/) { return receiver(buf, n); };
+  }
+
+  if (req.method == "DELETE" && !req.has_header("Content-Length")) {
+    return true;
+  }
+
+  if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr,
+                            out, true)) {
+    return false;
+  }
+
+  if (req.is_multipart_form_data()) {
+    if (!multipart_form_data_parser.is_valid()) {
+      res.status = 400;
+      return false;
+    }
+  }
+
+  return true;
+}
+
+bool Server::handle_file_request(const Request &req, Response &res,
+                                        bool head) {
+  for (const auto &entry : base_dirs_) {
+    // Prefix match
+    if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) {
+      std::string sub_path = "/" + req.path.substr(entry.mount_point.size());
+      if (detail::is_valid_path(sub_path)) {
+        auto path = entry.base_dir + sub_path;
+        if (path.back() == '/') { path += "index.html"; }
+
+        if (detail::is_file(path)) {
+          detail::read_file(path, res.body);
+          auto type =
+              detail::find_content_type(path, file_extension_and_mimetype_map_);
+          if (type) { res.set_header("Content-Type", type); }
+          for (const auto &kv : entry.headers) {
+            res.set_header(kv.first.c_str(), kv.second);
+          }
+          res.status = req.has_header("Range") ? 206 : 200;
+          if (!head && file_request_handler_) {
+            file_request_handler_(req, res);
+          }
+          return true;
+        }
+      }
+    }
+  }
+  return false;
+}
+
+socket_t
+Server::create_server_socket(const char *host, int port, int socket_flags,
+                             SocketOptions socket_options) const {
+  return detail::create_socket(
+      host, port, address_family_, socket_flags, tcp_nodelay_,
+      std::move(socket_options),
+      [](socket_t sock, struct addrinfo &ai) -> bool {
+        if (::bind(sock, ai.ai_addr, static_cast<socklen_t>(ai.ai_addrlen))) {
+          return false;
+        }
+        if (::listen(sock, 5)) { // Listen through 5 channels
+          return false;
+        }
+        return true;
+      });
+}
+
+int Server::bind_internal(const char *host, int port, int socket_flags) {
+  if (!is_valid()) { return -1; }
+
+  svr_sock_ = create_server_socket(host, port, socket_flags, socket_options_);
+  if (svr_sock_ == INVALID_SOCKET) { return -1; }
+
+  if (port == 0) {
+    struct sockaddr_storage addr;
+    socklen_t addr_len = sizeof(addr);
+    if (getsockname(svr_sock_, reinterpret_cast<struct sockaddr *>(&addr),
+                    &addr_len) == -1) {
+      return -1;
+    }
+    if (addr.ss_family == AF_INET) {
+      return ntohs(reinterpret_cast<struct sockaddr_in *>(&addr)->sin_port);
+    } else if (addr.ss_family == AF_INET6) {
+      return ntohs(reinterpret_cast<struct sockaddr_in6 *>(&addr)->sin6_port);
+    } else {
+      return -1;
+    }
+  } else {
+    return port;
+  }
+}
+
+bool Server::listen_internal() {
+  auto ret = true;
+  is_running_ = true;
+
+  {
+    std::unique_ptr<TaskQueue> task_queue(new_task_queue());
+
+    while (svr_sock_ != INVALID_SOCKET) {
+#ifndef _WIN32
+      if (idle_interval_sec_ > 0 || idle_interval_usec_ > 0) {
+#endif
+        auto val = detail::select_read(svr_sock_, idle_interval_sec_,
+                                       idle_interval_usec_);
+        if (val == 0) { // Timeout
+          task_queue->on_idle();
+          continue;
+        }
+#ifndef _WIN32
+      }
+#endif
+      socket_t sock = accept(svr_sock_, nullptr, nullptr);
+
+      if (sock == INVALID_SOCKET) {
+        if (errno == EMFILE) {
+          // The per-process limit of open file descriptors has been reached.
+          // Try to accept new connections after a short sleep.
+          std::this_thread::sleep_for(std::chrono::milliseconds(1));
+          continue;
+        }
+        if (svr_sock_ != INVALID_SOCKET) {
+          detail::close_socket(svr_sock_);
+          ret = false;
+        } else {
+          ; // The server socket was closed by user.
+        }
+        break;
+      }
+
+      {
+        timeval tv;
+        tv.tv_sec = static_cast<long>(read_timeout_sec_);
+        tv.tv_usec = static_cast<decltype(tv.tv_usec)>(read_timeout_usec_);
+        setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv));
+      }
+      {
+        timeval tv;
+        tv.tv_sec = static_cast<long>(write_timeout_sec_);
+        tv.tv_usec = static_cast<decltype(tv.tv_usec)>(write_timeout_usec_);
+        setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv));
+      }
+
+#if __cplusplus > 201703L
+      task_queue->enqueue([=, this]() { process_and_close_socket(sock); });
+#else
+      task_queue->enqueue([=]() { process_and_close_socket(sock); });
+#endif
+    }
+
+    task_queue->shutdown();
+  }
+
+  is_running_ = false;
+  return ret;
+}
+
+bool Server::routing(Request &req, Response &res, Stream &strm) {
+  if (pre_routing_handler_ &&
+      pre_routing_handler_(req, res) == HandlerResponse::Handled) {
+    return true;
+  }
+
+  // File handler
+  bool is_head_request = req.method == "HEAD";
+  if ((req.method == "GET" || is_head_request) &&
+      handle_file_request(req, res, is_head_request)) {
+    return true;
+  }
+
+  if (detail::expect_content(req)) {
+    // Content reader handler
+    {
+      ContentReader reader(
+          [&](ContentReceiver receiver) {
+            return read_content_with_content_receiver(
+                strm, req, res, std::move(receiver), nullptr, nullptr);
+          },
+          [&](MultipartContentHeader header, ContentReceiver receiver) {
+            return read_content_with_content_receiver(strm, req, res, nullptr,
+                                                      std::move(header),
+                                                      std::move(receiver));
+          });
+
+      if (req.method == "POST") {
+        if (dispatch_request_for_content_reader(
+                req, res, std::move(reader),
+                post_handlers_for_content_reader_)) {
+          return true;
+        }
+      } else if (req.method == "PUT") {
+        if (dispatch_request_for_content_reader(
+                req, res, std::move(reader),
+                put_handlers_for_content_reader_)) {
+          return true;
+        }
+      } else if (req.method == "PATCH") {
+        if (dispatch_request_for_content_reader(
+                req, res, std::move(reader),
+                patch_handlers_for_content_reader_)) {
+          return true;
+        }
+      } else if (req.method == "DELETE") {
+        if (dispatch_request_for_content_reader(
+                req, res, std::move(reader),
+                delete_handlers_for_content_reader_)) {
+          return true;
+        }
+      }
+    }
+
+    // Read content into `req.body`
+    if (!read_content(strm, req, res)) { return false; }
+  }
+
+  // Regular handler
+  if (req.method == "GET" || req.method == "HEAD") {
+    return dispatch_request(req, res, get_handlers_);
+  } else if (req.method == "POST") {
+    return dispatch_request(req, res, post_handlers_);
+  } else if (req.method == "PUT") {
+    return dispatch_request(req, res, put_handlers_);
+  } else if (req.method == "DELETE") {
+    return dispatch_request(req, res, delete_handlers_);
+  } else if (req.method == "OPTIONS") {
+    return dispatch_request(req, res, options_handlers_);
+  } else if (req.method == "PATCH") {
+    return dispatch_request(req, res, patch_handlers_);
+  }
+
+  res.status = 400;
+  return false;
+}
+
+bool Server::dispatch_request(Request &req, Response &res,
+                                     const Handlers &handlers) {
+  for (const auto &x : handlers) {
+    const auto &pattern = x.first;
+    const auto &handler = x.second;
+
+    if (std::regex_match(req.path, req.matches, pattern)) {
+      handler(req, res);
+      return true;
+    }
+  }
+  return false;
+}
+
+void Server::apply_ranges(const Request &req, Response &res,
+                                 std::string &content_type,
+                                 std::string &boundary) {
+  if (req.ranges.size() > 1) {
+    boundary = detail::make_multipart_data_boundary();
+
+    auto it = res.headers.find("Content-Type");
+    if (it != res.headers.end()) {
+      content_type = it->second;
+      res.headers.erase(it);
+    }
+
+    res.headers.emplace("Content-Type",
+                        "multipart/byteranges; boundary=" + boundary);
+  }
+
+  auto type = detail::encoding_type(req, res);
+
+  if (res.body.empty()) {
+    if (res.content_length_ > 0) {
+      size_t length = 0;
+      if (req.ranges.empty()) {
+        length = res.content_length_;
+      } else if (req.ranges.size() == 1) {
+        auto offsets =
+            detail::get_range_offset_and_length(req, res.content_length_, 0);
+        auto offset = offsets.first;
+        length = offsets.second;
+        auto content_range = detail::make_content_range_header_field(
+            offset, length, res.content_length_);
+        res.set_header("Content-Range", content_range);
+      } else {
+        length = detail::get_multipart_ranges_data_length(req, res, boundary,
+                                                          content_type);
+      }
+      res.set_header("Content-Length", std::to_string(length));
+    } else {
+      if (res.content_provider_) {
+        if (res.is_chunked_content_provider_) {
+          res.set_header("Transfer-Encoding", "chunked");
+          if (type == detail::EncodingType::Gzip) {
+            res.set_header("Content-Encoding", "gzip");
+          } else if (type == detail::EncodingType::Brotli) {
+            res.set_header("Content-Encoding", "br");
+          }
+        }
+      }
+    }
+  } else {
+    if (req.ranges.empty()) {
+      ;
+    } else if (req.ranges.size() == 1) {
+      auto offsets =
+          detail::get_range_offset_and_length(req, res.body.size(), 0);
+      auto offset = offsets.first;
+      auto length = offsets.second;
+      auto content_range = detail::make_content_range_header_field(
+          offset, length, res.body.size());
+      res.set_header("Content-Range", content_range);
+      if (offset < res.body.size()) {
+        res.body = res.body.substr(offset, length);
+      } else {
+        res.body.clear();
+        res.status = 416;
+      }
+    } else {
+      std::string data;
+      if (detail::make_multipart_ranges_data(req, res, boundary, content_type,
+                                             data)) {
+        res.body.swap(data);
+      } else {
+        res.body.clear();
+        res.status = 416;
+      }
+    }
+
+    if (type != detail::EncodingType::None) {
+      std::unique_ptr<detail::compressor> compressor;
+      std::string content_encoding;
+
+      if (type == detail::EncodingType::Gzip) {
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+        compressor = detail::make_unique<detail::gzip_compressor>();
+        content_encoding = "gzip";
+#endif
+      } else if (type == detail::EncodingType::Brotli) {
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+        compressor = detail::make_unique<detail::brotli_compressor>();
+        content_encoding = "br";
+#endif
+      }
+
+      if (compressor) {
+        std::string compressed;
+        if (compressor->compress(res.body.data(), res.body.size(), true,
+                                 [&](const char *data, size_t data_len) {
+                                   compressed.append(data, data_len);
+                                   return true;
+                                 })) {
+          res.body.swap(compressed);
+          res.set_header("Content-Encoding", content_encoding);
+        }
+      }
+    }
+
+    auto length = std::to_string(res.body.size());
+    res.set_header("Content-Length", length);
+  }
+}
+
+bool Server::dispatch_request_for_content_reader(
+    Request &req, Response &res, ContentReader content_reader,
+    const HandlersForContentReader &handlers) {
+  for (const auto &x : handlers) {
+    const auto &pattern = x.first;
+    const auto &handler = x.second;
+
+    if (std::regex_match(req.path, req.matches, pattern)) {
+      handler(req, res, content_reader);
+      return true;
+    }
+  }
+  return false;
+}
+
+bool
+Server::process_request(Stream &strm, bool close_connection,
+                        bool &connection_closed,
+                        const std::function<void(Request &)> &setup_request) {
+  std::array<char, 2048> buf{};
+
+  detail::stream_line_reader line_reader(strm, buf.data(), buf.size());
+
+  // Connection has been closed on client
+  if (!line_reader.getline()) { return false; }
+
+  Request req;
+  Response res;
+
+  res.version = "HTTP/1.1";
+
+  for (const auto &header : default_headers_) {
+    if (res.headers.find(header.first) == res.headers.end()) {
+      res.headers.insert(header);
+    }
+  }
+
+#ifdef _WIN32
+  // TODO: Increase FD_SETSIZE statically (libzmq), dynamically (MySQL).
+#else
+#ifndef CPPHTTPLIB_USE_POLL
+  // Socket file descriptor exceeded FD_SETSIZE...
+  if (strm.socket() >= FD_SETSIZE) {
+    Headers dummy;
+    detail::read_headers(strm, dummy);
+    res.status = 500;
+    return write_response(strm, close_connection, req, res);
+  }
+#endif
+#endif
+
+  // Check if the request URI doesn't exceed the limit
+  if (line_reader.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
+    Headers dummy;
+    detail::read_headers(strm, dummy);
+    res.status = 414;
+    return write_response(strm, close_connection, req, res);
+  }
+
+  // Request line and headers
+  if (!parse_request_line(line_reader.ptr(), req) ||
+      !detail::read_headers(strm, req.headers)) {
+    res.status = 400;
+    return write_response(strm, close_connection, req, res);
+  }
+
+  if (req.get_header_value("Connection") == "close") {
+    connection_closed = true;
+  }
+
+  if (req.version == "HTTP/1.0" &&
+      req.get_header_value("Connection") != "Keep-Alive") {
+    connection_closed = true;
+  }
+
+  strm.get_remote_ip_and_port(req.remote_addr, req.remote_port);
+  req.set_header("REMOTE_ADDR", req.remote_addr);
+  req.set_header("REMOTE_PORT", std::to_string(req.remote_port));
+
+  if (req.has_header("Range")) {
+    const auto &range_header_value = req.get_header_value("Range");
+    if (!detail::parse_range_header(range_header_value, req.ranges)) {
+      res.status = 416;
+      return write_response(strm, close_connection, req, res);
+    }
+  }
+
+  if (setup_request) { setup_request(req); }
+
+  if (req.get_header_value("Expect") == "100-continue") {
+    auto status = 100;
+    if (expect_100_continue_handler_) {
+      status = expect_100_continue_handler_(req, res);
+    }
+    switch (status) {
+    case 100:
+    case 417:
+      strm.write_format("HTTP/1.1 %d %s\r\n\r\n", status,
+                        detail::status_message(status));
+      break;
+    default: return write_response(strm, close_connection, req, res);
+    }
+  }
+
+  // Rounting
+  bool routed = false;
+  try {
+    routed = routing(req, res, strm);
+  } catch (std::exception &e) {
+    if (exception_handler_) {
+      exception_handler_(req, res, e);
+      routed = true;
+    } else {
+      res.status = 500;
+      res.set_header("EXCEPTION_WHAT", e.what());
+    }
+  } catch (...) {
+    res.status = 500;
+    res.set_header("EXCEPTION_WHAT", "UNKNOWN");
+  }
+
+  if (routed) {
+    if (res.status == -1) { res.status = req.ranges.empty() ? 200 : 206; }
+    return write_response_with_content(strm, close_connection, req, res);
+  } else {
+    if (res.status == -1) { res.status = 404; }
+    return write_response(strm, close_connection, req, res);
+  }
+}
+
+bool Server::is_valid() const { return true; }
+
+bool Server::process_and_close_socket(socket_t sock) {
+  auto ret = detail::process_server_socket(
+      sock, keep_alive_max_count_, keep_alive_timeout_sec_, read_timeout_sec_,
+      read_timeout_usec_, write_timeout_sec_, write_timeout_usec_,
+      [this](Stream &strm, bool close_connection, bool &connection_closed) {
+        return process_request(strm, close_connection, connection_closed,
+                               nullptr);
+      });
+
+  detail::shutdown_socket(sock);
+  detail::close_socket(sock);
+  return ret;
+}
+
+// HTTP client implementation
+ClientImpl::ClientImpl(const std::string &host)
+    : ClientImpl(host, 80, std::string(), std::string()) {}
+
+ClientImpl::ClientImpl(const std::string &host, int port)
+    : ClientImpl(host, port, std::string(), std::string()) {}
+
+ClientImpl::ClientImpl(const std::string &host, int port,
+                              const std::string &client_cert_path,
+                              const std::string &client_key_path)
+    : host_(host), port_(port),
+      host_and_port_(adjust_host_string(host) + ":" + std::to_string(port)),
+      client_cert_path_(client_cert_path), client_key_path_(client_key_path) {}
+
+ClientImpl::~ClientImpl() {
+  std::lock_guard<std::mutex> guard(socket_mutex_);
+  shutdown_socket(socket_);
+  close_socket(socket_);
+}
+
+bool ClientImpl::is_valid() const { return true; }
+
+void ClientImpl::copy_settings(const ClientImpl &rhs) {
+  client_cert_path_ = rhs.client_cert_path_;
+  client_key_path_ = rhs.client_key_path_;
+  connection_timeout_sec_ = rhs.connection_timeout_sec_;
+  read_timeout_sec_ = rhs.read_timeout_sec_;
+  read_timeout_usec_ = rhs.read_timeout_usec_;
+  write_timeout_sec_ = rhs.write_timeout_sec_;
+  write_timeout_usec_ = rhs.write_timeout_usec_;
+  basic_auth_username_ = rhs.basic_auth_username_;
+  basic_auth_password_ = rhs.basic_auth_password_;
+  bearer_token_auth_token_ = rhs.bearer_token_auth_token_;
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  digest_auth_username_ = rhs.digest_auth_username_;
+  digest_auth_password_ = rhs.digest_auth_password_;
+#endif
+  keep_alive_ = rhs.keep_alive_;
+  follow_location_ = rhs.follow_location_;
+  url_encode_ = rhs.url_encode_;
+  address_family_ = rhs.address_family_;
+  tcp_nodelay_ = rhs.tcp_nodelay_;
+  socket_options_ = rhs.socket_options_;
+  compress_ = rhs.compress_;
+  decompress_ = rhs.decompress_;
+  interface_ = rhs.interface_;
+  proxy_host_ = rhs.proxy_host_;
+  proxy_port_ = rhs.proxy_port_;
+  proxy_basic_auth_username_ = rhs.proxy_basic_auth_username_;
+  proxy_basic_auth_password_ = rhs.proxy_basic_auth_password_;
+  proxy_bearer_token_auth_token_ = rhs.proxy_bearer_token_auth_token_;
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  proxy_digest_auth_username_ = rhs.proxy_digest_auth_username_;
+  proxy_digest_auth_password_ = rhs.proxy_digest_auth_password_;
+#endif
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  ca_cert_file_path_ = rhs.ca_cert_file_path_;
+  ca_cert_dir_path_ = rhs.ca_cert_dir_path_;
+  ca_cert_store_ = rhs.ca_cert_store_;
+#endif
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  server_certificate_verification_ = rhs.server_certificate_verification_;
+#endif
+  logger_ = rhs.logger_;
+}
+
+socket_t ClientImpl::create_client_socket(Error &error) const {
+  if (!proxy_host_.empty() && proxy_port_ != -1) {
+    return detail::create_client_socket(
+        proxy_host_.c_str(), proxy_port_, address_family_, tcp_nodelay_,
+        socket_options_, connection_timeout_sec_, connection_timeout_usec_,
+        read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
+        write_timeout_usec_, interface_, error);
+  }
+  return detail::create_client_socket(
+      host_.c_str(), port_, address_family_, tcp_nodelay_, socket_options_,
+      connection_timeout_sec_, connection_timeout_usec_, read_timeout_sec_,
+      read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, interface_,
+      error);
+}
+
+bool ClientImpl::create_and_connect_socket(Socket &socket,
+                                                  Error &error) {
+  auto sock = create_client_socket(error);
+  if (sock == INVALID_SOCKET) { return false; }
+  socket.sock = sock;
+  return true;
+}
+
+void ClientImpl::shutdown_ssl(Socket & /*socket*/,
+                                     bool /*shutdown_gracefully*/) {
+  // If there are any requests in flight from threads other than us, then it's
+  // a thread-unsafe race because individual ssl* objects are not thread-safe.
+  assert(socket_requests_in_flight_ == 0 ||
+         socket_requests_are_from_thread_ == std::this_thread::get_id());
+}
+
+void ClientImpl::shutdown_socket(Socket &socket) {
+  if (socket.sock == INVALID_SOCKET) { return; }
+  detail::shutdown_socket(socket.sock);
+}
+
+void ClientImpl::close_socket(Socket &socket) {
+  // If there are requests in flight in another thread, usually closing
+  // the socket will be fine and they will simply receive an error when
+  // using the closed socket, but it is still a bug since rarely the OS
+  // may reassign the socket id to be used for a new socket, and then
+  // suddenly they will be operating on a live socket that is different
+  // than the one they intended!
+  assert(socket_requests_in_flight_ == 0 ||
+         socket_requests_are_from_thread_ == std::this_thread::get_id());
+
+  // It is also a bug if this happens while SSL is still active
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  assert(socket.ssl == nullptr);
+#endif
+  if (socket.sock == INVALID_SOCKET) { return; }
+  detail::close_socket(socket.sock);
+  socket.sock = INVALID_SOCKET;
+}
+
+bool ClientImpl::read_response_line(Stream &strm, const Request &req,
+                                           Response &res) {
+  std::array<char, 2048> buf;
+
+  detail::stream_line_reader line_reader(strm, buf.data(), buf.size());
+
+  if (!line_reader.getline()) { return false; }
+
+  const static std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n");
+
+  std::cmatch m;
+  if (!std::regex_match(line_reader.ptr(), m, re)) {
+    return req.method == "CONNECT";
+  }
+  res.version = std::string(m[1]);
+  res.status = std::stoi(std::string(m[2]));
+  res.reason = std::string(m[3]);
+
+  // Ignore '100 Continue'
+  while (res.status == 100) {
+    if (!line_reader.getline()) { return false; } // CRLF
+    if (!line_reader.getline()) { return false; } // next response line
+
+    if (!std::regex_match(line_reader.ptr(), m, re)) { return false; }
+    res.version = std::string(m[1]);
+    res.status = std::stoi(std::string(m[2]));
+    res.reason = std::string(m[3]);
+  }
+
+  return true;
+}
+
+bool ClientImpl::send(Request &req, Response &res, Error &error) {
+  std::lock_guard<std::recursive_mutex> request_mutex_guard(request_mutex_);
+
+  {
+    std::lock_guard<std::mutex> guard(socket_mutex_);
+    // Set this to false immediately - if it ever gets set to true by the end of
+    // the request, we know another thread instructed us to close the socket.
+    socket_should_be_closed_when_request_is_done_ = false;
+
+    auto is_alive = false;
+    if (socket_.is_open()) {
+      is_alive = detail::select_write(socket_.sock, 0, 0) > 0;
+      if (!is_alive) {
+        // Attempt to avoid sigpipe by shutting down nongracefully if it seems
+        // like the other side has already closed the connection Also, there
+        // cannot be any requests in flight from other threads since we locked
+        // request_mutex_, so safe to close everything immediately
+        const bool shutdown_gracefully = false;
+        shutdown_ssl(socket_, shutdown_gracefully);
+        shutdown_socket(socket_);
+        close_socket(socket_);
+      }
+    }
+
+    if (!is_alive) {
+      if (!create_and_connect_socket(socket_, error)) { return false; }
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+      // TODO: refactoring
+      if (is_ssl()) {
+        auto &scli = static_cast<SSLClient &>(*this);
+        if (!proxy_host_.empty() && proxy_port_ != -1) {
+          bool success = false;
+          if (!scli.connect_with_proxy(socket_, res, success, error)) {
+            return success;
+          }
+        }
+
+        if (!scli.initialize_ssl(socket_, error)) { return false; }
+      }
+#endif
+    }
+
+    // Mark the current socket as being in use so that it cannot be closed by
+    // anyone else while this request is ongoing, even though we will be
+    // releasing the mutex.
+    if (socket_requests_in_flight_ > 1) {
+      assert(socket_requests_are_from_thread_ == std::this_thread::get_id());
+    }
+    socket_requests_in_flight_ += 1;
+    socket_requests_are_from_thread_ = std::this_thread::get_id();
+  }
+
+  for (const auto &header : default_headers_) {
+    if (req.headers.find(header.first) == req.headers.end()) {
+      req.headers.insert(header);
+    }
+  }
+
+  auto close_connection = !keep_alive_;
+  auto ret = process_socket(socket_, [&](Stream &strm) {
+    return handle_request(strm, req, res, close_connection, error);
+  });
+
+  // Briefly lock mutex in order to mark that a request is no longer ongoing
+  {
+    std::lock_guard<std::mutex> guard(socket_mutex_);
+    socket_requests_in_flight_ -= 1;
+    if (socket_requests_in_flight_ <= 0) {
+      assert(socket_requests_in_flight_ == 0);
+      socket_requests_are_from_thread_ = std::thread::id();
+    }
+
+    if (socket_should_be_closed_when_request_is_done_ || close_connection ||
+        !ret) {
+      shutdown_ssl(socket_, true);
+      shutdown_socket(socket_);
+      close_socket(socket_);
+    }
+  }
+
+  if (!ret) {
+    if (error == Error::Success) { error = Error::Unknown; }
+  }
+
+  return ret;
+}
+
+Result ClientImpl::send(const Request &req) {
+  auto req2 = req;
+  return send_(std::move(req2));
+}
+
+Result ClientImpl::send_(Request &&req) {
+  auto res = detail::make_unique<Response>();
+  auto error = Error::Success;
+  auto ret = send(req, *res, error);
+  return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)};
+}
+
+bool ClientImpl::handle_request(Stream &strm, Request &req,
+                                       Response &res, bool close_connection,
+                                       Error &error) {
+  if (req.path.empty()) {
+    error = Error::Connection;
+    return false;
+  }
+
+  auto req_save = req;
+
+  bool ret;
+
+  if (!is_ssl() && !proxy_host_.empty() && proxy_port_ != -1) {
+    auto req2 = req;
+    req2.path = "http://" + host_and_port_ + req.path;
+    ret = process_request(strm, req2, res, close_connection, error);
+    req = req2;
+    req.path = req_save.path;
+  } else {
+    ret = process_request(strm, req, res, close_connection, error);
+  }
+
+  if (!ret) { return false; }
+
+  if (300 < res.status && res.status < 400 && follow_location_) {
+    req = req_save;
+    ret = redirect(req, res, error);
+  }
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  if ((res.status == 401 || res.status == 407) &&
+      req.authorization_count_ < 5) {
+    auto is_proxy = res.status == 407;
+    const auto &username =
+        is_proxy ? proxy_digest_auth_username_ : digest_auth_username_;
+    const auto &password =
+        is_proxy ? proxy_digest_auth_password_ : digest_auth_password_;
+
+    if (!username.empty() && !password.empty()) {
+      std::map<std::string, std::string> auth;
+      if (detail::parse_www_authenticate(res, auth, is_proxy)) {
+        Request new_req = req;
+        new_req.authorization_count_ += 1;
+        new_req.headers.erase(is_proxy ? "Proxy-Authorization"
+                                       : "Authorization");
+        new_req.headers.insert(detail::make_digest_authentication_header(
+            req, auth, new_req.authorization_count_, detail::random_string(10),
+            username, password, is_proxy));
+
+        Response new_res;
+
+        ret = send(new_req, new_res, error);
+        if (ret) { res = new_res; }
+      }
+    }
+  }
+#endif
+
+  return ret;
+}
+
+bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
+  if (req.redirect_count_ == 0) {
+    error = Error::ExceedRedirectCount;
+    return false;
+  }
+
+  auto location = detail::decode_url(res.get_header_value("location"), true);
+  if (location.empty()) { return false; }
+
+  const static std::regex re(
+      R"((?:(https?):)?(?://(?:\[([\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*(?:\?[^#]*)?)(?:#.*)?)");
+
+  std::smatch m;
+  if (!std::regex_match(location, m, re)) { return false; }
+
+  auto scheme = is_ssl() ? "https" : "http";
+
+  auto next_scheme = m[1].str();
+  auto next_host = m[2].str();
+  if (next_host.empty()) { next_host = m[3].str(); }
+  auto port_str = m[4].str();
+  auto next_path = m[5].str();
+
+  auto next_port = port_;
+  if (!port_str.empty()) {
+    next_port = std::stoi(port_str);
+  } else if (!next_scheme.empty()) {
+    next_port = next_scheme == "https" ? 443 : 80;
+  }
+
+  if (next_scheme.empty()) { next_scheme = scheme; }
+  if (next_host.empty()) { next_host = host_; }
+  if (next_path.empty()) { next_path = "/"; }
+
+  if (next_scheme == scheme && next_host == host_ && next_port == port_) {
+    return detail::redirect(*this, req, res, next_path, location, error);
+  } else {
+    if (next_scheme == "https") {
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+      SSLClient cli(next_host.c_str(), next_port);
+      cli.copy_settings(*this);
+      if (ca_cert_store_) { cli.set_ca_cert_store(ca_cert_store_); }
+      return detail::redirect(cli, req, res, next_path, location, error);
+#else
+      return false;
+#endif
+    } else {
+      ClientImpl cli(next_host.c_str(), next_port);
+      cli.copy_settings(*this);
+      return detail::redirect(cli, req, res, next_path, location, error);
+    }
+  }
+}
+
+bool ClientImpl::write_content_with_provider(Stream &strm,
+                                                    const Request &req,
+                                                    Error &error) {
+  auto is_shutting_down = []() { return false; };
+
+  if (req.is_chunked_content_provider_) {
+    // TODO: Brotli suport
+    std::unique_ptr<detail::compressor> compressor;
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+    if (compress_) {
+      compressor = detail::make_unique<detail::gzip_compressor>();
+    } else
+#endif
+    {
+      compressor = detail::make_unique<detail::nocompressor>();
+    }
+
+    return detail::write_content_chunked(strm, req.content_provider_,
+                                         is_shutting_down, *compressor, error);
+  } else {
+    return detail::write_content(strm, req.content_provider_, 0,
+                                 req.content_length_, is_shutting_down, error);
+  }
+} // namespace httplib
+
+bool ClientImpl::write_request(Stream &strm, Request &req,
+                                      bool close_connection, Error &error) {
+  // Prepare additional headers
+  if (close_connection) {
+    if (!req.has_header("Connection")) {
+      req.headers.emplace("Connection", "close");
+    }
+  }
+
+  if (!req.has_header("Host")) {
+    if (is_ssl()) {
+      if (port_ == 443) {
+        req.headers.emplace("Host", host_);
+      } else {
+        req.headers.emplace("Host", host_and_port_);
+      }
+    } else {
+      if (port_ == 80) {
+        req.headers.emplace("Host", host_);
+      } else {
+        req.headers.emplace("Host", host_and_port_);
+      }
+    }
+  }
+
+  if (!req.has_header("Accept")) { req.headers.emplace("Accept", "*/*"); }
+
+  if (!req.has_header("User-Agent")) {
+    req.headers.emplace("User-Agent", "cpp-httplib/0.9");
+  }
+
+  if (req.body.empty()) {
+    if (req.content_provider_) {
+      if (!req.is_chunked_content_provider_) {
+        if (!req.has_header("Content-Length")) {
+          auto length = std::to_string(req.content_length_);
+          req.headers.emplace("Content-Length", length);
+        }
+      }
+    } else {
+      if (req.method == "POST" || req.method == "PUT" ||
+          req.method == "PATCH") {
+        req.headers.emplace("Content-Length", "0");
+      }
+    }
+  } else {
+    if (!req.has_header("Content-Type")) {
+      req.headers.emplace("Content-Type", "text/plain");
+    }
+
+    if (!req.has_header("Content-Length")) {
+      auto length = std::to_string(req.body.size());
+      req.headers.emplace("Content-Length", length);
+    }
+  }
+
+  if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) {
+    if (!req.has_header("Authorization")) {
+      req.headers.insert(make_basic_authentication_header(
+          basic_auth_username_, basic_auth_password_, false));
+    }
+  }
+
+  if (!proxy_basic_auth_username_.empty() &&
+      !proxy_basic_auth_password_.empty()) {
+    if (!req.has_header("Proxy-Authorization")) {
+      req.headers.insert(make_basic_authentication_header(
+          proxy_basic_auth_username_, proxy_basic_auth_password_, true));
+    }
+  }
+
+  if (!bearer_token_auth_token_.empty()) {
+    if (!req.has_header("Authorization")) {
+      req.headers.insert(make_bearer_token_authentication_header(
+          bearer_token_auth_token_, false));
+    }
+  }
+
+  if (!proxy_bearer_token_auth_token_.empty()) {
+    if (!req.has_header("Proxy-Authorization")) {
+      req.headers.insert(make_bearer_token_authentication_header(
+          proxy_bearer_token_auth_token_, true));
+    }
+  }
+
+  // Request line and headers
+  {
+    detail::BufferStream bstrm;
+
+    const auto &path = url_encode_ ? detail::encode_url(req.path) : req.path;
+    bstrm.write_format("%s %s HTTP/1.1\r\n", req.method.c_str(), path.c_str());
+
+    detail::write_headers(bstrm, req.headers);
+
+    // Flush buffer
+    auto &data = bstrm.get_buffer();
+    if (!detail::write_data(strm, data.data(), data.size())) {
+      error = Error::Write;
+      return false;
+    }
+  }
+
+  // Body
+  if (req.body.empty()) {
+    return write_content_with_provider(strm, req, error);
+  }
+
+  return detail::write_data(strm, req.body.data(), req.body.size());
+}
+
+std::unique_ptr<Response> ClientImpl::send_with_content_provider(
+    Request &req,
+    // const char *method, const char *path, const Headers &headers,
+    const char *body, size_t content_length, ContentProvider content_provider,
+    ContentProviderWithoutLength content_provider_without_length,
+    const char *content_type, Error &error) {
+
+  // Request req;
+  // req.method = method;
+  // req.headers = headers;
+  // req.path = path;
+
+  if (content_type) { req.headers.emplace("Content-Type", content_type); }
+
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+  if (compress_) { req.headers.emplace("Content-Encoding", "gzip"); }
+#endif
+
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+  if (compress_ && !content_provider_without_length) {
+    // TODO: Brotli support
+    detail::gzip_compressor compressor;
+
+    if (content_provider) {
+      auto ok = true;
+      size_t offset = 0;
+      DataSink data_sink;
+
+      data_sink.write = [&](const char *data, size_t data_len) -> bool {
+        if (ok) {
+          auto last = offset + data_len == content_length;
+
+          auto ret = compressor.compress(
+              data, data_len, last, [&](const char *data, size_t data_len) {
+                req.body.append(data, data_len);
+                return true;
+              });
+
+          if (ret) {
+            offset += data_len;
+          } else {
+            ok = false;
+          }
+        }
+        return ok;
+      };
+
+      data_sink.is_writable = [&](void) { return ok && true; };
+
+      while (ok && offset < content_length) {
+        if (!content_provider(offset, content_length - offset, data_sink)) {
+          error = Error::Canceled;
+          return nullptr;
+        }
+      }
+    } else {
+      if (!compressor.compress(body, content_length, true,
+                               [&](const char *data, size_t data_len) {
+                                 req.body.append(data, data_len);
+                                 return true;
+                               })) {
+        error = Error::Compression;
+        return nullptr;
+      }
+    }
+  } else
+#endif
+  {
+    if (content_provider) {
+      req.content_length_ = content_length;
+      req.content_provider_ = std::move(content_provider);
+      req.is_chunked_content_provider_ = false;
+    } else if (content_provider_without_length) {
+      req.content_length_ = 0;
+      req.content_provider_ = detail::ContentProviderAdapter(
+          std::move(content_provider_without_length));
+      req.is_chunked_content_provider_ = true;
+      req.headers.emplace("Transfer-Encoding", "chunked");
+    } else {
+      req.body.assign(body, content_length);
+      ;
+    }
+  }
+
+  auto res = detail::make_unique<Response>();
+  return send(req, *res, error) ? std::move(res) : nullptr;
+}
+
+Result ClientImpl::send_with_content_provider(
+    const char *method, const char *path, const Headers &headers,
+    const char *body, size_t content_length, ContentProvider content_provider,
+    ContentProviderWithoutLength content_provider_without_length,
+    const char *content_type) {
+  Request req;
+  req.method = method;
+  req.headers = headers;
+  req.path = path;
+
+  auto error = Error::Success;
+
+  auto res = send_with_content_provider(
+      req,
+      // method, path, headers,
+      body, content_length, std::move(content_provider),
+      std::move(content_provider_without_length), content_type, error);
+
+  return Result{std::move(res), error, std::move(req.headers)};
+}
+
+std::string
+ClientImpl::adjust_host_string(const std::string &host) const {
+  if (host.find(':') != std::string::npos) { return "[" + host + "]"; }
+  return host;
+}
+
+bool ClientImpl::process_request(Stream &strm, Request &req,
+                                        Response &res, bool close_connection,
+                                        Error &error) {
+  // Send request
+  if (!write_request(strm, req, close_connection, error)) { return false; }
+
+  // Receive response and headers
+  if (!read_response_line(strm, req, res) ||
+      !detail::read_headers(strm, res.headers)) {
+    error = Error::Read;
+    return false;
+  }
+
+  // Body
+  if ((res.status != 204) && req.method != "HEAD" && req.method != "CONNECT") {
+    auto redirect = 300 < res.status && res.status < 400 && follow_location_;
+
+    if (req.response_handler && !redirect) {
+      if (!req.response_handler(res)) {
+        error = Error::Canceled;
+        return false;
+      }
+    }
+
+    auto out =
+        req.content_receiver
+            ? static_cast<ContentReceiverWithProgress>(
+                  [&](const char *buf, size_t n, uint64_t off, uint64_t len) {
+                    if (redirect) { return true; }
+                    auto ret = req.content_receiver(buf, n, off, len);
+                    if (!ret) { error = Error::Canceled; }
+                    return ret;
+                  })
+            : static_cast<ContentReceiverWithProgress>(
+                  [&](const char *buf, size_t n, uint64_t /*off*/,
+                      uint64_t /*len*/) {
+                    if (res.body.size() + n > res.body.max_size()) {
+                      return false;
+                    }
+                    res.body.append(buf, n);
+                    return true;
+                  });
+
+    auto progress = [&](uint64_t current, uint64_t total) {
+      if (!req.progress || redirect) { return true; }
+      auto ret = req.progress(current, total);
+      if (!ret) { error = Error::Canceled; }
+      return ret;
+    };
+
+    int dummy_status;
+    if (!detail::read_content(strm, res, (std::numeric_limits<size_t>::max)(),
+                              dummy_status, std::move(progress), std::move(out),
+                              decompress_)) {
+      if (error != Error::Canceled) { error = Error::Read; }
+      return false;
+    }
+  }
+
+  if (res.get_header_value("Connection") == "close" ||
+      (res.version == "HTTP/1.0" && res.reason != "Connection established")) {
+    // TODO this requires a not-entirely-obvious chain of calls to be correct
+    // for this to be safe. Maybe a code refactor (such as moving this out to
+    // the send function and getting rid of the recursiveness of the mutex)
+    // could make this more obvious.
+
+    // This is safe to call because process_request is only called by
+    // handle_request which is only called by send, which locks the request
+    // mutex during the process. It would be a bug to call it from a different
+    // thread since it's a thread-safety issue to do these things to the socket
+    // if another thread is using the socket.
+    std::lock_guard<std::mutex> guard(socket_mutex_);
+    shutdown_ssl(socket_, true);
+    shutdown_socket(socket_);
+    close_socket(socket_);
+  }
+
+  // Log
+  if (logger_) { logger_(req, res); }
+
+  return true;
+}
+
+bool
+ClientImpl::process_socket(const Socket &socket,
+                           std::function<bool(Stream &strm)> callback) {
+  return detail::process_client_socket(
+      socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
+      write_timeout_usec_, std::move(callback));
+}
+
+bool ClientImpl::is_ssl() const { return false; }
+
+Result ClientImpl::Get(const char *path) {
+  return Get(path, Headers(), Progress());
+}
+
+Result ClientImpl::Get(const char *path, Progress progress) {
+  return Get(path, Headers(), std::move(progress));
+}
+
+Result ClientImpl::Get(const char *path, const Headers &headers) {
+  return Get(path, headers, Progress());
+}
+
+Result ClientImpl::Get(const char *path, const Headers &headers,
+                              Progress progress) {
+  Request req;
+  req.method = "GET";
+  req.path = path;
+  req.headers = headers;
+  req.progress = std::move(progress);
+
+  return send_(std::move(req));
+}
+
+Result ClientImpl::Get(const char *path,
+                              ContentReceiver content_receiver) {
+  return Get(path, Headers(), nullptr, std::move(content_receiver), nullptr);
+}
+
+Result ClientImpl::Get(const char *path,
+                              ContentReceiver content_receiver,
+                              Progress progress) {
+  return Get(path, Headers(), nullptr, std::move(content_receiver),
+             std::move(progress));
+}
+
+Result ClientImpl::Get(const char *path, const Headers &headers,
+                              ContentReceiver content_receiver) {
+  return Get(path, headers, nullptr, std::move(content_receiver), nullptr);
+}
+
+Result ClientImpl::Get(const char *path, const Headers &headers,
+                              ContentReceiver content_receiver,
+                              Progress progress) {
+  return Get(path, headers, nullptr, std::move(content_receiver),
+             std::move(progress));
+}
+
+Result ClientImpl::Get(const char *path,
+                              ResponseHandler response_handler,
+                              ContentReceiver content_receiver) {
+  return Get(path, Headers(), std::move(response_handler),
+             std::move(content_receiver), nullptr);
+}
+
+Result ClientImpl::Get(const char *path, const Headers &headers,
+                              ResponseHandler response_handler,
+                              ContentReceiver content_receiver) {
+  return Get(path, headers, std::move(response_handler),
+             std::move(content_receiver), nullptr);
+}
+
+Result ClientImpl::Get(const char *path,
+                              ResponseHandler response_handler,
+                              ContentReceiver content_receiver,
+                              Progress progress) {
+  return Get(path, Headers(), std::move(response_handler),
+             std::move(content_receiver), std::move(progress));
+}
+
+Result ClientImpl::Get(const char *path, const Headers &headers,
+                              ResponseHandler response_handler,
+                              ContentReceiver content_receiver,
+                              Progress progress) {
+  Request req;
+  req.method = "GET";
+  req.path = path;
+  req.headers = headers;
+  req.response_handler = std::move(response_handler);
+  req.content_receiver =
+      [content_receiver](const char *data, size_t data_length,
+                         uint64_t /*offset*/, uint64_t /*total_length*/) {
+        return content_receiver(data, data_length);
+      };
+  req.progress = std::move(progress);
+
+  return send_(std::move(req));
+}
+
+Result ClientImpl::Get(const char *path, const Params &params,
+                              const Headers &headers, Progress progress) {
+  if (params.empty()) { return Get(path, headers); }
+
+  std::string path_with_query = detail::append_query_params(path, params);
+  return Get(path_with_query.c_str(), headers, progress);
+}
+
+Result ClientImpl::Get(const char *path, const Params &params,
+                              const Headers &headers,
+                              ContentReceiver content_receiver,
+                              Progress progress) {
+  return Get(path, params, headers, nullptr, content_receiver, progress);
+}
+
+Result ClientImpl::Get(const char *path, const Params &params,
+                              const Headers &headers,
+                              ResponseHandler response_handler,
+                              ContentReceiver content_receiver,
+                              Progress progress) {
+  if (params.empty()) {
+    return Get(path, headers, response_handler, content_receiver, progress);
+  }
+
+  std::string path_with_query = detail::append_query_params(path, params);
+  return Get(path_with_query.c_str(), headers, response_handler,
+             content_receiver, progress);
+}
+
+Result ClientImpl::Head(const char *path) {
+  return Head(path, Headers());
+}
+
+Result ClientImpl::Head(const char *path, const Headers &headers) {
+  Request req;
+  req.method = "HEAD";
+  req.headers = headers;
+  req.path = path;
+
+  return send_(std::move(req));
+}
+
+Result ClientImpl::Post(const char *path) {
+  return Post(path, std::string(), nullptr);
+}
+
+Result ClientImpl::Post(const char *path, const char *body,
+                               size_t content_length,
+                               const char *content_type) {
+  return Post(path, Headers(), body, content_length, content_type);
+}
+
+Result ClientImpl::Post(const char *path, const Headers &headers,
+                               const char *body, size_t content_length,
+                               const char *content_type) {
+  return send_with_content_provider("POST", path, headers, body, content_length,
+                                    nullptr, nullptr, content_type);
+}
+
+Result ClientImpl::Post(const char *path, const std::string &body,
+                               const char *content_type) {
+  return Post(path, Headers(), body, content_type);
+}
+
+Result ClientImpl::Post(const char *path, const Headers &headers,
+                               const std::string &body,
+                               const char *content_type) {
+  return send_with_content_provider("POST", path, headers, body.data(),
+                                    body.size(), nullptr, nullptr,
+                                    content_type);
+}
+
+Result ClientImpl::Post(const char *path, const Params &params) {
+  return Post(path, Headers(), params);
+}
+
+Result ClientImpl::Post(const char *path, size_t content_length,
+                               ContentProvider content_provider,
+                               const char *content_type) {
+  return Post(path, Headers(), content_length, std::move(content_provider),
+              content_type);
+}
+
+Result ClientImpl::Post(const char *path,
+                               ContentProviderWithoutLength content_provider,
+                               const char *content_type) {
+  return Post(path, Headers(), std::move(content_provider), content_type);
+}
+
+Result ClientImpl::Post(const char *path, const Headers &headers,
+                               size_t content_length,
+                               ContentProvider content_provider,
+                               const char *content_type) {
+  return send_with_content_provider("POST", path, headers, nullptr,
+                                    content_length, std::move(content_provider),
+                                    nullptr, content_type);
+}
+
+Result ClientImpl::Post(const char *path, const Headers &headers,
+                               ContentProviderWithoutLength content_provider,
+                               const char *content_type) {
+  return send_with_content_provider("POST", path, headers, nullptr, 0, nullptr,
+                                    std::move(content_provider), content_type);
+}
+
+Result ClientImpl::Post(const char *path, const Headers &headers,
+                               const Params &params) {
+  auto query = detail::params_to_query_str(params);
+  return Post(path, headers, query, "application/x-www-form-urlencoded");
+}
+
+Result ClientImpl::Post(const char *path,
+                               const MultipartFormDataItems &items) {
+  return Post(path, Headers(), items);
+}
+
+Result ClientImpl::Post(const char *path, const Headers &headers,
+                               const MultipartFormDataItems &items) {
+  return Post(path, headers, items, detail::make_multipart_data_boundary());
+}
+Result ClientImpl::Post(const char *path, const Headers &headers,
+                               const MultipartFormDataItems &items,
+                               const std::string &boundary) {
+  for (size_t i = 0; i < boundary.size(); i++) {
+    char c = boundary[i];
+    if (!std::isalnum(c) && c != '-' && c != '_') {
+      return Result{nullptr, Error::UnsupportedMultipartBoundaryChars};
+    }
+  }
+
+  std::string body;
+
+  for (const auto &item : items) {
+    body += "--" + boundary + "\r\n";
+    body += "Content-Disposition: form-data; name=\"" + item.name + "\"";
+    if (!item.filename.empty()) {
+      body += "; filename=\"" + item.filename + "\"";
+    }
+    body += "\r\n";
+    if (!item.content_type.empty()) {
+      body += "Content-Type: " + item.content_type + "\r\n";
+    }
+    body += "\r\n";
+    body += item.content + "\r\n";
+  }
+
+  body += "--" + boundary + "--\r\n";
+
+  std::string content_type = "multipart/form-data; boundary=" + boundary;
+  return Post(path, headers, body, content_type.c_str());
+}
+
+Result ClientImpl::Put(const char *path) {
+  return Put(path, std::string(), nullptr);
+}
+
+Result ClientImpl::Put(const char *path, const char *body,
+                              size_t content_length, const char *content_type) {
+  return Put(path, Headers(), body, content_length, content_type);
+}
+
+Result ClientImpl::Put(const char *path, const Headers &headers,
+                              const char *body, size_t content_length,
+                              const char *content_type) {
+  return send_with_content_provider("PUT", path, headers, body, content_length,
+                                    nullptr, nullptr, content_type);
+}
+
+Result ClientImpl::Put(const char *path, const std::string &body,
+                              const char *content_type) {
+  return Put(path, Headers(), body, content_type);
+}
+
+Result ClientImpl::Put(const char *path, const Headers &headers,
+                              const std::string &body,
+                              const char *content_type) {
+  return send_with_content_provider("PUT", path, headers, body.data(),
+                                    body.size(), nullptr, nullptr,
+                                    content_type);
+}
+
+Result ClientImpl::Put(const char *path, size_t content_length,
+                              ContentProvider content_provider,
+                              const char *content_type) {
+  return Put(path, Headers(), content_length, std::move(content_provider),
+             content_type);
+}
+
+Result ClientImpl::Put(const char *path,
+                              ContentProviderWithoutLength content_provider,
+                              const char *content_type) {
+  return Put(path, Headers(), std::move(content_provider), content_type);
+}
+
+Result ClientImpl::Put(const char *path, const Headers &headers,
+                              size_t content_length,
+                              ContentProvider content_provider,
+                              const char *content_type) {
+  return send_with_content_provider("PUT", path, headers, nullptr,
+                                    content_length, std::move(content_provider),
+                                    nullptr, content_type);
+}
+
+Result ClientImpl::Put(const char *path, const Headers &headers,
+                              ContentProviderWithoutLength content_provider,
+                              const char *content_type) {
+  return send_with_content_provider("PUT", path, headers, nullptr, 0, nullptr,
+                                    std::move(content_provider), content_type);
+}
+
+Result ClientImpl::Put(const char *path, const Params &params) {
+  return Put(path, Headers(), params);
+}
+
+Result ClientImpl::Put(const char *path, const Headers &headers,
+                              const Params &params) {
+  auto query = detail::params_to_query_str(params);
+  return Put(path, headers, query, "application/x-www-form-urlencoded");
+}
+
+Result ClientImpl::Patch(const char *path) {
+  return Patch(path, std::string(), nullptr);
+}
+
+Result ClientImpl::Patch(const char *path, const char *body,
+                                size_t content_length,
+                                const char *content_type) {
+  return Patch(path, Headers(), body, content_length, content_type);
+}
+
+Result ClientImpl::Patch(const char *path, const Headers &headers,
+                                const char *body, size_t content_length,
+                                const char *content_type) {
+  return send_with_content_provider("PATCH", path, headers, body,
+                                    content_length, nullptr, nullptr,
+                                    content_type);
+}
+
+Result ClientImpl::Patch(const char *path, const std::string &body,
+                                const char *content_type) {
+  return Patch(path, Headers(), body, content_type);
+}
+
+Result ClientImpl::Patch(const char *path, const Headers &headers,
+                                const std::string &body,
+                                const char *content_type) {
+  return send_with_content_provider("PATCH", path, headers, body.data(),
+                                    body.size(), nullptr, nullptr,
+                                    content_type);
+}
+
+Result ClientImpl::Patch(const char *path, size_t content_length,
+                                ContentProvider content_provider,
+                                const char *content_type) {
+  return Patch(path, Headers(), content_length, std::move(content_provider),
+               content_type);
+}
+
+Result ClientImpl::Patch(const char *path,
+                                ContentProviderWithoutLength content_provider,
+                                const char *content_type) {
+  return Patch(path, Headers(), std::move(content_provider), content_type);
+}
+
+Result ClientImpl::Patch(const char *path, const Headers &headers,
+                                size_t content_length,
+                                ContentProvider content_provider,
+                                const char *content_type) {
+  return send_with_content_provider("PATCH", path, headers, nullptr,
+                                    content_length, std::move(content_provider),
+                                    nullptr, content_type);
+}
+
+Result ClientImpl::Patch(const char *path, const Headers &headers,
+                                ContentProviderWithoutLength content_provider,
+                                const char *content_type) {
+  return send_with_content_provider("PATCH", path, headers, nullptr, 0, nullptr,
+                                    std::move(content_provider), content_type);
+}
+
+Result ClientImpl::Delete(const char *path) {
+  return Delete(path, Headers(), std::string(), nullptr);
+}
+
+Result ClientImpl::Delete(const char *path, const Headers &headers) {
+  return Delete(path, headers, std::string(), nullptr);
+}
+
+Result ClientImpl::Delete(const char *path, const char *body,
+                                 size_t content_length,
+                                 const char *content_type) {
+  return Delete(path, Headers(), body, content_length, content_type);
+}
+
+Result ClientImpl::Delete(const char *path, const Headers &headers,
+                                 const char *body, size_t content_length,
+                                 const char *content_type) {
+  Request req;
+  req.method = "DELETE";
+  req.headers = headers;
+  req.path = path;
+
+  if (content_type) { req.headers.emplace("Content-Type", content_type); }
+  req.body.assign(body, content_length);
+
+  return send_(std::move(req));
+}
+
+Result ClientImpl::Delete(const char *path, const std::string &body,
+                                 const char *content_type) {
+  return Delete(path, Headers(), body.data(), body.size(), content_type);
+}
+
+Result ClientImpl::Delete(const char *path, const Headers &headers,
+                                 const std::string &body,
+                                 const char *content_type) {
+  return Delete(path, headers, body.data(), body.size(), content_type);
+}
+
+Result ClientImpl::Options(const char *path) {
+  return Options(path, Headers());
+}
+
+Result ClientImpl::Options(const char *path, const Headers &headers) {
+  Request req;
+  req.method = "OPTIONS";
+  req.headers = headers;
+  req.path = path;
+
+  return send_(std::move(req));
+}
+
+size_t ClientImpl::is_socket_open() const {
+  std::lock_guard<std::mutex> guard(socket_mutex_);
+  return socket_.is_open();
+}
+
+void ClientImpl::stop() {
+  std::lock_guard<std::mutex> guard(socket_mutex_);
+
+  // If there is anything ongoing right now, the ONLY thread-safe thing we can
+  // do is to shutdown_socket, so that threads using this socket suddenly
+  // discover they can't read/write any more and error out. Everything else
+  // (closing the socket, shutting ssl down) is unsafe because these actions are
+  // not thread-safe.
+  if (socket_requests_in_flight_ > 0) {
+    shutdown_socket(socket_);
+
+    // Aside from that, we set a flag for the socket to be closed when we're
+    // done.
+    socket_should_be_closed_when_request_is_done_ = true;
+    return;
+  }
+
+  // Otherwise, sitll holding the mutex, we can shut everything down ourselves
+  shutdown_ssl(socket_, true);
+  shutdown_socket(socket_);
+  close_socket(socket_);
+}
+
+void ClientImpl::set_connection_timeout(time_t sec, time_t usec) {
+  connection_timeout_sec_ = sec;
+  connection_timeout_usec_ = usec;
+}
+
+void ClientImpl::set_read_timeout(time_t sec, time_t usec) {
+  read_timeout_sec_ = sec;
+  read_timeout_usec_ = usec;
+}
+
+void ClientImpl::set_write_timeout(time_t sec, time_t usec) {
+  write_timeout_sec_ = sec;
+  write_timeout_usec_ = usec;
+}
+
+void ClientImpl::set_basic_auth(const char *username,
+                                       const char *password) {
+  basic_auth_username_ = username;
+  basic_auth_password_ = password;
+}
+
+void ClientImpl::set_bearer_token_auth(const char *token) {
+  bearer_token_auth_token_ = token;
+}
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+void ClientImpl::set_digest_auth(const char *username,
+                                        const char *password) {
+  digest_auth_username_ = username;
+  digest_auth_password_ = password;
+}
+#endif
+
+void ClientImpl::set_keep_alive(bool on) { keep_alive_ = on; }
+
+void ClientImpl::set_follow_location(bool on) { follow_location_ = on; }
+
+void ClientImpl::set_url_encode(bool on) { url_encode_ = on; }
+
+void ClientImpl::set_default_headers(Headers headers) {
+  default_headers_ = std::move(headers);
+}
+
+void ClientImpl::set_address_family(int family) {
+  address_family_ = family;
+}
+
+void ClientImpl::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; }
+
+void ClientImpl::set_socket_options(SocketOptions socket_options) {
+  socket_options_ = std::move(socket_options);
+}
+
+void ClientImpl::set_compress(bool on) { compress_ = on; }
+
+void ClientImpl::set_decompress(bool on) { decompress_ = on; }
+
+void ClientImpl::set_interface(const char *intf) { interface_ = intf; }
+
+void ClientImpl::set_proxy(const char *host, int port) {
+  proxy_host_ = host;
+  proxy_port_ = port;
+}
+
+void ClientImpl::set_proxy_basic_auth(const char *username,
+                                             const char *password) {
+  proxy_basic_auth_username_ = username;
+  proxy_basic_auth_password_ = password;
+}
+
+void ClientImpl::set_proxy_bearer_token_auth(const char *token) {
+  proxy_bearer_token_auth_token_ = token;
+}
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+void ClientImpl::set_proxy_digest_auth(const char *username,
+                                              const char *password) {
+  proxy_digest_auth_username_ = username;
+  proxy_digest_auth_password_ = password;
+}
+#endif
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+void ClientImpl::set_ca_cert_path(const char *ca_cert_file_path,
+                                         const char *ca_cert_dir_path) {
+  if (ca_cert_file_path) { ca_cert_file_path_ = ca_cert_file_path; }
+  if (ca_cert_dir_path) { ca_cert_dir_path_ = ca_cert_dir_path; }
+}
+
+void ClientImpl::set_ca_cert_store(X509_STORE *ca_cert_store) {
+  if (ca_cert_store && ca_cert_store != ca_cert_store_) {
+    ca_cert_store_ = ca_cert_store;
+  }
+}
+#endif
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+void ClientImpl::enable_server_certificate_verification(bool enabled) {
+  server_certificate_verification_ = enabled;
+}
+#endif
+
+void ClientImpl::set_logger(Logger logger) {
+  logger_ = std::move(logger);
+}
+
+/*
+ * SSL Implementation
+ */
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+namespace detail {
+
+template <typename U, typename V>
+SSL *ssl_new(socket_t sock, SSL_CTX *ctx, std::mutex &ctx_mutex,
+                    U SSL_connect_or_accept, V setup) {
+  SSL *ssl = nullptr;
+  {
+    std::lock_guard<std::mutex> guard(ctx_mutex);
+    ssl = SSL_new(ctx);
+  }
+
+  if (ssl) {
+    set_nonblocking(sock, true);
+    auto bio = BIO_new_socket(static_cast<int>(sock), BIO_NOCLOSE);
+    BIO_set_nbio(bio, 1);
+    SSL_set_bio(ssl, bio, bio);
+
+    if (!setup(ssl) || SSL_connect_or_accept(ssl) != 1) {
+      SSL_shutdown(ssl);
+      {
+        std::lock_guard<std::mutex> guard(ctx_mutex);
+        SSL_free(ssl);
+      }
+      set_nonblocking(sock, false);
+      return nullptr;
+    }
+    BIO_set_nbio(bio, 0);
+    set_nonblocking(sock, false);
+  }
+
+  return ssl;
+}
+
+void ssl_delete(std::mutex &ctx_mutex, SSL *ssl,
+                       bool shutdown_gracefully) {
+  // sometimes we may want to skip this to try to avoid SIGPIPE if we know
+  // the remote has closed the network connection
+  // Note that it is not always possible to avoid SIGPIPE, this is merely a
+  // best-efforts.
+  if (shutdown_gracefully) { SSL_shutdown(ssl); }
+
+  std::lock_guard<std::mutex> guard(ctx_mutex);
+  SSL_free(ssl);
+}
+
+template <typename U>
+bool ssl_connect_or_accept_nonblocking(socket_t sock, SSL *ssl,
+                                       U ssl_connect_or_accept,
+                                       time_t timeout_sec,
+                                       time_t timeout_usec) {
+  int res = 0;
+  while ((res = ssl_connect_or_accept(ssl)) != 1) {
+    auto err = SSL_get_error(ssl, res);
+    switch (err) {
+    case SSL_ERROR_WANT_READ:
+      if (select_read(sock, timeout_sec, timeout_usec) > 0) { continue; }
+      break;
+    case SSL_ERROR_WANT_WRITE:
+      if (select_write(sock, timeout_sec, timeout_usec) > 0) { continue; }
+      break;
+    default: break;
+    }
+    return false;
+  }
+  return true;
+}
+
+template <typename T>
+bool
+process_server_socket_ssl(SSL *ssl, socket_t sock, size_t keep_alive_max_count,
+                          time_t keep_alive_timeout_sec,
+                          time_t read_timeout_sec, time_t read_timeout_usec,
+                          time_t write_timeout_sec, time_t write_timeout_usec,
+                          T callback) {
+  return process_server_socket_core(
+      sock, keep_alive_max_count, keep_alive_timeout_sec,
+      [&](bool close_connection, bool &connection_closed) {
+        SSLSocketStream strm(sock, ssl, read_timeout_sec, read_timeout_usec,
+                             write_timeout_sec, write_timeout_usec);
+        return callback(strm, close_connection, connection_closed);
+      });
+}
+
+template <typename T>
+bool
+process_client_socket_ssl(SSL *ssl, socket_t sock, time_t read_timeout_sec,
+                          time_t read_timeout_usec, time_t write_timeout_sec,
+                          time_t write_timeout_usec, T callback) {
+  SSLSocketStream strm(sock, ssl, read_timeout_sec, read_timeout_usec,
+                       write_timeout_sec, write_timeout_usec);
+  return callback(strm);
+}
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+static std::shared_ptr<std::vector<std::mutex>> openSSL_locks_;
+
+class SSLThreadLocks {
+public:
+  SSLThreadLocks() {
+    openSSL_locks_ =
+        std::make_shared<std::vector<std::mutex>>(CRYPTO_num_locks());
+    CRYPTO_set_locking_callback(locking_callback);
+  }
+
+  ~SSLThreadLocks() { CRYPTO_set_locking_callback(nullptr); }
+
+private:
+  static void locking_callback(int mode, int type, const char * /*file*/,
+                               int /*line*/) {
+    auto &lk = (*openSSL_locks_)[static_cast<size_t>(type)];
+    if (mode & CRYPTO_LOCK) {
+      lk.lock();
+    } else {
+      lk.unlock();
+    }
+  }
+};
+
+#endif
+
+class SSLInit {
+public:
+  SSLInit() {
+#if OPENSSL_VERSION_NUMBER < 0x1010001fL
+    SSL_load_error_strings();
+    SSL_library_init();
+#else
+    OPENSSL_init_ssl(
+        OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
+#endif
+  }
+
+  ~SSLInit() {
+#if OPENSSL_VERSION_NUMBER < 0x1010001fL
+    ERR_free_strings();
+#endif
+  }
+
+private:
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+  SSLThreadLocks thread_init_;
+#endif
+};
+
+// SSL socket stream implementation
+SSLSocketStream::SSLSocketStream(socket_t sock, SSL *ssl,
+                                        time_t read_timeout_sec,
+                                        time_t read_timeout_usec,
+                                        time_t write_timeout_sec,
+                                        time_t write_timeout_usec)
+    : sock_(sock), ssl_(ssl), read_timeout_sec_(read_timeout_sec),
+      read_timeout_usec_(read_timeout_usec),
+      write_timeout_sec_(write_timeout_sec),
+      write_timeout_usec_(write_timeout_usec) {
+  SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY);
+}
+
+SSLSocketStream::~SSLSocketStream() {}
+
+bool SSLSocketStream::is_readable() const {
+  return detail::select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0;
+}
+
+bool SSLSocketStream::is_writable() const {
+  return detail::select_write(sock_, write_timeout_sec_, write_timeout_usec_) >
+         0;
+}
+
+ssize_t SSLSocketStream::read(char *ptr, size_t size) {
+  if (SSL_pending(ssl_) > 0) {
+    return SSL_read(ssl_, ptr, static_cast<int>(size));
+  } else if (is_readable()) {
+    auto ret = SSL_read(ssl_, ptr, static_cast<int>(size));
+    if (ret < 0) {
+      auto err = SSL_get_error(ssl_, ret);
+      int n = 1000;
+#ifdef _WIN32
+      while (--n >= 0 &&
+             (err == SSL_ERROR_WANT_READ ||
+              err == SSL_ERROR_SYSCALL && WSAGetLastError() == WSAETIMEDOUT)) {
+#else
+      while (--n >= 0 && err == SSL_ERROR_WANT_READ) {
+#endif
+        if (SSL_pending(ssl_) > 0) {
+          return SSL_read(ssl_, ptr, static_cast<int>(size));
+        } else if (is_readable()) {
+          std::this_thread::sleep_for(std::chrono::milliseconds(1));
+          ret = SSL_read(ssl_, ptr, static_cast<int>(size));
+          if (ret >= 0) { return ret; }
+          err = SSL_get_error(ssl_, ret);
+        } else {
+          return -1;
+        }
+      }
+    }
+    return ret;
+  }
+  return -1;
+}
+
+ssize_t SSLSocketStream::write(const char *ptr, size_t size) {
+  if (is_writable()) { return SSL_write(ssl_, ptr, static_cast<int>(size)); }
+  return -1;
+}
+
+void SSLSocketStream::get_remote_ip_and_port(std::string &ip,
+                                                    int &port) const {
+  detail::get_remote_ip_and_port(sock_, ip, port);
+}
+
+socket_t SSLSocketStream::socket() const { return sock_; }
+
+static SSLInit sslinit_;
+
+} // namespace detail
+
+// SSL HTTP server implementation
+SSLServer::SSLServer(const char *cert_path, const char *private_key_path,
+                            const char *client_ca_cert_file_path,
+                            const char *client_ca_cert_dir_path) {
+  ctx_ = SSL_CTX_new(TLS_method());
+
+  if (ctx_) {
+    SSL_CTX_set_options(ctx_,
+                        SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 |
+                            SSL_OP_NO_COMPRESSION |
+                            SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);
+
+    // auto ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
+    // SSL_CTX_set_tmp_ecdh(ctx_, ecdh);
+    // EC_KEY_free(ecdh);
+
+    if (SSL_CTX_use_certificate_chain_file(ctx_, cert_path) != 1 ||
+        SSL_CTX_use_PrivateKey_file(ctx_, private_key_path, SSL_FILETYPE_PEM) !=
+            1) {
+      SSL_CTX_free(ctx_);
+      ctx_ = nullptr;
+    } else if (client_ca_cert_file_path || client_ca_cert_dir_path) {
+      // if (client_ca_cert_file_path) {
+      //   auto list = SSL_load_client_CA_file(client_ca_cert_file_path);
+      //   SSL_CTX_set_client_CA_list(ctx_, list);
+      // }
+
+      SSL_CTX_load_verify_locations(ctx_, client_ca_cert_file_path,
+                                    client_ca_cert_dir_path);
+
+      SSL_CTX_set_verify(
+          ctx_,
+          SSL_VERIFY_PEER |
+              SSL_VERIFY_FAIL_IF_NO_PEER_CERT, // SSL_VERIFY_CLIENT_ONCE,
+          nullptr);
+    }
+  }
+}
+
+SSLServer::SSLServer(X509 *cert, EVP_PKEY *private_key,
+                            X509_STORE *client_ca_cert_store) {
+  ctx_ = SSL_CTX_new(SSLv23_server_method());
+
+  if (ctx_) {
+    SSL_CTX_set_options(ctx_,
+                        SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 |
+                            SSL_OP_NO_COMPRESSION |
+                            SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);
+
+    if (SSL_CTX_use_certificate(ctx_, cert) != 1 ||
+        SSL_CTX_use_PrivateKey(ctx_, private_key) != 1) {
+      SSL_CTX_free(ctx_);
+      ctx_ = nullptr;
+    } else if (client_ca_cert_store) {
+
+      SSL_CTX_set_cert_store(ctx_, client_ca_cert_store);
+
+      SSL_CTX_set_verify(
+          ctx_,
+          SSL_VERIFY_PEER |
+              SSL_VERIFY_FAIL_IF_NO_PEER_CERT, // SSL_VERIFY_CLIENT_ONCE,
+          nullptr);
+    }
+  }
+}
+
+SSLServer::~SSLServer() {
+  if (ctx_) { SSL_CTX_free(ctx_); }
+}
+
+bool SSLServer::is_valid() const { return ctx_; }
+
+bool SSLServer::process_and_close_socket(socket_t sock) {
+  auto ssl = detail::ssl_new(
+      sock, ctx_, ctx_mutex_,
+      [&](SSL *ssl) {
+        return detail::ssl_connect_or_accept_nonblocking(
+            sock, ssl, SSL_accept, read_timeout_sec_, read_timeout_usec_);
+      },
+      [](SSL * /*ssl*/) { return true; });
+
+  bool ret = false;
+  if (ssl) {
+    ret = detail::process_server_socket_ssl(
+        ssl, sock, keep_alive_max_count_, keep_alive_timeout_sec_,
+        read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
+        write_timeout_usec_,
+        [this, ssl](Stream &strm, bool close_connection,
+                    bool &connection_closed) {
+          return process_request(strm, close_connection, connection_closed,
+                                 [&](Request &req) { req.ssl = ssl; });
+        });
+
+    // Shutdown gracefully if the result seemed successful, non-gracefully if
+    // the connection appeared to be closed.
+    const bool shutdown_gracefully = ret;
+    detail::ssl_delete(ctx_mutex_, ssl, shutdown_gracefully);
+  }
+
+  detail::shutdown_socket(sock);
+  detail::close_socket(sock);
+  return ret;
+}
+
+// SSL HTTP client implementation
+SSLClient::SSLClient(const std::string &host)
+    : SSLClient(host, 443, std::string(), std::string()) {}
+
+SSLClient::SSLClient(const std::string &host, int port)
+    : SSLClient(host, port, std::string(), std::string()) {}
+
+SSLClient::SSLClient(const std::string &host, int port,
+                            const std::string &client_cert_path,
+                            const std::string &client_key_path)
+    : ClientImpl(host, port, client_cert_path, client_key_path) {
+  ctx_ = SSL_CTX_new(SSLv23_client_method());
+
+  detail::split(&host_[0], &host_[host_.size()], '.',
+                [&](const char *b, const char *e) {
+                  host_components_.emplace_back(std::string(b, e));
+                });
+  if (!client_cert_path.empty() && !client_key_path.empty()) {
+    if (SSL_CTX_use_certificate_file(ctx_, client_cert_path.c_str(),
+                                     SSL_FILETYPE_PEM) != 1 ||
+        SSL_CTX_use_PrivateKey_file(ctx_, client_key_path.c_str(),
+                                    SSL_FILETYPE_PEM) != 1) {
+      SSL_CTX_free(ctx_);
+      ctx_ = nullptr;
+    }
+  }
+}
+
+SSLClient::SSLClient(const std::string &host, int port,
+                            X509 *client_cert, EVP_PKEY *client_key)
+    : ClientImpl(host, port) {
+  ctx_ = SSL_CTX_new(SSLv23_client_method());
+
+  detail::split(&host_[0], &host_[host_.size()], '.',
+                [&](const char *b, const char *e) {
+                  host_components_.emplace_back(std::string(b, e));
+                });
+  if (client_cert != nullptr && client_key != nullptr) {
+    if (SSL_CTX_use_certificate(ctx_, client_cert) != 1 ||
+        SSL_CTX_use_PrivateKey(ctx_, client_key) != 1) {
+      SSL_CTX_free(ctx_);
+      ctx_ = nullptr;
+    }
+  }
+}
+
+SSLClient::~SSLClient() {
+  if (ctx_) { SSL_CTX_free(ctx_); }
+  // Make sure to shut down SSL since shutdown_ssl will resolve to the
+  // base function rather than the derived function once we get to the
+  // base class destructor, and won't free the SSL (causing a leak).
+  shutdown_ssl_impl(socket_, true);
+}
+
+bool SSLClient::is_valid() const { return ctx_; }
+
+void SSLClient::set_ca_cert_store(X509_STORE *ca_cert_store) {
+  if (ca_cert_store) {
+    if (ctx_) {
+      if (SSL_CTX_get_cert_store(ctx_) != ca_cert_store) {
+        // Free memory allocated for old cert and use new store `ca_cert_store`
+        SSL_CTX_set_cert_store(ctx_, ca_cert_store);
+      }
+    } else {
+      X509_STORE_free(ca_cert_store);
+    }
+  }
+}
+
+long SSLClient::get_openssl_verify_result() const {
+  return verify_result_;
+}
+
+SSL_CTX *SSLClient::ssl_context() const { return ctx_; }
+
+bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) {
+  return is_valid() && ClientImpl::create_and_connect_socket(socket, error);
+}
+
+// Assumes that socket_mutex_ is locked and that there are no requests in flight
+bool SSLClient::connect_with_proxy(Socket &socket, Response &res,
+                                          bool &success, Error &error) {
+  success = true;
+  Response res2;
+  if (!detail::process_client_socket(
+          socket.sock, read_timeout_sec_, read_timeout_usec_,
+          write_timeout_sec_, write_timeout_usec_, [&](Stream &strm) {
+            Request req2;
+            req2.method = "CONNECT";
+            req2.path = host_and_port_;
+            return process_request(strm, req2, res2, false, error);
+          })) {
+    // Thread-safe to close everything because we are assuming there are no
+    // requests in flight
+    shutdown_ssl(socket, true);
+    shutdown_socket(socket);
+    close_socket(socket);
+    success = false;
+    return false;
+  }
+
+  if (res2.status == 407) {
+    if (!proxy_digest_auth_username_.empty() &&
+        !proxy_digest_auth_password_.empty()) {
+      std::map<std::string, std::string> auth;
+      if (detail::parse_www_authenticate(res2, auth, true)) {
+        Response res3;
+        if (!detail::process_client_socket(
+                socket.sock, read_timeout_sec_, read_timeout_usec_,
+                write_timeout_sec_, write_timeout_usec_, [&](Stream &strm) {
+                  Request req3;
+                  req3.method = "CONNECT";
+                  req3.path = host_and_port_;
+                  req3.headers.insert(detail::make_digest_authentication_header(
+                      req3, auth, 1, detail::random_string(10),
+                      proxy_digest_auth_username_, proxy_digest_auth_password_,
+                      true));
+                  return process_request(strm, req3, res3, false, error);
+                })) {
+          // Thread-safe to close everything because we are assuming there are
+          // no requests in flight
+          shutdown_ssl(socket, true);
+          shutdown_socket(socket);
+          close_socket(socket);
+          success = false;
+          return false;
+        }
+      }
+    } else {
+      res = res2;
+      return false;
+    }
+  }
+
+  return true;
+}
+
+bool SSLClient::load_certs() {
+  bool ret = true;
+
+  std::call_once(initialize_cert_, [&]() {
+    std::lock_guard<std::mutex> guard(ctx_mutex_);
+    if (!ca_cert_file_path_.empty()) {
+      if (!SSL_CTX_load_verify_locations(ctx_, ca_cert_file_path_.c_str(),
+                                         nullptr)) {
+        ret = false;
+      }
+    } else if (!ca_cert_dir_path_.empty()) {
+      if (!SSL_CTX_load_verify_locations(ctx_, nullptr,
+                                         ca_cert_dir_path_.c_str())) {
+        ret = false;
+      }
+    } else {
+#ifdef _WIN32
+      detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_));
+#else
+      SSL_CTX_set_default_verify_paths(ctx_);
+#endif
+    }
+  });
+
+  return ret;
+}
+
+bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
+  auto ssl = detail::ssl_new(
+      socket.sock, ctx_, ctx_mutex_,
+      [&](SSL *ssl) {
+        if (server_certificate_verification_) {
+          if (!load_certs()) {
+            error = Error::SSLLoadingCerts;
+            return false;
+          }
+          SSL_set_verify(ssl, SSL_VERIFY_NONE, nullptr);
+        }
+
+        if (!detail::ssl_connect_or_accept_nonblocking(
+                socket.sock, ssl, SSL_connect, connection_timeout_sec_,
+                connection_timeout_usec_)) {
+          error = Error::SSLConnection;
+          return false;
+        }
+
+        if (server_certificate_verification_) {
+          verify_result_ = SSL_get_verify_result(ssl);
+
+          if (verify_result_ != X509_V_OK) {
+            error = Error::SSLServerVerification;
+            return false;
+          }
+
+          auto server_cert = SSL_get_peer_certificate(ssl);
+
+          if (server_cert == nullptr) {
+            error = Error::SSLServerVerification;
+            return false;
+          }
+
+          if (!verify_host(server_cert)) {
+            X509_free(server_cert);
+            error = Error::SSLServerVerification;
+            return false;
+          }
+          X509_free(server_cert);
+        }
+
+        return true;
+      },
+      [&](SSL *ssl) {
+        SSL_set_tlsext_host_name(ssl, host_.c_str());
+        return true;
+      });
+
+  if (ssl) {
+    socket.ssl = ssl;
+    return true;
+  }
+
+  shutdown_socket(socket);
+  close_socket(socket);
+  return false;
+}
+
+void SSLClient::shutdown_ssl(Socket &socket, bool shutdown_gracefully) {
+  shutdown_ssl_impl(socket, shutdown_gracefully);
+}
+
+void SSLClient::shutdown_ssl_impl(Socket &socket,
+                                         bool shutdown_gracefully) {
+  if (socket.sock == INVALID_SOCKET) {
+    assert(socket.ssl == nullptr);
+    return;
+  }
+  if (socket.ssl) {
+    detail::ssl_delete(ctx_mutex_, socket.ssl, shutdown_gracefully);
+    socket.ssl = nullptr;
+  }
+  assert(socket.ssl == nullptr);
+}
+
+bool
+SSLClient::process_socket(const Socket &socket,
+                          std::function<bool(Stream &strm)> callback) {
+  assert(socket.ssl);
+  return detail::process_client_socket_ssl(
+      socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_,
+      write_timeout_sec_, write_timeout_usec_, std::move(callback));
+}
+
+bool SSLClient::is_ssl() const { return true; }
+
+bool SSLClient::verify_host(X509 *server_cert) const {
+  /* Quote from RFC2818 section 3.1 "Server Identity"
+
+     If a subjectAltName extension of type dNSName is present, that MUST
+     be used as the identity. Otherwise, the (most specific) Common Name
+     field in the Subject field of the certificate MUST be used. Although
+     the use of the Common Name is existing practice, it is deprecated and
+     Certification Authorities are encouraged to use the dNSName instead.
+
+     Matching is performed using the matching rules specified by
+     [RFC2459].  If more than one identity of a given type is present in
+     the certificate (e.g., more than one dNSName name, a match in any one
+     of the set is considered acceptable.) Names may contain the wildcard
+     character * which is considered to match any single domain name
+     component or component fragment. E.g., *.a.com matches foo.a.com but
+     not bar.foo.a.com. f*.com matches foo.com but not bar.com.
+
+     In some cases, the URI is specified as an IP address rather than a
+     hostname. In this case, the iPAddress subjectAltName must be present
+     in the certificate and must exactly match the IP in the URI.
+
+  */
+  return verify_host_with_subject_alt_name(server_cert) ||
+         verify_host_with_common_name(server_cert);
+}
+
+bool
+SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const {
+  auto ret = false;
+
+  auto type = GEN_DNS;
+
+  struct in6_addr addr6;
+  struct in_addr addr;
+  size_t addr_len = 0;
+
+#ifndef __MINGW32__
+  if (inet_pton(AF_INET6, host_.c_str(), &addr6)) {
+    type = GEN_IPADD;
+    addr_len = sizeof(struct in6_addr);
+  } else if (inet_pton(AF_INET, host_.c_str(), &addr)) {
+    type = GEN_IPADD;
+    addr_len = sizeof(struct in_addr);
+  }
+#endif
+
+  auto alt_names = static_cast<const struct stack_st_GENERAL_NAME *>(
+      X509_get_ext_d2i(server_cert, NID_subject_alt_name, nullptr, nullptr));
+
+  if (alt_names) {
+    auto dsn_matched = false;
+    auto ip_mached = false;
+
+    auto count = sk_GENERAL_NAME_num(alt_names);
+
+    for (decltype(count) i = 0; i < count && !dsn_matched; i++) {
+      auto val = sk_GENERAL_NAME_value(alt_names, i);
+      if (val->type == type) {
+        auto name = (const char *)ASN1_STRING_get0_data(val->d.ia5);
+        auto name_len = (size_t)ASN1_STRING_length(val->d.ia5);
+
+        switch (type) {
+        case GEN_DNS: dsn_matched = check_host_name(name, name_len); break;
+
+        case GEN_IPADD:
+          if (!memcmp(&addr6, name, addr_len) ||
+              !memcmp(&addr, name, addr_len)) {
+            ip_mached = true;
+          }
+          break;
+        }
+      }
+    }
+
+    if (dsn_matched || ip_mached) { ret = true; }
+  }
+
+  GENERAL_NAMES_free((STACK_OF(GENERAL_NAME) *)alt_names);
+  return ret;
+}
+
+bool SSLClient::verify_host_with_common_name(X509 *server_cert) const {
+  const auto subject_name = X509_get_subject_name(server_cert);
+
+  if (subject_name != nullptr) {
+    char name[BUFSIZ];
+    auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName,
+                                              name, sizeof(name));
+
+    if (name_len != -1) {
+      return check_host_name(name, static_cast<size_t>(name_len));
+    }
+  }
+
+  return false;
+}
+
+bool SSLClient::check_host_name(const char *pattern,
+                                       size_t pattern_len) const {
+  if (host_.size() == pattern_len && host_ == pattern) { return true; }
+
+  // Wildcard match
+  // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484
+  std::vector<std::string> pattern_components;
+  detail::split(&pattern[0], &pattern[pattern_len], '.',
+                [&](const char *b, const char *e) {
+                  pattern_components.emplace_back(std::string(b, e));
+                });
+
+  if (host_components_.size() != pattern_components.size()) { return false; }
+
+  auto itr = pattern_components.begin();
+  for (const auto &h : host_components_) {
+    auto &p = *itr;
+    if (p != h && p != "*") {
+      auto partial_match = (p.size() > 0 && p[p.size() - 1] == '*' &&
+                            !p.compare(0, p.size() - 1, h));
+      if (!partial_match) { return false; }
+    }
+    ++itr;
+  }
+
+  return true;
+}
+#endif
+
+// Universal client implementation
+Client::Client(const std::string &scheme_host_port)
+    : Client(scheme_host_port, std::string(), std::string()) {}
+
+Client::Client(const std::string &scheme_host_port,
+                      const std::string &client_cert_path,
+                      const std::string &client_key_path) {
+  const static std::regex re(
+      R"((?:([a-z]+):\/\/)?(?:\[([\d:]+)\]|([^:/?#]+))(?::(\d+))?)");
+
+  std::smatch m;
+  if (std::regex_match(scheme_host_port, m, re)) {
+    auto scheme = m[1].str();
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+    if (!scheme.empty() && (scheme != "http" && scheme != "https")) {
+#else
+    if (!scheme.empty() && scheme != "http") {
+#endif
+      std::string msg = "'" + scheme + "' scheme is not supported.";
+      throw std::invalid_argument(msg);
+      return;
+    }
+
+    auto is_ssl = scheme == "https";
+
+    auto host = m[2].str();
+    if (host.empty()) { host = m[3].str(); }
+
+    auto port_str = m[4].str();
+    auto port = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80);
+
+    if (is_ssl) {
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+      cli_ = detail::make_unique<SSLClient>(host.c_str(), port,
+                                            client_cert_path, client_key_path);
+      is_ssl_ = is_ssl;
+#endif
+    } else {
+      cli_ = detail::make_unique<ClientImpl>(host.c_str(), port,
+                                             client_cert_path, client_key_path);
+    }
+  } else {
+    cli_ = detail::make_unique<ClientImpl>(scheme_host_port, 80,
+                                           client_cert_path, client_key_path);
+  }
+}
+
+Client::Client(const std::string &host, int port)
+    : cli_(detail::make_unique<ClientImpl>(host, port)) {}
+
+Client::Client(const std::string &host, int port,
+                      const std::string &client_cert_path,
+                      const std::string &client_key_path)
+    : cli_(detail::make_unique<ClientImpl>(host, port, client_cert_path,
+                                           client_key_path)) {}
+
+Client::~Client() {}
+
+bool Client::is_valid() const {
+  return cli_ != nullptr && cli_->is_valid();
+}
+
+Result Client::Get(const char *path) { return cli_->Get(path); }
+Result Client::Get(const char *path, const Headers &headers) {
+  return cli_->Get(path, headers);
+}
+Result Client::Get(const char *path, Progress progress) {
+  return cli_->Get(path, std::move(progress));
+}
+Result Client::Get(const char *path, const Headers &headers,
+                          Progress progress) {
+  return cli_->Get(path, headers, std::move(progress));
+}
+Result Client::Get(const char *path, ContentReceiver content_receiver) {
+  return cli_->Get(path, std::move(content_receiver));
+}
+Result Client::Get(const char *path, const Headers &headers,
+                          ContentReceiver content_receiver) {
+  return cli_->Get(path, headers, std::move(content_receiver));
+}
+Result Client::Get(const char *path, ContentReceiver content_receiver,
+                          Progress progress) {
+  return cli_->Get(path, std::move(content_receiver), std::move(progress));
+}
+Result Client::Get(const char *path, const Headers &headers,
+                          ContentReceiver content_receiver, Progress progress) {
+  return cli_->Get(path, headers, std::move(content_receiver),
+                   std::move(progress));
+}
+Result Client::Get(const char *path, ResponseHandler response_handler,
+                          ContentReceiver content_receiver) {
+  return cli_->Get(path, std::move(response_handler),
+                   std::move(content_receiver));
+}
+Result Client::Get(const char *path, const Headers &headers,
+                          ResponseHandler response_handler,
+                          ContentReceiver content_receiver) {
+  return cli_->Get(path, headers, std::move(response_handler),
+                   std::move(content_receiver));
+}
+Result Client::Get(const char *path, ResponseHandler response_handler,
+                          ContentReceiver content_receiver, Progress progress) {
+  return cli_->Get(path, std::move(response_handler),
+                   std::move(content_receiver), std::move(progress));
+}
+Result Client::Get(const char *path, const Headers &headers,
+                          ResponseHandler response_handler,
+                          ContentReceiver content_receiver, Progress progress) {
+  return cli_->Get(path, headers, std::move(response_handler),
+                   std::move(content_receiver), std::move(progress));
+}
+Result Client::Get(const char *path, const Params &params,
+                          const Headers &headers, Progress progress) {
+  return cli_->Get(path, params, headers, progress);
+}
+Result Client::Get(const char *path, const Params &params,
+                          const Headers &headers,
+                          ContentReceiver content_receiver, Progress progress) {
+  return cli_->Get(path, params, headers, content_receiver, progress);
+}
+Result Client::Get(const char *path, const Params &params,
+                          const Headers &headers,
+                          ResponseHandler response_handler,
+                          ContentReceiver content_receiver, Progress progress) {
+  return cli_->Get(path, params, headers, response_handler, content_receiver,
+                   progress);
+}
+
+Result Client::Head(const char *path) { return cli_->Head(path); }
+Result Client::Head(const char *path, const Headers &headers) {
+  return cli_->Head(path, headers);
+}
+
+Result Client::Post(const char *path) { return cli_->Post(path); }
+Result Client::Post(const char *path, const char *body,
+                           size_t content_length, const char *content_type) {
+  return cli_->Post(path, body, content_length, content_type);
+}
+Result Client::Post(const char *path, const Headers &headers,
+                           const char *body, size_t content_length,
+                           const char *content_type) {
+  return cli_->Post(path, headers, body, content_length, content_type);
+}
+Result Client::Post(const char *path, const std::string &body,
+                           const char *content_type) {
+  return cli_->Post(path, body, content_type);
+}
+Result Client::Post(const char *path, const Headers &headers,
+                           const std::string &body, const char *content_type) {
+  return cli_->Post(path, headers, body, content_type);
+}
+Result Client::Post(const char *path, size_t content_length,
+                           ContentProvider content_provider,
+                           const char *content_type) {
+  return cli_->Post(path, content_length, std::move(content_provider),
+                    content_type);
+}
+Result Client::Post(const char *path,
+                           ContentProviderWithoutLength content_provider,
+                           const char *content_type) {
+  return cli_->Post(path, std::move(content_provider), content_type);
+}
+Result Client::Post(const char *path, const Headers &headers,
+                           size_t content_length,
+                           ContentProvider content_provider,
+                           const char *content_type) {
+  return cli_->Post(path, headers, content_length, std::move(content_provider),
+                    content_type);
+}
+Result Client::Post(const char *path, const Headers &headers,
+                           ContentProviderWithoutLength content_provider,
+                           const char *content_type) {
+  return cli_->Post(path, headers, std::move(content_provider), content_type);
+}
+Result Client::Post(const char *path, const Params &params) {
+  return cli_->Post(path, params);
+}
+Result Client::Post(const char *path, const Headers &headers,
+                           const Params &params) {
+  return cli_->Post(path, headers, params);
+}
+Result Client::Post(const char *path,
+                           const MultipartFormDataItems &items) {
+  return cli_->Post(path, items);
+}
+Result Client::Post(const char *path, const Headers &headers,
+                           const MultipartFormDataItems &items) {
+  return cli_->Post(path, headers, items);
+}
+Result Client::Post(const char *path, const Headers &headers,
+                           const MultipartFormDataItems &items,
+                           const std::string &boundary) {
+  return cli_->Post(path, headers, items, boundary);
+}
+Result Client::Put(const char *path) { return cli_->Put(path); }
+Result Client::Put(const char *path, const char *body,
+                          size_t content_length, const char *content_type) {
+  return cli_->Put(path, body, content_length, content_type);
+}
+Result Client::Put(const char *path, const Headers &headers,
+                          const char *body, size_t content_length,
+                          const char *content_type) {
+  return cli_->Put(path, headers, body, content_length, content_type);
+}
+Result Client::Put(const char *path, const std::string &body,
+                          const char *content_type) {
+  return cli_->Put(path, body, content_type);
+}
+Result Client::Put(const char *path, const Headers &headers,
+                          const std::string &body, const char *content_type) {
+  return cli_->Put(path, headers, body, content_type);
+}
+Result Client::Put(const char *path, size_t content_length,
+                          ContentProvider content_provider,
+                          const char *content_type) {
+  return cli_->Put(path, content_length, std::move(content_provider),
+                   content_type);
+}
+Result Client::Put(const char *path,
+                          ContentProviderWithoutLength content_provider,
+                          const char *content_type) {
+  return cli_->Put(path, std::move(content_provider), content_type);
+}
+Result Client::Put(const char *path, const Headers &headers,
+                          size_t content_length,
+                          ContentProvider content_provider,
+                          const char *content_type) {
+  return cli_->Put(path, headers, content_length, std::move(content_provider),
+                   content_type);
+}
+Result Client::Put(const char *path, const Headers &headers,
+                          ContentProviderWithoutLength content_provider,
+                          const char *content_type) {
+  return cli_->Put(path, headers, std::move(content_provider), content_type);
+}
+Result Client::Put(const char *path, const Params &params) {
+  return cli_->Put(path, params);
+}
+Result Client::Put(const char *path, const Headers &headers,
+                          const Params &params) {
+  return cli_->Put(path, headers, params);
+}
+Result Client::Patch(const char *path) { return cli_->Patch(path); }
+Result Client::Patch(const char *path, const char *body,
+                            size_t content_length, const char *content_type) {
+  return cli_->Patch(path, body, content_length, content_type);
+}
+Result Client::Patch(const char *path, const Headers &headers,
+                            const char *body, size_t content_length,
+                            const char *content_type) {
+  return cli_->Patch(path, headers, body, content_length, content_type);
+}
+Result Client::Patch(const char *path, const std::string &body,
+                            const char *content_type) {
+  return cli_->Patch(path, body, content_type);
+}
+Result Client::Patch(const char *path, const Headers &headers,
+                            const std::string &body, const char *content_type) {
+  return cli_->Patch(path, headers, body, content_type);
+}
+Result Client::Patch(const char *path, size_t content_length,
+                            ContentProvider content_provider,
+                            const char *content_type) {
+  return cli_->Patch(path, content_length, std::move(content_provider),
+                     content_type);
+}
+Result Client::Patch(const char *path,
+                            ContentProviderWithoutLength content_provider,
+                            const char *content_type) {
+  return cli_->Patch(path, std::move(content_provider), content_type);
+}
+Result Client::Patch(const char *path, const Headers &headers,
+                            size_t content_length,
+                            ContentProvider content_provider,
+                            const char *content_type) {
+  return cli_->Patch(path, headers, content_length, std::move(content_provider),
+                     content_type);
+}
+Result Client::Patch(const char *path, const Headers &headers,
+                            ContentProviderWithoutLength content_provider,
+                            const char *content_type) {
+  return cli_->Patch(path, headers, std::move(content_provider), content_type);
+}
+Result Client::Delete(const char *path) { return cli_->Delete(path); }
+Result Client::Delete(const char *path, const Headers &headers) {
+  return cli_->Delete(path, headers);
+}
+Result Client::Delete(const char *path, const char *body,
+                             size_t content_length, const char *content_type) {
+  return cli_->Delete(path, body, content_length, content_type);
+}
+Result Client::Delete(const char *path, const Headers &headers,
+                             const char *body, size_t content_length,
+                             const char *content_type) {
+  return cli_->Delete(path, headers, body, content_length, content_type);
+}
+Result Client::Delete(const char *path, const std::string &body,
+                             const char *content_type) {
+  return cli_->Delete(path, body, content_type);
+}
+Result Client::Delete(const char *path, const Headers &headers,
+                             const std::string &body,
+                             const char *content_type) {
+  return cli_->Delete(path, headers, body, content_type);
+}
+Result Client::Options(const char *path) { return cli_->Options(path); }
+Result Client::Options(const char *path, const Headers &headers) {
+  return cli_->Options(path, headers);
+}
+
+bool Client::send(Request &req, Response &res, Error &error) {
+  return cli_->send(req, res, error);
+}
+
+Result Client::send(const Request &req) { return cli_->send(req); }
+
+size_t Client::is_socket_open() const { return cli_->is_socket_open(); }
+
+void Client::stop() { cli_->stop(); }
+
+void Client::set_default_headers(Headers headers) {
+  cli_->set_default_headers(std::move(headers));
+}
+
+void Client::set_address_family(int family) {
+  cli_->set_address_family(family);
+}
+
+void Client::set_tcp_nodelay(bool on) { cli_->set_tcp_nodelay(on); }
+
+void Client::set_socket_options(SocketOptions socket_options) {
+  cli_->set_socket_options(std::move(socket_options));
+}
+
+void Client::set_connection_timeout(time_t sec, time_t usec) {
+  cli_->set_connection_timeout(sec, usec);
+}
+
+void Client::set_read_timeout(time_t sec, time_t usec) {
+  cli_->set_read_timeout(sec, usec);
+}
+
+void Client::set_write_timeout(time_t sec, time_t usec) {
+  cli_->set_write_timeout(sec, usec);
+}
+
+void Client::set_basic_auth(const char *username, const char *password) {
+  cli_->set_basic_auth(username, password);
+}
+void Client::set_bearer_token_auth(const char *token) {
+  cli_->set_bearer_token_auth(token);
+}
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+void Client::set_digest_auth(const char *username,
+                                    const char *password) {
+  cli_->set_digest_auth(username, password);
+}
+#endif
+
+void Client::set_keep_alive(bool on) { cli_->set_keep_alive(on); }
+void Client::set_follow_location(bool on) {
+  cli_->set_follow_location(on);
+}
+
+void Client::set_url_encode(bool on) { cli_->set_url_encode(on); }
+
+void Client::set_compress(bool on) { cli_->set_compress(on); }
+
+void Client::set_decompress(bool on) { cli_->set_decompress(on); }
+
+void Client::set_interface(const char *intf) {
+  cli_->set_interface(intf);
+}
+
+void Client::set_proxy(const char *host, int port) {
+  cli_->set_proxy(host, port);
+}
+void Client::set_proxy_basic_auth(const char *username,
+                                         const char *password) {
+  cli_->set_proxy_basic_auth(username, password);
+}
+void Client::set_proxy_bearer_token_auth(const char *token) {
+  cli_->set_proxy_bearer_token_auth(token);
+}
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+void Client::set_proxy_digest_auth(const char *username,
+                                          const char *password) {
+  cli_->set_proxy_digest_auth(username, password);
+}
+#endif
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+void Client::enable_server_certificate_verification(bool enabled) {
+  cli_->enable_server_certificate_verification(enabled);
+}
+#endif
+
+void Client::set_logger(Logger logger) { cli_->set_logger(logger); }
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+void Client::set_ca_cert_path(const char *ca_cert_file_path,
+                                     const char *ca_cert_dir_path) {
+  cli_->set_ca_cert_path(ca_cert_file_path, ca_cert_dir_path);
+}
+
+void Client::set_ca_cert_store(X509_STORE *ca_cert_store) {
+  if (is_ssl_) {
+    static_cast<SSLClient &>(*cli_).set_ca_cert_store(ca_cert_store);
+  } else {
+    cli_->set_ca_cert_store(ca_cert_store);
+  }
+}
+
+long Client::get_openssl_verify_result() const {
+  if (is_ssl_) {
+    return static_cast<SSLClient &>(*cli_).get_openssl_verify_result();
+  }
+  return -1; // NOTE: -1 doesn't match any of X509_V_ERR_???
+}
+
+SSL_CTX *Client::ssl_context() const {
+  if (is_ssl_) { return static_cast<SSLClient &>(*cli_).ssl_context(); }
+  return nullptr;
+}
+#endif
+
+} // namespace httplib
diff --git a/src/third_party/httplib.h b/src/third_party/httplib.h
new file mode 100644 (file)
index 0000000..f2b4efa
--- /dev/null
@@ -0,0 +1,1800 @@
+//
+//  httplib.h
+//
+//  Copyright (c) 2021 Yuji Hirose. All rights reserved.
+//  MIT License
+//
+
+#ifndef CPPHTTPLIB_HTTPLIB_H
+#define CPPHTTPLIB_HTTPLIB_H
+
+/*
+ * Configuration
+ */
+
+#ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND
+#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND 5
+#endif
+
+#ifndef CPPHTTPLIB_KEEPALIVE_MAX_COUNT
+#define CPPHTTPLIB_KEEPALIVE_MAX_COUNT 5
+#endif
+
+#ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND
+#define CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND 300
+#endif
+
+#ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND
+#define CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND 0
+#endif
+
+#ifndef CPPHTTPLIB_READ_TIMEOUT_SECOND
+#define CPPHTTPLIB_READ_TIMEOUT_SECOND 5
+#endif
+
+#ifndef CPPHTTPLIB_READ_TIMEOUT_USECOND
+#define CPPHTTPLIB_READ_TIMEOUT_USECOND 0
+#endif
+
+#ifndef CPPHTTPLIB_WRITE_TIMEOUT_SECOND
+#define CPPHTTPLIB_WRITE_TIMEOUT_SECOND 5
+#endif
+
+#ifndef CPPHTTPLIB_WRITE_TIMEOUT_USECOND
+#define CPPHTTPLIB_WRITE_TIMEOUT_USECOND 0
+#endif
+
+#ifndef CPPHTTPLIB_IDLE_INTERVAL_SECOND
+#define CPPHTTPLIB_IDLE_INTERVAL_SECOND 0
+#endif
+
+#ifndef CPPHTTPLIB_IDLE_INTERVAL_USECOND
+#ifdef _WIN32
+#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 10000
+#else
+#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 0
+#endif
+#endif
+
+#ifndef CPPHTTPLIB_REQUEST_URI_MAX_LENGTH
+#define CPPHTTPLIB_REQUEST_URI_MAX_LENGTH 8192
+#endif
+
+#ifndef CPPHTTPLIB_REDIRECT_MAX_COUNT
+#define CPPHTTPLIB_REDIRECT_MAX_COUNT 20
+#endif
+
+#ifndef CPPHTTPLIB_PAYLOAD_MAX_LENGTH
+#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH ((std::numeric_limits<size_t>::max)())
+#endif
+
+#ifndef CPPHTTPLIB_TCP_NODELAY
+#define CPPHTTPLIB_TCP_NODELAY false
+#endif
+
+#ifndef CPPHTTPLIB_RECV_BUFSIZ
+#define CPPHTTPLIB_RECV_BUFSIZ size_t(4096u)
+#endif
+
+#ifndef CPPHTTPLIB_COMPRESSION_BUFSIZ
+#define CPPHTTPLIB_COMPRESSION_BUFSIZ size_t(16384u)
+#endif
+
+#ifndef CPPHTTPLIB_THREAD_POOL_COUNT
+#define CPPHTTPLIB_THREAD_POOL_COUNT                                           \
+  ((std::max)(8u, std::thread::hardware_concurrency() > 0                      \
+                      ? std::thread::hardware_concurrency() - 1                \
+                      : 0))
+#endif
+
+#ifndef CPPHTTPLIB_RECV_FLAGS
+#define CPPHTTPLIB_RECV_FLAGS 0
+#endif
+
+#ifndef CPPHTTPLIB_SEND_FLAGS
+#define CPPHTTPLIB_SEND_FLAGS 0
+#endif
+
+/*
+ * Headers
+ */
+
+#ifdef _WIN32
+#ifndef _CRT_SECURE_NO_WARNINGS
+#define _CRT_SECURE_NO_WARNINGS
+#endif //_CRT_SECURE_NO_WARNINGS
+
+#ifndef _CRT_NONSTDC_NO_DEPRECATE
+#define _CRT_NONSTDC_NO_DEPRECATE
+#endif //_CRT_NONSTDC_NO_DEPRECATE
+
+#if defined(_MSC_VER)
+#ifdef _WIN64
+using ssize_t = __int64;
+#else
+using ssize_t = int;
+#endif
+
+#if _MSC_VER < 1900
+#define snprintf _snprintf_s
+#endif
+#endif // _MSC_VER
+
+#ifndef S_ISREG
+#define S_ISREG(m) (((m)&S_IFREG) == S_IFREG)
+#endif // S_ISREG
+
+#ifndef S_ISDIR
+#define S_ISDIR(m) (((m)&S_IFDIR) == S_IFDIR)
+#endif // S_ISDIR
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif // NOMINMAX
+
+#include <io.h>
+#include <winsock2.h>
+
+#include <wincrypt.h>
+#include <ws2tcpip.h>
+
+#ifndef WSA_FLAG_NO_HANDLE_INHERIT
+#define WSA_FLAG_NO_HANDLE_INHERIT 0x80
+#endif
+
+#ifdef _MSC_VER
+#pragma comment(lib, "ws2_32.lib")
+#pragma comment(lib, "crypt32.lib")
+#pragma comment(lib, "cryptui.lib")
+#endif
+
+#ifndef strcasecmp
+#define strcasecmp _stricmp
+#endif // strcasecmp
+
+using socket_t = SOCKET;
+#ifdef CPPHTTPLIB_USE_POLL
+#define poll(fds, nfds, timeout) WSAPoll(fds, nfds, timeout)
+#endif
+
+#else // not _WIN32
+
+#include <arpa/inet.h>
+#include <cstring>
+#include <ifaddrs.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#ifdef __linux__
+#include <resolv.h>
+#endif
+#include <netinet/tcp.h>
+#ifdef CPPHTTPLIB_USE_POLL
+#include <poll.h>
+#endif
+#include <csignal>
+#include <pthread.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+using socket_t = int;
+#define INVALID_SOCKET (-1)
+#endif //_WIN32
+
+#include <algorithm>
+#include <array>
+#include <atomic>
+#include <cassert>
+#include <cctype>
+#include <climits>
+#include <condition_variable>
+#include <errno.h>
+#include <fcntl.h>
+#include <fstream>
+#include <functional>
+#include <iomanip>
+#include <iostream>
+#include <list>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <random>
+#include <regex>
+#include <set>
+#include <sstream>
+#include <string>
+#include <sys/stat.h>
+#include <thread>
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+#include <openssl/err.h>
+#include <openssl/md5.h>
+#include <openssl/ssl.h>
+#include <openssl/x509v3.h>
+
+#if defined(_WIN32) && defined(OPENSSL_USE_APPLINK)
+#include <openssl/applink.c>
+#endif
+
+#include <iostream>
+#include <sstream>
+
+#if OPENSSL_VERSION_NUMBER < 0x1010100fL
+#error Sorry, OpenSSL versions prior to 1.1.1 are not supported
+#endif
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+#include <openssl/crypto.h>
+inline const unsigned char *ASN1_STRING_get0_data(const ASN1_STRING *asn1) {
+  return M_ASN1_STRING_data(asn1);
+}
+#endif
+#endif
+
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+#include <zlib.h>
+#endif
+
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+#include <brotli/decode.h>
+#include <brotli/encode.h>
+#endif
+
+/*
+ * Declaration
+ */
+namespace httplib {
+
+namespace detail {
+
+/*
+ * Backport std::make_unique from C++14.
+ *
+ * NOTE: This code came up with the following stackoverflow post:
+ * https://stackoverflow.com/questions/10149840/c-arrays-and-make-unique
+ *
+ */
+
+template <class T, class... Args>
+typename std::enable_if<!std::is_array<T>::value, std::unique_ptr<T>>::type
+make_unique(Args &&... args) {
+  return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
+}
+
+template <class T>
+typename std::enable_if<std::is_array<T>::value, std::unique_ptr<T>>::type
+make_unique(std::size_t n) {
+  typedef typename std::remove_extent<T>::type RT;
+  return std::unique_ptr<T>(new RT[n]);
+}
+
+struct ci {
+  bool operator()(const std::string &s1, const std::string &s2) const {
+    return std::lexicographical_compare(s1.begin(), s1.end(), s2.begin(),
+                                        s2.end(),
+                                        [](unsigned char c1, unsigned char c2) {
+                                          return ::tolower(c1) < ::tolower(c2);
+                                        });
+  }
+};
+
+} // namespace detail
+
+using Headers = std::multimap<std::string, std::string, detail::ci>;
+
+using Params = std::multimap<std::string, std::string>;
+using Match = std::smatch;
+
+using Progress = std::function<bool(uint64_t current, uint64_t total)>;
+
+struct Response;
+using ResponseHandler = std::function<bool(const Response &response)>;
+
+struct MultipartFormData {
+  std::string name;
+  std::string content;
+  std::string filename;
+  std::string content_type;
+};
+using MultipartFormDataItems = std::vector<MultipartFormData>;
+using MultipartFormDataMap = std::multimap<std::string, MultipartFormData>;
+
+class DataSink {
+public:
+  DataSink() : os(&sb_), sb_(*this) {}
+
+  DataSink(const DataSink &) = delete;
+  DataSink &operator=(const DataSink &) = delete;
+  DataSink(DataSink &&) = delete;
+  DataSink &operator=(DataSink &&) = delete;
+
+  std::function<bool(const char *data, size_t data_len)> write;
+  std::function<void()> done;
+  std::function<bool()> is_writable;
+  std::ostream os;
+
+private:
+  class data_sink_streambuf : public std::streambuf {
+  public:
+    explicit data_sink_streambuf(DataSink &sink) : sink_(sink) {}
+
+  protected:
+    std::streamsize xsputn(const char *s, std::streamsize n) {
+      sink_.write(s, static_cast<size_t>(n));
+      return n;
+    }
+
+  private:
+    DataSink &sink_;
+  };
+
+  data_sink_streambuf sb_;
+};
+
+using ContentProvider =
+    std::function<bool(size_t offset, size_t length, DataSink &sink)>;
+
+using ContentProviderWithoutLength =
+    std::function<bool(size_t offset, DataSink &sink)>;
+
+using ContentProviderResourceReleaser = std::function<void(bool success)>;
+
+using ContentReceiverWithProgress =
+    std::function<bool(const char *data, size_t data_length, uint64_t offset,
+                       uint64_t total_length)>;
+
+using ContentReceiver =
+    std::function<bool(const char *data, size_t data_length)>;
+
+using MultipartContentHeader =
+    std::function<bool(const MultipartFormData &file)>;
+
+class ContentReader {
+public:
+  using Reader = std::function<bool(ContentReceiver receiver)>;
+  using MultipartReader = std::function<bool(MultipartContentHeader header,
+                                             ContentReceiver receiver)>;
+
+  ContentReader(Reader reader, MultipartReader multipart_reader)
+      : reader_(std::move(reader)),
+        multipart_reader_(std::move(multipart_reader)) {}
+
+  bool operator()(MultipartContentHeader header,
+                  ContentReceiver receiver) const {
+    return multipart_reader_(std::move(header), std::move(receiver));
+  }
+
+  bool operator()(ContentReceiver receiver) const {
+    return reader_(std::move(receiver));
+  }
+
+  Reader reader_;
+  MultipartReader multipart_reader_;
+};
+
+using Range = std::pair<ssize_t, ssize_t>;
+using Ranges = std::vector<Range>;
+
+struct Request {
+  std::string method;
+  std::string path;
+  Headers headers;
+  std::string body;
+
+  std::string remote_addr;
+  int remote_port = -1;
+
+  // for server
+  std::string version;
+  std::string target;
+  Params params;
+  MultipartFormDataMap files;
+  Ranges ranges;
+  Match matches;
+
+  // for client
+  ResponseHandler response_handler;
+  ContentReceiverWithProgress content_receiver;
+  Progress progress;
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  const SSL *ssl = nullptr;
+#endif
+
+  bool has_header(const char *key) const;
+  std::string get_header_value(const char *key, size_t id = 0) const;
+  template <typename T>
+  T get_header_value(const char *key, size_t id = 0) const;
+  size_t get_header_value_count(const char *key) const;
+  void set_header(const char *key, const char *val);
+  void set_header(const char *key, const std::string &val);
+
+  bool has_param(const char *key) const;
+  std::string get_param_value(const char *key, size_t id = 0) const;
+  size_t get_param_value_count(const char *key) const;
+
+  bool is_multipart_form_data() const;
+
+  bool has_file(const char *key) const;
+  MultipartFormData get_file_value(const char *key) const;
+
+  // private members...
+  size_t redirect_count_ = CPPHTTPLIB_REDIRECT_MAX_COUNT;
+  size_t content_length_ = 0;
+  ContentProvider content_provider_;
+  bool is_chunked_content_provider_ = false;
+  size_t authorization_count_ = 0;
+};
+
+struct Response {
+  std::string version;
+  int status = -1;
+  std::string reason;
+  Headers headers;
+  std::string body;
+  std::string location; // Redirect location
+
+  bool has_header(const char *key) const;
+  std::string get_header_value(const char *key, size_t id = 0) const;
+  template <typename T>
+  T get_header_value(const char *key, size_t id = 0) const;
+  size_t get_header_value_count(const char *key) const;
+  void set_header(const char *key, const char *val);
+  void set_header(const char *key, const std::string &val);
+
+  void set_redirect(const char *url, int status = 302);
+  void set_redirect(const std::string &url, int status = 302);
+  void set_content(const char *s, size_t n, const char *content_type);
+  void set_content(const std::string &s, const char *content_type);
+
+  void set_content_provider(
+      size_t length, const char *content_type, ContentProvider provider,
+      ContentProviderResourceReleaser resource_releaser = nullptr);
+
+  void set_content_provider(
+      const char *content_type, ContentProviderWithoutLength provider,
+      ContentProviderResourceReleaser resource_releaser = nullptr);
+
+  void set_chunked_content_provider(
+      const char *content_type, ContentProviderWithoutLength provider,
+      ContentProviderResourceReleaser resource_releaser = nullptr);
+
+  Response() = default;
+  Response(const Response &) = default;
+  Response &operator=(const Response &) = default;
+  Response(Response &&) = default;
+  Response &operator=(Response &&) = default;
+  ~Response() {
+    if (content_provider_resource_releaser_) {
+      content_provider_resource_releaser_(content_provider_success_);
+    }
+  }
+
+  // private members...
+  size_t content_length_ = 0;
+  ContentProvider content_provider_;
+  ContentProviderResourceReleaser content_provider_resource_releaser_;
+  bool is_chunked_content_provider_ = false;
+  bool content_provider_success_ = false;
+};
+
+class Stream {
+public:
+  virtual ~Stream() = default;
+
+  virtual bool is_readable() const = 0;
+  virtual bool is_writable() const = 0;
+
+  virtual ssize_t read(char *ptr, size_t size) = 0;
+  virtual ssize_t write(const char *ptr, size_t size) = 0;
+  virtual void get_remote_ip_and_port(std::string &ip, int &port) const = 0;
+  virtual socket_t socket() const = 0;
+
+  template <typename... Args>
+  ssize_t write_format(const char *fmt, const Args &... args);
+  ssize_t write(const char *ptr);
+  ssize_t write(const std::string &s);
+};
+
+class TaskQueue {
+public:
+  TaskQueue() = default;
+  virtual ~TaskQueue() = default;
+
+  virtual void enqueue(std::function<void()> fn) = 0;
+  virtual void shutdown() = 0;
+
+  virtual void on_idle(){};
+};
+
+class ThreadPool : public TaskQueue {
+public:
+  explicit ThreadPool(size_t n) : shutdown_(false) {
+    while (n) {
+      threads_.emplace_back(worker(*this));
+      n--;
+    }
+  }
+
+  ThreadPool(const ThreadPool &) = delete;
+  ~ThreadPool() override = default;
+
+  void enqueue(std::function<void()> fn) override {
+    std::unique_lock<std::mutex> lock(mutex_);
+    jobs_.push_back(std::move(fn));
+    cond_.notify_one();
+  }
+
+  void shutdown() override {
+    // Stop all worker threads...
+    {
+      std::unique_lock<std::mutex> lock(mutex_);
+      shutdown_ = true;
+    }
+
+    cond_.notify_all();
+
+    // Join...
+    for (auto &t : threads_) {
+      t.join();
+    }
+  }
+
+private:
+  struct worker {
+    explicit worker(ThreadPool &pool) : pool_(pool) {}
+
+    void operator()() {
+      for (;;) {
+        std::function<void()> fn;
+        {
+          std::unique_lock<std::mutex> lock(pool_.mutex_);
+
+          pool_.cond_.wait(
+              lock, [&] { return !pool_.jobs_.empty() || pool_.shutdown_; });
+
+          if (pool_.shutdown_ && pool_.jobs_.empty()) { break; }
+
+          fn = pool_.jobs_.front();
+          pool_.jobs_.pop_front();
+        }
+
+        assert(true == static_cast<bool>(fn));
+        fn();
+      }
+    }
+
+    ThreadPool &pool_;
+  };
+  friend struct worker;
+
+  std::vector<std::thread> threads_;
+  std::list<std::function<void()>> jobs_;
+
+  bool shutdown_;
+
+  std::condition_variable cond_;
+  std::mutex mutex_;
+};
+
+using Logger = std::function<void(const Request &, const Response &)>;
+
+using SocketOptions = std::function<void(socket_t sock)>;
+
+inline void default_socket_options(socket_t sock) {
+  int yes = 1;
+#ifdef _WIN32
+  setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<char *>(&yes),
+             sizeof(yes));
+  setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
+             reinterpret_cast<char *>(&yes), sizeof(yes));
+#else
+#ifdef SO_REUSEPORT
+  setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast<void *>(&yes),
+             sizeof(yes));
+#else
+  setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<void *>(&yes),
+             sizeof(yes));
+#endif
+#endif
+}
+
+class Server {
+public:
+  using Handler = std::function<void(const Request &, Response &)>;
+
+  using ExceptionHandler =
+      std::function<void(const Request &, Response &, std::exception &e)>;
+
+  enum class HandlerResponse {
+    Handled,
+    Unhandled,
+  };
+  using HandlerWithResponse =
+      std::function<HandlerResponse(const Request &, Response &)>;
+
+  using HandlerWithContentReader = std::function<void(
+      const Request &, Response &, const ContentReader &content_reader)>;
+
+  using Expect100ContinueHandler =
+      std::function<int(const Request &, Response &)>;
+
+  Server();
+
+  virtual ~Server();
+
+  virtual bool is_valid() const;
+
+  Server &Get(const std::string &pattern, Handler handler);
+  Server &Post(const std::string &pattern, Handler handler);
+  Server &Post(const std::string &pattern, HandlerWithContentReader handler);
+  Server &Put(const std::string &pattern, Handler handler);
+  Server &Put(const std::string &pattern, HandlerWithContentReader handler);
+  Server &Patch(const std::string &pattern, Handler handler);
+  Server &Patch(const std::string &pattern, HandlerWithContentReader handler);
+  Server &Delete(const std::string &pattern, Handler handler);
+  Server &Delete(const std::string &pattern, HandlerWithContentReader handler);
+  Server &Options(const std::string &pattern, Handler handler);
+
+  bool set_base_dir(const std::string &dir,
+                    const std::string &mount_point = nullptr);
+  bool set_mount_point(const std::string &mount_point, const std::string &dir,
+                       Headers headers = Headers());
+  bool remove_mount_point(const std::string &mount_point);
+  Server &set_file_extension_and_mimetype_mapping(const char *ext,
+                                                  const char *mime);
+  Server &set_file_request_handler(Handler handler);
+
+  Server &set_error_handler(HandlerWithResponse handler);
+  Server &set_error_handler(Handler handler);
+  Server &set_exception_handler(ExceptionHandler handler);
+  Server &set_pre_routing_handler(HandlerWithResponse handler);
+  Server &set_post_routing_handler(Handler handler);
+
+  Server &set_expect_100_continue_handler(Expect100ContinueHandler handler);
+  Server &set_logger(Logger logger);
+
+  Server &set_address_family(int family);
+  Server &set_tcp_nodelay(bool on);
+  Server &set_socket_options(SocketOptions socket_options);
+
+  Server &set_default_headers(Headers headers);
+
+  Server &set_keep_alive_max_count(size_t count);
+  Server &set_keep_alive_timeout(time_t sec);
+
+  Server &set_read_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  Server &set_read_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  Server &set_write_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  Server &set_write_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  Server &set_idle_interval(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  Server &set_idle_interval(const std::chrono::duration<Rep, Period> &duration);
+
+  Server &set_payload_max_length(size_t length);
+
+  bool bind_to_port(const char *host, int port, int socket_flags = 0);
+  int bind_to_any_port(const char *host, int socket_flags = 0);
+  bool listen_after_bind();
+
+  bool listen(const char *host, int port, int socket_flags = 0);
+
+  bool is_running() const;
+  void stop();
+
+  std::function<TaskQueue *(void)> new_task_queue;
+
+protected:
+  bool process_request(Stream &strm, bool close_connection,
+                       bool &connection_closed,
+                       const std::function<void(Request &)> &setup_request);
+
+  std::atomic<socket_t> svr_sock_;
+  size_t keep_alive_max_count_ = CPPHTTPLIB_KEEPALIVE_MAX_COUNT;
+  time_t keep_alive_timeout_sec_ = CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND;
+  time_t read_timeout_sec_ = CPPHTTPLIB_READ_TIMEOUT_SECOND;
+  time_t read_timeout_usec_ = CPPHTTPLIB_READ_TIMEOUT_USECOND;
+  time_t write_timeout_sec_ = CPPHTTPLIB_WRITE_TIMEOUT_SECOND;
+  time_t write_timeout_usec_ = CPPHTTPLIB_WRITE_TIMEOUT_USECOND;
+  time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;
+  time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;
+  size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
+
+private:
+  using Handlers = std::vector<std::pair<std::regex, Handler>>;
+  using HandlersForContentReader =
+      std::vector<std::pair<std::regex, HandlerWithContentReader>>;
+
+  socket_t create_server_socket(const char *host, int port, int socket_flags,
+                                SocketOptions socket_options) const;
+  int bind_internal(const char *host, int port, int socket_flags);
+  bool listen_internal();
+
+  bool routing(Request &req, Response &res, Stream &strm);
+  bool handle_file_request(const Request &req, Response &res,
+                           bool head = false);
+  bool dispatch_request(Request &req, Response &res, const Handlers &handlers);
+  bool
+  dispatch_request_for_content_reader(Request &req, Response &res,
+                                      ContentReader content_reader,
+                                      const HandlersForContentReader &handlers);
+
+  bool parse_request_line(const char *s, Request &req);
+  void apply_ranges(const Request &req, Response &res,
+                    std::string &content_type, std::string &boundary);
+  bool write_response(Stream &strm, bool close_connection, const Request &req,
+                      Response &res);
+  bool write_response_with_content(Stream &strm, bool close_connection,
+                                   const Request &req, Response &res);
+  bool write_response_core(Stream &strm, bool close_connection,
+                           const Request &req, Response &res,
+                           bool need_apply_ranges);
+  bool write_content_with_provider(Stream &strm, const Request &req,
+                                   Response &res, const std::string &boundary,
+                                   const std::string &content_type);
+  bool read_content(Stream &strm, Request &req, Response &res);
+  bool
+  read_content_with_content_receiver(Stream &strm, Request &req, Response &res,
+                                     ContentReceiver receiver,
+                                     MultipartContentHeader multipart_header,
+                                     ContentReceiver multipart_receiver);
+  bool read_content_core(Stream &strm, Request &req, Response &res,
+                         ContentReceiver receiver,
+                         MultipartContentHeader mulitpart_header,
+                         ContentReceiver multipart_receiver);
+
+  virtual bool process_and_close_socket(socket_t sock);
+
+  struct MountPointEntry {
+    std::string mount_point;
+    std::string base_dir;
+    Headers headers;
+  };
+  std::vector<MountPointEntry> base_dirs_;
+
+  std::atomic<bool> is_running_;
+  std::map<std::string, std::string> file_extension_and_mimetype_map_;
+  Handler file_request_handler_;
+  Handlers get_handlers_;
+  Handlers post_handlers_;
+  HandlersForContentReader post_handlers_for_content_reader_;
+  Handlers put_handlers_;
+  HandlersForContentReader put_handlers_for_content_reader_;
+  Handlers patch_handlers_;
+  HandlersForContentReader patch_handlers_for_content_reader_;
+  Handlers delete_handlers_;
+  HandlersForContentReader delete_handlers_for_content_reader_;
+  Handlers options_handlers_;
+  HandlerWithResponse error_handler_;
+  ExceptionHandler exception_handler_;
+  HandlerWithResponse pre_routing_handler_;
+  Handler post_routing_handler_;
+  Logger logger_;
+  Expect100ContinueHandler expect_100_continue_handler_;
+
+  int address_family_ = AF_UNSPEC;
+  bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY;
+  SocketOptions socket_options_ = default_socket_options;
+
+  Headers default_headers_;
+};
+
+enum class Error {
+  Success = 0,
+  Unknown,
+  Connection,
+  BindIPAddress,
+  Read,
+  Write,
+  ExceedRedirectCount,
+  Canceled,
+  SSLConnection,
+  SSLLoadingCerts,
+  SSLServerVerification,
+  UnsupportedMultipartBoundaryChars,
+  Compression,
+};
+
+inline std::string to_string(const Error error) {
+  switch (error) {
+  case Error::Success: return "Success";
+  case Error::Connection: return "Connection";
+  case Error::BindIPAddress: return "BindIPAddress";
+  case Error::Read: return "Read";
+  case Error::Write: return "Write";
+  case Error::ExceedRedirectCount: return "ExceedRedirectCount";
+  case Error::Canceled: return "Canceled";
+  case Error::SSLConnection: return "SSLConnection";
+  case Error::SSLLoadingCerts: return "SSLLoadingCerts";
+  case Error::SSLServerVerification: return "SSLServerVerification";
+  case Error::UnsupportedMultipartBoundaryChars:
+    return "UnsupportedMultipartBoundaryChars";
+  case Error::Compression: return "Compression";
+  case Error::Unknown: return "Unknown";
+  default: break;
+  }
+
+  return "Invalid";
+}
+
+inline std::ostream &operator<<(std::ostream &os, const Error &obj) {
+  os << to_string(obj);
+  os << " (" << static_cast<std::underlying_type<Error>::type>(obj) << ')';
+  return os;
+}
+
+class Result {
+public:
+  Result(std::unique_ptr<Response> &&res, Error err,
+         Headers &&request_headers = Headers{})
+      : res_(std::move(res)), err_(err),
+        request_headers_(std::move(request_headers)) {}
+  // Response
+  operator bool() const { return res_ != nullptr; }
+  bool operator==(std::nullptr_t) const { return res_ == nullptr; }
+  bool operator!=(std::nullptr_t) const { return res_ != nullptr; }
+  const Response &value() const { return *res_; }
+  Response &value() { return *res_; }
+  const Response &operator*() const { return *res_; }
+  Response &operator*() { return *res_; }
+  const Response *operator->() const { return res_.get(); }
+  Response *operator->() { return res_.get(); }
+
+  // Error
+  Error error() const { return err_; }
+
+  // Request Headers
+  bool has_request_header(const char *key) const;
+  std::string get_request_header_value(const char *key, size_t id = 0) const;
+  template <typename T>
+  T get_request_header_value(const char *key, size_t id = 0) const;
+  size_t get_request_header_value_count(const char *key) const;
+
+private:
+  std::unique_ptr<Response> res_;
+  Error err_;
+  Headers request_headers_;
+};
+
+class ClientImpl {
+public:
+  explicit ClientImpl(const std::string &host);
+
+  explicit ClientImpl(const std::string &host, int port);
+
+  explicit ClientImpl(const std::string &host, int port,
+                      const std::string &client_cert_path,
+                      const std::string &client_key_path);
+
+  virtual ~ClientImpl();
+
+  virtual bool is_valid() const;
+
+  Result Get(const char *path);
+  Result Get(const char *path, const Headers &headers);
+  Result Get(const char *path, Progress progress);
+  Result Get(const char *path, const Headers &headers, Progress progress);
+  Result Get(const char *path, ContentReceiver content_receiver);
+  Result Get(const char *path, const Headers &headers,
+             ContentReceiver content_receiver);
+  Result Get(const char *path, ContentReceiver content_receiver,
+             Progress progress);
+  Result Get(const char *path, const Headers &headers,
+             ContentReceiver content_receiver, Progress progress);
+  Result Get(const char *path, ResponseHandler response_handler,
+             ContentReceiver content_receiver);
+  Result Get(const char *path, const Headers &headers,
+             ResponseHandler response_handler,
+             ContentReceiver content_receiver);
+  Result Get(const char *path, ResponseHandler response_handler,
+             ContentReceiver content_receiver, Progress progress);
+  Result Get(const char *path, const Headers &headers,
+             ResponseHandler response_handler, ContentReceiver content_receiver,
+             Progress progress);
+
+  Result Get(const char *path, const Params &params, const Headers &headers,
+             Progress progress = nullptr);
+  Result Get(const char *path, const Params &params, const Headers &headers,
+             ContentReceiver content_receiver, Progress progress = nullptr);
+  Result Get(const char *path, const Params &params, const Headers &headers,
+             ResponseHandler response_handler, ContentReceiver content_receiver,
+             Progress progress = nullptr);
+
+  Result Head(const char *path);
+  Result Head(const char *path, const Headers &headers);
+
+  Result Post(const char *path);
+  Result Post(const char *path, const char *body, size_t content_length,
+              const char *content_type);
+  Result Post(const char *path, const Headers &headers, const char *body,
+              size_t content_length, const char *content_type);
+  Result Post(const char *path, const std::string &body,
+              const char *content_type);
+  Result Post(const char *path, const Headers &headers, const std::string &body,
+              const char *content_type);
+  Result Post(const char *path, size_t content_length,
+              ContentProvider content_provider, const char *content_type);
+  Result Post(const char *path, ContentProviderWithoutLength content_provider,
+              const char *content_type);
+  Result Post(const char *path, const Headers &headers, size_t content_length,
+              ContentProvider content_provider, const char *content_type);
+  Result Post(const char *path, const Headers &headers,
+              ContentProviderWithoutLength content_provider,
+              const char *content_type);
+  Result Post(const char *path, const Params &params);
+  Result Post(const char *path, const Headers &headers, const Params &params);
+  Result Post(const char *path, const MultipartFormDataItems &items);
+  Result Post(const char *path, const Headers &headers,
+              const MultipartFormDataItems &items);
+  Result Post(const char *path, const Headers &headers,
+              const MultipartFormDataItems &items, const std::string &boundary);
+
+  Result Put(const char *path);
+  Result Put(const char *path, const char *body, size_t content_length,
+             const char *content_type);
+  Result Put(const char *path, const Headers &headers, const char *body,
+             size_t content_length, const char *content_type);
+  Result Put(const char *path, const std::string &body,
+             const char *content_type);
+  Result Put(const char *path, const Headers &headers, const std::string &body,
+             const char *content_type);
+  Result Put(const char *path, size_t content_length,
+             ContentProvider content_provider, const char *content_type);
+  Result Put(const char *path, ContentProviderWithoutLength content_provider,
+             const char *content_type);
+  Result Put(const char *path, const Headers &headers, size_t content_length,
+             ContentProvider content_provider, const char *content_type);
+  Result Put(const char *path, const Headers &headers,
+             ContentProviderWithoutLength content_provider,
+             const char *content_type);
+  Result Put(const char *path, const Params &params);
+  Result Put(const char *path, const Headers &headers, const Params &params);
+
+  Result Patch(const char *path);
+  Result Patch(const char *path, const char *body, size_t content_length,
+               const char *content_type);
+  Result Patch(const char *path, const Headers &headers, const char *body,
+               size_t content_length, const char *content_type);
+  Result Patch(const char *path, const std::string &body,
+               const char *content_type);
+  Result Patch(const char *path, const Headers &headers,
+               const std::string &body, const char *content_type);
+  Result Patch(const char *path, size_t content_length,
+               ContentProvider content_provider, const char *content_type);
+  Result Patch(const char *path, ContentProviderWithoutLength content_provider,
+               const char *content_type);
+  Result Patch(const char *path, const Headers &headers, size_t content_length,
+               ContentProvider content_provider, const char *content_type);
+  Result Patch(const char *path, const Headers &headers,
+               ContentProviderWithoutLength content_provider,
+               const char *content_type);
+
+  Result Delete(const char *path);
+  Result Delete(const char *path, const Headers &headers);
+  Result Delete(const char *path, const char *body, size_t content_length,
+                const char *content_type);
+  Result Delete(const char *path, const Headers &headers, const char *body,
+                size_t content_length, const char *content_type);
+  Result Delete(const char *path, const std::string &body,
+                const char *content_type);
+  Result Delete(const char *path, const Headers &headers,
+                const std::string &body, const char *content_type);
+
+  Result Options(const char *path);
+  Result Options(const char *path, const Headers &headers);
+
+  bool send(Request &req, Response &res, Error &error);
+  Result send(const Request &req);
+
+  size_t is_socket_open() const;
+
+  void stop();
+
+  void set_default_headers(Headers headers);
+
+  void set_address_family(int family);
+  void set_tcp_nodelay(bool on);
+  void set_socket_options(SocketOptions socket_options);
+
+  void set_connection_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  void
+  set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_read_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  void set_read_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_write_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  void set_write_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_basic_auth(const char *username, const char *password);
+  void set_bearer_token_auth(const char *token);
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  void set_digest_auth(const char *username, const char *password);
+#endif
+
+  void set_keep_alive(bool on);
+  void set_follow_location(bool on);
+
+  void set_url_encode(bool on);
+
+  void set_compress(bool on);
+
+  void set_decompress(bool on);
+
+  void set_interface(const char *intf);
+
+  void set_proxy(const char *host, int port);
+  void set_proxy_basic_auth(const char *username, const char *password);
+  void set_proxy_bearer_token_auth(const char *token);
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  void set_proxy_digest_auth(const char *username, const char *password);
+#endif
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  void set_ca_cert_path(const char *ca_cert_file_path,
+                        const char *ca_cert_dir_path = nullptr);
+  void set_ca_cert_store(X509_STORE *ca_cert_store);
+#endif
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  void enable_server_certificate_verification(bool enabled);
+#endif
+
+  void set_logger(Logger logger);
+
+protected:
+  struct Socket {
+    socket_t sock = INVALID_SOCKET;
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+    SSL *ssl = nullptr;
+#endif
+
+    bool is_open() const { return sock != INVALID_SOCKET; }
+  };
+
+  Result send_(Request &&req);
+
+  virtual bool create_and_connect_socket(Socket &socket, Error &error);
+
+  // All of:
+  //   shutdown_ssl
+  //   shutdown_socket
+  //   close_socket
+  // should ONLY be called when socket_mutex_ is locked.
+  // Also, shutdown_ssl and close_socket should also NOT be called concurrently
+  // with a DIFFERENT thread sending requests using that socket.
+  virtual void shutdown_ssl(Socket &socket, bool shutdown_gracefully);
+  void shutdown_socket(Socket &socket);
+  void close_socket(Socket &socket);
+
+  bool process_request(Stream &strm, Request &req, Response &res,
+                       bool close_connection, Error &error);
+
+  bool write_content_with_provider(Stream &strm, const Request &req,
+                                   Error &error);
+
+  void copy_settings(const ClientImpl &rhs);
+
+  // Socket endoint information
+  const std::string host_;
+  const int port_;
+  const std::string host_and_port_;
+
+  // Current open socket
+  Socket socket_;
+  mutable std::mutex socket_mutex_;
+  std::recursive_mutex request_mutex_;
+
+  // These are all protected under socket_mutex
+  size_t socket_requests_in_flight_ = 0;
+  std::thread::id socket_requests_are_from_thread_ = std::thread::id();
+  bool socket_should_be_closed_when_request_is_done_ = false;
+
+  // Default headers
+  Headers default_headers_;
+
+  // Settings
+  std::string client_cert_path_;
+  std::string client_key_path_;
+
+  time_t connection_timeout_sec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND;
+  time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND;
+  time_t read_timeout_sec_ = CPPHTTPLIB_READ_TIMEOUT_SECOND;
+  time_t read_timeout_usec_ = CPPHTTPLIB_READ_TIMEOUT_USECOND;
+  time_t write_timeout_sec_ = CPPHTTPLIB_WRITE_TIMEOUT_SECOND;
+  time_t write_timeout_usec_ = CPPHTTPLIB_WRITE_TIMEOUT_USECOND;
+
+  std::string basic_auth_username_;
+  std::string basic_auth_password_;
+  std::string bearer_token_auth_token_;
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  std::string digest_auth_username_;
+  std::string digest_auth_password_;
+#endif
+
+  bool keep_alive_ = false;
+  bool follow_location_ = false;
+
+  bool url_encode_ = true;
+
+  int address_family_ = AF_UNSPEC;
+  bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY;
+  SocketOptions socket_options_ = nullptr;
+
+  bool compress_ = false;
+  bool decompress_ = true;
+
+  std::string interface_;
+
+  std::string proxy_host_;
+  int proxy_port_ = -1;
+
+  std::string proxy_basic_auth_username_;
+  std::string proxy_basic_auth_password_;
+  std::string proxy_bearer_token_auth_token_;
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  std::string proxy_digest_auth_username_;
+  std::string proxy_digest_auth_password_;
+#endif
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  std::string ca_cert_file_path_;
+  std::string ca_cert_dir_path_;
+
+  X509_STORE *ca_cert_store_ = nullptr;
+#endif
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  bool server_certificate_verification_ = true;
+#endif
+
+  Logger logger_;
+
+private:
+  socket_t create_client_socket(Error &error) const;
+  bool read_response_line(Stream &strm, const Request &req, Response &res);
+  bool write_request(Stream &strm, Request &req, bool close_connection,
+                     Error &error);
+  bool redirect(Request &req, Response &res, Error &error);
+  bool handle_request(Stream &strm, Request &req, Response &res,
+                      bool close_connection, Error &error);
+  std::unique_ptr<Response> send_with_content_provider(
+      Request &req,
+      // const char *method, const char *path, const Headers &headers,
+      const char *body, size_t content_length, ContentProvider content_provider,
+      ContentProviderWithoutLength content_provider_without_length,
+      const char *content_type, Error &error);
+  Result send_with_content_provider(
+      const char *method, const char *path, const Headers &headers,
+      const char *body, size_t content_length, ContentProvider content_provider,
+      ContentProviderWithoutLength content_provider_without_length,
+      const char *content_type);
+
+  std::string adjust_host_string(const std::string &host) const;
+
+  virtual bool process_socket(const Socket &socket,
+                              std::function<bool(Stream &strm)> callback);
+  virtual bool is_ssl() const;
+};
+
+class Client {
+public:
+  // Universal interface
+  explicit Client(const std::string &scheme_host_port);
+
+  explicit Client(const std::string &scheme_host_port,
+                  const std::string &client_cert_path,
+                  const std::string &client_key_path);
+
+  // HTTP only interface
+  explicit Client(const std::string &host, int port);
+
+  explicit Client(const std::string &host, int port,
+                  const std::string &client_cert_path,
+                  const std::string &client_key_path);
+
+  ~Client();
+
+  bool is_valid() const;
+
+  Result Get(const char *path);
+  Result Get(const char *path, const Headers &headers);
+  Result Get(const char *path, Progress progress);
+  Result Get(const char *path, const Headers &headers, Progress progress);
+  Result Get(const char *path, ContentReceiver content_receiver);
+  Result Get(const char *path, const Headers &headers,
+             ContentReceiver content_receiver);
+  Result Get(const char *path, ContentReceiver content_receiver,
+             Progress progress);
+  Result Get(const char *path, const Headers &headers,
+             ContentReceiver content_receiver, Progress progress);
+  Result Get(const char *path, ResponseHandler response_handler,
+             ContentReceiver content_receiver);
+  Result Get(const char *path, const Headers &headers,
+             ResponseHandler response_handler,
+             ContentReceiver content_receiver);
+  Result Get(const char *path, const Headers &headers,
+             ResponseHandler response_handler, ContentReceiver content_receiver,
+             Progress progress);
+  Result Get(const char *path, ResponseHandler response_handler,
+             ContentReceiver content_receiver, Progress progress);
+
+  Result Get(const char *path, const Params &params, const Headers &headers,
+             Progress progress = nullptr);
+  Result Get(const char *path, const Params &params, const Headers &headers,
+             ContentReceiver content_receiver, Progress progress = nullptr);
+  Result Get(const char *path, const Params &params, const Headers &headers,
+             ResponseHandler response_handler, ContentReceiver content_receiver,
+             Progress progress = nullptr);
+
+  Result Head(const char *path);
+  Result Head(const char *path, const Headers &headers);
+
+  Result Post(const char *path);
+  Result Post(const char *path, const char *body, size_t content_length,
+              const char *content_type);
+  Result Post(const char *path, const Headers &headers, const char *body,
+              size_t content_length, const char *content_type);
+  Result Post(const char *path, const std::string &body,
+              const char *content_type);
+  Result Post(const char *path, const Headers &headers, const std::string &body,
+              const char *content_type);
+  Result Post(const char *path, size_t content_length,
+              ContentProvider content_provider, const char *content_type);
+  Result Post(const char *path, ContentProviderWithoutLength content_provider,
+              const char *content_type);
+  Result Post(const char *path, const Headers &headers, size_t content_length,
+              ContentProvider content_provider, const char *content_type);
+  Result Post(const char *path, const Headers &headers,
+              ContentProviderWithoutLength content_provider,
+              const char *content_type);
+  Result Post(const char *path, const Params &params);
+  Result Post(const char *path, const Headers &headers, const Params &params);
+  Result Post(const char *path, const MultipartFormDataItems &items);
+  Result Post(const char *path, const Headers &headers,
+              const MultipartFormDataItems &items);
+  Result Post(const char *path, const Headers &headers,
+              const MultipartFormDataItems &items, const std::string &boundary);
+  Result Put(const char *path);
+  Result Put(const char *path, const char *body, size_t content_length,
+             const char *content_type);
+  Result Put(const char *path, const Headers &headers, const char *body,
+             size_t content_length, const char *content_type);
+  Result Put(const char *path, const std::string &body,
+             const char *content_type);
+  Result Put(const char *path, const Headers &headers, const std::string &body,
+             const char *content_type);
+  Result Put(const char *path, size_t content_length,
+             ContentProvider content_provider, const char *content_type);
+  Result Put(const char *path, ContentProviderWithoutLength content_provider,
+             const char *content_type);
+  Result Put(const char *path, const Headers &headers, size_t content_length,
+             ContentProvider content_provider, const char *content_type);
+  Result Put(const char *path, const Headers &headers,
+             ContentProviderWithoutLength content_provider,
+             const char *content_type);
+  Result Put(const char *path, const Params &params);
+  Result Put(const char *path, const Headers &headers, const Params &params);
+  Result Patch(const char *path);
+  Result Patch(const char *path, const char *body, size_t content_length,
+               const char *content_type);
+  Result Patch(const char *path, const Headers &headers, const char *body,
+               size_t content_length, const char *content_type);
+  Result Patch(const char *path, const std::string &body,
+               const char *content_type);
+  Result Patch(const char *path, const Headers &headers,
+               const std::string &body, const char *content_type);
+  Result Patch(const char *path, size_t content_length,
+               ContentProvider content_provider, const char *content_type);
+  Result Patch(const char *path, ContentProviderWithoutLength content_provider,
+               const char *content_type);
+  Result Patch(const char *path, const Headers &headers, size_t content_length,
+               ContentProvider content_provider, const char *content_type);
+  Result Patch(const char *path, const Headers &headers,
+               ContentProviderWithoutLength content_provider,
+               const char *content_type);
+
+  Result Delete(const char *path);
+  Result Delete(const char *path, const Headers &headers);
+  Result Delete(const char *path, const char *body, size_t content_length,
+                const char *content_type);
+  Result Delete(const char *path, const Headers &headers, const char *body,
+                size_t content_length, const char *content_type);
+  Result Delete(const char *path, const std::string &body,
+                const char *content_type);
+  Result Delete(const char *path, const Headers &headers,
+                const std::string &body, const char *content_type);
+
+  Result Options(const char *path);
+  Result Options(const char *path, const Headers &headers);
+
+  bool send(Request &req, Response &res, Error &error);
+  Result send(const Request &req);
+
+  size_t is_socket_open() const;
+
+  void stop();
+
+  void set_default_headers(Headers headers);
+
+  void set_address_family(int family);
+  void set_tcp_nodelay(bool on);
+  void set_socket_options(SocketOptions socket_options);
+
+  void set_connection_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  void
+  set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_read_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  void set_read_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_write_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  void set_write_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_basic_auth(const char *username, const char *password);
+  void set_bearer_token_auth(const char *token);
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  void set_digest_auth(const char *username, const char *password);
+#endif
+
+  void set_keep_alive(bool on);
+  void set_follow_location(bool on);
+
+  void set_url_encode(bool on);
+
+  void set_compress(bool on);
+
+  void set_decompress(bool on);
+
+  void set_interface(const char *intf);
+
+  void set_proxy(const char *host, int port);
+  void set_proxy_basic_auth(const char *username, const char *password);
+  void set_proxy_bearer_token_auth(const char *token);
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  void set_proxy_digest_auth(const char *username, const char *password);
+#endif
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  void enable_server_certificate_verification(bool enabled);
+#endif
+
+  void set_logger(Logger logger);
+
+  // SSL
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  void set_ca_cert_path(const char *ca_cert_file_path,
+                        const char *ca_cert_dir_path = nullptr);
+
+  void set_ca_cert_store(X509_STORE *ca_cert_store);
+
+  long get_openssl_verify_result() const;
+
+  SSL_CTX *ssl_context() const;
+#endif
+
+private:
+  std::unique_ptr<ClientImpl> cli_;
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  bool is_ssl_ = false;
+#endif
+};
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+class SSLServer : public Server {
+public:
+  SSLServer(const char *cert_path, const char *private_key_path,
+            const char *client_ca_cert_file_path = nullptr,
+            const char *client_ca_cert_dir_path = nullptr);
+
+  SSLServer(X509 *cert, EVP_PKEY *private_key,
+            X509_STORE *client_ca_cert_store = nullptr);
+
+  ~SSLServer() override;
+
+  bool is_valid() const override;
+
+private:
+  bool process_and_close_socket(socket_t sock) override;
+
+  SSL_CTX *ctx_;
+  std::mutex ctx_mutex_;
+};
+
+class SSLClient : public ClientImpl {
+public:
+  explicit SSLClient(const std::string &host);
+
+  explicit SSLClient(const std::string &host, int port);
+
+  explicit SSLClient(const std::string &host, int port,
+                     const std::string &client_cert_path,
+                     const std::string &client_key_path);
+
+  explicit SSLClient(const std::string &host, int port, X509 *client_cert,
+                     EVP_PKEY *client_key);
+
+  ~SSLClient() override;
+
+  bool is_valid() const override;
+
+  void set_ca_cert_store(X509_STORE *ca_cert_store);
+
+  long get_openssl_verify_result() const;
+
+  SSL_CTX *ssl_context() const;
+
+private:
+  bool create_and_connect_socket(Socket &socket, Error &error) override;
+  void shutdown_ssl(Socket &socket, bool shutdown_gracefully) override;
+  void shutdown_ssl_impl(Socket &socket, bool shutdown_socket);
+
+  bool process_socket(const Socket &socket,
+                      std::function<bool(Stream &strm)> callback) override;
+  bool is_ssl() const override;
+
+  bool connect_with_proxy(Socket &sock, Response &res, bool &success,
+                          Error &error);
+  bool initialize_ssl(Socket &socket, Error &error);
+
+  bool load_certs();
+
+  bool verify_host(X509 *server_cert) const;
+  bool verify_host_with_subject_alt_name(X509 *server_cert) const;
+  bool verify_host_with_common_name(X509 *server_cert) const;
+  bool check_host_name(const char *pattern, size_t pattern_len) const;
+
+  SSL_CTX *ctx_;
+  std::mutex ctx_mutex_;
+  std::once_flag initialize_cert_;
+
+  std::vector<std::string> host_components_;
+
+  long verify_result_ = 0;
+
+  friend class ClientImpl;
+};
+#endif
+
+/*
+ * Implementation of template methods.
+ */
+
+namespace detail {
+
+template <typename T, typename U>
+inline void duration_to_sec_and_usec(const T &duration, U callback) {
+  auto sec = std::chrono::duration_cast<std::chrono::seconds>(duration).count();
+  auto usec = std::chrono::duration_cast<std::chrono::microseconds>(
+                  duration - std::chrono::seconds(sec))
+                  .count();
+  callback(sec, usec);
+}
+
+template <typename T>
+inline T get_header_value(const Headers & /*headers*/, const char * /*key*/,
+                          size_t /*id*/ = 0, uint64_t /*def*/ = 0) {}
+
+template <>
+inline uint64_t get_header_value<uint64_t>(const Headers &headers,
+                                           const char *key, size_t id,
+                                           uint64_t def) {
+  auto rng = headers.equal_range(key);
+  auto it = rng.first;
+  std::advance(it, static_cast<ssize_t>(id));
+  if (it != rng.second) {
+    return std::strtoull(it->second.data(), nullptr, 10);
+  }
+  return def;
+}
+
+} // namespace detail
+
+template <typename T>
+inline T Request::get_header_value(const char *key, size_t id) const {
+  return detail::get_header_value<T>(headers, key, id, 0);
+}
+
+template <typename T>
+inline T Response::get_header_value(const char *key, size_t id) const {
+  return detail::get_header_value<T>(headers, key, id, 0);
+}
+
+template <typename... Args>
+inline ssize_t Stream::write_format(const char *fmt, const Args &... args) {
+  const auto bufsiz = 2048;
+  std::array<char, bufsiz> buf;
+
+#if defined(_MSC_VER) && _MSC_VER < 1900
+  auto sn = _snprintf_s(buf.data(), bufsiz - 1, buf.size() - 1, fmt, args...);
+#else
+  auto sn = snprintf(buf.data(), buf.size() - 1, fmt, args...);
+#endif
+  if (sn <= 0) { return sn; }
+
+  auto n = static_cast<size_t>(sn);
+
+  if (n >= buf.size() - 1) {
+    std::vector<char> glowable_buf(buf.size());
+
+    while (n >= glowable_buf.size() - 1) {
+      glowable_buf.resize(glowable_buf.size() * 2);
+#if defined(_MSC_VER) && _MSC_VER < 1900
+      n = static_cast<size_t>(_snprintf_s(&glowable_buf[0], glowable_buf.size(),
+                                          glowable_buf.size() - 1, fmt,
+                                          args...));
+#else
+      n = static_cast<size_t>(
+          snprintf(&glowable_buf[0], glowable_buf.size() - 1, fmt, args...));
+#endif
+    }
+    return write(&glowable_buf[0], n);
+  } else {
+    return write(buf.data(), n);
+  }
+}
+
+template <class Rep, class Period>
+inline Server &
+Server::set_read_timeout(const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(
+      duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); });
+  return *this;
+}
+
+template <class Rep, class Period>
+inline Server &
+Server::set_write_timeout(const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(
+      duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); });
+  return *this;
+}
+
+template <class Rep, class Period>
+inline Server &
+Server::set_idle_interval(const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(
+      duration, [&](time_t sec, time_t usec) { set_idle_interval(sec, usec); });
+  return *this;
+}
+
+template <typename T>
+inline T Result::get_request_header_value(const char *key, size_t id) const {
+  return detail::get_header_value<T>(request_headers_, key, id, 0);
+}
+
+template <class Rep, class Period>
+inline void ClientImpl::set_connection_timeout(
+    const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t usec) {
+    set_connection_timeout(sec, usec);
+  });
+}
+
+template <class Rep, class Period>
+inline void ClientImpl::set_read_timeout(
+    const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(
+      duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); });
+}
+
+template <class Rep, class Period>
+inline void ClientImpl::set_write_timeout(
+    const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(
+      duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); });
+}
+
+template <class Rep, class Period>
+inline void Client::set_connection_timeout(
+    const std::chrono::duration<Rep, Period> &duration) {
+  cli_->set_connection_timeout(duration);
+}
+
+template <class Rep, class Period>
+inline void
+Client::set_read_timeout(const std::chrono::duration<Rep, Period> &duration) {
+  cli_->set_read_timeout(duration);
+}
+
+template <class Rep, class Period>
+inline void
+Client::set_write_timeout(const std::chrono::duration<Rep, Period> &duration) {
+  cli_->set_write_timeout(duration);
+}
+
+/*
+ * Forward declarations and types that will be part of the .h file if split into
+ * .h + .cc.
+ */
+
+std::pair<std::string, std::string> make_range_header(Ranges ranges);
+
+std::pair<std::string, std::string>
+make_basic_authentication_header(const std::string &username,
+                                 const std::string &password,
+                                 bool is_proxy = false);
+
+namespace detail {
+
+std::string encode_query_param(const std::string &value);
+
+void read_file(const std::string &path, std::string &out);
+
+std::string trim_copy(const std::string &s);
+
+void split(const char *b, const char *e, char d,
+           std::function<void(const char *, const char *)> fn);
+
+bool process_client_socket(socket_t sock, time_t read_timeout_sec,
+                           time_t read_timeout_usec, time_t write_timeout_sec,
+                           time_t write_timeout_usec,
+                           std::function<bool(Stream &)> callback);
+
+socket_t create_client_socket(const char *host, int port, int address_family,
+                              bool tcp_nodelay, SocketOptions socket_options,
+                              time_t connection_timeout_sec,
+                              time_t connection_timeout_usec,
+                              time_t read_timeout_sec, time_t read_timeout_usec,
+                              time_t write_timeout_sec,
+                              time_t write_timeout_usec,
+                              const std::string &intf, Error &error);
+
+const char *get_header_value(const Headers &headers, const char *key,
+                             size_t id = 0, const char *def = nullptr);
+
+std::string params_to_query_str(const Params &params);
+
+void parse_query_text(const std::string &s, Params &params);
+
+bool parse_range_header(const std::string &s, Ranges &ranges);
+
+int close_socket(socket_t sock);
+
+enum class EncodingType { None = 0, Gzip, Brotli };
+
+EncodingType encoding_type(const Request &req, const Response &res);
+
+class BufferStream : public Stream {
+public:
+  BufferStream() = default;
+  ~BufferStream() override = default;
+
+  bool is_readable() const override;
+  bool is_writable() const override;
+  ssize_t read(char *ptr, size_t size) override;
+  ssize_t write(const char *ptr, size_t size) override;
+  void get_remote_ip_and_port(std::string &ip, int &port) const override;
+  socket_t socket() const override;
+
+  const std::string &get_buffer() const;
+
+private:
+  std::string buffer;
+  size_t position = 0;
+};
+
+class compressor {
+public:
+  virtual ~compressor() = default;
+
+  typedef std::function<bool(const char *data, size_t data_len)> Callback;
+  virtual bool compress(const char *data, size_t data_length, bool last,
+                        Callback callback) = 0;
+};
+
+class decompressor {
+public:
+  virtual ~decompressor() = default;
+
+  virtual bool is_valid() const = 0;
+
+  typedef std::function<bool(const char *data, size_t data_len)> Callback;
+  virtual bool decompress(const char *data, size_t data_length,
+                          Callback callback) = 0;
+};
+
+class nocompressor : public compressor {
+public:
+  virtual ~nocompressor() = default;
+
+  bool compress(const char *data, size_t data_length, bool /*last*/,
+                Callback callback) override;
+};
+
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+class gzip_compressor : public compressor {
+public:
+  gzip_compressor();
+  ~gzip_compressor();
+
+  bool compress(const char *data, size_t data_length, bool last,
+                Callback callback) override;
+
+private:
+  bool is_valid_ = false;
+  z_stream strm_;
+};
+
+class gzip_decompressor : public decompressor {
+public:
+  gzip_decompressor();
+  ~gzip_decompressor();
+
+  bool is_valid() const override;
+
+  bool decompress(const char *data, size_t data_length,
+                  Callback callback) override;
+
+private:
+  bool is_valid_ = false;
+  z_stream strm_;
+};
+#endif
+
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+class brotli_compressor : public compressor {
+public:
+  brotli_compressor();
+  ~brotli_compressor();
+
+  bool compress(const char *data, size_t data_length, bool last,
+                Callback callback) override;
+
+private:
+  BrotliEncoderState *state_ = nullptr;
+};
+
+class brotli_decompressor : public decompressor {
+public:
+  brotli_decompressor();
+  ~brotli_decompressor();
+
+  bool is_valid() const override;
+
+  bool decompress(const char *data, size_t data_length,
+                  Callback callback) override;
+
+private:
+  BrotliDecoderResult decoder_r;
+  BrotliDecoderState *decoder_s = nullptr;
+};
+#endif
+
+// NOTE: until the read size reaches `fixed_buffer_size`, use `fixed_buffer`
+// to store data. The call can set memory on stack for performance.
+class stream_line_reader {
+public:
+  stream_line_reader(Stream &strm, char *fixed_buffer,
+                     size_t fixed_buffer_size);
+  const char *ptr() const;
+  size_t size() const;
+  bool end_with_crlf() const;
+  bool getline();
+
+private:
+  void append(char c);
+
+  Stream &strm_;
+  char *fixed_buffer_;
+  const size_t fixed_buffer_size_;
+  size_t fixed_buffer_used_size_ = 0;
+  std::string glowable_buffer_;
+};
+
+} // namespace detail
+
+
+} // namespace httplib
+
+#endif // CPPHTTPLIB_HTTPLIB_H
index df1a3ac3cdce9ace964454a5403b564b422c538f..2877f048e8648f0147826bcfe83f6356dcd18350 100644 (file)
@@ -35,6 +35,8 @@
 #endif
 
 #define ARRAY_SIZE(x) sizeof(x)/sizeof(x[0])
+#define TRUE 1
+#define FALSE 0
 
 // Ugh, this struct is already pretty heavy.
 // Will probably need to move arguments to a second buffer to support more than one.
@@ -55,19 +57,26 @@ typedef struct raw_event {
        };
 } raw_event_t;
 
-static raw_event_t *buffer;
-static volatile int count;
-static int is_tracing = 0;
+static raw_event_t *event_buffer;
+static raw_event_t *flush_buffer;
+static volatile int event_count;
+static int is_tracing = FALSE;
+static int is_flushing = FALSE;
+static int events_in_progress = 0;
 static int64_t time_offset;
 static int first_line = 1;
 static FILE *f;
 static __thread int cur_thread_id;     // Thread local storage
 static int cur_process_id;
 static pthread_mutex_t mutex;
+static pthread_mutex_t event_mutex;
 
 #define STRING_POOL_SIZE 100
 static char *str_pool[100];
 
+// forward declaration
+void mtr_flush_with_state(int);
+
 // Tiny portability layer.
 // Exposes:
 //      get_cur_thread_id()
@@ -164,15 +173,17 @@ void mtr_init_from_stream(void *stream) {
 #ifndef MTR_ENABLED
        return;
 #endif
-       buffer = (raw_event_t *)malloc(INTERNAL_MINITRACE_BUFFER_SIZE * sizeof(raw_event_t));
+       event_buffer = (raw_event_t *)malloc(INTERNAL_MINITRACE_BUFFER_SIZE * sizeof(raw_event_t));
+       flush_buffer = (raw_event_t *)malloc(INTERNAL_MINITRACE_BUFFER_SIZE * sizeof(raw_event_t));
        is_tracing = 1;
-       count = 0;
+       event_count = 0;
        f = (FILE *)stream;
        const char *header = "{\"traceEvents\":[\n";
        fwrite(header, 1, strlen(header), f);
        time_offset = (uint64_t)(mtr_time_s() * 1000000);
        first_line = 1;
        pthread_mutex_init(&mutex, 0);
+       pthread_mutex_init(&event_mutex, 0);
 }
 
 void mtr_init(const char *json_file) {
@@ -187,14 +198,18 @@ void mtr_shutdown() {
 #ifndef MTR_ENABLED
        return;
 #endif
-       is_tracing = 0;
-       mtr_flush();
+       pthread_mutex_lock(&mutex);
+       is_tracing = FALSE;
+       pthread_mutex_unlock(&mutex);
+       mtr_flush_with_state(TRUE);
+
        fwrite("\n]}\n", 1, 4, f);
        fclose(f);
        pthread_mutex_destroy(&mutex);
+       pthread_mutex_destroy(&event_mutex);
        f = 0;
-       free(buffer);
-       buffer = 0;
+       free(event_buffer);
+       event_buffer = 0;
        for (i = 0; i < STRING_POOL_SIZE; i++) {
                if (str_pool[i]) {
                        free(str_pool[i]);
@@ -222,18 +237,26 @@ void mtr_start() {
 #ifndef MTR_ENABLED
        return;
 #endif
-       is_tracing = 1;
+       pthread_mutex_lock(&mutex);
+       is_tracing = TRUE;
+       pthread_mutex_unlock(&mutex);
 }
 
 void mtr_stop() {
 #ifndef MTR_ENABLED
        return;
 #endif
-       is_tracing = 0;
+       pthread_mutex_lock(&mutex);
+       is_tracing = FALSE;
+       pthread_mutex_unlock(&mutex);
 }
 
 // TODO: fwrite more than one line at a time.
-void mtr_flush() {
+// Flushing is thread safe and process async
+// using double-buffering mechanism.
+// Aware: only one flushing process may be 
+// running at any point of time
+void mtr_flush_with_state(int is_last) {
 #ifndef MTR_ENABLED
        return;
 #endif
@@ -241,15 +264,36 @@ void mtr_flush() {
        char linebuf[1024];
        char arg_buf[1024];
        char id_buf[256];
-       // We have to lock while flushing. So we really should avoid flushing as much as possible.
-
-
+       int event_count_copy = 0;
+       int events_in_progress_copy = 1;
+       raw_event_t *event_buffer_tmp = NULL;
+
+       // small critical section to swap buffers
+       // - no any new events can be spawn while
+       //   swapping since they tied to the same mutex
+       // - checks for any flushing in process
        pthread_mutex_lock(&mutex);
-       int old_tracing = is_tracing;
-       is_tracing = 0; // Stop logging even if using interlocked increments instead of the mutex. Can cause data loss.
+       // if not flushing already
+       if (is_flushing) {
+               pthread_mutex_unlock(&mutex);
+               return;
+       }
+       is_flushing = TRUE;
+       event_count_copy = event_count;
+       event_buffer_tmp = flush_buffer;
+       flush_buffer = event_buffer;
+       event_buffer = event_buffer_tmp;
+       event_count = 0;
+       // waiting for any unfinished events before swap
+       while (events_in_progress_copy != 0) {
+               pthread_mutex_lock(&event_mutex);
+               events_in_progress_copy = events_in_progress;
+               pthread_mutex_unlock(&event_mutex);
+       }
+       pthread_mutex_unlock(&mutex);
 
-       for (i = 0; i < count; i++) {
-               raw_event_t *raw = &buffer[i];
+       for (i = 0; i < event_count_copy; i++) {
+               raw_event_t *raw = &flush_buffer[i];
                int len;
                switch (raw->arg_type) {
                case MTR_ARG_TYPE_INT:
@@ -305,18 +349,41 @@ void mtr_flush() {
                                cat, raw->pid, raw->tid, raw->ts - time_offset, raw->ph, raw->name, arg_buf, id_buf);
                fwrite(linebuf, 1, len, f);
                first_line = 0;
+
+               if (raw->arg_type == MTR_ARG_TYPE_STRING_COPY) {
+                       free((void*)raw->a_str);
+               }
+               #ifdef MTR_COPY_EVENT_CATEGORY_AND_NAME
+               free(raw->name);
+               free(raw->cat);
+               #endif
        }
-       count = 0;
-       is_tracing = old_tracing;
+
+       pthread_mutex_lock(&mutex);
+       is_flushing = is_last;
        pthread_mutex_unlock(&mutex);
 }
 
+void mtr_flush() {
+       mtr_flush_with_state(FALSE);
+}
+
 void internal_mtr_raw_event(const char *category, const char *name, char ph, void *id) {
 #ifndef MTR_ENABLED
        return;
 #endif
-       if (!is_tracing || count >= INTERNAL_MINITRACE_BUFFER_SIZE)
+       pthread_mutex_lock(&mutex);
+       if (!is_tracing || event_count >= INTERNAL_MINITRACE_BUFFER_SIZE) {
+               pthread_mutex_unlock(&mutex);
                return;
+       }
+       raw_event_t *ev = &event_buffer[event_count];
+       ++event_count;
+       pthread_mutex_lock(&event_mutex);
+       ++events_in_progress;
+       pthread_mutex_unlock(&event_mutex);
+       pthread_mutex_unlock(&mutex);
+
        double ts = mtr_time_s();
        if (!cur_thread_id) {
                cur_thread_id = get_cur_thread_id();
@@ -325,18 +392,20 @@ void internal_mtr_raw_event(const char *category, const char *name, char ph, voi
                cur_process_id = get_cur_process_id();
        }
 
-#if 0 && _WIN32        // This should work, feel free to enable if you're adventurous and need performance.
-       int bufPos = InterlockedExchangeAdd((LONG volatile *)&count, 1);
-       raw_event_t *ev = &buffer[bufPos];
-#else
-       pthread_mutex_lock(&mutex);
-       raw_event_t *ev = &buffer[count];
-       count++;
-       pthread_mutex_unlock(&mutex);
-#endif
+#ifdef MTR_COPY_EVENT_CATEGORY_AND_NAME
+       const size_t category_len = strlen(category);
+       ev->cat = malloc(category_len + 1);
+       strcpy(ev->cat, category);
 
+       const size_t name_len = strlen(name);
+       ev->name = malloc(name_len + 1);
+       strcpy(ev->name, name);
+
+#else
        ev->cat = category;
        ev->name = name;
+#endif
+
        ev->id = id;
        ev->ph = ph;
        if (ev->ph == 'X') {
@@ -350,14 +419,28 @@ void internal_mtr_raw_event(const char *category, const char *name, char ph, voi
        ev->tid = cur_thread_id;
        ev->pid = cur_process_id;
        ev->arg_type = MTR_ARG_TYPE_NONE;
+
+       pthread_mutex_lock(&event_mutex);
+       --events_in_progress;
+       pthread_mutex_unlock(&event_mutex);
 }
 
 void internal_mtr_raw_event_arg(const char *category, const char *name, char ph, void *id, mtr_arg_type arg_type, const char *arg_name, void *arg_value) {
 #ifndef MTR_ENABLED
        return;
 #endif
-       if (!is_tracing || count >= INTERNAL_MINITRACE_BUFFER_SIZE)
+       pthread_mutex_lock(&mutex);
+       if (!is_tracing || event_count >= INTERNAL_MINITRACE_BUFFER_SIZE) {
+               pthread_mutex_unlock(&mutex);
                return;
+       }
+       raw_event_t *ev = &event_buffer[event_count];
+       ++event_count;
+       pthread_mutex_lock(&event_mutex);
+       ++events_in_progress;
+       pthread_mutex_unlock(&event_mutex);
+       pthread_mutex_unlock(&mutex);
+
        if (!cur_thread_id) {
                cur_thread_id = get_cur_thread_id();
        }
@@ -366,18 +449,20 @@ void internal_mtr_raw_event_arg(const char *category, const char *name, char ph,
        }
        double ts = mtr_time_s();
 
-#if 0 && _WIN32        // This should work, feel free to enable if you're adventurous and need performance.
-       int bufPos = InterlockedExchangeAdd((LONG volatile *)&count, 1);
-       raw_event_t *ev = &buffer[bufPos];
-#else
-       pthread_mutex_lock(&mutex);
-       raw_event_t *ev = &buffer[count];
-       count++;
-       pthread_mutex_unlock(&mutex);
-#endif
+#ifdef MTR_COPY_EVENT_CATEGORY_AND_NAME
+       const size_t category_len = strlen(category);
+       ev->cat = malloc(category_len + 1);
+       strcpy(ev->cat, category);
+
+       const size_t name_len = strlen(name);
+       ev->name = malloc(name_len + 1);
+       strcpy(ev->name, name);
 
+#else
        ev->cat = category;
        ev->name = name;
+#endif
+
        ev->id = id;
        ev->ts = (int64_t)(ts * 1000000);
        ev->ph = ph;
@@ -391,5 +476,9 @@ void internal_mtr_raw_event_arg(const char *category, const char *name, char ph,
        case MTR_ARG_TYPE_STRING_COPY: ev->a_str = strdup((const char*)arg_value); break;
        case MTR_ARG_TYPE_NONE: break;
        }
+
+       pthread_mutex_lock(&event_mutex);
+       --events_in_progress;
+       pthread_mutex_unlock(&event_mutex);
 }
 
diff --git a/src/third_party/nonstd/expected.hpp b/src/third_party/nonstd/expected.hpp
new file mode 100644 (file)
index 0000000..69bee75
--- /dev/null
@@ -0,0 +1,2492 @@
+// This version targets C++11 and later.
+//
+// Copyright (C) 2016-2018 Martin Moene.
+//
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+// expected lite is based on:
+//   A proposal to add a utility class to represent expected monad
+//   by Vicente J. Botet Escriba and Pierre Talbot. http:://wg21.link/p0323
+
+#ifndef NONSTD_EXPECTED_LITE_HPP
+#define NONSTD_EXPECTED_LITE_HPP
+
+#define expected_lite_MAJOR  0
+#define expected_lite_MINOR  5
+#define expected_lite_PATCH  0
+
+#define expected_lite_VERSION  expected_STRINGIFY(expected_lite_MAJOR) "." expected_STRINGIFY(expected_lite_MINOR) "." expected_STRINGIFY(expected_lite_PATCH)
+
+#define expected_STRINGIFY(  x )  expected_STRINGIFY_( x )
+#define expected_STRINGIFY_( x )  #x
+
+// expected-lite configuration:
+
+#define nsel_EXPECTED_DEFAULT  0
+#define nsel_EXPECTED_NONSTD   1
+#define nsel_EXPECTED_STD      2
+
+// tweak header support:
+
+#ifdef __has_include
+# if __has_include(<nonstd/expected.tweak.hpp>)
+#  include <nonstd/expected.tweak.hpp>
+# endif
+#define expected_HAVE_TWEAK_HEADER  1
+#else
+#define expected_HAVE_TWEAK_HEADER  0
+//# pragma message("expected.hpp: Note: Tweak header not supported.")
+#endif
+
+// expected selection and configuration:
+
+#if !defined( nsel_CONFIG_SELECT_EXPECTED )
+# define nsel_CONFIG_SELECT_EXPECTED  ( nsel_HAVE_STD_EXPECTED ? nsel_EXPECTED_STD : nsel_EXPECTED_NONSTD )
+#endif
+
+// Proposal revisions:
+//
+// DXXXXR0: --
+// N4015  : -2 (2014-05-26)
+// N4109  : -1 (2014-06-29)
+// P0323R0:  0 (2016-05-28)
+// P0323R1:  1 (2016-10-12)
+// -------:
+// P0323R2:  2 (2017-06-15)
+// P0323R3:  3 (2017-10-15)
+// P0323R4:  4 (2017-11-26)
+// P0323R5:  5 (2018-02-08)
+// P0323R6:  6 (2018-04-02)
+// P0323R7:  7 (2018-06-22) *
+//
+// expected-lite uses 2 and higher
+
+#ifndef  nsel_P0323R
+# define nsel_P0323R  7
+#endif
+
+// Control presence of exception handling (try and auto discover):
+
+#ifndef nsel_CONFIG_NO_EXCEPTIONS
+# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
+#  define nsel_CONFIG_NO_EXCEPTIONS  0
+# else
+#  define nsel_CONFIG_NO_EXCEPTIONS  1
+# endif
+#endif
+
+// C++ language version detection (C++20 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+#ifndef   nsel_CPLUSPLUS
+# if defined(_MSVC_LANG ) && !defined(__clang__)
+#  define nsel_CPLUSPLUS  (_MSC_VER == 1900 ? 201103L : _MSVC_LANG )
+# else
+#  define nsel_CPLUSPLUS  __cplusplus
+# endif
+#endif
+
+#define nsel_CPP98_OR_GREATER  ( nsel_CPLUSPLUS >= 199711L )
+#define nsel_CPP11_OR_GREATER  ( nsel_CPLUSPLUS >= 201103L )
+#define nsel_CPP14_OR_GREATER  ( nsel_CPLUSPLUS >= 201402L )
+#define nsel_CPP17_OR_GREATER  ( nsel_CPLUSPLUS >= 201703L )
+#define nsel_CPP20_OR_GREATER  ( nsel_CPLUSPLUS >= 202000L )
+
+// Use C++20 std::expected if available and requested:
+
+#if nsel_CPP20_OR_GREATER && defined(__has_include )
+# if __has_include( <expected> )
+#  define nsel_HAVE_STD_EXPECTED  1
+# else
+#  define nsel_HAVE_STD_EXPECTED  0
+# endif
+#else
+# define  nsel_HAVE_STD_EXPECTED  0
+#endif
+
+#define  nsel_USES_STD_EXPECTED  ( (nsel_CONFIG_SELECT_EXPECTED == nsel_EXPECTED_STD) || ((nsel_CONFIG_SELECT_EXPECTED == nsel_EXPECTED_DEFAULT) && nsel_HAVE_STD_EXPECTED) )
+
+//
+// in_place: code duplicated in any-lite, expected-lite, expected-lite, value-ptr-lite, variant-lite:
+//
+
+#ifndef nonstd_lite_HAVE_IN_PLACE_TYPES
+#define nonstd_lite_HAVE_IN_PLACE_TYPES  1
+
+// C++17 std::in_place in <utility>:
+
+#if nsel_CPP17_OR_GREATER
+
+#include <utility>
+
+namespace nonstd {
+
+using std::in_place;
+using std::in_place_type;
+using std::in_place_index;
+using std::in_place_t;
+using std::in_place_type_t;
+using std::in_place_index_t;
+
+#define nonstd_lite_in_place_t(      T)  std::in_place_t
+#define nonstd_lite_in_place_type_t( T)  std::in_place_type_t<T>
+#define nonstd_lite_in_place_index_t(K)  std::in_place_index_t<K>
+
+#define nonstd_lite_in_place(      T)    std::in_place_t{}
+#define nonstd_lite_in_place_type( T)    std::in_place_type_t<T>{}
+#define nonstd_lite_in_place_index(K)    std::in_place_index_t<K>{}
+
+} // namespace nonstd
+
+#else // nsel_CPP17_OR_GREATER
+
+#include <cstddef>
+
+namespace nonstd {
+namespace detail {
+
+template< class T >
+struct in_place_type_tag {};
+
+template< std::size_t K >
+struct in_place_index_tag {};
+
+} // namespace detail
+
+struct in_place_t {};
+
+template< class T >
+inline in_place_t in_place( detail::in_place_type_tag<T> = detail::in_place_type_tag<T>() )
+{
+    return in_place_t();
+}
+
+template< std::size_t K >
+inline in_place_t in_place( detail::in_place_index_tag<K> = detail::in_place_index_tag<K>() )
+{
+    return in_place_t();
+}
+
+template< class T >
+inline in_place_t in_place_type( detail::in_place_type_tag<T> = detail::in_place_type_tag<T>() )
+{
+    return in_place_t();
+}
+
+template< std::size_t K >
+inline in_place_t in_place_index( detail::in_place_index_tag<K> = detail::in_place_index_tag<K>() )
+{
+    return in_place_t();
+}
+
+// mimic templated typedef:
+
+#define nonstd_lite_in_place_t(      T)  nonstd::in_place_t(&)( nonstd::detail::in_place_type_tag<T>  )
+#define nonstd_lite_in_place_type_t( T)  nonstd::in_place_t(&)( nonstd::detail::in_place_type_tag<T>  )
+#define nonstd_lite_in_place_index_t(K)  nonstd::in_place_t(&)( nonstd::detail::in_place_index_tag<K> )
+
+#define nonstd_lite_in_place(      T)    nonstd::in_place_type<T>
+#define nonstd_lite_in_place_type( T)    nonstd::in_place_type<T>
+#define nonstd_lite_in_place_index(K)    nonstd::in_place_index<K>
+
+} // namespace nonstd
+
+#endif // nsel_CPP17_OR_GREATER
+#endif // nonstd_lite_HAVE_IN_PLACE_TYPES
+
+//
+// Using std::expected:
+//
+
+#if nsel_USES_STD_EXPECTED
+
+#include <expected>
+
+namespace nonstd {
+
+    using std::expected;
+//  ...
+}
+
+#else // nsel_USES_STD_EXPECTED
+
+#include <cassert>
+#include <exception>
+#include <functional>
+#include <initializer_list>
+#include <memory>
+#include <new>
+#include <system_error>
+#include <type_traits>
+#include <utility>
+
+// additional includes:
+
+#if nsel_CONFIG_NO_EXCEPTIONS
+// already included: <cassert>
+#else
+# include <stdexcept>
+#endif
+
+// C++ feature usage:
+
+#if nsel_CPP11_OR_GREATER
+# define nsel_constexpr  constexpr
+#else
+# define nsel_constexpr  /*constexpr*/
+#endif
+
+#if nsel_CPP14_OR_GREATER
+# define nsel_constexpr14 constexpr
+#else
+# define nsel_constexpr14 /*constexpr*/
+#endif
+
+#if nsel_CPP17_OR_GREATER
+# define nsel_inline17 inline
+#else
+# define nsel_inline17 /*inline*/
+#endif
+
+// Compiler versions:
+//
+// MSVC++  6.0  _MSC_VER == 1200  nsel_COMPILER_MSVC_VERSION ==  60  (Visual Studio 6.0)
+// MSVC++  7.0  _MSC_VER == 1300  nsel_COMPILER_MSVC_VERSION ==  70  (Visual Studio .NET 2002)
+// MSVC++  7.1  _MSC_VER == 1310  nsel_COMPILER_MSVC_VERSION ==  71  (Visual Studio .NET 2003)
+// MSVC++  8.0  _MSC_VER == 1400  nsel_COMPILER_MSVC_VERSION ==  80  (Visual Studio 2005)
+// MSVC++  9.0  _MSC_VER == 1500  nsel_COMPILER_MSVC_VERSION ==  90  (Visual Studio 2008)
+// MSVC++ 10.0  _MSC_VER == 1600  nsel_COMPILER_MSVC_VERSION == 100  (Visual Studio 2010)
+// MSVC++ 11.0  _MSC_VER == 1700  nsel_COMPILER_MSVC_VERSION == 110  (Visual Studio 2012)
+// MSVC++ 12.0  _MSC_VER == 1800  nsel_COMPILER_MSVC_VERSION == 120  (Visual Studio 2013)
+// MSVC++ 14.0  _MSC_VER == 1900  nsel_COMPILER_MSVC_VERSION == 140  (Visual Studio 2015)
+// MSVC++ 14.1  _MSC_VER >= 1910  nsel_COMPILER_MSVC_VERSION == 141  (Visual Studio 2017)
+// MSVC++ 14.2  _MSC_VER >= 1920  nsel_COMPILER_MSVC_VERSION == 142  (Visual Studio 2019)
+
+#if defined(_MSC_VER) && !defined(__clang__)
+# define nsel_COMPILER_MSVC_VER      (_MSC_VER )
+# define nsel_COMPILER_MSVC_VERSION  (_MSC_VER / 10 - 10 * ( 5 + (_MSC_VER < 1900)) )
+#else
+# define nsel_COMPILER_MSVC_VER      0
+# define nsel_COMPILER_MSVC_VERSION  0
+#endif
+
+#define nsel_COMPILER_VERSION( major, minor, patch )  ( 10 * ( 10 * (major) + (minor) ) + (patch) )
+
+#if defined(__clang__)
+# define nsel_COMPILER_CLANG_VERSION  nsel_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+#else
+# define nsel_COMPILER_CLANG_VERSION  0
+#endif
+
+#if defined(__GNUC__) && !defined(__clang__)
+# define nsel_COMPILER_GNUC_VERSION  nsel_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#else
+# define nsel_COMPILER_GNUC_VERSION  0
+#endif
+
+// half-open range [lo..hi):
+//#define nsel_BETWEEN( v, lo, hi ) ( (lo) <= (v) && (v) < (hi) )
+
+// Method enabling
+
+#define nsel_REQUIRES_0(...) \
+    template< bool B = (__VA_ARGS__), typename std::enable_if<B, int>::type = 0 >
+
+#define nsel_REQUIRES_T(...) \
+    , typename std::enable_if< (__VA_ARGS__), int >::type = 0
+
+#define nsel_REQUIRES_R(R, ...) \
+    typename std::enable_if< (__VA_ARGS__), R>::type
+
+#define nsel_REQUIRES_A(...) \
+    , typename std::enable_if< (__VA_ARGS__), void*>::type = nullptr
+
+// Presence of language and library features:
+
+#ifdef _HAS_CPP0X
+# define nsel_HAS_CPP0X  _HAS_CPP0X
+#else
+# define nsel_HAS_CPP0X  0
+#endif
+
+//#define nsel_CPP11_140  (nsel_CPP11_OR_GREATER || nsel_COMPILER_MSVC_VER >= 1900)
+
+// Clang, GNUC, MSVC warning suppression macros:
+
+#ifdef __clang__
+# pragma clang diagnostic push
+#elif defined  __GNUC__
+# pragma  GCC  diagnostic push
+#endif // __clang__
+
+#if nsel_COMPILER_MSVC_VERSION >= 140
+# pragma warning( push )
+# define nsel_DISABLE_MSVC_WARNINGS(codes)  __pragma( warning(disable: codes) )
+#else
+# define nsel_DISABLE_MSVC_WARNINGS(codes)
+#endif
+
+#ifdef __clang__
+# define nsel_RESTORE_WARNINGS()  _Pragma("clang diagnostic pop")
+#elif defined __GNUC__
+# define nsel_RESTORE_WARNINGS()  _Pragma("GCC diagnostic pop")
+#elif nsel_COMPILER_MSVC_VERSION >= 140
+# define nsel_RESTORE_WARNINGS()  __pragma( warning( pop ) )
+#else
+# define nsel_RESTORE_WARNINGS()
+#endif
+
+// Suppress the following MSVC (GSL) warnings:
+// - C26409: Avoid calling new and delete explicitly, use std::make_unique<T> instead (r.11)
+
+nsel_DISABLE_MSVC_WARNINGS( 26409 )
+
+//
+// expected:
+//
+
+namespace nonstd { namespace expected_lite {
+
+// type traits C++17:
+
+namespace std17 {
+
+#if nsel_CPP17_OR_GREATER
+
+using std::conjunction;
+using std::is_swappable;
+using std::is_nothrow_swappable;
+
+#else // nsel_CPP17_OR_GREATER
+
+namespace detail {
+
+using std::swap;
+
+struct is_swappable
+{
+    template< typename T, typename = decltype( swap( std::declval<T&>(), std::declval<T&>() ) ) >
+    static std::true_type test( int /* unused */);
+
+    template< typename >
+    static std::false_type test(...);
+};
+
+struct is_nothrow_swappable
+{
+    // wrap noexcept(expr) in separate function as work-around for VC140 (VS2015):
+
+    template< typename T >
+    static constexpr bool satisfies()
+    {
+        return noexcept( swap( std::declval<T&>(), std::declval<T&>() ) );
+    }
+
+    template< typename T >
+    static auto test( int ) -> std::integral_constant<bool, satisfies<T>()>{}
+
+    template< typename >
+    static auto test(...) -> std::false_type;
+};
+} // namespace detail
+
+// is [nothow] swappable:
+
+template< typename T >
+struct is_swappable : decltype( detail::is_swappable::test<T>(0) ){};
+
+template< typename T >
+struct is_nothrow_swappable : decltype( detail::is_nothrow_swappable::test<T>(0) ){};
+
+// conjunction:
+
+template< typename... > struct conjunction : std::true_type{};
+template< typename B1 > struct conjunction<B1> : B1{};
+
+template< typename B1, typename... Bn >
+struct conjunction<B1, Bn...> : std::conditional<bool(B1::value), conjunction<Bn...>, B1>::type{};
+
+#endif // nsel_CPP17_OR_GREATER
+
+} // namespace std17
+
+// type traits C++20:
+
+namespace std20 {
+
+#if nsel_CPP20_OR_GREATER
+
+using std::remove_cvref;
+
+#else
+
+template< typename T >
+struct remove_cvref
+{
+    typedef typename std::remove_cv< typename std::remove_reference<T>::type >::type type;
+};
+
+#endif
+
+} // namespace std20
+
+// forward declaration:
+
+template< typename T, typename E >
+class expected;
+
+namespace detail {
+
+/// discriminated union to hold value or 'error'.
+
+template< typename T, typename E >
+class storage_t_impl
+{
+    template< typename, typename > friend class nonstd::expected_lite::expected;
+
+public:
+    using value_type = T;
+    using error_type = E;
+
+    // no-op construction
+    storage_t_impl() {}
+    ~storage_t_impl() {}
+
+    explicit storage_t_impl( bool has_value )
+        : m_has_value( has_value )
+    {}
+
+    void construct_value( value_type const & e )
+    {
+        new( &m_value ) value_type( e );
+    }
+
+    void construct_value( value_type && e )
+    {
+        new( &m_value ) value_type( std::move( e ) );
+    }
+
+    template< class... Args >
+    void emplace_value( Args&&... args )
+    {
+        new( &m_value ) value_type( std::forward<Args>(args)...);
+    }
+
+    template< class U, class... Args >
+    void emplace_value( std::initializer_list<U> il, Args&&... args )
+    {
+        new( &m_value ) value_type( il, std::forward<Args>(args)... );
+    }
+
+    void destruct_value()
+    {
+        m_value.~value_type();
+    }
+
+    void construct_error( error_type const & e )
+    {
+        new( &m_error ) error_type( e );
+    }
+
+    void construct_error( error_type && e )
+    {
+        new( &m_error ) error_type( std::move( e ) );
+    }
+
+    template< class... Args >
+    void emplace_error( Args&&... args )
+    {
+        new( &m_error ) error_type( std::forward<Args>(args)...);
+    }
+
+    template< class U, class... Args >
+    void emplace_error( std::initializer_list<U> il, Args&&... args )
+    {
+        new( &m_error ) error_type( il, std::forward<Args>(args)... );
+    }
+
+    void destruct_error()
+    {
+        m_error.~error_type();
+    }
+
+    constexpr value_type const & value() const &
+    {
+        return m_value;
+    }
+
+    value_type & value() &
+    {
+        return m_value;
+    }
+
+    constexpr value_type const && value() const &&
+    {
+        return std::move( m_value );
+    }
+
+    nsel_constexpr14 value_type && value() &&
+    {
+        return std::move( m_value );
+    }
+
+    value_type const * value_ptr() const
+    {
+        return &m_value;
+    }
+
+    value_type * value_ptr()
+    {
+        return &m_value;
+    }
+
+    error_type const & error() const &
+    {
+        return m_error;
+    }
+
+    error_type & error() &
+    {
+        return m_error;
+    }
+
+    constexpr error_type const && error() const &&
+    {
+        return std::move( m_error );
+    }
+
+    nsel_constexpr14 error_type && error() &&
+    {
+        return std::move( m_error );
+    }
+
+    bool has_value() const
+    {
+        return m_has_value;
+    }
+
+    void set_has_value( bool v )
+    {
+        m_has_value = v;
+    }
+
+private:
+    union
+    {
+        value_type m_value;
+        error_type m_error;
+    };
+
+    bool m_has_value = false;
+};
+
+/// discriminated union to hold only 'error'.
+
+template< typename E >
+struct storage_t_impl<void, E>
+{
+    template< typename, typename > friend class nonstd::expected_lite::expected;
+
+public:
+    using value_type = void;
+    using error_type = E;
+
+    // no-op construction
+    storage_t_impl() {}
+    ~storage_t_impl() {}
+
+    explicit storage_t_impl( bool has_value )
+        : m_has_value( has_value )
+    {}
+
+    void construct_error( error_type const & e )
+    {
+        new( &m_error ) error_type( e );
+    }
+
+    void construct_error( error_type && e )
+    {
+        new( &m_error ) error_type( std::move( e ) );
+    }
+
+    template< class... Args >
+    void emplace_error( Args&&... args )
+    {
+        new( &m_error ) error_type( std::forward<Args>(args)...);
+    }
+
+    template< class U, class... Args >
+    void emplace_error( std::initializer_list<U> il, Args&&... args )
+    {
+        new( &m_error ) error_type( il, std::forward<Args>(args)... );
+    }
+
+    void destruct_error()
+    {
+        m_error.~error_type();
+    }
+
+    error_type const & error() const &
+    {
+        return m_error;
+    }
+
+    error_type & error() &
+    {
+        return m_error;
+    }
+
+    constexpr error_type const && error() const &&
+    {
+        return std::move( m_error );
+    }
+
+    nsel_constexpr14 error_type && error() &&
+    {
+        return std::move( m_error );
+    }
+
+    bool has_value() const
+    {
+        return m_has_value;
+    }
+
+    void set_has_value( bool v )
+    {
+        m_has_value = v;
+    }
+
+private:
+    union
+    {
+        char m_dummy;
+        error_type m_error;
+    };
+
+    bool m_has_value = false;
+};
+
+template< typename T, typename E, bool isConstructable, bool isMoveable >
+class storage_t
+{
+public:
+    storage_t() = default;
+    ~storage_t() = default;
+
+    explicit storage_t( bool has_value )
+        : storage_t_impl<T, E>( has_value )
+    {}
+
+    storage_t( storage_t const & other ) = delete;
+    storage_t( storage_t &&      other ) = delete;
+};
+
+template< typename T, typename E >
+class storage_t<T, E, true, true> : public storage_t_impl<T, E>
+{
+public:
+    storage_t() = default;
+    ~storage_t() = default;
+
+    explicit storage_t( bool has_value )
+        : storage_t_impl<T, E>( has_value )
+    {}
+
+    storage_t( storage_t const & other )
+        : storage_t_impl<T, E>( other.has_value() )
+    {
+        if ( this->has_value() ) this->construct_value( other.value() );
+        else                     this->construct_error( other.error() );
+    }
+
+    storage_t(storage_t && other )
+        : storage_t_impl<T, E>( other.has_value() )
+    {
+        if ( this->has_value() ) this->construct_value( std::move( other.value() ) );
+        else                     this->construct_error( std::move( other.error() ) );
+    }
+};
+
+template< typename E >
+class storage_t<void, E, true, true> : public storage_t_impl<void, E>
+{
+public:
+    storage_t() = default;
+    ~storage_t() = default;
+
+    explicit storage_t( bool has_value )
+        : storage_t_impl<void, E>( has_value )
+    {}
+
+    storage_t( storage_t const & other )
+        : storage_t_impl<void, E>( other.has_value() )
+    {
+        if ( this->has_value() ) ;
+        else                     this->construct_error( other.error() );
+    }
+
+    storage_t(storage_t && other )
+        : storage_t_impl<void, E>( other.has_value() )
+    {
+        if ( this->has_value() ) ;
+        else                     this->construct_error( std::move( other.error() ) );
+    }
+};
+
+template< typename T, typename E >
+class storage_t<T, E, true, false> : public storage_t_impl<T, E>
+{
+public:
+    storage_t() = default;
+    ~storage_t() = default;
+
+    explicit storage_t( bool has_value )
+        : storage_t_impl<T, E>( has_value )
+    {}
+
+    storage_t( storage_t const & other )
+        : storage_t_impl<T, E>(other.has_value())
+    {
+        if ( this->has_value() ) this->construct_value( other.value() );
+        else                     this->construct_error( other.error() );
+    }
+
+    storage_t( storage_t && other ) = delete;
+};
+
+template< typename E >
+class storage_t<void, E, true, false> : public storage_t_impl<void, E>
+{
+public:
+    storage_t() = default;
+    ~storage_t() = default;
+
+    explicit storage_t( bool has_value )
+        : storage_t_impl<void, E>( has_value )
+    {}
+
+    storage_t( storage_t const & other )
+        : storage_t_impl<void, E>(other.has_value())
+    {
+        if ( this->has_value() ) ;
+        else                     this->construct_error( other.error() );
+    }
+
+    storage_t( storage_t && other ) = delete;
+};
+
+template< typename T, typename E >
+class storage_t<T, E, false, true> : public storage_t_impl<T, E>
+{
+public:
+    storage_t() = default;
+    ~storage_t() = default;
+
+    explicit storage_t( bool has_value )
+        : storage_t_impl<T, E>( has_value )
+    {}
+
+    storage_t( storage_t const & other ) = delete;
+
+    storage_t( storage_t && other )
+        : storage_t_impl<T, E>( other.has_value() )
+    {
+        if ( this->has_value() ) this->construct_value( std::move( other.value() ) );
+        else                     this->construct_error( std::move( other.error() ) );
+    }
+};
+
+template< typename E >
+class storage_t<void, E, false, true> : public storage_t_impl<void, E>
+{
+public:
+    storage_t() = default;
+    ~storage_t() = default;
+
+    explicit storage_t( bool has_value )
+        : storage_t_impl<void, E>( has_value )
+    {}
+
+    storage_t( storage_t const & other ) = delete;
+
+    storage_t( storage_t && other )
+        : storage_t_impl<void, E>( other.has_value() )
+    {
+        if ( this->has_value() ) ;
+        else                     this->construct_error( std::move( other.error() ) );
+    }
+};
+
+} // namespace detail
+
+/// x.x.5 Unexpected object type; unexpected_type; C++17 and later can also use aliased type unexpected.
+
+#if nsel_P0323R <= 2
+template< typename E = std::exception_ptr >
+class unexpected_type
+#else
+template< typename E >
+class unexpected_type
+#endif // nsel_P0323R
+{
+public:
+    using error_type = E;
+
+    // x.x.5.2.1 Constructors
+
+//  unexpected_type() = delete;
+
+    constexpr unexpected_type( unexpected_type const & ) = default;
+    constexpr unexpected_type( unexpected_type && ) = default;
+
+    template< typename... Args
+        nsel_REQUIRES_T(
+            std::is_constructible<E, Args&&...>::value
+        )
+    >
+    constexpr explicit unexpected_type( nonstd_lite_in_place_t(E), Args &&... args )
+    : m_error( std::forward<Args>( args )...)
+    {}
+
+    template< typename U, typename... Args
+        nsel_REQUIRES_T(
+            std::is_constructible<E, std::initializer_list<U>, Args&&...>::value
+        )
+    >
+    constexpr explicit unexpected_type( nonstd_lite_in_place_t(E), std::initializer_list<U> il, Args &&... args )
+    : m_error( il, std::forward<Args>( args )...)
+    {}
+
+    template< typename E2
+        nsel_REQUIRES_T(
+            std::is_constructible<E,E2>::value
+            && !std::is_same< typename std20::remove_cvref<E2>::type, nonstd_lite_in_place_t(E2) >::value
+            && !std::is_same< typename std20::remove_cvref<E2>::type, unexpected_type >::value
+        )
+    >
+    constexpr explicit unexpected_type( E2 && error )
+    : m_error( std::forward<E2>( error ) )
+    {}
+
+    template< typename E2
+        nsel_REQUIRES_T(
+            std::is_constructible<    E, E2>::value
+            && !std::is_constructible<E, unexpected_type<E2>       &   >::value
+            && !std::is_constructible<E, unexpected_type<E2>           >::value
+            && !std::is_constructible<E, unexpected_type<E2> const &   >::value
+            && !std::is_constructible<E, unexpected_type<E2> const     >::value
+            && !std::is_convertible<     unexpected_type<E2>       &, E>::value
+            && !std::is_convertible<     unexpected_type<E2>        , E>::value
+            && !std::is_convertible<     unexpected_type<E2> const &, E>::value
+            && !std::is_convertible<     unexpected_type<E2> const  , E>::value
+            && !std::is_convertible< E2 const &, E>::value /*=> explicit */
+        )
+    >
+    constexpr explicit unexpected_type( unexpected_type<E2> const & error )
+    : m_error( E{ error.value() } )
+    {}
+
+    template< typename E2
+        nsel_REQUIRES_T(
+            std::is_constructible<    E, E2>::value
+            && !std::is_constructible<E, unexpected_type<E2>       &   >::value
+            && !std::is_constructible<E, unexpected_type<E2>           >::value
+            && !std::is_constructible<E, unexpected_type<E2> const &   >::value
+            && !std::is_constructible<E, unexpected_type<E2> const     >::value
+            && !std::is_convertible<     unexpected_type<E2>       &, E>::value
+            && !std::is_convertible<     unexpected_type<E2>        , E>::value
+            && !std::is_convertible<     unexpected_type<E2> const &, E>::value
+            && !std::is_convertible<     unexpected_type<E2> const  , E>::value
+            &&  std::is_convertible< E2 const &, E>::value /*=> explicit */
+        )
+    >
+    constexpr /*non-explicit*/ unexpected_type( unexpected_type<E2> const & error )
+    : m_error( error.value() )
+    {}
+
+    template< typename E2
+        nsel_REQUIRES_T(
+            std::is_constructible<    E, E2>::value
+            && !std::is_constructible<E, unexpected_type<E2>       &   >::value
+            && !std::is_constructible<E, unexpected_type<E2>           >::value
+            && !std::is_constructible<E, unexpected_type<E2> const &   >::value
+            && !std::is_constructible<E, unexpected_type<E2> const     >::value
+            && !std::is_convertible<     unexpected_type<E2>       &, E>::value
+            && !std::is_convertible<     unexpected_type<E2>        , E>::value
+            && !std::is_convertible<     unexpected_type<E2> const &, E>::value
+            && !std::is_convertible<     unexpected_type<E2> const  , E>::value
+            && !std::is_convertible< E2 const &, E>::value /*=> explicit */
+        )
+    >
+    constexpr explicit unexpected_type( unexpected_type<E2> && error )
+    : m_error( E{ std::move( error.value() ) } )
+    {}
+
+    template< typename E2
+        nsel_REQUIRES_T(
+            std::is_constructible<    E, E2>::value
+            && !std::is_constructible<E, unexpected_type<E2>       &   >::value
+            && !std::is_constructible<E, unexpected_type<E2>           >::value
+            && !std::is_constructible<E, unexpected_type<E2> const &   >::value
+            && !std::is_constructible<E, unexpected_type<E2> const     >::value
+            && !std::is_convertible<     unexpected_type<E2>       &, E>::value
+            && !std::is_convertible<     unexpected_type<E2>        , E>::value
+            && !std::is_convertible<     unexpected_type<E2> const &, E>::value
+            && !std::is_convertible<     unexpected_type<E2> const  , E>::value
+            &&  std::is_convertible< E2 const &, E>::value /*=> non-explicit */
+        )
+    >
+    constexpr /*non-explicit*/ unexpected_type( unexpected_type<E2> && error )
+    : m_error( std::move( error.value() ) )
+    {}
+
+    // x.x.5.2.2 Assignment
+
+    nsel_constexpr14 unexpected_type& operator=( unexpected_type const & ) = default;
+    nsel_constexpr14 unexpected_type& operator=( unexpected_type && ) = default;
+
+    template< typename E2 = E >
+    nsel_constexpr14 unexpected_type & operator=( unexpected_type<E2> const & other )
+    {
+        unexpected_type{ other.value() }.swap( *this );
+        return *this;
+    }
+
+    template< typename E2 = E >
+    nsel_constexpr14 unexpected_type & operator=( unexpected_type<E2> && other )
+    {
+        unexpected_type{ std::move( other.value() ) }.swap( *this );
+        return *this;
+    }
+
+    // x.x.5.2.3 Observers
+
+    nsel_constexpr14 E & value() & noexcept
+    {
+        return m_error;
+    }
+
+    constexpr E const & value() const & noexcept
+    {
+        return m_error;
+    }
+
+#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490
+
+    nsel_constexpr14 E && value() && noexcept
+    {
+        return std::move( m_error );
+    }
+
+    constexpr E const && value() const && noexcept
+    {
+        return std::move( m_error );
+    }
+
+#endif
+
+    // x.x.5.2.4 Swap
+
+    nsel_REQUIRES_R( void,
+        std17::is_swappable<E>::value
+    )
+    swap( unexpected_type & other ) noexcept (
+        std17::is_nothrow_swappable<E>::value
+    )
+    {
+        using std::swap;
+        swap( m_error, other.m_error );
+    }
+
+    // TODO: ??? unexpected_type: in-class friend operator==, !=
+
+private:
+    error_type m_error;
+};
+
+#if nsel_CPP17_OR_GREATER
+
+/// template deduction guide:
+
+template< typename E >
+unexpected_type( E ) -> unexpected_type< E >;
+
+#endif
+
+/// class unexpected_type, std::exception_ptr specialization (P0323R2)
+
+#if !nsel_CONFIG_NO_EXCEPTIONS
+#if  nsel_P0323R <= 2
+
+// TODO: Should expected be specialized for particular E types such as exception_ptr and how?
+//       See p0323r7 2.1. Ergonomics, http://wg21.link/p0323
+template<>
+class unexpected_type< std::exception_ptr >
+{
+public:
+    using error_type = std::exception_ptr;
+
+    unexpected_type() = delete;
+
+    ~unexpected_type(){}
+
+    explicit unexpected_type( std::exception_ptr const & error )
+    : m_error( error )
+    {}
+
+    explicit unexpected_type(std::exception_ptr && error )
+    : m_error( std::move( error ) )
+    {}
+
+    template< typename E >
+    explicit unexpected_type( E error )
+    : m_error( std::make_exception_ptr( error ) )
+    {}
+
+    std::exception_ptr const & value() const
+    {
+        return m_error;
+    }
+
+    std::exception_ptr & value()
+    {
+        return m_error;
+    }
+
+private:
+    std::exception_ptr m_error;
+};
+
+#endif // nsel_P0323R
+#endif // !nsel_CONFIG_NO_EXCEPTIONS
+
+/// x.x.4, Unexpected equality operators
+
+template< typename E1, typename E2 >
+constexpr bool operator==( unexpected_type<E1> const & x, unexpected_type<E2> const & y )
+{
+    return x.value() == y.value();
+}
+
+template< typename E1, typename E2 >
+constexpr bool operator!=( unexpected_type<E1> const & x, unexpected_type<E2> const & y )
+{
+    return ! ( x == y );
+}
+
+#if nsel_P0323R <= 2
+
+template< typename E >
+constexpr bool operator<( unexpected_type<E> const & x, unexpected_type<E> const & y )
+{
+    return x.value() < y.value();
+}
+
+template< typename E >
+constexpr bool operator>( unexpected_type<E> const & x, unexpected_type<E> const & y )
+{
+    return ( y < x );
+}
+
+template< typename E >
+constexpr bool operator<=( unexpected_type<E> const & x, unexpected_type<E> const & y )
+{
+    return ! ( y < x  );
+}
+
+template< typename E >
+constexpr bool operator>=( unexpected_type<E> const & x, unexpected_type<E> const & y )
+{
+    return ! ( x < y );
+}
+
+#endif // nsel_P0323R
+
+/// x.x.5 Specialized algorithms
+
+template< typename E
+    nsel_REQUIRES_T(
+        std17::is_swappable<E>::value
+    )
+>
+void swap( unexpected_type<E> & x, unexpected_type<E> & y) noexcept ( noexcept ( x.swap(y) ) )
+{
+    x.swap( y );
+}
+
+#if nsel_P0323R <= 2
+
+// unexpected: relational operators for std::exception_ptr:
+
+inline constexpr bool operator<( unexpected_type<std::exception_ptr> const & /*x*/, unexpected_type<std::exception_ptr> const & /*y*/ )
+{
+    return false;
+}
+
+inline constexpr bool operator>( unexpected_type<std::exception_ptr> const & /*x*/, unexpected_type<std::exception_ptr> const & /*y*/ )
+{
+    return false;
+}
+
+inline constexpr bool operator<=( unexpected_type<std::exception_ptr> const & x, unexpected_type<std::exception_ptr> const & y )
+{
+    return ( x == y );
+}
+
+inline constexpr bool operator>=( unexpected_type<std::exception_ptr> const & x, unexpected_type<std::exception_ptr> const & y )
+{
+    return ( x == y );
+}
+
+#endif // nsel_P0323R
+
+// unexpected: traits
+
+#if nsel_P0323R <= 3
+
+template< typename E>
+struct is_unexpected : std::false_type {};
+
+template< typename E>
+struct is_unexpected< unexpected_type<E> > : std::true_type {};
+
+#endif // nsel_P0323R
+
+// unexpected: factory
+
+// keep make_unexpected() removed in p0323r2 for pre-C++17:
+
+template< typename E>
+nsel_constexpr14 auto
+make_unexpected( E && value ) -> unexpected_type< typename std::decay<E>::type >
+{
+    return unexpected_type< typename std::decay<E>::type >( std::forward<E>(value) );
+}
+
+#if nsel_P0323R <= 3
+
+/*nsel_constexpr14*/ auto inline
+make_unexpected_from_current_exception() -> unexpected_type< std::exception_ptr >
+{
+    return unexpected_type< std::exception_ptr >( std::current_exception() );
+}
+
+#endif // nsel_P0323R
+
+/// x.x.6, x.x.7 expected access error
+
+template< typename E >
+class bad_expected_access;
+
+/// x.x.7 bad_expected_access<void>: expected access error
+
+template <>
+class bad_expected_access< void > : public std::exception
+{
+public:
+    explicit bad_expected_access()
+    : std::exception()
+    {}
+};
+
+/// x.x.6 bad_expected_access: expected access error
+
+#if !nsel_CONFIG_NO_EXCEPTIONS
+
+template< typename E >
+class bad_expected_access : public bad_expected_access< void >
+{
+public:
+    using error_type = E;
+
+    explicit bad_expected_access( error_type error )
+    : m_error( error )
+    {}
+
+    virtual char const * what() const noexcept override
+    {
+        return "bad_expected_access";
+    }
+
+    nsel_constexpr14 error_type & error() &
+    {
+        return m_error;
+    }
+
+    constexpr error_type const & error() const &
+    {
+        return m_error;
+    }
+
+#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490
+
+    nsel_constexpr14 error_type && error() &&
+    {
+        return std::move( m_error );
+    }
+
+    constexpr error_type const && error() const &&
+    {
+        return std::move( m_error );
+    }
+
+#endif
+
+private:
+    error_type m_error;
+};
+
+#endif // nsel_CONFIG_NO_EXCEPTIONS
+
+/// x.x.8 unexpect tag, in_place_unexpected tag: construct an error
+
+struct unexpect_t{};
+using in_place_unexpected_t = unexpect_t;
+
+nsel_inline17 constexpr unexpect_t unexpect{};
+nsel_inline17 constexpr unexpect_t in_place_unexpected{};
+
+/// class error_traits
+
+#if nsel_CONFIG_NO_EXCEPTIONS
+
+namespace detail {
+    inline bool text( char const * /*text*/ ) { return true; }
+}
+
+template< typename Error >
+struct error_traits
+{
+    static void rethrow( Error const & /*e*/ )
+    {
+        assert( false && detail::text("throw bad_expected_access<Error>{ e };") );
+    }
+};
+
+template<>
+struct error_traits< std::exception_ptr >
+{
+    static void rethrow( std::exception_ptr const & /*e*/ )
+    {
+        assert( false && detail::text("throw bad_expected_access<std::exception_ptr>{ e };") );
+    }
+};
+
+template<>
+struct error_traits< std::error_code >
+{
+    static void rethrow( std::error_code const & /*e*/ )
+    {
+        assert( false && detail::text("throw std::system_error( e );") );
+    }
+};
+
+#else // nsel_CONFIG_NO_EXCEPTIONS
+
+template< typename Error >
+struct error_traits
+{
+    static void rethrow( Error const & e )
+    {
+        throw bad_expected_access<Error>{ e };
+    }
+};
+
+template<>
+struct error_traits< std::exception_ptr >
+{
+    static void rethrow( std::exception_ptr const & e )
+    {
+        std::rethrow_exception( e );
+    }
+};
+
+template<>
+struct error_traits< std::error_code >
+{
+    static void rethrow( std::error_code const & e )
+    {
+        throw std::system_error( e );
+    }
+};
+
+#endif // nsel_CONFIG_NO_EXCEPTIONS
+
+} // namespace expected_lite
+
+// provide nonstd::unexpected_type:
+
+using expected_lite::unexpected_type;
+
+namespace expected_lite {
+
+/// class expected
+
+#if nsel_P0323R <= 2
+template< typename T, typename E = std::exception_ptr >
+class expected
+#else
+template< typename T, typename E >
+class expected
+#endif // nsel_P0323R
+{
+private:
+    template< typename, typename > friend class expected;
+
+public:
+    using value_type = T;
+    using error_type = E;
+    using unexpected_type = nonstd::unexpected_type<E>;
+
+    template< typename U >
+    struct rebind
+    {
+        using type = expected<U, error_type>;
+    };
+
+    // x.x.4.1 constructors
+
+    nsel_REQUIRES_0(
+        std::is_default_constructible<T>::value
+    )
+    nsel_constexpr14 expected()
+    : contained( true )
+    {
+        contained.construct_value( value_type() );
+    }
+
+    nsel_constexpr14 expected( expected const & ) = default;
+    nsel_constexpr14 expected( expected &&      ) = default;
+
+    template< typename U, typename G
+        nsel_REQUIRES_T(
+            std::is_constructible<    T, U const &>::value
+            &&  std::is_constructible<E, G const &>::value
+            && !std::is_constructible<T, expected<U, G>       &    >::value
+            && !std::is_constructible<T, expected<U, G>       &&   >::value
+            && !std::is_constructible<T, expected<U, G> const &    >::value
+            && !std::is_constructible<T, expected<U, G> const &&   >::value
+            && !std::is_convertible<     expected<U, G>       & , T>::value
+            && !std::is_convertible<     expected<U, G>       &&, T>::value
+            && !std::is_convertible<     expected<U, G> const & , T>::value
+            && !std::is_convertible<     expected<U, G> const &&, T>::value
+            && (!std::is_convertible<U const &, T>::value || !std::is_convertible<G const &, E>::value ) /*=> explicit */
+        )
+    >
+    nsel_constexpr14 explicit expected( expected<U, G> const & other )
+    : contained( other.has_value() )
+    {
+        if ( has_value() ) contained.construct_value( T{ other.contained.value() } );
+        else               contained.construct_error( E{ other.contained.error() } );
+    }
+
+    template< typename U, typename G
+        nsel_REQUIRES_T(
+            std::is_constructible<    T, U const &>::value
+            &&  std::is_constructible<E, G const &>::value
+            && !std::is_constructible<T, expected<U, G>       &    >::value
+            && !std::is_constructible<T, expected<U, G>       &&   >::value
+            && !std::is_constructible<T, expected<U, G> const &    >::value
+            && !std::is_constructible<T, expected<U, G> const &&   >::value
+            && !std::is_convertible<     expected<U, G>       & , T>::value
+            && !std::is_convertible<     expected<U, G>       &&, T>::value
+            && !std::is_convertible<     expected<U, G> const  &, T>::value
+            && !std::is_convertible<     expected<U, G> const &&, T>::value
+            && !(!std::is_convertible<U const &, T>::value || !std::is_convertible<G const &, E>::value ) /*=> non-explicit */
+        )
+    >
+    nsel_constexpr14 /*non-explicit*/ expected( expected<U, G> const & other )
+    : contained( other.has_value() )
+    {
+        if ( has_value() ) contained.construct_value( other.contained.value() );
+        else               contained.construct_error( other.contained.error() );
+    }
+
+    template< typename U, typename G
+        nsel_REQUIRES_T(
+            std::is_constructible<    T, U>::value
+            &&  std::is_constructible<E, G>::value
+            && !std::is_constructible<T, expected<U, G>       &    >::value
+            && !std::is_constructible<T, expected<U, G>       &&   >::value
+            && !std::is_constructible<T, expected<U, G> const &    >::value
+            && !std::is_constructible<T, expected<U, G> const &&   >::value
+            && !std::is_convertible<     expected<U, G>       & , T>::value
+            && !std::is_convertible<     expected<U, G>       &&, T>::value
+            && !std::is_convertible<     expected<U, G> const & , T>::value
+            && !std::is_convertible<     expected<U, G> const &&, T>::value
+            && (!std::is_convertible<U, T>::value || !std::is_convertible<G, E>::value ) /*=> explicit */
+        )
+    >
+    nsel_constexpr14 explicit expected( expected<U, G> && other )
+    : contained( other.has_value() )
+    {
+        if ( has_value() ) contained.construct_value( T{ std::move( other.contained.value() ) } );
+        else               contained.construct_error( E{ std::move( other.contained.error() ) } );
+    }
+
+    template< typename U, typename G
+        nsel_REQUIRES_T(
+            std::is_constructible<    T, U>::value
+            &&  std::is_constructible<E, G>::value
+            && !std::is_constructible<T, expected<U, G>      &     >::value
+            && !std::is_constructible<T, expected<U, G>      &&    >::value
+            && !std::is_constructible<T, expected<U, G> const &    >::value
+            && !std::is_constructible<T, expected<U, G> const &&   >::value
+            && !std::is_convertible<     expected<U, G>       & , T>::value
+            && !std::is_convertible<     expected<U, G>       &&, T>::value
+            && !std::is_convertible<     expected<U, G> const & , T>::value
+            && !std::is_convertible<     expected<U, G> const &&, T>::value
+            && !(!std::is_convertible<U, T>::value || !std::is_convertible<G, E>::value ) /*=> non-explicit */
+        )
+    >
+    nsel_constexpr14 /*non-explicit*/ expected( expected<U, G> && other )
+    : contained( other.has_value() )
+    {
+        if ( has_value() ) contained.construct_value( std::move( other.contained.value() ) );
+        else               contained.construct_error( std::move( other.contained.error() ) );
+    }
+
+    template< typename U = T
+        nsel_REQUIRES_T(
+            std::is_copy_constructible<U>::value
+        )
+    >
+    nsel_constexpr14 expected( value_type const & value )
+    : contained( true )
+    {
+        contained.construct_value( value );
+    }
+
+    template< typename U = T
+        nsel_REQUIRES_T(
+            std::is_constructible<T,U&&>::value
+            && !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value
+            && !std::is_same<        expected<T,E>     , typename std20::remove_cvref<U>::type>::value
+            && !std::is_same<nonstd::unexpected_type<E>, typename std20::remove_cvref<U>::type>::value
+            && !std::is_convertible<U&&,T>::value /*=> explicit */
+        )
+    >
+    nsel_constexpr14 explicit expected( U && value ) noexcept
+    (
+        std::is_nothrow_move_constructible<U>::value &&
+        std::is_nothrow_move_constructible<E>::value
+    )
+    : contained( true )
+    {
+        contained.construct_value( T{ std::forward<U>( value ) } );
+    }
+
+    template< typename U = T
+        nsel_REQUIRES_T(
+            std::is_constructible<T,U&&>::value
+            && !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value
+            && !std::is_same<        expected<T,E>     , typename std20::remove_cvref<U>::type>::value
+            && !std::is_same<nonstd::unexpected_type<E>, typename std20::remove_cvref<U>::type>::value
+            &&  std::is_convertible<U&&,T>::value /*=> non-explicit */
+        )
+    >
+    nsel_constexpr14 /*non-explicit*/ expected( U && value ) noexcept
+    (
+        std::is_nothrow_move_constructible<U>::value &&
+        std::is_nothrow_move_constructible<E>::value
+    )
+    : contained( true )
+    {
+        contained.construct_value( std::forward<U>( value ) );
+    }
+
+    // construct error:
+
+    template< typename G = E
+        nsel_REQUIRES_T(
+            std::is_constructible<E, G const &   >::value
+            && !std::is_convertible< G const &, E>::value /*=> explicit */
+        )
+    >
+    nsel_constexpr14 explicit expected( nonstd::unexpected_type<G> const & error )
+    : contained( false )
+    {
+        contained.construct_error( E{ error.value() } );
+    }
+
+    template< typename G = E
+            nsel_REQUIRES_T(
+            std::is_constructible<E, G const &   >::value
+            && std::is_convertible<  G const &, E>::value /*=> non-explicit */
+        )
+    >
+    nsel_constexpr14 /*non-explicit*/ expected( nonstd::unexpected_type<G> const & error )
+    : contained( false )
+    {
+        contained.construct_error( error.value() );
+    }
+
+    template< typename G = E
+        nsel_REQUIRES_T(
+            std::is_constructible<E, G&&   >::value
+            && !std::is_convertible< G&&, E>::value /*=> explicit */
+        )
+    >
+    nsel_constexpr14 explicit expected( nonstd::unexpected_type<G> && error )
+    : contained( false )
+    {
+        contained.construct_error( E{ std::move( error.value() ) } );
+    }
+
+    template< typename G = E
+        nsel_REQUIRES_T(
+            std::is_constructible<E, G&&   >::value
+            && std::is_convertible<  G&&, E>::value /*=> non-explicit */
+        )
+    >
+    nsel_constexpr14 /*non-explicit*/ expected( nonstd::unexpected_type<G> && error )
+    : contained( false )
+    {
+        contained.construct_error( std::move( error.value() ) );
+    }
+
+    // in-place construction, value
+
+    template< typename... Args
+        nsel_REQUIRES_T(
+            std::is_constructible<T, Args&&...>::value
+        )
+    >
+    nsel_constexpr14 explicit expected( nonstd_lite_in_place_t(T), Args&&... args )
+    : contained( true )
+    {
+        contained.emplace_value( std::forward<Args>( args )... );
+    }
+
+    template< typename U, typename... Args
+        nsel_REQUIRES_T(
+            std::is_constructible<T, std::initializer_list<U>, Args&&...>::value
+        )
+    >
+    nsel_constexpr14 explicit expected( nonstd_lite_in_place_t(T), std::initializer_list<U> il, Args&&... args )
+    : contained( true )
+    {
+        contained.emplace_value( il, std::forward<Args>( args )... );
+    }
+
+    // in-place construction, error
+
+    template< typename... Args
+        nsel_REQUIRES_T(
+            std::is_constructible<E, Args&&...>::value
+        )
+    >
+    nsel_constexpr14 explicit expected( unexpect_t, Args&&... args )
+    : contained( false )
+    {
+        contained.emplace_error( std::forward<Args>( args )... );
+    }
+
+    template< typename U, typename... Args
+        nsel_REQUIRES_T(
+            std::is_constructible<E, std::initializer_list<U>, Args&&...>::value
+        )
+    >
+    nsel_constexpr14 explicit expected( unexpect_t, std::initializer_list<U> il, Args&&... args )
+    : contained( false )
+    {
+        contained.emplace_error( il, std::forward<Args>( args )... );
+    }
+
+    // x.x.4.2 destructor
+
+    // TODO: ~expected: triviality
+    // Effects: If T is not cv void and is_trivially_destructible_v<T> is false and bool(*this), calls val.~T(). If is_trivially_destructible_v<E> is false and !bool(*this), calls unexpect.~unexpected<E>().
+    // Remarks: If either T is cv void or is_trivially_destructible_v<T> is true, and is_trivially_destructible_v<E> is true, then this destructor shall be a trivial destructor.
+
+    ~expected()
+    {
+        if ( has_value() ) contained.destruct_value();
+        else               contained.destruct_error();
+    }
+
+    // x.x.4.3 assignment
+
+    expected & operator=( expected const & other )
+    {
+        expected( other ).swap( *this );
+        return *this;
+    }
+
+    expected & operator=( expected && other ) noexcept
+    (
+        std::is_nothrow_move_constructible<   T>::value
+        && std::is_nothrow_move_assignable<   T>::value
+        && std::is_nothrow_move_constructible<E>::value     // added for missing
+        && std::is_nothrow_move_assignable<   E>::value )   //   nothrow above
+    {
+        expected( std::move( other ) ).swap( *this );
+        return *this;
+    }
+
+    template< typename U
+        nsel_REQUIRES_T(
+            !std::is_same<expected<T,E>, typename std20::remove_cvref<U>::type>::value
+            && std17::conjunction<std::is_scalar<T>, std::is_same<T, std::decay<U>> >::value
+            && std::is_constructible<T ,U>::value
+            && std::is_assignable<   T&,U>::value
+            && std::is_nothrow_move_constructible<E>::value )
+    >
+    expected & operator=( U && value )
+    {
+        expected( std::forward<U>( value ) ).swap( *this );
+        return *this;
+    }
+
+    template< typename G
+        nsel_REQUIRES_T(
+            std::is_copy_constructible<E>::value    // TODO: std::is_nothrow_copy_constructible<E>
+            && std::is_copy_assignable<E>::value
+        )
+    >
+    expected & operator=( nonstd::unexpected_type<G> const & error )
+    {
+        expected( unexpect, error.value() ).swap( *this );
+        return *this;
+    }
+
+    template< typename G
+        nsel_REQUIRES_T(
+            std::is_move_constructible<E>::value    // TODO: std::is_nothrow_move_constructible<E>
+            && std::is_move_assignable<E>::value
+        )
+    >
+    expected & operator=( nonstd::unexpected_type<G> && error )
+    {
+        expected( unexpect, std::move( error.value() ) ).swap( *this );
+        return *this;
+    }
+
+    template< typename... Args
+        nsel_REQUIRES_T(
+            std::is_nothrow_constructible<T, Args&&...>::value
+        )
+    >
+    value_type & emplace( Args &&... args )
+    {
+        expected( nonstd_lite_in_place(T), std::forward<Args>(args)... ).swap( *this );
+        return value();
+    }
+
+    template< typename U, typename... Args
+        nsel_REQUIRES_T(
+            std::is_nothrow_constructible<T, std::initializer_list<U>&, Args&&...>::value
+        )
+    >
+    value_type & emplace( std::initializer_list<U> il, Args &&... args )
+    {
+        expected( nonstd_lite_in_place(T), il, std::forward<Args>(args)... ).swap( *this );
+        return value();
+    }
+
+    // x.x.4.4 swap
+
+    template< typename U=T, typename G=E >
+    nsel_REQUIRES_R( void,
+        std17::is_swappable<   U>::value
+        && std17::is_swappable<G>::value
+        && ( std::is_move_constructible<U>::value || std::is_move_constructible<G>::value )
+    )
+    swap( expected & other ) noexcept
+    (
+        std::is_nothrow_move_constructible<T>::value && std17::is_nothrow_swappable<T&>::value &&
+        std::is_nothrow_move_constructible<E>::value && std17::is_nothrow_swappable<E&>::value
+    )
+    {
+        using std::swap;
+
+        if      (   bool(*this) &&   bool(other) ) { swap( contained.value(), other.contained.value() ); }
+        else if ( ! bool(*this) && ! bool(other) ) { swap( contained.error(), other.contained.error() ); }
+        else if (   bool(*this) && ! bool(other) ) { error_type t( std::move( other.error() ) );
+                                                     other.contained.destruct_error();
+                                                     other.contained.construct_value( std::move( contained.value() ) );
+                                                     contained.destruct_value();
+                                                     contained.construct_error( std::move( t ) );
+                                                     bool has_value = contained.has_value();
+                                                     bool other_has_value = other.has_value();
+                                                     other.contained.set_has_value(has_value);
+                                                     contained.set_has_value(other_has_value);
+                                                   }
+        else if ( ! bool(*this) &&   bool(other) ) { other.swap( *this ); }
+    }
+
+    // x.x.4.5 observers
+
+    constexpr value_type const * operator ->() const
+    {
+        return assert( has_value() ), contained.value_ptr();
+    }
+
+    value_type * operator ->()
+    {
+        return assert( has_value() ), contained.value_ptr();
+    }
+
+    constexpr value_type const & operator *() const &
+    {
+        return assert( has_value() ), contained.value();
+    }
+
+    value_type & operator *() &
+    {
+        return assert( has_value() ), contained.value();
+    }
+
+#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490
+
+    constexpr value_type const && operator *() const &&
+    {
+        return assert( has_value() ), std::move( contained.value() );
+    }
+
+    nsel_constexpr14 value_type && operator *() &&
+    {
+        return assert( has_value() ), std::move( contained.value() );
+    }
+
+#endif
+
+    constexpr explicit operator bool() const noexcept
+    {
+        return has_value();
+    }
+
+    constexpr bool has_value() const noexcept
+    {
+        return contained.has_value();
+    }
+
+    constexpr value_type const & value() const &
+    {
+        return has_value()
+            ? ( contained.value() )
+            : ( error_traits<error_type>::rethrow( contained.error() ), contained.value() );
+    }
+
+    value_type & value() &
+    {
+        return has_value()
+            ? ( contained.value() )
+            : ( error_traits<error_type>::rethrow( contained.error() ), contained.value() );
+    }
+
+#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490
+
+    constexpr value_type const && value() const &&
+    {
+        return std::move( has_value()
+            ? ( contained.value() )
+            : ( error_traits<error_type>::rethrow( contained.error() ), contained.value() ) );
+    }
+
+    nsel_constexpr14 value_type && value() &&
+    {
+        return std::move( has_value()
+            ? ( contained.value() )
+            : ( error_traits<error_type>::rethrow( contained.error() ), contained.value() ) );
+    }
+
+#endif
+
+    constexpr error_type const & error() const &
+    {
+        return assert( ! has_value() ), contained.error();
+    }
+
+    error_type & error() &
+    {
+        return assert( ! has_value() ), contained.error();
+    }
+
+#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490
+
+    constexpr error_type const && error() const &&
+    {
+        return assert( ! has_value() ), std::move( contained.error() );
+    }
+
+    error_type && error() &&
+    {
+        return assert( ! has_value() ), std::move( contained.error() );
+    }
+
+#endif
+
+    constexpr unexpected_type get_unexpected() const
+    {
+        return make_unexpected( contained.error() );
+    }
+
+    template< typename Ex >
+    bool has_exception() const
+    {
+        using ContainedEx = typename std::remove_reference< decltype( get_unexpected().value() ) >::type;
+        return ! has_value() && std::is_base_of< Ex, ContainedEx>::value;
+    }
+
+    template< typename U
+        nsel_REQUIRES_T(
+            std::is_copy_constructible< T>::value
+            && std::is_convertible<U&&, T>::value
+        )
+    >
+    value_type value_or( U && v ) const &
+    {
+        return has_value()
+            ? contained.value()
+            : static_cast<T>( std::forward<U>( v ) );
+    }
+
+    template< typename U
+        nsel_REQUIRES_T(
+            std::is_move_constructible< T>::value
+            && std::is_convertible<U&&, T>::value
+        )
+    >
+    value_type value_or( U && v ) &&
+    {
+        return has_value()
+            ? std::move( contained.value() )
+            : static_cast<T>( std::forward<U>( v ) );
+    }
+
+    // unwrap()
+
+//  template <class U, class E>
+//  constexpr expected<U,E> expected<expected<U,E>,E>::unwrap() const&;
+
+//  template <class T, class E>
+//  constexpr expected<T,E> expected<T,E>::unwrap() const&;
+
+//  template <class U, class E>
+//  expected<U,E> expected<expected<U,E>, E>::unwrap() &&;
+
+//  template <class T, class E>
+//  template expected<T,E> expected<T,E>::unwrap() &&;
+
+    // factories
+
+//  template< typename Ex, typename F>
+//  expected<T,E> catch_exception(F&& f);
+
+//  template< typename F>
+//  expected<decltype(func(declval<T>())),E> map(F&& func) ;
+
+//  template< typename F>
+//  'see below' bind(F&& func);
+
+//  template< typename F>
+//  expected<T,E> catch_error(F&& f);
+
+//  template< typename F>
+//  'see below' then(F&& func);
+
+private:
+    detail::storage_t
+    <
+        T
+        ,E
+        , std::is_copy_constructible<T>::value && std::is_copy_constructible<E>::value
+        , std::is_move_constructible<T>::value && std::is_move_constructible<E>::value
+    >
+    contained;
+};
+
+/// class expected, void specialization
+
+template< typename E >
+class expected<void, E>
+{
+private:
+    template< typename, typename > friend class expected;
+
+public:
+    using value_type = void;
+    using error_type = E;
+    using unexpected_type = nonstd::unexpected_type<E>;
+
+    // x.x.4.1 constructors
+
+    constexpr expected() noexcept
+        : contained( true )
+    {}
+
+    nsel_constexpr14 expected( expected const & other ) = default;
+    nsel_constexpr14 expected( expected &&      other ) = default;
+
+    constexpr explicit expected( nonstd_lite_in_place_t(void) )
+        : contained( true )
+    {}
+
+    template< typename G = E
+        nsel_REQUIRES_T(
+            !std::is_convertible<G const &, E>::value /*=> explicit */
+        )
+    >
+    nsel_constexpr14 explicit expected( nonstd::unexpected_type<G> const & error )
+        : contained( false )
+    {
+        contained.construct_error( E{ error.value() } );
+    }
+
+    template< typename G = E
+        nsel_REQUIRES_T(
+            std::is_convertible<G const &, E>::value /*=> non-explicit */
+        )
+    >
+    nsel_constexpr14 /*non-explicit*/ expected( nonstd::unexpected_type<G> const & error )
+        : contained( false )
+    {
+        contained.construct_error( error.value() );
+    }
+
+    template< typename G = E
+        nsel_REQUIRES_T(
+            !std::is_convertible<G&&, E>::value /*=> explicit */
+        )
+    >
+    nsel_constexpr14 explicit expected( nonstd::unexpected_type<G> && error )
+        : contained( false )
+    {
+        contained.construct_error( E{ std::move( error.value() ) } );
+    }
+
+    template< typename G = E
+        nsel_REQUIRES_T(
+            std::is_convertible<G&&, E>::value /*=> non-explicit */
+        )
+    >
+    nsel_constexpr14 /*non-explicit*/ expected( nonstd::unexpected_type<G> && error )
+        : contained( false )
+    {
+        contained.construct_error( std::move( error.value() ) );
+    }
+
+    template< typename... Args
+        nsel_REQUIRES_T(
+            std::is_constructible<E, Args&&...>::value
+        )
+    >
+    nsel_constexpr14 explicit expected( unexpect_t, Args&&... args )
+        : contained( false )
+    {
+        contained.emplace_error( std::forward<Args>( args )... );
+    }
+
+    template< typename U, typename... Args
+        nsel_REQUIRES_T(
+            std::is_constructible<E, std::initializer_list<U>, Args&&...>::value
+        )
+    >
+    nsel_constexpr14 explicit expected( unexpect_t, std::initializer_list<U> il, Args&&... args )
+        : contained( false )
+    {
+        contained.emplace_error( il, std::forward<Args>( args )... );
+    }
+
+    // destructor
+
+    ~expected()
+    {
+        if ( ! has_value() )
+        {
+            contained.destruct_error();
+        }
+    }
+
+    // x.x.4.3 assignment
+
+    expected & operator=( expected const & other )
+    {
+        expected( other ).swap( *this );
+        return *this;
+    }
+
+    expected & operator=( expected && other ) noexcept
+    (
+        std::is_nothrow_move_assignable<E>::value &&
+        std::is_nothrow_move_constructible<E>::value )
+    {
+        expected( std::move( other ) ).swap( *this );
+        return *this;
+    }
+
+    void emplace()
+    {
+        expected().swap( *this );
+    }
+
+    // x.x.4.4 swap
+
+    template< typename G = E >
+    nsel_REQUIRES_R( void,
+        std17::is_swappable<G>::value
+        && std::is_move_constructible<G>::value
+    )
+    swap( expected & other ) noexcept
+    (
+        std::is_nothrow_move_constructible<E>::value && std17::is_nothrow_swappable<E&>::value
+    )
+    {
+        using std::swap;
+
+        if      ( ! bool(*this) && ! bool(other) ) { swap( contained.error(), other.contained.error() ); }
+        else if (   bool(*this) && ! bool(other) ) { contained.construct_error( std::move( other.error() ) );
+                                                     bool has_value = contained.has_value();
+                                                     bool other_has_value = other.has_value();
+                                                     other.contained.set_has_value(has_value);
+                                                     contained.set_has_value(other_has_value);
+                                                     }
+        else if ( ! bool(*this) &&   bool(other) ) { other.swap( *this ); }
+    }
+
+    // x.x.4.5 observers
+
+    constexpr explicit operator bool() const noexcept
+    {
+        return has_value();
+    }
+
+    constexpr bool has_value() const noexcept
+    {
+        return contained.has_value();
+    }
+
+    void value() const
+    {
+        if ( ! has_value() )
+        {
+            error_traits<error_type>::rethrow( contained.error() );
+        }
+    }
+
+    constexpr error_type const & error() const &
+    {
+        return assert( ! has_value() ), contained.error();
+    }
+
+    error_type & error() &
+    {
+        return assert( ! has_value() ), contained.error();
+    }
+
+#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490
+
+    constexpr error_type const && error() const &&
+    {
+        return assert( ! has_value() ), std::move( contained.error() );
+    }
+
+    error_type && error() &&
+    {
+        return assert( ! has_value() ), std::move( contained.error() );
+    }
+
+#endif
+
+    constexpr unexpected_type get_unexpected() const
+    {
+        return make_unexpected( contained.error() );
+    }
+
+    template< typename Ex >
+    bool has_exception() const
+    {
+        using ContainedEx = typename std::remove_reference< decltype( get_unexpected().value() ) >::type;
+        return ! has_value() && std::is_base_of< Ex, ContainedEx>::value;
+    }
+
+//  template constexpr 'see below' unwrap() const&;
+//
+//  template 'see below' unwrap() &&;
+
+    // factories
+
+//  template< typename Ex, typename F>
+//  expected<void,E> catch_exception(F&& f);
+//
+//  template< typename F>
+//  expected<decltype(func()), E> map(F&& func) ;
+//
+//  template< typename F>
+//  'see below' bind(F&& func) ;
+//
+//  template< typename F>
+//  expected<void,E> catch_error(F&& f);
+//
+//  template< typename F>
+//  'see below' then(F&& func);
+
+private:
+    detail::storage_t
+    <
+        void
+        , E
+        , std::is_copy_constructible<E>::value
+        , std::is_move_constructible<E>::value
+    >
+    contained;
+};
+
+// x.x.4.6 expected<>: comparison operators
+
+template< typename T1, typename E1, typename T2, typename E2 >
+constexpr bool operator==( expected<T1,E1> const & x, expected<T2,E2> const & y )
+{
+    return bool(x) != bool(y) ? false : bool(x) == false ? x.error() == y.error() : *x == *y;
+}
+
+template< typename T1, typename E1, typename T2, typename E2 >
+constexpr bool operator!=( expected<T1,E1> const & x, expected<T2,E2> const & y )
+{
+    return !(x == y);
+}
+
+template< typename E1, typename E2 >
+constexpr bool operator==( expected<void,E1> const & x, expected<void,E1> const & y )
+{
+    return bool(x) != bool(y) ? false : bool(x) == false ? x.error() == y.error() : true;
+}
+
+#if nsel_P0323R <= 2
+
+template< typename T, typename E >
+constexpr bool operator<( expected<T,E> const & x, expected<T,E> const & y )
+{
+    return (!y) ? false : (!x) ? true : *x < *y;
+}
+
+template< typename T, typename E >
+constexpr bool operator>( expected<T,E> const & x, expected<T,E> const & y )
+{
+    return (y < x);
+}
+
+template< typename T, typename E >
+constexpr bool operator<=( expected<T,E> const & x, expected<T,E> const & y )
+{
+    return !(y < x);
+}
+
+template< typename T, typename E >
+constexpr bool operator>=( expected<T,E> const & x, expected<T,E> const & y )
+{
+    return !(x < y);
+}
+
+#endif
+
+// x.x.4.7 expected: comparison with T
+
+template< typename T1, typename E1, typename T2 >
+constexpr bool operator==( expected<T1,E1> const & x, T2 const & v )
+{
+    return bool(x) ? *x == v : false;
+}
+
+template< typename T1, typename E1, typename T2 >
+constexpr bool operator==(T2 const & v, expected<T1,E1> const & x )
+{
+    return bool(x) ? v == *x : false;
+}
+
+template< typename T1, typename E1, typename T2 >
+constexpr bool operator!=( expected<T1,E1> const & x, T2 const & v )
+{
+    return bool(x) ? *x != v : true;
+}
+
+template< typename T1, typename E1, typename T2 >
+constexpr bool operator!=( T2 const & v, expected<T1,E1> const & x )
+{
+    return bool(x) ? v != *x : true;
+}
+
+#if nsel_P0323R <= 2
+
+template< typename T, typename E >
+constexpr bool operator<( expected<T,E> const & x, T const & v )
+{
+    return bool(x) ? *x < v : true;
+}
+
+template< typename T, typename E >
+constexpr bool operator<( T const & v, expected<T,E> const & x )
+{
+    return bool(x) ? v < *x : false;
+}
+
+template< typename T, typename E >
+constexpr bool operator>( T const & v, expected<T,E> const & x )
+{
+    return bool(x) ? *x < v : false;
+}
+
+template< typename T, typename E >
+constexpr bool operator>( expected<T,E> const & x, T const & v )
+{
+    return bool(x) ? v < *x : false;
+}
+
+template< typename T, typename E >
+constexpr bool operator<=( T const & v, expected<T,E> const & x )
+{
+    return bool(x) ? ! ( *x < v ) : false;
+}
+
+template< typename T, typename E >
+constexpr bool operator<=( expected<T,E> const & x, T const & v )
+{
+    return bool(x) ? ! ( v < *x ) : true;
+}
+
+template< typename T, typename E >
+constexpr bool operator>=( expected<T,E> const & x, T const & v )
+{
+    return bool(x) ? ! ( *x < v ) : false;
+}
+
+template< typename T, typename E >
+constexpr bool operator>=( T const & v, expected<T,E> const & x )
+{
+    return bool(x) ? ! ( v < *x ) : true;
+}
+
+#endif // nsel_P0323R
+
+// x.x.4.8 expected: comparison with unexpected_type
+
+template< typename T1, typename E1 , typename E2 >
+constexpr bool operator==( expected<T1,E1> const & x, unexpected_type<E2> const & u )
+{
+    return (!x) ? x.get_unexpected() == u : false;
+}
+
+template< typename T1, typename E1 , typename E2 >
+constexpr bool operator==( unexpected_type<E2> const & u, expected<T1,E1> const & x )
+{
+    return ( x == u );
+}
+
+template< typename T1, typename E1 , typename E2 >
+constexpr bool operator!=( expected<T1,E1> const & x, unexpected_type<E2> const & u )
+{
+    return ! ( x == u );
+}
+
+template< typename T1, typename E1 , typename E2 >
+constexpr bool operator!=( unexpected_type<E2> const & u, expected<T1,E1> const & x )
+{
+    return ! ( x == u );
+}
+
+#if nsel_P0323R <= 2
+
+template< typename T, typename E >
+constexpr bool operator<( expected<T,E> const & x, unexpected_type<E> const & u )
+{
+    return (!x) ? ( x.get_unexpected() < u ) : false;
+}
+
+template< typename T, typename E >
+constexpr bool operator<( unexpected_type<E> const & u, expected<T,E> const & x )
+{
+  return (!x) ? ( u < x.get_unexpected() ) : true ;
+}
+
+template< typename T, typename E >
+constexpr bool operator>( expected<T,E> const & x, unexpected_type<E> const & u )
+{
+    return ( u < x );
+}
+
+template< typename T, typename E >
+constexpr bool operator>( unexpected_type<E> const & u, expected<T,E> const & x )
+{
+    return ( x < u );
+}
+
+template< typename T, typename E >
+constexpr bool operator<=( expected<T,E> const & x, unexpected_type<E> const & u )
+{
+    return ! ( u < x );
+}
+
+template< typename T, typename E >
+constexpr bool operator<=( unexpected_type<E> const & u, expected<T,E> const & x)
+{
+    return ! ( x < u );
+}
+
+template< typename T, typename E >
+constexpr bool operator>=( expected<T,E> const & x, unexpected_type<E> const & u  )
+{
+    return ! ( u > x );
+}
+
+template< typename T, typename E >
+constexpr bool operator>=( unexpected_type<E> const & u, expected<T,E> const & x )
+{
+    return ! ( x > u );
+}
+
+#endif // nsel_P0323R
+
+/// x.x.x Specialized algorithms
+
+template< typename T, typename E
+    nsel_REQUIRES_T(
+        ( std::is_void<T>::value || std::is_move_constructible<T>::value )
+        && std::is_move_constructible<E>::value
+        && std17::is_swappable<T>::value
+        && std17::is_swappable<E>::value )
+>
+void swap( expected<T,E> & x, expected<T,E> & y ) noexcept ( noexcept ( x.swap(y) ) )
+{
+    x.swap( y );
+}
+
+#if nsel_P0323R <= 3
+
+template< typename T >
+constexpr auto make_expected( T && v ) -> expected< typename std::decay<T>::type >
+{
+    return expected< typename std::decay<T>::type >( std::forward<T>( v ) );
+}
+
+// expected<void> specialization:
+
+auto inline make_expected() -> expected<void>
+{
+    return expected<void>( in_place );
+}
+
+template< typename T >
+constexpr auto make_expected_from_current_exception() -> expected<T>
+{
+    return expected<T>( make_unexpected_from_current_exception() );
+}
+
+template< typename T >
+auto make_expected_from_exception( std::exception_ptr v ) -> expected<T>
+{
+    return expected<T>( unexpected_type<std::exception_ptr>( std::forward<std::exception_ptr>( v ) ) );
+}
+
+template< typename T, typename E >
+constexpr auto make_expected_from_error( E e ) -> expected<T, typename std::decay<E>::type>
+{
+    return expected<T, typename std::decay<E>::type>( make_unexpected( e ) );
+}
+
+template< typename F
+    nsel_REQUIRES_T( ! std::is_same<typename std::result_of<F()>::type, void>::value )
+>
+/*nsel_constexpr14*/
+auto make_expected_from_call( F f ) -> expected< typename std::result_of<F()>::type >
+{
+    try
+    {
+        return make_expected( f() );
+    }
+    catch (...)
+    {
+        return make_unexpected_from_current_exception();
+    }
+}
+
+template< typename F
+    nsel_REQUIRES_T( std::is_same<typename std::result_of<F()>::type, void>::value )
+>
+/*nsel_constexpr14*/
+auto make_expected_from_call( F f ) -> expected<void>
+{
+    try
+    {
+        f();
+        return make_expected();
+    }
+    catch (...)
+    {
+        return make_unexpected_from_current_exception();
+    }
+}
+
+#endif // nsel_P0323R
+
+} // namespace expected_lite
+
+using namespace expected_lite;
+
+// using expected_lite::expected;
+// using ...
+
+} // namespace nonstd
+
+namespace std {
+
+// expected: hash support
+
+template< typename T, typename E >
+struct hash< nonstd::expected<T,E> >
+{
+    using result_type = std::size_t;
+    using argument_type = nonstd::expected<T,E>;
+
+    constexpr result_type operator()(argument_type const & arg) const
+    {
+        return arg ? std::hash<T>{}(*arg) : result_type{};
+    }
+};
+
+// TBD - ?? remove? see spec.
+template< typename T, typename E >
+struct hash< nonstd::expected<T&,E> >
+{
+    using result_type = std::size_t;
+    using argument_type = nonstd::expected<T&,E>;
+
+    constexpr result_type operator()(argument_type const & arg) const
+    {
+        return arg ? std::hash<T>{}(*arg) : result_type{};
+    }
+};
+
+// TBD - implement
+// bool(e), hash<expected<void,E>>()(e) shall evaluate to the hashing true;
+// otherwise it evaluates to an unspecified value if E is exception_ptr or
+// a combination of hashing false and hash<E>()(e.error()).
+
+template< typename E >
+struct hash< nonstd::expected<void,E> >
+{
+};
+
+} // namespace std
+
+namespace nonstd {
+
+// void unexpected() is deprecated && removed in C++17
+
+#if nsel_CPP17_OR_GREATER || nsel_COMPILER_MSVC_VERSION > 141
+template< typename E >
+using unexpected = unexpected_type<E>;
+#endif
+
+} // namespace nonstd
+
+#undef nsel_REQUIRES
+#undef nsel_REQUIRES_0
+#undef nsel_REQUIRES_T
+
+nsel_RESTORE_WARNINGS()
+
+#endif // nsel_USES_STD_EXPECTED
+
+#endif // NONSTD_EXPECTED_LITE_HPP
diff --git a/src/third_party/url.cpp b/src/third_party/url.cpp
new file mode 100644 (file)
index 0000000..373b6a1
--- /dev/null
@@ -0,0 +1,930 @@
+#include "url.hpp"
+//#include <boost/regex.hpp>
+#include <algorithm>
+#include <cstdlib>
+#include <iterator>
+#include <sstream>
+#include <cstdint>
+#include <iostream>
+
+
+
+
+namespace {
+
+static const uint8_t tbl[256] = {
+    0,0,0,0, 0,0,0,0,     // NUL SOH STX ETX  EOT ENQ ACK BEL
+    0,0,0,0, 0,0,0,0,     // BS  HT  LF  VT   FF  CR  SO  SI
+    0,0,0,0, 0,0,0,0,     // DLE DC1 DC2 DC3  DC4 NAK SYN ETB
+    0,0,0,0, 0,0,0,0,     // CAN EM  SUB ESC  FS  GS  RS  US
+    0x00,0x01,0x00,0x00, 0x01,0x20,0x01,0x01, // SP ! " #  $ % & '
+    0x01,0x01,0x01,0x01, 0x01,0x01,0x01,0x08, //  ( ) * +  , - . /
+    0x01,0x01,0x01,0x01, 0x01,0x01,0x01,0x01, //  0 1 2 3  4 5 6 7
+    0x01,0x01,0x04,0x01, 0x00,0x01,0x00,0x10, //  8 9 : ;  < = > ?
+    0x02,0x01,0x01,0x01, 0x01,0x01,0x01,0x01, //  @ A B C  D E F G
+    0x01,0x01,0x01,0x01, 0x01,0x01,0x01,0x01, //  H I J K  L M N O
+    0x01,0x01,0x01,0x01, 0x01,0x01,0x01,0x01, //  P Q R S  T U V W
+    0x01,0x01,0x01,0x00, 0x00,0x00,0x00,0x01, //  X Y Z [  \ ] ^ _
+    0x00,0x01,0x01,0x01, 0x01,0x01,0x01,0x01, //  ` a b c  d e f g
+    0x01,0x01,0x01,0x01, 0x01,0x01,0x01,0x01, //  h i j k  l m n o
+    0x01,0x01,0x01,0x01, 0x01,0x01,0x01,0x01, //  p q r s  t u v w
+    0x01,0x01,0x01,0x00, 0x00,0x00,0x01,0x00, //  x y z {  | } ~ DEL
+    0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+    0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+    0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+    0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0
+};
+
+
+inline bool is_char(char c, std::uint8_t mask) {
+    return (tbl[static_cast<unsigned char>(c)]&mask) != 0;
+}
+
+
+inline bool is_chars(const char* s, const char* e, std::uint8_t mask) {
+    while(s!=e)
+        if (!is_char(*s++,mask))
+            return false;
+    return true;
+}
+
+
+inline bool is_alpha(char c) {
+    return (c>='A'&&c<='Z')||(c>='a'&&c<='z');
+}
+
+
+inline bool is_num(char c) {
+    return c>='0'&&c<='9';
+}
+
+
+inline bool is_alnum(char c) {
+    return is_alpha(c)||is_num(c);
+}
+
+
+inline bool is_hexdigit(char c) {
+    return is_num(c)||(c>='A'&&c<='F')||(c>='a'&&c<='f');
+}
+
+
+inline bool is_uint(const char *&s, const char *e, uint32_t max) {
+    if (s==e || !is_num(*s))
+        return false;
+    const char *t=s;
+    uint32_t val = *t++-'0';
+    if (val)
+        while(t!=e && is_num(*t))
+           val=val*10+(*t++-'0');
+    if (val>max)
+        return false;
+    s=t;
+    return true;
+}
+
+
+inline char get_hex_digit(char c) {
+    if (c>='0'&&c<='9')
+        return c-'0';
+    if (c>='A'&&c<='F')
+        return c-'A'+10;
+    if (c>='a'&&c<='f')
+        return c-'a'+10;
+    return -1;
+}
+
+
+inline void to_lower(std::string& s) {
+    for(auto& c : s)
+        if (c>='A' && c<='Z')
+            c |= 0x20;
+}
+
+
+inline const char* find_first_of(const char *s, const char *e, const char *q) {
+    for(; s!=e; ++s)
+        for(const char *t=q; *t; ++t)
+            if (*s==*t)
+                return s;
+    return e;
+}
+
+
+inline const char* find_char(const char *s, const char *e, const char c) {
+    while (s!=e && *s!=c)
+        ++s;
+    return s;
+}
+
+
+inline bool is_scheme(const char *s, const char *e)
+{
+    if (!s||!e||s==e||!is_alpha(*s))
+        return false;
+    char c;
+    while(++s!=e)
+        if (!is_alnum(c=*s)&&c!='+'&&c!='-'&&c!='.')
+            return false;
+    return true;
+}
+
+
+inline bool is_scheme(const std::string &s) {
+    return is_scheme(s.data(),s.data()+s.length());
+}
+
+
+std::string normalize_scheme(const char *b, const char *e) {
+    std::string o(b,e-b);
+    to_lower(o);
+    return o;
+}
+
+
+inline bool is_ipv4(const char *s, const char *e) {
+    size_t l=e-s;
+    if (l<7 || l>254)
+        return false;
+    for (const char *p=s; p!=e; ++p)
+        if (*p!='.'&&!is_num(*p))
+            return false;
+    return true;
+}
+
+
+inline bool is_ipv4(const std::string &s) {
+    return is_ipv4(s.data(),s.data()+s.length());
+}
+
+
+inline bool is_valid_ipv4(const char *s, const char *e) {
+    return is_uint(s,e,255) && s!=e && *s++=='.' &&
+           is_uint(s,e,255) && s!=e && *s++=='.' &&
+           is_uint(s,e,255) && s!=e && *s++=='.' &&
+           is_uint(s,e,255) && s==e;
+}
+
+
+inline bool is_valid_ipv4(const std::string &s) {
+    return is_valid_ipv4(s.data(),s.data()+s.length());
+}
+
+
+inline bool is_reg_name(const char *s, const char *e) {
+    return is_chars(s, e, 0x01);
+}
+
+
+inline bool is_reg_name(const std::string &s) {
+    return is_reg_name(s.data(),s.data()+s.length());
+}
+
+
+std::string normalize_reg_name(const std::string& s) {
+    std::string o(s);
+    to_lower(o); // see rfc 4343
+    return o;
+}
+
+
+bool is_ipv6(const char *s, const char *e) {
+    size_t l=e-s;
+    if (l<2 || l>254)
+        return false;
+    for (const char *p=s; p!=e; ++p)
+        if (*p!=':'&&*p!='.'&&!is_hexdigit(*p))
+            return false;
+    return true;
+}
+
+
+inline bool is_ipv6(const std::string &s) {
+    return is_ipv6(s.data(),s.data()+s.length());
+}
+
+
+bool is_valid_ipv6(const char *s, const char *e) {
+    if ((e-s)>39||(e-s)<2)
+        return false;
+    bool null_field=false;
+    const char *b=s, *p=s;
+    int nfields=0, ndigits=0;
+    if (p[0]==':') {
+        if (p[1]!=':')
+            return false;
+        null_field=true;
+        b=(p+=2);
+        if (p==e)
+            return true;
+    }
+    while(p!=e) {
+        if (*p=='.') {
+            return ((!null_field&&nfields==6)||(null_field&&nfields<7))&&is_valid_ipv4(b, e);
+        } else if (*p==':') {
+            if (ndigits==0) {
+                if (null_field)
+                    return false;
+                null_field=true;
+            } else {
+                ++nfields;
+                ndigits=0;
+            }
+            b=++p;
+        } else {
+            if ((++ndigits>4) || !is_hexdigit(*p++))
+                return false;
+        }
+    }
+    if (ndigits>0)
+        ++nfields;
+    else {
+        if (e[-1]==':') {
+            if (e[-2]==':' && nfields<8)
+                return true;
+            return false;
+        }
+    }
+    return (!null_field&&nfields==8)||(null_field&&nfields<8);
+}
+
+
+inline bool is_valid_ipv6(const std::string &s) {
+    return is_valid_ipv6(s.data(),s.data()+s.length());
+}
+
+
+std::string normalize_IPv6(const char *s, const char *e) {
+    if (!is_ipv6(s, e))
+        throw Url::parse_error("IPv6 ["+std::string(s,e-s)+"] is invalid");
+    if ((e-s)==2 && s[0]==':' && s[1]==':')
+        return std::string(s,e-s);
+
+    // Split IPv6 at colons
+    const size_t token_size = 10;
+    const char *p=s, *tokens[token_size];
+    if (*p==':')
+        ++p;
+    if (e[-1]==':')
+        --e;
+    const char *b=p;
+    size_t i=0;
+    while (p!=e) {
+        if (*p++==':') {
+            if (i+1 >= token_size) {
+                throw Url::parse_error("IPv6 ["+std::string(s,e-s)+"] is invalid");
+            }
+            tokens[i++]=b;
+            b=p;
+        }
+    }
+    if (i<8)
+        tokens[i++]=b;
+    tokens[i]=p;
+    size_t ntokens=i;
+
+    // Get IPv4 address which is normalized by default
+    const char *ipv4_b=nullptr, *ipv4_e=nullptr;
+    if ((tokens[ntokens]-tokens[ntokens-1])>5) {
+        ipv4_b=tokens[ntokens-1];
+        ipv4_e=tokens[ntokens];
+        --ntokens;
+    }
+
+    // Decode the fields
+    const size_t fields_size = 8;
+    std::uint16_t fields[fields_size];
+    size_t null_pos=8, null_len=0, nfields=0;
+    for(size_t i=0; i<ntokens; ++i) {
+        const char *p=tokens[i];
+        if (p==tokens[i+1] || *p==':')
+            null_pos=i;
+        else {
+            if (nfields >= fields_size) {
+                throw Url::parse_error("IPv6 ["+std::string(s,e-s)+"] is invalid");
+            }
+            std::uint16_t field=get_hex_digit(*p++);
+            while (p!=tokens[i+1] && *p!=':')
+                field=(field<<4)|get_hex_digit(*p++);
+            fields[nfields++]=field;
+        }
+    }
+    i = nfields;
+    nfields=(ipv4_b)?6:8;
+    if (i<nfields) {
+        if (i<null_pos) {
+            throw Url::parse_error("IPv6 ["+std::string(s,e-s)+"] is invalid");
+        }
+        size_t last=nfields;
+        if (i!=null_pos)
+            do fields[--last]=fields[--i]; while (i!=null_pos);
+        do fields[--last]=0; while (last!=null_pos);
+    }
+
+    // locate first longer sequence of zero
+    i=null_len=0;
+    null_pos=nfields;
+    size_t first=0;
+    for(;;) {
+        while (i<nfields && fields[i]!=0)
+            ++i;
+        if (i==nfields)
+            break;
+        first=i;
+        while (i<nfields && fields[i]==0)
+            ++i;
+        if ((i-first)>null_len) {
+            null_pos=first;
+            null_len=i-first;
+        }
+        if (i==nfields)
+            break;
+    }
+    if (null_len==1) {
+        null_pos=nfields;
+        null_len=1;
+    }
+
+    // Encode normalized IPv6
+    std::stringstream str;
+    if (null_pos==0) {
+        str << std::hex << ':';
+        i=null_len;
+    } else {
+        str << std::hex << fields[0];
+        for (i=1; i<null_pos; ++i)
+            str << ':' << fields[i];
+        if (i<nfields)
+            str << ':';
+        i+=null_len;
+        if (i==8 && null_len!=0)
+            str << ':';
+    }
+    for (; i<nfields; ++i)
+        str << ':' << fields[i];
+    if (ipv4_b)
+        str << ':' << std::string(ipv4_b, ipv4_e-ipv4_b);
+
+    return str.str();
+}
+
+
+inline std::string normalize_IPv6(const std::string &s) {
+    return normalize_IPv6(s.data(),s.data()+s.length());
+}
+
+
+inline bool is_port(const char *s, const char *e) {
+    return is_uint(s,e,65535) && s==e;
+}
+
+
+inline bool is_port(const std::string &s) {
+    return is_port(s.data(),s.data()+s.length());
+}
+
+
+std::string normalize_path(const std::string& s) {
+    if (s.empty())
+        return s;
+    std::string elem;
+    std::vector<std::string> elems;
+    std::stringstream si(s);
+
+    while(!std::getline(si, elem, '/').eof()){
+        if (elem=="" || elem==".")
+            continue;
+        if (elem=="..") {
+            if (!elems.empty())
+                elems.pop_back();
+            continue;
+        }
+        elems.push_back(elem);
+    }
+    if (elem==".")
+        elems.push_back("");
+    else if (elem=="..") {
+        if (!elems.empty())
+            elems.pop_back();
+    }
+    else
+        elems.push_back(elem);
+
+    std::stringstream so;
+    if (s[0]=='/')
+        so << '/';
+    if (!elems.empty()) {
+        auto it=elems.begin(), end=elems.end();
+        so << *it;
+        while(++it!=end)
+            so << '/' << *it;
+    }
+    return so.str();
+}
+
+
+std::string decode(const char *s, const char *e) {
+    std::string o;
+    o.reserve(e-s);
+    while(s!=e) {
+        char c=*s++, a, b;
+        if (c=='%') {
+            if (s==e || (a=get_hex_digit(*s++))<0 || s==e || (b=get_hex_digit(*s++))<0)
+                throw Url::parse_error("Invalid percent encoding");
+            c=(a<<4)|b;
+        }
+        o.push_back(c);
+    }
+    return o;
+}
+
+
+std::string decode_plus(const char *s, const char *e) {
+    std::string o;
+    o.reserve(e-s);
+    while(s!=e) {
+        char c=*s++, a, b;
+        if (c=='+')
+            c=' ';
+        else if (c=='%') {
+            if (s==e || (a=get_hex_digit(*s++))<0 || s==e || (b=get_hex_digit(*s++))<0)
+                throw Url::parse_error("Invalid percent encoding");
+            c=(a<<4)|b;
+        }
+        o.push_back(c);
+    }
+    return o;
+}
+
+
+class encode {
+    public:
+        encode(const std::string& s, std::uint8_t mask) : m_s(s), m_mask(mask) {}
+    private:
+        const std::string& m_s;
+        std::uint8_t m_mask;
+    friend std::ostream& operator<< (std::ostream& o, const encode& e) {
+        for (const char c:e.m_s)
+            if (is_char(c,e.m_mask))
+                o<<c;
+            else
+                o<<'%'<<"0123456789ABCDEF"[((uint8_t)c)>>4]<<"0123456789ABCDEF"[((uint8_t)c)&0xF];
+        return o;
+    }
+};
+
+
+class encode_query_key {
+    public:
+        encode_query_key(const std::string& s, std::uint8_t mask) : m_s(s), m_mask(mask) {}
+    private:
+        const std::string& m_s;
+        std::uint8_t m_mask;
+    friend std::ostream& operator<< (std::ostream& o, const encode_query_key& e) {
+        for (const char c:e.m_s)
+            if (c==' ')
+                o<<'+';
+            else if (c=='+')
+                o<<"%2B";
+            else if (c=='=')
+                o<<"%3D";
+            else if (c=='&')
+                o<<"%26";
+            else if (c==';')
+                o<<"%3B";
+            else if (is_char(c,e.m_mask))
+                o<<c;
+            else
+                o<<'%'<<"0123456789ABCDEF"[((uint8_t)c)>>4]<<"0123456789ABCDEF"[((uint8_t)c)&0xF];
+        return o;
+    }
+};
+
+
+class encode_query_val {
+    public:
+        encode_query_val(const std::string& s, std::uint8_t mask) : m_s(s), m_mask(mask) {}
+    private:
+        const std::string& m_s;
+        std::uint8_t m_mask;
+    friend std::ostream& operator<< (std::ostream& o, const encode_query_val& e) {
+        for (const char c:e.m_s)
+            if (c==' ')
+                o<<'+';
+            else if (c=='+')
+                o<<"%2B";
+            else if (c=='&')
+                o<<"%26";
+            else if (c==';')
+                o<<"%3B";
+            else if (is_char(c,e.m_mask))
+                o<<c;
+            else
+                o<<'%'<<"0123456789ABCDEF"[((uint8_t)c)>>4]<<"0123456789ABCDEF"[((uint8_t)c)&0xF];
+        return o;
+    }
+};
+
+
+
+} // end of anonymous namnespace
+// ---------------------------------------------------------------------
+
+
+// Copy assignment
+void Url::assign(const Url &url) {
+    m_parse=url.m_parse;
+    m_built=url.m_built;
+    if (m_parse) {
+        m_scheme=url.m_scheme;
+        m_user=url.m_user;
+        m_host=url.m_host;
+        m_ip_v=url.m_ip_v;
+        m_port=url.m_port;
+        m_path=url.m_path;
+        m_query=url.m_query;
+        m_fragment=url.m_fragment;
+    }
+    if (!m_parse || m_built)
+        m_url=url.m_url;
+}
+
+
+// Move assignment
+void Url::assign(Url&& url) {
+    m_parse=url.m_parse;
+    m_built=url.m_built;
+    if (m_parse) {
+        m_scheme=std::move(url.m_scheme);
+        m_user=std::move(url.m_user);
+        m_host=std::move(url.m_host);
+        m_ip_v=std::move(url.m_ip_v);
+        m_port=std::move(url.m_port);
+        m_path=std::move(url.m_path);
+        m_query=std::move(url.m_query);
+        m_fragment=std::move(url.m_fragment);
+    }
+    if (!m_parse || m_built)
+        m_url=std::move(url.m_url);
+}
+
+
+Url &Url::scheme(const std::string& s) {
+    if (!is_scheme(s))
+        throw Url::parse_error("Invalid scheme '"+s+"'");
+    lazy_parse();
+    std::string o(s);
+    to_lower(o);
+    if (o!=m_scheme) {
+        m_scheme=o;
+        m_built=false;
+        if ((m_scheme=="http" && m_port=="80") || (m_scheme=="https" && m_port=="443"))
+            m_port="";
+    }
+    return *this;
+}
+
+
+Url &Url::user_info(const std::string& s) {
+    if (s.length()>256)
+        throw Url::parse_error("User info is longer than 256 characters '"+s+"'");
+    lazy_parse();
+    if (m_user!=s) {
+        m_user=s;
+        m_built=false;
+    }
+    return *this;
+}
+
+
+Url &Url::host(const std::string& h, std::uint8_t ip_v) {
+    if (h.length()>253)
+        throw Url::parse_error("Host is longer than 253 characters '"+h+"'");
+    lazy_parse();
+    std::string o;
+    if (h.empty())
+        ip_v=-1;
+    else if (is_ipv4(h)) {
+        if (!is_valid_ipv4(h))
+            throw Url::parse_error("Invalid IPv4 address '"+h+"'");
+        ip_v=4;
+        o=h;
+    } else if(ip_v!=0&&ip_v!=4&&ip_v!=6) {
+        if (!is_ipv6(h)) {
+            throw Url::parse_error("Invalid IPvFuture address '"+h+"'");
+        }
+        o=h;
+    } else if (is_ipv6(h)) {
+        if (!is_valid_ipv6(h))
+            throw Url::parse_error("Invalid IPv6 address '"+h+"'");
+        ip_v=6;
+        o=normalize_IPv6(h);
+    } else if (is_reg_name(h)) {
+        ip_v=0;
+        o=normalize_reg_name(h);
+    } else
+        throw Url::parse_error("Invalid host '"+h+"'");
+    if (m_host!=o||m_ip_v!=ip_v) {
+        m_host=o;
+        m_ip_v=ip_v;
+        m_built=false;
+    }
+    return *this;
+}
+
+
+Url &Url::port(const std::string& p) {
+    if (!is_port(p))
+        throw Url::parse_error("Invalid port '"+p+"'");
+    lazy_parse();
+    std::string o(p);
+    if ((m_scheme=="http" && o=="80") || (m_scheme=="https" && o=="443"))
+        o="";
+    if (m_port!=o) {
+        m_port=o;
+        m_built=false;
+    }
+    return *this;
+}
+
+
+Url &Url::path(const std::string& p) {
+    if (p.length()>8000)
+        throw Url::parse_error("Path is longer than 8000 characters '"+p+"'");
+    lazy_parse();
+    std::string o(normalize_path(p));
+    if (m_path!=o) {
+        m_path=o;
+        m_built=false;
+    }
+    return *this;
+}
+
+
+Url &Url::fragment(const std::string& f) {
+    if (f.length()>256)
+        throw Url::parse_error("Fragment is longer than 256 characters '"+f+"'");
+    lazy_parse();
+    if (m_fragment!=f) {
+        m_fragment=f;
+        m_built=false;
+    }
+    return *this;
+}
+
+
+Url &Url::clear() {
+    m_url.clear();
+    m_scheme.clear();
+    m_user.clear();
+    m_host.clear();
+    m_port.clear();
+    m_path.clear();
+    m_query.clear();
+    m_fragment.clear();
+    m_ip_v=-1;
+    m_built=true;
+    m_parse=true;
+    return *this;
+}
+
+
+void Url::parse_url() const {
+    if (m_url.empty()) {
+        const_cast<Url*>(this)->clear();
+        m_parse=m_built=true;
+        return;
+    }
+    if (m_url.length()>8000)
+        throw Url::parse_error("URI is longer than 8000 characters");
+
+    const char *s=m_url.data(), *e=s+m_url.length();
+    std::int8_t ip_v=-1;
+    const char *scheme_b, *scheme_e, *user_b, *user_e, *host_b, *host_e,
+            *port_b, *port_e, *path_b, *path_e, *query_b, *query_e,
+            *fragment_b, *fragment_e;
+    scheme_b=scheme_e=user_b=user_e=host_b=host_e=port_b=port_e=path_b=
+            path_e=query_b=query_e=fragment_b=fragment_e=nullptr;
+
+    const char *b=s, *p=find_first_of(b, e, ":/?#");
+    if (p==e) {
+        if (!is_chars(b, p, 0x2F))
+            throw Url::parse_error("Path '"+std::string(b,p)+"' in '"+std::string(s,e-s)+"' is invalid");
+        path_b=b;
+        path_e=e;
+    } else {
+        // get schema if any
+        if (*p==':') {
+            if (!is_scheme(b, p))
+                throw Url::parse_error("Scheme in '"+std::string(s,e-s)+"' is invalid");
+            scheme_b=b;
+            scheme_e=p;
+            p=find_first_of(b=p+1, e, "/?#");
+        }
+        // get authority if any
+        if (p!=e && *p=='/' && (e-b)>1 && b[0]=='/' && b[1]=='/') {
+            const char *ea=find_first_of(b+=2, e, "/?#"); // locate end of authority
+            p=find_char(b, ea, '@');
+            // get user info if any
+            if (p!=ea) {
+                if (!is_chars(b, p, 0x05))
+                    throw Url::parse_error("User info in '"+std::string(s,e-s)+"' is invalid");
+                user_b=b;
+                user_e=p;
+                b=p+1;
+            }
+            // Get IP literal if any
+            if (*b=='[') {
+                // locate end of IP literal
+                p=find_char(++b, ea, ']');
+                if (*p!=']')
+                    throw Url::parse_error("Missing ] in '"+std::string(s,e-s)+"'");
+                // decode IPvFuture protocol version
+                if (*b=='v') {
+                    if (is_hexdigit(*++b)) {
+                        ip_v=get_hex_digit(*b);
+                        if (is_hexdigit(*++b)) {
+                            ip_v=(ip_v<<8)|get_hex_digit(*b);
+                        }
+                    }
+                    if (ip_v==-1||*b++!='.'||!is_chars(b,p,0x05))
+                        throw Url::parse_error("Host address in '"+std::string(s,e-s)+"' is invalid");
+                } else if (is_ipv6(b,p)) {
+                    ip_v=6;
+                } else
+                    throw Url::parse_error("Host address in '"+std::string(s,e-s)+"' is invalid");
+                host_b=b;
+                host_e=p;
+                b=p+1;
+            } else {
+                p=find_char(b, ea, ':');
+                if (is_ipv4(b, p))
+                    ip_v=4;
+                else if (is_reg_name(b, p))
+                    ip_v=0;
+                else
+                    throw Url::parse_error("Host address in '"+std::string(s,e-s)+"' is invalid");
+                host_b=b;
+                host_e=p;
+                b=p;
+            }
+            //get port if any
+            if (b!=ea&&*b==':') {
+                if (!is_port(++b, ea))
+                    throw Url::parse_error("Port '"+std::string(b,ea-b)+"' in '"+std::string(s,e-s)+"' is invalid");
+                port_b=b;
+                port_e=ea;
+            }
+            b=ea;
+        }
+        p=find_first_of(b,e,"?#");
+        if (!is_chars(b, p, 0x2F))
+            throw Url::parse_error("Path '"+std::string(b,p)+"' in '"+std::string(s,e-s)+"' is invalid");
+        path_b=b;
+        path_e=p;
+        if (p!=e && *p=='?') {
+            p=find_char(b=p+1,e,'#');
+            query_b=b;
+            query_e=p;
+        }
+        if (p!=e && *p=='#') {
+            if (!is_chars(p+1, e, 0x3F))
+                throw Url::parse_error("Fragment '"+std::string(p+1,e)+"' in '"+std::string(s,e-s)+"' is invalid");
+            fragment_b=p+1;
+            fragment_e=e;
+        }
+    }
+    std::string _scheme, _user, _host, _port, _path, _query, _fragment;
+    Query query_v;
+
+    if (scheme_b)
+        _scheme=normalize_scheme(scheme_b, scheme_e);
+    if (user_b)
+        _user=decode(user_b, user_e);
+    if (host_b) {
+        _host=decode(host_b, host_e);
+        if (ip_v==0)
+            _host=normalize_reg_name(_host);
+        else if (ip_v==6)
+            _host=normalize_IPv6(_host);
+    }
+    if (port_b)
+        _port=std::string(port_b,port_e-port_b);
+    if (path_b)
+        _path=normalize_path(decode(path_b, path_e));
+    if (query_b) {
+        _query=std::string(query_b, query_e);
+        p=b=query_b;
+        while (p!=query_e) {
+            p=find_first_of(b, query_e, "=;&");
+            if (!is_chars(b, p, 0x3F))
+                throw Url::parse_error("Query key '"+std::string(b,p)+"' in '"+std::string(s,e-s)+"' is invalid");
+            std::string key(decode_plus(b,p)), val;
+            if (p!=query_e) {
+                if (*p=='=') {
+                    p=find_first_of(b=p+1, query_e, ";&");
+                    if (!is_chars(b, p, 0x3F))
+                        throw Url::parse_error("Query value '"+std::string(b,p)+"' in '"+std::string(s,e-s)+"' is invalid");
+                    val=decode_plus(b,p);
+                }
+                b=p+1;
+            }
+            query_v.emplace_back(key, val);
+        }
+    }
+    if (fragment_b)
+        _fragment=decode(fragment_b, fragment_e);
+
+    m_scheme=_scheme;
+    m_user=_user;
+    m_host=_host;
+    m_ip_v=ip_v;
+    m_port=_port;
+    m_path=_path;
+    m_query=query_v;
+    m_fragment=_fragment;
+    m_parse=true;
+    m_built=false;
+}
+
+
+void Url::build_url() const {
+    lazy_parse();
+    std::stringstream url;
+    if (!m_scheme.empty())
+        url<<m_scheme<<":";
+    if (!m_host.empty()) {
+        url<<"//";
+        if (!m_user.empty())
+            url<<encode(m_user, 0x05)<<'@';
+        if (m_ip_v==0||m_ip_v==4)
+            url<<m_host;
+        else if (m_ip_v==6)
+            url<<"["<<m_host<<"]";
+        else
+            url<<"[v"<<std::hex<<(int)m_ip_v<<std::dec<<'.'<<m_host<<"]";
+        if (!m_port.empty())
+            if (!((m_scheme=="http"&&m_port=="80")||(m_scheme=="https"&&m_port=="443")))
+                url<<":"<<m_port;
+    } else {
+        if (!m_user.empty())
+            throw Url::build_error("User info defined, but host is empty");
+        if (!m_port.empty())
+            throw Url::build_error("Port defined, but host is empty");
+        if (!m_path.empty()) {
+            const char *b=m_path.data(), *e=b+m_path.length(), *p=find_first_of(b,e,":/");
+            if (p!=e && *p==':')
+                throw Url::build_error("The first segment of the relative path can't contain ':'");
+        }
+    }
+    if (!m_path.empty()) {
+        if (m_path[0]!='/'&&!m_host.empty())
+            throw Url::build_error("Path must start with '/' when host is not empty");
+        url<<encode(m_path, 0x0F);
+    }
+    if (!m_query.empty()) {
+        url<<"?";
+        auto it = m_query.begin(), end = m_query.end();
+        if (it->key().empty())
+            throw Url::build_error("First query entry has no key");
+        url<<encode_query_key(it->key(), 0x1F);
+        if (!it->val().empty())
+            url<<"="<<encode_query_val(it->val(), 0x1F);
+        while(++it!=end) {
+            if (it->key().empty())
+                throw Url::build_error("A query entry has no key");
+            url<<"&"<<encode_query_key(it->key(), 0x1F);
+            if (!it->val().empty())
+                url<<"="<<encode_query_val(it->val(), 0x1F);
+        }
+    }
+    if (!m_fragment.empty())
+        url<<"#"<<encode(m_fragment, 0x1F);
+    m_built=false;
+    m_url=url.str();
+}
+
+
+// Output
+std::ostream& Url::output(std::ostream &o) const {
+    lazy_parse();
+    if(!m_built) build_url();
+    o<<"Url:{url("<<m_url<<")";
+    if (!m_scheme.empty()) o << " scheme("<<m_scheme<<")";
+    if (!m_user.empty()) o << " user_info("<<m_user<<")";
+    if (m_ip_v!=-1) o << " host("<<m_host<<") IPv("<<(int)m_ip_v<<")";
+    if (!m_port.empty()) o << " port("<<m_port<<")";
+    if (!m_path.empty()) o << " path("<<m_path<<")";
+    if (!m_query.empty()) {
+        std::stringstream str;
+        str<<" query(";
+        for (const auto& q:m_query)
+            str<<q;
+        std::string s(str.str());
+        o<<s.substr(0,s.length()-1)<<")";
+    }
+    if (!m_fragment.empty()) o << "fragment("<<m_fragment<<") ";
+    o<<"}";
+    return o;
+}
+
diff --git a/src/third_party/url.hpp b/src/third_party/url.hpp
new file mode 100644 (file)
index 0000000..07124ca
--- /dev/null
@@ -0,0 +1,208 @@
+#ifndef URL_HPP
+#define URL_HPP
+
+#include <vector>
+#include <utility>
+#include <stdexcept>
+#include <cstdint>
+#include <ostream>
+#include <utility>
+#include <string>
+
+class Url {
+public:
+    // Exception thut may be thrown when decoding an URL or an assigning value
+    class parse_error: public std::invalid_argument {
+    public:
+        parse_error(const std::string &reason) : std::invalid_argument(reason) {}
+    };
+
+    // Exception that may be thrown when building an URL
+    class build_error: public std::runtime_error {
+    public:
+        build_error(const std::string &reason) : std::runtime_error(reason) {}
+    };
+
+    // Default constructor
+    Url() : m_parse(true),m_built(true),m_ip_v(-1) {}
+
+    // Copy initializer constructor
+    Url(const Url &url) : m_ip_v(-1) {assign(url);}
+
+    // Move constructor
+    Url(Url&& url) : m_ip_v(-1) {assign(std::move(url));}
+
+    // Construct Url with the given string
+    Url(const std::string &url_str) : m_url(url_str),m_parse(false),m_built(false),m_ip_v(-1) {}
+
+    // Assign the given URL string
+    Url &operator=(const std::string &url_str) {return str(url_str);}
+
+    // Assign the given Url object
+    Url &operator=(const Url &url) {assign(url); return *this;}
+
+    // Move the given Url object
+    Url &operator=(Url&& url) {assign(std::move(url)); return *this;}
+
+    // Clear the Url object
+    Url &clear();
+
+    // Build Url if needed and return it as string
+    std::string str() const {if(!m_built) build_url(); return m_url;}
+
+    // Set the Url to the given string. All fields are overwritten
+    Url& str(const std::string &url_str) {m_url=url_str; m_built=m_parse=false; return *this;}
+
+    // Get scheme
+    const std::string& scheme() const {lazy_parse(); return m_scheme;}
+
+    // Set scheme
+    Url &scheme(const std::string& s);
+
+    // Get user info
+    const std::string& user_info() const {lazy_parse(); return m_user;}
+
+    // Set user info
+    Url &user_info(const std::string& s);
+
+    // Get host
+    const std::string& host() const {lazy_parse(); return m_host;}
+
+    // Set host
+    Url &host(const std::string& h, uint8_t ip_v=0);
+
+    // Get host IP version: 0=name, 4=IPv4, 6=IPv6, -1=undefined
+    std::int8_t ip_version() const {lazy_parse(); return m_ip_v;}
+
+    // Get port
+    const std::string& port() const {lazy_parse(); return m_port;}
+
+    // Set Port given as string
+    Url &port(const std::string& str);
+
+    // Set port given as a 16bit unsigned integer
+    Url &port(std::uint16_t num) {return port(std::to_string(num));}
+
+    // Get path
+    const std::string& path() const {lazy_parse(); return m_path;}
+
+    // Set path
+    Url &path(const std::string& str);
+
+    class KeyVal {
+    public:
+        // Default constructor
+        KeyVal() {}
+
+        // Construct with provided Key and Value strings
+        KeyVal(const std::string &key, const std::string &val) : m_key(key),m_val(val) {}
+
+        // Construct with provided Key string, val will be empty
+        KeyVal(const std::string &key) : m_key(key) {}
+
+        // Equality test operator
+        bool operator==(const KeyVal &q) const {return m_key==q.m_key&&m_val==q.m_val;}
+
+        // Swap this with q
+        void swap(KeyVal& q) {std::swap(m_key,q.m_key); std::swap(m_val,q.m_val);}
+
+        // Get key
+        const std::string& key() const {return m_key;}
+
+        // Set key
+        void key(const std::string &k) {m_key=k;}
+
+        // Get value
+        const std::string& val() const {return m_val;}
+
+        // Set value
+        void val(const std::string &v) {m_val=v;}
+
+        // Output key value pair
+        friend std::ostream& operator<<(std::ostream &o, const KeyVal &kv)
+            {o<<"<key("<<kv.m_key<<") val("<<kv.m_val<<")> "; return o;}
+
+    private:
+        std::string m_key;
+        std::string m_val;
+    };
+
+    // Define Query as vector of Key Value pairs
+    typedef std::vector<KeyVal> Query;
+
+    // Get a reference to the query vector for read only access
+    const Query& query() const {lazy_parse(); return m_query;}
+
+    // Get a reference to a specific Key Value pair in the query vector for read only access
+    const KeyVal& query(size_t i) const {
+        lazy_parse();
+        if (i>=m_query.size())
+            throw std::out_of_range("Invalid Url query index ("+std::to_string(i)+")");
+        return m_query[i];
+    }
+
+    // Get a reference to the query vector for a writable access
+    Query& set_query() {lazy_parse(); m_built=false; return m_query;}
+
+    // Get a reference to specific Key Value pair in the query vector for a writable access
+    KeyVal& set_query(size_t i) {
+        lazy_parse();
+        if (i>=m_query.size())
+            throw std::out_of_range("Invalid Url query index ("+std::to_string(i)+")");
+        m_built=false;
+        return m_query[i];
+    }
+
+    // Set the query vector to the Query vector q
+    Url &set_query(const Query &q)
+        {lazy_parse(); if (q != m_query) {m_query=q; m_built=false;} return *this;}
+
+    // Append KeyVal kv to the query
+    Url &add_query(const KeyVal &kv)
+        {lazy_parse(); m_built=false; m_query.push_back(kv); return *this;}
+
+    // Append key val pair to the query
+    Url &add_query(const std::string &key, const std::string &val)
+        {lazy_parse(); m_built=false; m_query.emplace_back(key,val); return *this;}
+
+    // Append key with empty val to the query
+    Url &add_query(const std::string &key)
+        {lazy_parse(); m_built=false; m_query.emplace_back(key); return *this;}
+
+    // Get the fragment
+    const std::string& fragment() const {lazy_parse(); return m_fragment;}
+
+    // Set the fragment
+    Url &fragment(const std::string& f);
+
+    // Output
+    std::ostream& output(std::ostream &o) const;
+
+    // Output strean operator
+    friend std::ostream& operator<<(std::ostream &o, const Url &u) {return u.output(o);}
+
+private:
+    void assign(const Url &url);
+    void assign(Url&& url);
+    void build_url() const;
+    void lazy_parse() const {if (!m_parse) parse_url();}
+    void parse_url() const;
+
+    mutable std::string m_scheme;
+    mutable std::string m_user;
+    mutable std::string m_host;
+    mutable std::string m_port;
+    mutable std::string m_path;
+    mutable Query m_query;
+    mutable std::string m_fragment;
+    mutable std::string m_url;
+    mutable bool m_parse;
+    mutable bool m_built;
+    mutable std::int8_t m_ip_v;
+};
+
+
+
+
+#endif // URL_HPP
+
index 8963b89061e7ca3e4212bfbfe30b2acfa098420a..d0bea77652081a29a14fbcc4e03f240133627115 100644 (file)
@@ -45,7 +45,6 @@
 #include <direct.h>
 #include <io.h>
 
-#define WIN32_LEAN_AND_MEAN
 #define NOMINMAX 1
 #define WIN32_NO_STATUS
 #include <windows.h>
diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt
new file mode 100644 (file)
index 0000000..0685ad9
--- /dev/null
@@ -0,0 +1,10 @@
+set(
+  sources
+  ${CMAKE_CURRENT_SOURCE_DIR}/TextTable.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/Tokenizer.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/file.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/path.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/string.cpp
+)
+
+target_sources(ccache_framework PRIVATE ${sources})
diff --git a/src/util/TextTable.cpp b/src/util/TextTable.cpp
new file mode 100644 (file)
index 0000000..618f7b7
--- /dev/null
@@ -0,0 +1,153 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "TextTable.hpp"
+
+#include <assertions.hpp>
+
+#include <third_party/fmt/core.h>
+
+#include <algorithm>
+
+namespace util {
+
+void
+TextTable::add_heading(const std::string& text)
+{
+  Cell cell(text);
+  cell.m_heading = true;
+  m_rows.push_back({cell});
+}
+
+void
+TextTable::add_row(const std::vector<Cell>& cells)
+{
+  m_rows.emplace_back();
+  for (const auto& cell : cells) {
+    for (size_t i = 0; i < cell.m_colspan - 1; ++i) {
+      Cell dummy("");
+      dummy.m_colspan = 0;
+      m_rows.back().push_back(dummy);
+    }
+    m_rows.back().push_back(cell);
+
+    m_columns = std::max(m_columns, m_rows.back().size());
+  }
+}
+
+std::vector<size_t>
+TextTable::compute_column_widths() const
+{
+  std::vector<size_t> result(m_columns, 0);
+
+  for (size_t column_index = 0; column_index < m_columns; ++column_index) {
+    for (const auto& row : m_rows) {
+      if (column_index >= row.size()) {
+        continue;
+      }
+      const auto& cell = row[column_index];
+      if (cell.m_heading || cell.m_colspan == 0) {
+        continue;
+      }
+
+      size_t width_of_left_cols_in_span = 0;
+      for (size_t i = 0; i < cell.m_colspan - 1; ++i) {
+        width_of_left_cols_in_span += 1 + result[column_index - i - 1];
+      }
+      result[column_index] = std::max(
+        result[column_index],
+        cell.m_text.length()
+          - std::min(width_of_left_cols_in_span, cell.m_text.length()));
+    }
+  }
+
+  return result;
+}
+
+std::string
+TextTable::render() const
+{
+  auto column_widths = compute_column_widths();
+
+  std::string result;
+  for (const auto& row : m_rows) {
+    std::string r;
+    bool first = true;
+    for (size_t i = 0; i < row.size(); ++i) {
+      const auto& cell = row[i];
+      if (cell.m_colspan == 0) {
+        continue;
+      }
+      if (first) {
+        first = false;
+      } else {
+        r += ' ';
+      }
+
+      size_t width = 0;
+      for (size_t j = i + 1 - cell.m_colspan; j <= i; ++j) {
+        width += column_widths[j] + (j == i ? 0 : 1);
+      }
+      r += fmt::format(
+        (cell.m_right_align ? "{:>{}}" : "{:<{}}"), cell.m_text, width);
+    }
+    result.append(r, 0, r.find_last_not_of(' ') + 1);
+    result += '\n';
+  }
+  return result;
+}
+
+TextTable::Cell::Cell(const std::string& text)
+  : m_text(text),
+    m_right_align(false)
+{
+}
+
+TextTable::Cell::Cell(const char* text) : Cell(std::string(text))
+{
+}
+
+TextTable::Cell::Cell(const uint64_t number)
+  : m_text(fmt::format("{}", number)),
+    m_right_align(true)
+{
+}
+
+TextTable::Cell&
+TextTable::Cell::left_align()
+{
+  m_right_align = false;
+  return *this;
+}
+
+TextTable::Cell&
+TextTable::Cell::colspan(const size_t columns)
+{
+  ASSERT(columns >= 1);
+  m_colspan = columns;
+  return *this;
+}
+
+TextTable::Cell&
+TextTable::Cell::right_align()
+{
+  m_right_align = true;
+  return *this;
+}
+
+} // namespace util
diff --git a/src/util/TextTable.hpp b/src/util/TextTable.hpp
new file mode 100644 (file)
index 0000000..05c0e0e
--- /dev/null
@@ -0,0 +1,60 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+namespace util {
+
+class TextTable
+{
+public:
+  class Cell
+  {
+  public:
+    Cell(const std::string& text);
+    Cell(const char* text);
+    Cell(uint64_t number);
+
+    Cell& colspan(size_t columns);
+    Cell& left_align();
+    Cell& right_align();
+
+  private:
+    friend TextTable;
+
+    const std::string m_text;
+    bool m_right_align = false;
+    bool m_heading = false;
+    size_t m_colspan = 1;
+  };
+
+  void add_heading(const std::string& text);
+  void add_row(const std::vector<Cell>& cells);
+  std::string render() const;
+
+private:
+  std::vector<std::vector<Cell>> m_rows;
+  size_t m_columns = 0;
+
+  std::vector<size_t> compute_column_widths() const;
+};
+
+} // namespace util
diff --git a/src/util/Timer.hpp b/src/util/Timer.hpp
new file mode 100644 (file)
index 0000000..afbf7ab
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <chrono>
+#include <string>
+
+class Timer
+{
+public:
+  Timer();
+
+  double measure_s() const;
+  double measure_ms() const;
+
+private:
+  std::chrono::steady_clock::time_point m_start;
+};
+
+inline Timer::Timer() : m_start(std::chrono::steady_clock::now())
+{
+}
+
+inline double
+Timer::measure_s() const
+{
+  using namespace std::chrono;
+  return duration_cast<duration<double>>(steady_clock::now() - m_start).count();
+}
+
+inline double
+Timer::measure_ms() const
+{
+  return measure_s() * 1000;
+}
diff --git a/src/util/Tokenizer.cpp b/src/util/Tokenizer.cpp
new file mode 100644 (file)
index 0000000..9b27c8f
--- /dev/null
@@ -0,0 +1,53 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "Tokenizer.hpp"
+
+namespace util {
+
+void
+Tokenizer::Iterator::advance(bool initial)
+{
+  constexpr auto npos = nonstd::string_view::npos;
+  const auto string = m_tokenizer.m_string;
+  const auto delimiters = m_tokenizer.m_delimiters;
+  const auto mode = m_tokenizer.m_mode;
+
+  DEBUG_ASSERT(m_left <= m_right);
+  DEBUG_ASSERT(m_right <= string.length());
+
+  do {
+    if (initial) {
+      initial = false;
+    } else if (m_right == string.length()) {
+      m_left = npos;
+    } else {
+      m_left = m_right + 1;
+    }
+    if (m_left != npos) {
+      const auto delim_pos = string.find_first_of(delimiters, m_left);
+      m_right = delim_pos == npos ? string.length() : delim_pos;
+    }
+  } while (mode == Mode::skip_empty && m_left == m_right);
+
+  if (mode == Mode::skip_last_empty && m_left == string.length()) {
+    m_left = npos;
+  }
+}
+
+} // namespace util
diff --git a/src/util/Tokenizer.hpp b/src/util/Tokenizer.hpp
new file mode 100644 (file)
index 0000000..90cb0c0
--- /dev/null
@@ -0,0 +1,130 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <assertions.hpp>
+
+#include <third_party/fmt/core.h>
+#include <third_party/nonstd/optional.hpp>
+#include <third_party/nonstd/string_view.hpp>
+
+namespace util {
+
+// An instance of this class can be used in a range-based for loop to split a
+// string into tokens at any of the characters in a string of delimiters.
+class Tokenizer
+{
+public:
+  enum class Mode {
+    include_empty,   // Include empty tokens.
+    skip_empty,      // Skip empty tokens.
+    skip_last_empty, // Include empty tokens except the last one.
+  };
+
+  // Split `string` into tokens at any of the characters in `separators` which
+  // must neither be the empty string nor a nullptr.
+  Tokenizer(nonstd::string_view string,
+            const char* delimiters,
+            Mode mode = Mode::skip_empty);
+
+  class Iterator
+  {
+  public:
+    Iterator(const Tokenizer& tokenizer, size_t start_pos);
+
+    Iterator operator++();
+    bool operator!=(const Iterator& other) const;
+    nonstd::string_view operator*() const;
+
+  private:
+    const Tokenizer& m_tokenizer;
+    size_t m_left;
+    size_t m_right;
+
+    void advance(bool initial);
+  };
+
+  Iterator begin();
+  Iterator end();
+
+private:
+  friend Iterator;
+
+  const nonstd::string_view m_string;
+  const char* const m_delimiters;
+  const Mode m_mode;
+};
+
+inline Tokenizer::Tokenizer(const nonstd::string_view string,
+                            const char* const delimiters,
+                            const Tokenizer::Mode mode)
+  : m_string(string),
+    m_delimiters(delimiters),
+    m_mode(mode)
+{
+  DEBUG_ASSERT(delimiters != nullptr && delimiters[0] != '\0');
+}
+
+inline Tokenizer::Iterator::Iterator(const Tokenizer& tokenizer,
+                                     const size_t start_pos)
+  : m_tokenizer(tokenizer),
+    m_left(start_pos),
+    m_right(start_pos)
+{
+  if (start_pos == 0) {
+    advance(true);
+  } else {
+    DEBUG_ASSERT(start_pos == nonstd::string_view::npos);
+  }
+}
+
+inline Tokenizer::Iterator
+Tokenizer::Iterator::operator++()
+{
+  advance(false);
+  return *this;
+}
+
+inline bool
+Tokenizer::Iterator::operator!=(const Iterator& other) const
+{
+  return &m_tokenizer != &other.m_tokenizer || m_left != other.m_left;
+}
+
+inline nonstd::string_view
+Tokenizer::Iterator::operator*() const
+{
+  DEBUG_ASSERT(m_left <= m_right);
+  DEBUG_ASSERT(m_right <= m_tokenizer.m_string.length());
+  return m_tokenizer.m_string.substr(m_left, m_right - m_left);
+}
+
+inline Tokenizer::Iterator
+Tokenizer::begin()
+{
+  return Iterator(*this, 0);
+}
+
+inline Tokenizer::Iterator
+Tokenizer::end()
+{
+  return Iterator(*this, nonstd::string_view::npos);
+}
+
+} // namespace util
diff --git a/src/util/expected.hpp b/src/util/expected.hpp
new file mode 100644 (file)
index 0000000..95926f9
--- /dev/null
@@ -0,0 +1,66 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <utility>
+
+namespace util {
+
+// --- Interface ---
+
+// Return value of `value` (where `T` typically is `nonstd::expected`) or throw
+// an exception of type `E` with a `T::error_type` as the argument.
+template<typename E, typename T>
+typename T::value_type value_or_throw(const T& value);
+template<typename E, typename T>
+typename T::value_type value_or_throw(T&& value);
+
+#define TRY(x_)                                                                \
+  do {                                                                         \
+    const auto result = x_;                                                    \
+    if (!result) {                                                             \
+      return nonstd::make_unexpected(result.error());                          \
+    }                                                                          \
+  } while (false)
+
+// --- Inline implementations ---
+
+template<typename E, typename T>
+inline typename T::value_type
+value_or_throw(const T& value)
+{
+  if (value) {
+    return *value;
+  } else {
+    throw E(value.error());
+  }
+}
+
+template<typename E, typename T>
+inline typename T::value_type
+value_or_throw(T&& value)
+{
+  if (value) {
+    return std::move(*value);
+  } else {
+    throw E(value.error());
+  }
+}
+
+} // namespace util
diff --git a/src/util/file.cpp b/src/util/file.cpp
new file mode 100644 (file)
index 0000000..85c62c1
--- /dev/null
@@ -0,0 +1,49 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "file.hpp"
+
+#include <Logging.hpp>
+#include <Util.hpp>
+#include <core/exceptions.hpp>
+#include <fmtmacros.hpp>
+
+namespace util {
+
+void
+create_cachedir_tag(const std::string& dir)
+{
+  constexpr char cachedir_tag[] =
+    "Signature: 8a477f597d28d172789f06886806bc55\n"
+    "# This file is a cache directory tag created by ccache.\n"
+    "# For information about cache directory tags, see:\n"
+    "#\thttp://www.brynosaurus.com/cachedir/\n";
+
+  const std::string path = FMT("{}/CACHEDIR.TAG", dir);
+  const auto stat = Stat::stat(path);
+  if (stat) {
+    return;
+  }
+  try {
+    Util::write_file(path, cachedir_tag);
+  } catch (const core::Error& e) {
+    LOG("Failed to create {}: {}", path, e.what());
+  }
+}
+
+} // namespace util
diff --git a/src/util/file.hpp b/src/util/file.hpp
new file mode 100644 (file)
index 0000000..e0af9dd
--- /dev/null
@@ -0,0 +1,29 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <string>
+
+namespace util {
+
+// --- Interface ---
+
+void create_cachedir_tag(const std::string& dir);
+
+} // namespace util
diff --git a/src/util/path.cpp b/src/util/path.cpp
new file mode 100644 (file)
index 0000000..0648c20
--- /dev/null
@@ -0,0 +1,61 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "path.hpp"
+
+#include <Util.hpp>
+#include <fmtmacros.hpp>
+
+#ifdef _WIN32
+const char k_path_delimiter[] = ";";
+#else
+const char k_path_delimiter[] = ":";
+#endif
+
+namespace util {
+
+bool
+is_absolute_path(nonstd::string_view path)
+{
+#ifdef _WIN32
+  if (path.length() >= 2 && path[1] == ':'
+      && (path[2] == '/' || path[2] == '\\')) {
+    return true;
+  }
+#endif
+  return !path.empty() && path[0] == '/';
+}
+
+std::vector<std::string>
+split_path_list(nonstd::string_view path_list)
+{
+  return Util::split_into_strings(path_list, k_path_delimiter);
+}
+
+std::string
+to_absolute_path(nonstd::string_view path)
+{
+  if (util::is_absolute_path(path)) {
+    return std::string(path);
+  } else {
+    return Util::normalize_absolute_path(
+      FMT("{}/{}", Util::get_actual_cwd(), path));
+  }
+}
+
+} // namespace util
diff --git a/src/util/path.hpp b/src/util/path.hpp
new file mode 100644 (file)
index 0000000..3e26251
--- /dev/null
@@ -0,0 +1,56 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <third_party/nonstd/string_view.hpp>
+
+#include <string>
+#include <vector>
+
+namespace util {
+
+// --- Interface ---
+
+// Return whether `path` is absolute.
+bool is_absolute_path(nonstd::string_view path);
+
+// Return whether `path` includes at least one directory separator.
+bool is_full_path(nonstd::string_view path);
+
+// Split a list of paths (such as the content of $PATH on Unix platforms or
+// %PATH% on Windows platforms) into paths.
+std::vector<std::string> split_path_list(nonstd::string_view path_list);
+
+// Make `path` an absolute path.
+std::string to_absolute_path(nonstd::string_view path);
+
+// --- Inline implementations ---
+
+inline bool
+is_full_path(const nonstd::string_view path)
+{
+#ifdef _WIN32
+  if (path.find('\\') != nonstd::string_view::npos) {
+    return true;
+  }
+#endif
+  return path.find('/') != nonstd::string_view::npos;
+}
+
+} // namespace util
diff --git a/src/util/string.cpp b/src/util/string.cpp
new file mode 100644 (file)
index 0000000..38df7d9
--- /dev/null
@@ -0,0 +1,196 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "string.hpp"
+
+#include <fmtmacros.hpp>
+
+#include <cctype>
+#include <iostream>
+
+namespace util {
+
+nonstd::expected<double, std::string>
+parse_double(const std::string& value)
+{
+  size_t end;
+  double result;
+  bool failed = false;
+  try {
+    result = std::stod(value, &end);
+  } catch (const std::exception&) {
+    failed = true;
+  }
+
+  if (failed || end != value.size()) {
+    return nonstd::make_unexpected(
+      FMT("invalid floating point: \"{}\"", value));
+  } else {
+    return result;
+  }
+}
+
+nonstd::expected<int64_t, std::string>
+parse_signed(const std::string& value,
+             const nonstd::optional<int64_t> min_value,
+             const nonstd::optional<int64_t> max_value,
+             const nonstd::string_view description)
+{
+  const std::string stripped_value = strip_whitespace(value);
+
+  size_t end = 0;
+  long long result = 0;
+  bool failed = false;
+  try {
+    // Note: sizeof(long long) is guaranteed to be >= sizeof(int64_t)
+    result = std::stoll(stripped_value, &end, 10);
+  } catch (std::exception&) {
+    failed = true;
+  }
+  if (failed || end != stripped_value.size()) {
+    return nonstd::make_unexpected(
+      FMT("invalid integer: \"{}\"", stripped_value));
+  }
+
+  const int64_t min = min_value ? *min_value : INT64_MIN;
+  const int64_t max = max_value ? *max_value : INT64_MAX;
+  if (result < min || result > max) {
+    return nonstd::make_unexpected(
+      FMT("{} must be between {} and {}", description, min, max));
+  } else {
+    return result;
+  }
+}
+
+nonstd::expected<mode_t, std::string>
+parse_umask(const std::string& value)
+{
+  return util::parse_unsigned(value, 0, 0777, "umask", 8);
+}
+
+nonstd::expected<uint64_t, std::string>
+parse_unsigned(const std::string& value,
+               const nonstd::optional<uint64_t> min_value,
+               const nonstd::optional<uint64_t> max_value,
+               const nonstd::string_view description,
+               const int base)
+{
+  const std::string stripped_value = strip_whitespace(value);
+
+  size_t end = 0;
+  unsigned long long result = 0;
+  bool failed = false;
+  if (starts_with(stripped_value, "-")) {
+    failed = true;
+  } else {
+    try {
+      // Note: sizeof(unsigned long long) is guaranteed to be >=
+      // sizeof(uint64_t)
+      result = std::stoull(stripped_value, &end, base);
+    } catch (std::exception&) {
+      failed = true;
+    }
+  }
+  if (failed || end != stripped_value.size()) {
+    const auto base_info = base == 8 ? "octal " : "";
+    return nonstd::make_unexpected(
+      FMT("invalid unsigned {}integer: \"{}\"", base_info, stripped_value));
+  }
+
+  const uint64_t min = min_value ? *min_value : 0;
+  const uint64_t max = max_value ? *max_value : UINT64_MAX;
+  if (result < min || result > max) {
+    return nonstd::make_unexpected(
+      FMT("{} must be between {} and {}", description, min, max));
+  } else {
+    return result;
+  }
+}
+
+nonstd::expected<std::string, std::string>
+percent_decode(nonstd::string_view string)
+{
+  const auto from_hex = [](const char digit) {
+    return static_cast<uint8_t>(
+      std::isdigit(digit) ? digit - '0' : std::tolower(digit) - 'a' + 10);
+  };
+
+  std::string result;
+  result.reserve(string.size());
+  for (size_t i = 0; i < string.size(); ++i) {
+    if (string[i] != '%') {
+      result += string[i];
+    } else if (i + 2 >= string.size() || !std::isxdigit(string[i + 1])
+               || !std::isxdigit(string[i + 2])) {
+      return nonstd::make_unexpected(
+        FMT("invalid percent-encoded string at position {}: {}", i, string));
+    } else {
+      const char ch = static_cast<char>(from_hex(string[i + 1]) << 4
+                                        | from_hex(string[i + 2]));
+      result += ch;
+      i += 2;
+    }
+  }
+
+  return result;
+}
+
+std::string
+replace_first(const nonstd::string_view string,
+              const nonstd::string_view from,
+              const nonstd::string_view to)
+{
+  if (from.empty()) {
+    return std::string(string);
+  }
+
+  std::string result;
+  const auto pos = string.find(from);
+  if (pos != nonstd::string_view::npos) {
+    result.append(string.data(), pos);
+    result.append(to.data(), to.length());
+    result.append(string.data() + pos + from.size());
+  } else {
+    result = std::string(string);
+  }
+  return result;
+}
+
+std::pair<nonstd::string_view, nonstd::optional<nonstd::string_view>>
+split_once(const nonstd::string_view string, const char split_char)
+{
+  const size_t sep_pos = string.find(split_char);
+  if (sep_pos == nonstd::string_view::npos) {
+    return std::make_pair(string, nonstd::nullopt);
+  } else {
+    return std::make_pair(string.substr(0, sep_pos),
+                          string.substr(sep_pos + 1));
+  }
+}
+
+std::string
+strip_whitespace(const nonstd::string_view string)
+{
+  const auto is_space = [](const int ch) { return std::isspace(ch); };
+  const auto start = std::find_if_not(string.begin(), string.end(), is_space);
+  const auto end =
+    std::find_if_not(string.rbegin(), string.rend(), is_space).base();
+  return start < end ? std::string(start, end) : std::string();
+}
+
+} // namespace util
diff --git a/src/util/string.hpp b/src/util/string.hpp
new file mode 100644 (file)
index 0000000..62b0670
--- /dev/null
@@ -0,0 +1,162 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <third_party/nonstd/expected.hpp>
+#include <third_party/nonstd/optional.hpp>
+#include <third_party/nonstd/string_view.hpp>
+
+#include <sys/stat.h> // for mode_t
+
+#include <cstring>
+#include <string>
+#include <utility>
+
+namespace util {
+
+// --- Interface ---
+
+// Return true if `suffix` is a suffix of `string`.
+bool ends_with(nonstd::string_view string, nonstd::string_view suffix);
+
+// Join stringified elements of `container` delimited by `delimiter` into a
+// string. There must exist an `std::string to_string(T::value_type)` function.
+template<typename T>
+std::string join(const T& container, const nonstd::string_view delimiter);
+
+// Join stringified elements between input iterators `begin` and `end` delimited
+// by `delimiter` into a string. There must exist an `std::string
+// to_string(T::value_type)` function.
+template<typename T>
+std::string
+join(const T& begin, const T& end, const nonstd::string_view delimiter);
+
+// Parse a string into a double.
+//
+// Returns an error string if `value` cannot be parsed as a double.
+nonstd::expected<double, std::string> parse_double(const std::string& value);
+
+// Parse a string into a signed integer.
+//
+// Returns an error string if `value` cannot be parsed as an int64_t or if the
+// value falls out of the range [`min_value`, `max_value`]. `min_value` and
+// `max_value` default to min and max values of int64_t. `description` is
+// included in the error message for range violations.
+nonstd::expected<int64_t, std::string>
+parse_signed(const std::string& value,
+             nonstd::optional<int64_t> min_value = nonstd::nullopt,
+             nonstd::optional<int64_t> max_value = nonstd::nullopt,
+             nonstd::string_view description = "integer");
+
+// Parse `value` (an octal integer).
+nonstd::expected<mode_t, std::string> parse_umask(const std::string& value);
+
+// Parse a string into an unsigned integer.
+//
+// Returns an error string if `value` cannot be parsed as an uint64_t with base
+// `base`, or if the value falls out of the range [`min_value`, `max_value`].
+// `min_value` and `max_value` default to min and max values of uint64_t.
+// `description` is included in the error message for range violations.
+nonstd::expected<uint64_t, std::string>
+parse_unsigned(const std::string& value,
+               nonstd::optional<uint64_t> min_value = nonstd::nullopt,
+               nonstd::optional<uint64_t> max_value = nonstd::nullopt,
+               nonstd::string_view description = "integer",
+               int base = 10);
+
+// Percent-decode[1] `string`.
+//
+// [1]: https://en.wikipedia.org/wiki/Percent-encoding
+nonstd::expected<std::string, std::string>
+percent_decode(nonstd::string_view string);
+
+// Replace the first occurrence of `from` to `to` in `string`.
+std::string replace_first(nonstd::string_view string,
+                          nonstd::string_view from,
+                          nonstd::string_view to);
+
+// Split `string` into two parts using `split_char` as the delimiter. The second
+// part will be `nullopt` if there is no `split_char` in `string.`
+std::pair<nonstd::string_view, nonstd::optional<nonstd::string_view>>
+split_once(nonstd::string_view string, char split_char);
+
+// Return true if `prefix` is a prefix of `string`.
+bool starts_with(const char* string, nonstd::string_view prefix);
+
+// Return true if `prefix` is a prefix of `string`.
+bool starts_with(nonstd::string_view string, nonstd::string_view prefix);
+
+// Strip whitespace from left and right side of a string.
+[[nodiscard]] std::string strip_whitespace(nonstd::string_view string);
+
+// Convert `string` to a string. This function is used when joining
+// `std::string`s with `util::join`.
+std::string to_string(const std::string& string);
+
+// --- Inline implementations ---
+
+inline bool
+ends_with(const nonstd::string_view string, const nonstd::string_view suffix)
+{
+  return string.ends_with(suffix);
+}
+
+template<typename T>
+inline std::string
+join(const T& container, const nonstd::string_view delimiter)
+{
+  return join(container.begin(), container.end(), delimiter);
+}
+
+template<typename T>
+inline std::string
+join(const T& begin, const T& end, const nonstd::string_view delimiter)
+{
+  std::string result;
+  for (auto it = begin; it != end; ++it) {
+    if (it != begin) {
+      result.append(delimiter.data(), delimiter.length());
+    }
+    result += to_string(*it);
+  }
+  return result;
+}
+
+inline bool
+starts_with(const char* const string, const nonstd::string_view prefix)
+{
+  // Optimized version of starts_with(string_view, string_view): avoid computing
+  // the length of the string argument.
+  return std::strncmp(string, prefix.data(), prefix.length()) == 0;
+}
+
+inline bool
+starts_with(const nonstd::string_view string, const nonstd::string_view prefix)
+{
+  return string.starts_with(prefix);
+}
+
+// Convert `string` to `string`. This is used by util::join.
+inline std::string
+to_string(const std::string& string)
+{
+  return string;
+}
+
+} // namespace util
index ae0df8e8a6f5f34ee92b2b546d4c1ab3eea8a4e8..fa8feb3ff3806b7df69d439b3290617f4da0cf78 100644 (file)
@@ -7,21 +7,8 @@ function(addtest name)
   set_tests_properties(
     "test.${name}"
     PROPERTIES
-    ENVIRONMENT "CCACHE=${CMAKE_BINARY_DIR}/ccache;EXIT_IF_SKIPPED=true")
-
-  if(${CMAKE_VERSION} VERSION_LESS "3.9")
-    # Older CMake versions treat skipped tests as errors. Therefore, resort to
-    # parsing output for those cases (exit code is not considered). Skipped
-    # tests will appear as "Passed".
-    set_tests_properties(
-      "test.${name}"
-      PROPERTIES
-      PASS_REGULAR_EXPRESSION "PASSED|Passed|Skipped"
-      FAIL_REGULAR_EXPRESSION "[Ww]arning|[Ff]ail|[Er]rror")
-  else()
-    set_tests_properties("test.${name}" PROPERTIES SKIP_RETURN_CODE 125)
-  endif()
-
+    ENVIRONMENT "CCACHE=$<TARGET_FILE:ccache>;EXIT_IF_SKIPPED=true"
+    SKIP_RETURN_CODE 125)
 endfunction()
 
 if(${CMAKE_VERSION} VERSION_LESS "3.15")
@@ -65,7 +52,13 @@ addtest(profiling_hip_clang)
 addtest(readonly)
 addtest(readonly_direct)
 addtest(sanitize_blacklist)
+addtest(secondary_file)
+addtest(secondary_http)
+addtest(secondary_redis)
+addtest(secondary_url)
 addtest(serialize_diagnostics)
 addtest(source_date_epoch)
 addtest(split_dwarf)
+addtest(stats_log)
+addtest(trim_dir)
 addtest(upgrade)
diff --git a/test/http-client b/test/http-client
new file mode 100755 (executable)
index 0000000..d7a60c9
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+
+# This is a simple HTTP client to test readiness of the asynchronously
+# launched HTTP server.
+
+import sys
+import time
+import urllib.request
+
+
+def run(url, timeout, basic_auth):
+    deadline = time.time() + timeout
+    req = urllib.request.Request(url, method="HEAD")
+    if basic_auth:
+        import base64
+
+        encoded_credentials = base64.b64encode(
+            basic_auth.encode("ascii")
+        ).decode("ascii")
+        req.add_header("Authorization", f"Basic {encoded_credentials}")
+    while True:
+        try:
+            response = urllib.request.urlopen(req)
+            print(f"Connection successful (code: {response.getcode()})")
+            break
+        except urllib.error.URLError as e:
+            print(e.reason)
+            if time.time() > deadline:
+                print(
+                    f"All connection attempts failed within {timeout} seconds."
+                )
+                sys.exit(1)
+            time.sleep(0.5)
+
+
+if __name__ == "__main__":
+    import argparse
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "--basic-auth", "-B", help="Basic auth tuple like user:pass"
+    )
+    parser.add_argument(
+        "--timeout",
+        "-t",
+        metavar="TIMEOUT",
+        default=10,
+        type=int,
+        help="Maximum seconds to wait for successful connection attempt "
+        "[default: 10 seconds]",
+    )
+    parser.add_argument("url", type=str, help="URL to connect to")
+    args = parser.parse_args()
+
+    run(
+        url=args.url,
+        timeout=args.timeout,
+        basic_auth=args.basic_auth,
+    )
diff --git a/test/http-server b/test/http-server
new file mode 100755 (executable)
index 0000000..4c5791f
--- /dev/null
@@ -0,0 +1,172 @@
+#!/usr/bin/env python3
+
+# This is a simple HTTP server based on the HTTPServer and
+# SimpleHTTPRequestHandler. It has been extended with PUT
+# and DELETE functionality to store or delete results.
+#
+# See: https://github.com/python/cpython/blob/main/Lib/http/server.py
+
+from functools import partial
+from http import HTTPStatus
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+import os
+import signal
+import socket
+import sys
+
+
+class AuthenticationError(Exception):
+    pass
+
+
+class PUTEnabledHTTPRequestHandler(SimpleHTTPRequestHandler):
+    def __init__(self, *args, basic_auth=None, **kwargs):
+        self.basic_auth = None
+        if basic_auth:
+            import base64
+
+            self.basic_auth = base64.b64encode(
+                basic_auth.encode("ascii")
+            ).decode("ascii")
+        super().__init__(*args, **kwargs)
+
+    def do_GET(self):
+        try:
+            self._handle_auth()
+            super().do_GET()
+        except AuthenticationError:
+            self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
+
+    def do_HEAD(self):
+        try:
+            self._handle_auth()
+            super().do_HEAD()
+        except AuthenticationError:
+            self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
+
+    def do_PUT(self):
+        path = self.translate_path(self.path)
+        os.makedirs(os.path.dirname(path), exist_ok=True)
+        try:
+            self._handle_auth()
+            file_length = int(self.headers["Content-Length"])
+            with open(path, "wb") as output_file:
+                output_file.write(self.rfile.read(file_length))
+            self.send_response(HTTPStatus.CREATED)
+            self.send_header("Content-Length", "0")
+            self.end_headers()
+        except AuthenticationError:
+            self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
+        except OSError:
+            self.send_error(
+                HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot open file for writing"
+            )
+
+    def do_DELETE(self):
+        path = self.translate_path(self.path)
+        try:
+            self._handle_auth()
+            os.remove(path)
+            self.send_response(HTTPStatus.OK)
+            self.send_header("Content-Length", "0")
+            self.end_headers()
+        except AuthenticationError:
+            self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
+        except OSError:
+            self.send_error(
+                HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot delete file"
+            )
+
+    def _handle_auth(self):
+        if not self.basic_auth:
+            return
+        authorization = self.headers.get("authorization")
+        if authorization:
+            authorization = authorization.split()
+            if len(authorization) == 2:
+                if (
+                    authorization[0] == "Basic"
+                    and authorization[1] == self.basic_auth
+                ):
+                    return
+        raise AuthenticationError("Authentication required")
+
+
+def _get_best_family(*address):
+    infos = socket.getaddrinfo(
+        *address,
+        type=socket.SOCK_STREAM,
+        flags=socket.AI_PASSIVE,
+    )
+    family, type, proto, canonname, sockaddr = next(iter(infos))
+    return family, sockaddr
+
+
+def run(HandlerClass, ServerClass, port, bind):
+    HandlerClass.protocol_version = "HTTP/1.1"
+    ServerClass.address_family, addr = _get_best_family(bind, port)
+
+    with ServerClass(addr, HandlerClass) as httpd:
+        host, port = httpd.socket.getsockname()[:2]
+        url_host = f"[{host}]" if ":" in host else host
+        print(
+            f"Serving HTTP on {host} port {port} "
+            f"(http://{url_host}:{port}/) ..."
+        )
+        try:
+            httpd.serve_forever()
+        except KeyboardInterrupt:
+            print("\nKeyboard interrupt received, exiting.")
+            sys.exit(0)
+
+
+def on_terminate(signum, frame):
+    sys.stdout.flush()
+    sys.stderr.flush()
+    sys.exit(128 + signum)
+
+
+if __name__ == "__main__":
+    import argparse
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "--basic-auth", "-B", help="Basic auth tuple like user:pass"
+    )
+    parser.add_argument(
+        "--bind",
+        "-b",
+        metavar="ADDRESS",
+        help="Specify alternate bind address " "[default: all interfaces]",
+    )
+    parser.add_argument(
+        "--directory",
+        "-d",
+        default=os.getcwd(),
+        help="Specify alternative directory " "[default:current directory]",
+    )
+    parser.add_argument(
+        "port",
+        action="store",
+        default=8080,
+        type=int,
+        nargs="?",
+        help="Specify alternate port [default: 8080]",
+    )
+    args = parser.parse_args()
+
+    handler_class = partial(
+        PUTEnabledHTTPRequestHandler, basic_auth=args.basic_auth
+    )
+
+    os.chdir(args.directory)
+
+    signal.signal(signal.SIGINT, on_terminate)
+    signal.signal(signal.SIGTERM, on_terminate)
+
+    run(
+        HandlerClass=handler_class,
+        ServerClass=HTTPServer,
+        port=args.port,
+        bind=args.bind,
+    )
index cbdd98f0622ff4355587fb8c8781e2f2fb811b44..957f5afac08ddb3dadc2c1b529a774cb2d1c96b6 100755 (executable)
--- a/test/run
+++ b/test/run
@@ -27,6 +27,7 @@ skip_code=125
 if [[ -t 1 ]]; then
     ansi_boldgreen='\033[1;32m'
     ansi_boldred='\033[1;31m'
+    ansi_boldyellow='\033[1;93m'
     ansi_bold='\033[1m'
     ansi_reset='\033[1;0m'
 fi
@@ -39,6 +40,10 @@ red() {
     printf "$ansi_boldred%s$ansi_reset\n" "$*"
 }
 
+yellow() {
+    printf "$ansi_boldyellow%s$ansi_reset\n" "$*"
+}
+
 bold() {
     printf "$ansi_bold%s$ansi_reset\n" "$*"
 }
@@ -55,8 +60,15 @@ test_failed_internal() {
     echo "Test case:      $(bold $CURRENT_TEST)"
     echo "Failure reason: $(red "$1")"
     echo
-    echo "ccache -s:"
-    $CCACHE -s
+    echo "Actual statistics counters"
+    echo "=========================="
+    while read -r key value; do
+        if [[ $value > 0 ]]; then
+            printf "$(yellow %-32s) $(yellow %s)\n" "$key" "$value"
+        else
+            printf "%-32s %s\n" "$key" "$value"
+        fi
+    done < <($CCACHE --print-stats | grep -v '^stats_')
     echo
     echo "Test data and log file have been left in $TESTDIR / $TEST_FAILED_SYMLINK"
     symlink_testdir_on_failure
@@ -161,18 +173,14 @@ expect_stat() {
     local line
     local value=""
 
-    while IFS= read -r line; do
-        if [[ $line = *"$stat"* ]]; then
-            value="${line:32}"
-            # remove leading & trailing whitespace
-            value="${value#${value%%[![:space:]]*}}"
-            value="${value%${value##*[![:space:]]}}"
+    while read -r key value; do
+        if [[ $key == $stat ]]; then
             break
         fi
-    done < <($CCACHE -s)
+    done < <($CCACHE --print-stats)
 
     if [ "$expected_value" != "$value" ]; then
-        test_failed_internal "Expected \"$stat\" to be $expected_value, actual $value"
+        test_failed_internal "Expected $stat to be $expected_value, actual $value"
     fi
 }
 
@@ -356,6 +364,7 @@ reset_environment() {
     unset TERM
     unset XDG_CACHE_HOME
     unset XDG_CONFIG_HOME
+    export PWD=$(pwd)
 
     export CCACHE_DETECT_SHEBANG=1
     export CCACHE_DIR=$ABS_TESTDIR/.ccache
@@ -398,13 +407,24 @@ run_suite() {
     SUITE_$suite_name
     echo
 
+    terminate_all_children
+
     return 0
 }
 
+terminate_all_children() {
+    local pids="$(jobs -p)"
+    if [[ -n "$pids" ]]; then
+        kill $pids >/dev/null 2>&1
+        wait >/dev/null 2>&1
+    fi
+}
+
 TEST() {
     CURRENT_TEST=$1
     CCACHE_COMPILE="$CCACHE $COMPILER"
 
+    terminate_all_children
     reset_environment
 
     if $verbose; then
@@ -432,6 +452,8 @@ TEST() {
 
 export LC_ALL=C
 
+trap terminate_all_children EXIT # also clean up after exceptional code flow
+
 if pwd | grep '[^A-Za-z0-9/.,=_%+-]' >/dev/null 2>&1; then
     cat <<EOF
 Error: The test suite doesn't work in directories with whitespace or other
@@ -450,6 +472,8 @@ export PATH
 
 if [ -n "$CC" ]; then
     COMPILER="$CC"
+elif [[ "$OSTYPE" == "darwin"* && -x "$(command -v clang)" ]]; then
+    COMPILER=clang
 else
     COMPILER=gcc
 fi
@@ -463,13 +487,17 @@ COMPILER_TYPE_GCC=false
 COMPILER_USES_LLVM=false
 COMPILER_USES_MINGW=false
 
+ABS_ROOT_DIR="$(cd $(dirname "$0"); pwd)"
+readonly HTTP_CLIENT="${ABS_ROOT_DIR}/http-client"
+readonly HTTP_SERVER="${ABS_ROOT_DIR}/http-server"
+
 HOST_OS_APPLE=false
 HOST_OS_LINUX=false
 HOST_OS_FREEBSD=false
 HOST_OS_WINDOWS=false
 HOST_OS_CYGWIN=false
 
-compiler_version="`$COMPILER --version 2>&1 | head -1`"
+compiler_version="`$COMPILER --version 2>/dev/null | head -1`"
 case $compiler_version in
     *gcc*|*g++*|2.95*)
         COMPILER_TYPE_GCC=true
@@ -479,8 +507,8 @@ case $compiler_version in
         CLANG_VERSION_SUFFIX=$(echo "${COMPILER%% *}" | sed 's/.*clang//')
         ;;
     *)
-        echo "WARNING: Compiler $COMPILER not supported (version: $compiler_version) -- not running tests" >&2
-        exit 0
+        echo "WARNING: Compiler $COMPILER not supported (version: $compiler_version) -- Skipped running tests" >&2
+        exit $skip_code
         ;;
 esac
 
@@ -570,7 +598,7 @@ if [ "$REAL_COMPILER" = "$COMPILER" ]; then
 else
     echo "Compiler:         $COMPILER ($REAL_COMPILER)"
 fi
-echo "Compiler version: $($COMPILER --version | head -n 1)"
+echo "Compiler version: $($COMPILER --version 2>/dev/null | head -n 1)"
 
 REAL_NVCC=$(find_compiler nvcc)
 REAL_CUOBJDUMP=$(find_compiler cuobjdump)
index 37b7755daa86bfe72b4fd993bfcaeaedb673f65d..a9839009d526160e13eab3e2ab287b1b247d5454 100644 (file)
@@ -5,15 +5,27 @@ base_tests() {
     $REAL_COMPILER -c -o reference_test1.o test1.c
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat direct_cache_miss 0
+    expect_stat preprocessed_cache_miss 1
+    expect_stat primary_storage_hit 0
+    expect_stat primary_storage_miss 1
+    expect_stat secondary_storage_hit 0
+    expect_stat secondary_storage_miss 0
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat direct_cache_miss 0
+    expect_stat preprocessed_cache_miss 1
+    expect_stat primary_storage_hit 1
+    expect_stat primary_storage_miss 1
+    expect_stat secondary_storage_hit 0
+    expect_stat secondary_storage_miss 0
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 
     # -------------------------------------------------------------------------
@@ -24,21 +36,21 @@ base_tests() {
     $REAL_COMPILER -c -o reference_test1.o test1.c
 
     $CCACHE $COMPILER -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 
     $CCACHE $CCACHE $COMPILER -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 
     $CCACHE $CCACHE $CCACHE $COMPILER -c test1.c
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 
     # -------------------------------------------------------------------------
@@ -55,13 +67,13 @@ base_tests() {
     TEST "Debug option"
 
     $CCACHE_COMPILE -c test1.c -g
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
 
     $CCACHE_COMPILE -c test1.c -g
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     $REAL_COMPILER -c -o reference_test1.o test1.c -g
     expect_equal_object_files reference_test1.o test1.o
@@ -70,12 +82,12 @@ base_tests() {
     TEST "Output option"
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c test1.c -o foo.o
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     $REAL_COMPILER -c -o reference_test1.o test1.c
     expect_equal_object_files reference_test1.o foo.o
@@ -84,16 +96,16 @@ base_tests() {
     TEST "Output option without space"
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c test1.c -odir
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c test1.c -optf
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 1
 
     $REAL_COMPILER -c -o reference_test1.o test1.c
     expect_equal_object_files reference_test1.o dir
@@ -103,30 +115,36 @@ base_tests() {
     TEST "Called for link"
 
     $CCACHE_COMPILE test1.c -o test 2>/dev/null
-    expect_stat 'called for link' 1
+    expect_stat called_for_link 1
 
     $CCACHE_COMPILE -c test1.c
     $CCACHE_COMPILE test1.o -o test 2>/dev/null
-    expect_stat 'called for link' 2
+    expect_stat called_for_link 2
 
     # -------------------------------------------------------------------------
-    TEST "No input file"
+    TEST "No existing input file"
 
     $CCACHE_COMPILE -c foo.c 2>/dev/null
-    expect_stat 'no input file' 1
+    expect_stat no_input_file 1
+
+    # -------------------------------------------------------------------------
+    TEST "No input file on command line"
+
+    $CCACHE_COMPILE -c -O2 2>/dev/null
+    expect_stat no_input_file 1
 
     # -------------------------------------------------------------------------
     TEST "Called for preprocessing"
 
     $CCACHE_COMPILE -E -c test1.c >/dev/null 2>&1
-    expect_stat 'called for preprocessing' 1
+    expect_stat called_for_preprocessing 1
 
     # -------------------------------------------------------------------------
     TEST "Multiple source files"
 
     touch test2.c
     $CCACHE_COMPILE -c test1.c test2.c
-    expect_stat 'multiple source files' 1
+    expect_stat multiple_source_files 1
 
     # -------------------------------------------------------------------------
     TEST "Couldn't find the compiler"
@@ -141,26 +159,26 @@ base_tests() {
     TEST "Bad compiler arguments"
 
     $CCACHE_COMPILE -c test1.c -I 2>/dev/null
-    expect_stat 'bad compiler arguments' 1
+    expect_stat bad_compiler_arguments 1
 
     # -------------------------------------------------------------------------
     TEST "Unsupported source language"
 
     ln -f test1.c test1.ccc
     $CCACHE_COMPILE -c test1.ccc 2>/dev/null
-    expect_stat 'unsupported source language' 1
+    expect_stat unsupported_source_language 1
 
     # -------------------------------------------------------------------------
     TEST "Unsupported compiler option"
 
     $CCACHE_COMPILE -M foo -c test1.c >/dev/null 2>&1
-    expect_stat 'unsupported compiler option' 1
+    expect_stat unsupported_compiler_option 1
 
     # -------------------------------------------------------------------------
     TEST "Compiler produced stdout"
 
     $CCACHE echo foo -c test1.c >/dev/null
-    expect_stat 'compiler produced stdout' 1
+    expect_stat compiler_produced_stdout 1
 
     # -------------------------------------------------------------------------
     TEST "Output to directory"
@@ -168,7 +186,7 @@ base_tests() {
     mkdir testd
     $CCACHE_COMPILE -o testd -c test1.c >/dev/null 2>&1
     rmdir testd >/dev/null 2>&1
-    expect_stat 'could not write to output file' 1
+    expect_stat bad_output_file 1
 
     # -------------------------------------------------------------------------
     TEST "Output to file in nonexistent directory"
@@ -176,22 +194,16 @@ base_tests() {
     mkdir out
 
     $CCACHE_COMPILE -c test1.c -o out/foo.o
-    expect_stat 'could not write to output file' ""
-    expect_stat 'cache miss' 1
+    expect_stat bad_output_file 0
+    expect_stat cache_miss 1
 
     rm -rf out
 
     $CCACHE_COMPILE -c test1.c -o out/foo.o 2>/dev/null
-    expect_stat 'could not write to output file' 1
-    expect_stat 'cache miss' 1
+    expect_stat bad_output_file 1
+    expect_stat cache_miss 1
     expect_missing out/foo.o
 
-    # -------------------------------------------------------------------------
-    TEST "No input file"
-
-    $CCACHE_COMPILE -c -O2 2>/dev/null
-    expect_stat 'no input file' 1
-
     # -------------------------------------------------------------------------
     TEST "No file extension"
 
@@ -199,14 +211,14 @@ base_tests() {
     touch src/foo
 
     $CCACHE_COMPILE -x c -c src/foo
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists foo.o
     rm foo.o
 
     $CCACHE_COMPILE -x c -c src/foo
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
     expect_exists foo.o
     rm foo.o
 
@@ -220,14 +232,14 @@ if ! $HOST_OS_WINDOWS; then
     touch src/foo.
 
     $CCACHE_COMPILE -x c -c src/foo.
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists foo.o
     rm foo.o
 
     $CCACHE_COMPILE -x c -c src/foo.
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
     expect_exists foo.o
     rm foo.o
 
@@ -241,14 +253,14 @@ fi
     touch src/foo.c.c
 
     $CCACHE_COMPILE -c src/foo.c.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists foo.c.o
     rm foo.c.o
 
     $CCACHE_COMPILE -c src/foo.c.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
     expect_exists foo.c.o
     rm foo.c.o
 
@@ -258,42 +270,42 @@ fi
     TEST "LANG"
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
 
     LANG=foo $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 2
 
     LANG=foo $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 2
 
     # -------------------------------------------------------------------------
     TEST "LANG with sloppiness"
 
     CCACHE_SLOPPINESS=locale LANG=foo $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
 
     CCACHE_SLOPPINESS=locale LANG=foo $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
 
     CCACHE_SLOPPINESS=locale LANG=bar $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
 
     # -------------------------------------------------------------------------
     TEST "Result file is compressed"
@@ -311,27 +323,27 @@ fi
     TEST "Corrupt result file"
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
 
     result_file=$(find $CCACHE_DIR -name '*R')
     printf foo | dd of=$result_file bs=3 count=1 seek=20 conv=notrunc >&/dev/null
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 1
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 1
 
     # -------------------------------------------------------------------------
     TEST "CCACHE_DEBUG"
@@ -355,9 +367,15 @@ fi
     TEST "CCACHE_DEBUG with too hard option"
 
     CCACHE_DEBUG=1 $CCACHE_COMPILE -c test1.c -save-temps
-    expect_stat 'unsupported compiler option' 1
+    expect_stat unsupported_compiler_option 1
     expect_exists test1.o.ccache-log
 
+    # -------------------------------------------------------------------------
+    TEST "CCACHE_DEBUGDIR"
+
+    CCACHE_DEBUG=1 CCACHE_DEBUGDIR=debugdir $CCACHE_COMPILE -c test1.c
+    expect_contains debugdir"$(pwd -P)"/test1.o.ccache-log "Result: cache_miss"
+
     # -------------------------------------------------------------------------
     TEST "CCACHE_DISABLE"
 
@@ -375,14 +393,14 @@ fi
     echo '// initial comment' >test1.c
     cat test1-saved.c >>test1.c
     CCACHE_COMMENTS=1 $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     echo '// different comment' >test1.c
     cat test1-saved.c >>test1.c
     CCACHE_COMMENTS=1 $CCACHE_COMPILE -c test1.c
     mv test1-saved.c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     $REAL_COMPILER -c -o reference_test1.o test1.c
     expect_equal_object_files reference_test1.o test1.o
@@ -391,8 +409,8 @@ fi
     TEST "CCACHE_NOSTATS"
 
     CCACHE_NOSTATS=1 $CCACHE_COMPILE -c test1.c -O -O
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
 
     # -------------------------------------------------------------------------
     TEST "stats file forward compatibility"
@@ -405,9 +423,9 @@ fi
        echo $i
     done > "$stats_file"
 
-    expect_stat 'cache miss' 5
+    expect_stat cache_miss 5
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache miss' 6
+    expect_stat cache_miss 6
     expect_contains "$stats_file" 101
     expect_newer_than "$stats_file" "$CCACHE_DIR/timestamp_reference"
 
@@ -419,25 +437,27 @@ fi
 
     echo "0 0 0 0 1234567890123456789" >"$stats_file"
 
-    expect_stat 'cache miss' 1234567890123456789
+    expect_stat cache_miss 1234567890123456789
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache miss' 1234567890123456790
+    expect_stat cache_miss 1234567890123456790
 
     # -------------------------------------------------------------------------
     TEST "CCACHE_RECACHE"
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat recache 0
 
     CCACHE_RECACHE=1 $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat recache 1
 
     $REAL_COMPILER -c -o reference_test1.o test1.c
     expect_equal_object_files reference_test1.o test1.o
 
-    expect_stat 'files in cache' 1
+    expect_stat files_in_cache 1
 
     # -------------------------------------------------------------------------
     TEST "Directory is hashed if using -g"
@@ -448,19 +468,19 @@ fi
 
     cd dir1
     $CCACHE_COMPILE -c test1.c -g
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     $CCACHE_COMPILE -c test1.c -g
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     cd ../dir2
     $CCACHE_COMPILE -c test1.c -g
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
     $CCACHE_COMPILE -c test1.c -g
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Directory is not hashed if not using -g"
@@ -471,16 +491,16 @@ fi
 
     cd dir1
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     cd ../dir2
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Directory is not hashed if using -g -g0"
@@ -491,16 +511,16 @@ fi
 
     cd dir1
     $CCACHE_COMPILE -c test1.c -g -g0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     $CCACHE_COMPILE -c test1.c -g -g0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     cd ../dir2
     $CCACHE_COMPILE -c test1.c -g -g0
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Directory is not hashed if using -gz"
@@ -513,16 +533,16 @@ fi
 
         cd dir1
         $CCACHE_COMPILE -c test1.c -gz
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 1
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 1
         $CCACHE_COMPILE -c test1.c -gz
-        expect_stat 'cache hit (preprocessed)' 1
-        expect_stat 'cache miss' 1
+        expect_stat preprocessed_cache_hit 1
+        expect_stat cache_miss 1
 
         cd ../dir2
         $CCACHE_COMPILE -c test1.c -gz
-        expect_stat 'cache hit (preprocessed)' 2
-        expect_stat 'cache miss' 1
+        expect_stat preprocessed_cache_hit 2
+        expect_stat cache_miss 1
     fi
 
     # -------------------------------------------------------------------------
@@ -536,16 +556,16 @@ fi
 
         cd dir1
         $CCACHE_COMPILE -c test1.c -gz=zlib
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 1
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 1
         $CCACHE_COMPILE -c test1.c -gz=zlib
-        expect_stat 'cache hit (preprocessed)' 1
-        expect_stat 'cache miss' 1
+        expect_stat preprocessed_cache_hit 1
+        expect_stat cache_miss 1
 
         cd ../dir2
         $CCACHE_COMPILE -c test1.c -gz=zlib
-        expect_stat 'cache hit (preprocessed)' 2
-        expect_stat 'cache miss' 1
+        expect_stat preprocessed_cache_hit 2
+        expect_stat cache_miss 1
     fi
 
     # -------------------------------------------------------------------------
@@ -557,16 +577,16 @@ fi
 
     cd dir1
     CCACHE_NOHASHDIR=1 $CCACHE_COMPILE -c test1.c -g
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     CCACHE_NOHASHDIR=1 $CCACHE_COMPILE -c test1.c -g
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     cd ../dir2
     CCACHE_NOHASHDIR=1 $CCACHE_COMPILE -c test1.c -g
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "CCACHE_EXTRAFILES"
@@ -575,35 +595,35 @@ fi
     echo "b" >b
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     CCACHE_EXTRAFILES="a${PATH_DELIM}b" $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     CCACHE_EXTRAFILES="a${PATH_DELIM}b" $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
 
     echo b2 >b
 
     CCACHE_EXTRAFILES="a${PATH_DELIM}b" $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 3
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 3
 
     CCACHE_EXTRAFILES="a${PATH_DELIM}b" $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 3
-    expect_stat 'cache miss' 3
+    expect_stat preprocessed_cache_hit 3
+    expect_stat cache_miss 3
 
     CCACHE_EXTRAFILES="doesntexist" $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 3
-    expect_stat 'cache miss' 3
-    expect_stat 'error hashing extra file' 1
+    expect_stat preprocessed_cache_hit 3
+    expect_stat cache_miss 3
+    expect_stat error_hashing_extra_file 1
 
     # -------------------------------------------------------------------------
     TEST "CCACHE_PREFIX"
@@ -623,24 +643,24 @@ EOF
 int foo;
 EOF
     PATH=.:$PATH CCACHE_PREFIX="prefix-a prefix-b" $CCACHE_COMPILE -c file.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_content prefix.result "a
 b"
 
     PATH=.:$PATH CCACHE_PREFIX="prefix-a prefix-b" $CCACHE_COMPILE -c file.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
     expect_content prefix.result "a
 b"
 
     rm -f prefix.result
     PATH=.:$PATH CCACHE_PREFIX_CPP="prefix-a prefix-b" $CCACHE_COMPILE -c file.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 1
     expect_content prefix.result "a
 b"
 
@@ -651,27 +671,21 @@ b"
         generate_code $i test$i.c
         $CCACHE_COMPILE -c test$i.c
     done
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 32
-    expect_stat 'files in cache' 32
-
-    # -------------------------------------------------------------------------
-    TEST "Called for preprocessing"
-
-    $CCACHE_COMPILE -c test1.c -E >test1.i
-    expect_stat 'called for preprocessing' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 32
+    expect_stat files_in_cache 32
 
     # -------------------------------------------------------------------------
     TEST "Direct .i compile"
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $REAL_COMPILER -c test1.c -E >test1.i
     $CCACHE_COMPILE -c test1.i
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "-x c"
@@ -679,12 +693,12 @@ b"
     ln -f test1.c test1.ccc
 
     $CCACHE_COMPILE -x c -c test1.ccc
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -x c -c test1.ccc
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "-xc"
@@ -692,92 +706,92 @@ b"
     ln -f test1.c test1.ccc
 
     $CCACHE_COMPILE -xc -c test1.ccc
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -xc -c test1.ccc
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "-x none"
 
     $CCACHE_COMPILE -x assembler -x none -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -x assembler -x none -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "-x unknown"
 
     $CCACHE_COMPILE -x unknown -c test1.c 2>/dev/null
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
-    expect_stat 'unsupported source language' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat unsupported_source_language 1
 
     # -------------------------------------------------------------------------
     TEST "-x c -c /dev/null"
 
     $CCACHE_COMPILE -x c -c /dev/null -o null.o 2>/dev/null
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -x c -c /dev/null -o null.o 2>/dev/null
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "-D not hashed"
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -DNOT_AFFECTING=1 -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "-S"
 
     $CCACHE_COMPILE -S test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -S test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c test1.s
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -c test1.s
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "-frecord-gcc-switches"
 
     if $REAL_COMPILER -frecord-gcc-switches -c test1.c >&/dev/null; then
         $CCACHE_COMPILE -frecord-gcc-switches -c test1.c
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 1
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 1
 
         $CCACHE_COMPILE -frecord-gcc-switches -c test1.c
-        expect_stat 'cache hit (preprocessed)' 1
-        expect_stat 'cache miss' 1
+        expect_stat preprocessed_cache_hit 1
+        expect_stat cache_miss 1
 
         $CCACHE_COMPILE -frecord-gcc-switches -Wall -c test1.c
-        expect_stat 'cache hit (preprocessed)' 1
-        expect_stat 'cache miss' 2
+        expect_stat preprocessed_cache_hit 1
+        expect_stat cache_miss 2
 
         $CCACHE_COMPILE -frecord-gcc-switches -Wall -c test1.c
-        expect_stat 'cache hit (preprocessed)' 2
-        expect_stat 'cache miss' 2
+        expect_stat preprocessed_cache_hit 2
+        expect_stat cache_miss 2
     fi
 
     # -------------------------------------------------------------------------
@@ -786,31 +800,31 @@ b"
     $REAL_COMPILER -c -o reference_test1.o test1.c
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 
     CCACHE_COMPILER=$COMPILER_BIN $CCACHE \
         non_existing_compiler_will_be_overridden_anyway \
         $COMPILER_ARGS -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 
     CCACHE_COMPILER=$COMPILER_BIN $CCACHE same/for/relative \
         $COMPILER_ARGS -c test1.c
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 
     CCACHE_COMPILER=$COMPILER_BIN $CCACHE /and/even/absolute/compilers \
         $COMPILER_ARGS -c test1.c
-    expect_stat 'cache hit (preprocessed)' 3
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 3
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 
     # -------------------------------------------------------------------------
@@ -864,21 +878,21 @@ EOF
     chmod +x compiler.sh
     backdate compiler.sh
     CCACHE_COMPILERCHECK=mtime $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     sed_in_place 's/comment/yoghurt/' compiler.sh # Don't change the size
     chmod +x compiler.sh
     backdate compiler.sh # Don't change the timestamp
 
     CCACHE_COMPILERCHECK=mtime $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     touch compiler.sh
     CCACHE_COMPILERCHECK=mtime $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "CCACHE_COMPILERCHECK=content"
@@ -892,17 +906,17 @@ EOF
     chmod +x compiler.sh
 
     CCACHE_COMPILERCHECK=content $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_COMPILERCHECK=content $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
     echo "# Compiler upgrade" >>compiler.sh
 
     CCACHE_COMPILERCHECK=content $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "CCACHE_COMPILERCHECK=none"
@@ -916,17 +930,17 @@ EOF
     chmod +x compiler.sh
 
     CCACHE_COMPILERCHECK=none $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_COMPILERCHECK=none $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     echo "# Compiler upgrade" >>compiler.sh
     CCACHE_COMPILERCHECK=none $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "CCACHE_COMPILERCHECK=string"
@@ -940,20 +954,20 @@ EOF
     chmod +x compiler.sh
 
     CCACHE_COMPILERCHECK=string:foo $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_COMPILERCHECK=string:foo $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     CCACHE_COMPILERCHECK=string:bar $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     CCACHE_COMPILERCHECK=string:bar $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "CCACHE_COMPILERCHECK=command"
@@ -967,13 +981,13 @@ EOF
     chmod +x compiler.sh
 
     CCACHE_COMPILERCHECK='echo %compiler%' $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     echo "# Compiler upgrade" >>compiler.sh
     CCACHE_COMPILERCHECK="echo ./compiler.sh" $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     cat <<EOF >foobar.sh
 #!/bin/sh
@@ -982,12 +996,12 @@ echo bar
 EOF
     chmod +x foobar.sh
     CCACHE_COMPILERCHECK='./foobar.sh' $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     CCACHE_COMPILERCHECK='echo foo; echo bar' $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "CCACHE_COMPILERCHECK=unknown_command"
@@ -1001,7 +1015,7 @@ EOF
     chmod +x compiler.sh
 
     CCACHE_COMPILERCHECK="unknown_command" $CCACHE ./compiler.sh -c test1.c 2>/dev/null
-    expect_stat 'compiler check failed' 1
+    expect_stat compiler_check_failed 1
 
 
     # -------------------------------------------------------------------------
@@ -1021,8 +1035,8 @@ EOF
 
     $CCACHE -M 5 >/dev/null
     $CCACHE_COMPILE -MMD -c test.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     result_file=$(find "$CCACHE_DIR" -name '*R')
     level_2_dir=$(dirname "$result_file")
     level_1_dir=$(dirname $(dirname "$result_file"))
@@ -1038,15 +1052,15 @@ EOF
 
     rm test.o test.d
     $CCACHE_COMPILE -MMD -c test.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
     expect_perm test.o -rw-r--r--
     expect_perm test.d -rw-r--r--
 
     $CCACHE_COMPILE -o test test.o
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'called for link' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat called_for_link 1
     expect_perm test -rwxr-xr-x
 
     # A non-cache-miss case which affects the stats file on level 2:
@@ -1054,7 +1068,7 @@ EOF
     rm -rf "$CCACHE_DIR"
 
     $CCACHE_COMPILE --version >/dev/null
-    expect_stat 'no input file' 1
+    expect_stat no_input_file 1
     stats_file=$(find "$CCACHE_DIR" -name stats)
     level_2_dir=$(dirname "$stats_file")
     level_1_dir=$(dirname $(dirname "$stats_file"))
@@ -1078,13 +1092,13 @@ EOF
 EOF
     chmod +x no-object-prefix
     CCACHE_PREFIX=$(pwd)/no-object-prefix $CCACHE_COMPILE -c test_no_obj.c
-    expect_stat 'compiler produced no output' 1
+    expect_stat compiler_produced_no_output 1
 
     CCACHE_PREFIX=$(pwd)/no-object-prefix $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
-    expect_stat 'files in cache' 0
-    expect_stat 'compiler produced no output' 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat files_in_cache 0
+    expect_stat compiler_produced_no_output 2
 
     # -------------------------------------------------------------------------
     TEST "No object file due to -fsyntax-only"
@@ -1096,15 +1110,15 @@ EOF
     expect_contains reference_stderr.txt "This triggers a compiler warning"
 
     $CCACHE_COMPILE -Wall -c stderr.c -fsyntax-only 2>stderr.txt
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_text_content reference_stderr.txt stderr.txt
 
     $CCACHE_COMPILE -Wall -c stderr.c -fsyntax-only 2>stderr.txt
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_text_content reference_stderr.txt stderr.txt
 
     # -------------------------------------------------------------------------
@@ -1120,18 +1134,18 @@ touch test_empty_obj.o
 EOF
     chmod +x empty-object-prefix
     CCACHE_PREFIX=`pwd`/empty-object-prefix $CCACHE_COMPILE -c test_empty_obj.c
-    expect_stat 'compiler produced empty output' 1
+    expect_stat compiler_produced_empty_output 1
 
     # -------------------------------------------------------------------------
     TEST "Output to /dev/null"
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c test1.c -o /dev/null
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Caching stderr"
@@ -1163,15 +1177,15 @@ EOF
 
     unset CCACHE_NOCPP2
     stderr=$($CCACHE ./compiler.sh -c test1.c 2>stderr)
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_content stderr "[cc_stderr]"
 
     stderr=$(CCACHE_NOCPP2=1 $CCACHE ./compiler.sh -c test1.c 2>stderr)
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 2
     expect_content stderr "[cpp_stderr][cc_stderr]"
 
     # -------------------------------------------------------------------------
@@ -1184,14 +1198,14 @@ EOF
     mv test.d reference.d
 
     $CCACHE_COMPILE -c test.c -MMD 2>test.stderr
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content reference.stderr test.stderr
     expect_equal_content reference.d test.d
 
     $CCACHE_COMPILE -c test.c -MMD 2>test.stderr
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
     expect_equal_content reference.stderr test.stderr
     expect_equal_content reference.d test.d
 
@@ -1200,63 +1214,63 @@ EOF
 
     $CCACHE_COMPILE -c test1.c
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
 
     $CCACHE -z >/dev/null
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat files_in_cache 1
 
     # -------------------------------------------------------------------------
     TEST "--clear"
 
     $CCACHE_COMPILE -c test1.c
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
 
     $CCACHE -C >/dev/null
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 0
 
     # -------------------------------------------------------------------------
     TEST "-P -c"
 
     $CCACHE_COMPILE -P -c test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -P -c test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "-P -E"
 
-    $CCACHE_COMPILE -P -E test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
-    expect_stat 'called for preprocessing' 1
+    $CCACHE_COMPILE -P -E test1.c >/dev/null
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat called_for_preprocessing 1
 
     # -------------------------------------------------------------------------
     TEST "-Wp,-P"
 
     $CCACHE_COMPILE -c -Wp,-P test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c -Wp,-P test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "-Wp,-P,-DFOO"
@@ -1267,29 +1281,29 @@ EOF
     # object file produced when compiling without ccache.)
 
     $CCACHE_COMPILE -c -Wp,-P,-DFOO test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
-    expect_stat 'unsupported compiler option' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat unsupported_compiler_option 1
 
     $CCACHE_COMPILE -c -Wp,-DFOO,-P test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
-    expect_stat 'unsupported compiler option' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat unsupported_compiler_option 2
 
     # -------------------------------------------------------------------------
     TEST "-Wp,-D"
 
     $CCACHE_COMPILE -c -Wp,-DFOO test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c -DFOO test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Handling of compiler-only arguments"
@@ -1303,27 +1317,27 @@ EOF
     backdate compiler.sh
 
     $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     if [ -z "$CCACHE_NOCPP2" ]; then
         expect_content compiler.args "[-E test1.c][-c -o test1.o test1.c]"
     fi
     rm compiler.args
 
     $CCACHE ./compiler.sh -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_content compiler.args "[-E test1.c]"
     rm compiler.args
 
     # Even though -Werror is not passed to the preprocessor, it should be part
     # of the hash, so we expect a cache miss:
     $CCACHE ./compiler.sh -c -Werror -rdynamic test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 2
     if [ -z "$CCACHE_NOCPP2" ]; then
         expect_content compiler.args "[-E test1.c][-Werror -rdynamic -c -o test1.o test1.c]"
     fi
@@ -1367,14 +1381,14 @@ EOF
     chmod +x buggy-cpp
 
     $CCACHE ./buggy-cpp -c file.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE ./buggy-cpp -DNOT_AFFECTING=1 -c file.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
 if ! $HOST_OS_WINDOWS; then
@@ -1385,18 +1399,18 @@ __asm__(".incbin \"/dev/null\"");
 EOF
 
     $CCACHE_COMPILE -c incbin.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
-    expect_stat 'unsupported code directive' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat unsupported_code_directive 1
 
     cat <<EOF >incbin.s
 .incbin "/dev/null";
 EOF
 
     $CCACHE_COMPILE -c incbin.s
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
-    expect_stat 'unsupported code directive' 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat unsupported_code_directive 2
 fi
 
     # -------------------------------------------------------------------------
@@ -1418,16 +1432,16 @@ EOF
 
     N=1 $CCACHE ./compiler.sh -c test1.c 2>stderr.txt
     stderr=$(cat stderr.txt)
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     if [ "$stderr" != "1Pu1Cu1Cc" ]; then
         test_failed "Unexpected stderr: $stderr != 1Pu1Cu1Cc"
     fi
 
     N=2 $CCACHE ./compiler.sh -c test1.c 2>stderr.txt
     stderr=$(cat stderr.txt)
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
     if [ "$stderr" != "2Pu1Cc" ]; then
         test_failed "Unexpected stderr: $stderr != 2Pu1Cc"
     fi
index d1d0aef153cac45be92cf92bc64826230d01fc67..0bc83cbce292ced45a3f37b433344781b0cbb29e 100644 (file)
@@ -19,30 +19,30 @@ SUITE_basedir() {
 
     cd dir1
     CCACHE_BASEDIR="`pwd`" $CCACHE_COMPILE -I`pwd`/include -c src/test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     cd ../dir2
     CCACHE_BASEDIR="`pwd`" $CCACHE_COMPILE -I`pwd`/include -c src/test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Disabled (default) CCACHE_BASEDIR"
 
     cd dir1
     CCACHE_BASEDIR="`pwd`" $CCACHE_COMPILE -I`pwd`/include -c src/test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # CCACHE_BASEDIR="" is the default:
     $CCACHE_COMPILE -I`pwd`/include -c src/test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
 if ! $HOST_OS_WINDOWS && ! $HOST_OS_CYGWIN; then
@@ -50,18 +50,18 @@ if ! $HOST_OS_WINDOWS && ! $HOST_OS_CYGWIN; then
 
     cd dir1
     CCACHE_BASEDIR="`pwd`" $CCACHE_COMPILE -I$(pwd)/include -c src/test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     mkdir subdir
 
     # Rewriting triggered by CCACHE_BASEDIR should handle paths with multiple
     # slashes, redundant "/." parts and "foo/.." parts correctly.
     CCACHE_BASEDIR=$(pwd) $CCACHE_COMPILE -I$(pwd)//./subdir/../include -c $(pwd)/src/test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 fi
 
     # -------------------------------------------------------------------------
@@ -134,19 +134,19 @@ if ! $HOST_OS_WINDOWS && ! $HOST_OS_CYGWIN; then
 
     cd dir1/src
     CCACHE_BASEDIR=/ $CCACHE_COMPILE -I$(pwd)/../include -c $(pwd)/test.c -o $(pwd)/build/test.o
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     cd ../../dir2/src
     # Apparent CWD:
     CCACHE_BASEDIR=/ $CCACHE_COMPILE -I$(pwd)/../include -c $(pwd)/test.c -o $(pwd)/build/test.o
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
 
     # Actual CWD (e.g. from $(CURDIR) in a Makefile):
     CCACHE_BASEDIR=/ $CCACHE_COMPILE -I$(pwd -P)/../include -c $(pwd -P)/test.c -o $(pwd -P)/build/test.o
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 1
 fi
 
     # -------------------------------------------------------------------------
@@ -173,19 +173,19 @@ if ! $HOST_OS_WINDOWS && ! $HOST_OS_CYGWIN; then
 
     cd build1
     CCACHE_BASEDIR=/ $CCACHE_COMPILE -I$(pwd)/src/include -c $(pwd)/src/src/test.c -o $(pwd)/test.o
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     cd ../build2
     # Apparent CWD:
     CCACHE_BASEDIR=/ $CCACHE_COMPILE -I$(pwd)/src/include -c $(pwd)/src/src/test.c -o $(pwd)/test.o
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
 
     # Actual CWD:
     CCACHE_BASEDIR=/ $CCACHE_COMPILE -I$(pwd -P)/src/include -c $(pwd -P)/src/src/test.c -o $(pwd -P)/test.o
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 1
 fi
 
     # -------------------------------------------------------------------------
@@ -203,17 +203,17 @@ EOF
 EOF
 
     CCACHE_BASEDIR=`pwd` $CCACHE_COMPILE -Wall -W -I`pwd` -c `pwd`/stderr.c -o `pwd`/stderr.o 2>stderr.txt
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     if grep `pwd` stderr.txt >/dev/null 2>&1; then
         test_failed "Base dir (`pwd`) found in stderr:\n`cat stderr.txt`"
     fi
 
     CCACHE_BASEDIR=`pwd` $CCACHE_COMPILE -Wall -W -I`pwd` -c `pwd`/stderr.c -o `pwd`/stderr.o 2>stderr.txt
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     if grep `pwd` stderr.txt >/dev/null 2>&1; then
         test_failed "Base dir (`pwd`) found in stderr:\n`cat stderr.txt`"
     fi
@@ -225,16 +225,16 @@ EOF
         clear_cache
         cd dir1
         CCACHE_BASEDIR="`pwd`" $CCACHE_COMPILE -I`pwd`/include -MD -${option}`pwd`/test.d -c src/test.c
-        expect_stat 'cache hit (direct)' 0
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 1
+        expect_stat direct_cache_hit 0
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 1
         cd ..
 
         cd dir2
         CCACHE_BASEDIR="`pwd`" $CCACHE_COMPILE -I`pwd`/include -MD -${option}`pwd`/test.d -c src/test.c
-        expect_stat 'cache hit (direct)' 1
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 1
+        expect_stat direct_cache_hit 1
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 1
         cd ..
     done
 
@@ -248,9 +248,9 @@ EOF
         clear_cache
         cd dir1
         CCACHE_BASEDIR="/" $CCACHE_COMPILE -I`pwd`/include -MD -${option}`pwd`/test.d -c src/test.c
-        expect_stat 'cache hit (direct)' 0
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 1
+        expect_stat direct_cache_hit 0
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 1
         # Check that there is no absolute path in the dependency file:
         while read line; do
             for file in $line; do
@@ -263,9 +263,9 @@ EOF
 
         cd dir2
         CCACHE_BASEDIR="/" $CCACHE_COMPILE -I`pwd`/include -MD -${option}`pwd`/test.d -c src/test.c
-        expect_stat 'cache hit (direct)' 1
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 1
+        expect_stat direct_cache_hit 1
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 1
         cd ..
     done
 
@@ -285,30 +285,62 @@ EOF
     $REAL_COMPILER -c $pwd/test.c 2>reference.stderr
 
     CCACHE_ABSSTDERR=1 CCACHE_BASEDIR="$pwd" $CCACHE_COMPILE -c $pwd/test.c 2>ccache.stderr
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content reference.stderr ccache.stderr
 
     CCACHE_ABSSTDERR=1 CCACHE_BASEDIR="$pwd" $CCACHE_COMPILE -c $pwd/test.c 2>ccache.stderr
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content reference.stderr ccache.stderr
 
     if $REAL_COMPILER -fdiagnostics-color=always -c test.c 2>/dev/null; then
         $REAL_COMPILER -fdiagnostics-color=always -c $pwd/test.c 2>reference.stderr
 
         CCACHE_ABSSTDERR=1 CCACHE_BASEDIR="$pwd" $CCACHE_COMPILE -fdiagnostics-color=always -c $pwd/test.c 2>ccache.stderr
-        expect_stat 'cache hit (direct)' 2
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 1
+        expect_stat direct_cache_hit 2
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 1
         expect_equal_content reference.stderr ccache.stderr
 
         CCACHE_ABSSTDERR=1 CCACHE_BASEDIR="$pwd" $CCACHE_COMPILE -fdiagnostics-color=always -c $pwd/test.c 2>ccache.stderr
-        expect_stat 'cache hit (direct)' 3
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 1
+        expect_stat direct_cache_hit 3
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 1
         expect_equal_content reference.stderr ccache.stderr
     fi
+
+    # -------------------------------------------------------------------------
+    TEST "Relative PWD"
+
+    cd dir1
+    CCACHE_BASEDIR="$(pwd)" PWD=. $CCACHE_COMPILE -I$(pwd)/include -c src/test.c
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+
+    cd ../dir2
+    CCACHE_BASEDIR="$(pwd)" PWD=. $CCACHE_COMPILE -I$(pwd)/include -c src/test.c
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+
+    # -------------------------------------------------------------------------
+    TEST "Unset PWD"
+
+    unset PWD
+
+    cd dir1
+    CCACHE_BASEDIR="$(pwd)" $CCACHE_COMPILE -I$(pwd)/include -c src/test.c
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+
+    cd ../dir2
+    CCACHE_BASEDIR="$(pwd)" $CCACHE_COMPILE -I$(pwd)/include -c src/test.c
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 }
index ef2e8d57672a71ef49b5ca4f8ecef5999660ce6e..90dbe20cc7285e7972abc0eb01a33836cc95e572 100644 (file)
@@ -29,16 +29,16 @@ SUITE_cache_levels() {
     TEST "Empty cache"
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_on_level R 2
     expect_on_level M 2
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_on_level R 2
     expect_on_level M 2
 
@@ -49,16 +49,16 @@ SUITE_cache_levels() {
     add_fake_files_counters $files
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' $((files + 2))
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache $((files + 2))
     expect_on_level R 2
     expect_on_level M 2
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' $((files + 2))
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache $((files + 2))
     expect_on_level R 2
     expect_on_level M 2
 
@@ -69,16 +69,16 @@ SUITE_cache_levels() {
     add_fake_files_counters $files
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' $((files + 2))
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache $((files + 2))
     expect_on_level R 3
     expect_on_level M 3
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' $((files + 2))
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache $((files + 2))
     expect_on_level R 3
     expect_on_level M 3
 
@@ -89,16 +89,16 @@ SUITE_cache_levels() {
     add_fake_files_counters $files
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' $((files + 2))
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache $((files + 2))
     expect_on_level R 4
     expect_on_level M 4
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' $((files + 2))
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache $((files + 2))
     expect_on_level R 4
     expect_on_level M 4
 
@@ -109,16 +109,16 @@ SUITE_cache_levels() {
     add_fake_files_counters $files
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' $((files + 2))
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache $((files + 2))
     expect_on_level R 4
     expect_on_level M 4
 
     $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' $((files + 2))
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache $((files + 2))
     expect_on_level R 4
     expect_on_level M 4
 }
index b2c53a598a755c3fe3f1d84610c7e706a2c0091a..18c2055fe13936645465c1ce2fd181072a7c72ea 100644 (file)
@@ -20,8 +20,8 @@ SUITE_cleanup() {
 
     $CCACHE -C >/dev/null
     expect_file_count 0 '*R' $CCACHE_DIR
-    expect_stat 'files in cache' 0
-    expect_stat 'cleanups performed' 1
+    expect_stat files_in_cache 0
+    expect_stat cleanups_performed 1
 
     # -------------------------------------------------------------------------
     TEST "Forced cache cleanup, no limits"
@@ -31,8 +31,8 @@ SUITE_cleanup() {
     $CCACHE -F 0 -M 0 >/dev/null
     $CCACHE -c >/dev/null
     expect_file_count 10 '*R' $CCACHE_DIR
-    expect_stat 'files in cache' 10
-    expect_stat 'cleanups performed' 0
+    expect_stat files_in_cache 10
+    expect_stat cleanups_performed 0
 
     # -------------------------------------------------------------------------
     TEST "Forced cache cleanup, file limit"
@@ -45,8 +45,8 @@ SUITE_cleanup() {
     $CCACHE -F 160 -M 0 >/dev/null
     $CCACHE -c >/dev/null
     expect_file_count 10 '*R' $CCACHE_DIR
-    expect_stat 'files in cache' 10
-    expect_stat 'cleanups performed' 0
+    expect_stat files_in_cache 10
+    expect_stat cleanups_performed 0
 
     # Reduce file limit
     #
@@ -54,8 +54,8 @@ SUITE_cleanup() {
     $CCACHE -F 112 -M 0 >/dev/null
     $CCACHE -c >/dev/null
     expect_file_count 7 '*R' $CCACHE_DIR
-    expect_stat 'files in cache' 7
-    expect_stat 'cleanups performed' 1
+    expect_stat files_in_cache 7
+    expect_stat cleanups_performed 1
     for i in 0 1 2; do
         file=$CCACHE_DIR/a/result${i}R
         expect_missing $CCACHE_DIR/a/result${i}R
@@ -81,8 +81,8 @@ SUITE_cleanup() {
         $CCACHE -F 0 -M 256K >/dev/null
         $CCACHE -c >/dev/null
         expect_file_count 3 '*R' $CCACHE_DIR
-        expect_stat 'files in cache' 3
-        expect_stat 'cleanups performed' 1
+        expect_stat files_in_cache 3
+        expect_stat cleanups_performed 1
         for i in 0 1 2 3 4 5 6; do
             file=$CCACHE_DIR/a/result${i}R
             expect_missing $file
@@ -102,14 +102,14 @@ SUITE_cleanup() {
     $CCACHE -F 160 -M 0 >/dev/null
 
     expect_file_count 160 '*R' $CCACHE_DIR
-    expect_stat 'files in cache' 160
-    expect_stat 'cleanups performed' 0
+    expect_stat files_in_cache 160
+    expect_stat cleanups_performed 0
 
     touch empty.c
     CCACHE_LIMIT_MULTIPLE=0.9 $CCACHE_COMPILE -c empty.c -o empty.o
     expect_file_count 159 '*R' $CCACHE_DIR
-    expect_stat 'files in cache' 159
-    expect_stat 'cleanups performed' 1
+    expect_stat files_in_cache 159
+    expect_stat cleanups_performed 1
 
     # -------------------------------------------------------------------------
     TEST "Automatic cache cleanup, limit_multiple 0.7"
@@ -121,14 +121,14 @@ SUITE_cleanup() {
     $CCACHE -F 160 -M 0 >/dev/null
 
     expect_file_count 160 '*R' $CCACHE_DIR
-    expect_stat 'files in cache' 160
-    expect_stat 'cleanups performed' 0
+    expect_stat files_in_cache 160
+    expect_stat cleanups_performed 0
 
     touch empty.c
     CCACHE_LIMIT_MULTIPLE=0.7 $CCACHE_COMPILE -c empty.c -o empty.o
     expect_file_count 157 '*R' $CCACHE_DIR
-    expect_stat 'files in cache' 157
-    expect_stat 'cleanups performed' 1
+    expect_stat files_in_cache 157
+    expect_stat cleanups_performed 1
 
     # -------------------------------------------------------------------------
     TEST "No cleanup of new unknown file"
@@ -137,12 +137,12 @@ SUITE_cleanup() {
 
     touch $CCACHE_DIR/a/abcd.unknown
     $CCACHE -F 0 -M 0 -c >/dev/null # update counters
-    expect_stat 'files in cache' 11
+    expect_stat files_in_cache 11
 
     $CCACHE -F 160 -M 0 >/dev/null
     $CCACHE -c >/dev/null
     expect_exists $CCACHE_DIR/a/abcd.unknown
-    expect_stat 'files in cache' 10
+    expect_stat files_in_cache 10
 
     # -------------------------------------------------------------------------
     TEST "Cleanup of old unknown file"
@@ -152,11 +152,11 @@ SUITE_cleanup() {
     touch $CCACHE_DIR/a/abcd.unknown
     backdate $CCACHE_DIR/a/abcd.unknown
     $CCACHE -F 0 -M 0 -c >/dev/null # update counters
-    expect_stat 'files in cache' 11
+    expect_stat files_in_cache 11
 
     $CCACHE -F 160 -M 0 -c >/dev/null
     expect_missing $CCACHE_DIR/a/abcd.unknown
-    expect_stat 'files in cache' 10
+    expect_stat files_in_cache 10
 
     # -------------------------------------------------------------------------
     TEST "Cleanup of tmp file"
@@ -164,11 +164,11 @@ SUITE_cleanup() {
     mkdir -p $CCACHE_DIR/a
     touch $CCACHE_DIR/a/abcd.tmp.efgh
     $CCACHE -c >/dev/null # update counters
-    expect_stat 'files in cache' 1
+    expect_stat files_in_cache 1
     backdate $CCACHE_DIR/a/abcd.tmp.efgh
     $CCACHE -c >/dev/null
     expect_missing $CCACHE_DIR/a/abcd.tmp.efgh
-    expect_stat 'files in cache' 0
+    expect_stat files_in_cache 0
 
     # -------------------------------------------------------------------------
     TEST "No cleanup of .nfs* files"
@@ -179,7 +179,7 @@ SUITE_cleanup() {
     $CCACHE -F 0 -M 0 >/dev/null
     $CCACHE -c >/dev/null
     expect_file_count 1 '.nfs*' $CCACHE_DIR
-    expect_stat 'files in cache' 10
+    expect_stat files_in_cache 10
 
     # -------------------------------------------------------------------------
     TEST "Cleanup of old files by age"
@@ -190,13 +190,13 @@ SUITE_cleanup() {
 
     $CCACHE --evict-older-than 1d >/dev/null
     expect_file_count 1 '*R' $CCACHE_DIR
-    expect_stat 'files in cache' 1
+    expect_stat files_in_cache 1
 
     $CCACHE --evict-older-than 1d  >/dev/null
     expect_file_count 1 '*R' $CCACHE_DIR
-    expect_stat 'files in cache' 1
+    expect_stat files_in_cache 1
 
     backdate $CCACHE_DIR/a/nowR
     $CCACHE --evict-older-than 10s  >/dev/null
-    expect_stat 'files in cache' 0
+    expect_stat files_in_cache 0
 }
index 7c83714c4b503208993a0d5d8765b785ef9855ed..b7264bde5e9cdb0aae36211b6c55df56d9aa0526 100644 (file)
@@ -97,8 +97,8 @@ color_diagnostics_test() {
     # Check that subsequently running on a TTY generates a cache hit.
     color_diagnostics_run_on_pty test1.output "$CCACHE_COMPILE -Wreturn-type -c -o test1.o test1.c"
     color_diagnostics_expect_color test1.output
-    expect_stat 'cache miss' 1
-    expect_stat 'cache hit (preprocessed)' 1
+    expect_stat cache_miss 1
+    expect_stat preprocessed_cache_hit 1
 
     # -------------------------------------------------------------------------
     TEST "Colored diagnostics automatically enabled when stderr is a TTY (run_second_cpp=$run_second_cpp)"
@@ -110,8 +110,8 @@ color_diagnostics_test() {
     # Check that subsequently running without a TTY generates a cache hit.
     $CCACHE_COMPILE -Wreturn-type -c -o test1.o test1.c 2>test1.stderr
     color_diagnostics_expect_no_color test1.stderr
-    expect_stat 'cache miss' 1
-    expect_stat 'cache hit (preprocessed)' 1
+    expect_stat cache_miss 1
+    expect_stat preprocessed_cache_hit 1
 
     if $COMPILER_TYPE_GCC; then
         # ---------------------------------------------------------------------
@@ -122,7 +122,7 @@ color_diagnostics_test() {
         if $CCACHE_COMPILE -fcolor-diagnostics -c test.c >&/dev/null; then
             test_failed "-fcolor-diagnostics unexpectedly accepted by GCC"
         fi
-        expect_stat 'unsupported compiler option' 1
+        expect_stat unsupported_compiler_option 1
 
         # ---------------------------------------------------------------------
         TEST "-fcolor-diagnostics not accepted for GCC for cached result"
@@ -136,7 +136,7 @@ color_diagnostics_test() {
         if $CCACHE_COMPILE -fcolor-diagnostics -c test.c >&/dev/null; then
             test_failed "-fcolor-diagnostics unexpectedly accepted by GCC"
         fi
-        expect_stat 'unsupported compiler option' 1
+        expect_stat unsupported_compiler_option 1
 
         # ---------------------------------------------------------------------
         TEST "-fcolor-diagnostics passed to underlying compiler for unknown compiler type"
@@ -150,7 +150,7 @@ color_diagnostics_test() {
         # Verify that -fcolor-diagnostics was passed to the compiler for the
         # unknown compiler case, i.e. ccache did not exit early with
         # "unsupported compiler option".
-        expect_stat 'compile failed' 1
+        expect_stat compile_failed 1
     fi
 
     if $COMPILER_TYPE_CLANG; then
@@ -190,8 +190,8 @@ color_diagnostics_test() {
                     ;;
             esac
         done
-        expect_stat 'cache miss' 1
-        expect_stat 'cache hit (preprocessed)' 3
+        expect_stat cache_miss 1
+        expect_stat preprocessed_cache_hit 3
     done < <(
         A=({color,nocolor},{tty,notty})
         color_diagnostics_generate_permutations "${#A[@]}"
index 6655778dfe3aa6e093e856a94f7696632fae7e3d..f2a83a3aba3af10f6521b5bc7cdc813505a3d2a1 100644 (file)
@@ -41,25 +41,25 @@ SUITE_cpp1() {
     $REAL_COMPILER $cpp_flag -c -o reference_test1.o test1.c
 
     $CCACHE_COMPILE $cpp_flag -c test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 
     unset CCACHE_NODIRECT
 
     $CCACHE_COMPILE $cpp_flag -c test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_equal_object_files reference_test1.o test1.o
 
     $CCACHE_COMPILE $cpp_flag -c test1.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_equal_object_files reference_test1.o test1.o
 }
index ce36bfb5aed5675ce441a9adf2a18383b0802f12..d0cc264bf4688eb8a1fd1dc670e8f2a244cfac59 100644 (file)
@@ -26,19 +26,19 @@ SUITE_debug_prefix_map() {
 
     cd dir1
     CCACHE_BASEDIR=$(pwd) $CCACHE_COMPILE -I$(pwd)/include -g -fdebug-prefix-map=$(pwd)=some_name_not_likely_to_exist_in_path -c $(pwd)/src/test.c -o $(pwd)/test.o
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_objdump_not_contains test.o "$(pwd)"
     expect_objdump_contains test.o some_name_not_likely_to_exist_in_path
 
     cd ../dir2
     CCACHE_BASEDIR=$(pwd) $CCACHE_COMPILE -I$(pwd)/include -g -fdebug-prefix-map=$(pwd)=some_name_not_likely_to_exist_in_path -c $(pwd)/src/test.c -o $(pwd)/test.o
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_objdump_not_contains test.o "$(pwd)"
 
     # -------------------------------------------------------------------------
@@ -46,18 +46,18 @@ SUITE_debug_prefix_map() {
 
     cd dir1
     CCACHE_BASEDIR=$(pwd) $CCACHE_COMPILE -I$(pwd)/include -g -fdebug-prefix-map=$(pwd)=some_name_not_likely_to_exist_in_path -fdebug-prefix-map=foo=bar -c $(pwd)/src/test.c -o $(pwd)/test.o
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_objdump_not_contains test.o "$(pwd)"
     expect_objdump_contains test.o some_name_not_likely_to_exist_in_path
 
     cd ../dir2
     CCACHE_BASEDIR=$(pwd) $CCACHE_COMPILE -I$(pwd)/include -g -fdebug-prefix-map=$(pwd)=some_name_not_likely_to_exist_in_path -fdebug-prefix-map=foo=bar -c $(pwd)/src/test.c -o $(pwd)/test.o
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_objdump_not_contains test.o "$(pwd)"
 }
index 0d9fcc5bef45af9017d34e61bcd3340c8058b466..24944acbc141a8330688dfd85a25b3f7421f6fe0 100644 (file)
@@ -96,32 +96,36 @@ SUITE_depend() {
 
     CCACHE_DEPEND=1 $CCACHE_COMPILE $DEPSFLAGS_CCACHE -c test.c
     expect_equal_object_files reference_test.o test.o
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2 # result + manifest
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat direct_cache_miss 1
+    expect_stat preprocessed_cache_miss 0
+    expect_stat files_in_cache 2 # result + manifest
 
     CCACHE_DEPEND=1 $CCACHE_COMPILE $DEPSFLAGS_CCACHE -c test.c
     expect_equal_object_files reference_test.o test.o
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat direct_cache_miss 1
+    expect_stat preprocessed_cache_miss 0
+    expect_stat files_in_cache 2
 
     # -------------------------------------------------------------------------
     TEST "No dependency file"
 
     CCACHE_DEPEND=1 $CCACHE_COMPILE -MP -MMD -MF /dev/null -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2 # result + manifest
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2 # result + manifest
 
     CCACHE_DEPEND=1 $CCACHE_COMPILE -MP -MMD -MF /dev/null -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     # -------------------------------------------------------------------------
     TEST "No explicit dependency file"
@@ -130,17 +134,17 @@ SUITE_depend() {
 
     CCACHE_DEPEND=1 $CCACHE_COMPILE -MD -c test.c
     expect_equal_object_files reference_test.o test.o
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2 # result + manifest
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2 # result + manifest
 
     CCACHE_DEPEND=1 $CCACHE_COMPILE -MD -c test.c
     expect_equal_object_files reference_test.o test.o
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     # -------------------------------------------------------------------------
     TEST "Dependency file paths converted to relative if CCACHE_BASEDIR specified"
@@ -165,15 +169,15 @@ EOF
     $REAL_COMPILER -MD -Wall -W -c cpp-warning.c 2>stderr-baseline.txt
 
     CCACHE_DEPEND=1 $CCACHE_COMPILE -MD -Wall -W -c cpp-warning.c 2>stderr-orig.txt
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_content stderr-orig.txt "`cat stderr-baseline.txt`"
 
     CCACHE_DEPEND=1 $CCACHE_COMPILE -MD -Wall -W -c cpp-warning.c 2>stderr-mf.txt
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_content stderr-mf.txt "`cat stderr-baseline.txt`"
 
     # -------------------------------------------------------------------------
@@ -197,20 +201,20 @@ EOF
     CCACHE_DEPEND=1 CCACHE_BASEDIR=$BASEDIR1 $CCACHE_COMPILE $DEPFLAGS -c test.c
     expect_equal_object_files reference_test.o test.o
     expect_equal_content reference_test.d test.d
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2      # result + manifest
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2      # result + manifest
 
     # Recompile dir1 first time.
     generate_reference_compiler_output
     CCACHE_DEPEND=1 CCACHE_BASEDIR=$BASEDIR1 $CCACHE_COMPILE $DEPFLAGS -c test.c
     expect_equal_object_files reference_test.o test.o
     expect_equal_content reference_test.d test.d
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     # Compile dir2. dir2 header changes the object file compared to dir1.
     cd $BASEDIR2
@@ -218,10 +222,10 @@ EOF
     CCACHE_DEPEND=1 CCACHE_BASEDIR=$BASEDIR2 $CCACHE_COMPILE $DEPFLAGS -c test.c
     expect_equal_object_files reference_test.o test.o
     expect_equal_content reference_test.d test.d
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 3      # 2x result, 1x manifest
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 3      # 2x result, 1x manifest
 
     # Compile dir3. dir3 header change does not change object file compared to
     # dir1, but ccache still adds an additional .o/.d file in the cache due to
@@ -231,10 +235,10 @@ EOF
     CCACHE_DEPEND=1 CCACHE_BASEDIR=$BASEDIR3 $CCACHE_COMPILE $DEPFLAGS -c test.c
     expect_equal_object_files reference_test.o test.o
     expect_equal_content reference_test.d test.d
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 3
-    expect_stat 'files in cache' 4      # 3x result, 1x manifest
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 3
+    expect_stat files_in_cache 4      # 3x result, 1x manifest
 
     # Compile dir4. dir4 header adds a new dependency.
     cd $BASEDIR4
@@ -243,10 +247,10 @@ EOF
     expect_equal_object_files reference_test.o test.o
     expect_equal_content reference_test.d test.d
     expect_different_content reference_test.d $BASEDIR1/test.d
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 4
-    expect_stat 'files in cache' 5      # 4x result, 1x manifest
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 4
+    expect_stat files_in_cache 5      # 4x result, 1x manifest
 
     # Recompile dir1 second time.
     cd $BASEDIR1
@@ -254,10 +258,10 @@ EOF
     CCACHE_DEPEND=1 CCACHE_BASEDIR=$BASEDIR1 $CCACHE_COMPILE $DEPFLAGS -c test.c
     expect_equal_object_files reference_test.o test.o
     expect_equal_content reference_test.d test.d
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 4
-    expect_stat 'files in cache' 5
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 4
+    expect_stat files_in_cache 5
 
     # Recompile dir2.
     cd $BASEDIR2
@@ -265,10 +269,10 @@ EOF
     CCACHE_DEPEND=1 CCACHE_BASEDIR=$BASEDIR2 $CCACHE_COMPILE $DEPFLAGS -c test.c
     expect_equal_object_files test.o test.o
     expect_equal_content reference_test.d test.d
-    expect_stat 'cache hit (direct)' 3
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 4
-    expect_stat 'files in cache' 5
+    expect_stat direct_cache_hit 3
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 4
+    expect_stat files_in_cache 5
 
     # Recompile dir3.
     cd $BASEDIR3
@@ -276,10 +280,10 @@ EOF
     CCACHE_DEPEND=1 CCACHE_BASEDIR=$BASEDIR3 $CCACHE_COMPILE $DEPFLAGS -c test.c
     expect_equal_object_files reference_test.o test.o
     expect_equal_content reference_test.d test.d
-    expect_stat 'cache hit (direct)' 4
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 4
-    expect_stat 'files in cache' 5
+    expect_stat direct_cache_hit 4
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 4
+    expect_stat files_in_cache 5
 
     # Recompile dir4.
     cd $BASEDIR4
@@ -288,10 +292,10 @@ EOF
     expect_equal_object_files reference_test.o test.o
     expect_equal_content reference_test.d test.d
     expect_different_content reference_test.d $BASEDIR1/test.d
-    expect_stat 'cache hit (direct)' 5
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 4
-    expect_stat 'files in cache' 5
+    expect_stat direct_cache_hit 5
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 4
+    expect_stat files_in_cache 5
 
     # -------------------------------------------------------------------------
     TEST "Source file with special characters"
@@ -301,16 +305,16 @@ EOF
     mv 'file with$special#characters.d' reference.d
 
     CCACHE_DEPEND=1 $CCACHE_COMPILE -MMD -c 'file with$special#characters.c'
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content 'file with$special#characters.d' reference.d
 
     rm 'file with$special#characters.d'
 
     CCACHE_DEPEND=1 $CCACHE_COMPILE -MMD -c 'file with$special#characters.c'
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content 'file with$special#characters.d' reference.d
 }
index 44f959fd3db979392e1631fa98e6eb8e705dda95..4e22c93b8bc9af95c6a6eaad996373fd9fa5b96c 100644 (file)
@@ -30,20 +30,32 @@ SUITE_direct() {
     $REAL_COMPILER -c -o reference_test.o test.c
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2 # result + manifest
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat direct_cache_miss 1
+    expect_stat preprocessed_cache_miss 1
+    expect_stat primary_storage_hit 0
+    expect_stat primary_storage_miss 2 # result + manifest
+    expect_stat secondary_storage_hit 0
+    expect_stat secondary_storage_miss 0
+    expect_stat files_in_cache 2 # result + manifest
     expect_equal_object_files reference_test.o test.o
 
     manifest_file=$(find $CCACHE_DIR -name '*M')
     backdate $manifest_file
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat direct_cache_miss 1
+    expect_stat preprocessed_cache_miss 1
+    expect_stat primary_storage_hit 2 # result + manifest
+    expect_stat primary_storage_miss 2 # result + manifest
+    expect_stat secondary_storage_hit 0
+    expect_stat secondary_storage_miss 0
+    expect_stat files_in_cache 2
     expect_equal_object_files reference_test.o test.o
     expect_newer_than $manifest_file test.c
 
@@ -51,64 +63,64 @@ SUITE_direct() {
     TEST "Corrupt manifest file"
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     manifest_file=`find $CCACHE_DIR -name '*M'`
     rm $manifest_file
     touch $manifest_file
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "CCACHE_NODIRECT"
 
     CCACHE_NODIRECT=1 $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_NODIRECT=1 $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Modified include file"
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     echo "int test3_2;" >>test3.h
     backdate test3.h
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Removed but previously compiled header file"
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     rm test3.h
     cat <<EOF >test1.h
@@ -118,14 +130,14 @@ EOF
     backdate test1.h
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Calculation of dependency file names"
@@ -138,13 +150,13 @@ EOF
         dep_file=testdir/`echo test$ext | sed 's/\.[^.]*\$//'`.d
 
         $CCACHE_COMPILE -MD -c test.c -o testdir/test$ext
-        expect_stat 'cache hit (direct)' $((3 * i))
-        expect_stat 'cache miss' $((i + 1))
+        expect_stat direct_cache_hit $((3 * i))
+        expect_stat cache_miss $((i + 1))
         rm -f $dep_file
 
         $CCACHE_COMPILE -MD -c test.c -o testdir/test$ext
-        expect_stat 'cache hit (direct)' $((3 * i + 1))
-        expect_stat 'cache miss' $((i + 1))
+        expect_stat direct_cache_hit $((3 * i + 1))
+        expect_stat cache_miss $((i + 1))
         expect_exists $dep_file
         if ! grep "test$ext:" $dep_file >/dev/null 2>&1; then
             test_failed "$dep_file does not contain \"test$ext:\""
@@ -152,13 +164,13 @@ EOF
 
         dep_target=foo.bar
         $CCACHE_COMPILE -MD -MQ $dep_target -c test.c -o testdir/test$ext
-        expect_stat 'cache hit (direct)' $((3 * i + 1))
-        expect_stat 'cache miss' $((i + 2))
+        expect_stat direct_cache_hit $((3 * i + 1))
+        expect_stat cache_miss $((i + 2))
         rm -f $dep_target
 
         $CCACHE_COMPILE -MD -MQ $dep_target -c test.c -o testdir/test$ext
-        expect_stat 'cache hit (direct)' $((3 * i + 2))
-        expect_stat 'cache miss' $((i + 2))
+        expect_stat direct_cache_hit $((3 * i + 2))
+        expect_stat cache_miss $((i + 2))
         expect_exists $dep_file
         if ! grep $dep_target $dep_file >/dev/null 2>&1; then
             test_failed "$dep_file does not contain $dep_target"
@@ -166,7 +178,7 @@ EOF
 
         i=$((i + 1))
     done
-    expect_stat 'files in cache' $((2 * i + 2))
+    expect_stat files_in_cache $((2 * i + 2))
 
     # -------------------------------------------------------------------------
     TEST "-MMD for different source files"
@@ -208,14 +220,14 @@ EOF
 
             # cache miss
             $CCACHE_COMPILE -c test.c $dep_args $obj_args
-            expect_stat 'cache hit (direct)' 0
-            expect_stat 'cache miss' 1
+            expect_stat direct_cache_hit 0
+            expect_stat cache_miss 1
             expect_equal_text_content $dep_file.real $dep_file
 
             # cache hit
             $CCACHE_COMPILE -c test.c $dep_args $obj_args
-            expect_stat 'cache hit (direct)' 1
-            expect_stat 'cache miss' 1
+            expect_stat direct_cache_hit 1
+            expect_stat cache_miss 1
             expect_equal_text_content $dep_file.real $dep_file
 
             # change object file name
@@ -244,8 +256,8 @@ EOF
                 $CCACHE_COMPILE $option -c test1.c -o $obj
                 diff -u orig.d $dep
                 expect_equal_content $dep orig.d
-                expect_stat 'cache hit (direct)' $i
-                expect_stat 'cache miss' 1
+                expect_stat direct_cache_hit $i
+                expect_stat cache_miss 1
 
                 i=$((i + 1))
             done
@@ -272,8 +284,8 @@ EOF
             $CCACHE_COMPILE -MMD -c $src -o $obj
             dep=$(echo $obj | sed 's/\.o$/.d/')
             expect_content $dep "$obj: $src"
-            expect_stat 'cache hit (direct)' $hit
-            expect_stat 'cache miss' 1
+            expect_stat direct_cache_hit $hit
+            expect_stat cache_miss 1
             hit=$((hit + 1))
 
             rm $orig_dep
@@ -316,9 +328,9 @@ EOF
     TEST "-Wp,-MD"
 
     $CCACHE_COMPILE -c -Wp,-MD,other.d test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected.d
 
     $REAL_COMPILER -c -Wp,-MD,other.d test.c -o reference_test.o
@@ -326,16 +338,16 @@ EOF
 
     rm -f other.d
     $CCACHE_COMPILE -c -Wp,-MD,other.d test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected.d
     expect_equal_object_files reference_test.o test.o
 
     $CCACHE_COMPILE -c -Wp,-MD,different_name.d test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content different_name.d expected.d
     expect_equal_object_files reference_test.o test.o
 
@@ -343,9 +355,9 @@ EOF
     TEST "-Wp,-MMD"
 
     $CCACHE_COMPILE -c -Wp,-MMD,other.d test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected_mmd.d
 
     $REAL_COMPILER -c -Wp,-MMD,other.d test.c -o reference_test.o
@@ -353,16 +365,16 @@ EOF
 
     rm -f other.d
     $CCACHE_COMPILE -c -Wp,-MMD,other.d test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected_mmd.d
     expect_equal_object_files reference_test.o test.o
 
     $CCACHE_COMPILE -c -Wp,-MMD,different_name.d test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content different_name.d expected_mmd.d
     expect_equal_object_files reference_test.o test.o
 
@@ -370,14 +382,14 @@ EOF
     TEST "-Wp,-D"
 
     $CCACHE_COMPILE -c -Wp,-DFOO test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c -DFOO test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "-Wp, with multiple arguments"
@@ -388,15 +400,15 @@ EOF
     touch source.c
 
     $CCACHE_COMPILE -c -Wp,-MMD,source.d,-MT,source.o source.c 2>/dev/null
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_content source.d "source.o: source.c"
 
     $CCACHE_COMPILE -c -Wp,-MMD,source.d,-MT,source.o source.c 2>/dev/null
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
     expect_content source.d "source.o: source.c"
 
     # -------------------------------------------------------------------------
@@ -422,17 +434,17 @@ EOF
         $CCACHE_COMPILE -c test.c
         $CCACHE_COMPILE -c test.c
     done
-    expect_stat 'cache hit (direct)' 5
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 5
+    expect_stat direct_cache_hit 5
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 5
 
     # -------------------------------------------------------------------------
     TEST "-MD"
 
     $CCACHE_COMPILE -c -MD test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content test.d expected.d
 
     $REAL_COMPILER -c -MD test.c -o reference_test.o
@@ -440,9 +452,9 @@ EOF
 
     rm -f test.d
     $CCACHE_COMPILE -c -MD test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content test.d expected.d
     expect_equal_object_files reference_test.o test.o
 
@@ -454,17 +466,17 @@ int test() { return 0; }
 EOF
 
     $CCACHE_COMPILE -c -fprofile-arcs -ftest-coverage code.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists code.gcno
 
     rm code.gcno
 
     $CCACHE_COMPILE -c -fprofile-arcs -ftest-coverage code.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists code.gcno
 
     # -------------------------------------------------------------------------
@@ -476,17 +488,17 @@ EOF
 
     if $REAL_COMPILER -c -fstack-usage code.c >/dev/null 2>&1; then
         $CCACHE_COMPILE -c -fstack-usage code.c
-        expect_stat 'cache hit (direct)' 0
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 1
+        expect_stat direct_cache_hit 0
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 1
         expect_exists code.su
 
         rm code.su
 
         $CCACHE_COMPILE -c -fstack-usage code.c
-        expect_stat 'cache hit (direct)' 1
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 1
+        expect_stat direct_cache_hit 1
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 1
         expect_exists code.su
     fi
 
@@ -494,9 +506,9 @@ EOF
     TEST "Direct mode on cache created by ccache without direct mode support"
 
     CCACHE_NODIRECT=1 $CCACHE_COMPILE -c -MD test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content test.d expected.d
     $REAL_COMPILER -c -MD test.c -o reference_test.o
     expect_equal_object_files reference_test.o test.o
@@ -504,27 +516,27 @@ EOF
     rm -f test.d
 
     CCACHE_NODIRECT=1 $CCACHE_COMPILE -c -MD test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
     expect_equal_content test.d expected.d
     expect_equal_object_files reference_test.o test.o
 
     rm -f test.d
 
     $CCACHE_COMPILE -c -MD test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 1
     expect_equal_content test.d expected.d
     expect_equal_object_files reference_test.o test.o
 
     rm -f test.d
 
     $CCACHE_COMPILE -c -MD test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 1
     expect_equal_content test.d expected.d
     expect_equal_object_files reference_test.o test.o
 
@@ -532,9 +544,9 @@ EOF
     TEST "-MF"
 
     $CCACHE_COMPILE -c -MD -MF other.d test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected.d
     $REAL_COMPILER -c -MD -MF other.d test.c -o reference_test.o
     expect_equal_object_files reference_test.o test.o
@@ -542,25 +554,25 @@ EOF
     rm -f other.d
 
     $CCACHE_COMPILE -c -MD -MF other.d test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected.d
     expect_equal_object_files reference_test.o test.o
 
     $CCACHE_COMPILE -c -MD -MF different_name.d test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content different_name.d expected.d
     expect_equal_object_files reference_test.o test.o
 
     rm -f different_name.d
 
     $CCACHE_COMPILE -c -MD -MFthird_name.d test.c
-    expect_stat 'cache hit (direct)' 3
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 3
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content third_name.d expected.d
     expect_equal_object_files reference_test.o test.o
 
@@ -570,31 +582,31 @@ EOF
     TEST "MF /dev/null"
 
     $CCACHE_COMPILE -c -MD -MF /dev/null test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2 # result + manifest
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2 # result + manifest
 
     $CCACHE_COMPILE -c -MD -MF /dev/null test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     $CCACHE_COMPILE -c -MD -MF test.d test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 4
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 4
     expect_equal_content test.d expected.d
 
     rm -f test.d
 
     $CCACHE_COMPILE -c -MD -MF test.d test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 4
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 4
     expect_equal_content test.d expected.d
 
     # -------------------------------------------------------------------------
@@ -610,20 +622,20 @@ int stderr(void)
 }
 EOF
     $CCACHE_COMPILE -Wall -W -c cpp-warning.c 2>stderr-orig.txt
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_NODIRECT=1 $CCACHE_COMPILE -Wall -W -c cpp-warning.c 2>stderr-cpp.txt
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
     expect_content stderr-cpp.txt "`cat stderr-orig.txt`"
 
     $CCACHE_COMPILE -Wall -W -c cpp-warning.c 2>stderr-mf.txt
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
     expect_content stderr-mf.txt "`cat stderr-orig.txt`"
 
     # -------------------------------------------------------------------------
@@ -632,14 +644,14 @@ EOF
     touch empty.c
 
     $CCACHE_COMPILE -c empty.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c empty.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Empty include file"
@@ -650,13 +662,13 @@ EOF
 EOF
     backdate empty.h
     $CCACHE_COMPILE -c include_empty.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     $CCACHE_COMPILE -c include_empty.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "The source file path is included in the hash"
@@ -668,34 +680,34 @@ EOF
     cp file.c file2.c
 
     $CCACHE_COMPILE -c file.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c file.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c file2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -c file2.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -c $(pwd)/file.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 3
 
     $CCACHE_COMPILE -c $(pwd)/file.c
-    expect_stat 'cache hit (direct)' 3
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 3
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 3
 
     # -------------------------------------------------------------------------
     TEST "The source file path is included even if sloppiness = file_macro"
@@ -707,34 +719,34 @@ EOF
     cp file.c file2.c
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS file_macro" $CCACHE_COMPILE -c file.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS file_macro" $CCACHE_COMPILE -c file.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS file_macro" $CCACHE_COMPILE -c file2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS file_macro" $CCACHE_COMPILE -c file2.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS file_macro" $CCACHE_COMPILE -c $(pwd)/file.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 3
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS file_macro" $CCACHE_COMPILE -c $(pwd)/file.c
-    expect_stat 'cache hit (direct)' 3
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 3
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 3
 
     # -------------------------------------------------------------------------
     TEST "Relative includes for identical source code in different directories"
@@ -758,24 +770,24 @@ EOF
     backdate b/file.h
 
     $CCACHE_COMPILE -c a/file.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c a/file.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c b/file.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -c b/file.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "__TIME__ in source file disables direct mode"
@@ -786,14 +798,14 @@ int test;
 EOF
 
     $CCACHE_COMPILE -c time.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c time.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "__TIME__ in include file disables direct mode"
@@ -809,14 +821,14 @@ EOF
 EOF
 
     $CCACHE_COMPILE -c time_h.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c time_h.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "__TIME__ in source file ignored if sloppy"
@@ -827,14 +839,14 @@ int test;
 EOF
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE -c time.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE -c time.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "__TIME__ in include file ignored if sloppy"
@@ -848,14 +860,14 @@ EOF
 #include "time.h"
 EOF
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE -c time_h.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE -c time_h.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Too new include file disables direct mode"
@@ -869,14 +881,14 @@ EOF
     touch -t 203801010000 new.h
 
     $CCACHE_COMPILE -c new.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c new.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "__DATE__ in header file results in direct cache hit as the date remains the same"
@@ -893,14 +905,14 @@ EOF
     backdate test_date2.c test_date2.h
 
     $CCACHE_COMPILE -MP -MMD -MF test_date2.d -c test_date2.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -MP -MMD -MF test_date2.d -c test_date2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "New include file ignored if sloppy"
@@ -914,27 +926,27 @@ EOF
     touch -t 203801010000 new.h
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS include_file_mtime" $CCACHE_COMPILE -c new.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS include_file_mtime" $CCACHE_COMPILE -c new.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Sloppy Clang index store"
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS clang_index_store" $CCACHE_COMPILE -index-store-path foo -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS clang_index_store" $CCACHE_COMPILE -index-store-path bar -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     # Check that environment variables that affect the preprocessor are taken
@@ -955,24 +967,24 @@ EOF
     backdate subdir1/foo.h subdir2/foo.h
 
     CPATH=subdir1 $CCACHE_COMPILE -c foo.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CPATH=subdir1 $CCACHE_COMPILE -c foo.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CPATH=subdir2 $CCACHE_COMPILE -c foo.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2 # subdir2 is part of the preprocessor output
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2 # subdir2 is part of the preprocessor output
 
     CPATH=subdir2 $CCACHE_COMPILE -c foo.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Comment in strings"
@@ -980,21 +992,21 @@ EOF
     echo 'const char *comment = " /* \\\\u" "foo" " */";' >comment.c
 
     $CCACHE_COMPILE -c comment.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c comment.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     echo 'const char *comment = " /* \\\\u" "goo" " */";' >comment.c
 
     $CCACHE_COMPILE -c comment.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "#line directives with troublesome files"
@@ -1053,12 +1065,12 @@ int main(void)
 EOF
 
     $CCACHE_COMPILE -B -L -DFOO -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -B -L -DBAR -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "CCACHE_IGNOREHEADERS with filename"
@@ -1104,40 +1116,42 @@ EOF
     TEST "CCACHE_IGNOREOPTIONS"
 
     CCACHE_IGNOREOPTIONS="-DTEST=1" $CCACHE_COMPILE -DTEST=1 -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_IGNOREOPTIONS="-DTEST=1*" $CCACHE_COMPILE -DTEST=1 -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_IGNOREOPTIONS="-DTEST=1*" $CCACHE_COMPILE -DTEST=12 -c test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_IGNOREOPTIONS="-DTEST=2*" $CCACHE_COMPILE -DTEST=12 -c test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "CCACHE_RECACHE doesn't add a new manifest entry"
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2 # result + manifest
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat recache 0
+    expect_stat files_in_cache 2 # result + manifest
 
     manifest_file=$(find $CCACHE_DIR -name '*M')
     cp $manifest_file saved.manifest
 
     CCACHE_RECACHE=1 $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat recache 1
+    expect_stat files_in_cache 2
 
     expect_equal_content $manifest_file saved.manifest
 }
index 57b6ed9fdbfc3cd4911d4a331ceaf32cdde28c72..4e510900134fb7a2803ffa0f8c2ebfe52fa8599d 100644 (file)
@@ -1,5 +1,5 @@
 SUITE_direct_gcc_PROBE() {
-    if [[ $REAL_COMPILER != *"gcc"* ]]; then
+    if [[ "${COMPILER_TYPE_GCC}" != "true" ]]; then
         echo "Skipping GCC only test cases"
     fi
 }
@@ -36,9 +36,9 @@ SUITE_direct_gcc() {
     TEST "DEPENDENCIES_OUTPUT environment variable"
 
     DEPENDENCIES_OUTPUT="other.d" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected_dependencies_output.d
 
     DEPENDENCIES_OUTPUT="other.d" $REAL_COMPILER -c test.c -o reference_test.o
@@ -46,16 +46,16 @@ SUITE_direct_gcc() {
 
     rm -f other.d
     DEPENDENCIES_OUTPUT="other.d" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected_dependencies_output.d
     expect_equal_object_files reference_test.o test.o
 
     DEPENDENCIES_OUTPUT="different_name.d" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content different_name.d expected_dependencies_output.d
     expect_equal_object_files reference_test.o test.o
 
@@ -63,9 +63,9 @@ SUITE_direct_gcc() {
     TEST "DEPENDENCIES_OUTPUT environment variable with target"
 
     DEPENDENCIES_OUTPUT="other.d target.o" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected_dependencies_output_target.d
 
     DEPENDENCIES_OUTPUT="other.d target.o" $REAL_COMPILER -c test.c -o reference_test.o
@@ -73,16 +73,16 @@ SUITE_direct_gcc() {
 
     rm -f other.d
     DEPENDENCIES_OUTPUT="other.d target.o" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected_dependencies_output_target.d
     expect_equal_object_files reference_test.o test.o
 
     DEPENDENCIES_OUTPUT="different_name.d target.o" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content different_name.d expected_dependencies_output_target.d
     expect_equal_object_files reference_test.o test.o
 
@@ -90,9 +90,9 @@ SUITE_direct_gcc() {
     TEST "SUNPRO_DEPENDENCIES environment variable"
 
     SUNPRO_DEPENDENCIES="other.d" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected_sunpro_dependencies.d
 
     SUNPRO_DEPENDENCIES="other.d" $REAL_COMPILER -c test.c -o reference_test.o
@@ -100,16 +100,16 @@ SUITE_direct_gcc() {
 
     rm -f other.d
     SUNPRO_DEPENDENCIES="other.d" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected_sunpro_dependencies.d
     expect_equal_object_files reference_test.o test.o
 
     SUNPRO_DEPENDENCIES="different_name.d" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content different_name.d expected_sunpro_dependencies.d
     expect_equal_object_files reference_test.o test.o
 
@@ -117,9 +117,9 @@ SUITE_direct_gcc() {
     TEST "SUNPRO_DEPENDENCIES environment variable with target"
 
     SUNPRO_DEPENDENCIES="other.d target.o" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected_sunpro_dependencies_target.d
 
     SUNPRO_DEPENDENCIES="other.d target.o" $REAL_COMPILER -c test.c -o reference_test.o
@@ -127,16 +127,16 @@ SUITE_direct_gcc() {
 
     rm -f other.d
     SUNPRO_DEPENDENCIES="other.d target.o" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content other.d expected_sunpro_dependencies_target.d
     expect_equal_object_files reference_test.o test.o
 
     SUNPRO_DEPENDENCIES="different_name.d target.o" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_equal_content different_name.d expected_sunpro_dependencies_target.d
     expect_equal_object_files reference_test.o test.o
 
@@ -144,12 +144,12 @@ SUITE_direct_gcc() {
     TEST "DEPENDENCIES_OUTPUT environment variable set to /dev/null"
 
     DEPENDENCIES_OUTPUT="/dev/null" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     DEPENDENCIES_OUTPUT="other.d" $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 }
index 72d76d2bf74847b6d4a9f29ff47d1973b7ca4159..3ae1b462e9a10a9abcd5a73b698493f03d213fbf 100644 (file)
@@ -14,16 +14,16 @@ SUITE_fileclone() {
     $REAL_COMPILER -c -o reference_test.o test.c
 
     CCACHE_FILECLONE=1 $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_equal_object_files reference_test.o test.o
 
     # Note: CCACHE_DEBUG=1 below is needed for the test case.
     CCACHE_FILECLONE=1 CCACHE_DEBUG=1 $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_equal_object_files reference_test.o test.o
     if ! grep -q 'Cloning.*to test.o' test.o.ccache-log; then
         test_failed "Did not try to clone file"
@@ -40,16 +40,16 @@ SUITE_fileclone() {
     $REAL_COMPILER -c -o reference_test.o test.c
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test.o test.o
 
     # Note: CCACHE_DEBUG=1 below is needed for the test case.
     CCACHE_FILECLONE=1 CCACHE_DEBUG=1 $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test.o test.o
     if grep -q 'Cloning' test.o.ccache-log; then
         test_failed "Tried to clone"
index e37edce5323449dddbdb83e25ad03edff8af2309..76b0f29483e60f9d9bd36893f9a73cb1198b4787 100644 (file)
@@ -16,17 +16,17 @@ SUITE_hardlink() {
     $REAL_COMPILER -c -o reference_test1.o test1.c
 
     CCACHE_HARDLINK=1 $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_equal_object_files reference_test1.o test1.o
 
     mv test1.o test1.o.saved
 
     CCACHE_HARDLINK=1 $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     if [ ! test1.o -ef test1.o.saved ]; then
         test_failed "Object files not hard linked"
     fi
@@ -37,16 +37,16 @@ SUITE_hardlink() {
     generate_code 1 test1.c
 
     CCACHE_HARDLINK=1 $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     mv test1.o test1.o.saved
 
     CCACHE_HARDLINK=1 $CCACHE_COMPILE -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     # -------------------------------------------------------------------------
     TEST "Overwrite assembler"
@@ -57,25 +57,25 @@ SUITE_hardlink() {
     $REAL_COMPILER -c -o reference_test1.o test1.s
 
     CCACHE_HARDLINK=1 $CCACHE_COMPILE -c test1.s
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     generate_code 2 test1.c
     $REAL_COMPILER -S -o test1.s test1.c
 
     CCACHE_HARDLINK=1 $CCACHE_COMPILE -c test1.s
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 4
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 4
 
     generate_code 1 test1.c
     $REAL_COMPILER -S -o test1.s test1.c
 
     CCACHE_HARDLINK=1 $CCACHE_COMPILE -c test1.s
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 4
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 4
     expect_equal_object_files reference_test1.o test1.o
 
     # -------------------------------------------------------------------------
@@ -86,11 +86,11 @@ SUITE_hardlink() {
     generate_code 1 test1.c
 
     CCACHE_HARDLINK=1 CCACHE_DEPEND=1 $CCACHE_COMPILE -c -MMD -MF test1.d.tmp test1.c
-    expect_stat 'cache hit (direct)' 0
+    expect_stat direct_cache_hit 0
     mv test1.d.tmp test1.d || test_failed "first mv failed"
 
     CCACHE_HARDLINK=1 CCACHE_DEPEND=1 $CCACHE_COMPILE -c -MMD -MF test1.d.tmp test1.c
-    expect_stat 'cache hit (direct)' 1
+    expect_stat direct_cache_hit 1
     mv test1.d.tmp test1.d || test_failed "second mv failed"
 
     # -------------------------------------------------------------------------
@@ -103,22 +103,22 @@ SUITE_hardlink() {
     echo "int x;" >test1.c
 
     $CCACHE_COMPILE -c -MMD test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
     expect_content test1.d "test1.o: test1.c"
 
     touch test1.h
     echo '#include "test1.h"' >>test1.c
 
     $CCACHE_COMPILE -c -MMD test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
     expect_content test1.d "test1.o: test1.c test1.h"
 
     echo "int x;" >test1.c
 
     $CCACHE_COMPILE -c -MMD test1.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
     expect_content test1.d "test1.o: test1.c"
 }
index ef9c92490dac28897015373de87aa5157bd0565d..e328ac662fca204bf5a6513756853970482d8b9a 100644 (file)
@@ -29,7 +29,7 @@ expect_inode_cache_type() {
     local log_file=$(echo $source_file | sed 's/\.c$/.o.ccache-log/')
     local actual=$(grep -c "inode cache $type: $source_file" "$log_file")
     if [ $actual -ne $expected ]; then
-        test_failed "Found $actual (expected $expected) $type for $source_file"
+        test_failed_internal "Found $actual (expected $expected) $type for $source_file"
     fi
 }
 
@@ -78,9 +78,9 @@ inode_cache_tests() {
     expect_inode_cache 1 0 0 test2.c
 
     # -------------------------------------------------------------------------
-    TEST "Soft link"
+    TEST "Symbolic link"
 
-    echo "// soft linked" > test1.c
+    echo "// symbolically linked" > test1.c
     ln -fs test1.c test2.c
     $CCACHE_COMPILE -c test1.c
     $CCACHE_COMPILE -c test2.c
index 4d527fba5c4fa3cdd0c1e0f06466f892311e3e5a..a7bb40511d3da298d035d1b5d2b34832b8a8127c 100644 (file)
@@ -12,18 +12,18 @@ SUITE_input_charset() {
     printf '#include <wchar.h>\nwchar_t foo[] = L"\xbf";\n' >latin1.c
 
     $CCACHE_COMPILE -c -finput-charset=latin1 latin1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c -finput-charset=latin1 latin1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     CCACHE_NOCPP2=1 $CCACHE_COMPILE -c -finput-charset=latin1 latin1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     CCACHE_NOCPP2=1 $CCACHE_COMPILE -c -finput-charset=latin1 latin1.c
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
 }
index ca8822170338330222871b6b376c68378bd5fcea..22bf0b52a1ef1bfe745b304b90e3a500ce0bc965 100644 (file)
@@ -27,18 +27,18 @@ SUITE_ivfsoverlay() {
     TEST "without sloppy ivfsoverlay"
 
     $CCACHE_COMPILE -ivfsoverlay test.yaml -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 0
-    expect_stat 'unsupported compiler option' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat unsupported_compiler_option 1
 
     # -------------------------------------------------------------------------
     TEST "with sloppy ivfsoverlay"
 
-    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS ivfsoverlay" $CCACHE_COMPILE -ivfsoverlay test.yaml -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS include_file_mtime ivfsoverlay" $CCACHE_COMPILE -ivfsoverlay test.yaml -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
-    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS ivfsoverlay" $CCACHE_COMPILE -ivfsoverlay test.yaml -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
+    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS include_file_mtime ivfsoverlay" $CCACHE_COMPILE -ivfsoverlay test.yaml -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
 }
index dea1513938d0ec833464d607dd6bec8d4a648270..cf371318bf37e52f5019d3878c50d63373d8ae7a 100644 (file)
@@ -22,15 +22,15 @@ SUITE_masquerading() {
     $REAL_COMPILER -c -o reference_test1.o test1.c
 
     ./$COMPILER_BIN $COMPILER_ARGS -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 
     ./$COMPILER_BIN $COMPILER_ARGS -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 
     # -------------------------------------------------------------------------
@@ -39,14 +39,14 @@ SUITE_masquerading() {
     $REAL_COMPILER -c -o reference_test1.o test1.c
 
     $PWD/$COMPILER_BIN $COMPILER_ARGS -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 
     $PWD/$COMPILER_BIN $COMPILER_ARGS -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_object_files reference_test1.o test1.o
 }
index 51ddb2a3e4a73c1d42b0ef9e80b0a9ca14eae8ee..bd5ada924555e872da1d54fc1e87157f30294ef8 100644 (file)
@@ -34,39 +34,39 @@ SUITE_modules() {
     # -------------------------------------------------------------------------
     TEST "fall back to real compiler, no sloppiness"
 
-    $CCACHE_COMPILE -fmodules -fcxx-modules -c test1.cpp -MD
-    expect_stat "can't use modules" 1
+    $CCACHE_COMPILE -x c++ -fmodules -fcxx-modules -c test1.cpp -MD
+    expect_stat could_not_use_modules 1
 
-    $CCACHE_COMPILE -fmodules -fcxx-modules -c test1.cpp -MD
-    expect_stat "can't use modules" 2
+    $CCACHE_COMPILE -x c++ -fmodules -fcxx-modules -c test1.cpp -MD
+    expect_stat could_not_use_modules 2
 
     # -------------------------------------------------------------------------
     TEST "fall back to real compiler, no depend mode"
 
     unset CCACHE_DEPEND
 
-    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -fmodules -fcxx-modules -c test1.cpp -MD
-    expect_stat "can't use modules" 1
+    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -x c++ -fmodules -fcxx-modules -c test1.cpp -MD
+    expect_stat could_not_use_modules 1
 
-    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -fmodules -fcxx-modules -c test1.cpp -MD
-    expect_stat "can't use modules" 2
+    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -x c++ -fmodules -fcxx-modules -c test1.cpp -MD
+    expect_stat could_not_use_modules 2
 
     # -------------------------------------------------------------------------
     TEST "cache hit"
 
-    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -fmodules -fcxx-modules -c test1.cpp -MD
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -x c++ -fmodules -fcxx-modules -c test1.cpp -MD
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
-    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -fmodules -fcxx-modules -c test1.cpp -MD
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
+    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -x c++ -fmodules -fcxx-modules -c test1.cpp -MD
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "cache miss"
 
-    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -MD -fmodules -fcxx-modules -c test1.cpp -MD
-    expect_stat 'cache miss' 1
+    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -MD -x c++ -fmodules -fcxx-modules -c test1.cpp -MD
+    expect_stat cache_miss 1
 
     cat <<EOF >test1.h
 #include <string>
@@ -74,12 +74,12 @@ void f();
 EOF
     backdate test1.h
 
-    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -MD -fmodules -fcxx-modules -c test1.cpp -MD
-    expect_stat 'cache miss' 2
+    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -MD -x c++ -fmodules -fcxx-modules -c test1.cpp -MD
+    expect_stat cache_miss 2
 
     echo >>module.modulemap
     backdate test1.h
 
-    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -MD -fmodules -fcxx-modules -c test1.cpp -MD
-    expect_stat 'cache miss' 3
+    CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS modules" $CCACHE_COMPILE -MD -x c++ -fmodules -fcxx-modules -c test1.cpp -MD
+    expect_stat cache_miss 3
 }
index 786b71beddfee2fd23137cc1494c87c7d6883b33..2f5c77bb2430bd3b0bc3ad06097e738195735d68 100644 (file)
@@ -16,25 +16,34 @@ SUITE_multi_arch() {
 
     # Different arches shouldn't affect each other
     $CCACHE_COMPILE -arch i386 -c test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -arch x86_64 -c test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -arch i386 -c test1.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
 
     # Multiple arches should be cached too
     $CCACHE_COMPILE -arch i386 -arch x86_64 -c test1.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 3
 
     $CCACHE_COMPILE -arch i386 -arch x86_64 -c test1.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 3
+
+    # A single -Xarch_* matching -arch is supported.
+    $CCACHE_COMPILE -arch x86_64 -Xarch_x86_64 -I. -c test1.c
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 4
+
+    $CCACHE_COMPILE -arch x86_64 -Xarch_x86_64 -I. -c test1.c
+    expect_stat direct_cache_hit 3
+    expect_stat cache_miss 4
 
     # -------------------------------------------------------------------------
     TEST "cache hit, preprocessor mode"
@@ -42,23 +51,23 @@ SUITE_multi_arch() {
     export CCACHE_NODIRECT=1
 
     $CCACHE_COMPILE -arch i386 -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -arch x86_64 -c test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -arch i386 -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     # Multiple arches should be cached too
     $CCACHE_COMPILE -arch i386 -arch x86_64 -c test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 3
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 3
 
     $CCACHE_COMPILE -arch i386 -arch x86_64 -c test1.c
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 3
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 3
 }
index 60afe71820de722f854fcf3d41cddcb890e5b5cd..a7cb7bbd6b86fe255b7a8bd367415607ca9c7ee8 100644 (file)
@@ -12,15 +12,15 @@ SUITE_no_compression() {
     $REAL_COMPILER -c -o reference_test.o test.c
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_equal_object_files reference_test.o test.o
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_equal_object_files reference_test.o test.o
 
     # -------------------------------------------------------------------------
@@ -39,42 +39,42 @@ SUITE_no_compression() {
     TEST "Hash sum equal for compressed and uncompressed files"
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
 
     unset CCACHE_NOCOMPRESS
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Corrupt result file"
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     result_file=$(find $CCACHE_DIR -name '*R')
     printf foo | dd of=$result_file bs=3 count=1 seek=20 conv=notrunc >&/dev/null
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 2
 
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 2
 }
index 386015df9e6f434b09b5a61b04448155a7340750..bf435c7181c7de66d6ceeb60a454be515b96df06 100644 (file)
@@ -67,15 +67,15 @@ nvcc_tests() {
 
     # First compile.
     $ccache_nvcc_cpp test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     $ccache_nvcc_cpp test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     # -------------------------------------------------------------------------
@@ -92,39 +92,39 @@ nvcc_tests() {
     expect_different_content reference_test2.dump reference_test3.dump
 
     $ccache_nvcc_cuda test_cuda.cu
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test1.dump test1.dump
 
     # Other GPU.
     $ccache_nvcc_cuda $nvcc_opts_gpu1 test_cuda.cu
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 2
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test2.dump test1.dump
 
     $ccache_nvcc_cuda $nvcc_opts_gpu1 test_cuda.cu
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 2
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test2.dump test1.dump
 
     # Another GPU.
     $ccache_nvcc_cuda $nvcc_opts_gpu2 test_cuda.cu
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 3
-    expect_stat 'files in cache' 3
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 3
+    expect_stat files_in_cache 3
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test3.dump test1.dump
 
     $ccache_nvcc_cuda $nvcc_opts_gpu2 test_cuda.cu
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 3
-    expect_stat 'files in cache' 3
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 3
+    expect_stat files_in_cache 3
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test3.dump test1.dump
 
@@ -135,16 +135,16 @@ nvcc_tests() {
     $cuobjdump reference_test4.o > reference_test4.dump
 
     $ccache_nvcc_cuda -dc -o test_cuda.o test_cuda.cu
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     $cuobjdump test_cuda.o > test4.dump
     expect_equal_content test4.dump reference_test4.dump
 
     $ccache_nvcc_cuda -dc -o test_cuda.o test_cuda.cu
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     $cuobjdump test_cuda.o > test4.dump
     expect_equal_content test4.dump reference_test4.dump
 
@@ -156,29 +156,29 @@ nvcc_tests() {
     expect_different_content reference_test1.o reference_test2.o
 
     $ccache_nvcc_cpp test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     # Specified define, but unused. Can only be found by preprocessed mode.
     $ccache_nvcc_cpp -DDUMMYENV=1 test_cpp.cu
-    expect_stat "cache hit (preprocessed)" 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     # Specified used define.
     $ccache_nvcc_cpp -DNUM=10 test_cpp.cu
-    expect_stat "cache hit (preprocessed)" 1
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 2
     expect_equal_content reference_test2.o test_cpp.o
 
     $ccache_nvcc_cpp -DNUM=10 test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 2
     expect_equal_content reference_test2.o test_cpp.o
 
     # -------------------------------------------------------------------------
@@ -189,27 +189,27 @@ nvcc_tests() {
     expect_different_content reference_test1.o reference_test2.o
 
     $ccache_nvcc_cpp -optf test1.optf test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     $ccache_nvcc_cpp -optf test1.optf test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     $ccache_nvcc_cpp -optf test2.optf test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 2
     expect_equal_content reference_test2.o test_cpp.o
 
     $ccache_nvcc_cpp -optf test2.optf test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 2
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 2
     expect_equal_content reference_test2.o test_cpp.o
 
     # -------------------------------------------------------------------------
@@ -220,15 +220,15 @@ nvcc_tests() {
 
     # First compile.
     $ccache_nvcc_cpp --compiler-bindir $REAL_COMPILER_BIN test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     $ccache_nvcc_cpp --compiler-bindir $REAL_COMPILER_BIN test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     # -------------------------------------------------------------------------
@@ -239,15 +239,15 @@ nvcc_tests() {
 
     # First compile.
     $ccache_nvcc_cpp -ccbin $REAL_COMPILER_BIN test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     $ccache_nvcc_cpp -ccbin $REAL_COMPILER_BIN test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     # -------------------------------------------------------------------------
@@ -258,15 +258,15 @@ nvcc_tests() {
 
     # First compile.
     $ccache_nvcc_cpp --output-directory . test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     $ccache_nvcc_cpp --output-directory . test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     # -------------------------------------------------------------------------
@@ -276,29 +276,29 @@ nvcc_tests() {
 
     # First compile.
     $ccache_nvcc_cpp -odir . test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     $ccache_nvcc_cpp -odir . test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content reference_test1.o test_cpp.o
 
     # -------------------------------------------------------------------------
     TEST "Option -Werror"
 
     $ccache_nvcc_cpp -Werror cross-execution-space-call test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
 
     $ccache_nvcc_cpp -Werror cross-execution-space-call test_cpp.cu
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
 }
 
 SUITE_nvcc_PROBE() {
index 3762153a0f2eaf911df12bb1219021ab2dffae81..873e659cba4946ea4d7db98e4c6e0fcecb603fa0 100644 (file)
@@ -29,15 +29,15 @@ SUITE_nvcc_direct() {
 
     # First compile.
     $ccache_nvcc_cpp test_cpp.cu
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_equal_content reference_test1.o test_cpp.o
 
     $ccache_nvcc_cpp test_cpp.cu
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_equal_content reference_test1.o test_cpp.o
 
     # -------------------------------------------------------------------------
@@ -54,39 +54,39 @@ SUITE_nvcc_direct() {
     expect_different_content reference_test2.dump reference_test3.dump
 
     $ccache_nvcc_cuda test_cuda.cu
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test1.dump test1.dump
 
     # Other GPU.
     $ccache_nvcc_cuda $nvcc_opts_gpu1 test_cuda.cu
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 4
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 4
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test2.dump test1.dump
 
     $ccache_nvcc_cuda $nvcc_opts_gpu1 test_cuda.cu
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 4
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 4
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test2.dump test1.dump
 
     # Another GPU.
     $ccache_nvcc_cuda $nvcc_opts_gpu2 test_cuda.cu
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 3
-    expect_stat 'files in cache' 6
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 3
+    expect_stat files_in_cache 6
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test3.dump test1.dump
 
     $ccache_nvcc_cuda $nvcc_opts_gpu2 test_cuda.cu
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 3
-    expect_stat 'files in cache' 6
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 3
+    expect_stat files_in_cache 6
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test3.dump test1.dump
 
@@ -98,30 +98,30 @@ SUITE_nvcc_direct() {
     expect_different_content reference_test1.o reference_test2.o
 
     $ccache_nvcc_cpp test_cpp.cu
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_equal_content reference_test1.o test_cpp.o
 
     # Specified define, but unused. Can only be found by preprocessed mode.
     $ccache_nvcc_cpp -DDUMMYENV=1 test_cpp.cu
-    expect_stat "cache hit (preprocessed)" 1
-    expect_stat "cache hit (direct)" 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 3
+    expect_stat preprocessed_cache_hit 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 3
     expect_equal_content reference_test1.o test_cpp.o
 
     # Specified used define.
     $ccache_nvcc_cpp -DNUM=10 test_cpp.cu
-    expect_stat "cache hit (direct)" 0
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 5
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 5
     expect_equal_content reference_test2.o test_cpp.o
 
     $ccache_nvcc_cpp -DNUM=10 test_cpp.cu
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 5
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 5
     expect_equal_content reference_test2.o test_cpp.o
 
     # -------------------------------------------------------------------------
@@ -132,26 +132,26 @@ SUITE_nvcc_direct() {
     expect_different_content reference_test1.o reference_test2.o
 
     $ccache_nvcc_cpp -optf test1.optf test_cpp.cu
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_equal_content reference_test1.o test_cpp.o
 
     $ccache_nvcc_cpp -optf test1.optf test_cpp.cu
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_equal_content reference_test1.o test_cpp.o
 
     $ccache_nvcc_cpp -optf test2.optf test_cpp.cu
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 4
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 4
     expect_equal_content reference_test2.o test_cpp.o
 
     $ccache_nvcc_cpp -optf test2.optf test_cpp.cu
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 4
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 4
     expect_equal_content reference_test2.o test_cpp.o
 }
index a19432ad532532659af640eba91f83031ad46b92..e18c664fc85314a70aa8a38d3a192d448d639280 100644 (file)
@@ -22,6 +22,11 @@ SUITE_nvcc_ldir_PROBE() {
     elif [ ! -d $nvcc_idir ]; then
         echo "include directory $nvcc_idir not found"
     fi
+
+    echo "int main() { return 0; }" | $REAL_NVCC -Wno-deprecated-gpu-targets -ccbin $REAL_COMPILER_BIN -c -x cu - 
+    if [ $? -ne 0 ]; then
+        echo "nvcc of a canary failed; Is CUDA compatible with the host compiler ($REAL_COMPILER_BIN)?"
+    fi
 }
 
 SUITE_nvcc_ldir_SETUP() {
@@ -53,16 +58,16 @@ SUITE_nvcc_ldir() {
 
     # First compile.
     $ccache_nvcc_cuda $TEST_OPTS test_cuda.cu
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test1.dump test1.dump
 
     $ccache_nvcc_cuda $TEST_OPTS test_cuda.cu
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test1.dump test1.dump
 
@@ -75,16 +80,16 @@ SUITE_nvcc_ldir() {
 
     # First compile.
     $ccache_nvcc_cuda $TEST_OPTS test_cuda.cu
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test1.dump test1.dump
 
     $ccache_nvcc_cuda $TEST_OPTS test_cuda.cu
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     $cuobjdump test_cuda.o > test1.dump
     expect_equal_content reference_test1.dump test1.dump
 
index 97a5c5e0a7c6b3284a244f7fa0681cd95e68a351..ad77c8224d73c5c1f98d8b700436a4d6509eb7e5 100644 (file)
@@ -66,56 +66,56 @@ pch_suite_common() {
     TEST "Create .gch, -c, no -o, without opt-in"
 
     $CCACHE_COMPILE $SYSROOT -c pch.h
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
-    expect_stat "can't use precompiled header" 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat could_not_use_precompiled_header 1
 
     # -------------------------------------------------------------------------
     TEST "Create .gch, no -c, -o, without opt-in"
 
     $CCACHE_COMPILE pch.h -o pch.gch
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
-    expect_stat "can't use precompiled header" 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat could_not_use_precompiled_header 1
 
     # -------------------------------------------------------------------------
     TEST "Create .gch, -c, no -o, with opt-in"
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -c pch.h
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     rm pch.h.gch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -c pch.h
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists pch.h.gch
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
     rm pch.h.gch
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -c pch.h
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
     expect_exists pch.h.gch
 
     # -------------------------------------------------------------------------
     TEST "Create .gch, no -c, -o, with opt-in"
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT pch.h -o pch.gch
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT pch.h -o pch.gch
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists pch.gch
 
     # -------------------------------------------------------------------------
@@ -126,12 +126,12 @@ pch_suite_common() {
     rm pch.h
 
     $CCACHE_COMPILE $SYSROOT -c pch.c 2>/dev/null
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
     # Preprocessor error because GCC can't find the real include file when
     # trying to preprocess (gcc -E will be called by ccache):
-    expect_stat 'preprocessor error' 1
+    expect_stat preprocessor_error 1
 
     # -------------------------------------------------------------------------
     TEST "Use .gch, -include, no sloppiness"
@@ -140,11 +140,11 @@ pch_suite_common() {
     backdate pch.h.gch
 
     $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
     # Must enable sloppy time macros:
-    expect_stat "can't use precompiled header" 1
+    expect_stat could_not_use_precompiled_header 1
 
     # -------------------------------------------------------------------------
     TEST "Use .gch, -include"
@@ -153,14 +153,14 @@ pch_suite_common() {
     backdate pch.h.gch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
@@ -168,14 +168,14 @@ pch_suite_common() {
     backdate pch.h.gch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Use .gch, preprocessor mode, -include"
@@ -184,14 +184,14 @@ pch_suite_common() {
     backdate pch.h.gch
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
@@ -199,28 +199,28 @@ pch_suite_common() {
     backdate pch.h.gch
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Create .gch, -c, -o"
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -c pch.h -o pch.h.gch
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     rm -f pch.h.gch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -c pch.h -o pch.h.gch
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists pch.h.gch
 
     echo '#include <string.h> /*change pch*/' >>pch.h
@@ -228,9 +228,9 @@ pch_suite_common() {
     rm pch.h.gch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -c pch.h -o pch.h.gch
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
     expect_exists pch.h.gch
 
     # -------------------------------------------------------------------------
@@ -242,26 +242,26 @@ pch_suite_common() {
     echo "original checksum" > pch.h.gch.sum
 
     CCACHE_PCH_EXTSUM=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_PCH_EXTSUM=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     echo "other checksum" > pch.h.gch.sum
     CCACHE_PCH_EXTSUM=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     echo "original checksum" > pch.h.gch.sum
     CCACHE_PCH_EXTSUM=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # With GCC, a newly generated PCH is always different, even if the contents
     # should be exactly the same. And Clang stores file timestamps, so in this
@@ -274,9 +274,9 @@ pch_suite_common() {
     backdate pch.h.gch
 
     CCACHE_PCH_EXTSUM=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 3
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 3
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Use .gch, -include, no PCH_EXTSUM"
@@ -287,21 +287,21 @@ pch_suite_common() {
     echo "original checksum" > pch.h.gch.sum
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # external checksum not used, so no cache miss when changed
     echo "other checksum" > pch.h.gch.sum
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Use .gch, -include, other dir for .gch"
@@ -312,14 +312,14 @@ pch_suite_common() {
     rm -f pch.h.gch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include dir/pch.h pch2.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include dir/pch.h pch2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
@@ -327,14 +327,14 @@ pch_suite_common() {
     backdate dir/pch.h.gch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include dir/pch.h pch2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include dir/pch.h pch2.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
     rm -rf dir
 
     # -------------------------------------------------------------------------
@@ -346,14 +346,14 @@ pch_suite_common() {
     rm -f pch.h.gch
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include dir/pch.h pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include dir/pch.h pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
@@ -361,14 +361,14 @@ pch_suite_common() {
     backdate dir/pch.h.gch
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include dir/pch.h pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include dir/pch.h pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
     rm -rf dir
 
     # -------------------------------------------------------------------------
@@ -378,14 +378,14 @@ pch_suite_common() {
     backdate pch.h.gch
 
     CCACHE_DEPEND=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c -MD -MF pch.d
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_DEPEND=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c -MD -MF pch.d
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
@@ -393,14 +393,14 @@ pch_suite_common() {
     backdate pch.h.gch
 
     CCACHE_DEPEND=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c -MD -MF pch.d
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     CCACHE_DEPEND=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c -MD -MF pch.d
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 }
 
 pch_suite_gcc() {
@@ -412,14 +412,14 @@ pch_suite_gcc() {
     rm pch.h
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Use .gch, #include, no sloppiness"
@@ -429,10 +429,10 @@ pch_suite_gcc() {
     rm pch.h
 
     $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
     # Must enable sloppy time macros:
-    expect_stat "can't use precompiled header" 1
+    expect_stat could_not_use_precompiled_header 1
 
     # -------------------------------------------------------------------------
     TEST "Use .gch, #include"
@@ -442,14 +442,14 @@ pch_suite_gcc() {
     rm pch.h
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
@@ -457,14 +457,14 @@ pch_suite_gcc() {
     backdate pch.h.gch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Use .gch, preprocessor mode, #include"
@@ -474,14 +474,14 @@ pch_suite_gcc() {
     rm pch.h
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
@@ -489,14 +489,14 @@ pch_suite_gcc() {
     backdate pch.h.gch
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Create and use .gch directory"
@@ -504,41 +504,41 @@ pch_suite_gcc() {
     mkdir pch.h.gch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -x c-header -c pch.h -o pch.h.gch/foo
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     rm pch.h.gch/foo
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -x c-header -c pch.h -o pch.h.gch/foo
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists pch.h.gch/foo
 
     backdate pch.h.gch/foo
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     echo "updated" >>pch.h.gch/foo # GCC seems to cope with this...
     backdate pch.h.gch/foo
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 3
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 3
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 3
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 3
 
     # -------------------------------------------------------------------------
     TEST "Use .gch, #include, PCH_EXTSUM=1"
@@ -549,26 +549,26 @@ pch_suite_gcc() {
     echo "original checksum" > pch.h.gch.sum
 
     CCACHE_PCH_EXTSUM=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_PCH_EXTSUM=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     echo "other checksum" > pch.h.gch.sum
     CCACHE_PCH_EXTSUM=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     echo "original checksum" > pch.h.gch.sum
     CCACHE_PCH_EXTSUM=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # With GCC, a newly generated PCH is always different, even if the contents
     # should be exactly the same. And Clang stores file timestamps, so in this
@@ -581,9 +581,9 @@ pch_suite_gcc() {
     backdate pch.h.gch
 
     CCACHE_PCH_EXTSUM=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 3
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 3
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Use .gch, #include, no PCH_EXTSUM"
@@ -594,21 +594,21 @@ pch_suite_gcc() {
     echo "original checksum" > pch.h.gch.sum
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # external checksum not used, so no cache miss when changed
     echo "other checksum" > pch.h.gch.sum
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -fpch-preprocess pch.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Too new PCH file"
@@ -628,10 +628,10 @@ pch_suite_gcc() {
     touch -d "@$(($(date +%s) + 60))" lib.h.gch # 1 minute in the future
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines,time_macros" $CCACHE_COMPILE -include lib.h -c main.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
-    expect_stat "can't use precompiled header" 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat could_not_use_precompiled_header 1
 }
 
 pch_suite_clang() {
@@ -649,25 +649,25 @@ EOF
     sleep 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -c pch2.h
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     touch test.h
     sleep 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -c pch2.h
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     $REAL_COMPILER $SYSROOT -c -include pch2.h pch2.c
     expect_exists pch2.o
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -c pch2.h
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Use .pch, -include, no sloppiness"
@@ -676,11 +676,11 @@ EOF
     backdate pch.h.pch
 
     $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
     # Must enable sloppy time macros:
-    expect_stat "can't use precompiled header" 1
+    expect_stat could_not_use_precompiled_header 1
 
     # -------------------------------------------------------------------------
     TEST "Use .pch, -include"
@@ -689,14 +689,14 @@ EOF
     backdate pch.h.pch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
@@ -704,9 +704,9 @@ EOF
     backdate pch.h.pch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Use .pch, preprocessor mode, -include"
@@ -715,14 +715,14 @@ EOF
     backdate pch.h.pch
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
@@ -730,14 +730,14 @@ EOF
     backdate pch.h.pch
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include pch.h pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Use .pch, -include-pch"
@@ -746,14 +746,14 @@ EOF
     backdate pch.h.pch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include-pch pch.h.pch pch2.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include-pch pch.h.pch pch2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
@@ -761,9 +761,9 @@ EOF
     backdate pch.h.pch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include-pch pch.h.pch pch2.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Use .pch, preprocessor mode, -include-pch"
@@ -772,14 +772,14 @@ EOF
     backdate pch.h.pch
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include-pch pch.h.pch pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include-pch pch.h.pch pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
@@ -787,37 +787,37 @@ EOF
     backdate pch.h.pch
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include-pch pch.h.pch pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     CCACHE_NODIRECT=1 CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -c -include-pch pch.h.pch pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 2
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 2
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Create .pch with -Xclang options"
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -Xclang -emit-pch -o pch.h.pch -c pch.h
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     rm pch.h.pch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -Xclang -emit-pch -o pch.h.pch -c pch.h
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists pch.h.pch
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
     rm pch.h.pch
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS pch_defines" $CCACHE_COMPILE $SYSROOT -Xclang -emit-pch -o pch.h.pch -c pch.h
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
     expect_exists pch.h.pch
 
     # -------------------------------------------------------------------------
@@ -827,14 +827,14 @@ EOF
     backdate pch.h.pch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -Xclang -include-pch -Xclang pch.h.pch -Xclang -include -Xclang pch.h -c pch.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -Xclang -include-pch -Xclang pch.h.pch -Xclang -include -Xclang pch.h -c pch.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     echo '#include <string.h> /*change pch*/' >>pch.h
     backdate pch.h
@@ -842,12 +842,12 @@ EOF
     backdate pch.h.pch
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -Xclang -include-pch -Xclang pch.h.pch -Xclang -include -Xclang pch.h -c pch.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 
     CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS time_macros" $CCACHE_COMPILE $SYSROOT -Xclang -include-pch -Xclang pch.h.pch -Xclang -include -Xclang pch.h -c pch.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
 }
index dbe36d41f9211bedf600950be5bae854393c92f4..aa0c93ed094ac79222268448a18627086320da84 100644 (file)
@@ -21,32 +21,32 @@ SUITE_profiling() {
     TEST "-fprofile-use, missing file"
 
     $CCACHE_COMPILE -fprofile-use -c test.c 2>/dev/null
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 0
-    expect_stat 'no input file' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat no_input_file 1
 
     # -------------------------------------------------------------------------
     TEST "-fbranch-probabilities, missing file"
 
     $CCACHE_COMPILE -fbranch-probabilities -c test.c 2>/dev/null
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 0
-    expect_stat 'no input file' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat no_input_file 1
 
     # -------------------------------------------------------------------------
     TEST "-fprofile-use=file, missing file"
 
     $CCACHE_COMPILE -fprofile-use=data.gcda -c test.c 2>/dev/null
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 0
-    expect_stat 'no input file' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat no_input_file 1
 
     # -------------------------------------------------------------------------
     TEST "-fprofile-use"
 
     $CCACHE_COMPILE -fprofile-generate -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $COMPILER -fprofile-generate test.o -o test
 
@@ -54,19 +54,19 @@ SUITE_profiling() {
     merge_profiling_data .
 
     $CCACHE_COMPILE -fprofile-use -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -fprofile-use -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
 
     ./test
     merge_profiling_data .
 
     $CCACHE_COMPILE -fprofile-use -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 3
 
     # -------------------------------------------------------------------------
     TEST "-fprofile-use=dir"
@@ -74,8 +74,8 @@ SUITE_profiling() {
     mkdir data
 
     $CCACHE_COMPILE -fprofile-generate=data -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $COMPILER -fprofile-generate test.o -o test
 
@@ -83,19 +83,70 @@ SUITE_profiling() {
     merge_profiling_data data
 
     $CCACHE_COMPILE -fprofile-use=data -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -fprofile-use=data -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
 
     ./test
     merge_profiling_data data
 
     $CCACHE_COMPILE -fprofile-use=data -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 3
+
+    # -------------------------------------------------------------------------
+    TEST "-fprofile-generate=dir in different directories"
+
+    mkdir -p dir1/data dir2/data
+
+    cd dir1
+
+    $CCACHE_COMPILE -Werror -fprofile-generate=data -c ../test.c \
+        || test_failed "compilation error"
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+
+    $CCACHE_COMPILE -Werror -fprofile-generate=data -c ../test.c \
+        || test_failed "compilation error"
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+
+    $COMPILER -Werror -fprofile-generate test.o -o test \
+        || test_failed "compilation error"
+
+    ./test || test_failed "execution error"
+    merge_profiling_data data
+
+    $CCACHE_COMPILE -Werror -fprofile-use=data -c ../test.c \
+        || test_failed "compilation error"
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
+
+    cd ../dir2
+
+    $CCACHE_COMPILE -Werror -fprofile-generate=data -c ../test.c \
+        || test_failed "compilation error"
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 3
+
+    $CCACHE_COMPILE -Werror -fprofile-generate=data -c ../test.c \
+        || test_failed "compilation error"
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 3
+
+    $COMPILER -Werror -fprofile-generate test.o -o test \
+        || test_failed "compilation error"
+
+    ./test || test_failed "execution error"
+    merge_profiling_data data
+
+    $CCACHE_COMPILE -Werror -fprofile-use=data -c ../test.c \
+        || test_failed "compilation error"
+    # Note: No expect_stat here since GCC and Clang behave differently – just
+    # check that the compiler doesn't warn about not finding the profile data.
 
     # -------------------------------------------------------------------------
     TEST "-ftest-coverage with -fprofile-dir"
@@ -116,14 +167,14 @@ SUITE_profiling() {
             rm "$gcno_name"
 
             $CCACHE_COMPILE $flag -ftest-coverage -c $dir/test.c -o $dir/test.o
-            expect_stat 'cache hit (direct)' 0
-            expect_stat 'cache miss' 1
+            expect_stat direct_cache_hit 0
+            expect_stat cache_miss 1
             expect_exists "$gcno_name"
             rm "$gcno_name"
 
             $CCACHE_COMPILE $flag -ftest-coverage -c $dir/test.c -o $dir/test.o
-            expect_stat 'cache hit (direct)' 1
-            expect_stat 'cache miss' 1
+            expect_stat direct_cache_hit 1
+            expect_stat cache_miss 1
             expect_exists "$gcno_name"
             rm "$gcno_name"
         done
@@ -135,22 +186,22 @@ SUITE_profiling() {
     mkdir obj1 obj2
 
     $CCACHE_COMPILE -fprofile-arcs -c test.c -o obj1/test.o
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -fprofile-arcs -c test.c -o obj1/test.o
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -fprofile-arcs -c test.c -o obj2/test.o
     expect_different_content obj1/test.o obj2/test.o # different paths to .gcda file
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -fprofile-arcs -c test.c -o obj2/test.o
     expect_different_content obj1/test.o obj2/test.o # different paths to .gcda file
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 2
 }
 
 merge_profiling_data() {
index 8f8073d797888ad130c8fac0487281fed797724d..a6ac5c972b9933a7838e1a9dac01bf9f96da73f9 100644 (file)
@@ -22,8 +22,8 @@ SUITE_profiling_clang() {
     mkdir data
 
     $CCACHE_COMPILE -fprofile-generate=data -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $COMPILER -fprofile-generate=data test.o -o test
 
@@ -31,19 +31,19 @@ SUITE_profiling_clang() {
     llvm-profdata$CLANG_VERSION_SUFFIX merge -output foo.profdata data/default_*.profraw
 
     $CCACHE_COMPILE -fprofile-use=foo.profdata -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -fprofile-use=foo.profdata -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
 
     ./test
     llvm-profdata$CLANG_VERSION_SUFFIX merge -output foo.profdata data/default_*.profraw
 
     $CCACHE_COMPILE -fprofile-use=foo.profdata -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 3
 
     # -------------------------------------------------------------------------
     TEST "-fprofile-instr-use"
@@ -51,8 +51,8 @@ SUITE_profiling_clang() {
     mkdir data
 
     $CCACHE_COMPILE -fprofile-instr-generate -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $COMPILER -fprofile-instr-generate test.o -o test
 
@@ -60,25 +60,25 @@ SUITE_profiling_clang() {
     llvm-profdata$CLANG_VERSION_SUFFIX merge -output default.profdata default.profraw
 
     $CCACHE_COMPILE -fprofile-instr-use -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -fprofile-instr-use -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
 
     echo >>default.profdata  # Dummy change to trigger modification
 
     $CCACHE_COMPILE -fprofile-instr-use -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 3
 
     # -------------------------------------------------------------------------
     TEST "-fprofile-instr-use=file"
 
     $CCACHE_COMPILE -fprofile-instr-generate=foo.profraw -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $COMPILER -fprofile-instr-generate=data=foo.profraw test.o -o test
 
@@ -86,18 +86,18 @@ SUITE_profiling_clang() {
     llvm-profdata$CLANG_VERSION_SUFFIX merge -output foo.profdata foo.profraw
 
     $CCACHE_COMPILE -fprofile-instr-use=foo.profdata -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -fprofile-instr-use=foo.profdata -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
 
     echo >>foo.profdata  # Dummy change to trigger modification
 
     $CCACHE_COMPILE -fprofile-instr-use=foo.profdata -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 3
 
     # -------------------------------------------------------------------------
     TEST "-fprofile-sample-use"
@@ -105,30 +105,30 @@ SUITE_profiling_clang() {
     echo 'main:1:1' > sample.prof
 
     $CCACHE_COMPILE -fprofile-sample-use=sample.prof -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE -fprofile-sample-use=sample.prof -fprofile-sample-accurate -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -fprofile-sample-use=sample.prof -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -fprofile-sample-use=sample.prof -fprofile-sample-accurate -c test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 2
 
     echo 'main:2:2' > sample.prof
 
     $CCACHE_COMPILE -fprofile-sample-use=sample.prof -c test.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 3
 
      echo 'main:1:1' > sample.prof
 
     $CCACHE_COMPILE -fprofile-sample-use=sample.prof -c test.c
-    expect_stat 'cache hit (direct)' 3
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 3
+    expect_stat cache_miss 3
 }
index e67b6fa26eaf714d6ddf4de9319985969154a8a9..cd892c3ea018c550fb0a5e89199c83f7aebcbd7e 100644 (file)
@@ -14,26 +14,26 @@ SUITE_profiling_gcc() {
     TEST "-fbranch-probabilities"
 
     $CCACHE_COMPILE -fprofile-generate -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $COMPILER -fprofile-generate test.o -o test
 
     ./test
 
     $CCACHE_COMPILE -fbranch-probabilities -c test.c 2>/dev/null
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -fbranch-probabilities -c test.c 2>/dev/null
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
 
     ./test
 
     $CCACHE_COMPILE -fbranch-probabilities -c test.c 2>/dev/null
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 3
 
     # -------------------------------------------------------------------------
     TEST "-fprofile-dir=dir + -fprofile-use"
@@ -41,26 +41,26 @@ SUITE_profiling_gcc() {
     mkdir data
 
     $CCACHE_COMPILE -fprofile-dir=data -fprofile-generate -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $COMPILER -fprofile-generate test.o -o test
 
     ./test
 
     $CCACHE_COMPILE -fprofile-dir=data -fprofile-use -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -fprofile-dir=data -fprofile-use -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
 
     ./test
 
     $CCACHE_COMPILE -fprofile-dir=data -fprofile-use -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 3
 
     # -------------------------------------------------------------------------
     TEST "-fprofile-use + -fprofile-dir=dir"
@@ -68,26 +68,26 @@ SUITE_profiling_gcc() {
     mkdir data
 
     $CCACHE_COMPILE -fprofile-generate -fprofile-dir=data -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $COMPILER -fprofile-generate test.o -o test
 
     ./test
 
     $CCACHE_COMPILE -fprofile-use -fprofile-dir=data -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -fprofile-use -fprofile-dir=data -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
 
     ./test
 
     $CCACHE_COMPILE -fprofile-use -fprofile-dir=data -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 3
 
     # -------------------------------------------------------------------------
     TEST "-fprofile-dir=path1 + -fprofile-use=path2"
@@ -95,24 +95,24 @@ SUITE_profiling_gcc() {
     mkdir data
 
     $CCACHE_COMPILE -fprofile-dir=data2 -fprofile-generate=data -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $COMPILER -fprofile-generate test.o -o test
 
     ./test
 
     $CCACHE_COMPILE -fprofile-dir=data2 -fprofile-use=data -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE -fprofile-dir=data2 -fprofile-use=data -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
 
     ./test
 
     $CCACHE_COMPILE -fprofile-dir=data2 -fprofile-use=data -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 3
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 3
 }
index fa4418bd52c5c50c5f4ca6d2ac9d8198612eca3f..c54060d415ad2bf0fcda42b765b2a11d8a35135c 100644 (file)
@@ -19,37 +19,37 @@ SUITE_profiling_hip_clang() {
     hip_opts="-x hip --cuda-gpu-arch=gfx900 -nogpulib"
 
     $CCACHE_COMPILE $hip_opts -c test1.hip
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE $hip_opts -c test1.hip
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
 
     $CCACHE_COMPILE $hip_opts --cuda-gpu-arch=gfx906 -c test1.hip
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
 
     $CCACHE_COMPILE $hip_opts -c test2.hip
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 3
+    expect_stat preprocessed_cache_hit 0
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 3
 
     $CCACHE_COMPILE $hip_opts -c test2.hip
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 3
+    expect_stat preprocessed_cache_hit 0
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 3
 
     $CCACHE_COMPILE $hip_opts -Dx=x -c test2.hip
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 3
+    expect_stat preprocessed_cache_hit 1
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 3
 
     $CCACHE_COMPILE $hip_opts -Dx=y -c test2.hip
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 4
+    expect_stat preprocessed_cache_hit 1
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 4
 }
index 200f5c0de48426a57778f4490dcb5023fd9efa4a..fa673d8ede5f3e235999bd568d04e492307e3da2 100644 (file)
@@ -18,8 +18,8 @@ SUITE_readonly() {
 
     # Cache a compilation.
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     rm test.o
 
     # Make the cache read-only.
@@ -53,9 +53,9 @@ SUITE_readonly() {
     if [ $? -ne 0 ]; then
         test_failed "Failure when compiling test2.c read-only"
     fi
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 0
 
     # -------------------------------------------------------------------------
     # Check that read-only mode and direct mode work together.
@@ -63,8 +63,8 @@ SUITE_readonly() {
 
     # Cache a compilation.
     $CCACHE_COMPILE -c test.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     rm test.o
 
     # Make the cache read-only.
index be07104ff8606d36f836bc9b7a3825e59688369a..738c2bf7c555648f0024d18bf0042358048e617c 100644 (file)
@@ -18,25 +18,33 @@ SUITE_readonly_direct() {
     TEST "Direct hit"
 
     $CCACHE_COMPILE -c test.c -o test.o
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat direct_cache_miss 1
+    expect_stat preprocessed_cache_miss 1
 
     CCACHE_READONLY_DIRECT=1 $CCACHE_COMPILE -c test.c -o test.o
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat direct_cache_miss 1
+    expect_stat preprocessed_cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "Direct miss doesn't lead to preprocessed hit"
 
     $CCACHE_COMPILE -c test.c -o test.o
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat direct_cache_miss 1
+    expect_stat preprocessed_cache_miss 1
 
     CCACHE_READONLY_DIRECT=1 $CCACHE_COMPILE -DFOO -c test.c -o test.o
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 2
+    expect_stat direct_cache_miss 2
+    expect_stat preprocessed_cache_miss 1
 }
index de135f99d253159b33ffd5f103a9cb011ac7081a..8ca4362630406fec4db00d7c26637018f412d6f3 100644 (file)
@@ -21,26 +21,26 @@ SUITE_sanitize_blacklist() {
     $REAL_COMPILER -c -fsanitize-blacklist=blacklist.txt test1.c
 
     $CCACHE_COMPILE -c -fsanitize-blacklist=blacklist.txt test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     $CCACHE_COMPILE -c -fsanitize-blacklist=blacklist.txt test1.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     echo "fun:bar" >blacklist.txt
 
     $CCACHE_COMPILE -c -fsanitize-blacklist=blacklist.txt test1.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 4
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 4
 
     $CCACHE_COMPILE -c -fsanitize-blacklist=blacklist.txt test1.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 4
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 4
 
     # -------------------------------------------------------------------------
     TEST "Unsuccessful compilation"
@@ -55,7 +55,7 @@ SUITE_sanitize_blacklist() {
         test_failed "Expected an error compiling test1.c"
     fi
 
-    expect_stat 'error hashing extra file' 1
+    expect_stat error_hashing_extra_file 1
 
     # -------------------------------------------------------------------------
     TEST "Multiple -fsanitize-blacklist"
@@ -63,24 +63,24 @@ SUITE_sanitize_blacklist() {
     $REAL_COMPILER -c -fsanitize-blacklist=blacklist2.txt -fsanitize-blacklist=blacklist.txt test1.c
 
     $CCACHE_COMPILE -c -fsanitize-blacklist=blacklist2.txt -fsanitize-blacklist=blacklist.txt test1.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     $CCACHE_COMPILE -c -fsanitize-blacklist=blacklist2.txt -fsanitize-blacklist=blacklist.txt test1.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     echo "fun_2:foo" >blacklist2.txt
 
     $CCACHE_COMPILE -c -fsanitize-blacklist=blacklist2.txt -fsanitize-blacklist=blacklist.txt test1.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 4
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 4
 
     $CCACHE_COMPILE -c -fsanitize-blacklist=blacklist2.txt -fsanitize-blacklist=blacklist.txt test1.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 2
-    expect_stat 'files in cache' 4
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 4
 }
diff --git a/test/suites/secondary_file.bash b/test/suites/secondary_file.bash
new file mode 100644 (file)
index 0000000..5fc5183
--- /dev/null
@@ -0,0 +1,249 @@
+# This test suite verified both the file storage backend and the secondary
+# storage framework itself.
+
+SUITE_secondary_file_SETUP() {
+    unset CCACHE_NODIRECT
+    export CCACHE_SECONDARY_STORAGE="file:$PWD/secondary"
+
+    generate_code 1 test.c
+}
+
+SUITE_secondary_file() {
+    # -------------------------------------------------------------------------
+    TEST "Base case"
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_stat primary_storage_hit 0
+    expect_stat primary_storage_miss 2 # result + manifest
+    expect_stat secondary_storage_hit 0
+    expect_stat secondary_storage_miss 2 # result + manifest
+    expect_exists secondary/CACHEDIR.TAG
+    subdirs=$(find secondary -type d | wc -l)
+    if [ "${subdirs}" -lt 2 ]; then # "secondary" itself counts as one
+        test_failed "Expected subdirectories in secondary"
+    fi
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat primary_storage_hit 2 # result + manifest
+    expect_stat primary_storage_miss 2 # result + manifest
+    expect_stat secondary_storage_hit 0
+    expect_stat secondary_storage_miss 2 # result + manifest
+    expect_stat files_in_cache 2
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    $CCACHE -C >/dev/null
+    expect_stat files_in_cache 0
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 1
+    expect_stat primary_storage_hit 2
+    expect_stat primary_storage_miss 4 # 2 * (result + manifest)
+    expect_stat secondary_storage_hit 2 # result + manifest
+    expect_stat secondary_storage_miss 2 # result + manifest
+    expect_stat files_in_cache 2 # fetched from secondary
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    # -------------------------------------------------------------------------
+    TEST "Flat layout"
+
+    CCACHE_SECONDARY_STORAGE+="|layout=flat"
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_exists secondary/CACHEDIR.TAG
+    subdirs=$(find secondary -type d | wc -l)
+    if [ "${subdirs}" -ne 1 ]; then # "secondary" itself counts as one
+        test_failed "Expected no subdirectories in secondary"
+    fi
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    $CCACHE -C >/dev/null
+    expect_stat files_in_cache 0
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2 # fetched from secondary
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    # -------------------------------------------------------------------------
+    TEST "Two directories"
+
+    CCACHE_SECONDARY_STORAGE+=" file://$PWD/secondary_2"
+    mkdir secondary_2
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+    expect_file_count 3 '*' secondary_2 # CACHEDIR.TAG + result + manifest
+
+    $CCACHE -C >/dev/null
+    expect_stat files_in_cache 0
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+    expect_file_count 3 '*' secondary_2 # CACHEDIR.TAG + result + manifest
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2 # fetched from secondary
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+    expect_file_count 3 '*' secondary_2 # CACHEDIR.TAG + result + manifest
+
+    $CCACHE -C >/dev/null
+    expect_stat files_in_cache 0
+
+    rm -r secondary/??
+    expect_file_count 1 '*' secondary # CACHEDIR.TAG
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2 # fetched from secondary_2
+    expect_file_count 1 '*' secondary # CACHEDIR.TAG
+    expect_file_count 3 '*' secondary_2 # CACHEDIR.TAG + result + manifest
+
+    # -------------------------------------------------------------------------
+    TEST "Read-only"
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    $CCACHE -C >/dev/null
+    expect_stat files_in_cache 0
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    CCACHE_SECONDARY_STORAGE+="|read-only"
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2 # fetched from secondary
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    echo 'int x;' >> test.c
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
+    expect_stat files_in_cache 4
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    # -------------------------------------------------------------------------
+    TEST "umask"
+
+    CCACHE_SECONDARY_STORAGE="file://$PWD/secondary|umask=022"
+    rm -rf secondary
+    $CCACHE_COMPILE -c test.c
+    expect_perm secondary drwxr-xr-x
+    expect_perm secondary/CACHEDIR.TAG -rw-r--r--
+
+    CCACHE_SECONDARY_STORAGE="file://$PWD/secondary|umask=000"
+    $CCACHE -C >/dev/null
+    rm -rf secondary
+    $CCACHE_COMPILE -c test.c
+    expect_perm secondary drwxrwxrwx
+    expect_perm secondary/CACHEDIR.TAG -rw-rw-rw-
+
+    # -------------------------------------------------------------------------
+    TEST "Sharding"
+
+    CCACHE_SECONDARY_STORAGE="file://$PWD/secondary/*|shards=a,b(2)"
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    if [ ! -d secondary/a ] && [ ! -d secondary/b ]; then
+        test_failed "Expected secondary/a or secondary/b to exist"
+    fi
+
+    # -------------------------------------------------------------------------
+    TEST "Reshare"
+
+    CCACHE_SECONDARY_STORAGE="" $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_stat primary_storage_hit 0
+    expect_stat primary_storage_miss 2
+    expect_stat secondary_storage_hit 0
+    expect_stat secondary_storage_miss 0
+    expect_missing secondary
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat primary_storage_hit 2
+    expect_stat primary_storage_miss 2
+    expect_stat secondary_storage_hit 0
+    expect_stat secondary_storage_miss 0
+    expect_missing secondary
+
+    CCACHE_RESHARE=1 $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 1
+    expect_stat primary_storage_hit 4
+    expect_stat primary_storage_miss 2
+    expect_stat secondary_storage_hit 0
+    expect_stat secondary_storage_miss 0
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    $CCACHE -C >/dev/null
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 3
+    expect_stat cache_miss 1
+    expect_stat primary_storage_hit 4
+    expect_stat primary_storage_miss 4
+    expect_stat secondary_storage_hit 2
+    expect_stat secondary_storage_miss 0
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    # -------------------------------------------------------------------------
+    TEST "Don't share hits"
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_stat primary_storage_hit 0
+    expect_stat primary_storage_miss 2
+    expect_stat secondary_storage_hit 0
+    expect_stat secondary_storage_miss 2
+    expect_file_count 3 '*' secondary # CACHEDIR.TAG + result + manifest
+
+    $CCACHE -C >/dev/null
+    expect_stat files_in_cache 0
+
+    CCACHE_SECONDARY_STORAGE+="|share-hits=false"
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 0
+    expect_stat primary_storage_hit 0
+    expect_stat primary_storage_miss 4
+    expect_stat secondary_storage_hit 2
+    expect_stat secondary_storage_miss 2
+    expect_file_count 0
+}
diff --git a/test/suites/secondary_http.bash b/test/suites/secondary_http.bash
new file mode 100644 (file)
index 0000000..25c7939
--- /dev/null
@@ -0,0 +1,207 @@
+start_http_server() {
+    local port="$1"
+    local cache_dir="$2"
+    local credentials="$3" # optional parameter
+
+    mkdir -p "${cache_dir}"
+    "${HTTP_SERVER}" --bind localhost --directory "${cache_dir}" "${port}" \
+        ${credentials:+--basic-auth ${credentials}} \
+        &>http-server.log &
+    "${HTTP_CLIENT}" "http://localhost:${port}" &>http-client.log \
+        ${credentials:+--basic-auth ${credentials}} \
+        || test_failed_internal "Cannot connect to server"
+}
+
+maybe_start_ipv6_http_server() {
+    local port="$1"
+    local cache_dir="$2"
+    local credentials="$3" # optional parameter
+
+    mkdir -p "${cache_dir}"
+    "${HTTP_SERVER}" --bind "::1" --directory "${cache_dir}" "${port}" \
+        ${credentials:+--basic-auth ${credentials}} \
+        &>http-server.log &
+    "${HTTP_CLIENT}" "http://[::1]:${port}" &>http-client.log \
+        ${credentials:+--basic-auth ${credentials}} \
+        || return 1
+}
+
+SUITE_secondary_http_PROBE() {
+    if ! "${HTTP_SERVER}" --help >/dev/null 2>&1; then
+        echo "cannot execute ${HTTP_SERVER} - Python 3 might be missing"
+    fi
+}
+
+SUITE_secondary_http_SETUP() {
+    unset CCACHE_NODIRECT
+
+    generate_code 1 test.c
+}
+
+SUITE_secondary_http() {
+    # -------------------------------------------------------------------------
+    TEST "Subdirs layout"
+
+    start_http_server 12780 secondary
+    export CCACHE_SECONDARY_STORAGE="http://localhost:12780"
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_file_count 2 '*' secondary # result + manifest
+    subdirs=$(find secondary -type d | wc -l)
+    if [ "${subdirs}" -lt 2 ]; then # "secondary" itself counts as one
+        test_failed "Expected subdirectories in secondary"
+    fi
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_file_count 2 '*' secondary # result + manifest
+
+    $CCACHE -C >/dev/null
+    expect_stat files_in_cache 0
+    expect_file_count 2 '*' secondary # result + manifest
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2 # fetched from secondary
+    expect_file_count 2 '*' secondary # result + manifest
+
+    # -------------------------------------------------------------------------
+    TEST "Flat layout"
+
+    start_http_server 12780 secondary
+    export CCACHE_SECONDARY_STORAGE="http://localhost:12780|layout=flat"
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_file_count 2 '*' secondary # result + manifest
+    subdirs=$(find secondary -type d | wc -l)
+    if [ "${subdirs}" -ne 1 ]; then # "secondary" itself counts as one
+        test_failed "Expected no subdirectories in secondary"
+    fi
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_file_count 2 '*' secondary # result + manifest
+
+    $CCACHE -C >/dev/null
+    expect_stat files_in_cache 0
+    expect_file_count 2 '*' secondary # result + manifest
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2 # fetched from secondary
+    expect_file_count 2 '*' secondary # result + manifest
+
+    # -------------------------------------------------------------------------
+    TEST "Bazel layout"
+
+    start_http_server 12780 secondary
+    mkdir secondary/ac
+    export CCACHE_SECONDARY_STORAGE="http://localhost:12780|layout=bazel"
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_file_count 2 '*' secondary/ac # result + manifest
+    if [ "$(ls secondary/ac | grep -Ec '^[0-9a-f]{64}$')" -ne 2 ]; then
+        test_failed "Bazel layout filenames not as expected"
+    fi
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_file_count 2 '*' secondary/ac # result + manifest
+
+    $CCACHE -C >/dev/null
+    expect_stat files_in_cache 0
+    expect_file_count 2 '*' secondary/ac # result + manifest
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2 # fetched from secondary
+    expect_file_count 2 '*' secondary/ac # result + manifest
+
+    # -------------------------------------------------------------------------
+    TEST "Basic auth"
+
+    start_http_server 12780 secondary "somebody:secret123"
+    export CCACHE_SECONDARY_STORAGE="http://somebody:secret123@localhost:12780"
+
+    CCACHE_DEBUG=1 $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_file_count 2 '*' secondary # result + manifest
+    expect_not_contains test.o.ccache-log secret123
+
+    # -------------------------------------------------------------------------
+    TEST "Basic auth required"
+
+    start_http_server 12780 secondary "somebody:secret123"
+    # no authentication configured on client
+    export CCACHE_SECONDARY_STORAGE="http://localhost:12780"
+
+    CCACHE_DEBUG=1 $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_file_count 0 '*' secondary # result + manifest
+    expect_contains test.o.ccache-log "status code: 401"
+
+    # -------------------------------------------------------------------------
+    TEST "Basic auth failed"
+
+    start_http_server 12780 secondary "somebody:secret123"
+    export CCACHE_SECONDARY_STORAGE="http://somebody:wrong@localhost:12780"
+
+    CCACHE_DEBUG=1 $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_file_count 0 '*' secondary # result + manifest
+    expect_not_contains test.o.ccache-log secret123
+    expect_contains test.o.ccache-log "status code: 401"
+
+     # -------------------------------------------------------------------------
+    TEST "IPv6 address"
+
+    if maybe_start_ipv6_http_server 12780 secondary; then
+        export CCACHE_SECONDARY_STORAGE="http://[::1]:12780"
+
+        $CCACHE_COMPILE -c test.c
+        expect_stat direct_cache_hit 0
+        expect_stat cache_miss 1
+        expect_stat files_in_cache 2
+        expect_file_count 2 '*' secondary # result + manifest
+
+        $CCACHE_COMPILE -c test.c
+        expect_stat direct_cache_hit 1
+        expect_stat cache_miss 1
+        expect_stat files_in_cache 2
+        expect_file_count 2 '*' secondary # result + manifest
+
+        $CCACHE -C >/dev/null
+        expect_stat files_in_cache 0
+        expect_file_count 2 '*' secondary # result + manifest
+
+        $CCACHE_COMPILE -c test.c
+        expect_stat direct_cache_hit 2
+        expect_stat cache_miss 1
+        expect_stat files_in_cache 2 # fetched from secondary
+        expect_file_count 2 '*' secondary # result + manifest
+    fi
+}
diff --git a/test/suites/secondary_redis.bash b/test/suites/secondary_redis.bash
new file mode 100644 (file)
index 0000000..3e70384
--- /dev/null
@@ -0,0 +1,114 @@
+SUITE_secondary_redis_PROBE() {
+    if ! $CCACHE --version | fgrep -q -- redis-storage &> /dev/null; then
+        echo "redis-storage not available"
+        return
+    fi
+    if ! command -v redis-server &> /dev/null; then
+        echo "redis-server not found"
+        return
+    fi
+    if ! command -v redis-cli &> /dev/null; then
+        echo "redis-cli not found"
+        return
+    fi
+}
+
+start_redis_server() {
+    local port="$1"
+    local password="${2:-}"
+
+    redis-server --bind localhost --port "${port}" >/dev/null &
+    # Wait for server start.
+    i=0
+    while [ $i -lt 100 ] && ! redis-cli -p "${port}" ping &>/dev/null; do
+        sleep 0.1
+        i=$((i + 1))
+    done
+
+    if [ -n "${password}" ]; then
+        redis-cli -p "${port}" config set requirepass "${password}" &>/dev/null
+    fi
+}
+
+SUITE_secondary_redis_SETUP() {
+    unset CCACHE_NODIRECT
+
+    generate_code 1 test.c
+}
+
+expect_number_of_redis_cache_entries() {
+    local expected=$1
+    local url=$2
+    local actual
+
+    actual=$(redis-cli -u "$url" keys "ccache:*" 2>/dev/null | wc -l)
+    if [ "$actual" -ne "$expected" ]; then
+        test_failed_internal "Found $actual (expected $expected) entries in $url"
+    fi
+}
+
+SUITE_secondary_redis() {
+    # -------------------------------------------------------------------------
+    TEST "Base case"
+
+    port=7777
+    redis_url="redis://localhost:${port}"
+    export CCACHE_SECONDARY_STORAGE="${redis_url}"
+
+    start_redis_server "${port}"
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_number_of_redis_cache_entries 2 "$redis_url" # result + manifest
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_number_of_redis_cache_entries 2 "$redis_url" # result + manifest
+
+    $CCACHE -C >/dev/null
+    expect_stat files_in_cache 0
+    expect_number_of_redis_cache_entries 2 "$redis_url" # result + manifest
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2 # fetched from secondary
+    expect_number_of_redis_cache_entries 2 "$redis_url" # result + manifest
+
+    # -------------------------------------------------------------------------
+    TEST "Password"
+
+    port=7777
+    password=secret123
+    redis_url="redis://${password}@localhost:${port}"
+    export CCACHE_SECONDARY_STORAGE="${redis_url}"
+
+    start_redis_server "${port}" "${password}"
+
+    CCACHE_DEBUG=1 $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_number_of_redis_cache_entries 2 "$redis_url" # result + manifest
+    expect_not_contains test.o.ccache-log "${password}"
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
+    expect_number_of_redis_cache_entries 2 "$redis_url" # result + manifest
+
+    $CCACHE -C >/dev/null
+    expect_stat files_in_cache 0
+    expect_number_of_redis_cache_entries 2 "$redis_url" # result + manifest
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2 # fetched from secondary
+    expect_number_of_redis_cache_entries 2 "$redis_url" # result + manifest
+}
diff --git a/test/suites/secondary_url.bash b/test/suites/secondary_url.bash
new file mode 100644 (file)
index 0000000..74fe082
--- /dev/null
@@ -0,0 +1,33 @@
+SUITE_secondary_url_SETUP() {
+    generate_code 1 test.c
+}
+
+SUITE_secondary_url() {
+    # -------------------------------------------------------------------------
+    TEST "Reject empty url (without config attributes)"
+
+    export CCACHE_SECONDARY_STORAGE="|"
+    $CCACHE_COMPILE -c test.c 2>stderr.log
+    expect_contains stderr.log "must provide a URL"
+
+    # -------------------------------------------------------------------------
+    TEST "Reject empty url (but with config attributes)"
+
+    export CCACHE_SECONDARY_STORAGE="|key=value"
+    $CCACHE_COMPILE -c test.c 2>stderr.log
+    expect_contains stderr.log "must provide a URL"
+
+    # -------------------------------------------------------------------------
+    TEST "Reject invalid url"
+
+    export CCACHE_SECONDARY_STORAGE="://qwerty"
+    $CCACHE_COMPILE -c test.c 2>stderr.log
+    expect_contains stderr.log "Cannot parse URL"
+
+    # -------------------------------------------------------------------------
+    TEST "Reject missing scheme"
+
+    export CCACHE_SECONDARY_STORAGE="/qwerty"
+    $CCACHE_COMPILE -c test.c 2>stderr.log
+    expect_contains stderr.log "URL scheme must not be empty"
+}
index 9ec2eb8af37b128a6430af86c19753f535da5f16..6163da73ef278f29f34a642f0d0be0ab522208b8 100644 (file)
@@ -17,17 +17,17 @@ SUITE_serialize_diagnostics() {
     $REAL_COMPILER -c --serialize-diagnostics expected.dia test1.c
 
     $CCACHE_COMPILE -c --serialize-diagnostics test.dia test1.c
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content expected.dia test.dia
 
     rm test.dia
 
     $CCACHE_COMPILE -c --serialize-diagnostics test.dia test1.c
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 1
     expect_equal_content expected.dia test.dia
 
     # -------------------------------------------------------------------------
@@ -39,10 +39,10 @@ SUITE_serialize_diagnostics() {
     fi
 
     $CCACHE_COMPILE -c --serialize-diagnostics test.dia error.c 2>test.stderr
-    expect_stat 'compile failed' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 0
-    expect_stat 'files in cache' 0
+    expect_stat compile_failed 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 0
+    expect_stat files_in_cache 0
     expect_equal_content expected.dia test.dia
     expect_equal_content expected.stderr test.stderr
 
@@ -71,15 +71,15 @@ EOF
 
     cd dir1
     CCACHE_BASEDIR=`pwd` $CCACHE_COMPILE -w -MD -MF `pwd`/test.d -I`pwd`/include --serialize-diagnostics `pwd`/test.dia -c src/test.c -o `pwd`/test.o
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 
     cd ../dir2
     CCACHE_BASEDIR=`pwd` $CCACHE_COMPILE -w -MD -MF `pwd`/test.d -I`pwd`/include --serialize-diagnostics `pwd`/test.dia -c src/test.c -o `pwd`/test.o
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
 }
index d9d11bac02088b5bcc27924f251a6d7e8a3cec53..c5efdc7af7b35e57439e02de1b6857aabc8741f7 100644 (file)
@@ -18,19 +18,19 @@ SUITE_source_date_epoch() {
     unset CCACHE_NODIRECT
 
     SOURCE_DATE_EPOCH=1 $CCACHE_COMPILE -c without_temporal_macros.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     SOURCE_DATE_EPOCH=1 $CCACHE_COMPILE -c without_temporal_macros.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     SOURCE_DATE_EPOCH=2 $CCACHE_COMPILE -c without_temporal_macros.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "With __DATE__ macro"
@@ -38,19 +38,19 @@ SUITE_source_date_epoch() {
     unset CCACHE_NODIRECT
 
     SOURCE_DATE_EPOCH=1 $CCACHE_COMPILE -c with_date_macro.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     SOURCE_DATE_EPOCH=1 $CCACHE_COMPILE -c with_date_macro.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     SOURCE_DATE_EPOCH=2 $CCACHE_COMPILE -c with_date_macro.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     # -------------------------------------------------------------------------
     TEST "With __TIME__ macro"
@@ -58,19 +58,19 @@ SUITE_source_date_epoch() {
     unset CCACHE_NODIRECT
 
     SOURCE_DATE_EPOCH=1 $CCACHE_COMPILE -c with_time_macro.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     SOURCE_DATE_EPOCH=1 $CCACHE_COMPILE -c with_time_macro.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 
     SOURCE_DATE_EPOCH=2 $CCACHE_COMPILE -c with_time_macro.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "With __TIME__ and time_macros sloppiness"
@@ -78,22 +78,22 @@ SUITE_source_date_epoch() {
     unset CCACHE_NODIRECT
 
     CCACHE_SLOPPINESS=time_macros SOURCE_DATE_EPOCH=1 $CCACHE_COMPILE -c with_time_macro.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS=time_macros SOURCE_DATE_EPOCH=1 $CCACHE_COMPILE -c with_time_macro.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     CCACHE_SLOPPINESS=time_macros SOURCE_DATE_EPOCH=2 $CCACHE_COMPILE -c with_time_macro.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     SOURCE_DATE_EPOCH=1 $CCACHE_COMPILE -c with_time_macro.c
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache hit (preprocessed)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 2
+    expect_stat preprocessed_cache_hit 1
+    expect_stat cache_miss 1
 }
index 28a329345cefe7af50456cc685b2e60441000a4a..7b77ca8ebd72dc52240366e1a9d0ad14321f8766 100644 (file)
@@ -31,19 +31,19 @@ SUITE_split_dwarf() {
 
     cd dir1
     $CCACHE_COMPILE -I$(pwd)/include -c src/test.c -gsplit-dwarf
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
     $CCACHE_COMPILE -I$(pwd)/include -c src/test.c -gsplit-dwarf
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
 
     cd ../dir2
     $CCACHE_COMPILE -I$(pwd)/include -c src/test.c -gsplit-dwarf
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
     $CCACHE_COMPILE -I$(pwd)/include -c src/test.c -gsplit-dwarf
-    expect_stat 'cache hit (direct)' 2
-    expect_stat 'cache miss' 2
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 2
 
     # -------------------------------------------------------------------------
     TEST "Output filename is hashed if using -gsplit-dwarf"
@@ -62,18 +62,18 @@ SUITE_split_dwarf() {
         $CCACHE_COMPILE -I$(pwd)/include -c src/test.c -o test.o -gsplit-dwarf
         expect_equal_object_files reference.o test.o
         expect_equal_object_files reference.dwo test.dwo
-        expect_stat 'cache hit (direct)' 0
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 1
-        expect_stat 'files in cache' 2
+        expect_stat direct_cache_hit 0
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 1
+        expect_stat files_in_cache 2
 
         $CCACHE_COMPILE -I$(pwd)/include -c src/test.c -o test.o -gsplit-dwarf
         expect_equal_object_files reference.o test.o
         expect_equal_object_files reference.dwo test.dwo
-        expect_stat 'cache hit (direct)' 1
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 1
-        expect_stat 'files in cache' 2
+        expect_stat direct_cache_hit 1
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 1
+        expect_stat files_in_cache 2
 
         $REAL_COMPILER -I$(pwd)/include -c src/test.c -o test2.o -gsplit-dwarf
         mv test2.o reference2.o
@@ -82,18 +82,18 @@ SUITE_split_dwarf() {
         $CCACHE_COMPILE -I$(pwd)/include -c src/test.c -o test2.o -gsplit-dwarf
         expect_equal_object_files reference2.o test2.o
         expect_equal_object_files reference2.dwo test2.dwo
-        expect_stat 'cache hit (direct)' 1
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 2
-        expect_stat 'files in cache' 4
+        expect_stat direct_cache_hit 1
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 2
+        expect_stat files_in_cache 4
 
         $CCACHE_COMPILE -I$(pwd)/include -c src/test.c -o test2.o -gsplit-dwarf
         expect_equal_object_files reference2.o test2.o
         expect_equal_object_files reference2.dwo test2.dwo
-        expect_stat 'cache hit (direct)' 2
-        expect_stat 'cache hit (preprocessed)' 0
-        expect_stat 'cache miss' 2
-        expect_stat 'files in cache' 4
+        expect_stat direct_cache_hit 2
+        expect_stat preprocessed_cache_hit 0
+        expect_stat cache_miss 2
+        expect_stat files_in_cache 4
     fi
     # Else: Compiler does not produce stable object file output when compiling
     # the same source to the same output filename twice (DW_AT_GNU_dwo_id
@@ -104,18 +104,18 @@ SUITE_split_dwarf() {
 
     cd dir1
     CCACHE_BASEDIR=$(pwd) $CCACHE_COMPILE -I$(pwd)/include -gsplit-dwarf -fdebug-prefix-map=$(pwd)=. -c $(pwd)/src/test.c -o $(pwd)/test.o
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_objdump_not_contains test.o "$(pwd)"
 
     cd ../dir2
     CCACHE_BASEDIR=$(pwd) $CCACHE_COMPILE -I$(pwd)/include -gsplit-dwarf -fdebug-prefix-map=$(pwd)=. -c $(pwd)/src/test.c -o $(pwd)/test.o
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
-    expect_stat 'files in cache' 2
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
+    expect_stat files_in_cache 2
     expect_objdump_not_contains test.o "$(pwd)"
 
     # -------------------------------------------------------------------------
@@ -127,15 +127,15 @@ SUITE_split_dwarf() {
     $REAL_COMPILER -gsplit-dwarf -g1 -c test.c -o reference.o
 
     $CCACHE_COMPILE -gsplit-dwarf -g1 -c test.c
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     rm -f test.dwo
 
     $CCACHE_COMPILE -gsplit-dwarf -g1 -c test.c
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
 
     if [ -f reference.dwo ] && [ ! -f test.dwo ]; then
         test_failed ".dwo missing"
@@ -147,33 +147,33 @@ SUITE_split_dwarf() {
     TEST "Object file without dot"
 
     $CCACHE_COMPILE -gsplit-dwarf -c test.c -o test
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists test.dwo
 
     rm test.dwo
 
     $CCACHE_COMPILE -gsplit-dwarf -c test.c -o test
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists test.dwo
 
     # -------------------------------------------------------------------------
     TEST "Object file with two dots"
 
     $CCACHE_COMPILE -gsplit-dwarf -c test.c -o test.x.y
-    expect_stat 'cache hit (direct)' 0
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 0
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists test.x.dwo
 
     rm test.x.dwo
 
     $CCACHE_COMPILE -gsplit-dwarf -c test.c -o test.x.y
-    expect_stat 'cache hit (direct)' 1
-    expect_stat 'cache hit (preprocessed)' 0
-    expect_stat 'cache miss' 1
+    expect_stat direct_cache_hit 1
+    expect_stat preprocessed_cache_hit 0
+    expect_stat cache_miss 1
     expect_exists test.x.dwo
 }
diff --git a/test/suites/stats_log.bash b/test/suites/stats_log.bash
new file mode 100644 (file)
index 0000000..e90efaf
--- /dev/null
@@ -0,0 +1,27 @@
+SUITE_stats_log_SETUP() {
+    generate_code 1 test.c
+    unset CCACHE_NODIRECT
+    export CCACHE_STATSLOG=stats.log
+}
+
+SUITE_stats_log() {
+    # -------------------------------------------------------------------------
+    TEST "CCACHE_STATSLOG"
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+
+    $CCACHE_COMPILE -c test.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+
+    expect_content stats.log "# test.c
+cache_miss
+direct_cache_miss
+preprocessed_cache_miss
+primary_storage_miss
+# test.c
+direct_cache_hit
+primary_storage_hit"
+}
diff --git a/test/suites/trim_dir.bash b/test/suites/trim_dir.bash
new file mode 100644 (file)
index 0000000..b23f233
--- /dev/null
@@ -0,0 +1,42 @@
+SUITE_trim_dir() {
+    # -------------------------------------------------------------------------
+    TEST "Trim secondary cache directory"
+
+    if $HOST_OS_APPLE; then
+        one_mb=1m
+    else
+        one_mb=1M
+    fi
+    for subdir in aa bb cc; do
+        mkdir -p secondary/$subdir
+        dd if=/dev/zero of=secondary/$subdir/1 count=1 bs=$one_mb 2>/dev/null
+        dd if=/dev/zero of=secondary/$subdir/2 count=1 bs=$one_mb 2>/dev/null
+    done
+
+    backdate secondary/bb/2 secondary/cc/1
+    $CCACHE --trim-dir secondary --trim-max-size 4.5M --trim-method mtime \
+            >/dev/null
+
+    expect_exists secondary/aa/1
+    expect_exists secondary/aa/2
+    expect_exists secondary/bb/1
+    expect_missing secondary/bb/2
+    expect_missing secondary/cc/1
+    expect_exists secondary/cc/2
+
+    # -------------------------------------------------------------------------
+    TEST "Trim primary cache directory"
+
+    mkdir -p primary/0
+    touch primary/0/stats
+    if $CCACHE --trim-dir primary --trim-max-size 0 &>/dev/null; then
+        test_failed "Expected failure"
+    fi
+
+    rm -rf primary
+    mkdir primary
+    touch primary/ccache.conf
+    if $CCACHE --trim-dir primary --trim-max-size 0 &>/dev/null; then
+        test_failed "Expected failure"
+    fi
+}
index 233bed369c081f4c9e8cfb8165ae5def177b3733..69786c3b68c4f357cbef063aa24e1097bdedaa20 100644 (file)
@@ -11,7 +11,7 @@ SUITE_upgrade() {
     else
         expected=$HOME/.cache/ccache
     fi
-    actual=$($CCACHE -s | sed -n 's/^cache directory *//p')
+    actual=$($CCACHE -k cache_dir)
     if [ "$actual" != "$expected" ]; then
         test_failed "expected cache directory $expected, actual $actual"
     fi
@@ -21,7 +21,7 @@ SUITE_upgrade() {
     else
         expected=$HOME/.config/ccache/ccache.conf
     fi
-    actual=$($CCACHE -s | sed -n 's/^primary config *//p')
+    actual=$($CCACHE -sv | sed -n 's/ *Primary config: *//p')
     if [ "$actual" != "$expected" ]; then
         test_failed "expected primary config $expected actual $actual"
     fi
@@ -36,13 +36,13 @@ SUITE_upgrade() {
     export XDG_CONFIG_HOME=/elsewhere/config
 
     expected=$XDG_CACHE_HOME/ccache
-    actual=$($CCACHE -s | sed -n 's/^cache directory *//p')
+    actual=$($CCACHE -k cache_dir)
     if [ "$actual" != "$expected" ]; then
         test_failed "expected cache directory $expected, actual $actual"
     fi
 
     expected=$XDG_CONFIG_HOME/ccache/ccache.conf
-    actual=$($CCACHE -s | sed -n 's/^primary config *//p')
+    actual=$($CCACHE -sv | sed -n 's/ *Primary config: *//p')
     if [ "$actual" != "$expected" ]; then
         test_failed "expected primary config $expected actual $actual"
     fi
@@ -58,13 +58,13 @@ SUITE_upgrade() {
     mkdir $HOME/.ccache
 
     expected=$HOME/.ccache
-    actual=$($CCACHE -s | sed -n 's/^cache directory *//p')
+    actual=$($CCACHE -k cache_dir)
     if [ "$actual" != "$expected" ]; then
         test_failed "expected cache directory $expected, actual $actual"
     fi
 
     expected=$HOME/.ccache/ccache.conf
-    actual=$($CCACHE -s | sed -n 's/^primary config *//p')
+    actual=$($CCACHE -sv | sed -n 's/ *Primary config: *//p')
     if [ "$actual" != "$expected" ]; then
         test_failed "expected primary config $expected actual $actual"
     fi
@@ -79,13 +79,13 @@ SUITE_upgrade() {
     export XDG_CONFIG_HOME=/elsewhere/config
 
     expected=$CCACHE_DIR
-    actual=$($CCACHE -s | sed -n 's/^cache directory *//p')
+    actual=$($CCACHE -k cache_dir)
     if [ "$actual" != "$expected" ]; then
         test_failed "expected cache directory $expected, actual $actual"
     fi
 
     expected=$CCACHE_DIR/ccache.conf
-    actual=$($CCACHE -s | sed -n 's/^primary config *//p')
+    actual=$($CCACHE -sv | sed -n 's/ *Primary config: *//p')
     if [ "$actual" != "$expected" ]; then
         test_failed "expected primary config $expected actual $actual"
     fi
@@ -105,13 +105,13 @@ SUITE_upgrade() {
     echo 'cache_dir = /nowhere' > $CCACHE_CONFIGPATH2
 
     expected=$XDG_CACHE_HOME/ccache
-    actual=$($CCACHE -s | sed -n 's/^cache directory *//p')
+    actual=$($CCACHE -k cache_dir)
     if [ "$actual" != "$expected" ]; then
         test_failed "expected cache directory $expected, actual $actual"
     fi
 
     expected=$XDG_CONFIG_HOME/ccache/ccache.conf
-    actual=$($CCACHE -s | sed -n 's/^primary config *//p')
+    actual=$($CCACHE -sv | sed -n 's/ *Primary config: *//p')
     if [ "$actual" != "$expected" ]; then
         test_failed "expected primary config $expected actual $actual"
     fi
index 48cf058ecb9212659128fe571e34545c20280c9f..f8711a102bf586f9c1c2cac3eb4140b1f845fc48 100644 (file)
@@ -5,22 +5,31 @@ set(
   test_Args.cpp
   test_AtomicFile.cpp
   test_Checksum.cpp
-  test_Compression.cpp
   test_Config.cpp
-  test_Counters.cpp
   test_Depfile.cpp
   test_FormatNonstdStringView.cpp
   test_Hash.cpp
   test_Lockfile.cpp
   test_NullCompression.cpp
   test_Stat.cpp
-  test_Statistics.cpp
   test_Util.cpp
   test_ZstdCompression.cpp
   test_argprocessing.cpp
   test_ccache.cpp
   test_compopt.cpp
-  test_hashutil.cpp)
+  test_compression_types.cpp
+  test_core_Statistics.cpp
+  test_core_StatisticsCounters.cpp
+  test_core_StatsLog.cpp
+  test_hashutil.cpp
+  test_storage_primary_StatsFile.cpp
+  test_storage_primary_util.cpp
+  test_util_TextTable.cpp
+  test_util_Tokenizer.cpp
+  test_util_expected.cpp
+  test_util_path.cpp
+  test_util_string.cpp
+)
 
 if(INODE_CACHE_SUPPORTED)
   list(APPEND source_files test_InodeCache.cpp)
@@ -34,7 +43,7 @@ add_executable(unittest ${source_files})
 
 target_link_libraries(
   unittest
-  PRIVATE standard_settings standard_warnings ccache_lib third_party_lib)
+  PRIVATE standard_settings standard_warnings ccache_framework third_party)
 
 target_include_directories(unittest PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ${ccache_SOURCE_DIR}/src)
 
index fa8f7f01b32cb368d371679aaafba6637ea3bebe..d97ddba91b7699765579f133ade730e1309952b9 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 #include "TestUtil.hpp"
 
 #include "../src/Util.hpp"
-#include "../src/exceptions.hpp"
 #include "../src/fmtmacros.hpp"
 
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
 namespace TestUtil {
 
 size_t TestContext::m_subdir_counter = 0;
@@ -29,7 +35,7 @@ size_t TestContext::m_subdir_counter = 0;
 TestContext::TestContext() : m_test_dir(Util::get_actual_cwd())
 {
   if (Util::base_name(Util::dir_name(m_test_dir)) != "testdir") {
-    throw Error("TestContext instantiated outside test directory");
+    throw core::Error("TestContext instantiated outside test directory");
   }
   ++m_subdir_counter;
   std::string subtest_dir = FMT("{}/test_{}", m_test_dir, m_subdir_counter);
@@ -50,7 +56,8 @@ void
 check_chdir(const std::string& dir)
 {
   if (chdir(dir.c_str()) != 0) {
-    throw Error("failed to change directory to {}: {}", dir, strerror(errno));
+    throw core::Error(
+      "failed to change directory to {}: {}", dir, strerror(errno));
   }
 }
 
index 0f06d3ee31d516e7d8dff25a4e29e6a5cb5d1762..6be4969b239f991090010e708ccc759d5be0a580 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -18,8 +18,7 @@
 
 #pragma once
 
-#include "system.hpp"
-
+#include <cstddef>
 #include <string>
 
 #ifdef _MSC_VER
diff --git a/unittest/test_Compression.cpp b/unittest/test_Compression.cpp
deleted file mode 100644 (file)
index d143e06..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "../src/Compression.hpp"
-#include "../src/Config.hpp"
-
-#include "third_party/doctest.h"
-
-TEST_SUITE_BEGIN("Compression");
-
-TEST_CASE("Compression::level_from_config")
-{
-  Config config;
-  CHECK(Compression::level_from_config(config) == 0);
-}
-
-TEST_CASE("Compression::type_from_config")
-{
-  Config config;
-  CHECK(Compression::type_from_config(config) == Compression::Type::zstd);
-}
-
-TEST_CASE("Compression::type_from_int")
-{
-  CHECK(Compression::type_from_int(0) == Compression::Type::none);
-  CHECK(Compression::type_from_int(1) == Compression::Type::zstd);
-  CHECK_THROWS_WITH(Compression::type_from_int(2), "Unknown type: 2");
-}
-
-TEST_CASE("Compression::type_to_string")
-{
-  CHECK(Compression::type_to_string(Compression::Type::none) == "none");
-  CHECK(Compression::type_to_string(Compression::Type::zstd) == "zstd");
-}
-
-TEST_SUITE_END();
index e509c9cee9932ac551afd08bf3da50ed554866a4..3b62174c4012e11edd917a58072dcdea934fd763 100644 (file)
 // Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
 #include "../src/Config.hpp"
-#include "../src/Sloppiness.hpp"
 #include "../src/Util.hpp"
-#include "../src/exceptions.hpp"
 #include "../src/fmtmacros.hpp"
 #include "TestUtil.hpp"
 
+#include <core/exceptions.hpp>
+
 #include "third_party/doctest.h"
 #include "third_party/fmt/core.h"
 
@@ -70,11 +70,12 @@ TEST_CASE("Config: default values")
   CHECK_FALSE(config.read_only());
   CHECK_FALSE(config.read_only_direct());
   CHECK_FALSE(config.recache());
+  CHECK_FALSE(config.reshare());
   CHECK(config.run_second_cpp());
-  CHECK(config.sloppiness() == 0);
+  CHECK(config.sloppiness().to_bitmask() == 0);
   CHECK(config.stats());
   CHECK(config.temporary_dir().empty()); // Set later
-  CHECK(config.umask() == std::numeric_limits<uint32_t>::max());
+  CHECK(config.umask() == nonstd::nullopt);
 }
 
 TEST_CASE("Config::update_from_file")
@@ -125,6 +126,7 @@ TEST_CASE("Config::update_from_file")
     "read_only = true\n"
     "read_only_direct = true\n"
     "recache = true\n"
+    "reshare = true\n"
     "run_second_cpp = false\n"
     "sloppiness =     time_macros   ,include_file_mtime"
     "  include_file_ctime,file_stat_matches,file_stat_matches_ctime,pch_defines"
@@ -164,16 +166,21 @@ TEST_CASE("Config::update_from_file")
   CHECK(config.read_only());
   CHECK(config.read_only_direct());
   CHECK(config.recache());
+  CHECK(config.reshare());
   CHECK_FALSE(config.run_second_cpp());
-  CHECK(config.sloppiness()
-        == (SLOPPY_INCLUDE_FILE_MTIME | SLOPPY_INCLUDE_FILE_CTIME
-            | SLOPPY_TIME_MACROS | SLOPPY_FILE_STAT_MATCHES
-            | SLOPPY_FILE_STAT_MATCHES_CTIME | SLOPPY_SYSTEM_HEADERS
-            | SLOPPY_PCH_DEFINES | SLOPPY_CLANG_INDEX_STORE
-            | SLOPPY_IVFSOVERLAY));
+  CHECK(config.sloppiness().to_bitmask()
+        == (static_cast<uint32_t>(core::Sloppy::include_file_mtime)
+            | static_cast<uint32_t>(core::Sloppy::include_file_ctime)
+            | static_cast<uint32_t>(core::Sloppy::time_macros)
+            | static_cast<uint32_t>(core::Sloppy::file_stat_matches)
+            | static_cast<uint32_t>(core::Sloppy::file_stat_matches_ctime)
+            | static_cast<uint32_t>(core::Sloppy::system_headers)
+            | static_cast<uint32_t>(core::Sloppy::pch_defines)
+            | static_cast<uint32_t>(core::Sloppy::clang_index_store)
+            | static_cast<uint32_t>(core::Sloppy::ivfsoverlay)));
   CHECK_FALSE(config.stats());
   CHECK(config.temporary_dir() == FMT("{}_foo", user));
-  CHECK(config.umask() == 0777);
+  CHECK(config.umask() == 0777u);
 }
 
 TEST_CASE("Config::update_from_file, error handling")
@@ -219,7 +226,7 @@ TEST_CASE("Config::update_from_file, error handling")
   {
     Util::write_file("ccache.conf", "umask = ");
     CHECK(config.update_from_file("ccache.conf"));
-    CHECK(config.umask() == std::numeric_limits<uint32_t>::max());
+    CHECK(config.umask() == nonstd::nullopt);
   }
 
   SUBCASE("invalid size")
@@ -234,7 +241,8 @@ TEST_CASE("Config::update_from_file, error handling")
   {
     Util::write_file("ccache.conf", "sloppiness = time_macros, foo");
     CHECK(config.update_from_file("ccache.conf"));
-    CHECK(config.sloppiness() == SLOPPY_TIME_MACROS);
+    CHECK(config.sloppiness().to_bitmask()
+          == static_cast<uint32_t>(core::Sloppy::time_macros));
   }
 
   SUBCASE("invalid unsigned")
@@ -287,11 +295,12 @@ TEST_CASE("Config::update_from_environment")
 TEST_CASE("Config::set_value_in_file")
 {
   TestContext test_context;
+  Config config;
 
   SUBCASE("set new value")
   {
     Util::write_file("ccache.conf", "path = vanilla\n");
-    Config::set_value_in_file("ccache.conf", "compiler", "chocolate");
+    config.set_value_in_file("ccache.conf", "compiler", "chocolate");
     std::string content = Util::read_file("ccache.conf");
     CHECK(content == "path = vanilla\ncompiler = chocolate\n");
   }
@@ -299,7 +308,7 @@ TEST_CASE("Config::set_value_in_file")
   SUBCASE("existing value")
   {
     Util::write_file("ccache.conf", "path = chocolate\nstats = chocolate\n");
-    Config::set_value_in_file("ccache.conf", "path", "vanilla");
+    config.set_value_in_file("ccache.conf", "path", "vanilla");
     std::string content = Util::read_file("ccache.conf");
     CHECK(content == "path = vanilla\nstats = chocolate\n");
   }
@@ -308,9 +317,9 @@ TEST_CASE("Config::set_value_in_file")
   {
     Util::write_file("ccache.conf", "path = chocolate\nstats = chocolate\n");
     try {
-      Config::set_value_in_file("ccache.conf", "foo", "bar");
+      config.set_value_in_file("ccache.conf", "foo", "bar");
       CHECK(false);
-    } catch (const Error& e) {
+    } catch (const core::Error& e) {
       CHECK(std::string(e.what()) == "unknown configuration option \"foo\"");
     }
 
@@ -321,7 +330,7 @@ TEST_CASE("Config::set_value_in_file")
   SUBCASE("unknown sloppiness")
   {
     Util::write_file("ccache.conf", "path = vanilla\n");
-    Config::set_value_in_file("ccache.conf", "sloppiness", "foo");
+    config.set_value_in_file("ccache.conf", "sloppiness", "foo");
     std::string content = Util::read_file("ccache.conf");
     CHECK(content == "path = vanilla\nsloppiness = foo\n");
   }
@@ -329,8 +338,8 @@ TEST_CASE("Config::set_value_in_file")
   SUBCASE("comments are kept")
   {
     Util::write_file("ccache.conf", "# c1\npath = blueberry\n#c2\n");
-    Config::set_value_in_file("ccache.conf", "path", "vanilla");
-    Config::set_value_in_file("ccache.conf", "compiler", "chocolate");
+    config.set_value_in_file("ccache.conf", "path", "vanilla");
+    config.set_value_in_file("ccache.conf", "compiler", "chocolate");
     std::string content = Util::read_file("ccache.conf");
     CHECK(content == "# c1\npath = vanilla\n#c2\ncompiler = chocolate\n");
   }
@@ -351,7 +360,7 @@ TEST_CASE("Config::get_string_value")
     try {
       config.get_string_value("foo");
       CHECK(false);
-    } catch (const Error& e) {
+    } catch (const core::Error& e) {
       CHECK(std::string(e.what()) == "unknown configuration option \"foo\"");
     }
   }
@@ -400,11 +409,14 @@ TEST_CASE("Config::visit_items")
     "read_only = true\n"
     "read_only_direct = true\n"
     "recache = true\n"
+    "reshare = true\n"
     "run_second_cpp = false\n"
+    "secondary_storage = ss\n"
     "sloppiness = include_file_mtime, include_file_ctime, time_macros,"
     " file_stat_matches, file_stat_matches_ctime, pch_defines, system_headers,"
     " clang_index_store, ivfsoverlay\n"
     "stats = false\n"
+    "stats_log = sl\n"
     "temporary_dir = td\n"
     "umask = 022\n");
 
@@ -413,11 +425,10 @@ TEST_CASE("Config::visit_items")
 
   std::vector<std::string> received_items;
 
-  config.visit_items([&](const std::string& key,
-                         const std::string& value,
-                         const std::string& origin) {
-    received_items.push_back(FMT("({}) {} = {}", origin, key, value));
-  });
+  config.visit_items(
+    [&](const auto& key, const auto& value, const auto& origin) {
+      received_items.push_back(FMT("({}) {} = {}", origin, key, value));
+    });
 
   std::vector<std::string> expected = {
     "(test.conf) absolute_paths_in_stderr = true",
@@ -457,11 +468,14 @@ TEST_CASE("Config::visit_items")
     "(test.conf) read_only = true",
     "(test.conf) read_only_direct = true",
     "(test.conf) recache = true",
+    "(test.conf) reshare = true",
     "(test.conf) run_second_cpp = false",
+    "(test.conf) secondary_storage = ss",
     "(test.conf) sloppiness = include_file_mtime, include_file_ctime,"
     " time_macros, pch_defines, file_stat_matches, file_stat_matches_ctime,"
     " system_headers, clang_index_store, ivfsoverlay",
     "(test.conf) stats = false",
+    "(test.conf) stats_log = sl",
     "(test.conf) temporary_dir = td",
     "(test.conf) umask = 022",
   };
diff --git a/unittest/test_Counters.cpp b/unittest/test_Counters.cpp
deleted file mode 100644 (file)
index b4d3be3..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "../src/Counters.hpp"
-#include "../src/Statistic.hpp"
-#include "TestUtil.hpp"
-
-#include "third_party/doctest.h"
-
-using TestUtil::TestContext;
-
-TEST_SUITE_BEGIN("Counters");
-
-TEST_CASE("Counters")
-{
-  TestContext test_context;
-
-  Counters counters;
-  CHECK(counters.size() == static_cast<size_t>(Statistic::END));
-
-  SUBCASE("Get and set statistic")
-  {
-    CHECK(counters.get(Statistic::cache_miss) == 0);
-    counters.set(Statistic::cache_miss, 27);
-    CHECK(counters.get(Statistic::cache_miss) == 27);
-  }
-
-  SUBCASE("Get and set raw index")
-  {
-    CHECK(counters.get_raw(4) == 0);
-    counters.set_raw(4, 27);
-    CHECK(counters.get(Statistic::cache_miss) == 27);
-  }
-
-  SUBCASE("Set future raw counter")
-  {
-    const auto future_index = static_cast<size_t>(Statistic::END) + 2;
-    counters.set_raw(future_index, 42);
-    CHECK(counters.get_raw(future_index) == 42);
-  }
-
-  SUBCASE("Increment single counter")
-  {
-    counters.set(Statistic::cache_miss, 4);
-
-    counters.increment(Statistic::cache_miss);
-    CHECK(counters.get(Statistic::cache_miss) == 5);
-
-    counters.increment(Statistic::cache_miss, -3);
-    CHECK(counters.get(Statistic::cache_miss) == 2);
-
-    counters.increment(Statistic::cache_miss, -3);
-    CHECK(counters.get(Statistic::cache_miss) == 0);
-  }
-
-  SUBCASE("Increment many counters")
-  {
-    counters.set(Statistic::direct_cache_hit, 3);
-    counters.set(Statistic::cache_miss, 2);
-    counters.set(Statistic::files_in_cache, 10);
-    counters.set(Statistic::cache_size_kibibyte, 1);
-
-    Counters updates;
-    updates.set(Statistic::direct_cache_hit, 6);
-    updates.set(Statistic::cache_miss, 5);
-    updates.set(Statistic::files_in_cache, -1);
-    updates.set(Statistic::cache_size_kibibyte, -4);
-
-    counters.increment(updates);
-    CHECK(counters.get(Statistic::direct_cache_hit) == 9);
-    CHECK(counters.get(Statistic::cache_miss) == 7);
-    CHECK(counters.get(Statistic::files_in_cache) == 9);
-    CHECK(counters.get(Statistic::cache_size_kibibyte) == 0); // No wrap-around
-  }
-}
-
-TEST_SUITE_END();
index b098800b41ba6246b1363287d97f876baa9c443c..c4f7e3530291151699a4d2b56010cd09925a3380 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -45,9 +45,10 @@ TEST_CASE("Depfile::rewrite_paths")
   const auto cwd = ctx.actual_cwd;
   ctx.has_absolute_include_headers = true;
 
-  const auto content = FMT("foo.o: bar.c {0}/bar.h \\\n {1}/fie.h {0}/fum.h\n",
-                           cwd,
-                           Util::dir_name(cwd));
+  const auto content =
+    FMT("foo.o: bar.c {0}/bar.h \\\n\n {1}/fie.h {0}/fum.h\n",
+        cwd,
+        Util::dir_name(cwd));
 
   SUBCASE("Base directory not in dep file content")
   {
@@ -67,8 +68,8 @@ TEST_CASE("Depfile::rewrite_paths")
   {
     ctx.config.set_base_dir(cwd);
     const auto actual = Depfile::rewrite_paths(ctx, content);
-    const auto expected =
-      FMT("foo.o: bar.c ./bar.h \\\n {}/fie.h ./fum.h\n", Util::dir_name(cwd));
+    const auto expected = FMT("foo.o: bar.c ./bar.h \\\n\n {}/fie.h ./fum.h\n",
+                              Util::dir_name(cwd));
     REQUIRE(actual);
     CHECK(*actual == expected);
   }
index 7a01512ea619c0ac572ac489d494276e838630e0..d95cb3d1b12d966c573e7d10c0c4e913be364016 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 #include "../src/Stat.hpp"
 #include "TestUtil.hpp"
 
+#include <core/wincompat.hpp>
+
 #include "third_party/doctest.h"
 
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
 TEST_SUITE_BEGIN("LockFile");
 
 using TestUtil::TestContext;
index 22da2f55f7280cf417b5e6d9845a36d23045686e..6f87596986514d4fef6e9cd00b5a8b912dd3c578 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 // this program; if not, write to the Free Software Foundation, Inc., 51
 // Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-#include "../src/Compression.hpp"
-#include "../src/Compressor.hpp"
-#include "../src/Decompressor.hpp"
 #include "../src/File.hpp"
 #include "TestUtil.hpp"
 
+#include <compression/Compressor.hpp>
+#include <compression/Decompressor.hpp>
+#include <compression/types.hpp>
+
 #include "third_party/doctest.h"
 
+#include <cstring>
+
+using compression::Compressor;
+using compression::Decompressor;
 using TestUtil::TestContext;
 
 TEST_SUITE_BEGIN("NullCompression");
 
-TEST_CASE("Compression::Type::none roundtrip")
+TEST_CASE("compression::Type::none roundtrip")
 {
   TestContext test_context;
 
   File f("data.uncompressed", "w");
   auto compressor =
-    Compressor::create_from_type(Compression::Type::none, f.get(), 1);
+    Compressor::create_from_type(compression::Type::none, f.get(), 1);
   CHECK(compressor->actual_compression_level() == 0);
   compressor->write("foobar", 6);
   compressor->finalize();
 
   f.open("data.uncompressed", "r");
   auto decompressor =
-    Decompressor::create_from_type(Compression::Type::none, f.get());
+    Decompressor::create_from_type(compression::Type::none, f.get());
 
   char buffer[4];
   decompressor->read(buffer, 4);
index e5b6eeb8385fd7fb2e7fa0f7510afb627dd2c66a..9253f6857df1bd98737495b260b98fcc6a004488 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -21,6 +21,9 @@
 #include "../src/Util.hpp"
 #include "TestUtil.hpp"
 
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+
 #include "third_party/doctest.h"
 
 #ifdef HAVE_UNISTD_H
@@ -618,7 +621,7 @@ TEST_CASE("Win32 No Sharing")
   Finalizer cleanup([&] { CloseHandle(handle); });
 
   // Sanity check we can't open the file for read/write access.
-  REQUIRE_THROWS_AS(Util::read_file("file"), const Error&);
+  REQUIRE_THROWS_AS(Util::read_file("file"), const core::Error&);
 
   SUBCASE("stat file no sharing")
   {
diff --git a/unittest/test_Statistics.cpp b/unittest/test_Statistics.cpp
deleted file mode 100644 (file)
index 0e647fb..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright (C) 2011-2020 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "../src/Statistic.hpp"
-#include "../src/Statistics.hpp"
-#include "../src/Util.hpp"
-#include "../src/fmtmacros.hpp"
-#include "TestUtil.hpp"
-
-#include "third_party/doctest.h"
-
-using TestUtil::TestContext;
-
-TEST_SUITE_BEGIN("Statistics");
-
-TEST_CASE("Read nonexistent")
-{
-  TestContext test_context;
-
-  Counters counters = Statistics::read("test");
-
-  REQUIRE(counters.size() == static_cast<size_t>(Statistic::END));
-  CHECK(counters.get(Statistic::cache_miss) == 0);
-}
-
-TEST_CASE("Read bad")
-{
-  TestContext test_context;
-
-  Util::write_file("test", "bad 1 2 3 4 5\n");
-  Counters counters = Statistics::read("test");
-
-  REQUIRE(counters.size() == static_cast<size_t>(Statistic::END));
-  CHECK(counters.get(Statistic::cache_miss) == 0);
-}
-
-TEST_CASE("Read existing")
-{
-  TestContext test_context;
-
-  Util::write_file("test", "0 1 2 3 27 5\n");
-  Counters counters = Statistics::read("test");
-
-  REQUIRE(counters.size() == static_cast<size_t>(Statistic::END));
-  CHECK(counters.get(Statistic::cache_miss) == 27);
-  CHECK(counters.get(Statistic::could_not_use_modules) == 0);
-}
-
-TEST_CASE("Read future counters")
-{
-  TestContext test_context;
-
-  std::string content;
-  size_t count = static_cast<size_t>(Statistic::END) + 1;
-  for (size_t i = 0; i < count; ++i) {
-    content += FMT("{}\n", i);
-  }
-
-  Util::write_file("test", content);
-  Counters counters = Statistics::read("test");
-
-  REQUIRE(counters.size() == count);
-  for (size_t i = 0; i < count; ++i) {
-    CHECK(counters.get_raw(i) == i);
-  }
-}
-
-TEST_CASE("Update")
-{
-  TestContext test_context;
-
-  Util::write_file("test", "0 1 2 3 27 5\n");
-
-  auto counters = Statistics::update("test", [](Counters& cs) {
-    cs.increment(Statistic::internal_error, 1);
-    cs.increment(Statistic::cache_miss, 6);
-  });
-  REQUIRE(counters);
-
-  CHECK(counters->get(Statistic::internal_error) == 4);
-  CHECK(counters->get(Statistic::cache_miss) == 33);
-
-  counters = Statistics::read("test");
-  CHECK(counters->get(Statistic::internal_error) == 4);
-  CHECK(counters->get(Statistic::cache_miss) == 33);
-}
-
-TEST_SUITE_END();
index cb2be415c2d126cde905a236b92dd7a321c93fde..afe7e1cc2d457b09b87a531771988f2a7fb2828d 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 #include "../src/fmtmacros.hpp"
 #include "TestUtil.hpp"
 
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+
 #include "third_party/doctest.h"
 #include "third_party/nonstd/optional.hpp"
 
+#include <fcntl.h>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
 #include <algorithm>
 
 using doctest::Approx;
@@ -169,24 +178,6 @@ TEST_CASE("Util::strip_ansi_csi_seqs")
   CHECK(Util::strip_ansi_csi_seqs(input) == "Normal, bold, red, bold green.\n");
 }
 
-TEST_CASE("Util::ends_with")
-{
-  CHECK(Util::ends_with("", ""));
-  CHECK(Util::ends_with("x", ""));
-  CHECK(Util::ends_with("x", "x"));
-  CHECK(Util::ends_with("xy", ""));
-  CHECK(Util::ends_with("xy", "y"));
-  CHECK(Util::ends_with("xy", "xy"));
-  CHECK(Util::ends_with("xyz", ""));
-  CHECK(Util::ends_with("xyz", "z"));
-  CHECK(Util::ends_with("xyz", "yz"));
-  CHECK(Util::ends_with("xyz", "xyz"));
-
-  CHECK_FALSE(Util::ends_with("", "x"));
-  CHECK_FALSE(Util::ends_with("x", "y"));
-  CHECK_FALSE(Util::ends_with("x", "xy"));
-}
-
 TEST_CASE("Util::ensure_dir_exists")
 {
   TestContext test_context;
@@ -244,37 +235,6 @@ TEST_CASE("Util::fallocate")
   CHECK(Stat::stat(filename).size() == 20000);
 }
 
-TEST_CASE("Util::for_each_level_1_subdir")
-{
-  std::vector<std::string> actual;
-  Util::for_each_level_1_subdir(
-    "cache_dir",
-    [&](const std::string& subdir, const Util::ProgressReceiver&) {
-      actual.push_back(subdir);
-    },
-    [](double) {});
-
-  std::vector<std::string> expected = {
-    "cache_dir/0",
-    "cache_dir/1",
-    "cache_dir/2",
-    "cache_dir/3",
-    "cache_dir/4",
-    "cache_dir/5",
-    "cache_dir/6",
-    "cache_dir/7",
-    "cache_dir/8",
-    "cache_dir/9",
-    "cache_dir/a",
-    "cache_dir/b",
-    "cache_dir/c",
-    "cache_dir/d",
-    "cache_dir/e",
-    "cache_dir/f",
-  };
-  CHECK(actual == expected);
-}
-
 TEST_CASE("Util::format_argv_for_logging")
 {
   const char* argv_0[] = {nullptr};
@@ -356,63 +316,13 @@ TEST_CASE("Util::get_extension")
 static inline std::string
 os_path(std::string path)
 {
-#if !defined(HAVE_DIRENT_H) && DIR_DELIM_CH != '/'
-  std::replace(path.begin(), path.end(), '/', DIR_DELIM_CH);
+#if defined(_WIN32) && !defined(HAVE_DIRENT_H)
+  std::replace(path.begin(), path.end(), '/', '\\');
 #endif
 
   return path;
 }
 
-TEST_CASE("Util::get_level_1_files")
-{
-  TestContext test_context;
-
-  Util::create_dir("e/m/p/t/y");
-
-  Util::create_dir("0/1");
-  Util::create_dir("0/f/c");
-  Util::write_file("0/file_a", "");
-  Util::write_file("0/1/file_b", "1");
-  Util::write_file("0/1/file_c", "12");
-  Util::write_file("0/f/c/file_d", "123");
-
-  auto null_receiver = [](double) {};
-
-  SUBCASE("nonexistent subdirectory")
-  {
-    const auto files = Util::get_level_1_files("2", null_receiver);
-    CHECK(files.empty());
-  }
-
-  SUBCASE("empty subdirectory")
-  {
-    const auto files = Util::get_level_1_files("e", null_receiver);
-    CHECK(files.empty());
-  }
-
-  SUBCASE("simple case")
-  {
-    auto files = Util::get_level_1_files("0", null_receiver);
-    REQUIRE(files.size() == 4);
-
-    // Files within a level are in arbitrary order, sort them to be able to
-    // verify them.
-    std::sort(
-      files.begin(), files.end(), [](const CacheFile& f1, const CacheFile& f2) {
-        return f1.path() < f2.path();
-      });
-
-    CHECK(files[0].path() == os_path("0/1/file_b"));
-    CHECK(files[0].lstat().size() == 1);
-    CHECK(files[1].path() == os_path("0/1/file_c"));
-    CHECK(files[1].lstat().size() == 2);
-    CHECK(files[2].path() == os_path("0/f/c/file_d"));
-    CHECK(files[2].lstat().size() == 3);
-    CHECK(files[3].path() == os_path("0/file_a"));
-    CHECK(files[3].lstat().size() == 0);
-  }
-}
-
 TEST_CASE("Util::get_relative_path")
 {
 #ifdef _WIN32
@@ -442,14 +352,6 @@ TEST_CASE("Util::get_relative_path")
 #endif
 }
 
-TEST_CASE("Util::get_path_in_cache")
-{
-  CHECK(Util::get_path_in_cache("/zz/ccache", 1, "ABCDEF.suffix")
-        == "/zz/ccache/A/BCDEF.suffix");
-  CHECK(Util::get_path_in_cache("/zz/ccache", 4, "ABCDEF.suffix")
-        == "/zz/ccache/A/B/C/D/EF.suffix");
-}
-
 TEST_CASE("Util::hard_link")
 {
   TestContext test_context;
@@ -471,7 +373,7 @@ TEST_CASE("Util::hard_link")
 
   SUBCASE("Link nonexistent file")
   {
-    CHECK_THROWS_AS(Util::hard_link("old", "new"), Error);
+    CHECK_THROWS_AS(Util::hard_link("old", "new"), core::Error);
   }
 }
 
@@ -534,22 +436,6 @@ TEST_CASE("Util::int_to_big_endian")
   CHECK(bytes[7] == 0xca);
 }
 
-TEST_CASE("Util::is_absolute_path")
-{
-#ifdef _WIN32
-  CHECK(Util::is_absolute_path("C:/"));
-  CHECK(Util::is_absolute_path("C:\\foo/fie"));
-  CHECK(Util::is_absolute_path("/C:\\foo/fie")); // MSYS/Cygwin path
-  CHECK(!Util::is_absolute_path(""));
-  CHECK(!Util::is_absolute_path("foo\\fie/fum"));
-  CHECK(!Util::is_absolute_path("C:foo/fie"));
-#endif
-  CHECK(Util::is_absolute_path("/"));
-  CHECK(Util::is_absolute_path("/foo/fie"));
-  CHECK(!Util::is_absolute_path(""));
-  CHECK(!Util::is_absolute_path("foo/fie"));
-}
-
 TEST_CASE("Util::is_dir_separator")
 {
   CHECK(!Util::is_dir_separator('x'));
@@ -700,46 +586,6 @@ TEST_CASE("Util::parse_duration")
     "invalid suffix (supported: d (day) and s (second)): \"2\"");
 }
 
-TEST_CASE("Util::parse_signed")
-{
-  CHECK(Util::parse_signed("0") == 0);
-  CHECK(Util::parse_signed("2") == 2);
-  CHECK(Util::parse_signed("-17") == -17);
-  CHECK(Util::parse_signed("42") == 42);
-  CHECK(Util::parse_signed("0666") == 666);
-  CHECK(Util::parse_signed(" 777 ") == 777);
-
-  CHECK_THROWS_WITH(Util::parse_signed(""), "invalid integer: \"\"");
-  CHECK_THROWS_WITH(Util::parse_signed("x"), "invalid integer: \"x\"");
-  CHECK_THROWS_WITH(Util::parse_signed("0x"), "invalid integer: \"0x\"");
-  CHECK_THROWS_WITH(Util::parse_signed("0x4"), "invalid integer: \"0x4\"");
-
-  // Custom description not used for invalid value.
-  CHECK_THROWS_WITH(Util::parse_signed("apple", nullopt, nullopt, "banana"),
-                    "invalid integer: \"apple\"");
-
-  // Boundary values.
-  CHECK_THROWS_WITH(Util::parse_signed("-9223372036854775809"),
-                    "invalid integer: \"-9223372036854775809\"");
-  CHECK(Util::parse_signed("-9223372036854775808") == INT64_MIN);
-  CHECK(Util::parse_signed("9223372036854775807") == INT64_MAX);
-  CHECK_THROWS_WITH(Util::parse_signed("9223372036854775808"),
-                    "invalid integer: \"9223372036854775808\"");
-
-  // Min and max values.
-  CHECK_THROWS_WITH(Util::parse_signed("-2", -1, 1),
-                    "integer must be between -1 and 1");
-  CHECK(Util::parse_signed("-1", -1, 1) == -1);
-  CHECK(Util::parse_signed("0", -1, 1) == 0);
-  CHECK(Util::parse_signed("1", -1, 1) == 1);
-  CHECK_THROWS_WITH(Util::parse_signed("2", -1, 1),
-                    "integer must be between -1 and 1");
-
-  // Custom description used for boundary violation.
-  CHECK_THROWS_WITH(Util::parse_signed("0", 1, 2, "banana"),
-                    "banana must be between 1 and 2");
-}
-
 TEST_CASE("Util::parse_size")
 {
   CHECK(Util::parse_size("0") == 0);
@@ -764,35 +610,6 @@ TEST_CASE("Util::parse_size")
   CHECK_THROWS_WITH(Util::parse_size("10x"), "invalid size: \"10x\"");
 }
 
-TEST_CASE("Util::parse_unsigned")
-{
-  CHECK(Util::parse_unsigned("0") == 0);
-  CHECK(Util::parse_unsigned("2") == 2);
-  CHECK(Util::parse_unsigned("42") == 42);
-  CHECK(Util::parse_unsigned("0666") == 666);
-  CHECK(Util::parse_unsigned(" 777 ") == 777);
-
-  CHECK_THROWS_WITH(Util::parse_unsigned(""), "invalid unsigned integer: \"\"");
-  CHECK_THROWS_WITH(Util::parse_unsigned("x"),
-                    "invalid unsigned integer: \"x\"");
-  CHECK_THROWS_WITH(Util::parse_unsigned("0x"),
-                    "invalid unsigned integer: \"0x\"");
-  CHECK_THROWS_WITH(Util::parse_unsigned("0x4"),
-                    "invalid unsigned integer: \"0x4\"");
-
-  // Custom description not used for invalid value.
-  CHECK_THROWS_WITH(Util::parse_unsigned("apple", nullopt, nullopt, "banana"),
-                    "invalid unsigned integer: \"apple\"");
-
-  // Boundary values.
-  CHECK_THROWS_WITH(Util::parse_unsigned("-1"),
-                    "invalid unsigned integer: \"-1\"");
-  CHECK(Util::parse_unsigned("0") == 0);
-  CHECK(Util::parse_unsigned("18446744073709551615") == UINT64_MAX);
-  CHECK_THROWS_WITH(Util::parse_unsigned("18446744073709551616"),
-                    "invalid unsigned integer: \"18446744073709551616\"");
-}
-
 TEST_CASE("Util::read_file and Util::write_file")
 {
   TestContext test_context;
@@ -861,133 +678,8 @@ TEST_CASE("Util::same_program_name")
 #endif
 }
 
-TEST_CASE("Util::split_into_views")
-{
-  {
-    CHECK(Util::split_into_views("", "/").empty());
-  }
-  {
-    CHECK(Util::split_into_views("///", "/").empty());
-  }
-  {
-    auto s = Util::split_into_views("a/b", "/");
-    REQUIRE(s.size() == 2);
-    CHECK(s.at(0) == "a");
-    CHECK(s.at(1) == "b");
-  }
-  {
-    auto s = Util::split_into_views("a/b", "x");
-    REQUIRE(s.size() == 1);
-    CHECK(s.at(0) == "a/b");
-  }
-  {
-    auto s = Util::split_into_views("a/b:c", "/:");
-    REQUIRE(s.size() == 3);
-    CHECK(s.at(0) == "a");
-    CHECK(s.at(1) == "b");
-    CHECK(s.at(2) == "c");
-  }
-  {
-    auto s = Util::split_into_views(":a//b..:.c/:/.", "/:.");
-    REQUIRE(s.size() == 3);
-    CHECK(s.at(0) == "a");
-    CHECK(s.at(1) == "b");
-    CHECK(s.at(2) == "c");
-  }
-  {
-    auto s = Util::split_into_views(".0.1.2.3.4.5.6.7.8.9.", "/:.+_abcdef");
-    REQUIRE(s.size() == 10);
-    CHECK(s.at(0) == "0");
-    CHECK(s.at(9) == "9");
-  }
-}
-
-TEST_CASE("Util::split_into_strings")
-{
-  {
-    CHECK(Util::split_into_strings("", "/").empty());
-  }
-  {
-    CHECK(Util::split_into_strings("///", "/").empty());
-  }
-  {
-    auto s = Util::split_into_strings("a/b", "/");
-    REQUIRE(s.size() == 2);
-    CHECK(s.at(0) == "a");
-    CHECK(s.at(1) == "b");
-  }
-  {
-    auto s = Util::split_into_strings("a/b", "x");
-    REQUIRE(s.size() == 1);
-    CHECK(s.at(0) == "a/b");
-  }
-  {
-    auto s = Util::split_into_strings("a/b:c", "/:");
-    REQUIRE(s.size() == 3);
-    CHECK(s.at(0) == "a");
-    CHECK(s.at(1) == "b");
-    CHECK(s.at(2) == "c");
-  }
-  {
-    auto s = Util::split_into_strings(":a//b..:.c/:/.", "/:.");
-    REQUIRE(s.size() == 3);
-    CHECK(s.at(0) == "a");
-    CHECK(s.at(1) == "b");
-    CHECK(s.at(2) == "c");
-  }
-  {
-    auto s = Util::split_into_strings(".0.1.2.3.4.5.6.7.8.9.", "/:.+_abcdef");
-    REQUIRE(s.size() == 10);
-    CHECK(s.at(0) == "0");
-    CHECK(s.at(9) == "9");
-  }
-}
-
-TEST_CASE("Util::starts_with")
-{
-  // starts_with(const char*, string_view)
-  CHECK(Util::starts_with("", ""));
-  CHECK(Util::starts_with("x", ""));
-  CHECK(Util::starts_with("x", "x"));
-  CHECK(Util::starts_with("xy", ""));
-  CHECK(Util::starts_with("xy", "x"));
-  CHECK(Util::starts_with("xy", "xy"));
-  CHECK(Util::starts_with("xyz", ""));
-  CHECK(Util::starts_with("xyz", "x"));
-  CHECK(Util::starts_with("xyz", "xy"));
-  CHECK(Util::starts_with("xyz", "xyz"));
-
-  CHECK_FALSE(Util::starts_with("", "x"));
-  CHECK_FALSE(Util::starts_with("x", "y"));
-  CHECK_FALSE(Util::starts_with("x", "xy"));
-
-  // starts_with(string_view, string_view)
-  CHECK(Util::starts_with(std::string(""), ""));
-  CHECK(Util::starts_with(std::string("x"), ""));
-  CHECK(Util::starts_with(std::string("x"), "x"));
-  CHECK(Util::starts_with(std::string("xy"), ""));
-  CHECK(Util::starts_with(std::string("xy"), "x"));
-  CHECK(Util::starts_with(std::string("xy"), "xy"));
-  CHECK(Util::starts_with(std::string("xyz"), ""));
-  CHECK(Util::starts_with(std::string("xyz"), "x"));
-  CHECK(Util::starts_with(std::string("xyz"), "xy"));
-  CHECK(Util::starts_with(std::string("xyz"), "xyz"));
-
-  CHECK_FALSE(Util::starts_with(std::string(""), "x"));
-  CHECK_FALSE(Util::starts_with(std::string("x"), "y"));
-  CHECK_FALSE(Util::starts_with(std::string("x"), "xy"));
-}
-
-TEST_CASE("Util::strip_whitespace")
-{
-  CHECK(Util::strip_whitespace("") == "");
-  CHECK(Util::strip_whitespace("x") == "x");
-  CHECK(Util::strip_whitespace(" x") == "x");
-  CHECK(Util::strip_whitespace("x ") == "x");
-  CHECK(Util::strip_whitespace(" x ") == "x");
-  CHECK(Util::strip_whitespace(" \n\tx \n\t") == "x");
-  CHECK(Util::strip_whitespace("  x  y  ") == "x  y");
-}
+// Util::split_into_strings and Util::split_into_views are tested implicitly in
+// test_util_Tokenizer.cpp.
 
 TEST_CASE("Util::to_lowercase")
 {
index a72f59e56d5120fd611b2bb50a84a9c248530ca4..fd06dc4e8425d4189d06055923001bf711e4f0d5 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2019-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 // this program; if not, write to the Free Software Foundation, Inc., 51
 // Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-#include "../src/Compression.hpp"
-#include "../src/Compressor.hpp"
-#include "../src/Decompressor.hpp"
 #include "../src/File.hpp"
 #include "TestUtil.hpp"
 
+#include <compression/Compressor.hpp>
+#include <compression/Decompressor.hpp>
+#include <compression/types.hpp>
+
 #include "third_party/doctest.h"
 
+#include <cstring>
+
+using compression::Compressor;
+using compression::Decompressor;
 using TestUtil::TestContext;
 
 TEST_SUITE_BEGIN("ZstdCompression");
 
-TEST_CASE("Small Compression::Type::zstd roundtrip")
+TEST_CASE("Small compression::Type::zstd roundtrip")
 {
   TestContext test_context;
 
   File f("data.zstd", "wb");
   auto compressor =
-    Compressor::create_from_type(Compression::Type::zstd, f.get(), 1);
+    Compressor::create_from_type(compression::Type::zstd, f.get(), 1);
   CHECK(compressor->actual_compression_level() == 1);
   compressor->write("foobar", 6);
   compressor->finalize();
 
   f.open("data.zstd", "rb");
   auto decompressor =
-    Decompressor::create_from_type(Compression::Type::zstd, f.get());
+    Decompressor::create_from_type(compression::Type::zstd, f.get());
 
   char buffer[4];
   decompressor->read(buffer, 4);
@@ -62,7 +67,7 @@ TEST_CASE("Small Compression::Type::zstd roundtrip")
                     "failed to read from zstd input stream");
 }
 
-TEST_CASE("Large compressible Compression::Type::zstd roundtrip")
+TEST_CASE("Large compressible compression::Type::zstd roundtrip")
 {
   TestContext test_context;
 
@@ -70,7 +75,7 @@ TEST_CASE("Large compressible Compression::Type::zstd roundtrip")
 
   File f("data.zstd", "wb");
   auto compressor =
-    Compressor::create_from_type(Compression::Type::zstd, f.get(), 1);
+    Compressor::create_from_type(compression::Type::zstd, f.get(), 1);
   for (size_t i = 0; i < 1000; i++) {
     compressor->write(data, sizeof(data));
   }
@@ -78,7 +83,7 @@ TEST_CASE("Large compressible Compression::Type::zstd roundtrip")
 
   f.open("data.zstd", "rb");
   auto decompressor =
-    Decompressor::create_from_type(Compression::Type::zstd, f.get());
+    Decompressor::create_from_type(compression::Type::zstd, f.get());
 
   char buffer[sizeof(data)];
   for (size_t i = 0; i < 1000; i++) {
@@ -94,7 +99,7 @@ TEST_CASE("Large compressible Compression::Type::zstd roundtrip")
                     "failed to read from zstd input stream");
 }
 
-TEST_CASE("Large uncompressible Compression::Type::zstd roundtrip")
+TEST_CASE("Large uncompressible compression::Type::zstd roundtrip")
 {
   TestContext test_context;
 
@@ -105,13 +110,13 @@ TEST_CASE("Large uncompressible Compression::Type::zstd roundtrip")
 
   File f("data.zstd", "wb");
   auto compressor =
-    Compressor::create_from_type(Compression::Type::zstd, f.get(), 1);
+    Compressor::create_from_type(compression::Type::zstd, f.get(), 1);
   compressor->write(data, sizeof(data));
   compressor->finalize();
 
   f.open("data.zstd", "rb");
   auto decompressor =
-    Decompressor::create_from_type(Compression::Type::zstd, f.get());
+    Decompressor::create_from_type(compression::Type::zstd, f.get());
 
   char buffer[sizeof(data)];
   decompressor->read(buffer, sizeof(buffer));
index 829a7a746805ca92624ea994aeef7fd9879e1e2b..09c27b946900f1394d8eb2503d74e018f695d25f 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2010-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2010-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 #include "../src/Args.hpp"
 #include "../src/Config.hpp"
 #include "../src/Context.hpp"
-#include "../src/Statistic.hpp"
 #include "../src/Util.hpp"
 #include "../src/fmtmacros.hpp"
 #include "TestUtil.hpp"
 #include "argprocessing.hpp"
 
+#include <core/Statistic.hpp>
+#include <core/wincompat.hpp>
+
 #include "third_party/doctest.h"
 
 #include <algorithm>
 
+using core::Statistic;
 using TestUtil::TestContext;
 
 namespace {
index 021c73d589099880c1f8ad2373250f17dd8ed94a..5a8dd6d18d082adade1cf1fcfc4e5cae663ab2a6 100644 (file)
 #include "../src/Finalizer.hpp"
 #include "TestUtil.hpp"
 
+#include <core/wincompat.hpp>
+
 #include "third_party/doctest.h"
 #include "third_party/win32/mktemp.h"
 
+#include <sddl.h>
+
 #include <algorithm>
 #include <memory>
 #include <ostream>
-#include <sddl.h>
 #include <utility>
 
 using TestUtil::TestContext;
index cd59588ae82e8ac2ea091ad74dfbd3b644b87034..e465a97c5bf368b3b440774a9dc9f8eb5f1e686a 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
 // Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
 #include "../src/Context.hpp"
-#include "../src/Sloppiness.hpp"
 #include "../src/ccache.hpp"
 #include "../src/fmtmacros.hpp"
 #include "TestUtil.hpp"
 
+#include <core/wincompat.hpp>
+
 #include "third_party/doctest.h"
 #include "third_party/nonstd/optional.hpp"
 
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
 #ifdef MYNAME
 #  define CCACHE_NAME MYNAME
 #else
@@ -42,11 +47,10 @@ helper(const char* args,
        const char* find_executable_return_string = nullptr)
 {
   const auto find_executable_stub =
-    [&find_executable_return_string](
-      const Context&, const std::string& s, const std::string&) -> std::string {
-    return find_executable_return_string ? find_executable_return_string
-                                         : "resolved_" + s;
-  };
+    [&find_executable_return_string](const auto&, const auto& s, const auto&) {
+      return find_executable_return_string ? find_executable_return_string
+                                           : "resolved_" + s;
+    };
 
   Context ctx;
   ctx.config.set_compiler(config_compiler);
diff --git a/unittest/test_compression_types.cpp b/unittest/test_compression_types.cpp
new file mode 100644 (file)
index 0000000..82edb0e
--- /dev/null
@@ -0,0 +1,52 @@
+// Copyright (C) 2019-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "../src/Config.hpp"
+
+#include <compression/types.hpp>
+
+#include "third_party/doctest.h"
+
+TEST_SUITE_BEGIN("compression");
+
+TEST_CASE("compression::level_from_config")
+{
+  Config config;
+  CHECK(compression::level_from_config(config) == 0);
+}
+
+TEST_CASE("compression::type_from_config")
+{
+  Config config;
+  CHECK(compression::type_from_config(config) == compression::Type::zstd);
+}
+
+TEST_CASE("compression::type_from_int")
+{
+  CHECK(compression::type_from_int(0) == compression::Type::none);
+  CHECK(compression::type_from_int(1) == compression::Type::zstd);
+  CHECK_THROWS_WITH(compression::type_from_int(2), "Unknown type: 2");
+}
+
+TEST_CASE("compression::type_to_string")
+{
+  CHECK(compression::type_to_string(compression::Type::none) == "none");
+  CHECK(compression::type_to_string(compression::Type::zstd) == "zstd");
+}
+
+TEST_SUITE_END();
diff --git a/unittest/test_core_Statistics.cpp b/unittest/test_core_Statistics.cpp
new file mode 100644 (file)
index 0000000..3926259
--- /dev/null
@@ -0,0 +1,50 @@
+// Copyright (C) 2011-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "TestUtil.hpp"
+
+#include <core/Statistic.hpp>
+#include <core/Statistics.hpp>
+
+#include <third_party/doctest.h>
+
+#include <iostream> // macOS bug: https://github.com/onqtam/doctest/issues/126
+
+using core::Statistic;
+using core::Statistics;
+using core::StatisticsCounters;
+using TestUtil::TestContext;
+
+TEST_SUITE_BEGIN("core::Statistics");
+
+TEST_CASE("get_statistics_ids")
+{
+  TestContext test_context;
+
+  StatisticsCounters counters;
+  counters.increment(Statistic::cache_size_kibibyte);
+  counters.increment(Statistic::cache_miss);
+  counters.increment(Statistic::direct_cache_hit);
+  counters.increment(Statistic::autoconf_test);
+
+  std::vector<std::string> expected = {
+    "autoconf_test", "cache_miss", "direct_cache_hit"};
+  CHECK(Statistics(counters).get_statistics_ids() == expected);
+}
+
+TEST_SUITE_END();
diff --git a/unittest/test_core_StatisticsCounters.cpp b/unittest/test_core_StatisticsCounters.cpp
new file mode 100644 (file)
index 0000000..01db989
--- /dev/null
@@ -0,0 +1,95 @@
+// Copyright (C) 2020-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "TestUtil.hpp"
+
+#include <core/Statistic.hpp>
+#include <core/StatisticsCounters.hpp>
+
+#include "third_party/doctest.h"
+
+using core::Statistic;
+using core::StatisticsCounters;
+using TestUtil::TestContext;
+
+TEST_SUITE_BEGIN("core::StatisticsCounters");
+
+TEST_CASE("StatisticsCounters")
+{
+  TestContext test_context;
+
+  StatisticsCounters counters;
+  CHECK(counters.size() == static_cast<size_t>(Statistic::END));
+
+  SUBCASE("Get and set statistic")
+  {
+    CHECK(counters.get(Statistic::cache_miss) == 0);
+    counters.set(Statistic::cache_miss, 27);
+    CHECK(counters.get(Statistic::cache_miss) == 27);
+  }
+
+  SUBCASE("Get and set raw index")
+  {
+    CHECK(counters.get_raw(4) == 0);
+    counters.set_raw(4, 27);
+    CHECK(counters.get(Statistic::cache_miss) == 27);
+  }
+
+  SUBCASE("Set future raw counter")
+  {
+    const auto future_index = static_cast<size_t>(Statistic::END) + 2;
+    counters.set_raw(future_index, 42);
+    CHECK(counters.get_raw(future_index) == 42);
+  }
+
+  SUBCASE("Increment single counter")
+  {
+    counters.set(Statistic::cache_miss, 4);
+
+    counters.increment(Statistic::cache_miss);
+    CHECK(counters.get(Statistic::cache_miss) == 5);
+
+    counters.increment(Statistic::cache_miss, -3);
+    CHECK(counters.get(Statistic::cache_miss) == 2);
+
+    counters.increment(Statistic::cache_miss, -3);
+    CHECK(counters.get(Statistic::cache_miss) == 0);
+  }
+
+  SUBCASE("Increment many counters")
+  {
+    counters.set(Statistic::direct_cache_hit, 3);
+    counters.set(Statistic::cache_miss, 2);
+    counters.set(Statistic::files_in_cache, 10);
+    counters.set(Statistic::cache_size_kibibyte, 1);
+
+    StatisticsCounters updates;
+    updates.set(Statistic::direct_cache_hit, 6);
+    updates.set(Statistic::cache_miss, 5);
+    updates.set(Statistic::files_in_cache, -1);
+    updates.set(Statistic::cache_size_kibibyte, -4);
+
+    counters.increment(updates);
+    CHECK(counters.get(Statistic::direct_cache_hit) == 9);
+    CHECK(counters.get(Statistic::cache_miss) == 7);
+    CHECK(counters.get(Statistic::files_in_cache) == 9);
+    CHECK(counters.get(Statistic::cache_size_kibibyte) == 0); // No wrap-around
+  }
+}
+
+TEST_SUITE_END();
diff --git a/unittest/test_core_StatsLog.cpp b/unittest/test_core_StatsLog.cpp
new file mode 100644 (file)
index 0000000..132288a
--- /dev/null
@@ -0,0 +1,55 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "TestUtil.hpp"
+
+#include <Util.hpp>
+#include <core/StatsLog.hpp>
+
+#include <third_party/doctest.h>
+
+using core::Statistic;
+using core::StatsLog;
+using TestUtil::TestContext;
+
+TEST_SUITE_BEGIN("core::StatsFile");
+
+TEST_CASE("read")
+{
+  TestContext test_context;
+
+  Util::write_file("stats.log", "# comment\ndirect_cache_hit\n");
+  const auto counters = StatsLog("stats.log").read();
+
+  CHECK(counters.get(Statistic::direct_cache_hit) == 1);
+  CHECK(counters.get(Statistic::cache_miss) == 0);
+}
+
+TEST_CASE("log_result")
+{
+  TestContext test_context;
+
+  StatsLog stats_log("stats.log");
+  stats_log.log_result("foo.c", {"cache_miss"});
+  stats_log.log_result("bar.c", {"preprocessed_cache_hit"});
+
+  CHECK(Util::read_file("stats.log")
+        == "# foo.c\ncache_miss\n# bar.c\npreprocessed_cache_hit\n");
+}
+
+TEST_SUITE_END();
index 8f76516573b65eb988680f04bb4f261de4648de3..0ae17c62440de1a11d4ad33d5353edfeac119492 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2010-2020 Joel Rosdahl and other contributors
+// Copyright (C) 2010-2021 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -16,7 +16,6 @@
 // this program; if not, write to the Free Software Foundation, Inc., 51
 // Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-#include "../src/Context.hpp"
 #include "../src/Hash.hpp"
 #include "../src/hashutil.hpp"
 #include "TestUtil.hpp"
@@ -108,8 +107,6 @@ TEST_CASE("hash_multicommand_output")
 
 TEST_CASE("hash_multicommand_output_error_handling")
 {
-  Context ctx;
-
   Hash h1;
   Hash h2;
 
diff --git a/unittest/test_storage_primary_StatsFile.cpp b/unittest/test_storage_primary_StatsFile.cpp
new file mode 100644 (file)
index 0000000..2a7cdad
--- /dev/null
@@ -0,0 +1,106 @@
+// Copyright (C) 2011-2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "TestUtil.hpp"
+
+#include <Util.hpp>
+#include <core/Statistic.hpp>
+#include <fmtmacros.hpp>
+#include <storage/primary/StatsFile.hpp>
+
+#include <third_party/doctest.h>
+
+using core::Statistic;
+using storage::primary::StatsFile;
+using TestUtil::TestContext;
+
+TEST_SUITE_BEGIN("storage::primary::StatsFile");
+
+TEST_CASE("Read nonexistent")
+{
+  TestContext test_context;
+
+  const auto counters = StatsFile("test").read();
+
+  REQUIRE(counters.size() == static_cast<size_t>(Statistic::END));
+  CHECK(counters.get(Statistic::cache_miss) == 0);
+}
+
+TEST_CASE("Read bad")
+{
+  TestContext test_context;
+
+  Util::write_file("test", "bad 1 2 3 4 5\n");
+  const auto counters = StatsFile("test").read();
+
+  REQUIRE(counters.size() == static_cast<size_t>(Statistic::END));
+  CHECK(counters.get(Statistic::cache_miss) == 0);
+}
+
+TEST_CASE("Read existing")
+{
+  TestContext test_context;
+
+  Util::write_file("test", "0 1 2 3 27 5\n");
+  const auto counters = StatsFile("test").read();
+
+  REQUIRE(counters.size() == static_cast<size_t>(Statistic::END));
+  CHECK(counters.get(Statistic::cache_miss) == 27);
+  CHECK(counters.get(Statistic::could_not_use_modules) == 0);
+}
+
+TEST_CASE("Read future counters")
+{
+  TestContext test_context;
+
+  std::string content;
+  size_t count = static_cast<size_t>(Statistic::END) + 1;
+  for (size_t i = 0; i < count; ++i) {
+    content += FMT("{}\n", i);
+  }
+
+  Util::write_file("test", content);
+  const auto counters = StatsFile("test").read();
+
+  REQUIRE(counters.size() == count);
+  for (size_t i = 0; i < count; ++i) {
+    CHECK(counters.get_raw(i) == i);
+  }
+}
+
+TEST_CASE("Update")
+{
+  TestContext test_context;
+
+  Util::write_file("test", "0 1 2 3 27 5\n");
+
+  auto counters = StatsFile("test").update([](auto& cs) {
+    cs.increment(Statistic::internal_error, 1);
+    cs.increment(Statistic::cache_miss, 6);
+  });
+  REQUIRE(counters);
+
+  CHECK(counters->get(Statistic::internal_error) == 4);
+  CHECK(counters->get(Statistic::cache_miss) == 33);
+
+  counters = StatsFile("test").read();
+  CHECK(counters->get(Statistic::internal_error) == 4);
+  CHECK(counters->get(Statistic::cache_miss) == 33);
+}
+
+TEST_SUITE_END();
diff --git a/unittest/test_storage_primary_util.cpp b/unittest/test_storage_primary_util.cpp
new file mode 100644 (file)
index 0000000..b847075
--- /dev/null
@@ -0,0 +1,120 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "TestUtil.hpp"
+
+#include <Util.hpp>
+#include <storage/primary/util.hpp>
+
+#include <third_party/doctest.h>
+
+#include <string>
+
+using TestUtil::TestContext;
+
+static inline std::string
+os_path(std::string path)
+{
+#if defined(_WIN32) && !defined(HAVE_DIRENT_H)
+  std::replace(path.begin(), path.end(), '/', '\\');
+#endif
+
+  return path;
+}
+
+TEST_SUITE_BEGIN("storage::primary::util");
+
+TEST_CASE("storage::primary::for_each_level_1_subdir")
+{
+  std::vector<std::string> actual;
+  storage::primary::for_each_level_1_subdir(
+    "cache_dir",
+    [&](const auto& subdir, const auto&) { actual.push_back(subdir); },
+    [](double) {});
+
+  std::vector<std::string> expected = {
+    "cache_dir/0",
+    "cache_dir/1",
+    "cache_dir/2",
+    "cache_dir/3",
+    "cache_dir/4",
+    "cache_dir/5",
+    "cache_dir/6",
+    "cache_dir/7",
+    "cache_dir/8",
+    "cache_dir/9",
+    "cache_dir/a",
+    "cache_dir/b",
+    "cache_dir/c",
+    "cache_dir/d",
+    "cache_dir/e",
+    "cache_dir/f",
+  };
+  CHECK(actual == expected);
+}
+
+TEST_CASE("storage::primary::get_level_1_files")
+{
+  TestContext test_context;
+
+  Util::create_dir("e/m/p/t/y");
+
+  Util::create_dir("0/1");
+  Util::create_dir("0/f/c");
+  Util::write_file("0/file_a", "");
+  Util::write_file("0/1/file_b", "1");
+  Util::write_file("0/1/file_c", "12");
+  Util::write_file("0/f/c/file_d", "123");
+
+  auto null_receiver = [](double) {};
+
+  SUBCASE("nonexistent subdirectory")
+  {
+    const auto files = storage::primary::get_level_1_files("2", null_receiver);
+    CHECK(files.empty());
+  }
+
+  SUBCASE("empty subdirectory")
+  {
+    const auto files = storage::primary::get_level_1_files("e", null_receiver);
+    CHECK(files.empty());
+  }
+
+  SUBCASE("simple case")
+  {
+    auto files = storage::primary::get_level_1_files("0", null_receiver);
+    REQUIRE(files.size() == 4);
+
+    // Files within a level are in arbitrary order, sort them to be able to
+    // verify them.
+    std::sort(files.begin(), files.end(), [](const auto& f1, const auto& f2) {
+      return f1.path() < f2.path();
+    });
+
+    CHECK(files[0].path() == os_path("0/1/file_b"));
+    CHECK(files[0].lstat().size() == 1);
+    CHECK(files[1].path() == os_path("0/1/file_c"));
+    CHECK(files[1].lstat().size() == 2);
+    CHECK(files[2].path() == os_path("0/f/c/file_d"));
+    CHECK(files[2].lstat().size() == 3);
+    CHECK(files[3].path() == os_path("0/file_a"));
+    CHECK(files[3].lstat().size() == 0);
+  }
+}
+
+TEST_SUITE_END();
diff --git a/unittest/test_util_TextTable.cpp b/unittest/test_util_TextTable.cpp
new file mode 100644 (file)
index 0000000..3624073
--- /dev/null
@@ -0,0 +1,121 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include <util/TextTable.hpp>
+
+#include <third_party/doctest.h>
+
+#include <iostream> // macOS bug: https://github.com/onqtam/doctest/issues/126
+
+TEST_CASE("TextTable")
+{
+  using C = util::TextTable::Cell;
+
+  util::TextTable table;
+
+  SUBCASE("empty")
+  {
+    CHECK(table.render() == "");
+  }
+
+  SUBCASE("1x1")
+  {
+    table.add_row({"a"});
+    CHECK(table.render() == "a\n");
+  }
+
+  SUBCASE("2x1 with space prefix/suffix")
+  {
+    table.add_row({std::string(" a "), C(" b ")});
+    CHECK(table.render() == " a   b\n");
+  }
+
+  SUBCASE("1x2")
+  {
+    table.add_row({"a"});
+    table.add_row({1});
+    CHECK(table.render() == "a\n1\n");
+  }
+
+  SUBCASE("3 + 2")
+  {
+    table.add_row({"a", "b", "c"});
+    table.add_row({"aa", "bbb"});
+    CHECK(table.render()
+          == ("a  b   c\n"
+              "aa bbb\n"));
+  }
+
+  SUBCASE("strings and numbers")
+  {
+    table.add_row({"a", 123, "cc"});
+    table.add_row({"aa", 4, "ccc"});
+    table.add_row({"aaa", 56, "c"});
+    CHECK(table.render()
+          == ("a   123 cc\n"
+              "aa    4 ccc\n"
+              "aaa  56 c\n"));
+  }
+
+  SUBCASE("left align")
+  {
+    table.add_row({"a", 123, "cc"});
+    table.add_row({"aa", C(4).left_align(), "ccc"});
+    table.add_row({"aaa", 56, "c"});
+    CHECK(table.render()
+          == ("a   123 cc\n"
+              "aa  4   ccc\n"
+              "aaa  56 c\n"));
+  }
+
+  SUBCASE("right align")
+  {
+    table.add_row({"a", "bbb", "cc"});
+    table.add_row(
+      {C("aa").right_align(), C("b").right_align(), C("ccc").right_align()});
+    table.add_row({"aaa", "bb", "c"});
+    CHECK(table.render()
+          == ("a   bbb cc\n"
+              " aa   b ccc\n"
+              "aaa bb  c\n"));
+  }
+
+  SUBCASE("heading")
+  {
+    table.add_row({"a", "b", "c"});
+    table.add_heading("DDDDDD");
+    table.add_row({"aaa", "bbb", "ccc"});
+    CHECK(table.render()
+          == ("a   b   c\n"
+              "DDDDDD\n"
+              "aaa bbb ccc\n"));
+  }
+
+  SUBCASE("colspan")
+  {
+    table.add_row({C("22").colspan(2), C("2r").colspan(2).right_align()});
+    table.add_row({C("1").colspan(1), C("22222").colspan(2), "1"});
+    table.add_row({"1", "1", "1", "1", "1"});
+    table.add_row({"1", C("3333333333").colspan(3), "1"});
+    CHECK(table.render()
+          == ("22        2r\n"      // 4 columns
+              "1 22222 1\n"         // 4 columns
+              "1 1 1   1    1\n"    // 5 columns
+              "1 3333333333 1\n")); // 5 columns
+  }
+}
diff --git a/unittest/test_util_Tokenizer.cpp b/unittest/test_util_Tokenizer.cpp
new file mode 100644 (file)
index 0000000..76c9e02
--- /dev/null
@@ -0,0 +1,138 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "../src/Util.hpp"
+
+#include "third_party/doctest.h"
+
+TEST_CASE("util::Tokenizer")
+{
+  using Mode = util::Tokenizer::Mode;
+
+  SUBCASE("include empty tokens")
+  {
+    {
+      const auto s = Util::split_into_views("", "/", Mode::include_empty);
+      REQUIRE(s.size() == 1);
+      CHECK(s[0] == "");
+    }
+    {
+      const auto s = Util::split_into_views("/", "/", Mode::include_empty);
+      REQUIRE(s.size() == 2);
+      CHECK(s[0] == "");
+      CHECK(s[1] == "");
+    }
+    {
+      const auto s = Util::split_into_views("a/", "/", Mode::include_empty);
+      REQUIRE(s.size() == 2);
+      CHECK(s[0] == "a");
+      CHECK(s[1] == "");
+    }
+    {
+      const auto s = Util::split_into_views("/b", "/", Mode::include_empty);
+      REQUIRE(s.size() == 2);
+      CHECK(s[0] == "");
+      CHECK(s[1] == "b");
+    }
+    {
+      const auto s = Util::split_into_views("a/b", "/", Mode::include_empty);
+      REQUIRE(s.size() == 2);
+      CHECK(s[0] == "a");
+      CHECK(s[1] == "b");
+    }
+    {
+      const auto s = Util::split_into_views("/a:", "/:", Mode::include_empty);
+      REQUIRE(s.size() == 3);
+      CHECK(s[0] == "");
+      CHECK(s[1] == "a");
+      CHECK(s[2] == "");
+    }
+  }
+
+  SUBCASE("skip empty")
+  {
+    CHECK(Util::split_into_views("", "/", Mode::skip_empty).empty());
+    CHECK(Util::split_into_views("///", "/", Mode::skip_empty).empty());
+    {
+      const auto s = Util::split_into_views("a/b", "/", Mode::skip_empty);
+      REQUIRE(s.size() == 2);
+      CHECK(s[0] == "a");
+      CHECK(s[1] == "b");
+    }
+    {
+      const auto s = Util::split_into_views("a/b", "x", Mode::skip_empty);
+      REQUIRE(s.size() == 1);
+      CHECK(s[0] == "a/b");
+    }
+    {
+      const auto s = Util::split_into_views("a/b:c", "/:", Mode::skip_empty);
+      REQUIRE(s.size() == 3);
+      CHECK(s[0] == "a");
+      CHECK(s[1] == "b");
+      CHECK(s[2] == "c");
+    }
+    {
+      const auto s =
+        Util::split_into_views(":a//b..:.c/:/.", "/:.", Mode::skip_empty);
+      REQUIRE(s.size() == 3);
+      CHECK(s[0] == "a");
+      CHECK(s[1] == "b");
+      CHECK(s[2] == "c");
+    }
+    {
+      const auto s = Util::split_into_views(
+        ".0.1.2.3.4.5.6.7.8.9.", "/:.+_abcdef", Mode::skip_empty);
+      REQUIRE(s.size() == 10);
+      CHECK(s[0] == "0");
+      CHECK(s[9] == "9");
+    }
+  }
+
+  SUBCASE("skip last empty token")
+  {
+    CHECK(Util::split_into_views("", "/", Mode::skip_last_empty).empty());
+    {
+      const auto s = Util::split_into_views("/", "/", Mode::skip_last_empty);
+      REQUIRE(s.size() == 1);
+      CHECK(s[0] == "");
+    }
+    {
+      const auto s = Util::split_into_views("a/", "/", Mode::skip_last_empty);
+      REQUIRE(s.size() == 1);
+      CHECK(s[0] == "a");
+    }
+    {
+      const auto s = Util::split_into_views("/b", "/", Mode::skip_last_empty);
+      REQUIRE(s.size() == 2);
+      CHECK(s[0] == "");
+      CHECK(s[1] == "b");
+    }
+    {
+      const auto s = Util::split_into_views("a/b", "/", Mode::skip_last_empty);
+      REQUIRE(s.size() == 2);
+      CHECK(s[0] == "a");
+      CHECK(s[1] == "b");
+    }
+    {
+      const auto s = Util::split_into_views("/a:", "/:", Mode::skip_last_empty);
+      REQUIRE(s.size() == 2);
+      CHECK(s[0] == "");
+      CHECK(s[1] == "a");
+    }
+  }
+}
diff --git a/unittest/test_util_expected.cpp b/unittest/test_util_expected.cpp
new file mode 100644 (file)
index 0000000..74f8527
--- /dev/null
@@ -0,0 +1,55 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include <util/expected.hpp>
+
+#include <third_party/doctest.h>
+#include <third_party/nonstd/expected.hpp>
+
+#include <iostream> // macOS bug: https://github.com/onqtam/doctest/issues/126
+#include <memory>
+#include <stdexcept>
+#include <string>
+
+class TestException : public std::runtime_error
+{
+  using std::runtime_error::runtime_error;
+};
+
+TEST_CASE("util::value_or_throw")
+{
+  using util::value_or_throw;
+
+  SUBCASE("const ref")
+  {
+    const nonstd::expected<int, const char*> with_value = 42;
+    const nonstd::expected<int, const char*> without_value =
+      nonstd::make_unexpected("no value");
+
+    CHECK(value_or_throw<TestException>(with_value) == 42);
+    CHECK_THROWS_WITH(value_or_throw<TestException>(without_value), "no value");
+  }
+
+  SUBCASE("move")
+  {
+    const std::string value = "value";
+    nonstd::expected<std::unique_ptr<std::string>, const char*> with_value =
+      std::make_unique<std::string>(value);
+    CHECK(*value_or_throw<TestException>(std::move(with_value)) == value);
+  }
+}
diff --git a/unittest/test_util_path.cpp b/unittest/test_util_path.cpp
new file mode 100644 (file)
index 0000000..ea3f335
--- /dev/null
@@ -0,0 +1,96 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include <Util.hpp>
+#include <fmtmacros.hpp>
+#include <util/path.hpp>
+
+#include <third_party/doctest.h>
+
+TEST_CASE("util::is_absolute_path")
+{
+#ifdef _WIN32
+  CHECK(util::is_absolute_path("C:/"));
+  CHECK(util::is_absolute_path("C:\\foo/fie"));
+  CHECK(util::is_absolute_path("/C:\\foo/fie")); // MSYS/Cygwin path
+  CHECK(!util::is_absolute_path(""));
+  CHECK(!util::is_absolute_path("foo\\fie/fum"));
+  CHECK(!util::is_absolute_path("C:foo/fie"));
+#endif
+  CHECK(util::is_absolute_path("/"));
+  CHECK(util::is_absolute_path("/foo/fie"));
+  CHECK(!util::is_absolute_path(""));
+  CHECK(!util::is_absolute_path("foo/fie"));
+}
+
+TEST_CASE("util::is_absolute_path")
+{
+  CHECK(!util::is_full_path(""));
+  CHECK(!util::is_full_path("foo"));
+  CHECK(util::is_full_path("/foo"));
+  CHECK(util::is_full_path("foo/"));
+  CHECK(util::is_full_path("foo/bar"));
+#ifdef _WIN32
+  CHECK(util::is_full_path("foo\\bar"));
+#else
+  CHECK(!util::is_full_path("foo\\bar"));
+#endif
+}
+
+TEST_CASE("util::split_path_list")
+{
+  CHECK(util::split_path_list("").empty());
+  {
+    const auto v = util::split_path_list("a");
+    REQUIRE(v.size() == 1);
+    CHECK(v[0] == "a");
+  }
+  {
+    const auto v = util::split_path_list("a/b");
+    REQUIRE(v.size() == 1);
+    CHECK(v[0] == "a/b");
+  }
+  {
+#ifdef _WIN32
+    const auto v = util::split_path_list("a/b;c");
+#else
+    const auto v = util::split_path_list("a/b:c");
+#endif
+    REQUIRE(v.size() == 2);
+    CHECK(v[0] == "a/b");
+    CHECK(v[1] == "c");
+  }
+}
+
+TEST_CASE("util::to_absolute_path")
+{
+  CHECK(util::to_absolute_path("/foo/bar") == "/foo/bar");
+
+#ifdef _WIN32
+  CHECK(util::to_absolute_path("C:\\foo\\bar") == "C:\\foo\\bar");
+#endif
+
+  const auto cwd = Util::get_actual_cwd();
+
+  CHECK(util::to_absolute_path("") == cwd);
+  CHECK(util::to_absolute_path(".") == cwd);
+  CHECK(util::to_absolute_path("..") == Util::dir_name(cwd));
+  CHECK(util::to_absolute_path("foo") == FMT("{}/foo", cwd));
+  CHECK(util::to_absolute_path("../foo/bar")
+        == FMT("{}/foo/bar", Util::dir_name(cwd)));
+}
diff --git a/unittest/test_util_string.cpp b/unittest/test_util_string.cpp
new file mode 100644 (file)
index 0000000..2754304
--- /dev/null
@@ -0,0 +1,277 @@
+// Copyright (C) 2021 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include <util/string.hpp>
+
+#include <third_party/doctest.h>
+
+#include <vector>
+
+static bool
+operator==(
+  std::pair<nonstd::string_view, nonstd::optional<nonstd::string_view>> left,
+  std::pair<nonstd::string_view, nonstd::optional<nonstd::string_view>> right)
+{
+  return left.first == right.first && left.second == right.second;
+}
+
+TEST_SUITE_BEGIN("util");
+
+TEST_CASE("util::ends_with")
+{
+  CHECK(util::ends_with("", ""));
+  CHECK(util::ends_with("x", ""));
+  CHECK(util::ends_with("x", "x"));
+  CHECK(util::ends_with("xy", ""));
+  CHECK(util::ends_with("xy", "y"));
+  CHECK(util::ends_with("xy", "xy"));
+  CHECK(util::ends_with("xyz", ""));
+  CHECK(util::ends_with("xyz", "z"));
+  CHECK(util::ends_with("xyz", "yz"));
+  CHECK(util::ends_with("xyz", "xyz"));
+
+  CHECK_FALSE(util::ends_with("", "x"));
+  CHECK_FALSE(util::ends_with("x", "y"));
+  CHECK_FALSE(util::ends_with("x", "xy"));
+}
+
+TEST_CASE("util::join")
+{
+  {
+    std::vector<std::string> v;
+    CHECK(util::join(v, "|") == "");
+  }
+  {
+    std::vector<std::string> v{"a"};
+    CHECK(util::join(v, "|") == "a");
+  }
+  {
+    std::vector<std::string> v{"a", " b ", "c|"};
+    CHECK(util::join(v, "|") == "a| b |c|");
+    CHECK(util::join(v.begin(), v.end(), "|") == "a| b |c|");
+    CHECK(util::join(v.begin() + 1, v.end(), "|") == " b |c|");
+  }
+  {
+    std::vector<nonstd::string_view> v{"1", "2"};
+    CHECK(util::join(v, " ") == "1 2");
+  }
+}
+
+TEST_CASE("util::parse_double")
+{
+  CHECK(*util::parse_double("0") == doctest::Approx(0.0));
+  CHECK(*util::parse_double(".0") == doctest::Approx(0.0));
+  CHECK(*util::parse_double("0.") == doctest::Approx(0.0));
+  CHECK(*util::parse_double("0.0") == doctest::Approx(0.0));
+  CHECK(*util::parse_double("2.1") == doctest::Approx(2.1));
+  CHECK(*util::parse_double("-42.789") == doctest::Approx(-42.789));
+
+  CHECK(util::parse_double("").error() == "invalid floating point: \"\"");
+  CHECK(util::parse_double("x").error() == "invalid floating point: \"x\"");
+}
+
+TEST_CASE("util::parse_signed")
+{
+  CHECK(*util::parse_signed("0") == 0);
+  CHECK(*util::parse_signed("2") == 2);
+  CHECK(*util::parse_signed("-17") == -17);
+  CHECK(*util::parse_signed("42") == 42);
+  CHECK(*util::parse_signed("0666") == 666);
+  CHECK(*util::parse_signed(" 777 ") == 777);
+
+  CHECK(util::parse_signed("").error() == "invalid integer: \"\"");
+  CHECK(util::parse_signed("x").error() == "invalid integer: \"x\"");
+  CHECK(util::parse_signed("0x").error() == "invalid integer: \"0x\"");
+  CHECK(util::parse_signed("0x4").error() == "invalid integer: \"0x4\"");
+
+  // Custom description not used for invalid value.
+  CHECK(util::parse_signed("apple", nonstd::nullopt, nonstd::nullopt, "banana")
+          .error()
+        == "invalid integer: \"apple\"");
+
+  // Boundary values.
+  CHECK(util::parse_signed("-9223372036854775809").error()
+        == "invalid integer: \"-9223372036854775809\"");
+  CHECK(*util::parse_signed("-9223372036854775808") == INT64_MIN);
+  CHECK(*util::parse_signed("9223372036854775807") == INT64_MAX);
+  CHECK(util::parse_signed("9223372036854775808").error()
+        == "invalid integer: \"9223372036854775808\"");
+
+  // Min and max values.
+  CHECK(util::parse_signed("-2", -1, 1).error()
+        == "integer must be between -1 and 1");
+  CHECK(*util::parse_signed("-1", -1, 1) == -1);
+  CHECK(*util::parse_signed("0", -1, 1) == 0);
+  CHECK(*util::parse_signed("1", -1, 1) == 1);
+  CHECK(util::parse_signed("2", -1, 1).error()
+        == "integer must be between -1 and 1");
+
+  // Custom description used for boundary violation.
+  CHECK(util::parse_signed("0", 1, 2, "banana").error()
+        == "banana must be between 1 and 2");
+}
+
+TEST_CASE("util::parse_umask")
+{
+  CHECK(util::parse_umask("1") == 01u);
+  CHECK(util::parse_umask("002") == 2u);
+  CHECK(util::parse_umask("777") == 0777u);
+  CHECK(util::parse_umask("0777") == 0777u);
+
+  CHECK(util::parse_umask("").error()
+        == "invalid unsigned octal integer: \"\"");
+  CHECK(util::parse_umask(" ").error()
+        == "invalid unsigned octal integer: \"\"");
+  CHECK(util::parse_umask("088").error()
+        == "invalid unsigned octal integer: \"088\"");
+}
+
+TEST_CASE("util::parse_unsigned")
+{
+  CHECK(*util::parse_unsigned("0") == 0);
+  CHECK(*util::parse_unsigned("2") == 2);
+  CHECK(*util::parse_unsigned("42") == 42);
+  CHECK(*util::parse_unsigned("0666") == 666);
+  CHECK(*util::parse_unsigned(" 777 ") == 777);
+
+  CHECK(util::parse_unsigned("").error() == "invalid unsigned integer: \"\"");
+  CHECK(util::parse_unsigned("x").error() == "invalid unsigned integer: \"x\"");
+  CHECK(util::parse_unsigned("0x").error()
+        == "invalid unsigned integer: \"0x\"");
+  CHECK(util::parse_unsigned("0x4").error()
+        == "invalid unsigned integer: \"0x4\"");
+
+  // Custom description not used for invalid value.
+  CHECK(
+    util::parse_unsigned("apple", nonstd::nullopt, nonstd::nullopt, "banana")
+      .error()
+    == "invalid unsigned integer: \"apple\"");
+
+  // Boundary values.
+  CHECK(util::parse_unsigned("-1").error()
+        == "invalid unsigned integer: \"-1\"");
+  CHECK(*util::parse_unsigned("0") == 0);
+  CHECK(*util::parse_unsigned("18446744073709551615") == UINT64_MAX);
+  CHECK(util::parse_unsigned("18446744073709551616").error()
+        == "invalid unsigned integer: \"18446744073709551616\"");
+
+  // Base
+  CHECK(*util::parse_unsigned("0666", nonstd::nullopt, nonstd::nullopt, "", 8)
+        == 0666);
+  CHECK(*util::parse_unsigned("0666", nonstd::nullopt, nonstd::nullopt, "", 10)
+        == 666);
+  CHECK(*util::parse_unsigned("0666", nonstd::nullopt, nonstd::nullopt, "", 16)
+        == 0x666);
+}
+
+TEST_CASE("util::percent_decode")
+{
+  CHECK(util::percent_decode("") == "");
+  CHECK(util::percent_decode("a") == "a");
+  CHECK(util::percent_decode("%61") == "a");
+  CHECK(util::percent_decode("%ab") == "\xab");
+  CHECK(util::percent_decode("%aB") == "\xab");
+  CHECK(util::percent_decode("%Ab") == "\xab");
+  CHECK(util::percent_decode("%AB") == "\xab");
+  CHECK(util::percent_decode("a%25b%7cc") == "a%b|c");
+
+  CHECK(util::percent_decode("%").error()
+        == "invalid percent-encoded string at position 0: %");
+  CHECK(util::percent_decode("%6").error()
+        == "invalid percent-encoded string at position 0: %6");
+  CHECK(util::percent_decode("%%").error()
+        == "invalid percent-encoded string at position 0: %%");
+  CHECK(util::percent_decode("a%0g").error()
+        == "invalid percent-encoded string at position 1: a%0g");
+}
+
+TEST_CASE("util::replace_first")
+{
+  CHECK(util::replace_first("", "", "") == "");
+  CHECK(util::replace_first("x", "", "") == "x");
+  CHECK(util::replace_first("", "x", "") == "");
+  CHECK(util::replace_first("", "", "x") == "");
+  CHECK(util::replace_first("x", "y", "z") == "x");
+  CHECK(util::replace_first("x", "x", "y") == "y");
+  CHECK(util::replace_first("xabcyabcz", "abc", "defdef") == "xdefdefyabcz");
+}
+
+TEST_CASE("util::split_once")
+{
+  using nonstd::nullopt;
+  using std::make_pair;
+  using util::split_once;
+
+  CHECK(split_once("", '=') == make_pair("", nullopt));
+  CHECK(split_once("a", '=') == make_pair("a", nullopt));
+  CHECK(split_once("=a", '=') == make_pair("", "a"));
+  CHECK(split_once("a=", '=') == make_pair("a", ""));
+  CHECK(split_once("a==", '=') == make_pair("a", "="));
+  CHECK(split_once("a=b", '=') == make_pair("a", "b"));
+  CHECK(split_once("a=b=", '=') == make_pair("a", "b="));
+  CHECK(split_once("a=b=c", '=') == make_pair("a", "b=c"));
+  CHECK(split_once("x y", ' ') == make_pair("x", "y"));
+}
+
+TEST_CASE("util::starts_with")
+{
+  // starts_with(const char*, string_view)
+  CHECK(util::starts_with("", ""));
+  CHECK(util::starts_with("x", ""));
+  CHECK(util::starts_with("x", "x"));
+  CHECK(util::starts_with("xy", ""));
+  CHECK(util::starts_with("xy", "x"));
+  CHECK(util::starts_with("xy", "xy"));
+  CHECK(util::starts_with("xyz", ""));
+  CHECK(util::starts_with("xyz", "x"));
+  CHECK(util::starts_with("xyz", "xy"));
+  CHECK(util::starts_with("xyz", "xyz"));
+
+  CHECK_FALSE(util::starts_with("", "x"));
+  CHECK_FALSE(util::starts_with("x", "y"));
+  CHECK_FALSE(util::starts_with("x", "xy"));
+
+  // starts_with(string_view, string_view)
+  CHECK(util::starts_with(std::string(""), ""));
+  CHECK(util::starts_with(std::string("x"), ""));
+  CHECK(util::starts_with(std::string("x"), "x"));
+  CHECK(util::starts_with(std::string("xy"), ""));
+  CHECK(util::starts_with(std::string("xy"), "x"));
+  CHECK(util::starts_with(std::string("xy"), "xy"));
+  CHECK(util::starts_with(std::string("xyz"), ""));
+  CHECK(util::starts_with(std::string("xyz"), "x"));
+  CHECK(util::starts_with(std::string("xyz"), "xy"));
+  CHECK(util::starts_with(std::string("xyz"), "xyz"));
+
+  CHECK_FALSE(util::starts_with(std::string(""), "x"));
+  CHECK_FALSE(util::starts_with(std::string("x"), "y"));
+  CHECK_FALSE(util::starts_with(std::string("x"), "xy"));
+}
+
+TEST_CASE("util::strip_whitespace")
+{
+  CHECK(util::strip_whitespace("") == "");
+  CHECK(util::strip_whitespace("x") == "x");
+  CHECK(util::strip_whitespace(" x") == "x");
+  CHECK(util::strip_whitespace("x ") == "x");
+  CHECK(util::strip_whitespace(" x ") == "x");
+  CHECK(util::strip_whitespace(" \n\tx \n\t") == "x");
+  CHECK(util::strip_whitespace("  x  y  ") == "x  y");
+}
+
+TEST_SUITE_END();