@@ -49,6 +49,10 @@ export struct DownloadToFileResult {
4949 int statusCode { 0 };
5050 std::string error;
5151 std::int64_t bytesWritten { 0 };
52+ std::optional<std::int64_t > expectedBytes;
53+ std::string finalUrl;
54+ std::string etag;
55+ std::string lastModified;
5256 bool ok () const { return statusCode >= 200 && statusCode < 300 && error.empty (); }
5357};
5458
@@ -186,6 +190,28 @@ static std::string read_line(TlsSocket& sock, int timeoutMs) {
186190 return line;
187191}
188192
193+ static std::expected<std::string, std::string>
194+ read_complete_line (TlsSocket& sock, int timeoutMs) {
195+ std::string line;
196+ char c {};
197+ while (true ) {
198+ if (!sock.wait_readable (timeoutMs)) {
199+ return std::unexpected (" timeout or EOF before CRLF" );
200+ }
201+ int ret = sock.read (&c, 1 );
202+ if (ret <= 0 ) {
203+ return std::unexpected (" EOF before CRLF" );
204+ }
205+ line += c;
206+ if (line.size () >= 2
207+ && line[line.size () - 2 ] == ' \r '
208+ && line[line.size () - 1 ] == ' \n ' ) {
209+ line.resize (line.size () - 2 );
210+ return line;
211+ }
212+ }
213+ }
214+
189215// Write all data to socket
190216static bool write_all (TlsSocket& sock, const std::string& data) {
191217 int total = 0 ;
@@ -216,6 +242,20 @@ static int parse_hex(std::string_view s) {
216242 return result;
217243}
218244
245+ export std::optional<std::int64_t >
246+ parse_chunk_size_line (std::string_view line) {
247+ if (line.empty ()) return std::nullopt ;
248+ std::uint64_t value {};
249+ auto [end, error] = std::from_chars (
250+ line.data (), line.data () + line.size (), value, 16 );
251+ if (error != std::errc{} || end != line.data () + line.size ()
252+ || value > static_cast <std::uint64_t >(
253+ std::numeric_limits<int >::max ())) {
254+ return std::nullopt ;
255+ }
256+ return static_cast <std::int64_t >(value);
257+ }
258+
219259// Case-insensitive string comparison
220260static bool iequals (std::string_view a, std::string_view b) {
221261 if (a.size () != b.size ()) return false ;
@@ -870,6 +910,8 @@ private:
870910 std::int64_t contentLength = -1 ;
871911 bool connectionClose = false ;
872912 std::string location;
913+ std::string etag;
914+ std::string lastModified;
873915
874916 while (true ) {
875917 std::string line = read_line (*sock, config_.readTimeoutMs );
@@ -894,6 +936,10 @@ private:
894936 connectionClose = true ;
895937 if (iequals (key, " Location" ))
896938 location = valStr;
939+ if (iequals (key, " ETag" ))
940+ etag = valStr;
941+ if (iequals (key, " Last-Modified" ))
942+ lastModified = valStr;
897943 }
898944
899945 // Follow redirects
@@ -920,6 +966,11 @@ private:
920966 return result;
921967 }
922968
969+ result.finalUrl = url;
970+ result.etag = std::move (etag);
971+ result.lastModified = std::move (lastModified);
972+ if (contentLength >= 0 ) result.expectedBytes = contentLength;
973+
923974 // Open output file
924975 std::error_code ec;
925976 std::filesystem::create_directories (destFile.parent_path (), ec);
@@ -950,15 +1001,45 @@ private:
9501001 if (chunked) {
9511002 while (true ) {
9521003 if (cancelled ()) return result;
953- std::string sizeLine = read_line (*sock, config_.readTimeoutMs );
1004+ auto sizeResult = read_complete_line (
1005+ *sock, config_.readTimeoutMs );
1006+ if (!sizeResult) {
1007+ result.error = " Invalid chunk size line: "
1008+ + std::move (sizeResult).error ();
1009+ result.bytesWritten = downloaded;
1010+ sock->close ();
1011+ pool_.erase (poolKey);
1012+ return result;
1013+ }
1014+ std::string sizeLine = std::move (*sizeResult);
9541015 auto semi = sizeLine.find (' ;' );
9551016 if (semi != std::string::npos) sizeLine = sizeLine.substr (0 , semi);
9561017 while (!sizeLine.empty () && (sizeLine.back () == ' ' || sizeLine.back () == ' \t ' ))
9571018 sizeLine.pop_back ();
9581019
959- int chunkSize = parse_hex (sizeLine);
1020+ auto parsedChunkSize = parse_chunk_size_line (sizeLine);
1021+ if (!parsedChunkSize) {
1022+ result.error = " Invalid chunk size: " + sizeLine;
1023+ result.bytesWritten = downloaded;
1024+ sock->close ();
1025+ pool_.erase (poolKey);
1026+ return result;
1027+ }
1028+ int chunkSize = static_cast <int >(*parsedChunkSize);
9601029 if (chunkSize == 0 ) {
961- read_line (*sock, config_.readTimeoutMs );
1030+ for (;;) {
1031+ auto trailer = read_complete_line (
1032+ *sock, config_.readTimeoutMs );
1033+ if (!trailer) {
1034+ result.error = " Invalid chunk trailer: "
1035+ + std::move (trailer).error ();
1036+ result.bytesWritten = downloaded;
1037+ sock->close ();
1038+ pool_.erase (poolKey);
1039+ return result;
1040+ }
1041+ if (trailer->empty ()) break ;
1042+ }
9621043 break ;
9631044 }
9641045
@@ -979,7 +1060,15 @@ private:
9791060 remaining -= toRead;
9801061 if (onProgress) onProgress (totalBytes, downloaded);
9811062 }
982- read_line (*sock, config_.readTimeoutMs );
1063+ auto delimiter = read_complete_line (
1064+ *sock, config_.readTimeoutMs );
1065+ if (!delimiter || !delimiter->empty ()) {
1066+ result.error = " Missing CRLF after chunk data" ;
1067+ result.bytesWritten = downloaded;
1068+ sock->close ();
1069+ pool_.erase (poolKey);
1070+ return result;
1071+ }
9831072 }
9841073 } else if (contentLength > 0 ) {
9851074 char buf[8192 ];
0 commit comments