summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorcorvid <devnull@localhost>2014-07-17 03:24:01 +0000
committercorvid <devnull@localhost>2014-07-17 03:24:01 +0000
commite75e4a0217096ebd7f425278403ab687911b0bb6 (patch)
treedb0a7f268471e8388e982a8fc363221b73802fa8 /src
parentf626a943944d0184491bab2c75a09788986b0d04 (diff)
http persistent connections initial patch
Diffstat (limited to 'src')
-rw-r--r--src/IO/IO.c4
-rw-r--r--src/IO/http.c115
-rw-r--r--src/cache.c129
-rw-r--r--src/cache.h3
-rw-r--r--src/capi.c10
-rw-r--r--src/decode.c24
-rw-r--r--src/decode.h16
7 files changed, 226 insertions, 75 deletions
diff --git a/src/IO/IO.c b/src/IO/IO.c
index a0a8bba5..43a3ff3a 100644
--- a/src/IO/IO.c
+++ b/src/IO/IO.c
@@ -350,6 +350,7 @@ void a_IO_ccc(int Op, int Branch, int Dir, ChainLink *Info,
switch (Op) {
case OpStart:
io = IO_new(IOWrite);
+ io->Info = Info;
Info->LocalKey = io;
break;
case OpSend:
@@ -406,9 +407,10 @@ void a_IO_ccc(int Op, int Branch, int Dir, ChainLink *Info,
IO_submit(io);
}
break;
+ case OpEnd:
case OpAbort:
io = Info->LocalKey;
- IO_close_fd(io, IO_StopRdWr);
+ IO_close_fd(io, Op == OpEnd ? IO_StopRd : IO_StopRdWr);
IO_free(io);
dFree(Info);
break;
diff --git a/src/IO/http.c b/src/IO/http.c
index 23101ba7..917cb95e 100644
--- a/src/IO/http.c
+++ b/src/IO/http.c
@@ -79,7 +79,8 @@ typedef struct {
typedef struct {
char *host;
- int active_connections;
+ Dlist *active_fds;
+ int avail_fd;
SocketQueue_t main_q, image_q; /* imgs have separate lower-priority queue */
} HostConnection_t;
@@ -89,6 +90,7 @@ static SocketData_t* Http_socket_dequeue(HostConnection_t *hc);
static HostConnection_t *Http_host_connection_get(const char *host);
static void Http_host_connection_remove(HostConnection_t *hc);
static int Http_connect_socket(ChainLink *Info);
+static void Http_send_query(ChainLink *Info, SocketData_t *S);
static void Http_socket_free(int SKey);
/*
@@ -160,8 +162,9 @@ static int Http_sock_new(void)
static void Http_connect_queued_sockets(HostConnection_t *hc)
{
+ bool_t all_is_well = TRUE;
SocketData_t *sd;
- while (hc->active_connections < prefs.http_max_conns &&
+ while (dList_length(hc->active_fds) < prefs.http_max_conns &&
(sd = Http_socket_dequeue(hc))) {
sd->flags &= ~HTTP_SOCKET_QUEUED;
@@ -170,18 +173,30 @@ static void Http_connect_queued_sockets(HostConnection_t *hc)
dFree(sd);
} else if (a_Web_valid(sd->web)) {
/* start connecting the socket */
- if (Http_connect_socket(sd->Info) < 0) {
+ if (hc->avail_fd != -1) {
+ sd->SockFD = hc->avail_fd;
+ hc->avail_fd = -1;
+ } else if (Http_connect_socket(sd->Info) < 0) {
ChainLink *Info = sd->Info;
+ all_is_well = FALSE;
MSG_BW(sd->web, 1, "ERROR: %s", dStrerror(sd->Err));
a_Chain_bfcb(OpAbort, Info, NULL, "Both");
Http_socket_free(VOIDP2INT(Info->LocalKey)); /* free sd */
dFree(Info);
- } else {
+ }
+ if (all_is_well) {
+ a_Chain_bcb(OpSend, sd->Info, &sd->SockFD, "FD");
+ a_Chain_fcb(OpSend, sd->Info, &sd->SockFD, "FD");
+ Http_send_query(sd->Info, sd);
sd->connected_to = hc->host;
- hc->active_connections++;
+ dList_append(hc->active_fds, (void *)sd->SockFD);
}
}
}
+ if (hc->avail_fd != -1) {
+ dClose(hc->avail_fd);
+ hc->avail_fd = -1;
+ }
}
/*
@@ -199,9 +214,9 @@ static void Http_socket_free(int SKey)
} else {
if (S->connected_to) {
HostConnection_t *hc = Http_host_connection_get(S->connected_to);
- hc->active_connections--;
+ dList_remove_fast(hc->active_fds, (void *)S->SockFD);
Http_connect_queued_sockets(hc);
- if (hc->active_connections == 0)
+ if (dList_length(hc->active_fds) == 0)
Http_host_connection_remove(hc);
}
dFree(S);
@@ -275,6 +290,9 @@ Dstr *a_Http_make_query_str(const DilloUrl *url, const DilloUrl *requester,
web_flags & WEB_Stylesheet ? "text/css,*/*;q=0.1" :
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
+ const char *connection_hdr_val =
+ dStrAsciiCasecmp(URL_SCHEME(url), "http") == 0 ? "keep-alive" : "close";
+
if (use_proxy) {
dStr_sprintfa(request_uri, "%s%s",
URL_STR(url),
@@ -309,15 +327,15 @@ Dstr *a_Http_make_query_str(const DilloUrl *url, const DilloUrl *requester,
"DNT: 1\r\n"
"%s" /* proxy auth */
"%s" /* referer */
- "Connection: close\r\n"
+ "Connection: %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"%s" /* cookies */
"\r\n",
request_uri->str, URL_AUTHORITY(url), prefs.http_user_agent,
accept_hdr_value, HTTP_Language_hdr, auth ? auth : "",
- proxy_auth->str, referer, content_type->str, (long)URL_DATA(url)->len,
- cookies);
+ proxy_auth->str, referer, connection_hdr_val, content_type->str,
+ (long)URL_DATA(url)->len, cookies);
dStr_append_l(query, URL_DATA(url)->str, URL_DATA(url)->len);
dStr_free(content_type, TRUE);
} else {
@@ -333,13 +351,13 @@ Dstr *a_Http_make_query_str(const DilloUrl *url, const DilloUrl *requester,
"DNT: 1\r\n"
"%s" /* proxy auth */
"%s" /* referer */
- "Connection: close\r\n"
+ "Connection: %s\r\n"
"%s" /* cache control */
"%s" /* cookies */
"\r\n",
request_uri->str, URL_AUTHORITY(url), prefs.http_user_agent,
accept_hdr_value, HTTP_Language_hdr, auth ? auth : "",
- proxy_auth->str, referer,
+ proxy_auth->str, referer, connection_hdr_val,
(URL_FLAGS(url) & URL_E2EQuery) ?
"Pragma: no-cache\r\nCache-Control: no-cache\r\n" : "",
cookies);
@@ -447,9 +465,6 @@ static int Http_connect_socket(ChainLink *Info)
dClose(S->SockFD);
MSG("Http_connect_socket ERROR: %s\n", dStrerror(S->Err));
} else {
- a_Chain_bcb(OpSend, Info, &S->SockFD, "FD");
- a_Chain_fcb(OpSend, Info, &S->SockFD, "FD");
- Http_send_query(S->Info, S);
return 0; /* Success */
}
}
@@ -618,6 +633,28 @@ static int Http_get(ChainLink *Info, void *Data1)
return 0;
}
+static void Http_make_fd_available(int fd)
+{
+ int i, j;
+
+ for (i = 0; i < dList_length(host_connections); i++) {
+ HostConnection_t *hc =
+ (HostConnection_t*) dList_nth_data(host_connections, i);
+
+ for (j = 0; j < dList_length(hc->active_fds); j++) {
+ void *data = dList_nth_data(hc->active_fds, j);
+
+ if (fd == VOIDP2INT(data)) {
+ _MSG("host %s ", hc->host);
+ _MSG("Shift fd %d from active to avail\n", fd);
+ dList_remove_fast (hc->active_fds, data);
+ hc->avail_fd = fd;
+ return;
+ }
+ }
+ }
+}
+
/*
* CCC function for the HTTP module
*/
@@ -665,6 +702,51 @@ void a_Http_ccc(int Op, int Branch, int Dir, ChainLink *Info,
break;
}
}
+ } else if (Branch == 2) {
+ if (Dir == FWD) {
+ /* Receiving from server */
+ switch (Op) {
+ case OpSend:
+ /* Data1 = dbuf */
+ a_Chain_fcb(OpSend, Info, Data1, "send_page_2eof");
+ break;
+ case OpEnd:
+ a_Chain_fcb(OpEnd, Info, NULL, NULL);
+ dFree(Info);
+ break;
+ default:
+ MSG_WARN("Unused CCC\n");
+ break;
+ }
+ } else { /* 2 BCK */
+ switch (Op) {
+ case OpStart:
+ a_Chain_link_new(Info, a_Http_ccc, BCK, a_IO_ccc, 2, 2);
+ a_Chain_bcb(OpStart, Info, NULL, NULL); /* IORead */
+ break;
+ case OpSend:
+ if (Data2) {
+ if (!strcmp(Data2, "FD")) {
+ Info->LocalKey = (void *)*(int*)Data1;
+ a_Chain_bcb(OpSend, Info, Data1, Data2);
+ } else if (!strcmp(Data2, "reply_complete")) {
+ int fd = VOIDP2INT(Info->LocalKey);
+
+ Http_make_fd_available(fd);
+ a_Chain_bfcb(OpEnd, Info, NULL, NULL);
+ dFree(Info);
+ }
+ }
+ break;
+ case OpAbort:
+ a_Chain_bcb(OpAbort, Info, NULL, NULL);
+ dFree(Info);
+ break;
+ default:
+ MSG_WARN("Unused CCC\n");
+ break;
+ }
+ }
}
}
@@ -726,6 +808,8 @@ static HostConnection_t *Http_host_connection_get(const char *host)
Http_socket_queue_init(&hc->main_q);
Http_socket_queue_init(&hc->image_q);
hc->host = dStrdup(host);
+ hc->active_fds = dList_new(prefs.http_max_conns);
+ hc->avail_fd = -1;
dList_append(host_connections, hc);
return hc;
@@ -736,6 +820,7 @@ static void Http_host_connection_remove(HostConnection_t *hc)
assert(hc->main_q.head == NULL && hc->image_q.head == NULL);
dList_remove_fast(host_connections, hc);
dFree(hc->host);
+ dList_free(hc->active_fds);
dFree(hc);
}
diff --git a/src/cache.c b/src/cache.c
index c4bc1f9b..4b216c55 100644
--- a/src/cache.c
+++ b/src/cache.c
@@ -55,7 +55,7 @@ typedef struct {
Dstr *Data; /* Pointer to raw data */
Dstr *UTF8Data; /* Data after charset translation */
int DataRefcount; /* Reference count */
- Decode *TransferDecoder; /* Transfer decoder (e.g., chunked) */
+ DecodeTransfer *TransferDecoder; /* Transfer decoder (e.g., chunked) */
Decode *ContentDecoder; /* Data decoder (e.g., gzip) */
Decode *CharsetDecoder; /* Translates text to UTF-8 encoding */
int ExpectedSize; /* Goal size of the HTTP transfer (0 if unknown)*/
@@ -205,7 +205,7 @@ static void Cache_entry_init(CacheEntry_t *NewEntry, const DilloUrl *Url)
NewEntry->CharsetDecoder = NULL;
NewEntry->ExpectedSize = 0;
NewEntry->TransferSize = 0;
- NewEntry->Flags = CA_IsEmpty;
+ NewEntry->Flags = CA_IsEmpty | CA_KeepAlive;
}
/*
@@ -308,7 +308,7 @@ static void Cache_entry_free(CacheEntry_t *entry)
if (entry->CharsetDecoder)
a_Decode_free(entry->CharsetDecoder);
if (entry->TransferDecoder)
- a_Decode_free(entry->TransferDecoder);
+ a_Decode_transfer_free(entry->TransferDecoder);
if (entry->ContentDecoder)
a_Decode_free(entry->ContentDecoder);
dFree(entry);
@@ -652,7 +652,8 @@ static Dlist *Cache_parse_multiple_fields(const char *header,
static void Cache_parse_header(CacheEntry_t *entry)
{
char *header = entry->Header->str;
- char *Length, *Type, *location_str, *encoding;
+ bool_t server1point0 = !strncmp(entry->Header->str, "HTTP/1.0", 8);
+ char *Length, *Type, *location_str, *encoding, *connection;
#ifndef DISABLE_COOKIES
Dlist *Cookies;
#endif
@@ -716,6 +717,17 @@ static void Cache_parse_header(CacheEntry_t *entry)
dList_free(warnings);
}
+ if (server1point0)
+ entry->Flags &= ~CA_KeepAlive;
+
+ if ((connection = Cache_parse_field(header, "Connection"))) {
+ if (!dStrAsciiCasecmp(connection, "close"))
+ entry->Flags &= ~CA_KeepAlive;
+ else if (server1point0 && !dStrAsciiCasecmp(connection, "keep-alive"))
+ entry->Flags |= CA_KeepAlive;
+ dFree(connection);
+ }
+
/*
* Get Transfer-Encoding and initialize decoder
*/
@@ -834,6 +846,53 @@ static int Cache_get_header(CacheEntry_t *entry,
return 0;
}
+static void Cache_finish_msg(CacheEntry_t *entry)
+{
+ if (entry->Flags & CA_GotData) {
+ /* already finished */
+ return;
+ }
+
+ if ((entry->ExpectedSize || entry->TransferSize) &&
+ entry->TypeHdr == NULL) {
+ MSG_HTTP("Message with a body lacked Content-Type header.\n");
+ }
+ if ((entry->Flags & CA_GotLength) &&
+ (entry->ExpectedSize != entry->TransferSize)) {
+ MSG_HTTP("Content-Length does NOT match message body at\n"
+ "%s\n", URL_STR_(entry->Url));
+ MSG("Expected size: %d, Transfer size: %d\n",
+ entry->ExpectedSize, entry->TransferSize);
+ }
+ if (!entry->TransferSize && !(entry->Flags & CA_Redirect) &&
+ (entry->Flags & WEB_RootUrl)) {
+ char *eol = strchr(entry->Header->str, '\n');
+ if (eol) {
+ char *status_line = dStrndup(entry->Header->str,
+ eol - entry->Header->str);
+ MSG_HTTP("Body was empty. Server sent status: %s\n", status_line);
+ dFree(status_line);
+ }
+ }
+ entry->Flags |= CA_GotData;
+ entry->Flags &= ~CA_Stopped; /* it may catch up! */
+ if (entry->TransferDecoder) {
+ a_Decode_transfer_free(entry->TransferDecoder);
+ entry->TransferDecoder = NULL;
+ }
+ if (entry->ContentDecoder) {
+ a_Decode_free(entry->ContentDecoder);
+ entry->ContentDecoder = NULL;
+ }
+ dStr_fit(entry->Data); /* fit buffer size! */
+
+ if ((entry = Cache_process_queue(entry))) {
+ if (entry->Flags & CA_GotHeader) {
+ Cache_unref_data(entry);
+ }
+ }
+}
+
/*
* Receive new data, update the reception buffer (for next read), update the
* cache, and service the client queue.
@@ -842,16 +901,17 @@ static int Cache_get_header(CacheEntry_t *entry,
* 'Op' is the operation to perform
* 'VPtr' is a (void) pointer to the IO control structure
*/
-void a_Cache_process_dbuf(int Op, const char *buf, size_t buf_size,
- const DilloUrl *Url)
+bool_t a_Cache_process_dbuf(int Op, const char *buf, size_t buf_size,
+ const DilloUrl *Url)
{
int offset, len;
const char *str;
Dstr *dstr1, *dstr2, *dstr3;
+ bool_t done = FALSE;
CacheEntry_t *entry = Cache_entry_search(Url);
/* Assert a valid entry (not aborted) */
- dReturn_if_fail (entry != NULL);
+ dReturn_val_if_fail (entry != NULL, FALSE);
_MSG("__a_Cache_process_dbuf__\n");
@@ -875,7 +935,8 @@ void a_Cache_process_dbuf(int Op, const char *buf, size_t buf_size,
/* Decode arrived data (<= 3 stages) */
if (entry->TransferDecoder) {
- dstr1 = a_Decode_process(entry->TransferDecoder, str, len);
+ dstr1 = a_Decode_transfer_process(entry->TransferDecoder, str,len);
+ done = a_Decode_transfer_finished(entry->TransferDecoder);
str = dstr1->str;
len = dstr1->len;
}
@@ -896,51 +957,27 @@ void a_Cache_process_dbuf(int Op, const char *buf, size_t buf_size,
if (entry->Data->len)
entry->Flags &= ~CA_IsEmpty;
- entry = Cache_process_queue(entry);
- }
- } else if (Op == IOClose) {
- if ((entry->ExpectedSize || entry->TransferSize) &&
- entry->TypeHdr == NULL) {
- MSG_HTTP("Message with a body lacked Content-Type header.\n");
- }
- if ((entry->Flags & CA_GotLength) &&
- (entry->ExpectedSize != entry->TransferSize)) {
- MSG_HTTP("Content-Length does NOT match message body at\n"
- "%s\n", URL_STR_(entry->Url));
- MSG("Expected size: %d, Transfer size: %d\n",
- entry->ExpectedSize, entry->TransferSize);
- }
- if (!entry->TransferSize && !(entry->Flags & CA_Redirect) &&
- (entry->Flags & WEB_RootUrl)) {
- char *eol = strchr(entry->Header->str, '\n');
- if (eol) {
- char *status_line = dStrndup(entry->Header->str,
- eol - entry->Header->str);
- MSG_HTTP("Body was empty. Server sent status: %s\n", status_line);
- dFree(status_line);
+ if ((entry->Flags & CA_GotLength) &&
+ (entry->TransferSize >= entry->ExpectedSize)) {
+ done = TRUE;
}
- }
- entry->Flags |= CA_GotData;
- entry->Flags &= ~CA_Stopped; /* it may catch up! */
- if (entry->TransferDecoder) {
- a_Decode_free(entry->TransferDecoder);
- entry->TransferDecoder = NULL;
- }
- if (entry->ContentDecoder) {
- a_Decode_free(entry->ContentDecoder);
- entry->ContentDecoder = NULL;
- }
- dStr_fit(entry->Data); /* fit buffer size! */
-
- if ((entry = Cache_process_queue(entry))) {
- if (entry->Flags & CA_GotHeader) {
- Cache_unref_data(entry);
+ if (!(entry->Flags & CA_KeepAlive)) {
+ /* Let IOClose finish it later */
+ done = FALSE;
}
+
+ entry = Cache_process_queue(entry);
+
+ if (entry && done)
+ Cache_finish_msg(entry);
}
+ } else if (Op == IOClose) {
+ Cache_finish_msg(entry);
} else if (Op == IOAbort) {
/* unused */
MSG("a_Cache_process_dbuf Op = IOAbort; not implemented!\n");
}
+ return done;
}
/*
diff --git a/src/cache.h b/src/cache.h
index c39e4600..f3b064f2 100644
--- a/src/cache.h
+++ b/src/cache.h
@@ -33,6 +33,7 @@ extern "C" {
#define CA_InternalUrl 0x800 /* URL content is generated by dillo */
#define CA_HugeFile 0x1000 /* URL content is too big */
#define CA_IsEmpty 0x2000 /* True until a byte of content arrives */
+#define CA_KeepAlive 0x4000
typedef struct CacheClient CacheClient_t;
@@ -67,7 +68,7 @@ const char *a_Cache_set_content_type(const DilloUrl *url, const char *ctype,
const char *from);
uint_t a_Cache_get_flags(const DilloUrl *url);
uint_t a_Cache_get_flags_with_redirection(const DilloUrl *url);
-void a_Cache_process_dbuf(int Op, const char *buf, size_t buf_size,
+bool_t a_Cache_process_dbuf(int Op, const char *buf, size_t buf_size,
const DilloUrl *Url);
int a_Cache_download_enabled(const DilloUrl *url);
void a_Cache_entry_remove_by_url(DilloUrl *url);
diff --git a/src/capi.c b/src/capi.c
index 531ae34d..1f4de0d9 100644
--- a/src/capi.c
+++ b/src/capi.c
@@ -714,7 +714,10 @@ void a_Capi_ccc(int Op, int Branch, int Dir, ChainLink *Info,
Capi_conn_ref(conn);
Info->LocalKey = conn;
conn->InfoRecv = Info;
- a_Chain_link_new(Info, a_Capi_ccc, BCK, a_Dpi_ccc, 2, 2);
+ if (strcmp(conn->server, "http") == 0)
+ a_Chain_link_new(Info, a_Capi_ccc, BCK, a_Http_ccc, 2, 2);
+ else
+ a_Chain_link_new(Info, a_Capi_ccc, BCK, a_Dpi_ccc, 2, 2);
a_Chain_bcb(OpStart, Info, NULL, Data2);
break;
case OpSend:
@@ -744,7 +747,10 @@ void a_Capi_ccc(int Op, int Branch, int Dir, ChainLink *Info,
if (strcmp(Data2, "send_page_2eof") == 0) {
/* Data1 = dbuf */
DataBuf *dbuf = Data1;
- a_Cache_process_dbuf(IORead, dbuf->Buf, dbuf->Size, conn->url);
+ bool_t finished = a_Cache_process_dbuf(IORead, dbuf->Buf,
+ dbuf->Size, conn->url);
+ if (finished)
+ a_Chain_bcb(OpSend, Info, NULL, "reply_complete");
} else if (strcmp(Data2, "send_status_message") == 0) {
a_UIcmd_set_msg(conn->bw, "%s", Data1);
} else if (strcmp(Data2, "chat") == 0) {
diff --git a/src/decode.c b/src/decode.c
index 53a0d621..76dd6ef7 100644
--- a/src/decode.c
+++ b/src/decode.c
@@ -21,9 +21,10 @@
static const int bufsize = 8*1024;
/*
- * Decode chunked data
+ * Decode 'Transfer-Encoding: chunked' data
*/
-static Dstr *Decode_chunked(Decode *dc, const char *instr, int inlen)
+Dstr *a_Decode_transfer_process(DecodeTransfer *dc, const char *instr,
+ int inlen)
{
char *inputPtr, *eol;
int inputRemaining;
@@ -66,6 +67,7 @@ static Dstr *Decode_chunked(Decode *dc, const char *instr, int inlen)
}
if (!(chunkRemaining = strtol(inputPtr, NULL, 0x10))) {
+ dc->finished = TRUE;
break; /* A chunk length of 0 means we're done! */
}
inputRemaining -= (eol - inputPtr) + 1;
@@ -80,10 +82,16 @@ static Dstr *Decode_chunked(Decode *dc, const char *instr, int inlen)
return output;
}
-static void Decode_chunked_free(Decode *dc)
+bool_t a_Decode_transfer_finished(DecodeTransfer *dc)
+{
+ return dc->finished;
+}
+
+void a_Decode_transfer_free(DecodeTransfer *dc)
{
dFree(dc->state);
dStr_free(dc->leftover, 1);
+ dFree(dc);
}
static void Decode_compression_free(Decode *dc)
@@ -280,19 +288,17 @@ static void Decode_charset_free(Decode *dc)
/*
* Initialize transfer decoder. Currently handles "chunked".
*/
-Decode *a_Decode_transfer_init(const char *format)
+DecodeTransfer *a_Decode_transfer_init(const char *format)
{
- Decode *dc = NULL;
+ DecodeTransfer *dc = NULL;
if (format && !dStrAsciiCasecmp(format, "chunked")) {
int *chunk_remaining = dNew(int, 1);
*chunk_remaining = 0;
- dc = dNew(Decode, 1);
+ dc = dNew(DecodeTransfer, 1);
dc->leftover = dStr_new("");
dc->state = chunk_remaining;
- dc->decode = Decode_chunked;
- dc->free = Decode_chunked_free;
- dc->buffer = NULL; /* not used */
+ dc->finished = FALSE;
_MSG("chunked!\n");
}
return dc;
diff --git a/src/decode.h b/src/decode.h
index 279807a6..06c987f6 100644
--- a/src/decode.h
+++ b/src/decode.h
@@ -15,7 +15,21 @@ typedef struct Decode {
void (*free) (struct Decode *dc);
} Decode;
-Decode *a_Decode_transfer_init(const char *format);
+/* I'm not going to shoehorn the decoders into the same form anymore. They
+ * can evolve independently.
+ */
+typedef struct DecodeTransfer {
+ Dstr *leftover;
+ void *state;
+ bool_t finished; /* has the terminating chunk been seen? */
+} DecodeTransfer;
+
+DecodeTransfer *a_Decode_transfer_init(const char *format);
+Dstr *a_Decode_transfer_process(DecodeTransfer *dc, const char *instr,
+ int inlen);
+bool_t a_Decode_transfer_finished(DecodeTransfer *dc);
+void a_Decode_transfer_free(DecodeTransfer *dc);
+
Decode *a_Decode_content_init(const char *format);
Decode *a_Decode_charset_init(const char *format);
Dstr *a_Decode_process(Decode *dc, const char *instr, int inlen);