-
Notifications
You must be signed in to change notification settings - Fork 202
[RFC] Create a 'safe' strbuf API #2230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: ds/trace2-tolerate-failed-timestamp
Are you sure you want to change the base?
Changes from all commits
b177970
8d30730
3b3c672
ebd91b9
6e654dc
dea925f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,17 +2,25 @@ | |
|
|
||
| #include "git-compat-util.h" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Junio C Hamano wrote on the Git mailing list (how to reply to this email): "Derrick Stolee via GitGitGadget" <gitgitgadget@gmail.com> writes:
> +int jw_release(struct json_writer *jw)
> {
> - strbuf_release(&jw->json);
> - strbuf_release(&jw->open_stack);
> + enum safe_result result = SUCCESS;
> +
> + /* attempt both removals without short-circuiting. */
> + result = sstrbuf_release(&jw->json) || result;
> + result = sstrbuf_release(&jw->open_stack) || result;
> +
> + return result;
> }
This is puzzling in a few ways.
"enum safe_result" so far has been SUCCESS==0 and MEMORY_ERROR==1.
Presumably in some future we would gain other kind of error symbols,
but when that happens is this meant to act as an enumeration of
different kinds errors? Or an enumeration of bitmasks that can
signal different kinds of errors?
If we mean "enum safe_result" is an enumeration of different kinds
of errors, then the "result" variable and the returned value from
here would be able to report a *single* kind of error, and it may
be common to report the first error we encounter, in which case
enum safe_result result = SUCCESS;
enum safe_result res;
res = sstrbuf_release(&jw->json);
if (!result && res)
result = res;
res = sstrbuf_release(&jw->open_stack);
if (!result && res)
result = res;
return result;
would be slightly longer, far easier to reason about, and is a lot
more futureproof. What you wrote, with "||", does not really allow
anything other than "is it still zero, or coalesce any non-zero
value to 1".
On the other hand, if we mean "enum safe_result" is an enumeration
of bitmasks, each bit representing different kind of error, then
enum safe_result result = 0;
result |= sstrbuf_release(&jw->json);
result |= sstrbuf_release(&jw->open_stack);
return result;
would probably be what you want. That way you can add different
functions that returns different bit to signal a different kind of
error and or it in.
result |= some_function();There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Junio C Hamano wrote on the Git mailing list (how to reply to this email): Junio C Hamano <gitster@pobox.com> writes:
> "Derrick Stolee via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
>> +int jw_release(struct json_writer *jw)
>> {
>> - strbuf_release(&jw->json);
>> - strbuf_release(&jw->open_stack);
>> + enum safe_result result = SUCCESS;
>> +
>> + /* attempt both removals without short-circuiting. */
>> + result = sstrbuf_release(&jw->json) || result;
>> + result = sstrbuf_release(&jw->open_stack) || result;
>> +
>> + return result;
>> }
>
> This is puzzling in a few ways.
> If we mean "enum safe_result" is an enumeration of different kinds
> of errors, then the "result" variable and the returned value from
> ...
> On the other hand, if we mean "enum safe_result" is an enumeration
> of bitmasks, each bit representing different kind of error, then
> ...
I forgot the third possibility. Regardless of which interpretation
of "enum safe_result" we use, if jw_release() is designed to say "0
for success, non-zero for failure", then almost as written but
declaring "result" as a plain "int"
int result = 0;
result = sstrbuf_release(&jw->json) || result;
result = sstrbuf_release(&jw->open_stack) || result;
return result;
would probably make sense, even though the "|| result" construct is
a bit unusual in C.
Thanks.
|
||
| #include "json-writer.h" | ||
| #include "strbuf.h" | ||
| /* banned-die must be last. */ | ||
| #include "banned-die.h" | ||
|
|
||
| void jw_init(struct json_writer *jw) | ||
| { | ||
| struct json_writer blank = JSON_WRITER_INIT; | ||
| memcpy(jw, &blank, sizeof(*jw));; | ||
| } | ||
|
|
||
| void jw_release(struct json_writer *jw) | ||
| int jw_release(struct json_writer *jw) | ||
| { | ||
| strbuf_release(&jw->json); | ||
| strbuf_release(&jw->open_stack); | ||
| enum safe_result result = SUCCESS; | ||
|
|
||
| /* attempt both removals without short-circuiting. */ | ||
| result = sstrbuf_release(&jw->json) || result; | ||
| result = sstrbuf_release(&jw->open_stack) || result; | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| /* | ||
|
|
@@ -98,16 +106,17 @@ static void maybe_add_comma(struct json_writer *jw) | |
| jw->need_comma = 1; | ||
| } | ||
|
|
||
| static void fmt_double(struct json_writer *jw, int precision, | ||
| double value) | ||
| static int fmt_double(struct json_writer *jw, int precision, | ||
| double value) | ||
| { | ||
| if (precision < 0) { | ||
| strbuf_addf(&jw->json, "%f", value); | ||
| return 0; | ||
| } else { | ||
| struct strbuf fmt = STRBUF_INIT; | ||
| strbuf_addf(&fmt, "%%.%df", precision); | ||
| strbuf_addf(&jw->json, fmt.buf, value); | ||
| strbuf_release(&fmt); | ||
| return sstrbuf_release(&fmt); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -234,8 +243,8 @@ static void kill_indent(struct strbuf *sb, | |
| } | ||
| } | ||
|
|
||
| static void append_sub_jw(struct json_writer *jw, | ||
| const struct json_writer *value) | ||
| static int append_sub_jw(struct json_writer *jw, | ||
| const struct json_writer *value) | ||
| { | ||
| /* | ||
| * If both are pretty, increase the indentation of the sub_jw | ||
|
|
@@ -254,18 +263,17 @@ static void append_sub_jw(struct json_writer *jw, | |
| struct strbuf sb = STRBUF_INIT; | ||
| increase_indent(&sb, value, jw->open_stack.len * 2); | ||
| strbuf_addbuf(&jw->json, &sb); | ||
| strbuf_release(&sb); | ||
| return; | ||
| return sstrbuf_release(&sb); | ||
| } | ||
| if (!jw->pretty && value->pretty) { | ||
| struct strbuf sb = STRBUF_INIT; | ||
| kill_indent(&sb, value); | ||
| strbuf_addbuf(&jw->json, &sb); | ||
| strbuf_release(&sb); | ||
| return; | ||
| return sstrbuf_release(&sb); | ||
| } | ||
|
|
||
| strbuf_addbuf(&jw->json, &value->json); | ||
| return 0; | ||
| } | ||
|
|
||
| void jw_object_sub_jw(struct json_writer *jw, const char *key, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| #include "git-compat-util.h" | ||
| #include "strbuf-safe.h" | ||
| #include "banned-die.h" | ||
|
|
||
| /* | ||
| * A safe version of ALLOC_GROW from git-compat-util.h and | ||
| * xrealloc() from wrapper.c. | ||
| */ | ||
| #define SAFE_ALLOC_GROW(x, nr, alloc) \ | ||
| do { \ | ||
| if ((nr) > alloc) { \ | ||
| if (alloc_nr(alloc) < (nr)) \ | ||
| alloc = (nr); \ | ||
| else \ | ||
| alloc = alloc_nr(alloc); \ | ||
| if (srealloc((void **)&(x), alloc)) \ | ||
| return MEMORY_ERROR; \ | ||
| } \ | ||
| } while (0) | ||
|
|
||
| enum safe_result sstrbuf_grow(struct strbuf *sb, size_t extra) | ||
| { | ||
| int new_buf = !sb->alloc; | ||
| size_t new_len = st_add3(sb->len, extra, 1); | ||
| if (new_buf) | ||
| sb->buf = NULL; | ||
|
|
||
| SAFE_ALLOC_GROW(sb->buf, new_len, sb->alloc); | ||
|
|
||
| if (new_buf) | ||
| sb->buf[0] = '\0'; | ||
|
|
||
| return SUCCESS; | ||
| } | ||
|
|
||
| enum safe_result sstrbuf_init(struct strbuf *sb, size_t hint) | ||
| { | ||
| struct strbuf blank = STRBUF_INIT; | ||
| memcpy(sb, &blank, sizeof(*sb)); | ||
| if (!hint) | ||
| return 0; | ||
| return sstrbuf_grow(sb, hint); | ||
| } | ||
|
|
||
| enum safe_result sstrbuf_release(struct strbuf *sb) | ||
| { | ||
| if (sb->alloc) { | ||
| free(sb->buf); | ||
| return sstrbuf_init(sb, 0); | ||
| } | ||
| return 0; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| #ifndef STRBUF_SAFE_H | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Junio C Hamano wrote on the Git mailing list (how to reply to this email): "Derrick Stolee via GitGitGadget" <gitgitgadget@gmail.com> writes:
> +/*
> + * NOTE FOR STRBUF DEVELOPERS
> + *
> + * strbuf is a low-level primitive; as such it should interact only
> + * with other low-level primitives. Do not introduce new functions
> + * which interact with higher-level APIs.
> + *
> + * This header file specifically conatins the "safe" API surface for
> + * working with strbufs. The implementations of these methods avoid
> + * using die() and other exits. Thus, these methods are appropriate
> + * for use within lower-level APIs such as trace2.
> + */
I have to wonder if this is somewhat backwards, in that the longer
term goal for us should be to make most of the service routines like
strbuf, string_list, csum_file, etc., free of die() and be "safe".
A recent trend under the label "libification" is to make the use of
the_repository more explicit and pass a "struct repository *" as a
parameter instead more widely throughout the code flow, but it would
be equally if not more useful change to expand the "safe" API surface
so that callers of more service routines take responsibility to act
on errors.
And picking strbuf as the first instance of such generic service
library certainly is a good idea. Its interface is well defined.
We may want to rename functions that _happen_ to use a strbuf to
return their results but otherwise has nothing to do with strbuf
away from strbuf_ prefix (strbuf_realpath() etc. in abspath.h are
prime examples) as part of this first step, though.There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Mark C. Chu-Carroll" wrote on the Git mailing list (how to reply to this email): General comment: I really like the idea of this. While I haven't
encountered this specific issue with git, I've dealt with similar issues
in other systems, and even if the cascading error case is rare, it's
incredibly frustrating to deal with the loss of error details because
they used unsafe operations to generate their messages!
I'm not really qualified to comment much on the code yet, but there's a
couple of small writing style things that I'll nitpick for
clarity/readibilty. Feel free to ignore these if you disagree.
On Fri Sep 18, 2026 at 9:02 AM EDT, Derrick Stolee via GitGitGadget wrote:
> From: Derrick Stolee <stolee@gmail.com>
>
> The strbuf library is an important API used all over the Git codebase.
> Contributors use it in nearly any string-manipulating action. However, the
> implementation uses other helping functions that die() on failure instead of
> returning an error code. Thus, the strbuf API isn't _safe_.
>
> In particular, we cannot include 'banned-die.h' in 'strbuf.c'.
I think we prefer to avoid "we" in these comments; and
the "in particular" here feels a little abrupt - maybe "In order to
ensure that strbuf functions can't call die, strbuf.c should not include ..."
> To start the creation of a safe strbuf API, move the struct definition into
> a new 'strbuf-safe.h' header file. All consumers of 'strbuf.h' will consume
> that header transitively.
>
> In the future, we will hope to have consumers that need a 'safe' API will
> include 'strbuf-safe.h' instead of 'strbuf.h'.
Again, avoiding we; and I don't think the quotes belong there.
Maybe "In the future, consumers that need a safe API will include ..."
> We will see in future changes the inclusion of new implementations that
> return an error code instead of halting.
The structure of this sentence is confusing. I had to read it a couple
of times to figure out how to parse it. Better something like:
"Future changes will include new implementations that return an error
code instead of halting."
>
> Signed-off-by: Derrick Stolee <stolee@gmail.com>
> ---
> strbuf-safe.h | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++
> strbuf.h | 74 +++----------------------------------------
> 2 files changed, 92 insertions(+), 70 deletions(-)
> create mode 100644 strbuf-safe.h
>
> diff --git a/strbuf-safe.h b/strbuf-safe.h
> new file mode 100644
> index 0000000000..3cf14545bb
> --- /dev/null
> +++ b/strbuf-safe.h
> @@ -0,0 +1,88 @@
> +#ifndef STRBUF_SAFE_H
> +#define STRBUF_SAFE_H
> +
> +/*
> + * NOTE FOR STRBUF DEVELOPERS
> + *
> + * strbuf is a low-level primitive; as such it should interact only
> + * with other low-level primitives. Do not introduce new functions
> + * which interact with higher-level APIs.
> + *
> + * This header file specifically conatins the "safe" API surface for
> + * working with strbufs. The implementations of these methods avoid
> + * using die() and other exits. Thus, these methods are appropriate
> + * for use within lower-level APIs such as trace2.
> + */
As with the prose comments above, safe shouldn't be in quotes.
> +
> +struct string_list;
> +
> +/**
> + * strbufs are meant to be used with all the usual C string and memory
> + * APIs. Given that the length of the buffer is known, it's often better to
> + * use the mem* functions than a str* one (e.g., memchr vs. strchr).
> + * Though, one has to be careful about the fact that str* functions often
> + * stop on NULs and that strbufs may have embedded NULs.
> + *
> + * A strbuf is NUL terminated for convenience, but no function in the
> + * strbuf API actually relies on the string being free of NULs.
> + *
> + * strbufs have some invariants that are very important to keep in mind:
I think this should be stronger - they shouldn't just be kept in mind, they
should be strictly enforced: "strbufs have some invariants that _must_
be maintained".
> + *
> + * - The `buf` member is never NULL, so it can be used in any usual C
> + * string operations safely. strbufs _have_ to be initialized either by
> + * `strbuf_init()` or by `= STRBUF_INIT` before the invariants, though.
I think "must" is better that "have to" here; I know some non-native
english speakers get confused by that.
> + *
> + * Do *not* assume anything on what `buf` really is (e.g. if it is
> + * allocated memory or not), use `strbuf_detach()` to unwrap a memory
> + * buffer from its strbuf shell in a safe way. That is the sole supported
> + * way. This will give you a malloced buffer that you can later `free()`.
> + *
> + * However, it is totally safe to modify anything in the string pointed by
> + * the `buf` member, between the indices `0` and `len-1` (inclusive).
> + *
> + * - The `buf` member is a byte array that has at least `len + 1` bytes
> + * allocated. The extra byte is used to store a `'\0'`, allowing the
> + * `buf` member to be a valid C-string. All strbuf functions ensure this
> + * invariant is preserved.
I think there should be a "must" before ensure".
> + *
> + * NOTE: It is OK to "play" with the buffer directly if you work it this
> + * way:
I don't think "play" is good, and in any case it shouldn't be in quotes.
Maybe "It is OK to manipulate the buffer directly..."
> + *
> + * strbuf_grow(sb, SOME_SIZE); <1>
> + * strbuf_setlen(sb, sb->len + SOME_OTHER_SIZE);
> + *
> + * <1> Here, the memory array starting at `sb->buf`, and of length
> + * `strbuf_avail(sb)` is all yours, and you can be sure that
> + * `strbuf_avail(sb)` is at least `SOME_SIZE`.
> + *
> + * NOTE: `SOME_OTHER_SIZE` must be smaller or equal to `strbuf_avail(sb)`.
> + *
> + * Doing so is safe, though if it has to be done in many places, adding the
> + * missing API to the strbuf module is the way to go.
> + *
> + * WARNING: Do _not_ assume that the area that is yours is of size `alloc
> + * - 1` even if it's true in the current implementation. Alloc is somehow a
> + * "private" member that should not be messed with. Use `strbuf_avail()`
> + * instead.
Again, the quote around private. (I had an undergrad advisor who was a
stickler about the right way to use quotes, and he pounded into me so that
now I die a little bit every time I see them used as emphasis or as a marker
of "not really".)
> +*/
> +
> +/**
> + * Data Structures
> + * ---------------
> + */
> +
> +/**
> + * This is the string buffer structure. The `len` member can be used to
> + * determine the current length of the string, and `buf` member provides
> + * access to the string itself.
> + */
> +struct strbuf {
> + size_t alloc;
> + size_t len;
> + char *buf;
> +};
> +
> +extern char strbuf_slopbuf[];
> +#define STRBUF_INIT { .buf = strbuf_slopbuf }
> +
> +#endif /* STRBUF_SAFE_H */
Repeat comments above for the repetitions in the other file.
--
Mark Craig Chu-Carroll (@MarkChuCarroll at gitlab)
*** Software Tools/Math Geek - Software Engineer at Gitlab
*** Work Email: mcarroll@gitlab.com / markchucarroll@fastmail.com
*** Personal Blog: http://goodmath.org/blog / Personal email: markcc@gmail.com
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Junio C Hamano wrote on the Git mailing list (how to reply to this email): "Mark C. Chu-Carroll" <markchucarroll@fastmail.com> writes:
> General comment: I really like the idea of this. While I haven't
> encountered this specific issue with git, I've dealt with similar issues
> in other systems, and even if the cascading error case is rare, it's
> incredibly frustrating to deal with the loss of error details because
> they used unsafe operations to generate their messages!
If I understand correctly what this topic aims at, you'll see the
"loss of error details" either way. Either we ran out of memory
inside strbuf call and die, or we fail to allocate memory to format
the details and end up not showing it.
> On Fri Sep 18, 2026 at 9:02 AM EDT, Derrick Stolee via GitGitGadget wrote:
>> From: Derrick Stolee <stolee@gmail.com>
>>
>> In particular, we cannot include 'banned-die.h' in 'strbuf.c'.
>
> I think we prefer to avoid "we" in these comments; and
The third word of your comment should not be "we" but "I", if that
"we" intends to include me and others who wrote many commit log
messages ;-) |
||
| #define STRBUF_SAFE_H | ||
|
|
||
| /* | ||
| * NOTE FOR STRBUF DEVELOPERS | ||
| * | ||
| * strbuf is a low-level primitive; as such it should interact only | ||
| * with other low-level primitives. Do not introduce new functions | ||
| * which interact with higher-level APIs. | ||
| * | ||
| * This header file specifically conatins the "safe" API surface for | ||
| * working with strbufs. The implementations of these methods avoid | ||
| * using die() and other exits. Thus, these methods are appropriate | ||
| * for use within lower-level APIs such as trace2. | ||
| */ | ||
|
|
||
| struct string_list; | ||
|
|
||
| /** | ||
| * strbufs are meant to be used with all the usual C string and memory | ||
| * APIs. Given that the length of the buffer is known, it's often better to | ||
| * use the mem* functions than a str* one (e.g., memchr vs. strchr). | ||
| * Though, one has to be careful about the fact that str* functions often | ||
| * stop on NULs and that strbufs may have embedded NULs. | ||
| * | ||
| * A strbuf is NUL terminated for convenience, but no function in the | ||
| * strbuf API actually relies on the string being free of NULs. | ||
| * | ||
| * strbufs have some invariants that are very important to keep in mind: | ||
| * | ||
| * - The `buf` member is never NULL, so it can be used in any usual C | ||
| * string operations safely. strbufs _have_ to be initialized either by | ||
| * `strbuf_init()` or by `= STRBUF_INIT` before the invariants, though. | ||
| * | ||
| * Do *not* assume anything on what `buf` really is (e.g. if it is | ||
| * allocated memory or not), use `strbuf_detach()` to unwrap a memory | ||
| * buffer from its strbuf shell in a safe way. That is the sole supported | ||
| * way. This will give you a malloced buffer that you can later `free()`. | ||
| * | ||
| * However, it is totally safe to modify anything in the string pointed by | ||
| * the `buf` member, between the indices `0` and `len-1` (inclusive). | ||
| * | ||
| * - The `buf` member is a byte array that has at least `len + 1` bytes | ||
| * allocated. The extra byte is used to store a `'\0'`, allowing the | ||
| * `buf` member to be a valid C-string. All strbuf functions ensure this | ||
| * invariant is preserved. | ||
| * | ||
| * NOTE: It is OK to "play" with the buffer directly if you work it this | ||
| * way: | ||
| * | ||
| * strbuf_grow(sb, SOME_SIZE); <1> | ||
| * strbuf_setlen(sb, sb->len + SOME_OTHER_SIZE); | ||
| * | ||
| * <1> Here, the memory array starting at `sb->buf`, and of length | ||
| * `strbuf_avail(sb)` is all yours, and you can be sure that | ||
| * `strbuf_avail(sb)` is at least `SOME_SIZE`. | ||
| * | ||
| * NOTE: `SOME_OTHER_SIZE` must be smaller or equal to `strbuf_avail(sb)`. | ||
| * | ||
| * Doing so is safe, though if it has to be done in many places, adding the | ||
| * missing API to the strbuf module is the way to go. | ||
| * | ||
| * WARNING: Do _not_ assume that the area that is yours is of size `alloc | ||
| * - 1` even if it's true in the current implementation. Alloc is somehow a | ||
| * "private" member that should not be messed with. Use `strbuf_avail()` | ||
| * instead. | ||
| */ | ||
|
|
||
| /** | ||
| * Data Structures | ||
| * --------------- | ||
| */ | ||
|
|
||
| /** | ||
| * This is the string buffer structure. The `len` member can be used to | ||
| * determine the current length of the string, and `buf` member provides | ||
| * access to the string itself. | ||
| */ | ||
| struct strbuf { | ||
| size_t alloc; | ||
| size_t len; | ||
| char *buf; | ||
| }; | ||
|
|
||
| extern char strbuf_slopbuf[]; | ||
| #define STRBUF_INIT { .buf = strbuf_slopbuf } | ||
|
|
||
| enum safe_result { | ||
| SUCCESS = 0, | ||
| MEMORY_ERROR, | ||
| }; | ||
|
|
||
| enum safe_result sstrbuf_grow(struct strbuf *sb, size_t extra); | ||
| enum safe_result sstrbuf_init(struct strbuf *sb, size_t hint); | ||
| enum safe_result sstrbuf_release(struct strbuf *sb); | ||
|
|
||
| #endif /* STRBUF_SAFE_H */ | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Junio C Hamano wrote on the Git mailing list (how to reply to this email):