ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
mdl_discover.c
Go to the documentation of this file.
1
10#include "mdl_discover.h"
11
12#include <stdio.h>
13#include <string.h>
14
15#include "mdl_fetch_internal.h"
16#include "mdl_net.h"
17#include "mdl_search.h"
18#include "mdl_stream_internal.h"
19#include "mdl_url_guard.h"
20#include "ra8_attributes.h"
21#include "ra8_err.h"
22
24typedef enum : uint16_t {
29
46 ra8_io_stream_t* stream,
47 const char* first,
48 const char* second,
49 const char* third)
50{
51 if ((req->io_error != nullptr) && (*req->io_error != k_ra8_ok)) {
52 return;
53 }
54 ra8_err_t error = priv_mdl_stream_text(k_ra8_ok, stream, first);
55 error = priv_mdl_stream_text(error, stream, second);
56 error = priv_mdl_stream_text(error, stream, third);
57 if ((error != k_ra8_ok) && (req->io_error != nullptr) && (*req->io_error == k_ra8_ok)) {
58 *req->io_error = error;
59 }
60}
61
77RA8_INTERNAL static uint32_t internal_max_u32(uint32_t a, uint32_t b)
78{
79 return (a > b) ? a : b;
80}
81
84{
85 return (m == k_mdl_discover_search) ? "search" : "browse";
86}
87
106RA8_INTERNAL static bool
107internal_discover_url(const mdl_discover_req_t* req, char* out, size_t cap, int* rc)
108{
109 *rc = 2;
110 if (req->mode == k_mdl_discover_browse) {
111 if (req->site->browse_url[0] == '\0') {
113 req->diagnostic,
114 "mdl: this site descriptor has no browse_url; browsing is ",
115 "",
116 "unavailable\n");
117 return false;
118 }
119 const int n = snprintf(out, cap, "%s", req->site->browse_url);
120 if ((n < 0) || ((size_t)n >= cap)) {
121 internal_discover_text3(req, req->diagnostic, "mdl: browse_url is too long\n", "", "");
122 return false;
123 }
124 return true;
125 }
126 if (req->site->search_url[0] == '\0') {
128 req->diagnostic,
129 "mdl: this site descriptor has no search_url; searching is ",
130 "",
131 "unavailable\n");
132 return false;
133 }
134 if ((req->term == nullptr) || (req->term[0] == '\0')) {
135 internal_discover_text3(req, req->diagnostic, "mdl: --search needs a non-empty TERM\n", "", "");
136 return false;
137 }
138 char encoded[k_disc_term_max];
139 if (!mdl_query_encode(req->term, encoded, sizeof(encoded))) {
141 req->diagnostic,
142 "mdl: search term is too long to encode\n",
143 "",
144 "");
145 return false;
146 }
147 if (!mdl_search_build_url(req->site->search_url, encoded, out, cap)) {
149 req->diagnostic,
150 "mdl: search_url '",
151 req->site->search_url,
152 "' has no ");
154 req->diagnostic,
155 "",
157 " placeholder (or is too long)\n");
158 return false;
159 }
160 return (req->io_error == nullptr) || (*req->io_error == k_ra8_ok);
161}
162
183 const char* url,
184 size_t* out_len,
185 long* out_status)
186{
187 uint32_t crawl = 0U;
188 if (!mdl_session_url_allowed(req->session, url, &crawl)) {
189 return k_ra8_fail; /* robots refused (message already printed) */
190 }
191 char hostbuf[k_mdl_gov_host_max];
192 const char* host = mdl_url_host(url, hostbuf, sizeof(hostbuf)) ? hostbuf : nullptr;
193 const uint32_t jmin = internal_max_u32(req->site->chapter_delay_min, crawl);
194 const uint32_t jmax = internal_max_u32(req->site->chapter_delay_max, crawl);
195 const mdl_net_req_t nreq = {.user_agent = req->session->user_agent,
196 .referer = nullptr,
197 .timeout_ms = req->timeout_ms};
199 for (uint8_t attempt = 0U; attempt < (uint8_t)k_disc_max_attempts; ++attempt) {
200 if (mdl_governor_acquire(req->gov, host, jmin, jmax) == k_ra8_err_would_block) {
202 continue; /* no slot reserved, so nothing to release */
203 }
204 mdl_net_resp_t resp = {};
205 rc =
206 mdl_net_get_buf(req->session->net, url, &nreq, req->page_buf, req->page_cap, out_len, &resp);
207 mdl_governor_observe(req->gov, host, resp.status, resp.retry_after);
208 mdl_governor_release(req->gov, host);
209 *out_status = resp.status;
211 break;
212 }
213 }
214 return rc;
215}
216
230{
231 ra8_io_stream_t* output = req->output;
232 ra8_err_t error = k_ra8_ok;
233 if (req->mode == k_mdl_discover_search) {
234 error = priv_mdl_stream_text(error, output, "search results for '");
235 error = priv_mdl_stream_text(error, output, (req->term != nullptr) ? req->term : "");
236 error = priv_mdl_stream_text(error, output, "' (");
237 } else {
238 error = priv_mdl_stream_text(error, output, "latest updates on ");
239 error = priv_mdl_stream_text(error, output, req->site->host);
240 error = priv_mdl_stream_text(error, output, " (");
241 }
242 error = priv_mdl_stream_u64(error, output, n);
243 error = priv_mdl_stream_text(error, output, "):\n");
244 if ((error != k_ra8_ok) && (req->io_error != nullptr)) {
245 *req->io_error = error;
246 }
247}
248
261{
262 const mdl_hit_list_t* hits = req->hits;
263 for (size_t i = 0U; i < hits->count; ++i) {
264 ra8_err_t error = priv_mdl_stream_text(k_ra8_ok, req->output, " [");
265 error = priv_mdl_stream_u64(error, req->output, i + 1U);
266 error = priv_mdl_stream_text(error, req->output, "] ");
267 error = priv_mdl_stream_text(error, req->output, hits->hits[i].title);
268 error = priv_mdl_stream_text(error, req->output, "\n ");
269 error = priv_mdl_stream_text(error, req->output, hits->hits[i].url);
270 error = priv_mdl_stream_text(error, req->output, "\n");
271 if (error != k_ra8_ok) {
272 *req->io_error = error;
273 return;
274 }
275 }
276}
277
290{
291 const char* pat = (req->site->search_result_contains[0] != '\0')
293 : "(none configured)";
294 if (req->mode == k_mdl_discover_search) {
295 ra8_err_t error = priv_mdl_stream_text(k_ra8_ok, req->diagnostic, "mdl: no results for '");
296 error = priv_mdl_stream_text(error, req->diagnostic, (req->term != nullptr) ? req->term : "");
297 error = priv_mdl_stream_text(error, req->diagnostic, "' -- the page had ");
298 error = priv_mdl_stream_u64(error, req->diagnostic, req->hits->anchors_seen);
299 error = priv_mdl_stream_text(error,
300 req->diagnostic,
301 " link(s) but none matched the result pattern '");
302 error = priv_mdl_stream_text(error, req->diagnostic, pat);
303 error = priv_mdl_stream_text(error,
304 req->diagnostic,
305 "' (a genuine no-match, or the result markup changed)\n");
306 if (error != k_ra8_ok) {
307 *req->io_error = error;
308 }
309 } else {
310 ra8_err_t error = priv_mdl_stream_text(k_ra8_ok, req->diagnostic, "mdl: nothing to browse on ");
311 error = priv_mdl_stream_text(error, req->diagnostic, req->site->host);
312 error = priv_mdl_stream_text(error, req->diagnostic, " -- the page had ");
313 error = priv_mdl_stream_u64(error, req->diagnostic, req->hits->anchors_seen);
314 error = priv_mdl_stream_text(error,
315 req->diagnostic,
316 " link(s) but none matched the result pattern '");
317 error = priv_mdl_stream_text(error, req->diagnostic, pat);
318 error = priv_mdl_stream_text(error, req->diagnostic, "'\n");
319 if (error != k_ra8_ok) {
320 *req->io_error = error;
321 }
322 }
323}
324
337{
339 req,
340 req->diagnostic,
341 "mdl: could not read a results page from ",
342 req->site->host,
343 " -- the response carried no links, so its markup may have changed or the "
344 "request was blocked\n");
345}
346
366 const mdl_hit_t* chosen,
367 size_t pick,
368 char* out_url,
369 size_t out_cap)
370{
371 const int n = snprintf(out_url, out_cap, "%s", chosen->url);
372 if ((n < 0) || ((size_t)n >= out_cap)) {
374 req->diagnostic,
375 "mdl: selected URL is too long to hand off\n",
376 "",
377 "");
378 if (out_cap > 0U) {
379 out_url[0] = '\0';
380 }
381 return false;
382 }
383 ra8_err_t error = priv_mdl_stream_text(k_ra8_ok, req->output, "selected [");
384 error = priv_mdl_stream_u64(error, req->output, pick);
385 error = priv_mdl_stream_text(error, req->output, "] ");
386 error = priv_mdl_stream_text(error, req->output, chosen->title);
387 error = priv_mdl_stream_text(error, req->output, "\n ");
388 error = priv_mdl_stream_text(error, req->output, chosen->url);
389 error = priv_mdl_stream_text(error, req->output, "\n");
390 if (error != k_ra8_ok) {
391 *req->io_error = error;
392 return false;
393 }
394 return true;
395}
396
417 mdl_search_outcome_t outcome,
418 size_t pick,
419 char* out_url,
420 size_t out_cap)
421{
422 const mdl_hit_list_t* hits = req->hits;
423 if (outcome == k_mdl_search_have_results) {
424 internal_print_header(req, hits->count);
427 req->output,
428 " (add --pick N to download ",
429 (req->mode == k_mdl_discover_search) ? "result" : "update",
430 " N, e.g. --pick 1 --format cbz)\n");
431 } else if (outcome == k_mdl_search_zero_results) {
433 } else {
435 }
436
437 if ((req->io_error != nullptr) && (*req->io_error != k_ra8_ok)) {
438 return 1;
439 }
440
441 if (pick == 0U) {
442 return (outcome == k_mdl_search_markup_changed) ? 1 : 0;
443 }
444 if ((out_url == nullptr) || (out_cap == 0U)) {
446 req->diagnostic,
447 "mdl: --pick needs an output buffer to hand off the selection\n",
448 "",
449 "");
450 return (*req->io_error == k_ra8_ok) ? 2 : 1;
451 }
452 if (outcome != k_mdl_search_have_results) {
453 ra8_err_t error = priv_mdl_stream_text(k_ra8_ok, req->diagnostic, "mdl: --pick ");
454 error = priv_mdl_stream_u64(error, req->diagnostic, pick);
455 error = priv_mdl_stream_text(error, req->diagnostic, " but the ");
456 error = priv_mdl_stream_text(error, req->diagnostic, internal_mode_word(req->mode));
457 *req->io_error = priv_mdl_stream_text(error, req->diagnostic, " returned no results\n");
458 return 1;
459 }
460 if (pick > hits->count) {
461 ra8_err_t error = priv_mdl_stream_text(k_ra8_ok, req->diagnostic, "mdl: --pick ");
462 error = priv_mdl_stream_u64(error, req->diagnostic, pick);
463 error = priv_mdl_stream_text(error, req->diagnostic, " out of range (1..");
464 error = priv_mdl_stream_u64(error, req->diagnostic, hits->count);
465 *req->io_error = priv_mdl_stream_text(error, req->diagnostic, ")\n");
466 return (*req->io_error == k_ra8_ok) ? 2 : 1;
467 }
468 return internal_select_hit(req, &hits->hits[pick - 1U], pick, out_url, out_cap) ? 0 : 1;
469}
470
471int mdl_discover_run(const mdl_discover_req_t* req, size_t pick, char* out_url, size_t out_cap)
472{
473 if ((out_url != nullptr) && (out_cap > 0U)) {
474 out_url[0] = '\0';
475 }
476 if ((req == nullptr) || (req->session == nullptr) || (req->gov == nullptr) ||
477 (req->site == nullptr) || (req->page_buf == nullptr) || (req->hits == nullptr) ||
478 (req->output == nullptr) || (req->diagnostic == nullptr) || (req->io_error == nullptr)) {
479 if ((req != nullptr) && (req->diagnostic != nullptr)) {
481 req->diagnostic,
482 "mdl: discovery called with missing dependencies\n");
483 }
484 return 2;
485 }
486 char url[k_disc_url_max];
487 int rc = 0;
488 if (!internal_discover_url(req, url, sizeof(url), &rc)) {
489 return (*req->io_error == k_ra8_ok) ? rc : 1;
490 }
491 size_t len = 0U;
492 long status = 0;
493 const ra8_err_t frc = internal_discover_fetch(req, url, &len, &status);
494 if (frc != k_ra8_ok) {
495 char reason[k_mdl_reason_max];
496 priv_mdl_fetch_reason(frc, status, reason, sizeof(reason));
498 req->diagnostic,
499 "mdl: ",
501 " request failed -- ");
502 internal_discover_text3(req, req->diagnostic, "", reason, "\n");
503 return 1;
504 }
505 const ra8_err_t parsed =
506 mdl_extract_hits(req->page_buf, len, url, req->site->search_result_contains, req->hits);
507 if (parsed != k_ra8_ok) {
509 req->diagnostic,
510 "mdl: ",
512 " results exceeded the bounded hit table\n");
513 return 1;
514 }
516 const mdl_search_outcome_t outcome = mdl_search_classify(req->hits);
517 return internal_present_and_pick(req, outcome, pick, out_url, out_cap);
518}
static void internal_print_header(const mdl_discover_req_t *req, size_t n)
Print the numbered results header for the run's mode.
static void internal_print_markup_changed(const mdl_discover_req_t *req)
The honest "the markup changed" message, distinct from no results.
static const char * internal_mode_word(mdl_discover_mode_t m)
The human word for a discovery mode, for diagnostics.
static ra8_err_t internal_discover_fetch(const mdl_discover_req_t *req, const char *url, size_t *out_len, long *out_status)
Robots-gate and governed-fetch the results page into req->page_buf.
int mdl_discover_run(const mdl_discover_req_t *req, size_t pick, char *out_url, size_t out_cap)
Run one search/browse discovery: fetch, parse, present, and select.
static void internal_print_hits(const mdl_discover_req_t *req)
Print every hit as [N] title over its series URL.
static int internal_present_and_pick(const mdl_discover_req_t *req, mdl_search_outcome_t outcome, size_t pick, char *out_url, size_t out_cap)
Present the outcome and, when pick != 0, resolve the selection.
static void internal_print_zero(const mdl_discover_req_t *req)
The honest "no results" message, distinct from a broken page.
static void internal_discover_text3(const mdl_discover_req_t *req, ra8_io_stream_t *stream, const char *first, const char *second, const char *third)
Append three discovery fragments and latch the first sink failure.
mdl_discover_size_t
Local buffer sizes and the bounded governed-retry budget.
@ k_disc_url_max
Composed results-URL buffer bytes.
@ k_disc_max_attempts
1 initial + up to 3 governed retries.
@ k_disc_term_max
Percent-encoded term buffer bytes.
static uint32_t internal_max_u32(uint32_t a, uint32_t b)
Larger of two unsigned values.
static bool internal_discover_url(const mdl_discover_req_t *req, char *out, size_t cap, int *rc)
Build the results URL for this run; false + message on a config gap.
static bool internal_select_hit(const mdl_discover_req_t *req, const mdl_hit_t *chosen, size_t pick, char *out_url, size_t out_cap)
Copy the picked hit's URL out and announce it; false + message on overrun.
Search / browse discovery: governed fetch of a results page, parse, honest presentation,...
mdl_discover_mode_t
Which discovery endpoint of a site descriptor to query.
@ k_mdl_discover_browse
browse_url latest-updates page, no term.
@ k_mdl_discover_search
search_url with a {q} term substitution.
ra8_err_t mdl_extract_hits(const char *html, size_t html_len, const char *base_url, const char *url_contains, mdl_hit_list_t *out)
Scan html for <a href> links, keeping each hit's title and URL.
bool priv_mdl_fetch_is_retryable(ra8_err_t rc)
Whether a failed transfer result is worth retrying.
Definition mdl_fetch.c:110
void priv_mdl_fetch_reason(ra8_err_t err, long status, char *buf, size_t cap)
Render a human-readable reason for a transfer result and HTTP status.
Definition mdl_fetch.c:130
Module-private fetch-loop decisions promoted for host unit tests and the CLI's end-of-run reporting.
@ k_mdl_reason_max
Failure-reason string buffer bytes.
Streaming HTTP(S) GET seam (a real function-pointer vtable) for the host manga downloader (v0).
ra8_err_t mdl_net_get_buf(mdl_net_iface_t *net, const char *url, const mdl_net_req_t *req, char *buf, size_t cap, size_t *out_len, mdl_net_resp_t *resp)
GET url fully into a caller buffer (used for HTML pages).
Definition mdl_net.c:78
void mdl_governor_observe(mdl_governor_t *g, const char *host, long status, const char *retry_after)
Feed a completed request's outcome back into the host's governor state.
@ k_mdl_gov_host_max
Host-key buffer bytes (matches config).
ra8_err_t mdl_governor_acquire(mdl_governor_t *g, const char *host, uint32_t jitter_min_ms, uint32_t jitter_max_ms)
Reserve an in-flight slot for a request to host, pacing as required.
void mdl_governor_release(mdl_governor_t *g, const char *host)
Release the in-flight slot reserved by a matching mdl_governor_acquire.
Pure search/discovery policy: query encoding, URL templating, and the honest zero-vs-broken result cl...
const char * mdl_search_placeholder(void)
The {q} placeholder a search-URL template must contain.
Definition mdl_search.c:29
mdl_search_outcome_t
The honest outcome of a parsed search/browse results page.
Definition mdl_search.h:57
@ k_mdl_search_markup_changed
No links at all: the page could not be read.
Definition mdl_search.h:60
@ k_mdl_search_have_results
One or more hits matched; print them.
Definition mdl_search.h:58
@ k_mdl_search_zero_results
Links present, none matched: a real no-hit.
Definition mdl_search.h:59
size_t mdl_search_filter_series_hits(mdl_hit_list_t *hits, const char *chapter_marker)
Remove chapter links and duplicate series URLs from discovery hits.
Definition mdl_search.c:163
bool mdl_query_encode(const char *term, char *out, size_t cap)
Percent-encode a raw query term for safe inclusion in a URL.
Definition mdl_search.c:75
mdl_search_outcome_t mdl_search_classify(const mdl_hit_list_t *hits)
Classify a parsed results page into an honest search outcome.
Definition mdl_search.c:149
bool mdl_search_build_url(const char *tmpl, const char *encoded_term, char *out, size_t cap)
Expand a query-URL template by substituting the encoded term for {q}.
Definition mdl_search.c:108
bool mdl_session_url_allowed(mdl_session_t *session, const char *url, uint32_t *crawl_delay_ms)
Decide whether url may be fetched under robots.txt, and its delay.
ra8_err_t priv_mdl_stream_text(ra8_err_t prior, ra8_io_stream_t *stream, const char *text)
Append text unless an earlier operation already failed.
Definition mdl_stream.c:16
ra8_err_t priv_mdl_stream_u64(ra8_err_t prior, ra8_io_stream_t *stream, uint64_t value)
Append one unsigned integer unless an earlier append failed.
Definition mdl_stream.c:21
Private bounded text helpers over the portable byte-stream contract.
Pure URL / address safety predicates for the libcurl backend.
bool mdl_url_host(const char *url, char *out, size_t cap)
Extract the authority (host and optional port) from an http(s) URL.
Annotation-attribute framework macros for ra8-firmware.
#define RA8_INTERNAL
Marker that a function is intended to be static (file-local).
Error Code Definitions for ra8-firmware.
@ k_ra8_err_would_block
Non-blocking operation would have blocked.
Definition ra8_err.h:210
@ k_ra8_fail
Generic unspecified failure.
Definition ra8_err.h:133
@ k_ra8_ok
Success – operation completed with all postconditions satisfied.
Definition ra8_err.h:119
ra8_err_codes_t ra8_err_t
Canonical error-return type used by every ra8-firmware API.
Definition ra8_err.h:546
Injected dependencies and inputs for one discovery run.
mdl_hit_list_t * hits
Receives the parsed hits (borrowed).
ra8_io_stream_t * output
Borrowed normal-output byte sink.
mdl_session_t * session
Initialised session (net + UA + robots).
const char * term
Raw search term; ignored/NULL for browse.
ra8_err_t * io_error
Caller-owned first sink failure latch.
mdl_governor_t * gov
Initialised per-host politeness governor.
const mdl_site_t * site
Descriptor holding the discovery keys.
uint32_t timeout_ms
Per-request time budget, milliseconds.
mdl_discover_mode_t mode
Search or browse.
size_t page_cap
Capacity of page_buf in bytes.
ra8_io_stream_t * diagnostic
Borrowed diagnostic byte sink.
char * page_buf
Results-page fetch scratch (borrowed).
Bounded list of titled hits plus the raw anchor tally.
Definition mdl_extract.h:68
mdl_hit_t hits[k_mdl_max_hits]
Deduplicated titled hits.
Definition mdl_extract.h:69
size_t count
Number of valid hits.
Definition mdl_extract.h:70
size_t anchors_seen
Total resolvable <a href> anchors.
Definition mdl_extract.h:71
One discovery hit: a human-facing title paired with a series URL.
Definition mdl_extract.h:50
char title[k_mdl_hit_title_max]
Display title, NUL-terminated.
Definition mdl_extract.h:51
char url[k_mdl_url_max]
Absolute series URL, NUL-terminated.
Definition mdl_extract.h:52
Per-request session parameters.
Definition mdl_net.h:43
Per-transfer response metadata surfaced to the politeness governor.
Definition mdl_net.h:120
long status
Finished transfer's HTTP status, 0 if none.
Definition mdl_net.h:121
char retry_after[k_mdl_retry_after_max]
Raw Retry-After value, "" when absent.
Definition mdl_net.h:122
mdl_net_iface_t * net
Backend used for robots.txt fetches.
Definition mdl_session.h:40
const char * user_agent
Full UA header (caller-owned).
Definition mdl_session.h:41
uint32_t chapter_delay_min
Inter-chapter spacing floor.
Definition mdl_config.h:89
char browse_url[k_mdl_search_url_max]
Latest-updates page (no {q}).
Definition mdl_config.h:67
char search_url[k_mdl_search_url_max]
Query template with a {q} slot.
Definition mdl_config.h:63
uint32_t chapter_delay_max
Inter-chapter spacing ceiling.
Definition mdl_config.h:90
char host[k_mdl_host_max]
Site host, e.g.
Definition mdl_config.h:48
char chapter_url_contains[k_mdl_match_max]
href must contain this.
Definition mdl_config.h:53
char search_result_contains[k_mdl_match_max]
Series-link substring on results.
Definition mdl_config.h:65
Caller-allocated byte-stream handle binding a sink to its context.