MacZip4Win - ShiftJISでZIPアーカイブを作成してくれるソフト

taimamiso2009-01-27


Ranaとの日々: MacZip4Win

本当に本当に素晴らしいソフト。日本人Macユーザ待望のソフト。これでWindowsを立ち上げずして、zipが送れます。作者様に大感謝。

なお、私はMacPortsPerlを入れて、システムのPerl(/usr/bin/perl)を置き換えていたので、

Applescript error: Can't locate Zip.pm in @INC ...

というようなエラーが出た。ZIPモジュールをインストールして解決。

$ sudo port install p5-archive-zip

TMail パーサのデバッグ方法

あれ!? TMail のメール解析がおかしいぞ!? というときは、パーサまで手を入れないといけないかも知れない。そのデバッグの仕方を書いておく。

パーサのデバッグフラグをONにする

TMailを使うスクリプトの中で、

require "tmail"

TMail::Parser.const_set(:MAILP_DEBUG, true)    # <== ココ
mail = TMail::Mail.parse(IO.read(mailfile))

こうするとデバッグ情報が出力される。また通常は、ヘッダの中身を解析していてエラーが起こった場合は、通常何も出力されずに無視されて、エンコード時にそのヘッダが捨てられてしまう。デバッグをONにすると、中断こそしないがそのエラーも出力される。

まずこれで実行して、構文解析エラーが原因なのかどうかが調べてみるとよい。エラーメッセージだけで、対応方法が分かる場合がある。

パーサの中の、どの解析ルールが原因なのかを調べないといけない場合は、次のステップに進もう。

raccのインストール

gem install racc

デバッグ情報付きパーサの生成

TMailのパーサは、 lib/tmail/parser.rb である。これは、ルール定義ファイル parser.y から、racc によって自動生成される。

cd tmail-1.2.3.1
racc -t -o lib/tmail/parser.rb lib/tmail/parser.y

オプション"-t"はデバッグ情報付きのパーサが生成される。

ふたたび実行

デバッグ情報入りのパーサだと、豊富な情報がたくさん出てくる。

(例)解析部分:

Content-Type: multipart/mixed; 

(例)出力:

read    :CTYPE(CTYPE) :CTYPE
shift   CTYPE
[ (CTYPE :CTYPE) ]
goto    12
[ 0 12 ]
read    :TOKEN(TOKEN) "multipart"
shift   TOKEN
[ (CTYPE :CTYPE) (TOKEN "multipart") ]
goto    62
[ 0 12 62 ]
read    "/"("/") "/"
shift   "/"
[ (CTYPE :CTYPE) (TOKEN "multipart") ("/" "/") ]
goto    99
[ 0 12 62 99 ]
read    :TOKEN(TOKEN) "mixed"
shift   TOKEN
[ (CTYPE :CTYPE) (TOKEN "multipart") ("/" "/") (TOKEN "mixed") ]
goto    126
[ 0 12 62 99 126 ]
reduce  <none> --> params
[ (CTYPE :CTYPE) (TOKEN "multipart") ("/" "/") (TOKEN "mixed") (params {}) ]
goto    136
[ 0 12 62 99 126 136 ]
read    ";"(";") ";"
shift   ";"
[ (CTYPE :CTYPE) (TOKEN "multipart") ("/" "/") (TOKEN "mixed") (params {}) (";" ";") ]
  • read TOKEN_CONTENT … スキャナがトークンとその中身 TOKEN_CONTENT を読み取った。(パーサは、スキャナによって文字列をトークンに分解したものを処理する)
  • shift TOKEN … パーサが次のトークン TOKEN を引っ張ってきた。
  • goto RULE … 他のルール RULE に飛んだ
  • reduce FROM --> TO … ルール FROM からルール TO に戻ってきた。
  • [ (CTYPE :CTYPE) ... ] … 解析している階層のトレース(うまく説明できない)

パーサを修正する

原因となるルールが特定できたら、 lib/tmail/parser.y を修正する。(またはデバッグ出力を入れる)そして再び上記のraccを実行して、再度実行してみる…を繰り返す。

修正が完了したら、

cd tmail-1.2.3.1
racc -E -o lib/tmail/parser.rb lib/tmail/parser.y

デバッグ情報は取り除こう。ちなみに -E はraccライブラリを parser.rb に埋め込むスイッチ。

発見した TMail-1.2.3.1 のバグ各種

TMail-1.2.3.1 のバグを色々直した。それぞれ詳しいことは以下。各修正を適用したTMailのgemをダウンロードに置いてあるので、手っ取り早く使いたい方は、これをダウンロードして、 gem install tmail-1.2.3.1.gem でインストールしてほしい。

ついでにこの gem には、leave a note [message] behind on Rails: RailsのMailer(TMail)のメールアドレスドット問題の修正も取り込ませていただいた。

正しく動作する保証は無いですが、不具合があればコメントしていただけると助かります。

Content-Typeで値がクォートを含んだまま取り込まれる

TMailプロジェクトに送信したパッチ: RubyForge: TMail: Modify: 23165 - ContentTypeHeader should unquote parameter's value on encode

例えば

Content-Type: image/jpeg; name="\e$B4A;z\e(B.jpg"

という生JISのファイル名がクォートされて指定されていると、なぜかエンコードした後に、

Content-Type: image/jpeg; name*=iso-2022-jp'ja'%22%1b$B4A%3bz%1b%28B.jpg%22

%22(")が前後にくっついてしまう。本当は、

Content-Type: image/jpeg; name*=iso-2022-jp'ja'%1b$B4A%3bz%1b%28B.jpg

でないといけない。

AppleMail 添付ファイルパートの Content-Type が解析エラー

TMailプロジェクトに送信したパッチ: RubyForge: TMail: Modify: 23681 - Fix parse error on AppleMail's bad Content-Type parameter value - unquoted but bencoded

AppleMailに日本語ファイル名を添付させてみよう。するとこんなパートが生成される。

--Apple-Mail-1-993553537
Content-Transfer-Encoding: base64
Content-Type: application/pdf;
	x-mac-type=50444620;
	x-unix-mode=0644;
	x-mac-creator=4341524F;
	name==?ISO-2022-JP?B?GyRCQXdFRTdPRX0/XhsoQi5wZGY=?=
Content-Disposition: inline;
	filename*=ISO-2022-JP''%1B%24BAwEE7OE%7D%3F%5E%1B%28B.pdf

これをパースし、このパートのcontent_typeを得ようとすると

part.content_type    => nil

nilが帰ってくる。さらに、encodedでエンコードすると、

--Apple-Mail-1-993553537
Content-Transfer-Encoding: base64
Content-Disposition: inline;
	filename*=ISO-2022-JP''%1B%24BAwEE7OE%7D%3F%5E%1B%28B.pdf

Content-Type そのものが消えてしまう。(メーラ上の動作では、本文として展開されてしまう)

実は、Content-Type をパースする際に構文エラーが発生しているのだ。 name==?ISO- の2つ目の = に出会った時点で、これは有効なトークンではないとエラーになってしまう。確かに、RFC 2045(対訳)多目的インターネットメール拡張 パート1 Content-Type ヘッダフィールドの文法 によれば、これはクォートしないといけない。 AppleMail が間違っているのだ。

Content-Type をパースする前に、このようなパターンを見つけてクォートさせてやることで解決した。

ちなみに、AppleMailに長いファイル名で添付をさせると

Content-Type: application/pdf;
	x-unix-mode=0644;
	name="=?ISO-2022-JP?B?GyRCRDkhPCQkRDkhPCQkRDkhPCQkRDkhPCQkGyhC?=
 =?ISO-2022-JP?B?GyRCRDkhPCQkRDkhPCQkRDkhPCQkRDkhPCQkGyhC?=
 =?ISO-2022-JP?B?GyRCRDkhPCQkRDkhPCQkRDkhPCQkRDkhPCQkGyhC?=
 =?ISO-2022-JP?B?GyRCRDkhPCQkRDkhPCQkRDkhPCQkGyhCLnBkZg==?="

ちゃんとクォートされる。1行で収まる場合のみ、クォートしてくれないらしい。

TMailで作成した添付ファイル名の文字化け

TMailで添付ファイル付きのメールを1から作成しようとすると、このようなスクリプトになるが、

filename = "任意の日本語ファイル名"   # JISにすること
filedata = "ファイルのデータ"

require "base64"
require "tmail"

mail = TMail::Mail.new()

part = TMail::Mail.new()
part.transfer_encoding = "7bit"
part.set_content_type('text', 'plain', 'charset'=>'iso-2022-jp')
part.body = "honbun."
mail.parts << part

part = TMail::Mail.new()
part.set_content_type('application', 'octet-stream', 'name' => filename)
part.set_disposition("attachment")
part.transfer_encoding = "base64"
part.body = Base64.encode64(filedata)
mail.parts << part

mail.encoded

filename によっては、途中でファイル名が化けたりする。

例えば、「何かチラシ.pdf」をこのスクリプトで作成すると、メーラで開いたときにはファイル名が「何か汁初酬.pdf」になってしまう。まあ笑えるからいいか、ってんなわけない。

このヘッダを見ると

Content-Type: application/octet-stream;
	name*=iso-2022-jp'ja'%1b$B2%3f$+%A%i%7%1b%28B.pdf

となっている。原因は、エンコード時にJIS文字列中の % をエスケープしてくれてないからである。正しくは、

Content-Type: application/octet-stream;
	name*=iso-2022-jp'ja'%1b$B2%3f$+%25A%25i%257%1b%28B.pdf

でないといけない。

encode.rb の encode_value の中で、 TOKEN_UNSAFE という正規表現を %xx に置き換えている。TOKEN_UNSAFEの宣言は utils.rb にある。それが使っている値である、 aspecial/tspecial に % を追加して修正した。

しかし、これだと他のメソッドにも影響が出そうだが…。ちょっと不安である。それに aspecial/tspecial の定義が、 RFC と違ってきてしまう。

このパッチはトラッカーにsubmitしていない。パッチは以下の通りである。

Index: lib/tmail/utils.rb
===================================================================
--- lib/tmail/utils.rb	(リビジョン 261)
+++ lib/tmail/utils.rb	(作業コピー)
@@ -109,8 +109,8 @@
   # It also provides methods you can call to determine if a string is safe
   module TextUtils
 
-    aspecial     = %Q|()<>[]:;.\\,"|
-    tspecial     = %Q|()<>[];:\\,"/?=|
+    aspecial     = %Q|()<>[]:;.\\,"%|
+    tspecial     = %Q|()<>[];:\\,"/?=%|
     lwsp         = %Q| \t\r\n|
     control      = %Q|\x00-\x1f\x7f-\xff|
 

AWT/Swing でエラーダイアログを表示する。(Sunの実装限定)

後でこのプロパティ名で検索したら、sun.awt.exception.handler - ある学生さんのシステム開発日記に書かれていた。

うぐぐ、 "awt 例外 キャッチ" とか "awt 例外 ダイアログ" とかで検索して見つからなかったから、諦めて調べたが…。 "awt 例外 ハンドラ" と検索すればよかったのか。(2番目に出てくる)

良い検索キーワードが閃けば、時間を節約できるなあ…。

AWT/Swingのイベントハンドラで例外が発生すると、ユーザにはそんなことは何も分からずに、ただスタックトレースが吐き出されるだけである。

これを自動的に例外が発生したら、エラーダイアログを出すなり何なりの処理をさせたいと思った。

しかし、例えば addExceptionHandler() とか、そのような目的のためのAPIは無いようだ。

"sun.awt.exception.handler" に例外処理クラスを設定

AWTの例外処理ソースを追っていくと、

  • EventDispatchThread.pumpOneEventForHierarchy で例外が catch される。
  • EventDispatchThread.processException
  • EventDispatchThread.handleException

という流れで例外が処理されている。この handleException の中のソースを見て、どうすればよいかが分かった。

次の例外処理クラスを作って、

public class AWTExceptionHandler {
	public static void setup() {
		System.setProperty("sun.awt.exception.handler", AWTExceptionHandler.class.getName());
	}
	public void handle(Throwable ex) {
		// エラー処理を書いてくれぃっ!
		ex.printStacktrace();
	}
}

アプリケーションの初期化処理の中で、

AWTExceptionHandler.setup();

を呼べばよい。

これにより、イベントハンドラで発生した例外すべてを、 handle() の中で処理できる。
もちろんこの中で、ダイアログ表示も可能。

たぶんSunの実装限定と思われるが、Mac ( AppleJVM )でも動作したので、大方問題無いだろうと思う。

ThreadGroup.uncaughtException を使う方法

ちなみに、

catch を書かなくても例外発生時にエラーメッセージを出したい - Java Solution

ThreadGroup.uncaughtException を使う方法もあるようだ。

ActiveRecord のような軽量O/Rの調査

ActiveObjects

activeobjects: ホーム
civic site : ActiveObjectsすげー

結局hibernateにしました。

全然軽量じゃねえよ、とツッコまれそう。
http://www.hibernate.org/

これにしようと沢山勉強した後で、
英語読むの大変だなあ…、エラーメッセージ分かりにくいなあ…、
とか思ってたら、
Kuina-Dao(from Seasar2)
を発見。

魅力的だ………うぬぬ、しかし、今さら乗り換えてイチから勉強できるかぁ!
既にアプリのコード、べったりhibernate依存で書いてるし。

Ramaze

Rails を使ったアプリをさくらサーバで動かそうとしたら、現状は次の方法しかない。

dispatch.cgi
CGI経由でRailsを毎回起動。むちゃくちゃ重い。
gateway.cgi
1回目の処理以降しばらく常駐するので、最初のアクセス後しばらくは速い。でもプロセスは一定時間で強制終了させられるので、あくまでもしばらく。それに、さくら側が常駐プログラム禁止ですよとしているのに、こういう抜け穴のようなやり方をしても、いずれ規制されるのではないだろうか。この手法が広がっている様子にちょっと懸念を抱く。

Rails がもっとCGI起動で軽く動くようになってくれるか、 mod_rails のようなものが登場してくれるかすれば良いのだろうが、現時点では Rails をさくらで動かすのは難しそう。

なので、代替となるWebアプリケーションフレームワークを色々と模索してたら、 Ramaze を発見。なかなか良さそうなので、これを WEBrickmongrel のような常駐式ではなく CGI で動かせるかどうか試みてみる。

[Ramaze : The Modular Web Framework] - http://ramaze.net/

Ramaze を CGI として動かす

プロジェクト(scratch)作成

ramaze --create scratch --adapter cgi

--adapter cgiとしても、生成されるのはWebrick用のソースである。

public/dispatch.cgi を作成

#!/usr/bin/env ruby

require 'rubygems'
require 'ramaze'

Ramaze::Log.loggers = [ Ramaze::Logger::Informer.new( __DIR__("../ramaze.log") ) ]
Ramaze::Global.adapter = :cgi

$ramaze_0 = __DIR__("../start.rb")
alias $0 $ramaze_0

Dir.chdir File.dirname($0)
load File.basename($0)

なんで、 $0 = __DIR__("../start.rb") ではないのかというと、ディレクトリのフルパスが長すぎる(例えば /path/to/very/long/path )と $0 に格納したら、途中で切れて( /path/to/ve )しまうからだ。

[ruby-dev:29124] Re: [ BUG ] $0 cannot keep the given string - no...@ruby-lang.org - org.ruby-lang.ruby-dev - MarkMail

なお、start.rb と同じフォルダに chdir してから start.rb に制御を移さないと、なぜか "no such action: /page" というエラーになってしまう。

(追記)今はもっとシンプルに書いて、こうしている。

#!/usr/bin/env ruby

# Go to application directory.
Dir.chdir(File.dirname($0))
Dir.chdir("..")

require 'rubygems'
require 'ramaze'

Ramaze::Log.loggers = [ Ramaze::Logger::Informer.new("ramaze.log") ]
Ramaze::Global.adapter = :cgi

$0 = "start.rb"
load $0

start.rb を編集

Ramaze.start :adapter => :webrick, :port => 7000
となっているのを単に、
Ramaze.start
としてやる。

public/.htaccess作成

RewriteEngine on
RewriteRule ^(.*)$ dispatch.cgi [QSA,L]

Illegal seek エラー?

なんかエラーになった!

Errno::ESPIPE - Illegal seek

原因箇所は:

/opt/local/lib/ruby/gems/1.8/gems/rack-0.9.1/lib/rack/request.rb:
  in rewind @env["rack.input"].rewind if @env["rack.input"].respond_to?(:rewind)

これは、

$ ruby -e "IO.pipe[0].rewind"   
-e:1:in `rewind': Illegal seek (Errno::ESPIPE)
        from -e:1

としたのと同じこと。 CGI Adapter はあんまりテストされていないのかな…。とりあえず、

Index: request.rb
===================================================================
--- request.rb	(revision 6)
+++ request.rb	(working copy)
@@ -113,7 +113,9 @@
             Utils::Multipart.parse_multipart(env)
           @env["rack.request.form_vars"] = @env["rack.input"].read
           @env["rack.request.form_hash"] = Utils.parse_query(@env["rack.request.form_vars"])
-          @env["rack.input"].rewind if @env["rack.input"].respond_to?(:rewind)
+          begin
+            @env["rack.input"].rewind if @env["rack.input"].respond_to?(:rewind)
+          rescue Errno::ESPIPE; end
         end
         @env["rack.request.form_hash"]
       else

という風に直して、

再度アクセスする

http://localhost/path/to/scratch/public/

めでたく成功〜。

Welcome to Ramaze!

Ramaze is working correctly with this application, now you can start working.

Intel Core2Duo 2.1GHz のマイマシンで、実行時間は約 0.3 秒だった。実際アプリを組めばさらに時間がかかるだろうが、これなら十分CGIとして動かせそうである。

$ command time ./scratch/public/dispatch.cgi < /dev/null > /dev/null
        0.29 real         0.23 user         0.05 sys