root/lang/ruby/uuid/trunk/lib/uuid.rb

Revision 30649, 7.5 kB (checked in by shyouhei, 5 years ago)

some tuneup; typical usage for an uuid object is for a hash key, so it should be wiser for us to tune #hash and #eql?.

Line 
1#!/usr/bin/env ruby
2# Copyright(c) 2005 URABE, Shyouhei.
3#
4# Permission is hereby granted, free of  charge, to any person obtaining a copy
5# of  this code, to  deal in  the code  without restriction,  including without
6# limitation  the rights  to  use, copy,  modify,  merge, publish,  distribute,
7# sublicense, and/or sell copies of the code, and to permit persons to whom the
8# code is furnished to do so, subject to the following conditions:
9#
10#        The above copyright notice and this permission notice shall be
11#        included in all copies or substantial portions of the code.
12#
13# THE  CODE IS  PROVIDED "AS  IS",  WITHOUT WARRANTY  OF ANY  KIND, EXPRESS  OR
14# IMPLIED,  INCLUDING BUT  NOT LIMITED  TO THE  WARRANTIES  OF MERCHANTABILITY,
15# FITNESS FOR A PARTICULAR PURPOSE  AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
16# AUTHOR  OR  COPYRIGHT  HOLDER BE  LIABLE  FOR  ANY  CLAIM, DAMAGES  OR  OTHER
17# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18# OUT OF  OR IN CONNECTION WITH  THE CODE OR THE  USE OR OTHER  DEALINGS IN THE
19# CODE.
20
21%w[
22        digest/md5
23        digest/sha1
24        tmpdir
25].each do |f|
26        require f
27end
28
29# Pure ruby UUID generator, which is compatible with RFC4122
30class UUID
31        # UUID epoch is 15th Oct. 1582
32        UNIXEpoch = 0x01B21DD213814000 # in 100-nanoseconds resolution
33
34        private_class_method :new
35
36        private
37        def initialize str
38                tmp = str.unpack "C*"
39                @num = tmp.inject do |r, i|
40                        r * 256 | i
41                end
42                @num.freeze
43                self.freeze
44        end
45
46        public
47
48        def raw_bytes
49                ret = String.new
50                tmp = @num
51                16.times do |i|
52                        x, y = tmp.divmod 256
53                        ret << y
54                        tmp = x
55                end
56                ret.reverse!
57                ret
58        end
59
60        class << self
61                def mask ver, str # :nodoc
62                        ver = ver & 15
63                        v = str[6]
64                        v &= 0b0000_1111
65                        v |= ver << 4
66                        str[6] = v
67                        r = str[8]
68                        r &= 0b0011_1111
69                        r |= 0b1000_0000
70                        str[8] = r
71                        str
72                end
73
74                def prand # :nodoc:
75                        rand 0x100000000
76                end
77
78                private :mask, :prand
79
80                # UUID generation using SHA1. Recommended over create_md5.
81                # Namespace object is another UUID, some of them are pre-defined below.
82                def create_sha1 str, namespace
83                        sha1 = Digest::SHA1.new
84                        sha1.update namespace.raw_bytes
85                        sha1.update str
86                        sum = sha1.digest
87                        raw = mask 5, sum[0..15]
88                        new raw
89                end
90
91                # UUID generation using MD5 (for backward compat.)
92                def create_md5 str, namespace
93                        md5 = Digest::MD5.new
94                        md5.update namespace.raw_bytes
95                        md5.update str
96                        sum = md5.digest
97                        raw = mask 3, sum[0..16]
98                        new raw
99                end
100
101                # UUID  generation  using  random-number  generator.   From  it's  random
102                # nature, there's  no warranty that  the created ID is  really universaly
103                # unique.
104                def create_random
105                        rnd = [prand, prand, prand, prand].pack "N4"
106                        raw = mask 4, rnd
107                        new raw
108                end
109
110                def read_state fp                         # :nodoc:
111                        fp.rewind
112                        Marshal.load fp.read
113                end
114
115                def write_state fp, c, m  # :nodoc:
116                        fp.rewind
117                        str = Marshal.dump [c, m]
118                        fp.write str
119                end
120
121                private :read_state, :write_state
122                STATE_FILE = 'ruby-uuid'
123
124                # create  the "version  1" UUID  with current  system clock,  current UTC
125                # timestamp, and the IEEE 802 address (so-called MAC address).
126                #
127                # Speed notice: it's slow.  It writes  some data into hard drive on every
128                # invokation. If you want to speed  this up, try remounting tmpdir with a
129                # memory based filesystem  (such as tmpfs).  STILL slow?  then no way but
130                # rewrite it with c :)
131                def create clock=nil, time=Time.now, mac_addr=nil
132                        c = t = m = nil
133                        Dir.chdir Dir.tmpdir do
134                                unless FileTest.exist? STATE_FILE then
135                                        # Generate a pseudo MAC address because we have no pure-ruby way
136                                        # to know  the MAC  address of the  NIC this system  uses.  Note
137                                        # that cheating  with pseudo arresses here  is completely legal:
138                                        # see Section 4.5 of RFC4122 for details.
139                                        sha1 = Digest::SHA1.new
140                                        256.times do
141                                                r = [prand].pack "N"
142                                                sha1.update r
143                                        end
144                                        str = sha1.digest
145                                        r = rand 34 # 40-6
146                                        node = str[r, 6] || str
147                                        node[0] |= 0x01 # multicast bit
148                                        k = rand 0x40000
149                                        open STATE_FILE, 'w' do |fp|
150                                                fp.flock IO::LOCK_EX
151                                                write_state fp, k, node
152                                                fp.chmod 0o777 # must be world writable
153                                        end
154                                end
155                                open STATE_FILE, 'r+' do |fp|
156                                        fp.flock IO::LOCK_EX
157                                        c, m = read_state fp
158                                        c += 1 # important; increment here
159                                        write_state fp, c, m
160                                end
161                        end
162                        c = clock & 0b11_1111_1111_1111 if clock
163                        m = mac_addr if mac_addr
164                        time = Time.at time if time.is_a? Float
165                        case time
166                        when Time
167                                t = time.to_i * 10_000_000 + time.tv_usec * 10 + UNIXEpoch
168                        when Integer
169                                t = time + UNIXEpoch
170                        else
171                                raise TypeError, "cannot convert ``#{time}'' into Time."
172                        end
173
174                        tl = t & 0xFFFF_FFFF
175                        tm = t >> 32
176                        tm = tm & 0xFFFF
177                        th = t >> 48
178                        th = th & 0b0000_1111_1111_1111
179                        th = th | 0b0001_0000_0000_0000
180                        cl = c & 0b0000_0000_1111_1111
181                        ch = c & 0b0011_1111_0000_0000
182                        ch = ch >> 8
183                        ch = ch | 0b1000_0000
184                        pack tl, tm, th, ch, cl, m
185                end
186
187                # A  simple GUID  parser:  just ignores  unknown  characters and  convert
188                # hexadecimal dump into 16-octet object.
189                def parse obj
190                        str = obj.to_s.sub %r/\Aurn:uuid:/, ''
191                        str.gsub! %r/[^0-9A-Fa-f]/, ''
192                        raw = str[0..31].to_a.pack 'H*'
193                        new raw
194                end
195
196                # The 'primitive constructor' of this class
197                # Note UUID.pack(uuid.unpack) == uuid
198                def pack tl, tm, th, ch, cl, n
199                        raw = [tl, tm, th, ch, cl, n].pack "NnnCCa6"
200                        new raw
201                end
202        end
203
204        # The 'primitive deconstructor', or the dual to pack.
205        # Note UUID.pack(uuid.unpack) == uuid
206        def unpack
207                raw_bytes.unpack "NnnCCa6"
208        end
209
210        # The timestamp of this UUID.
211        # Throws RageError if that time exceeds UNIX time range
212        def time
213                a = unpack
214                tl = a[0]
215                tm = a[1]
216                th = a[2] & 0x0FFF
217                t = tl
218                t += tm << 32
219                t += th << 48
220                t -= UNIXEpoch
221                tv_sec = t / 10_000_000
222                t -= tv_sec * 10_000_000
223                tv_usec = t / 10
224                Time.at tv_sec, tv_usec
225        end
226
227        # The version of this UUID
228        def version
229                v = unpack[2] & 0b1111_0000_0000_0000
230                v >> 12
231        end
232
233        # The clock sequence of this UUID
234        def clock
235                a = unpack
236                ch = a[3] & 0b0001_1111
237                cl = a[4]
238                c = cl
239                c += ch << 8
240                c
241        end
242
243        # The IEEE 802 address in a hexadecimal format
244        def node
245                m = unpack[5].unpack 'C*'
246                '%02x%02x%02x%02x%02x%02x' % m
247        end
248        alias mac_address node
249        alias ieee802 node
250
251        # Generate the string representation (a.k.a GUID) of this UUID
252        def to_s
253                a = unpack
254                a[-1] = mac_address
255                "%08x-%04x-%04x-%02x%02x-%s" % a
256        end
257        alias guid to_s
258
259        # Convert into a RFC4122-comforming URN representation
260        def to_uri
261                "urn:uuid:" + self.to_s
262        end
263        alias urn to_uri
264        alias inspect to_uri
265
266        # Convert into 128-bit unsigned integer
267        # Typically a Bignum instance, but can be a Fixnum.
268        def to_int
269                @num
270        end
271        alias to_i to_int
272
273        # Two  UUIDs  are  said  to  be  equal if  and  only  if  their  (byte-order
274        # canonicalized) integer representations are equivallent.  Refer RFC4122 for
275        # details.
276        def == other
277                to_i == other.to_i
278        end
279        alias eql? ==
280
281        # Two identical UUIDs should have same hash
282        def hash
283                to_i
284        end
285
286        include Comparable
287        # UUIDs are comparable (don't know what benefits are there, though).
288        def <=> other
289                to_s <=> other.to_s
290        end
291
292        # Pre-defined UUID Namespaces described in RFC4122 Appendix C.
293        NameSpace_DNS = parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
294        NameSpace_URL = parse "6ba7b811-9dad-11d1-80b4-00c04fd430c8"
295        NameSpace_OID = parse "6ba7b812-9dad-11d1-80b4-00c04fd430c8"
296        NameSpace_X500 = parse "6ba7b814-9dad-11d1-80b4-00c04fd430c8"
297
298        # The Nil UUID in RFC4122 Section 4.1.7
299        Nil = parse "00000000-0000-0000-0000-000000000000"
300end
301
302
303# Local Variables:
304# mode: ruby
305# coding: utf-8
306# indent-tabs-mode: t
307# tab-width: 3
308# ruby-indent-level: 3
309# fill-column: 79
310# default-justification: full
311# End:
312# vi: ts=3 sw=3
Note: See TracBrowser for help on using the browser.