| OLD | NEW |
| 1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "chrome/installer/util/delete_reg_key_work_item.h" | 5 #include "chrome/installer/util/registry_key_backup.h" |
| 6 | 6 |
| 7 #include <shlwapi.h> | |
| 8 #include <algorithm> | 7 #include <algorithm> |
| 9 #include <limits> | 8 #include <limits> |
| 10 #include <vector> | 9 #include <vector> |
| 11 | 10 |
| 12 #include "base/logging.h" | 11 #include "base/logging.h" |
| 13 #include "base/rand_util.h" | |
| 14 #include "base/stringprintf.h" | |
| 15 #include "base/win/registry.h" | 12 #include "base/win/registry.h" |
| 16 #include "chrome/installer/util/logging_installer.h" | |
| 17 | 13 |
| 18 using base::win::RegKey; | 14 using base::win::RegKey; |
| 19 | 15 |
| 20 namespace { | 16 namespace { |
| 21 const REGSAM kKeyReadNoNotify = (KEY_READ) & ~(KEY_NOTIFY); | 17 const REGSAM kKeyReadNoNotify = (KEY_READ) & ~(KEY_NOTIFY); |
| 22 } | 18 } // namespace |
| 23 | 19 |
| 24 // A container for a registry key, its values, and its subkeys. We don't use | 20 // A container for a registry key, its values, and its subkeys. |
| 25 // more obvious methods for various reasons: | 21 class RegistryKeyBackup::KeyData { |
| 26 // - RegCopyTree isn't supported pre-Vista, so we'd have to do something | |
| 27 // different for XP anyway. | |
| 28 // - SHCopyKey can't copy subkeys into a volatile destination, so we'd have to | |
| 29 // worry about polluting the registry. | |
| 30 // We don't persist security attributes since we only delete keys that we own, | |
| 31 // and we don't set custom attributes on them anyway. | |
| 32 class DeleteRegKeyWorkItem::RegKeyBackup { | |
| 33 public: | 22 public: |
| 34 RegKeyBackup(); | 23 KeyData(); |
| 24 ~KeyData(); |
| 35 bool Initialize(const RegKey& key); | 25 bool Initialize(const RegKey& key); |
| 36 bool WriteTo(RegKey* key) const; | 26 bool WriteTo(RegKey* key) const; |
| 37 | 27 |
| 38 private: | 28 private: |
| 39 // A container for a registry value. | 29 class ValueData; |
| 40 class RegValueBackup { | |
| 41 public: | |
| 42 RegValueBackup(); | |
| 43 void Initialize(const wchar_t* name_buffer, DWORD name_size, | |
| 44 DWORD type, const uint8* data, DWORD data_size); | |
| 45 const std::wstring& name_str() const { return name_; } | |
| 46 const wchar_t* name() const { return name_.empty() ? NULL : name_.c_str(); } | |
| 47 DWORD type() const { return type_; } | |
| 48 const uint8* data() const { return data_.empty() ? NULL : &data_[0]; } | |
| 49 DWORD data_len() const { return static_cast<DWORD>(data_.size()); } | |
| 50 | 30 |
| 51 private: | 31 scoped_array<ValueData> values_; |
| 52 std::wstring name_; | 32 scoped_array<std::wstring> subkey_names_; |
| 53 std::vector<uint8> data_; | 33 scoped_array<KeyData> subkeys_; |
| 54 DWORD type_; | 34 DWORD num_values_; |
| 35 DWORD num_subkeys_; |
| 55 | 36 |
| 56 DISALLOW_COPY_AND_ASSIGN(RegValueBackup); | 37 DISALLOW_COPY_AND_ASSIGN(KeyData); |
| 57 }; | |
| 58 | |
| 59 scoped_array<RegValueBackup> values_; | |
| 60 scoped_array<std::wstring> subkey_names_; | |
| 61 scoped_array<RegKeyBackup> subkeys_; | |
| 62 ptrdiff_t num_values_; | |
| 63 ptrdiff_t num_subkeys_; | |
| 64 | |
| 65 DISALLOW_COPY_AND_ASSIGN(RegKeyBackup); | |
| 66 }; | 38 }; |
| 67 | 39 |
| 68 DeleteRegKeyWorkItem::RegKeyBackup::RegValueBackup::RegValueBackup() | 40 // A container for a registry value. |
| 41 class RegistryKeyBackup::KeyData::ValueData { |
| 42 public: |
| 43 ValueData(); |
| 44 ~ValueData(); |
| 45 void Initialize(const wchar_t* name_buffer, DWORD name_size, |
| 46 DWORD type, const uint8* data, DWORD data_size); |
| 47 const std::wstring& name_str() const { return name_; } |
| 48 const wchar_t* name() const { return name_.empty() ? NULL : name_.c_str(); } |
| 49 DWORD type() const { return type_; } |
| 50 const uint8* data() const { return data_.empty() ? NULL : &data_[0]; } |
| 51 DWORD data_len() const { return static_cast<DWORD>(data_.size()); } |
| 52 |
| 53 private: |
| 54 std::wstring name_; |
| 55 std::vector<uint8> data_; |
| 56 DWORD type_; |
| 57 |
| 58 DISALLOW_COPY_AND_ASSIGN(ValueData); |
| 59 }; |
| 60 |
| 61 RegistryKeyBackup::KeyData::ValueData::ValueData() |
| 69 : type_(REG_NONE) { | 62 : type_(REG_NONE) { |
| 70 } | 63 } |
| 71 | 64 |
| 72 void DeleteRegKeyWorkItem::RegKeyBackup::RegValueBackup::Initialize( | 65 RegistryKeyBackup::KeyData::ValueData::~ValueData() |
| 66 { |
| 67 } |
| 68 |
| 69 void RegistryKeyBackup::KeyData::ValueData::Initialize( |
| 73 const wchar_t* name_buffer, | 70 const wchar_t* name_buffer, |
| 74 DWORD name_size, | 71 DWORD name_size, |
| 75 DWORD type, const uint8* data, | 72 DWORD type, |
| 73 const uint8* data, |
| 76 DWORD data_size) { | 74 DWORD data_size) { |
| 77 name_.assign(name_buffer, name_size); | 75 name_.assign(name_buffer, name_size); |
| 78 type_ = type; | 76 type_ = type; |
| 79 data_.assign(data, data + data_size); | 77 data_.assign(data, data + data_size); |
| 80 } | 78 } |
| 81 | 79 |
| 82 DeleteRegKeyWorkItem::RegKeyBackup::RegKeyBackup() | 80 RegistryKeyBackup::KeyData::KeyData() |
| 83 : num_values_(0), | 81 : num_values_(0), |
| 84 num_subkeys_(0) { | 82 num_subkeys_(0) { |
| 85 } | 83 } |
| 86 | 84 |
| 85 RegistryKeyBackup::KeyData::~KeyData() |
| 86 { |
| 87 } |
| 88 |
| 87 // Initializes this object by reading the values and subkeys of |key|. | 89 // Initializes this object by reading the values and subkeys of |key|. |
| 88 // Security descriptors are not backed up. | 90 // Security descriptors are not backed up. |
| 89 bool DeleteRegKeyWorkItem::RegKeyBackup::Initialize(const RegKey& key) { | 91 bool RegistryKeyBackup::KeyData::Initialize(const RegKey& key) { |
| 90 DCHECK(key.Valid()); | 92 scoped_array<ValueData> values; |
| 91 | |
| 92 scoped_array<RegValueBackup> values; | |
| 93 scoped_array<std::wstring> subkey_names; | 93 scoped_array<std::wstring> subkey_names; |
| 94 scoped_array<RegKeyBackup> subkeys; | 94 scoped_array<KeyData> subkeys; |
| 95 | 95 |
| 96 DWORD num_subkeys = 0; | 96 DWORD num_subkeys = 0; |
| 97 DWORD max_subkey_name_len = 0; | 97 DWORD max_subkey_name_len = 0; |
| 98 DWORD num_values = 0; | 98 DWORD num_values = 0; |
| 99 DWORD max_value_name_len = 0; | 99 DWORD max_value_name_len = 0; |
| 100 DWORD max_value_len = 0; | 100 DWORD max_value_len = 0; |
| 101 LONG result = RegQueryInfoKey(key.Handle(), NULL, NULL, NULL, | 101 LONG result = RegQueryInfoKey(key.Handle(), NULL, NULL, NULL, |
| 102 &num_subkeys, &max_subkey_name_len, NULL, | 102 &num_subkeys, &max_subkey_name_len, NULL, |
| 103 &num_values, &max_value_name_len, | 103 &num_values, &max_value_name_len, |
| 104 &max_value_len, NULL, NULL); | 104 &max_value_len, NULL, NULL); |
| 105 if (result != ERROR_SUCCESS) { | 105 if (result != ERROR_SUCCESS) { |
| 106 LOG(ERROR) << "Failed getting info of key to backup, result: " << result; | 106 LOG(ERROR) << "Failed getting info of key to backup, result: " << result; |
| 107 return false; | 107 return false; |
| 108 } | 108 } |
| 109 if (max_subkey_name_len >= std::numeric_limits<DWORD>::max() - 1 || | 109 if (max_subkey_name_len >= std::numeric_limits<DWORD>::max() - 1 || |
| 110 max_value_name_len >= std::numeric_limits<DWORD>::max() - 1) { | 110 max_value_name_len >= std::numeric_limits<DWORD>::max() - 1) { |
| 111 LOG(ERROR) | 111 LOG(ERROR) |
| 112 << "Failed backing up key; subkeys and/or names are out of range."; | 112 << "Failed backing up key; subkeys and/or names are out of range."; |
| 113 return false; | 113 return false; |
| 114 } | 114 } |
| 115 DWORD max_name_len = std::max(max_subkey_name_len, max_value_name_len) + 1; | 115 DWORD max_name_len = std::max(max_subkey_name_len, max_value_name_len) + 1; |
| 116 scoped_array<wchar_t> name_buffer(new wchar_t[max_name_len]); | 116 scoped_array<wchar_t> name_buffer(new wchar_t[max_name_len]); |
| 117 | 117 |
| 118 // Backup the values. | 118 // Backup the values. |
| 119 if (num_values != 0) { | 119 if (num_values != 0) { |
| 120 values.reset(new RegValueBackup[num_values]); | 120 values.reset(new ValueData[num_values]); |
| 121 scoped_array<uint8> value_buffer(new uint8[max_value_len]); | 121 scoped_array<uint8> value_buffer(new uint8[max_value_len]); |
| 122 DWORD name_size = 0; | 122 DWORD name_size = 0; |
| 123 DWORD value_type = REG_NONE; | 123 DWORD value_type = REG_NONE; |
| 124 DWORD value_size = 0; | 124 DWORD value_size = 0; |
| 125 | 125 |
| 126 for (DWORD i = 0; i < num_values; ) { | 126 for (DWORD i = 0; i < num_values; ) { |
| 127 name_size = max_name_len; | 127 name_size = max_name_len; |
| 128 value_size = max_value_len; | 128 value_size = max_value_len; |
| 129 result = RegEnumValue(key.Handle(), i, name_buffer.get(), &name_size, | 129 result = RegEnumValue(key.Handle(), i, name_buffer.get(), &name_size, |
| 130 NULL, &value_type, value_buffer.get(), &value_size); | 130 NULL, &value_type, value_buffer.get(), &value_size); |
| 131 switch (result) { | 131 switch (result) { |
| 132 case ERROR_NO_MORE_ITEMS: | 132 case ERROR_NO_MORE_ITEMS: |
| 133 num_values = i; | 133 num_values = i; |
| 134 break; | 134 break; |
| 135 case ERROR_SUCCESS: | 135 case ERROR_SUCCESS: |
| 136 values[i].Initialize(name_buffer.get(), name_size, value_type, | 136 values[i].Initialize(name_buffer.get(), name_size, value_type, |
| 137 value_buffer.get(), value_size); | 137 value_buffer.get(), value_size); |
| 138 ++i; | 138 ++i; |
| 139 break; | 139 break; |
| 140 case ERROR_MORE_DATA: | 140 case ERROR_MORE_DATA: |
| 141 if (value_size > max_value_len) { | 141 if (value_size > max_value_len) { |
| 142 max_value_len = value_size; | 142 max_value_len = value_size; |
| 143 value_buffer.reset(); // Release to heap before new allocation. |
| 143 value_buffer.reset(new uint8[max_value_len]); | 144 value_buffer.reset(new uint8[max_value_len]); |
| 144 } else { | 145 } else { |
| 145 DCHECK(max_name_len - 1 < name_size); | 146 DCHECK_LT(max_name_len - 1, name_size); |
| 146 if (name_size >= std::numeric_limits<DWORD>::max() - 1) { | 147 if (name_size >= std::numeric_limits<DWORD>::max() - 1) { |
| 147 LOG(ERROR) << "Failed backing up key; value name out of range."; | 148 LOG(ERROR) << "Failed backing up key; value name out of range."; |
| 148 return false; | 149 return false; |
| 149 } | 150 } |
| 150 max_name_len = name_size + 1; | 151 max_name_len = name_size + 1; |
| 152 name_buffer.reset(); // Release to heap before new allocation. |
| 151 name_buffer.reset(new wchar_t[max_name_len]); | 153 name_buffer.reset(new wchar_t[max_name_len]); |
| 152 } | 154 } |
| 153 break; | 155 break; |
| 154 default: | 156 default: |
| 155 LOG(ERROR) << "Failed backing up value " << i << ", result: " | 157 LOG(ERROR) << "Failed backing up value " << i << ", result: " |
| 156 << result; | 158 << result; |
| 157 return false; | 159 return false; |
| 158 } | 160 } |
| 159 } | 161 } |
| 160 DLOG_IF(WARNING, RegEnumValue(key.Handle(), num_values, name_buffer.get(), | 162 DLOG_IF(WARNING, RegEnumValue(key.Handle(), num_values, name_buffer.get(), |
| 161 &name_size, NULL, &value_type, NULL, | 163 &name_size, NULL, &value_type, NULL, |
| 162 NULL) != ERROR_NO_MORE_ITEMS) | 164 NULL) != ERROR_NO_MORE_ITEMS) |
| 163 << "Concurrent modifications to registry key during backup operation."; | 165 << "Concurrent modifications to registry key during backup operation."; |
| 164 } | 166 } |
| 165 | 167 |
| 166 // Backup the subkeys. | 168 // Backup the subkeys. |
| 167 if (num_subkeys != 0) { | 169 if (num_subkeys != 0) { |
| 168 subkey_names.reset(new std::wstring[num_subkeys]); | 170 subkey_names.reset(new std::wstring[num_subkeys]); |
| 169 subkeys.reset(new RegKeyBackup[num_subkeys]); | 171 subkeys.reset(new KeyData[num_subkeys]); |
| 170 DWORD name_size = 0; | 172 DWORD name_size = 0; |
| 171 | 173 |
| 172 // Get the names of them. | 174 // Get the names of them. |
| 173 for (DWORD i = 0; i < num_subkeys; ) { | 175 for (DWORD i = 0; i < num_subkeys; ) { |
| 174 name_size = max_name_len; | 176 name_size = max_name_len; |
| 175 result = RegEnumKeyEx(key.Handle(), i, name_buffer.get(), &name_size, | 177 result = RegEnumKeyEx(key.Handle(), i, name_buffer.get(), &name_size, |
| 176 NULL, NULL, NULL, NULL); | 178 NULL, NULL, NULL, NULL); |
| 177 switch (result) { | 179 switch (result) { |
| 178 case ERROR_NO_MORE_ITEMS: | 180 case ERROR_NO_MORE_ITEMS: |
| 179 num_subkeys = i; | 181 num_subkeys = i; |
| (...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 221 values_.swap(values); | 223 values_.swap(values); |
| 222 subkey_names_.swap(subkey_names); | 224 subkey_names_.swap(subkey_names); |
| 223 subkeys_.swap(subkeys); | 225 subkeys_.swap(subkeys); |
| 224 num_values_ = num_values; | 226 num_values_ = num_values; |
| 225 num_subkeys_ = num_subkeys; | 227 num_subkeys_ = num_subkeys; |
| 226 | 228 |
| 227 return true; | 229 return true; |
| 228 } | 230 } |
| 229 | 231 |
| 230 // Writes the values and subkeys of this object into |key|. | 232 // Writes the values and subkeys of this object into |key|. |
| 231 bool DeleteRegKeyWorkItem::RegKeyBackup::WriteTo(RegKey* key) const { | 233 bool RegistryKeyBackup::KeyData::WriteTo(RegKey* key) const { |
| 234 DCHECK(key); |
| 235 |
| 232 LONG result = ERROR_SUCCESS; | 236 LONG result = ERROR_SUCCESS; |
| 233 | 237 |
| 234 // Write the values. | 238 // Write the values. |
| 235 for (int i = 0; i < num_values_; ++i) { | 239 for (DWORD i = 0; i < num_values_; ++i) { |
| 236 const RegValueBackup& value = values_[i]; | 240 const ValueData& value = values_[i]; |
| 237 result = RegSetValueEx(key->Handle(), value.name(), 0, value.type(), | 241 result = RegSetValueEx(key->Handle(), value.name(), 0, value.type(), |
| 238 value.data(), value.data_len()); | 242 value.data(), value.data_len()); |
| 239 if (result != ERROR_SUCCESS) { | 243 if (result != ERROR_SUCCESS) { |
| 240 LOG(ERROR) << "Failed writing value \"" << value.name_str() | 244 LOG(ERROR) << "Failed writing value \"" << value.name_str() |
| 241 << "\", result: " << result; | 245 << "\", result: " << result; |
| 242 return false; | 246 return false; |
| 243 } | 247 } |
| 244 } | 248 } |
| 245 | 249 |
| 246 // Write the subkeys. | 250 // Write the subkeys. |
| 247 RegKey subkey; | 251 RegKey subkey; |
| 248 for (int i = 0; i < num_subkeys_; ++i) { | 252 for (DWORD i = 0; i < num_subkeys_; ++i) { |
| 249 const std::wstring& name = subkey_names_[i]; | 253 const std::wstring& name = subkey_names_[i]; |
| 250 | 254 |
| 251 result = subkey.Create(key->Handle(), name.c_str(), KEY_WRITE); | 255 result = subkey.Create(key->Handle(), name.c_str(), KEY_WRITE); |
| 252 if (result != ERROR_SUCCESS) { | 256 if (result != ERROR_SUCCESS) { |
| 253 LOG(ERROR) << "Failed creating subkey \"" << name << "\", result: " | 257 LOG(ERROR) << "Failed creating subkey \"" << name << "\", result: " |
| 254 << result; | 258 << result; |
| 255 return false; | 259 return false; |
| 256 } | 260 } |
| 257 if (!subkeys_[i].WriteTo(&subkey)) { | 261 if (!subkeys_[i].WriteTo(&subkey)) { |
| 258 LOG(ERROR) << "Failed writing subkey \"" << name << "\", result: " | 262 LOG(ERROR) << "Failed writing subkey \"" << name << "\", result: " |
| 259 << result; | 263 << result; |
| 260 return false; | 264 return false; |
| 261 } | 265 } |
| 262 } | 266 } |
| 263 | 267 |
| 264 return true; | 268 return true; |
| 265 } | 269 } |
| 266 | 270 |
| 267 DeleteRegKeyWorkItem::~DeleteRegKeyWorkItem() { | 271 RegistryKeyBackup::RegistryKeyBackup() { |
| 268 } | 272 } |
| 269 | 273 |
| 270 DeleteRegKeyWorkItem::DeleteRegKeyWorkItem(HKEY predefined_root, | 274 RegistryKeyBackup::~RegistryKeyBackup() { |
| 271 const std::wstring& path) | |
| 272 : predefined_root_(predefined_root), | |
| 273 path_(path) { | |
| 274 // It's a safe bet that we don't want to delete one of the root trees. | |
| 275 DCHECK(!path.empty()); | |
| 276 } | 275 } |
| 277 | 276 |
| 278 bool DeleteRegKeyWorkItem::Do() { | 277 bool RegistryKeyBackup::Initialize(HKEY root, const wchar_t* key_path) { |
| 279 scoped_ptr<RegKeyBackup> backup; | 278 DCHECK(key_path); |
| 280 | 279 |
| 281 // Only try to make a backup if we're not configured to ignore failures. | 280 RegKey key; |
| 282 if (!ignore_failure_) { | 281 scoped_ptr<KeyData> key_data; |
| 283 RegKey original_key; | |
| 284 | 282 |
| 285 // Does the key exist? | 283 // Does the key exist? |
| 286 LONG result = original_key.Open(predefined_root_, path_.c_str(), | 284 LONG result = key.Open(root, key_path, kKeyReadNoNotify); |
| 287 kKeyReadNoNotify); | 285 if (result == ERROR_SUCCESS) { |
| 288 if (result == ERROR_SUCCESS) { | 286 key_data.reset(new KeyData()); |
| 289 backup.reset(new RegKeyBackup()); | 287 if (!key_data->Initialize(key)) { |
| 290 if (!backup->Initialize(original_key)) { | 288 LOG(ERROR) << "Failed to backup key at " << key_path; |
| 291 LOG(ERROR) << "Failed to backup key at " << path_; | 289 return false; |
| 292 return ignore_failure_; | |
| 293 } | |
| 294 } else if (result != ERROR_FILE_NOT_FOUND) { | |
| 295 LOG(ERROR) << "Failed to open key at " << path_ | |
| 296 << " to create backup, result: " << result; | |
| 297 return ignore_failure_; | |
| 298 } | 290 } |
| 291 } else if (result != ERROR_FILE_NOT_FOUND) { |
| 292 LOG(ERROR) << "Failed to open key at " << key_path |
| 293 << " to create backup, result: " << result; |
| 294 return false; |
| 299 } | 295 } |
| 300 | 296 |
| 301 // Delete the key. | 297 key_data_.swap(key_data); |
| 302 LONG result = SHDeleteKey(predefined_root_, path_.c_str()); | |
| 303 if (result != ERROR_SUCCESS && result != ERROR_FILE_NOT_FOUND) { | |
| 304 LOG(ERROR) << "Failed to delete key at " << path_ << ", result: " | |
| 305 << result; | |
| 306 return ignore_failure_; | |
| 307 } | |
| 308 | |
| 309 // We've succeeded, so remember any backup we may have made. | |
| 310 backup_.swap(backup); | |
| 311 | |
| 312 return true; | 298 return true; |
| 313 } | 299 } |
| 314 | 300 |
| 315 void DeleteRegKeyWorkItem::Rollback() { | 301 bool RegistryKeyBackup::WriteTo(HKEY root, const wchar_t* key_path) const { |
| 316 if (ignore_failure_ || backup_.get() == NULL) | 302 DCHECK(key_path); |
| 317 return; | |
| 318 | 303 |
| 319 // Delete anything in the key before restoring the backup in case someone else | 304 bool success = false; |
| 320 // put new data in the key after Do(). | 305 |
| 321 LONG result = SHDeleteKey(predefined_root_, path_.c_str()); | 306 if (key_data_.get() != NULL) { |
| 322 if (result != ERROR_SUCCESS && result != ERROR_FILE_NOT_FOUND) { | 307 RegKey dest_key; |
| 323 LOG(ERROR) << "Failed to delete key at " << path_ << " in rollback, " | 308 LONG result = dest_key.Create(root, key_path, KEY_WRITE); |
| 324 "result: " << result; | 309 if (result != ERROR_SUCCESS) { |
| 310 LOG(ERROR) << "Failed to create destination key at " << key_path |
| 311 << " to write backup, result: " << result; |
| 312 } else { |
| 313 success = key_data_->WriteTo(&dest_key); |
| 314 LOG_IF(ERROR, !success) << "Failed to write key data."; |
| 315 } |
| 316 } else { |
| 317 success = true; |
| 325 } | 318 } |
| 326 | 319 |
| 327 // Restore the old contents. The restoration takes on its default security | 320 return success; |
| 328 // attributes; any custom attributes are lost. | |
| 329 RegKey original_key; | |
| 330 result = original_key.Create(predefined_root_, path_.c_str(), KEY_WRITE); | |
| 331 if (result != ERROR_SUCCESS) { | |
| 332 LOG(ERROR) << "Failed to create original key at " << path_ | |
| 333 << " in rollback, result: " << result; | |
| 334 } else { | |
| 335 if (!backup_->WriteTo(&original_key)) | |
| 336 LOG(ERROR) << "Failed to restore key in rollback, result: " << result; | |
| 337 } | |
| 338 } | 321 } |
| OLD | NEW |