pgcrypto 0.3.6 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +5 -1
  3. data/Gemfile +1 -4
  4. data/README.markdown +126 -33
  5. data/VERSION +1 -1
  6. data/lib/active_record/connection_adapters/pgcrypto_adapter.rb +4 -0
  7. data/lib/active_record/connection_adapters/pgcrypto_adapter/rails_3.rb +20 -0
  8. data/lib/active_record/connection_adapters/pgcrypto_adapter/rails_4.rb +22 -0
  9. data/lib/pgcrypto.rb +15 -105
  10. data/lib/pgcrypto/adapter.rb +162 -0
  11. data/lib/pgcrypto/column.rb +9 -8
  12. data/lib/pgcrypto/column_converter.rb +26 -0
  13. data/lib/pgcrypto/generators/base_generator.rb +12 -0
  14. data/lib/{generators/pgcrypto → pgcrypto/generators}/install/USAGE +0 -0
  15. data/lib/{generators/pgcrypto → pgcrypto/generators}/install/install_generator.rb +2 -5
  16. data/lib/{generators/pgcrypto → pgcrypto/generators}/install/templates/initializer.rb +0 -0
  17. data/lib/pgcrypto/generators/install/templates/migration.rb +5 -0
  18. data/lib/pgcrypto/generators/upgrade/USAGE +8 -0
  19. data/lib/pgcrypto/generators/upgrade/templates/migration.rb +22 -0
  20. data/lib/pgcrypto/generators/upgrade/upgrade_generator.rb +15 -0
  21. data/lib/pgcrypto/has_encrypted_column.rb +25 -0
  22. data/lib/pgcrypto/key.rb +0 -10
  23. data/lib/pgcrypto/key_manager.rb +11 -0
  24. data/lib/pgcrypto/railtie.rb +15 -0
  25. data/lib/pgcrypto/table.rb +11 -0
  26. data/lib/pgcrypto/table_manager.rb +2 -10
  27. data/lib/tasks/pgcrypto.rake +7 -0
  28. data/pgcrypto.gemspec +24 -17
  29. data/spec/lib/pgcrypto_spec.rb +101 -100
  30. data/spec/spec_helper.rb +27 -28
  31. metadata +20 -36
  32. data/lib/generators/pgcrypto/install/templates/migration.rb +0 -17
  33. data/lib/pgcrypto/active_record.rb +0 -88
  34. data/lib/pgcrypto/arel.rb +0 -24
@@ -1,18 +1,19 @@
1
1
  module PGCrypto
2
2
  class Column < ActiveRecord::Base
3
3
 
4
- attr_accessible :name if respond_to? :attr_accessible
5
-
6
4
  self.table_name = 'pgcrypto_columns'
7
5
 
8
- before_save :set_owner_table
9
- belongs_to :owner, :autosave => false, :inverse_of => :pgcrypto_columns, :polymorphic => true
6
+ belongs_to :owner, polymorphic: true
10
7
 
11
- default_scope { select(%w(id owner_id owner_type owner_table)) }
8
+ has_encrypted_column :value
12
9
 
13
- protected
14
- def set_owner_table
15
- self.owner_table = self.owner.class.table_name
10
+ def self.tables_and_columns
11
+ tables_and_columns = []
12
+ select('DISTINCT owner_type, name').each do |column|
13
+ tables_and_columns.push [column.owner_type.constantize.table_name, column.name]
14
+ end
15
+ tables_and_columns
16
16
  end
17
+
17
18
  end
18
19
  end
@@ -0,0 +1,26 @@
1
+ require 'pgcrypto/column'
2
+
3
+ module PGCrypto
4
+ class ColumnConverter
5
+
6
+ def self.migrate!
7
+ new.migrate!
8
+ end
9
+
10
+ def migrate!
11
+ PGCrypto::Column.find_each(batch_size: 100) do |column|
12
+ migrate_column(column)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def migrate_column(column)
19
+ if column.owner
20
+ column.owner.update_column(column.name, column.value)
21
+ puts "Migrated #{column.owner}##{column.name}"
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,12 @@
1
+ require 'rails/generators/migration'
2
+ require 'rails/generators/active_record/migration'
3
+
4
+ class BaseGenerator < Rails::Generators::Base
5
+ include Rails::Generators::Migration
6
+ extend ActiveRecord::Generators::Migration
7
+
8
+ def self.next_migration_number(*args)
9
+ Time.now.utc.strftime("%Y%m%d%H%M%S").to_i.to_s
10
+ end
11
+
12
+ end
@@ -1,11 +1,8 @@
1
- require 'rails/generators/migration'
2
- require 'rails/generators/active_record/migration'
1
+ require 'pgcrypto/generators/base_generator'
3
2
 
4
3
  module Pgcrypto
5
4
  module Generators
6
- class InstallGenerator < Rails::Generators::Base
7
- include Rails::Generators::Migration
8
- extend ActiveRecord::Generators::Migration
5
+ class InstallGenerator < BaseGenerator
9
6
 
10
7
  source_root File.expand_path('../templates', __FILE__)
11
8
 
@@ -0,0 +1,5 @@
1
+ class InstallPgcrypto < ActiveRecord::Migration
2
+ def change
3
+ enable_extension "pgcrypto"
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates the PGCrypto upgrade-from-0.3.x migration
3
+
4
+ Example:
5
+ rails generate pgcrypto:install
6
+
7
+ This will create:
8
+ db/migrate/XXXXXX_upgrade_pgcrypto_to_0_4_0.rb
@@ -0,0 +1,22 @@
1
+ require 'pgcrypto/column'
2
+ require 'pgcrypto/column_converter'
3
+
4
+ class UpgradePgcryptoTo040 < ActiveRecord::Migration
5
+ def up
6
+ # Add columns based on the ones we already know exist
7
+ PGCrypto::Column.tables_and_columns do |table, column|
8
+ add_column table, column, :binary
9
+ end
10
+
11
+ # Migrate column data
12
+ PGCrypto::ColumnConverter.migrate!
13
+
14
+ # Drop the old, now-unused columns table
15
+ # COMMENT THIS IN IF YOU REALLY WANT IT
16
+ # drop_table :pgcrypto_columns
17
+ end
18
+
19
+ def down
20
+ raise ActiveRecord::IrreversibleMigration
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ require 'pgcrypto/generators/base_generator'
2
+
3
+ module Pgcrypto
4
+ module Generators
5
+ class UpgradeGenerator < BaseGenerator
6
+
7
+ source_root File.expand_path('../templates', __FILE__)
8
+
9
+ def copy_migration
10
+ migration_template("migration.rb", "db/migrate/upgrade_pgcrypto_to_0_4_0.rb")
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ module PGCrypto
2
+ module HasEncryptedColumn
3
+ def has_encrypted_column(*column_names)
4
+ options = column_names.extract_options!
5
+ options.reverse_merge(type: :pgp)
6
+
7
+ column_names.each do |column_name|
8
+ # Stash the encryption type in our module
9
+ PGCrypto[table_name][column_name.to_s] ||= options.symbolize_keys
10
+ end
11
+ end
12
+
13
+ def pgcrypto(*args)
14
+ if defined? Rails
15
+ Rails.logger.debug "[DEPRECATION WARNING] `pgcrypto' is deprecated. Please use `has_encrypted_column' instead!"
16
+ end
17
+ has_encrypted_column(*args)
18
+ end
19
+
20
+ end
21
+ end
22
+
23
+ if defined? ActiveRecord::Base
24
+ ActiveRecord::Base.extend PGCrypto::HasEncryptedColumn
25
+ end
@@ -1,14 +1,4 @@
1
1
  module PGCrypto
2
- class KeyManager < Hash
3
- def []=(key, value)
4
- unless value.is_a?(Key)
5
- value = Key.new(value)
6
- end
7
- value.name = key
8
- super key, value
9
- end
10
- end
11
-
12
2
  class Key
13
3
  attr_accessor :name, :password, :value
14
4
  attr_reader :path
@@ -0,0 +1,11 @@
1
+ module PGCrypto
2
+ class KeyManager < Hash
3
+ def []=(key, value)
4
+ unless value.is_a?(Key)
5
+ value = Key.new(value)
6
+ end
7
+ value.name = key
8
+ super key, value
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ module PGCrypto
2
+ class Railtie < Rails::Railtie
3
+ generators do
4
+ require 'pgcrypto/generators/install/install_generator'
5
+ require 'pgcrypto/generators/upgrade/upgrade_generator'
6
+ end
7
+
8
+ rake_tasks do
9
+ tasks = File.join(File.dirname(__FILE__), '../tasks/*.rake')
10
+ Dir[tasks].each do |file|
11
+ load file
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ module PGCrypto
2
+ class Table < Hash
3
+ def [](key)
4
+ super(key.to_sym)
5
+ end
6
+
7
+ def []=(key, value)
8
+ super key.to_sym, value
9
+ end
10
+ end
11
+ end
@@ -1,14 +1,6 @@
1
- module PGCrypto
2
- class Table < Hash
3
- def [](key)
4
- super(key.to_sym)
5
- end
6
-
7
- def []=(key, value)
8
- super key.to_sym, value
9
- end
10
- end
1
+ require 'pgcrypto/table'
11
2
 
3
+ module PGCrypto
12
4
  class TableManager < Table
13
5
  def [](key)
14
6
  return {} unless key
@@ -0,0 +1,7 @@
1
+ namespace :pgcrypto do
2
+ desc "Migrate PGCrypto 0.3.x-style columns to 0.4 style"
3
+ task migrate_old_columns: :environment do
4
+ require 'pgcrypto/column_converter'
5
+ PGCrypto::ColumnConverter.migrate!
6
+ end
7
+ end
@@ -2,14 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
+ # stub: pgcrypto 0.4.0 ruby lib
5
6
 
6
7
  Gem::Specification.new do |s|
7
8
  s.name = "pgcrypto"
8
- s.version = "0.3.5"
9
+ s.version = "0.4.0"
9
10
 
10
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib"]
11
13
  s.authors = ["Flip Sasser"]
12
- s.date = "2012-09-02"
14
+ s.date = "2014-08-24"
13
15
  s.description = "\n PGCrypto is an ActiveRecord::Base extension that allows you to asymmetrically\n encrypt PostgreSQL columns with as little trouble as possible. It's totally\n freaking rad.\n "
14
16
  s.email = "flip@x451.com"
15
17
  s.extra_rdoc_files = [
@@ -26,16 +28,28 @@ Gem::Specification.new do |s|
26
28
  "README.markdown",
27
29
  "Rakefile",
28
30
  "VERSION",
29
- "lib/generators/pgcrypto/install/USAGE",
30
- "lib/generators/pgcrypto/install/install_generator.rb",
31
- "lib/generators/pgcrypto/install/templates/initializer.rb",
32
- "lib/generators/pgcrypto/install/templates/migration.rb",
31
+ "lib/active_record/connection_adapters/pgcrypto_adapter.rb",
32
+ "lib/active_record/connection_adapters/pgcrypto_adapter/rails_3.rb",
33
+ "lib/active_record/connection_adapters/pgcrypto_adapter/rails_4.rb",
33
34
  "lib/pgcrypto.rb",
34
- "lib/pgcrypto/active_record.rb",
35
- "lib/pgcrypto/arel.rb",
35
+ "lib/pgcrypto/adapter.rb",
36
36
  "lib/pgcrypto/column.rb",
37
+ "lib/pgcrypto/column_converter.rb",
38
+ "lib/pgcrypto/generators/base_generator.rb",
39
+ "lib/pgcrypto/generators/install/USAGE",
40
+ "lib/pgcrypto/generators/install/install_generator.rb",
41
+ "lib/pgcrypto/generators/install/templates/initializer.rb",
42
+ "lib/pgcrypto/generators/install/templates/migration.rb",
43
+ "lib/pgcrypto/generators/upgrade/USAGE",
44
+ "lib/pgcrypto/generators/upgrade/templates/migration.rb",
45
+ "lib/pgcrypto/generators/upgrade/upgrade_generator.rb",
46
+ "lib/pgcrypto/has_encrypted_column.rb",
37
47
  "lib/pgcrypto/key.rb",
48
+ "lib/pgcrypto/key_manager.rb",
49
+ "lib/pgcrypto/railtie.rb",
50
+ "lib/pgcrypto/table.rb",
38
51
  "lib/pgcrypto/table_manager.rb",
52
+ "lib/tasks/pgcrypto.rake",
39
53
  "pgcrypto.gemspec",
40
54
  "spec/lib/pgcrypto_spec.rb",
41
55
  "spec/spec_helper.rb",
@@ -45,25 +59,18 @@ Gem::Specification.new do |s|
45
59
  "spec/support/public.password.key"
46
60
  ]
47
61
  s.homepage = "http://github.com/Plinq/pgcrypto"
48
- s.require_paths = ["lib"]
49
- s.rubygems_version = "1.8.24"
62
+ s.rubygems_version = "2.4.1"
50
63
  s.summary = "A transparent ActiveRecord::Base extension for encrypted columns"
51
64
 
52
65
  if s.respond_to? :specification_version then
53
- s.specification_version = 3
66
+ s.specification_version = 4
54
67
 
55
68
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
56
- s.add_runtime_dependency(%q<activerecord>, [">= 3.2"])
57
- s.add_runtime_dependency(%q<big_spoon>, [">= 0.2.1"])
58
69
  s.add_development_dependency(%q<jeweler>, [">= 0"])
59
70
  else
60
- s.add_dependency(%q<activerecord>, [">= 3.2"])
61
- s.add_dependency(%q<big_spoon>, [">= 0.2.1"])
62
71
  s.add_dependency(%q<jeweler>, [">= 0"])
63
72
  end
64
73
  else
65
- s.add_dependency(%q<activerecord>, [">= 3.2"])
66
- s.add_dependency(%q<big_spoon>, [">= 0.2.1"])
67
74
  s.add_dependency(%q<jeweler>, [">= 0"])
68
75
  end
69
76
  end
@@ -4,102 +4,100 @@ require 'spec_helper'
4
4
  # ActiveRecord::Base.logger = Logger.new(STDOUT)
5
5
 
6
6
  specs = proc do
7
- it "should extend ActiveRecord::Base" do
8
- PGCryptoTestModel.should respond_to(:pgcrypto)
9
- end
10
-
11
- it "should have readers and writers" do
12
- model = PGCryptoTestModel.new
13
- model.should respond_to(:test_column)
14
- model.should respond_to(:test_column=)
15
- end
16
-
17
- it "should be settable on create" do
18
- model = PGCryptoTestModel.new(:test_column => 'this is a test')
19
- model.save!.should be_true
20
- end
21
-
22
- it "should be settable on update" do
23
- model = PGCryptoTestModel.create!
24
- model.test_column = 'this is another test'
25
- model.save!.should be_true
26
- end
27
-
28
- it "should be update-able" do
29
- model = PGCryptoTestModel.create!(:test_column => 'i am test column')
30
- model.update_attributes!(:test_column => 'but now i am a different column, son').should be_true
31
- model.test_column.should == 'but now i am a different column, son'
32
- end
33
-
34
- it "should be retrievable at create" do
35
- model = PGCryptoTestModel.create!(:test_column => 'i am test column')
36
- model.test_column.should == 'i am test column'
37
- end
38
-
39
- it "should be retrievable after create" do
40
- model = PGCryptoTestModel.create!(:test_column => 'i should return to you')
41
- PGCryptoTestModel.find(model.id).test_column.should == 'i should return to you'
42
- end
43
-
44
- it "should be retrievable at update" do
45
- model = PGCryptoTestModel.create!(:test_column => 'i will update')
46
- model.test_column.should == 'i will update'
47
- model.update_attributes!(:test_column => 'i updated')
48
- model.test_column.should == 'i updated'
49
- end
50
-
51
- it "should be retrievable without update" do
52
- model = PGCryptoTestModel.create!(:test_column => 'i will update')
53
- model.test_column.should == 'i will update'
54
- model.test_column = 'i updated'
55
- model.test_column.should == 'i updated'
56
- end
57
-
58
- it "should be searchable" do
59
- model = PGCryptoTestModel.create!(:test_column => 'i am findable!')
60
- PGCryptoTestModel.where(:test_column => model.test_column).should == [model]
61
- end
62
-
63
- it "should track changes" do
64
- model = PGCryptoTestModel.create!(:test_column => 'i am clean')
65
- model.test_column = "now i'm not!"
66
- model.test_column_changed?.should be_true
67
- end
68
-
69
- it "should not be dirty if unchanged" do
70
- model = PGCryptoTestModel.create!(:test_column => 'i am clean')
71
- model.test_column = 'i am clean'
72
- model.test_column_changed?.should_not be_true
73
- end
74
-
75
- it "should reload with the class" do
76
- model = PGCryptoTestModel.create!(:test_column => 'i am clean')
77
- model.test_column = 'i am dirty'
7
+
8
+ let(:stored_raw) {
9
+ connection = PGCryptoTestModel.connection
10
+ result = connection.select_one("SELECT encrypted_text FROM pgcrypto_test_models LIMIT 1")
11
+ result['encrypted_text']
12
+ }
13
+
14
+ # Default test text
15
+ let(:text) { "text to encrypt" }
16
+ # That text as it appears un-encrypted in a binary column - we'll compare
17
+ # this to what gets set to ensure the text is properly encrypted
18
+ let(:text_raw) { "\\x7465787420746f20656e6372797074" }
19
+
20
+ let(:text_2) { "something else entirely" }
21
+ let(:text_2_raw) { "\\x736f6d657468696e6720656c736520656e746972656c79" }
22
+
23
+ it "extends ActiveRecord::Base" do
24
+ expect(PGCryptoTestModel).to respond_to(:has_encrypted_column)
25
+ expect(PGCryptoTestModel).to respond_to(:pgcrypto)
26
+ end
27
+
28
+ it "encrypts text on insert" do
29
+ PGCryptoTestModel.create!(name: 'foobar', encrypted_text: text)
30
+ expect(stored_raw).not_to eq(text_raw)
31
+ expect(PGCryptoTestModel.last.name).to eq('foobar')
32
+ end
33
+
34
+ it "encrypts new text on update" do
35
+ PGCryptoTestModel.create.tap do |model|
36
+ model.encrypted_text = text
37
+ model.save!
38
+ end
39
+ expect(stored_raw).not_to eq(text_raw)
40
+ end
41
+
42
+ it "encrypts changed text on update" do
43
+ PGCryptoTestModel.create!(encrypted_text: text).tap do |model|
44
+ model.update_attributes!(encrypted_text: text_2)
45
+ end
46
+ expect(stored_raw).not_to eq(text_2_raw)
47
+ end
48
+
49
+ it "keeps plaintext versions of the encrypted text" do
50
+ model = PGCryptoTestModel.create!(encrypted_text: text)
51
+ expect(model.encrypted_text).to eq(text)
52
+ end
53
+
54
+ it "decrypts text when it is selected" do
55
+ model = PGCryptoTestModel.create!(encrypted_text: text)
56
+ expect(PGCryptoTestModel.find(model.id).encrypted_text).to eq(text)
57
+ end
58
+
59
+ it "retrieves decrypted text after update" do
60
+ model = PGCryptoTestModel.create!(:encrypted_text => 'i will update')
61
+ expect(PGCryptoTestModel.find(model.id).encrypted_text).to eq('i will update')
62
+ model.update_attributes!(encrypted_text: 'i updated', name: 'testy mctesterson')
63
+ expect(PGCryptoTestModel.find(model.id).encrypted_text).to eq('i updated')
64
+ end
65
+
66
+ it "retrieves decrypted text without update" do
67
+ model = PGCryptoTestModel.create!(:encrypted_text => 'i will update')
68
+ expect(PGCryptoTestModel.find(model.id).encrypted_text).to eq('i will update')
69
+ model.encrypted_text = 'i updated'
70
+ expect(model.encrypted_text).to eq('i updated')
71
+ end
72
+
73
+ it "supports querying encrypted columns transparently" do
74
+ model = PGCryptoTestModel.create!(:encrypted_text => 'i am findable!')
75
+ expect(PGCryptoTestModel.where(encrypted_text: model.encrypted_text)).to eq([model])
76
+ end
77
+
78
+ it "tracks changes" do
79
+ model = PGCryptoTestModel.create!(:encrypted_text => 'i am clean')
80
+ model.encrypted_text = "now i'm not!"
81
+ expect(model.encrypted_text_changed?).to be_truthy
82
+ end
83
+
84
+ it "is not dirty if attributes are unchanged" do
85
+ model = PGCryptoTestModel.create!(:encrypted_text => 'i am clean')
86
+ model.encrypted_text = 'i am clean'
87
+ expect(model.encrypted_text_changed?).not_to be_truthy
88
+ end
89
+
90
+ it "reloads with the class" do
91
+ model = PGCryptoTestModel.create!(:encrypted_text => 'i am clean')
92
+ model.encrypted_text = 'i am dirty'
78
93
  model.reload
79
- model.test_column.should == 'i am clean'
80
- model.test_column_changed?.should_not be_true
81
- end
82
-
83
- it "should allow direct setting of values as well" do
84
- model = PGCryptoTestModel.create!(:test_column => 'one')
85
- model.test_column.should == 'one'
86
- model.test_column = 'two'
87
- model.save!.should be_true
88
- model.select_pgcrypto_column(:test_column).value.should == 'two'
89
- end
90
-
91
- it "should delete the column when I set the value to nil" do
92
- model = PGCryptoTestModel.create!(:test_column => 'one')
93
- model.test_column = nil
94
- model.save!
95
- model.select_pgcrypto_column(:test_column).should be_nil
96
- end
97
-
98
- it "should plz work" do
99
- model = PGCryptoTestModel.find(PGCryptoTestModel.create!(:test_column => 'one'))
100
- model.test_column = 'two'
101
- model.save!
102
- model.select_pgcrypto_column(:test_column).value.should == 'two'
94
+ expect(model.encrypted_text).to eq('i am clean')
95
+ expect(model.encrypted_text_changed?).not_to be_truthy
96
+ end
97
+
98
+ it "decrypts direct selects" do
99
+ model = PGCryptoTestModel.create!(:encrypted_text => 'to be selected...')
100
+ expect(PGCryptoTestModel.select([:id, :encrypted_text]).where(id: model.id).first).to eq(model)
103
101
  end
104
102
  end
105
103
 
@@ -111,7 +109,7 @@ describe PGCrypto do
111
109
  PGCrypto.keys[:public] = {:path => File.join(keypath, 'public.key')}
112
110
  end
113
111
 
114
- # instance_eval(&specs)
112
+ instance_eval(&specs)
115
113
  end
116
114
 
117
115
  describe "with password-protected keys" do
@@ -123,10 +121,13 @@ describe PGCrypto do
123
121
  instance_eval(&specs)
124
122
  end
125
123
 
126
- describe "with Brett's keys" do
127
- before :each do
128
- PGCrypto.keys[:private] = {:path => File.join(keypath, 'private.brett.key'), :password => '4a13zhUF'}
129
- PGCrypto.keys[:public] = {:path => File.join(keypath, 'public.brett.key')}
124
+ describe "with the PostGIS adapter" do
125
+ before :all do
126
+ gem 'activerecord-postgis-adapter', ActiveRecord::VERSION::MAJOR == 3 ? '< 0.7' : '>= 1.1'
127
+ require 'activerecord-postgis-adapter'
128
+ PGCrypto.keys[:private] = {:path => File.join(keypath, 'private.key')}
129
+ PGCrypto.keys[:public] = {:path => File.join(keypath, 'public.key')}
130
+ PGCrypto.base_adapter = ActiveRecord::ConnectionAdapters::PostGISAdapter::MainAdapter
130
131
  end
131
132
 
132
133
  instance_eval(&specs)