ip_filter 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/CHANGELOG +29 -0
  5. data/Gemfile.lock +117 -0
  6. data/LICENSE +20 -0
  7. data/README.rdoc +232 -0
  8. data/Rakefile +6 -0
  9. data/data/geoip/country_code.yml +255 -0
  10. data/data/geoip/country_code3.yml +255 -0
  11. data/data/geoip/country_continent.yml +255 -0
  12. data/data/geoip/country_name.yml +255 -0
  13. data/data/geoip/time_zone.yml +677 -0
  14. data/lib/geoip.rb +559 -0
  15. data/lib/ip_filter.rb +100 -0
  16. data/lib/ip_filter/cache.rb +30 -0
  17. data/lib/ip_filter/cache/dallistore.rb +39 -0
  18. data/lib/ip_filter/cache/redis.rb +26 -0
  19. data/lib/ip_filter/configuration.rb +47 -0
  20. data/lib/ip_filter/controller/geo_ip_lookup.rb +78 -0
  21. data/lib/ip_filter/lookups/base.rb +60 -0
  22. data/lib/ip_filter/lookups/geoip.rb +41 -0
  23. data/lib/ip_filter/providers/max_mind.rb +52 -0
  24. data/lib/ip_filter/providers/s3.rb +51 -0
  25. data/lib/ip_filter/railtie.rb +23 -0
  26. data/lib/ip_filter/request.rb +14 -0
  27. data/lib/ip_filter/results/base.rb +39 -0
  28. data/lib/ip_filter/results/geoip.rb +19 -0
  29. data/lib/ip_filter/version.rb +3 -0
  30. data/spec/cache/dallistore_spec.rb +16 -0
  31. data/spec/cache/redis_spec.rb +56 -0
  32. data/spec/controller/ip_controller_spec.rb +56 -0
  33. data/spec/fixtures/GeoIP.dat +0 -0
  34. data/spec/fixtures/LICENSE.txt +31 -0
  35. data/spec/fixtures/country.dat +0 -0
  36. data/spec/ip_filter_spec.rb +19 -0
  37. data/spec/providers/max_mind_spec.rb +11 -0
  38. data/spec/providers/s3_spec.rb +11 -0
  39. data/spec/spec_helper.rb +40 -0
  40. data/spec/support/enable_dallistore_cache.rb +15 -0
  41. data/spec/support/enable_redis_cache.rb +15 -0
  42. metadata +85 -0
@@ -0,0 +1,559 @@
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(
61
+ File.join(File.dirname(__FILE__), '..', 'data', 'geoip')
62
+ )
63
+
64
+ # Ordered list of the ISO3166 2-character country codes, ordered by
65
+ # GeoIP ID
66
+ CountryCode = YAML.load_file(File.join(DATA_DIR, 'country_code.yml'))
67
+
68
+ # Ordered list of the ISO3166 3-character country codes, ordered by
69
+ # GeoIP ID
70
+ CountryCode3 = YAML.load_file(File.join(DATA_DIR, 'country_code3.yml'))
71
+
72
+ # Ordered list of the English names of the countries, ordered by GeoIP ID
73
+ CountryName = YAML.load_file(File.join(DATA_DIR, 'country_name.yml'))
74
+
75
+ # Ordered list of the ISO3166 2-character continent code of the countries,
76
+ # ordered by GeoIP ID
77
+ CountryContinent = YAML.load_file(File.join(DATA_DIR, 'country_continent.yml'))
78
+
79
+ # Hash of the timezone codes mapped to timezone name, per zoneinfo
80
+ TimeZone = YAML.load_file(File.join(DATA_DIR, 'time_zone.yml'))
81
+
82
+ GEOIP_COUNTRY_EDITION = 1
83
+ GEOIP_CITY_EDITION_REV1 = 2
84
+ GEOIP_REGION_EDITION_REV1 = 3
85
+ GEOIP_ISP_EDITION = 4
86
+ GEOIP_ORG_EDITION = 5
87
+ GEOIP_CITY_EDITION_REV0 = 6
88
+ GEOIP_REGION_EDITION_REV0 = 7
89
+ GEOIP_PROXY_EDITION = 8
90
+ GEOIP_ASNUM_EDITION = 9
91
+ GEOIP_NETSPEED_EDITION = 10
92
+
93
+ COUNTRY_BEGIN = 16_776_960 #:nodoc:
94
+ STATE_BEGIN_REV0 = 16_700_000 #:nodoc:
95
+ STATE_BEGIN_REV1 = 16_000_000 #:nodoc:
96
+ STRUCTURE_INFO_MAX_SIZE = 20 #:nodoc:
97
+ DATABASE_INFO_MAX_SIZE = 100 #:nodoc:
98
+ MAX_ORG_RECORD_LENGTH = 300 #:nodoc:
99
+ MAX_ASN_RECORD_LENGTH = 300 #:nodoc: unverified
100
+ US_OFFSET = 1 #:nodoc:
101
+ CANADA_OFFSET = 677 #:nodoc:
102
+ WORLD_OFFSET = 1353 #:nodoc:
103
+ FIPS_RANGE = 360 #:nodoc:
104
+ FULL_RECORD_LENGTH = 50 #:nodoc:
105
+
106
+ STANDARD_RECORD_LENGTH = 3 #:nodoc:
107
+ SEGMENT_RECORD_LENGTH = 3 #:nodoc:
108
+
109
+ class Country < Struct.new(:request, :ip, :country_code, :country_code2, :country_code3, :country_name, :continent_code)
110
+ def to_hash
111
+ Hash[each_pair.to_a].each_with_object({}) { |(k,v), h| h[k.to_sym] = v }
112
+ end
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
+ def to_hash
118
+ Hash[each_pair.to_a].each_with_object({}) { |(k,v), h| h[k.to_sym] = v }
119
+ end
120
+ end
121
+
122
+ class ASN < Struct.new(:number, :asn)
123
+ alias as_num number
124
+ end
125
+
126
+ # The Edition number that identifies which kind of database you've opened
127
+ attr_reader :database_type
128
+
129
+ alias databaseType database_type
130
+
131
+ # Open the GeoIP database and determine the file format version.
132
+ #
133
+ # +filename+ is a String holding the path to the GeoIP.dat file
134
+ # +options+ is an integer holding caching flags (unimplemented)
135
+ #
136
+ def initialize(filename, flags = 0)
137
+ @mutex = Mutex.new unless IO.respond_to?(:pread)
138
+
139
+ @flags = flags
140
+ @database_type = GEOIP_COUNTRY_EDITION
141
+ @record_length = STANDARD_RECORD_LENGTH
142
+ @file = File.open(filename, 'rb')
143
+
144
+ detect_database_type!
145
+ end
146
+
147
+ # Search the GeoIP database for the specified host, returning country
148
+ # info.
149
+ #
150
+ # +hostname+ is a String holding the host's DNS name or numeric IP
151
+ # address.
152
+ #
153
+ # If the database is a City database (normal), return the result that
154
+ # +city+ would return.
155
+ #
156
+ # Otherwise, return a Country object with the seven elements:
157
+ # * The host or IP address string as requested
158
+ # * The IP address string after looking up the host
159
+ # * The GeoIP country-ID as an integer (N.B. this is excluded from the
160
+ # city results!)
161
+ # * The two-character country code (ISO 3166-1 alpha-2)
162
+ # * The three-character country code (ISO 3166-2 alpha-3)
163
+ # * The ISO 3166 English-language name of the country
164
+ # * The two-character continent code
165
+ #
166
+ def country(hostname)
167
+ if @database_type == GEOIP_CITY_EDITION_REV0 ||
168
+ @database_type == GEOIP_CITY_EDITION_REV1
169
+ return city(hostname)
170
+ end
171
+
172
+ ip = lookup_ip(hostname)
173
+
174
+ # Convert numeric IP address to an integer
175
+ ipnum = iptonum(ip)
176
+
177
+ if @database_type != GEOIP_COUNTRY_EDITION &&
178
+ @database_type != GEOIP_PROXY_EDITION &&
179
+ @database_type != GEOIP_NETSPEED_EDITION
180
+ throw "Invalid GeoIP database type, can't look up Country by IP"
181
+ end
182
+
183
+ code = (seek_record(ipnum) - COUNTRY_BEGIN)
184
+
185
+ Country.new(
186
+ hostname, # Requested hostname
187
+ ip, # Ip address as dotted quad
188
+ code, # GeoIP's country code
189
+ CountryCode[code], # ISO3166-1 alpha-2 code
190
+ CountryCode3[code], # ISO3166-2 alpha-3 code
191
+ CountryName[code], # Country name, per ISO 3166
192
+ CountryContinent[code] # Continent code.
193
+ )
194
+ end
195
+
196
+ # Search the GeoIP database for the specified host, returning city info.
197
+ #
198
+ # +hostname+ is a String holding the host's DNS name or numeric IP
199
+ # address.
200
+ #
201
+ # Returns a City object with the fourteen elements:
202
+ # * The host or IP address string as requested
203
+ # * The IP address string after looking up the host
204
+ # * The two-character country code (ISO 3166-1 alpha-2)
205
+ # * The three-character country code (ISO 3166-2 alpha-3)
206
+ # * The ISO 3166 English-language name of the country
207
+ # * The two-character continent code
208
+ # * The region name (state or territory)
209
+ # * The city name
210
+ # * The postal code (zipcode)
211
+ # * The latitude
212
+ # * The longitude
213
+ # * The USA dma_code if known (only REV1 City database)
214
+ # * The USA area_code if known (only REV1 City database)
215
+ # * The timezone name, if known
216
+ #
217
+ def city(hostname)
218
+ ip = lookup_ip(hostname)
219
+
220
+ # Convert numeric IP address to an integer
221
+ ipnum = iptonum(ip)
222
+
223
+ if @database_type != GEOIP_CITY_EDITION_REV0 &&
224
+ @database_type != GEOIP_CITY_EDITION_REV1
225
+ throw "Invalid GeoIP database type, can't look up City by IP"
226
+ end
227
+
228
+ pos = seek_record(ipnum)
229
+
230
+ # This next statement was added to MaxMind's C version after it was
231
+ # rewritten in Ruby. It prevents unassigned IP addresses from returning
232
+ # bogus data. There was concern over whether the changes to an
233
+ # application's behaviour were always correct, but this has been tested
234
+ # using an exhaustive search of the top 16 bits of the IP address space.
235
+ # The records where the change takes effect contained *no* valid data.
236
+ # If you're concerned, email me, and I'll send you the test program so
237
+ # you can test whatever IP range you think is causing problems,
238
+ # as I don't care to undertake an exhaustive search of the 32-bit space.
239
+ if pos != @database_segments[0]
240
+ read_city(pos, hostname, ip)
241
+ end
242
+ end
243
+
244
+ # Search a ISP GeoIP database for the specified host, returning the ISP
245
+ # Not all GeoIP databases contain ISP information.
246
+ # Check http://maxmind.com
247
+ #
248
+ # +hostname+ is a String holding the host's DNS name or numeric IP
249
+ # address.
250
+ #
251
+ # Returns the ISP name.
252
+ #
253
+ def isp(hostname)
254
+ ip = lookup_ip(hostname)
255
+
256
+ # Convert numeric IP address to an integer
257
+ ipnum = iptonum(ip)
258
+
259
+ if @database_type != GEOIP_ISP_EDITION &&
260
+ @database_type != GEOIP_ORG_EDITION
261
+ throw "Invalid GeoIP database type, can't look up Organization/ISP by IP"
262
+ end
263
+
264
+ pos = seek_record(ipnum)
265
+ off = pos + (2 * @record_length - 1) * @database_segments[0]
266
+
267
+ record = atomic_read(MAX_ORG_RECORD_LENGTH, off)
268
+ record = record.sub(/\000.*/n, '')
269
+ record
270
+ end
271
+
272
+ # Search a ASN GeoIP database for the specified host, returning the AS
273
+ # number and description.
274
+ #
275
+ # +hostname+ is a String holding the host's DNS name or numeric
276
+ # IP address.
277
+ #
278
+ # Returns the AS number and description.
279
+ #
280
+ # Source:
281
+ # http://geolite.maxmind.com/download/geoip/database/asnum/GeoIPASNum.dat.gz
282
+ #
283
+ def asn(hostname)
284
+ ip = lookup_ip(hostname)
285
+
286
+ # Convert numeric IP address to an integer
287
+ ipnum = iptonum(ip)
288
+
289
+ if (@database_type != GEOIP_ASNUM_EDITION)
290
+ throw "Invalid GeoIP database type, can't look up ASN by IP"
291
+ end
292
+
293
+ pos = seek_record(ipnum)
294
+ off = pos + (2 * @record_length - 1) * @database_segments[0]
295
+
296
+ record = atomic_read(MAX_ASN_RECORD_LENGTH, off)
297
+ record = record.sub(/\000.*/n, '')
298
+
299
+ # AS####, Description
300
+ ASN.new($1, $2) if record =~ /^(AS\d+)\s(.*)$/
301
+ end
302
+
303
+ # Search a ISP GeoIP database for the specified host, returning the
304
+ # organization.
305
+ #
306
+ # +hostname+ is a String holding the host's DNS name or numeric
307
+ # IP address.
308
+ #
309
+ # Returns the organization associated with it.
310
+ #
311
+ alias_method(:organization, :isp) # Untested, according to Maxmind docs this should work
312
+
313
+ # Iterate through a GeoIP city database
314
+ def each
315
+ return enum_for unless block_given?
316
+
317
+ if @database_type != GEOIP_CITY_EDITION_REV0 &&
318
+ @database_type != GEOIP_CITY_EDITION_REV1
319
+ throw "Invalid GeoIP database type, can't iterate thru non-City database"
320
+ end
321
+
322
+ @iter_pos = @database_segments[0] + 1
323
+ num = 0
324
+
325
+ until (rec = read_city(@iter_pos)).nil?
326
+ yield rec
327
+ print "#{num}: #{@iter_pos}\n" if ((num += 1) % 1000) == 0
328
+ end
329
+
330
+ @iter_pos = nil
331
+ self
332
+ end
333
+
334
+ private
335
+
336
+ # Detects the type of the database.
337
+ def detect_database_type! # :nodoc:
338
+ @file.seek(-3, IO::SEEK_END)
339
+
340
+ 0.upto(STRUCTURE_INFO_MAX_SIZE - 1) do |i|
341
+ if @file.read(3).bytes.all? { |byte| byte == 255 }
342
+ @database_type = if @file.respond_to?(:getbyte)
343
+ @file.getbyte
344
+ else
345
+ @file.getc
346
+ end
347
+
348
+ @database_type -= 105 if @database_type >= 106
349
+
350
+ if @database_type == GEOIP_REGION_EDITION_REV0
351
+ # Region Edition, pre June 2003
352
+ @database_segments = [STATE_BEGIN_REV0]
353
+ elsif @database_type == GEOIP_REGION_EDITION_REV1
354
+ # Region Edition, post June 2003
355
+ @database_segments = [STATE_BEGIN_REV1]
356
+ elsif @database_type == GEOIP_CITY_EDITION_REV0 ||
357
+ @database_type == GEOIP_CITY_EDITION_REV1 ||
358
+ @database_type == GEOIP_ORG_EDITION ||
359
+ @database_type == GEOIP_ISP_EDITION ||
360
+ @database_type == GEOIP_ASNUM_EDITION
361
+
362
+ # City/Org Editions have two segments, read offset of second segment
363
+ @database_segments = [0]
364
+ sr = @file.read(3).unpack('C*')
365
+ @database_segments[0] += le_to_ui(sr)
366
+
367
+ if @database_type == GEOIP_ORG_EDITION ||
368
+ @database_type == GEOIP_ISP_EDITION
369
+ @record_length = 4
370
+ end
371
+ end
372
+
373
+ break
374
+ else
375
+ @file.seek(-4, IO::SEEK_CUR)
376
+ end
377
+ end
378
+
379
+ if @database_type == GEOIP_COUNTRY_EDITION ||
380
+ @database_type == GEOIP_PROXY_EDITION ||
381
+ @database_type == GEOIP_NETSPEED_EDITION
382
+ @database_segments = [COUNTRY_BEGIN]
383
+ end
384
+ end
385
+
386
+ # Search the GeoIP database for the specified host, returning city info.
387
+ #
388
+ # +hostname+ is a String holding the host's DNS name or numeric
389
+ # IP address.
390
+ #
391
+ # Returns an array of fourteen elements:
392
+ # * All elements from the country query (except GeoIP's country code,
393
+ # bah!)
394
+ # * The region (state or territory) name
395
+ # * The city name
396
+ # * The postal code (zipcode)
397
+ # * The latitude
398
+ # * The longitude
399
+ # * The dma_code and area_code, if available (REV1 City database)
400
+ # * The timezone name, if known
401
+ #
402
+ def read_city(pos, hostname = '', ip = '') #:nodoc:
403
+ off = pos + (2 * @record_length - 1) * @database_segments[0]
404
+ record = atomic_read(FULL_RECORD_LENGTH, off)
405
+
406
+ return unless record && record.size == FULL_RECORD_LENGTH
407
+
408
+ # The country code is the first byte:
409
+ code = record[0]
410
+ code = code.ord if code.respond_to?(:ord)
411
+ record = record[1..-1]
412
+ @iter_pos += 1 unless @iter_pos.nil?
413
+
414
+ spl = record.split("\x00", 4)
415
+ # Get the region:
416
+ region = spl[0]
417
+ @iter_pos += (region.size + 1) unless @iter_pos.nil?
418
+
419
+ # Get the city:
420
+ city = spl[1]
421
+ @iter_pos += (city.size + 1) unless @iter_pos.nil?
422
+ # set the correct encoding in ruby 1.9 compatible environments:
423
+ city.force_encoding('iso-8859-1') if city.respond_to?(:force_encoding)
424
+
425
+ # Get the postal code:
426
+ postal_code = spl[2]
427
+ @iter_pos += (postal_code.size + 1) unless @iter_pos.nil?
428
+
429
+ record = spl[3]
430
+
431
+ # Get the latitude/longitude:
432
+ if record && record[0, 3]
433
+ latitude = (le_to_ui(record[0, 3].unpack('C*')) / 10000.0) - 180
434
+ record = record[3..-1]
435
+
436
+ @iter_pos += 3 unless @iter_pos.nil?
437
+ else
438
+ latitude = ''
439
+ end
440
+
441
+ if record && record[0, 3]
442
+ longitude = le_to_ui(record[0, 3].unpack('C*')) / 10000.0 - 180
443
+ record = record[3..-1]
444
+
445
+ @iter_pos += 3 unless @iter_pos.nil?
446
+ else
447
+ longitude = ''
448
+ end
449
+
450
+ # UNTESTED
451
+ if record &&
452
+ record[0,3] &&
453
+ @database_type == GEOIP_CITY_EDITION_REV1 &&
454
+ CountryCode[code] == "US"
455
+
456
+ dmaarea_combo = le_to_ui(record[0,3].unpack('C*'))
457
+ dma_code = (dmaarea_combo / 1000)
458
+ area_code = (dmaarea_combo % 1000)
459
+
460
+ @iter_pos += 3 unless @iter_pos.nil?
461
+ else
462
+ dma_code = nil
463
+ area_code = nil
464
+ end
465
+
466
+ City.new(
467
+ hostname, # Requested hostname
468
+ ip, # Ip address as dotted quad
469
+ CountryCode[code], # ISO3166-1 code
470
+ CountryCode3[code], # ISO3166-2 code
471
+ CountryName[code], # Country name, per IS03166
472
+ CountryContinent[code], # Continent code.
473
+ region, # Region name
474
+ city, # City name
475
+ postal_code, # Postal code
476
+ latitude,
477
+ longitude,
478
+ dma_code,
479
+ area_code,
480
+ (TimeZone["#{CountryCode[code]}#{region}"] ||
481
+ TimeZone["#{CountryCode[code]}"])
482
+ )
483
+ end
484
+
485
+ def lookup_ip(ip_or_hostname) # :nodoc:
486
+ unless ip_or_hostname.is_a?(String) && ip_or_hostname !~ /^[0-9.]+$/
487
+ return ip_or_hostname
488
+ end
489
+
490
+ # Lookup IP address, we were given a name
491
+ ip = IPSocket.getaddress(ip_or_hostname)
492
+ ip = '0.0.0.0' if ip == '::1'
493
+ ip
494
+ end
495
+
496
+ # Convert numeric IP address to Integer.
497
+ def iptonum(ip) #:nodoc:
498
+ if ip.is_a?(String) &&
499
+ ip =~ %r{/^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/}
500
+ ip = be_to_ui(Regexp.last_match.to_a.slice(1..4))
501
+ else
502
+ ip = ip.to_i
503
+ end
504
+
505
+ ip
506
+ end
507
+
508
+ def seek_record(ipnum) #:nodoc:
509
+ # Binary search in the file.
510
+ # Records are pairs of little-endian integers, each of @record_length.
511
+ offset = 0
512
+ mask = 0x80000000
513
+
514
+ 31.downto(0) do |depth|
515
+ off = (@record_length * 2 * offset)
516
+ buf = atomic_read(@record_length * 2, off)
517
+
518
+ buf.slice!(0...@record_length) if ((ipnum & mask) != 0)
519
+ offset = le_to_ui(buf[0...@record_length].unpack('C*'))
520
+
521
+ if offset >= @database_segments[0]
522
+ return offset
523
+ end
524
+
525
+ mask >>= 1
526
+ end
527
+ end
528
+
529
+ # Convert a big-endian array of numeric bytes to unsigned int.
530
+ #
531
+ # Returns the unsigned Integer.
532
+ #
533
+ def be_to_ui(s) #:nodoc:
534
+ i = 0
535
+ s.each { |b| i = ((i << 8) | (b.to_i & 0x0ff)) }
536
+ i
537
+ end
538
+
539
+ # Same for little-endian
540
+ def le_to_ui(s) #:nodoc:
541
+ be_to_ui(s.reverse)
542
+ end
543
+
544
+ # reads +length+ bytes from +offset+ as atomically as possible
545
+ # if IO.pread is available, it'll use that (making it both multithread
546
+ # and multiprocess-safe). Otherwise we will use a mutex to synchronize
547
+ # access (only providing protection against multiple threads, but not
548
+ # file descriptors shared across multiple processes).
549
+ def atomic_read(length, offset) #:nodoc:
550
+ if @mutex
551
+ @mutex.synchronize do
552
+ @file.seek(offset)
553
+ @file.read(length)
554
+ end
555
+ else
556
+ IO.pread(@file.fileno, length, offset)
557
+ end
558
+ end
559
+ end