keepassx 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +30 -0
  3. data/.gitignore +9 -0
  4. data/.rubocop.yml +64 -0
  5. data/.travis.yml +12 -3
  6. data/Gemfile +4 -2
  7. data/Guardfile +16 -0
  8. data/LICENSE +19 -0
  9. data/README.md +33 -0
  10. data/Rakefile +3 -2
  11. data/keepassx.gemspec +20 -10
  12. data/lib/keepassx.rb +42 -3
  13. data/lib/keepassx/aes_crypt.rb +16 -6
  14. data/lib/keepassx/database.rb +218 -27
  15. data/lib/keepassx/database/dumper.rb +87 -0
  16. data/lib/keepassx/database/finder.rb +102 -0
  17. data/lib/keepassx/database/loader.rb +217 -0
  18. data/lib/keepassx/entry.rb +70 -38
  19. data/lib/keepassx/field/base.rb +191 -0
  20. data/lib/keepassx/field/entry.rb +32 -0
  21. data/lib/keepassx/field/group.rb +27 -0
  22. data/lib/keepassx/fieldable.rb +161 -0
  23. data/lib/keepassx/group.rb +93 -20
  24. data/lib/keepassx/hashable_payload.rb +6 -0
  25. data/lib/keepassx/header.rb +102 -27
  26. data/lib/keepassx/version.rb +5 -0
  27. data/spec/factories.rb +23 -0
  28. data/spec/fixtures/database_empty.kdb +0 -0
  29. data/spec/fixtures/database_test.kdb +0 -0
  30. data/spec/fixtures/database_test_dumped.yml +76 -0
  31. data/spec/fixtures/database_with_key.kdb +0 -0
  32. data/spec/fixtures/database_with_key.key +1 -0
  33. data/spec/fixtures/database_with_key2.key +1 -0
  34. data/spec/fixtures/test_data_array.yml +113 -0
  35. data/spec/fixtures/test_data_array_dumped.yml +124 -0
  36. data/spec/keepassx/database_spec.rb +491 -29
  37. data/spec/keepassx/entry_spec.rb +95 -0
  38. data/spec/keepassx/group_spec.rb +92 -0
  39. data/spec/keepassx_spec.rb +17 -0
  40. data/spec/spec_helper.rb +59 -3
  41. metadata +143 -69
  42. data/.rvmrc +0 -1
  43. data/Gemfile.lock +0 -28
  44. data/lib/keepassx/entry_field.rb +0 -49
  45. data/lib/keepassx/group_field.rb +0 -44
  46. data/spec/test_database.kdb +0 -0
@@ -1,56 +1,518 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Keepassx::Database do
4
- describe 'self.open' do
5
- it "creates a new instance of the databse with the file" do
6
- db = Keepassx::Database.open(TEST_DATABASE_PATH)
7
- db.should_not be_nil
4
+
5
+ GROUPS_COUNT = 5
6
+ ENTRIES_COUNT = 5
7
+
8
+ let(:data_array) { YAML.load(File.read(File.join(FIXTURE_PATH, 'test_data_array.yml'))) }
9
+ let(:data_array_dumped) { File.read(File.join(FIXTURE_PATH, 'test_data_array_dumped.yml')) }
10
+
11
+ let(:test_db) { Keepassx::Database.new(TEST_DATABASE_PATH) }
12
+ let(:test_db_dumped) { File.read(File.join(FIXTURE_PATH, 'database_test_dumped.yml')) }
13
+
14
+ let(:empty_db) { Keepassx::Database.new(EMPTY_DATABASE_PATH) }
15
+ let(:test_group) { build(:group) }
16
+ let(:test_entry) { build(:entry) }
17
+
18
+ let(:keyfile_db) { Keepassx::Database.new(KEYFILE_DATABASE_PATH) }
19
+ let(:keyfile) { File.join(FIXTURE_PATH, 'database_with_key.key') }
20
+ let(:keyfile2) { File.join(FIXTURE_PATH, 'database_with_key2.key') }
21
+
22
+ describe 'empty database' do
23
+ before :each do
24
+ empty_db.unlock('test')
25
+ end
26
+
27
+ it 'should have 2 groups' do
28
+ expect(empty_db.groups.size).to eq 2
29
+ expect(empty_db.groups.map(&:name).sort).to eq ['Internet', 'eMail']
30
+ end
31
+
32
+ it 'should have 2 special entries' do
33
+ expect(empty_db.entries.size).to eq 2
34
+ expect(empty_db.entries.map(&:name).sort).to eq ['Meta-Info', 'Meta-Info']
35
+ end
36
+
37
+ it 'should have 1 KPX_CUSTOM_ICONS_4 entry' do
38
+ entry = empty_db.find_entry(notes: 'KPX_CUSTOM_ICONS_4')
39
+ expect(entry).to be_a(Keepassx::Entry)
40
+ expect(entry.notes).to eq 'KPX_CUSTOM_ICONS_4'
41
+ end
42
+
43
+ it 'should have 1 KPX_GROUP_TREE_STATE entry' do
44
+ entry = empty_db.find_entry(notes: 'KPX_GROUP_TREE_STATE')
45
+ expect(entry).to be_a(Keepassx::Entry)
46
+ expect(entry.notes).to eq 'KPX_GROUP_TREE_STATE'
8
47
  end
9
48
  end
10
49
 
11
- describe "unlock" do
12
- before :each do
13
- @db = Keepassx::Database.open(TEST_DATABASE_PATH)
14
- @db.should be_valid
50
+
51
+ describe '.new' do
52
+ context 'when database is instanciated from file' do
53
+ let(:test_db) { described_class.new(File.open(TEST_DATABASE_PATH)) }
54
+
55
+ before :each do
56
+ test_db.unlock('testmasterpassword')
57
+ end
58
+
59
+ it 'properly initialized from file' do
60
+ expect { test_db }.to_not raise_error
61
+ end
62
+
63
+ it 'should have valid headers' do
64
+ expect(test_db.valid?).to be true
65
+ end
66
+
67
+ it 'should have valid length' do
68
+ expect(test_db.length).to eq 1457
69
+ end
70
+
71
+ it 'should have valid encryption_type headers' do
72
+ expect(test_db.header.encryption_type).to eq 'SHA2'
73
+ end
74
+
75
+ it 'has groups_count counter properly set' do
76
+ expect(test_db.header.groups_count).to eq GROUPS_COUNT
77
+ end
78
+
79
+ it 'has entries_count counter properly set' do
80
+ expect(test_db.header.entries_count).to eq ENTRIES_COUNT
81
+ end
82
+
83
+ it 'contains proper number of test groups' do
84
+ expect(test_db.groups.length).to eq GROUPS_COUNT
85
+ end
86
+
87
+ it 'contains proper number of test entries' do
88
+ expect(test_db.entries.length).to eq ENTRIES_COUNT
89
+ end
90
+
91
+ it 'preserves original data' do
92
+ expect(test_db.to_yaml(skip_date: true)).to eq test_db_dumped
93
+ end
15
94
  end
16
95
 
17
- it "returns true when the master password is correct" do
18
- @db.unlock('testmasterpassword').should be_true
96
+ context 'when database is instanciated from string' do
97
+ let(:test_db) { described_class.new(TEST_DATABASE_PATH) }
98
+
99
+ before :each do
100
+ test_db.unlock('testmasterpassword')
101
+ end
102
+
103
+ it 'properly initialized from string' do
104
+ expect { test_db }.to_not raise_error
105
+ end
106
+
107
+ it 'should have valid headers' do
108
+ expect(test_db.valid?).to be true
109
+ end
110
+
111
+ it 'should have valid length' do
112
+ expect(test_db.length).to eq 1457
113
+ end
114
+
115
+ it 'should have valid encryption_type headers' do
116
+ expect(test_db.header.encryption_type).to eq 'SHA2'
117
+ end
118
+
119
+ it 'has groups_count counter properly set' do
120
+ expect(test_db.header.groups_count).to eq GROUPS_COUNT
121
+ end
122
+
123
+ it 'has entries_count counter properly set' do
124
+ expect(test_db.header.entries_count).to eq ENTRIES_COUNT
125
+ end
126
+
127
+ it 'contains proper number of test groups' do
128
+ expect(test_db.groups.length).to eq GROUPS_COUNT
129
+ end
130
+
131
+ it 'contains proper number of test entries' do
132
+ expect(test_db.entries.length).to eq ENTRIES_COUNT
133
+ end
134
+
135
+ it 'preserves original data' do
136
+ expect(test_db.to_yaml(skip_date: true)).to eq test_db_dumped
137
+ end
19
138
  end
20
139
 
21
- it "returns false when the master password is incorrect" do
22
- @db.unlock('bad password').should be_false
140
+ context 'when database is instanciated from array' do
141
+ let(:test_db) { described_class.new(data_array) }
142
+
143
+ it 'properly initialized from Array' do
144
+ expect { test_db }.to_not raise_error
145
+ end
146
+
147
+ it 'should have valid headers' do
148
+ expect(test_db.valid?).to be true
149
+ end
150
+
151
+ it 'should have valid length' do
152
+ expect(test_db.length).to eq 2189
153
+ end
154
+
155
+ it 'should have valid encryption_type headers' do
156
+ expect(test_db.header.encryption_type).to eq 'SHA2'
157
+ end
158
+
159
+ it 'has groups_count counter properly set' do
160
+ expect(test_db.header.groups_count).to eq 13
161
+ end
162
+
163
+ it 'has entries_count counter properly set' do
164
+ expect(test_db.header.entries_count).to eq 4
165
+ end
166
+
167
+ it 'contains proper number of test groups' do
168
+ expect(test_db.groups.length).to eq 13
169
+ end
170
+
171
+ it 'contains proper number of test entries' do
172
+ expect(test_db.entries.length).to eq 4
173
+ end
174
+
175
+ it 'preserves original data' do
176
+ expect(test_db.to_yaml(skip_date: true)).to eq data_array_dumped
177
+ end
178
+ end
179
+ end
180
+
181
+
182
+ describe '#unlock' do
183
+ context 'when no key file is needed' do
184
+ it 'returns true when the master password is correct' do
185
+ expect(test_db.unlock('testmasterpassword')).to be true
186
+ end
187
+
188
+ it 'returns false when the master password is incorrect' do
189
+ expect(test_db.unlock('bad password')).to be false
190
+ end
191
+ end
192
+
193
+ context 'when a key file is needed' do
194
+ it 'returns true when the master password is correct and a valid keyfile is given' do
195
+ expect(keyfile_db.unlock('test', keyfile)).to be true
196
+ end
197
+
198
+ it 'returns false when the master password is incorrect' do
199
+ expect(keyfile_db.unlock('bad password', keyfile)).to be false
200
+ end
201
+
202
+ it 'returns false when the keyfile is missing' do
203
+ expect(keyfile_db.unlock('test')).to be false
204
+ end
205
+
206
+ it 'returns false when the keyfile dont match' do
207
+ expect(keyfile_db.unlock('test', keyfile2)).to be false
208
+ end
23
209
  end
24
210
  end
25
211
 
26
- describe "an unlocked database" do
212
+
213
+ describe '#locked?' do
214
+ it 'returns true when database is locked' do
215
+ expect(test_db.locked?).to be true
216
+ end
217
+
218
+ it 'returns false when database is unlocked' do
219
+ test_db.unlock('testmasterpassword')
220
+ expect(test_db.locked?).to be false
221
+ end
222
+ end
223
+
224
+
225
+ describe '#to_a' do
226
+ it 'returns Array database representation' do
227
+ expect(described_class.new(data_array).to_a.class).to be Array
228
+ end
229
+ end
230
+
231
+
232
+ describe '#checksum' do
233
+ let(:db1) { described_class.new data_array }
234
+ let(:db2) { described_class.new data_array }
235
+
236
+ it 'has the same checksum for the same data' do
237
+ expect(db1.checksum).to eq db2.checksum
238
+ end
239
+ end
240
+
241
+
242
+ context 'unlocked database' do
27
243
  before :each do
28
- @db = Keepassx::Database.open(TEST_DATABASE_PATH)
29
- @db.unlock('testmasterpassword')
244
+ test_db.unlock('testmasterpassword')
245
+ end
246
+
247
+ describe '#entries' do
248
+ it 'has entries' do
249
+ expect(test_db.entries.map(&:name).sort).to eq ['Meta-Info', 'Meta-Info', 'entry2', 'test entry', 'test entry 2']
250
+ end
251
+ end
252
+
253
+ describe '#groups' do
254
+ it 'has groups' do
255
+ expect(test_db.groups.map(&:name).sort).to eq ['Backup', 'Internet', 'Web', 'Wikipedia', 'eMail']
256
+ end
257
+ end
258
+
259
+ describe '#find_entry' do
260
+ it 'can find entries by their name' do
261
+ expect(test_db.find_entry('test entry').password).to eq 'testpassword'
262
+ expect(test_db.find_entry(name: 'test entry').creation_time).to eq Time.local(2011, 9, 3, 15, 34, 47)
263
+ expect(test_db.find_entry('foo')).to be nil
264
+ end
265
+ end
266
+
267
+ describe '#find_group' do
268
+ it 'can find groups by their name' do
269
+ expect(test_db.find_group('Backup').name).to eq 'Backup'
270
+ expect(test_db.find_group('foo')).to be nil
271
+ end
272
+
273
+ it 'has "Internet" group level properly set' do
274
+ expect(test_db.find_group('Internet').level).to eq 0
275
+ end
276
+
277
+ it 'has "Internet" group parent properly set' do
278
+ expect(test_db.find_group('Internet').parent).to be nil
279
+ end
280
+
281
+ it 'has "Web" group level properly set' do
282
+ expect(test_db.find_group('Web').level).to eq 1
283
+ end
284
+
285
+ it 'has "Web" group parent properly set' do
286
+ expect(test_db.find_group('Web').parent).to eq test_db.find_group('Internet')
287
+ end
288
+
289
+ it 'has "Wikipedia" group level properly set' do
290
+ expect(test_db.find_group('Wikipedia').level).to eq 2
291
+ end
292
+
293
+ it 'has "Wikipedia" group parent properly set' do
294
+ expect(test_db.find_group('Wikipedia').parent).to eq test_db.find_group('Web')
295
+ end
296
+
297
+ it 'has "eMail" group level properly set' do
298
+ expect(test_db.find_group('eMail').level).to eq 0
299
+ end
300
+
301
+ it 'has "eMail" group parent properly set' do
302
+ expect(test_db.find_group('eMail').parent).to be nil
303
+ end
30
304
  end
31
305
 
32
- it "can find entries by their title" do
33
- @db.entry("test entry").password.should == "testpassword"
306
+ describe '#find_entries' do
307
+ it 'should returns a list of entries' do
308
+ expect(test_db.find_entries).to eq test_db.entries
309
+ expect { |b| test_db.find_entries(&b) }.to yield_successive_args(*test_db.entries)
310
+ end
34
311
  end
35
312
 
36
- it "can find groups" do
37
- @db.groups.map(&:name).sort.should == ["Backup", "Internet", "eMail"]
313
+ describe '#find_groups' do
314
+ it 'should returns a list of groups' do
315
+ expect(test_db.find_groups).to eq test_db.groups
316
+ expect { |b| test_db.find_groups(&b) }.to yield_successive_args(*test_db.groups)
317
+ end
38
318
  end
39
319
 
40
- it "can search for entries" do
41
- entries = @db.search "test"
42
- entries.first.title.should == "test entry"
320
+ describe '#search' do
321
+ it 'can search for entries' do
322
+ entries = test_db.search('test')
323
+ expect(entries.first.name).to eq 'test entry'
324
+ end
325
+
326
+ it 'can search for entries case-insensitively' do
327
+ entries = test_db.search('TEST')
328
+ expect(entries.first.name).to eq 'test entry'
329
+ end
330
+
331
+ # it 'will find the current values of entries with history' do
332
+ # entries = test_db.search 'entry2'
333
+ # expect(entries.size).to eq 1
334
+ # expect(entries.first.name).to eq 'entry2'
335
+ # expect(entries.first.backup?).to be true
336
+ # end
43
337
  end
44
338
 
45
- it "can search for entries case-insensitively" do
46
- entries = @db.search "TEST"
47
- entries.first.title.should == "test entry"
339
+ describe '#add_group' do
340
+ context 'when arg is a Keepassx::Group' do
341
+ it 'should increment groups_count' do
342
+ expect(test_db.groups.size).to eq GROUPS_COUNT
343
+ test_db.add_group(test_group)
344
+ expect(test_db.groups.size).to eq GROUPS_COUNT + 1
345
+ expect(test_db.header.groups_count).to eq GROUPS_COUNT + 1
346
+ end
347
+ end
348
+
349
+ context 'when arg is a Hash of options' do
350
+ it 'should increment groups_count' do
351
+ expect(test_db.groups.size).to eq GROUPS_COUNT
352
+ test_db.add_group(attributes_for(:group))
353
+ expect(test_db.groups.size).to eq GROUPS_COUNT + 1
354
+ expect(test_db.header.groups_count).to eq GROUPS_COUNT + 1
355
+ end
356
+ end
357
+
358
+ context 'when arg is neither a Keepassx::Group or a Hash of options' do
359
+ it 'should raise an error' do
360
+ expect { test_db.add_group(nil) }.to raise_error(ArgumentError)
361
+ end
362
+ end
363
+
364
+ context 'with nested groups' do
365
+ it 'should increment groups_count' do
366
+ expect(test_db.groups.size).to eq GROUPS_COUNT
367
+ parent_group = test_db.add_group(attributes_for(:group, id: 0, name: 'parent_group'))
368
+ expect(test_db.groups.size).to eq GROUPS_COUNT + 1
369
+ expect(test_db.groups).to include(parent_group)
370
+ child_group = test_db.add_group(attributes_for(:group, id: 1, name: 'child_group', parent: parent_group))
371
+ expect(test_db.groups.size).to eq GROUPS_COUNT + 2
372
+ expect(test_db.header.groups_count).to eq GROUPS_COUNT + 2
373
+ expect(child_group.parent).to eq parent_group
374
+ end
375
+
376
+ it 'should increment groups_count' do
377
+ expect(test_db.groups.size).to eq GROUPS_COUNT
378
+ parent_group = test_db.add_group(attributes_for(:group, id: 0, name: 'parent_group'))
379
+ expect(test_db.groups.size).to eq GROUPS_COUNT + 1
380
+ expect(test_db.groups).to include(parent_group)
381
+ child_group = test_db.add_group(attributes_for(:group, id: 1, name: 'child_group', parent: :parent_group))
382
+ expect(test_db.groups.size).to eq GROUPS_COUNT + 2
383
+ expect(test_db.header.groups_count).to eq GROUPS_COUNT + 2
384
+ expect(child_group.parent).to eq parent_group
385
+ end
386
+ end
48
387
  end
49
388
 
50
- it "will find the current values of entries with history" do
51
- entries = @db.search "entry2"
52
- entries.size.should == 1
53
- entries.first.title.should == "entry2"
389
+ describe '#add_entry' do
390
+ context 'when arg is a Keepassx::Entry' do
391
+ it 'should increment entries_count' do
392
+ expect(test_db.entries.size).to eq ENTRIES_COUNT
393
+ test_db.add_entry(test_entry)
394
+ expect(test_db.entries.size).to eq ENTRIES_COUNT + 1
395
+ expect(test_db.header.entries_count).to eq ENTRIES_COUNT + 1
396
+ end
397
+ end
398
+
399
+ context 'when arg is a Hash of options' do
400
+ it 'should increment entries_count' do
401
+ expect(test_db.entries.size).to eq ENTRIES_COUNT
402
+ test_db.add_entry(attributes_for(:entry))
403
+ expect(test_db.entries.size).to eq ENTRIES_COUNT + 1
404
+ expect(test_db.header.entries_count).to eq ENTRIES_COUNT + 1
405
+ end
406
+ end
407
+
408
+ context 'when arg is neither a Keepassx::Group or a Hash of options' do
409
+ it 'should raise an error' do
410
+ expect { test_db.add_group(nil) }.to raise_error(ArgumentError)
411
+ end
412
+ end
413
+ end
414
+
415
+ describe '#delete_group' do
416
+ it 'should decrement entries_count' do
417
+ group = test_db.find_group('eMail')
418
+ expect(test_db.groups.size).to eq GROUPS_COUNT
419
+ expect(test_db.header.groups_count).to eq GROUPS_COUNT
420
+ test_db.delete_group(group)
421
+ expect(test_db.groups.size).to eq GROUPS_COUNT - 1
422
+ expect(test_db.header.groups_count).to eq GROUPS_COUNT - 1
423
+ end
424
+ end
425
+
426
+ describe '#delete_entry' do
427
+ it 'should decrement entries_count' do
428
+ entry = test_db.find_entry('test entry')
429
+ expect(test_db.entries.size).to eq ENTRIES_COUNT
430
+ expect(test_db.header.entries_count).to eq ENTRIES_COUNT
431
+ test_db.delete_entry(entry)
432
+ expect(test_db.entries.size).to eq ENTRIES_COUNT - 1
433
+ expect(test_db.header.entries_count).to eq ENTRIES_COUNT - 1
434
+ end
435
+ end
436
+
437
+ describe '#save' do
438
+ context 'when database is saved from an existing file' do
439
+ it 'should save the database in KeePassX format' do
440
+ # Save database in /tmp to not override existing one
441
+ expect { test_db.save(path: '/tmp/keepass1.kdb') }.to_not raise_error
442
+ expect(File.exist?('/tmp/keepass1.kdb')).to be true
443
+
444
+ # Reopen it and compare with original db
445
+ db = described_class.new('/tmp/keepass1.kdb')
446
+ expect(db.locked?).to be true
447
+ db.unlock('testmasterpassword')
448
+ expect(db.to_yaml).to eq test_db.to_yaml
449
+
450
+ # Be sure to delete existing tmp files
451
+ expect(File.unlink('/tmp/keepass1.kdb')).to eq 1
452
+ expect(File.exist?('/tmp/keepass1.kdb')).to be false
453
+ end
454
+ end
455
+
456
+ context 'when database is saved from a data file' do
457
+ it 'should raise an error if path is not set' do
458
+ test_db = described_class.new(data_array)
459
+ expect { test_db.save }.to raise_error(ArgumentError)
460
+ end
461
+
462
+ it 'should raise an error if path is not set' do
463
+ test_db = described_class.new(data_array)
464
+ expect { test_db.save(password: 'foo') }.to raise_error(ArgumentError)
465
+ end
466
+
467
+ it 'should raise an error if password is not set' do
468
+ test_db = described_class.new(data_array)
469
+ expect { test_db.save(path: '/tmp/keepass2.kdb') }.to raise_error(ArgumentError)
470
+ end
471
+
472
+ it 'should save the database if the path and the password are set' do
473
+ # Create new db from array of data
474
+ test_db = described_class.new(data_array)
475
+
476
+ # Save database in /tmp
477
+ expect { test_db.save(path: '/tmp/keepass2.kdb', password: 'testmasterpassword') }.to_not raise_error
478
+ expect(File.exist?('/tmp/keepass2.kdb')).to be true
479
+
480
+ # Reopen it and compare with original db
481
+ db = described_class.new('/tmp/keepass2.kdb')
482
+ expect(db.locked?).to be true
483
+ db.unlock('testmasterpassword')
484
+ expect(db.to_yaml).to eq test_db.to_yaml
485
+
486
+ # Be sure to delete existing tmp files
487
+ expect(File.unlink('/tmp/keepass2.kdb')).to eq 1
488
+ expect(File.exist?('/tmp/keepass2.kdb')).to be false
489
+ end
490
+ end
491
+ end
492
+ end
493
+
494
+
495
+ describe 'create database from scratch' do
496
+ it 'should allow creation of database from scratch' do
497
+ # Create a new Database object
498
+ db = described_class.new('/tmp/test_db.kdb')
499
+ # Add a group
500
+ group = db.add_group(name: 'Foo')
501
+ # Add an entry in this group
502
+ entry = db.add_entry(name: 'Bar', group: group)
503
+ # Save database
504
+ expect { db.save(password: 'testpassword') }.to_not raise_error
505
+ # Do some checks
506
+ expect(File.exist?('/tmp/test_db.kdb')).to be true
507
+
508
+ # Reopen it and compare with original db
509
+ new_db = described_class.new('/tmp/test_db.kdb')
510
+ expect(new_db.locked?).to be true
511
+ new_db.unlock('testpassword')
512
+ expect(new_db.to_yaml(skip_date: true)).to eq db.to_yaml(skip_date: true)
513
+
514
+ # Be sure to delete existing tmp files
515
+ expect(File.unlink('/tmp/test_db.kdb')).to eq 1
54
516
  end
55
517
  end
56
518
  end