#! /your/favourite/path/to/ruby # -*- coding: utf-8 -*- # Copyright (c) 2014 Urabe, Shyouhei. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # - Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # - Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # # - Neither the name of Internet Society, IETF or IETF Trust, nor the # names of specific contributors, may be used to endorse or promote # products derived from this software without specific prior written # permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # The Strings, as described in RFC7159 section 7. class RFC7159::String < RFC7159::Value # Parse the AST from parser, and convert into corrsponding values. # @param [::Array] ast the AST, generated by the parser # @return [String] evaluated instance # @raise [ArgumentError] malformed input def self.from_ast ast type, *ary = *ast raise ArgumentError, "not an object: #{ast.inspect}" if type != :string new ary end # @return [::String] converte string def plain_old_ruby_object return @str end alias to_s plain_old_ruby_object alias to_str plain_old_ruby_object # @return [::String] the string, escaped def inspect sprintf "#<%p:%#016x %p>", self.class, self.object_id << 1, @str end # For pretty print # @param [PP] pp the pp def pretty_print pp hdr = sprintf '#<%p:%#016x', self.class, self.object_id << 1 pp.group 1, hdr, '>' do pp.text ' ' @str.pretty_print pp end end # @return [string] original string def to_json *; # Here '"', which is UTF-8, and @orig, which might be UTF-16, should be # aligned. We take UTF-8 because we are not interested in generating # UTF-16 JSON and so on. '"' << @orig.flatten.join('').encode(Encoding::UTF_8) << '"' end # String comparisons are defined in RFC7159 section 8.3. We follow that. def == other self.to_str == other.to_str rescue NoMethodError return false end private private_class_method:new # @private def initialize ary @orig = ary enc = ary[0][0].encoding rescue Encoding::US_ASCII # empty string path1 = ary.map do |i| case i when Array # ['\\', 'u', 'F', 'F', 'E', 'E'] or something case i[1].encode(Encoding::US_ASCII) when "\x22" then 0x0022 # " quotation mark U+0022 when "\x5C" then 0x005C # \ reverse solidus U+005C when "\x2F" then 0x002F # / solidus U+002F when "\x62" then 0x0008 # b backspace U+0008 when "\x66" then 0x000C # f form feed U+000C when "\x6E" then 0x000A # n line feed U+000A when "\x72" then 0x000D # r carriage return U+000D when "\x74" then 0x0009 # t tab U+0009 when "\x75" then # uXXXX U+XXXX i[2..5].join.encode(Encoding::US_ASCII).to_i 16 else raise "invalid escape: #{i.inspect}" end else i.ord end end # RFC7159 section 8.1 states that the JSON text itself shall be written # in a sort of Unicode. However the parsed JSON value's content strings # are not always Unicode-valid, according to its section 8.2. Then what? # It says nothing. Here, we try to preserve the JSON text's encoding # i.e. if the JSON text is in UTF-16, we try UTF-16. If that doesn't # fit, we give up and take BINARY. buf = nil path2 = path1.each_with_object Array.new do |i, r| if buf.nil? next buf = i else case buf when 0xD800..0xDBFF case i when 0xDC00..0xDFFF # valid surrogate pair utf16str = [buf, i].pack 'nn' utf16str.force_encoding Encoding::UTF_16BE r << utf16str[0].ord buf = nil # consumed else # buf is a garbage r << buf buf = i end else # buf is a normal char r << buf buf = i end end end path2 << buf if buf # buf might remain path3 = path2.each_with_object ''.b do |i, r| case enc when Encoding::UTF_32BE then j = [i].pack 'N' when Encoding::UTF_32LE then j = [i].pack 'V' when Encoding::UTF_16BE then j = [i].pack 'n' when Encoding::UTF_16LE then j = [i].pack 'v' else j = [i].pack 'U' # sort of UTF-8 end r << j.b end path4 = path3.dup.force_encoding enc # @str = path4.valid_encoding? ? path4 : path3 @str = path4 @str.freeze end end # # Dialogue about evaluating JSON's string # ---- # 2014.03.17.txt:20:50:01 >#ruby-ja@ircnet:shyouhei < JSONのRFC、文字列が"\uDEAD"とかなっててもvalidだよって書いてあるけど、 # 2014.03.17.txt:20:50:14 >#ruby-ja@ircnet:shyouhei < それはいいのだが # 2014.03.17.txt:20:50:32 >#ruby-ja@ircnet:shyouhei < たとえばそのJSONがUTF-16で書かれているとして # 2014.03.17.txt:20:50:59 >#ruby-ja@ircnet:shyouhei < UTF-16の"\uDEAD"的なのをRubyで作ろうと思うとなかなかむずかしいな # 2014.03.17.txt:20:51:55 >#ruby-ja@ircnet:shyouhei < "\\uDEAD"という文字列(ただしUTF-16)を入力したら"\u{DEAD}"という文字列(ただしUTF-16)を出力する関数 # 2014.03.17.txt:20:52:08 >#ruby-ja@ircnet:shyouhei < むずい。 # 2014.03.17.txt:20:52:09 <#ruby-ja@ircnet:nurse > "\xDE\xAD".force_encoding("utf-16be")とかになっちゃいますなぁ # 2014.03.17.txt:20:52:34 <#ruby-ja@ircnet:nurse > [0xDEAD].pack("n").force_encoding("utf-16be")のが素直かな # 2014.03.17.txt:20:53:35 >#ruby-ja@ircnet:shyouhei < なんか実務上はそこまでがんばるより例外で死んだ方がしあわせになれそうではある # 2014.03.17.txt:20:54:00 >#ruby-ja@ircnet:shyouhei < 誰も幸せにしなさそう # 2014.03.17.txt:20:54:26 <#ruby-ja@ircnet:nurse > 死んじゃダメで、ゲタにするのが正解じゃないっけ # 2014.03.17.txt:20:54:54 >#ruby-ja@ircnet:shyouhei < それがより正しそうですね # 2014.03.17.txt:20:55:56 >#ruby-ja@ircnet:shyouhei < JSONはサロゲートペアもなんとかせねばならんので面倒そうだ # 2014.03.17.txt:20:57:06 >#ruby-ja@ircnet:shyouhei < (\uXYZW が単体でNGぽいくても次にサロゲートペアが続くかもしれん) # 2014.03.17.txt:20:57:37 >#ruby-ja@ircnet:shyouhei < めんどう! # 2014.03.17.txt:20:57:42 >#ruby-ja@ircnet:shyouhei < UTF16しねばいいのに # 2014.03.17.txt:20:59:06 <#ruby-ja@ircnet:nurse > とりあえずそのままUTF-16にしてみて、encodeでinvalid replaceすればいい気がする # 2014.03.17.txt:21:00:33 >#ruby-ja@ircnet:shyouhei < すでにUTF16な文字列にサロゲートペアの片割れ的なバイナリをがしょがしょって後ろから足してからencodeするとよしなにする? # 2014.03.17.txt:21:01:13 >#ruby-ja@ircnet:shyouhei < (頭の悪い発言なのは自覚しております) # 2014.03.17.txt:21:01:29 <#ruby-ja@ircnet:nurse > invalid: :replaceつけてUTF-8にするなり、UTF-16のままscrubすれば # 2014.03.17.txt:21:02:45 >#ruby-ja@ircnet:shyouhei < invalidなのはよいとして "\uFOO\uBAR" てきなサロゲートペアてきJSON文字列をちゃんとRuby的に(正しいUTF16文字列)に復元するシナリオ # 2014.03.17.txt:21:03:46 <#ruby-ja@ircnet:nurse > たぶんAScii-8BITで足さないとエラーになる気がする # 2014.03.17.txt:21:04:05 <#ruby-ja@ircnet:nurse > そこいがいは、無心につなげて、最後にencodeまたはscrubが正解ではないかと # 2014.03.17.txt:21:04:13 >#ruby-ja@ircnet:shyouhei < あきらめて全部バイナリと思ってくっつけておいてから最後にencodeか # 2014.03.17.txt:21:05:20 <#ruby-ja@ircnet:nurse > ASCII-8BITだと文字列のvalidチェックしない分速いし。 # 2014.03.17.txt:21:06:33 >#ruby-ja@ircnet:shyouhei < 世の中のJSONパーザがUTF16サポートしないという姿勢にはそれなりの理由があることがわかった。 # 2014.03.17.txt:21:07:17 <#ruby-ja@ircnet:nurse > そもそもHTTPで文字列流すのにASCII非互換ってのが邪悪である # 2014.03.17.txt:21:15:04 <#ruby-ja@ircnet:nurse > 例のOpenBSDのsignifyをportableにしたらRubyでも使えるかなぁ # 2014.03.17.txt:21:18:39 <#ruby-ja@ircnet:nurse > ていうか卜部さんはJSONパーサでも書いてるのかしら # 2014.03.17.txt:21:18:56 <#ruby-ja@ircnet:nurse > って、聞いちゃいけない質問な気がした # ---- # 2014.03.25.txt:16:08:14 >#ruby-ja@ircnet:shyouhei < "\u{dead}" を入力されたときに "\\uDEAD" を出力する関数を作成せよ # 2014.03.25.txt:16:09:21 >#ruby-ja@ircnet:shyouhei < str.force_encoding('utf-8').scrub {|c| "\\u" + c.unpack('H*") } はだめぽい # 2014.03.25.txt:16:14:13 >#ruby-ja@ircnet:shyouhei < primitive_convertでなんとかなるのかこれ # 2014.03.25.txt:16:20:10 <#ruby-ja@ircnet:n0kada > "\u{dead}"ってinvalidなんだっけ # 2014.03.25.txt:16:22:29 >#ruby-ja@ircnet:shyouhei < サロゲートペアのかたほう # 2014.03.25.txt:16:22:44 >#ruby-ja@ircnet:shyouhei < それだけではinvalidすね # 2014.03.25.txt:16:34:47 >#ruby-ja@ircnet:shyouhei < お、"\u{dead}".unpack('U*')で0xdeadが取得できる # 2014.03.25.txt:16:34:57 >#ruby-ja@ircnet:shyouhei < ここからなんとかすればいいのか…? # 2014.03.25.txt:16:35:00 >#ruby-ja@ircnet:shyouhei < しかしどうする # 2014.03.25.txt:16:35:08 <#ruby-ja@ircnet:akr > "\u{dead}".unpack("U*").map {|c| 0xD800 <= c && c <= 0xDFFF ? "\\u%04X" % c : [c].pack("U") }.join # 2014.03.25.txt:16:38:16 >#ruby-ja@ircnet:shyouhei < おお。 # 2014.03.25.txt:16:38:46 >#ruby-ja@ircnet:shyouhei < scrubでなんとかするのは筋が悪いことが分かりつつある # 2014.03.25.txt:16:39:36 >#ruby-ja@ircnet:shyouhei < まずは文字列じゃなくてコードポイントの配列にして、そこでごにょってから、さいごに文字列になおすのが色々正しい雰囲気を感じる # 2014.03.25.txt:16:39:53 <#ruby-ja@ircnet:akr > encoding が壊れている時に、文字の範囲を確定するのは難しいので。 # 2014.03.25.txt:16:43:08 <#ruby-ja@ircnet:n0kada > unpackはサロゲートペアの片割れも扱える仕様なんだっけ # 2014.03.25.txt:16:43:41 <#ruby-ja@ircnet:akr > 仕様かどうかは知らない # 2014.03.25.txt:16:44:36 <#ruby-ja@ircnet:akr > 伝統的に寛大だったとは思う # 2014.03.25.txt:16:45:41 (#ruby-ja@ircnet:n0kada ) $ grep -r surrogate spec/rubyspec/core/string/unpack/ # 2014.03.25.txt:16:45:42 (#ruby-ja@ircnet:n0kada ) bash: exit 1 # 2014.03.25.txt:16:46:06 <#ruby-ja@ircnet:n0kada > rubyspecが持ってないとは意外だな # 2014.03.25.txt:16:46:18 <#ruby-ja@ircnet:n0kada > こういう重箱の隅はお得意だろうに # # Local Variables: # mode: ruby # coding: utf-8-unix # indent-tabs-mode: t # tab-width: 3 # ruby-indent-level: 3 # fill-column: 79 # default-justification: full # End: