Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add netstream-based HTTP stream support

Force FFI stream functions into the server staticlib so C code can call
into the network stream layer. Route read/lseek and file-size queries
for HTTP(S) fds through the stream abstraction (metadata_filesize /
FS_PREFIX) and update many metadata parsers to use the new APIs.

Add stream_content_type declaration and normalize HTTP(S) URLs in
playlist path formatting. Update build artifacts and workspace settings
(u i simulator make, Cargo.toml, CLI build script) and adjust Zig types
for mp3entry. Include small header fixes and a temporary debug print.

+881 -219
+61
Cargo.lock
··· 575 575 ] 576 576 577 577 [[package]] 578 + name = "assert-json-diff" 579 + version = "2.0.2" 580 + source = "registry+https://github.com/rust-lang/crates.io-index" 581 + checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" 582 + dependencies = [ 583 + "serde", 584 + "serde_json", 585 + ] 586 + 587 + [[package]] 578 588 name = "ast_node" 579 589 version = "0.9.9" 580 590 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1716 1726 version = "1.0.2" 1717 1727 source = "registry+https://github.com/rust-lang/crates.io-index" 1718 1728 checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 1729 + 1730 + [[package]] 1731 + name = "colored" 1732 + version = "3.1.1" 1733 + source = "registry+https://github.com/rust-lang/crates.io-index" 1734 + checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" 1735 + dependencies = [ 1736 + "windows-sys 0.59.0", 1737 + ] 1719 1738 1720 1739 [[package]] 1721 1740 name = "compact_str" ··· 6844 6863 ] 6845 6864 6846 6865 [[package]] 6866 + name = "mockito" 6867 + version = "1.7.2" 6868 + source = "registry+https://github.com/rust-lang/crates.io-index" 6869 + checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0" 6870 + dependencies = [ 6871 + "assert-json-diff", 6872 + "bytes", 6873 + "colored", 6874 + "futures-core", 6875 + "http 1.1.0", 6876 + "http-body 1.0.1", 6877 + "http-body-util", 6878 + "hyper 1.4.1", 6879 + "hyper-util", 6880 + "log", 6881 + "pin-project-lite", 6882 + "rand 0.9.0", 6883 + "regex", 6884 + "serde_json", 6885 + "serde_urlencoded", 6886 + "similar", 6887 + "tokio", 6888 + ] 6889 + 6890 + [[package]] 6847 6891 name = "moka" 6848 6892 version = "0.12.7" 6849 6893 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7001 7045 dependencies = [ 7002 7046 "libc", 7003 7047 "winapi", 7048 + ] 7049 + 7050 + [[package]] 7051 + name = "netstream" 7052 + version = "0.1.0" 7053 + dependencies = [ 7054 + "libc", 7055 + "mockito", 7056 + "once_cell", 7057 + "reqwest", 7004 7058 ] 7005 7059 7006 7060 [[package]] ··· 9082 9136 "lazy_static", 9083 9137 "local-ip-addr", 9084 9138 "md5", 9139 + "netstream", 9085 9140 "owo-colors 4.1.0", 9086 9141 "queryst", 9087 9142 "rand 0.8.5", ··· 9937 9992 version = "0.1.4" 9938 9993 source = "registry+https://github.com/rust-lang/crates.io-index" 9939 9994 checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" 9995 + 9996 + [[package]] 9997 + name = "similar" 9998 + version = "2.7.0" 9999 + source = "registry+https://github.com/rust-lang/crates.io-index" 10000 + checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" 9940 10001 9941 10002 [[package]] 9942 10003 name = "siphasher"
+4 -1
Cargo.toml
··· 6 6 "webui", 7 7 ] 8 8 resolver = "2" 9 - exclude = ["rmpc", "crates/controls", "crates/netstream"] 9 + exclude = ["rmpc", "crates/controls"] 10 + 11 + [profile.release] 12 + panic = "abort" 10 13 11 14 [workspace.package] 12 15 authors = ["Tsiry Sandratraina <tsiry.sndr@fluentci.io"]
+13
apps/buffering.c
··· 19 19 * 20 20 ****************************************************************************/ 21 21 #include "config.h" 22 + #include <stdio.h> 22 23 #include <string.h> 23 24 #include "system.h" 24 25 #include "storage.h" ··· 640 641 if (h->path[0] != '\0') 641 642 h->fd = stream_open(h->path, O_RDONLY); 642 643 644 + fprintf(stderr, "[buffering] buffer_handle: hid=%d type=%d path=%s -> fd=%d filesize=%lu end=%lu\n", 645 + handle_id, (int)h->type, h->path, h->fd, 646 + (unsigned long)h->filesize, (unsigned long)h->end); 647 + 643 648 if (h->fd == -1) { 644 649 /* could not open the file, truncate it where it is */ 645 650 h->filesize = h->end; ··· 692 697 /* rc is the actual amount read */ 693 698 ssize_t rc = stream_read(h->fd, ringbuf_ptr(widx), copy_n); 694 699 700 + if (h->end == 0) { 701 + /* Log the very first read result for each handle */ 702 + fprintf(stderr, "[buffering] buffer_handle: hid=%d type=%d first_read copy_n=%zd -> rc=%zd fd=%d\n", 703 + handle_id, (int)h->type, (ssize_t)copy_n, rc, h->fd); 704 + } 705 + 695 706 if (rc <= 0) { 696 707 /* Some kind of filesystem error, maybe recoverable if not codec */ 708 + fprintf(stderr, "[buffering] buffer_handle: hid=%d type=%d read FAILED rc=%zd end=%lu filesize=%lu\n", 709 + handle_id, (int)h->type, rc, (unsigned long)h->end, (unsigned long)h->filesize); 697 710 if (h->type == TYPE_CODEC) { 698 711 logf("Partial codec"); 699 712 break;
+55 -12
apps/playback.c
··· 47 47 #include "settings.h" 48 48 #include "audiohw.h" 49 49 #include "general.h" 50 + #include "streamfd.h" 50 51 #include <stdio.h> 51 52 52 53 #ifdef HAVE_TAGCACHE ··· 1756 1757 1757 1758 struct mp3entry *cur_id3 = valid_mp3entry(bufgetid3(info.id3_hid)); 1758 1759 1760 + fprintf(stderr, "[playback] audio_start_codec: id3_hid=%d cur_id3=%p\n", 1761 + info.id3_hid, (void*)cur_id3); 1762 + 1759 1763 if (!cur_id3) 1764 + { 1765 + fprintf(stderr, "[playback] audio_start_codec: ERROR no valid id3\n"); 1760 1766 return false; 1767 + } 1768 + 1769 + fprintf(stderr, "[playback] audio_start_codec: path=%s codectype=%u length=%lu elapsed=%lu audio_hid=%d\n", 1770 + cur_id3->path, cur_id3->codectype, cur_id3->length, cur_id3->elapsed, info.audio_hid); 1761 1771 1762 1772 buf_pin_handle(info.id3_hid, true); 1763 1773 1764 1774 if (!audio_init_codec(&info, cur_id3)) 1765 1775 { 1776 + fprintf(stderr, "[playback] audio_start_codec: ERROR audio_init_codec failed\n"); 1766 1777 buf_pin_handle(info.id3_hid, false); 1767 1778 return false; 1768 1779 } ··· 1831 1842 ci.filesize = buf_filesize(info.audio_hid); 1832 1843 buf_set_base_handle(info.audio_hid); 1833 1844 1845 + fprintf(stderr, "[playback] audio_start_codec: ci.audio_hid=%d ci.filesize=%lu calling codec_go()\n", 1846 + ci.audio_hid, (unsigned long)ci.filesize); 1834 1847 /* All required data is now available for the codec */ 1835 1848 codec_go(); 1836 1849 ··· 2147 2160 if (!path) 2148 2161 break; 2149 2162 2150 - /* Test for broken playlists by probing for the files */ 2151 - if (file_exists(path)) 2152 - { 2153 - fd = open(path, O_RDONLY); 2154 - if (fd >= 0) 2155 - break; 2156 - } 2163 + /* Test for broken playlists by probing for the file/stream. */ 2164 + fd = stream_open(path, O_RDONLY); 2165 + fprintf(stderr, "[playback] audio_load_track: stream_open(%s) -> fd=%d\n", path, fd); 2166 + if (fd != -1) 2167 + break; 2157 2168 logf("Open failed %s", path); 2158 2169 2159 2170 /* only skip if failed track has a successor in playlist */ ··· 2194 2205 if (filling == STATE_FULL || 2195 2206 (info.id3_hid = bufopen(path, 0, TYPE_ID3, NULL)) < 0) 2196 2207 { 2208 + fprintf(stderr, "[playback] audio_load_track: bufopen ID3 failed or buffer full (id3_hid=%d filling=%d)\n", 2209 + info.id3_hid, filling); 2197 2210 /* Buffer or track list is full */ 2198 2211 struct mp3entry *ub_id3; 2199 2212 ··· 2202 2215 /* Load the metadata for the first unbuffered track */ 2203 2216 ub_id3 = id3_get(UNBUFFERED_ID3); 2204 2217 2205 - if (fd >= 0) 2218 + if (fd != -1) 2206 2219 { 2207 2220 id3_mutex_lock(); 2208 2221 get_metadata(ub_id3, fd, path); ··· 2224 2237 { 2225 2238 track_list_free_info(&info); 2226 2239 track_list.in_progress_hid = 0; 2227 - if (fd >= 0) 2228 - close(fd); 2240 + if (fd != -1) 2241 + stream_close(fd); 2229 2242 return LOAD_TRACK_ERR_FAILED; 2230 2243 } 2231 2244 2232 2245 /* Successful load initiation */ 2233 2246 track_list.in_progress_hid = info.self_hid; 2234 2247 } 2235 - if (fd >= 0) 2236 - close(fd); 2248 + if (fd != -1) 2249 + stream_close(fd); 2237 2250 return LOAD_TRACK_OK; 2238 2251 } 2239 2252 ··· 2264 2277 2265 2278 struct mp3entry *track_id3 = valid_mp3entry(bufgetid3(infop->id3_hid)); 2266 2279 2280 + fprintf(stderr, "[playback] audio_finish_load_track: id3_hid=%d track_id3=%p\n", 2281 + infop->id3_hid, (void*)track_id3); 2282 + 2267 2283 if (!track_id3) 2268 2284 { 2269 2285 /* This is an error condition. Track cannot be played without valid 2270 2286 metadata; skip the track. */ 2271 2287 logf("No metadata"); 2288 + fprintf(stderr, "[playback] audio_finish_load_track: ERROR no valid metadata\n"); 2272 2289 trackstat = LOAD_TRACK_ERR_FINISH_FAILED; 2273 2290 goto audio_finish_load_track_exit; 2274 2291 } 2275 2292 2293 + fprintf(stderr, "[playback] audio_finish_load_track: path=%s codectype=%u length=%lu elapsed=%lu filesize=%lu first_frame_off=%lu\n", 2294 + track_id3->path, track_id3->codectype, track_id3->length, 2295 + track_id3->elapsed, track_id3->FS_PREFIX(filesize), 2296 + track_id3->first_frame_offset); 2297 + 2276 2298 struct track_info user_cur; 2277 2299 2278 2300 #ifdef HAVE_PLAY_FREQ 2279 2301 track_list_user_current(0, &user_cur); 2280 2302 bool is_current_user = infop->self_hid == user_cur.self_hid; 2303 + fprintf(stderr, "[playback] audio_finish_load_track: HAVE_PLAY_FREQ check is_current_user=%d\n", (int)is_current_user); 2281 2304 if (audio_auto_change_frequency(track_id3, is_current_user)) 2282 2305 { 2283 2306 // frequency switch requires full re-buffering, so stop buffering 2284 2307 filling = STATE_STOPPED; 2285 2308 logf("buffering stopped (current_track: %b, current_user: %b)", infop->self_hid == cur_info.self_hid, is_current_user); 2309 + fprintf(stderr, "[playback] audio_finish_load_track: HAVE_PLAY_FREQ triggered early exit\n"); 2286 2310 if (is_current_user) 2287 2311 // audio_finish_load_track_exit not needed as playback restart is already initiated 2288 2312 return trackstat; ··· 2291 2315 } 2292 2316 #endif 2293 2317 2318 + fprintf(stderr, "[playback] audio_finish_load_track: calling audio_load_cuesheet\n"); 2294 2319 /* Try to load a cuesheet for the track */ 2295 2320 if (!audio_load_cuesheet(infop, track_id3)) 2296 2321 { 2297 2322 /* No space for cuesheet on buffer, not an error */ 2298 2323 filling = STATE_FULL; 2324 + fprintf(stderr, "[playback] audio_finish_load_track: cuesheet buffer full -> exit\n"); 2299 2325 goto audio_finish_load_track_exit; 2300 2326 } 2327 + fprintf(stderr, "[playback] audio_finish_load_track: cuesheet OK\n"); 2301 2328 2302 2329 #ifdef HAVE_ALBUMART 2303 2330 /* Try to load album art for the track */ 2304 2331 int retval = audio_load_albumart(infop, track_id3, infop->self_hid == cur_info.self_hid); 2332 + fprintf(stderr, "[playback] audio_finish_load_track: albumart retval=%d\n", retval); 2305 2333 if (retval == ERR_BITMAP_TOO_LARGE) 2306 2334 { 2307 2335 /* No space for album art on buffer because the file is larger than the buffer. ··· 2310 2338 { 2311 2339 /* No space for album art on buffer, not an error */ 2312 2340 filling = STATE_FULL; 2341 + fprintf(stderr, "[playback] audio_finish_load_track: albumart buffer full -> exit\n"); 2313 2342 goto audio_finish_load_track_exit; 2314 2343 } 2315 2344 #endif ··· 2372 2401 } 2373 2402 2374 2403 logf("load track: %s", track_id3->path); 2404 + fprintf(stderr, "[playback] audio_finish_load_track: audiotype=%d file_offset(before)=%lld\n", 2405 + audiotype, (long long)file_offset); 2375 2406 2376 2407 if (file_offset > AUDIO_REBUFFER_GUESS_SIZE) 2377 2408 { ··· 2385 2416 file_offset = track_id3->first_frame_offset; 2386 2417 } 2387 2418 2419 + fprintf(stderr, "[playback] audio_finish_load_track: bufopen path=%s offset=%lld audiotype=%d\n", 2420 + track_id3->path, (long long)file_offset, audiotype); 2421 + 2388 2422 int hid = bufopen(track_id3->path, file_offset, audiotype, NULL); 2423 + fprintf(stderr, "[playback] audio_finish_load_track: bufopen -> hid=%d\n", hid); 2389 2424 2390 2425 if (hid >= 0) 2391 2426 { ··· 2427 2462 } 2428 2463 2429 2464 audio_finish_load_track_exit: 2465 + fprintf(stderr, "[playback] audio_finish_load_track: EXIT trackstat=%d filling=%d\n", trackstat, (int)filling); 2430 2466 if (trackstat >= LOAD_TRACK_OK && !track_info_sync(infop)) 2431 2467 { 2432 2468 logf("Track info sync failed"); ··· 2584 2620 { 2585 2621 struct track_info info, user_cur; 2586 2622 2623 + fprintf(stderr, "[playback] audio_on_finish_load_track: id3_hid=%d buf_is_handle=%d\n", 2624 + id3_hid, buf_is_handle(id3_hid)); 2625 + 2587 2626 if (!buf_is_handle(id3_hid) || !track_list_last(0, &info)) 2627 + { 2628 + fprintf(stderr, "[playback] audio_on_finish_load_track: early return (buf_is_handle=%d track_list_last=%d)\n", 2629 + buf_is_handle(id3_hid), track_list_last(0, &info)); 2588 2630 return; 2631 + } 2589 2632 2590 2633 track_list_user_current(1, &user_cur); 2591 2634 if (info.self_hid == user_cur.self_hid)
+28
apps/playlist.c
··· 564 564 565 565 src[len] = '\0'; 566 566 567 + /* HTTP(S) URLs are absolute — copy them as-is, no directory prepending. 568 + * Also normalise a spurious leading '/' (e.g. "/http://...") that may 569 + * have been stored in an older control file by a previous buggy build. */ 570 + { 571 + char *url_src = src; 572 + if (*url_src == '/' && 573 + (strncmp(url_src + 1, "http://", 7) == 0 || 574 + strncmp(url_src + 1, "https://", 8) == 0)) 575 + url_src++; 576 + if (strncmp(url_src, "http://", 7) == 0 || strncmp(url_src, "https://", 8) == 0) 577 + { 578 + strlcpy(dest, url_src, buf_length); 579 + return (ssize_t)strlen(dest); 580 + } 581 + } 582 + 567 583 /* Replace backslashes with forward slashes */ 568 584 path_correct_separators(src, src); 569 585 ··· 3023 3039 return NULL; 3024 3040 3025 3041 temp_ptr = buf; 3042 + 3043 + /* HTTP(S) URLs don't live on the filesystem — return as-is. 3044 + * Normalise a spurious leading '/' (e.g. "/http://...") from stale data. */ 3045 + { 3046 + char *url_ptr = buf; 3047 + if (*url_ptr == '/' && 3048 + (strncmp(url_ptr + 1, "http://", 7) == 0 || 3049 + strncmp(url_ptr + 1, "https://", 8) == 0)) 3050 + url_ptr++; 3051 + if (strncmp(url_ptr, "http://", 7) == 0 || strncmp(url_ptr, "https://", 8) == 0) 3052 + return url_ptr; 3053 + } 3026 3054 3027 3055 /* remove bogus dirs from beginning of path 3028 3056 (workaround for buggy playlist creation tools) */
+31 -11
apps/streamfd.c
··· 15 15 #include <string.h> 16 16 #include <stdint.h> 17 17 #include <fcntl.h> 18 + #include <stdio.h> 18 19 19 20 /* ------------------------------------------------------------------ 20 21 * C declarations for the Rust ABI exported by crates/netstream. ··· 23 24 extern int64_t rb_net_read (int32_t h, void *dst, size_t n); 24 25 extern int64_t rb_net_lseek (int32_t h, int64_t off, int32_t whence); 25 26 extern int64_t rb_net_len (int32_t h); 27 + extern int64_t rb_net_content_type(int32_t h, char *dst, size_t n); 26 28 extern void rb_net_close (int32_t h); 27 29 28 30 /* ------------------------------------------------------------------ */ ··· 48 50 49 51 if (path_is_url(path)) { 50 52 int32_t h = rb_net_open(path); 53 + fprintf(stderr, "[streamfd] stream_open(URL): url=%s handle=%d\n", path, (int)h); 51 54 if (h < 0) 52 55 return -1; 53 - /* Map handle 0 -> -1000, handle 1 -> -1001, etc. */ 54 - return STREAM_HTTP_FD_BASE - (int)h; 56 + int fd = STREAM_HTTP_FD_BASE - (int)h; 57 + fprintf(stderr, "[streamfd] stream_open: url=%s -> http_fd=%d\n", path, fd); 58 + return fd; 55 59 } 56 60 57 - return open(path, flags); 61 + int fd = open(path, flags); 62 + fprintf(stderr, "[streamfd] stream_open(file): path=%s -> fd=%d\n", path, fd); 63 + return fd; 58 64 } 59 65 60 66 ssize_t stream_read(int fd, void *buf, size_t n) 61 67 { 62 68 if (stream_is_http_fd(fd)) { 63 69 int64_t r = rb_net_read(http_fd_to_handle(fd), buf, n); 70 + fprintf(stderr, "[streamfd] stream_read: http_fd=%d n=%zu -> %lld\n", fd, n, (long long)r); 64 71 return (ssize_t)r; 65 72 } 66 - return read(fd, buf, n); 73 + ssize_t r = read(fd, buf, n); 74 + return r; 67 75 } 68 76 69 77 off_t stream_lseek(int fd, off_t off, int whence) 70 78 { 71 79 if (stream_is_http_fd(fd)) { 72 80 int64_t r = rb_net_lseek(http_fd_to_handle(fd), (int64_t)off, whence); 81 + fprintf(stderr, "[streamfd] stream_lseek: http_fd=%d off=%lld whence=%d -> %lld\n", 82 + fd, (long long)off, whence, (long long)r); 73 83 return (off_t)r; 74 84 } 75 85 return lseek(fd, off, whence); ··· 80 90 if (fd == -1) 81 91 return 0; 82 92 if (stream_is_http_fd(fd)) { 93 + fprintf(stderr, "[streamfd] stream_close: http_fd=%d (handle=%d)\n", 94 + fd, http_fd_to_handle(fd)); 83 95 rb_net_close(http_fd_to_handle(fd)); 84 96 return 0; 85 97 } ··· 90 102 { 91 103 if (stream_is_http_fd(fd)) { 92 104 int64_t len = rb_net_len(http_fd_to_handle(fd)); 105 + off_t result; 93 106 if (len < 0) { 94 - /* 95 - * Content-Length unknown: return a large sentinel (~2 GiB). 96 - * The buffering layer truncates h->filesize to h->end when 97 - * read() returns 0 (EOF), so this is safe for finite streams. 98 - */ 99 - return (off_t)0x7FFFFFFF; 107 + result = (off_t)0x7FFFFFFF; 108 + } else { 109 + result = (off_t)len; 100 110 } 101 - return (off_t)len; 111 + fprintf(stderr, "[streamfd] stream_filesize_fd: http_fd=%d -> %lld (raw_len=%lld)\n", 112 + fd, (long long)result, (long long)len); 113 + return result; 102 114 } 103 115 return filesize(fd); 116 + } 117 + 118 + ssize_t stream_content_type(int fd, char *buf, size_t n) 119 + { 120 + if (!stream_is_http_fd(fd)) 121 + return -1; 122 + 123 + return (ssize_t)rb_net_content_type(http_fd_to_handle(fd), buf, n); 104 124 } 105 125 106 126 #endif /* SIMULATOR || APPLICATION */
+11
apps/streamfd.h
··· 85 85 */ 86 86 off_t stream_filesize_fd(int fd); 87 87 88 + /** 89 + * Copy the normalized Content-Type associated with @p fd into @p buf. 90 + * 91 + * For HTTP streams this returns the response Content-Type without parameters. 92 + * For regular files this returns -1. 93 + * 94 + * @return Full string length on success, or -1 if unknown/unavailable. 95 + */ 96 + ssize_t stream_content_type(int fd, char *buf, size_t n); 97 + 88 98 #else /* !STREAM_HTTP_ENABLED */ 89 99 90 100 /* ··· 100 110 #define stream_lseek(fd, off, whence) lseek((fd), (off), (whence)) 101 111 #define stream_close(fd) close(fd) 102 112 #define stream_filesize_fd(fd) filesize(fd) 113 + #define stream_content_type(fd, buf, n) ((ssize_t)-1) 103 114 104 115 #endif /* STREAM_HTTP_ENABLED */ 105 116
+3
cli/build.rs
··· 1 1 fn main() -> Result<(), Box<dyn std::error::Error>> { 2 + // Re-link whenever the C/Zig static library changes. 3 + println!("cargo:rerun-if-changed=../build-lib/librockbox.a"); 4 + 2 5 tonic_build::configure() 3 6 .out_dir("src/api") 4 7 .file_descriptor_set_path("src/api/rockbox_descriptor.bin")
+1
crates/graphql/src/server.rs
··· 66 66 67 67 async fn index_file(req: HttpRequest) -> Result<NamedFile, actix_web::Error> { 68 68 let id = req.match_info().get("id").unwrap(); 69 + println!("{}", id); 69 70 let id = id.split('.').next().unwrap(); 70 71 let mut path = PathBuf::new(); 71 72
-3
crates/netstream/Cargo.toml
··· 1 - [workspace] 2 - members = ["."] 3 - 4 1 [package] 5 2 name = "netstream" 6 3 version = "0.1.0"
+401 -83
crates/netstream/src/lib.rs
··· 1 1 use once_cell::sync::Lazy; 2 2 use std::collections::HashMap; 3 3 use std::ffi::CStr; 4 - use std::io::Read; 4 + use std::io::{self, Read}; 5 5 use std::os::raw::c_char; 6 6 use std::sync::atomic::{AtomicI32, Ordering}; 7 - use std::sync::Mutex; 7 + use std::sync::{Arc, Mutex}; 8 8 9 9 /// Sentinel handle ID returned on error. 10 10 const INVALID_HANDLE: i32 = -1; ··· 14 14 url: String, 15 15 pos: u64, 16 16 content_length: Option<u64>, 17 + content_type: Option<String>, 17 18 response: Option<reqwest::blocking::Response>, 18 - client: reqwest::blocking::Client, 19 19 } 20 20 21 21 impl StreamState { 22 - fn new(url: String, client: reqwest::blocking::Client) -> Option<Self> { 23 - let response = client.get(&url).send().ok()?; 22 + fn response_content_type(resp: &reqwest::blocking::Response) -> Option<String> { 23 + let value = resp 24 + .headers() 25 + .get(reqwest::header::CONTENT_TYPE)? 26 + .to_str() 27 + .ok()?; 28 + let value = value.split(';').next()?.trim(); 29 + if value.is_empty() { 30 + None 31 + } else { 32 + Some(value.to_ascii_lowercase()) 33 + } 34 + } 35 + 36 + fn new(url: String) -> Option<Self> { 37 + let response = CLIENT.get(&url).send().ok()?; 24 38 if !response.status().is_success() { 25 39 return None; 26 40 } 27 41 let content_length = response.content_length(); 42 + let content_type = Self::response_content_type(&response); 28 43 Some(StreamState { 29 44 url, 30 45 pos: 0, 31 46 content_length, 47 + content_type, 32 48 response: Some(response), 33 - client, 34 49 }) 35 50 } 36 51 52 + fn skip_bytes(resp: &mut reqwest::blocking::Response, mut to_skip: u64) -> bool { 53 + let mut buf = [0u8; 8192]; 54 + 55 + while to_skip > 0 { 56 + let chunk = usize::min(to_skip as usize, buf.len()); 57 + match resp.read(&mut buf[..chunk]) { 58 + Ok(0) => return false, 59 + Ok(bytes_read) => to_skip -= bytes_read as u64, 60 + Err(_) => return false, 61 + } 62 + } 63 + 64 + true 65 + } 66 + 67 + fn update_content_length_from_content_range(&mut self, resp: &reqwest::blocking::Response) { 68 + if self.content_length.is_some() { 69 + return; 70 + } 71 + 72 + if let Some(cr) = resp.headers().get("content-range") { 73 + if let Ok(cr_str) = cr.to_str() { 74 + if let Some(total_str) = cr_str.split('/').last() { 75 + if let Ok(total) = total_str.trim().parse::<u64>() { 76 + self.content_length = Some(total); 77 + } 78 + } 79 + } 80 + } 81 + } 82 + 83 + fn parse_content_range_start(resp: &reqwest::blocking::Response) -> Option<u64> { 84 + let value = resp.headers().get("content-range")?.to_str().ok()?; 85 + let value = value.strip_prefix("bytes ")?; 86 + let (range, _) = value.split_once('/')?; 87 + let (start, _) = range.split_once('-')?; 88 + start.trim().parse::<u64>().ok() 89 + } 90 + 37 91 /// Re-issue the request starting at `new_pos` using an HTTP Range header. 38 - /// Returns `true` on success, `false` if the server doesn't support Range 39 - /// or if the request fails. 92 + /// Falls back to reopening from byte 0 and discarding bytes if the server 93 + /// ignores Range and responds with the full body. 40 94 fn seek_to(&mut self, new_pos: u64) -> bool { 41 95 self.response = None; 42 - let result = self 43 - .client 96 + let result = CLIENT 44 97 .get(&self.url) 45 98 .header("Range", format!("bytes={}-", new_pos)) 46 99 .send(); 47 100 48 101 match result { 49 - Ok(resp) 50 - if resp.status().is_success() || resp.status().as_u16() == 206 => 51 - { 52 - // Try to extract total length from Content-Range if not yet known. 102 + Ok(resp) if resp.status().as_u16() == 206 => { 103 + self.update_content_length_from_content_range(&resp); 104 + if self.content_type.is_none() { 105 + self.content_type = Self::response_content_type(&resp); 106 + } 107 + 108 + if Self::parse_content_range_start(&resp) != Some(new_pos) { 109 + return false; 110 + } 111 + 112 + self.response = Some(resp); 113 + self.pos = new_pos; 114 + true 115 + } 116 + Ok(mut resp) if resp.status().is_success() => { 53 117 if self.content_length.is_none() { 54 - if let Some(cr) = resp.headers().get("content-range") { 55 - if let Ok(cr_str) = cr.to_str() { 56 - // Format: "bytes START-END/TOTAL" 57 - if let Some(total_str) = cr_str.split('/').last() { 58 - if let Ok(total) = total_str.trim().parse::<u64>() { 59 - self.content_length = Some(total); 60 - } 61 - } 62 - } 63 - } 118 + self.content_length = resp.content_length(); 119 + } 120 + if self.content_type.is_none() { 121 + self.content_type = Self::response_content_type(&resp); 64 122 } 123 + 124 + if new_pos > 0 && !Self::skip_bytes(&mut resp, new_pos) { 125 + return false; 126 + } 127 + 65 128 self.response = Some(resp); 66 129 self.pos = new_pos; 67 130 true ··· 71 134 } 72 135 } 73 136 74 - static STREAMS: Lazy<Mutex<HashMap<i32, StreamState>>> = 137 + fn read_as_file<R: Read>(reader: &mut R, buf: &mut [u8]) -> io::Result<usize> { 138 + let mut total = 0; 139 + 140 + while total < buf.len() { 141 + match reader.read(&mut buf[total..]) { 142 + Ok(0) => break, 143 + Ok(bytes_read) => total += bytes_read, 144 + Err(err) if err.kind() == io::ErrorKind::Interrupted => continue, 145 + Err(err) => { 146 + if total > 0 { 147 + break; 148 + } 149 + return Err(err); 150 + } 151 + } 152 + } 153 + 154 + Ok(total) 155 + } 156 + 157 + static STREAMS: Lazy<Mutex<HashMap<i32, Arc<Mutex<StreamState>>>>> = 75 158 Lazy::new(|| Mutex::new(HashMap::new())); 76 159 160 + static CLIENT: Lazy<reqwest::blocking::Client> = Lazy::new(|| { 161 + reqwest::blocking::Client::builder() 162 + .use_rustls_tls() 163 + .build() 164 + .expect("failed to build global HTTP client") 165 + }); 166 + 77 167 static NEXT_HANDLE: AtomicI32 = AtomicI32::new(0); 78 168 79 169 // ------------------------------------------------------------------ ··· 94 184 Err(_) => return INVALID_HANDLE, 95 185 }; 96 186 97 - let client = match reqwest::blocking::Client::builder() 98 - .use_rustls_tls() 99 - .build() 100 - { 101 - Ok(c) => c, 102 - Err(_) => return INVALID_HANDLE, 103 - }; 104 - 105 - let state = match StreamState::new(url_str, client) { 106 - Some(s) => s, 107 - None => return INVALID_HANDLE, 187 + let state = match StreamState::new(url_str.clone()) { 188 + Some(s) => { 189 + eprintln!( 190 + "[netstream] rb_net_open: url={} content_length={:?} content_type={:?}", 191 + url_str, s.content_length, s.content_type 192 + ); 193 + s 194 + } 195 + None => { 196 + eprintln!("[netstream] rb_net_open: FAILED url={}", url_str); 197 + return INVALID_HANDLE; 198 + } 108 199 }; 109 200 110 201 let handle = NEXT_HANDLE.fetch_add(1, Ordering::SeqCst); 111 - STREAMS.lock().unwrap().insert(handle, state); 202 + STREAMS 203 + .lock() 204 + .unwrap() 205 + .insert(handle, Arc::new(Mutex::new(state))); 206 + eprintln!( 207 + "[netstream] rb_net_open: url={} -> handle={}", 208 + url_str, handle 209 + ); 112 210 handle 113 211 } 114 212 ··· 122 220 if dst.is_null() || n == 0 { 123 221 return 0; 124 222 } 125 - let mut streams = STREAMS.lock().unwrap(); 126 - let state = match streams.get_mut(&h) { 127 - Some(s) => s, 128 - None => return -1, 223 + // Acquire the global map lock only long enough to clone the per-handle Arc, 224 + // then release it so other handles can proceed concurrently. 225 + let handle_arc = { 226 + let streams = STREAMS.lock().unwrap(); 227 + match streams.get(&h) { 228 + Some(arc) => arc.clone(), 229 + None => return -1, 230 + } 129 231 }; 232 + let mut state = handle_arc.lock().unwrap(); 233 + let pos_before = state.pos; 130 234 let resp = match &mut state.response { 131 235 Some(r) => r, 132 236 None => return -1, 133 237 }; 134 238 let buf = std::slice::from_raw_parts_mut(dst as *mut u8, n); 135 - match resp.read(buf) { 239 + match read_as_file(resp, buf) { 136 240 Ok(bytes_read) => { 137 241 state.pos += bytes_read as u64; 242 + eprintln!( 243 + "[netstream] rb_net_read: h={} n={} pos_before={} -> read={} pos_after={}", 244 + h, n, pos_before, bytes_read, state.pos 245 + ); 138 246 bytes_read as i64 139 247 } 140 - Err(_) => -1, 248 + Err(e) => { 249 + eprintln!( 250 + "[netstream] rb_net_read: h={} n={} pos={} -> ERROR {:?}", 251 + h, n, pos_before, e 252 + ); 253 + -1 254 + } 141 255 } 142 256 } 143 257 ··· 150 264 const SEEK_CUR: libc::c_int = 1; 151 265 const SEEK_END: libc::c_int = 2; 152 266 153 - let mut streams = STREAMS.lock().unwrap(); 154 - let state = match streams.get_mut(&h) { 155 - Some(s) => s, 156 - None => return -1, 267 + // Acquire the global map lock only long enough to clone the per-handle Arc, 268 + // then release it so other handles can proceed concurrently. 269 + let handle_arc = { 270 + let streams = STREAMS.lock().unwrap(); 271 + match streams.get(&h) { 272 + Some(arc) => arc.clone(), 273 + None => return -1, 274 + } 157 275 }; 276 + let mut state = handle_arc.lock().unwrap(); 158 277 159 278 let new_pos: u64 = match whence { 160 279 x if x == SEEK_SET => { ··· 193 312 194 313 // Fast-path: already there (no need to restart the request). 195 314 if new_pos == state.pos { 315 + eprintln!( 316 + "[netstream] rb_net_lseek: h={} off={} whence={} -> already at pos={} (no-op)", 317 + h, off, whence, state.pos 318 + ); 196 319 return state.pos as i64; 197 320 } 198 321 322 + let old_pos = state.pos; 199 323 if state.seek_to(new_pos) { 324 + eprintln!( 325 + "[netstream] rb_net_lseek: h={} off={} whence={} old_pos={} -> new_pos={}", 326 + h, off, whence, old_pos, state.pos 327 + ); 200 328 state.pos as i64 201 329 } else { 330 + eprintln!( 331 + "[netstream] rb_net_lseek: h={} off={} whence={} old_pos={} -> FAILED", 332 + h, off, whence, old_pos 333 + ); 202 334 -1 203 335 } 204 336 } ··· 206 338 /// Return the total content length of stream `h`, or -1 if unknown. 207 339 #[no_mangle] 208 340 pub extern "C" fn rb_net_len(h: i32) -> i64 { 209 - let streams = STREAMS.lock().unwrap(); 210 - match streams.get(&h) { 211 - Some(state) => state.content_length.map(|l| l as i64).unwrap_or(-1), 212 - None => -1, 341 + let handle_arc = { 342 + let streams = STREAMS.lock().unwrap(); 343 + match streams.get(&h) { 344 + Some(arc) => arc.clone(), 345 + None => return -1, 346 + } 347 + }; 348 + let len = handle_arc 349 + .lock() 350 + .unwrap() 351 + .content_length 352 + .map(|l| l as i64) 353 + .unwrap_or(-1); 354 + eprintln!("[netstream] rb_net_len: h={} -> {}", h, len); 355 + len 356 + } 357 + 358 + /// Copy the normalized Content-Type for stream `h` into `dst`. 359 + /// Returns the full string length on success, or -1 if unavailable. 360 + /// 361 + /// # Safety 362 + /// `dst` must point to a writable buffer of at least `n` bytes when `n > 0`. 363 + #[no_mangle] 364 + pub unsafe extern "C" fn rb_net_content_type(h: i32, dst: *mut c_char, n: libc::size_t) -> i64 { 365 + let handle_arc = { 366 + let streams = STREAMS.lock().unwrap(); 367 + match streams.get(&h) { 368 + Some(arc) => arc.clone(), 369 + None => return -1, 370 + } 371 + }; 372 + let state = handle_arc.lock().unwrap(); 373 + let content_type = match state.content_type.as_deref() { 374 + Some(value) => value, 375 + None => return -1, 376 + }; 377 + 378 + if !dst.is_null() && n > 0 { 379 + let bytes = content_type.as_bytes(); 380 + let copy_len = usize::min(bytes.len(), n.saturating_sub(1)); 381 + std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst as *mut u8, copy_len); 382 + *dst.add(copy_len) = 0; 213 383 } 384 + 385 + content_type.len() as i64 214 386 } 215 387 216 388 /// Close stream `h` and release its resources. 217 389 #[no_mangle] 218 390 pub extern "C" fn rb_net_close(h: i32) { 391 + eprintln!("[netstream] rb_net_close: h={}", h); 219 392 STREAMS.lock().unwrap().remove(&h); 220 393 } 221 394 ··· 225 398 #[cfg(test)] 226 399 mod tests { 227 400 use super::*; 228 - use std::ffi::CString; 229 401 use mockito::Matcher; 402 + use std::ffi::CString; 403 + use std::io::Cursor; 230 404 231 405 /// Helper: build a NUL-terminated C URL string for a path on the mock server. 232 406 fn c_url(server: &mockito::Server, path: &str) -> CString { 233 407 CString::new(format!("{}{}", server.url(), path)).unwrap() 234 408 } 235 409 410 + struct PartialReader { 411 + inner: Cursor<Vec<u8>>, 412 + chunk_size: usize, 413 + } 414 + 415 + impl PartialReader { 416 + fn new(data: &[u8], chunk_size: usize) -> Self { 417 + Self { 418 + inner: Cursor::new(data.to_vec()), 419 + chunk_size, 420 + } 421 + } 422 + } 423 + 424 + impl Read for PartialReader { 425 + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { 426 + let chunk = usize::min(self.chunk_size, buf.len()); 427 + self.inner.read(&mut buf[..chunk]) 428 + } 429 + } 430 + 236 431 // ------------------------------------------------------------------ 237 432 // Open / close 238 433 // ------------------------------------------------------------------ ··· 254 449 255 450 rb_net_close(handle); 256 451 // After close, rb_net_len should return -1 (unknown handle). 257 - assert_eq!(rb_net_len(handle), -1, "closed handle should return -1 from rb_net_len"); 452 + assert_eq!( 453 + rb_net_len(handle), 454 + -1, 455 + "closed handle should return -1 from rb_net_len" 456 + ); 258 457 } 259 458 260 459 /// Passing a null pointer returns INVALID_HANDLE. ··· 270 469 // Port 19998 is extremely unlikely to be in use. 271 470 let url = CString::new("http://127.0.0.1:19998/file.mp3").unwrap(); 272 471 let handle = unsafe { rb_net_open(url.as_ptr()) }; 273 - assert_eq!(handle, INVALID_HANDLE, "unreachable server should return INVALID_HANDLE"); 472 + assert_eq!( 473 + handle, INVALID_HANDLE, 474 + "unreachable server should return INVALID_HANDLE" 475 + ); 274 476 } 275 477 276 478 /// A 404 response causes rb_net_open to return INVALID_HANDLE. 277 479 #[test] 278 480 fn test_open_404() { 279 481 let mut server = mockito::Server::new(); 280 - let _mock = server 281 - .mock("GET", "/missing.mp3") 282 - .with_status(404) 283 - .create(); 482 + let _mock = server.mock("GET", "/missing.mp3").with_status(404).create(); 284 483 285 484 let url = c_url(&server, "/missing.mp3"); 286 485 let handle = unsafe { rb_net_open(url.as_ptr()) }; 287 - assert_eq!(handle, INVALID_HANDLE, "404 response should return INVALID_HANDLE"); 486 + assert_eq!( 487 + handle, INVALID_HANDLE, 488 + "404 response should return INVALID_HANDLE" 489 + ); 288 490 } 289 491 290 492 // ------------------------------------------------------------------ ··· 311 513 rb_net_close(handle); 312 514 } 313 515 516 + #[test] 517 + fn test_content_type_is_available() { 518 + let mut server = mockito::Server::new(); 519 + let _mock = server 520 + .mock("GET", "/typed") 521 + .with_status(200) 522 + .with_header("content-type", "audio/m4a; charset=binary") 523 + .with_body("data") 524 + .create(); 525 + 526 + let url = c_url(&server, "/typed"); 527 + let handle = unsafe { rb_net_open(url.as_ptr()) }; 528 + assert!(handle >= 0); 529 + 530 + let mut buf = vec![0i8; 32]; 531 + let n = unsafe { rb_net_content_type(handle, buf.as_mut_ptr(), buf.len()) }; 532 + assert_eq!(n, "audio/m4a".len() as i64); 533 + 534 + let content_type = unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(); 535 + assert_eq!(content_type, "audio/m4a"); 536 + 537 + rb_net_close(handle); 538 + } 539 + 314 540 /// rb_net_len returns the value from the Content-Length response header. 315 541 #[test] 316 542 fn test_unknown_content_length() { ··· 331 557 332 558 // mockito sets content-length = body.len() when no explicit header is given. 333 559 let len = rb_net_len(handle); 334 - assert_eq!(len, body.len() as i64, "rb_net_len should reflect the server's content-length"); 560 + assert_eq!( 561 + len, 562 + body.len() as i64, 563 + "rb_net_len should reflect the server's content-length" 564 + ); 335 565 336 566 rb_net_close(handle); 337 567 } ··· 356 586 assert!(handle >= 0); 357 587 358 588 let mut buf = vec![0u8; 64]; 359 - let n = unsafe { 360 - rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) 361 - }; 589 + let n = unsafe { rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) }; 362 590 assert_eq!(n, body.len() as i64); 363 591 assert_eq!(&buf[..n as usize], body); 364 592 365 593 rb_net_close(handle); 366 594 } 367 595 596 + #[test] 597 + fn test_read_as_file_retries_partial_reads() { 598 + let mut reader = PartialReader::new(b"Hello, Rockbox!", 3); 599 + let mut buf = vec![0u8; 15]; 600 + 601 + let n = read_as_file(&mut reader, &mut buf).unwrap(); 602 + 603 + assert_eq!(n, 15); 604 + assert_eq!(&buf, b"Hello, Rockbox!"); 605 + } 606 + 368 607 /// rb_net_read returns 0 at EOF (after all bytes have been consumed). 369 608 #[test] 370 609 fn test_read_eof() { ··· 382 621 383 622 let mut buf = vec![0u8; 1024]; 384 623 // First read drains the body. 385 - let n1 = unsafe { 386 - rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) 387 - }; 624 + let n1 = unsafe { rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) }; 388 625 assert_eq!(n1, body.len() as i64); 389 626 390 627 // Second read should signal EOF. 391 - let n2 = unsafe { 392 - rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) 393 - }; 628 + let n2 = unsafe { rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) }; 394 629 assert_eq!(n2, 0, "second read should return 0 at EOF"); 395 630 396 631 rb_net_close(handle); ··· 400 635 #[test] 401 636 fn test_read_invalid_handle() { 402 637 let mut buf = vec![0u8; 16]; 403 - let result = unsafe { 404 - rb_net_read(i32::MAX, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) 405 - }; 638 + let result = 639 + unsafe { rb_net_read(i32::MAX, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) }; 406 640 assert_eq!(result, -1, "read on unknown handle should return -1"); 407 641 } 408 642 ··· 444 678 445 679 // Read the remaining 8 bytes and verify they match the tail of full_body. 446 680 let mut buf = vec![0u8; 16]; 447 - let n = unsafe { 448 - rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) 449 - }; 681 + let n = unsafe { rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) }; 450 682 assert_eq!(n, 8); 451 683 assert_eq!(&buf[..8], &full_body[8..]); 452 684 ··· 471 703 472 704 // Read 5 bytes → position advances to 5. 473 705 let mut buf = vec![0u8; 5]; 474 - let n = unsafe { 475 - rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, 5) 476 - }; 706 + let n = unsafe { rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, 5) }; 477 707 assert_eq!(n, 5); 478 708 479 709 // SEEK_CUR(0) should return current position without a new HTTP request. ··· 510 740 assert!(handle >= 0); 511 741 512 742 let pos = rb_net_lseek(handle, -2, libc::SEEK_END); 513 - assert_eq!(pos, 8, "SEEK_END(-2) on 10-byte file should give position 8"); 743 + assert_eq!( 744 + pos, 8, 745 + "SEEK_END(-2) on 10-byte file should give position 8" 746 + ); 514 747 515 748 rb_net_close(handle); 516 749 } ··· 543 776 544 777 // Seeking past the beginning is invalid: SEEK_END with |offset| > length. 545 778 let pos = rb_net_lseek(handle, -100, libc::SEEK_END); 546 - assert_eq!(pos, -1, "SEEK_END with offset beyond file start should return -1"); 779 + assert_eq!( 780 + pos, -1, 781 + "SEEK_END with offset beyond file start should return -1" 782 + ); 547 783 548 784 rb_net_close(handle); 549 785 } ··· 574 810 assert!(handle >= 0); 575 811 576 812 let result = rb_net_lseek(handle, 50, libc::SEEK_SET); 577 - assert_eq!(result, -1, "seek should fail gracefully when Range is not supported"); 813 + assert_eq!( 814 + result, -1, 815 + "seek should fail gracefully when Range is not supported" 816 + ); 817 + 818 + rb_net_close(handle); 819 + } 820 + 821 + /// If the server ignores Range and returns 200 with the full body, seek 822 + /// still succeeds by discarding bytes until the requested position. 823 + #[test] 824 + fn test_seek_falls_back_when_range_is_ignored() { 825 + let full_body: &[u8] = b"0123456789ABCDEF"; 826 + let mut server = mockito::Server::new(); 827 + 828 + let _initial = server 829 + .mock("GET", "/ignore-range.mp3") 830 + .match_header("range", Matcher::Missing) 831 + .with_status(200) 832 + .with_header("content-length", "16") 833 + .with_body(full_body) 834 + .create(); 835 + 836 + let _ignored_range = server 837 + .mock("GET", "/ignore-range.mp3") 838 + .match_header("range", "bytes=8-") 839 + .with_status(200) 840 + .with_header("content-length", "16") 841 + .with_body(full_body) 842 + .create(); 843 + 844 + let url = c_url(&server, "/ignore-range.mp3"); 845 + let handle = unsafe { rb_net_open(url.as_ptr()) }; 846 + assert!(handle >= 0); 847 + 848 + let new_pos = rb_net_lseek(handle, 8, libc::SEEK_SET); 849 + assert_eq!(new_pos, 8, "seek should land at byte 8 even without 206"); 850 + 851 + let mut buf = vec![0u8; 16]; 852 + let n = unsafe { rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) }; 853 + assert_eq!(n, 8); 854 + assert_eq!(&buf[..8], &full_body[8..]); 855 + 856 + rb_net_close(handle); 857 + } 858 + 859 + /// A malformed 206 response that does not start at the requested offset 860 + /// must fail instead of silently desynchronizing the stream position. 861 + #[test] 862 + fn test_seek_rejects_wrong_content_range_start() { 863 + let full_body: &[u8] = b"0123456789ABCDEF"; 864 + let mut server = mockito::Server::new(); 865 + 866 + let _initial = server 867 + .mock("GET", "/bad-range.mp3") 868 + .match_header("range", Matcher::Missing) 869 + .with_status(200) 870 + .with_header("content-length", "16") 871 + .with_body(full_body) 872 + .create(); 873 + 874 + let _bad_range = server 875 + .mock("GET", "/bad-range.mp3") 876 + .match_header("range", "bytes=8-") 877 + .with_status(206) 878 + .with_header("content-range", "bytes 0-7/16") 879 + .with_body(&full_body[..8]) 880 + .create(); 881 + 882 + let url = c_url(&server, "/bad-range.mp3"); 883 + let handle = unsafe { rb_net_open(url.as_ptr()) }; 884 + assert!(handle >= 0); 885 + 886 + let result = rb_net_lseek(handle, 8, libc::SEEK_SET); 887 + assert_eq!(result, -1, "mismatched Content-Range should fail"); 578 888 579 889 rb_net_close(handle); 580 890 } ··· 610 920 assert!(handle >= 0); 611 921 612 922 // Total length is known from the initial response. 613 - assert_eq!(rb_net_len(handle), 10, "length should be known from initial response"); 923 + assert_eq!( 924 + rb_net_len(handle), 925 + 10, 926 + "length should be known from initial response" 927 + ); 614 928 615 929 // Seeking causes a 206 response whose Content-Range also confirms the total length. 616 930 let pos = rb_net_lseek(handle, 5, libc::SEEK_SET); 617 931 assert_eq!(pos, 5, "seek should succeed"); 618 932 619 933 // Length is still correctly reported after seek. 620 - assert_eq!(rb_net_len(handle), 10, "length should remain correct after seek"); 934 + assert_eq!( 935 + rb_net_len(handle), 936 + 10, 937 + "length should remain correct after seek" 938 + ); 621 939 622 940 rb_net_close(handle); 623 941 }
+1
crates/network/src/lib.rs
··· 59 59 "audio/ogg" => Ok("ogg"), 60 60 "audio/flac" => Ok("flac"), 61 61 "audio/x-m4a" => Ok("m4a"), 62 + "audio/m4a" => Ok("m4a"), 62 63 "audio/aac" => Ok("aac"), 63 64 "video/mp4" => Ok("mp4"), 64 65 "audio/wav" => Ok("wav"),
+1
crates/server/Cargo.toml
··· 28 28 rockbox-rpc = {path = "../rpc"} 29 29 rockbox-search = {path = "../search"} 30 30 rockbox-settings = {path = "../settings"} 31 + netstream = { path = "../netstream" } 31 32 rockbox-sys = {path = "../sys"} 32 33 rockbox-tracklist = {path = "../tracklist"} 33 34 rockbox-traits = {path = "../traits"}
+4 -12
crates/server/src/handlers/playlists.rs
··· 7 7 use rand::seq::SliceRandom; 8 8 use rockbox_graphql::read_files; 9 9 use rockbox_library::repo; 10 - use rockbox_network::download_tracks; 11 10 use rockbox_sys::{ 12 11 self as rb, 13 12 types::{playlist_amount::PlaylistAmount, playlist_info::PlaylistInfo}, ··· 27 26 return Ok(()); 28 27 } 29 28 let body = req.body.as_ref().unwrap(); 30 - let mut new_playlist: NewPlaylist = serde_json::from_str(body).unwrap(); 29 + let new_playlist: NewPlaylist = serde_json::from_str(body).unwrap(); 31 30 32 31 if new_playlist.tracks.is_empty() { 33 32 return Ok(()); 34 33 } 35 34 36 - new_playlist.tracks = download_tracks(new_playlist.tracks).await?; 37 - 38 - let dir = new_playlist.tracks[0].clone(); 39 - let dir_parts: Vec<_> = dir.split('/').collect(); 40 - let dir = dir_parts[0..dir_parts.len() - 1].join("/"); 41 - let status = rb::playlist::create(&dir, None); 42 - if status == -1 { 43 - res.set_status(500); 44 - return Ok(()); 45 - } 35 + // URLs are passed as-is; codec detection happens in the C metadata layer 36 + // via probe_content_type_format(), which reads the HTTP Content-Type header 37 + // and overrides any extension-based guess. 46 38 let start_index = rb::playlist::build_playlist( 47 39 new_playlist.tracks.iter().map(|t| t.as_str()).collect(), 48 40 0,
+40 -17
crates/server/src/lib.rs
··· 26 26 pub mod player_events; 27 27 pub mod scan; 28 28 29 + // Force netstream FFI symbols into the staticlib output. 30 + // These functions are called from C code (streamfd.c) but not from any Rust 31 + // code, so rustc would otherwise drop the entire crate from librockbox_server.a. 32 + #[allow(dead_code)] 33 + mod _netstream { 34 + use rbnetstream::{rb_net_close, rb_net_len, rb_net_lseek, rb_net_open, rb_net_read}; 35 + use std::ffi::{c_char, c_void}; 36 + #[used] 37 + static FN_OPEN: unsafe extern "C" fn(*const c_char) -> i32 = rb_net_open; 38 + #[used] 39 + static FN_READ: unsafe extern "C" fn(i32, *mut c_void, usize) -> i64 = rb_net_read; 40 + #[used] 41 + static FN_LSEEK: extern "C" fn(i32, i64, i32) -> i64 = rb_net_lseek; 42 + #[used] 43 + static FN_LEN: extern "C" fn(i32) -> i64 = rb_net_len; 44 + #[used] 45 + static FN_CLOSE: extern "C" fn(i32) = rb_net_close; 46 + } 47 + 29 48 pub const AUDIO_EXTENSIONS: [&str; 17] = [ 30 49 "mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "ac3", "opus", 31 50 "spx", "sid", "ape", "wma", ··· 340 359 } 341 360 }; 342 361 343 - let mut entries: Vec<Mp3Entry> = vec![]; 344 - 345 362 let mut current_playlist = rb::playlist::get_current(); 346 363 let amount = rb::playlist::amount(); 347 364 365 + // Collect track info while holding the mutex (quick — no I/O). 366 + let mut track_infos: Vec<(String, String)> = Vec::with_capacity(amount as usize); 348 367 for i in 0..amount { 349 368 let info = rb::playlist::get_track_info(i); 350 - let mut entry = rb::metadata::get_metadata(-1, &info.filename); 369 + let hash = format!("{:x}", md5::compute(info.filename.as_bytes())); 370 + track_infos.push((hash, info.filename)); 371 + } 351 372 352 - let hash = format!("{:x}", md5::compute(info.filename.as_bytes())); 373 + // Release the mutex before any slow network I/O so that player 374 + // commands (play, pause, …) are not blocked while metadata is fetched. 375 + drop(player_mutex); 353 376 354 - if let Some(entry) = metadata_cache.get(&hash) { 377 + // Probe metadata for uncached tracks without holding player_mutex. 378 + let mut entries: Vec<Mp3Entry> = vec![]; 379 + for (hash, filename) in &track_infos { 380 + if let Some(entry) = metadata_cache.get(hash) { 355 381 entries.push(entry.clone()); 356 382 continue; 357 383 } 384 + 385 + let mut entry = rb::metadata::get_metadata(-1, filename); 358 386 359 387 let track = rt 360 - .block_on(repo::track::find_by_md5(pool.clone(), &hash)) 388 + .block_on(repo::track::find_by_md5(pool.clone(), hash)) 361 389 .unwrap(); 362 390 363 - if track.is_none() { 364 - entries.push(entry); 365 - continue; 391 + if track.is_some() { 392 + entry.album_art = track.as_ref().map(|t| t.album_art.clone()).flatten(); 393 + entry.album_id = track.as_ref().map(|t| t.album_id.clone()); 394 + entry.artist_id = track.as_ref().map(|t| t.artist_id.clone()); 395 + entry.genre_id = track.as_ref().map(|t| t.genre_id.clone()); 366 396 } 367 397 368 - entry.album_art = track.as_ref().map(|t| t.album_art.clone()).flatten(); 369 - entry.album_id = track.as_ref().map(|t| t.album_id.clone()); 370 - entry.artist_id = track.as_ref().map(|t| t.artist_id.clone()); 371 - entry.genre_id = track.as_ref().map(|t| t.genre_id.clone()); 372 - 373 - metadata_cache.insert(hash, entry.clone()); 398 + metadata_cache.insert(hash.clone(), entry.clone()); 374 399 entries.push(entry); 375 400 } 376 - 377 - drop(player_mutex); 378 401 379 402 current_playlist.amount = amount; 380 403 current_playlist.max_playlist_size = rb::playlist::max_playlist_size();
+17 -3
lib/rbcodec/codecs/libm4a/demux.c
··· 678 678 static bool read_chunk_mdia(qtmovie_t *qtmovie, size_t chunk_len) 679 679 { 680 680 size_t size_remaining = chunk_len - 8; 681 + fourcc_t handler = 0; 681 682 682 683 while (size_remaining) 683 684 { ··· 696 697 697 698 switch (sub_chunk_id) 698 699 { 700 + case MAKEFOURCC('h','d','l','r'): 701 + /* version/flags + pre_defined */ 702 + stream_skip(qtmovie->stream, 8); 703 + handler = stream_read_uint32(qtmovie->stream); 704 + if (sub_chunk_len > 20) 705 + stream_skip(qtmovie->stream, sub_chunk_len - 20); 706 + break; 699 707 case MAKEFOURCC('m','i','n','f'): 700 - if (!read_chunk_minf(qtmovie, sub_chunk_len)) { 701 - return false; 708 + if (handler == MAKEFOURCC('s','o','u','n')) 709 + { 710 + if (!read_chunk_minf(qtmovie, sub_chunk_len)) { 711 + return false; 712 + } 713 + } 714 + else 715 + { 716 + stream_skip(qtmovie->stream, sub_chunk_len - 8); 702 717 } 703 718 break; 704 719 default: ··· 867 882 return 0; 868 883 } 869 884 870 -
+3 -2
lib/rbcodec/metadata/a52.c
··· 21 21 22 22 #include <stdio.h> 23 23 #include "metadata.h" 24 + #include "metadata_common.h" 24 25 #include "logf.h" 25 26 #include "metadata_parsers.h" 26 27 #include "platform.h" ··· 71 72 72 73 id3->bitrate = a52_bitrates[i >> 1]; 73 74 id3->vbr = false; 74 - id3->filesize = filesize(fd); 75 + id3->FS_PREFIX(filesize) = filesize(fd); 75 76 76 77 switch (buf[4] & 0xc0) 77 78 { ··· 97 98 } 98 99 99 100 /* One A52 frame contains 6 blocks, each containing 256 samples */ 100 - totalsamples = id3->filesize / id3->bytesperframe * 6 * 256; 101 + totalsamples = id3->FS_PREFIX(filesize) / id3->bytesperframe * 6 * 256; 101 102 id3->length = totalsamples / id3->frequency * 1000; 102 103 return true; 103 104 }
+2 -2
lib/rbcodec/metadata/aac.c
··· 75 75 entry->id3v1len = 0; 76 76 entry->id3v2len = getid3v2len(fd); 77 77 entry->first_frame_offset = entry->id3v2len; 78 - entry->filesize = filesize(fd) - entry->first_frame_offset; 78 + entry->FS_PREFIX(filesize) = filesize(fd) - entry->first_frame_offset; 79 79 entry->needs_upsampling_correction = false; 80 80 81 81 if (entry->id3v2len) ··· 138 138 return get_mp4_metadata(fd, entry); 139 139 } 140 140 141 - entry->length = (unsigned long)((entry->filesize * 8LL + (entry->bitrate >> 1)) / entry->bitrate); 141 + entry->length = (unsigned long)((entry->FS_PREFIX(filesize) * 8LL + (entry->bitrate >> 1)) / entry->bitrate); 142 142 143 143 return true; 144 144 }
+1 -1
lib/rbcodec/metadata/adx.c
··· 70 70 id3->bitrate = id3->frequency * channels * 18 * 8 / 32 / 1000; 71 71 id3->length = get_long_be(&buf[12]) / id3->frequency * 1000; 72 72 id3->vbr = false; 73 - id3->filesize = filesize(fd); 73 + id3->FS_PREFIX(filesize) = filesize(fd); 74 74 75 75 /* get loop info */ 76 76 if (!memcmp(buf+0x10,"\x01\xF4\x03",3)) {
+1 -1
lib/rbcodec/metadata/aiff.c
··· 145 145 } 146 146 147 147 id3->vbr = false; /* AIFF files are CBR */ 148 - id3->filesize = filesize(fd); 148 + id3->FS_PREFIX(filesize) = filesize(fd); 149 149 } 150 150 else 151 151 {
+2 -2
lib/rbcodec/metadata/asap.c
··· 242 242 bool get_asap_metadata(int fd, struct mp3entry* id3) 243 243 { 244 244 245 - int filelength = filesize(fd); 245 + int filelength = metadata_filesize(fd); 246 246 247 247 if(parse_sap_header(fd, id3, filelength) == false) 248 248 { ··· 254 254 id3->frequency = 44100; 255 255 256 256 id3->vbr = false; 257 - id3->filesize = filelength; 257 + id3->FS_PREFIX(filesize) = filelength; 258 258 id3->genre_string = id3_get_num_genre(36); 259 259 260 260 return true;
+1 -1
lib/rbcodec/metadata/asf.c
··· 642 642 header. 643 643 */ 644 644 id3->first_frame_offset = lseek(fd, 0, SEEK_CUR) + 26; 645 - id3->filesize = filesize(fd); 645 + id3->FS_PREFIX(filesize) = filesize(fd); 646 646 /* We copy the wfx struct to the MP3 TOC field in the id3 struct so 647 647 the codec doesn't need to parse the header object again */ 648 648 memcpy(id3->toc, &wfx, sizeof(wfx));
+3 -3
lib/rbcodec/metadata/au.c
··· 59 59 int offset; 60 60 61 61 id3->vbr = false; /* All Sun audio files are CBR */ 62 - id3->filesize = filesize(fd); 62 + id3->FS_PREFIX(filesize) = filesize(fd); 63 63 id3->length = 0; 64 64 65 65 lseek(fd, 0, SEEK_SET); ··· 72 72 * bits per sample: 8 bit 73 73 * channel: mono 74 74 */ 75 - numbytes = id3->filesize; 75 + numbytes = id3->FS_PREFIX(filesize); 76 76 id3->frequency = 8000; 77 77 id3->bitrate = 8; 78 78 } ··· 90 90 /* data size */ 91 91 numbytes = get_long_be(buf + 8); 92 92 if (numbytes == (uint32_t)0xffffffff) 93 - numbytes = id3->filesize - offset; 93 + numbytes = id3->FS_PREFIX(filesize) - offset; 94 94 95 95 id3->frequency = get_long_be(buf + 16); 96 96 id3->bitrate = get_au_bitspersample(get_long_be(buf + 12)) * get_long_be(buf + 20)
+1 -1
lib/rbcodec/metadata/ay.c
··· 135 135 return false; 136 136 137 137 id3->vbr = false; 138 - id3->filesize = filesize(fd); 138 + id3->FS_PREFIX(filesize) = filesize(fd); 139 139 140 140 id3->bitrate = 706; 141 141 id3->frequency = 44100;
+2 -2
lib/rbcodec/metadata/flac.c
··· 122 122 } 123 123 124 124 id3->vbr = true; /* All FLAC files are VBR */ 125 - id3->filesize = filesize(fd); 125 + id3->FS_PREFIX(filesize) = filesize(fd); 126 126 id3->frequency = (buf[10] << 12) | (buf[11] << 4) 127 127 | ((buf[12] & 0xf0) >> 4); 128 128 rc = true; /* Got vital metadata */ ··· 134 134 { 135 135 /* Calculate track length (in ms) and estimate the bitrate (in kbit/s) */ 136 136 id3->length = ((int64_t) totalsamples * 1000) / id3->frequency; 137 - id3->bitrate = (id3->filesize * 8) / id3->length; 137 + id3->bitrate = (id3->FS_PREFIX(filesize) * 8) / id3->length; 138 138 } 139 139 else if (totalsamples == 0) 140 140 {
+1 -1
lib/rbcodec/metadata/gbs.c
··· 52 52 return false; 53 53 54 54 id3->vbr = false; 55 - id3->filesize = filesize(fd); 55 + id3->FS_PREFIX(filesize) = filesize(fd); 56 56 /* we only render 16 bits, 44.1KHz, Stereo */ 57 57 id3->bitrate = 706; 58 58 id3->frequency = 44100;
+1 -1
lib/rbcodec/metadata/hes.c
··· 26 26 return false; 27 27 28 28 id3->vbr = false; 29 - id3->filesize = filesize(fd); 29 + id3->FS_PREFIX(filesize) = filesize(fd); 30 30 /* we only render 16 bits, 44.1KHz, Stereo */ 31 31 id3->bitrate = 706; 32 32 id3->frequency = 44100;
+1 -1
lib/rbcodec/metadata/kss.c
··· 40 40 return false; 41 41 42 42 id3->vbr = false; 43 - id3->filesize = filesize(fd); 43 + id3->FS_PREFIX(filesize) = filesize(fd); 44 44 /* we only render 16 bits, 44.1KHz, Stereo */ 45 45 id3->bitrate = 706; 46 46 id3->frequency = 44100;
+97 -4
lib/rbcodec/metadata/metadata.c
··· 34 34 35 35 #include "metadata/metadata_common.h" 36 36 37 + /* Unified stream I/O: routes HTTP(S) fds through the Rust netstream layer. 38 + * Only available in simulator / hosted SDL application builds. */ 39 + #if defined(SIMULATOR) || defined(APPLICATION) 40 + #include "streamfd.h" 41 + #endif 42 + 37 43 static bool get_shn_metadata(int fd, struct mp3entry *id3) 38 44 { 39 45 /* TODO: read the id3v2 header if it exists */ 40 46 id3->vbr = true; 41 - id3->filesize = filesize(fd); 47 + id3->FS_PREFIX(filesize) = filesize(fd); 42 48 return skip_id3v2(fd, id3); 43 49 } 44 50 ··· 47 53 id3->bitrate = 706; 48 54 id3->frequency = 44100; 49 55 id3->vbr = false; 50 - id3->filesize = filesize(fd); 56 + id3->FS_PREFIX(filesize) = filesize(fd); 51 57 id3->genre_string = id3_get_num_genre(36); 52 58 return true; 53 59 } ··· 414 420 return AFMT_UNKNOWN; 415 421 } 416 422 423 + #if defined(SIMULATOR) || defined(APPLICATION) 424 + static unsigned int probe_content_type_format(int fd) 425 + { 426 + char content_type[64]; 427 + if (stream_content_type(fd, content_type, sizeof(content_type)) < 0) 428 + return AFMT_UNKNOWN; 429 + 430 + if (!strcasecmp(content_type, "audio/mpeg") || 431 + !strcasecmp(content_type, "audio/mp3") || 432 + !strcasecmp(content_type, "audio/mpa")) 433 + return AFMT_MPA_L3; 434 + 435 + if (!strcasecmp(content_type, "audio/mp4") || 436 + !strcasecmp(content_type, "audio/m4a") || 437 + !strcasecmp(content_type, "audio/x-m4a") || 438 + !strcasecmp(content_type, "video/mp4") || 439 + !strcasecmp(content_type, "application/mp4")) 440 + return AFMT_MP4_AAC; 441 + 442 + if (!strcasecmp(content_type, "audio/aac") || 443 + !strcasecmp(content_type, "audio/aacp")) 444 + return AFMT_AAC_BSF; 445 + 446 + if (!strcasecmp(content_type, "audio/ogg") || 447 + !strcasecmp(content_type, "application/ogg")) 448 + return AFMT_OGG_VORBIS; 449 + 450 + if (!strcasecmp(content_type, "audio/opus")) 451 + return AFMT_OPUS; 452 + 453 + if (!strcasecmp(content_type, "audio/flac") || 454 + !strcasecmp(content_type, "audio/x-flac")) 455 + return AFMT_FLAC; 456 + 457 + if (!strcasecmp(content_type, "audio/wav") || 458 + !strcasecmp(content_type, "audio/wave") || 459 + !strcasecmp(content_type, "audio/x-wav")) 460 + return AFMT_PCM_WAV; 461 + 462 + if (!strcasecmp(content_type, "audio/x-ms-wma") || 463 + !strcasecmp(content_type, "audio/asf")) 464 + return AFMT_WMA; 465 + 466 + return AFMT_UNKNOWN; 467 + } 468 + #endif 469 + 417 470 /* Get metadata for track - return false if parsing showed problems with the 418 471 * file that would prevent playback. supply a filedescriptor <0 and the file will be opened 419 472 * and closed automatically within the get_metadata call ··· 427 480 const struct afmt_entry *entry; 428 481 int logfd = 0; 429 482 DEBUGF("Read metadata for %s\n", trackname); 483 + fprintf(stderr, "[metadata] get_metadata_ex: trackname=%s fd=%d\n", trackname, fd); 430 484 const char *res_str = "\n"; 431 485 432 486 /* Clear the mp3entry to avoid having bogus pointers appear */ 433 487 wipe_mp3entry(id3); 434 488 435 - if (fd < 0) 489 + /* fd == -1 is the "not yet opened" sentinel. In simulator / APPLICATION 490 + * builds HTTP stream handles are encoded as fd <= -1000 and must not be 491 + * treated as "no fd provided". */ 492 + if (fd == -1) 436 493 { 494 + #if defined(SIMULATOR) || defined(APPLICATION) 495 + fd = stream_open(trackname, O_RDONLY); 496 + #else 437 497 fd = open(trackname, O_RDONLY); 498 + #endif 438 499 flags |= METADATA_CLOSE_FD_ON_EXIT; 439 - if (fd < 0) 500 + fprintf(stderr, "[metadata] get_metadata_ex: opened fd=%d for %s\n", fd, trackname); 501 + if (fd == -1) 440 502 { 441 503 DEBUGF("Error opening %s\n", trackname); 442 504 res_str = " - [Error opening]\n"; ··· 447 509 448 510 /* Take our best guess at the codec type based on file extension */ 449 511 id3->codectype = probe_file_format(trackname); 512 + fprintf(stderr, "[metadata] probe_file_format: %s -> codectype=%u\n", trackname, id3->codectype); 513 + #if defined(SIMULATOR) || defined(APPLICATION) 514 + /* For HTTP streams the Content-Type header is more reliable than the 515 + * file extension (e.g. .m4a is used for both ALAC and AAC). Let the 516 + * server's content-type override the extension-based guess whenever it 517 + * resolves to a known codec. */ 518 + if (stream_is_http_fd(fd)) 519 + { 520 + int ct_codec = probe_content_type_format(fd); 521 + fprintf(stderr, "[metadata] probe_content_type_format: fd=%d -> codectype=%u\n", fd, ct_codec); 522 + if (ct_codec != AFMT_UNKNOWN) 523 + id3->codectype = ct_codec; 524 + } 525 + #endif 450 526 451 527 /* default values for embedded cuesheets */ 452 528 id3->has_embedded_cuesheet = false; ··· 455 531 entry = &audio_formats[id3->codectype]; 456 532 457 533 /* Load codec specific track tag information and confirm the codec type. */ 534 + fprintf(stderr, "[metadata] parsing: trackname=%s codec=%u parser=%s\n", 535 + trackname, id3->codectype, entry->label ? entry->label : "none"); 458 536 if (!entry->parse_func) 459 537 { 460 538 DEBUGF("nothing to parse for %s (format %s)\n", trackname, entry->label); 539 + fprintf(stderr, "[metadata] ERROR: no parser for %s (codec=%u)\n", trackname, id3->codectype); 461 540 res_str = " - [No parser]\n"; 462 541 success = false; 463 542 } 464 543 else if (!entry->parse_func(fd, id3)) 465 544 { 466 545 DEBUGF("parsing %s failed (format: %s)\n", trackname, entry->label); 546 + fprintf(stderr, "[metadata] ERROR: parse failed for %s (format %s)\n", trackname, entry->label); 467 547 res_str = " - [Parser failed]\n"; 468 548 success = false; 469 549 wipe_mp3entry(id3); /* ensure the mp3entry is clear */ 470 550 } 551 + else 552 + { 553 + fprintf(stderr, "[metadata] parsed OK: %s length=%lu elapsed=%lu freq=%lu filesize=%lu codectype=%u first_frame_off=%lu\n", 554 + trackname, id3->length, id3->elapsed, id3->frequency, 555 + id3->FS_PREFIX(filesize), id3->codectype, id3->first_frame_offset); 556 + } 471 557 558 + #if defined(SIMULATOR) || defined(APPLICATION) 559 + if ((flags & METADATA_CLOSE_FD_ON_EXIT)) 560 + stream_close(fd); 561 + else 562 + stream_lseek(fd, 0, SEEK_SET); 563 + #else 472 564 if ((flags & METADATA_CLOSE_FD_ON_EXIT)) 473 565 close(fd); 474 566 else 475 567 lseek(fd, 0, SEEK_SET); 568 + #endif 476 569 477 570 if (success && (flags & METADATA_EXCLUDE_ID3_PATH) == 0) 478 571 {
+37
lib/rbcodec/metadata/metadata_common.h
··· 21 21 #include <inttypes.h> 22 22 #include "metadata.h" 23 23 24 + /* In simulator / hosted-SDL-app builds, file descriptors for HTTP(S) streams 25 + * are encoded as integers <= STREAM_HTTP_FD_BASE (-1000). Plain POSIX 26 + * read() / lseek() / filesize() do not handle those values. Route all 27 + * metadata I/O through the stream abstraction so that HTTP handles work 28 + * identically to normal file descriptors. 29 + * 30 + * streamfd.h is in apps/, which is on the include path via -I$(APPSDIR) in 31 + * uisimulator.make. The macros below are intentionally placed before the 32 + * read_uint* macros so those expansions are also redirected. */ 33 + #if defined(SIMULATOR) || defined(APPLICATION) 34 + #include "streamfd.h" 35 + /* Route read() and lseek() through the stream abstraction so that HTTP(S) 36 + * file descriptors (encoded as integers <= STREAM_HTTP_FD_BASE = -1000) are 37 + * handled correctly by all metadata parsers. 38 + * 39 + * filesize() is intentionally NOT redefined here. It is also a member name 40 + * of struct mp3entry (expanded via FS_PREFIX in firmware/include/file.h), so 41 + * redefining it as a function-like macro would break every `id3->filesize` 42 + * struct access with a "no member named 'filesize'" compiler error. 43 + * 44 + * Instead, metadata parsers that need a correct file size (e.g. for CBR 45 + * duration estimation) must call metadata_filesize(fd), defined below. 46 + * stream_filesize_fd() returns Content-Length for HTTP fds and delegates to 47 + * the platform filesize() for regular fds. */ 48 + #ifdef read 49 + #undef read 50 + #endif 51 + #define read(fd, buf, n) stream_read((fd), (buf), (n)) 52 + #ifdef lseek 53 + #undef lseek 54 + #endif 55 + #define lseek(fd, off, whence) stream_lseek((fd), (off), (whence)) 56 + #define metadata_filesize(fd) stream_filesize_fd((fd)) 57 + #else 58 + #define metadata_filesize(fd) filesize(fd) 59 + #endif /* SIMULATOR || APPLICATION */ 60 + 24 61 #ifdef ROCKBOX_BIG_ENDIAN 25 62 #define IS_BIG_ENDIAN 1 26 63 #else
+1 -1
lib/rbcodec/metadata/mod.c
··· 96 96 id3->frequency = 44100; 97 97 id3->length = 120*1000; 98 98 id3->vbr = false; 99 - id3->filesize = filesize(fd); 99 + id3->FS_PREFIX(filesize) = filesize(fd); 100 100 101 101 return true; 102 102 }
+2 -2
lib/rbcodec/metadata/monkeys.c
··· 83 83 } 84 84 85 85 id3->vbr = true; /* All APE files are VBR */ 86 - id3->filesize = filesize(fd); 86 + id3->FS_PREFIX(filesize) = filesize(fd); 87 87 88 88 totalsamples = finalframeblocks; 89 89 if (totalframes > 1) 90 90 totalsamples += blocksperframe * (totalframes-1); 91 91 92 92 id3->length = ((int64_t) totalsamples * 1000) / id3->frequency; 93 - id3->bitrate = (id3->filesize * 8) / id3->length; 93 + id3->bitrate = (id3->FS_PREFIX(filesize) * 8) / id3->length; 94 94 95 95 read_ape_tags(fd, id3); 96 96 return true;
+6 -6
lib/rbcodec/metadata/mp3.c
··· 73 73 74 74 /* Subtract the meta information from the file size to get 75 75 the true size of the MP3 stream */ 76 - entry->filesize -= entry->id3v1len + entry->id3v2len; 76 + entry->FS_PREFIX(filesize) -= entry->id3v1len + entry->id3v2len; 77 77 78 78 /* Validate byte count, in case the file has been edited without 79 79 * updating the header. 80 80 */ 81 81 if (info.byte_count) 82 82 { 83 - const unsigned long expected = entry->filesize - entry->id3v1len 83 + const unsigned long expected = entry->FS_PREFIX(filesize) - entry->id3v1len 84 84 - entry->id3v2len; 85 85 const unsigned long diff = MAX(10240, info.byte_count / 20); 86 86 ··· 101 101 } 102 102 } 103 103 104 - entry->filesize -= bytecount; 104 + entry->FS_PREFIX(filesize) -= bytecount; 105 105 bytecount += entry->id3v2len; 106 106 107 107 entry->bitrate = info.bitrate; ··· 130 130 if (info.bitrate < 8) 131 131 filetime = 0; 132 132 else 133 - filetime = entry->filesize / (info.bitrate >> 3); 133 + filetime = entry->FS_PREFIX(filesize) / (info.bitrate >> 3); 134 134 /* bitrate is in kbps so this delivers milliseconds. Doing bitrate / 8 135 135 * instead of filesize * 8 is exact, because mpeg audio bitrates are 136 136 * always multiples of 8, and it avoids overflows. */ ··· 163 163 bool get_mp3_metadata(int fd, struct mp3entry *entry) 164 164 { 165 165 entry->title = NULL; 166 - entry->filesize = filesize(fd); 166 + entry->FS_PREFIX(filesize) = metadata_filesize(fd); 167 167 entry->id3v1len = getid3v1len(fd); 168 168 entry->id3v2len = getid3v2len(fd); 169 169 entry->tracknum = 0; ··· 181 181 setid3v1title(fd, entry); 182 182 } 183 183 184 - if(!entry->length || (entry->filesize < 8 )) 184 + if(!entry->length || (entry->FS_PREFIX(filesize) < 8 )) 185 185 /* no song length or less than 8 bytes is hereby considered to be an 186 186 invalid mp3 and won't be played by us! */ 187 187 return false;
+2 -1
lib/rbcodec/metadata/mp3data.c
··· 40 40 #include "platform.h" 41 41 42 42 #include "metadata.h" 43 + #include "metadata_common.h" 43 44 #include "metadata/metadata_parsers.h" 44 45 45 46 //#define DEBUG_VERBOSE ··· 609 610 if(result) 610 611 return result; 611 612 612 - info->byte_count = filesize(fd) - getid3v1len(fd) - offset - bytecount; 613 + info->byte_count = metadata_filesize(fd) - getid3v1len(fd) - offset - bytecount; 613 614 } 614 615 615 616 return bytecount;
+6 -6
lib/rbcodec/metadata/mp4.c
··· 777 777 if(size == 0) 778 778 break; 779 779 /* mdat chunks accumulate! */ 780 - id3->filesize += size; 780 + id3->FS_PREFIX(filesize) += size; 781 781 if(id3->samples > 0) { 782 782 /* We've already seen the moov chunk. */ 783 783 done = true; ··· 821 821 bool get_mp4_metadata(int fd, struct mp3entry* id3) 822 822 { 823 823 id3->codectype = AFMT_UNKNOWN; 824 - id3->filesize = 0; 824 + id3->FS_PREFIX(filesize) = 0; 825 825 errno = 0; 826 826 827 - if (read_mp4_container(fd, id3, filesize(fd)) && (errno == 0) 827 + if (read_mp4_container(fd, id3, metadata_filesize(fd)) && (errno == 0) 828 828 && (id3->samples > 0) && (id3->frequency > 0) 829 - && (id3->filesize > 0)) 829 + && (id3->FS_PREFIX(filesize) > 0)) 830 830 { 831 831 if (id3->codectype == AFMT_UNKNOWN) 832 832 { ··· 844 844 return false; 845 845 } 846 846 847 - id3->bitrate = ((int64_t) id3->filesize * 8) / id3->length; 847 + id3->bitrate = ((int64_t) id3->FS_PREFIX(filesize) * 8) / id3->length; 848 848 DEBUGF("MP4 bitrate %d, frequency %ld Hz, length %ld ms\n", 849 849 id3->bitrate, id3->frequency, id3->length); 850 850 } ··· 853 853 logf("MP4 metadata error"); 854 854 DEBUGF("MP4 metadata error. errno %d, samples %ld, frequency %ld, " 855 855 "filesize %ld\n", errno, id3->samples, id3->frequency, 856 - id3->filesize); 856 + id3->FS_PREFIX(filesize)); 857 857 return false; 858 858 } 859 859
+2 -2
lib/rbcodec/metadata/mpc.c
··· 212 212 return false; 213 213 } 214 214 215 - id3->filesize = filesize(fd); 216 - id3->bitrate = id3->filesize * 8 / id3->length; 215 + id3->FS_PREFIX(filesize) = filesize(fd); 216 + id3->bitrate = id3->FS_PREFIX(filesize) * 8 / id3->length; 217 217 218 218 read_ape_tags(fd, id3); 219 219 return true;
+1 -1
lib/rbcodec/metadata/nsf.c
··· 262 262 return false; 263 263 264 264 id3->vbr = false; 265 - id3->filesize = filesize(fd); 265 + id3->FS_PREFIX(filesize) = filesize(fd); 266 266 /* we only render 16 bits, 44.1KHz, Mono */ 267 267 id3->bitrate = 706; 268 268 id3->frequency = 44100;
+3 -3
lib/rbcodec/metadata/ogg.c
··· 134 134 return false; 135 135 } 136 136 137 - id3->filesize = filesize(fd); 137 + id3->FS_PREFIX(filesize) = filesize(fd); 138 138 139 139 /* We need to ensure the serial number from this page is the same as the 140 140 * one from the last page (since we only support a single bitstream). ··· 148 148 */ 149 149 150 150 /* A page is always < 64 kB */ 151 - if (lseek(fd, -(MIN(64 * 1024, id3->filesize)), SEEK_END) < 0) 151 + if (lseek(fd, -(MIN(64 * 1024, id3->FS_PREFIX(filesize))), SEEK_END) < 0) 152 152 { 153 153 return false; 154 154 } ··· 232 232 return false; 233 233 } 234 234 235 - id3->bitrate = (((int64_t) id3->filesize - comment_size) * 8) / id3->length; 235 + id3->bitrate = (((int64_t) id3->FS_PREFIX(filesize) - comment_size) * 8) / id3->length; 236 236 237 237 return true; 238 238 }
+3 -2
lib/rbcodec/metadata/oma.c
··· 48 48 #include <string.h> 49 49 #include "platform.h" 50 50 #include "metadata.h" 51 + #include "metadata_common.h" 51 52 #include "metadata_parsers.h" 52 53 53 54 #define EA3_HEADER_SIZE 96 ··· 179 180 180 181 /* Currently, there's no means of knowing the duration * 181 182 * directly from the the file so we calculate it. */ 182 - id3->filesize = filesize(fd); 183 - id3->length = ((id3->filesize - id3->first_frame_offset) * 8) / id3->bitrate; 183 + id3->FS_PREFIX(filesize) = filesize(fd); 184 + id3->length = ((id3->FS_PREFIX(filesize) - id3->first_frame_offset) * 8) / id3->bitrate; 184 185 return true; 185 186 }
+2 -2
lib/rbcodec/metadata/rm.c
··· 267 267 rmctx->data_offset = skipped + 8; 268 268 rmctx->bit_rate = rmctx->block_align * rmctx->sample_rate / 192; 269 269 if (rmctx->block_align) 270 - rmctx->nb_packets = (filesize(fd) - rmctx->data_offset) / rmctx->block_align; 270 + rmctx->nb_packets = (metadata_filesize(fd) - rmctx->data_offset) / rmctx->block_align; 271 271 if (rmctx->sample_rate) 272 272 rmctx->duration = (uint32_t)(256LL * 6 * 1000 * rmctx->nb_packets / rmctx->sample_rate); 273 273 rmctx->flags |= RM_RAW_DATASTREAM; ··· 505 505 id3->bitrate = (rmctx->bit_rate + 500) / 1000; 506 506 id3->frequency = rmctx->sample_rate; 507 507 id3->length = rmctx->duration; 508 - id3->filesize = filesize(fd); 508 + id3->FS_PREFIX(filesize) = metadata_filesize(fd); 509 509 return true; 510 510 }
+1 -1
lib/rbcodec/metadata/sgc.c
··· 54 54 return false; 55 55 56 56 id3->vbr = false; 57 - id3->filesize = filesize(fd); 57 + id3->FS_PREFIX(filesize) = filesize(fd); 58 58 /* we only render 16 bits, 44.1KHz, Stereo */ 59 59 id3->bitrate = 706; 60 60 id3->frequency = 44100;
+1 -1
lib/rbcodec/metadata/sid.c
··· 83 83 */ 84 84 id3->length = (buf[0xf]-1)*1000; 85 85 id3->vbr = false; 86 - id3->filesize = filesize(fd); 86 + id3->FS_PREFIX(filesize) = filesize(fd); 87 87 88 88 return true; 89 89 }
+1 -1
lib/rbcodec/metadata/smaf.c
··· 446 446 id3->composer = NULL; 447 447 448 448 id3->vbr = false; /* All SMAF files are CBR */ 449 - id3->filesize = filesize(fd); 449 + id3->FS_PREFIX(filesize) = filesize(fd); 450 450 451 451 /* check File Chunk and Contents Info Chunk */ 452 452 lseek(fd, 0, SEEK_SET);
+1 -1
lib/rbcodec/metadata/spc.c
··· 125 125 id3->frequency = 32000; 126 126 id3->length = length+fade; 127 127 128 - id3->filesize = filesize(fd); 128 + id3->FS_PREFIX(filesize) = filesize(fd); 129 129 id3->genre_string = id3_get_num_genre(36); 130 130 131 131 return true;
+2 -2
lib/rbcodec/metadata/tta.c
··· 54 54 static void read_id3_tags(int fd, struct mp3entry* id3) 55 55 { 56 56 id3->title = NULL; 57 - id3->filesize = filesize(fd); 57 + id3->FS_PREFIX(filesize) = filesize(fd); 58 58 id3->id3v2len = getid3v2len(fd); 59 59 id3->tracknum = 0; 60 60 id3->discnum = 0; ··· 98 98 id3->length = ((GET_HEADER(ttahdr, DATA_LENGTH)) / id3->frequency) * 1000LL; 99 99 bps = (GET_HEADER(ttahdr, BITS_PER_SAMPLE)); 100 100 101 - datasize = id3->filesize - id3->first_frame_offset; 101 + datasize = id3->FS_PREFIX(filesize) - id3->first_frame_offset; 102 102 origsize = (GET_HEADER(ttahdr, DATA_LENGTH)) * ((bps + 7) / 8) * channels; 103 103 104 104 id3->bitrate = (int) ((uint64_t) datasize * id3->frequency * channels * bps
+2 -2
lib/rbcodec/metadata/vgm.c
··· 153 153 } 154 154 155 155 id3->vbr = false; 156 - id3->filesize = filesize(fd); 156 + id3->FS_PREFIX(filesize) = filesize(fd); 157 157 158 158 id3->bitrate = 1411; 159 159 id3->frequency = 44100; ··· 179 179 180 180 /* Seek to gd3 offset and read as 181 181 many bytes posible */ 182 - gd3_offset = id3->filesize - (header_size + gd3_offset); 182 + gd3_offset = id3->FS_PREFIX(filesize) - (header_size + gd3_offset); 183 183 if ((lseek(fd, -gd3_offset, SEEK_END) < 0) 184 184 || ((read_bytes = read(fd, buf, ID3V2_BUF_SIZE)) <= 0)) 185 185 return true;
+2 -2
lib/rbcodec/metadata/vox.c
··· 42 42 id3->frequency = 8000; 43 43 id3->bitrate = 8000 * 4 / 1000; 44 44 id3->vbr = false; /* All VOX files are CBR */ 45 - id3->filesize = filesize(fd); 46 - id3->length = id3->filesize >> 2; 45 + id3->FS_PREFIX(filesize) = filesize(fd); 46 + id3->length = id3->FS_PREFIX(filesize) >> 2; 47 47 48 48 return true; 49 49 }
+2 -2
lib/rbcodec/metadata/vtx.c
··· 102 102 if (lseek(fd, 0, SEEK_SET) < 0) 103 103 goto exit_bad; 104 104 105 - if (filesize(fd) < 20) 105 + if (metadata_filesize(fd) < 20) 106 106 goto exit_bad; 107 107 108 108 uint hdr = Reader_ReadWord(fd); ··· 142 142 id3->bitrate = 706; 143 143 id3->frequency = 44100; // XXX allow this to be configured? 144 144 145 - id3->filesize = filesize(fd); 145 + id3->FS_PREFIX(filesize) = metadata_filesize(fd); 146 146 id3->length = info.frames * 1000 / info.playerfreq; 147 147 148 148 return true;
+3 -3
lib/rbcodec/metadata/wave.c
··· 306 306 memset(&fmt, 0, sizeof(struct wave_fmt)); 307 307 308 308 id3->vbr = false; /* All Wave/Wave64 files are CBR */ 309 - id3->filesize = filesize(fd); 309 + id3->FS_PREFIX(filesize) = filesize(fd); 310 310 311 311 /* get RIFF chunk header */ 312 312 lseek(fd, 0, SEEK_SET); ··· 376 376 chunksize += ((is_64)? ((1 + ~chunksize) & 0x07) : (chunksize & 1)); 377 377 378 378 offset += chunksize; 379 - if (offset >= id3->filesize) 379 + if (offset >= id3->FS_PREFIX(filesize)) 380 380 break; 381 381 382 382 lseek(fd, chunksize - read_data, SEEK_CUR); ··· 400 400 /* Calculate track length (in ms) and estimate the bitrate (in kbit/s) */ 401 401 id3->length = (fmt.formattag != WAVE_FORMAT_ATRAC3)? 402 402 (uint64_t)fmt.totalsamples * 1000 / id3->frequency : 403 - ((id3->filesize - id3->first_frame_offset) * 8) / id3->bitrate; 403 + ((id3->FS_PREFIX(filesize) - id3->first_frame_offset) * 8) / id3->bitrate; 404 404 405 405 /* output header/id3 info (for debug) */ 406 406 DEBUGF("%s header info ----\n", (is_64)? "wave64" : "wave");
+3 -3
lib/rbcodec/metadata/wavpack.c
··· 79 79 } 80 80 81 81 id3->vbr = true; /* All WavPack files are VBR */ 82 - id3->filesize = filesize (fd); 82 + id3->FS_PREFIX(filesize) = filesize (fd); 83 83 84 84 /* check up to 16 headers before we give up finding one with audio */ 85 85 ··· 134 134 /* if the total number of samples is still unknown, make a guess on the high side (for now) */ 135 135 136 136 if (totalsamples == (uint32_t) -1) { 137 - totalsamples = id3->filesize * 3; 137 + totalsamples = id3->FS_PREFIX(filesize) * 3; 138 138 139 139 if (!(flags & HYBRID_FLAG)) 140 140 totalsamples /= 2; ··· 144 144 } 145 145 146 146 id3->length = ((int64_t) totalsamples * 1000) / id3->frequency; 147 - id3->bitrate = id3->filesize / (id3->length / 8); 147 + id3->bitrate = id3->FS_PREFIX(filesize) / (id3->length / 8); 148 148 149 149 read_ape_tags(fd, id3); 150 150 return true;
+1 -1
uisimulator/uisimulator.make
··· 27 27 # Rust network-stream static library (built from crates/netstream). 28 28 NETSTREAM_LIB = $(BUILDDIR)/librbnetstream.a 29 29 NETSTREAM_MANIFEST = $(ROOTDIR)/crates/netstream/Cargo.toml 30 - NETSTREAM_CARGO_LIB = $(ROOTDIR)/crates/netstream/target/release/librbnetstream.a 30 + NETSTREAM_CARGO_LIB = $(ROOTDIR)/target/release/librbnetstream.a 31 31 32 32 .PHONY: netstream-lib 33 33 netstream-lib:
+10 -10
zig/src/rockbox/metadata.zig
··· 35 35 id3version: u8, 36 36 codectype: u32, 37 37 bitrate: u32, 38 - frequency: u32, 39 - id3v2len: u32, 40 - id3v1len: u32, 41 - first_frame_offset: u32, 42 - filesize: u32, 43 - length: u32, 44 - elapsed: u32, 38 + frequency: c_ulong, 39 + id3v2len: c_ulong, 40 + id3v1len: c_ulong, 41 + first_frame_offset: c_ulong, 42 + filesize: c_ulong, 43 + length: c_ulong, 44 + elapsed: c_ulong, 45 45 lead_trim: c_int, 46 46 tail_trim: c_int, 47 47 samples: u64, 48 - frame_count: u32, 49 - bytesperframe: u32, 48 + frame_count: c_ulong, 49 + bytesperframe: c_ulong, 50 50 vbr: bool, 51 51 has_toc: bool, 52 52 toc: [100]u8, 53 53 needs_upsampling_correction: bool, 54 54 id3v2buf: [ID3V2_BUF_SIZE]u8, 55 55 id3v1buf: [4][92]u8, 56 - offset: u32, 56 + offset: c_ulong, 57 57 index: c_int, 58 58 skip_resume_adjustments: bool, 59 59 autoresumable: u8,