package aceboot::bodyparser
import std.collection.*
import aceboot::web.*
let FORM_KEY = "__ace_form"
/**
* 请求解析中间件:按 Content-Type 解析请求体。
* 当前支持 application/x-www-form-urlencoded,解析结果存入 ctx.state,
* 通过 formValue(ctx, name) 读取。读取的是 ctx.rawBody()(由适配层/测试填充)。
*
* JSON / multipart 待后续补充(JSON 可在 M3/上层用 stdx.encoding.json 或独立解析器)。
*/
public func bodyParser(): Middleware {
return {
ctx: Context, next: Next =>
let ct = ctx.header("content-type") ?? ""
if (ctx.hasRequestBodyStream() && (ct.contains("application/x-www-form-urlencoded") || ct.contains("multipart/form-data"))) {
throw Exception("bodyParser cannot be used with streaming request bodies")
}
if (ct.contains("application/x-www-form-urlencoded")) {
match (ctx.rawBody()) {
case Some(body) => parseForm(ctx, body)
case None => ()
}
} else if (ct.contains("multipart/form-data")) {
let boundary = extractBoundary(ct)
if (boundary != "") {
match (ctx.rawBodyBytes()) {
case Some(body) => parseMultipartBytes(ctx, body, boundary)
case None => ()
}
}
}
next()
}
}
// 从 Content-Type 提取 boundary=... 的值(可能后接 ;)。
func extractBoundary(ct: String): String {
let marker = "boundary="
match (ct.indexOf(marker)) {
case Some(i) =>
let b = ct[i + marker.size..ct.size]
match (b.indexOf(";")) {
case Some(j) => b[0..j]
case None => b
}
case None => ""
}
}
// UploadedFile / FILE_KEY / fileValue / fileValues 在 aceboot::web(facade 可见,供 @File/@Files 宏)。
// 在 hay 中从 from 起找子序列 needle 的首位置。
func indexOfBytes(hay: Array<UInt8>, needle: Array<UInt8>, from: Int64): ?Int64 {
if (needle.size == 0 || hay.size < needle.size) {
return None
}
var i = from
while (i <= hay.size - needle.size) {
var matched = true
for (j in 0..needle.size) {
if (hay[i + j] != needle[j]) {
matched = false
break
}
}
if (matched) {
return i
}
i++
}
return None
}
/**
* 字节级解析 multipart/form-data(二进制安全)。按 --boundary 字节序列切分,
* 每段头部(ASCII)解析 name/filename/Content-Type,正文保留原始字节:
* 带 filename → 文件(存 FILE_KEY,经 fileValue 读);无 filename → 文本字段(存 FORM_KEY)。
*/
func parseMultipartBytes(ctx: Context, body: Array<UInt8>, boundary: String): Unit {
let form = HashMap<String, String>()
let files = HashMap<String, ArrayList<UploadedFile>>()
let delim = ("--" + boundary).toArray()
let sep = [13u8, 10u8, 13u8, 10u8] // \r\n\r\n
// 收集所有 delim 位置。
let positions = ArrayList<Int64>()
var idx = indexOfBytes(body, delim, 0)
while (true) {
match (idx) {
case Some(pos) =>
positions.add(pos)
idx = indexOfBytes(body, delim, pos + delim.size)
case None => break
}
}
// 相邻 delim 之间为一段。
var k = 0
while (k + 1 < positions.size) {
let segStart = positions[k] + delim.size
let segEnd = positions[k + 1]
k++
if (segEnd <= segStart) {
continue
}
var seg = body[segStart..segEnd]
// 去前导 \r\n
if (seg.size >= 2 && seg[0] == 13u8 && seg[1] == 10u8) {
seg = seg[2..seg.size]
}
// 头/正文分隔
match (indexOfBytes(seg, sep, 0)) {
case Some(hsep) =>
let head = String.fromUtf8(seg[0..hsep])
var content = seg[hsep + 4..seg.size]
// 去正文尾部 \r\n
if (content.size >= 2 && content[content.size - 2] == 13u8 && content[content.size - 1] == 10u8) {
content = content[0..content.size - 2]
}
let name = extractDispositionName(head)
if (name != "") {
let filename = extractAttr(head, "filename=\"")
if (filename != "") {
let uf = UploadedFile(name, filename, extractContentType(head), content)
match (files.get(name)) {
case Some(list) => list.add(uf) // 同名多文件
case None =>
let l = ArrayList<UploadedFile>()
l.add(uf)
files.add(name, l)
}
} else {
form.add(name, String.fromUtf8(content))
}
}
case None => ()
}
}
ctx.state.add(FORM_KEY, form)
ctx.state.add(FILE_KEY, files)
}
// 从头部取 marker(如 `filename="`)后到下一个引号的值;无则空串。
func extractAttr(head: String, marker: String): String {
match (head.indexOf(marker)) {
case Some(i) =>
let rest = head[i + marker.size..head.size]
match (rest.indexOf("\"")) {
case Some(j) => rest[0..j]
case None => ""
}
case None => ""
}
}
// 取 part 头里的 Content-Type 值(到行尾)。
func extractContentType(head: String): String {
let marker = "Content-Type:"
match (head.indexOf(marker)) {
case Some(i) =>
var v = head[i + marker.size..head.size]
match (v.indexOf("\r")) {
case Some(j) => v = v[0..j]
case None => ()
}
trimLead(v)
case None => "application/octet-stream"
}
}
func trimLead(s: String): String {
let bs = s.toArray()
var i = 0
while (i < bs.size && bs[i] == 32u8) {
i++
}
String.fromUtf8(bs[i..bs.size])
}
// 取 part 头里 Content-Disposition 的 name="..." 值。
func extractDispositionName(part: String): String {
let marker = "name=\""
match (part.indexOf(marker)) {
case Some(i) =>
let rest = part[i + marker.size..part.size]
match (rest.indexOf("\"")) {
case Some(j) => rest[0..j]
case None => ""
}
case None => ""
}
}
func parseForm(ctx: Context, body: String): Unit {
let form = HashMap<String, String>()
for (pair in body.split("&")) {
if (pair == "") {
continue
}
let kv = pair.split("=")
if (kv.size >= 2) {
form.add(urlDecode(kv[0]), urlDecode(kv[1]))
} else {
form.add(urlDecode(kv[0]), "")
}
}
ctx.state.add(FORM_KEY, form)
}
/** 读取已解析的表单字段。 */
public func formValue(ctx: Context, name: String): ?String {
match (ctx.state.get(FORM_KEY)) {
case Some(v) => match (v as HashMap<String, String>) {
case Some(m) => m.get(name)
case None => None
}
case None => None
}
}
func hexVal(b: UInt8): Int64 {
if (b >= 48u8 && b <= 57u8) {
return Int64(b) - 48
}
if (b >= 65u8 && b <= 70u8) {
return Int64(b) - 65 + 10
}
if (b >= 97u8 && b <= 102u8) {
return Int64(b) - 97 + 10
}
return -1
}
/** application/x-www-form-urlencoded 解码:'+' 视作空格,'%XX' 解码为字节。 */
func urlDecode(s: String): String {
let bytes = s.toArray()
let out = ArrayList<UInt8>()
var i = 0
while (i < bytes.size) {
let b = bytes[i]
if (b == 43u8) {
out.add(32u8)
i += 1
} else if (b == 37u8 && i + 2 < bytes.size) {
let h = hexVal(bytes[i + 1])
let l = hexVal(bytes[i + 2])
if (h >= 0 && l >= 0) {
out.add(UInt8(h * 16 + l))
i += 3
} else {
out.add(b)
i += 1
}
} else {
out.add(b)
i += 1
}
}
return String.fromUtf8(out.toArray())
}