From 0e1eaa525a8f4e7d5aa99a615217437e73c3c9b7 Mon Sep 17 00:00:00 2001 From: Alexey Melnichuk Date: Thu, 12 Oct 2017 14:22:24 +0300 Subject: [PATCH] Add. New cURL MIME API --- .travis.yml | 28 +- appveyor.yml | 2 +- examples/lcurl/smtp-mime.lua | 85 ++++++ rockspecs/lua-curl-scm-0.rockspec | 2 +- src/lceasy.c | 95 +++++- src/lceasy.h | 21 +- src/lcmime.c | 479 ++++++++++++++++++++++++++++++ src/lcmime.h | 66 ++++ src/lcopteasy.h | 4 + src/lcurl.c | 8 +- src/lcutils.c | 12 +- src/lcutils.h | 2 + test/run.lua | 1 + test/test_curl.lua | 30 +- test/test_easy.lua | 65 +--- test/test_form.lua | 70 +++-- test/test_mime.lua | 188 ++++++++++++ test/utils.lua | 89 ++++++ 18 files changed, 1116 insertions(+), 131 deletions(-) create mode 100644 examples/lcurl/smtp-mime.lua create mode 100644 src/lcmime.c create mode 100644 src/lcmime.h create mode 100644 test/test_mime.lua create mode 100644 test/utils.lua diff --git a/.travis.yml b/.travis.yml index 098c160..7e0facd 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,10 +30,9 @@ 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 @@ -68,6 +61,13 @@ script: # - lunit.sh test_form.lua # - lunit.sh test_curl.lua +before_cache: + # - cd $TRAVIS_BUILD_DIR/test + # - coveralls -b .. -r .. --dump c.report.json + # - luacov-coveralls -j c.report.json -v + # - luarocks remove lluv-gsmmodem + # - rm -f /home/travis/.cache/pip/log/debug.log + after_success: - coveralls -b .. -r .. --dump c.report.json - luacov-coveralls -j c.report.json -v diff --git a/appveyor.yml b/appveyor.yml index f3446e4..08190a5 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.56.0 matrix: - LUA: "lua 5.1" 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/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..936a286 100644 --- a/src/lceasy.c +++ b/src/lceasy.c @@ -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 * @@ -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/lcmime.c b/src/lcmime.c new file mode 100644 index 0000000..43d221e --- /dev/null +++ b/src/lcmime.c @@ -0,0 +1,479 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2017 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" + +#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; + +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; + + return 0; +} + +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; +} + +//{ 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; + + 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 = 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)); + 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 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. */ + /* So do not set it unless `curl_mime_subparts(p->part, NULL)` does not free mime */ + + 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); + } + + /* issues #1961 */ + + /* 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); + } + } +} + +#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 + +static int lcurl_mime_part_assing_ext(lua_State *L, lcurl_mime_part_t *p, int i){ + const char *mime_type = NULL, *mime_name = NULL; + int headers = 0; + CURLcode ret; + + if(IS_TABLE(L, i)) headers = i; + else if(IS_NILORSTR(L, i)){ + mime_type = lua_tostring(L, i); + if(IS_TABLE(L, i+1)) headers = i+1; + else if(IS_NILORSTR(L, i+1)){ + mime_name = lua_tostring(L, i+1); + if(IS_TABLE(L, i+2)) headers = i+2; + } + } + + if(mime_type){ + ret = curl_mime_type(p->part, 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); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + } + + if(headers){ + struct curl_slist *list = lcurl_util_to_slist(L, headers); + 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); + } + } + + return 0; +} + +// part:data(str[, type[, name]][, headers]) +static int lcurl_mime_part_data(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + size_t len; const char *data = luaL_checklstring(L, 2, &len); + CURLcode ret; + + /*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, p, 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, p, 3); + if (res) return res; + } + + lua_settop(L, 1); + return 1; +} + +// part:filedata(path[, type[, name]][, 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, p, 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 = lcurl_util_to_slist(L, 2); + CURLcode ret; + + luaL_argcheck(L, list, 2, "array 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 = luaL_checkstring(L, 2); + CURLcode ret; + + 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 = luaL_checkstring(L, 2); + CURLcode ret; + + 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; +} + + +//} + +static const struct luaL_Reg lcurl_mime_methods[] = { + + {"addpart", lcurl_mime_addpart }, + + {"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 }, + {"type", lcurl_mime_part_type }, + + {"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..c3d2c8f 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} }; @@ -215,6 +220,7 @@ static int luaopen_lcurl_(lua_State *L, const struct luaL_Reg *func){ 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_mime_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); 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/test_curl.lua b/test/test_curl.lua index dbf4b25..3e65497 100644 --- a/test/test_curl.lua +++ b/test/test_curl.lua @@ -18,6 +18,12 @@ local scurl = require "cURL.safe" local json = require "dkjson" local fname = "./test.download" +local utils = require "utils" + +-- libcurl 7.56.0 does not add `Content-Type: text/plain` +-- not sure is it bug or not +local text_plain = utils.is_curl_eq(7,56,0) and 'test/plain' or 'text/plain' + local ENABLE = true local _ENV = TEST_CASE'version' if ENABLE then @@ -222,11 +228,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 +244,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 +269,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 +298,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 +306,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 +337,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 +351,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..d61d335 100644 --- a/test/test_easy.lua +++ b/test/test_easy.lua @@ -17,6 +17,7 @@ local scurl = require "lcurl.safe" local json = require "dkjson" local path = require "path" local upath = require "path".new('/') +local utils = require "utils" local url = "http://example.com" local fname = "./test.download" @@ -26,66 +27,8 @@ local fname = "./test.download" -- 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 @@ -499,7 +442,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 diff --git a/test/test_form.lua b/test/test_form.lua index 96da473..11ac8e8 100644 --- a/test/test_form.lua +++ b/test/test_form.lua @@ -12,6 +12,12 @@ end local TEST_CASE = assert(lunit.TEST_CASE) local skip = lunit.skip or function() end +local utils = require "utils" + +-- libcurl 7.56.0 does not add `Content-Type: text/plain` +-- not sure is it bug or not +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 +48,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 +64,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 +103,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 +123,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 +186,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 +296,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 +322,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 +370,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 +394,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 +403,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 +415,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 +427,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 +439,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 +480,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..602297c --- /dev/null +++ b/test/test_mime.lua @@ -0,0 +1,188 @@ +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 function weak_ptr(val) + return setmetatable({value = val}, {__mode = 'v'}) +end + +local function gc_collect(n) + for i = 1, (n or 10) do + collectgarbage("collect") + end +end + +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 + +end + +RUN() diff --git a/test/utils.lua b/test/utils.lua new file mode 100644 index 0000000..798a105 --- /dev/null +++ b/test/utils.lua @@ -0,0 +1,89 @@ +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 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; + 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