mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
fix(core): match IPOT's unknown-code reply exactly; check column labels
An unknown code is only the reply that is exactly "Invalid stock code!", not any page with that text (e.g. inside a script). The header row must be IPOT's nine labels in order, so a page labelling the columns otherwise is rejected rather than read by position (Codex re-check, round 10). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
parent
ee4fe4b852
commit
828d16c92e
1 changed files with 31 additions and 4 deletions
|
|
@ -299,6 +299,11 @@ fn sole_child<'a>(parent: ElementRef<'a>, name: &str) -> Option<ElementRef<'a>>
|
||||||
const MAX_PAGE_BYTES: usize = 64 * 1024;
|
const MAX_PAGE_BYTES: usize = 64 * 1024;
|
||||||
const MAX_PAGE_TAGS: usize = 2_000;
|
const MAX_PAGE_TAGS: usize = 2_000;
|
||||||
|
|
||||||
|
/// The column labels, in IPOT's order.
|
||||||
|
const COLUMNS: [&str; 9] = [
|
||||||
|
"Buyer", "B.Lot", "B.Val", "B.Avg", "#", "Seller", "S.Lot", "S.Val", "S.Avg",
|
||||||
|
];
|
||||||
|
|
||||||
/// The footer's labels, in the order IPOT writes them.
|
/// The footer's labels, in the order IPOT writes them.
|
||||||
const FOOTER: [&str; 4] = ["T. Val :", "F. NVal :", "T.Lot :", "Avg :"];
|
const FOOTER: [&str; 4] = ["T. Val :", "F. NVal :", "T.Lot :", "Avg :"];
|
||||||
|
|
||||||
|
|
@ -332,11 +337,11 @@ pub fn parse_broker_summary(html: &str) -> Result<IpotPage, IdxError> {
|
||||||
if html.len() > MAX_PAGE_BYTES || html.matches('<').count() > MAX_PAGE_TAGS {
|
if html.len() > MAX_PAGE_BYTES || html.matches('<').count() > MAX_PAGE_TAGS {
|
||||||
return Err(parse_error("page larger than IPOT serves"));
|
return Err(parse_error("page larger than IPOT serves"));
|
||||||
}
|
}
|
||||||
let page = Html::parse_fragment(html);
|
// For an unknown code the whole response is this one line.
|
||||||
let shown: String = page.root_element().text().collect();
|
if html.trim().eq_ignore_ascii_case("invalid stock code!") {
|
||||||
if shown.to_ascii_lowercase().contains("invalid stock code") {
|
|
||||||
return Ok(IpotPage::Invalid);
|
return Ok(IpotPage::Invalid);
|
||||||
}
|
}
|
||||||
|
let page = Html::parse_fragment(html);
|
||||||
if !known_markup(&page) {
|
if !known_markup(&page) {
|
||||||
return Err(parse_error("markup the page does not use"));
|
return Err(parse_error("markup the page does not use"));
|
||||||
}
|
}
|
||||||
|
|
@ -344,9 +349,19 @@ pub fn parse_broker_summary(html: &str) -> Result<IpotPage, IdxError> {
|
||||||
.filter(|table| placed(*table, &TABLE_PATH))
|
.filter(|table| placed(*table, &TABLE_PATH))
|
||||||
.ok_or_else(|| parse_error("no single table where the page puts it"))?;
|
.ok_or_else(|| parse_error("no single table where the page puts it"))?;
|
||||||
let parts: Vec<ElementRef> = table.child_elements().collect();
|
let parts: Vec<ElementRef> = table.child_elements().collect();
|
||||||
let [_, body, tfoot] = parts[..] else {
|
let [head, body, tfoot] = parts[..] else {
|
||||||
return Err(parse_error("unexpected table parts"));
|
return Err(parse_error("unexpected table parts"));
|
||||||
};
|
};
|
||||||
|
// The columns must be labelled as IPOT labels them, in its order.
|
||||||
|
let headers: Option<Vec<String>> = sole_child(head, "tr").and_then(|row| {
|
||||||
|
row.child_elements()
|
||||||
|
.map(|th| (th.value().name() == "th").then(|| leaf_text(th)).flatten())
|
||||||
|
.map(|text| text.map(|text| clean(&text)))
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
if headers.as_deref() != Some(&COLUMNS.map(String::from)[..]) {
|
||||||
|
return Err(parse_error("unexpected column labels"));
|
||||||
|
}
|
||||||
let names: Vec<&str> = parts.iter().map(|part| part.value().name()).collect();
|
let names: Vec<&str> = parts.iter().map(|part| part.value().name()).collect();
|
||||||
if names != ["thead", "tbody", "tfoot"] {
|
if names != ["thead", "tbody", "tfoot"] {
|
||||||
return Err(parse_error("unexpected table parts"));
|
return Err(parse_error("unexpected table parts"));
|
||||||
|
|
@ -835,9 +850,21 @@ mod tests {
|
||||||
"deeply nested divs",
|
"deeply nested divs",
|
||||||
"<div>".repeat(4_000) + &"</div>".repeat(4_000) + DAY,
|
"<div>".repeat(4_000) + &"</div>".repeat(4_000) + DAY,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"swapped column labels",
|
||||||
|
DAY.replacen(">B.Lot<", ">B.Tmp<", 1)
|
||||||
|
.replacen(">B.Val<", ">B.Lot<", 1)
|
||||||
|
.replacen(">B.Tmp<", ">B.Val<", 1),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
for (what, page) in hidden.into_iter().chain(more) {
|
for (what, page) in hidden.into_iter().chain(more) {
|
||||||
assert!(read(&page).is_none(), "{what}");
|
assert!(read(&page).is_none(), "{what}");
|
||||||
}
|
}
|
||||||
|
let script_text = DAY.replacen("></script>", ">/* invalid stock code */</script>", 1);
|
||||||
|
assert_eq!(
|
||||||
|
read(&script_text),
|
||||||
|
Some(original),
|
||||||
|
"script text is not shown"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue