s7n 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +28 -0
- data/LICENCE +24 -0
- data/README.rdoc +54 -0
- data/Rakefile +67 -0
- data/bin/s7ncli +23 -0
- data/lib/s7n.rb +12 -0
- data/lib/s7n/action.rb +7 -0
- data/lib/s7n/attribute.rb +226 -0
- data/lib/s7n/cipher.rb +110 -0
- data/lib/s7n/configuration.rb +41 -0
- data/lib/s7n/entry.rb +106 -0
- data/lib/s7n/entry_collection.rb +44 -0
- data/lib/s7n/entry_template.rb +10 -0
- data/lib/s7n/exception.rb +116 -0
- data/lib/s7n/file.rb +77 -0
- data/lib/s7n/gpass_file.rb +203 -0
- data/lib/s7n/key.rb +83 -0
- data/lib/s7n/message_catalog.rb +5 -0
- data/lib/s7n/s7n_file.rb +47 -0
- data/lib/s7n/s7ncli.rb +226 -0
- data/lib/s7n/s7ncli/attribute_command.rb +83 -0
- data/lib/s7n/s7ncli/command.rb +215 -0
- data/lib/s7n/s7ncli/entry_collection_command.rb +63 -0
- data/lib/s7n/s7ncli/entry_command.rb +728 -0
- data/lib/s7n/s7ncli/option.rb +101 -0
- data/lib/s7n/secret_generator.rb +30 -0
- data/lib/s7n/undo_stack.rb +7 -0
- data/lib/s7n/unicode_data.rb +29 -0
- data/lib/s7n/utils.rb +37 -0
- data/lib/s7n/version.rb +3 -0
- data/lib/s7n/world.rb +158 -0
- data/po/ja/s7n.po +533 -0
- data/po/s7n.pot +533 -0
- data/s7n.gemspec +30 -0
- data/test/s7n/attribute_test.rb +50 -0
- data/test/s7n/gpass_file_test.rb +169 -0
- data/test/s7n/gpass_file_test/passwords.gps.empty +1 -0
- data/test/s7n/gpass_file_test/passwords.gps.one_entry +2 -0
- data/test/s7n/gpass_file_test/passwords.gps.three_entries +3 -0
- data/test/s7n/gpass_file_test/passwords.gps.with_folders +0 -0
- data/test/s7n/secret_generator_test.rb +29 -0
- data/test/s7n/unicode_data_test.rb +28 -0
- data/test/s7n/world_test.rb +35 -0
- data/test/test_helper.rb +11 -0
- metadata +153 -0
@@ -0,0 +1,203 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require "s7n/key"
|
4
|
+
require "s7n/cipher"
|
5
|
+
require "s7n/entry_collection"
|
6
|
+
require "s7n/attribute"
|
7
|
+
require "s7n/exception"
|
8
|
+
require "gettext"
|
9
|
+
|
10
|
+
module S7n
|
11
|
+
# GPass のファイルに関する処理を表現する。
|
12
|
+
class GPassFile
|
13
|
+
include GetText
|
14
|
+
|
15
|
+
# GPass のファイルが格納されているデフォルトのパス。
|
16
|
+
DEFAULT_PATH = "~/.gpass/passwords.gps"
|
17
|
+
|
18
|
+
# Blowfish の初期化配列。
|
19
|
+
IV = [5, 23, 1, 123, 12, 3, 54, 94].pack("C8")
|
20
|
+
|
21
|
+
# GPass のファイルであることを示すマジック。
|
22
|
+
MAGIC_PATTERN = /\AGPassFile version (\d\.\d\.\d)/
|
23
|
+
|
24
|
+
# GPass で扱う秘密情報の種類と属性。
|
25
|
+
ENTRY_ATTRIBUTE_TEMPLATES = {
|
26
|
+
"entry" => [TextAttribute.new("name" => N_("name")),
|
27
|
+
TextAttribute.new("name" => N_("description")),
|
28
|
+
DateTimeAttribute.new("name" => N_("created_at")),
|
29
|
+
DateTimeAttribute.new("name" => N_("updated_at")),
|
30
|
+
BooleanAttribute.new("name" => N_("expiration")),
|
31
|
+
DateTimeAttribute.new("name" => N_("expire_at"))],
|
32
|
+
"folder" => ["entry"],
|
33
|
+
"password" => ["entry",
|
34
|
+
TextAttribute.new("name" => N_("username")),
|
35
|
+
TextAttribute.new("name" => N_("password"),
|
36
|
+
"secret" => true)],
|
37
|
+
"general" => ["password",
|
38
|
+
TextAttribute.new("name" => N_("hostname"))],
|
39
|
+
"shell" => ["general"],
|
40
|
+
"website" => ["password",
|
41
|
+
TextAttribute.new("name" => N_("url"))],
|
42
|
+
}
|
43
|
+
|
44
|
+
ENTRY_ATTRIBUTE_TEMPLATES.each do |name, template|
|
45
|
+
loop do
|
46
|
+
replaced = false
|
47
|
+
template.each_with_index do |attr, i|
|
48
|
+
if attr.is_a?(String)
|
49
|
+
template[i] = ENTRY_ATTRIBUTE_TEMPLATES[attr]
|
50
|
+
replaced = true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
if replaced
|
54
|
+
template.flatten!
|
55
|
+
else
|
56
|
+
break
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def read(passphrase, path = DEFAULT_PATH)
|
62
|
+
res = EntryCollection.new
|
63
|
+
File.open(File.expand_path(path), "r") do |f|
|
64
|
+
f.flock(File::LOCK_EX)
|
65
|
+
s = decrypt(f, passphrase)
|
66
|
+
parse(s, res)
|
67
|
+
end
|
68
|
+
return res
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def decrypt(io, passphrase)
|
74
|
+
key = Key.create_instance("SHA-1", passphrase)
|
75
|
+
cipher = Cipher.create_instance("BF-CBC", :key => key, :iv => IV)
|
76
|
+
return cipher.decrypt(io)
|
77
|
+
end
|
78
|
+
|
79
|
+
def parse(s, entry_collection)
|
80
|
+
if s.slice!(MAGIC_PATTERN).nil?
|
81
|
+
raise InvalidPassphrase
|
82
|
+
end
|
83
|
+
version = $1
|
84
|
+
# TODO: 1.1.0 より前のバージョンもサポートする。
|
85
|
+
if version != "1.1.0"
|
86
|
+
raise GPassFileError, _("not supported version: %s") % version
|
87
|
+
end
|
88
|
+
id_entry = {}
|
89
|
+
while !s.empty?
|
90
|
+
res = parse_gpass_entry(s)
|
91
|
+
id = res["id"]
|
92
|
+
entry = res["entry"]
|
93
|
+
if id_entry.key?(id)
|
94
|
+
raise GPassFileError, _("duplicated entry id: %s") % id
|
95
|
+
end
|
96
|
+
id_entry[id] = entry
|
97
|
+
parent = id_entry[res["parent_id"]]
|
98
|
+
if parent
|
99
|
+
entry.tags.push(*parent.tags)
|
100
|
+
end
|
101
|
+
if res["type"] == "folder"
|
102
|
+
entry.tags.push(entry.name)
|
103
|
+
else
|
104
|
+
entry_collection.add_entries(entry)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def parse_gpass_entry(s)
|
110
|
+
id = unpack_number(s)
|
111
|
+
parent_id = unpack_number(s)
|
112
|
+
type = unpack_string(s, unpack_number(s))
|
113
|
+
bytes = unpack_number(s)
|
114
|
+
if s.length < bytes
|
115
|
+
raise DataLengthError.new(s.length, bytes)
|
116
|
+
end
|
117
|
+
entry = Entry.new
|
118
|
+
unpack_attributes(entry, type, s.slice!(0, bytes))
|
119
|
+
return {
|
120
|
+
"id" => id,
|
121
|
+
"parent_id" => parent_id,
|
122
|
+
"type" => type,
|
123
|
+
"entry" => entry,
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
def unpack_attributes(entry, type, s)
|
128
|
+
template = ENTRY_ATTRIBUTE_TEMPLATES[type]
|
129
|
+
if !template
|
130
|
+
raise GPassFileError, _("invalid entry type: type=<%s>") % type
|
131
|
+
end
|
132
|
+
template.each do |a|
|
133
|
+
attr = a.dup
|
134
|
+
case attr
|
135
|
+
when NumericAttribute
|
136
|
+
attr.value = unpack_variable_number(s)
|
137
|
+
when BooleanAttribute
|
138
|
+
attr.value = unpack_boolean(s)
|
139
|
+
when DateTimeAttribute
|
140
|
+
attr.value = unpack_time(s)
|
141
|
+
when TextAttribute
|
142
|
+
attr.value = unpack_string(s, unpack_variable_number(s))
|
143
|
+
end
|
144
|
+
entry.add_attributes(attr)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# s で指定したバイト列から 4 バイト取り出し、リトルエンディアンの数
|
149
|
+
# 値として unpack した結果を返す。
|
150
|
+
def unpack_number(s)
|
151
|
+
if s.length < 4
|
152
|
+
raise DataLengthError.new(s.length, 4)
|
153
|
+
end
|
154
|
+
return s.slice!(0, 4).unpack("V").first
|
155
|
+
end
|
156
|
+
|
157
|
+
# s で指定したバイト列から 8 ビット目が 0 のバイトを見つけるまで 1
|
158
|
+
# バイトずつ取り出し、4 バイトのリトルエンディアンの数値として
|
159
|
+
# unpack した結果を返す。
|
160
|
+
def unpack_variable_number(s)
|
161
|
+
n = 0
|
162
|
+
base = 1
|
163
|
+
i = 0
|
164
|
+
while (t = s.slice!(0)) && i <= 5
|
165
|
+
t = t.ord
|
166
|
+
if t & 0x80 == 0
|
167
|
+
n += base * t
|
168
|
+
break
|
169
|
+
else
|
170
|
+
n += base * (t & 0x7f)
|
171
|
+
base *= 0x80
|
172
|
+
end
|
173
|
+
i += 1
|
174
|
+
end
|
175
|
+
return [n].pack("V").unpack("I").first
|
176
|
+
end
|
177
|
+
|
178
|
+
# s で指定したバイト列から n で指定した長さを取り出し、それを文字列
|
179
|
+
# として返す。
|
180
|
+
def unpack_string(s, n)
|
181
|
+
if s.length < n
|
182
|
+
raise DataLengthError.new(s.length, n)
|
183
|
+
end
|
184
|
+
return s.slice!(0, n).force_encoding("utf-8")
|
185
|
+
end
|
186
|
+
|
187
|
+
# s で指定したバイト列から unpack_variable_number を使用して数値を
|
188
|
+
# 取り出す。それを time_t の値として扱い、DateTimeオブジェクトを返す。
|
189
|
+
def unpack_time(s)
|
190
|
+
return Time.at(unpack_variable_number(s)).to_datetime
|
191
|
+
end
|
192
|
+
|
193
|
+
# s で指定したバイト列から unpack_variable_number を使用して数値を
|
194
|
+
# 取り出す。それが 0 であれば false を、そうでなければ true を返す。
|
195
|
+
def unpack_boolean(s)
|
196
|
+
return unpack_variable_number(s) != 0
|
197
|
+
end
|
198
|
+
|
199
|
+
# GPass のファイルの処理中に発生したエラーを表現する例外クラス。
|
200
|
+
class GPassFileError < ApplicationError
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
data/lib/s7n/key.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
|
5
|
+
module S7n
|
6
|
+
# 暗号化用のキーを表現する。
|
7
|
+
class Key
|
8
|
+
@@key_classes = {}
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def create_instance(type, passphrase)
|
12
|
+
return @@key_classes[type].new(passphrase)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def key_type(name)
|
18
|
+
if @@key_classes.nil?
|
19
|
+
@@key_classes = {}
|
20
|
+
end
|
21
|
+
@@key_classes[name] = self
|
22
|
+
self.const_set("KEY_TYPE", name)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :key
|
27
|
+
attr_reader :length
|
28
|
+
|
29
|
+
def initialize(passphrase)
|
30
|
+
@key = OpenSSL::Digest.digest(hash_type, passphrase)
|
31
|
+
@length = @key.length
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def hash_type
|
37
|
+
begin
|
38
|
+
return self.class.const_get("HASH_TYPE")
|
39
|
+
rescue NameError
|
40
|
+
return self.class.const_get("KEY_TYPE")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Md5Key < Key
|
46
|
+
key_type "MD5"
|
47
|
+
end
|
48
|
+
|
49
|
+
class Sha1Key < Key
|
50
|
+
key_type "SHA-1"
|
51
|
+
HASH_TYPE = "SHA1"
|
52
|
+
end
|
53
|
+
|
54
|
+
begin
|
55
|
+
OpenSSL::Digest.const_get("SHA224")
|
56
|
+
|
57
|
+
class Sha224Key < Key
|
58
|
+
key_type "SHA-224"
|
59
|
+
HASH_TYPE = "SHA224"
|
60
|
+
end
|
61
|
+
rescue NameError
|
62
|
+
end
|
63
|
+
|
64
|
+
begin
|
65
|
+
OpenSSL::Digest.const_get("SHA256")
|
66
|
+
|
67
|
+
class Sha256Key < Key
|
68
|
+
key_type "SHA-256"
|
69
|
+
HASH_TYPE = "SHA256"
|
70
|
+
end
|
71
|
+
rescue NameError
|
72
|
+
end
|
73
|
+
|
74
|
+
begin
|
75
|
+
OpenSSL::Digest.const_get("SHA512")
|
76
|
+
|
77
|
+
class Sha512Key < Key
|
78
|
+
key_type "SHA-512"
|
79
|
+
HASH_TYPE = "SHA512"
|
80
|
+
end
|
81
|
+
rescue NameError
|
82
|
+
end
|
83
|
+
end
|
data/lib/s7n/s7n_file.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "stringio"
|
5
|
+
require "s7n/utils"
|
6
|
+
require "s7n/cipher"
|
7
|
+
|
8
|
+
module S7n
|
9
|
+
# s7 オリジナルのファイル形式で機密情報の読み書きを表現する。
|
10
|
+
module S7nFile
|
11
|
+
# マジック。
|
12
|
+
MAGIC = "s7 file version 0\n"
|
13
|
+
|
14
|
+
# data で指定したバイト列を暗号化して書き込む。
|
15
|
+
def write(path, master_key, cipher_type, data)
|
16
|
+
cipher = Cipher.create_instance(cipher_type, :passphrase => master_key)
|
17
|
+
FileUtils.touch(path)
|
18
|
+
File.open(path, "r+b") do |f|
|
19
|
+
f.flock(File::LOCK_EX)
|
20
|
+
data = [MAGIC,
|
21
|
+
cipher_type + "\n",
|
22
|
+
cipher.encrypt(StringIO.new(data))].join
|
23
|
+
f.write(data)
|
24
|
+
f.truncate(f.pos)
|
25
|
+
end
|
26
|
+
FileUtils.chmod(0600, path)
|
27
|
+
end
|
28
|
+
module_function :write
|
29
|
+
|
30
|
+
# path で指定したファイルを読み込み、master_key で指定したマスター
|
31
|
+
# キーを使用して復号する。
|
32
|
+
def read(path, master_key)
|
33
|
+
File.open(path, "rb") do |f|
|
34
|
+
f.flock(File::LOCK_EX)
|
35
|
+
magic = f.gets
|
36
|
+
if MAGIC != magic
|
37
|
+
raise InvalidPassphrase
|
38
|
+
end
|
39
|
+
cipher_type = f.gets.chomp
|
40
|
+
cipher = Cipher.create_instance(cipher_type,
|
41
|
+
:passphrase => master_key)
|
42
|
+
return cipher.decrypt(f)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
module_function :read
|
46
|
+
end
|
47
|
+
end
|
data/lib/s7n/s7ncli.rb
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require "readline"
|
5
|
+
require "gettext"
|
6
|
+
require "fileutils"
|
7
|
+
require "s7n/s7ncli/option"
|
8
|
+
require "s7n/s7ncli/command"
|
9
|
+
command_pattern =
|
10
|
+
File.expand_path("s7ncli/*_command.rb", File.dirname(__FILE__))
|
11
|
+
Dir.glob(command_pattern).each do |path|
|
12
|
+
require path.gsub(/\.rb\z/, "")
|
13
|
+
end
|
14
|
+
|
15
|
+
# s7n のコマンドラインインタフェースを表現する。
|
16
|
+
module S7n
|
17
|
+
class S7nCli
|
18
|
+
include GetText
|
19
|
+
|
20
|
+
# 最初に実行するメソッド。
|
21
|
+
def self.run(argv)
|
22
|
+
obj = S7nCli.new
|
23
|
+
obj.parse_command_line_argument(argv)
|
24
|
+
obj.run
|
25
|
+
end
|
26
|
+
|
27
|
+
# prompt に指定した文字列を表示後、Readline ライブラリを利用してユー
|
28
|
+
# ザからの入力を得る。
|
29
|
+
def self.input_string(prompt = "", add_history = true)
|
30
|
+
input = Readline.readline(prompt, add_history)
|
31
|
+
input.force_encoding("utf-8") if input
|
32
|
+
return input
|
33
|
+
end
|
34
|
+
|
35
|
+
# prompt に指定した文字列を表示後、エコーバックせずにユーザからの入
|
36
|
+
# 力を得る。パスフレーズの入力を想定。
|
37
|
+
def self.input_string_without_echo(prompt = "")
|
38
|
+
print(prompt) if prompt.length > 0
|
39
|
+
STDOUT.flush
|
40
|
+
Utils.execute("stty -echo")
|
41
|
+
begin
|
42
|
+
input = STDIN.gets
|
43
|
+
ensure
|
44
|
+
Utils.execute("stty echo")
|
45
|
+
puts("")
|
46
|
+
end
|
47
|
+
if input
|
48
|
+
input.chomp!
|
49
|
+
input.force_encoding("utf-8")
|
50
|
+
end
|
51
|
+
return input
|
52
|
+
end
|
53
|
+
|
54
|
+
# prompt に指定した文字列を表示後、ユーザからの入力を得る。
|
55
|
+
# それがyes、y、true、t、okのいずれかであれば true を返し、
|
56
|
+
# そうでなければ false を返す。大小文字は区別しない。
|
57
|
+
# ^D などにより何も入力しない場合、defualt で指定した真偽値を返す。
|
58
|
+
def self.input_boolean(prompt = "", default = false)
|
59
|
+
if input = Readline.readline(prompt, false)
|
60
|
+
input.strip!
|
61
|
+
if input.empty?
|
62
|
+
return default
|
63
|
+
end
|
64
|
+
input.force_encoding("utf-8")
|
65
|
+
if /\Ay|yes|t|true|ok\z/i =~ input
|
66
|
+
return true
|
67
|
+
else
|
68
|
+
return false
|
69
|
+
end
|
70
|
+
else
|
71
|
+
return default
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# World クラスのインスタンス。
|
76
|
+
attr_accessor :world
|
77
|
+
|
78
|
+
def initialize
|
79
|
+
@world = World.new
|
80
|
+
end
|
81
|
+
|
82
|
+
# コマンドラインオプションを解析する。
|
83
|
+
def parse_command_line_argument(argv)
|
84
|
+
option_parser = OptionParser.new { |opts|
|
85
|
+
opts.banner = _("usage: s7ncli [options]")
|
86
|
+
opts.separator("")
|
87
|
+
opts.separator(_("options:"))
|
88
|
+
opts.on("-d", "--base-dir=DIR",
|
89
|
+
_("Specify the base directory. Default is '%s'.") % world.base_dir) do |dir|
|
90
|
+
world.base_dir = dir
|
91
|
+
end
|
92
|
+
opts.on("--debug",
|
93
|
+
_("Turn on debug mode. Default is '%s'.") % world.debug) do
|
94
|
+
world.debug = true
|
95
|
+
end
|
96
|
+
opts.on("-v", "--version", _("Show version.")) do
|
97
|
+
puts(_("s7ncli, version %s") % S7n::VERSION)
|
98
|
+
exit(0)
|
99
|
+
end
|
100
|
+
opts.on("-h", "--help",
|
101
|
+
_("Show help and exit.")) do
|
102
|
+
puts(opts)
|
103
|
+
exit(0)
|
104
|
+
end
|
105
|
+
}
|
106
|
+
option_parser.parse(argv)
|
107
|
+
end
|
108
|
+
|
109
|
+
# メインの処理を行う。
|
110
|
+
def run
|
111
|
+
if File.exist?(world.base_dir)
|
112
|
+
if !File.directory?(world.base_dir)
|
113
|
+
Utils::shred_string(master_key)
|
114
|
+
raise InvalidPath.new(world.base_dir)
|
115
|
+
end
|
116
|
+
world.lock
|
117
|
+
world.start_logging
|
118
|
+
begin
|
119
|
+
master_key =
|
120
|
+
S7nCli.input_string_without_echo(_("Enter the master key for s7n: "))
|
121
|
+
if master_key.nil?
|
122
|
+
exit(1)
|
123
|
+
end
|
124
|
+
world.load(S7nFile.read(world.secrets_path, master_key))
|
125
|
+
world.master_key = master_key
|
126
|
+
rescue InvalidPassphrase => e
|
127
|
+
puts(e.message)
|
128
|
+
retry
|
129
|
+
end
|
130
|
+
else
|
131
|
+
master_key =
|
132
|
+
S7nCli.input_string_without_echo(_("Enter the master key for s7n: "))
|
133
|
+
if master_key.nil?
|
134
|
+
exit(1)
|
135
|
+
end
|
136
|
+
confirmation =
|
137
|
+
S7nCli.input_string_without_echo(_("Comfirm the master key for s7n: "))
|
138
|
+
if master_key != confirmation
|
139
|
+
Utils::shred_string(master_key)
|
140
|
+
Utils::shred_string(confirmation) if !confirmation.nil?
|
141
|
+
raise InvalidPassphrase
|
142
|
+
end
|
143
|
+
world.master_key = master_key
|
144
|
+
FileUtils.mkdir_p(world.base_dir)
|
145
|
+
FileUtils.chmod(0700, world.base_dir)
|
146
|
+
world.start_logging
|
147
|
+
world.logger.info(_("created base directory: %s") % world.base_dir)
|
148
|
+
world.save(true)
|
149
|
+
world.lock
|
150
|
+
end
|
151
|
+
begin
|
152
|
+
path = File.join(world.base_dir, "s7ncli")
|
153
|
+
if File.file?(path)
|
154
|
+
begin
|
155
|
+
hash = YAML.load(S7nFile.read(path, world.master_key))
|
156
|
+
hash["history"].each_line do |line|
|
157
|
+
Readline::HISTORY.push(line.chomp)
|
158
|
+
end
|
159
|
+
rescue
|
160
|
+
puts(_("could not read: %s") % path)
|
161
|
+
raise
|
162
|
+
end
|
163
|
+
end
|
164
|
+
loop do
|
165
|
+
begin
|
166
|
+
prompt = "%ss7n> " % (world.changed ? "*" : "")
|
167
|
+
input = S7nCli.input_string(prompt, true)
|
168
|
+
if !input
|
169
|
+
puts("")
|
170
|
+
break
|
171
|
+
end
|
172
|
+
name, argv = *Command.split_name_and_argv(input)
|
173
|
+
if !name
|
174
|
+
next
|
175
|
+
end
|
176
|
+
command = Command.create_instance(name, world)
|
177
|
+
if command
|
178
|
+
if !command.run(argv)
|
179
|
+
break
|
180
|
+
end
|
181
|
+
else
|
182
|
+
raise ApplicationError, _("Unknown command: %s") % name
|
183
|
+
end
|
184
|
+
rescue Interrupt
|
185
|
+
puts("")
|
186
|
+
retry
|
187
|
+
rescue => e
|
188
|
+
puts(e.message)
|
189
|
+
if world.debug
|
190
|
+
puts(_("----- back trace -----"))
|
191
|
+
e.backtrace.each do |line|
|
192
|
+
puts(" #{line}")
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
ensure
|
198
|
+
begin
|
199
|
+
world.save
|
200
|
+
begin
|
201
|
+
path = File.join(world.base_dir, "s7ncli")
|
202
|
+
# TODO: history の上限を設ける。
|
203
|
+
hash = {
|
204
|
+
"history" => Readline::HISTORY.to_a.join("\n"),
|
205
|
+
}
|
206
|
+
S7nFile.write(path, world.master_key,
|
207
|
+
world.configuration.cipher_type, hash.to_yaml)
|
208
|
+
rescue
|
209
|
+
puts(_("could not write: %s") % path)
|
210
|
+
puts($!)
|
211
|
+
end
|
212
|
+
ensure
|
213
|
+
world.unlock
|
214
|
+
end
|
215
|
+
end
|
216
|
+
rescue ApplicationError => e
|
217
|
+
puts(e.message)
|
218
|
+
if world.debug
|
219
|
+
puts(GetText._("----- back trace -----"))
|
220
|
+
e.backtrace.each do |line|
|
221
|
+
puts(" #{line}")
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|