module SimpleCov::CLI::Serve

def announce(stdout, server, dir)

def announce(stdout, server, dir)
  port = server.addr[1]
  host = server.addr[3]
  stdout.puts("simplecov serve: serving #{dir} at http://#{host}:#{port}/")
  stdout.puts("Press Ctrl-C to stop.")
end

def drain_headers(client)

def drain_headers(client)
  loop { break if client.readline.strip.empty? }
end

def error(stderr, message)

def error(stderr, message)
  stderr.puts("simplecov serve: #{message}")
  1
end

def handle_connection(client, root)

can't crash the server.
writes a status response. Wide rescue so a misbehaving client
Reads one HTTP request line, drains headers, serves the file or
def handle_connection(client, root)
  method, path = client.readline.split
  drain_headers(client)
  return respond(client, 405) unless method == "GET"
  file = resolve(path, root)
  return respond(client, file == :forbidden ? 403 : 404) unless file.is_a?(String)
  respond(client, 200, File.binread(file), MIME[File.extname(file).downcase])
rescue StandardError
  # Misbehaving clients (truncated requests, connection resets,
  # invalid encoding) shouldn't take the whole server down.
  nil
ensure
  # simplecov:disable — `client` is the parameter, never nil here;
  # the `&.` is purely defensive in case of future refactors
  client&.close
  # simplecov:enable
end

def inside?(path, root)

def inside?(path, root)
  path == root || path.start_with?(root + File::SEPARATOR)
end

def parse(args)

def parse(args)
  opts = {port: 0, host: "127.0.0.1"} #: Hash[Symbol, untyped]
  OptionParser.new do |o|
    o.on("--port N", Integer) { |v| opts[:port] = v }
    o.on("--host HOST")       { |v| opts[:host] = v }
  end.parse(args)
  opts
end

def resolve(request_path, root)

nil for "not found".
a traversal attempt (including symlinks that escape root), or
Returns the absolute path of the file to serve, :forbidden for
def resolve(request_path, root)
  path = request_path.split("?", 2).first.to_s.sub(%r{^/}, "")
  absolute_root = File.realpath(root)
  candidate = File.expand_path(path.empty? ? "index.html" : path, absolute_root)
  # Reject `..` traversal and absolute-path attempts before
  # touching disk so they're 403, not 404.
  return :forbidden unless inside?(candidate, absolute_root)
  candidate = File.join(candidate, "index.html") if File.directory?(candidate)
  return nil unless File.file?(candidate)
  # Resolve symlinks last and re-check: a file inside root could
  # be a symlink pointing outside (e.g. /etc/passwd).
  real = File.realpath(candidate)
  inside?(real, absolute_root) ? real : :forbidden
rescue Errno::ENOENT
  # simplecov:disable — TOCTOU: candidate vanished between
  # File.file? and File.realpath. Treat as "not found".
  nil
  # simplecov:enable
end

def respond(client, status, body = "", content_type = "text/plain")

def respond(client, status, body = "", content_type = "text/plain")
  client.write("HTTP/1.1 #{status} #{STATUS_TEXT[status] || 'Error'}\r\n",
               "Content-Type: #{content_type || 'application/octet-stream'}\r\n",
               "Content-Length: #{body.bytesize}\r\n",
               "Connection: close\r\n\r\n")
  client.write(body)
end

def run(args, stdout:, stderr:, **)

def run(args, stdout:, stderr:, **)
  opts = parse(args)
  dir = SimpleCov::CLI.coverage_dir
  return error(stderr, "#{dir} doesn't exist; run your test suite first") unless File.directory?(dir)
  require "socket"
  with_server(opts) do |server|
    announce(stdout, server, dir)
    serve_loop(server, dir, stdout)
    0
  end
end

def serve_loop(server, dir, stdout)

def serve_loop(server, dir, stdout)
  loop { handle_connection(server.accept, dir) }
rescue Interrupt
  stdout.puts("\nsimplecov serve: stopping")
end

def with_server(opts)

def with_server(opts)
  # The receiver cast works around an rbs stdlib gap: TCPSocket's
  # explicit `self.new` shadows TCPServer#initialize's (host, port) form.
  server = (_ = TCPServer).new(opts[:host], opts[:port]) #: TCPServer
  begin
    yield server
  ensure
    server.close
  end
end