From b561dd18c4146310f8d7627f49c5f5c868d48501 Mon Sep 17 00:00:00 2001 From: Tim Stack Date: Wed, 30 Aug 2023 06:55:24 -0700 Subject: [PATCH] [fstat_vtab] add error column --- src/base/fs_util.hh | 8 + src/fstat_vtab.cc | 159 +++++++++++++----- src/internals/sql-ref.rst | 5 +- src/lnav.cc | 2 +- src/lnav_commands.cc | 4 +- src/lnav_util.hh | 8 - src/scripts/docker-url-handler.lnav | 17 +- src/state-extension-functions.cc | 14 +- test/expected/expected.am | 4 + ...a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out | 7 +- ...16a6fd0ff235a7877e1ea93b22d873a3609402.err | 3 +- ...edc93426e6767aa44ab2356c55327553dcdc8d.err | 3 +- ...664c9cda0ae1c48333e21051b5e0eeafd5b4bc.err | 3 +- ...540973a0dc86320d84706845a15608196ae5be.err | 3 +- ...a83fc90c850cdd11e3136a1a02b79c5879824b.err | 0 ...a83fc90c850cdd11e3136a1a02b79c5879824b.out | 0 ...34a453403934587bbbdde355281a956d1fbe5f.err | 0 ...34a453403934587bbbdde355281a956d1fbe5f.out | 2 + test/test_sql_fs_func.sh | 4 + 19 files changed, 178 insertions(+), 68 deletions(-) create mode 100644 test/expected/test_sql_fs_func.sh_2aa83fc90c850cdd11e3136a1a02b79c5879824b.err create mode 100644 test/expected/test_sql_fs_func.sh_2aa83fc90c850cdd11e3136a1a02b79c5879824b.out create mode 100644 test/expected/test_sql_fs_func.sh_9234a453403934587bbbdde355281a956d1fbe5f.err create mode 100644 test/expected/test_sql_fs_func.sh_9234a453403934587bbbdde355281a956d1fbe5f.out diff --git a/src/base/fs_util.hh b/src/base/fs_util.hh index b9253ff8..db107a08 100644 --- a/src/base/fs_util.hh +++ b/src/base/fs_util.hh @@ -41,6 +41,14 @@ namespace lnav { namespace filesystem { +inline bool +is_glob(const std::string& fn) +{ + return (fn.find('*') != std::string::npos + || fn.find('?') != std::string::npos + || fn.find('[') != std::string::npos); +} + inline int statp(const ghc::filesystem::path& path, struct stat* buf) { diff --git a/src/fstat_vtab.cc b/src/fstat_vtab.cc index 4440fa32..4d7b7ae9 100644 --- a/src/fstat_vtab.cc +++ b/src/fstat_vtab.cc @@ -64,6 +64,7 @@ enum { FSTAT_COL_ATIME, FSTAT_COL_MTIME, FSTAT_COL_CTIME, + FSTAT_COL_ERROR, FSTAT_COL_PATTERN, FSTAT_COL_DATA, }; @@ -93,6 +94,7 @@ CREATE TABLE fstat ( st_atime DATETIME, st_mtime DATETIME, st_ctime DATETIME, + error TEXT, pattern TEXT HIDDEN, data BLOB HIDDEN ); @@ -104,20 +106,20 @@ CREATE TABLE fstat ( static_root_mem c_glob; size_t c_path_index{0}; struct stat c_stat; + std::string c_error; - cursor(sqlite3_vtab* vt) : base({vt}) + explicit cursor(sqlite3_vtab* vt) : base({vt}) { memset(&this->c_stat, 0, sizeof(this->c_stat)); } void load_stat() { - while ((this->c_path_index < this->c_glob->gl_pathc) - && lstat(this->c_glob->gl_pathv[this->c_path_index], - &this->c_stat) - == -1) - { - this->c_path_index += 1; + auto rc = lstat(this->c_glob->gl_pathv[this->c_path_index], + &this->c_stat); + + if (rc == -1) { + this->c_error = strerror(errno); } } @@ -173,13 +175,23 @@ CREATE TABLE fstat ( break; } case FSTAT_COL_DEV: - sqlite3_result_int(ctx, vc.c_stat.st_dev); + if (vc.c_error.empty()) { + sqlite3_result_int(ctx, vc.c_stat.st_dev); + } else { + sqlite3_result_null(ctx); + } break; case FSTAT_COL_INO: - sqlite3_result_int64(ctx, vc.c_stat.st_ino); + if (vc.c_error.empty()) { + sqlite3_result_int64(ctx, vc.c_stat.st_ino); + } else { + sqlite3_result_null(ctx); + } break; case FSTAT_COL_TYPE: - if (S_ISREG(vc.c_stat.st_mode)) { + if (!vc.c_error.empty()) { + sqlite3_result_null(ctx); + } else if (S_ISREG(vc.c_stat.st_mode)) { sqlite3_result_text(ctx, "reg", 3, SQLITE_STATIC); } else if (S_ISBLK(vc.c_stat.st_mode)) { sqlite3_result_text(ctx, "blk", 3, SQLITE_STATIC); @@ -196,60 +208,124 @@ CREATE TABLE fstat ( } break; case FSTAT_COL_MODE: - sqlite3_result_int(ctx, vc.c_stat.st_mode & 0777); + if (vc.c_error.empty()) { + sqlite3_result_int(ctx, vc.c_stat.st_mode & 0777); + } else { + sqlite3_result_null(ctx); + } break; case FSTAT_COL_NLINK: - sqlite3_result_int(ctx, vc.c_stat.st_nlink); + if (vc.c_error.empty()) { + sqlite3_result_int(ctx, vc.c_stat.st_nlink); + } else { + sqlite3_result_null(ctx); + } break; case FSTAT_COL_UID: - sqlite3_result_int(ctx, vc.c_stat.st_uid); + if (vc.c_error.empty()) { + sqlite3_result_int(ctx, vc.c_stat.st_uid); + } else { + sqlite3_result_null(ctx); + } break; case FSTAT_COL_USER: { - struct passwd* pw = getpwuid(vc.c_stat.st_uid); + if (vc.c_error.empty()) { + struct passwd* pw = getpwuid(vc.c_stat.st_uid); - if (pw != nullptr) { - sqlite3_result_text(ctx, pw->pw_name, -1, SQLITE_TRANSIENT); + if (pw != nullptr) { + sqlite3_result_text( + ctx, pw->pw_name, -1, SQLITE_TRANSIENT); + } else { + sqlite3_result_int(ctx, vc.c_stat.st_uid); + } } else { - sqlite3_result_int(ctx, vc.c_stat.st_uid); + sqlite3_result_null(ctx); } break; } case FSTAT_COL_GID: - sqlite3_result_int(ctx, vc.c_stat.st_gid); + if (vc.c_error.empty()) { + sqlite3_result_int(ctx, vc.c_stat.st_gid); + } else { + sqlite3_result_null(ctx); + } break; case FSTAT_COL_GROUP: { - struct group* gr = getgrgid(vc.c_stat.st_gid); + if (vc.c_error.empty()) { + struct group* gr = getgrgid(vc.c_stat.st_gid); - if (gr != nullptr) { - sqlite3_result_text(ctx, gr->gr_name, -1, SQLITE_TRANSIENT); + if (gr != nullptr) { + sqlite3_result_text( + ctx, gr->gr_name, -1, SQLITE_TRANSIENT); + } else { + sqlite3_result_int(ctx, vc.c_stat.st_gid); + } } else { - sqlite3_result_int(ctx, vc.c_stat.st_gid); + sqlite3_result_null(ctx); } break; } case FSTAT_COL_RDEV: - sqlite3_result_int(ctx, vc.c_stat.st_rdev); + if (vc.c_error.empty()) { + sqlite3_result_int(ctx, vc.c_stat.st_rdev); + } else { + sqlite3_result_null(ctx); + } break; case FSTAT_COL_SIZE: - sqlite3_result_int64(ctx, vc.c_stat.st_size); + if (vc.c_error.empty()) { + sqlite3_result_int64(ctx, vc.c_stat.st_size); + } else { + sqlite3_result_null(ctx); + } break; case FSTAT_COL_BLKSIZE: - sqlite3_result_int(ctx, vc.c_stat.st_blksize); + if (vc.c_error.empty()) { + sqlite3_result_int(ctx, vc.c_stat.st_blksize); + } else { + sqlite3_result_null(ctx); + } break; case FSTAT_COL_BLOCKS: - sqlite3_result_int(ctx, vc.c_stat.st_blocks); + if (vc.c_error.empty()) { + sqlite3_result_int(ctx, vc.c_stat.st_blocks); + } else { + sqlite3_result_null(ctx); + } break; case FSTAT_COL_ATIME: - sql_strftime(time_buf, sizeof(time_buf), vc.c_stat.st_atime, 0); - sqlite3_result_text(ctx, time_buf, -1, SQLITE_TRANSIENT); + if (vc.c_error.empty()) { + sql_strftime( + time_buf, sizeof(time_buf), vc.c_stat.st_atime, 0); + sqlite3_result_text(ctx, time_buf, -1, SQLITE_TRANSIENT); + } else { + sqlite3_result_null(ctx); + } break; case FSTAT_COL_MTIME: - sql_strftime(time_buf, sizeof(time_buf), vc.c_stat.st_mtime, 0); - sqlite3_result_text(ctx, time_buf, -1, SQLITE_TRANSIENT); + if (vc.c_error.empty()) { + sql_strftime( + time_buf, sizeof(time_buf), vc.c_stat.st_mtime, 0); + sqlite3_result_text(ctx, time_buf, -1, SQLITE_TRANSIENT); + } else { + sqlite3_result_null(ctx); + } break; case FSTAT_COL_CTIME: - sql_strftime(time_buf, sizeof(time_buf), vc.c_stat.st_ctime, 0); - sqlite3_result_text(ctx, time_buf, -1, SQLITE_TRANSIENT); + if (vc.c_error.empty()) { + sql_strftime( + time_buf, sizeof(time_buf), vc.c_stat.st_ctime, 0); + sqlite3_result_text(ctx, time_buf, -1, SQLITE_TRANSIENT); + } else { + sqlite3_result_null(ctx); + } + break; + case FSTAT_COL_ERROR: + if (vc.c_error.empty()) { + sqlite3_result_null(ctx); + } else { + to_sqlite(ctx, vc.c_error); + } break; case FSTAT_COL_PATTERN: sqlite3_result_text(ctx, @@ -259,7 +335,9 @@ CREATE TABLE fstat ( break; case FSTAT_COL_DATA: { auto fs_path = ghc::filesystem::path{path}; - if (S_ISREG(vc.c_stat.st_mode)) { + if (!vc.c_error.empty()) { + sqlite3_result_null(ctx); + } else if (S_ISREG(vc.c_stat.st_mode)) { auto open_res = lnav::filesystem::open_file(fs_path, O_RDONLY); @@ -366,14 +444,17 @@ rcFilter(sqlite3_vtab_cursor* pVtabCursor, const char* pattern = (const char*) sqlite3_value_text(argv[0]); pCur->c_pattern = pattern; - switch (glob(pattern, + + auto glob_flags = GLOB_ERR; + if (!lnav::filesystem::is_glob(pCur->c_pattern)) { + glob_flags |= GLOB_NOCHECK; + } + #ifdef GLOB_TILDE - GLOB_TILDE | + glob_flags |= GLOB_TILDE; #endif - GLOB_ERR, - nullptr, - pCur->c_glob.inout())) - { + + switch (glob(pattern, glob_flags, nullptr, pCur->c_glob.inout())) { case GLOB_NOSPACE: pVtabCursor->pVtab->zErrMsg = sqlite3_mprintf("No space to perform glob()"); diff --git a/src/internals/sql-ref.rst b/src/internals/sql-ref.rst index 843807c5..e87d71cb 100644 --- a/src/internals/sql-ref.rst +++ b/src/internals/sql-ref.rst @@ -3127,13 +3127,14 @@ radians(*degrees*) .. _raise_error: -raise_error(*msg*) -^^^^^^^^^^^^^^^^^^ +raise_error(*msg*, *\[reason\]*) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Raises an error with the given message when executed **Parameters** * **msg\*** --- The error message + * **reason** --- The reason the error occurred ---- diff --git a/src/lnav.cc b/src/lnav.cc index 38159d68..1d324678 100644 --- a/src/lnav.cc +++ b/src/lnav.cc @@ -2955,7 +2955,7 @@ SELECT tbl_name FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL TABLE%' fmt::format(FMT_STRING(":open {}"), file_path)); } #endif - else if (is_glob(file_path)) + else if (lnav::filesystem::is_glob(file_path)) { lnav_data.ld_active_files.fc_file_names[file_path].with_tail( !(lnav_data.ld_flags & LNF_HEADLESS)); diff --git a/src/lnav_commands.cc b/src/lnav_commands.cc index d4f86eed..b7fb8cfd 100644 --- a/src/lnav_commands.cc +++ b/src/lnav_commands.cc @@ -2699,7 +2699,7 @@ com_open(exec_context& ec, std::string cmdline, std::vector& args) } retval = "info: watching -- " + fn; - } else if (is_glob(fn.c_str())) { + } else if (lnav::filesystem::is_glob(fn.c_str())) { fc.fc_file_names.emplace(fn, loo); files_to_front.emplace_back( loo.loo_filename.empty() ? fn : loo.loo_filename, file_loc); @@ -2827,7 +2827,7 @@ com_open(exec_context& ec, std::string cmdline, std::vector& args) } }); lnav_data.ld_preview_view.set_needs_update(); - } else if (is_glob(fn.c_str())) { + } else if (lnav::filesystem::is_glob(fn.c_str())) { static_root_mem gl; if (glob(fn.c_str(), GLOB_NOCHECK, nullptr, gl.inout()) == 0) { diff --git a/src/lnav_util.hh b/src/lnav_util.hh index 2819b49e..13b5ac6c 100644 --- a/src/lnav_util.hh +++ b/src/lnav_util.hh @@ -81,14 +81,6 @@ to_string(const char* s) } } // namespace std -inline bool -is_glob(const std::string& fn) -{ - return (fn.find('*') != std::string::npos - || fn.find('?') != std::string::npos - || fn.find('[') != std::string::npos); -} - inline void rusagesub(const struct rusage& left, const struct rusage& right, diff --git a/src/scripts/docker-url-handler.lnav b/src/scripts/docker-url-handler.lnav index 588cf62b..93b0857f 100755 --- a/src/scripts/docker-url-handler.lnav +++ b/src/scripts/docker-url-handler.lnav @@ -7,6 +7,8 @@ jget(url, '/path') AS docker_path FROM (SELECT parse_url($1) AS url) +;SELECT substr($docker_path, 2) AS docker_relpath + ;SELECT CASE $docker_hostname WHEN 'compose' THEN ( @@ -18,8 +20,19 @@ ), char(10) ) AS cmds - FROM fstat(substr($docker_path, 2)), - json_each(yaml_to_json(data), '$.services') as compose_services + FROM fstat($docker_relpath) AS st, + json_each( + yaml_to_json( + ifnull( + st.data, + raise_error( + 'Cannot read compose configuration: ' || $docker_relpath, + st.error + ) + ) + ), + '$.services' + ) as compose_services ) ELSE CASE $docker_path diff --git a/src/state-extension-functions.cc b/src/state-extension-functions.cc index b3824710..55c19c73 100644 --- a/src/state-extension-functions.cc +++ b/src/state-extension-functions.cc @@ -99,9 +99,14 @@ sql_lnav_version() } static int64_t -sql_error(const char* str) +sql_error(const char* str, nonstd::optional reason) { - throw sqlite_func_error("{}", str); + auto um = lnav::console::user_message::error(str); + + if (reason) { + um.with_reason(reason->to_string()); + } + throw um; } static nonstd::optional @@ -155,7 +160,10 @@ state_extension_functions(struct FuncDef** basic_funcs, help_text("raise_error", "Raises an error with the given message when executed") .sql_function() - .with_parameter({"msg", "The error message"})) + .with_parameter({"msg", "The error message"}) + .with_parameter( + help_text("reason", "The reason the error occurred") + .optional())) .with_flags(SQLITE_UTF8), sqlite_func_adapter::builder( diff --git a/test/expected/expected.am b/test/expected/expected.am index 05ad065b..1d0f7a38 100644 --- a/test/expected/expected.am +++ b/test/expected/expected.am @@ -736,6 +736,8 @@ EXPECTED_FILES = \ $(srcdir)/%reldir%/test_sql_fs_func.sh_18ddc138b263dd06f3fe81fec05bc4330caffef7.out \ $(srcdir)/%reldir%/test_sql_fs_func.sh_20a76db446a0a558dcbdf41033f97d4a22ca1bfa.err \ $(srcdir)/%reldir%/test_sql_fs_func.sh_20a76db446a0a558dcbdf41033f97d4a22ca1bfa.out \ + $(srcdir)/%reldir%/test_sql_fs_func.sh_2aa83fc90c850cdd11e3136a1a02b79c5879824b.err \ + $(srcdir)/%reldir%/test_sql_fs_func.sh_2aa83fc90c850cdd11e3136a1a02b79c5879824b.out \ $(srcdir)/%reldir%/test_sql_fs_func.sh_2c3f66e78deb8721b1d1fe5a787e9958895401d7.err \ $(srcdir)/%reldir%/test_sql_fs_func.sh_2c3f66e78deb8721b1d1fe5a787e9958895401d7.out \ $(srcdir)/%reldir%/test_sql_fs_func.sh_34baa8050f8278d7b68c29e53bdd9f37da0f34c8.err \ @@ -756,6 +758,8 @@ EXPECTED_FILES = \ $(srcdir)/%reldir%/test_sql_fs_func.sh_7b5d7dd8d0003ab83e3e5cb0a5ce802fe9a0e3b3.out \ $(srcdir)/%reldir%/test_sql_fs_func.sh_917ffde411c1425e8a6addae0170900dcd553986.err \ $(srcdir)/%reldir%/test_sql_fs_func.sh_917ffde411c1425e8a6addae0170900dcd553986.out \ + $(srcdir)/%reldir%/test_sql_fs_func.sh_9234a453403934587bbbdde355281a956d1fbe5f.err \ + $(srcdir)/%reldir%/test_sql_fs_func.sh_9234a453403934587bbbdde355281a956d1fbe5f.out \ $(srcdir)/%reldir%/test_sql_fs_func.sh_9e2c0a90ce333365ff7354375f2c609bc27135c8.err \ $(srcdir)/%reldir%/test_sql_fs_func.sh_9e2c0a90ce333365ff7354375f2c609bc27135c8.out \ $(srcdir)/%reldir%/test_sql_fs_func.sh_a247b137e71124e496f1beab56c7fe85717c4199.err \ diff --git a/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out b/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out index dd945991..c9cd89c8 100644 --- a/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out +++ b/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out @@ -3759,11 +3759,12 @@ For support questions, email: -raise_error(msg) +raise_error(msg, [reason]) ══════════════════════════════════════════════════════════════════════ Raises an error with the given message when executed -Parameter - msg The error message +Parameters + msg The error message + reason The reason the error occurred random() diff --git a/test/expected/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.err b/test/expected/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.err index bc74c2d6..782fe5ee 100644 --- a/test/expected/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.err +++ b/test/expected/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.err @@ -1,4 +1,3 @@ -✘ error: call to raise_error(msg) failed - reason: oops! +✘ error: oops!  --> command-option:1  | ;SELECT raise_error('oops!')  diff --git a/test/expected/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.err b/test/expected/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.err index 600e19d5..979055d2 100644 --- a/test/expected/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.err +++ b/test/expected/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.err @@ -1,5 +1,4 @@ -✘ error: call to raise_error(msg) failed - reason: no data was redirected to lnav's standard-input +✘ error: no data was redirected to lnav's standard-input  --> command-option:1  | |rename-stdin foo   --> ../test/.lnav/formats/default/rename-stdin.lnav:7 diff --git a/test/expected/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.err b/test/expected/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.err index 1a0c2b04..d0680566 100644 --- a/test/expected/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.err +++ b/test/expected/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.err @@ -1,5 +1,4 @@ -✘ error: call to raise_error(msg) failed - reason: expecting the new name for stdin as the first argument +✘ error: expecting the new name for stdin as the first argument  --> command-option:1  | |rename-stdin   --> ../test/.lnav/formats/default/rename-stdin.lnav:6 diff --git a/test/expected/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.err b/test/expected/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.err index c7ee159c..a179b11f 100644 --- a/test/expected/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.err +++ b/test/expected/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.err @@ -1,5 +1,4 @@ -✘ error: call to raise_error(msg) failed - reason: oops! +✘ error: oops!  --> command-option:2  | ;SELECT $inum, $nul, $fnum, $str, raise_error('oops!')  = note: the bound parameters are set as follows: diff --git a/test/expected/test_sql_fs_func.sh_2aa83fc90c850cdd11e3136a1a02b79c5879824b.err b/test/expected/test_sql_fs_func.sh_2aa83fc90c850cdd11e3136a1a02b79c5879824b.err new file mode 100644 index 00000000..e69de29b diff --git a/test/expected/test_sql_fs_func.sh_2aa83fc90c850cdd11e3136a1a02b79c5879824b.out b/test/expected/test_sql_fs_func.sh_2aa83fc90c850cdd11e3136a1a02b79c5879824b.out new file mode 100644 index 00000000..e69de29b diff --git a/test/expected/test_sql_fs_func.sh_9234a453403934587bbbdde355281a956d1fbe5f.err b/test/expected/test_sql_fs_func.sh_9234a453403934587bbbdde355281a956d1fbe5f.err new file mode 100644 index 00000000..e69de29b diff --git a/test/expected/test_sql_fs_func.sh_9234a453403934587bbbdde355281a956d1fbe5f.out b/test/expected/test_sql_fs_func.sh_9234a453403934587bbbdde355281a956d1fbe5f.out new file mode 100644 index 00000000..2329234a --- /dev/null +++ b/test/expected/test_sql_fs_func.sh_9234a453403934587bbbdde355281a956d1fbe5f.out @@ -0,0 +1,2 @@ +st_parent st_name st_dev st_ino st_type st_mode st_nlink st_uid st_user st_gid st_group st_rdev st_size st_blksize st_blocks st_atime st_mtime st_ctime error +/ non-existent No such file or directory diff --git a/test/test_sql_fs_func.sh b/test/test_sql_fs_func.sh index be935cca..7955c572 100644 --- a/test/test_sql_fs_func.sh +++ b/test/test_sql_fs_func.sh @@ -53,3 +53,7 @@ run_cap_test ./drive_sql "select joinpath('foo', 'bar', 'baz')" run_cap_test ./drive_sql "select joinpath('foo', 'bar', 'baz', '/root')" run_cap_test ${lnav_test} -Nn -c ";SELECT shell_exec('echo hi')" + +run_cap_test ${lnav_test} -Nn -c ";SELECT * FROM fstat('/non-existent')" + +run_cap_test ${lnav_test} -Nn -c ";SELECT * FROM fstat('/*.non-existent')"