跳到主要内容

Boost.URL

阅读量

0

阅读人次

0

快速上手

URLs

解析 集装箱

分段

分层方案通常将路径(Path)解释为斜线分隔的百分号编码(Percent Encoded)字符串序列,称为段。 在此库中,可以使用这些引用基础 URL 的独立双向视图类型来访问片段:

类型访问对象描述
segments_viewsegments解码分段的只读范围
segments_refsegments解码分段的可修改范围
segments_encoded_viewencoded_segments分段的只读范围
segments_encoded_refencoded_segments分段的可修改范围

首先,我们观察这些关于路径和段的不变性:

  • 所有 URL 都有一个路径
  • 路径是绝对的、相对的或空的
  • 以“/”开头的路径是绝对路径
  • 相对路径不能出现在认证后面
  • 每个路径都映射到一个唯一的段范围,加上一个布尔值指示该路径是否是绝对路径。
  • 每个段范围,加上指示路径是否为绝对路径的布尔值,映射到一个唯一路径。

以下 URL 包含一个包含三个分段的路径:“path”、“to”和“file.txt”:

http://www.example.com/path/to/file.txt

为了理解路径和段之间的关系,我们定义了这个函数 segs() ,它返回一个字符串列表,该列表对应于段容器中的元素:

auto segs( string_view s ) -> std::list< std::string > {
url_view u( s );
std::list< std::string > seq;
for( auto seg : u.encoded_segments() )
seq.push_back( seg.decode() );
return seq;
}

在此表中,我们显示了调用具有不同路径的段的结果。 这演示了库如何针对各种有趣的情况实现上述不变性:

ssegs(s)是否为绝对路径
""{ }
"/"{ }
"./"{ "" }
"usr"{ "usr" }
"./usr"{ "usr" }
"/index.htm"{ "index.htm" }
"/images/cat-pic.gif"{ "images", "cat-pic.gif" }
"images/cat-pic.gif"{ "images", "cat-pic.gif" }
"/fast//query"{ "fast", "", "query" }
"fast//"{ "fast", "", "" }
"/./"{ "" }
".//"{ "", "" }

这意味着两个路径可能映射到相同的段序列。 在路径“usr”和“./usr”中,“./”是一个前缀,它可能是维护 url_view_base 实例始终引用有效 URL 的不变性所必需的。 因此,两个路径都映射到 { "usr" }。 另一方面,每个序列确定给定 URL 的唯一路径。 例如,将段设置为 {"a"} 将始终映射到“./a”或“a”,具体取决于前缀.是保持 URL 有效所必需的。

序列不会迭代前导. 当需要保持 URL 有效时。 因此,当我们将 { "x", "y", "z" } 分配给段时,序列总是在其后包含 { "x", "y", "z" }。 它从不包含 { ".", "x", "y", "z" } 因为 "." 需要包括在内。 换句话说,段容器的内容是可信的,路径字符串是它们的函数。 反之亦然。

修改路径的各个部分或设置整个路径的库算法尝试与预期的行为一致,就好像操作是在等效序列上执行的一样。 例如,如果一条路径映射到三元素序列 {“a”、“b”、“c”},那么擦除中间部分应该导致序列 {“a”、“c”}。 库总是努力准确地完成调用者的请求; 但是,在某些情况下,这会导致 URL 无效,或者 URL 语义发生显着且不需要的更改。

例如考虑以下 URL:

url u = url().set_path( "kyle:xy" );

该库生成 URL 字符串“kyle%3Axy”而不是“kyle:xy”,因为后者会有意想不到的方案。 下表展示了通过对包含路径的 URL 执行各种修改所获得的结果:

URL操作结果
"info:kyle:xy"remove_scheme()"kyle%3Axy"
"kyle%3Axy"set_scheme( "gopher" )"gopher:kyle:xy"
"http://www.example.com//kyle:xy"remove_authority()"http:/.//kyle:xy"
"//www.example.com//kyle:xy"remove_authority()"/.//kyle:xy"
"http://www.example.com//kyle:xy"remove_origin()"/.//kyle:xy"
"info:kyle:xy"remove_origin()"kyle%3Axy"
"/kyle:xy"set_path_absolute( false )"kyle%3Axy"
"kyle%3Axy"set_path_absolute( true )"/kyle:xy"
""set_path( "kyle:xy" )"kyle%3Axy"
""set_path( "//foo/fighters.txt" )"/.//foo/fighters.txt"
"my%3Asharona/billa%3Abong"normalize()"my%3Asharona/billa:bong"
"./my:sharona"normalize()"my%3Asharona"

参数

正常化 字符串令牌

百分号编码

编码

encode()可用于使用指定的 CharSet 对字符串进行百分比编码。

std::string s = encode("hello world!", unreserved_chars);
assert(s == "hello%20world%21");

可以使用encode_opts调整一些参数,例如编码空格作为加号 (+):

encoding_opts opt;
opt.space_as_plus = true;
std::string s = encode("msg=hello world", pchars, opt);
assert(s == "msg=hello+world");

函数的结果类型也可以通过 StringToken 指定,以便可以重用或追加字符串。

std::string s;
encode("hello ", pchars, {}, string_token::assign_to(s));
encode("world", pchars, {}, string_token::append_to(s));
assert(s == "hello%20world");

我们还可以在尝试编码之前使用 encoded_size 来确定所需的大小:

string_view e = "hello world";
std::string s;
s.reserve(encoded_size(e, pchars));
encode(e, pchars, {}, string_token::assign_to(s));
assert(s == "hello%20world");

在其他场景下,字符串也可以直接编码到缓冲区中:

string_view e = "hello world";
std::string s;
s.resize(encoded_size(e, pchars));
encode(&s[0], s.size(), e, pchars);
assert(s == "hello%20world");

验证

类 pct_string_view 表示参考百分比编码字符串:

pct_string_view sv = "hello%20world";
assert(sv == "hello%20world");

pct_string_view 类似于 string_view,主要区别在于底层缓冲区的百分比编码始终有效。 尝试从无效字符串直接构造 pct_string_view 会引发异常。

为了简单地验证一个字符串而不重复出现异常,可以使用 make_pct_string_view 返回一个结果:

result<pct_string_view> rs = make_pct_string_view("hello%20world");
assert(rs.has_value());
pct_string_view sv = rs.value();
assert(sv == "hello%20world");

这意味着 make_pct_string_view 也可用于验证字符串并保留该信息以备将来使用。 类中的修改函数(例如 url)需要已经验证过的 pct_string_view 实例。 这完全消除了重新验证此信息或从这些函数中抛出异常的责任:

pct_string_view s = "path/to/file";
url u;
u.set_encoded_path(s);
assert(u.buffer() == "path/to/file");

当异常是可接受的时,一种常见的模式是让文字字符串或其他可转换为 string_view 的类型隐式转换为 pct_string_view。

url u;
u.set_encoded_path("path/to/file");
assert(u.buffer() == "path/to/file");

如果输入无效,请注意当 pct_string_view 是隐式构造时抛出异常,而不是来自修改函数。

当 pct_string_view 来自另一个数据也被确保被验证的来源时,重用验证保证特别有用:

url_view uv("path/to/file");
url u;
u.set_encoded_path(uv.encoded_path());
assert(u.buffer() == "path/to/file");

在上面的示例中,set_encoded_path 不会重新验证来自 encoded_path 的任何信息,因为这些引用作为 pct_string_view 传递。

解码

类 pct_string_view 表示参考百分比编码字符串。 decode_view 类似于 pct_string_view,主要区别在于底层缓冲区总是解引用解码字符。

pct_string_view es("hello%20world");
assert(es == "hello%20world");

decode_view dv("hello%20world");
assert(dv == "hello world");

decode_view 也可以使用 operator* 从 pct_string_view 创建。 这也让我们有机会验证外部字符串:

result<pct_string_view> rs = make_pct_string_view("hello%20world");
assert(rs.has_value());
pct_string_view s = rs.value();
decode_view dv = *s;
assert(dv == "hello world");

当需要访问已解码的字符串进行比较而无需显式将字符串解码到缓冲区中时,这尤其有用:

url_view u = parse_relative_ref("user/john%20doe/profile%20photo.jpg").value();
std::vector<std::string> route = {"user", "john doe", "profile photo.jpg"};
auto segs = u.encoded_segments();
auto it0 = segs.begin();
auto end0 = segs.end();
auto it1 = route.begin();
auto end1 = route.end();
while (
it0 != end0 &&
it1 != end1)
{
pct_string_view seg0 = *it0;
decode_view dseg0 = *seg0;
string_view seg1 = *it1;
if (dseg0 == seg1)
{
++it0;
++it1;
}
else
{
break;
}
}
bool route_match = it0 == end0 && it1 == end1;
assert(route_match);

成员函数 pct_string_view::decode 可用于将数据解码到缓冲区中。 与自由函数编码一样,可以自定义解码选项和字符串标记。

pct_string_view s = "user/john%20doe/profile%20photo.jpg";
std::string buf;
buf.resize(s.decoded_size());
s.decode({}, string_token::assign_to(buf));
assert(buf == "user/john doe/profile photo.jpg");

格式化

自定义

对于范围广泛的应用程序,库的容器接口对于使用通用语法或众所周知的方案的 URL 来说已经足够了。 然而,在更复杂的情况下,希望超出库提供的范围:

  • 为其他方案创建新的自定义容器
  • 将 URL 的解析合并到封闭语法中
  • 在非 URL 上下文中解析 rfc3986 元素(authority_view 就是一个例子)。
  • 定义用于解析非 URL 字符串的新 ABNF 规则

为了启用这些用例,该库提供了一套用于处理低 ASCII 字符串的通用工具,并公开了 rfc3986 中有用规则的接口。 这些设施的设计目标是:

  • 不使用 std::locale 或 std::char_traits
  • 没有外来字符类型,只有低 ASCII 字符
  • 没有内存分配(或有界分配)
  • 具有非终端规则的灵活组合
  • 针对 RFC 中常见的语法进行了优化
  • 易于扩展

通用设施嵌套在完全限定的命名空间 boost::urls::grammar 中,而特定于 rfc3986 的解析算法的头文件位于<boost/url/rfc/>目录中。 本节解释了定义和解析新语法的通用工具的设计和使用。

解析规则

规则是一个对象,它试图将输入字符缓冲区的开头与特定语法相匹配。 如果匹配成功,它返回一个包含值的结果,如果匹配失败,则返回一个 error_code。 规则不直接调用。 相反,它们作为值传递给解析函数,连同要处理的输入字符缓冲区。 第一个重载要求整个输入字符串匹配,否则会发生错误。 第二个重载在成功时将输入缓冲区指针推进到第一个未使用的字符,从而允许按顺序解析数据流:

template< class Rule >
auto parse( string_view s, Rule const& r) -> result< typename Rule::value_type >;

template< class Rule >
auto parse( char const *& it, char const* end, Rule const& r) -> result< typename Rule::value_type >;

为了满足规则概念,类或结构必须声明嵌套类型 value_type 指示成功返回值的类型,以及具有规定签名的 const 成员函数parse()。 在下面的代码中,我们定义了一个匹配单个逗号的规则:

struct comma_rule_t {
// The type of value returned upon success
using value_type = string_view;

// The algorithm which checks for a match
result< value_type > parse( char const*& it, char const* end ) const {
if( it != end && *it == ',')
return string_view( it++, 1 );
return error::mismatch;
}
};

由于规则是按值传递的,因此为了语法方便,我们声明了一个类型的 constexpr 变量。 规则的变量名通常以 _rule 为后缀:

constexpr comma_rule_t comma_rule{};

现在我们可以用输入的字符串和规则变量调用 parse()

result< string_view > rv = parse( ",", comma_rule );
assert( rv.has_value() && rv.value() == "," );

规则表达式可以有多种样式。 上面定义的规则是编译时常量。 unsigned_rule 匹配一个无符号十进制整数。 这里我们在运行时构建规则,并指定用于保存结果的无符号整数的类型以及模板参数:

result< unsigned short > rv = parse( "16384", unsigned_rule< unsigned short >{} );

函数 delim_rule() 返回匹配传递的字符文字的规则。 这是我们之前定义的逗号规则的更通用版本。 还有一个重载,它与字符集中的一个字符完全匹配。

result< string_view > rv = parse( ",", delim_rule(',') );

错误处理

当规则无法匹配时,或者如果规则检测到输入存在不可恢复的问题,它会返回从 error_code 分配的结果,指示失败。 当使用具有字符指针作为输入和输出参数的解析重载时,由规则来定义错误时指向哪个字符。 当规则匹配成功时,指针总是更改为指向输入中第一个未使用的字符,或者如果所有输入都已使用则指向结束指针。

字符集

字符集表示低 ASCII 字符的子集,用作构建规则的构建块。 该库将它们建模为可调用的谓词,可使用此等效签名调用:

/// Return true if ch is in the set
bool( char ch ) const noexcept;

CharSet 概念描述了这些类型的语法和语义要求。 这里我们声明了一个包含水平和垂直空白字符的字符集类型:

struct ws_chars_t {
constexpr bool operator()( char c ) const noexcept {
return c == '\t' || c == ' ' || c == '\r' || c == '\n';
}
};

萃取类型is_charset判断一个类型是否满足要求:

static_assert( is_charset< ws_chars_t >::value, "CharSet requirements not met" );

字符集总是作为值传递。 与规则一样,为了符号方便,我们声明了一个类型的实例。 constexpr 指定用于使其成为零成本抽象:

constexpr ws_chars_t ws_chars{};

为获得最佳结果,请确保用户定义的字符集类型是 constexpr 可构造的。

函数 find_if 和 find_if_not 用于从字符串中搜索第一个匹配或第一个不匹配的字符。 下面的示例跳过任何前导空白,然后返回从第一个非空白字符到最后一个非空白字符的所有内容:

string_view get_token( string_view s ) noexcept {
auto it0 = s.data();
auto const end = it0 + s.size();

// find the first non-whitespace character
it0 = find_if_not( it0, end, ws_chars );

if( it0 == end ) {
// all whitespace or empty string
return {};
}

// find the next whitespace character
auto it1 = find_if( it0, end, ws_chars );

// [it0, it1) is the part we want
return string_view( it0, it1 - it0 );
}

现在可以这样调用该函数:

assert( get_token( " \t john-doe\r\n \t jane-doe\r\n") == "john-doe" );

该库提供了这些常用的字符集:

描述
alnum_chars包含大小写字母和数字。
alpha_chars包含大写字母和小写字母。
digit_chars包含十进制数字字符。
hexdig_chars包含大写和小写的十六进制数字字符。
vchars包含可见字符(即非空白)。

lut_chars类型

lut_chars 类型满足 CharSet 要求并提供优化的 constexpr 实现,该实现为指定字符集提供增强的性能和符号便利性。 编译时实例可以从字符串构造:

constexpr lut_chars vowels = "AEIOU" "aeiou";

我们可以使用 operator+operator- 符号在编译时从集合中添加和删除元素。 例如,有时字符“y”听起来像元音:

constexpr auto vowels_and_y = vowels + 'y' + 'Y';

该类型以其实现命名,它是打包位的查找表(“lut”)。 这允许多种构造方法和灵活的组合。 这里我们使用 lambda 创建可见字符集:

struct is_visible {
constexpr bool operator()( char ch ) const noexcept {
return ch >= 33 && ch <= 126;
}
};
constexpr lut_chars visible_chars( is_visible{} ); // (since C++11)

也可以:

constexpr lut_chars visible_chars( [](char ch) { return ch >= 33 && ch <= 126; } ); // (since C++17)

差异可以用 operator- 计算:

constexpr auto visible_non_vowels = visible_chars - vowels;

我们还可以删除单个字符:

constexpr auto visible_non_vowels_or_y = visible_chars - vowels - 'y';

复合规则

到目前为止显示的规则定义了终端符号,代表不可分割的语法单元。 为了解析更复杂的事物,解析器组合器(或复合规则)是一种规则,它接受一个或多个规则作为参数并将它们组合起来形成更高阶的算法。 在本节中,我们将介绍库提供的复合规则,以及如何使用它们来表达更复杂的语法。

元组规则

考虑以下语法:

version = "v" dec-octet "." dec-octet

我们可以使用 tuple_rule 来表达这一点,它按顺序匹配一个或多个指定的规则。 下面使用一些字符文字和两个十进制八位字节定义了一个序列,这是一种表示 0 到 255 之间数字的奇特方式:

constexpr auto version_rule = tuple_rule( delim_rule( 'v' ), dec_octet_rule, delim_rule( '.' ), dec_octet_rule );

此规则的值类型为 std::tuple,其类型对应于构造时指定的每个规则的值类型。 十进制八位字节由 dec_octet_rule 表示,它将其结果存储在一个无符号字符中:

result< std::tuple< string_view, unsigned char, string_view, unsigned char > > rv = parse( "v42.44800", version_rule );

要从 std::tuple 中提取元素,必须使用函数 std::get。 在这种情况下,我们不关心匹配字符文字的值。 tuple_rule 丢弃值类型为 void 的匹配结果。 我们可以使用 squelch 复合规则将匹配值类型转换为 void,并重新制定我们的规则:

constexpr auto version_rule = tuple_rule( squelch( delim_rule( 'v' ) ), dec_octet_rule, squelch( delim_rule( '.' ) ), dec_octet_rule );

result< std::tuple< unsigned char, unsigned char > > rv = parse( "v42.44800", version_rule );

当除了一个值类型之外的所有值类型都为 void 时,将省略 std::tuple 并将剩余的值类型提升为匹配结果:

// port     = ":" unsigned-short

constexpr auto port_rule = tuple_rule( squelch( delim_rule( ':' ) ), unsigned_rule< unsigned short >{} );

result< unsigned short > rv = parse( ":443", port_rule );

可选规则

括号中的 BNF 元素表示可选组件。 这些是使用 optional_rule 表示的,其值类型是可选的。 例如,我们可以将上面的端口规则调整为可选组件:

// port     = [ ":" unsigned-short ]

constexpr auto port_rule = optional_rule( tuple_rule( squelch( delim_rule( ':' ) ), unsigned_rule< unsigned short >{} ) );

result< optional< unsigned short > > rv = parse( ":8080", port_rule );

assert( rv->has_value() && rv->value() == 8080 );

在此示例中,我们建立了一个规则来将端点表示为具有可选端口的 IPv4 地址:

// ipv4_address = dec-octet "." dec-octet "." dec-octet "." dec-octet
//
// port = ":" unsigned-short
//
// endpoint = ipv4_address [ port ]

constexpr auto endpoint_rule = tuple_rule(
tuple_rule(
dec_octet_rule, squelch( delim_rule( '.' ) ),
dec_octet_rule, squelch( delim_rule( '.' ) ),
dec_octet_rule, squelch( delim_rule( '.' ) ),
dec_octet_rule ),
optional_rule(
tuple_rule(
squelch( delim_rule( ':' ) ),
unsigned_rule< unsigned short >{} ) ) );

这可以简化; 该库提供了 ipv4_address_rule,其结果类型为 ipv4_address,提供了比将地址简单地表示为四个数字的集合更多的实用性:

constexpr auto endpoint_rule = tuple_rule(
ipv4_address_rule,
optional_rule(
tuple_rule(
squelch( delim_rule( ':' ) ),
unsigned_rule< unsigned short >{} ) ) );

result< std::tuple< ipv4_address, optional< unsigned short > > > rv = parse( "192.168.0.1:443", endpoint_rule );

变体规则

由不带引号的斜杠分隔的 BNF 元素表示一组可供选择的元素,其中一个元素可能匹配。 我们使用 variant_rule 来表示它们,其值类型是 variant。 考虑以下来自 rfc7230 的 HTTP 生产规则:

request-target = origin-form
/ absolute-form
/ authority-form
/ asterisk-form

请求目标可以是其中之一。 这里我们定义规则,使用库自带的origin_form_rule、absolute_uri_rule、authority_rule,解析一个字符串得到一个结果:

constexpr auto request_target_rule = variant_rule(
origin_form_rule,
absolute_uri_rule,
authority_rule,
delim_rule('*') );

result< variant< url_view, url_view, authority_view, string_view > > rv = parse( "/results.htm?page=4", request_target_rule );

在下一节中,我们将讨论解析重复元素的工具。

范围

到目前为止,我们所研究的规则有一个共同点; 它们产生的值的大小是固定的,并且在编译时已知。 但是,语法可以指定元素的重复。 例如考虑以下语法:

chunk-ext      = *( ";" token )

BNF 表示法中的星号运算符表示重复。 在这种情况下,括号中有零个或多个表达式。 可以使用函数 range_rule 来表示此产生式,该函数返回允许指定规则重复指定次数的规则。 以下规则匹配上面定义的 chunk-ext 的语法:

constexpr auto chunk_ext_rule = range_rule(
tuple_rule( squelch( delim_rule( ';' ) ), token_rule( alnum_chars ) ) );

此规则生成一个范围,即 ForwardRange,其值类型与传递给函数的规则的值类型相同。 在本例中,类型是 string_view,因为元组有一个未压制的元素,即 token_rule。 可以迭代该范围以产生结果,而无需为每个元素分配内存。 以下代码:

result< range< string_view > > rv = parse( ";johndoe;janedoe;end", chunk_ext_rule );

for( auto s : rv.value() )
std::cout << s << "\n";

产生这个输出:

johndoe
janedoe
end

有时,使用单一规则并不那么容易表达重复。 以逗号分隔的标记列表的以下语法为例,它必须至少包含一个元素:

token-list    = token *( "," token )

我们可以使用 range_rule 的重载来表达这一点,它接受两个参数:执行第一个匹配时使用的规则,以及执行每个后续匹配时使用的规则。 该函数的两个重载都有额外的可选参数,用于指定最小重复次数,或最小和最大重复次数。 由于我们的列表可能不为空,因此以下规则完美地捕获了标记列表语法:

constexpr auto token_list_rule = range_rule(
token_rule( alnum_chars ),
tuple_rule( squelch( delim_rule( ',' ) ), token_rule( alnum_chars ) ),
1 );

以下代码:

result< range< string_view > > rv = parse( "johndoe,janedoe,end", token_list_rule );

for( auto s : rv.value() )
std::cout << s << "\n";

产生如下输出:

johndoe
janedoe
end

在下一节中,我们将讨论特定于 rfc3986 的可用规则。

更多

这些是库提供的规则和复合规则。 有关详细信息,请参阅相应的参考部分。

名称描述
dec_octet_rule匹配 0 到 255 之间的一个整数。
delim_rule匹配一个字符常量。
literal_rule精确匹配一个字符串。
not_empty_rule将匹配的空字符串改为错误。
optional_rule如果解析失败则忽略规则,保持输入指针不变。
range_rule匹配重复数量的元素。
token_rule匹配字符集中的一串字符。
tuple_rule按顺序匹配一系列指定规则。
unsigned_rule匹配十进制形式的无符号整数。
variant_rule匹配规则指定的一组备选方案中的一个。

RFC 3986

概念

字符集 规则 StringToken

示例

二维码 挑剔的 邮寄地址 磁力链接 文件路由器 路由器