| /*************************************************************************** |
| * _ _ ____ _ |
| * Project ___| | | | _ \| | |
| * / __| | | | |_) | | |
| * | (__| |_| | _ <| |___ |
| * \___|\___/|_| \_\_____| |
| * |
| * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al. |
| * |
| * This software is licensed as described in the file COPYING, which |
| * you should have received as part of this distribution. The terms |
| * are also available at https://curl.se/docs/copyright.html. |
| * |
| * You may opt to use, copy, modify, merge, publish, distribute and/or sell |
| * copies of the Software, and permit persons to whom the Software is |
| * furnished to do so, under the terms of the COPYING file. |
| * |
| * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY |
| * KIND, either express or implied. |
| * |
| * SPDX-License-Identifier: curl |
| * |
| ***************************************************************************/ |
| |
| #include "curl_setup.h" |
| #include "bufq.h" |
| |
| /* The last 3 #include files should be in this order */ |
| #include "curl_printf.h" |
| #include "curl_memory.h" |
| #include "memdebug.h" |
| |
| static bool chunk_is_empty(const struct buf_chunk *chunk) |
| { |
| return chunk->r_offset >= chunk->w_offset; |
| } |
| |
| static bool chunk_is_full(const struct buf_chunk *chunk) |
| { |
| return chunk->w_offset >= chunk->dlen; |
| } |
| |
| static size_t chunk_len(const struct buf_chunk *chunk) |
| { |
| return chunk->w_offset - chunk->r_offset; |
| } |
| |
| static size_t chunk_space(const struct buf_chunk *chunk) |
| { |
| return chunk->dlen - chunk->w_offset; |
| } |
| |
| static void chunk_reset(struct buf_chunk *chunk) |
| { |
| chunk->next = NULL; |
| chunk->r_offset = chunk->w_offset = 0; |
| } |
| |
| static size_t chunk_append(struct buf_chunk *chunk, |
| const unsigned char *buf, size_t len) |
| { |
| unsigned char *p = &chunk->x.data[chunk->w_offset]; |
| size_t n = chunk->dlen - chunk->w_offset; |
| DEBUGASSERT(chunk->dlen >= chunk->w_offset); |
| if(n) { |
| n = CURLMIN(n, len); |
| memcpy(p, buf, n); |
| chunk->w_offset += n; |
| } |
| return n; |
| } |
| |
| static size_t chunk_read(struct buf_chunk *chunk, |
| unsigned char *buf, size_t len) |
| { |
| unsigned char *p = &chunk->x.data[chunk->r_offset]; |
| size_t n = chunk->w_offset - chunk->r_offset; |
| DEBUGASSERT(chunk->w_offset >= chunk->r_offset); |
| if(!n) { |
| return 0; |
| } |
| else if(n <= len) { |
| memcpy(buf, p, n); |
| chunk->r_offset = chunk->w_offset = 0; |
| return n; |
| } |
| else { |
| memcpy(buf, p, len); |
| chunk->r_offset += len; |
| return len; |
| } |
| } |
| |
| static ssize_t chunk_slurpn(struct buf_chunk *chunk, size_t max_len, |
| Curl_bufq_reader *reader, |
| void *reader_ctx, CURLcode *err) |
| { |
| unsigned char *p = &chunk->x.data[chunk->w_offset]; |
| size_t n = chunk->dlen - chunk->w_offset; /* free amount */ |
| ssize_t nread; |
| |
| DEBUGASSERT(chunk->dlen >= chunk->w_offset); |
| if(!n) { |
| *err = CURLE_AGAIN; |
| return -1; |
| } |
| if(max_len && n > max_len) |
| n = max_len; |
| nread = reader(reader_ctx, p, n, err); |
| if(nread > 0) { |
| DEBUGASSERT((size_t)nread <= n); |
| chunk->w_offset += nread; |
| } |
| return nread; |
| } |
| |
| static void chunk_peek(const struct buf_chunk *chunk, |
| const unsigned char **pbuf, size_t *plen) |
| { |
| DEBUGASSERT(chunk->w_offset >= chunk->r_offset); |
| *pbuf = &chunk->x.data[chunk->r_offset]; |
| *plen = chunk->w_offset - chunk->r_offset; |
| } |
| |
| static void chunk_peek_at(const struct buf_chunk *chunk, size_t offset, |
| const unsigned char **pbuf, size_t *plen) |
| { |
| offset += chunk->r_offset; |
| DEBUGASSERT(chunk->w_offset >= offset); |
| *pbuf = &chunk->x.data[offset]; |
| *plen = chunk->w_offset - offset; |
| } |
| |
| static size_t chunk_skip(struct buf_chunk *chunk, size_t amount) |
| { |
| size_t n = chunk->w_offset - chunk->r_offset; |
| DEBUGASSERT(chunk->w_offset >= chunk->r_offset); |
| if(n) { |
| n = CURLMIN(n, amount); |
| chunk->r_offset += n; |
| if(chunk->r_offset == chunk->w_offset) |
| chunk->r_offset = chunk->w_offset = 0; |
| } |
| return n; |
| } |
| |
| static void chunk_list_free(struct buf_chunk **anchor) |
| { |
| struct buf_chunk *chunk; |
| while(*anchor) { |
| chunk = *anchor; |
| *anchor = chunk->next; |
| free(chunk); |
| } |
| } |
| |
| |
| |
| void Curl_bufcp_init(struct bufc_pool *pool, |
| size_t chunk_size, size_t spare_max) |
| { |
| DEBUGASSERT(chunk_size > 0); |
| DEBUGASSERT(spare_max > 0); |
| memset(pool, 0, sizeof(*pool)); |
| pool->chunk_size = chunk_size; |
| pool->spare_max = spare_max; |
| } |
| |
| static CURLcode bufcp_take(struct bufc_pool *pool, |
| struct buf_chunk **pchunk) |
| { |
| struct buf_chunk *chunk = NULL; |
| |
| if(pool->spare) { |
| chunk = pool->spare; |
| pool->spare = chunk->next; |
| --pool->spare_count; |
| chunk_reset(chunk); |
| *pchunk = chunk; |
| return CURLE_OK; |
| } |
| |
| chunk = calloc(1, sizeof(*chunk) + pool->chunk_size); |
| if(!chunk) { |
| *pchunk = NULL; |
| return CURLE_OUT_OF_MEMORY; |
| } |
| chunk->dlen = pool->chunk_size; |
| *pchunk = chunk; |
| return CURLE_OK; |
| } |
| |
| static void bufcp_put(struct bufc_pool *pool, |
| struct buf_chunk *chunk) |
| { |
| if(pool->spare_count >= pool->spare_max) { |
| free(chunk); |
| } |
| else { |
| chunk_reset(chunk); |
| chunk->next = pool->spare; |
| pool->spare = chunk; |
| ++pool->spare_count; |
| } |
| } |
| |
| void Curl_bufcp_free(struct bufc_pool *pool) |
| { |
| chunk_list_free(&pool->spare); |
| pool->spare_count = 0; |
| } |
| |
| static void bufq_init(struct bufq *q, struct bufc_pool *pool, |
| size_t chunk_size, size_t max_chunks, int opts) |
| { |
| DEBUGASSERT(chunk_size > 0); |
| DEBUGASSERT(max_chunks > 0); |
| memset(q, 0, sizeof(*q)); |
| q->chunk_size = chunk_size; |
| q->max_chunks = max_chunks; |
| q->pool = pool; |
| q->opts = opts; |
| } |
| |
| void Curl_bufq_init2(struct bufq *q, size_t chunk_size, size_t max_chunks, |
| int opts) |
| { |
| bufq_init(q, NULL, chunk_size, max_chunks, opts); |
| } |
| |
| void Curl_bufq_init(struct bufq *q, size_t chunk_size, size_t max_chunks) |
| { |
| bufq_init(q, NULL, chunk_size, max_chunks, BUFQ_OPT_NONE); |
| } |
| |
| void Curl_bufq_initp(struct bufq *q, struct bufc_pool *pool, |
| size_t max_chunks, int opts) |
| { |
| bufq_init(q, pool, pool->chunk_size, max_chunks, opts); |
| } |
| |
| void Curl_bufq_free(struct bufq *q) |
| { |
| chunk_list_free(&q->head); |
| chunk_list_free(&q->spare); |
| q->tail = NULL; |
| q->chunk_count = 0; |
| } |
| |
| void Curl_bufq_reset(struct bufq *q) |
| { |
| struct buf_chunk *chunk; |
| while(q->head) { |
| chunk = q->head; |
| q->head = chunk->next; |
| chunk->next = q->spare; |
| q->spare = chunk; |
| } |
| q->tail = NULL; |
| } |
| |
| size_t Curl_bufq_len(const struct bufq *q) |
| { |
| const struct buf_chunk *chunk = q->head; |
| size_t len = 0; |
| while(chunk) { |
| len += chunk_len(chunk); |
| chunk = chunk->next; |
| } |
| return len; |
| } |
| |
| size_t Curl_bufq_space(const struct bufq *q) |
| { |
| size_t space = 0; |
| if(q->tail) |
| space += chunk_space(q->tail); |
| if(q->spare) { |
| struct buf_chunk *chunk = q->spare; |
| while(chunk) { |
| space += chunk->dlen; |
| chunk = chunk->next; |
| } |
| } |
| if(q->chunk_count < q->max_chunks) { |
| space += (q->max_chunks - q->chunk_count) * q->chunk_size; |
| } |
| return space; |
| } |
| |
| bool Curl_bufq_is_empty(const struct bufq *q) |
| { |
| return !q->head || chunk_is_empty(q->head); |
| } |
| |
| bool Curl_bufq_is_full(const struct bufq *q) |
| { |
| if(!q->tail || q->spare) |
| return FALSE; |
| if(q->chunk_count < q->max_chunks) |
| return FALSE; |
| if(q->chunk_count > q->max_chunks) |
| return TRUE; |
| /* we have no spares and cannot make more, is the tail full? */ |
| return chunk_is_full(q->tail); |
| } |
| |
| static struct buf_chunk *get_spare(struct bufq *q) |
| { |
| struct buf_chunk *chunk = NULL; |
| |
| if(q->spare) { |
| chunk = q->spare; |
| q->spare = chunk->next; |
| chunk_reset(chunk); |
| return chunk; |
| } |
| |
| if(q->chunk_count >= q->max_chunks && (!(q->opts & BUFQ_OPT_SOFT_LIMIT))) |
| return NULL; |
| |
| if(q->pool) { |
| if(bufcp_take(q->pool, &chunk)) |
| return NULL; |
| ++q->chunk_count; |
| return chunk; |
| } |
| else { |
| chunk = calloc(1, sizeof(*chunk) + q->chunk_size); |
| if(!chunk) |
| return NULL; |
| chunk->dlen = q->chunk_size; |
| ++q->chunk_count; |
| return chunk; |
| } |
| } |
| |
| static void prune_head(struct bufq *q) |
| { |
| struct buf_chunk *chunk; |
| |
| while(q->head && chunk_is_empty(q->head)) { |
| chunk = q->head; |
| q->head = chunk->next; |
| if(q->tail == chunk) |
| q->tail = q->head; |
| if(q->pool) { |
| bufcp_put(q->pool, chunk); |
| --q->chunk_count; |
| } |
| else if((q->chunk_count > q->max_chunks) || |
| (q->opts & BUFQ_OPT_NO_SPARES)) { |
| /* SOFT_LIMIT allowed us more than max. free spares until |
| * we are at max again. Or free them if we are configured |
| * to not use spares. */ |
| free(chunk); |
| --q->chunk_count; |
| } |
| else { |
| chunk->next = q->spare; |
| q->spare = chunk; |
| } |
| } |
| } |
| |
| static struct buf_chunk *get_non_full_tail(struct bufq *q) |
| { |
| struct buf_chunk *chunk; |
| |
| if(q->tail && !chunk_is_full(q->tail)) |
| return q->tail; |
| chunk = get_spare(q); |
| if(chunk) { |
| /* new tail, and possibly new head */ |
| if(q->tail) { |
| q->tail->next = chunk; |
| q->tail = chunk; |
| } |
| else { |
| DEBUGASSERT(!q->head); |
| q->head = q->tail = chunk; |
| } |
| } |
| return chunk; |
| } |
| |
| ssize_t Curl_bufq_write(struct bufq *q, |
| const unsigned char *buf, size_t len, |
| CURLcode *err) |
| { |
| struct buf_chunk *tail; |
| ssize_t nwritten = 0; |
| size_t n; |
| |
| DEBUGASSERT(q->max_chunks > 0); |
| while(len) { |
| tail = get_non_full_tail(q); |
| if(!tail) { |
| if((q->chunk_count < q->max_chunks) || (q->opts & BUFQ_OPT_SOFT_LIMIT)) { |
| *err = CURLE_OUT_OF_MEMORY; |
| return -1; |
| } |
| break; |
| } |
| n = chunk_append(tail, buf, len); |
| if(!n) |
| break; |
| nwritten += n; |
| buf += n; |
| len -= n; |
| } |
| if(nwritten == 0 && len) { |
| *err = CURLE_AGAIN; |
| return -1; |
| } |
| *err = CURLE_OK; |
| return nwritten; |
| } |
| |
| CURLcode Curl_bufq_cwrite(struct bufq *q, |
| const char *buf, size_t len, |
| size_t *pnwritten) |
| { |
| ssize_t n; |
| CURLcode result; |
| n = Curl_bufq_write(q, (const unsigned char *)buf, len, &result); |
| *pnwritten = (n < 0)? 0 : (size_t)n; |
| return result; |
| } |
| |
| ssize_t Curl_bufq_read(struct bufq *q, unsigned char *buf, size_t len, |
| CURLcode *err) |
| { |
| ssize_t nread = 0; |
| size_t n; |
| |
| *err = CURLE_OK; |
| while(len && q->head) { |
| n = chunk_read(q->head, buf, len); |
| if(n) { |
| nread += n; |
| buf += n; |
| len -= n; |
| } |
| prune_head(q); |
| } |
| if(nread == 0) { |
| *err = CURLE_AGAIN; |
| return -1; |
| } |
| return nread; |
| } |
| |
| CURLcode Curl_bufq_cread(struct bufq *q, char *buf, size_t len, |
| size_t *pnread) |
| { |
| ssize_t n; |
| CURLcode result; |
| n = Curl_bufq_read(q, (unsigned char *)buf, len, &result); |
| *pnread = (n < 0)? 0 : (size_t)n; |
| return result; |
| } |
| |
| bool Curl_bufq_peek(struct bufq *q, |
| const unsigned char **pbuf, size_t *plen) |
| { |
| if(q->head && chunk_is_empty(q->head)) { |
| prune_head(q); |
| } |
| if(q->head && !chunk_is_empty(q->head)) { |
| chunk_peek(q->head, pbuf, plen); |
| return TRUE; |
| } |
| *pbuf = NULL; |
| *plen = 0; |
| return FALSE; |
| } |
| |
| bool Curl_bufq_peek_at(struct bufq *q, size_t offset, |
| const unsigned char **pbuf, size_t *plen) |
| { |
| struct buf_chunk *c = q->head; |
| size_t clen; |
| |
| while(c) { |
| clen = chunk_len(c); |
| if(!clen) |
| break; |
| if(offset >= clen) { |
| offset -= clen; |
| c = c->next; |
| continue; |
| } |
| chunk_peek_at(c, offset, pbuf, plen); |
| return TRUE; |
| } |
| *pbuf = NULL; |
| *plen = 0; |
| return FALSE; |
| } |
| |
| void Curl_bufq_skip(struct bufq *q, size_t amount) |
| { |
| size_t n; |
| |
| while(amount && q->head) { |
| n = chunk_skip(q->head, amount); |
| amount -= n; |
| prune_head(q); |
| } |
| } |
| |
| ssize_t Curl_bufq_pass(struct bufq *q, Curl_bufq_writer *writer, |
| void *writer_ctx, CURLcode *err) |
| { |
| const unsigned char *buf; |
| size_t blen; |
| ssize_t nwritten = 0; |
| |
| while(Curl_bufq_peek(q, &buf, &blen)) { |
| ssize_t chunk_written; |
| |
| chunk_written = writer(writer_ctx, buf, blen, err); |
| if(chunk_written < 0) { |
| if(!nwritten || *err != CURLE_AGAIN) { |
| /* blocked on first write or real error, fail */ |
| nwritten = -1; |
| } |
| break; |
| } |
| if(!chunk_written) { |
| if(!nwritten) { |
| /* treat as blocked */ |
| *err = CURLE_AGAIN; |
| nwritten = -1; |
| } |
| break; |
| } |
| Curl_bufq_skip(q, (size_t)chunk_written); |
| nwritten += chunk_written; |
| } |
| return nwritten; |
| } |
| |
| ssize_t Curl_bufq_write_pass(struct bufq *q, |
| const unsigned char *buf, size_t len, |
| Curl_bufq_writer *writer, void *writer_ctx, |
| CURLcode *err) |
| { |
| ssize_t nwritten = 0, n; |
| |
| *err = CURLE_OK; |
| while(len) { |
| if(Curl_bufq_is_full(q)) { |
| /* try to make room in case we are full */ |
| n = Curl_bufq_pass(q, writer, writer_ctx, err); |
| if(n < 0) { |
| if(*err != CURLE_AGAIN) { |
| /* real error, fail */ |
| return -1; |
| } |
| /* would block, bufq is full, give up */ |
| break; |
| } |
| } |
| |
| /* Add whatever is remaining now to bufq */ |
| n = Curl_bufq_write(q, buf, len, err); |
| if(n < 0) { |
| if(*err != CURLE_AGAIN) { |
| /* real error, fail */ |
| return -1; |
| } |
| /* no room in bufq */ |
| break; |
| } |
| /* edge case of writer returning 0 (and len is >0) |
| * break or we might enter an infinite loop here */ |
| if(n == 0) |
| break; |
| |
| /* Maybe only part of `data` has been added, continue to loop */ |
| buf += (size_t)n; |
| len -= (size_t)n; |
| nwritten += (size_t)n; |
| } |
| |
| if(!nwritten && len) { |
| *err = CURLE_AGAIN; |
| return -1; |
| } |
| *err = CURLE_OK; |
| return nwritten; |
| } |
| |
| ssize_t Curl_bufq_sipn(struct bufq *q, size_t max_len, |
| Curl_bufq_reader *reader, void *reader_ctx, |
| CURLcode *err) |
| { |
| struct buf_chunk *tail = NULL; |
| ssize_t nread; |
| |
| *err = CURLE_AGAIN; |
| tail = get_non_full_tail(q); |
| if(!tail) { |
| if(q->chunk_count < q->max_chunks) { |
| *err = CURLE_OUT_OF_MEMORY; |
| return -1; |
| } |
| /* full, blocked */ |
| *err = CURLE_AGAIN; |
| return -1; |
| } |
| |
| nread = chunk_slurpn(tail, max_len, reader, reader_ctx, err); |
| if(nread < 0) { |
| return -1; |
| } |
| else if(nread == 0) { |
| /* eof */ |
| *err = CURLE_OK; |
| } |
| return nread; |
| } |
| |
| /** |
| * Read up to `max_len` bytes and append it to the end of the buffer queue. |
| * if `max_len` is 0, no limit is imposed and the call behaves exactly |
| * the same as `Curl_bufq_slurp()`. |
| * Returns the total amount of buf read (may be 0) or -1 on other |
| * reader errors. |
| * Note that even in case of a -1 chunks may have been read and |
| * the buffer queue will have different length than before. |
| */ |
| static ssize_t bufq_slurpn(struct bufq *q, size_t max_len, |
| Curl_bufq_reader *reader, void *reader_ctx, |
| CURLcode *err) |
| { |
| ssize_t nread = 0, n; |
| |
| *err = CURLE_AGAIN; |
| while(1) { |
| |
| n = Curl_bufq_sipn(q, max_len, reader, reader_ctx, err); |
| if(n < 0) { |
| if(!nread || *err != CURLE_AGAIN) { |
| /* blocked on first read or real error, fail */ |
| nread = -1; |
| } |
| else |
| *err = CURLE_OK; |
| break; |
| } |
| else if(n == 0) { |
| /* eof */ |
| *err = CURLE_OK; |
| break; |
| } |
| nread += (size_t)n; |
| if(max_len) { |
| DEBUGASSERT((size_t)n <= max_len); |
| max_len -= (size_t)n; |
| if(!max_len) |
| break; |
| } |
| /* give up slurping when we get less bytes than we asked for */ |
| if(q->tail && !chunk_is_full(q->tail)) |
| break; |
| } |
| return nread; |
| } |
| |
| ssize_t Curl_bufq_slurp(struct bufq *q, Curl_bufq_reader *reader, |
| void *reader_ctx, CURLcode *err) |
| { |
| return bufq_slurpn(q, 0, reader, reader_ctx, err); |
| } |