package triad package http import curl._ import curlh._ import scala.collection.{Map, mutable} import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future import scala.scalanative.native._ import scala.util.{Failure, Success, Try} object CurlBackend { type Chunk = CStruct4[Ptr[CStruct0], Ptr[CStruct0], CSize, Ptr[Byte]] implicit class ChunksOps(val self: Ptr[Chunk]) extends AnyVal { @inline def prev: Ptr[Chunk] = (!self._1).cast[Ptr[Chunk]] @inline def prev_=(other: Ptr[Chunk]): Unit = !(self._1) = other.cast[Ptr[CStruct0]] @inline def next: Ptr[Chunk] = (!self._2).cast[Ptr[Chunk]] @inline def next_=(other: Ptr[Chunk]): Unit = !(self._2) = other.cast[Ptr[CStruct0]] @inline def size: CSize = !(self._3) @inline def size_=(value: CSize): Unit = !(self._3) = value @inline def buffer: Ptr[Byte] = !(self._4) @inline def buffer_=(value: Ptr[Byte]): Unit = !(self._4) = value } object Chunk { @inline def NullPtr[T] = 0.cast[Ptr[T]] def allocHead() = allocAppend(0, NullPtr[Chunk]) def allocAppend(size: CSize, head: Ptr[Chunk] = NullPtr[Chunk]): Ptr[Chunk] = { val chunk: Ptr[Chunk] = stdlib.malloc(sizeof[Chunk]).cast[Ptr[Chunk]] if (chunk == NullPtr[Chunk]) return NullPtr[Chunk] chunk.buffer = stdlib.malloc(size) if (chunk.buffer == NullPtr[Chunk] && size != 0) { stdlib.free(chunk.cast[Ptr[Byte]]) return NullPtr[Chunk] } chunk.size = size if (head == NullPtr[Chunk]) { // this will be the head chunk.next = chunk chunk.prev = chunk } else { val last = head.prev last.next = chunk chunk.prev = last head.prev = chunk chunk.next = head } chunk } def freeAll(head: Ptr[Chunk]): Unit = { var chunk: Ptr[Chunk] = head do { val next = chunk.next stdlib.free(chunk.buffer) stdlib.free(chunk.cast[Ptr[Byte]]) chunk = next } while (chunk != head) } def toArray(head: Ptr[Chunk]): Array[Byte] = { val buffer = new ArrayBuffer[Byte]() var chunk = head do { val next = chunk.next var i = 0l while (i < next.size) { buffer += next.buffer(i) i += 1 } chunk = next } while (chunk != head) buffer.toArray } def traverse(head: Ptr[Chunk])(fct: Array[Byte] => Unit) = { var chunk = head do { val next = chunk.next val buffer = new ArrayBuffer[Byte]() var i = 0l while (i < next.size) { buffer += next.buffer(i) i += 1 } chunk = next fct(buffer.toArray) } while (chunk != head) } } private def receive(data: Ptr[Byte], size: CSize, nmemb: CSize, userdata: Ptr[Byte]): CSize = { val head = userdata.cast[Ptr[Chunk]] val length = size * nmemb val chunk = Chunk.allocAppend(length, head) string.memcpy(chunk.buffer, data, chunk.size) chunk.size } private val receivePtr: WriteFunction = CFunctionPtr.fromFunction4(receive) private def chain[A](success: A)(calls: (() => A)*) = { var result: A = success for (c <- calls if result == success) { result = c() } result } private def request(request: Request)(implicit z: Zone): Try[Response] = { val curl: CURL = curl_easy_init() if (curl != null) { val errorBuffer = stackalloc[Byte](CURL_ERROR_SIZE) !errorBuffer = 0 val requestHeaders = stackalloc[curl_slist](1) !requestHeaders = 0.cast[curl_slist] val responseChunks = Chunk.allocHead() val responseHeaderChunks = Chunk.allocHead() val curlResult = chain(CURLcode.CURL_OK)( () => curl_easy_setopt(curl, CURLoption.CURLOPT_ERRORBUFFER, errorBuffer), () => curl_easy_setopt(curl, CURLoption.CURLOPT_CUSTOMREQUEST, toCString(request.method)), () => curl_easy_setopt(curl, CURLoption.CURLOPT_URL, toCString(request.url)), () => { val buffer = ArrayUtils.toBuffer(request.body) curl_easy_setopt(curl, CURLoption.CURLOPT_POSTFIELDS, buffer) curl_easy_setopt(curl, CURLoption.CURLOPT_POSTFIELDSIZE, request.body.size) }, () => { for ((k, v) <- request.headers) { !requestHeaders = curl_slist_append(!requestHeaders, toCString(s"$k:$v")) } curl_easy_setopt(curl, CURLoption.CURLOPT_HTTPHEADER, !requestHeaders) }, () => curl_easy_setopt(curl, CURLoption.CURLOPT_WRITEFUNCTION, receivePtr), () => curl_easy_setopt(curl, CURLoption.CURLOPT_WRITEDATA, responseChunks), () => curl_easy_setopt(curl, CURLoption.CURLOPT_HEADERDATA, responseHeaderChunks), () => curl_easy_perform(curl) ) val result = curlResult match { case CURLcode.CURL_OK => val responseCode: Ptr[Long] = stackalloc[Long](1) curl_easy_getinfo(curl, CURLINFO.CURLINFO_RESPONSE_CODE, responseCode) val responseHeaders = mutable.HashMap.empty[String, String] Chunk.traverse(responseHeaderChunks) { headerChunk => val line = new String(headerChunk, "utf-8").trim if (line.contains(":")) { val parts = line.split(":", 2) responseHeaders += parts(0) -> parts(1) } } Success( Response( statusCode = (!responseCode).toInt, headers = responseHeaders.toMap, body = Chunk.toArray(responseChunks) )) case code => val message = curl_easy_strerror(curl, code) Failure( new RuntimeException( s"${fromCString(errorBuffer)} (curl exit status $code)")) } Chunk.freeAll(responseChunks) Chunk.freeAll(responseHeaderChunks) curl_slist_free_all(!requestHeaders) curl_easy_cleanup(curl) result } else { Failure(new RuntimeException(s"curl failed to initialize")) } } def curlVersion = fromCString(curl_version()) } trait CurlBackend extends Backend { def send(req: Request): Future[Response] = Zone { implicit z => Future.fromTry(CurlBackend.request(req)) } }