mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 01:35: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_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.
|
||||
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 {
|
||||
return Err(parse_error("page larger than IPOT serves"));
|
||||
}
|
||||
let page = Html::parse_fragment(html);
|
||||
let shown: String = page.root_element().text().collect();
|
||||
if shown.to_ascii_lowercase().contains("invalid stock code") {
|
||||
// For an unknown code the whole response is this one line.
|
||||
if html.trim().eq_ignore_ascii_case("invalid stock code!") {
|
||||
return Ok(IpotPage::Invalid);
|
||||
}
|
||||
let page = Html::parse_fragment(html);
|
||||
if !known_markup(&page) {
|
||||
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))
|
||||
.ok_or_else(|| parse_error("no single table where the page puts it"))?;
|
||||
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"));
|
||||
};
|
||||
// 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();
|
||||
if names != ["thead", "tbody", "tfoot"] {
|
||||
return Err(parse_error("unexpected table parts"));
|
||||
|
|
@ -835,9 +850,21 @@ mod tests {
|
|||
"deeply nested divs",
|
||||
"<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) {
|
||||
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