id3 0.5.0 → 1.0.0.pre4
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.
- data/CHANGES +14 -0
- data/LICENSE.html +8 -1
- data/README.md +39 -0
- data/docs/ID3_comparison.html +10 -2
- data/docs/ID3_comparison2.html +29 -21
- data/docs/ID3v2_frames_overview.txt +172 -35
- data/{lib → docs}/hexdump.rb +0 -0
- data/docs/index.html +29 -9
- data/lib/helpers/hash_extensions.rb +20 -0
- data/lib/helpers/hexdump.rb +136 -0
- data/lib/helpers/invert_hash.rb +128 -0
- data/lib/helpers/recursive_helper.rb +39 -0
- data/lib/helpers/restricted_ordered_hash.rb +88 -0
- data/lib/helpers/ruby_1.8_1.9_compatibility.rb +62 -0
- data/lib/id3.rb +23 -1252
- data/lib/id3/audiofile.rb +261 -0
- data/lib/id3/constants.rb +292 -0
- data/lib/id3/frame.rb +178 -0
- data/lib/id3/frame_array.rb +19 -0
- data/lib/id3/generic_tag.rb +73 -0
- data/lib/id3/id3.rb +159 -0
- data/lib/id3/io_extensions.rb +44 -0
- data/lib/id3/module_methods.rb +127 -0
- data/lib/id3/string_extensions.rb +40 -0
- data/lib/id3/tag1.rb +131 -0
- data/lib/id3/tag2.rb +261 -0
- metadata +87 -58
- data/README +0 -18
- data/docs/ID3v2_frames_comparison.txt +0 -197
- data/lib/invert_hash.rb +0 -105
@@ -0,0 +1,20 @@
|
|
1
|
+
#
|
2
|
+
# EXTENSIONS to Class Hash
|
3
|
+
#
|
4
|
+
|
5
|
+
# include Hash#inverse from Facets of Ruby or from our helper
|
6
|
+
# then Monkey-Patch the Hash#invert method:
|
7
|
+
|
8
|
+
require 'helpers/invert_hash'
|
9
|
+
|
10
|
+
class Hash
|
11
|
+
# original Hash#invert is still available as Hash#old_invert
|
12
|
+
alias old_invert invert
|
13
|
+
|
14
|
+
# monkey-patching Hash#invert method - it's backwards compatible, but preserves duplicate values in the hash
|
15
|
+
def invert
|
16
|
+
self.inverse
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# ==============================================================================
|
2
|
+
# EXTENDING CLASS STRING
|
3
|
+
# ==============================================================================
|
4
|
+
# --
|
5
|
+
# (C) Copyright 2004 by Tilo Sloboda <tools@unixgods.org>
|
6
|
+
#
|
7
|
+
# License:
|
8
|
+
# Freely available under the terms of the OpenSource "Artistic License"
|
9
|
+
# in combination with the Addendum A (below)
|
10
|
+
#
|
11
|
+
# In case you did not get a copy of the license along with the software,
|
12
|
+
# it is also available at: http://www.unixgods.org/~tilo/artistic-license.html
|
13
|
+
#
|
14
|
+
# Addendum A:
|
15
|
+
# THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU!
|
16
|
+
# SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
17
|
+
# REPAIR OR CORRECTION.
|
18
|
+
#
|
19
|
+
# IN NO EVENT WILL THE COPYRIGHT HOLDERS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
|
20
|
+
# SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY
|
21
|
+
# TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
|
22
|
+
# INACCURATE OR USELESS OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
23
|
+
# TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF THE COPYRIGHT HOLDERS OR OTHER PARTY HAS BEEN
|
24
|
+
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
25
|
+
#++
|
26
|
+
|
27
|
+
# this is written for Ruby version < 1.9 - unfortunately they changed the I/O and String classes significantly.
|
28
|
+
# have to fix this up for newer Ruby versions.
|
29
|
+
|
30
|
+
if RUBY_VERSION >= "1.9.0"
|
31
|
+
ZEROBYTE = "\x00".force_encoding(Encoding::BINARY) unless defined? ZEROBYTE
|
32
|
+
|
33
|
+
else # older Ruby versions:
|
34
|
+
|
35
|
+
class String
|
36
|
+
alias bytesize size
|
37
|
+
|
38
|
+
def getbyte(x) # when accessing a string and selecting x-th byte to do calculations , as defined in Ruby 1.9
|
39
|
+
self[x] # returns an integer
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
ZEROBYTE = "\0" unless defined? ZEROBYTE
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
class String
|
48
|
+
#
|
49
|
+
# prints out a good'ol hexdump of the data contained in the string
|
50
|
+
#
|
51
|
+
# parameters: sparse: true / false do we want to print multiple lines with zero values?
|
52
|
+
|
53
|
+
def hexdump(sparse = false)
|
54
|
+
|
55
|
+
selfsize = self.bytesize
|
56
|
+
|
57
|
+
first = true
|
58
|
+
|
59
|
+
print "\n index 0 1 2 3 4 5 6 7 8 9 A B C D E F\n\n"
|
60
|
+
|
61
|
+
lines,rest = selfsize.divmod(16)
|
62
|
+
address = 0; i = 0 # we count them independently for future extension.
|
63
|
+
|
64
|
+
while lines > 0
|
65
|
+
str = self[i..i+15]
|
66
|
+
|
67
|
+
# we don't print lines with all zeroes, unless it's the last line
|
68
|
+
|
69
|
+
if str == ZEROBYTE * 16 # if the 16 bytes are all zero
|
70
|
+
|
71
|
+
if (!sparse) || (sparse && lines == 1 && rest == 0)
|
72
|
+
str.tr!("\000-\037\177-\377",'.')
|
73
|
+
|
74
|
+
printf( "%08x %8s %8s %8s %8s %s\n",
|
75
|
+
address, self[i..i+3].unpack('H8').first, self[i+4..i+7].unpack('H8').first,
|
76
|
+
self[i+8..i+11].unpack('H8').first, self[i+12..i+15].unpack('H8').first, str)
|
77
|
+
else
|
78
|
+
print " .... 00 .. 00 00 .. 00 00 .. 00 00 .. 00 ................\n" if first
|
79
|
+
first = false
|
80
|
+
end
|
81
|
+
|
82
|
+
else # print string which is not all zeros
|
83
|
+
|
84
|
+
str.tr!("\000-\037\177-\377",'.')
|
85
|
+
|
86
|
+
printf( "%08x %8s %8s %8s %8s %s\n",
|
87
|
+
address, self[i..i+3].unpack('H8').first, self[i+4..i+7].unpack('H8').first,
|
88
|
+
self[i+8..i+11].unpack('H8').first, self[i+12..i+15].unpack('H8').first, str)
|
89
|
+
first = true
|
90
|
+
end
|
91
|
+
i += 16; address += 16; lines -= 1
|
92
|
+
end
|
93
|
+
|
94
|
+
# now do the remaining bytes, which don't fit a full line..
|
95
|
+
# yikes - this is truly ugly! REWRITE THIS!!
|
96
|
+
|
97
|
+
if rest > 0
|
98
|
+
chunks2,rest2 = rest.divmod(4)
|
99
|
+
j = i; k = 0
|
100
|
+
if (i < selfsize)
|
101
|
+
printf( "%08x ", address)
|
102
|
+
while (i < selfsize)
|
103
|
+
printf "%02x", self.getbyte(i)
|
104
|
+
i += 1; k += 1
|
105
|
+
print " " if ((i % 4) == 0)
|
106
|
+
end
|
107
|
+
for i in (k..15)
|
108
|
+
print " "
|
109
|
+
end
|
110
|
+
str = self[j..selfsize]
|
111
|
+
str.tr!("\000-\037\177-\377",'.')
|
112
|
+
print " " * (4 - chunks2+1)
|
113
|
+
printf(" %s\n", str)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
|
122
|
+
__END__
|
123
|
+
|
124
|
+
|
125
|
+
# you would use it like this:
|
126
|
+
|
127
|
+
require './hexdump'
|
128
|
+
|
129
|
+
s = "some random long string"
|
130
|
+
|
131
|
+
t = s + ZEROBYTE*80 + s + ZEROBYTE*64 + s + "bla bla bla!"
|
132
|
+
t.hexdump(true) # surpress consecutive lines with zero values
|
133
|
+
t.hexdump # same as t.hexdump(false)
|
134
|
+
|
135
|
+
|
136
|
+
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# ==============================================================================
|
2
|
+
# EXTENDING CLASS HASH
|
3
|
+
# ==============================================================================
|
4
|
+
#--
|
5
|
+
# (C) Copyright 2004 by Tilo Sloboda <tools@unixgods.org>
|
6
|
+
#
|
7
|
+
# updated: Time-stamp: <Mon, 24 Oct 2011, 23:03:29 PDT tilo>
|
8
|
+
#
|
9
|
+
# License:
|
10
|
+
# Freely available under the terms of the OpenSource "Artistic License"
|
11
|
+
# in combination with the Addendum A (below)
|
12
|
+
#
|
13
|
+
# In case you did not get a copy of the license along with the software,
|
14
|
+
# it is also available at: http://www.unixgods.org/~tilo/artistic-license.html
|
15
|
+
#
|
16
|
+
# Addendum A:
|
17
|
+
# THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU!
|
18
|
+
# SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
19
|
+
# REPAIR OR CORRECTION.
|
20
|
+
#
|
21
|
+
# IN NO EVENT WILL THE COPYRIGHT HOLDERS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
|
22
|
+
# SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY
|
23
|
+
# TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
|
24
|
+
# INACCURATE OR USELESS OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
25
|
+
# TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF THE COPYRIGHT HOLDERS OR OTHER PARTY HAS BEEN
|
26
|
+
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
27
|
+
#++
|
28
|
+
# ==============================================================================
|
29
|
+
|
30
|
+
# Project Homepage: http://www.unixgods.org/~tilo/Ruby/invert_hash.html
|
31
|
+
#
|
32
|
+
# This also appears in the "Facets of Ruby" library, and is mentioned in the O'Reilly Ruby Cookbook
|
33
|
+
#
|
34
|
+
# Ruby's Hash.invert method can't handle the common case that two or more hash entries have the same value.
|
35
|
+
#
|
36
|
+
# hash.invert.invert == h # => ? # is not generally true for Ruby's standard Hash#invert method
|
37
|
+
#
|
38
|
+
# hash.inverse.inverse == h # => true # is true, even if the hash has duplicate values
|
39
|
+
#
|
40
|
+
# If you have a Math background, you would expect that performing an "invert" operation twice would result in the original hash.
|
41
|
+
#
|
42
|
+
# The Hash#inverse method provides this.
|
43
|
+
#
|
44
|
+
#
|
45
|
+
# If you want to permanently overload Ruby's original invert method, you may want to do this:
|
46
|
+
#
|
47
|
+
# class Hash
|
48
|
+
# alias old_invert invert # old Hash#invert is still accessible as Hash#old_invert
|
49
|
+
#
|
50
|
+
# def invert
|
51
|
+
# self.inverse # Hash#invert is not using inverse method
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
|
55
|
+
class Hash
|
56
|
+
|
57
|
+
# Returns a new hash, using given hash's values as keys and using keys as values.
|
58
|
+
# If the input hash has duplicate values, the resulting hash will contain arrays as values.
|
59
|
+
# If you perform inverse twice, the output is identical to the original hash.
|
60
|
+
# e.g. no data is lost.
|
61
|
+
#
|
62
|
+
# hash = { 'zero' => 0 , 'one' => 1, 'two' => 2, 'three' => 3 , # English numbers
|
63
|
+
# 'null' => 0, 'eins' => 1, 'zwei' => 2 , 'drei' => 3 } # German numbers
|
64
|
+
#
|
65
|
+
# # Hash#inverse keeps track of duplicates, and preserves the input data
|
66
|
+
#
|
67
|
+
# hash.inverse # => { 0=>["null", "zero"], 1=>["eins", "one"], 2=>["zwei", "two"], 3=>["drei", "three"] }
|
68
|
+
#
|
69
|
+
# hash.inverse.inverse # => { "null"=>0, "zero"=>0, "eins"=>1, "one"=>1, "zwei"=>2, "two"=>2, "drei"=>3, "three"=>3 }
|
70
|
+
#
|
71
|
+
# hash.inverse.inverse == hash # => true # works as you'd expect
|
72
|
+
#
|
73
|
+
# # In Comparison:
|
74
|
+
# #
|
75
|
+
# # the standard Hash#invert loses data when dupclicate values are present
|
76
|
+
#
|
77
|
+
# hash.invert # => { 0=>"null", 1=>"eins", 2=>"zwei", 3=>"drei" }
|
78
|
+
# hash.invert.invert # => { "null"=>0, "eins"=>1, "zwei"=>2, "drei"=>3 } # data is lost
|
79
|
+
#
|
80
|
+
# hash.invert.invert == hash # => false # oops, data was lost!
|
81
|
+
#
|
82
|
+
|
83
|
+
def inverse
|
84
|
+
i = Hash.new
|
85
|
+
self.each_pair{ |k,v|
|
86
|
+
if (v.class == Array)
|
87
|
+
v.each{ |x|
|
88
|
+
if i.has_key?(x)
|
89
|
+
i[x] = [k,i[x]].flatten
|
90
|
+
else
|
91
|
+
i[x] = k
|
92
|
+
end
|
93
|
+
}
|
94
|
+
else
|
95
|
+
if i.has_key?(v)
|
96
|
+
i[v] = [k,i[v]].flatten
|
97
|
+
else
|
98
|
+
i[v] = k
|
99
|
+
end
|
100
|
+
end
|
101
|
+
}
|
102
|
+
return i
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
#--
|
109
|
+
#
|
110
|
+
# require 'active_support'
|
111
|
+
#
|
112
|
+
# class Hash
|
113
|
+
#
|
114
|
+
# def inverse
|
115
|
+
# i = ActiveSupport::OrderedHash.new
|
116
|
+
# self.each_pair{ |k,v|
|
117
|
+
# if (v.class == Array)
|
118
|
+
# v.each{ |x|
|
119
|
+
# i[x] = i.has_key?(x) ? [i[x],k].flatten : k
|
120
|
+
# }
|
121
|
+
# else
|
122
|
+
# i[v] = i.has_key?(v) ? [i[v],k].flatten : k
|
123
|
+
# end
|
124
|
+
# }
|
125
|
+
# return i
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# module ID3
|
2
|
+
# module Helpers
|
3
|
+
|
4
|
+
# ------------------------------------------------------------------------------
|
5
|
+
# recursiveDirectoryDescend
|
6
|
+
# do action for files matching regexp
|
7
|
+
#
|
8
|
+
# could be extended to array of (regexp,action) pairs
|
9
|
+
#
|
10
|
+
def recursive_dir_descend(dir,regexp,action)
|
11
|
+
# print "dir : #{dir}\n"
|
12
|
+
|
13
|
+
olddir = Dir.pwd
|
14
|
+
dirp = Dir.open(dir)
|
15
|
+
Dir.chdir(dir)
|
16
|
+
pwd = Dir.pwd
|
17
|
+
@dirN += 1
|
18
|
+
|
19
|
+
for file in dirp
|
20
|
+
file.chomp
|
21
|
+
next if file =~ /^\.\.?$/
|
22
|
+
filename = "#{pwd}/#{file}"
|
23
|
+
|
24
|
+
if File::directory?(filename)
|
25
|
+
recursive_dir_descend(filename,regexp,action)
|
26
|
+
else
|
27
|
+
@fileN += 1
|
28
|
+
if file =~ regexp
|
29
|
+
# evaluate action
|
30
|
+
eval action
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
Dir.chdir(olddir)
|
35
|
+
end
|
36
|
+
# ------------------------------------------------------------------------------
|
37
|
+
|
38
|
+
# end
|
39
|
+
# end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'id3/frame_array'
|
2
|
+
|
3
|
+
# ==============================================================================
|
4
|
+
# Class RestrictedOrderedHash
|
5
|
+
# this is a helper Class for ID3::Frame
|
6
|
+
#
|
7
|
+
# this is a helper Class for GenericTag
|
8
|
+
#
|
9
|
+
# this is from 2002 .. new Ruby Versions now have "OrderedHash" .. but I'll keep this class for now.
|
10
|
+
|
11
|
+
class RestrictedOrderedHash < ActiveSupport::OrderedHash
|
12
|
+
|
13
|
+
attr_accessor :locked
|
14
|
+
|
15
|
+
def lock
|
16
|
+
@locked = true
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@locked = false
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
alias old_store []=
|
25
|
+
|
26
|
+
def []= (key,val)
|
27
|
+
if self[key]
|
28
|
+
# self.old_store(key,val) # this would overwrite the old_value if a key already exists (duplicate ID3-Frames)
|
29
|
+
|
30
|
+
# strictly speaking, we only need this for the ID3v2 Tag class Tag2:
|
31
|
+
if self[key].class != ID3::FrameArray # Make this ID3::FrameArray < Array
|
32
|
+
old_value = self[key]
|
33
|
+
new_value = ID3::FrameArray.new
|
34
|
+
new_value << old_value # make old_value a FrameArray
|
35
|
+
self.old_store(key, new_value )
|
36
|
+
end
|
37
|
+
self[key] << val
|
38
|
+
|
39
|
+
else
|
40
|
+
if @locked
|
41
|
+
# we're not allowed to add new keys!
|
42
|
+
raise ArgumentError, "You can not add new keys! The ID3-frame #{@name} has fixed entries!\n" +
|
43
|
+
" valid key are: " + self.keys.join(",") +"\n"
|
44
|
+
else
|
45
|
+
self.old_store(key,val)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# users can not delete entries from a locked hash..
|
51
|
+
|
52
|
+
alias old_delete delete
|
53
|
+
|
54
|
+
def delete(key)
|
55
|
+
if !@locked
|
56
|
+
old_delete(key)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
# ==============================================================================
|
63
|
+
# Class RestrictedOrderedHashWithMultipleValues
|
64
|
+
# this is a helper Class for ID3::Frame
|
65
|
+
#
|
66
|
+
# same as the parent class, but if a key is already present, it stores multiple values as an Array of values
|
67
|
+
|
68
|
+
# class RestrictedOrderedHashWithMultipleValues < RestrictedOrderedHash
|
69
|
+
# alias old_store2 []=
|
70
|
+
|
71
|
+
# # if key already in Hash, then replace value with [ old_value ] and append new value to it.
|
72
|
+
# def []= (key,val)
|
73
|
+
|
74
|
+
# puts "Key: #{key} , Val: #{val} , Class: #{self[key].class}"
|
75
|
+
|
76
|
+
# if self[key]
|
77
|
+
# if self[key].class == ID3::Frame
|
78
|
+
# old_value = self[key]
|
79
|
+
# self[key] = [ old_value ]
|
80
|
+
# end
|
81
|
+
# self[key] << value
|
82
|
+
# else
|
83
|
+
# self.old_store2(key,val)
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
|
87
|
+
# end
|
88
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# ==============================================================================
|
2
|
+
# Loading Libraries and Stuff needed for Ruby 1.9 vs 1.8 Compatibility
|
3
|
+
# ==============================================================================
|
4
|
+
# the idea here is to define a couple of go-between methods for different classes
|
5
|
+
# which are differently defined depending on which Ruby version it is -- thereby
|
6
|
+
# abstracting from the particular Ruby version's API of those classes
|
7
|
+
|
8
|
+
if RUBY_VERSION >= "1.9.0"
|
9
|
+
require "digest/md5"
|
10
|
+
require "digest/sha1"
|
11
|
+
include Digest
|
12
|
+
|
13
|
+
require 'fileutils' # replaces ftools
|
14
|
+
include FileUtils::Verbose
|
15
|
+
|
16
|
+
class File
|
17
|
+
def read_bytes(n) # returns a string containing bytes
|
18
|
+
# self.read(n)
|
19
|
+
# self.sysread(n)
|
20
|
+
self.bytes.take(n)
|
21
|
+
end
|
22
|
+
def write_bytes(bytes)
|
23
|
+
self.syswrite(bytes)
|
24
|
+
end
|
25
|
+
def get_byte
|
26
|
+
self.getbyte # returns a number 0..255
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
ZEROBYTE = "\x00".force_encoding(Encoding::BINARY) unless defined? ZEROBYTE
|
31
|
+
|
32
|
+
else # older Ruby versions:
|
33
|
+
require 'rubygems'
|
34
|
+
|
35
|
+
require "md5"
|
36
|
+
require "sha1"
|
37
|
+
|
38
|
+
require 'ftools'
|
39
|
+
def move(a,b)
|
40
|
+
File.move(a,b)
|
41
|
+
end
|
42
|
+
|
43
|
+
class String
|
44
|
+
def getbyte(x) # when accessing a string and selecting x-th byte to do calculations , as defined in Ruby 1.9
|
45
|
+
self[x] # returns an integer
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class File
|
50
|
+
def read_bytes(n)
|
51
|
+
self.read(n) # should use sysread here as well?
|
52
|
+
end
|
53
|
+
def write_bytes(bytes)
|
54
|
+
self.write(bytes) # should use syswrite here as well?
|
55
|
+
end
|
56
|
+
def get_byte # in older Ruby versions <1.9 getc returned a byte, e.g. a number 0..255
|
57
|
+
self.getc # returns a number 0..255
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
ZEROBYTE = "\0" unless defined? ZEROBYTE
|
62
|
+
end
|