adept_geoip 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/geoip.gemspec ADDED
@@ -0,0 +1,57 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "geoip"
8
+ s.version = "1.1.2"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Clifford Heath", "Roland Moriz"]
12
+ s.date = "2012-01-26"
13
+ s.description = "GeoIP searches a GeoIP database for a given host or IP address, and\nreturns information about the country where the IP address is allocated,\nand the city, ISP and other information, if you have that database version."
14
+ s.email = ["clifford.heath@gmail.com", "rmoriz@gmail.com"]
15
+ s.executables = ["geoip"]
16
+ s.extra_rdoc_files = [
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ "History.rdoc",
21
+ "README.rdoc",
22
+ "Rakefile",
23
+ "bin/geoip",
24
+ "config/website.yml",
25
+ "data/geoip/country_code.yml",
26
+ "data/geoip/country_code3.yml",
27
+ "data/geoip/country_continent.yml",
28
+ "data/geoip/country_name.yml",
29
+ "data/geoip/time_zone.yml",
30
+ "geoip.gemspec",
31
+ "lib/geoip.rb",
32
+ "script/destroy",
33
+ "script/generate",
34
+ "script/txt2html",
35
+ "test/test_geoip.rb",
36
+ "test/test_helper.rb",
37
+ "website/index.txt",
38
+ "website/javascripts/rounded_corners_lite.inc.js",
39
+ "website/stylesheets/screen.css",
40
+ "website/template.rhtml"
41
+ ]
42
+ s.homepage = "http://github.com/cjheath/geoip"
43
+ s.licenses = ["MIT"]
44
+ s.require_paths = ["lib"]
45
+ s.rubygems_version = "1.8.10"
46
+ s.summary = "GeoIP searches a GeoIP database for a given host or IP address, and returns information about the country where the IP address is allocated, and the city, ISP and other information, if you have that database version."
47
+
48
+ if s.respond_to? :specification_version then
49
+ s.specification_version = 3
50
+
51
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
52
+ else
53
+ end
54
+ else
55
+ end
56
+ end
57
+
data/lib/geoip.rb ADDED
@@ -0,0 +1,563 @@
1
+ #
2
+ # Native Ruby reader for the GeoIP database
3
+ # Lookup the country where IP address is allocated
4
+ #
5
+ # = COPYRIGHT
6
+ #
7
+ # This version Copyright (C) 2005 Clifford Heath
8
+ # Derived from the C version, Copyright (C) 2003 MaxMind LLC
9
+ #
10
+ # This library is free software; you can redistribute it and/or
11
+ # modify it under the terms of the GNU General Public
12
+ # License as published by the Free Software Foundation; either
13
+ # version 2.1 of the License, or (at your option) any later version.
14
+ #
15
+ # This library is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18
+ # General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU General Public
21
+ # License along with this library; if not, write to the Free Software
22
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23
+ #
24
+ # = SYNOPSIS
25
+ #
26
+ # require 'geoip'
27
+ # p GeoIP.new('/usr/share/GeoIP/GeoIP.dat').country("www.netscape.sk")
28
+ #
29
+ # = DESCRIPTION
30
+ #
31
+ # GeoIP searches a GeoIP database for a given host or IP address, and
32
+ # returns information about the country where the IP address is allocated.
33
+ #
34
+ # = PREREQUISITES
35
+ #
36
+ # You need at least the free GeoIP.dat, for which the last known download
37
+ # location is <http://www.maxmind.com/download/geoip/database/GeoIP.dat.gz>
38
+ # This API requires the file to be decompressed for searching. Other versions
39
+ # of this database are available for purchase which contain more detailed
40
+ # information, but this information is not returned by this implementation.
41
+ # See www.maxmind.com for more information.
42
+ #
43
+
44
+ require 'thread' # Needed for Mutex
45
+ require 'socket'
46
+ begin
47
+ require 'io/extra' # for IO.pread
48
+ rescue LoadError
49
+ # oh well, hope they're not forking after initializing
50
+ end
51
+
52
+ require 'yaml'
53
+
54
+ class GeoIP
55
+
56
+ # The GeoIP GEM version number
57
+ VERSION = "1.1.2"
58
+
59
+ # The +data/+ directory for geoip
60
+ DATA_DIR = File.expand_path(File.join(File.dirname(__FILE__),'..','data','geoip'))
61
+
62
+ # Ordered list of the ISO3166 2-character country codes, ordered by
63
+ # GeoIP ID
64
+ CountryCode = YAML.load_file(File.join(DATA_DIR,'country_code.yml'))
65
+
66
+ # Ordered list of the ISO3166 3-character country codes, ordered by
67
+ # GeoIP ID
68
+ CountryCode3 = YAML.load_file(File.join(DATA_DIR,'country_code3.yml'))
69
+
70
+ # Ordered list of the English names of the countries, ordered by GeoIP ID
71
+ CountryName = YAML.load_file(File.join(DATA_DIR,'country_name.yml'))
72
+
73
+ # Ordered list of the ISO3166 2-character continent code of the countries,
74
+ # ordered by GeoIP ID
75
+ CountryContinent = YAML.load_file(File.join(DATA_DIR,'country_continent.yml'))
76
+
77
+ # Hash of the timezone codes mapped to timezone name, per zoneinfo
78
+ TimeZone = YAML.load_file(File.join(DATA_DIR,'time_zone.yml'))
79
+
80
+ GEOIP_COUNTRY_EDITION = 1
81
+ GEOIP_CITY_EDITION_REV1 = 2
82
+ GEOIP_REGION_EDITION_REV1 = 3
83
+ GEOIP_ISP_EDITION = 4
84
+ GEOIP_ORG_EDITION = 5
85
+ GEOIP_CITY_EDITION_REV0 = 6
86
+ GEOIP_REGION_EDITION_REV0 = 7
87
+ GEOIP_PROXY_EDITION = 8
88
+ GEOIP_ASNUM_EDITION = 9
89
+ GEOIP_NETSPEED_EDITION = 10
90
+
91
+ COUNTRY_BEGIN = 16776960 #:nodoc:
92
+ STATE_BEGIN_REV0 = 16700000 #:nodoc:
93
+ STATE_BEGIN_REV1 = 16000000 #:nodoc:
94
+ STRUCTURE_INFO_MAX_SIZE = 20 #:nodoc:
95
+ DATABASE_INFO_MAX_SIZE = 100 #:nodoc:
96
+ MAX_ORG_RECORD_LENGTH = 300 #:nodoc:
97
+ MAX_ASN_RECORD_LENGTH = 300 #:nodoc: unverified
98
+ US_OFFSET = 1 #:nodoc:
99
+ CANADA_OFFSET = 677 #:nodoc:
100
+ WORLD_OFFSET = 1353 #:nodoc:
101
+ FIPS_RANGE = 360 #:nodoc:
102
+ FULL_RECORD_LENGTH = 50 #:nodoc:
103
+
104
+ STANDARD_RECORD_LENGTH = 3 #:nodoc:
105
+ SEGMENT_RECORD_LENGTH = 3 #:nodoc:
106
+
107
+ class Country < Struct.new(:request, :ip, :country_code, :country_code2, :country_code3, :country_name, :continent_code)
108
+
109
+ def to_hash
110
+ Hash[each_pair.to_a]
111
+ end
112
+
113
+ end
114
+
115
+ class City < Struct.new(:request, :ip, :country_code2, :country_code3, :country_name, :continent_code,
116
+ :region_name, :city_name, :postal_code, :latitude, :longitude, :dma_code, :area_code, :timezone)
117
+
118
+ def to_hash
119
+ Hash[each_pair.to_a]
120
+ end
121
+
122
+ end
123
+
124
+ class ASN < Struct.new(:number, :asn)
125
+
126
+ alias as_num number
127
+
128
+ end
129
+
130
+ # The Edition number that identifies which kind of database you've opened
131
+ attr_reader :database_type
132
+
133
+ alias databaseType database_type
134
+
135
+ # Open the GeoIP database and determine the file format version.
136
+ #
137
+ # +filename+ is a String holding the path to the GeoIP.dat file
138
+ # +options+ is an integer holding caching flags (unimplemented)
139
+ #
140
+ def initialize(filename, flags = 0)
141
+ @mutex = unless IO.respond_to?(:pread)
142
+ Mutex.new
143
+ end
144
+
145
+ @flags = flags
146
+ @database_type = GEOIP_COUNTRY_EDITION
147
+ @record_length = STANDARD_RECORD_LENGTH
148
+ @file = File.open(filename, 'rb')
149
+
150
+ detect_database_type!
151
+ end
152
+
153
+ # Search the GeoIP database for the specified host, returning country
154
+ # info.
155
+ #
156
+ # +hostname+ is a String holding the host's DNS name or numeric IP
157
+ # address.
158
+ #
159
+ # If the database is a City database (normal), return the result that
160
+ # +city+ would return.
161
+ #
162
+ # Otherwise, return a Country object with the seven elements:
163
+ # * The host or IP address string as requested
164
+ # * The IP address string after looking up the host
165
+ # * The GeoIP country-ID as an integer (N.B. this is excluded from the
166
+ # city results!)
167
+ # * The two-character country code (ISO 3166-1 alpha-2)
168
+ # * The three-character country code (ISO 3166-2 alpha-3)
169
+ # * The ISO 3166 English-language name of the country
170
+ # * The two-character continent code
171
+ #
172
+ def country(hostname)
173
+ if (@database_type == GEOIP_CITY_EDITION_REV0 ||
174
+ @database_type == GEOIP_CITY_EDITION_REV1)
175
+ return city(hostname)
176
+ end
177
+
178
+ ip = lookup_ip(hostname)
179
+
180
+ # Convert numeric IP address to an integer
181
+ ipnum = iptonum(ip)
182
+
183
+ if (@database_type != GEOIP_COUNTRY_EDITION &&
184
+ @database_type != GEOIP_PROXY_EDITION &&
185
+ @database_type != GEOIP_NETSPEED_EDITION)
186
+ throw "Invalid GeoIP database type, can't look up Country by IP"
187
+ end
188
+
189
+ code = (seek_record(ipnum) - COUNTRY_BEGIN)
190
+
191
+ Country.new(
192
+ hostname, # Requested hostname
193
+ ip, # Ip address as dotted quad
194
+ code, # GeoIP's country code
195
+ CountryCode[code], # ISO3166-1 alpha-2 code
196
+ CountryCode3[code], # ISO3166-2 alpha-3 code
197
+ CountryName[code], # Country name, per ISO 3166
198
+ CountryContinent[code] # Continent code.
199
+ )
200
+ end
201
+
202
+ # Search the GeoIP database for the specified host, returning city info.
203
+ #
204
+ # +hostname+ is a String holding the host's DNS name or numeric IP
205
+ # address.
206
+ #
207
+ # Returns a City object with the fourteen elements:
208
+ # * The host or IP address string as requested
209
+ # * The IP address string after looking up the host
210
+ # * The two-character country code (ISO 3166-1 alpha-2)
211
+ # * The three-character country code (ISO 3166-2 alpha-3)
212
+ # * The ISO 3166 English-language name of the country
213
+ # * The two-character continent code
214
+ # * The region name (state or territory)
215
+ # * The city name
216
+ # * The postal code (zipcode)
217
+ # * The latitude
218
+ # * The longitude
219
+ # * The USA dma_code if known (only REV1 City database)
220
+ # * The USA area_code if known (only REV1 City database)
221
+ # * The timezone name, if known
222
+ #
223
+ def city(hostname)
224
+ ip = lookup_ip(hostname)
225
+
226
+ # Convert numeric IP address to an integer
227
+ ipnum = iptonum(ip)
228
+
229
+ if (@database_type != GEOIP_CITY_EDITION_REV0 &&
230
+ @database_type != GEOIP_CITY_EDITION_REV1)
231
+ throw "Invalid GeoIP database type, can't look up City by IP"
232
+ end
233
+
234
+ pos = seek_record(ipnum)
235
+
236
+ # This next statement was added to MaxMind's C version after it was
237
+ # rewritten in Ruby. It prevents unassigned IP addresses from returning
238
+ # bogus data. There was concern over whether the changes to an
239
+ # application's behaviour were always correct, but this has been tested
240
+ # using an exhaustive search of the top 16 bits of the IP address space.
241
+ # The records where the change takes effect contained *no* valid data.
242
+ # If you're concerned, email me, and I'll send you the test program so
243
+ # you can test whatever IP range you think is causing problems,
244
+ # as I don't care to undertake an exhaustive search of the 32-bit space.
245
+ unless pos == @database_segments[0]
246
+ read_city(pos, hostname, ip)
247
+ end
248
+ end
249
+
250
+ # Search a ISP GeoIP database for the specified host, returning the ISP
251
+ # Not all GeoIP databases contain ISP information.
252
+ # Check http://maxmind.com
253
+ #
254
+ # +hostname+ is a String holding the host's DNS name or numeric IP
255
+ # address.
256
+ #
257
+ # Returns the ISP name.
258
+ #
259
+ def isp(hostname)
260
+ ip = lookup_ip(hostname)
261
+
262
+ # Convert numeric IP address to an integer
263
+ ipnum = iptonum(ip)
264
+
265
+ if (@database_type != GEOIP_ISP_EDITION &&
266
+ @database_type != GEOIP_ORG_EDITION)
267
+ throw "Invalid GeoIP database type, can't look up Organization/ISP by IP"
268
+ end
269
+
270
+ pos = seek_record(ipnum)
271
+ off = pos + (2*@record_length - 1) * @database_segments[0]
272
+
273
+ record = atomic_read(MAX_ORG_RECORD_LENGTH, off)
274
+ record = record.sub(/\000.*/n, '')
275
+ record
276
+ end
277
+
278
+ # Search a ASN GeoIP database for the specified host, returning the AS
279
+ # number and description.
280
+ #
281
+ # +hostname+ is a String holding the host's DNS name or numeric
282
+ # IP address.
283
+ #
284
+ # Returns the AS number and description.
285
+ #
286
+ # Source:
287
+ # http://geolite.maxmind.com/download/geoip/database/asnum/GeoIPASNum.dat.gz
288
+ #
289
+ def asn(hostname)
290
+ ip = lookup_ip(hostname)
291
+
292
+ # Convert numeric IP address to an integer
293
+ ipnum = iptonum(ip)
294
+
295
+ if (@database_type != GEOIP_ASNUM_EDITION)
296
+ throw "Invalid GeoIP database type, can't look up ASN by IP"
297
+ end
298
+
299
+ pos = seek_record(ipnum)
300
+ off = pos + (2*@record_length - 1) * @database_segments[0]
301
+
302
+ record = atomic_read(MAX_ASN_RECORD_LENGTH, off)
303
+ record = record.sub(/\000.*/n, '')
304
+
305
+ # AS####, Description
306
+ ASN.new($1, $2) if record =~ /^(AS\d+)\s(.*)$/
307
+ end
308
+
309
+ # Search a ISP GeoIP database for the specified host, returning the
310
+ # organization.
311
+ #
312
+ # +hostname+ is a String holding the host's DNS name or numeric
313
+ # IP address.
314
+ #
315
+ # Returns the organization associated with it.
316
+ #
317
+ alias_method(:organization, :isp) # Untested, according to Maxmind docs this should work
318
+
319
+ # Iterate through a GeoIP city database
320
+ def each
321
+ return enum_for unless block_given?
322
+
323
+ if (@database_type != GEOIP_CITY_EDITION_REV0 &&
324
+ @database_type != GEOIP_CITY_EDITION_REV1)
325
+ throw "Invalid GeoIP database type, can't iterate thru non-City database"
326
+ end
327
+
328
+ @iter_pos = @database_segments[0] + 1
329
+ num = 0
330
+
331
+ until ((rec = read_city(@iter_pos)).nil?)
332
+ yield rec
333
+ print "#{num}: #{@iter_pos}\n" if((num += 1) % 1000 == 0)
334
+ end
335
+
336
+ @iter_pos = nil
337
+ return self
338
+ end
339
+
340
+ private
341
+
342
+ # Detects the type of the database.
343
+ def detect_database_type! # :nodoc:
344
+ @file.seek(-3, IO::SEEK_END)
345
+
346
+ 0.upto(STRUCTURE_INFO_MAX_SIZE - 1) do |i|
347
+ if @file.read(3).bytes.all? { |byte| byte == 255 }
348
+ @database_type = if @file.respond_to?(:getbyte)
349
+ @file.getbyte
350
+ else
351
+ @file.getc
352
+ end
353
+
354
+ @database_type -= 105 if @database_type >= 106
355
+
356
+ if (@database_type == GEOIP_REGION_EDITION_REV0)
357
+ # Region Edition, pre June 2003
358
+ @database_segments = [STATE_BEGIN_REV0]
359
+ elsif (@database_type == GEOIP_REGION_EDITION_REV1)
360
+ # Region Edition, post June 2003
361
+ @database_segments = [STATE_BEGIN_REV1]
362
+ elsif (@database_type == GEOIP_CITY_EDITION_REV0 ||
363
+ @database_type == GEOIP_CITY_EDITION_REV1 ||
364
+ @database_type == GEOIP_ORG_EDITION ||
365
+ @database_type == GEOIP_ISP_EDITION ||
366
+ @database_type == GEOIP_ASNUM_EDITION)
367
+
368
+ # City/Org Editions have two segments, read offset of second segment
369
+ @database_segments = [0]
370
+ sr = @file.read(3).unpack("C*")
371
+ @database_segments[0] += le_to_ui(sr)
372
+
373
+ if (@database_type == GEOIP_ORG_EDITION ||
374
+ @database_type == GEOIP_ISP_EDITION)
375
+ @record_length = 4
376
+ end
377
+ end
378
+
379
+ break
380
+ else
381
+ @file.seek(-4, IO::SEEK_CUR)
382
+ end
383
+ end
384
+
385
+ if (@database_type == GEOIP_COUNTRY_EDITION ||
386
+ @database_type == GEOIP_PROXY_EDITION ||
387
+ @database_type == GEOIP_NETSPEED_EDITION)
388
+ @database_segments = [COUNTRY_BEGIN]
389
+ end
390
+ end
391
+
392
+ # Search the GeoIP database for the specified host, returning city info.
393
+ #
394
+ # +hostname+ is a String holding the host's DNS name or numeric
395
+ # IP address.
396
+ #
397
+ # Returns an array of fourteen elements:
398
+ # * All elements from the country query (except GeoIP's country code,
399
+ # bah!)
400
+ # * The region (state or territory) name
401
+ # * The city name
402
+ # * The postal code (zipcode)
403
+ # * The latitude
404
+ # * The longitude
405
+ # * The dma_code and area_code, if available (REV1 City database)
406
+ # * The timezone name, if known
407
+ #
408
+ def read_city(pos, hostname = '', ip = '') #:nodoc:
409
+ off = pos + (2*@record_length - 1) * @database_segments[0]
410
+ record = atomic_read(FULL_RECORD_LENGTH, off)
411
+
412
+ return unless (record && record.size == FULL_RECORD_LENGTH)
413
+
414
+ # The country code is the first byte:
415
+ code = record[0]
416
+ code = code.ord if code.respond_to?(:ord)
417
+ record = record[1..-1]
418
+ @iter_pos += 1 unless @iter_pos.nil?
419
+
420
+ spl = record.split("\x00", 4)
421
+ # Get the region:
422
+ region = spl[0]
423
+ @iter_pos += (region.size + 1) unless @iter_pos.nil?
424
+
425
+ # Get the city:
426
+ city = spl[1]
427
+ @iter_pos += (city.size + 1) unless @iter_pos.nil?
428
+ # set the correct encoding in ruby 1.9 compatible environments:
429
+ city.force_encoding('iso-8859-1') if city.respond_to?(:force_encoding)
430
+
431
+ # Get the postal code:
432
+ postal_code = spl[2]
433
+ @iter_pos += (postal_code.size + 1) unless @iter_pos.nil?
434
+
435
+ record = spl[3]
436
+
437
+ # Get the latitude/longitude:
438
+ if (record && record[0,3])
439
+ latitude = (le_to_ui(record[0,3].unpack('C*')) / 10000.0) - 180
440
+ record = record[3..-1]
441
+
442
+ @iter_pos += 3 unless @iter_pos.nil?
443
+ else
444
+ latitude = ''
445
+ end
446
+
447
+ if (record && record[0,3])
448
+ longitude = le_to_ui(record[0,3].unpack('C*')) / 10000.0 - 180
449
+ record = record[3..-1]
450
+
451
+ @iter_pos += 3 unless @iter_pos.nil?
452
+ else
453
+ longitude = ''
454
+ end
455
+
456
+ # UNTESTED
457
+ if (record &&
458
+ record[0,3] &&
459
+ @database_type == GEOIP_CITY_EDITION_REV1 &&
460
+ CountryCode[code] == "US")
461
+
462
+ dmaarea_combo = le_to_ui(record[0,3].unpack('C*'))
463
+ dma_code = (dmaarea_combo / 1000)
464
+ area_code = (dmaarea_combo % 1000)
465
+
466
+ @iter_pos += 3 unless @iter_pos.nil?
467
+ else
468
+ dma_code, area_code = nil, nil
469
+ end
470
+
471
+ City.new(
472
+ hostname, # Requested hostname
473
+ ip, # Ip address as dotted quad
474
+ CountryCode[code], # ISO3166-1 code
475
+ CountryCode3[code], # ISO3166-2 code
476
+ CountryName[code], # Country name, per IS03166
477
+ CountryContinent[code], # Continent code.
478
+ region, # Region name
479
+ city, # City name
480
+ postal_code, # Postal code
481
+ latitude,
482
+ longitude,
483
+ dma_code,
484
+ area_code,
485
+ (TimeZone["#{CountryCode[code]}#{region}"] || TimeZone["#{CountryCode[code]}"])
486
+ )
487
+ end
488
+
489
+ def lookup_ip(ip_or_hostname) # :nodoc:
490
+ return ip_or_hostname unless (ip_or_hostname.kind_of?(String) && ip_or_hostname !~ /^[0-9.]+$/)
491
+
492
+ # Lookup IP address, we were given a name
493
+ ip = IPSocket.getaddress(ip_or_hostname)
494
+ ip = '0.0.0.0' if ip == '::1'
495
+ ip
496
+ end
497
+
498
+ # Convert numeric IP address to Integer.
499
+ def iptonum(ip) #:nodoc:
500
+ if (ip.kind_of?(String) &&
501
+ ip =~ /^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/)
502
+ ip = be_to_ui(Regexp.last_match().to_a.slice(1..4))
503
+ else
504
+ ip = ip.to_i
505
+ end
506
+
507
+ return ip
508
+ end
509
+
510
+ def seek_record(ipnum) #:nodoc:
511
+ # Binary search in the file.
512
+ # Records are pairs of little-endian integers, each of @record_length.
513
+ offset = 0
514
+ mask = 0x80000000
515
+
516
+ 31.downto(0) do |depth|
517
+ off = (@record_length * 2 * offset)
518
+ buf = atomic_read(@record_length * 2, off)
519
+
520
+ buf.slice!(0...@record_length) if ((ipnum & mask) != 0)
521
+ offset = le_to_ui(buf[0...@record_length].unpack("C*"))
522
+
523
+ if (offset >= @database_segments[0])
524
+ return offset
525
+ end
526
+
527
+ mask >>= 1
528
+ end
529
+ end
530
+
531
+ # Convert a big-endian array of numeric bytes to unsigned int.
532
+ #
533
+ # Returns the unsigned Integer.
534
+ #
535
+ def be_to_ui(s) #:nodoc:
536
+ i = 0
537
+
538
+ s.each { |b| i = ((i << 8) | (b.to_i & 0x0ff)) }
539
+ return i
540
+ end
541
+
542
+ # Same for little-endian
543
+ def le_to_ui(s) #:nodoc:
544
+ be_to_ui(s.reverse)
545
+ end
546
+
547
+ # reads +length+ bytes from +offset+ as atomically as possible
548
+ # if IO.pread is available, it'll use that (making it both multithread
549
+ # and multiprocess-safe). Otherwise we'll use a mutex to synchronize
550
+ # access (only providing protection against multiple threads, but not
551
+ # file descriptors shared across multiple processes).
552
+ def atomic_read(length, offset) #:nodoc:
553
+ if @mutex
554
+ @mutex.synchronize do
555
+ @file.seek(offset)
556
+ @file.read(length)
557
+ end
558
+ else
559
+ IO.pread(@file.fileno, length, offset)
560
+ end
561
+ end
562
+
563
+ end