Files
RetroArch/libretro-common/lists/string_list.c
T
Arnold de Ruiterandlibretroadmin e337babce9 core_info: read an info file added after its core was cached, keep served entries in a rewritten cache (#20007)
* Fix core info cache ignoring late .info files and dropping cached entries on rewrite

* core_info: write the cache straight from the core list

The cache writer takes the core list's entries as they are instead of
a deep copy of every installed core, and a core read from its info
file no longer gets copied into a cache list that is not written.

core_info_cache_test also checks that an unchanged core set leaves the
cache alone, and runs under ASan and UBSan in Linux-samples-tasks.

* core_info: read cached entries in place, take core paths from the listing

The cache reader parses each entry straight into the next slot of the
cache list instead of a separate allocation moved in afterwards, and a
firmware list grows geometrically rather than one entry per realloc.
A core's path is taken over from the directory listing, which is freed
at the end of the same pass, instead of being copied.

core_info_cache_test adds a start served from the cache alone, with
more cores than the list starts with and long firmware lists, and a
cache cut off inside an entry, which must be discarded without a leak.

* string_list: add packed splits, use them for core info lists

string_split_packed() splits into a single allocation holding the
list, its elements and the tokens, marked by cap 0 with elements
present. string_list_free() releases it in one free; growing it and
string_list_deinitialize() are refused, so a packed list stays
read-only.

Every pipe-separated core info field is split this way, from the
cache and from info files alike, which takes a warm start over the
full libretro-core-info set from 16661 allocations to 8851. The
string_split_packed sample checks it against string_split and the
refusals, under ASan and UBSan and as a mingw-w64 build under Wine.

---------

Co-authored-by: libretroadmin <[email protected]>
2026-11-06 14:16:58 +01:00

604 lines
16 KiB
C

/* Copyright (C) 2010-2020 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (string_list.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <retro_posix_source.h>
#include <stdint.h>
#include <string.h>
#include <ctype.h>
#include <lists/string_list.h>
#include <compat/strl.h>
#include <compat/posix_string.h>
static bool string_list_deinitialize_internal(struct string_list *list)
{
if (!list)
return false;
if (list->elems)
{
unsigned i;
for (i = 0; i < (unsigned)list->size; i++)
{
if (list->elems[i].data)
free(list->elems[i].data);
if (list->elems[i].userdata)
free(list->elems[i].userdata);
}
free(list->elems);
}
list->elems = NULL;
return true;
}
#define STRING_LIST_PACKED(list) (!(list)->cap && (list)->elems)
bool string_list_capacity(struct string_list *list, size_t cap)
{
struct string_list_elem *new_data;
/* Public API, so hold it to its contract. A cap below size would
* realloc the array out from under the live elements while size
* still counted them, and the next walk or free ran off the end;
* a cap of zero is a realloc(p, 0), which on glibc frees p and
* returns NULL - reported here as failure with elems dangling. */
if (!list || cap < list->size || cap == 0 || STRING_LIST_PACKED(list))
return false;
/* Guard the byte-count multiplication: a huge cap would wrap and
* realloc a buffer far smaller than the caller expects. */
if (cap > SIZE_MAX / sizeof(*new_data))
return false;
new_data = (struct string_list_elem*)
realloc(list->elems, cap * sizeof(*new_data));
if (!new_data)
return false;
/* Slots in [size, cap) are never read: string_list_free() only walks
* [0, size), and the append paths fully initialise a slot when it
* becomes live. So there is no need to zero the whole new half on
* every doubling, which for large lists was a memset over hundreds of
* thousands of elements to add one entry. */
list->elems = new_data;
list->cap = cap;
return true;
}
void string_list_free(struct string_list *list)
{
if (!list)
return;
if (!STRING_LIST_PACKED(list))
string_list_deinitialize_internal(list);
free(list);
}
bool string_list_deinitialize(struct string_list *list)
{
if (!list || STRING_LIST_PACKED(list))
return false;
if (!string_list_deinitialize_internal(list))
return false;
list->elems = NULL;
list->size = 0;
list->cap = 0;
return true;
}
struct string_list *string_list_new(void)
{
struct string_list_elem *elems = NULL;
struct string_list *list = (struct string_list*)
malloc(sizeof(*list));
if (!list)
return NULL;
list->cap = 0;
list->size = 0;
list->elems = NULL;
elems = (struct string_list_elem*)
calloc(32, sizeof(*elems));
if (!elems)
{
free(list);
return NULL;
}
list->elems = elems;
list->cap = 32;
return list;
}
bool string_list_initialize(struct string_list *list)
{
struct string_list_elem *elems = NULL;
if (!list)
return false;
elems = (struct string_list_elem*)
calloc(32, sizeof(*elems));
if (!elems)
{
string_list_deinitialize(list);
return false;
}
list->elems = elems;
list->size = 0;
list->cap = 32;
return true;
}
bool string_list_append(struct string_list *list, const char *elem,
union string_list_elem_attr attr)
{
char *data_dup = NULL;
if (list->size >= list->cap)
{
if (list->cap > SIZE_MAX / 2)
return false;
if (!string_list_capacity(list,
(list->cap > 0) ? (list->cap * 2) : 32))
return false;
}
data_dup = strdup(elem);
if (!data_dup)
return false;
list->elems[list->size].data = data_dup;
list->elems[list->size].attr = attr;
/* Slot is not pre-zeroed (see string_list_capacity); userdata must
* be NULL so string_list_free() does not free garbage. */
list->elems[list->size].userdata = NULL;
list->size++;
return true;
}
bool string_list_append_n(struct string_list *list, const char *elem,
size_t len, union string_list_elem_attr attr)
{
char *data_dup = NULL;
if (list->size >= list->cap)
{
if (list->cap > SIZE_MAX / 2)
return false;
if (!string_list_capacity(list,
(list->cap > 0) ? (list->cap * 2) : 32))
return false;
}
/* len + 1 wraps to 0 at SIZE_MAX; the memcpy below would then run
* len bytes into a zero-byte allocation. Not reachable from a real
* string, but this is a public entry point taking any size_t. */
if (len == SIZE_MAX)
return false;
data_dup = (char*)malloc(len + 1);
if (!data_dup)
return false;
memcpy(data_dup, elem, len);
data_dup[len] = '\0';
list->elems[list->size].data = data_dup;
list->elems[list->size].attr = attr;
/* Slot is not pre-zeroed (see string_list_capacity); userdata must
* be NULL so string_list_free() does not free garbage. */
list->elems[list->size].userdata = NULL;
list->size++;
return true;
}
void string_list_join_concat(char *s, size_t len,
const struct string_list *list, const char *delim)
{
size_t _len = strlen(s);
/* If @s is already 'full', nothing
* further can be added
* > This condition will also be triggered
* if @s is not NULL-terminated,
* in which case any attempt to increment
* @s or decrement @len would lead to
* undefined behaviour */
if (_len < len)
{
size_t i;
size_t dlen = strlen(delim);
for (i = 0; i < list->size; i++)
{
/* strlcpy() reports the length it was handed rather than the
* length it wrote, so an append that truncates would carry
* _len past len and leave every later len - _len wrapping to
* a very large size_t with s + _len already past the end.
* Fill what is left and stop instead. */
size_t elen = strlen(list->elems[i].data);
if (_len + elen >= len)
{
strlcpy(s + _len, list->elems[i].data, len - _len);
break;
}
_len += strlcpy(s + _len, list->elems[i].data, len - _len);
if ((i + 1) < list->size)
{
if (_len + dlen >= len)
{
strlcpy(s + _len, delim, len - _len);
break;
}
_len += strlcpy(s + _len, delim, len - _len);
}
}
}
}
void string_list_join_concat_special(char *s, size_t len,
const struct string_list *list, const char *delim)
{
size_t _len = strlen(s);
/* As in string_list_join_concat() above: @s already being full
* leaves nothing to add, and an append is made only once it is
* known to fit. */
if (_len < len)
{
size_t i;
size_t dlen = strlen(delim);
for (i = 0; i < list->size; i++)
{
size_t elen = strlen(list->elems[i].data);
if (_len + elen >= len)
{
strlcpy(s + _len, list->elems[i].data, len - _len);
break;
}
_len += strlcpy(s + _len, list->elems[i].data, len - _len);
if ((i + 1) < list->size)
{
if (_len + dlen >= len)
{
strlcpy(s + _len, delim, len - _len);
break;
}
_len += strlcpy(s + _len, delim, len - _len);
}
}
}
}
/**
* Count delimited tokens in @str without modifying it.
* Treats any character in @delim as a separator (same as strtok).
* Returns the number of non-empty tokens.
*/
static size_t string_count_tokens(const char *str, const char *delim)
{
size_t count = 0;
bool in_tok = false;
const char *p = str;
for (; *p != '\0'; p++)
{
if (strchr(delim, (unsigned char)*p))
in_tok = false;
else if (!in_tok)
{
in_tok = true;
count++;
}
}
return count;
}
struct string_list *string_split(const char *str, const char *delim)
{
const char *p = str;
struct string_list *list = string_list_new();
if (!list)
return NULL;
while (*p)
{
union string_list_elem_attr attr;
const char *tok;
while (*p && strchr(delim, *p))
p++;
if (!*p)
break;
tok = p;
while (*p && !strchr(delim, *p))
p++;
attr.i = 0;
if (!string_list_append_n(list, tok, p - tok, attr))
goto error;
}
return list;
error:
string_list_free(list);
return NULL;
}
struct string_list *string_split_packed(const char *str, const char *delim)
{
size_t i, n, slen, head;
char *out;
const char *p;
struct string_list *list;
if (!str || !delim)
return NULL;
n = string_count_tokens(str, delim);
slen = strlen(str);
head = sizeof(*list) + n * sizeof(struct string_list_elem);
if (n > (SIZE_MAX - sizeof(*list)) / sizeof(struct string_list_elem)
|| slen >= SIZE_MAX - head)
return NULL;
if (!(list = (struct string_list*)malloc(head + slen + 1)))
return NULL;
list->size = n;
list->cap = 0;
list->elems = n ? (struct string_list_elem*)(list + 1) : NULL;
out = (char*)list + head;
p = str;
for (i = 0; i < n; i++)
{
const char *tok;
while (*p && strchr(delim, *p))
p++;
tok = p;
while (*p && !strchr(delim, *p))
p++;
memcpy(out, tok, (size_t)(p - tok));
list->elems[i].data = out;
list->elems[i].userdata = NULL;
list->elems[i].attr.i = 0;
out += p - tok;
*out++ = '\0';
}
return list;
}
bool string_split_noalloc(struct string_list *list,
const char *str, const char *delim)
{
size_t _len;
const char *end;
const char *p = str;
if (!list || !str || !delim || !*delim)
return false;
/* Compute delimiter length once. */
{
const char *d = delim;
while (*d)
d++;
_len = d - delim;
}
/* Pre-size to avoid repeated reallocs. */
{
size_t __len = string_count_tokens(str, delim);
if (__len > list->cap)
{
if (!string_list_capacity(list, __len))
return false;
}
}
while (*p)
{
union string_list_elem_attr attr;
size_t __len;
attr.i = 0;
end = strstr(p, delim);
if (end)
{
__len = end - p;
if (__len > 0)
{
if (!string_list_append_n(list, p, __len, attr))
return false;
}
p = end + _len;
}
else
{
const char *s = p;
while (*s)
s++;
__len = s - p;
if (__len > 0)
{
if (!string_list_append_n(list, p, __len, attr))
return false;
}
break;
}
}
return true;
}
int string_list_find_elem(const struct string_list *list, const char *elem)
{
if (list && elem)
{
size_t i;
for (i = 0; i < list->size; i++)
{
const unsigned char *p1 = (const unsigned char*)list->elems[i].data;
const unsigned char *p2 = (const unsigned char*)elem;
while ((*p1 | 32) == (*p2 | 32) || (*p1 == *p2))
{
if (*p1 == '\0')
return (int)(i + 1);
p1++;
p2++;
}
}
}
return 0;
}
bool string_list_find_elem_prefix(const struct string_list *list,
const char *prefix, const char *elem)
{
if (list)
{
size_t i;
char prefixed[255];
size_t _len = strlcpy(prefixed, prefix, sizeof(prefixed));
strlcpy(prefixed + _len, elem, sizeof(prefixed) - _len);
for (i = 0; i < list->size; i++)
{
const char *data = list->elems[i].data;
const char *a = data;
const char *b = elem;
while (tolower((unsigned char)*a) == tolower((unsigned char)*b))
{
if (*a == '\0')
return true;
a++;
b++;
}
a = data;
b = prefixed;
while (tolower((unsigned char)*a) == tolower((unsigned char)*b))
{
if (*a == '\0')
return true;
a++;
b++;
}
}
}
return false;
}
struct string_list *string_list_clone(const struct string_list *src)
{
size_t i;
struct string_list_elem *elems = NULL;
struct string_list *dest = (struct string_list*)
malloc(sizeof(struct string_list));
if (!dest)
return NULL;
dest->elems = NULL;
dest->size = src->size;
dest->cap = (src->cap < dest->size) ? dest->size : src->cap;
elems = (struct string_list_elem*)
calloc(dest->cap, sizeof(struct string_list_elem));
if (!elems)
{
free(dest);
return NULL;
}
dest->elems = elems;
for (i = 0; i < src->size; i++)
{
const char *_src = src->elems[i].data;
size_t slen = _src ? strlen(_src) : 0;
dest->elems[i].data = NULL;
dest->elems[i].attr = src->elems[i].attr;
if (slen != 0)
{
char *ret = (char*)malloc(slen + 1);
/* Pre-patch: on malloc failure 'dest->elems[i].data'
* silently stayed NULL and the loop moved on. That left
* the caller holding a dest with dest->size set to
* src->size but some .data pointers NULL - a trap for
* every consumer that assumes .data is non-NULL.
* string_list_find_elem (line 349), string_list_
* join_concat (line 200), and others all dereference
* .data unconditionally. Single empty-string inputs
* are represented by slen == 0 and never hit this
* branch, so NULL here always means OOM, not 'empty
* element was intended'.
*
* Tear down the partially-cloned destination and
* return NULL. Callers of string_list_clone already
* handle NULL returns (clone-failure is an OOM signal)
* - NULL is both safer and more informative than a
* silently-corrupted list. */
if (!ret)
{
size_t j;
for (j = 0; j < i; j++)
if (dest->elems[j].data)
free(dest->elems[j].data);
free(dest->elems);
free(dest);
return NULL;
}
memcpy(ret, _src, slen + 1); /* memcpy > strcpy: no NUL scan */
dest->elems[i].data = ret;
}
}
return dest;
}