Skip to main content

atmos/kernel/
http_body.rs

1//! HTTP 応答から**本文の範囲**を求める(純粋モジュール)。
2//!
3//! 【2026-08-05】まとめ取得(keep-alive)で
4//! **ヘッダを含めて本文として返す**バグを出した。
5//! キャッシュへ `HTTP/1.1 200 OK...` ごと書き込まれ、
6//! Font Awesome が 1 つも登録されず、
7//! ページ上のアイコンがすべて空の四角になっていた。
8//!
9//! 境界の計算はネットワークもハードウェアも要らないので、
10//! ここへ切り出して単体試験で固定する。
11
12/// ヘッダの終わり(=**本文の開始位置**)を返す。
13///
14/// `\r\n\r\n` の**直後**を指す。見つからなければ `None`。
15pub fn body_start(raw: &[u8]) -> Option<usize> {
16    raw.windows(4).position(|w| w == b"\r\n\r\n").map(|i| i + 4)
17}
18
19/// `Content-Length` の値を返す(ヘッダ部だけを見る)。
20pub fn content_length(raw: &[u8]) -> Option<usize> {
21    let end = body_start(raw)?;
22    // ヘッダは ASCII なのでバイト列のまま探す。
23    // 文字列の添字操作は UTF-8 の途中で切ると panic しうる。
24    const KEY: &[u8] = b"content-length:";
25    let head = &raw[..end];
26    let mut i = 0usize;
27    while i + KEY.len() <= head.len() {
28        let hit = head[i..i + KEY.len()]
29            .iter()
30            .zip(KEY.iter())
31            .all(|(a, b)| a.to_ascii_lowercase() == *b);
32        if hit {
33            // 値は行末(CR)まで。先頭の空白は読み飛ばす。
34            let mut j = i + KEY.len();
35            let mut v: usize = 0;
36            let mut seen = false;
37            while j < head.len() && head[j] != 13 {
38                match head[j] {
39                    d @ 48..=57 => {
40                        v = v.checked_mul(10)?.checked_add((d - 48) as usize)?;
41                        seen = true;
42                    }
43                    32 | 9 if !seen => {}
44                    _ => break,
45                }
46                j += 1;
47            }
48            return if seen { Some(v) } else { None };
49        }
50        i += 1;
51    }
52    None
53}
54
55/// 本文の範囲 `start..end` を返す。
56///
57/// **ここを間違えるとヘッダ込みで保存される**(実際に起きた)。
58/// 足りていなければ `None`(まだ読み切っていない)。
59pub fn body_range(raw: &[u8]) -> Option<(usize, usize)> {
60    let start = body_start(raw)?;
61    let len = content_length(raw)?;
62    let end = start.checked_add(len)?;
63    if raw.len() >= end {
64        Some((start, end))
65    } else {
66        None
67    }
68}
69
70/// 応答が**途中で切れている**か。
71///
72/// `Content-Length` が分かっていて、受信済み本文がそれより短ければ `true`。
73/// `chunked`(長さ不明)は判定できないので `false`。
74///
75/// 【2026-08-05】この計算は `tls.rs` に 3 つの写しがあった
76/// (`find_header_end` / `parse_content_length` / `response_looks_truncated`)。
77/// 片方だけ直す事故を防ぐためここへ一本化した。
78pub fn looks_truncated(raw: &[u8]) -> bool {
79    let Some(start) = body_start(raw) else {
80        return false;
81    };
82    let Some(len) = content_length(raw) else {
83        return false;
84    };
85    raw.len().saturating_sub(start) < len
86}