use crate::{
detect::{PureHtml, SiteType},
error::{Result, SelectorResultExt},
httpdirectoryentry::HttpDirectoryEntry,
scrapers::{h5ai::scrape_h5ai, miniserve::scrape_miniserve, snt::scrape_snt, stil::scrape_stil, ul::scrape_ul},
};
use scraper::{ElementRef, Html, Selector};
use tracing::{debug, info, trace, warn};
use unwrap_unreachable::UnwrapUnreachable;
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub(crate) fn are_table_headers_present(table: ElementRef) -> bool {
let th_selector = Selector::parse("th").unreachable();
for th in table.select(&th_selector) {
let columns: Vec<_> = th.text().collect();
for column in columns {
let lowered = column.to_lowercase();
if lowered.contains("last modified")
|| lowered.contains("modified")
|| lowered.contains("date")
|| lowered.contains("modification time")
|| lowered.contains("last modification")
{
return true;
}
}
}
warn!("This table does not contain any date header field");
false
}
pub(crate) fn extract_link<'a>(col: &ElementRef<'a>, link_selector: &Selector) -> &'a str {
col.select(link_selector).next().and_then(|a| a.value().attr("href")).unwrap_or_default()
}
fn extract_name_and_link<'a>(
one_line_iter: &mut impl Iterator<Item = &'a ElementRef<'a>>,
link_selector: &Selector,
) -> (Vec<&'a str>, &'a str) {
let Some(first_col) = one_line_iter.next() else {
return (vec![], "");
};
let first_col_txt = remove_empty_cell(first_col.text().collect());
if first_col_txt.is_empty() {
let Some(name_col) = one_line_iter.next() else {
return (vec![], "");
};
let name = remove_empty_cell(name_col.text().collect());
(name, extract_link(name_col, link_selector))
} else {
(first_col_txt, extract_link(first_col, link_selector))
}
}
pub(crate) fn extract_col_text<'a>(one_line_iter: &mut impl Iterator<Item = &'a ElementRef<'a>>) -> Vec<&'a str> {
one_line_iter.next().map(|col| remove_empty_cell(col.text().collect())).unwrap_or_default()
}
pub(crate) fn build_entry<'a>(
name: &[&'a str],
date: &[&'a str],
size: &[&'a str],
link: &'a str,
) -> Option<HttpDirectoryEntry> {
let &name = name.first()?;
let (date, size) = match (date.first(), size.first()) {
(Some(&d), Some(&s)) => (d, s),
(Some(&d), None) => (d, " - "),
(None, Some(&s)) => ("", s),
(None, None) => ("", " - "),
};
Some(HttpDirectoryEntry::new(name, date, size, link))
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub(crate) fn scrape_table(body: &str) -> Vec<HttpDirectoryEntry> {
let html = Html::parse_document(body);
let table_selector = Selector::parse("table").unreachable();
let row_selector = Selector::parse("tr").unreachable();
let col_selector = Selector::parse("td").unreachable();
let link_selector = Selector::parse("a").unreachable();
html.select(&table_selector)
.filter(|&table| are_table_headers_present(table))
.flat_map(|table| table.select(&row_selector).collect::<Vec<_>>())
.filter_map(|row| {
let one_line: Vec<_> = row.select(&col_selector).collect();
let mut iter = one_line.iter();
let (name, link) = extract_name_and_link(&mut iter, &link_selector);
trace!("name: {name:?}, link: {link}");
let date = extract_col_text(&mut iter);
let size = extract_col_text(&mut iter);
trace!("date: {date:?}, size: {size:?}");
build_entry(&name, &date, &size, link)
})
.collect()
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
fn scrape_pre_with_img(body: &str) -> Result<Vec<HttpDirectoryEntry>> {
let mut should_be_considered_valid = false;
let mut http_dir_entry = vec![];
let html = Html::parse_document(body);
let pre_selector = Selector::parse("pre").with_selector("pre")?;
let pre_iter = html.select(&pre_selector);
for pre in pre_iter {
if pre.inner_html().contains("<img") {
debug!("Analyzing <pre> tag with <img> tag");
for line in pre.inner_html().split("<img") {
let new_line = strip_until_stop(line, "<a", false);
if !new_line.is_empty() {
trace!("{new_line}");
let href = new_line.split("</a>").map(str::trim).collect::<Vec<&str>>();
trace!("{href:?}");
if href.len() >= 4 {
should_be_considered_valid = is_this_a_real_header(&href);
} else if href.len() >= 2 {
let (link, name) = get_link_and_name(href[0]);
if name.to_lowercase() == "parent directory" {
http_dir_entry.push(HttpDirectoryEntry::ParentDirectory(link.to_string()));
} else {
let (date, size) = get_date_and_size(href[1]);
http_dir_entry.push(HttpDirectoryEntry::new(name, date, size, link));
}
}
}
}
if should_be_considered_valid {
return Ok(http_dir_entry);
}
trace!("Unable to get entry from this body (no headers ?):\n{body}");
} else {
trace!("Unable to get entry from this body (no <img> tag):\n{}", pre.inner_html());
}
}
Ok(http_dir_entry)
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub(crate) fn remove_empty_cell(mut vector: Vec<&str>) -> Vec<&str> {
vector.retain(|v| !v.trim().is_empty());
vector
}
fn get_date_and_size(line: &str) -> (&str, &str) {
let index = match line.find(' ') {
Some(index) => match line[index + 1..].find(' ') {
Some(index2) => index + index2 + 1,
None => index,
},
None => 0,
};
trace!("index: {index}");
let date = &line[0..index];
let size_and_description = &line[index..];
let line_split: Vec<&str> = size_and_description.trim().split(' ').collect();
let size = if line_split.len() >= 2 {
line_split[0]
} else {
size_and_description
};
trace!(" -> date: {date}, size: {size}");
(date, size)
}
pub fn get_link_and_name(column: &str) -> (&str, &str) {
if let Some(num) = column.find('>') {
let name = &column[num + 1..];
let link = match &column[0..num].strip_prefix(r#"<a href=""#) {
Some(link) => match link.strip_suffix(r#"""#) {
Some(l) => l,
None => link,
},
None => match column[0..num].strip_suffix(r#"""#) {
Some(l) => l,
None => &column[0..num],
},
};
let link = link.trim();
let name = name.trim();
trace!(" -> link: {link}, name: {name}");
(link, name)
} else {
let name = column.trim();
trace!(" -> link: , name: {name}");
("", name)
}
}
fn is_this_a_real_header(href: &[&str]) -> bool {
let name = strip_until_stop(href[0], ">", true);
let date = strip_until_stop(href[1], ">", true);
let size = strip_until_stop(href[2], ">", true);
trace!("This is the header: {name}, {date}, {size}");
name.to_lowercase() == "name" && date.to_lowercase() == "last modified" && size.to_lowercase() == "size"
}
fn strip_until_stop<'a>(line: &'a str, stop: &str, remove: bool) -> &'a str {
match line.find(stop) {
Some(mut num) => {
if !remove && num >= 1 {
num -= 1;
}
match line.strip_prefix(&line[0..=num]) {
Some(line_without_prefix) => line_without_prefix.trim(),
None => line.trim(),
}
}
None => line.trim(),
}
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
fn scrape_pre_simple(body: &str) -> Result<Vec<HttpDirectoryEntry>> {
let mut http_dir_entry = vec![];
let html = Html::parse_document(body);
let pre_selector = Selector::parse("pre").with_selector("pre")?;
let pre_iter = html.select(&pre_selector);
for pre in pre_iter {
debug!("Analyzing <pre> tag");
for line in pre.inner_html().lines() {
if !line.is_empty() {
trace!("{line}");
let href = line.split("</a>").map(str::trim).collect::<Vec<&str>>();
trace!("{href:?}");
if href.len() >= 2 {
let (link, name) = get_link_and_name(href[0]);
if name.to_lowercase() == "../" {
http_dir_entry.push(HttpDirectoryEntry::ParentDirectory(link.to_string()));
} else {
let (date, size) = get_date_and_size(href[1]);
http_dir_entry.push(HttpDirectoryEntry::new(name, date, size, link));
}
}
}
}
}
Ok(http_dir_entry)
}
pub fn scrape_body(body: &str) -> Result<Vec<HttpDirectoryEntry>> {
match SiteType::detect(body) {
SiteType::H5ai(version) => {
info!("H5ai powered version {version} website detected");
scrape_h5ai(body, &version)
}
SiteType::Snt => {
info!("SNT index generator website detected");
scrape_snt(body)
}
SiteType::MiniServe(version) => {
info!("Miniserve version {version} website detected");
scrape_miniserve(body, &version)
}
SiteType::Stil => {
info!("Stil STatic Index List website detected");
scrape_stil(body)
}
SiteType::NotNamed(html) => match html {
PureHtml::Table => {
info!("Body has <table> tag");
Ok(scrape_table(body))
}
PureHtml::Pre => {
info!("Body has <pre> tag");
let http_dir_entry = scrape_pre_with_img(body)?;
if http_dir_entry.is_empty() {
let http_dir_entry = scrape_pre_simple(body)?;
Ok(http_dir_entry)
} else {
Ok(http_dir_entry)
}
}
PureHtml::Ul => {
info!("Body has no <table>, nor <pre> but <ul> tag");
scrape_ul(body)
}
},
SiteType::None => {
warn!("Site type has not been detected: doing nothing");
Ok(vec![])
}
}
}
#[cfg(test)]
mod test {
use super::is_this_a_real_header;
#[test]
fn test_is_this_a_real_header() {
let href = vec!["Name", "Last modified", "Size", "Description"];
let header = is_this_a_real_header(&href);
assert!(header);
}
#[test]
fn test_this_is_not_a_real_header() {
let href = vec!["Size", "Last modified", "Description", "Name"];
let header = is_this_a_real_header(&href);
assert!(!header);
}
}