Imported File#ftype spec from rubyspecs.
[rbx.git] / lib / iconv.rb
blob45660890ddca19504f475ec5c7aed8c978cf4d02
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       rest.each_with_index do |x, i|
97         begin
98           converted << cd.iconv(x)
99         rescue Failure => e
100           converted << e.success
101           raise e.class.new(e.message, converted, [e.failed] + rest[i + 1..-1])
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