From e6278363a80d51d7edea8d77938fd09d91dcc083 Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Thu, 26 Jun 2025 18:03:04 +1000 Subject: [PATCH] Claude Code fixes. StandardRB --- lib/moviehash.rb | 115 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 36 deletions(-) diff --git a/lib/moviehash.rb b/lib/moviehash.rb index eb64a4f..7a04e33 100644 --- a/lib/moviehash.rb +++ b/lib/moviehash.rb @@ -6,12 +6,35 @@ require "uri" module Moviehash class Error < StandardError; end + class FileNotFoundError < Error; end + class NetworkError < Error; end - CHUNK_SIZE = 64 * 1024 # in bytes + class InvalidInputError < Error; end + + DEFAULT_CHUNK_SIZE = 64 * 1024 # in bytes + DEFAULT_TIMEOUT = 30 # seconds + HASH_MASK = 0xffffffffffffffff + + class << self + attr_writer :chunk_size, :timeout + + def chunk_size + @chunk_size || DEFAULT_CHUNK_SIZE + end + + def timeout + @timeout || DEFAULT_TIMEOUT + end + + def configure + yield self if block_given? + end + end def self.compute_hash(url) + validate_input(url) data = url.start_with?("http") ? data_from_url(url) : data_from_file(url) hash = data[:filesize] @@ -23,73 +46,93 @@ module Moviehash end def self.data_from_file(path) - filesize = File.size(path) + raise FileNotFoundError, "File not found: #{path}" unless File.exist?(path) + raise FileNotFoundError, "Path is a directory: #{path}" if File.directory?(path) - data = { filesize: filesize, chunks: [] } + begin + filesize = File.size(path) + data = {filesize: filesize, chunks: []} - File.open(path, "rb") do |f| - data[:chunks] << f.read(CHUNK_SIZE) - f.seek([0, filesize - CHUNK_SIZE].max, IO::SEEK_SET) - data[:chunks] << f.read(CHUNK_SIZE) + File.open(path, "rb") do |f| + data[:chunks] << f.read(chunk_size) + f.seek([0, filesize - chunk_size].max, IO::SEEK_SET) + data[:chunks] << f.read(chunk_size) + end + + data + rescue Errno::EACCES + raise FileNotFoundError, "Permission denied: #{path}" + rescue Errno::ENOENT + raise FileNotFoundError, "File not found: #{path}" + rescue => e + raise Error, "Failed to read file #{path}: #{e.message}" end - - data end def self.data_from_url(url) uri = URI(url) + raise NetworkError, "Invalid URL scheme: #{uri.scheme}" unless %w[http https].include?(uri.scheme) + http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = (uri.scheme == "https") + http.read_timeout = timeout + http.open_timeout = timeout # Get the file size response = http.request_head(uri.path) - filesize = response["content-length"].to_i + raise NetworkError, "HTTP #{response.code}: #{response.message}" unless response.code == "200" - data = { filesize: filesize, chunks: [] } + filesize = response["content-length"]&.to_i + raise NetworkError, "Server did not provide content-length header" unless filesize && filesize > 0 + + data = {filesize: filesize, chunks: []} # Process the beginning of the file - response = http.get(uri.path, { "Range" => "bytes=0-#{CHUNK_SIZE - 1}" }) + response = http.get(uri.path, {"Range" => "bytes=0-#{chunk_size - 1}"}) + raise NetworkError, "Failed to fetch beginning chunk: HTTP #{response.code}" unless response.code.start_with?("2") data[:chunks] << response.body # Process the end of the file - start_byte = [0, filesize - CHUNK_SIZE].max - response = http.get(uri.path, { "Range" => "bytes=#{start_byte}-#{filesize - 1}" }) + start_byte = [0, filesize - chunk_size].max + response = http.get(uri.path, {"Range" => "bytes=#{start_byte}-#{filesize - 1}"}) + raise NetworkError, "Failed to fetch ending chunk: HTTP #{response.code}" unless response.code.start_with?("2") data[:chunks] << response.body data + rescue URI::InvalidURIError => e + raise NetworkError, "Invalid URL: #{e.message}" + rescue Net::OpenTimeout, Net::ReadTimeout => e + raise NetworkError, "Request timeout: #{e.message}" + rescue SocketError => e + raise NetworkError, "Network error: #{e.message}" + rescue => e + raise NetworkError, "Failed to fetch from URL: #{e.message}" end def self.process_chunk(chunk, hash) + return hash unless chunk + chunk.unpack("Q*").each do |n| - hash = hash + n & 0xffffffffffffffff + hash = hash + n & HASH_MASK end hash end -end -def self.old_compute_hash(path) - filesize = File.size(path) - hash = filesize + private - format("%016x", hash) + def self.validate_input(input) + raise InvalidInputError, "Input cannot be nil" if input.nil? + raise InvalidInputError, "Input cannot be empty" if input.empty? + raise InvalidInputError, "Input must be a string" unless input.is_a?(String) - # Read 64 kbytes, divide up into 64 bits and add each - # to hash. Do for beginning and end of file. - File.open(path, "rb") do |f| - # Q = unsigned long long = 64 bit - f.read(CHUNK_SIZE).unpack("Q*").each do |n| - hash = hash + n & 0xffffffffffffffff # to remain as 64 bit number + if input.start_with?("http") + begin + uri = URI(input) + raise InvalidInputError, "Invalid URL: missing host" unless uri.host + rescue URI::InvalidURIError => e + raise InvalidInputError, "Invalid URL: #{e.message}" + end end - format("%016x", hash) - f.seek([0, filesize - CHUNK_SIZE].max, IO::SEEK_SET) - - # And again for the end of the file - f.read(CHUNK_SIZE).unpack("Q*").each do |n| - hash = hash + n & 0xffffffffffffffff - end - format("%016x", hash) end - - format("%016x", hash) end