//! Phase 0 library checks. //! //! Koh 3 depends on five things outside its own source: SQLite (verified in //! src/storage), Blake3, zlib, SHA-256/HMAC and an HTTP client. This file pins //! the behaviour Koh relies on for the four stdlib ones, against vectors //! produced by other implementations of the same algorithms. If a V upgrade //! breaks an assumption, it breaks here rather than in a save. module tests import compress.zlib import crypto.blake3 import crypto.hmac import crypto.sha256 import net import net.http import os import time // Koh 3 is built and tested on V master at b9a2d87, and that revision is the pin: // V master moves, and a newer V may break Koh at any time. Building with any // other compiler fails here rather than somewhere stranger. fn test_built_with_the_pinned_v() { assert @VCURRENTHASH == 'b9a2d87' } fn test_koh_refuses_a_build_without_garbage_collection() { output := os.join_path(os.temp_dir(), 'koh-no-gc-${os.getpid()}') result := os.execute('v -gc none -o "${output}" "${@VMODROOT}/src/koh.v"') assert result.exit_code != 0 assert result.output.contains('Koh requires garbage collection') } fn test_linux_release_script_builds_without_zig() { dir := os.join_path(os.temp_dir(), 'koh-release-script-${time.now().unix_nano()}') tools := os.join_path(dir, 'tools') os.mkdir_all(tools)! for name in ['dirname', 'mkdir', 'cc'] { os.symlink(os.find_abs_path_of_executable(name)!, os.join_path(tools, name))! } os.cp('${@VMODROOT}/build.sh', os.join_path(dir, 'build.sh'))! v := os.join_path(tools, 'v') os.write_file(v, '#!/bin/sh if [ "$1" = version ]; then echo "V 0.5.2 b9a2d87"; exit 0; fi printf "%s\\n" "$@" > args while [ "$#" -gt 0 ]; do if [ "$1" = -o ]; then : > "$2"; exit 0; fi shift done exit 1 ')! os.chmod(v, 0o755)! for cpu in ['aarch64', 'x86_64'] { uname := os.join_path(tools, 'uname') os.write_file(uname, '#!/bin/sh\ncase "$1" in -s) echo Linux ;; -m) echo ${cpu} ;; esac\n')! os.chmod(uname, 0o755)! result := os.execute('PATH="${tools}" KOH_TARGET=linux-${cpu} /bin/sh "${dir}/build.sh"') assert result.exit_code == 0, result.output assert os.is_file(os.join_path(dir, 'build/releases/koh-linux-${cpu}')) args := os.read_file(os.join_path(dir, 'args'))! assert args.contains('-gc\nboehm\n') assert args.contains('-d\nuse_bundled_libgc\n') assert args.contains('-ldflags\n-static -s\n') } } // Vectors from the official blake3 bindings, cross-checked against the // naming of real Koh 2 objects: `.koh/objects//` is // blake3(uncompressed bytes) of a zlib stream. fn test_blake3_vectors() { assert blake3.sum256(''.bytes()).hex() == 'af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262' assert blake3.sum256('abc'.bytes()).hex() == '6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85' assert blake3.sum256('The quick brown fox jumps over the lazy dog'.bytes()).hex() == '2f1514181aadccd913abd94cfa592701a5686ab23f8df1dff1b74710febc6d4a' } fn test_blake3_streaming_matches_one_shot() { mut d := blake3.Digest.new_hash()! d.write('The quick brown '.bytes())! d.write('fox jumps over the lazy dog'.bytes())! assert d.checksum(32) == blake3.sum256('The quick brown fox jumps over the lazy dog'.bytes()) } fn test_zlib_round_trip() { // Object bytes must survive a trip through the store byte for byte, // including an empty payload and one with every byte value. mut all_bytes := []u8{len: 256} for i in 0 .. 256 { all_bytes[i] = u8(i) } mut payloads := [[]u8{}, 'hello koh'.bytes()] payloads << all_bytes for payload in payloads { packed := zlib.compress(payload)! assert zlib.decompress(packed)! == payload } } fn test_sha256_and_hmac_vectors() { assert sha256.sum256('abc'.bytes()).hex() == 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' assert sha256.hexhash('abc') == 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' // HMAC-SHA256, the primitive behind SigV4 signing. mac := hmac.new('key'.bytes(), 'The quick brown fox jumps over the lazy dog'.bytes(), sha256.sum256, 64) assert mac.hex() == 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8' } struct EtagHandler { mut: seen_if_none_match string } fn (mut h EtagHandler) handle(req http.Request) http.Response { h.seen_if_none_match = req.header.get_custom('If-None-Match', http.HeaderQueryConfig{}) or { '' } mut resp := http.Response{ body: 'payload' } if h.seen_if_none_match == '*' { resp.status_code = 412 resp.status_msg = 'Precondition Failed' } else { resp.status_code = 200 resp.status_msg = 'OK' } resp.header.add_custom('ETag', '"3b1f"') or {} return resp } // Starts the probe server on the first free port, so parallel test runs do not // fight over a fixed one. fn start_probe_server() !(int, &http.Server) { for port in [19301, 19302, 19303, 19304, 19305] { listener := net.listen_tcp(.ip, '127.0.0.1:${port}') or { continue } mut server := &http.Server{ listener: listener handler: EtagHandler{} show_startup_message: false } spawn server.listen_and_serve() server.wait_till_running()! return port, server } return error('no free port for the probe server') } // The S3 backend needs custom request headers, the response status, and the // ETag that conditional writes are keyed on. TLS itself is exercised against a // live bucket, not here. fn test_http_client_sends_headers_and_reads_status_and_etag() { port, mut server := start_probe_server()! defer { server.stop() } url := 'http://127.0.0.1:${port}/probe' mut hdr := http.new_header() hdr.add_custom('If-None-Match', '*')! resp := http.fetch( url: url method: .get header: hdr )! assert resp.status_code == 412 assert resp.header.get_custom('ETag', http.HeaderQueryConfig{}) or { '' } == '"3b1f"' plain := http.fetch(url: url, method: .get)! assert plain.status_code == 200 assert plain.body == 'payload' // Retries: the S3 backend retries network errors and 5xx. A dead port must // surface as an error rather than a hang. if _ := http.fetch( url: 'http://127.0.0.1:19399/gone' method: .get read_timeout: 2 * time.second ) { assert false, 'a request to a closed port should fail' } }