diff --git a/.travis.yml b/.travis.yml index 098c160..878383e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,23 +9,17 @@ env: matrix: include: - - compiler: ": Lua51-osx" - env: LUA="lua 5.1" + - env: LUA="lua 5.1" os: osx - - compiler: ": Lua51" - env: LUA="lua 5.1" + - env: LUA="lua 5.1" os: linux - - compiler: ": Lua52" - env: LUA="lua 5.2" + - env: LUA="lua 5.2" os: linux - - compiler: ": Lua53" - env: LUA="lua 5.3" + - env: LUA="lua 5.3" os: linux - - compiler: ": LuaJIT20" - env: LUA="luajit 2.0" + - env: LUA="luajit 2.0" os: linux - - compiler: ": LuaJIT21" - env: LUA="luajit 2.1" + - env: LUA="luajit 2.1" os: linux cache: @@ -36,25 +30,35 @@ cache: branches: only: - master + - curl_mime before_install: - - export CC=gcc - - gcc --version - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export PATH=$PATH:~/Library/Python/2.7/bin/; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export LCURL_LD_FLAGS="-bundle -undefined dynamic_lookup -all_load --coverage"; fi - - pip install --user cpp-coveralls - - pip install --user hererocks + - pip2 install --user cpp-coveralls + - pip2 install --user hererocks - hererocks here -r^ --$LUA - source here/bin/activate + - luarocks show lluv > /dev/null 2>&1 || bash .travis/setup_uv.sh install: - luarocks make rockspecs/lua-curl-scm-0.rockspec CFLAGS="$LCURL_CC_FLAGS" LIBFLAG="$LCURL_LD_FLAGS" before_script: - luarocks show luacov-coveralls > /dev/null 2>&1 || luarocks install luacov-coveralls - - luarocks show lunitx > /dev/null 2>&1 || luarocks install lunitx - - luarocks show luafilesystem > /dev/null 2>&1 || luarocks install luafilesystem - - luarocks show dkjson > /dev/null 2>&1 || luarocks install dkjson --deps-mode=none + - luarocks show lunitx > /dev/null 2>&1 || luarocks install lunitx + - luarocks show luafilesystem > /dev/null 2>&1 || luarocks install luafilesystem + - luarocks show dkjson > /dev/null 2>&1 || luarocks install dkjson --deps-mode=none + - luarocks show pegasus > /dev/null 2>&1 || luarocks install pegasus http.parser + --server=http://luarocks.org/manifests/moteus + - luarocks show pegasus-router > /dev/null 2>&1 || luarocks install pegasus-router + --server=http://luarocks.org/dev + - luarocks show lluv > /dev/null 2>&1 || luarocks install lluv UV_DIR=$TRAVIS_BUILD_DIR/libuv + --server=http://luarocks.org/dev + - luarocks show lluv-pegasus > /dev/null 2>&1 || luarocks install lluv-pegasus + --server=http://luarocks.org/dev --deps-mode=none + - lua test/server.lua & + - curl -s http://127.0.0.1:7090/get script: - cd test @@ -63,14 +67,12 @@ script: - lua test_pause02.c.lua - lua test_multi_callback.lua - lua test_multi_nested_callback.lua - # - lunit.sh test_easy.lua - # - lunit.sh test_safe.lua - # - lunit.sh test_form.lua - # - lunit.sh test_curl.lua -after_success: +before_cache: - coveralls -b .. -r .. --dump c.report.json - luacov-coveralls -j c.report.json -v + - luarocks remove lua-curl + - rm -f /home/travis/.cache/pip/log/debug.log notifications: email: diff --git a/.travis/platform.sh b/.travis/platform.sh new file mode 100644 index 0000000..7259a7d --- /dev/null +++ b/.travis/platform.sh @@ -0,0 +1,15 @@ +if [ -z "${PLATFORM:-}" ]; then + PLATFORM=$TRAVIS_OS_NAME; +fi + +if [ "$PLATFORM" == "osx" ]; then + PLATFORM="macosx"; +fi + +if [ -z "$PLATFORM" ]; then + if [ "$(uname)" == "Linux" ]; then + PLATFORM="linux"; + else + PLATFORM="macosx"; + fi; +fi diff --git a/.travis/setup_uv.sh b/.travis/setup_uv.sh new file mode 100644 index 0000000..34eb88e --- /dev/null +++ b/.travis/setup_uv.sh @@ -0,0 +1,25 @@ +#! /bin/bash + +source .travis/platform.sh + +cd $TRAVIS_BUILD_DIR + +git clone https://github.com/libuv/libuv.git -b v1.x + +cd libuv + +git checkout v1.19.0 + +mkdir -p lib +mkdir -p build +git clone https://chromium.googlesource.com/external/gyp build/gyp + +if [ "$PLATFORM" == "macosx" ]; then + ./gyp_uv.py -f xcode && xcodebuild -ARCHS="x86_64" -project uv.xcodeproj -configuration Release -target All + cp ./build/Release/libuv.a ./lib; +else + ./gyp_uv.py -f make && BUILDTYPE=Release CFLAGS=-fPIC make -C out + cp ./out/Release/libuv.a ./lib; +fi + +cd $TRAVIS_BUILD_DIR diff --git a/appveyor.yml b/appveyor.yml index f3446e4..1fdfb3d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ shallow_clone: true environment: LR_EXTERNAL: c:\external - CURL_VER: 7.55.1 + CURL_VER: 7.59.0 matrix: - LUA: "lua 5.1" @@ -39,6 +39,7 @@ install: hererocks c:\hererocks --%LUA% --target %HR_TARGET% -rlatest ) - call c:\hererocks\bin\activate + - luarocks show luarocks-fetch-gitrec >nul 2>&1 || luarocks install luarocks-fetch-gitrec before_build: # external deps @@ -57,6 +58,12 @@ before_test: - luarocks show dkjson >nul 2>&1 || luarocks install dkjson - luarocks show luafilesystem >nul 2>&1 || luarocks install luafilesystem - luarocks show lua-path >nul 2>&1 || luarocks install lua-path + - luarocks show pegasus >nul 2>&1 || luarocks install pegasus http.parser + --server=http://luarocks.org/manifests/moteus + - luarocks show pegasus-router >nul 2>&1 || luarocks install pegasus-router + --server=http://luarocks.org/dev + - ps: $TestServer = Start-Process lua -ArgumentList test/server.lua -RedirectStandardOutput "$env:APPVEYOR_BUILD_FOLDER\server.stdout.txt" -RedirectStandardError "$env:APPVEYOR_BUILD_FOLDER\server.stderr.txt" -PassThru + - curl -s http://127.0.0.1:7090/get test_script: - echo "Testing..." @@ -69,3 +76,12 @@ test_script: after_test: - cd %APPVEYOR_BUILD_FOLDER% - .appveyor\pack_artifact.bat lua-curl bin-rock + +on_failure: + - ps: Stop-Process -Id $TestServer.Id + - ps: $path = "$env:APPVEYOR_BUILD_FOLDER\server.stderr.txt"; if (Test-Path $path -PathType Leaf) { Push-AppveyorArtifact $path; } else { echo "File $path does not exist"; } + - ps: $path = "$env:APPVEYOR_BUILD_FOLDER\server.stdout.txt"; if (Test-Path $path -PathType Leaf) { Push-AppveyorArtifact $path; } else { echo "File $path does not exist"; } + +on_finish: + - curl -s http://127.0.0.1:7090/get + - ps: Stop-Process -Id $TestServer.Id diff --git a/examples/lcurl/smtp-mime.lua b/examples/lcurl/smtp-mime.lua new file mode 100644 index 0000000..f829726 --- /dev/null +++ b/examples/lcurl/smtp-mime.lua @@ -0,0 +1,85 @@ +local curl = require "lcurl" + +local SMTP = { + url = "smtp://mail.example.com"; +} + +local FROM = "" +local TO = "" +local CC = "" +local FILE = "smtp-mime.lua" -- if you send this file do not forget it may have mail password +local CT_FILE = "application/lua" + +local DUMP_MIME = false + +local headers = { + "Date: Tue, 22 Aug 2017 14:08:43 +0100", + "To: " .. TO, + "From: " .. FROM .. " (Example User)", + "Cc: " .. CC .. " (Another example User)", + "Message-ID: ", + "Subject: example sending a MIME-formatted message", +} + +local inline_text = "" + .. "This is the inline text message of the e-mail.\r\n" + .. "\r\n" + .. " It could be a lot of lines that would be displayed in an e-mail\r\n" + .. "viewer that is not able to handle HTML.\r\n" + +local inline_html = "" + .. "\r\n" + .. "

This is the inline HTML message of the e-mail.

" + .. "
\r\n" + .. "

It could be a lot of HTML data that would be displayed by " + .. "e-mail viewers able to handle HTML.

" + .. "\r\n" + +local function dump_mime(type, data) + if type == curl.INFO_DATA_OUT then io.write(data) end +end + +local easy = curl.easy() + +local mime = easy:mime() do + local alt = easy:mime() + alt + :addpart() + :data(inline_html, "text/html") + alt + :addpart() + :data(inline_text) + mime:addpart() + :subparts(alt, "multipart/alternative", { + "Content-Disposition: inline" + }) + mime + :addpart() + :filedata(FILE, CT_FILE) +end + +easy:setopt{ + url = SMTP.url, + mail_from = FROM, + mail_rcpt = {TO, CC}, + httpheader = headers; + mimepost = mime; + ssl_verifyhost = false; + ssl_verifypeer = false; + username = SMTP.user; + password = SMTP.password; + upload = true; +} + +if DUMP_MIME then + easy:setopt{ + verbose = true; + debugfunction = dump_mime; + } +end + +easy:perform() + +easy:close() + +mime:free() diff --git a/lakefile b/lakefile index 2c4a123..55c77a0 100644 --- a/lakefile +++ b/lakefile @@ -32,6 +32,7 @@ target('test', install, function() run_test('test_form.lua') run_test('test_pause02.c.lua') run_test('test_curl.lua') + run_test('test_mime.lua') run_test('test_multi_callback.lua') run_test('test_multi_nested_callback.lua') diff --git a/msvc/lcurl.vcproj b/msvc/lcurl.vcproj index 6d58985..7c22f2e 100644 --- a/msvc/lcurl.vcproj +++ b/msvc/lcurl.vcproj @@ -1,7 +1,7 @@ + + @@ -251,6 +255,10 @@ RelativePath="..\src\lcinfoeasy.h" > + + diff --git a/rockspecs/lua-curl-scm-0.rockspec b/rockspecs/lua-curl-scm-0.rockspec index 22f5a00..2f284a8 100644 --- a/rockspecs/lua-curl-scm-0.rockspec +++ b/rockspecs/lua-curl-scm-0.rockspec @@ -63,7 +63,7 @@ build = { sources = { "src/l52util.c", "src/lceasy.c", "src/lcerror.c", "src/lchttppost.c", "src/lcurl.c", "src/lcutils.c", - "src/lcmulti.c", "src/lcshare.c", + "src/lcmulti.c", "src/lcshare.c","src/lcmime.c", }, incdirs = { "$(CURL_INCDIR)" }, libdirs = { "$(CURL_LIBDIR)" } diff --git a/src/lceasy.c b/src/lceasy.c index f3a7cac..683c041 100644 --- a/src/lceasy.c +++ b/src/lceasy.c @@ -1,7 +1,7 @@ /****************************************************************************** * Author: Alexey Melnichuk * -* Copyright (C) 2014 Alexey Melnichuk +* Copyright (C) 2014-2018 Alexey Melnichuk * * Licensed according to the included 'LICENSE' document * @@ -15,6 +15,7 @@ #include "lchttppost.h" #include "lcshare.h" #include "lcmulti.h" +#include "lcmime.h" #include static const char *LCURL_ERROR_TAG = "LCURL_ERROR_TAG"; @@ -29,7 +30,7 @@ static const char *LCURL_EASY = LCURL_EASY_NAME; #endif /* Before call curl_XXX function which can call any callback - * need set Curren Lua thread pointer in easy/multi contexts. + * need set Current Lua thread pointer in easy/multi contexts. * But it also possible that we already in callback call. * E.g. `curl_easy_pause` function may be called from write callback. * and it even may be called in different thread. @@ -41,7 +42,7 @@ static const char *LCURL_EASY = LCURL_EASY_NAME; * ``` * So we have to restore previews Lua state in callback contexts. * But if previews Lua state is NULL then we can just do not set it back. - * But set it to NULL make esier debug code. + * But set it to NULL make easier to debug code. */ void lcurl__easy_assign_lua(lua_State *L, lcurl_easy_t *p, lua_State *value, int assign_multi){ if(p->multi && assign_multi){ @@ -52,6 +53,11 @@ void lcurl__easy_assign_lua(lua_State *L, lcurl_easy_t *p, lua_State *value, int if(p->post){ p->post->L = value; } +#if LCURL_CURL_VER_GE(7,56,0) + if(p->mime){ + lcurl_mime_set_lua(L, p->mime, value); + } +#endif } } @@ -74,6 +80,9 @@ int lcurl_easy_create(lua_State *L, int error_mode){ p->L = NULL; p->post = NULL; p->multi = NULL; +#if LCURL_CURL_VER_GE(7,56,0) + p->mime = NULL; +#endif p->storage = lcurl_storage_init(L); p->wr.cb_ref = p->wr.ud_ref = LUA_NOREF; p->rd.cb_ref = p->rd.ud_ref = LUA_NOREF; @@ -125,6 +134,8 @@ static int lcurl_easy_cleanup(lua_State *L){ // In my tests when I cleanup some easy handle. // timerfunction called only for single multi handle. + // Also may be this function may call `close` callback + // for `curl_mimepart` structure. curL = p->L; lcurl__easy_assign_lua(L, p, L, 1); curl_easy_cleanup(p->curl); #ifndef LCURL_RESET_NULL_LUA @@ -135,6 +146,11 @@ static int lcurl_easy_cleanup(lua_State *L){ p->curl = NULL; } + p->post = NULL; +#if LCURL_CURL_VER_GE(7,56,0) + p->mime = NULL; +#endif + if(p->storage != LUA_NOREF){ p->storage = lcurl_storage_free(L, p->storage); } @@ -266,6 +282,15 @@ static int lcurl_easy_reset(lua_State *L){ return 1; } +#if LCURL_CURL_VER_GE(7,56,0) + +static int lcurl_easy_mime(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_mime_create(L, p->err_mode); +} + +#endif + //{ OPTIONS //{ set @@ -447,6 +472,26 @@ static int lcurl_easy_set_STREAM_DEPENDS_E(lua_State *L){ #endif +#if LCURL_CURL_VER_GE(7,56,0) + +static int lcurl_easy_set_MIMEPOST(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + lcurl_mime_t *mime = lcurl_getmime_at(L, 2); + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_MIMEPOST, mime->mime); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_preserve_iv(L, p->storage, CURLOPT_MIMEPOST, 2); + + p->mime = mime; + + lua_settop(L, 1); + return 1; +} + +#endif + //} //{ unset @@ -775,6 +820,25 @@ static int lcurl_easy_unset_STREAM_DEPENDS_E(lua_State *L){ #endif +#if LCURL_CURL_VER_GE(7,56,0) + +static int lcurl_easy_unset_MIMEPOST(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_MIMEPOST, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_remove_i(L, p->storage, CURLOPT_MIMEPOST); + + p->mime = NULL; + + lua_settop(L, 1); + return 1; +} + +#endif + //} //} @@ -1004,7 +1068,7 @@ static int lcurl_easy_set_WRITEFUNCTION(lua_State *L){ //{ Reader -static size_t lcurl_read_callback(lua_State *L, +size_t lcurl_read_callback(lua_State *L, lcurl_callback_t *rd, lcurl_read_buffer_t *rbuffer, char *buffer, size_t size, size_t nitems ){ @@ -1477,6 +1541,9 @@ static int lcurl_easy_setopt(lua_State *L){ #if LCURL_CURL_VER_GE(7,46,0) OPT_ENTRY(stream_depends, STREAM_DEPENDS, TTT, 0, 0) OPT_ENTRY(stream_depends_e, STREAM_DEPENDS_E, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,56,0) + OPT_ENTRY(mimepost, MIMEPOST, TTT, 0, 0) #endif } #undef OPT_ENTRY @@ -1511,6 +1578,9 @@ static int lcurl_easy_unsetopt(lua_State *L){ #if LCURL_CURL_VER_GE(7,46,0) OPT_ENTRY(stream_depends, STREAM_DEPENDS, TTT, 0, 0) OPT_ENTRY(stream_depends_e, STREAM_DEPENDS_E, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,56,0) + OPT_ENTRY(mimepost, MIMEPOST, TTT, 0, 0) #endif } #undef OPT_ENTRY @@ -1582,7 +1652,7 @@ static const struct luaL_Reg lcurl_easy_methods[] = { OPT_ENTRY(seekfunction, SEEKFUNCTION, TTT, 0, 0) OPT_ENTRY(debugfunction, DEBUGFUNCTION, TTT, 0, 0) #if LCURL_CURL_VER_GE(7,21,0) - OPT_ENTRY(fnmatch_function, FNMATCH_FUNCTION, TTT, 0, 0) + OPT_ENTRY(fnmatch_function, FNMATCH_FUNCTION, TTT, 0, 0) OPT_ENTRY(chunk_bgn_function, CHUNK_BGN_FUNCTION, TTT, 0, 0) OPT_ENTRY(chunk_end_function, CHUNK_END_FUNCTION, TTT, 0, 0) #endif @@ -1590,6 +1660,9 @@ static const struct luaL_Reg lcurl_easy_methods[] = { OPT_ENTRY(stream_depends, STREAM_DEPENDS, TTT, 0, 0) OPT_ENTRY(stream_depends_e, STREAM_DEPENDS_E, TTT, 0, 0) #endif +#if LCURL_CURL_VER_GE(7,56,0) + OPT_ENTRY(mimepost, MIMEPOST, TTT, 0, 0) +#endif #undef OPT_ENTRY #define OPT_ENTRY(L, N, T, S, D) { "unsetopt_"#L, lcurl_easy_unset_##N }, @@ -1604,7 +1677,7 @@ static const struct luaL_Reg lcurl_easy_methods[] = { OPT_ENTRY(seekfunction, SEEKFUNCTION, TTT, 0, 0) OPT_ENTRY(debugfunction, DEBUGFUNCTION, TTT, 0, 0) #if LCURL_CURL_VER_GE(7,21,0) - OPT_ENTRY(fnmatch_function, FNMATCH_FUNCTION, TTT, 0, 0) + OPT_ENTRY(fnmatch_function, FNMATCH_FUNCTION, TTT, 0, 0) OPT_ENTRY(chunk_bgn_function, CHUNK_BGN_FUNCTION, TTT, 0, 0) OPT_ENTRY(chunk_end_function, CHUNK_END_FUNCTION, TTT, 0, 0) #endif @@ -1612,12 +1685,19 @@ static const struct luaL_Reg lcurl_easy_methods[] = { OPT_ENTRY(stream_depends, STREAM_DEPENDS, TTT, 0, 0) OPT_ENTRY(stream_depends_e, STREAM_DEPENDS_E, TTT, 0, 0) #endif +#if LCURL_CURL_VER_GE(7,56,0) + OPT_ENTRY(mimepost, MIMEPOST, TTT, 0, 0) +#endif #undef OPT_ENTRY #define OPT_ENTRY(L, N, T, S) { "getinfo_"#L, lcurl_easy_get_##N }, #include "lcinfoeasy.h" #undef OPT_ENTRY +#if LCURL_CURL_VER_GE(7,56,0) + { "mime", lcurl_easy_mime }, +#endif + { "pause", lcurl_easy_pause }, { "reset", lcurl_easy_reset }, { "setopt", lcurl_easy_setopt }, @@ -1659,6 +1739,9 @@ static const lcurl_const_t lcurl_easy_opt[] = { OPT_ENTRY(stream_depends, STREAM_DEPENDS, TTT, 0, 0) OPT_ENTRY(stream_depends_e, STREAM_DEPENDS_E, TTT, 0, 0) #endif +#if LCURL_CURL_VER_GE(7,56,0) + OPT_ENTRY(mimepost, MIMEPOST, TTT, 0, 0) +#endif #undef OPT_ENTRY #undef FLG_ENTRY diff --git a/src/lceasy.h b/src/lceasy.h index e3275f1..916f53d 100644 --- a/src/lceasy.h +++ b/src/lceasy.h @@ -1,7 +1,7 @@ /****************************************************************************** * Author: Alexey Melnichuk * -* Copyright (C) 2014 Alexey Melnichuk +* Copyright (C) 2014-2017 Alexey Melnichuk * * Licensed according to the included 'LICENSE' document * @@ -37,9 +37,16 @@ enum { #if LCURL_CC_SUPPORT_FORWARD_TYPEDEF typedef struct lcurl_multi_tag lcurl_multi_t; +#if LCURL_CURL_VER_GE(7,56,0) +typedef struct lcurl_mime_tag lcurl_mime_t; +#endif #else struct lcurl_multi_tag; #define lcurl_multi_t struct lcurl_multi_tag +#if LCURL_CURL_VER_GE(7,56,0) +struct lcurl_mime_tag; +#define lcurl_mime_t struct lcurl_mime_tag; +#endif #endif typedef struct lcurl_easy_tag{ @@ -53,6 +60,10 @@ typedef struct lcurl_easy_tag{ lcurl_multi_t *multi; +#if LCURL_CURL_VER_GE(7,56,0) + lcurl_mime_t *mime; +#endif + CURL *curl; int storage; int lists[LCURL_LIST_COUNT]; @@ -77,8 +88,16 @@ void lcurl_easy_initlib(lua_State *L, int nup); void lcurl__easy_assign_lua(lua_State *L, lcurl_easy_t *p, lua_State *value, int assign_multi); +size_t lcurl_read_callback(lua_State *L, + lcurl_callback_t *rd, lcurl_read_buffer_t *rbuffer, + char *buffer, size_t size, size_t nitems +); + #if !LCURL_CC_SUPPORT_FORWARD_TYPEDEF #undef lcurl_multi_t +#ifdef lcurl_mime_t +#undef lcurl_mime_t +#endif #endif #endif diff --git a/src/lchttppost.c b/src/lchttppost.c index 0200339..1f5b3bf 100644 --- a/src/lchttppost.c +++ b/src/lchttppost.c @@ -44,6 +44,19 @@ static const char *LCURL_HTTPPOST = LCURL_HTTPPOST_NAME; # define LCURL_LEN_TYPE long #endif +/* 7.56.0 changed code for `curl_formget` if callback abort write. + * + * https://github.com/curl/curl/issues/1987#issuecomment-336139060 + * ... not sure its worth the effort to document its return codes to + * any further extent then it currently is. This function is very + * rarely used, and the new mime API doesn't even have a version of it. + **/ +#if LCURL_CURL_VER_GE(7,56,0) +# define LCURL_GET_CB_ERROR CURLE_READ_ERROR +#else +# define LCURL_GET_CB_ERROR (CURLcode)-1 +#endif + //{ stream static lcurl_hpost_stream_t *lcurl_hpost_stream_add(lua_State *L, lcurl_hpost_t *p){ @@ -525,7 +538,7 @@ static int lcurl_hpost_get(lua_State *L){ return lua_error(L); } - if((CURLcode)-1 == code){ + if(LCURL_GET_CB_ERROR == code){ if(((lua_gettop(L) == top+1))&&(lua_isstring(L, -1))){ return lua_error(L); } diff --git a/src/lcmime.c b/src/lcmime.c new file mode 100644 index 0000000..06c3550 --- /dev/null +++ b/src/lcmime.c @@ -0,0 +1,686 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2017-2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of lua-lcurl library. +******************************************************************************/ + +#include "lcurl.h" +#include "lcmime.h" +#include "lceasy.h" +#include "lcerror.h" +#include "lcutils.h" + +/* API Notes. + * 1. Each mime can be root or child. If mime is a child (subpart) then curl free it + * when parent mime is freed or when remove this part from parent. There no way reuse same mime. + * Its not clear is it possible use mime created by one easy handle when do preform in another. + * `m=e1:mime() e2:setopt_httpmime(m) e1:close() e2:perform()` + * + * // Attach child to root (root also can have parent) + * curl_mime_subparts(root, child); + * + * // curl free `child` and all its childs + * curl_mime_subparts(root, other_child_or_null); + * + * // forbidden + * curl_mime_free(child); + */ + +#if LCURL_CURL_VER_GE(7,56,0) + +#define LCURL_MIME_NAME LCURL_PREFIX" MIME" +static const char *LCURL_MIME = LCURL_MIME_NAME; + +#define LCURL_MIME_PART_NAME LCURL_PREFIX" MIME Part" +static const char *LCURL_MIME_PART = LCURL_MIME_PART_NAME; + +//{ Free mime and subparts + +static void lcurl_mime_part_remove_subparts(lua_State *L, lcurl_mime_part_t *p, int free_it); + +static lcurl_mime_t* lcurl_mime_part_get_subparts(lua_State *L, lcurl_mime_part_t *part){ + lcurl_mime_t *sub = NULL; + + if(LUA_NOREF != part->subpart_ref){ + lua_rawgeti(L, LCURL_LUA_REGISTRY, part->subpart_ref); + sub = lcurl_getmime_at(L, -1); + lua_pop(L, 1); + } + + return sub; +} + +static int lcurl_mime_part_reset(lua_State *L, lcurl_mime_part_t *p){ + p->part = NULL; + + luaL_unref(L, LCURL_LUA_REGISTRY, p->rd.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->rd.ud_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->rbuffer.ref); + + p->headers_ref = p->rbuffer.ref = p->rd.cb_ref = p->rd.ud_ref = LUA_NOREF; + + /*free only if we have no parents*/ + lcurl_mime_part_remove_subparts(L, p, 0); + + return 0; +} + +static int lcurl_mime_reset(lua_State *L, lcurl_mime_t *p){ + lcurl_mime_part_t *ptr; + + /* reset all parts*/ + for(ptr = p->parts; ptr; ptr=ptr->next){ + lcurl_mime_part_reset(L, ptr); + } + + if(LUA_NOREF != p->storage){ + p->storage = lcurl_storage_free(L, p->storage); + } + + p->parts = p->parent = NULL; + p->mime = NULL; + + /* remove weak reference to easy */ + lua_pushnil(L); + lua_rawsetp(L, LCURL_MIME_EASY, p); + + return 0; +} + +static void lcurl_mime_part_remove_subparts(lua_State *L, lcurl_mime_part_t *p, int free_it){ + lcurl_mime_t *sub = lcurl_mime_part_get_subparts(L, p); + if(sub){ + assert(LUA_NOREF != p->subpart_ref); + /* detach `subpart` mime from current mime part */ + /* if set `sub->parent = NULL` then gc for mime will try free curl_mime_free. */ + + luaL_unref(L, LCURL_LUA_REGISTRY, p->subpart_ref); + p->subpart_ref = LUA_NOREF; + + if(p->part && free_it){ + curl_mime_subparts(p->part, NULL); + } + + /* seems curl_mime_subparts(h, NULL) free asubparts. + so we have to invalidate all reference to all nested objects (part/mime). + NOTE. All resources already feed. So just need set all pointers to NULL + and free all Lua resources (like references and storages) + */ + { + lcurl_mime_part_t *ptr; + /* reset all parts*/ + for(ptr = sub->parts; ptr; ptr=ptr->next){ + lcurl_mime_part_remove_subparts(L, p, 0); + } + lcurl_mime_reset(L, sub); + } + } +} + +//} + +int lcurl_mime_set_lua(lua_State *L, lcurl_mime_t *p, lua_State *v){ + lcurl_mime_part_t *part; + for(part = p->parts; part; part=part->next){ + lcurl_mime_t *sub = lcurl_mime_part_get_subparts(L, part); + if(sub) lcurl_mime_set_lua(L, sub, v); + part->L = v; + } + return 0; +} + +#define IS_NILORSTR(L, i) (lua_type(L, i) == LUA_TSTRING) || (lua_type(L, i) == LUA_TNIL) +#define IS_TABLE(L, i) lua_type(L, i) == LUA_TTABLE +#define IS_FALSE(L, i) (lua_type(L, i) == LUA_TBOOLEAN) && (!lua_toboolean(L, i)) +#define IS_OPTSTR(L, i) (IS_FALSE(L, i)) || (IS_NILORSTR(L, i)) + +static int lutil_isarray(lua_State *L, int i){ + int ret = 0; + i = lua_absindex(L, i); + lua_pushnil(L); + if(lua_next(L, i)){ + ret = lua_isnumber(L, -2); + lua_pop(L, 2); + } + return ret; +} + +static int lcurl_mime_part_assign(lua_State *L, int part, const char *method){ + int top = lua_gettop(L); + + lua_pushvalue(L, part); + lua_insert(L, -2); + lua_getfield(L, -2, method); + lua_insert(L, -3); + lua_call(L, 2, LUA_MULTRET); + + return lua_gettop(L) - top + 1; +} + +static const char *lcurl_mime_part_fields[] = { + "data", "filedata", "name", "filename", "headers", "encoder", "type", NULL +}; + +static int lcurl_mime_part_assing_table(lua_State *L, int part, int t){ + int top = lua_gettop(L); + const char *method; int i; + + part = lua_absindex(L, part); + t = lua_absindex(L, t); + + if(lutil_isarray(L, t)){ + int ret; + lua_pushvalue(L, t); + ret = lcurl_mime_part_assign(L, part, "headers"); + if(ret != 1) return ret; + + lua_pop(L, 1); + + assert(top == lua_gettop(L)); + } + else{ + for(i=0;method = lcurl_mime_part_fields[i]; ++i){ + lua_getfield(L, t, method); + if(!lua_isnil(L, -1)){ + int ret = lcurl_mime_part_assign(L, part, method); + if(ret != 1) return ret; + } + lua_pop(L, 1); + + assert(top == lua_gettop(L)); + } + + lua_getfield(L, t, "subparts"); + if(!lua_isnil(L, -1)){ + if(IS_FALSE(L, -1) || lcurl_getmime_at(L, -1)){ + int ret = lcurl_mime_part_assign(L, part, "subparts"); + if(ret != 1) return ret; + } + } + lua_pop(L, 1); + assert(top == lua_gettop(L)); + } + + return 0; +} + +//{ MIME + +static lcurl_mime_part_t* lcurl_mime_parts_append(lcurl_mime_t *m, lcurl_mime_part_t *p){ + if(!m->parts) m->parts = p; + else{ + lcurl_mime_part_t *ptr = m->parts; + while(ptr->next)ptr = ptr->next; + ptr->next = p; + } + return p; +} + +static lcurl_mime_part_t* lcurl_mime_parts_find(lcurl_mime_t *m, lcurl_mime_part_t *p){ + lcurl_mime_part_t *ptr; + + for(ptr = m->parts; ptr; ptr = ptr->next){ + if(ptr == p) return p; + } + + return NULL; +} + +int lcurl_mime_create(lua_State *L, int error_mode){ + //! @todo make this function as method of easy handle + lcurl_easy_t *e = lcurl_geteasy(L); + + lcurl_mime_t *p = lutil_newudatap(L, lcurl_mime_t, LCURL_MIME); + + p->mime = curl_mime_init(e->curl); + + //! @todo return more accurate error category/code + if(!p->mime) return lcurl_fail_ex(L, error_mode, LCURL_ERROR_EASY, CURLE_FAILED_INIT); + + p->storage = lcurl_storage_init(L); + p->err_mode = error_mode; + p->parts = p->parent = NULL; + + /* weak reference from mime to easy handle */ + lua_pushvalue(L, 1); + lua_rawsetp(L, LCURL_MIME_EASY, (void*)p); + + return 1; +} + +lcurl_mime_t *lcurl_getmime_at(lua_State *L, int i){ + lcurl_mime_t *p = (lcurl_mime_t *)lutil_checkudatap (L, i, LCURL_MIME); + luaL_argcheck (L, p != NULL, i, LCURL_MIME_NAME" object expected"); + luaL_argcheck (L, p->mime != NULL, i, LCURL_MIME_NAME" object freed"); + return p; +} + +static int lcurl_mime_to_s(lua_State *L){ + lcurl_mime_t *p = (lcurl_mime_t *)lutil_checkudatap (L, 1, LCURL_MIME); + luaL_argcheck (L, p != NULL, 1, LCURL_MIME_NAME" object expected"); + + lua_pushfstring(L, LCURL_MIME_NAME" (%p)%s", (void*)p, + p->mime ? (p->parent ? " (subpart)" : "") : " (freed)" + ); + return 1; +} + +static int lcurl_mime_free(lua_State *L){ + lcurl_mime_t *p = (lcurl_mime_t *)lutil_checkudatap (L, 1, LCURL_MIME); + luaL_argcheck (L, p != NULL, 1, LCURL_MIME_NAME" object expected"); + + if((p->mime) && (NULL == p->parent)){ + curl_mime_free(p->mime); + } + + return lcurl_mime_reset(L, p); +} + +static int lcurl_mime_addpart(lua_State *L){ + lcurl_mime_t *p = lcurl_getmime(L); + int ret; + + lua_settop(L, 2); + + ret = lcurl_mime_part_create(L, p->err_mode); + if(ret != 1) return ret; + + /* store mime part in storage */ + lcurl_storage_preserve_value(L, p->storage, lua_absindex(L, -1)); + lcurl_mime_parts_append(p, lcurl_getmimepart_at(L, -1)); + + if(lua_istable(L, 2)){ + ret = lcurl_mime_part_assing_table(L, 3, 2); + if(ret) return ret; + } + + return 1; +} + +static int lcurl_mime_easy(lua_State *L){ + lcurl_mime_t *p = lcurl_getmime(L); + lua_rawgetp(L, LCURL_MIME_EASY, p); + return 1; +} + +//} + +//{ MIME Part + +int lcurl_mime_part_create(lua_State *L, int error_mode){ + //! @todo make this function as method of mime handle + lcurl_mime_t *m = lcurl_getmime(L); + + lcurl_mime_part_t *p = lutil_newudatap(L, lcurl_mime_part_t, LCURL_MIME_PART); + + p->part = curl_mime_addpart(m->mime); + + //! @todo return more accurate error category/code + if(!p->part) return lcurl_fail_ex(L, error_mode, LCURL_ERROR_EASY, CURLE_FAILED_INIT); + + p->rbuffer.ref = p->rd.cb_ref = p->rd.ud_ref = LUA_NOREF; + p->err_mode = error_mode; + p->subpart_ref = p->headers_ref = LUA_NOREF; + p->parent = m; + + return 1; +} + +lcurl_mime_part_t *lcurl_getmimepart_at(lua_State *L, int i){ + lcurl_mime_part_t *p = (lcurl_mime_part_t *)lutil_checkudatap (L, i, LCURL_MIME_PART); + luaL_argcheck (L, p != NULL, i, LCURL_MIME_PART_NAME" object expected"); + luaL_argcheck (L, p->part != NULL, i, LCURL_MIME_PART_NAME" object freed"); + return p; +} + +static int lcurl_mime_part_to_s(lua_State *L){ + lcurl_mime_part_t *p = (lcurl_mime_part_t *)lutil_checkudatap (L, 1, LCURL_MIME_PART); + luaL_argcheck (L, p != NULL, 1, LCURL_MIME_PART_NAME" object expected"); + + lua_pushfstring(L, LCURL_MIME_PART_NAME" (%p)%s", (void*)p, p->part ? "" : " (freed)"); + return 1; +} + +static int lcurl_mime_part_free(lua_State *L){ + lcurl_mime_part_t *p = (lcurl_mime_part_t *)lutil_checkudatap (L, 1, LCURL_MIME_PART); + luaL_argcheck (L, p != NULL, 1, LCURL_MIME_PART_NAME" object expected"); + + lcurl_mime_part_reset(L, p); + + return 0; +} + +static int lcurl_mime_part_assing_ext(lua_State *L, int part, int i){ +#define UNSET_VALUE (const char*)-1 + + const char *mime_type = NULL, *mime_name = NULL, *mime_fname = NULL; + int headers = 0; + CURLcode ret; + lcurl_mime_part_t *p = lcurl_getmimepart_at(L, part); + + if(IS_TABLE(L, i)) headers = i; + else if (IS_OPTSTR(L, i)) { + mime_type = IS_FALSE(L, i) ? UNSET_VALUE : lua_tostring(L, i); + if(IS_TABLE(L, i+1)) headers = i+1; + else if(IS_OPTSTR(L, i+1)){ + mime_name = IS_FALSE(L, i+1) ? UNSET_VALUE : lua_tostring(L, i+1); + if(IS_TABLE(L, i+2)) headers = i+2; + else if(IS_OPTSTR(L, i+2)){ + mime_fname = IS_FALSE(L, i+2) ? UNSET_VALUE : lua_tostring(L, i+2); + if(IS_TABLE(L, i+3)) headers = i+3; + else if(IS_FALSE(L, i+3)){ + headers = -1; + } + } + } + } + + if(mime_type){ + ret = curl_mime_type(p->part, mime_type == UNSET_VALUE ? NULL : mime_type); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + } + + if(mime_name){ + ret = curl_mime_name(p->part, mime_name == UNSET_VALUE ? NULL : mime_name); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + } + + if(mime_fname){ + ret = curl_mime_filename(p->part, mime_fname == UNSET_VALUE ? NULL : mime_fname); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + } + + if(headers){ + if(-1 == headers){ + ret = curl_mime_headers(p->part, NULL, 0); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + } + else + return lcurl_mime_part_assing_table(L, part, headers); + } + + return 0; + +#undef UNSET_VALUE +} + +// part:data(str[, type[, name[, filename]]][, headers]) +static int lcurl_mime_part_data(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + size_t len; const char *data; + CURLcode ret; + + if(IS_FALSE(L, 2)){ + data = NULL; + len = 0; + } + else{ + data = luaL_checklstring(L, 2, &len); + /*string too long*/ + if(len == CURL_ZERO_TERMINATED){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, CURLE_BAD_FUNCTION_ARGUMENT); + } + } + + /* curl_mime_data copies data */ + ret = curl_mime_data(p->part, data, len); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + if (lua_gettop(L) > 2){ + int res = lcurl_mime_part_assing_ext(L, 1, 3); + if (res) return res; + } + + lua_settop(L, 1); + return 1; +} + +// part:subparts(mime[, type[, name]][, headers]) +static int lcurl_mime_part_subparts(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + lcurl_mime_t *mime = lcurl_getmime_at(L, 2); + CURLcode ret; + + /* we can attach mime to only one part */ + if(mime->parent){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, CURLE_BAD_FUNCTION_ARGUMENT); + } + + /* if we already have one subpart then libcurl free it so we can not use any references to it */ + lcurl_mime_part_remove_subparts(L, p, 1); + + ret = curl_mime_subparts(p->part, mime->mime); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + lua_pushvalue(L, 2); + p->subpart_ref = luaL_ref(L, LCURL_LUA_REGISTRY); + mime->parent = p; + + if (lua_gettop(L) > 2){ + int res = lcurl_mime_part_assing_ext(L, 1, 3); + if (res) return res; + } + + lua_settop(L, 1); + return 1; +} + +// part:filedata(path[, type[, name[, filename]]][, headers]) +static int lcurl_mime_part_filedata(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + const char *data = luaL_checkstring(L, 2); + CURLcode ret; + + ret = curl_mime_filedata(p->part, data); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + if (lua_gettop(L) > 2){ + int res = lcurl_mime_part_assing_ext(L, 1, 3); + if (res) return res; + } + + lua_settop(L, 1); + return 1; +} + +// part:headers(t) +static int lcurl_mime_part_headers(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + struct curl_slist *list; + CURLcode ret; + + if(IS_FALSE(L, 2)){ + list = NULL; + } + else{ + list = lcurl_util_to_slist(L, 2); + luaL_argcheck(L, list, 2, "array or nil expected"); + } + + ret = curl_mime_headers(p->part, list, 1); + + if(ret != CURLE_OK){ + curl_slist_free_all(list); + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + lua_settop(L, 1); + return 1; +} + +// part:type(t) +static int lcurl_mime_part_type(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + const char *mime_type; + CURLcode ret; + + if(IS_FALSE(L, 2)){ + mime_type = NULL; + } + else{ + mime_type = luaL_checkstring(L, 2); + } + + ret = curl_mime_type(p->part, mime_type); + + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + lua_settop(L, 1); + return 1; +} + +// part:name(t) +static int lcurl_mime_part_name(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + const char *mime_name; + CURLcode ret; + + if(IS_FALSE(L, 2)){ + mime_name = NULL; + } + else{ + mime_name = luaL_checkstring(L, 2); + } + ret = curl_mime_name(p->part, mime_name); + + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + lua_settop(L, 1); + return 1; +} + +// part:filename(t) +static int lcurl_mime_part_filename(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + const char *mime_name; + CURLcode ret; + + if(IS_FALSE(L, 2)){ + mime_name = NULL; + } + else{ + mime_name = luaL_checkstring(L, 2); + } + ret = curl_mime_filename(p->part, mime_name); + + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + lua_settop(L, 1); + return 1; +} + +// part:encoder(t) +static int lcurl_mime_part_encoder(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + const char *mime_encode; + CURLcode ret; + + if(IS_FALSE(L, 2)){ + mime_encode = NULL; + } + else{ + mime_encode = luaL_checkstring(L, 2); + } + ret = curl_mime_encoder(p->part, mime_encode); + + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + lua_settop(L, 1); + return 1; +} + +//} + +static const struct luaL_Reg lcurl_mime_methods[] = { + + {"addpart", lcurl_mime_addpart }, + {"easy", lcurl_mime_easy }, + + {"free", lcurl_mime_free }, + {"__gc", lcurl_mime_free }, + {"__tostring", lcurl_mime_to_s }, + + {NULL,NULL} +}; + +static const struct luaL_Reg lcurl_mime_part_methods[] = { + + {"subparts", lcurl_mime_part_subparts }, + {"data", lcurl_mime_part_data }, + {"filedata", lcurl_mime_part_filedata }, + {"headers", lcurl_mime_part_headers }, + {"name", lcurl_mime_part_name }, + {"filename", lcurl_mime_part_filename }, + {"type", lcurl_mime_part_type }, + {"encoder", lcurl_mime_part_encoder }, + + + {"free", lcurl_mime_part_free }, + {"__gc", lcurl_mime_part_free }, + {"__tostring", lcurl_mime_part_to_s }, + + {NULL,NULL} +}; + +static int lcurl_pushvalues(lua_State *L, int nup) { + assert(lua_gettop(L) >= nup); + + if (nup > 0) { + int b = lua_absindex(L, -nup); + int e = lua_absindex(L, -1); + int i; + + lua_checkstack(L, nup); + + for(i = b; i <= e; ++i) + lua_pushvalue(L, i); + } + + return nup; +} + +#endif + +void lcurl_mime_initlib(lua_State *L, int nup){ +#if LCURL_CURL_VER_GE(7,56,0) + lcurl_pushvalues(L, nup); + + if(!lutil_createmetap(L, LCURL_MIME, lcurl_mime_methods, nup)) + lua_pop(L, nup); + lua_pop(L, 1); + + if(!lutil_createmetap(L, LCURL_MIME_PART, lcurl_mime_part_methods, nup)) + lua_pop(L, nup); + lua_pop(L, 1); + +#else + lua_pop(L, nup); +#endif +} + diff --git a/src/lcmime.h b/src/lcmime.h new file mode 100644 index 0000000..02ebf55 --- /dev/null +++ b/src/lcmime.h @@ -0,0 +1,66 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2017 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of lua-lcurl library. +******************************************************************************/ + +#ifndef _LCMIME_H_ +#define _LCMIME_H_ + +#include "lcurl.h" +#include "lcutils.h" +#include + +void lcurl_mime_initlib(lua_State *L, int nup); + +#if LCURL_CURL_VER_GE(7,56,0) + +typedef struct lcurl_mime_part_tag{ + lua_State *L; + + lcurl_callback_t rd; + lcurl_read_buffer_t rbuffer; + + curl_mimepart *part; + + struct lcurl_mime_tag *parent; /*always set and can not be changed*/ + + int subpart_ref; + int headers_ref; + + int err_mode; + + struct lcurl_mime_part_tag *next; +}lcurl_mime_part_t; + +typedef struct lcurl_mime_tag{ + curl_mime *mime; + + int storage; + int err_mode; + + lcurl_mime_part_t *parts; + lcurl_mime_part_t *parent; /*after set there no way change it*/ +}lcurl_mime_t; + +int lcurl_mime_create(lua_State *L, int error_mode); + +lcurl_mime_t *lcurl_getmime_at(lua_State *L, int i); + +#define lcurl_getmime(L) lcurl_getmime_at((L), 1) + +int lcurl_mime_part_create(lua_State *L, int error_mode); + +lcurl_mime_part_t *lcurl_getmimepart_at(lua_State *L, int i); + +#define lcurl_getmimepart(L) lcurl_getmimepart_at((L), 1) + +int lcurl_mime_set_lua(lua_State *L, lcurl_mime_t *p, lua_State *v); + +#endif + +#endif \ No newline at end of file diff --git a/src/lcopteasy.h b/src/lcopteasy.h index 35725d1..d683d84 100644 --- a/src/lcopteasy.h +++ b/src/lcopteasy.h @@ -401,6 +401,10 @@ OPT_ENTRY( request_target, REQUEST_TARGET, STR, 0, LCURL_DEF OPT_ENTRY( socks5_auth, SOCKS5_AUTH, LNG, 0, LCURL_DEFAULT_VALUE) #endif +#if LCURL_CURL_VER_GE(7,56,0) +OPT_ENTRY( ssh_compression, SSH_COMPRESSION, LNG, 0, LCURL_DEFAULT_VALUE) +#endif + #ifdef LCURL__TCP_FASTOPEN # define TCP_FASTOPEN LCURL__TCP_FASTOPEN # undef LCURL__TCP_FASTOPEN diff --git a/src/lcurl.c b/src/lcurl.c index 512fe0c..1ebc544 100644 --- a/src/lcurl.c +++ b/src/lcurl.c @@ -14,6 +14,7 @@ #include "lcshare.h" #include "lcerror.h" #include "lchttppost.h" +#include "lcmime.h" #include "lcutils.h" /*export*/ @@ -113,6 +114,10 @@ static int lcurl_version_info(lua_State *L){ #ifdef CURL_VERSION_HTTPS_PROXY lua_pushliteral(L, "HTTPS_PROXY"); lua_pushboolean(L, data->features & CURL_VERSION_HTTPS_PROXY ); lua_rawset(L, -3); #endif +#ifdef CURL_VERSION_MULTI_SSL + lua_pushliteral(L, "MULTI_SSL"); lua_pushboolean(L, data->features & CURL_VERSION_MULTI_SSL ); lua_rawset(L, -3); +#endif + lua_setfield(L, -2, "features"); /* bitmask, see defines below */ if(data->ssl_version){lua_pushstring(L, data->ssl_version); lua_setfield(L, -2, "ssl_version");} /* human readable string */ @@ -155,7 +160,7 @@ static const struct luaL_Reg lcurl_functions[] = { {"share", lcurl_share_new }, {"version", lcurl_version }, {"version_info", lcurl_version_info }, - + {NULL,NULL} }; @@ -184,6 +189,21 @@ static volatile int LCURL_INIT = 0; static const char* LCURL_REGISTRY = "LCURL Registry"; static const char* LCURL_USERVAL = "LCURL Uservalues"; +#if LCURL_CURL_VER_GE(7,56,0) +static const char* LCURL_MIME_EASY_MAP = "LCURL Mime easy"; +#endif + +#if LCURL_CURL_VER_GE(7,56,0) +#define NUP 3 +#else +#define NUP 2 +#endif + +#if LCURL_CURL_VER_GE(7,56,0) +#define LCURL_PUSH_NUP(L) lua_pushvalue(L, -NUP-1);lua_pushvalue(L, -NUP-1);lua_pushvalue(L, -NUP-1); +#else +#define LCURL_PUSH_NUP(L) lua_pushvalue(L, -NUP-1);lua_pushvalue(L, -NUP-1); +#endif static int luaopen_lcurl_(lua_State *L, const struct luaL_Reg *func){ if(!LCURL_INIT){ @@ -209,19 +229,32 @@ static int luaopen_lcurl_(lua_State *L, const struct luaL_Reg *func){ lcurl_util_new_weak_table(L, "k"); } +#if LCURL_CURL_VER_GE(7,56,0) + lua_rawgetp(L, LUA_REGISTRYINDEX, LCURL_MIME_EASY_MAP); + if(!lua_istable(L, -1)){ /* Mime->Easy */ + lua_pop(L, 1); + lcurl_util_new_weak_table(L, "v"); + } +#endif + lua_newtable(L); /* library */ - lua_pushvalue(L, -3); lua_pushvalue(L, -3); luaL_setfuncs(L, func, 2); - lua_pushvalue(L, -3); lua_pushvalue(L, -3); lcurl_error_initlib(L, 2); - lua_pushvalue(L, -3); lua_pushvalue(L, -3); lcurl_hpost_initlib(L, 2); - lua_pushvalue(L, -3); lua_pushvalue(L, -3); lcurl_easy_initlib (L, 2); - lua_pushvalue(L, -3); lua_pushvalue(L, -3); lcurl_multi_initlib(L, 2); - lua_pushvalue(L, -3); lua_pushvalue(L, -3); lcurl_share_initlib(L, 2); + LCURL_PUSH_NUP(L); luaL_setfuncs(L, func, NUP); + LCURL_PUSH_NUP(L); lcurl_error_initlib(L, NUP); + LCURL_PUSH_NUP(L); lcurl_hpost_initlib(L, NUP); + LCURL_PUSH_NUP(L); lcurl_easy_initlib (L, NUP); + LCURL_PUSH_NUP(L); lcurl_mime_initlib (L, NUP); + LCURL_PUSH_NUP(L); lcurl_multi_initlib(L, NUP); + LCURL_PUSH_NUP(L); lcurl_share_initlib(L, NUP); - lua_pushvalue(L, -3); lua_rawsetp(L, LUA_REGISTRYINDEX, LCURL_REGISTRY); - lua_pushvalue(L, -2); lua_rawsetp(L, LUA_REGISTRYINDEX, LCURL_USERVAL); + LCURL_PUSH_NUP(L); - lua_remove(L, -2); /* registry */ +#if LCURL_CURL_VER_GE(7,56,0) + lua_rawsetp(L, LUA_REGISTRYINDEX, LCURL_MIME_EASY_MAP); +#endif + + lua_rawsetp(L, LUA_REGISTRYINDEX, LCURL_USERVAL); + lua_rawsetp(L, LUA_REGISTRYINDEX, LCURL_REGISTRY); lcurl_util_set_const(L, lcurl_flags); diff --git a/src/lcurl.h b/src/lcurl.h index b9bd48c..d7a4570 100644 --- a/src/lcurl.h +++ b/src/lcurl.h @@ -1,7 +1,7 @@ /****************************************************************************** * Author: Alexey Melnichuk * -* Copyright (C) 2014 Alexey Melnichuk +* Copyright (C) 2014-2017 Alexey Melnichuk * * Licensed according to the included 'LICENSE' document * @@ -25,4 +25,7 @@ #define LCURL_USERVALUES lua_upvalueindex(2) +/* only for `mime` API */ +#define LCURL_MIME_EASY lua_upvalueindex(3) + #endif diff --git a/src/lcutils.c b/src/lcutils.c index c6baa6f..e4428fb 100644 --- a/src/lcutils.c +++ b/src/lcutils.c @@ -21,13 +21,21 @@ int lcurl_storage_init(lua_State *L){ } void lcurl_storage_preserve_value(lua_State *L, int storage, int i){ - assert(i > 0); + assert(i > 0 && i <= lua_gettop(L)); luaL_checkany(L, i); lua_rawgeti(L, LCURL_LUA_REGISTRY, storage); lua_pushvalue(L, i); lua_pushboolean(L, 1); lua_rawset(L, -3); lua_pop(L, 1); } +void lcurl_storage_remove_value(lua_State *L, int storage, int i){ + assert(i > 0 && i <= lua_gettop(L)); + luaL_checkany(L, i); + lua_rawgeti(L, LCURL_LUA_REGISTRY, storage); + lua_pushvalue(L, i); lua_pushnil(L); lua_rawset(L, -3); + lua_pop(L, 1); +} + static void lcurl_storage_ensure_t(lua_State *L, int t){ lua_rawgeti(L, -1, t); if(!lua_istable(L, -1)){ @@ -256,7 +264,7 @@ int lcurl_utils_apply_options(lua_State *L, int opt, int obj, int do_close, while(lua_next(L, opt) != 0){ int n; assert(lua_gettop(L) == (top + 2)); - + if(lua_type(L, -2) == LUA_TNUMBER){ /* [curl.OPT_URL] = "http://localhost" */ lua_pushvalue(L, -2); lua_insert(L, -2); /*Stack : opt, obj, k, k, v */ diff --git a/src/lcutils.h b/src/lcutils.h index 25a822d..4a3e280 100644 --- a/src/lcutils.h +++ b/src/lcutils.h @@ -55,6 +55,8 @@ int lcurl_storage_init(lua_State *L); void lcurl_storage_preserve_value(lua_State *L, int storage, int i); +void lcurl_storage_remove_value(lua_State *L, int storage, int i); + int lcurl_storage_preserve_slist(lua_State *L, int storage, struct curl_slist * list); struct curl_slist* lcurl_storage_remove_slist(lua_State *L, int storage, int idx); diff --git a/test/run.lua b/test/run.lua index 44a694b..e5feee2 100644 --- a/test/run.lua +++ b/test/run.lua @@ -27,6 +27,7 @@ print("") require "test_safe" require "test_easy" require "test_form" +require "test_mime" require "test_curl" RUN() diff --git a/test/server.lua b/test/server.lua new file mode 100644 index 0000000..a3053da --- /dev/null +++ b/test/server.lua @@ -0,0 +1,121 @@ +local function prequire(m) + local ok, err = pcall(require, m) + if not ok then return nil, err end + return err +end + +local uv = prequire "lluv" +local Pegasus = require (uv and "lluv.pegasus" or "pegasus") +local Router = require "pegasus.plugins.router" +local json = require "dkjson" +-- local pp = require "pp" + +local function decode_form(form) + return string.match(form, '\r\nContent%-Disposition:%s*form%-data;%s*name="(.-)".-\r\n\r\n(.-)\r\n') +end + +local function decode_params(str) + local params = {} + for k, v in string.gmatch(str, '([^=]+)=([^&]+)&?') do + params[k] = v + end + return params +end + +local function rand_bytes(n) + local res = {} + for i = 1, n do + res[#res + 1] = string.char(math.random(254)) + end + return table.concat(res) +end + +local r = Router:new() + +local server = Pegasus:new{ + plugins = { r }; + host = '127.0.0.1', port = 7090, timout = 10 +} + +local function recvFullBody(request, T1) + local body, counter = {}, 0 + + local result, status + while true do + result, status = request:receiveBody() + if result then + counter = 0 + body[#body + 1] = result + elseif status ~= 'timeout' then + break + else + counter = counter + 1 + if counter > T1 then break end + end + end + + return table.concat(body), status +end + +local function buildResponse(request) + local headers = request:headers() + local params = request:params() + local path = request:path() + local ip = request.ip + local host = headers and headers.Host or '127.0.0.1' + local url = string.format('http://%s%s', host, path) + + return { + args = params; + headers = headers; + origin = ip; + url = url; + } +end + +r:get('/get', function(request, response) + local result = buildResponse(request) + result.body = recvFullBody(request, 15) + + response:statusCode(200) + response:contentType('application/json') + response:write(json.encode(result, {indent = true})) +end) + +r:post('/post', function(request, response, params) + local result = buildResponse(request) + + local body, status = recvFullBody(request, 15) + + local name, data = decode_form(body) + if name then + result.form = {[name] = data} + else + result.form = decode_params(body) + end + + response:statusCode(200) + response:contentType('application/json') + response:write(json.encode(result, {indent = true})) +end) + +r:get('/bytes/:size', function(request, response, params) + local headers = request:headers() + local size = tonumber(params.size) or 1024 + local result = rand_bytes(size) + + response:statusCode(200) + response:addHeader('Connection', 'close') + response:contentType('application/octet-stream') + response:write(result) +end) + +server:start(function(request, response) + local headers = request:headers() + + response:statusCode(200) + response:addHeader('Content-Type', 'text/plain') + response:write('Hello from Pegasus') +end) + +if uv then uv.run() end diff --git a/test/test_curl.lua b/test/test_curl.lua index dbf4b25..2c279b4 100644 --- a/test/test_curl.lua +++ b/test/test_curl.lua @@ -18,6 +18,13 @@ local scurl = require "cURL.safe" local json = require "dkjson" local fname = "./test.download" +local utils = require "utils" + +-- Bug. libcurl 7.56.0 does not add `Content-Type: text/plain` +local text_plain = utils.is_curl_eq(7,56,0) and 'test/plain' or 'text/plain' + +local GET_URL = "http://127.0.0.1:7090/get" + local ENABLE = true local _ENV = TEST_CASE'version' if ENABLE then @@ -29,7 +36,7 @@ end end -local _ENV = TEST_CASE'easy' if ENABLE then +local _ENV = TEST_CASE'easy' if ENABLE then local e1, e2 function teardown() @@ -118,7 +125,7 @@ end local _ENV = TEST_CASE'multi_iterator' if ENABLE then -local url = "http://httpbin.org/get" +local url = GET_URL local c, t, m @@ -138,8 +145,7 @@ function teardown() end function test_add_handle() - - local base_url = 'http://httpbin.org/get?key=' + local base_url = url .. '?key=' local urls = { base_url .. "1", base_url .. "2", @@ -190,7 +196,9 @@ function test_add_handle() end function test_info_read() - local url = 'http://httpbin.org/get?key=1' + + local url = GET_URL .. '?key=1' + c = assert(curl.easy{url=url, writefunction=function() end}) assert_equal(m, m:add_handle(c)) @@ -222,11 +230,11 @@ function test_content_01() end function test_content_02() - post = assert(scurl.form{name02 = {'value02', type = "text/plain"}}) + post = assert(scurl.form{name02 = {'value02', type = text_plain}}) local data = assert_string(post:get()) assert_match("\r\n\r\nvalue02\r\n", data) assert_match('name="name02"', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) end function test_content_03() @@ -238,12 +246,12 @@ function test_content_03() end function test_content_04() - post = assert(scurl.form{name04 = {'value04', type = "text/plain", headers = {"Content-Encoding: gzip"}}}) + post = assert(scurl.form{name04 = {'value04', type = text_plain, headers = {"Content-Encoding: gzip"}}}) local data = assert_string(post:get()) assert_match("\r\n\r\nvalue04\r\n", data) assert_match('name="name04"', data) assert_match('Content%-Encoding: gzip\r\n', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) end function test_buffer_01() @@ -263,14 +271,14 @@ function test_buffer_02() post = assert(scurl.form{name02 = { name = 'file02', data = 'value02', - type = "text/plain", + type = text_plain, }}) local data = assert_string(post:get()) assert_match("\r\n\r\nvalue02\r\n", data) assert_match('name="name02"', data) assert_match('filename="file02"', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) assert_not_match('Content%-Type: application/octet%-stream\r\n', data) end @@ -292,7 +300,7 @@ function test_buffer_04() post = assert(scurl.form{name04 = { name = 'file04', data = 'value04', - type = "text/plain", + type = text_plain, headers = {"Content-Encoding: gzip"}, }}) @@ -300,7 +308,7 @@ function test_buffer_04() assert_match("\r\n\r\nvalue04\r\n", data) assert_match('name="name04"', data) assert_match('filename="file04"', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) assert_not_match('Content%-Type: application/octet%-stream\r\n', data) assert_match('Content%-Encoding: gzip\r\n', data) end @@ -331,13 +339,13 @@ function test_stream_03() name = 'file03', stream = function() end, length = 128, - type = 'text/plain', + type = text_plain, }}) local data = assert_string(post:get()) assert_match('name="name03"', data) assert_match('filename="file03"', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) end function test_stream_04() @@ -345,13 +353,13 @@ function test_stream_04() name = 'file04', stream = function() end, length = 128, - type = 'text/plain', + type = text_plain, headers = {"Content-Encoding: gzip"}, }}) local data = assert_string(post:get()) assert_match('name="name04"', data) assert_match('filename="file04"', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) assert_match('Content%-Encoding: gzip\r\n', data) end diff --git a/test/test_easy.lua b/test/test_easy.lua index 1b2c02e..3ffb408 100644 --- a/test/test_easy.lua +++ b/test/test_easy.lua @@ -17,75 +17,23 @@ local scurl = require "lcurl.safe" local json = require "dkjson" local path = require "path" local upath = require "path".new('/') -local url = "http://example.com" +local utils = require "utils" +-- local url = "http://127.0.0.1:7090/get" local fname = "./test.download" +-- local GET_URL = "http://example.com" +-- local POST_URL = "http://httpbin.org/post" +local GET_URL = "http://127.0.0.1:7090/get" +local POST_URL = "http://127.0.0.1:7090/post" + -- print("------------------------------------") -- print("Lua version: " .. (_G.jit and _G.jit.version or _G._VERSION)) -- print("cURL version: " .. curl.version()) -- print("------------------------------------") -- print("") -local function weak_ptr(val) - return setmetatable({value = val},{__mode = 'v'}) -end - -local function gc_collect() - collectgarbage("collect") - collectgarbage("collect") -end - -local function cver(min, maj, pat) - return min * 2^16 + maj * 2^8 + pat -end - -local function is_curl_ge(min, maj, pat) - assert(0x070908 == cver(7,9,8)) - return curl.version_info('version_num') >= cver(min, maj, pat) -end - -local function read_file(n) - local f, e = io.open(n, "r") - if not f then return nil, e end - local d, e = f:read("*all") - f:close() - return d, e -end - -local function get_bin_by(str,n) - local pos = 1 - n - return function() - pos = pos + n - return (str:sub(pos,pos+n-1)) - end -end - -local function strem(ch, n, m) - return n, get_bin_by( (ch):rep(n), m) -end - -local function Stream(ch, n, m) - local size, reader - - local _stream = {} - - function _stream:read(...) - _stream.called_ctx = self - _stream.called_co = coroutine.running() - return reader(...) - end - - function _stream:size() - return size - end - - function _stream:reset() - size, reader = strem(ch, n, m) - return self - end - - return _stream:reset() -end +local weak_ptr, gc_collect, is_curl_ge, read_file, stream, Stream = + utils.import('weak_ptr', 'gc_collect', 'is_curl_ge', 'read_file', 'stream', 'Stream') local ENABLE = true @@ -150,7 +98,7 @@ end function test_write_to_file() f = assert(io.open(fname, "w+b")) c = assert(curl.easy{ - url = url; + url = GET_URL; writefunction = f; }) @@ -159,7 +107,7 @@ end function test_write_abort_01() c = assert(scurl.easy{ - url = url; + url = GET_URL; writefunction = function(str) return #str - 1 end; }) @@ -169,7 +117,7 @@ end function test_write_abort_02() c = assert(scurl.easy{ - url = url; + url = GET_URL; writefunction = function(str) return false end; }) @@ -179,7 +127,7 @@ end function test_write_abort_03() c = assert(scurl.easy{ - url = url; + url = GET_URL; writefunction = function(str) return nil, "WRITEERROR" end; }) @@ -189,7 +137,7 @@ end function test_write_abort_04() c = assert(scurl.easy{ - url = url; + url = GET_URL; writefunction = function(str) return nil end; }) @@ -210,7 +158,7 @@ end function test_write_pass_01() c = assert(curl.easy{ - url = url; + url = GET_URL; writefunction = function(s) return #s end }) @@ -219,7 +167,7 @@ end function test_write_pass_02() c = assert(curl.easy{ - url = url; + url = GET_URL; writefunction = function() return end }) @@ -228,7 +176,7 @@ end function test_write_pass_03() c = assert(curl.easy{ - url = url; + url = GET_URL; writefunction = function() return true end }) @@ -241,7 +189,7 @@ function test_write_coro() co1 = coroutine.create(function() c = assert(curl.easy{ - url = url; + url = GET_URL; writefunction = function() called = coroutine.running() return true @@ -277,7 +225,7 @@ end function test_abort_01() c = assert(scurl.easy{ - url = url, + url = GET_URL, writefunction = pass, noprogress = false, progressfunction = function() return false end @@ -289,7 +237,7 @@ end function test_abort_02() c = assert(scurl.easy{ - url = url, + url = GET_URL, writefunction = pass, noprogress = false, progressfunction = function() return 0 end @@ -301,7 +249,7 @@ end function test_abort_03() c = assert(scurl.easy{ - url = url, + url = GET_URL, writefunction = pass, noprogress = false, progressfunction = function() return nil end @@ -313,7 +261,7 @@ end function test_abort_04() c = assert(scurl.easy{ - url = url, + url = GET_URL, writefunction = pass, noprogress = false, progressfunction = function() return nil, "PROGRESSERROR" end @@ -325,7 +273,7 @@ end function test_abort_05() c = assert(scurl.easy{ - url = url, + url = GET_URL, writefunction = pass, noprogress = false, progressfunction = function() error( "PROGRESSERROR" )end @@ -338,7 +286,7 @@ end function test_pass_01() c = assert(scurl.easy{ - url = url, + url = GET_URL, writefunction = pass, noprogress = false, progressfunction = function() end @@ -349,7 +297,7 @@ end function test_pass_02() c = assert(scurl.easy{ - url = url, + url = GET_URL, writefunction = pass, noprogress = false, progressfunction = function() return true end @@ -360,7 +308,7 @@ end function test_pass_03() c = assert(scurl.easy{ - url = url, + url = GET_URL, writefunction = pass, noprogress = false, progressfunction = function() return 1 end @@ -383,7 +331,7 @@ end function test_header_abort_01() c = assert(scurl.easy{ - url = url; + url = GET_URL; writefunction = dummy, headerfunction = function(str) return #str - 1 end; }) @@ -394,7 +342,7 @@ end function test_header_abort_02() c = assert(scurl.easy{ - url = url; + url = GET_URL; writefunction = dummy, headerfunction = function(str) return false end; }) @@ -405,7 +353,7 @@ end function test_header_abort_03() c = assert(scurl.easy{ - url = url; + url = GET_URL; writefunction = dummy, headerfunction = function(str) return nil, "WRITEERROR" end; }) @@ -416,7 +364,7 @@ end function test_header_abort_04() c = assert(scurl.easy{ - url = url; + url = GET_URL; writefunction = dummy, headerfunction = function(str) return nil end; }) @@ -438,7 +386,7 @@ end function test_header_pass_01() c = assert(curl.easy{ - url = url; + url = GET_URL; writefunction = dummy, headerfunction = function(s) return #s end }) @@ -448,7 +396,7 @@ end function test_header_pass_02() c = assert(curl.easy{ - url = url; + url = GET_URL; writefunction = dummy, headerfunction = function() return end }) @@ -458,7 +406,7 @@ end function test_header_pass_03() c = assert(curl.easy{ - url = url; + url = GET_URL; writefunction = dummy, headerfunction = function() return true end }) @@ -473,7 +421,7 @@ local _ENV = TEST_CASE'read_stream_callback' if ENABLE and is_curl_ge(7,30,0) th -- tested on WinXP(x32)/Win8(x64) libcurl/7.37.1 / libcurl/7.30.0 -local url = "http://httpbin.org/post" +local url = POST_URL local m, c, f, t @@ -499,7 +447,7 @@ function teardown() end function test() - assert_equal(f, f:add_stream('SSSSS', strem('X', 128, 13))) + assert_equal(f, f:add_stream('SSSSS', stream('X', 128, 13))) assert_equal(c, c:setopt_httppost(f)) -- should be called only stream callback @@ -922,10 +870,10 @@ end function test() do local fields = {} - for i = 1, 100 do fields[#fields + 1] = "key" .. i .. "=value"..i end + for i = 1, 100 do fields[#fields + 1] = string.format('key%d=value%d', i, i) end fields = table.concat(fields, '&') c = assert(curl.easy{ - url = "http://httpbin.org/post", + url = POST_URL, postfields = fields, writefunction = function()end, }) @@ -941,10 +889,10 @@ function test_unset() local pfields do local fields = {} - for i = 1, 100 do fields[#fields + 1] = "key" .. i .. "=value"..i end + for i = 1, 100 do fields[#fields + 1] = string.format('key%d=value%d', i, i) end fields = table.concat(fields, '&') c = assert(curl.easy{ - url = "http://httpbin.org/post", + url = POST_URL, postfields = fields, writefunction = function()end, }) diff --git a/test/test_form.lua b/test/test_form.lua index 96da473..08836cc 100644 --- a/test/test_form.lua +++ b/test/test_form.lua @@ -12,6 +12,11 @@ end local TEST_CASE = assert(lunit.TEST_CASE) local skip = lunit.skip or function() end +local utils = require "utils" + +-- Bug. libcurl 7.56.0 does not add `Content-Type: text/plain` +local text_plain = utils.is_curl_eq(7,56,0) and 'test/plain' or 'text/plain' + local curl = require "lcurl" local _ENV = TEST_CASE'add_content' do @@ -42,11 +47,11 @@ function test_01() end function test_02() - assert_equal(post, post:add_content('name02', 'value02', "text/plain")) + assert_equal(post, post:add_content('name02', 'value02', text_plain)) local data = assert_string(post:get()) assert_match("\r\n\r\nvalue02\r\n", data) assert_match('name="name02"', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) end function test_03() @@ -58,12 +63,12 @@ function test_03() end function test_04() - assert_equal(post, post:add_content('name04', 'value04', "text/plain", {"Content-Encoding: gzip"})) + assert_equal(post, post:add_content('name04', 'value04', text_plain, {"Content-Encoding: gzip"})) local data = assert_string(post:get()) assert_match("\r\n\r\nvalue04\r\n", data) assert_match('name="name04"', data) assert_match('Content%-Encoding: gzip\r\n', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) end function test_error() @@ -97,12 +102,12 @@ function test_01() end function test_02() - assert_equal(post, post:add_buffer('name02', 'file02', 'value02', "text/plain")) + assert_equal(post, post:add_buffer('name02', 'file02', 'value02', text_plain)) local data = assert_string(post:get()) assert_match("\r\n\r\nvalue02\r\n", data) assert_match('name="name02"', data) assert_match('filename="file02"', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) assert_not_match('Content%-Type: application/octet%-stream\r\n', data) end @@ -117,12 +122,12 @@ function test_03() end function test_04() - assert_equal(post, post:add_buffer('name04', 'file04', 'value04', "text/plain", {"Content-Encoding: gzip"})) + assert_equal(post, post:add_buffer('name04', 'file04', 'value04', text_plain, {"Content-Encoding: gzip"})) local data = assert_string(post:get()) assert_match("\r\n\r\nvalue04\r\n", data) assert_match('name="name04"', data) assert_match('filename="file04"', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) assert_not_match('Content%-Type: application/octet%-stream\r\n', data) assert_match('Content%-Encoding: gzip\r\n', data) end @@ -180,19 +185,19 @@ function test_02() end function test_03() - assert_equal(post, post:add_stream('name03', 'file03', 'text/plain', stream())) + assert_equal(post, post:add_stream('name03', 'file03', text_plain, stream())) local data = assert_string(post:get()) assert_match('name="name03"', data) assert_match('filename="file03"', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) end function test_04() - assert_equal(post, post:add_stream('name04', 'file04', 'text/plain', {"Content-Encoding: gzip"}, stream())) + assert_equal(post, post:add_stream('name04', 'file04', text_plain, {"Content-Encoding: gzip"}, stream())) local data = assert_string(post:get()) assert_match('name="name04"', data) assert_match('filename="file04"', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) assert_match('Content%-Encoding: gzip\r\n', data) end @@ -290,21 +295,21 @@ function test_01() end function test_02() - assert_equal(post, post:add_file('nameXX', form_path, 'text/plain')) + assert_equal(post, post:add_file('nameXX', form_path, text_plain)) local data = assert_string(post:get()) check_form(data) assert_match('filename="' .. form_file .. '"', data) assert_not_match('Content%-Type: application/octet%-stream\r\n', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) end function test_03() - assert_equal(post, post:add_file('nameXX', form_path, 'text/plain', 'renamedfile')) + assert_equal(post, post:add_file('nameXX', form_path, text_plain, 'renamedfile')) local data = assert_string(post:get()) check_form(data) assert_match('filename="' .. 'renamedfile' .. '"', data) assert_not_match('Content%-Type: application/octet%-stream\r\n', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) end function test_04() @@ -316,22 +321,22 @@ function test_04() end function test_05() - assert_equal(post, post:add_file('nameXX', form_path, 'text/plain', 'renamedfile', {"Content-Encoding: gzip"})) + assert_equal(post, post:add_file('nameXX', form_path, text_plain, 'renamedfile', {"Content-Encoding: gzip"})) local data = assert_string(post:get()) check_form(data) assert_match('filename="' .. 'renamedfile' .. '"', data) assert_not_match('Content%-Type: application/octet%-stream\r\n', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) assert_match('Content%-Encoding: gzip\r\n', data) end function test_05() - assert_equal(post, post:add_file('nameXX', form_path, 'text/plain', {"Content-Encoding: gzip"})) + assert_equal(post, post:add_file('nameXX', form_path, text_plain, {"Content-Encoding: gzip"})) local data = assert_string(post:get()) check_form(data) assert_match('filename="' .. form_file .. '"', data) assert_not_match('Content%-Type: application/octet%-stream\r\n', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) assert_match('Content%-Encoding: gzip\r\n', data) end @@ -364,7 +369,7 @@ end function test_error() assert_error(function() - assert_equal(post, post:add_file('nameXX', 'text/plain', form_path)) + assert_equal(post, post:add_file('nameXX', text_plain, form_path)) post:get() end) end @@ -388,7 +393,7 @@ local function check_form(data) assert_match("\r\n\r\nvalueXX\r\n", data) assert_match('name="nameXX"', data) assert_match('Content%-Encoding: gzip\r\n', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) end function test_writer_01() @@ -397,7 +402,7 @@ function test_writer_01() assert_equal(0, select("#", ...)) t[#t+1] = str end - assert_equal(post, post:add_content('nameXX', 'valueXX', "text/plain", {"Content-Encoding: gzip"})) + assert_equal(post, post:add_content('nameXX', 'valueXX', text_plain, {"Content-Encoding: gzip"})) assert_equal(post, post:get(writer)) check_form(table.concat(t)) end @@ -409,7 +414,7 @@ function test_writer_02() t[#t+1] = str return true end - assert_equal(post, post:add_content('nameXX', 'valueXX', "text/plain", {"Content-Encoding: gzip"})) + assert_equal(post, post:add_content('nameXX', 'valueXX', text_plain, {"Content-Encoding: gzip"})) assert_equal(post, post:get(writer)) check_form(table.concat(t)) end @@ -421,7 +426,7 @@ function test_writer_03() t[#t+1] = str return #str end - assert_equal(post, post:add_content('nameXX', 'valueXX', "text/plain", {"Content-Encoding: gzip"})) + assert_equal(post, post:add_content('nameXX', 'valueXX', text_plain, {"Content-Encoding: gzip"})) assert_equal(post, post:get(writer)) check_form(table.concat(t)) end @@ -433,40 +438,40 @@ function test_writer_context() assert_equal(0, select("#", ...)) T[#T+1] = str end - assert_equal(post, post:add_content('nameXX', 'valueXX', "text/plain", {"Content-Encoding: gzip"})) + assert_equal(post, post:add_content('nameXX', 'valueXX', text_plain, {"Content-Encoding: gzip"})) assert_equal(post, post:get(writer, t)) local data = table.concat(t) assert_match("\r\n\r\nvalueXX\r\n", data) assert_match('name="nameXX"', data) assert_match('Content%-Encoding: gzip\r\n', data) - assert_match('Content%-Type: text/plain\r\n', data) + assert_match('Content%-Type: ' .. text_plain .. '\r\n', data) end function test_abort_01() local err = {} local function writer() return nil, err end - assert_equal(post, post:add_content('nameXX', 'valueXX', "text/plain", {"Content-Encoding: gzip"})) + assert_equal(post, post:add_content('nameXX', 'valueXX', text_plain, {"Content-Encoding: gzip"})) local _, e = assert_nil(post:get(writer)) assert_equal(err, e) end function test_abort_02() local function writer() return 0 end - assert_equal(post, post:add_content('nameXX', 'valueXX', "text/plain", {"Content-Encoding: gzip"})) + assert_equal(post, post:add_content('nameXX', 'valueXX', text_plain, {"Content-Encoding: gzip"})) local _, e = assert_nil(post:get(writer)) assert_nil(e) end function test_abort_03() local function writer() return nil end - assert_equal(post, post:add_content('nameXX', 'valueXX', "text/plain", {"Content-Encoding: gzip"})) + assert_equal(post, post:add_content('nameXX', 'valueXX', text_plain, {"Content-Encoding: gzip"})) local _, e = assert_nil(post:get(writer)) assert_nil(e) end function test_abort_04() local function writer() return false end - assert_equal(post, post:add_content('nameXX', 'valueXX', "text/plain", {"Content-Encoding: gzip"})) + assert_equal(post, post:add_content('nameXX', 'valueXX', text_plain, {"Content-Encoding: gzip"})) local _, e = assert_nil(post:get(writer)) assert_nil(e) end @@ -474,7 +479,7 @@ end function test_error() local err = {} local function writer() error("WRITEERROR") end - assert_equal(post, post:add_content('nameXX', 'valueXX', "text/plain", {"Content-Encoding: gzip"})) + assert_equal(post, post:add_content('nameXX', 'valueXX', text_plain, {"Content-Encoding: gzip"})) assert_error_match("WRITEERROR", function() post:get(writer) end) diff --git a/test/test_mime.lua b/test/test_mime.lua new file mode 100644 index 0000000..e7239f4 --- /dev/null +++ b/test/test_mime.lua @@ -0,0 +1,481 @@ +local lunit, RUN = lunit do +RUN = lunit and function()end or function () + local res = lunit.run() + if res.errors + res.failed > 0 then + os.exit(-1) + end + return os.exit(0) +end +lunit = require "lunit" +end + +local TEST_CASE = assert(lunit.TEST_CASE) +local skip = lunit.skip or function() end + +local curl = require "lcurl" +local utils = require "utils" + +local weak_ptr, gc_collect, dump_mime_ = utils.import('weak_ptr', 'gc_collect', 'dump_mime') + +local GET_URL = 'http://127.0.0.1:7090/get' + +local function is_freed(c) + return not not string.find(tostring(c), '%(freed%)') +end + +local _ENV = TEST_CASE'mime lifetime' if not curl.OPT_MIMEPOST then +function test() skip("MIMI API supports since cURL 7.56.0") end +else + +local easy, mime + +function setup() + easy = curl.easy() +end + +function teardown() + if easy then easy:close() end + if mime then mime:free() end + easy, mime = nil +end + +function test_preserve_mime_part_reference() + -- mime part stores references to all parts + + local mime, part = easy:mime() do + part = weak_ptr(mime:addpart()) + end + gc_collect() + + assert_not_nil(part.value) + + mime = nil + gc_collect() + + assert_nil(part.value) + + easy:close() +end + +function test_free_mime_subparts() + -- when free root free all nodes + + -- mime + -- +- part3 + -- +- alt + -- +- part1 + -- +- part2 + + local mime, a, p1, p2, p3 = easy:mime() do + + local alt = easy:mime() + + local part1 = alt:addpart() + + local part2 = alt:addpart() + + local part3 = mime:addpart() + part3:subparts(alt, "multipart/alternative") + + a = weak_ptr(alt) + p1 = weak_ptr(part1) + p2 = weak_ptr(part2) + p3 = weak_ptr(part3) + end + + gc_collect() + + assert_not_nil(a.value) + assert_not_nil(p1.value) + assert_not_nil(p2.value) + assert_not_nil(p3.value) + + -- reamove reference to root node + mime = nil + gc_collect(4) + + assert_nil(a.value) + assert_nil(p1.value) + assert_nil(p2.value) + assert_nil(p3.value) + + easy:close() +end + +function test_preserve_mime_subparts() + -- if we have references to subnode but we free root + -- then all references have to become to invalid + + -- mime + -- +- part3 + -- +- alt + -- +- part1 + -- +- part2 + + local easy = curl.easy() + + local mime, a, p1, p2, p3 = easy:mime() do + + local alt = easy:mime() + + local part1 = alt:addpart() + + local part2 = alt:addpart() + + local part3 = mime:addpart() + part3:subparts(alt, "multipart/alternative") + + a = weak_ptr(alt) + p1 = weak_ptr(part1) + p2 = weak_ptr(part2) + p3 = weak_ptr(part3) + end + + gc_collect() + + assert_not_nil(a.value) + assert_not_nil(p1.value) + assert_not_nil(p2.value) + assert_not_nil(p3.value) + + -- save reference to subnode + local subnode = a.value + + mime = nil + + -- in this case call `free` to root node. + -- there no way to get reference to this node from child + -- so there no way to use it. + gc_collect() + + -- libcurl still close all childs + -- so all reference are invalid + + assert_not_nil(a.value) + assert_not_nil(is_freed(a.value)) + assert_nil(p1.value) + assert_nil(p2.value) + assert_nil(p3.value) + + easy:close() +end + +function test_preserve_mime_by_easy() + + local mime do + mime = weak_ptr(easy:mime()) + easy:setopt_mimepost(mime.value) + end + + gc_collect() + + assert_not_nil(mime.value) + + easy:unsetopt_mimepost() + + gc_collect() + + assert_nil(mime.value) +end + +function test_mime_does_not_ref_to_easy() + -- exists of mime object does not prevent easy from GC + + local mime = easy:mime() + local peasy = weak_ptr(easy) + easy = nil + + gc_collect() + + assert_nil(peasy.value) +end + +function test_mimepost_does_not_ref_to_easy() + -- exists of mime object does not prevent easy from GC + + -- create mime and set it as mimepost option + local mime = easy:mime() + easy:setopt_mimepost(mime) + + local peasy = weak_ptr(easy) + easy = nil + + gc_collect() + + assert_nil(peasy.value) +end + +end + +local _ENV = TEST_CASE'mime basic' if not curl.OPT_MIMEPOST then +function test() skip("MIMI API supports since cURL 7.56.0") end +else + +local easy, mime + +local function dump_mime(mime) + return dump_mime_(easy, mime, GET_URL) +end + +function setup() + easy = curl.easy() + mime = easy:mime() +end + +function teardown() + if easy then easy:close() end + if mime then mime:free() end + easy, mime = nil +end + +function test_data() + mime:addpart():data('hello') + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) +end + +function test_data_type() + mime:addpart():data('hello', 'test/html') + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_match('Content%-Type:%s+test/html', info) +end + +function test_data_type_name() + mime:addpart():data('hello', 'test/html', 'test') + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_match('Content%-Type:%s+test/html', info) + assert_match('Content%-Disposition:.-%sname="test"', info) +end + +function test_data_name() + mime:addpart():data('hello', nil, 'test') + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_match('Content%-Disposition:.-%sname="test"', info) +end + +function test_data_filename() + mime:addpart():data('hello', nil, nil, 'test.html') + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_match('Content%-Disposition:.-%sfilename="test%.html"', info) +end + +function test_data_should_not_unset_on_nil() + local part = mime:addpart():data('hello', 'test/html', 'test', { + 'X-Custom-Header: hello' + }) + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_match('Content%-Type:%s+test/html', info) + assert_match('Content%-Disposition:.-name="test"', info) + assert_match('X%-Custom%-Header:%s*hello', info) + + part:data('world', nil, 'test2') + info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nworld', info) + assert_match('Content%-Type:%s+test/html', info) + assert_match('Content%-Disposition:.-name="test2"', info) + assert_match('X%-Custom%-Header:%s*hello', info) + + part:data('!!!!!', 'text/xml', nil) + info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\n!!!!!', info) + assert_match('Content%-Type:%s+text/xml', info) + assert_match('Content%-Disposition:.-name="test2"', info) + assert_match('X%-Custom%-Header:%s*hello', info) + + part:data('!!!!!!!', 'text/xml', nil, nil) + info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\n!!!!!', info) + assert_match('Content%-Type:%s+text/xml', info) + assert_match('Content%-Disposition:.-name="test2"', info) + assert_match('X%-Custom%-Header:%s*hello', info) +end + +function test_data_headers() + mime:addpart():data('hello', { + 'X-Custom-Header: hello' + }) + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_match('X%-Custom%-Header:%s*hello', info) +end + +function test_unset_name() + mime:addpart():data('hello', 'test/html', 'test'):name(false) + + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_match('Content%-Type:%s+test/html', info) + assert_not_match('Content%-Disposition:.-name="test"', info) +end + +function test_unset_type() + mime:addpart():data('hello', 'test/html'):type(false) + + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_not_match('Content%-Type:%s+test/html', info) +end + +function test_unset_headers() + mime:addpart():data('hello', 'test/html',{ + 'X-Custom-Header: hello' + }):headers(false) + + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_not_match('X%-Custom%-Header:%s*hello', info) +end + +function test_unset_data() + mime:addpart():data('hello', 'test/html', 'test'):data(false) + + local info = assert_string(dump_mime(mime)) + assert_not_match('\r\n\r\nhello', info) + assert_match('Content%-Type:%s+test/html', info) + assert_match('Content%-Disposition:.-name="test"', info) +end + +function test_unset_data_type_1() + local part = mime:addpart():data('hello', 'test/html', 'test', { + 'X-Custom-Header: hello' + }):data('hello', false) + + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_not_match('Content%-Type:%s+test/html', info) + assert_match('Content%-Disposition:.-name="test"', info) + assert_match('X%-Custom%-Header:%s*hello', info) +end + +function test_unset_data_type_2() + local part = mime:addpart():data('hello', 'test/html', 'test', { + 'X-Custom-Header: hello' + }):data('hello', false, nil, nil) + + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_not_match('Content%-Type:%s+test/html', info) + assert_match('Content%-Disposition:.-name="test"', info) + assert_match('X%-Custom%-Header:%s*hello', info) +end + +function test_unset_data_name_1() + local part = mime:addpart():data('hello', 'test/html', 'test', { + 'X-Custom-Header: hello' + }):data('hello', nil, false) + + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_match('Content%-Type:%s+test/html', info) + assert_not_match('Content%-Disposition:.-name="test"', info) + assert_match('X%-Custom%-Header:%s*hello', info) +end + +function test_unset_data_name_2() + local part = mime:addpart():data('hello', 'test/html', 'test', { + 'X-Custom-Header: hello' + }):data('hello', nil, false, nil) + + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_match('Content%-Type:%s+test/html', info) + assert_not_match('Content%-Disposition:.-name="test"', info) + assert_match('X%-Custom%-Header:%s*hello', info) +end + +function test_unset_data_header() + local part = mime:addpart():data('hello', 'test/html', 'test', { + 'X-Custom-Header: hello' + }):data('hello', nil, nil, nil, false) + + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_match('Content%-Type:%s+test/html', info) + assert_match('Content%-Disposition:.-name="test"', info) + assert_not_match('X%-Custom%-Header:%s*hello', info) +end + +function test_unset_data_filename_1() + local part = mime:addpart():data('hello', 'test/html', 'test', 'test.html', { + 'X-Custom-Header: hello' + }):data('hello', nil, nil, false) + + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\nhello', info) + assert_match('Content%-Type:%s+test/html', info) + assert_match('Content%-Disposition:.-%sname="test"', info) + assert_not_match('Content%-Disposition:.-%sname="test%.html"', info) + assert_match('X%-Custom%-Header:%s*hello', info) +end + +function test_fail_pass_nil_as_first_arg() + local part = mime:addpart() + assert_error(function() part:data() end) + assert_error(function() part:data(nil) end) + + assert_error(function() part:name() end) + assert_error(function() part:name(nil) end) + + assert_error(function() part:type() end) + assert_error(function() part:type(nil) end) + + assert_error(function() part:headers() end) + assert_error(function() part:headers(nil) end) +end + +end + +local _ENV = TEST_CASE'mime addpart' if not curl.OPT_MIMEPOST then +function test() skip("MIMI API supports since cURL 7.56.0") end +else + +local easy, mime + +local function dump_mime(mime) + return dump_mime_(easy, mime, GET_URL) +end + +function setup() + easy = curl.easy() + mime = easy:mime() +end + +function teardown() + if easy then easy:close() end + if mime then mime:free() end + easy, mime = nil +end + +function test_empty_table() + assert(mime:addpart{}) +end + +function test_pass_args() + assert(mime:addpart{ + data = 'hello'; + encoder = 'base64'; + name = 'test'; + filename = 'test.html'; + type = 'test/html'; + headers = { + 'X-Custom-Header: hello'; + } + }) + + local info = assert_string(dump_mime(mime)) + assert_match('\r\n\r\naGVsbG8=', info) + assert_match('Content%-Type:%s+test/html', info) + assert_match('Content%-Disposition:.-%sname="test"', info) + assert_not_match('Content%-Disposition:.-%sname="test%.html"', info) + assert_match('X%-Custom%-Header:%s*hello', info) + assert_match('Content%-Transfer%-Encoding:%s*base64', info) +end + +end + +RUN() diff --git a/test/test_pause02.c.lua b/test/test_pause02.c.lua index 383c652..3a1fb4f 100644 --- a/test/test_pause02.c.lua +++ b/test/test_pause02.c.lua @@ -6,7 +6,8 @@ local curl = require "lcurl" local WAIT_COUNT = 15 local SIZE = 10 * 1024 -local RESOURCE_URL = "http://httpbin.org/bytes/" .. SIZE +-- local RESOURCE_URL = "http://httpbin.org/bytes/" .. SIZE +local RESOURCE_URL = "http://127.0.0.1:7090/bytes/" .. SIZE local State = { PAUSE = 0, -- write function should return CURL_WRITEFUNC_PAUSE diff --git a/test/utils.lua b/test/utils.lua new file mode 100644 index 0000000..6dce917 --- /dev/null +++ b/test/utils.lua @@ -0,0 +1,117 @@ +local curl = require "lcurl" + +local function weak_ptr(val) + return setmetatable({value = val},{__mode = 'v'}) +end + +local function gc_collect() + collectgarbage("collect") + collectgarbage("collect") +end + +local function cver(min, maj, pat) + return min * 2^16 + maj * 2^8 + pat +end + +local function is_curl_ge(min, maj, pat) + assert(0x070908 == cver(7,9,8)) + return curl.version_info('version_num') >= cver(min, maj, pat) +end + +local function is_curl_eq(min, maj, pat) + assert(0x070908 == cver(7,9,8)) + return curl.version_info('version_num') == cver(min, maj, pat) +end + +local function read_file(n) + local f, e = io.open(n, "r") + if not f then return nil, e end + local d, e = f:read("*all") + f:close() + return d, e +end + +local function get_bin_by(str,n) + local pos = 1 - n + return function() + pos = pos + n + return (str:sub(pos,pos+n-1)) + end +end + +local function stream(ch, n, m) + return n, get_bin_by( (ch):rep(n), m) +end + +local function Stream(ch, n, m) + local size, reader + + local _stream = {} + + function _stream:read(...) + _stream.called_ctx = self + _stream.called_co = coroutine.running() + return reader(...) + end + + function _stream:size() + return size + end + + function _stream:reset() + size, reader = stream(ch, n, m) + return self + end + + return _stream:reset() +end + +local function easy_dump_mime(easy, mime, url) + local buffer = {} + + local function dump_mime(type, data) + if type == curl.INFO_DATA_OUT then + buffer[#buffer + 1] = data + end + end + + local ok, err = easy:setopt{ + url = url or "http://127.0.0.1:7090"; + customrequest = "GET"; + mimepost = mime; + verbose = true; + debugfunction = dump_mime; + writefunction = function()end; + } + + if not ok then return nil, err end + + ok, err = easy:perform() + + if not ok then return nil, err end + + return table.concat(buffer) +end + +local utils = { + weak_ptr = weak_ptr; + gc_collect = gc_collect; + is_curl_ge = is_curl_ge; + is_curl_eq = is_curl_eq; + get_bin_by = get_bin_by; + read_file = read_file; + dump_mime = easy_dump_mime; + stream = stream; + Stream = Stream; +} + +utils.import = function(...) + local n, t = select('#', ...), {} + for i = 1, n do + local name = select(i, ...) + t[#t + 1] = assert(utils[name], 'unknown field: ' .. tostring(name)) + end + return (unpack or table.unpack)(t, 1, n) +end + +return utils \ No newline at end of file