デコード3

 懲りずにまたちょっと違うものを書いてみる。が、あまり美しくない。

!String methodsFor: 'converting'!

urlDecode

    | highBit hex asciiVal str |
    str := ReadWriteStream on: ''.
    highBit := hex := false.

    self do: [:each |
        (each ~= $% and: [hex = false])
            ifTrue: [
                (each = $+)
                    ifTrue: [str nextPut: (Character value: 16r20)]
                    ifFalse: [str nextPut: each].
            ]
            ifFalse: [
                (each = $%)
                    ifTrue: [hex := highBit := true]
                    ifFalse: [
                        (highBit)
                            ifTrue: [
                                asciiVal := each asUppercase digitValue * 16.
                                highBit := false.
                            ]
                            ifFalse: [
                                asciiVal := each asUppercase digitValue + asciiVal.
                                asciiVal > 255 ifTrue: [^self].
                                str nextPut: (Character value: asciiVal).
                                hex := false.
                            ].
                    ].
            ].
    ].

    ^str contents.
!

前の記事で書いたやつよりはちょっとだけ速くなった。
最近、「コレクション型であればとにかく do: [〜] したがる病」にかかってる気がする・・・ 年の瀬だというのに、何をしているんだ俺は。

デコード2

 昨日の記事で書いたデコーダをもうちょっと簡単に出来ないものかと思い、ちょっと違うものを書いてみた。

!String methodsFor: 'converting'!

urlDecode
    | inData str |

    str := ReadWriteStream on: ''.
    inData := self copyReplaceAll: '+' with: '%20'.
    inData := (inData tokenize: '%').

    ((inData at: 1) ~= '' or: [(inData at: 1) = ''])
        ifTrue: [
            str nextPutAll: (inData at: 1).
            inData := inData copyFrom: 2 to: inData size.
        ].

    inData do: [:each |
        | asciiVal |
        asciiVal := (each at: 1) asUppercase digitValue * 16 +
                    (each at: 2) asUppercase digitValue.

        asciiVal > 255 ifTrue: [^self].
            str nextPut: (Character value: asciiVal).

        (2 < each size)
            ifTrue: [str nextPutAll: (each copyFrom: 3 to: each size)].
    ].

    ^str contents.
!

ちょっと簡単になった? けど、遅そうだな・・・ 計測してみる。

#!/usr/bin/env gst

| str time |
str := '大量のURLエンコード文字列'.

time := Time millisecondsToRun: [
    str urlDecode.
].
time printNl.

予想通り、めっちゃ遅いorz 昨日のやつの方が約2倍は早い。他に何か良い方法はないかなぁ。
 まぁ、なにわともあれデコード出来るようになったので、テスト的にメールフォームCGIなんぞを作ってみる。

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>てすとmailフォーム</title>
</head>
<body>
<form action="sendMail.cgi" method="POST" accept-charset="UTF-8">
  お名前<br>
  <input name="name" size="30"><br>
  メールアドレス<br>
  <input name="email" size="30"><br>
  メッセージ<br>
  <textarea name="message" rows="10" cols="30"></textarea><br>
  <input type="submit" value="送信">
  <input type="reset" value="リセット">
</form>
</body>
</html>

sendMail.cgi

#!/usr/bin/env gst -Q

stdout nextPutAll: 'Content-Type: text/plain; charset=UTF-8';nl;nl;
nextPutAll: 'ありがとうございます^^'.

PackageLoader fileInPackage: 'NetClients'.
!

| inData message client |
inData := ((stdin nextAvailable: 
    (Smalltalk getenv: 'CONTENT_LENGTH') asInteger) tokenize: '&')
    inject: Dictionary new
    into: [:dic :each |
        | array |
        array := (each tokenize: '=').
        dic at: (array at: 1)
            put: (array at: 2) urlDecode; yourself
    ].

message := NetClients.MIME.MimeEntity readFrom:
('From: %1
To: %2
Subject: %3

[%4]さんからメッセージ
%5
' % {inData at: 'email'.
    'user@hoge.domain'.
    'Message'.
    inData at: 'name'.
    inData at: 'message'}
) readStream.

client := NetClients.SMTP.SMTPClient connectToHost: 'localhost'.
[client sendMessage: message]
    ensure: [client close].
!

 本当はもっとちゃんとエラー処理とかやらなきゃならないけど、まぁ、テストなんで、こんな感じでOK.メールも問題無く送信できた。SMTPライブラリは素のままだと使い勝手が良いとは言えないので、何かラップした方がよさそう。あと、このライブラリをCGIで使う場合、 smalltalk/net/SMTP.st の139行目あたりの aMessage inspect.コメントアウトしとかないといけない。でないと、inspectの結果がstdoutに出力されてしまう。デバッグ用には良いですけどね。さらに、 smalltalk/kernel/Metaclass.st の181行目と335行目あたりの Transcript nextPutAll: 'Recompiling classes...'; nl.コメントアウトしとかないと、 PackageLoader fileInPackage: でパッケージをfileinしたときに Recompiling classes... がstdoutに出力される時がある。自分は最初その事に気付かなくて、原因不明のBad headerエラーが出るなぁとか思ったりしてたんすけど、原因はこれだった。^^;

デコード

 ホームページなどのフォームから入力された日本語文字列をデコードしたいのです。けど、gnu-Smalltalkにはそんなメソッド用意されてないみたいっす。なので、Squeakの unescapePercentsWithTextEncoding: を参考にして(ぶっちゃけパクリました)作ってみた。

!String methodsFor: 'converting'!

urlDecode
    | inData str oldPos pos patt c asciiVal |

    str := ReadWriteStream on: ''.
    oldPos := 1.
    patt := '%([a-fA-F0-9][a-fA-F0-9])' asRegex.
    inData := self copyReplaceAll: '+' with: '%20'.

    [pos := (inData indexOfRegex: patt startingAt: oldPos 
        ifAbsent: [
            (oldPos <= inData size)
                ifTrue: [
                    str nextPutAll: (inData copyFrom: oldPos to: inData size).        
                    ^str contents
                ]
                ifFalse: [^str contents].
        ]) first. 
        pos > 0
    ]
    whileTrue: [
        str nextPutAll: (inData copyFrom: oldPos to: pos - 1).
        c := inData at: pos.
        (c = $% and: [pos + 2 <= inData size])
            ifTrue: [
                [c = $%] whileTrue: [
                    asciiVal := (inData at: pos+1) asUppercase digitValue * 16 +
                                (inData at: pos+2) asUppercase digitValue.

                    asciiVal > 255 ifTrue: [^self].
                        str nextPut: (Character value: asciiVal).

                    pos := pos + 3.
                    (pos <= inData size)
                         ifTrue: [ c := inData at: pos]
                         ifFalse: [c := nil].
                ].
                oldPos := pos.
            ]
            ifFalse: [
                str nextPut: c.
                oldPos := pos + 1
            ].
    ].
!

なんだかすごく読みづらいコードですね、ごめんなさい。
 んで、次の様なテストCGIで試してみた。

#!/usr/bin/env gst -Q

"testform.cgi"

| html inData |

html := 'Content-Type: text/html; charset=UTF-8

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>gst てすとふぉーむ</title>
</head>
<body>
<h1>gst てすとふぉーむ</h1>
<form action="testform.cgi" method="POST" accept-charset="UFT-8">
  <textarea name="text" rows="10" cols="40"></textarea><br>
  <input type="submit" value="入力">
</form>
<pre>
%1
</pre>
</body>
</html>
'.

((Smalltalk getenv: 'CONTENT_LENGTH') = nil)
    ifTrue: [stdout nextPutAll: html % {''}]
    ifFalse: [
        inData := ((stdin nextAvailable: 
            (Smalltalk getenv: 'CONTENT_LENGTH') asInteger) tokenize: '=') at: 2.
        stdout nextPutAll: html % {inData urlDecode}
    ].
!

 環境に合わせて charset をEUC-JP、Shift_JISUTF-8 の三つで試してみましたが、問題無くデコードできました。それよりも問題なのは、 stdin でした。最初はローカル内のapache2系を使って stdin next: というふうに読み出していて、特になんの問題も無かったので、自分が借りてるレンタルサーバーで使われているapache1.3系でテストしてみると、何故かエラーです。エラーの詳細を調べてみると、1023バイト以上のPOSTデータを正しく読み出せていないのが原因でした。何故こんなエラーが出るのか解からないまま、とりあえず、試しに stdin contents としてみたらちゃんと読み出せた。けど、この方法だと今度はapache2系でnilエラーが出るという摩訶不思議な現象。色々試した結果、stdin nextAvailable:だとどのバージョンのapacheでもちゃんと読み出せるようなので、とりあえずOK.(いいのか?)

ブラウザプラグイン

 Squeakではブラウザ用のプラグインをインストールすると、ブラウザ上で色々遊べちゃうという面白い機能があるのですが、自分の環境(Squeak3.8-05+Ubuntu 7.04+Firefox2.0.0.10)ではこのプラグインが使えませんTT Firefoxプラグインを認識してくれないのです。最初はインストールディレクトリが間違っているのだろうと思い、ディレクトリを変更したりして試したのですが、やっぱり無理、認識してくれない。
 何が原因なのかさっぱり解からないのですが、とりあえず別のブラウザで試してみる事にした。まず、Galeonを使ってみる。→何の問題も無くプラグインを認識した。次に、Epiphanyを使ってみる。→これまた何の問題も無い。そして、Operaも使ってみた。→やはり何の問題も無い。Firefoxだけが何故か認識しない。GaleonEpiphanyは標準でFirefoxと同じプラグインディレクトリを使う様になっているのでやはりインストールディレクトリが間違っているとは思えない。謎だ。
 色々検索してみたのですが、結局謎は解明出来ませんでした。とりあえず、こんな妙な現象があったぞ という記録でした。
 

CGIのエラー

 最近からgnu-SmalltalkCGIなんぞをやってみようと思い、ちょこちょことプログラムを作っているのですが、一番最初のテストCGIを動かした時に 500 Internal Server Error が出て実行できませんでした。apacheのエラーログを見てみると、Tk_Init failed: no display name and no $DISPLAY environment variable という何故かTkのエラーです。エラーの詳細を調べようと検索してみると、どうやらこれはTcl/Tkがライブラリを見付けられなかった時に出るエラーらしいのですが、何故にCGIプログラムでこのエラーが?勿論、プログラムでTcl/Tkは使っていません。ちょっと心当たりがあるとすればCGIのテストをする前にgstのブラウザを起動させて色々遊んでいたという事ぐらいです。(gst標準のブラウザはTkを使っている)怪しいとすればこのブラウザでしょう。という訳で、起動に使われる Run.st を開いて見てみると、

.
.
.
(Smalltalk includesKey: #BLOX) ifFalse: [
    PackageLoader fileInPackage: 'Browser'.
    ObjectMemory snapshot
]!
.
.
.

 特になんの問題も無さそうなのですが、最初の起動の時にGUI関連のライブラリを読み込んでイメージに保存しているというのが何かCGIエラーに関係しているのではなかろうかと思い、ObjectMemory snapshot の部分をコメントアウトして、イメージファイルを初期状態に戻し、CGIのテスト→OKでした。その後、ブラウザを起動してCGIのテストなどなど色々してみましたが同じ様なエラーは二度と出ませんでした。
 とりあえず、今の所本当のエラーの原因までは突き止めていないのですが、こんなエラーがあったぞ という記録でした。
 このエラーが出た自分の環境:
Ubuntu 7.04(feisty)
Tcl/Tk8.4
Tcl8.4-dev/Tk8.4-dev
Apache2.2.3
GNU Smalltalk2.3.6

文字列展開

 gnu-Smalltalkでは"%"を使って文字列を展開できる事を今日はじめて知った。
 とても簡単な例で申し訳ないが、こんな感じ。

#!/usr/bin/env gst

| title content html |
title := 'てすと'.
content := 'てすとこんてんつ'.

html := 'Content-Type: text/html

<html>
<head>
<title>%1</title>
</head>
<body>
%2

%3
</body>
</html>
'
% {title. content. DateTime now}.
stdout nextPutAll: html.
!

次のように出力される。

<html>
<head>
<title>てすと</title>
</head>
<body>
てすとこんてんつ

 2007-11-29T22:36:53+09:00
</body>
</html>

 ところで、こうゆう機能の事を「文字列展開」と言うのは正解なんだろうか? 他に適切な呼び名があるような気がするけど、とりあえず今はそう呼んでる。

Tcl/Tk8.5

bitWalkさんのブログからちょっとだけ引用させていただきます。

11月19日付けで Tcl/Tk 8.5 の3番目のβ版がリリースされました。

リリース予定は 11月16日だったのですが、Don Porter 氏が忙しかったためにリリースが遅れただけのようです。今回もバグフィックスが中心で、予定では最後のβ版となります。

なお正式版 8.5.0 のリリースは、12 月 14 日の予定になっています。Tcl/Tk 8.4.0 がリリースされたのが、2002 年 9 月 10 日のことですから、マイナーバージョンの更新は、実に 5 年ぶりとなります。定義では、8.5.0 の太字の部分はマイナーバージョンなのですが、あたかもメジャーバージョンがリリースされるような印象を受けます。

 きたきた、やっと来ましたよ8.5の正式版。β版でもいいからインストールしちゃおうかなーとか思ったりもしたけれど、やはり出来れば正式版が良いので、ずっと待ってました。8.5の正式リリースが楽しみな最大の理由は、このバージョンからLinuxでもアンチエイリアスなフォントがサポートされるからなんです。(Win,Macではすでにサポートされているらしい。)最近のLinuxアプリで使われるフォントはほとんどアンチエイリアスがかかっているので、それに見慣れているとTkアプリのギザギザフォントはちょっと・・・。
 現在、自分の環境のTkフォントはこんな感じ。

お気に入りのRhinote(右側)もこんな感じ。

 おぉぉぉ・・・ なんてことでしょう。 8.5の正式版が本当に待ち遠しいです。
 ちなみに、gnu-Smalltalkのブラウザも(GUIライブラリも)Tkを使っているのでフォントはギザギザっす。TT

追記
 Tcl/Tk8.5の正式版導入しました^^