xmllite/reader: Improve returned reader position for elements and attributes.
Signed-off-by: Nikolay Sivov <nsivov@codeweavers.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org>
This commit is contained in:
parent
e1c31e13a8
commit
6cf9524f07
|
@ -224,6 +224,12 @@ static const strval strval_empty = { emptyW };
|
|||
static const strval strval_xml = { xmlW, 3 };
|
||||
static const strval strval_xmlns = { xmlnsW, 5 };
|
||||
|
||||
struct reader_position
|
||||
{
|
||||
UINT line_number;
|
||||
UINT line_position;
|
||||
};
|
||||
|
||||
struct attribute
|
||||
{
|
||||
struct list entry;
|
||||
|
@ -231,6 +237,7 @@ struct attribute
|
|||
strval localname;
|
||||
strval qname;
|
||||
strval value;
|
||||
struct reader_position position;
|
||||
};
|
||||
|
||||
struct element
|
||||
|
@ -239,6 +246,7 @@ struct element
|
|||
strval prefix;
|
||||
strval localname;
|
||||
strval qname;
|
||||
struct reader_position position;
|
||||
};
|
||||
|
||||
struct ns
|
||||
|
@ -262,7 +270,7 @@ typedef struct
|
|||
DtdProcessing dtdmode;
|
||||
IXmlResolver *resolver;
|
||||
IUnknown *mlang;
|
||||
UINT line, pos; /* reader position in XML stream */
|
||||
struct reader_position position;
|
||||
struct list attrs; /* attributes list for current node */
|
||||
struct attribute *attr; /* current attribute */
|
||||
UINT attr_count;
|
||||
|
@ -388,7 +396,7 @@ static void reader_clear_attrs(xmlreader *reader)
|
|||
/* attribute data holds pointers to buffer data, so buffer shrink is not possible
|
||||
while we are on a node with attributes */
|
||||
static HRESULT reader_add_attr(xmlreader *reader, strval *prefix, strval *localname, strval *qname,
|
||||
strval *value)
|
||||
strval *value, const struct reader_position *position)
|
||||
{
|
||||
struct attribute *attr;
|
||||
|
||||
|
@ -402,6 +410,7 @@ static HRESULT reader_add_attr(xmlreader *reader, strval *prefix, strval *localn
|
|||
attr->localname = *localname;
|
||||
attr->qname = qname ? *qname : *localname;
|
||||
attr->value = *value;
|
||||
attr->position = *position;
|
||||
list_add_tail(&reader->attrs, &attr->entry);
|
||||
reader->attr_count++;
|
||||
|
||||
|
@ -539,7 +548,7 @@ static void reader_mark_ns_nodes(xmlreader *reader, struct element *element)
|
|||
}
|
||||
|
||||
static HRESULT reader_push_element(xmlreader *reader, strval *prefix, strval *localname,
|
||||
strval *qname)
|
||||
strval *qname, const struct reader_position *position)
|
||||
{
|
||||
struct element *element;
|
||||
HRESULT hr;
|
||||
|
@ -555,6 +564,7 @@ static HRESULT reader_push_element(xmlreader *reader, strval *prefix, strval *lo
|
|||
list_add_head(&reader->elements, &element->entry);
|
||||
reader_mark_ns_nodes(reader, element);
|
||||
reader->is_empty_element = FALSE;
|
||||
element->position = *position;
|
||||
}
|
||||
else
|
||||
reader_free_element(reader, element);
|
||||
|
@ -1048,7 +1058,7 @@ static void reader_skipn(xmlreader *reader, int n)
|
|||
while (*ptr++ && n--)
|
||||
{
|
||||
buffer->cur++;
|
||||
reader->pos++;
|
||||
reader->position.line_position++;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1067,14 +1077,14 @@ static int reader_skipspaces(xmlreader *reader)
|
|||
while (is_wchar_space(*ptr))
|
||||
{
|
||||
if (*ptr == '\r')
|
||||
reader->pos = 0;
|
||||
reader->position.line_position = 0;
|
||||
else if (*ptr == '\n')
|
||||
{
|
||||
reader->line++;
|
||||
reader->pos = 0;
|
||||
reader->position.line_number++;
|
||||
reader->position.line_position = 0;
|
||||
}
|
||||
else
|
||||
reader->pos++;
|
||||
reader->position.line_position++;
|
||||
|
||||
buffer->cur++;
|
||||
ptr = reader_get_ptr(reader);
|
||||
|
@ -1125,11 +1135,13 @@ static HRESULT reader_parse_eq(xmlreader *reader)
|
|||
static HRESULT reader_parse_versioninfo(xmlreader *reader)
|
||||
{
|
||||
static const WCHAR versionW[] = {'v','e','r','s','i','o','n',0};
|
||||
struct reader_position position;
|
||||
strval val, name;
|
||||
HRESULT hr;
|
||||
|
||||
if (!reader_skipspaces(reader)) return WC_E_WHITESPACE;
|
||||
|
||||
position = reader->position;
|
||||
if (reader_cmp(reader, versionW)) return WC_E_XMLDECL;
|
||||
reader_init_strvalue(reader_get_cur(reader), 7, &name);
|
||||
/* skip 'version' */
|
||||
|
@ -1152,7 +1164,7 @@ static HRESULT reader_parse_versioninfo(xmlreader *reader)
|
|||
/* skip "'"|'"' */
|
||||
reader_skipn(reader, 1);
|
||||
|
||||
return reader_add_attr(reader, NULL, &name, NULL, &val);
|
||||
return reader_add_attr(reader, NULL, &name, NULL, &val, &position);
|
||||
}
|
||||
|
||||
/* ([A-Za-z0-9._] | '-') */
|
||||
|
@ -1199,11 +1211,13 @@ static HRESULT reader_parse_encname(xmlreader *reader, strval *val)
|
|||
static HRESULT reader_parse_encdecl(xmlreader *reader)
|
||||
{
|
||||
static const WCHAR encodingW[] = {'e','n','c','o','d','i','n','g',0};
|
||||
struct reader_position position;
|
||||
strval name, val;
|
||||
HRESULT hr;
|
||||
|
||||
if (!reader_skipspaces(reader)) return S_FALSE;
|
||||
|
||||
position = reader->position;
|
||||
if (reader_cmp(reader, encodingW)) return S_FALSE;
|
||||
name.str = reader_get_ptr(reader);
|
||||
name.start = reader_get_cur(reader);
|
||||
|
@ -1228,7 +1242,7 @@ static HRESULT reader_parse_encdecl(xmlreader *reader)
|
|||
/* skip "'"|'"' */
|
||||
reader_skipn(reader, 1);
|
||||
|
||||
return reader_add_attr(reader, NULL, &name, NULL, &val);
|
||||
return reader_add_attr(reader, NULL, &name, NULL, &val, &position);
|
||||
}
|
||||
|
||||
/* [32] SDDecl ::= S 'standalone' Eq (("'" ('yes' | 'no') "'") | ('"' ('yes' | 'no') '"')) */
|
||||
|
@ -1237,12 +1251,14 @@ static HRESULT reader_parse_sddecl(xmlreader *reader)
|
|||
static const WCHAR standaloneW[] = {'s','t','a','n','d','a','l','o','n','e',0};
|
||||
static const WCHAR yesW[] = {'y','e','s',0};
|
||||
static const WCHAR noW[] = {'n','o',0};
|
||||
struct reader_position position;
|
||||
strval name, val;
|
||||
UINT start;
|
||||
HRESULT hr;
|
||||
|
||||
if (!reader_skipspaces(reader)) return S_FALSE;
|
||||
|
||||
position = reader->position;
|
||||
if (reader_cmp(reader, standaloneW)) return S_FALSE;
|
||||
reader_init_strvalue(reader_get_cur(reader), 10, &name);
|
||||
/* skip 'standalone' */
|
||||
|
@ -1270,7 +1286,7 @@ static HRESULT reader_parse_sddecl(xmlreader *reader)
|
|||
/* skip "'"|'"' */
|
||||
reader_skipn(reader, 1);
|
||||
|
||||
return reader_add_attr(reader, NULL, &name, NULL, &val);
|
||||
return reader_add_attr(reader, NULL, &name, NULL, &val, &position);
|
||||
}
|
||||
|
||||
/* [23] XMLDecl ::= '<?xml' VersionInfo EncodingDecl? SDDecl? S? '?>' */
|
||||
|
@ -1723,6 +1739,7 @@ static HRESULT reader_parse_externalid(xmlreader *reader)
|
|||
{
|
||||
static WCHAR systemW[] = {'S','Y','S','T','E','M',0};
|
||||
static WCHAR publicW[] = {'P','U','B','L','I','C',0};
|
||||
struct reader_position position = reader->position;
|
||||
strval name, sys;
|
||||
HRESULT hr;
|
||||
int cnt;
|
||||
|
@ -1739,7 +1756,7 @@ static HRESULT reader_parse_externalid(xmlreader *reader)
|
|||
if (FAILED(hr)) return hr;
|
||||
|
||||
reader_init_cstrvalue(publicW, strlenW(publicW), &name);
|
||||
hr = reader_add_attr(reader, NULL, &name, NULL, &pub);
|
||||
hr = reader_add_attr(reader, NULL, &name, NULL, &pub, &position);
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
cnt = reader_skipspaces(reader);
|
||||
|
@ -1750,7 +1767,7 @@ static HRESULT reader_parse_externalid(xmlreader *reader)
|
|||
if (FAILED(hr)) return S_OK;
|
||||
|
||||
reader_init_cstrvalue(systemW, strlenW(systemW), &name);
|
||||
hr = reader_add_attr(reader, NULL, &name, NULL, &sys);
|
||||
hr = reader_add_attr(reader, NULL, &name, NULL, &sys, &position);
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
return S_OK;
|
||||
|
@ -1764,7 +1781,7 @@ static HRESULT reader_parse_externalid(xmlreader *reader)
|
|||
if (FAILED(hr)) return hr;
|
||||
|
||||
reader_init_cstrvalue(systemW, strlenW(systemW), &name);
|
||||
return reader_add_attr(reader, NULL, &name, NULL, &sys);
|
||||
return reader_add_attr(reader, NULL, &name, NULL, &sys, &position);
|
||||
}
|
||||
|
||||
return S_FALSE;
|
||||
|
@ -2139,6 +2156,7 @@ static HRESULT reader_parse_attvalue(xmlreader *reader, strval *value)
|
|||
[15 NS] Attribute ::= NSAttName Eq AttValue | QName Eq AttValue */
|
||||
static HRESULT reader_parse_attribute(xmlreader *reader)
|
||||
{
|
||||
struct reader_position position = reader->position;
|
||||
strval prefix, local, qname, value;
|
||||
BOOL ns = FALSE, nsdef = FALSE;
|
||||
HRESULT hr;
|
||||
|
@ -2162,19 +2180,20 @@ static HRESULT reader_parse_attribute(xmlreader *reader)
|
|||
reader_push_ns(reader, nsdef ? &strval_xmlns : &local, &value, nsdef);
|
||||
|
||||
TRACE("%s=%s\n", debug_strval(reader, &local), debug_strval(reader, &value));
|
||||
return reader_add_attr(reader, &prefix, &local, &qname, &value);
|
||||
return reader_add_attr(reader, &prefix, &local, &qname, &value, &position);
|
||||
}
|
||||
|
||||
/* [12 NS] STag ::= '<' QName (S Attribute)* S? '>'
|
||||
[14 NS] EmptyElemTag ::= '<' QName (S Attribute)* S? '/>' */
|
||||
static HRESULT reader_parse_stag(xmlreader *reader, strval *prefix, strval *local, strval *qname)
|
||||
{
|
||||
struct reader_position position = reader->position;
|
||||
HRESULT hr;
|
||||
|
||||
hr = reader_parse_qname(reader, prefix, local, qname);
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
while (1)
|
||||
for (;;)
|
||||
{
|
||||
static const WCHAR endW[] = {'/','>',0};
|
||||
|
||||
|
@ -2188,6 +2207,7 @@ static HRESULT reader_parse_stag(xmlreader *reader, strval *prefix, strval *loca
|
|||
reader->empty_element.prefix = *prefix;
|
||||
reader->empty_element.localname = *local;
|
||||
reader->empty_element.qname = *qname;
|
||||
reader->empty_element.position = position;
|
||||
reader_mark_ns_nodes(reader, &reader->empty_element);
|
||||
return S_OK;
|
||||
}
|
||||
|
@ -2197,7 +2217,7 @@ static HRESULT reader_parse_stag(xmlreader *reader, strval *prefix, strval *loca
|
|||
{
|
||||
/* skip '>' */
|
||||
reader_skipn(reader, 1);
|
||||
return reader_push_element(reader, prefix, local, qname);
|
||||
return reader_push_element(reader, prefix, local, qname, &position);
|
||||
}
|
||||
|
||||
hr = reader_parse_attribute(reader);
|
||||
|
@ -2257,6 +2277,7 @@ static HRESULT reader_parse_element(xmlreader *reader)
|
|||
/* [13 NS] ETag ::= '</' QName S? '>' */
|
||||
static HRESULT reader_parse_endtag(xmlreader *reader)
|
||||
{
|
||||
struct reader_position position;
|
||||
strval prefix, local, qname;
|
||||
struct element *element;
|
||||
HRESULT hr;
|
||||
|
@ -2264,6 +2285,7 @@ static HRESULT reader_parse_endtag(xmlreader *reader)
|
|||
/* skip '</' */
|
||||
reader_skipn(reader, 2);
|
||||
|
||||
position = reader->position;
|
||||
hr = reader_parse_qname(reader, &prefix, &local, &qname);
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
|
@ -2279,6 +2301,9 @@ static HRESULT reader_parse_endtag(xmlreader *reader)
|
|||
element = LIST_ENTRY(list_head(&reader->elements), struct element, entry);
|
||||
if (!strval_eq(reader, &element->qname, &qname)) return WC_E_ELEMENTMATCH;
|
||||
|
||||
/* update position stored for start tag, we won't be using it */
|
||||
element->position = position;
|
||||
|
||||
reader->nodetype = XmlNodeType_EndElement;
|
||||
reader->is_empty_element = FALSE;
|
||||
reader_set_strvalue(reader, StringValue_Prefix, &prefix);
|
||||
|
@ -2484,7 +2509,7 @@ static HRESULT reader_parse_nextnode(xmlreader *reader)
|
|||
;
|
||||
}
|
||||
|
||||
while (1)
|
||||
for (;;)
|
||||
{
|
||||
switch (reader->instate)
|
||||
{
|
||||
|
@ -2496,6 +2521,9 @@ static HRESULT reader_parse_nextnode(xmlreader *reader)
|
|||
hr = readerinput_growraw(reader->input);
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
reader->position.line_number = 1;
|
||||
reader->position.line_position = 1;
|
||||
|
||||
/* try to detect encoding by BOM or data and set input code page */
|
||||
hr = readerinput_detectencoding(reader->input, &enc);
|
||||
TRACE("detected encoding %s, 0x%08x\n", enc == XmlEncoding_Unknown ? "(unknown)" :
|
||||
|
@ -2656,7 +2684,8 @@ static HRESULT WINAPI xmlreader_SetInput(IXmlReader* iface, IUnknown *input)
|
|||
This->input = NULL;
|
||||
}
|
||||
|
||||
This->line = This->pos = 0;
|
||||
This->position.line_number = 0;
|
||||
This->position.line_position = 0;
|
||||
reader_clear_elements(This);
|
||||
This->depth = 0;
|
||||
This->nodetype = XmlNodeType_None;
|
||||
|
@ -3238,28 +3267,66 @@ static BOOL WINAPI xmlreader_IsEmptyElement(IXmlReader* iface)
|
|||
return (reader_get_nodetype(This) == XmlNodeType_Element) ? This->is_empty_element : FALSE;
|
||||
}
|
||||
|
||||
static HRESULT WINAPI xmlreader_GetLineNumber(IXmlReader* iface, UINT *lineNumber)
|
||||
static HRESULT WINAPI xmlreader_GetLineNumber(IXmlReader* iface, UINT *line_number)
|
||||
{
|
||||
xmlreader *This = impl_from_IXmlReader(iface);
|
||||
const struct element *element;
|
||||
|
||||
TRACE("(%p %p)\n", This, lineNumber);
|
||||
TRACE("(%p %p)\n", This, line_number);
|
||||
|
||||
if (!lineNumber) return E_INVALIDARG;
|
||||
if (!line_number)
|
||||
return E_INVALIDARG;
|
||||
|
||||
*lineNumber = This->line;
|
||||
switch (reader_get_nodetype(This))
|
||||
{
|
||||
case XmlNodeType_Element:
|
||||
case XmlNodeType_EndElement:
|
||||
if (This->is_empty_element)
|
||||
element = &This->empty_element;
|
||||
else
|
||||
element = LIST_ENTRY(list_head(&This->elements), struct element, entry);
|
||||
|
||||
*line_number = element->position.line_number;
|
||||
break;
|
||||
case XmlNodeType_Attribute:
|
||||
*line_number = This->attr->position.line_number;
|
||||
break;
|
||||
default:
|
||||
*line_number = This->position.line_number;
|
||||
break;
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
static HRESULT WINAPI xmlreader_GetLinePosition(IXmlReader* iface, UINT *linePosition)
|
||||
static HRESULT WINAPI xmlreader_GetLinePosition(IXmlReader* iface, UINT *line_position)
|
||||
{
|
||||
xmlreader *This = impl_from_IXmlReader(iface);
|
||||
const struct element *element;
|
||||
|
||||
TRACE("(%p %p)\n", This, linePosition);
|
||||
TRACE("(%p %p)\n", This, line_position);
|
||||
|
||||
if (!linePosition) return E_INVALIDARG;
|
||||
if (!line_position)
|
||||
return E_INVALIDARG;
|
||||
|
||||
*linePosition = This->pos;
|
||||
switch (reader_get_nodetype(This))
|
||||
{
|
||||
case XmlNodeType_Element:
|
||||
case XmlNodeType_EndElement:
|
||||
if (This->is_empty_element)
|
||||
element = &This->empty_element;
|
||||
else
|
||||
element = LIST_ENTRY(list_head(&This->elements), struct element, entry);
|
||||
|
||||
*line_position = element->position.line_position;
|
||||
break;
|
||||
case XmlNodeType_Attribute:
|
||||
*line_position = This->attr->position.line_position;
|
||||
break;
|
||||
default:
|
||||
*line_position = This->position.line_position;
|
||||
break;
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
@ -3409,7 +3476,8 @@ HRESULT WINAPI CreateXmlReader(REFIID riid, void **obj, IMalloc *imalloc)
|
|||
reader->dtdmode = DtdProcessing_Prohibit;
|
||||
reader->resolver = NULL;
|
||||
reader->mlang = NULL;
|
||||
reader->line = reader->pos = 0;
|
||||
reader->position.line_number = 0;
|
||||
reader->position.line_position = 0;
|
||||
reader->imalloc = imalloc;
|
||||
if (imalloc) IMalloc_AddRef(imalloc);
|
||||
reader->nodetype = XmlNodeType_None;
|
||||
|
|
|
@ -781,7 +781,6 @@ todo_wine
|
|||
ok(hr == S_OK, "got %08x\n", hr);
|
||||
ok(type == XmlNodeType_Attribute, "got %d\n", type);
|
||||
|
||||
todo_wine
|
||||
TEST_READER_POSITION2(reader, 1, 7, ~0u, 55);
|
||||
|
||||
/* try to move from last attribute */
|
||||
|
@ -799,7 +798,6 @@ todo_wine
|
|||
|
||||
hr = IXmlReader_MoveToFirstAttribute(reader);
|
||||
ok(hr == S_OK, "got %08x\n", hr);
|
||||
todo_wine
|
||||
TEST_READER_POSITION2(reader, 1, 7, ~0u, 55);
|
||||
|
||||
hr = IXmlReader_GetAttributeCount(reader, NULL);
|
||||
|
@ -890,7 +888,6 @@ todo_wine
|
|||
hr = IXmlReader_GetNodeType(reader, &type);
|
||||
ok(hr == S_OK, "expected S_OK, got %08x\n", hr);
|
||||
ok(type == XmlNodeType_Attribute, "got %d\n", type);
|
||||
todo_wine
|
||||
TEST_READER_POSITION2(reader, 1, 7, ~0u, 21);
|
||||
|
||||
/* try to move from last attribute */
|
||||
|
@ -901,7 +898,6 @@ todo_wine
|
|||
hr = IXmlReader_Read(reader, &type);
|
||||
ok(hr == S_OK, "expected S_OK, got %08x\n", hr);
|
||||
ok(type == XmlNodeType_Element, "expected Element, got %s\n", type_to_str(type));
|
||||
todo_wine
|
||||
TEST_READER_POSITION2(reader, 1, 23, ~0u, 40);
|
||||
TEST_READER_STATE(reader, XmlReadState_Interactive);
|
||||
|
||||
|
@ -925,10 +921,10 @@ todo_wine
|
|||
todo_wine
|
||||
ok(hr == WC_E_SYNTAX || hr == WC_E_XMLCHARACTER /* XP */, "expected WC_E_SYNTAX, got %08x\n", hr);
|
||||
ok(type == XmlNodeType_None, "expected XmlNodeType_None, got %s\n", type_to_str(type));
|
||||
todo_wine {
|
||||
TEST_READER_POSITION(reader, 1, 41);
|
||||
todo_wine
|
||||
TEST_READER_STATE(reader, XmlReadState_Error);
|
||||
}
|
||||
|
||||
IStream_Release(stream);
|
||||
IXmlReader_Release(reader);
|
||||
}
|
||||
|
@ -2473,12 +2469,12 @@ todo_wine {
|
|||
|
||||
static void test_reader_position(void)
|
||||
{
|
||||
static const char *xml = "<c:a xmlns:c=\"nsdef c\" b=\"attr b\" />";
|
||||
static const char *xml = "<c:a xmlns:c=\"nsdef c\" b=\"attr b\"></c:a>";
|
||||
IXmlReader *reader;
|
||||
XmlNodeType type;
|
||||
IStream *stream;
|
||||
HRESULT hr;
|
||||
UINT position;
|
||||
HRESULT hr;
|
||||
|
||||
hr = CreateXmlReader(&IID_IXmlReader, (void **)&reader, NULL);
|
||||
ok(hr == S_OK, "S_OK, got %08x\n", hr);
|
||||
|
@ -2511,23 +2507,24 @@ todo_wine
|
|||
hr = IXmlReader_Read(reader, &type);
|
||||
ok(hr == S_OK, "got %08x\n", hr);
|
||||
ok(type == XmlNodeType_Element, "got type %d\n", type);
|
||||
todo_wine
|
||||
TEST_READER_POSITION2(reader, 1, 2, ~0u, 36);
|
||||
TEST_READER_POSITION2(reader, 1, 2, ~0u, 34);
|
||||
|
||||
hr = IXmlReader_MoveToNextAttribute(reader);
|
||||
ok(hr == S_OK, "got %08x\n", hr);
|
||||
todo_wine
|
||||
TEST_READER_POSITION2(reader, 1, 6, ~0u, 36);
|
||||
TEST_READER_POSITION2(reader, 1, 6, ~0u, 34);
|
||||
|
||||
hr = IXmlReader_MoveToNextAttribute(reader);
|
||||
ok(hr == S_OK, "got %08x\n", hr);
|
||||
todo_wine
|
||||
TEST_READER_POSITION2(reader, 1, 24, ~0u, 36);
|
||||
TEST_READER_POSITION2(reader, 1, 24, ~0u, 34);
|
||||
|
||||
hr = IXmlReader_MoveToElement(reader);
|
||||
ok(hr == S_OK, "got %08x\n", hr);
|
||||
todo_wine
|
||||
TEST_READER_POSITION2(reader, 1, 2, ~0u, 36);
|
||||
TEST_READER_POSITION2(reader, 1, 2, ~0u, 34);
|
||||
|
||||
hr = IXmlReader_Read(reader, &type);
|
||||
ok(hr == S_OK, "got %08x\n", hr);
|
||||
ok(type == XmlNodeType_EndElement, "got type %d\n", type);
|
||||
TEST_READER_POSITION2(reader, 1, 37, ~0u, 40);
|
||||
|
||||
IXmlReader_SetInput(reader, NULL);
|
||||
TEST_READER_POSITION(reader, 0, 0);
|
||||
|
|
Loading…
Reference in New Issue