Skip to content

Commit

Permalink
CCBC-1384: update parser for golang duration
Browse files Browse the repository at this point in the history
Now it follows golang behaviour more closely

Change-Id: I964e89710b40375a37497a07854008bfc3c19e51
Reviewed-on: http://review.couchbase.org/c/libcouchbase/+/155445
Tested-by: Build Bot <build@couchbase.com>
Reviewed-by: Brett Lawson <brett19@gmail.com>
  • Loading branch information
avsej committed Jun 10, 2021
1 parent 6014a47 commit b3f9e5e
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 42 deletions.
3 changes: 2 additions & 1 deletion cmake/source_files.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ SET(LCB_CORE_CXXSRC
src/http/http_io.cc
src/lcbht/lcbht.cc
src/newconfig.cc
src/n1ql/n1ql.cc
src/n1ql/ixmgmt.cc
src/n1ql/n1ql.cc
src/n1ql/query_utils.cc
src/cbft.cc
src/operations/cbflush.cc
src/operations/counter.cc
Expand Down
3 changes: 0 additions & 3 deletions src/n1ql/n1ql-internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ void lcb_n1qlcache_clear(lcb_N1QLCACHE *);

#ifdef __cplusplus
void lcb_n1qlcache_getplan(lcb_N1QLCACHE *cache, const std::string &key, std::string &out);

// Parse timeout value. Exposed for tests
lcb_U32 lcb_n1qlreq_parsetmo(const std::string &s);
}
#endif
#endif
40 changes: 9 additions & 31 deletions src/n1ql/n1ql.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
#include <regex>
#include <utility>

#include "query_utils.hh"

#include "capi/query.hh"
#include "capi/cmd_http.hh"

Expand Down Expand Up @@ -890,36 +892,6 @@ lcb_STATUS N1QLREQ::apply_plan(const Plan &plan)
return issue_htreq(bodystr);
}

lcb_U32 lcb_n1qlreq_parsetmo(const std::string &s)
{
double num;
int nchars, rv;

rv = sscanf(s.c_str(), "%lf%n", &num, &nchars);
if (rv != 1) {
return 0;
}
std::string mults = s.substr(nchars);

// Get the actual timeout value in microseconds. Note we can't use the macros
// since they will truncate the double value.
if (mults == "s") {
return num * static_cast<double>(LCB_S2US(1));
} else if (mults == "ms") {
return num * static_cast<double>(LCB_MS2US(1));
} else if (mults == "h") {
return num * static_cast<double>(LCB_S2US(3600));
} else if (mults == "us") {
return num;
} else if (mults == "m") {
return num * static_cast<double>(LCB_S2US(60));
} else if (mults == "ns") {
return LCB_NS2US(num);
} else {
return 0;
}
}

lcb_QUERY_HANDLE_::lcb_QUERY_HANDLE_(lcb_INSTANCE *obj, void *user_cookie, const lcb_CMDQUERY *cmd)
: cur_htresp(nullptr), htreq(nullptr), parser(new lcb::jsparse::Parser(lcb::jsparse::Parser::MODE_N1QL, this)),
cookie(user_cookie), callback(cmd->callback), instance(obj), lasterr(LCB_SUCCESS), flags(cmd->cmdflags),
Expand Down Expand Up @@ -979,7 +951,13 @@ lcb_QUERY_HANDLE_::lcb_QUERY_HANDLE_(lcb_INSTANCE *obj, void *user_cookie, const
tmoval = buf;
json["timeout"] = buf;
} else if (tmoval.isString()) {
timeout = lcb_n1qlreq_parsetmo(tmoval.asString());
try {
auto tmo_ns = lcb_parse_golang_duration(tmoval.asString());
timeout = std::chrono::duration_cast<std::chrono::microseconds>(tmo_ns).count();
} catch (const lcb_duration_parse_error &) {
lasterr = LCB_ERR_INVALID_ARGUMENT;
return;
}
} else {
// Timeout is not a string!
lasterr = LCB_ERR_INVALID_ARGUMENT;
Expand Down
199 changes: 199 additions & 0 deletions src/n1ql/query_utils.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/* -*- Mode: C; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
* Copyright 2016-2021 Couchbase, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "query_utils.hh"

/**
* leading_int consumes the leading [0-9]* from s.
*/
static bool leading_int(std::string &s, std::int64_t &v)
{
v = 0;
std::size_t i = 0;
for (; i < s.size(); ++i) {
auto c = s[i];

if (c < '0' || c > '9') {
break;
}

if (v > std::int64_t((1LLU << 63U) - 1LLU) / 10) {
return false;
}

v = v * 10 + std::int64_t(c) - '0';

if (v < 0) {
return false;
}
}
s = s.substr(i);
return true;
}

/**
* leading_fraction consumes the leading [0-9]* from s.
*
* It is used only for fractions, so does not return an error on overflow,
* it just stops accumulating precision.
*/

static void leading_fraction(std::string &s, std::int64_t &x, std::uint32_t &scale)
{
std::size_t i = 0;
scale = 1;

bool overflow = false;

for (; i < s.size(); ++i) {
auto c = s[i];

if (c < '0' || c > '9') {
break;
}

if (overflow) {
continue;
}

if (x > std::int64_t((1LLU << 63LLU) - 1LLU) / 10) {
// It's possible for overflow to give a positive number, so take care.
overflow = true;
continue;
}

auto y = x * 10 + std::int64_t(c) - '0';
if (y < 0) {
overflow = true;
continue;
}

x = y;
scale *= 10;
}

s = s.substr(i);
}

/**
* Parses a duration string.
* A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix,
* such as "300ms", "-1.5h" or "2h45m".
*
* Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
*/
std::chrono::nanoseconds lcb_parse_golang_duration(const std::string &text)
{
// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
std::string s = text;
std::chrono::nanoseconds d{0};
bool neg{false};

// Consume [-+]?
if (!s.empty()) {
auto c = s[0];

if (c == '-' || c == '+') {
neg = c == '-';
s = s.substr(1);
}
}
if (neg) {
throw lcb_duration_parse_error("negative durations are not supported: " + text);
}

// Special case: if all that is left is "0", this is zero.
if (s == "0") {
return std::chrono::nanoseconds::zero();
}

if (s.empty()) {
throw lcb_duration_parse_error("invalid duration: " + text);
}

while (!s.empty()) {
// The next character must be [0-9.]
if (!(s[0] == '.' || ('0' <= s[0] && s[0] <= '9'))) {
throw lcb_duration_parse_error("invalid duration: " + text);
}

// Consume [0-9]*
auto pl = s.size();

std::int64_t v{0}; // integer before decimal point
if (!leading_int(s, v)) {
throw lcb_duration_parse_error("invalid duration (leading_int overflow): " + text);
}

bool pre = pl != s.size(); // whether we consumed anything before a period

std::int64_t f{0}; // integer after decimal point
std::uint32_t scale{1}; // value = v + f/scale

// Consume (\.[0-9]*)?
bool post = false;
if (!s.empty() && s[0] == '.') {
s = s.substr(1);
pl = s.size();
leading_fraction(s, f, scale);
post = pl != s.size();
}

if (!pre && !post) {
// no digits (e.g. ".s" or "-.s")
throw lcb_duration_parse_error("invalid duration: " + text);
}

// Consume unit.
std::size_t i = 0;
for (; i < s.size(); ++i) {
auto c = s[i];
if (c == '.' || ('0' <= c && c <= '9')) {
break;
}
}
if (i == 0) {
throw lcb_duration_parse_error("missing unit in duration: " + text);
}

auto u = s.substr(0, i);
s = s.substr(i);

if (u == "ns") {
d += std::chrono::nanoseconds(v); /* no sub-nanoseconds, ignore 'f' */
} else if (u == "us" || u == "µs" /* U+00B5 = micro symbol */ || u == "μs" /* U+03BC = Greek letter mu */) {
d += std::chrono::microseconds(v) +
std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::microseconds(f)) / scale;
} else if (u == "ms") {
d += std::chrono::milliseconds(v) +
std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::milliseconds(f)) / scale;
} else if (u == "s") {
d += std::chrono::seconds(v) +
std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::seconds(f)) / scale;
} else if (u == "m") {
d += std::chrono::minutes(v) +
std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::minutes(f)) / scale;
} else if (u == "h") {
d += std::chrono::hours(v) +
std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::hours(f)) / scale;
} else {
throw lcb_duration_parse_error(std::string("unknown unit ").append(u).append(" in duration ").append(text));
}
}

return d;
}
41 changes: 41 additions & 0 deletions src/n1ql/query_utils.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* -*- Mode: C; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
* Copyright 2016-2021 Couchbase, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#ifndef LIBCOUCHBASE_N1QL_QUERY_UTILS_HH
#define LIBCOUCHBASE_N1QL_QUERY_UTILS_HH

#include <cstddef>
#include <cstdint>
#include <chrono>
#include <string>
#include <stdexcept>

/**
* @private
*/
class lcb_duration_parse_error : public std::runtime_error
{
public:
explicit lcb_duration_parse_error(const std::string &msg) : std::runtime_error(msg) {}
};

/**
* @private
*/
std::chrono::nanoseconds lcb_parse_golang_duration(const std::string &text);

#endif // LIBCOUCHBASE_N1QL_QUERY_UTILS_HH
16 changes: 9 additions & 7 deletions tests/basic/t_n1qlstrings.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,20 @@
#include "config.h"
#include <gtest/gtest.h>
#include <libcouchbase/couchbase.h>
#include "n1ql/n1ql-internal.h"

#include "n1ql/query_utils.hh"

class N1qLStringTests : public ::testing::Test
{
};

TEST_F(N1qLStringTests, testParseTimeout)
{
ASSERT_EQ(1500000, lcb_n1qlreq_parsetmo("1.5s"));
ASSERT_EQ(1500000, lcb_n1qlreq_parsetmo("1500ms"));
ASSERT_EQ(1500000, lcb_n1qlreq_parsetmo("1500000us"));
ASSERT_EQ(0, lcb_n1qlreq_parsetmo("blahblah"));
ASSERT_EQ(0, lcb_n1qlreq_parsetmo("124"));
ASSERT_EQ(0, lcb_n1qlreq_parsetmo("99z"));
ASSERT_EQ(std::chrono::nanoseconds(5003000LLU), lcb_parse_golang_duration("5ms3us"));
ASSERT_EQ(std::chrono::nanoseconds(1500000000LLU), lcb_parse_golang_duration("1.5s"));
ASSERT_EQ(std::chrono::nanoseconds(1500000000LLU), lcb_parse_golang_duration("1500ms"));
ASSERT_EQ(std::chrono::nanoseconds(1500000000LLU), lcb_parse_golang_duration("1500000us"));
ASSERT_THROW(lcb_parse_golang_duration("blahblah"), lcb_duration_parse_error);
ASSERT_THROW(lcb_parse_golang_duration("124"), lcb_duration_parse_error);
ASSERT_THROW(lcb_parse_golang_duration("99z"), lcb_duration_parse_error);
}

0 comments on commit b3f9e5e

Please sign in to comment.