[misc] more include shuffling

Improve xpath() error messages

Update alt-text in other themes
pull/1179/head
Tim Stack 10 months ago
parent 6c8a0d1bb7
commit 4f4fd4091f

@ -473,6 +473,7 @@ add_library(
fstat_vtab.hh
fts_fuzzy_match.hh
grep_highlighter.hh
hasher.hh
help_text.hh
help_text_formatter.hh
highlighter.hh

@ -212,6 +212,7 @@ noinst_HEADERS = \
fts_fuzzy_match.hh \
grep_highlighter.hh \
grep_proc.hh \
hasher.hh \
help.md \
help.txt \
help_text.hh \

@ -29,6 +29,8 @@
* @file archive_manager.cc
*/
#include <future>
#include <unistd.h>
#include "config.h"
@ -48,7 +50,7 @@
#include "base/lnav_log.hh"
#include "base/paths.hh"
#include "fmt/format.h"
#include "lnav_util.hh"
#include "hasher.hh"
namespace fs = ghc::filesystem;

@ -47,6 +47,7 @@ add_library(
is_utf8.hh
isc.hh
itertools.hh
line_range.hh
lnav.console.hh
lnav.console.into.hh
log_level_enum.hh

@ -44,6 +44,7 @@ noinst_HEADERS = \
is_utf8.hh \
isc.hh \
itertools.hh \
line_range.hh \
lnav_log.hh \
lnav.console.hh \
lnav.console.into.hh \

@ -535,3 +535,112 @@ line_range::shift(int32_t start, int32_t amount)
return *this;
}
string_attrs_t::const_iterator
find_string_attr(const string_attrs_t& sa, size_t near)
{
auto nearest = sa.end();
ssize_t last_diff = INT_MAX;
for (auto iter = sa.begin(); iter != sa.end(); ++iter) {
const auto& lr = iter->sa_range;
if (!lr.is_valid() || !lr.contains(near)) {
continue;
}
ssize_t diff = near - lr.lr_start;
if (diff < last_diff) {
last_diff = diff;
nearest = iter;
}
}
return nearest;
}
void
shift_string_attrs(string_attrs_t& sa, int32_t start, int32_t amount)
{
for (auto& iter : sa) {
iter.sa_range.shift(start, amount);
}
}
struct line_range
find_string_attr_range(const string_attrs_t& sa, string_attr_type_base* type)
{
auto iter = find_string_attr(sa, type);
if (iter != sa.end()) {
return iter->sa_range;
}
return line_range();
}
void
remove_string_attr(string_attrs_t& sa, const line_range& lr)
{
string_attrs_t::iterator iter;
while ((iter = find_string_attr(sa, lr)) != sa.end()) {
sa.erase(iter);
}
}
void
remove_string_attr(string_attrs_t& sa, string_attr_type_base* type)
{
for (auto iter = sa.begin(); iter != sa.end();) {
if (iter->sa_type == type) {
iter = sa.erase(iter);
} else {
++iter;
}
}
}
string_attrs_t::iterator
find_string_attr(string_attrs_t& sa, const line_range& lr)
{
string_attrs_t::iterator iter;
for (iter = sa.begin(); iter != sa.end(); ++iter) {
if (lr.contains(iter->sa_range)) {
break;
}
}
return iter;
}
string_attrs_t::const_iterator
find_string_attr(const string_attrs_t& sa,
const string_attr_type_base* type,
int start)
{
string_attrs_t::const_iterator iter;
for (iter = sa.begin(); iter != sa.end(); ++iter) {
if (iter->sa_type == type && iter->sa_range.lr_start >= start) {
break;
}
}
return iter;
}
nonstd::optional<const string_attr*>
get_string_attr(const string_attrs_t& sa,
const string_attr_type_base* type,
int start)
{
auto iter = find_string_attr(sa, type, start);
if (iter == sa.end()) {
return nonstd::nullopt;
}
return nonstd::make_optional(&(*iter));
}

@ -40,136 +40,10 @@
#include "fmt/format.h"
#include "intern_string.hh"
#include "line_range.hh"
#include "string_attr_type.hh"
#include "string_util.hh"
/**
* Encapsulates a range in a string.
*/
struct line_range {
enum class unit {
bytes,
codepoint,
};
int lr_start;
int lr_end;
unit lr_unit;
explicit line_range(int start = -1, int end = -1, unit u = unit::bytes)
: lr_start(start), lr_end(end), lr_unit(u)
{
}
bool is_valid() const { return this->lr_start != -1; }
int length() const
{
return this->lr_end == -1 ? INT_MAX : this->lr_end - this->lr_start;
}
bool empty() const { return this->length() == 0; }
void clear()
{
this->lr_start = -1;
this->lr_end = -1;
}
int end_for_string(const std::string& str) const
{
return this->lr_end == -1 ? str.length() : this->lr_end;
}
bool contains(int pos) const
{
return this->lr_start <= pos
&& (this->lr_end == -1 || pos < this->lr_end);
}
bool contains(const struct line_range& other) const
{
return this->contains(other.lr_start)
&& (this->lr_end == -1 || other.lr_end <= this->lr_end);
}
bool intersects(const struct line_range& other) const
{
if (this->contains(other.lr_start)) {
return true;
}
if (other.lr_end > 0 && this->contains(other.lr_end - 1)) {
return true;
}
if (other.contains(this->lr_start)) {
return true;
}
return false;
}
line_range intersection(const struct line_range& other) const;
line_range& shift(int32_t start, int32_t amount);
void ltrim(const char* str)
{
while (this->lr_start < this->lr_end && isspace(str[this->lr_start])) {
this->lr_start += 1;
}
}
bool operator<(const struct line_range& rhs) const
{
if (this->lr_start < rhs.lr_start) {
return true;
}
if (this->lr_start > rhs.lr_start) {
return false;
}
// this->lr_start == rhs.lr_start
if (this->lr_end == rhs.lr_end) {
return false;
}
// When the start is the same, the longer range has a lower priority
// than the shorter range.
if (rhs.lr_end == -1) {
return false;
}
if ((this->lr_end == -1) || (this->lr_end > rhs.lr_end)) {
return true;
}
return false;
}
bool operator==(const struct line_range& rhs) const
{
return (this->lr_start == rhs.lr_start && this->lr_end == rhs.lr_end);
}
const char* substr(const std::string& str) const
{
if (this->lr_start == -1) {
return str.c_str();
}
return &(str.c_str()[this->lr_start]);
}
size_t sublen(const std::string& str) const
{
if (this->lr_start == -1) {
return str.length();
}
if (this->lr_end == -1) {
return str.length() - this->lr_start;
}
return this->length();
}
};
inline line_range
to_line_range(const string_fragment& frag)
{
@ -220,35 +94,11 @@ struct string_attr_wrapper {
/** A map of line ranges to attributes for that range. */
using string_attrs_t = std::vector<string_attr>;
inline string_attrs_t::const_iterator
find_string_attr(const string_attrs_t& sa,
const string_attr_type_base* type,
int start = 0)
{
string_attrs_t::const_iterator iter;
for (iter = sa.begin(); iter != sa.end(); ++iter) {
if (iter->sa_type == type && iter->sa_range.lr_start >= start) {
break;
}
}
return iter;
}
inline nonstd::optional<const string_attr*>
get_string_attr(const string_attrs_t& sa,
const string_attr_type_base* type,
int start = 0)
{
auto iter = find_string_attr(sa, type, start);
if (iter == sa.end()) {
return nonstd::nullopt;
}
string_attrs_t::const_iterator find_string_attr(
const string_attrs_t& sa, const string_attr_type_base* type, int start = 0);
return nonstd::make_optional(&(*iter));
}
nonstd::optional<const string_attr*> get_string_attr(
const string_attrs_t& sa, const string_attr_type_base* type, int start = 0);
template<typename T>
inline nonstd::optional<string_attr_wrapper<T>>
@ -282,42 +132,11 @@ find_string_attr_containing(const string_attrs_t& sa,
return iter;
}
inline string_attrs_t::iterator
find_string_attr(string_attrs_t& sa, const struct line_range& lr)
{
string_attrs_t::iterator iter;
string_attrs_t::iterator find_string_attr(string_attrs_t& sa,
const struct line_range& lr);
for (iter = sa.begin(); iter != sa.end(); ++iter) {
if (lr.contains(iter->sa_range)) {
break;
}
}
return iter;
}
inline string_attrs_t::const_iterator
find_string_attr(const string_attrs_t& sa, size_t near)
{
auto nearest = sa.end();
ssize_t last_diff = INT_MAX;
for (auto iter = sa.begin(); iter != sa.end(); ++iter) {
const auto& lr = iter->sa_range;
if (!lr.is_valid() || !lr.contains(near)) {
continue;
}
ssize_t diff = near - lr.lr_start;
if (diff < last_diff) {
last_diff = diff;
nearest = iter;
}
}
return nearest;
}
string_attrs_t::const_iterator find_string_attr(const string_attrs_t& sa,
size_t near);
template<typename T>
inline string_attrs_t::const_iterator
@ -347,47 +166,14 @@ rfind_string_attr_if(const string_attrs_t& sa, ssize_t near, T predicate)
return nearest;
}
inline struct line_range
find_string_attr_range(const string_attrs_t& sa, string_attr_type_base* type)
{
auto iter = find_string_attr(sa, type);
if (iter != sa.end()) {
return iter->sa_range;
}
return line_range();
}
inline void
remove_string_attr(string_attrs_t& sa, const struct line_range& lr)
{
string_attrs_t::iterator iter;
struct line_range find_string_attr_range(const string_attrs_t& sa,
string_attr_type_base* type);
while ((iter = find_string_attr(sa, lr)) != sa.end()) {
sa.erase(iter);
}
}
void remove_string_attr(string_attrs_t& sa, const struct line_range& lr);
inline void
remove_string_attr(string_attrs_t& sa, string_attr_type_base* type)
{
for (auto iter = sa.begin(); iter != sa.end();) {
if (iter->sa_type == type) {
iter = sa.erase(iter);
} else {
++iter;
}
}
}
void remove_string_attr(string_attrs_t& sa, string_attr_type_base* type);
inline void
shift_string_attrs(string_attrs_t& sa, int32_t start, int32_t amount)
{
for (auto& iter : sa) {
iter.sa_range.shift(start, amount);
}
}
void shift_string_attrs(string_attrs_t& sa, int32_t start, int32_t amount);
struct text_wrap_settings {
text_wrap_settings& with_indent(int indent)

@ -37,6 +37,7 @@
#include "config.h"
#include "pcrepp/pcre2pp.hh"
#include "ww898/cp_utf8.hpp"
#include "xxHash/xxhash.h"
const static int TABLE_SIZE = 4095;
@ -301,3 +302,36 @@ string_fragment::to_string_with_case_style(case_style style) const
return retval;
}
uint32_t
string_fragment::front_codepoint() const
{
size_t index = 0;
try {
return ww898::utf::utf8::read(
[this, &index]() { return this->data()[index++]; });
} catch (const std::runtime_error& e) {
return this->data()[0];
}
}
Result<ssize_t, const char*>
string_fragment::codepoint_to_byte_index(ssize_t cp_index) const
{
ssize_t retval = 0;
while (cp_index > 0) {
if (retval >= this->length()) {
return Err("index is beyond the end of the string");
}
auto ch_len = TRY(ww898::utf::utf8::char_size([this, retval]() {
return std::make_pair(this->data()[retval],
this->length() - retval - 1);
}));
retval += ch_len;
cp_index -= 1;
}
return Ok(retval);
}

@ -42,9 +42,9 @@
#include "fmt/format.h"
#include "optional.hpp"
#include "result.h"
#include "scn/util/string_view.h"
#include "strnatcmp.h"
#include "ww898/cp_utf8.hpp"
struct string_fragment {
using iterator = const char*;
@ -157,16 +157,7 @@ struct string_fragment {
char front() const { return this->sf_string[this->sf_begin]; }
uint32_t front_codepoint() const
{
size_t index = 0;
try {
return ww898::utf::utf8::read(
[this, &index]() { return this->data()[index++]; });
} catch (const std::runtime_error& e) {
return this->data()[0];
}
}
uint32_t front_codepoint() const;
char back() const { return this->sf_string[this->sf_end - 1]; }
@ -183,25 +174,8 @@ struct string_fragment {
bool empty() const { return !this->is_valid() || length() == 0; }
Result<ssize_t, const char*> codepoint_to_byte_index(ssize_t cp_index) const
{
ssize_t retval = 0;
while (cp_index > 0) {
if (retval >= this->length()) {
return Err("index is beyond the end of the string");
}
auto ch_len = TRY(ww898::utf::utf8::char_size([this, retval]() {
return std::make_pair(this->data()[retval],
this->length() - retval - 1);
}));
retval += ch_len;
cp_index -= 1;
}
return Ok(retval);
}
Result<ssize_t, const char*> codepoint_to_byte_index(
ssize_t cp_index) const;
const char& operator[](int index) const
{
@ -315,7 +289,9 @@ struct string_fragment {
}
template<typename P>
string_fragment find_left_boundary(size_t start, P&& predicate) const
string_fragment find_left_boundary(size_t start,
P&& predicate,
size_t count = 1) const
{
assert((int) start <= this->length());
@ -324,25 +300,33 @@ struct string_fragment {
}
while (start > 0) {
if (predicate(this->data()[start])) {
start += 1;
break;
count -= 1;
if (count == 0) {
start += 1;
break;
}
}
start -= 1;
}
return string_fragment{
this->sf_string,
(int) start,
this->sf_begin + (int) start,
this->sf_end,
};
}
template<typename P>
string_fragment find_right_boundary(size_t start, P&& predicate) const
string_fragment find_right_boundary(size_t start,
P&& predicate,
size_t count = 1) const
{
while ((int) start < this->length()) {
if (predicate(this->data()[start])) {
break;
count -= 1;
if (count == 0) {
break;
}
}
start += 1;
}
@ -355,10 +339,14 @@ struct string_fragment {
}
template<typename P>
string_fragment find_boundaries_around(size_t start, P&& predicate) const
string_fragment find_boundaries_around(size_t start,
P&& predicate,
size_t count = 1) const
{
return this->template find_left_boundary(start, predicate)
.find_right_boundary(0, predicate);
auto left = this->template find_left_boundary(start, predicate, count);
return left.find_right_boundary(
start - left.sf_begin, predicate, count);
}
nonstd::optional<std::pair<uint32_t, string_fragment>> consume_codepoint()

@ -129,6 +129,12 @@ TEST_CASE("find_left_boundary")
auto world_sf = sf.find_left_boundary(
in1.length() - 3, [](auto ch) { return ch == '\n'; });
CHECK(world_sf.to_string() == "World!\n");
auto world_sf2 = sf.find_left_boundary(
in1.length() - 3, [](auto ch) { return ch == '\n'; }, 2);
CHECK(world_sf2.to_string() == "Hello,\nWorld!\n");
auto world_sf3 = sf.find_left_boundary(
in1.length() - 3, [](auto ch) { return ch == '\n'; }, 3);
CHECK(world_sf3.to_string() == "Hello,\nWorld!\n");
auto full_sf
= sf.find_left_boundary(3, [](auto ch) { return ch == '\n'; });
CHECK(full_sf.to_string() == in1);
@ -137,16 +143,35 @@ TEST_CASE("find_left_boundary")
TEST_CASE("find_right_boundary")
{
std::string in1 = "Hello,\nWorld!\n";
{
auto sf = string_fragment{in1};
const auto sf = string_fragment::from_const("Hello,\nWorld!\n");
auto world_sf = sf.find_right_boundary(
in1.length() - 3, [](auto ch) { return ch == '\n'; });
auto world_sf = sf.find_right_boundary(sf.length() - 3,
string_fragment::tag1{'\n'});
CHECK(world_sf.to_string() == "Hello,\nWorld!");
auto hello_sf
= sf.find_right_boundary(3, [](auto ch) { return ch == '\n'; });
CHECK(hello_sf.to_string() == "Hello,");
auto hello_sf2
= sf.find_right_boundary(3, string_fragment::tag1{'\n'}, 2);
CHECK(hello_sf2.to_string() == "Hello,\nWorld!");
}
}
TEST_CASE("find_boundaries_around")
{
{
const auto sf = string_fragment::from_const(
R"(Hello,
World!
Goodbye,
World!)");
auto all_sf1
= sf.find_boundaries_around(3, string_fragment::tag1{'\n'});
CHECK(all_sf1 == "Hello,");
auto all_sf2
= sf.find_boundaries_around(3, string_fragment::tag1{'\n'}, 2);
CHECK(all_sf2 == "Hello,\nWorld!");
}
}

@ -0,0 +1,162 @@
/**
* Copyright (c) 2023, Timothy Stack
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Timothy Stack nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef lnav_base_line_range_hh
#define lnav_base_line_range_hh
#include <string>
/**
* Encapsulates a range in a string.
*/
struct line_range {
enum class unit {
bytes,
codepoint,
};
int lr_start;
int lr_end;
unit lr_unit;
explicit line_range(int start = -1, int end = -1, unit u = unit::bytes)
: lr_start(start), lr_end(end), lr_unit(u)
{
}
bool is_valid() const { return this->lr_start != -1; }
int length() const
{
return this->lr_end == -1 ? INT_MAX : this->lr_end - this->lr_start;
}
bool empty() const { return this->length() == 0; }
void clear()
{
this->lr_start = -1;
this->lr_end = -1;
}
int end_for_string(const std::string& str) const
{
return this->lr_end == -1 ? str.length() : this->lr_end;
}
bool contains(int pos) const
{
return this->lr_start <= pos
&& (this->lr_end == -1 || pos < this->lr_end);
}
bool contains(const struct line_range& other) const
{
return this->contains(other.lr_start)
&& (this->lr_end == -1 || other.lr_end <= this->lr_end);
}
bool intersects(const struct line_range& other) const
{
if (this->contains(other.lr_start)) {
return true;
}
if (other.lr_end > 0 && this->contains(other.lr_end - 1)) {
return true;
}
if (other.contains(this->lr_start)) {
return true;
}
return false;
}
line_range intersection(const struct line_range& other) const;
line_range& shift(int32_t start, int32_t amount);
void ltrim(const char* str)
{
while (this->lr_start < this->lr_end && isspace(str[this->lr_start])) {
this->lr_start += 1;
}
}
bool operator<(const struct line_range& rhs) const
{
if (this->lr_start < rhs.lr_start) {
return true;
}
if (this->lr_start > rhs.lr_start) {
return false;
}
// this->lr_start == rhs.lr_start
if (this->lr_end == rhs.lr_end) {
return false;
}
// When the start is the same, the longer range has a lower priority
// than the shorter range.
if (rhs.lr_end == -1) {
return false;
}
if ((this->lr_end == -1) || (this->lr_end > rhs.lr_end)) {
return true;
}
return false;
}
bool operator==(const struct line_range& rhs) const
{
return (this->lr_start == rhs.lr_start && this->lr_end == rhs.lr_end);
}
const char* substr(const std::string& str) const
{
if (this->lr_start == -1) {
return str.c_str();
}
return &(str.c_str()[this->lr_start]);
}
size_t sublen(const std::string& str) const
{
if (this->lr_start == -1) {
return str.length();
}
if (this->lr_end == -1) {
return str.length() - this->lr_start;
}
return this->length();
}
};
#endif

@ -45,6 +45,45 @@ using namespace lnav::roles::literals;
namespace lnav {
namespace console {
snippet
snippet::from_content_with_offset(intern_string_t src,
const attr_line_t& content,
size_t offset,
const std::string& errmsg)
{
auto content_sf = string_fragment::from_str(content.get_string());
auto line_with_error = content_sf.find_boundaries_around(
offset, string_fragment::tag1{'\n'});
auto line_with_context = content_sf.find_boundaries_around(
offset, string_fragment::tag1{'\n'}, 3);
auto line_number = content_sf.sub_range(0, offset).count('\n');
auto erroff_in_line = offset - line_with_error.sf_begin;
attr_line_t pointer;
pointer.append(erroff_in_line, ' ')
.append("^ "_snippet_border)
.append(lnav::roles::error(errmsg))
.with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE));
snippet retval;
retval.s_content
= content.subline(line_with_context.sf_begin,
line_with_error.sf_end - line_with_context.sf_begin);
if (line_with_error.sf_end >= retval.s_content.get_string().size()) {
retval.s_content.append("\n");
}
retval.s_content.append(pointer).append(
content.subline(line_with_error.sf_end,
line_with_context.sf_end - line_with_error.sf_end));
retval.s_location = source_location{
src,
static_cast<int32_t>(1 + line_number),
};
return retval;
}
user_message
user_message::raw(const attr_line_t& al)
{

@ -42,6 +42,11 @@ namespace console {
void println(FILE* file, const attr_line_t& al);
struct snippet {
static snippet from_content_with_offset(intern_string_t src,
const attr_line_t& content,
size_t offset,
const std::string& errmsg);
static snippet from(intern_string_t src, const attr_line_t& content)
{
snippet retval;
@ -113,6 +118,26 @@ struct user_message {
return *this;
}
template<typename C>
user_message& with_context_snippets(C snippets)
{
this->um_snippets.insert(this->um_snippets.begin(),
std::make_move_iterator(std::begin(snippets)),
std::make_move_iterator(std::end(snippets)));
if (this->um_snippets.size() > 1) {
for (auto iter = this->um_snippets.begin();
iter != this->um_snippets.end();)
{
if (iter->s_content.empty()) {
iter = this->um_snippets.erase(iter);
} else {
++iter;
}
}
}
return *this;
}
template<typename C>
user_message& with_snippets(C snippets)
{
@ -121,7 +146,8 @@ struct user_message {
std::make_move_iterator(std::end(snippets)));
if (this->um_snippets.size() > 1) {
for (auto iter = this->um_snippets.begin();
iter != this->um_snippets.end();) {
iter != this->um_snippets.end();)
{
if (iter->s_content.empty()) {
iter = this->um_snippets.erase(iter);
} else {

@ -52,7 +52,6 @@
#include "shlex.hh"
#include "sql_util.hh"
#include "vtab_module.hh"
#include "yajlpp/json_ptr.hh"
using namespace lnav::roles::literals;
@ -409,7 +408,7 @@ execute_sql(exec_context& ec, const std::string& sql, std::string& alt_msg)
log_error("sqlite3_step error code: %d", retcode);
auto um = sqlite3_error_to_user_message(lnav_data.ld_db)
.with_snippets(ec.ec_source)
.with_context_snippets(ec.ec_source)
.with_note(bound_note);
return Err(um);

@ -40,11 +40,11 @@
#include <utility>
#include "archive_manager.hh"
#include "base/auto_pid.hh"
#include "base/future_util.hh"
#include "file_format.hh"
#include "logfile_fwd.hh"
#include "safe/safe.h"
#include "tailer/tailer.looper.hh"
struct tailer_progress {
std::string tp_message;

@ -35,6 +35,7 @@
#include "base/auto_fd.hh"
#include "base/fs_util.hh"
#include "base/intern_string.hh"
#include "base/lnav_log.hh"
#include "config.h"
file_format_t
@ -45,13 +46,21 @@ detect_file_format(const ghc::filesystem::path& filename)
}
file_format_t retval = file_format_t::UNKNOWN;
auto_fd fd;
if ((fd = lnav::filesystem::openp(filename, O_RDONLY)) != -1) {
auto open_res = lnav::filesystem::open_file(filename, O_RDONLY);
if (open_res.isErr()) {
log_error("unable to open file for format detection: %s -- %s",
filename.c_str(),
open_res.unwrapErr().c_str());
} else {
auto fd = open_res.unwrap();
uint8_t buffer[32];
ssize_t rc;
auto rc = read(fd, buffer, sizeof(buffer));
if ((rc = read(fd, buffer, sizeof(buffer))) > 0) {
if (rc < 0) {
log_error("unable to read file for format detection: %s -- %s",
filename.c_str(),
strerror(errno));
} else {
static auto SQLITE3_HEADER = "SQLite format 3";
auto header_frag = string_fragment::from_bytes(buffer, rc);

@ -0,0 +1,111 @@
/**
* Copyright (c) 2023, Timothy Stack
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Timothy Stack nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef lnav_hasher_hh
#define lnav_hasher_hh
#include <stdint.h>
#include "base/auto_mem.hh"
#include "base/intern_string.hh"
#include "byte_array.hh"
#include "spookyhash/SpookyV2.h"
class hasher {
public:
using array_t = byte_array<2, uint64_t>;
static constexpr size_t STRING_SIZE = array_t::STRING_SIZE;
hasher() { this->h_context.Init(0, 0); }
hasher& update(const std::string& str)
{
this->h_context.Update(str.data(), str.length());
return *this;
}
hasher& update(const string_fragment& str)
{
this->h_context.Update(str.data(), str.length());
return *this;
}
hasher& update(const char* bits, size_t len)
{
this->h_context.Update(bits, len);
return *this;
}
hasher& update(int64_t value)
{
value = SPOOKYHASH_LITTLE_ENDIAN_64(value);
this->h_context.Update(&value, sizeof(value));
return *this;
}
array_t to_array()
{
uint64_t h1;
uint64_t h2;
array_t retval;
this->h_context.Final(&h1, &h2);
*retval.out(0) = SPOOKYHASH_LITTLE_ENDIAN_64(h1);
*retval.out(1) = SPOOKYHASH_LITTLE_ENDIAN_64(h2);
return retval;
}
void to_string(auto_buffer& buf)
{
array_t bits = this->to_array();
bits.to_string(std::back_inserter(buf));
}
std::string to_string()
{
array_t bits = this->to_array();
return bits.to_string();
}
std::string to_uuid_string()
{
array_t bits = this->to_array();
return bits.to_uuid_string();
}
private:
SpookyHash h_context;
};
#endif

@ -57,8 +57,8 @@
#include "base/math_util.hh"
#include "base/paths.hh"
#include "fmtlib/fmt/format.h"
#include "hasher.hh"
#include "line_buffer.hh"
#include "lnav_util.hh"
#include "scn/scn.h"
using namespace std::chrono_literals;

@ -57,6 +57,7 @@
#include "db_sub_source.hh"
#include "field_overlay_source.hh"
#include "fmt/printf.h"
#include "hasher.hh"
#include "lnav.indexing.hh"
#include "lnav_commands.hh"
#include "lnav_config.hh"

@ -52,12 +52,10 @@
#include "base/intern_string.hh"
#include "base/lnav.console.hh"
#include "base/result.h"
#include "byte_array.hh"
#include "config.h"
#include "fmt/format.h"
#include "optional.hpp"
#include "ptimec.hh"
#include "spookyhash/SpookyV2.h"
#if SIZEOF_OFF_T == 8
# define FORMAT_OFF_T "%lld"
@ -67,77 +65,6 @@
# error "off_t has unhandled size..."
#endif
class hasher {
public:
using array_t = byte_array<2, uint64_t>;
static constexpr size_t STRING_SIZE = array_t::STRING_SIZE;
hasher() { this->h_context.Init(0, 0); }
hasher& update(const std::string& str)
{
this->h_context.Update(str.data(), str.length());
return *this;
}
hasher& update(const string_fragment& str)
{
this->h_context.Update(str.data(), str.length());
return *this;
}
hasher& update(const char* bits, size_t len)
{
this->h_context.Update(bits, len);
return *this;
}
hasher& update(int64_t value)
{
value = SPOOKYHASH_LITTLE_ENDIAN_64(value);
this->h_context.Update(&value, sizeof(value));
return *this;
}
array_t to_array()
{
uint64_t h1;
uint64_t h2;
array_t retval;
this->h_context.Final(&h1, &h2);
*retval.out(0) = SPOOKYHASH_LITTLE_ENDIAN_64(h1);
*retval.out(1) = SPOOKYHASH_LITTLE_ENDIAN_64(h2);
return retval;
}
void to_string(auto_buffer& buf)
{
array_t bits = this->to_array();
bits.to_string(std::back_inserter(buf));
}
std::string to_string()
{
array_t bits = this->to_array();
return bits.to_string();
}
std::string to_uuid_string()
{
array_t bits = this->to_array();
return bits.to_uuid_string();
}
private:
SpookyHash h_context;
};
bool change_to_parent_dir();
bool next_format(const char* const fmt[], int& index, int& locked_index);

@ -34,6 +34,7 @@
#include "base/lnav_log.hh"
#include "base/string_util.hh"
#include "config.h"
#include "hasher.hh"
#include "lnav_util.hh"
#include "logfile_sub_source.hh"
#include "sql_util.hh"

@ -35,7 +35,6 @@
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/param.h>
#include <sys/resource.h>
@ -47,6 +46,7 @@
#include "base/injector.hh"
#include "base/string_util.hh"
#include "config.h"
#include "hasher.hh"
#include "lnav_util.hh"
#include "log.watch.hh"
#include "log_format.hh"

@ -39,8 +39,8 @@
#include "base/paths.hh"
#include "base/time_util.hh"
#include "config.h"
#include "hasher.hh"
#include "line_buffer.hh"
#include "lnav_util.hh"
#include "piper.looper.cfg.hh"
using namespace std::chrono_literals;

@ -46,6 +46,7 @@
#include "base/paths.hh"
#include "command_executor.hh"
#include "config.h"
#include "hasher.hh"
#include "lnav.events.hh"
#include "lnav.hh"
#include "lnav_util.hh"

@ -39,10 +39,10 @@
#include <string.h>
#include <sys/types.h>
#include "base/attr_line.hh"
#include "base/auto_mem.hh"
#include "base/file_range.hh"
#include "base/intern_string.hh"
#include "base/line_range.hh"
#include "base/lnav_log.hh"
#include "scn/util/string_view.h"

@ -675,7 +675,7 @@ annotate_sql_with_error(sqlite3* db, const char* sql, const char* tail)
if (erroff != -1) {
auto line_with_error
= string_fragment(retval.get_string())
= string_fragment::from_str(retval.get_string())
.find_boundaries_around(erroff, string_fragment::tag1{'\n'});
auto erroff_in_line = erroff - line_with_error.sf_begin;

@ -38,7 +38,7 @@
#include "data_scanner.hh"
#include "diseases-json.h"
#include "ghc/filesystem.hpp"
#include "lnav_util.hh"
#include "hasher.hh"
#include "pcrepp/pcre2pp.hh"
#include "words-json.h"
#include "yajlpp/yajlpp_def.hh"

@ -16,8 +16,7 @@
"color": "semantic()"
},
"alt-text": {
"color": "Silver",
"bold": true
"background-color": "#262626"
},
"ok": {
"color": "Green",

@ -24,8 +24,6 @@
"background-color": ""
},
"alt-text": {
"color": "$white",
"background-color": "",
"bold": true
},
"ok": {

@ -25,8 +25,6 @@
"background-color": ""
},
"alt-text": {
"color": "",
"background-color": "",
"bold": true
},
"ok": {

@ -23,7 +23,6 @@
"background-color": "$black"
},
"alt-text": {
"color": "#f6f6f6",
"background-color": "#1c1c1c"
},
"ok": {

@ -24,9 +24,7 @@
"background-color": "#011627"
},
"alt-text": {
"color": "#d6deeb",
"background-color": "#011627",
"bold": true
"background-color": "#1c1c1c"
},
"ok": {
"color": "$green",

@ -33,9 +33,7 @@
"background-color": "$base03"
},
"alt-text": {
"color": "$base0",
"background-color": "$base03",
"bold": true
"background-color": "$base02"
},
"ok": {
"color": "$green",

@ -33,9 +33,7 @@
"background-color": "$base3"
},
"alt-text": {
"color": "$base00",
"background-color": "$base3",
"bold": true
"background-color": "$base2"
},
"ok": {
"color": "$green",

@ -35,7 +35,7 @@ void
unique_path_generator::add_source(
const std::shared_ptr<unique_path_source>& path_source)
{
ghc::filesystem::path path = path_source->get_path();
const auto path = path_source->get_path();
path_source->set_unique_path(path.filename());
path_source->set_path_prefix(path.parent_path());
@ -53,7 +53,7 @@ unique_path_generator::generate()
for (const auto& pair : this->upg_unique_paths) {
if (pair.second.size() == 1) {
if (loop_count > 0) {
std::shared_ptr<unique_path_source> src = pair.second[0];
const auto src = pair.second[0];
src->set_unique_path("[" + src->get_unique_path());
}
@ -67,7 +67,7 @@ unique_path_generator::generate()
do {
std::string common;
for (auto& src : pair.second) {
for (const auto& src : pair.second) {
auto& path = src->get_path_prefix();
if (common.empty()) {
@ -81,7 +81,7 @@ unique_path_generator::generate()
}
if (all_common) {
for (auto& src : pair.second) {
for (const auto& src : pair.second) {
auto& path = src->get_path_prefix();
auto par = path.parent_path();
@ -103,7 +103,7 @@ unique_path_generator::generate()
for (auto& src : collisions) {
const auto unique_path = src->get_unique_path();
auto& prefix = src->get_path_prefix();
const auto& prefix = src->get_path_prefix();
if (loop_count == 0) {
src->set_unique_path(prefix.filename().string() + "]/"
@ -113,7 +113,7 @@ unique_path_generator::generate()
+ unique_path);
}
ghc::filesystem::path parent = prefix.parent_path();
const auto parent = prefix.parent_path();
src->set_path_prefix(parent);

@ -46,6 +46,13 @@ to_sqlite(sqlite3_context* ctx, const lnav::console::user_message& um)
sqlite3_result_error(ctx, errmsg.c_str(), errmsg.size());
}
void
set_vtable_errmsg(sqlite3_vtab* vtab, const lnav::console::user_message& um)
{
vtab->zErrMsg = sqlite3_mprintf(
"%s%s", sqlitepp::ERROR_PREFIX, lnav::to_json(um).c_str());
}
lnav::console::user_message
sqlite3_error_to_user_message(sqlite3* db)
{

@ -218,6 +218,9 @@ struct from_sqlite<vtab_types::nullable<T>> {
void to_sqlite(sqlite3_context* ctx, const lnav::console::user_message& um);
void set_vtable_errmsg(sqlite3_vtab* vtab,
const lnav::console::user_message& um);
inline void
to_sqlite(sqlite3_context* ctx, null_value_t)
{

@ -34,7 +34,6 @@
#include "config.h"
#include "pugixml/pugixml.hpp"
#include "sql_help.hh"
#include "sql_util.hh"
#include "vtab_module.hh"
#include "xml_util.hh"
#include "yajlpp/yajlpp.hh"
@ -314,21 +313,38 @@ rcFilter(sqlite3_vtab_cursor* pVtabCursor,
pCur->c_value.assign(blob, byte_count);
auto parse_res = pCur->c_doc.load_string(pCur->c_value.c_str());
if (!parse_res) {
pVtabCursor->pVtab->zErrMsg
= sqlite3_mprintf("Invalid XML document at offset %d: %s",
static const intern_string_t ARG1 = intern_string::lookup("xmldoc");
auto attr_xmldoc
= attr_line_t(pCur->c_value)
.with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE));
auto um = lnav::console::user_message::error("Invalid XML document")
.with_reason(parse_res.description())
.with_snippet(
lnav::console::snippet::from_content_with_offset(
ARG1,
attr_xmldoc,
parse_res.offset,
parse_res.description());
parse_res.description()));
set_vtable_errmsg(pVtabCursor->pVtab, um);
return SQLITE_ERROR;
}
pCur->c_xpath = (const char*) sqlite3_value_text(argv[0]);
pCur->c_query = checkout_query(pCur->c_xpath);
if (!pCur->c_query) {
auto& res = pCur->c_query.result();
pVtabCursor->pVtab->zErrMsg
= sqlite3_mprintf("Invalid XPATH expression at offset %d: %s",
res.offset,
res.description());
static const intern_string_t ARG0 = intern_string::lookup("xpath");
const auto& res = pCur->c_query.result();
auto attr_xpath
= attr_line_t(pCur->c_xpath)
.with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE));
auto um = lnav::console::user_message::error("Invalid XPath expression")
.with_reason(res.description())
.with_snippet(
lnav::console::snippet::from_content_with_offset(
ARG0, attr_xpath, res.offset, res.description()));
set_vtable_errmsg(pVtabCursor->pVtab, um);
return SQLITE_ERROR;
}

@ -272,6 +272,7 @@ dist_noinst_DATA = \
dhcp.pcapng \
dhcp-trunc.pcapng \
expected_help.txt \
invalid-books.xml \
listview_output.0 \
listview_output.1 \
listview_output.2 \

@ -1014,10 +1014,14 @@ EXPECTED_FILES = \
$(srcdir)/%reldir%/test_sql_xml_func.sh_46dfa23e2effabf3fa150c4b871fd8d22b1c834d.out \
$(srcdir)/%reldir%/test_sql_xml_func.sh_4effabf11b59580e5f0727199eb74fba049c0cda.err \
$(srcdir)/%reldir%/test_sql_xml_func.sh_4effabf11b59580e5f0727199eb74fba049c0cda.out \
$(srcdir)/%reldir%/test_sql_xml_func.sh_81ad7678f080870956db37174bcf1054586cfbad.err \
$(srcdir)/%reldir%/test_sql_xml_func.sh_81ad7678f080870956db37174bcf1054586cfbad.out \
$(srcdir)/%reldir%/test_sql_xml_func.sh_8912b59d5b515ab1373a3d9bc635ebabacd01dfd.err \
$(srcdir)/%reldir%/test_sql_xml_func.sh_8912b59d5b515ab1373a3d9bc635ebabacd01dfd.out \
$(srcdir)/%reldir%/test_sql_xml_func.sh_b036c73528a446cba46625767517cdac868aba72.err \
$(srcdir)/%reldir%/test_sql_xml_func.sh_b036c73528a446cba46625767517cdac868aba72.out \
$(srcdir)/%reldir%/test_sql_xml_func.sh_bcbd691bb24c4f7bcb9fe0e035b290815f1c8874.err \
$(srcdir)/%reldir%/test_sql_xml_func.sh_bcbd691bb24c4f7bcb9fe0e035b290815f1c8874.out \
$(srcdir)/%reldir%/test_sql_xml_func.sh_fefeb387ae14d4171225ea06cbbff3ec43990cf0.err \
$(srcdir)/%reldir%/test_sql_xml_func.sh_fefeb387ae14d4171225ea06cbbff3ec43990cf0.out \
$(srcdir)/%reldir%/test_sql_yaml_func.sh_dc189d02e8979b7ed245d5d750f68b9965984699.err \

@ -844,11 +844,11 @@ For support questions, email:
Example
#1 To create a table named 'task_durations' that matches log messages with the pattern
'duration=(?<duration>\d+)':
:create-search-table task_durations duration=(?<duration>\d+)
:create-search-table task_durations duration=(?<duration>\d+)
:current-time
:current-time
══════════════════════════════════════════════════════════════════════
Print the current time in human-readable form and seconds since the
epoch

@ -0,0 +1,11 @@
✘ error: Invalid XML document
reason: Error parsing element attribute
 --> command-option:1
 | ;SELECT * FROM xpath('/catalog', (SELECT content FROM lnav_file LIMIT 1))
 --> xmldoc:35
 |  </description> 
 |  </book> 
 |  <book id=" bk104"> 
 |  ^ Error parsing element attribute
 |  <author>Corets, Eva</author> 
 |  <title>Oberon's Legacy</title> 

@ -1 +1 @@
error: sqlite3_exec failed -- Invalid XPATH expression at offset 5: Unrecognized node test
error: sqlite3_exec failed -- lnav-error:{"level":"error","message":{"str":"Invalid XPath expression","attrs":[]},"reason":{"str":"Unrecognized node test","attrs":[]},"snippets":[{"source":"xpath","line":1,"content":{"str":"/abc[\n ^ Unrecognized node test","attrs":[{"start":0,"end":5,"type":"role","value":40},{"start":11,"end":13,"type":"role","value":74},{"start":13,"end":35,"type":"role","value":5},{"start":6,"end":35,"type":"role","value":40},{"start":35,"end":35,"type":"role","value":40}]}}],"help":{"str":"","attrs":[]}}

@ -0,0 +1,7 @@
✘ error: Invalid XPath expression
reason: Expected ']' to match an opening '['
 --> command-option:1
 | ;SELECT * FROM xpath('/cat[alog', (SELECT content FROM lnav_file LIMIT 1))
 --> xpath:1
 | /cat[alog 
 |  ^ Expected ']' to match an opening '['

@ -1 +1 @@
error: sqlite3_exec failed -- Invalid XML document at offset 3: Error parsing start element tag
error: sqlite3_exec failed -- lnav-error:{"level":"error","message":{"str":"Invalid XML document","attrs":[]},"reason":{"str":"Error parsing start element tag","attrs":[]},"snippets":[{"source":"xmldoc","line":1,"content":{"str":"<abc\n ^ Error parsing start element tag","attrs":[{"start":0,"end":4,"type":"role","value":40},{"start":8,"end":10,"type":"role","value":74},{"start":10,"end":41,"type":"role","value":5},{"start":5,"end":41,"type":"role","value":40},{"start":41,"end":41,"type":"role","value":40}]}}],"help":{"str":"","attrs":[]}}

@ -0,0 +1,132 @@
<?xml version="1.0"?>
<catalog>
<book id="bk101">
<author>Gambardella, Matthew</author>
<title>XML Developer's Guide</title>
<genre>Computer</genre>
<price>44.95</price>
<publish_date>2000-10-01</publish_date>
<description>An in-depth look at creating applications
with XML.
</description>
</book>
<book id="bk102">
<author>Ralls, Kim</author>
<title>Midnight Rain</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2000-12-16</publish_date>
<description>A former architect battles corporate zombies,
an evil sorceress, and her own childhood to become queen
of the world.
</description>
</book>
<book id="bk103" abc=">
<author>Corets, Eva</author>
<title>Maeve Ascendant</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2000-11-17</publish_date>
<description>After the collapse of a nanotechnology
society in England, the young survivors lay the
foundation for a new society.
</description>
</book>
<book id=" bk104">
<author>Corets, Eva</author>
<title>Oberon's Legacy</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2001-03-10</publish_date>
<description>In post-apocalypse England, the mysterious
agent known only as Oberon helps to create a new life
for the inhabitants of London. Sequel to Maeve
Ascendant.
</description>
</book>
<book id="bk105">
<author>Corets, Eva</author>
<title>The Sundered Grail</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2001-09-10</publish_date>
<description>The two daughters of Maeve, half-sisters,
battle one another for control of England. Sequel to
Oberon's Legacy.
</description>
</book>
<book id="bk106">
<author>Randall, Cynthia</author>
<title>Lover Birds</title>
<genre>Romance</genre>
<price>4.95</price>
<publish_date>2000-09-02</publish_date>
<description>When Carla meets Paul at an ornithology
conference, tempers fly as feathers get ruffled.
</description>
</book>
<book id="bk107">
<author>Thurman, Paula</author>
<title>Splish Splash</title>
<genre>Romance</genre>
<price>4.95</price>
<publish_date>2000-11-02</publish_date>
<description>A deep sea diver finds true love twenty
thousand leagues beneath the sea.
</description>
</book>
<book id="bk108">
<author>Knorr, Stefan</author>
<title>Creepy Crawlies</title>
<genre>Horror</genre>
<price>4.95</price>
<publish_date>2000-12-06</publish_date>
<description>An anthology of horror stories about roaches,
centipedes, scorpions and other insects.
</description>
</book>
<book id="bk109">
<author>Kress, Peter</author>
<title>Paradox Lost</title>
<genre>Science Fiction</genre>
<price>6.95</price>
<publish_date>2000-11-02</publish_date>
<description>After an inadvertant trip through a Heisenberg
Uncertainty Device, James Salway discovers the problems
of being quantum.
</description>
</book>
<book id="bk110">
<author>O'Brien, Tim</author>
<title>Microsoft .NET: The Programming Bible</title>
<genre>Computer</genre>
<price>36.95</price>
<publish_date>2000-12-09</publish_date>
<description>Microsoft's .NET initiative is explored in
detail in this deep programmer's reference.
</description>
</book>
<book id="bk111">
<author>O'Brien, Tim</author>
<title>MSXML3: A Comprehensive Guide</title>
<genre>Computer</genre>
<price>36.95</price>
<publish_date>2000-12-01</publish_date>
<description>The Microsoft MSXML3 parser is covered in
detail, with attention to XML DOM interfaces, XSLT processing,
SAX and more.
</description>
</book>
<book id="bk112">
<author>Galos, Mike</author>
<title>Visual Studio 7: A Comprehensive Guide</title>
<genre>Computer</genre>
<price>49.95</price>
<publish_date>2001-04-16</publish_date>
<description>Microsoft Visual Studio 7 is explored in depth,
looking at how Visual Basic, Visual C++, C#, and ASP+ are
integrated into a comprehensive development
environment.
</description>
</book>
</catalog>

@ -1,5 +1,7 @@
#! /bin/bash
export YES_COLOR=1
run_cap_test ./drive_sql "SELECT * FROM xpath('/abc[', '<abc/>')"
run_cap_test ./drive_sql "SELECT * FROM xpath('/abc', '<abc')"
@ -9,3 +11,11 @@ run_cap_test ./drive_sql "SELECT * FROM xpath('/abc/def', '<abc/>')"
run_cap_test ./drive_sql "SELECT * FROM xpath('/abc/def[@a=\"b\"]', '<abc><def/><def a=\"b\">ghi</def></abc>')"
run_cap_test ./drive_sql "SELECT * FROM xpath('/abc/def', '<abc><def>Hello &gt;</def></abc>')"
run_cap_test ${lnav_test} -n \
-c ";SELECT * FROM xpath('/catalog', (SELECT content FROM lnav_file LIMIT 1))" \
${test_dir}/invalid-books.xml
run_cap_test ${lnav_test} -n \
-c ";SELECT * FROM xpath('/cat[alog', (SELECT content FROM lnav_file LIMIT 1))" \
${test_dir}/books.xml

Loading…
Cancel
Save