Updated RubySpec source to d6754b35 except language/def_spec.rb.
[rbx.git] / lib / iconv.rb
blob8148f9baf5d97587ab80df5e341ea76cfa7076c6
1 # Simple $5 Iconv implementation using FFI
4 class Iconv
6   set_ffi_lib FFI::USE_THIS_PROCESS_AS_LIBRARY, "libiconv"
8   attach_function "iconv_open",  :create,  [:string, :string], :pointer
9   attach_function "iconv_close", :close, [:pointer],         :int
10   attach_function "iconv", :convert, [:pointer, :pointer, :pointer, :pointer, :pointer], :long
12   module Failure
13     attr_reader :success
14     attr_reader :failed
15   
16     def initialize(mesg, success, failed)
17       super(mesg)
18       @success = success
19       @failed = failed
20     end
22     def inspect()
23       "<##{self.class}: #{success.inspect}, #{failed.inspect}>"
24     end
25   end
27   class BrokenLibrary < RuntimeError
28     include Failure
29   end
31   class IllegalSequence < ArgumentError
32     include Failure
33   end
35   class InvalidCharacter < ArgumentError
36     include Failure
37   end
39   class InvalidEncoding < ArgumentError
40     include Failure
41   end
43   class OutOfRange < RuntimeError
44     include Failure
45   end
47   def string_value(obj)
48     if obj.instance_of? String
49       obj
50     else
51       if obj.respond_to? :to_str
52         obj.to_str
53       else
54         raise TypeError.new "can't convert #{obj.class} into String"
55       end
56     end
57   end
59   def initialize(to, from)
60     @to, @from = string_value(to), string_value(from)
61     @handle = Iconv.create @to, @from
62     begin
63       Errno.handle if @handle.address == -1
64     rescue Errno::EINVAL
65       raise InvalidEncoding.new("invalid encoding (#{@to.inspect}, #{@from.inspect})", nil, [@to, @from])
66     end
67     @closed = false
68   end
70   def close
71     return if @closed
72     
73     begin
74       iconv nil
75     ensure
76       @closed = true
77       Errno.handle if Iconv.close(@handle) != 0
78     end
79   end
81   def self.open(to, from)
82     cd = new(to, from)
83     return cd unless block_given?
85     begin
86       yield cd
87     ensure
88       cd.close
89     end
90   end
92   def self.iconv(to, from, *rest)
93     converted = []
95     open(to, from) do |cd|
96       while x = rest.shift
97         begin
98           converted << cd.iconv(x)
99         rescue Failure => e
100           converted << e.success
101           raise e.class.new(e.message, converted, rest.unshift(e.failed))
102         end
103       end
104     end
105     
106     converted
107   end
109   def self.conv(to, from, str)
110     begin
111       iconv(to, from, str).join
112     rescue Failure => e
113       # e.success is nil if open throws InvalidEncoding
114       # e.success is an Array if #iconv throws any Failure
115       raise e.class.new(e.message, (e.success.instance_of? Array) ? e.success.join : e.success, e.failed)
116     end
117   end
119   def get_success(os, l2)
120     os.read_string(l2.read_int - os.address)
121   end
123   def get_failed(is, ic, l1)
124     (is + (l1.read_int - is.address)).read_string(ic.read_long)
125   end
127   private :get_success
128   private :get_failed
130   def iconv(str, start = 0, length = -1)
132     start = 0 if not start
133     length = -1 if not length
135     raise ArgumentError.new("closed iconv") if @closed
137     l1 = MemoryPointer.new(:pointer)
138     l2 = MemoryPointer.new(:pointer)
140     ic = MemoryPointer.new(:long)
141     if str then
143       if not str.instance_of? String then
144         if str.respond_to? :to_str then
145           str = str.to_str
146         else
147           raise TypeError.new "can't convert #{str.class} into String"
148         end
149       end
151       is = MemoryPointer.new(str.size + 10)
152       is.write_string str, str.size
154       l1.write_long is.address
156       start += str.size if start < 0
157       if start < 0 || start >= str.size
158         length = 0
159       else
160         length += str.size + 1 if length < 0
161         if length < 0
162           length = 0
163         else
164           length -= start
165           if length < 0
166             length = 0
167           else
168             l1.write_long(is.address + start)
169             length = str.size if length > str.size
170           end
171         end
172       end
174       ic.write_long length
176     else # if str is nil, reset the shift state
177       ic.write_long 0
178       l1.write_long 0
179     end
182     # Totally made up metric
183     output = 1024
184     os = MemoryPointer.new(output)
185     oc = MemoryPointer.new(:long)
187     result = ""
189     loop do
190       oc.write_long output
191       l2.write_long os.address
193       count = Iconv.convert @handle, l1, ic, l2, oc
195       if oc.read_long < 0 || oc.read_long > output then
196         raise OutOfRange.new("bug?(output length = #{output - oc.read_long})", get_success(os, l2), get_failed(is, ic, l1))
197       end
199       if count == -1 then
200         begin
201           Errno.handle if count == -1
202         rescue Errno::EILSEQ => e
203           raise IllegalSequence.new(nil, get_success(os, l2), get_failed(is, ic, l1))
204         rescue Errno::E2BIG => e
205           result += get_success(os, l2)
206           next
207         rescue Errno::EINVAL => e
208           failed = get_failed(is, ic, l1)
209           raise InvalidCharacter.new(failed.inspect, get_success(os, l2), failed)
210         rescue RuntimeError => e
211           raise BrokenLibrary.new(nil, get_success(os, l2), get_failed(is, ic, l1))
212         end
213       elsif ic.read_long > 0 then
214         raise IllegalSequence.new(nil, get_success(os, l2), get_failed(is, ic, l1))
215       end
217       result += get_success(os, l2)
218       break
219     end
221     result
223   ensure
224     [l1, l2, is, ic, os, oc].each do |mp|
225       mp.free if mp
226     end
227   end