Compare commits

...

11 Commits

Author SHA1 Message Date
Hammer 15b81a210f Passes JVM user.language arg to application.. This is important
Hook battery / PR-target hook battery (push) Successful in 11s
2026-07-21 15:19:51 -04:00
claridtimo 2e892c4ef4 Merge pull request #1 from claridtimo/add-pr-target-hook
Hook battery / PR-target hook battery (push) Has been cancelled
Add fork-PR-target hook (verbatim from bang-game #63) + battery CI gate
2026-07-10 16:38:45 +12:00
Claridtimo fdc0d97d06 Add fork-PR-target hook (verbatim from bang-game #63) + battery CI gate
Propagates the PreToolUse enforce-pr-target.sh backstop (byte-identical to
claridtimo/bang-game master) that denies any `gh pr create` not explicitly
aimed at claridtimo/*, guarding against accidental PRs on the greyhavens
upstream fork parent. Ships the 103-case regression battery and a CI job
(.github/workflows/hook-battery.yml) that gates it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3cmNbMM8gGpprBy8kcNuV
2026-07-10 16:33:24 +12:00
Claridtimo b266a73187 build: depend on samskivert 1.12-SNAPSHOT (local stack)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:39:45 +12:00
Michael Bayne 30f56a833c [maven-release-plugin] prepare for next development iteration 2026-05-12 12:50:08 -07:00
Michael Bayne 759b73aba3 [maven-release-plugin] prepare release getdown-1.8.10 2026-05-12 12:50:05 -07:00
Michael Bayne f621e7d810 Handle network error during proxy config better.
Don't assume we need a proxy and show the user an inscrutable dialog. Pop up a
dialog that says that we were unable to connect and tell them to check their
internet connection. From there they can either try again or configure a proxy
if they know that's a thing they might need to do.
2026-05-12 12:47:19 -07:00
Michael Bayne a5eae1a80c [maven-release-plugin] prepare for next development iteration 2026-04-27 15:55:51 -07:00
Michael Bayne 9a9a08ad88 [maven-release-plugin] prepare release getdown-1.8.9 2026-04-27 15:55:46 -07:00
Michael Bayne cd3302fea2 Add compatibility shim. 2026-04-27 15:55:02 -07:00
Michael Bayne 40a3f1dbbf [maven-release-plugin] prepare for next development iteration 2026-02-12 15:45:40 -08:00
41 changed files with 818 additions and 1432 deletions
+397
View File
@@ -0,0 +1,397 @@
#!/usr/bin/env bash
#
# PreToolUse(Bash) hook — hard backstop against opening PRs on the upstream fork parent.
#
# This repo (and the sibling fork clones) is a FORK of greyhavens/*. `gh pr create` with no
# -R/--repo defaults its base repo to the fork PARENT, so a bare invocation opens a PR against
# greyhavens by mistake (it has, more than once). The CLAUDE.md guidance + `gh repo set-default`
# are soft — this hook is the enforced version: it DENIES any `gh pr create` not explicitly aimed
# at a claridtimo/* repo, and feeds the reason back so the model just re-runs correctly.
#
# WHY it parses argv instead of grepping raw text: earlier revisions matched the raw command
# string, which mis-read a `-R claridtimo/…` substring inside a --title/--body (false allow) and
# split on shell operators inside a quoted value (false deny). In THIS repo those aren't
# pathological — we routinely write PRs whose titles/bodies contain gh examples, shell snippets,
# and ordinary apostrophes ("don't"). So the precise path tokenizes the command the way a shell
# actually would, with Python's `shlex` (the reference POSIX shell lexer — it handles nested quote
# types, e.g. an apostrophe inside a double-quoted title, which a bash/xargs tokenizer cannot do
# portably). Only a real `-R`/`--repo` flag token — never text inside a quoted value — counts as a
# target, and clause boundaries are only real operator tokens.
#
# Dependency posture: a PreToolUse hook that ERRORS is treated as non-blocking, so a hard
# dependency would silently REMOVE the guard on a box that lacks it. The precise path uses python3
# (stdlib only: json + shlex), guarded by `command -v`. If python3 is absent OR the command can't
# be parsed, the script DEGRADES to a conservative text check that never false-denies a targeted PR
# and still catches the common bare omission. It never exits non-zero; blocking is via the JSON
# "deny", not the exit code.
INPUT=$(cat)
# Cheap prefilter before anything else: a deny can only ever fire on a clause containing the
# tokens `gh` … `pr` … `create`, and no shell QUOTING can produce those tokens without the letters
# "gh" and "create" appearing verbatim in the raw input (JSON never escapes letters). So for the
# overwhelmingly common unrelated Bash call, skip the python3 spawn (and the greps) entirely.
# A deliberately backslash-escaped `crea\te` slips past this — deliberate evasion is out of the
# threat model (the hook guards against ACCIDENTAL omission), same class as shell aliases.
case "$INPUT" in *gh*) : ;; *) exit 0 ;; esac
case "$INPUT" in *create*) : ;; *) exit 0 ;; esac
emit_deny() {
cat <<'JSON'
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"PRs must target the claridtimo fork, never upstream greyhavens. This `gh pr create` has no explicit claridtimo target (as a real -R/--repo FLAG, not text inside a --title/--body), so gh would default its base repo to the upstream fork parent. Re-run with the flag aimed at claridtimo, e.g.:\n gh pr create -R claridtimo/<repo> --base <branch> --head <feature-branch> ...\nA target on a different &&-chained command, or one that only appears inside a quoted title/body, does NOT count."}}
JSON
}
# ---- Precise path: python3 (stdlib json + shlex) --------------------------------------------
# Emits exactly one word on stdout: DENY, ALLOW, or FALLBACK. Any crash / missing python / weird
# exit prints nothing, and we fall through to the degraded check below. Never trusts a partial parse.
if command -v python3 >/dev/null 2>&1; then
# Input goes via env var (HOOK_INPUT), leaving stdin free for the heredoc'd script itself.
verdict=$(HOOK_INPUT="$INPUT" python3 - <<'PY' 2>/dev/null
import json, re, shlex, sys, os
try:
cmd = json.loads(os.environ.get("HOOK_INPUT", "")).get("tool_input", {}).get("command", "")
except Exception:
print("FALLBACK"); sys.exit(0)
if not cmd:
print("FALLBACK"); sys.exit(0)
def scan_subst(s, i, opener):
# Scan a command-substitution / subshell body starting at s[i] (the char AFTER the opener).
# opener "(" ends at its nesting-matched ")" (honoring quotes and backslashes, the way bash
# re-parses the inside of $() as a fresh context); opener backtick ends at the next unescaped
# backtick (backticks don't nest unescaped). Unterminated bodies consume to end-of-string —
# that input is malformed shell that bash would refuse to run, so any verdict is safe.
# Returns (content, index_after_closer).
q2 = None
depth = 1
buf = []
j, n = i, len(s)
while j < n:
c = s[j]
if q2 is not None:
buf.append(c)
if c == q2:
q2 = None
elif c == "\\" and q2 == '"' and j + 1 < n:
buf.append(s[j + 1]); j += 2; continue
j += 1; continue
if c == "\\" and j + 1 < n:
buf.append(c); buf.append(s[j + 1]); j += 2; continue
if c in ("'", '"'):
q2 = c; buf.append(c); j += 1; continue
if opener == "(":
if c == "(":
depth += 1
elif c == ")":
depth -= 1
if depth == 0:
return "".join(buf), j + 1
elif c == chr(96):
return "".join(buf), j + 1
buf.append(c); j += 1
return "".join(buf), n
def to_separators(s, depth=0):
# shlex.split() is a quote-aware WORD splitter, not a shell control-operator parser: it only
# treats & | ; and newlines as separators when whitespace already surrounds them. Real bash
# splits on them regardless (`echo a&&echo b` is two commands). So a quote-aware pre-pass
# rewrites every shell metacharacter the way a shell lexer would:
# - unquoted control operators (& | ; newline) become " ; " — a clause boundary;
# - unquoted redirections (< >, and an & glued to one: 2>&1, &>f) become " " — a TOKEN
# boundary but NOT a clause boundary, since a redirect doesn't end the command
# (`gh pr create>out` must still be seen as a create, and a `-R` AFTER `2>&1` is still
# part of the same clause);
# - an unquoted # at word start drops the rest of the line (a comment; a mid-word # stays
# literal, matching shell) — a `-R claridtimo/…` living only in a comment can't count;
# - command substitutions and subshells — $(…), `…`, bare (…) — are EXTRACTED: the body is
# removed from the enclosing command (which stays contiguous, so a substitution used as a
# flag value can't split a targeted create away from its -R) and appended as its own
# " ; "-separated clause, recursively pre-passed, so an inner `gh pr create` is judged on
# its own. This applies inside DOUBLE quotes too — bash executes $()/backticks there
# (only single quotes are inert), so a create hidden in a --title "… $(gh pr create …)"
# is still caught (11th review round).
# Other quoted metacharacters (a --body/title) are preserved; a backslash-escaped one is
# preserved for shlex to handle. Remaining blind spots, all deliberate-evasion class (out of
# threat model — the guard is against ACCIDENTAL omission): shell aliases, backslash-escaped
# letters (`crea\te`), ${var@P}-style expansion tricks. Heredoc BODIES are not
# quote-delimited, so a bare `gh pr create` EXAMPLE inside a heredoc'd --body-file can
# false-DENY — the safe direction; re-run with the example inside a quoted --body or a file.
if depth > 10:
raise ValueError("substitution nesting too deep") # → caller falls back (conservative)
out = []
extracted = []
q = None
i, n = 0, len(s)
while i < n:
c = s[i]
if q is not None: # inside a quote
if q == '"':
# bash still runs $() and backticks inside double quotes — extract them
if c == "$" and i + 1 < n and s[i + 1] == "(":
body, i = scan_subst(s, i + 2, "(")
extracted.append(body); continue
if c == chr(96):
body, i = scan_subst(s, i + 1, chr(96))
extracted.append(body); continue
if c == "\\" and i + 1 < n:
if s[i + 1] == "\n": # line continuation: bash removes both chars
i += 2; continue
out.append(c); out.append(s[i + 1]); i += 2; continue
out.append(c)
if c == q:
q = None
i += 1; continue
if c in ("'", '"'):
q = c; out.append(c); i += 1; continue
if c == "\\" and i + 1 < n: # unquoted backslash escapes the next char
if s[i + 1] == "\n": # line continuation: bash removes both chars,
i += 2; continue # joining the surrounding text (16th round)
out.append(c); out.append(s[i + 1]); i += 2; continue
if c == "#" and (i == 0 or s[i - 1] in " \t&|;()<>\n\r" or s[i - 1] == chr(96)):
while i < n and s[i] != "\n": # comment: shell ignores to end of line
i += 1
continue
# The " __subst__ " placeholder keeps the token count intact: a substitution used as a
# flag VALUE (--title $(gen) -R …) must still occupy the value slot, or the value flag
# would consume the following -R as its value and false-deny a targeted create.
if c == "$" and i + 1 < n and s[i + 1] == "(":
body, i = scan_subst(s, i + 2, "(")
extracted.append(body); out.append(" __subst__ "); continue
if c == "(": # bare subshell / grouping
body, i = scan_subst(s, i + 1, "(")
extracted.append(body); out.append(" __subst__ "); continue
if c == chr(96):
body, i = scan_subst(s, i + 1, chr(96))
extracted.append(body); out.append(" __subst__ "); continue
if c in "<>":
out.append(" ") # redirection: token boundary, not clause boundary
i += 1; continue
if c == "&" and ((i + 1 < n and s[i + 1] in "<>") or (i > 0 and s[i - 1] in "<>")):
out.append(" ") # & that is part of a redirect (2>&1, &>f, <&0)
i += 1; continue
if c == "|":
if i + 1 < n and s[i + 1] == "|":
out.append(" ; "); i += 2; continue # || is OR — a plain clause boundary
if i + 1 < n and s[i + 1] == "&":
out.append(" __pipe__ "); i += 2; continue # |& pipes stdout+stderr
out.append(" __pipe__ "); i += 1; continue # a real pipe: the next clause reads
# this clause's stdout as ITS stdin — judge() uses this to catch `echo … | bash`
out.append(" ; " if c in "&;)\n\r" else c) # stray ")" = malformed; split conservatively
i += 1
for body in extracted:
out.append(" ; ")
out.append(to_separators(body, depth + 1))
return "".join(out)
# After the pre-pass every unquoted separator is a lone " ; " or " __pipe__ ", so these are the
# only clause-boundary tokens shlex can produce here (operator text inside quotes stays part of
# its value token). The pipe stays distinct because `echo "gh pr create …" | bash` EXECUTES the
# echoed text (17th review round) — judge() carries an echo/printf clause's payload across a
# pipe boundary and judges it when the receiving clause is a shell reading stdin (no -c).
OPS = {";", "__pipe__"}
# gh flags that consume the NEXT token as an opaque value; that value must never be read as an
# operator or a flag. -R/--repo are handled explicitly below (their value is what we inspect).
VALUE_FLAGS = {"-t","--title","-b","--body","-F","--body-file","-B","--base","-H","--head",
"-l","--label","-a","--assignee","-r","--reviewer","-m","--milestone",
"-p","--project","-T","--template","--recover"}
# Shell-wrapper basenames whose `-c <string>` argument is itself a command (15th review round:
# `bash -c "gh pr create …"` is an ORDINARY idiom, inside the accidental-omission threat model —
# and shlex collapsing the string to one opaque token had silently ALLOWED it, a regression vs
# the old raw-grep hook). Such strings are recursively judged as commands, as is everything
# after `eval` (which concatenates its args and executes them). Not covered: `ssh host "…"` /
# `su -c` (remote/privileged contexts our agents never route gh through — and the degraded grep
# below still catches those textually) and non-shell interpreters (`python -c 'os.system(…)'`,
# deliberate-evasion class).
SHELLS = {"bash", "sh", "zsh", "dash", "ksh"}
# Wrappers that keep the following word at command position (their own options/durations are
# skipped by the dash/numeric rules at the use site).
PREFIXES = {"sudo", "doas", "env", "nohup", "setsid", "command", "exec", "time", "xargs",
"nice", "ionice", "stdbuf", "timeout", "strace", "ltrace"}
def judge(cmd, depth=0):
# Returns True if any clause anywhere in cmd (including inside bash -c / eval strings) is an
# untargeted `gh pr create`. Raises ValueError on unparseable input → caller falls back.
if depth > 5:
raise ValueError("wrapper nesting too deep")
toks = shlex.split(to_separators(cmd)) # POSIX tokenization; unquoted newlines act as ";"
deny = False
gh = pr = create = targeted = False
cmd_pos = True # scanning the clause's COMMAND position (vs its arguments)
printed = None # args of the echo/printf clause just scanned (they were PRINTED)
piped = None # that payload, if the boundary we just crossed was a pipe
i, n = 0, len(toks)
while i < n:
t = toks[i]
if t in OPS: # clause boundary: judge the clause we just finished
if gh and pr and create and not targeted:
deny = True
# an echo/printf payload only survives across a PIPE — `echo … | bash` feeds it to
# the shell's stdin, while `echo …; bash` prints and moves on (17th review round)
piped = printed if t == "__pipe__" else None
printed = None
gh = pr = create = targeted = False
cmd_pos = True
i += 1
continue
if cmd_pos:
if re.match(r"^[A-Za-z_][A-Za-z0-9_]*=", t):
i += 1; continue # leading VAR=val assignment — still at command position
base = t.rsplit("/", 1)[-1]
# command-position-preserving prefixes and their option/duration arguments: after
# `sudo`/`env`/`timeout 5`/`nice -n 10`/`xargs`/… the NEXT word is still the invoked
# command
if base in PREFIXES or re.match(r"^[0-9]+[smhd]?$", t):
i += 1; continue
if t.startswith("-"):
# a prefix option may take a VALUE (`sudo -u root`, `xargs -I {}`): consume the
# following plain word as that value so the wrapper AFTER it is still judged at
# command position (20th review round: `sudo -u root bash -c "…"` bypassed the
# wrapper check when `root` closed command position). The word is NOT consumed
# when it is itself a shell/eval — a no-value option directly before the command
# (`env -i bash -c "…"`) is likelier than a value named after a shell.
nxt = toks[i + 1] if i + 1 < n and toks[i + 1] not in OPS else None
if nxt and not nxt.startswith("-") and not re.match(r"^[0-9]+[smhd]?$", nxt) \
and nxt.rsplit("/", 1)[-1] not in SHELLS and nxt != "eval" \
and not re.match(r"^[A-Za-z_][A-Za-z0-9_]*=", nxt):
i += 2; continue
i += 1; continue
cmd_pos = False
# this token IS the clause's invoked command. Wrapper/print semantics apply ONLY
# here: bash/eval/echo as a mere ARGUMENT (`grep eval -c "gh pr create test" f`,
# `… | grep bash`) neither executes nor prints anything (19th review round — those
# shapes were false-denied when wrappers matched anywhere in the clause).
if base in ("echo", "printf"):
# a print clause never EXECUTES its arguments — skip it whole, but REMEMBER
# them: if this clause pipes into a stdin-reading shell, the printed text
# becomes commands after all (16th/17th review rounds)
args = []
i += 1
while i < n and toks[i] not in OPS:
args.append(toks[i]); i += 1
printed = args
continue
if base in SHELLS:
# scan this clause for -c (alone or in a cluster like -lc); its argument is a
# command in its own right
j = i + 1
saw_c = False
while j < n and toks[j] not in OPS:
f = toks[j]
if f.startswith("-") and not f.startswith("--") and "c" in f:
saw_c = True
if j + 1 < n and toks[j + 1] not in OPS and judge(toks[j + 1], depth + 1):
deny = True
break
j += 1
# no -c: the shell reads stdin — if an echo/printf payload was piped in, judge
# it (`cat file | bash` etc. remain unjudgeable: unknown content, degraded grep
# only; multi-hop pipes like `echo … | tee f | bash` drop the payload — accepted)
if not saw_c and piped:
if judge(" ".join(piped), depth + 1):
deny = True
piped = None
# fall through: the shell token itself still walks the generic checks below
if t == "eval": # eval concatenates its args and executes them
j = i + 1
args = []
while j < n and toks[j] not in OPS:
args.append(toks[j]); j += 1
if args and judge(" ".join(args), depth + 1):
deny = True
i = j # the args were judged in recursion, not in this walk
continue
elif t in ("-exec", "-execdir", "-ok"):
cmd_pos = True # find(1): the token after -exec is an invoked command
i += 1
continue
if t == "gh" or t.endswith("/gh"): # bare `gh` or a full/relative path like /usr/bin/gh
gh = True
elif t == "pr" and gh:
pr = True
elif t == "create" and pr:
create = True
# Each -R/--repo SETS the target verdict from its own value — last flag wins, matching gh's
# repeated-flag semantics (a later -R greyhavens/... after -R claridtimo/... must UN-target).
if t in ("-R", "--repo"): # target flag; value is the next token
if i + 1 < n and toks[i + 1] not in OPS:
targeted = toks[i + 1].lower().startswith("claridtimo/")
i += 2
else: # dangling flag at a clause boundary: no value, and the
targeted = False # boundary token must still be processed (18th round)
i += 1
continue
if t.startswith("-R=") or t.startswith("--repo="):
targeted = t.split("=", 1)[1].lower().startswith("claridtimo/")
elif t.startswith("-R") and len(t) > 2: # -Rclaridtimo/… glued short form
targeted = t[2:].lower().startswith("claridtimo/")
if t in VALUE_FLAGS: # next token is this flag's opaque value — skip it,
# unless it is a clause boundary (a dangling value flag must not swallow the OPS
# token — the clause-reset/deny-check there is what the state machine relies on)
i += 2 if (i + 1 < n and toks[i + 1] not in OPS) else 1
continue
i += 1
if gh and pr and create and not targeted:
deny = True
return deny
try:
deny = judge(cmd)
except ValueError:
print("FALLBACK"); sys.exit(0) # unbalanced quotes / absurd nesting → degraded path decides
print("DENY" if deny else "ALLOW")
PY
)
case "$verdict" in
DENY) emit_deny; exit 0 ;;
ALLOW) exit 0 ;;
*) : ;; # FALLBACK / empty (python crashed or missing) → degraded path below
esac
fi
# ---- Degraded path: no python3, or the command couldn't be parsed — conservative -------------
# Deny only the clear-cut case: a `gh pr create` with no -R/--repo flag ADJACENT to a
# "claridtimo/" value anywhere in the input. The adjacency requirement matters: the hook stdin
# is the whole PreToolUse payload (cwd, transcript_path, …), so a bare "claridtimo/" substring
# test would false-ALLOW every bare create on a box whose checkout PATH contains "claridtimo"
# (10th review round) — and the degraded path exists precisely for such less-set-up boxes.
# Requiring the flag form still NEVER false-denies a targeted PR (every targeted create carries
# `-R claridtimo/…`, `-R=…`, `-Rclaridtimo/…`, or a --repo equivalent — all matched below).
#
# KNOWN, ACCEPTED under-blocks (12th review round) — this path prioritizes never-false-denying
# over completeness, and cannot have both without a real tokenizer:
# - a flag-shaped `-R claridtimo/…` inside a quoted --title/--body satisfies the check;
# - a real target on an UNRELATED chained clause (`gh pr view -R claridtimo/x; gh pr create`)
# satisfies a bare create elsewhere in the same input.
# Clause-scoping this fallback with sed/grep was TRIED (the v2 hook that #61 merged) and
# reverted: without a quote-aware tokenizer, operators inside quoted PR bodies split clauses
# wrongly and false-denied real targeted creates — the exact bug class this rework removes.
# Both shapes are pinned in test-enforce-pr-target.sh (degraded-ALLOW vs precise-DENY) so a
# future change flipping either direction fails the battery. Every box we actually use has
# python3; this is a last-resort backstop, and `gh repo set-default` (bin/setup-gh-defaults)
# remains the primary guard.
# [Cc]laridtimo: GitHub owner names are case-insensitive (the precise path lowercases the whole
# value; here only the realistic accidental variant, a capitalized C, is matched — the flag part
# stays case-sensitive so -r/--reviewer values are not read as targets).
# SEP: a separator between tokens can be REAL whitespace or its JSON-ESCAPED form — the hook
# stdin is a JSON document, so a newline/tab inside the command arrives as the two characters
# \n / \t and a shell line-continuation backslash as \\ (16th review round: a backslash-
# continued `gh pr \<newline>create` must still match on exactly the boxes this fallback
# protects; the same class must count as flag/value adjacency or a continued targeted create
# would false-deny).
SEP='([[:space:]]|\\n|\\t|\\r|\\\\)'
# Scope the greps to the "command" FIELD when extractable: the payload also carries description/
# cwd/transcript_path, and a description that MENTIONS the intended target must not satisfy the
# check for a command that forgot the flag — nor should a description quoting `gh pr create`
# false-deny an unrelated command (18th review round). The ERE walks escaped chars inside the
# JSON string value; if extraction yields nothing (unexpected payload shape), fall back to the
# whole input, which errs toward DENY only for inputs that contain the create phrase anyway.
# (grep only — the degraded path must not depend on anything beyond bash/grep/printf/cat)
SCOPE=$(printf '%s' "$INPUT" | grep -oE '"command"[[:space:]]*:[[:space:]]*"(\\.|[^"\\])*"')
[ -n "$SCOPE" ] || SCOPE="$INPUT"
if printf '%s' "$SCOPE" | grep -qE "gh${SEP}+pr${SEP}+create" \
&& ! printf '%s' "$SCOPE" | grep -qE "(-R|--repo)(=|${SEP})*[Cc]laridtimo/"; then
emit_deny
fi
exit 0
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
#
# Regression battery for enforce-pr-target.sh — run after ANY edit to the hook:
# .claude/hooks/test-enforce-pr-target.sh
# Exercises every bypass/false-deny class found across the #61/#63 review rounds, plus the
# degraded (no-python3) path via a stripped PATH. Exits non-zero on any failure.
# (Test-only dependency on python3 for safe JSON construction; the DEGRADED section still
# tests the hook itself without python3 on PATH.)
set -u
HOOK="$(cd "$(dirname "$0")" && pwd)/enforce-pr-target.sh"
pass=0; fail=0
run_case() { # expect(DENY|ALLOW) command [pathenv]
local expect="$1" cmd="$2" pathenv="${3:-$PATH}"
local input verdict out
input=$(python3 -c 'import json,sys; print(json.dumps({"tool_input":{"command":sys.argv[1]}}))' "$cmd")
out=$(printf '%s' "$input" | env PATH="$pathenv" bash "$HOOK")
_judge "$expect" "$out" "$cmd"
}
run_case_raw() { # expect(DENY|ALLOW) raw-json-input label [pathenv]
local expect="$1" input="$2" label="$3" pathenv="${4:-$PATH}"
local out
out=$(printf '%s' "$input" | env PATH="$pathenv" bash "$HOOK")
_judge "$expect" "$out" "$label"
}
_judge() {
local expect="$1" out="$2" label="$3" verdict
if printf '%s' "$out" | grep -q '"permissionDecision":"deny"'; then verdict=DENY; else verdict=ALLOW; fi
if [ "$verdict" = "$expect" ]; then
pass=$((pass+1)); echo "ok $expect $label"
else
fail=$((fail+1)); echo "FAIL want=$expect got=$verdict $label"
fi
}
echo "=== DENY: untargeted creates, incl. every historical bypass class ==="
run_case DENY 'gh pr create --title foo --body bar'
run_case DENY 'gh pr create>out --title foo' # glued redirect (round 9)
run_case DENY 'gh pr create</dev/null' # glued stdin redirect
run_case DENY 'gh pr create 2>&1' # redirect combo, still untargeted
run_case DENY 'gh pr create --title foo # -R claridtimo/x' # target only in a comment
run_case DENY 'git push && gh pr create -t x'
run_case DENY 'git push&&gh pr create -t x' # glued operator (v6)
run_case DENY 'url=$(gh pr create -t x)' # command substitution (v9)
run_case DENY 'echo `gh pr create -t x`' # backtick substitution (v9)
run_case DENY 'gh pr create --title "use -R claridtimo/bang-game"' # target text inside a title (v3)
run_case DENY 'gh pr create -R greyhavens/bang-game -t x'
run_case DENY 'gh pr create -R claridtimo/x -R greyhavens/y' # last -R wins (v8)
run_case DENY '/usr/bin/gh pr create -t x' # gh by path (v7)
run_case DENY $'git status\ngh pr create -t x' # multi-line (v6)
run_case DENY 'gh pr create -R claridtimo/x && gh pr create -t y' # second clause untargeted
echo "=== ALLOW: targeted creates + unrelated commands ==="
run_case ALLOW 'gh pr create -R claridtimo/bang-game -t x -b y'
run_case ALLOW 'gh pr create --repo claridtimo/bang-game -t x'
run_case ALLOW 'gh pr create --repo=claridtimo/bang-game -t x'
run_case ALLOW 'gh pr create -Rclaridtimo/bang-game -t x' # glued short form
run_case ALLOW "gh pr create -R claridtimo/x --body 'a && b; gh pr create'" # ops inside quoted body (v3)
run_case ALLOW "gh pr create -R claridtimo/x --title \"don't break\"" # apostrophe (v5)
run_case ALLOW 'gh pr create -R claridtimo/x > /tmp/out' # redirect after target
run_case ALLOW 'gh pr create > /tmp/out -R claridtimo/x' # redirect before target
run_case ALLOW 'gh pr create 2>&1 -R claridtimo/x' # &-in-redirect (was a v9 false-deny)
run_case ALLOW 'gh pr create>log -R claridtimo/x' # glued redirect, still targeted
run_case ALLOW 'gh pr list'
run_case ALLOW 'gh pr view 63 -R claridtimo/bang-game'
run_case ALLOW 'gh pr merge 63 -R claridtimo/bang-game --merge'
run_case ALLOW 'git commit -m "gh pr create later"' # words in a -m value
run_case ALLOW 'echo done' # prefilter early-exit (no gh)
run_case ALLOW './gradlew deploy && echo high create' # prefilter passes, no gh token
run_case ALLOW 'gh pr create --title=has#hash -R claridtimo/x' # mid-word # is NOT a comment
run_case ALLOW 'gh repo create claridtimo/new-repo' # repo create is not pr create
echo "=== Substitution extraction (round 11): inner commands judged, outer kept contiguous ==="
run_case DENY 'gh pr create -R claridtimo/x -t "notes: $(gh pr create -t oops)"' # dq-hidden create
run_case DENY 'gh pr create -R claridtimo/x -t "notes: `gh pr create -t oops`"' # dq-hidden backtick
run_case DENY 'gh pr create -R claridtimo/x -b "$(echo $(gh pr create -t deep))"' # nested substitution
run_case ALLOW 'gh pr create --title "cost $(compute) done" -R claridtimo/x' # dq subst mid-command
run_case ALLOW 'gh pr create --title $(gen-title) -R claridtimo/x' # unquoted subst mid-command
run_case ALLOW 'gh pr create -R claridtimo/x --title "(parens) are fine"' # plain parens in dq
run_case DENY 'gh pr create --recover -Rclaridtimo/x.txt' # --recover value is opaque, not a -R
echo "=== Shell wrappers (round 15): bash -c / eval strings are commands too ==="
run_case DENY 'bash -c "gh pr create -t x"'
run_case DENY "bash -lc 'gh pr create -t x'" # combined flag cluster
run_case DENY "sh -c 'gh pr create -t x'"
run_case DENY "/bin/bash -c 'gh pr create -t x'" # shell by path
run_case DENY 'eval "gh pr create -t x"'
run_case DENY 'eval gh pr create -t x' # eval with unquoted args
run_case DENY "nohup bash -c 'gh pr create -t x' &"
run_case ALLOW "bash -c 'gh pr create -R claridtimo/x -t y'" # targeted inside the wrapper
run_case ALLOW 'eval "gh pr view 63 -R claridtimo/x" && echo create' # wrapper runs no create
run_case ALLOW "bash -c 'echo ghost created'" # words, not tokens
echo "=== Wrappers only at command position (round 19) ==="
run_case ALLOW 'grep eval -c "gh pr create test" file.txt' # eval as a grep ARG
run_case ALLOW 'echo "gh pr create -t x" | grep bash' # bash as a grep ARG
run_case ALLOW 'git log --grep eval -- "gh pr create notes.md"' # wrapper words in args
run_case DENY 'sudo bash -c "gh pr create -t x"' # prefix keeps command position
run_case DENY 'timeout 5 bash -c "gh pr create -t x"' # numeric prefix arg
run_case DENY 'xargs bash -c "gh pr create -t x"'
run_case DENY 'find . -name "*.md" -exec bash -c "gh pr create -t x" \;' # -exec re-arms
run_case DENY 'VAR=1 env bash -lc "gh pr create -t x"'
run_case DENY 'sudo -u root bash -c "gh pr create -t x"' # option VALUE before the shell (round 20)
run_case DENY 'xargs -I {} bash -c "gh pr create -t x"' # unglued option value
run_case DENY 'env -i bash -c "gh pr create -t x"' # no-value option directly before shell
run_case DENY 'sudo -u root gh pr create -t x' # generic detection through prefixes
run_case ALLOW 'sudo -u root bash -c "gh pr create -R claridtimo/x"' # targeted inside sudo-wrapped shell
run_case ALLOW 'sudo -u root gh pr create -R claridtimo/x'
echo "=== echo/printf clauses print, not execute (round 16) ==="
run_case ALLOW 'echo bash -c "gh pr create -t x"' # echoed text, never run
run_case ALLOW 'echo gh pr create' # literal words to stdout
run_case ALLOW 'printf "%s\n" gh pr create' # printf variant
run_case DENY 'echo done && gh pr create -t x' # later clause still judged
echo "=== Piped echo payloads (round 17): echo | bash executes the text ==="
run_case DENY 'echo "gh pr create -t x" | bash'
run_case DENY 'printf "gh pr create -t x" | sh'
run_case DENY 'echo gh pr create -t x | bash' # unquoted payload
run_case ALLOW 'echo "gh pr create -R claridtimo/x" | bash' # targeted payload
run_case ALLOW 'echo "gh pr create -t x" | grep create' # pipe into a non-shell
run_case ALLOW 'echo "gh pr create -t x" || bash' # OR, not a pipe: bash gets no stdin script
run_case ALLOW 'echo done | bash'
run_case ALLOW 'echo "gh pr create -t x" ; bash' # printed then interactive shell
echo "=== Line continuations (round 16): JSON-escaped whitespace in the degraded greps ==="
run_case DENY $'gh pr \\\ncreate -t x' # continued bare create (precise)
echo "=== Case-insensitive owner match (round 13): GitHub owners are case-insensitive ==="
run_case ALLOW 'gh pr create -R Claridtimo/bang-game -t x' # capitalized owner, targeted
run_case ALLOW 'gh pr create --repo=CLARIDTIMO/bang-game -t x' # any-case owner (precise path)
run_case DENY 'gh pr create -R Greyhavens/bang-game -t x' # case variance is not a pass
echo "=== FALLBACK route (round 13): python3 present but tokenization fails ==="
# An unbalanced quote makes shlex raise → the precise path prints FALLBACK → the degraded grep
# decides. Distinct from the no-python3 route (PATH-stripped below): this exercises the
# ValueError branch inside the python script itself.
run_case DENY 'gh pr create -t "unbalanced'
run_case ALLOW 'gh pr create -R claridtimo/x -t "unbalanced'
echo "=== Dangling value flags must not swallow a clause boundary (round 18) ==="
run_case DENY 'gh pr create -t && true -R claridtimo/x' # -t at boundary; later clause has the -R
run_case DENY 'gh pr create --title ; true -R claridtimo/x'
run_case DENY 'gh pr create -R && true' # dangling -R itself is no target
echo "=== Payload-scoping: claridtimo in cwd/paths must NOT count as a target ==="
CWD_JSON='{"cwd":"/home/dev/claridtimo/bang-game","transcript_path":"/home/dev/claridtimo/t.jsonl","tool_input":{"command":"gh pr create -t x"}}'
run_case_raw DENY "$CWD_JSON" 'bare create + claridtimo-bearing cwd (precise)'
DESC_JSON='{"tool_input":{"command":"gh pr create -t x","description":"Open PR with -R claridtimo/bang-game"},"cwd":"/x"}'
DESC_JSON2='{"tool_input":{"command":"gh pr view 63 -R claridtimo/x","description":"docs mention gh pr create"},"cwd":"/x"}'
run_case_raw DENY "$DESC_JSON" 'bare create + target only in description (precise)'
echo "=== Degraded path (no python3 on PATH) ==="
FAKEBIN="$(mktemp -d)"
trap 'rm -rf "$FAKEBIN"' EXIT
for t in bash cat grep printf env sh; do ln -sf "$(command -v $t)" "$FAKEBIN/$t"; done
run_case DENY 'gh pr create -t x' "$FAKEBIN"
run_case ALLOW 'gh pr create -R claridtimo/x -t y' "$FAKEBIN"
run_case ALLOW 'gh pr create --repo=claridtimo/x' "$FAKEBIN"
run_case ALLOW 'gh pr create -Rclaridtimo/x' "$FAKEBIN"
run_case ALLOW 'echo done' "$FAKEBIN"
run_case ALLOW 'gh pr view 63 -R claridtimo/x' "$FAKEBIN"
run_case ALLOW 'gh pr create -R Claridtimo/x -t y' "$FAKEBIN" # capitalized owner (degraded)
run_case DENY $'gh pr \\\ncreate -t x' "$FAKEBIN" # continued bare create (round 16)
run_case ALLOW $'gh pr \\\ncreate -R claridtimo/x' "$FAKEBIN" # continued targeted create
run_case ALLOW $'gh pr create -t x -R \\\nclaridtimo/x' "$FAKEBIN" # continuation inside flag adjacency
run_case_raw DENY "$CWD_JSON" 'bare create + claridtimo-bearing cwd (degraded, round 10)' "$FAKEBIN"
run_case_raw DENY "$DESC_JSON" 'bare create + target only in description (degraded, round 18)' "$FAKEBIN"
run_case_raw ALLOW "$DESC_JSON2" 'targeted view + create phrase in description (degraded, round 18)' "$FAKEBIN"
echo "=== Degraded path: ACCEPTED under-blocks, pinned (round 12) ==="
# The tokenizer-free fallback deliberately allows these two shapes: scoping flags to clauses
# with sed/grep was the v2 approach and false-denied real targeted creates (quoted PR bodies
# containing shell text). The precise path DENIES both — asserted alongside so the asymmetry is
# pinned and a future "fix" that flips either direction fails here.
run_case DENY 'gh pr view -R claridtimo/x && gh pr create -t y' # precise: real deny
run_case ALLOW 'gh pr view -R claridtimo/x && gh pr create -t y' "$FAKEBIN" # degraded: accepted
run_case ALLOW 'gh pr create --title "see -R claridtimo/x docs"' "$FAKEBIN" # degraded: accepted
echo
echo "pass=$pass fail=$fail"
[ "$fail" -eq 0 ]
+16
View File
@@ -0,0 +1,16 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-pr-target.sh",
"timeout": 10
}
]
}
]
}
}
+17
View File
@@ -0,0 +1,17 @@
name: Hook battery
on:
push:
branches: [master, main]
pull_request:
jobs:
hook-battery:
name: PR-target hook battery
# The PreToolUse hook (.claude/hooks/enforce-pr-target.sh) is the enforced backstop against
# accidentally opening PRs on the upstream fork parent (fork of greyhavens/*). Verbatim copy
# of the bang-game hook (see claridtimo/bang-game#63 for the 21-round review history); the
# battery pins every bypass / false-deny class found there. Seconds-fast: bash+python3+grep.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run the enforce-pr-target regression battery
run: .claude/hooks/test-enforce-pr-target.sh
+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="getdown-core" />
<module name="getdown-ant" />
<module name="getdown-launcher" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="getdown-ant" options="-Xlint -Xlint:-serial -Xlint:-path" />
<module name="getdown-core" options="-Xlint -Xlint:-serial -Xlint:-path" />
<module name="getdown-launcher" options="-Xlint -Xlint:-serial -Xlint:-path" />
</option>
</component>
</project>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ant/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ant/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/core/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/launcher/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/launcher/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="lib-repo" />
<option name="name" value="lib-repo" />
<option name="url" value="file://F:\projects\bang-mods\getdown\launcher/../lib" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK" />
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+3 -3
View File
@@ -2,9 +2,9 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.threerings.getdown</groupId>
<groupId>net.affliction</groupId>
<artifactId>getdown</artifactId>
<version>1.8.8</version>
<version>1.0.0</version>
</parent>
<artifactId>getdown-ant</artifactId>
@@ -14,7 +14,7 @@
<dependencies>
<dependency>
<groupId>com.threerings.getdown</groupId>
<groupId>net.affliction</groupId>
<artifactId>getdown-core</artifactId>
<version>${project.version}</version>
</dependency>
+2 -2
View File
@@ -2,9 +2,9 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.threerings.getdown</groupId>
<groupId>net.affliction</groupId>
<artifactId>getdown</artifactId>
<version>1.8.8</version>
<version>1.0.0</version>
</parent>
<artifactId>getdown-core</artifactId>
@@ -1,54 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.tests;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import com.threerings.getdown.tools.Digester;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class DigesterIT {
@Test
public void testDigester () throws Exception {
Path appdir = Paths.get("src/it/resources/testapp");
Digester.createDigests(appdir.toFile(), null, null, null);
Path digest = appdir.resolve("digest.txt");
List<String> digestLines = Files.readAllLines(digest, StandardCharsets.UTF_8);
Files.delete(digest);
Path digest2 = appdir.resolve("digest2.txt");
List<String> digest2Lines = Files.readAllLines(digest2, StandardCharsets.UTF_8);
Files.delete(digest2);
assertEquals(Arrays.asList(
"getdown.txt = 9c9b2494929c99d44ae51034d59e1a1b",
"testapp.jar = 404dafa55e78b25ec0e3a936357b1883",
"funny%test dir/some=file.txt = d8e8fca2dc0f896fd7cb4cb0031ba249",
"crazyhashfile#txt = f29d23fd5ab1781bd8d0760b3a516f16",
"foo.jar = 46ca4cc9079d9d019bb30cd21ebbc1ec",
"script.sh = f66e8ea25598e67e99c47d9b0b2a2cdf",
"digest.txt = 11f9ba349cf9edacac4d72a3158447e5"
), digestLines);
assertEquals(Arrays.asList(
"getdown.txt = 1efecfae2a189002a6658f17d162b1922c7bde978944949276dc038a0df2461f",
"testapp.jar = c9cb1906afbf48f8654b416c3f831046bd3752a76137e5bf0a9af2f790bf48e0",
"funny%test dir/some=file.txt = f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2",
"crazyhashfile#txt = 6816889f922de38f145db215a28ad7c5e1badf7354b5cdab225a27486789fa3b",
"foo.jar = ea188b872e0496debcbe00aaadccccb12a8aa9b025bb62c130cd3d9b8540b062",
"script.sh = cca1c5c7628d9bf7533f655a9cfa6573d64afb8375f81960d1d832dc5135c988",
"digest2.txt = 41eacdabda8909bdbbf61e4f980867f4003c16a12f6770e6fc619b6af100e05b"
), digest2Lines);
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

@@ -1 +0,0 @@
Hello crazy world.
Binary file not shown.
@@ -1 +0,0 @@
test
-40
View File
@@ -1,40 +0,0 @@
# where our app is hosted on the internets
appbase = http://notused.com/testapp
# the jar file that contains our code
code = testapp.jar
# the main entry point of our app
class = com.threerings.testapp.TestApp
# we pass the appdir to our app so that it can upgrade getdown
apparg = %APPDIR%
# test the %env% mechanism
jvmarg = -Dusername=\%ENV.USER%
# test various java_*** configs, they are not interesting for digester
java_local_dir = jre
java_max_version = 1089999
java_min_version = [windows] 1080111
java_min_version = [!windows] 1080192
java_exact_version_required = [linux] true
java_location = [linux-amd64] /files/java/java_linux_64.zip
java_location = [linux-i386] /files/java/java_linux_32.zip
java_location = [mac] /files/java/java_mac_64.zip
java_location = [windows-amd64] /files/java/java_windows_64.zip
java_location = [windows-x86] /files/java/java_windows_32.zip
strict_comments = true
resource = funny%test dir/some=file.txt
resource = crazyhashfile#txt
uresource = foo.jar
xresource = script.sh
ui.name = Getdown Test App
ui.background_image = background.png
ui.progress = 17, 321, 458, 22
ui.progress_bar = 336600
ui.progress_text = FFFFFF
ui.status = 57, 245, 373, 68
ui.status_text = 000000
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
echo "Hello world!"
Binary file not shown.
@@ -1027,6 +1027,12 @@ public class Application
// add the marker indicating the app is running in getdown
args.add("-D" + Properties.GETDOWN + "=true");
// forward the current JVM's language setting to the launched app
String userLanguage = System.getProperty("user.language");
if (!StringUtil.isBlank(userLanguage)) {
args.add("-Duser.language=" + userLanguage);
}
// set the native library path if we have native resources
// @TODO optional getdown.txt parameter to set addCurrentLibraryPath to true or false?
ClassPath javaLibPath = PathBuilder.buildLibsPath(this, true);
@@ -1715,7 +1721,15 @@ public class Application
}
for (String rsrc : rsrcs) {
try {
list.add(createResource(rsrc, attrs));
Resource res = createResource(rsrc, attrs);
// skip duplicate entries: a config publisher bug once stacked the code list 11x,
// and on Windows the duplicated -classpath blew past the 32,767-char
// CreateProcess limit (error=206); duplicates also multiply download/verify work
if (list.contains(res)) {
log.warning("Skipping duplicate resource '" + rsrc + "'.");
continue;
}
list.add(res);
} catch (Exception e) {
log.warning("Invalid resource '" + rsrc + "'. " + e);
}
@@ -1,84 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.cache;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
import org.junit.*;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeTrue;
/**
* Validates that cache garbage is collected and deleted correctly.
*/
@RunWith(Parameterized.class)
public class GarbageCollectorTest
{
@Parameterized.Parameters(name = "{0}")
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {{ ".jar" }, { ".zip" }});
}
@Parameterized.Parameter
public String extension;
@Before public void setupFiles () throws IOException
{
_cachedFile = _folder.newFile("abc123" + extension);
_lastAccessedFile = _folder.newFile(
"abc123" + extension + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
}
@Test public void shouldDeleteCacheEntryIfRetentionPeriodIsReached ()
{
gcNow();
assertFalse(_cachedFile.exists());
assertFalse(_lastAccessedFile.exists());
}
@Test public void shouldDeleteCacheFolderIfFolderIsEmpty ()
{
gcNow();
assertFalse(_folder.getRoot().exists());
}
private void gcNow() {
GarbageCollector.collect(_folder.getRoot(), -1);
}
@Test public void shouldKeepFilesInCacheIfRententionPeriodIsNotReached ()
{
GarbageCollector.collect(_folder.getRoot(), TimeUnit.DAYS.toMillis(1));
assertTrue(_cachedFile.exists());
assertTrue(_lastAccessedFile.exists());
}
@Test public void shouldDeleteCachedFileIfLastAccessedFileIsMissing ()
{
assumeTrue(_lastAccessedFile.delete());
gcNow();
assertFalse(_cachedFile.exists());
}
@Test public void shouldDeleteLastAccessedFileIfCachedFileIsMissing ()
{
assumeTrue(_cachedFile.delete());
gcNow();
assertFalse(_lastAccessedFile.exists());
}
@Rule public final TemporaryFolder _folder = new TemporaryFolder();
private File _cachedFile;
private File _lastAccessedFile;
}
@@ -1,84 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.cache;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
import org.junit.*;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import static org.junit.Assert.*;
/**
* Asserts the correct functionality of the {@link ResourceCache}.
*/
@RunWith(Parameterized.class)
public class ResourceCacheTest
{
@Parameterized.Parameters(name = "{0}")
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {{ ".jar" }, { ".zip" }});
}
@Parameterized.Parameter
public String extension;
@Before public void setupCache () throws IOException {
_fileToCache = _folder.newFile("filetocache" + extension);
_cache = new ResourceCache(_folder.newFolder(".cache"));
}
@Test public void shouldCacheFile () throws IOException
{
assertEquals("abc123" + extension, cacheFile().getName());
}
private File cacheFile() throws IOException
{
return _cache.cacheFile(_fileToCache, "abc123", "abc123");
}
@Test public void shouldTrackFileUsage () throws IOException
{
String name = "abc123" + extension + ResourceCache.LAST_ACCESSED_FILE_SUFFIX;
File lastAccessedFile = new File(cacheFile().getParentFile(), name);
assertTrue(lastAccessedFile.exists());
}
@Test public void shouldNotCacheTheSameFile () throws Exception
{
File cachedFile = cacheFile();
cachedFile.setLastModified(YESTERDAY);
long expectedLastModified = cachedFile.lastModified();
// caching it another time
File sameCachedFile = cacheFile();
assertEquals(expectedLastModified, sameCachedFile.lastModified());
}
@Test public void shouldRememberWhenFileWasRequested () throws Exception
{
File cachedFile = cacheFile();
String name = cachedFile.getName() + ResourceCache.LAST_ACCESSED_FILE_SUFFIX;
File lastAccessedFile = new File(cachedFile.getParentFile(), name);
lastAccessedFile.setLastModified(YESTERDAY);
long lastAccessed = lastAccessedFile.lastModified();
// caching it another time
cacheFile();
assertTrue(lastAccessedFile.lastModified() > lastAccessed);
}
@Rule public final TemporaryFolder _folder = new TemporaryFolder();
private File _fileToCache;
private ResourceCache _cache;
private static final long YESTERDAY = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
}
@@ -1,75 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.data;
import java.io.IOException;
import java.io.StringReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.junit.*;
import static org.junit.Assert.*;
import com.threerings.getdown.util.Config;
public class ApplicationTest {
Application createApp () {
List<EnvConfig.Note> notes = new ArrayList<>();
EnvConfig env = EnvConfig.create(new String[0], notes);
EnvConfigTest.checkNoNotes(notes);
return new Application(env);
}
@Test public void testBaseConfig () throws Exception {
Application app = createApp();
URL appbase = new URL("https://test.com/foo/bar/");
Config config = new Config(Config.parseData(toReader(
"appbase", appbase.toString()
), Config.createOpts(true)));
app.initBase(config);
assertEquals(appbase, app.getRemoteURL(""));
}
@Test public void testVersionedBase () throws Exception {
Application app = createApp();
String rootAppbase = "https://test.com/foo/bar/";
Config config = new Config(Config.parseData(toReader(
"appbase", rootAppbase + "%VERSION%",
"version", "42"
), Config.createOpts(true)));
app.initBase(config);
assertEquals(new URL(rootAppbase + "42/"), app.getRemoteURL(""));
}
@Test public void testEnvVarBase () throws Exception {
// fiddling to make test work on Windows or Unix
String evar = System.getenv("USER") == null ? "USERNAME" : "USER";
Application app = createApp();
String rootAppbase = "https://test.com/foo/%ENV." + evar + "%/";
Config config = new Config(Config.parseData(toReader(
"appbase", rootAppbase + "%VERSION%",
"version", "42"
), Config.createOpts(true)));
app.initBase(config);
String expectAppbase = "https://test.com/foo/" + System.getenv(evar) + "/42/";
assertEquals(new URL(expectAppbase), app.getRemoteURL(""));
}
protected static StringReader toReader (String... pairs)
{
StringBuilder builder = new StringBuilder();
for (int ii = 0; ii < pairs.length; ii += 2) {
builder.append(pairs[ii]).append("=").append(pairs[ii+1]).append("\n");
}
return new StringReader(builder.toString());
}
}
@@ -1,54 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.data;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.LinkedHashSet;
import org.junit.*;
import org.junit.rules.TemporaryFolder;
import static org.junit.Assert.assertEquals;
/**
* Tests for {@link ClassPath}.
*/
public class ClassPathTest
{
@Before public void createJarsAndSetupClassPath () throws IOException
{
_firstJar = _folder.newFile("a.jar");
_secondJar = _folder.newFile("b.jar");
LinkedHashSet<File> classPathEntries = new LinkedHashSet<>();
classPathEntries.add(_firstJar);
classPathEntries.add(_secondJar);
_classPath = new ClassPath(classPathEntries);
}
@Test public void shouldCreateValidArgumentString ()
{
assertEquals(
"a.jar:b.jar",
_classPath.asArgumentString(_folder.getRoot()));
}
@Test public void shouldProvideJarUrls () throws URISyntaxException
{
URL[] actualUrls = _classPath.asUrls();
assertEquals(_firstJar, new File(actualUrls[0].toURI()));
assertEquals(_secondJar, new File(actualUrls[1].toURI()));
}
@Rule public final TemporaryFolder _folder = new TemporaryFolder();
private File _firstJar, _secondJar;
private ClassPath _classPath;
}
@@ -1,142 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.data;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.io.File;
import org.junit.*;
import static org.junit.Assert.*;
public class EnvConfigTest {
static final String CWD = System.getProperty("user.dir");
static final String TESTID = "testid";
static final String TESTBASE = "https://test.com/test";
private void debugNotes (List<EnvConfig.Note> notes) {
for (EnvConfig.Note note : notes) {
System.out.println(note.message);
}
}
static void checkNoNotes (List<EnvConfig.Note> notes) {
StringBuilder msg = new StringBuilder();
for (EnvConfig.Note note : notes) {
if (note.level != EnvConfig.Note.Level.INFO) {
msg.append("\n").append(note.message);
}
}
if (msg.length() > 0) {
fail("Unexpected notes:" + msg);
}
}
private void checkDir (EnvConfig cfg) {
assertTrue(cfg != null);
assertEquals(new File(CWD), cfg.appDir);
}
private void checkNoAppId (EnvConfig cfg) {
assertNull(cfg.appId);
}
private void checkAppId (EnvConfig cfg, String appId) {
assertEquals(appId, cfg.appId);
}
private void checkAppBase (EnvConfig cfg, String appBase) {
assertEquals(appBase, cfg.appBase);
}
private void checkNoAppBase (EnvConfig cfg) {
assertNull(cfg.appBase);
}
private void checkNoAppArgs (EnvConfig cfg) {
assertTrue(cfg.appArgs.isEmpty());
}
private void checkAppArgs (EnvConfig cfg, String... args) {
assertEquals(Arrays.asList(args), cfg.appArgs);
}
@Test public void testArgvDir () {
List<EnvConfig.Note> notes = new ArrayList<>();
String[] args = { CWD };
EnvConfig cfg = EnvConfig.create(args, notes);
// debugNotes(notes);
checkNoNotes(notes);
checkDir(cfg);
checkNoAppId(cfg);
checkNoAppBase(cfg);
checkNoAppArgs(cfg);
}
@Test public void testArgvDirId () {
List<EnvConfig.Note> notes = new ArrayList<>();
String[] args = { CWD, TESTID };
EnvConfig cfg = EnvConfig.create(args, notes);
// debugNotes(notes);
checkNoNotes(notes);
checkDir(cfg);
checkAppId(cfg, TESTID);
checkNoAppBase(cfg);
checkNoAppArgs(cfg);
}
@Test public void testArgvDirArgs () {
List<EnvConfig.Note> notes = new ArrayList<>();
String[] args = { CWD, "", "one", "two" };
EnvConfig cfg = EnvConfig.create(args, notes);
// debugNotes(notes);
checkNoNotes(notes);
checkDir(cfg);
checkNoAppId(cfg);
checkNoAppBase(cfg);
checkAppArgs(cfg, "one", "two");
}
@Test public void testArgvDirIdArgs () {
List<EnvConfig.Note> notes = new ArrayList<>();
String[] args = { CWD, TESTID, "one", "two" };
EnvConfig cfg = EnvConfig.create(args, notes);
// debugNotes(notes);
checkNoNotes(notes);
checkDir(cfg);
checkAppId(cfg, TESTID);
checkNoAppBase(cfg);
checkAppArgs(cfg, "one", "two");
}
private EnvConfig sysPropsConfig (List<EnvConfig.Note> notes, String... keyVals) {
for (int ii = 0; ii < keyVals.length; ii += 2) {
System.setProperty(keyVals[ii], keyVals[ii+1]);
}
EnvConfig cfg = EnvConfig.create(new String[0], notes);
for (int ii = 0; ii < keyVals.length; ii += 2) {
System.clearProperty(keyVals[ii]);
}
return cfg;
}
@Test public void testSysPropsDir () {
List<EnvConfig.Note> notes = new ArrayList<>();
EnvConfig cfg = sysPropsConfig(notes, "appdir", CWD);
// debugNotes(notes);
checkNoNotes(notes);
checkDir(cfg);
checkNoAppId(cfg);
checkNoAppBase(cfg);
checkNoAppArgs(cfg);
}
@Test public void testSysPropsDirIdBase () {
List<EnvConfig.Note> notes = new ArrayList<>();
EnvConfig cfg = sysPropsConfig(notes, "appdir", CWD, "appid", TESTID, "appbase", TESTBASE);
// debugNotes(notes);
checkNoNotes(notes);
checkDir(cfg);
checkAppId(cfg, TESTID);
checkAppBase(cfg, TESTBASE);
checkNoAppArgs(cfg);
}
}
@@ -1,58 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.data;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import org.junit.*;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class PathBuilderTest
{
@Before public void setupFilesAndResources () throws IOException
{
_firstJarFile = _appdir.newFile("a.jar");
_secondJarFile = _appdir.newFile("b.jar");
when(_firstJar.getFinalTarget()).thenReturn(_firstJarFile);
when(_secondJar.getFinalTarget()).thenReturn(_secondJarFile);
when(_application.getActiveCodeResources()).thenReturn(Arrays.asList(_firstJar, _secondJar));
when(_application.getAppDir()).thenReturn(_appdir.getRoot());
}
@Test public void shouldBuildDefaultClassPath () throws IOException
{
ClassPath classPath = PathBuilder.buildDefaultClassPath(_application);
assertEquals("a.jar:b.jar", classPath.asArgumentString(_appdir.getRoot()));
}
@Test public void shouldBuildCachedClassPath () throws IOException
{
when(_application.getDigest(_firstJar)).thenReturn("first");
when(_application.getDigest(_secondJar)).thenReturn("second");
when(_application.getCodeCacheRetentionDays()).thenReturn(1);
ClassPath classPath = PathBuilder.buildCachedClassPath(_application);
assertEquals(".cache/fi/first.jar:.cache/se/second.jar", classPath.asArgumentString(_appdir.getRoot()));
}
@Mock protected Application _application;
@Mock protected Resource _firstJar;
@Mock protected Resource _secondJar;
protected File _firstJarFile, _secondJarFile;
@Rule public final TemporaryFolder _appdir = new TemporaryFolder();
}
@@ -1,63 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.data;
import org.junit.*;
import static org.junit.Assert.*;
public class SysPropsTest {
@After public void clearProps () {
System.clearProperty("delay");
System.clearProperty("appbase_domain");
System.clearProperty("appbase_override");
}
private static final String[] APPBASES = {
"http://foobar.com/myapp",
"https://foobar.com/myapp",
"http://foobar.com:8080/myapp",
"https://foobar.com:8080/myapp"
};
@Test public void testStartDelay () {
assertEquals(0, SysProps.startDelay());
System.setProperty("delay", "x");
assertEquals(0, SysProps.startDelay());
System.setProperty("delay", "-7");
assertEquals(0, SysProps.startDelay());
System.setProperty("delay", "7");
assertEquals(7, SysProps.startDelay());
System.setProperty("delay", "1440");
assertEquals(1440, SysProps.startDelay());
System.setProperty("delay", "1441");
assertEquals(1440, SysProps.startDelay());
}
@Test public void testAppbaseDomain () {
System.setProperty("appbase_domain", "https://barbaz.com");
for (String appbase : APPBASES) {
assertEquals("https://barbaz.com/myapp", SysProps.overrideAppbase(appbase));
}
System.setProperty("appbase_domain", "http://barbaz.com");
for (String appbase : APPBASES) {
assertEquals("http://barbaz.com/myapp", SysProps.overrideAppbase(appbase));
}
}
@Test public void testAppbaseOverride () {
System.setProperty("appbase_override", "https://barbaz.com/newapp");
for (String appbase : APPBASES) {
assertEquals("https://barbaz.com/newapp", SysProps.overrideAppbase(appbase));
}
}
}
@@ -1,23 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
/**
* Tests {@link Color}.
*/
public class ColorTest
{
@Test
public void testBrightness() {
assertEquals(0, Color.brightness(0xFF000000), 0.0000001);
assertEquals(1, Color.brightness(0xFFFFFFFF), 0.0000001);
assertEquals(0.0117647, Color.brightness(0xFF010203), 0.0000001);
assertEquals(1, Color.brightness(0xFF00FFC8), 0.0000001);
}
}
@@ -1,171 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import java.util.Random;
import org.junit.*;
import static org.junit.Assert.*;
/**
* Tests {@link Config}.
*/
public class ConfigTest
{
public static class Pair {
public final String key;
public final String value;
public Pair (String key, String value) {
this.key = key;
this.value = value;
}
}
public static final Pair[] SIMPLE_PAIRS = {
new Pair("one", "two"),
new Pair("three", "four"),
new Pair("five", "six"),
new Pair("seven", "eight"),
new Pair("nine", "ten"),
};
@Test public void testSimplePairs () throws IOException
{
List<String[]> pairs = Config.parsePairs(
toReader(SIMPLE_PAIRS), Config.createOpts(true));
for (int ii = 0; ii < SIMPLE_PAIRS.length; ii++) {
assertEquals(SIMPLE_PAIRS[ii].key, pairs.get(ii)[0]);
assertEquals(SIMPLE_PAIRS[ii].value, pairs.get(ii)[1]);
}
}
@Test public void testQualifiedPairs () throws IOException
{
Pair linux = new Pair("one", "[linux] two");
Pair mac = new Pair("three", "[mac os x] four");
Pair linuxAndMac = new Pair("five", "[linux, mac os x] six");
Pair linux64 = new Pair("seven", "[linux-x86_64] eight");
Pair linux64s = new Pair("nine", "[linux-x86_64, linux-amd64] ten");
Pair mac64 = new Pair("eleven", "[mac os x-x86_64] twelve");
Pair win64 = new Pair("thirteen", "[windows-x86_64] fourteen");
Pair notWin = new Pair("fifteen", "[!windows] sixteen");
Pair[] pairs = { linux, mac, linuxAndMac, linux64, linux64s, mac64, win64, notWin };
Config.ParseOpts opts = Config.createOpts(false);
opts.osname = "linux";
opts.osarch = "i386";
List<String[]> parsed = Config.parsePairs(toReader(pairs), opts);
assertTrue(exists(parsed, linux.key));
assertFalse(exists(parsed, mac.key));
assertTrue(exists(parsed, linuxAndMac.key));
assertFalse(exists(parsed, linux64.key));
assertFalse(exists(parsed, linux64s.key));
assertFalse(exists(parsed, mac64.key));
assertFalse(exists(parsed, win64.key));
assertTrue(exists(parsed, notWin.key));
opts.osarch = "x86_64";
parsed = Config.parsePairs(toReader(pairs), opts);
assertTrue(exists(parsed, linux.key));
assertFalse(exists(parsed, mac.key));
assertTrue(exists(parsed, linuxAndMac.key));
assertTrue(exists(parsed, linux64.key));
assertTrue(exists(parsed, linux64s.key));
assertFalse(exists(parsed, mac64.key));
assertFalse(exists(parsed, win64.key));
assertTrue(exists(parsed, notWin.key));
opts.osarch = "amd64";
parsed = Config.parsePairs(toReader(pairs), opts);
assertTrue(exists(parsed, linux.key));
assertFalse(exists(parsed, mac.key));
assertTrue(exists(parsed, linuxAndMac.key));
assertFalse(exists(parsed, linux64.key));
assertTrue(exists(parsed, linux64s.key));
assertFalse(exists(parsed, mac64.key));
assertFalse(exists(parsed, win64.key));
assertTrue(exists(parsed, notWin.key));
opts.osname = "mac os x";
opts.osarch = "x86_64";
parsed = Config.parsePairs(toReader(pairs), opts);
assertFalse(exists(parsed, linux.key));
assertTrue(exists(parsed, mac.key));
assertTrue(exists(parsed, linuxAndMac.key));
assertFalse(exists(parsed, linux64.key));
assertFalse(exists(parsed, linux64s.key));
assertTrue(exists(parsed, mac64.key));
assertFalse(exists(parsed, win64.key));
assertTrue(exists(parsed, notWin.key));
opts.osname = "windows";
opts.osarch = "i386";
parsed = Config.parsePairs(toReader(pairs), opts);
assertFalse(exists(parsed, linux.key));
assertFalse(exists(parsed, mac.key));
assertFalse(exists(parsed, linuxAndMac.key));
assertFalse(exists(parsed, linux64.key));
assertFalse(exists(parsed, linux64s.key));
assertFalse(exists(parsed, mac64.key));
assertFalse(exists(parsed, win64.key));
assertFalse(exists(parsed, notWin.key));
opts.osarch = "x86_64";
parsed = Config.parsePairs(toReader(pairs), opts);
assertFalse(exists(parsed, linux.key));
assertFalse(exists(parsed, mac.key));
assertFalse(exists(parsed, linuxAndMac.key));
assertFalse(exists(parsed, linux64.key));
assertFalse(exists(parsed, linux64s.key));
assertFalse(exists(parsed, mac64.key));
assertTrue(exists(parsed, win64.key));
assertFalse(exists(parsed, notWin.key));
opts.osarch = "amd64";
parsed = Config.parsePairs(toReader(pairs), opts);
assertFalse(exists(parsed, linux.key));
assertFalse(exists(parsed, mac.key));
assertFalse(exists(parsed, linuxAndMac.key));
assertFalse(exists(parsed, linux64.key));
assertFalse(exists(parsed, linux64s.key));
assertFalse(exists(parsed, mac64.key));
assertFalse(exists(parsed, win64.key));
assertFalse(exists(parsed, notWin.key));
}
protected static boolean exists (List<String[]> pairs, String key)
{
for (String[] pair : pairs) {
if (pair[0].equals(key)) {
return true;
}
}
return false;
}
protected static StringReader toReader (Pair[] pairs)
{
StringBuilder builder = new StringBuilder();
for (Pair pair : pairs) {
// throw some whitespace in to ensure it's trimmed
builder.append(whitespace()).append(pair.key).
append(whitespace()).append("=").
append(whitespace()).append(pair.value).
append(whitespace()).append("\n");
}
return new StringReader(builder.toString());
}
protected static String whitespace ()
{
return _rando.nextBoolean() ? " " : "";
}
protected static final Random _rando = new Random();
}
@@ -1,60 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import org.junit.*;
import org.junit.rules.TemporaryFolder;
import static org.junit.Assert.*;
/**
* Tests {@link FileUtil}.
*/
public class FileUtilTest
{
@Test public void testReadLines () throws IOException
{
String data = "This is a test\nof a file with\na few lines\n";
List<String> lines = FileUtil.readLines(new StringReader(data));
String[] linesBySplit = data.split("\n");
assertEquals(linesBySplit.length, lines.size());
for (int ii = 0; ii < lines.size(); ii++) {
assertEquals(linesBySplit[ii], lines.get(ii));
}
}
@Test public void shouldCopyFile () throws IOException
{
File source = _folder.newFile("source.txt");
File target = new File(_folder.getRoot(), "target.txt");
assertFalse(target.exists());
FileUtil.copy(source, target);
assertTrue(target.exists());
}
@Test public void shouldRecursivelyWalkOverFilesAndFolders () throws IOException
{
_folder.newFile("a.txt");
new File(_folder.newFolder("b"), "b.txt").createNewFile();
class CountingVisitor implements FileUtil.Visitor {
int fileCount = 0;
@Override public void visit(File file) {
fileCount++;
}
}
CountingVisitor visitor = new CountingVisitor();
FileUtil.walkTree(_folder.getRoot(), visitor);
assertEquals(3, visitor.fileCount);
}
@Rule public final TemporaryFolder _folder = new TemporaryFolder();
}
@@ -1,159 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import static org.junit.Assert.assertEquals;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
/**
* Tests {@link HostWhitelist}.
*/
public class HostWhitelistTest
{
@Test
public void testVerify () throws MalformedURLException
{
checkCanVerify("foo.com", "http://foo.com", true);
checkCanVerify("foo.com", "http://foo.com/", true);
checkCanVerify("foo.com", "http://foo.com/x/y/z", true);
checkCanVerify("foo.com", "http://www.foo.com", false);
checkCanVerify("foo.com", "http://www.foo.com/", false);
checkCanVerify("foo.com", "http://www.foo.com/x/y/z", false);
checkCanVerify("foo.com", "http://a.b.foo.com", false);
checkCanVerify("foo.com", "http://a.b.foo.com/", false);
checkCanVerify("foo.com", "http://a.b.foo.com/x/y/z", false);
checkCanVerify("foo.com", "http://oo.com", false);
checkCanVerify("foo.com", "http://f.oo.com", false);
checkCanVerify("foo.com", "http://a.f.oo.com", false);
checkCanVerify("*.foo.com", "http://foo.com", false);
checkCanVerify("*.foo.com", "http://foo.com/", false);
checkCanVerify("*.foo.com", "http://foo.com/x/y/z", false);
checkCanVerify("*.foo.com", "http://www.foo.com", true);
checkCanVerify("*.foo.com", "http://www.foo.com/", true);
checkCanVerify("*.foo.com", "http://www.foo.com/x/y/z", true);
checkCanVerify("*.foo.com", "http://a.b.foo.com", true);
checkCanVerify("*.foo.com", "http://a.b.foo.com/", true);
checkCanVerify("*.foo.com", "http://a.b.foo.com/x/y/z", true);
checkCanVerify("*.foo.com", "http://oo.com", false);
checkCanVerify("*.foo.com", "http://f.oo.com", false);
checkCanVerify("*.foo.com", "http://a.f.oo.com", false);
checkCanVerify("*.com", "http://foo.com", true);
checkCanVerify("*.com", "http://foo.com/", true);
checkCanVerify("*.com", "http://foo.com/x/y/z", true);
checkCanVerify("*.com", "http://www.foo.com", true);
checkCanVerify("*.com", "http://www.foo.com/", true);
checkCanVerify("*.com", "http://www.foo.com/x/y/z", true);
checkCanVerify("*.com", "http://a.b.foo.com", true);
checkCanVerify("*.com", "http://a.b.foo.com/", true);
checkCanVerify("*.com", "http://a.b.foo.com/x/y/z", true);
checkCanVerify("*.com", "http://oo.com", true);
checkCanVerify("*.com", "http://f.oo.com", true);
checkCanVerify("*.com", "http://a.f.oo.com", true);
checkCanVerify("*.net", "http://foo.com", false);
checkCanVerify("*.net", "http://foo.com/", false);
checkCanVerify("*.net", "http://foo.com/x/y/z", false);
checkCanVerify("*.net", "http://www.foo.com", false);
checkCanVerify("*.net", "http://www.foo.com/", false);
checkCanVerify("*.net", "http://www.foo.com/x/y/z", false);
checkCanVerify("*.net", "http://a.b.foo.com", false);
checkCanVerify("*.net", "http://a.b.foo.com/", false);
checkCanVerify("*.net", "http://a.b.foo.com/x/y/z", false);
checkCanVerify("*.net", "http://oo.com", false);
checkCanVerify("*.net", "http://f.oo.com", false);
checkCanVerify("*.net", "http://a.f.oo.com", false);
checkCanVerify("www.*.com", "http://foo.com", false);
checkCanVerify("www.*.com", "http://foo.com/", false);
checkCanVerify("www.*.com", "http://foo.com/x/y/z", false);
checkCanVerify("www.*.com", "http://www.foo.com", true);
checkCanVerify("www.*.com", "http://www.foo.com/", true);
checkCanVerify("www.*.com", "http://www.foo.com/x/y/z", true);
checkCanVerify("www.*.com", "http://a.b.foo.com", false);
checkCanVerify("www.*.com", "http://a.b.foo.com/", false);
checkCanVerify("www.*.com", "http://a.b.foo.com/x/y/z", false);
checkCanVerify("www.*.com", "http://oo.com", false);
checkCanVerify("www.*.com", "http://f.oo.com", false);
checkCanVerify("www.*.com", "http://a.f.oo.com", false);
checkCanVerify("www.*.com", "http://www.a.f.oo.com", true);
checkCanVerify("foo.*", "http://foo.com", true);
checkCanVerify("foo.*", "http://foo.com/", true);
checkCanVerify("foo.*", "http://foo.com/x/y/z", true);
checkCanVerify("foo.*", "http://www.foo.com", false);
checkCanVerify("foo.*", "http://www.foo.com/", false);
checkCanVerify("foo.*", "http://www.foo.com/x/y/z", false);
checkCanVerify("foo.*", "http://a.b.foo.com", false);
checkCanVerify("foo.*", "http://a.b.foo.com/", false);
checkCanVerify("foo.*", "http://a.b.foo.com/x/y/z", false);
checkCanVerify("foo.*", "http://oo.com", false);
checkCanVerify("foo.*", "http://f.oo.com", false);
checkCanVerify("foo.*", "http://a.f.oo.com", false);
checkCanVerify("*.foo.*", "http://foo.com", false);
checkCanVerify("*.foo.*", "http://foo.com/", false);
checkCanVerify("*.foo.*", "http://foo.com/x/y/z", false);
checkCanVerify("*.foo.*", "http://www.foo.com", true);
checkCanVerify("*.foo.*", "http://www.foo.com/", true);
checkCanVerify("*.foo.*", "http://www.foo.com/x/y/z", true);
checkCanVerify("*.foo.*", "http://a.b.foo.com", true);
checkCanVerify("*.foo.*", "http://a.b.foo.com/", true);
checkCanVerify("*.foo.*", "http://a.b.foo.com/x/y/z", true);
checkCanVerify("*.foo.*", "http://oo.com", false);
checkCanVerify("*.foo.*", "http://f.oo.com", false);
checkCanVerify("*.foo.*", "http://a.f.oo.com", false);
checkCanVerify("127.0.0.1", "http://127.0.0.1", true);
checkCanVerify("127.0.0.1", "http://127.0.0.1/", true);
checkCanVerify("127.0.0.1", "http://127.0.0.1/x/y/z", true);
checkCanVerify("*.0.0.1", "http://127.0.0.1/abc", true);
checkCanVerify("127.*.0.1", "http://127.0.0.1/abc", true);
checkCanVerify("127.0.*.1", "http://127.0.0.1/abc", true);
checkCanVerify("127.0.0.*", "http://127.0.0.1/abc", true);
checkCanVerify("127.*.1", "http://127.0.0.1/abc", true);
checkCanVerify("*.0.1", "http://127.0.0.1/abc", true);
checkCanVerify("127.0.*", "http://127.0.0.1/abc", true);
checkCanVerify("*", "http://127.0.0.1/abc", true);
checkCanVerify("127.0.0.2", "http://127.0.0.1", false);
checkCanVerify("127.0.2.1", "http://127.0.0.1", false);
checkCanVerify("127.2.0.1", "http://127.0.0.1", false);
checkCanVerify("222.0.0.1", "http://127.0.0.1", false);
checkCanVerify("", "http://foo.com", true);
checkCanVerify("", "http://aaa.bbb.net/xyz", true);
checkCanVerify("", "https://127.0.0.1/abc", true);
checkCanVerify("aaa.bbb.com,xxx.yyy.com, *.jjj.net", "http://aaa.bbb.com/m", true);
checkCanVerify("aaa.bbb.com, xxx.yyy.com,*.jjj.net", "http://xxx.yyy.com/n", true);
checkCanVerify("aaa.bbb.com,xxx.yyy.com, *.jjj.net", "http://www.jjj.net/o", true);
}
private static void checkCanVerify (String whitelist, String url, boolean expectedToPass)
throws MalformedURLException
{
List<String> w = Arrays.asList(StringUtil.parseStringArray(whitelist));
URL u = new URL(url);
boolean passed;
try {
HostWhitelist.verify(w, u);
passed = true;
} catch (MalformedURLException e) {
passed = false;
}
assertEquals("with whitelist '" + whitelist + "' and URL '" + url + "'",
expectedToPass, passed);
}
}
@@ -1,28 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import org.junit.Test;
import static com.threerings.getdown.util.StringUtil.couldBeValidUrl;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Tests {@link StringUtil}.
*/
public class StringUtilTest
{
@Test public void testCouldBeValidUrl ()
{
assertTrue(couldBeValidUrl("http://www.foo.com/"));
assertTrue(couldBeValidUrl("http://www.foo.com/A-B-C/1_2_3/~bar/q.jsp?x=u+i&y=2;3;4#baz%20baz"));
assertTrue(couldBeValidUrl("https://user:secret@www.foo.com/"));
assertFalse(couldBeValidUrl("http://www.foo.com & echo hello"));
assertFalse(couldBeValidUrl("http://www.foo.com\""));
}
}
@@ -1,53 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class VersionUtilTest {
@Test
public void shouldParseJavaVersion ()
{
long version = VersionUtil.parseJavaVersion(
"(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?", "1.8.0_152");
assertEquals(1_080_152, version);
}
@Test
public void shouldParseJavaVersion8 ()
{
long version = VersionUtil.parseJavaVersion(
"(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?", "1.8");
assertEquals(1_080_000, version);
}
@Test
public void shouldParseJavaVersion9 ()
{
long version = VersionUtil.parseJavaVersion(
"(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?", "9");
assertEquals(9_000_000, version);
}
@Test
public void shouldParseJavaVersion10 ()
{
long version = VersionUtil.parseJavaVersion(
"(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?", "10");
assertEquals(10_000_000, version);
}
@Test
public void shouldParseJavaRuntimeVersion ()
{
long version = VersionUtil.parseJavaVersion(
"(\\d+)\\.(\\d+)\\.(\\d+)(_\\d+)?(-b\\d+)?", "1.8.0_131-b11");
assertEquals(108_013_111, version);
}
}
@@ -1 +0,0 @@
mock-maker-inline
+30 -94
View File
@@ -2,9 +2,9 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.threerings.getdown</groupId>
<groupId>net.affliction</groupId>
<artifactId>getdown</artifactId>
<version>1.8.8</version>
<version>1.0.0</version>
</parent>
<artifactId>getdown-launcher</artifactId>
@@ -21,7 +21,7 @@
<dependencies>
<dependency>
<groupId>com.threerings.getdown</groupId>
<groupId>net.affliction</groupId>
<artifactId>getdown-core</artifactId>
<version>${project.version}</version>
<optional>true</optional>
@@ -29,7 +29,7 @@
<dependency>
<groupId>com.samskivert</groupId>
<artifactId>samskivert</artifactId>
<version>1.2</version>
<version>1.13</version>
<optional>true</optional>
</dependency>
<dependency>
@@ -82,79 +82,6 @@
</executions>
</plugin>
<plugin>
<groupId>com.github.wvengen</groupId>
<artifactId>proguard-maven-plugin</artifactId>
<version>2.7.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>proguard</goal></goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.guardsquare</groupId>
<artifactId>proguard-base</artifactId>
<version>${proguard.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.guardsquare</groupId>
<artifactId>proguard-core</artifactId>
<version>${proguard-core.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<configuration>
<proguardVersion>${proguard.version}</proguardVersion>
<outputDirectory>${project.build.directory}</outputDirectory>
<outjar>${project.build.finalName}.jar</outjar>
<injar>${project.build.finalName}.jar</injar>
<assembly>
<inclusions>
<inclusion>
<groupId>com.threerings.getdown</groupId>
<artifactId>getdown-core</artifactId>
</inclusion>
<inclusion>
<groupId>com.samskivert</groupId>
<artifactId>samskivert</artifactId>
<filter>
!**/*.java,
!**/swing/RuntimeAdjust*,
!**/swing/util/ButtonUtil*,
!**/util/CalendarUtil*,
!**/util/Calendars*,
!**/util/Log4JLogger*,
!**/util/PrefsConfig*,
!**/util/SignalUtil*,
com/samskivert/Log.class,
**/samskivert/io/**,
**/samskivert/swing/**,
**/samskivert/text/**,
**/samskivert/util/**
</filter>
</inclusion>
<inclusion>
<groupId>jregistrykey</groupId>
<artifactId>jregistrykey</artifactId>
</inclusion>
</inclusions>
</assembly>
<obfuscate>true</obfuscate>
<options>
<option>-keep public class com.threerings.getdown.** { *; }</option>
<option>-keep public class ca.beq.util.win32.registry.** { *; }</option>
<option>-keepattributes Exceptions, InnerClasses, Signature</option>
</options>
<libs>
<lib>${rt.jar.path}</lib>
</libs>
<addMavenDescriptor>false</addMavenDescriptor>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
@@ -175,6 +102,32 @@
</archive>
</configuration>
</plugin>
<!-- Shadow (fat) jar plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.threerings.getdown.launcher.GetdownApp</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<finalName>${project.artifactId}-all</finalName>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
@@ -203,23 +156,6 @@
<activation>
<file><exists>${java.home}/jmods/java.base.jmod</exists></file>
</activation>
<build>
<plugins>
<plugin>
<groupId>com.github.wvengen</groupId>
<artifactId>proguard-maven-plugin</artifactId>
<configuration>
<libs>
<lib>${java.home}/jmods/java.base.jmod</lib>
<lib>${java.home}/jmods/java.desktop.jmod</lib>
<lib>${java.home}/jmods/java.logging.jmod</lib>
<lib>${java.home}/jmods/java.scripting.jmod</lib>
<lib>${java.home}/jmods/jdk.jsobject.jmod</lib>
</libs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
@@ -38,6 +38,7 @@ import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLayeredPane;
import javax.swing.JOptionPane;
import com.samskivert.swing.util.SwingUtil;
import com.threerings.getdown.data.Application;
@@ -75,6 +76,14 @@ public abstract class Getdown
}.start();
}
// This is just a compatibility shim. Way back in the day Getdown was launched via
// com.threerings.getdown.launcher.Getdown but now it's launched via
// com.threerings.getdown.launcher.GetdownApp. We want those old jpackage clients to work, so we
// need a main() on this class to keep them functioning.
public static void main (String[] args) {
GetdownApp.main(args);
}
public Getdown (EnvConfig envc)
{
try {
@@ -194,11 +203,17 @@ public abstract class Getdown
_dead = false;
// if we fail to detect a proxy, but we're allowed to run offline, then go ahead and
// run the app anyway because we're prepared to cope with not being able to update
if (detectProxy() || _app.allowOffline()) getdown();
else requestProxyInfo(false);
try {
if (detectProxy() || _app.allowOffline()) getdown();
else requestProxyInfo(false);
} catch (IOException ioe) {
log.info("Failed to detect if proxy is needed", "error", ioe);
log.info("We may need a proxy, or maybe their network is down.");
showConnectionFailedDialog();
}
}
protected boolean detectProxy () {
protected boolean detectProxy () throws IOException {
// first we have to initialize our application to get the appbase URL, etc.
log.info("Checking whether we need to use a proxy...");
try {
@@ -215,6 +230,7 @@ public abstract class Getdown
// see if we actually need a proxy
updateStatus("m.detecting_proxy");
URL configURL = _app.getConfigResource().getRemote();
// if this detection fails to connect, we'll let the IOException propagate out
if (!ProxyUtil.canLoadWithoutProxy(configURL, tryNoProxy ? 2 : 5)) {
// if we didn't auto-detect proxy first thing, do auto-detect now
return tryNoProxy ? ProxyUtil.autoDetectProxy(_app) : false;
@@ -254,6 +270,33 @@ public abstract class Getdown
showContainer();
}
/**
* Shows a dialog informing the user that we could not connect to the download servers,
* with buttons to try again or configure a proxy.
*/
protected void showConnectionFailedDialog () {
if (_silent) {
log.warning("Unable to connect to download servers. Exiting.");
return;
}
String message = _msgs.getString("m.proxy_connect_failed");
String tryAgain = _msgs.getString("m.try_again");
String configureProxy = _msgs.getString("m.configure_proxy_button");
Object[] options = { tryAgain, configureProxy };
int choice = JOptionPane.showOptionDialog(
null, message, "",
JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE,
null, options, options[0]);
if (choice == JOptionPane.YES_OPTION) {
// "Try again" — re-run the update/launch sequence
run();
} else if (choice == JOptionPane.NO_OPTION) {
// "Configure proxy" — show the proxy configuration panel
requestProxyInfo(false);
}
}
/**
* Downloads and installs (without verifying) any resources that are marked with a
* {@code PRELOAD} attribute.
@@ -124,35 +124,31 @@ public final class ProxyUtil {
}
public static boolean canLoadWithoutProxy (URL rurl, int timeoutSeconds)
throws IOException
{
log.info("Attempting to fetch without proxy: " + rurl);
URLConnection conn = Connector.DEFAULT.open(rurl, timeoutSeconds, timeoutSeconds);
// if the appbase is not an HTTP/S URL (like file:), then we don't need a proxy
if (!(conn instanceof HttpURLConnection)) {
return true;
}
// otherwise, try to make a HEAD request for this URL
HttpURLConnection hcon = (HttpURLConnection)conn;
try {
URLConnection conn = Connector.DEFAULT.open(rurl, timeoutSeconds, timeoutSeconds);
// if the appbase is not an HTTP/S URL (like file:), then we don't need a proxy
if (!(conn instanceof HttpURLConnection)) {
hcon.setRequestMethod("HEAD");
hcon.connect();
// make sure we got a satisfactory response code
int rcode = hcon.getResponseCode();
if (rcode == HttpURLConnection.HTTP_PROXY_AUTH ||
rcode == HttpURLConnection.HTTP_FORBIDDEN) {
log.warning("Got an 'HTTP credentials needed' response", "code", rcode);
return false;
} else {
return true;
}
// otherwise, try to make a HEAD request for this URL
HttpURLConnection hcon = (HttpURLConnection)conn;
try {
hcon.setRequestMethod("HEAD");
hcon.connect();
// make sure we got a satisfactory response code
int rcode = hcon.getResponseCode();
if (rcode == HttpURLConnection.HTTP_PROXY_AUTH ||
rcode == HttpURLConnection.HTTP_FORBIDDEN) {
log.warning("Got an 'HTTP credentials needed' response", "code", rcode);
} else {
return true;
}
} finally {
hcon.disconnect();
}
} catch (IOException ioe) {
log.info("Failed to HEAD " + rurl + ": " + ioe);
log.info("We probably need a proxy, but auto-detection failed.");
} finally {
hcon.disconnect();
}
return false;
}
public static void configProxy (Application app, String host, String port,
@@ -11,6 +11,10 @@ m.abort_cancel = Continue installation
m.detecting_proxy = Trying to auto-detect proxy settings
m.proxy_connect_failed = We were unable to connect to the download servers. Please check your Internet connection.
m.try_again = Try again
m.configure_proxy_button = Configure proxy
m.configure_proxy = <html>We were unable to connect to the application server to download data. \
<p> Please make sure that no virus scanner or firewall is blocking network communication with \
the server. \
@@ -1,149 +0,0 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.launcher;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.URL;
import org.junit.Test;
import static org.junit.Assert.*;
public class ProxyUtilTest {
static String[] strs (String... strs) { return strs; }
static String[] findPAC (String code, String url) throws Exception {
return ProxyUtil.findPACProxiesForURL(new StringReader(code), new URL(url));
}
static void testPAC (String code, String url, String... expectedProxies) throws Exception {
assertArrayEquals(expectedProxies, findPAC(code, url));
}
@Test public void testPACProxy () throws Exception {
// we use the Graal JavaScrip VM for testing, because JDK15 no longer bundles Nashorn, but
// it does not support calling back into Java unless we set this compatibility property
System.setProperty("polyglot.js.nashorn-compat", "true");
String EXAMPLE0 =
"function FindProxyForURL(url, host) {\n" +
" if (shExpMatch(host, '*.example.com')) { return 'DIRECT'; }\n" +
" if (isInNet(host, '10.0.0.0', '255.255.248.0')) {\n" +
" return 'PROXY fastproxy.example.com:8080';\n" +
" }\n" +
" return 'PROXY proxy.example.com:8080; DIRECT';\n" +
"}\n";
testPAC(EXAMPLE0, "http://test.example.com/", "DIRECT");
testPAC(EXAMPLE0, "http://10.0.1.1/", "PROXY fastproxy.example.com:8080");
testPAC(EXAMPLE0, "http://chicken.gov/", "PROXY proxy.example.com:8080", "DIRECT");
String EXAMPLE1 =
"function FindProxyForURL(url, host) {" +
" if (isPlainHostName(host) || dnsDomainIs(host, '.mozilla.org')) {" +
" return 'DIRECT';" +
" } else {" +
" return 'PROXY w3proxy.mozilla.org:8080; DIRECT';" +
" }" +
"}";
testPAC(EXAMPLE1, "http://test.example.com/", "PROXY w3proxy.mozilla.org:8080", "DIRECT");
testPAC(EXAMPLE1, "http://www.mozilla.org/", "DIRECT");
testPAC(EXAMPLE1, "http://foo.mozilla.org/", "DIRECT");
testPAC(EXAMPLE1, "http://localhost/", "DIRECT");
String EXAMPLE2 =
" function FindProxyForURL(url, host) {\n" +
" if ((isPlainHostName(host) || dnsDomainIs(host, '.mozilla.org')) &&\n" +
" !localHostOrDomainIs(host, 'www.mozilla.org') &&\n" +
" !localHostOrDomainIs(host, 'merchant.mozilla.org')) {\n" +
" return 'DIRECT';\n" +
" } else {\n" +
" return 'PROXY w3proxy.mozilla.org:8080; DIRECT';\n" +
" }\n" +
" }";
testPAC(EXAMPLE2, "http://test.example.com/", "PROXY w3proxy.mozilla.org:8080", "DIRECT");
testPAC(EXAMPLE2, "http://www.mozilla.org/", "PROXY w3proxy.mozilla.org:8080", "DIRECT");
testPAC(EXAMPLE2, "http://www/", "PROXY w3proxy.mozilla.org:8080", "DIRECT");
testPAC(EXAMPLE2, "http://foo.mozilla.org/", "DIRECT");
testPAC(EXAMPLE2, "http://localhost/", "DIRECT");
String EXAMPLE3A =
"function FindProxyForURL(url, host) {\n" +
" if (isResolvable(host)) return 'DIRECT';\n" +
" else return 'PROXY proxy.mydomain.com:8080';\n" +
"}";
testPAC(EXAMPLE3A, "http://www.mozilla.org/", "DIRECT");
testPAC(EXAMPLE3A, "http://doesnotexist.mozilla.org/", "PROXY proxy.mydomain.com:8080");
String EXAMPLE3B =
"function FindProxyForURL(url, host) {\n" +
" if (isPlainHostName(host) ||\n" +
" dnsDomainIs(host, '.mydomain.com') ||\n" +
" isResolvable(host)) {\n" +
" return 'DIRECT';\n" +
" } else {\n" +
" return 'PROXY proxy.mydomain.com:8080';\n" +
" }\n" +
"}";
testPAC(EXAMPLE3B, "http://plain/", "DIRECT");
testPAC(EXAMPLE3B, "http://foo.mydomain.com/", "DIRECT");
testPAC(EXAMPLE3B, "http://www.mozilla.org/", "DIRECT");
testPAC(EXAMPLE3B, "http://doesnotexist.mozilla.org/", "PROXY proxy.mydomain.com:8080");
// example 4
// function FindProxyForURL(url, host) {
// if (isInNet(host, '198.95.0.0', '255.255.0.0')) return 'DIRECT';
// else return 'PROXY proxy.mydomain.com:8080';
// }
// function FindProxyForURL(url, host) {
// if (isPlainHostName(host) ||
// dnsDomainIs(host, '.mydomain.com') ||
// isInNet(host, '198.95.0.0', '255.255.0.0')) {
// return 'DIRECT';
// } else {
// return 'PROXY proxy.mydomain.com:8080';
// }
// }
// example 5
// function FindProxyForURL(url, host) {
// if (isPlainHostName(host) || dnsDomainIs(host, '.mydomain.com')) return 'DIRECT';
// else if (shExpMatch(host, '*.com')) return 'PROXY proxy1.mydomain.com:8080; ' +
// 'PROXY proxy4.mydomain.com:8080';
// else if (shExpMatch(host, '*.edu')) return 'PROXY proxy2.mydomain.com:8080; ' +
// 'PROXY proxy4.mydomain.com:8080';
// else return 'PROXY proxy3.mydomain.com:8080; ' +
// 'PROXY proxy4.mydomain.com:8080';
// }
// example 6
// function FindProxyForURL(url, host) {
// if (url.substring(0, 5) == 'http:') {
// return 'PROXY http-proxy.mydomain.com:8080';
// }
// else if (url.substring(0, 4) == 'ftp:') {
// return 'PROXY ftp-proxy.mydomain.com:8080';
// }
// else if (url.substring(0, 7) == 'gopher:') {
// return 'PROXY gopher-proxy.mydomain.com:8080';
// }
// else if (url.substring(0, 6) == 'https:' ||
// url.substring(0, 6) == 'snews:') {
// return 'PROXY security-proxy.mydomain.com:8080';
// } else {
// return 'DIRECT';
// }
// }
String MYIP =
"function FindProxyForURL(url, host) {\n" +
" return 'PROXY ' + myIpAddress() + ':8080';\n" +
"}";
String myIp = InetAddress.getLocalHost().getHostAddress();
testPAC(MYIP, "http://testurl.com/", "PROXY " + myIp + ":8080");
}
}
+3 -3
View File
@@ -2,10 +2,10 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.threerings.getdown</groupId>
<groupId>net.affliction</groupId>
<artifactId>getdown</artifactId>
<packaging>pom</packaging>
<version>1.8.8</version>
<version>1.0.0</version>
<name>getdown</name>
<description>An application installer and updater.</description>
@@ -34,7 +34,7 @@
<connection>scm:git:git://github.com/threerings/getdown.git</connection>
<developerConnection>scm:git:git@github.com:threerings/getdown.git</developerConnection>
<url>https://github.com/threerings/getdown</url>
<tag>getdown-1.8.8</tag>
<tag>HEAD</tag>
</scm>
<properties>