When calling `agxbmore` on a 0-sized buffer, the new size to expand to would be
chosen based on the length of the current string being appended. This string
often originated from user’s input graph and hence its length was often
user-defined. There is nothing strictly wrong with this, but it is a little
unpredictable and sometimes leads to pathological outcomes, e.g:
agxbputc(&xb, 'a'); // expands 0-sized buffer to 1
agxbputc(&xb, 'b'); // doubles 1-sized buffer to 2
agxbputc(&xb, 'c'); // doubles 2-sized buffer to 4
agxbputc(&xb, 'd');
agxbputc(&xb, 'e'); // doubles 4-sized buffer to 8
…
This is functionally correct, but that is a lot of `realloc` calls just to
accrue 5 bytes.¹ It is more convenient to enlarge our initial allocation to
something matching the default hint when an agxbuf is initialized.
The situation described above has not typically been a concern in the past
because a 0-sized agxbuf is hard to obtain to begin with. You need to either
call `agxbinit` while supplying your own backing storage that also has 0 size
(why would you do this?) or reuse an agxbuf after `agxbdisown` (technically
legal, but no good reason to do this). Neither of these scenarios occur anywhere
in the Graphviz tree to my knowledge. However an upcoming change will make
0-sized agxbufs more common.
¹ Some of these `realloc`s are likely no-ops, as the system allocator generally
does not give you exactly 1 byte when you ask for 1. It will probably give you
more and then when you ask to double to 2, it will tell you your existing
pointer is just fine to keep on using. But the agxbuf code does not know any
of these details and will still repeatedly call into `realloc`.
char *nbuf; // new buffer
size = (size_t)(xb->eptr - xb->buf);
- nsize = 2 * size;
+ nsize = size == 0 ? BUFSIZ : (2 * size);
if (size + ssz > nsize)
nsize = size + ssz;
cnt = (size_t)(xb->ptr - xb->buf);