# HashPolicy The limits [[Painting/API/Hash/PackHasher]] enforces. Built through `HashPolicy.builder()`. Defaults are the strict ones — no private-network URLs, no file outside the roots handed in, 256 MiB cap. Painting builds two: a config policy for URLs the admin wrote themselves (private hosts allowed, since a LAN CDN or a self-hosted pack is an ordinary setup), and a stricter API policy for sources handed in by another plugin, driven by the `hash-security` block in the config. --- ## Source ```java package gg.lode.paintingapi.api.hash; import org.jetbrains.annotations.NotNull; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Limits applied to every hash {@link PackHasher} computes. * * <p>The defaults are the strict ones: no private-network URLs, no file outside * the roots handed in, and a hard byte cap. Callers acting on their own config * (the plugin hashing its own {@code resourcepacks:} block) relax them * deliberately; callers acting on input from another plugin should not. */ public final class HashPolicy { /** 256 MiB — comfortably above any client-loadable pack. */ public static final long DEFAULT_MAX_BYTES = 256L * 1024L * 1024L; private final List<Path> allowedRoots; private final long maxBytes; private final boolean allowPrivateHosts; private final int maxRedirects; private final int connectTimeoutMs; private final int readTimeoutMs; private HashPolicy(Builder b) { this.allowedRoots = Collections.unmodifiableList(new ArrayList<>(b.allowedRoots)); this.maxBytes = b.maxBytes; this.allowPrivateHosts = b.allowPrivateHosts; this.maxRedirects = b.maxRedirects; this.connectTimeoutMs = b.connectTimeoutMs; this.readTimeoutMs = b.readTimeoutMs; } public static @NotNull Builder builder() { return new Builder(); } /** * Directories a file source may resolve inside, after symlinks are resolved. * A relative path is tried against each in order. Empty means no file source * is accepted at all. */ public @NotNull List<Path> allowedRoots() { return allowedRoots; } public long maxBytes() { return maxBytes; } /** * Whether a URL may resolve to a loopback, link-local, site-local, or * otherwise non-public address. False keeps a URL handed in by another * plugin from reaching the host's own network. */ public boolean allowPrivateHosts() { return allowPrivateHosts; } public int maxRedirects() { return maxRedirects; } public int connectTimeoutMs() { return connectTimeoutMs; } public int readTimeoutMs() { return readTimeoutMs; } public static final class Builder { private final List<Path> allowedRoots = new ArrayList<>(); private long maxBytes = DEFAULT_MAX_BYTES; private boolean allowPrivateHosts = false; private int maxRedirects = 5; private int connectTimeoutMs = 15_000; private int readTimeoutMs = 30_000; public @NotNull Builder allowRoot(@NotNull Path root) { allowedRoots.add(root); return this; } public @NotNull Builder maxBytes(long maxBytes) { this.maxBytes = maxBytes > 0 ? maxBytes : DEFAULT_MAX_BYTES; return this; } public @NotNull Builder allowPrivateHosts(boolean allow) { this.allowPrivateHosts = allow; return this; } public @NotNull Builder maxRedirects(int maxRedirects) { this.maxRedirects = Math.max(0, maxRedirects); return this; } public @NotNull Builder timeouts(int connectTimeoutMs, int readTimeoutMs) { this.connectTimeoutMs = connectTimeoutMs; this.readTimeoutMs = readTimeoutMs; return this; } public @NotNull HashPolicy build() { return new HashPolicy(this); } } } ``` --- ## Related Pages - [[Painting/API/Hash/PackHasher]] — applies the policy - [[Painting/Server Owners/Configuration]] — the `hash-security` block that fills it in