diff options
author | Lennart Poettering <lennart@poettering.net> | 2014-08-19 19:05:11 +0200 |
---|---|---|
committer | Lennart Poettering <lennart@poettering.net> | 2014-08-19 19:06:39 +0200 |
commit | 8530dc4467691a893aa2e07319b18a84fec96cad (patch) | |
tree | ee5e6242a52b57b6a47ff86e5c731100e7a7240a | |
parent | 81163121e649523b4071f67ddc03c2db649036c5 (diff) |
tmpfiles: add new 'r' line type to add UIDs/GIDs to the pool to allocate UIDs/GIDs from
This way we can guarantee a limited amount of compatibility with
login.defs, by generate an appopriate "r" line out of it, on package
installation.
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile.am | 11 | ||||
-rw-r--r-- | man/sysusers.d.xml | 31 | ||||
-rw-r--r-- | src/shared/uid-range.c | 205 | ||||
-rw-r--r-- | src/shared/uid-range.h | 34 | ||||
-rw-r--r-- | src/sysusers/sysusers.c | 207 | ||||
-rw-r--r-- | src/test/test-uid-range.c | 91 |
7 files changed, 507 insertions, 73 deletions
diff --git a/.gitignore b/.gitignore index a3d8c4ea91..8189da71f0 100644 --- a/.gitignore +++ b/.gitignore @@ -228,6 +228,7 @@ /test-time /test-tmpfiles /test-udev +/test-uid-range /test-unifont /test-unit-file /test-unit-name diff --git a/Makefile.am b/Makefile.am index 3ef9c24313..d4d5775475 100644 --- a/Makefile.am +++ b/Makefile.am @@ -862,6 +862,8 @@ libsystemd_shared_la_SOURCES = \ src/shared/base-filesystem.h \ src/shared/memfd.c \ src/shared/memfd.h \ + src/shared/uid-range.c \ + src/shared/uid-range.h \ src/shared/nss-util.h nodist_libsystemd_shared_la_SOURCES = \ @@ -1322,7 +1324,8 @@ tests += \ test-capability \ test-async \ test-ratelimit \ - test-condition-util + test-condition-util \ + test-uid-range EXTRA_DIST += \ test/a.service \ @@ -1487,6 +1490,12 @@ test_util_SOURCES = \ test_util_LDADD = \ libsystemd-core.la +test_uid_range_SOURCES = \ + src/test/test-uid-range.c + +test_uid_range_LDADD = \ + libsystemd-core.la + test_socket_util_SOURCES = \ src/test/test-socket-util.c diff --git a/man/sysusers.d.xml b/man/sysusers.d.xml index 1832ecf0e6..18c71db63e 100644 --- a/man/sysusers.d.xml +++ b/man/sysusers.d.xml @@ -134,6 +134,25 @@ u root 0 "Superuser" /root</programlisting> will be implicitly created.</para></listitem> </varlistentry> + + <varlistentry> + <term><varname>r</varname></term> + <listitem><para>Add a range of + numeric UIDs/GIDs to the pool + to allocate new UIDs and GIDs + from. If no line of this type + is specified the range of + UIDs/GIDs is set to some + compiled-in default. Note that + both UIDs and GIDs are + allocated from the same pool, + in order to ensure that users + and groups of the same name + are likely to carry the same + numeric UID and + GID.</para></listitem> + </varlistentry> + </variablelist> </refsect2> @@ -154,6 +173,10 @@ u root 0 "Superuser" /root</programlisting> <para>For <varname>m</varname> lines this field should contain the user name to add to a group.</para> + + <para>For lines of type <varname>r</varname> + this field should be set to + <literal>-</literal>.</para> </refsect2> <refsect2> @@ -175,6 +198,14 @@ u root 0 "Superuser" /root</programlisting> <para>For <varname>m</varname> lines this field should contain the group name to add to a user to.</para> + + <para>For lines of type <varname>r</varname> + this field should be set to a UID/GID range in + the format <literal>FROM-TO</literal> where + both values are formatted as decimal ASCII + numbers. Alternatively, a single UID/GID may + be specified formatted as decimal ASCII + numbers.</para> </refsect2> <refsect2> diff --git a/src/shared/uid-range.c b/src/shared/uid-range.c new file mode 100644 index 0000000000..74c3be4a13 --- /dev/null +++ b/src/shared/uid-range.c @@ -0,0 +1,205 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2014 Lennart Poettering + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see <http://www.gnu.org/licenses/>. +***/ + +#include "util.h" +#include "uid-range.h" + +static bool uid_range_intersect(UidRange *range, uid_t start, uid_t nr) { + assert(range); + + return range->start <= start + nr && + range->start + range->nr >= start; +} + +static void uid_range_coalesce(UidRange **p, unsigned *n) { + unsigned i, j; + + assert(p); + assert(n); + + for (i = 0; i < *n; i++) { + for (j = i + 1; j < *n; j++) { + UidRange *x = (*p)+i, *y = (*p)+j; + + if (uid_range_intersect(x, y->start, y->nr)) { + uid_t begin, end; + + begin = MIN(x->start, y->start); + end = MAX(x->start + x->nr, y->start + y->nr); + + x->start = begin; + x->nr = end - begin; + + if (*n > j+1) + memmove(y, y+1, sizeof(UidRange) * (*n - j -1)); + + (*n) --; + j--; + } + } + } + +} + +static int uid_range_compare(const void *a, const void *b) { + const UidRange *x = a, *y = b; + + if (x->start < y->start) + return -1; + if (x->start > y->start) + return 1; + + if (x->nr < y->nr) + return -1; + if (x->nr > y->nr) + return 1; + + return 0; +} + +int uid_range_add(UidRange **p, unsigned *n, uid_t start, uid_t nr) { + bool found = false; + UidRange *x; + unsigned i; + + assert(p); + assert(n); + + if (nr <= 0) + return 0; + + for (i = 0; i < *n; i++) { + x = (*p) + i; + if (uid_range_intersect(x, start, nr)) { + found = true; + break; + } + } + + if (found) { + uid_t begin, end; + + begin = MIN(x->start, start); + end = MAX(x->start + x->nr, start + nr); + + x->start = begin; + x->nr = end - begin; + } else { + UidRange *t; + + t = realloc(*p, sizeof(UidRange) * (*n + 1)); + if (!t) + return -ENOMEM; + + *p = t; + x = t + ((*n) ++); + + x->start = start; + x->nr = nr; + } + + qsort(*p, *n, sizeof(UidRange), uid_range_compare); + uid_range_coalesce(p, n); + + return *n; +} + +int uid_range_add_str(UidRange **p, unsigned *n, const char *s) { + uid_t start, nr; + const char *t; + int r; + + assert(p); + assert(n); + assert(s); + + t = strchr(s, '-'); + if (t) { + char *b; + uid_t end; + + b = strndupa(s, t - s); + r = parse_uid(b, &start); + if (r < 0) + return r; + + r = parse_uid(t+1, &end); + if (r < 0) + return r; + + if (end < start) + return -EINVAL; + + nr = end - start + 1; + } else { + r = parse_uid(s, &start); + if (r < 0) + return r; + + nr = 1; + } + + return uid_range_add(p, n, start, nr); +} + +int uid_range_next_lower(const UidRange *p, unsigned n, uid_t *uid) { + uid_t closest = (uid_t) -1, candidate; + unsigned i; + + assert(p); + assert(uid); + + candidate = *uid - 1; + + for (i = 0; i < n; i++) { + uid_t begin, end; + + begin = p[i].start; + end = p[i].start + p[i].nr - 1; + + if (candidate >= begin && candidate <= end) { + *uid = candidate; + return 1; + } + + if (end < candidate) + closest = end; + } + + if (closest == (uid_t) -1) + return -EBUSY; + + *uid = closest; + return 1; +} + +bool uid_range_contains(const UidRange *p, unsigned n, uid_t uid) { + unsigned i; + + assert(p); + assert(uid); + + for (i = 0; i < n; i++) + if (uid >= p[i].start && uid < p[i].start + p[i].nr) + return true; + + return false; +} diff --git a/src/shared/uid-range.h b/src/shared/uid-range.h new file mode 100644 index 0000000000..d3dac8df63 --- /dev/null +++ b/src/shared/uid-range.h @@ -0,0 +1,34 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +#pragma once + +/*** + This file is part of systemd. + + Copyright 2014 Lennart Poettering + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see <http://www.gnu.org/licenses/>. +***/ + +#include <sys/types.h> + +typedef struct UidRange { + uid_t start, nr; +} UidRange; + +int uid_range_add(UidRange **p, unsigned *n, uid_t start, uid_t nr); +int uid_range_add_str(UidRange **p, unsigned *n, const char *s); + +int uid_range_next_lower(const UidRange *p, unsigned n, uid_t *uid); +bool uid_range_contains(const UidRange *p, unsigned n, uid_t uid); diff --git a/src/sysusers/sysusers.c b/src/sysusers/sysusers.c index eedc1e067e..1ff0e21a06 100644 --- a/src/sysusers/sysusers.c +++ b/src/sysusers/sysusers.c @@ -38,11 +38,13 @@ #include "utf8.h" #include "label.h" #include "fileio-label.h" +#include "uid-range.h" typedef enum ItemType { ADD_USER = 'u', ADD_GROUP = 'g', ADD_MEMBER = 'm', + ADD_RANGE = 'r', } ItemType; typedef struct Item { ItemType type; @@ -82,8 +84,9 @@ static Hashmap *members = NULL; static Hashmap *database_uid = NULL, *database_user = NULL; static Hashmap *database_gid = NULL, *database_group = NULL; -static uid_t search_uid = SYSTEM_UID_MAX; -static gid_t search_gid = SYSTEM_GID_MAX; +static uid_t search_uid = (uid_t) -1; +static UidRange *uid_range = NULL; +static unsigned n_uid_range = 0; #define UID_TO_PTR(u) (ULONG_TO_PTR(u+1)) #define PTR_TO_UID(u) ((uid_t) (PTR_TO_ULONG(u)-1)) @@ -916,7 +919,7 @@ static int add_user(Item *i) { if (read_id_from_file(i, &c, NULL) > 0) { - if (c <= 0 || c > SYSTEM_UID_MAX) + if (c <= 0 || !uid_range_contains(uid_range, n_uid_range, c)) log_debug("User ID " UID_FMT " of file not suitable for %s.", c, i->name); else { r = uid_is_ok(c, i->name); @@ -947,7 +950,12 @@ static int add_user(Item *i) { /* And if that didn't work either, let's try to find a free one */ if (!i->uid_set) { - for (; search_uid > 0; search_uid--) { + for (;;) { + r = uid_range_next_lower(uid_range, n_uid_range, &search_uid); + if (r < 0) { + log_error("No free user ID available for %s.", i->name); + return r; + } r = uid_is_ok(search_uid, i->name); if (r < 0) { @@ -957,15 +965,8 @@ static int add_user(Item *i) { break; } - if (search_uid <= 0) { - log_error("No free user ID available for %s.", i->name); - return -E2BIG; - } - i->uid_set = true; i->uid = search_uid; - - search_uid--; } r = hashmap_ensure_allocated(&todo_uids, trivial_hash_func, trivial_compare_func); @@ -1083,7 +1084,7 @@ static int add_group(Item *i) { if (read_id_from_file(i, NULL, &c) > 0) { - if (c <= 0 || c > SYSTEM_GID_MAX) + if (c <= 0 || !uid_range_contains(uid_range, n_uid_range, c)) log_debug("Group ID " GID_FMT " of file not suitable for %s.", c, i->name); else { r = gid_is_ok(c); @@ -1101,8 +1102,15 @@ static int add_group(Item *i) { /* And if that didn't work either, let's try to find a free one */ if (!i->gid_set) { - for (; search_gid > 0; search_gid--) { - r = gid_is_ok(search_gid); + for (;;) { + /* We look for new GIDs in the UID pool! */ + r = uid_range_next_lower(uid_range, n_uid_range, &search_uid); + if (r < 0) { + log_error("No free group ID available for %s.", i->name); + return r; + } + + r = gid_is_ok(search_uid); if (r < 0) { log_error("Failed to verify gid " GID_FMT ": %s", i->gid, strerror(-r)); return r; @@ -1110,15 +1118,8 @@ static int add_group(Item *i) { break; } - if (search_gid <= 0) { - log_error("No free group ID available for %s.", i->name); - return -E2BIG; - } - i->gid_set = true; - i->gid = search_gid; - - search_gid--; + i->gid = search_uid; } r = hashmap_ensure_allocated(&todo_gids, trivial_hash_func, trivial_compare_func); @@ -1384,7 +1385,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { {} }; - _cleanup_free_ char *action = NULL, *name = NULL, *id = NULL, *resolved_name = NULL, *description = NULL, *home = NULL; + _cleanup_free_ char *action = NULL, *name = NULL, *id = NULL, *resolved_name = NULL, *resolved_id = NULL, *description = NULL, *home = NULL; _cleanup_(item_freep) Item *i = NULL; Item *existing; Hashmap *h; @@ -1417,66 +1418,113 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { return -EINVAL; } - if (!IN_SET(action[0], ADD_USER, ADD_GROUP, ADD_MEMBER)) { + if (!IN_SET(action[0], ADD_USER, ADD_GROUP, ADD_MEMBER, ADD_RANGE)) { log_error("[%s:%u] Unknown command command type '%c'.", fname, line, action[0]); return -EBADMSG; } /* Verify name */ - r = specifier_printf(name, specifier_table, NULL, &resolved_name); - if (r < 0) { - log_error("[%s:%u] Failed to replace specifiers: %s", fname, line, name); - return r; + if (isempty(name) || streq(name, "-")) { + free(name); + name = NULL; } - if (!valid_user_group_name(resolved_name)) { - log_error("[%s:%u] '%s' is not a valid user or group name.", fname, line, resolved_name); - return -EINVAL; + if (name) { + r = specifier_printf(name, specifier_table, NULL, &resolved_name); + if (r < 0) { + log_error("[%s:%u] Failed to replace specifiers: %s", fname, line, name); + return r; + } + + if (!valid_user_group_name(resolved_name)) { + log_error("[%s:%u] '%s' is not a valid user or group name.", fname, line, resolved_name); + return -EINVAL; + } } - /* Simplify remaining columns */ + /* Verify id */ if (isempty(id) || streq(id, "-")) { free(id); id = NULL; } + if (id) { + r = specifier_printf(id, specifier_table, NULL, &resolved_id); + if (r < 0) { + log_error("[%s:%u] Failed to replace specifiers: %s", fname, line, name); + return r; + } + } + + /* Verify description */ if (isempty(description) || streq(description, "-")) { free(description); description = NULL; } + if (description) { + if (!valid_gecos(description)) { + log_error("[%s:%u] '%s' is not a valid GECOS field.", fname, line, description); + return -EINVAL; + } + } + + /* Verify home */ if (isempty(home) || streq(home, "-")) { free(home); home = NULL; } + if (home) { + if (!valid_home(home)) { + log_error("[%s:%u] '%s' is not a valid home directory field.", fname, line, home); + return -EINVAL; + } + } + switch (action[0]) { - case ADD_MEMBER: { - _cleanup_free_ char *resolved_id = NULL; - char **l; + case ADD_RANGE: + if (resolved_name) { + log_error("[%s:%u] Lines of type 'r' don't take a name field.", fname, line); + return -EINVAL; + } - /* Try to extend an existing member or group item */ + if (!resolved_id) { + log_error("[%s:%u] Lines of type 'r' require a ID range in the third field.", fname, line); + return -EINVAL; + } if (description) { - log_error("[%s:%u] Lines of type 'm' don't take a GECOS field.", fname, line); + log_error("[%s:%u] Lines of type 'r' don't take a GECOS field.", fname, line); return -EINVAL; } if (home) { - log_error("[%s:%u] Lines of type 'm' don't take a home directory field.", fname, line); + log_error("[%s:%u] Lines of type 'r' don't take a home directory field.", fname, line); return -EINVAL; } - if (!id) { - log_error("[%s:%u] Lines of type 'm' require a group name in the third field.", fname, line); + r = uid_range_add_str(&uid_range, &n_uid_range, resolved_id); + if (r < 0) { + log_error("[%s:%u] Invalid UID range %s.", fname, line, resolved_id); return -EINVAL; } - r = specifier_printf(id, specifier_table, NULL, &resolved_id); - if (r < 0) { - log_error("[%s:%u] Failed to replace specifiers: %s", fname, line, name); - return r; + return 0; + + case ADD_MEMBER: { + char **l; + + /* Try to extend an existing member or group item */ + if (!name) { + log_error("[%s:%u] Lines of type 'm' require a user name in the second field.", fname, line); + return -EINVAL; + } + + if (!resolved_id) { + log_error("[%s:%u] Lines of type 'm' require a group name in the third field.", fname, line); + return -EINVAL; } if (!valid_user_group_name(resolved_id)) { @@ -1484,6 +1532,16 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { return -EINVAL; } + if (description) { + log_error("[%s:%u] Lines of type 'm' don't take a GECOS field.", fname, line); + return -EINVAL; + } + + if (home) { + log_error("[%s:%u] Lines of type 'm' don't take a home directory field.", fname, line); + return -EINVAL; + } + r = hashmap_ensure_allocated(&members, string_hash_func, string_compare_func); if (r < 0) return log_oom(); @@ -1521,6 +1579,11 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { } case ADD_USER: + if (!name) { + log_error("[%s:%u] Lines of type 'u' require a user name in the second field.", fname, line); + return -EINVAL; + } + r = hashmap_ensure_allocated(&users, string_hash_func, string_compare_func); if (r < 0) return log_oom(); @@ -1529,14 +1592,14 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { if (!i) return log_oom(); - if (id) { - if (path_is_absolute(id)) { - i->uid_path = id; - id = NULL; + if (resolved_id) { + if (path_is_absolute(resolved_id)) { + i->uid_path = resolved_id; + resolved_id = NULL; path_kill_slashes(i->uid_path); } else { - r = parse_uid(id, &i->uid); + r = parse_uid(resolved_id, &i->uid); if (r < 0) { log_error("Failed to parse UID: %s", id); return -EBADMSG; @@ -1546,30 +1609,20 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { } } - if (description) { - if (!valid_gecos(description)) { - log_error("[%s:%u] '%s' is not a valid GECOS field.", fname, line, description); - return -EINVAL; - } - - i->description = description; - description = NULL; - } - - if (home) { - if (!valid_home(home)) { - log_error("[%s:%u] '%s' is not a valid home directory field.", fname, line, home); - return -EINVAL; - } + i->description = description; + description = NULL; - i->home = home; - home = NULL; - } + i->home = home; + home = NULL; h = users; break; case ADD_GROUP: + if (!name) { + log_error("[%s:%u] Lines of type 'g' require a user name in the second field.", fname, line); + return -EINVAL; + } if (description) { log_error("[%s:%u] Lines of type 'g' don't take a GECOS field.", fname, line); @@ -1589,14 +1642,14 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { if (!i) return log_oom(); - if (id) { - if (path_is_absolute(id)) { - i->gid_path = id; - id = NULL; + if (resolved_id) { + if (path_is_absolute(resolved_id)) { + i->gid_path = resolved_id; + resolved_id = NULL; path_kill_slashes(i->gid_path); } else { - r = parse_gid(id, &i->gid); + r = parse_gid(resolved_id, &i->gid); if (r < 0) { log_error("Failed to parse GID: %s", id); return -EBADMSG; @@ -1608,6 +1661,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { h = groups; break; + default: return -EBADMSG; } @@ -1812,6 +1866,15 @@ int main(int argc, char *argv[]) { } } + if (!uid_range) { + /* Default to default range of 1..SYSTEMD_UID_MAX */ + r = uid_range_add(&uid_range, &n_uid_range, 1, SYSTEM_UID_MAX); + if (r < 0) { + log_oom(); + goto finish; + } + } + r = add_implicit(); if (r < 0) goto finish; diff --git a/src/test/test-uid-range.c b/src/test/test-uid-range.c new file mode 100644 index 0000000000..5f3d871b6c --- /dev/null +++ b/src/test/test-uid-range.c @@ -0,0 +1,91 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2014 Lennart Poettering + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see <http://www.gnu.org/licenses/>. +***/ + +#include <stddef.h> + +#include "util.h" +#include "uid-range.h" + +int main(int argc, char *argv[]) { + _cleanup_free_ UidRange *p = NULL; + unsigned n = 0; + uid_t search; + + assert_se(uid_range_add_str(&p, &n, "500-999") >= 0); + assert_se(n == 1); + assert_se(p[0].start == 500); + assert_se(p[0].nr = 500); + + assert_se(!uid_range_contains(p, n, 499)); + assert_se(uid_range_contains(p, n, 500)); + assert_se(uid_range_contains(p, n, 999)); + assert_se(!uid_range_contains(p, n, 1000)); + + search = (uid_t) -1; + assert_se(uid_range_next_lower(p, n, &search)); + assert_se(search == 999); + assert_se(uid_range_next_lower(p, n, &search)); + assert_se(search == 998); + search = 501; + assert_se(uid_range_next_lower(p, n, &search)); + assert_se(search == 500); + assert_se(uid_range_next_lower(p, n, &search) == -EBUSY); + + assert_se(uid_range_add_str(&p, &n, "1000") >= 0); + assert_se(n == 1); + assert_se(p[0].start == 500); + assert_se(p[0].nr = 501); + + assert_se(uid_range_add_str(&p, &n, "30-40") >= 0); + assert_se(n == 2); + assert_se(p[0].start == 30); + assert_se(p[0].nr = 11); + assert_se(p[1].start == 500); + assert_se(p[1].nr = 501); + + assert_se(uid_range_add_str(&p, &n, "60-70") >= 0); + assert_se(n == 3); + assert_se(p[0].start == 30); + assert_se(p[0].nr = 11); + assert_se(p[1].start == 60); + assert_se(p[1].nr = 11); + assert_se(p[2].start == 500); + assert_se(p[2].nr = 501); + + assert_se(uid_range_add_str(&p, &n, "20-2000") >= 0); + assert_se(n == 1); + assert_se(p[0].start == 20); + assert_se(p[0].nr = 1981); + + assert_se(uid_range_add_str(&p, &n, "2002") >= 0); + assert_se(n == 2); + assert_se(p[0].start == 20); + assert_se(p[0].nr = 1981); + assert_se(p[1].start == 2002); + assert_se(p[1].nr = 1); + + assert_se(uid_range_add_str(&p, &n, "2001") >= 0); + assert_se(n == 1); + assert_se(p[0].start == 20); + assert_se(p[0].nr = 1983); + + return 0; +} |