ファイルが無ければ展開して実行するJScript

UWSC公式掲示板で、exeに画像が埋め込めれば良いのに、というのを見た。
興味が沸いたので、解決策を考えてみた。


思想としては、

  • ファイルがなければ、自ファイルから展開する
  • 展開したファイルを実行する

まずは解決策

例えば後で掲載するスクリプトを、CreateRunMaker.jsというファイル名で保存したなら、コマンドプロンプト等で、

cscript //nologo CreateRunMaker.js a.js b b.exe c.bmp

といった感じで実行する。
引数の「a.js b b.exe c.bmp」は、

No 意味
1 a.js 出力ファイル名
2 b 実行コマンド
3 b.exe No.3以降は出力ファイルに付与するファイル名
4 c.bmp 同上

b.exe / c.bmp は、見つかるところにないとエラーになります。(同一フォルダが基本)
両方がちゃんとあると、a.js が作成されます。
その a.js を実行すると、b.exe / c.bmp がなければ作成し、「b」(すなわちb.exe)を実行します。


以下がCreateRunMaker.js

WScript.Quit((function() {

	// 出力ファイル名、コマンドとファイルを受け取る
	if(WScript.Arguments.length < 3) {
		WScript.Echo("args : (out path) (cmd) (file) [(file) ...]");
		return 1;
	}

	// 実行用スクリプト
	var script = "WScript.Quit((function(){var d=WScript.CreateObject('Microsoft.XMLDOM').createElement('tmp');d.dataType='bin.hex';var e=WScript.CreateObject('ADODB.Stream');e.Open();e.Charset='Shift-JIS';e.Type=1;var f=function(b){d.nodeTypedValue=b;var a=d.text;var c=0,m=a.length;for(var i=0;i<m;i+=2){c*=256;c+=parseInt(a.substr(i,2),16)};return c};var g=function(){offset-=4;k.Position=offset;return f(k.Read(4))};var h=function(a){offset-=a;k.Position=offset;e.Position=0;e.SetEOS();e.Type=1;e.Write(k.Read(a));e.Position=0;e.Type=2;return e.ReadText()};var j=function(a,b){offset-=b;k.Position=offset;e.Position=0;e.SetEOS();e.Type=1;e.Write(k.Read(b));e.SaveToFile(a,2)};var k=WScript.CreateObject('ADODB.Stream');k.Open();k.Charset='iso-8859-1';k.Type=1;k.LoadFromFile(WScript.ScriptFullName);var l=k.Size,offset=l;var n=g();if(n!=0x6a756e6a){WScript.Echo('Script format error! :mark');return 1};n=g();if(n>offset){WScript.Echo('Script format error! :cmd len='+n);return 2};var o=h(n);var p=g();if(p>l){WScript.Echo('Script format error! :allNum='+p);return 3};var q=WScript.CreateObject('Scripting.FileSystemObject');for(var i=0;i<p;i++){var r=g();var s=h(r);var t=g();if(q.FileExists(s)){offset-=t}else{j(s,t)}};k.Close();k=null;q=null;e.Close();e=null;d=null;return WScript.CreateObject('WScript.Shell').run(o,1,false)})());";
	// バイト配列操作用オブジェクト
	var binMan = WScript.CreateObject('Microsoft.XMLDOM').createElement('tmp');
	binMan.dataType = "bin.hex";
	// 文字列変換用オブジェクト
	var strMan = WScript.CreateObject('ADODB.Stream');
	strMan.Open();
	strMan.Charset = "Shift-JIS";
	strMan.Type = 1;	// Binary

	// 数値をバイト配列化して書き込む関数(BigEndian)
	var writeInt = function(i) {
		binMan.text = ("0000000" + i.toString(16)).slice(-8);
		outs.Write(binMan.nodeTypedValue);
	}
	// 文字列書き込みする関数
	var writeStr = function(str, bNum) {
		strMan.Position = 0;
		strMan.SetEOS();
		strMan.Type = 2;	// Text
		strMan.WriteText(str);
		strMan.Position = 0;
		strMan.Type = 1;	// Binary
		outs.Write(strMan.Read());
		if(bNum) writeInt(strMan.Size);
	}
	// ファイル書き込みする関数
	var writeFile = function(path) {
		strMan.Position = 0;
		strMan.SetEOS();
		strMan.Type = 1;	// Binary
		strMan.LoadFromFile(path);
		outs.Write(strMan.Read());
		writeInt(strMan.Size);
		writeStr(path, true);
	}

	// スクリプトを出力する
	var outs = WScript.CreateObject('ADODB.Stream');
	outs.Open();
	outs.Charset = "iso-8859-1";
	outs.Type = 1;	// Binary
	writeStr(script, false);
	// 改行と0出力
	writeInt(0x0d0a0000);

	// 	指定ファイルを出力
	// 		データ部を出力、ファイル名を出力、サイズと長さを出力
	var allNum = 0;
	for(var i = 2; i < WScript.Arguments.length; i++) {
		writeFile(WScript.Arguments.item(i));
		allNum++;
	}

	// 全体数を出力
	writeInt(allNum);
	// コマンドを出力
	writeStr(WScript.Arguments.item(1), true);
	// 'junj'マーク出力
	writeStr('junj', false);

	outs.SaveToFile(WScript.Arguments.item(0), 2);	// adSaveCreateOverWrite
	outs.Close();
	outs = null;
	strMan.Close();
	strMan = null;
	binMan = null;

	return 0;
})());

備考

wshは後ろにごみがあっても気にしません。
それを利用して、バイナリファイルをくっつけてます。
ウイルス的にexeに憑依することも考えたけど、Generatorを作るのが面倒だったのでやめた。


サブフォルダ対応は、面倒だったのでやってません。
展開側でフォルダがなければ作るとかしないといけない気がして、、、。
あと、あんまり大きなファイルはだめです。ギガ単位とかは許して。
(理由は、一旦メモリーに展開しちゃうのと、添付のフォーマットのせい)


今回得た知識は、、、ADODB.Streamで扱うByte():バイト配列型は、
MSXMLが使えると、あっさり扱える、ということ。
これを使えば、文字コードを気にしながらバイナリーを扱うといった、
変なことをしないで済みます。


なお、ADODB.Streamで扱える文字コードは、
HKEY_CLASSES_ROOT\MIME\Database\Charset
にあるものの模様。


出力されるスクリプト本体は、以下のようなもの。

WScript.Quit((function() {

	// バイト配列操作用オブジェクト
	var binMan = WScript.CreateObject('Microsoft.XMLDOM').createElement('tmp');
	binMan.dataType = 'bin.hex';
	// 文字列変換用オブジェクト
	var strMan = WScript.CreateObject('ADODB.Stream');
	strMan.Open();
	strMan.Charset = 'Shift-JIS';
	strMan.Type = 1;	// Binary

	// バイト配列を数値化する関数(BigEndian)
	var b2i = function(b) {
		binMan.nodeTypedValue = b;
		var strb = binMan.text;
		var ret = 0, m = strb.length;
		for(var i = 0; i < m; i += 2) {
			ret *= 256;
			ret += parseInt(strb.substr(i, 2), 16);
		}
		return ret;
	}
	// offsetを4つずらし数値読み込みする関数
	var readInt = function() {
		offset -= 4;
		ins.Position = offset;
		return b2i(ins.Read(4));
	}
	// offsetを指定文字ずらし文字列読み込みする関数
	var readStr = function(num) {
		offset -= num;
		ins.Position = offset;
		strMan.Position = 0;
		strMan.SetEOS();
		strMan.Type = 1;	// Binary
		strMan.Write(ins.Read(num));
		strMan.Position = 0;
		strMan.Type = 2;	// Text
		return strMan.ReadText();
	}
	// offsetを指定文字ずらしファイル保存する関数
	var saveBin = function(path, size) {
		offset -= size;
		ins.Position = offset;
		strMan.Position = 0;
		strMan.SetEOS();
		strMan.Type = 1;	// Binary
		strMan.Write(ins.Read(size));
		strMan.SaveToFile(path, 2);	// adSaveCreateOverWrite
	}

	// 自ファイルを開き、後ろから読み、コマンドとファイル数を得る
	var ins = WScript.CreateObject('ADODB.Stream');
	ins.Open();
	ins.Charset = 'iso-8859-1';
	ins.Type = 1;	// Binary
	ins.LoadFromFile(WScript.ScriptFullName);

	// 後ろは「junj」であること
	var size = ins.Size, offset = size;
	var n = readInt();
	if(n != 0x6a756e6a) {
		WScript.Echo('Script format error! :mark');
		return 1;
	}
	// コマンド長さを取得
	n = readInt();
	if(n > offset) {
		WScript.Echo('Script format error! :cmd len=' + n);
		return 2;
	}
	// コマンドを取得
	var cmd = readStr(n);
	// 全体数を取得
	var allNum = readInt();
	if(allNum > size) {
		WScript.Echo('Script format error! :allNum=' + allNum);
		return 3;
	}

	// ファイル数分ループし、指定ファイルが存在するか確認する
	// 	あれば何もしない。なければ展開
	var fso = WScript.CreateObject('Scripting.FileSystemObject');
	for(var i = 0; i < allNum; i++) {
		var pathLen = readInt();
		var path = readStr(pathLen);
		var fileSize = readInt();
		if(fso.FileExists(path)) {
			offset -= fileSize;
		} else {
			saveBin(path, fileSize);
		}
	}
	ins.Close();
	ins = null;
	fso = null;
	strMan.Close();
	strMan = null;
	binMan = null;

	// ファイルチェックが終わったら、コマンドを実行する
	return WScript.CreateObject('WScript.Shell').run(cmd, 1, false);
})());

/packer/で圧縮してちょっと加工してます。


こういうインストーラーってあんまり見ないですよね。
一番近いのは、自己解凍形式の圧縮ファイルの自動実行利用ですけど、
それだと毎回ファイルを出力することになる。
これだと、いつも同じファイルを実行して、なければインストール、あればそのまま実行、ってことができます。


expand とか makecab を使って、cab圧縮したものを操作した方が良かった気がする。
モリー不足対策にもなったような、、、。
作成側は、スクリプトファイルとcab圧縮とコマンドやファイルリスト等をcopy /b連結。
実行側は、ファイルチェックと個別指定で展開。
、、、が、忘れることにする。


また、実行するコマンドでスクリプトを指定して、プロセス完了待ち後ファイル削除するようにすれば、実行中にしかファイルを存在しないようにできますが、、、それは、自己解凍形式圧縮ファイルの自動実行でやればいっか。
バグがあっても知りません(無責任!)