1 /*
2 * Copyright (c) 2023 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 //! This module provides interfaces for database management.
17 //! Databases are isolated based on users and protected by locks.
18
19 use core::ffi::c_void;
20 use std::{ffi::CStr, fs, ptr::null_mut, sync::Mutex};
21
22 use asset_common::{CallingInfo, OwnerType};
23 use asset_definition::{log_throw_error, ErrCode, Extension, Result, Value};
24 use asset_crypto_manager::secret_key::rename_key_alias;
25 use asset_log::{loge, logi};
26
27 use crate::{
28 statement::Statement,
29 table::Table,
30 types::{
31 column, sqlite_err_handle, DbMap, QueryOptions, COLUMN_INFO, DB_UPGRADE_VERSION, DB_UPGRADE_VERSION_V1,
32 DB_UPGRADE_VERSION_V2, DB_UPGRADE_VERSION_V3, SQLITE_OK, TABLE_NAME, UPGRADE_COLUMN_INFO,
33 UPGRADE_COLUMN_INFO_V2, UPGRADE_COLUMN_INFO_V3,
34 },
35 };
36
37 extern "C" {
SqliteOpen(file_name: *const u8, pp_db: *mut *mut c_void) -> i3238 fn SqliteOpen(file_name: *const u8, pp_db: *mut *mut c_void) -> i32;
SqliteCloseV2(db: *mut c_void) -> i3239 fn SqliteCloseV2(db: *mut c_void) -> i32;
SqliteExec(db: *mut c_void, sql: *const u8, msg: *mut *mut u8) -> i3240 fn SqliteExec(db: *mut c_void, sql: *const u8, msg: *mut *mut u8) -> i32;
SqliteFree(data: *mut c_void)41 fn SqliteFree(data: *mut c_void);
SqliteErrMsg(db: *mut c_void) -> *const u842 fn SqliteErrMsg(db: *mut c_void) -> *const u8;
43 }
44
45 /// each user have a Database file
46 pub(crate) struct UserDbLock {
47 pub(crate) user_id: i32,
48 pub(crate) mtx: Mutex<i32>,
49 }
50
51 static USER_DB_LOCK_LIST: Mutex<Vec<&'static UserDbLock>> = Mutex::new(Vec::new());
52
53 /// If the user exists, the reference to the lock is returned.
54 /// Otherwise, a new lock is created and its reference is returned.
get_file_lock_by_user_id(user_id: i32) -> &'static UserDbLock55 fn get_file_lock_by_user_id(user_id: i32) -> &'static UserDbLock {
56 let mut list = USER_DB_LOCK_LIST.lock().unwrap();
57 for f in list.iter() {
58 if f.user_id == user_id {
59 return f;
60 }
61 }
62 let nf = Box::new(UserDbLock { user_id, mtx: Mutex::new(user_id) });
63 // SAFETY: We just push item into USER_DB_LOCK_LIST, never remove item or modify item,
64 // so return a reference of leak item is safe.
65 let nf: &'static UserDbLock = Box::leak(nf);
66 list.push(nf);
67 list[list.len() - 1]
68 }
69
70 /// Struct used to store database files and connection information.
71 #[repr(C)]
72 pub struct Database {
73 pub(crate) path: String,
74 pub(crate) backup_path: String,
75 pub(crate) handle: usize, // Pointer to the database connection.
76 pub(crate) db_lock: &'static UserDbLock,
77 }
78
79 /// Callback for database upgrade.
80 pub type UpgradeDbCallback = fn(db: &Database, old_ver: u32, new_ver: u32) -> Result<()>;
81
82 #[cfg(not(test))]
83 const ROOT_PATH: &str = "/data/service/el1/public/asset_service";
84 #[cfg(test)]
85 const ROOT_PATH: &str = "/data/asset_test";
86
87 #[inline(always)]
fmt_db_path(user_id: i32) -> String88 fn fmt_db_path(user_id: i32) -> String {
89 format!("{}/{}/asset.db", ROOT_PATH, user_id)
90 }
91
92 #[inline(always)]
fmt_backup_path(path: &str) -> String93 fn fmt_backup_path(path: &str) -> String {
94 let mut bp = path.to_string();
95 bp.push_str(".backup");
96 bp
97 }
98
99 /// Get asset storage path.
get_path() -> String100 pub fn get_path() -> String {
101 ROOT_PATH.to_string()
102 }
103
104 impl Database {
105 /// Create a database.
build(user_id: i32) -> Result<Database>106 pub fn build(user_id: i32) -> Result<Database> {
107 let path = fmt_db_path(user_id);
108 let backup_path = fmt_backup_path(path.as_str());
109 let lock = get_file_lock_by_user_id(user_id);
110 let mut db = Database { path, backup_path, handle: 0, db_lock: lock };
111 let _lock = db.db_lock.mtx.lock().unwrap();
112 db.open_and_restore()?;
113 db.restore_if_exec_fail(|e: &Table| e.create(COLUMN_INFO))?;
114 db.upgrade(user_id, DB_UPGRADE_VERSION, |_, _, _| Ok(()))?;
115 Ok(db)
116 }
117
118 /// check is db ok
check_db_accessible(path: String, user_id: i32) -> Result<()>119 pub fn check_db_accessible(path: String, user_id: i32) -> Result<()> {
120 let lock = get_file_lock_by_user_id(user_id);
121 let mut db = Database { path: path.clone(), backup_path: path, handle: 0, db_lock: lock };
122 db.open()?;
123 let table = Table::new(TABLE_NAME, &db);
124 table.create(COLUMN_INFO)
125 }
126
127 // Open database connection.
open(&mut self) -> Result<()>128 pub(crate) fn open(&mut self) -> Result<()> {
129 let mut path_c = self.path.clone();
130 path_c.push('\0');
131
132 let ret = unsafe { SqliteOpen(path_c.as_ptr(), &mut self.handle as *mut usize as _) };
133 if ret == SQLITE_OK {
134 Ok(())
135 } else {
136 self.close();
137 log_throw_error!(sqlite_err_handle(ret), "[FATAL][DB]Open database failed, err={}", ret)
138 }
139 }
140
141 /// Open the database connection and restore the database if the connection fails.
open_and_restore(&mut self) -> Result<()>142 fn open_and_restore(&mut self) -> Result<()> {
143 let result = self.open();
144 let result = match result {
145 Err(ret) if ret.code == ErrCode::DataCorrupted => self.restore(),
146 ret => ret,
147 };
148 result
149 }
150
151 /// Close database connection.
close(&mut self)152 fn close(&mut self) {
153 if self.handle != 0 {
154 unsafe { SqliteCloseV2(self.handle as _) };
155 self.handle = 0;
156 }
157 }
158
159 /// Close database connection.
close_db(&mut self)160 pub(crate) fn close_db(&mut self) {
161 let _lock = self.db_lock.mtx.lock().unwrap();
162 self.close()
163 }
164
165 // Recovery the corrupt database and reopen it.
restore(&mut self) -> Result<()>166 pub(crate) fn restore(&mut self) -> Result<()> {
167 loge!("[WARNING]Database is corrupt, start to restore");
168 self.close();
169 if let Err(e) = fs::copy(&self.backup_path, &self.path) {
170 return log_throw_error!(ErrCode::FileOperationError, "[FATAL][DB]Recovery database failed, err={}", e);
171 }
172 self.open()
173 }
174
175 /// Get database version, default is 0.
get_db_version(&self) -> Result<u32>176 fn get_db_version(&self) -> Result<u32> {
177 let stmt = Statement::prepare("pragma user_version", self)?;
178 stmt.step()?;
179 let version = stmt.query_column_int(0);
180 Ok(version)
181 }
182
183 /// Get database version, default is 0.
184 #[allow(dead_code)]
get_version(&self) -> Result<u32>185 pub(crate) fn get_version(&self) -> Result<u32> {
186 let _lock = self.db_lock.mtx.lock().unwrap();
187 self.get_db_version()
188 }
189
190 /// Update the database version for database upgrade.
191 #[allow(dead_code)]
set_version(&self, ver: u32) -> Result<()>192 pub(crate) fn set_version(&self, ver: u32) -> Result<()> {
193 let sql = format!("pragma user_version = {}", ver);
194 self.exec(sql.as_str())
195 }
196
197 /// Upgrade database to new version.
198 #[allow(dead_code)]
upgrade(&mut self, user_id: i32, target_ver: u32, callback: UpgradeDbCallback) -> Result<()>199 pub fn upgrade(&mut self, user_id: i32, target_ver: u32, callback: UpgradeDbCallback) -> Result<()> {
200 let mut current_ver = self.get_db_version()?;
201 logi!("current database version: {}", current_ver);
202 if current_ver >= target_ver {
203 return Ok(());
204 }
205 while current_ver < target_ver {
206 match current_ver {
207 DB_UPGRADE_VERSION_V1 => {
208 self.restore_if_exec_fail(|e: &Table| e.upgrade(DB_UPGRADE_VERSION_V2, UPGRADE_COLUMN_INFO_V2))?;
209 current_ver += 1;
210 },
211 DB_UPGRADE_VERSION_V2 => {
212 self.restore_if_exec_fail(|e: &Table| e.upgrade(DB_UPGRADE_VERSION_V3, UPGRADE_COLUMN_INFO_V3))?;
213 current_ver += 1;
214 },
215 DB_UPGRADE_VERSION_V3 => {
216 if self.upgrade_key_alias(user_id)? {
217 self.restore_if_exec_fail(|e: &Table| e.upgrade(DB_UPGRADE_VERSION, UPGRADE_COLUMN_INFO))?;
218 current_ver += 1;
219 } else {
220 break;
221 }
222 },
223 _ => break,
224 }
225 }
226
227 callback(self, current_ver, target_ver)
228 }
229
upgrade_key_alias(&mut self, user_id: i32) -> Result<bool>230 fn upgrade_key_alias(&mut self, user_id: i32) -> Result<bool> {
231 let query_results = self.query_data_without_lock(
232 &vec![
233 column::OWNER_TYPE,
234 column::OWNER,
235 column::AUTH_TYPE,
236 column::ACCESSIBILITY,
237 column::REQUIRE_PASSWORD_SET,
238 ],
239 &DbMap::new(),
240 None,
241 true,
242 )?;
243
244 let mut upgrade_result = true;
245 for query_result in query_results {
246 let owner_type = query_result.get_enum_attr(&column::OWNER_TYPE)?;
247 let owner_info = query_result.get_bytes_attr(&column::OWNER)?;
248 let calling_info = CallingInfo::new(user_id, owner_type, owner_info.to_vec());
249 let auth_type = query_result.get_enum_attr(&column::AUTH_TYPE)?;
250 let access_type = query_result.get_enum_attr(&column::ACCESSIBILITY)?;
251 let require_password_set = query_result.get_bool_attr(&column::REQUIRE_PASSWORD_SET)?;
252 // upgrade_result is set to false as long as any call in the loop for renaming key alias returned false.
253 upgrade_result &= rename_key_alias(&calling_info, auth_type, access_type, require_password_set);
254 }
255
256 Ok(upgrade_result)
257 }
258
259 /// Delete database file.
260 #[allow(dead_code)]
delete(user_id: i32) -> Result<()>261 pub(crate) fn delete(user_id: i32) -> Result<()> {
262 let path = fmt_db_path(user_id);
263 let backup_path = fmt_backup_path(&path);
264 if let Err(e) = fs::remove_file(path) {
265 return log_throw_error!(ErrCode::FileOperationError, "[FATAL][DB]Delete database failed, err={}", e);
266 }
267
268 if let Err(e) = fs::remove_file(backup_path) {
269 return log_throw_error!(
270 ErrCode::FileOperationError,
271 "[FATAL][DB]Delete backup database failed, err={}",
272 e
273 );
274 }
275 Ok(())
276 }
277
278 /// Print the error message of database.
print_db_msg(&self)279 pub(crate) fn print_db_msg(&self) {
280 let msg = unsafe { SqliteErrMsg(self.handle as _) };
281 if !msg.is_null() {
282 let s = unsafe { CStr::from_ptr(msg as _) };
283 if let Ok(rs) = s.to_str() {
284 loge!("[FATAL][DB]Database error message: {}", rs);
285 }
286 }
287 }
288
289 /// execute sql without prepare
exec(&self, sql: &str) -> Result<()>290 pub fn exec(&self, sql: &str) -> Result<()> {
291 let mut sql_s = sql.to_string();
292 sql_s.push('\0');
293 let mut msg: *mut u8 = null_mut();
294 let ret = unsafe { SqliteExec(self.handle as _, sql_s.as_ptr(), &mut msg as _) };
295 if !msg.is_null() {
296 let s = unsafe { CStr::from_ptr(msg as _) };
297 if let Ok(rs) = s.to_str() {
298 return log_throw_error!(
299 sqlite_err_handle(ret),
300 "[FATAL]Database execute sql failed. error code={}, error msg={}",
301 ret,
302 rs
303 );
304 }
305 unsafe { SqliteFree(msg as _) };
306 }
307 if ret == SQLITE_OK {
308 Ok(())
309 } else {
310 log_throw_error!(sqlite_err_handle(ret), "[FATAL]Database execute sql failed. error code={}", ret)
311 }
312 }
313
314 /// execute func in db, if failed and error code is data corrupted then restore
restore_if_exec_fail<T, F: Fn(&Table) -> Result<T>>(&mut self, func: F) -> Result<T>315 pub(crate) fn restore_if_exec_fail<T, F: Fn(&Table) -> Result<T>>(&mut self, func: F) -> Result<T> {
316 let table = Table::new(TABLE_NAME, self);
317 let result = func(&table);
318 match result {
319 Err(ret) if ret.code == ErrCode::DataCorrupted => {
320 self.restore()?;
321 let table = Table::new(TABLE_NAME, self); // Database handle will be changed.
322 func(&table)
323 },
324 ret => ret,
325 }
326 }
327
328 /// Insert datas into database.
329 /// The datas is a map of column-data pair.
330 /// If the operation is successful, the number of inserted data is returned.
331 ///
332 /// # Examples
333 ///
334 /// ```
335 /// use asset_definition::Value;
336 /// use asset_db_operator::{database::Database, types::{column, DbMap}};
337 ///
338 /// // SQL: insert into table_name(Owner,OwnerType,Alias,value) values('owner',1,'alias','insert_value')
339 /// let datas = DbMap::new();
340 /// datas.insert(column::OWNER, Value::Bytes(b"owner".to_ver()));
341 /// datas.insert(column::OWNER_TYPE, Value::Number(OwnerType::Native as u32));
342 /// datas.insert(column::ALIAS, Value::Bytes(b"alias".to_ver()));
343 /// datas.insert("value", Value::Bytes(b"insert_value".to_vec()));
344 /// let user_id = 100;
345 /// let ret = Database::build(user_id)?.insert_datas(&datas);
346 /// ```
347 ///
348 #[inline(always)]
insert_datas(&mut self, datas: &DbMap) -> Result<i32>349 pub fn insert_datas(&mut self, datas: &DbMap) -> Result<i32> {
350 let _lock: std::sync::MutexGuard<'_, i32> = self.db_lock.mtx.lock().unwrap();
351 let closure = |e: &Table| {
352 let mut query = DbMap::new();
353 query.insert_attr(column::ALIAS, datas.get_bytes_attr(&column::ALIAS)?.clone());
354 query.insert_attr(column::OWNER, datas.get_bytes_attr(&column::OWNER)?.clone());
355 query.insert_attr(column::OWNER_TYPE, datas.get_enum_attr::<OwnerType>(&column::OWNER_TYPE)?);
356 if e.is_data_exists(&query, false)? {
357 log_throw_error!(ErrCode::Duplicated, "[FATAL]The data with the specified alias already exists.")
358 } else {
359 e.insert_row(datas)
360 }
361 };
362 self.restore_if_exec_fail(closure)
363 }
364
365 /// Delete datas from database.
366 /// The condition is a map of column-data pair.
367 /// If the operation is successful, the number of deleted data is returned.
368 ///
369 /// # Examples
370 ///
371 /// ```
372 /// use asset_definition::Value;
373 /// use asset_db_operator::{database::Database, types::{column, DbMap}};
374 ///
375 /// // SQL: delete from table_name where Owner='owner' and OwnerType=1 and Alias='alias' and value='delete_value'
376 /// let datas = DbMap::new();
377 /// datas.insert(column::OWNER, Value::Bytes(b"owner".to_ver()));
378 /// datas.insert(column::OWNER_TYPE, Value::Number(OwnerType::Native as u32));
379 /// datas.insert(column::ALIAS, Value::Bytes(b"alias".to_ver()));
380 /// datas.insert("value", Value::Bytes(b"delete_value".to_vec()));
381 /// let user_id = 100;
382 /// let ret = Database::build(user_id)?.delete_datas(&cond, None, false);
383 /// ```
384 ///
385 ///
386 #[inline(always)]
delete_datas( &mut self, condition: &DbMap, reverse_condition: Option<&DbMap>, is_filter_sync: bool, ) -> Result<i32>387 pub fn delete_datas(
388 &mut self,
389 condition: &DbMap,
390 reverse_condition: Option<&DbMap>,
391 is_filter_sync: bool,
392 ) -> Result<i32> {
393 let _lock = self.db_lock.mtx.lock().unwrap();
394 let closure = |e: &Table| e.delete_row(condition, reverse_condition, is_filter_sync);
395 self.restore_if_exec_fail(closure)
396 }
397
398 /// Delete datas from database with specific condition.
399 /// If the operation is successful, the number of deleted data is returned.
400 #[inline(always)]
delete_specific_condition_datas( &mut self, specific_cond: &str, condition_value: &[Value], ) -> Result<i32>401 pub fn delete_specific_condition_datas(
402 &mut self,
403 specific_cond: &str,
404 condition_value: &[Value],
405 ) -> Result<i32> {
406 let _lock = self.db_lock.mtx.lock().unwrap();
407 let closure = |e: &Table| e.delete_with_specific_cond(specific_cond, condition_value);
408 self.restore_if_exec_fail(closure)
409 }
410
411 /// Update datas in database.
412 /// The datas is a map of column-data pair.
413 /// If the operation is successful, the number of updated data is returned.
414 ///
415 /// # Examples
416 ///
417 /// ```
418 /// use asset_definition::Value;
419 /// use asset_db_operator::{database::Database, types::{column, DbMap}};
420 ///
421 /// // SQL: update table_name set alias='update_value' where Owner='owner' and OwnerType=1 and Alias='alias'
422 /// let cond = DbMap.new();
423 /// cond.insert(column::OWNER, Value::Bytes(b"owner".to_ver()));
424 /// cond.insert(column::OWNER_TYPE, Value::Number(OwnerType::Native as u32));
425 /// cond.insert(column::ALIAS, Value::Bytes(b"alias".to_ver()));
426 /// let datas = DbMap::from([("alias", Value::Bytes(b"update_value".to_vec()))]);
427 /// let user_id = 100;
428 /// let ret = Database::build(user_id)?.update_datas(&condition, true, &datas);
429 /// ```
430 #[inline(always)]
update_datas(&mut self, condition: &DbMap, is_filter_sync: bool, datas: &DbMap) -> Result<i32>431 pub fn update_datas(&mut self, condition: &DbMap, is_filter_sync: bool, datas: &DbMap) -> Result<i32> {
432 let _lock = self.db_lock.mtx.lock().unwrap();
433 let closure = |e: &Table| e.update_row(condition, is_filter_sync, datas);
434 self.restore_if_exec_fail(closure)
435 }
436
437 /// Check whether data exists in the database.
438 ///
439 /// # Examples
440 ///
441 /// ```
442 /// use asset_definition::Value;
443 /// use asset_db_operator::{database::Database, types::{column, DbMap}};
444 ///
445 /// // SQL: select count(*) as count from table_name where Owner='owner' and OwnerType=1 and Alias='alias'
446 /// let datas = DbMap::new();
447 /// datas.insert(column::OWNER, Value::Bytes(b"owner".to_ver()));
448 /// datas.insert(column::OWNER_TYPE, Value::Number(OwnerType::Native as u32));
449 /// datas.insert(column::ALIAS, Value::Bytes(b"alias".to_ver()));
450 /// let user_id = 100;
451 /// let exist = Database::build(user_id)?.is_data_exists(&datas, false);
452 /// ```
453 #[inline(always)]
is_data_exists(&mut self, condition: &DbMap, is_filter_sync: bool) -> Result<bool>454 pub fn is_data_exists(&mut self, condition: &DbMap, is_filter_sync: bool) -> Result<bool> {
455 let _lock = self.db_lock.mtx.lock().unwrap();
456 let closure = |e: &Table| e.is_data_exists(condition, is_filter_sync);
457 self.restore_if_exec_fail(closure)
458 }
459
460 /// Query data that meets specified conditions(can be empty) from the database.
461 /// If the operation is successful, the resultSet is returned.
462 ///
463 /// # Examples
464 ///
465 /// ```
466 /// use asset_definition::Value;
467 /// use asset_db_operator::{database::Database, types::{column, DbMap}};
468 ///
469 /// // SQL: select * from table_name where Owner='owner' and OwnerType=1 and Alias='alias'
470 /// let cond = DbMap::new();
471 /// cond.insert(column::OWNER, Value::Bytes(b"owner".to_ver()));
472 /// cond.insert(column::OWNER_TYPE, Value::Number(OwnerType::Native as u32));
473 /// cond.insert(column::ALIAS, Value::Bytes(b"alias".to_ver()));
474 /// let user_id = 100;
475 /// let ret = Database::build(user_id)?.query_datas(&vec![], &cond, None, false);
476 /// ```
477 #[inline(always)]
query_datas( &mut self, columns: &Vec<&'static str>, condition: &DbMap, query_options: Option<&QueryOptions>, is_filter_sync: bool, ) -> Result<Vec<DbMap>>478 pub fn query_datas(
479 &mut self,
480 columns: &Vec<&'static str>,
481 condition: &DbMap,
482 query_options: Option<&QueryOptions>,
483 is_filter_sync: bool,
484 ) -> Result<Vec<DbMap>> {
485 let _lock = self.db_lock.mtx.lock().unwrap();
486 let closure = |e: &Table| e.query_row(columns, condition, query_options, is_filter_sync, COLUMN_INFO);
487 self.restore_if_exec_fail(closure)
488 }
489
490 /// Query data that meets specified conditions(can be empty) from the database.
491 /// If the operation is successful, the resultSet is returned.
492 ///
493 /// # Examples
494 ///
495 /// ```
496 /// use asset_definition::Value;
497 /// use asset_db_operator::{database::Database, types::{column, DbMap}};
498 ///
499 /// // SQL: select * from table_name where Owner='owner' and OwnerType=1 and Alias='alias'
500 /// let cond = DbMap::new();
501 /// cond.insert(column::OWNER, Value::Bytes(b"owner".to_ver()));
502 /// cond.insert(column::OWNER_TYPE, Value::Number(OwnerType::Native as u32));
503 /// cond.insert(column::ALIAS, Value::Bytes(b"alias".to_ver()));
504 /// let user_id = 100;
505 /// let ret = Database::build(user_id)?.query_data_without_lock(&vec![], &cond, None, false);
506 /// ```
query_data_without_lock( &mut self, columns: &Vec<&'static str>, condition: &DbMap, query_options: Option<&QueryOptions>, is_filter_sync: bool, ) -> Result<Vec<DbMap>>507 pub fn query_data_without_lock(
508 &mut self,
509 columns: &Vec<&'static str>,
510 condition: &DbMap,
511 query_options: Option<&QueryOptions>,
512 is_filter_sync: bool,
513 ) -> Result<Vec<DbMap>> {
514 let closure = |e: &Table| e.query_row(columns, condition, query_options, is_filter_sync, COLUMN_INFO);
515 self.restore_if_exec_fail(closure)
516 }
517
518 /// Delete old data and insert new data.
replace_datas(&mut self, condition: &DbMap, is_filter_sync: bool, datas: &DbMap) -> Result<()>519 pub fn replace_datas(&mut self, condition: &DbMap, is_filter_sync: bool, datas: &DbMap) -> Result<()> {
520 let _lock = self.db_lock.mtx.lock().unwrap();
521 let closure = |e: &Table| e.replace_row(condition, is_filter_sync, datas);
522 self.restore_if_exec_fail(closure)
523 }
524 }
525
526 impl Drop for Database {
drop(&mut self)527 fn drop(&mut self) {
528 self.close_db()
529 }
530 }
531