This is the code I used to generate the quiz example. To give credit
where credit is due though, it was heavily inspired from some code
Allan Odgaard shared with me:
#!/usr/bin/env ruby -wKU
require “open-uri”
require “zlib”
begin
require “rubygems”
rescue LoadError
load without gems
end
begin
require “faster_csv”
FCSV.build_csv_interface
rescue LoadError
require “csv”
end
class IP
def initialize(address)
@address_chunks = address.split(“.”).map { |n| Integer(n) }
raise AgumentError, “Malformed IP” unless @address_chunks.size == 4
end
def to_i
@address_chunks.inject { |result, chunk| result * 256 + chunk }
end
STRING_SIZE = new(“255.255.255.255”).to_i.to_s.size
def to_s
“%#{STRING_SIZE}s” % to_i
end
end
class IPToCountryDB
REMOTE = “software77.net?
action=download”
LOCAL = “ip_to_country.txt”
RECORD_SIZE = IP::STRING_SIZE * 2 + 5
def self.build(path = LOCAL)
open(path, “w”) do |db|
open(REMOTE) do |url|
csv = Zlib::GzipReader.new(url)
last_range = Array.new
csv.each do |line|
next if line =~ /\A\s*(?:#|\z)/
from, to, country = CSV.parse_line(line).values_at(0..1, 4).
map { |f| Integer(f) rescue f }
if last_range[2] == country and last_range[1] + 1 == from
last_range[1] = to
else
build_line(db, last_range)
last_range = [from, to, country]
end
end
build_line(db, last_range)
end
end
end
def self.build_line(db, fields)
return if fields.empty?
db.printf(“%#{IP::STRING_SIZE}s\t%#{IP::STRING_SIZE}s\t%s\n”,
*fields)
end
private_class_method :build_line
def initialize(path = LOCAL)
begin
@db = open(path)
rescue Errno::ENOENT
self.class.build(path)
retry
end
end
def search(address)
binary_search(IP.new(address).to_i)
end
private
def binary_search(ip, min = 0, max = @db.stat.size / RECORD_SIZE)
return “Unknown” if min == max
middle = (min + max) / 2
@db.seek(RECORD_SIZE * middle, IO::SEEK_SET)
if @db.read(RECORD_SIZE) =~ /\A *(\d+)\t *(\d+)\t([A-Z]{2})\n\z/
if ip < $1.to_i then binary_search(ip, min, middle)
elsif ip > $2.to_i then binary_search(ip, middle + 1, max)
else $3
end
else
raise "Malformed database at offset #{RECORD_SIZE * middle}"
end
end
end
if FILE == $PROGRAM_NAME
require “optparse”
options = {:db => IPToCountryDB::LOCAL, :rebuild => false}
ARGV.options do |opts|
opts.banner = “Usage:\n” +
" #{File.basename($PROGRAM_NAME)} [-d PATH] IP\n" +
" #{File.basename($PROGRAM_NAME)} [-d PATH] -r"
opts.separator ""
opts.separator "Specific Options:"
opts.on("-d", "--db PATH", String, "The path to database file")
do |path|
options[:db] = path
end
opts.on(“-r”, “–rebuild”, “Rebuild the database”) do
options[:rebuild] = true
end
opts.separator "Common Options:"
opts.on("-h", "--help", "Show this message.") do
puts opts
exit
end
begin
opts.parse!
raise "No IP address given" unless options[:rebuild] or
ARGV.size == 1
rescue
puts opts
exit
end
end
if options[:rebuild]
IPToCountryDB.build(options[:db])
else
puts IPToCountryDB.new(options[:db]).search(ARGV.shift)
end
end
END
James Edward G. II