Use it from your language
Every fixture is a plain file over HTTPS, so fetching one needs no library and no client:
a GET, a SHA-256, and a comparison. Below is the same task in five languages — download
pdf/minimal.pdf and check it against the hash this service publishes — using each
language's standard library and nothing else. Every one of these runs in CI on each
change, against this site, so what you are reading is what was last executed rather than
what once worked.
Two files carry the hashes, and both are canonical.
manifest.json is the full record — path, bytes, MIME type, tags and
measured properties as well as sha256 — and it is what to read when you want to choose
files or check anything besides the hash. sha256sums.txt is the same
hashes in the format sha256sum -c reads, one line per file, which a two-line shell
script or a language without a bundled JSON parser can consume directly. The snippets use
whichever suits the language: Python, JavaScript and Go parse the manifest; shell and Java
read the sums file.
Shell
#!/usr/bin/env sh
# Fetch one fixture and check it against the published hash. POSIX sh and curl.
# macOS ships `shasum` where Linux ships `sha256sum`; both read this format.
set -eu
check=$(command -v sha256sum || echo "shasum -a 256")
curl -fsS --create-dirs -o pdf/minimal.pdf https://loremfile.dev/pdf/minimal.pdf
curl -fsS https://loremfile.dev/sha256sums.txt | grep ' pdf/minimal\.pdf$' | $check -c -
Python
#!/usr/bin/env python3
"""Fetch one fixture and check it against the hash in the manifest. Standard library."""
import hashlib
import json
import sys
import urllib.request
BASE, WANTED = "https://loremfile.dev/", "pdf/minimal.pdf"
with urllib.request.urlopen(BASE + "manifest.json") as response:
manifest = json.load(response)
entry = next(f for f in manifest["fixtures"] if f["path"] == WANTED)
with urllib.request.urlopen(BASE + WANTED) as response:
body = response.read()
digest = hashlib.sha256(body).hexdigest()
if digest != entry["sha256"]:
sys.exit(f"{WANTED}: got {digest}, the manifest says {entry['sha256']}")
print(f"{WANTED}: {len(body)} bytes, sha256 matches")
JavaScript
// Fetch one fixture and check it against the hash in the manifest. Node 18+, no packages.
import { createHash } from "node:crypto";
const BASE = "https://loremfile.dev/";
const WANTED = "pdf/minimal.pdf";
const manifest = await (await fetch(BASE + "manifest.json")).json();
const entry = manifest.fixtures.find((f) => f.path === WANTED);
const body = Buffer.from(await (await fetch(BASE + WANTED)).arrayBuffer());
const digest = createHash("sha256").update(body).digest("hex");
if (digest !== entry.sha256) {
throw new Error(`${WANTED}: got ${digest}, the manifest says ${entry.sha256}`);
}
console.log(`${WANTED}: ${body.length} bytes, sha256 matches`);
Go
// Fetch one fixture and check it against the hash in the manifest. Standard library.
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
const base, wanted = "https://loremfile.dev/", "pdf/minimal.pdf"
type manifest struct {
Fixtures []struct {
Path string `json:"path"`
SHA256 string `json:"sha256"`
} `json:"fixtures"`
}
func get(url string) ([]byte, error) {
response, err := http.Get(url)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s answered %s", url, response.Status)
}
return io.ReadAll(response.Body)
}
func main() {
raw, err := get(base + "manifest.json")
if err != nil {
log.Fatal(err)
}
var published manifest
if err := json.Unmarshal(raw, &published); err != nil {
log.Fatal(err)
}
want := ""
for _, fixture := range published.Fixtures {
if fixture.Path == wanted {
want = fixture.SHA256
}
}
body, err := get(base + wanted)
if err != nil {
log.Fatal(err)
}
sum := sha256.Sum256(body)
if got := hex.EncodeToString(sum[:]); got != want {
log.Fatalf("%s: got %s, the manifest says %s", wanted, got, want)
}
fmt.Printf("%s: %d bytes, sha256 matches\n", wanted, len(body))
}
Java
// Fetch one fixture and check it against the published hash. Run: java Verify.java
// Hashes come from sha256sums.txt rather than manifest.json: one line per file, so no
// JSON library is needed. Both are published and carry the same hashes.
import java.net.URI;
import java.net.http.*;
import java.security.MessageDigest;
import java.util.HexFormat;
public class Verify {
static final String BASE = "https://loremfile.dev/", WANTED = "pdf/minimal.pdf";
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String sums = get(client, "sha256sums.txt", HttpResponse.BodyHandlers.ofString());
String want = sums.lines()
.filter(line -> line.endsWith(" " + WANTED))
.findFirst().orElseThrow()
.split(" ")[0];
byte[] body = get(client, WANTED, HttpResponse.BodyHandlers.ofByteArray());
String got = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(body));
if (!got.equals(want)) throw new IllegalStateException(WANTED + ": " + got + " != " + want);
System.out.println(WANTED + ": " + body.length + " bytes, sha256 matches");
}
static <T> T get(HttpClient client, String path, HttpResponse.BodyHandler<T> as)
throws Exception {
var request = HttpRequest.newBuilder(URI.create(BASE + path)).build();
HttpResponse<T> response = client.send(request, as);
if (response.statusCode() != 200) throw new IllegalStateException(path + " answered " + response.statusCode());
return response.body();
}
}
Any other language
Nothing above is special to these five. The contract is small enough to implement
anywhere: a plain GET over HTTPS returns the bytes; CORS is open, so a browser may fetch
any fixture from any origin; Range requests are supported, so you can take the first
kilobyte of a 100 MB file; and every published hash is in manifest.json and
sha256sums.txt. Paths never change meaning — the bytes at a URL are fixed forever, and a
correction is published at a new path — so a hash you record today keeps matching.
Please stay under 30 requests a second, and prefer one request per file over retrying in a
loop. If you would rather not write any of this, the GitHub Action
and the loremfile command-line client do it for you, with the same verification.