aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-04-13 02:11:27 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-04-13 02:11:27 +0800
commitf640448109ca233d215ec1c07b795bd4313f428d (patch)
treea45aa53cee1823a35a5bfa64e57bf86d58c10318
parent0c214c3b0a94b1286ef252d6e57c153dd82f134f (diff)
downloadrwebbrowser-f640448109ca233d215ec1c07b795bd4313f428d.tar.gz
rwebbrowser-f640448109ca233d215ec1c07b795bd4313f428d.tar.xz
feat: add binary file download support by parsing Content-Type headers
-rw-r--r--include/ricochet/net/http_client.hh1
-rw-r--r--src/core/browser.cc78
-rw-r--r--src/net/http_client.cc12
3 files changed, 78 insertions, 13 deletions
diff --git a/include/ricochet/net/http_client.hh b/include/ricochet/net/http_client.hh
index 45c92e8..f6d8aa3 100644
--- a/include/ricochet/net/http_client.hh
+++ b/include/ricochet/net/http_client.hh
@@ -33,6 +33,7 @@ struct HttpResponse
int status_code { 0 };
std::unordered_map<std::string, std::string> headers {};
std::string body {};
+ std::string content_type {};
};
class HttpClient
diff --git a/src/core/browser.cc b/src/core/browser.cc
index 1457929..57ed496 100644
--- a/src/core/browser.cc
+++ b/src/core/browser.cc
@@ -135,6 +135,53 @@ std::string generate_bookmarks_html()
return html;
}
+PageData process_binary_download( const HttpRequest& req, const net::HttpResponse& resp )
+{
+ std::string fname = "download.dat";
+ const std::size_t slash = req.url.rfind( '/' );
+ if ( slash != std::string::npos && slash + 1 < req.url.length() ) {
+ fname = req.url.substr( slash + 1 );
+ const std::size_t q_pos = fname.find( '?' );
+ if ( q_pos != std::string::npos ) {
+ fname = fname.substr( 0, q_pos );
+ }
+ }
+ if ( fname.empty() ) {
+ fname = "download.dat";
+ }
+
+ std::ofstream out( fname, std::ios::binary );
+ out.write( resp.body.data(), static_cast<std::streamsize>( resp.body.size() ) );
+ out.close();
+
+ return { .lines = { "\033[1;32m[ Download Complete ]\033[0m",
+ "",
+ "Saved File : " + fname,
+ "File Type : " + resp.content_type,
+ "File Size : " + std::to_string( resp.body.size() ) + " bytes",
+ "",
+ "Press 'H' to return to the previous page." },
+ .links = {},
+ .inputs = {},
+ .title = "Download Manager" };
+}
+
+std::vector<std::string> split_into_lines( const std::string& text )
+{
+ std::vector<std::string> lines;
+ std::size_t start = 0;
+ while ( start < text.length() ) {
+ const std::size_t end = text.find( '\n', start );
+ if ( end == std::string::npos ) {
+ lines.push_back( text.substr( start ) );
+ break;
+ }
+ lines.push_back( text.substr( start, end - start ) );
+ start = end + 1;
+ }
+ return lines;
+}
+
PageData load_page( const HttpRequest& req )
{
std::string html_body;
@@ -146,6 +193,21 @@ PageData load_page( const HttpRequest& req )
if ( !response_result.has_value() ) {
return { .lines = { "[!] Failed to load: " + req.url }, .links = {}, .inputs = {}, .title = "Error" };
}
+
+ const auto& resp = response_result.value();
+ std::string c_type = resp.content_type;
+ for ( char& c : c_type ) {
+ if ( c >= 'A' && c <= 'Z' ) {
+ c = static_cast<char>( c + 32 );
+ }
+ }
+
+ const bool is_text
+ = c_type.starts_with( "text/" ) || c_type.contains( "xml" ) || c_type.contains( "json" ) || c_type.empty();
+
+ if ( !is_text ) {
+ return process_binary_download( req, resp );
+ }
html_body = response_result.value().body;
}
@@ -155,20 +217,10 @@ PageData load_page( const HttpRequest& req )
const auto dom_root = builder.build( lexer.tokenize( html_body ) );
const render::RenderResult result = renderer.render( dom_root );
-
- std::vector<std::string> lines;
- std::size_t start = 0;
- while ( start < result.text.length() ) {
- const std::size_t end = result.text.find( '\n', start );
- if ( end == std::string::npos ) {
- lines.push_back( result.text.substr( start ) );
- break;
- }
- lines.push_back( result.text.substr( start, end - start ) );
- start = end + 1;
- }
const std::string title = extract_title( dom_root );
- return { .lines = lines, .links = result.links, .inputs = result.inputs, .title = title };
+
+ return {
+ .lines = split_into_lines( result.text ), .links = result.links, .inputs = result.inputs, .title = title };
}
void draw_view( const tui::Terminal& terminal,
diff --git a/src/net/http_client.cc b/src/net/http_client.cc
index 240b234..7372247 100644
--- a/src/net/http_client.cc
+++ b/src/net/http_client.cc
@@ -314,6 +314,18 @@ std::expected<HttpResponse, std::string> HttpClient::fetch( std::string_view url
const auto header_end = raw_response.find( "\r\n\r\n" );
if ( header_end != std::string::npos ) {
response.body = raw_response.substr( header_end + 4 );
+ const std::string headers = raw_response.substr( 0, header_end );
+ std::size_t ct_pos = headers.find( "Content-Type: " );
+ if ( ct_pos == std::string::npos ) {
+ ct_pos = headers.find( "content-type: " );
+ }
+ if ( ct_pos != std::string::npos ) {
+ ct_pos += 14;
+ const std::size_t end_pos = headers.find( '\r', ct_pos );
+ if ( end_pos != std::string::npos ) {
+ response.content_type = headers.substr( ct_pos, end_pos - ct_pos );
+ }
+ }
if ( raw_response.size() >= 12 ) {
try {
response.status_code = std::stoi( raw_response.substr( 9, 3 ) );