RrunningW```
e48554c5创建于 2月3日历史提交
/*
Copyright (c) 2025 WuJingrun(吴京润)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 */
package f_http

import std.io.*
import f_collection.*
import f_data.*
import f_io.RotatableBuffer
import f_util.UUID
/**
https://www.rfc-editor.org/rfc/rfc1867
multipart/form-data
        Content-type: multipart/form-data; boundary=AaB03x

        --AaB03x
        content-disposition: form-data; name="field1"

        Joe Blow
        --AaB03x
        content-disposition: form-data; name="pics"; filename="file1.txt"
        Content-Type: text/plain

         ... contents of file1.txt ...
        --AaB03x--



multipart/form-data multipart/mixed
        Content-type: multipart/form-data; boundary=AaB03x

        --AaB03x
        content-disposition: form-data; name="field1"

        Joe Blow
        --AaB03x
        content-disposition: form-data; name="pics"
        Content-type: multipart/mixed; boundary=BbC04y

        --BbC04y
        Content-disposition: attachment; filename="file1.txt"
        Content-Type: text/plain

        ... contents of file1.txt ...
        --BbC04y
        Content-disposition: attachment; filename="file2.gif"
        Content-type: image/gif
        Content-Transfer-Encoding: binary

          ...contents of file2.gif...
        --BbC04y--
        --AaB03x--
 */
public class MultipartFormDataParser {
    private static let DOUBLE_CRLF = [b'\r', b'\n', b'\r', b'\n']
    private let stream: RotatableBuffer
    private let seperatorBoundaryBytes: Array<Byte>
    
    public MultipartFormDataParser(input: InputStream, private let mediaType: String, private let boundary: String){
        seperatorBoundaryBytes = '\r\n--${boundary}'.unsafeBytes()
        stream = RotatableBuffer(input, seperatorBoundaryBytes, halfBufferSize: HttpConfig.halfBufferSize)
    }
    public static func new(ctx: HttpContext){
        let contentType = ctx.request.headers.getFirst("Content-Type").getOrThrow()
        let boundary = contentType[contentType.indexOf('boundary=').getOrThrow() + 'boundary='.size ..]
        MultipartFormDataParser(ctx.request.body, contentType, boundary)
    }
    public func parse(): Data {
        let boundaryBytes = seperatorBoundaryBytes[2 ..]
        let path = Path('/tmp/${UUID.random()}')
        let crlf = Array<Byte>(2, repeat: 0)
        let parts = if(mediaType.startsWith('multipart/form-data')){
            DataMultipartTuples()
        }else if(mediaType.startsWith('multipart/mixed')){
            DataMultipartList()
        }else{
            throw MultipartException('${mediaType} is not be supported.')
        }
        var start = stream.indexOf(boundaryBytes)
        if(start < 0){
            throw MultipartException()
        }
        stream.addOffset(boundaryBytes.size)
        start += boundaryBytes.size
        while(true){
            let (l, _, _) = stream.read(crlf)
            match((crlf[0], crlf[1])){
                case (b'\r', b'\n') =>
                    start = stream.offset
                    var end = stream.indexOf(DOUBLE_CRLF, from: start)
                    var buf = Array<Byte>(end - start + DOUBLE_CRLF.size, repeat: 0)
                    stream.read(buf)
                    let partHeader = String.fromUtf8(buf).split('\r\n')
                    let contentDisposition = ContentDisposition.parse(partHeader[0]['Content-Disposition: '.size ..])
                    let filename = contentDisposition.getFilename()
                    end = stream.indexOf(seperatorBoundaryBytes)
                    if(end > 0){
                        buf = Array<Byte>(end - stream.offset, repeat: 0)
                        stream.read(buf)
                        if(filename.isEmpty()){
                            let value = if(let Some(charset) <- contentDisposition.getCharset()){
                                charset.newDecoder().decode(buf)
                            }else{
                                String.fromUtf8(buf)
                            }
                            let name = contentDisposition.getName()
                            if(let mediaType <- partHeader[1].replace('Content-Type:', '').trimAscii() && mediaType.size > 0){
                                let contentType = MediaTypes.parse(mediaType)
                                if(let p: DataMultipartTuples <- parts){
                                    p.add(name, contentType.toData(value))
                                }else if(let p: DataMultipartList <- parts){
                                    p.add(contentType.toData(value))
                                }
                            }else if(let p: DataMultipartTuples <- parts){
                                p.add(name, value.toData())
                            }else if(let p: DataMultipartList <- parts){
                                p.add(value.toData())
                            }
                        }else{
                            let part = MultipartFile(contentDisposition, ByteBuffer(buf), charset: contentDisposition.getCharset() ?? Charsets.UTF8)
                            part.setSize(buf.size)
                            if(let p: DataMultipartTuples <- parts){
                                p.add(contentDisposition.getName(), part)
                            }else if(let p: DataMultipartList <- parts){
                                p.add(part)
                            }
                        }
                    }else if (filename.isEmpty()){
                        let byteList = ArrayList<Byte>()
                        buf = Array<Byte>(1024, repeat: 0)
                        while(let (len, _, _) <- stream.read(buf)){
                            byteList.add(all: buf[0 .. len])
                            if(len < buf.size){
                                break
                            }
                        }
                        let bytes = byteList.unsafeData()
                        let value = if(let Some(charset) <- contentDisposition.getCharset()){
                            charset.newDecoder().decode(bytes)
                        }else{
                            String.fromUtf8(bytes)
                        }
                        let name = contentDisposition.getName()
                        if(let mediaType <- partHeader[1].replace('Content-Type:', '').trimAscii() && mediaType.size > 0){
                            let contentType = MediaTypes.parse(mediaType)
                            if(let p: DataMultipartTuples <- parts){
                                p.add(name, contentType.toData(value))
                            }else if(let p: DataMultipartList <- parts){
                                p.add(contentType.toData(value))
                            }
                        }else if(let p: DataMultipartTuples <- parts){
                            p.add(name, value.toData())
                        }else if(let p: DataMultipartList <- parts){
                            p.add(value.toData())
                        }
                    }else{
                        Directory.create(path, recursive: true)
                        let file = File(path.join(filename), ReadWrite)
                        buf = Array<Byte>(1024, repeat: 0)
                        var l = 0
                        while(let (len, _, _) <- stream.read(buf)){
                            l += len
                            if(len < buf.size){
                                file.write(buf[0..len])
                                break
                            }else{
                                file.write(buf)
                            }
                        }
                        file.seek(Begin(0))
                        let part = MultipartFile(contentDisposition, file, charset: contentDisposition.getCharset()?? Charsets.UTF8)
                        part.setSize(file.length)
                        if(let p: DataMultipartTuples <- parts){
                            p.add(contentDisposition.getName(), part)
                        }else if(let p: DataMultipartList <- parts){
                            p.add(part)
                        }
                    }
                case (b'-', b'-') => 
                    break
                case _ => throw MultipartException()
            }
        }
        parts
    }
}
public interface DataMultiparts<T> <: Data {
    func add(value: T): Unit
}
public struct DataMultipartTuples <: DataMultiparts<(String, Data)> & Iterable<(String, Data)>{
    private let map = HashMap<String, ArrayList<Data>>()

    public func add(value: (String, Data)): Unit {
        add(value[0], value[1])
    }
    public func add(name: String, input: Data): Unit {
        map.computeIfAbsent(name){ArrayList<Data>()}.add(input)
    }
    public func get(name: String){
        match(map.get(name)){
            case Some(x) where x.size == 1 => x[0]
            case Some(x) => DataIterable(x)
            case _ => DataNone.INSTANCE 
        }
    }
    public func iterator(): Iterator<(String, Data)> {
        DataMultipartTuplesIterator(map.iterator())
    }
    public static func tryParse(_: String): ?Data {
        throw IllegalAccessException()
    }
    public static func parse(_: String): Data {
        throw IllegalAccessException()
    }
    public static func tryFromData(data: Data, flag: DataConversionFlag): Any {
        throw IllegalAccessException()
    }
    public func toString(): String {
        throw IllegalAccessException()
    }
}
public struct DataMultipartList <: DataMultiparts<Data> & Iterable<Data>{
    private let list = ArrayList<Data>()

    public func add(input: Data){
        list.add(input)
    }
    public func get(index: Int64){
        list.get(index)
    }
    public func iterator(): Iterator<Data> {
        DataMultipartListIterator(list.iterator())
    }
    public static func tryParse(_: String): ?Data {
        throw IllegalAccessException()
    }
    public static func parse(_: String): Data {
        throw IllegalAccessException()
    }
    public static func tryFromData(data: Data, flag: DataConversionFlag): Any {
        throw IllegalAccessException()
    }
    public func toString(): String {
        throw IllegalAccessException()
    }
}
public class DataMultipartTuplesIterator <: Iterator<(String, Data)>{
    DataMultipartTuplesIterator(private let itr: Iterator<(String, ArrayList<Data>)>){}

    public func next(): ?(String, Data) {
        while(let Some((k, v)) <- itr.next()){
            return if(v.isEmpty()){
                continue
            }else if(v.size == 1){
                if(let l: DataMultipartList <- v[0]){
                    (k, DataIterable(l))
                }else{
                    (k, v[0])
                }
            }else{
                (k, DataIterable(v))
            }
        }
        None<(String, Data)>
    }
}
public class DataMultipartListIterator <: Iterator<Data> {
    DataMultipartListIterator(private let itr: Iterator<Data>){}

    public func next(): ?Data {
        while(let Some(d) <- itr.next()){
            d
        }
        None<Data>
    }
}