(* etag.ml - ETag parsing and comparison per RFC 7232 *) open Base (* Entity tag - record pointing into buffer *) type t = { weak : bool ; off : int ; len : int } type status = | Valid | Invalid let empty = { weak = false; off = 0; len = 0 } (* Maximum number of ETags in If-Match/If-None-Match header *) let max_tags = 16 (* Parse a single ETag value. Format: entity-tag = [ weak ] opaque-tag weak = %x57.2F ; "W/", case-sensitive opaque-tag = DQUOTE *etagc DQUOTE etagc = %x21 / %x23-7E / obs-text *) let parse buf (sp : Span.t) = let off = Span.off sp in let len = Span.len sp in if len < 2 then (Invalid, empty) (* Minimum: "" *) else let c0 = Base_bigstring.unsafe_get buf off in let c1 = Base_bigstring.unsafe_get buf (off + 1) in (* Check for weak indicator W/ *) let weak, quote_start = if Char.equal c0 'W' && Char.equal c1 '/' && len >= 4 then (true, off + 2) else (false, off) in let remaining = len - (quote_start - off) in if remaining < 2 then (Invalid, empty) else let first = Base_bigstring.unsafe_get buf quote_start in let last = Base_bigstring.unsafe_get buf (quote_start + remaining - 1) in if Char.equal first '"' && Char.equal last '"' then let tag_off = quote_start + 1 in let tag_len = remaining - 2 in (Valid, { weak; off = tag_off; len = tag_len }) else (Invalid, empty) ;; let to_string buf etag = Base_bigstring.To_string.sub buf ~pos:etag.off ~len:etag.len ;; (* If-Match / If-None-Match parsing *) type match_condition = | Any | Tags | Empty (* Skip optional whitespace *) let skip_ows buf ~pos ~len = let p = ref pos in while !p < len && ( let c = Base_bigstring.unsafe_get buf !p in Char.equal c ' ' || Char.equal c '\t' ) do Int.incr p done; !p ;; (* Parse comma-separated list of entity tags into array *) let parse_match_header buf (sp : Span.t) (tags : t array) = let off = Span.off sp in let len = Span.len sp in let end_pos = off + len in (* Skip leading whitespace *) let start = skip_ows buf ~pos:off ~len:end_pos in if start >= end_pos then (Empty, 0) else if Char.equal (Base_bigstring.unsafe_get buf start) '*' then (* Check it's just "*" possibly with trailing whitespace *) let after_star = skip_ows buf ~pos:(start + 1) ~len:end_pos in if after_star >= end_pos then (Any, 0) else (Empty, 0) else (* Parse comma-separated list of entity tags *) let pos = ref start in let count = ref 0 in let valid = ref true in while !valid && !pos < end_pos && !count < max_tags do pos := skip_ows buf ~pos:!pos ~len:end_pos; if !pos >= end_pos then valid := false else ( (* Find end of this tag (comma or end) *) let tag_start = !pos in let tag_end = ref !pos in let in_quote = ref false in while !tag_end < end_pos && (!in_quote || not (Char.equal (Base_bigstring.unsafe_get buf !tag_end) ',')) do if Char.equal (Base_bigstring.unsafe_get buf !tag_end) '"' then in_quote := not !in_quote; Int.incr tag_end done; (* Trim trailing whitespace from tag *) let trimmed_end = ref !tag_end in while !trimmed_end > tag_start && ( let c = Base_bigstring.unsafe_get buf (!trimmed_end - 1) in Char.equal c ' ' || Char.equal c '\t' ) do Int.decr trimmed_end done; let tag_span = Span.make ~off:tag_start ~len:(!trimmed_end - tag_start) in let (status, etag) = parse buf tag_span in (match status with | Valid -> Array.unsafe_set tags !count etag; Int.incr count | Invalid -> ()); (* Skip comma if present *) if !tag_end < end_pos && Char.equal (Base_bigstring.unsafe_get buf !tag_end) ',' then pos := !tag_end + 1 else pos := !tag_end ) done; if !count > 0 then (Tags, !count) else (Empty, 0) ;; (* Strong comparison: both must be strong, tags must match exactly *) let strong_match buf a b = if a.weak || b.weak then false else let a_len = a.len in let b_len = b.len in if a_len <> b_len then false else Base_bigstring.memcmp buf ~pos1:a.off buf ~pos2:b.off ~len:a_len = 0 ;; (* Weak comparison: only tags must match, ignore weak indicator *) let weak_match buf a b = let a_len = a.len in let b_len = b.len in if a_len <> b_len then false else Base_bigstring.memcmp buf ~pos1:a.off buf ~pos2:b.off ~len:a_len = 0 ;; let matches_any_weak buf etag tags ~count = let i = ref 0 in let found = ref false in while (not !found) && !i < count do if weak_match buf etag (Array.unsafe_get tags !i) then found := true else Int.incr i done; !found ;; let matches_any_strong buf etag tags ~count = let i = ref 0 in let found = ref false in while (not !found) && !i < count do if strong_match buf etag (Array.unsafe_get tags !i) then found := true else Int.incr i done; !found ;; (* Response writing *) let write_etag dst ~off etag src_buf = (* ETag: [W/]"tag"\r\n *) let off = Buf_write.string dst ~off "ETag: " in let off = if etag.weak then Buf_write.string dst ~off "W/" else off in let off = Buf_write.char dst ~off '"' in (* Copy tag value from source buffer *) let tag_off = etag.off in let tag_len = etag.len in for i = 0 to tag_len - 1 do Bigarray.Array1.unsafe_set dst (off + i) (Base_bigstring.unsafe_get src_buf (tag_off + i)) done; let off = off + tag_len in let off = Buf_write.char dst ~off '"' in Buf_write.crlf dst ~off ;; let write_etag_string dst ~off ~weak tag = let off = Buf_write.string dst ~off "ETag: " in let off = if weak then Buf_write.string dst ~off "W/" else off in let off = Buf_write.char dst ~off '"' in let off = Buf_write.string dst ~off tag in let off = Buf_write.char dst ~off '"' in Buf_write.crlf dst ~off ;; let pp buf fmt etag = let tag = to_string buf etag in if etag.weak then Stdlib.Format.fprintf fmt "W/\"%s\"" tag else Stdlib.Format.fprintf fmt "\"%s\"" tag ;;