jwt-hack 2.6.0

Hack the JWT (JSON Web Token) - A tool for JWT security testing and token manipulation
Documentation
# Bumps the jwt-hack version across every file that hardcodes it
# (Cargo.toml, Cargo.lock, snap/snapcraft.yaml, aur/PKGBUILD).
# Prompts for the new version interactively and prints a per-file checkmark.
#
# Pre-release suffixes are allowed (e.g. 2.6.0-dev.1, 2.6.0-rc.1).

CARGO_TOML   = "Cargo.toml"
CARGO_LOCK   = "Cargo.lock"
SNAP_YAML    = "snap/snapcraft.yaml"
AUR_PKGBUILD = "aur/PKGBUILD"

# Docs and README hardcode the version too: the site version badge, the
# landing-page hero badge, the docker pull examples, and the server
# health-response sample. Each entry is {path, pattern, prefix}; the bare
# version is capture group 2, `v?` matches any leading `v` *outside* that group,
# and `prefix` ("v" or "") is what the writer re-attaches. Keep in lockstep
# with the identical list in version_check.cr.
DOCS_TARGETS = [
  {path: "docs/templates/header.html",
   pattern: /(class="version-badge">)v?([^<]+)/, prefix: "v"},
  {path: "docs/content/index.md",
   pattern: /(hero_badge\s*=\s*")v?([^"]+)/, prefix: "v"},
  {path: "docs/content/get_started/installation.md",
   pattern: /(docker pull hahwul\/jwt-hack:)v?([0-9][^\s]*)/, prefix: "v"},
  {path: "README.md",
   pattern: /(docker pull hahwul\/jwt-hack:)v?([0-9][^\s]*)/, prefix: "v"},
  {path: "docs/content/usage/commands/server.md",
   pattern: /("version":\s*")v?([^"]+)/, prefix: ""},
]

# Read helpers (mirror version_check.cr).

def cargo_toml_version : String?
  content = File.read(CARGO_TOML)
  pkg = content.match(/^\[package\][\s\S]*?(?=^\[|\z)/m)
  return nil unless pkg
  match = pkg[0].match(/^version\s*=\s*"([^"]+)"/m)
  match ? match[1] : nil
rescue
  nil
end

def cargo_lock_version : String?
  content = File.read(CARGO_LOCK)
  match = content.match(/name\s*=\s*"jwt-hack"\s*\nversion\s*=\s*"([^"]+)"/)
  match ? match[1] : nil
rescue
  nil
end

def snap_version : String?
  content = File.read(SNAP_YAML)
  match = content.match(/^version:\s*['"]?v?([^'"\s]+)['"]?\s*$/m)
  match ? match[1] : nil
rescue
  nil
end

def aur_version : String?
  content = File.read(AUR_PKGBUILD)
  match = content.match(/^pkgver=([^\s]+)/m)
  match ? match[1].gsub('_', '-') : nil
rescue
  nil
end

# Generic reader for the DOCS_TARGETS above: capture group 2 holds the bare
# version (the optional leading `v` is matched outside the group and dropped).
def docs_version(path : String, pattern : Regex) : String?
  match = File.read(path).match(pattern)
  match ? match[2] : nil
rescue
  nil
end

# Write helpers — surgical regex replace, only the package's own version
# line (Cargo.toml's [package] block, the jwt-hack entry in Cargo.lock).
# Other dependencies and lockfile entries are left alone.

def update_cargo_toml(new_version : String) : Bool
  content = File.read(CARGO_TOML)
  pkg_match = content.match(/^\[package\][\s\S]*?(?=^\[|\z)/m)
  return false unless pkg_match
  pkg_block = pkg_match[0]
  updated_pkg = pkg_block.sub(/^(version\s*=\s*")[^"]+(")/m, "\\1#{new_version}\\2")
  return false if updated_pkg == pkg_block
  File.write(CARGO_TOML, content.sub(pkg_block, updated_pkg))
  true
rescue ex
  puts "  error: #{ex.message}"
  false
end

def update_cargo_lock(new_version : String) : Bool
  content = File.read(CARGO_LOCK)
  updated = content.sub(
    /(name\s*=\s*"jwt-hack"\s*\nversion\s*=\s*")[^"]+(")/,
    "\\1#{new_version}\\2",
  )
  return false if updated == content
  File.write(CARGO_LOCK, updated)
  true
rescue ex
  puts "  error: #{ex.message}"
  false
end

# snap convention here keeps the `v` prefix (matches release tags).
# NOTE: Crystal's `/m` flag is MULTILINE *and* DOTALL, so `.` matches
# newlines. Match the value with `[^\n]*` to stay on a single line —
# `.*` would swallow the rest of the file.
def update_snap(new_version : String) : Bool
  content = File.read(SNAP_YAML)
  updated = content.sub(/^(version:[ \t]*)[^\n]*/m, "\\1v#{new_version}")
  return false if updated == content
  File.write(SNAP_YAML, updated)
  true
rescue ex
  puts "  error: #{ex.message}"
  false
end

# AUR pkgver disallows hyphens; rewrite `-` as `_` so dev/rc tags remain
# valid for `makepkg --printsrcinfo`. Also resets pkgrel=1 on bump.
# `[^\n]*` (not `.*`) — see the DOTALL note on update_snap above.
def update_aur(new_version : String) : Bool
  content = File.read(AUR_PKGBUILD)
  aur_ver = new_version.gsub('-', '_')
  updated = content.sub(/^pkgver=[^\n]*/m, "pkgver=#{aur_ver}")
                   .sub(/^pkgrel=[^\n]*/m, "pkgrel=1")
  return false if updated == content
  File.write(AUR_PKGBUILD, updated)
  true
rescue ex
  puts "  error: #{ex.message}"
  false
end

# Generic writer for the DOCS_TARGETS: replace the matched version with
# `prefix + new_version`, keeping the surrounding context (capture group 1).
# `sub` touches only the first match, and each pattern is unique per file.
def update_docs(path : String, pattern : Regex, prefix : String, new_version : String) : Bool
  content = File.read(path)
  updated = content.sub(pattern, "\\1#{prefix}#{new_version}")
  return false if updated == content
  File.write(path, updated)
  true
rescue ex
  puts "  error: #{ex.message}"
  false
end

# Loose semver — allow numeric pre-release suffix (`-dev.1`, `-rc.2`,
# `-alpha`).
def valid_version?(version : String) : Bool
  !!(version =~ /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.]+)?$/)
end

# Status report.

# Every version-bearing file as {path, current_version, writer}. The writer
# takes the new version as an argument (rather than closing over it) so the
# list can be built up front, before the prompt. `current` is nil for any file
# that doesn't carry a version, which the loop below skips.
targets = [
  {CARGO_TOML, cargo_toml_version, ->(v : String) { update_cargo_toml(v) }},
  {CARGO_LOCK, cargo_lock_version, ->(v : String) { update_cargo_lock(v) }},
  {SNAP_YAML, snap_version, ->(v : String) { update_snap(v) }},
  {AUR_PKGBUILD, aur_version, ->(v : String) { update_aur(v) }},
] of Tuple(String, String?, Proc(String, Bool))
DOCS_TARGETS.each do |t|
  path, pattern, prefix = t[:path], t[:pattern], t[:prefix]
  targets << {path, docs_version(path, pattern), ->(v : String) { update_docs(path, pattern, prefix, v) }}
end

width = targets.map { |path, _, _| path.size }.max

puts "Current versions:"
targets.each do |path, ver, _|
  puts "  #{path.ljust(width)} #{ver || "Not found"}"
end
puts

versions = targets.map { |_, ver, _| ver }.compact
unique = versions.uniq

if unique.size > 1
  puts "Warning: versions disagree (#{unique.join(", ")})"
  puts
end

current = versions.first? || "unknown"
puts "Current: #{current}"
print "New version (Enter to cancel): "
input = gets
new_version = input.try(&.strip) || ""

if new_version.empty?
  puts "Cancelled."
  exit 0
end

unless valid_version?(new_version)
  puts "Invalid version: #{new_version} (expected X.Y.Z or X.Y.Z-suffix)"
  exit 1
end

if unique == [new_version]
  puts "No change."
  exit 0
end

puts
puts "Updating to #{new_version}..."

ok = 0
total = 0
changed = 0

targets.each do |path, ver, fn|
  next if ver.nil? # file doesn't carry a version — nothing to update
  total += 1
  print "  #{path.ljust(width)} "
  if ver == new_version
    puts "unchanged"
    ok += 1
  elsif fn.call(new_version)
    puts "ok"
    ok += 1
    changed += 1
  else
    puts "FAIL"
  end
end

puts
if ok == total
  puts "#{changed} updated, #{total - changed} already at #{new_version}."
else
  puts "Updated #{ok}/#{total} files; #{total - ok} failed."
  exit 1
end