+ds_extend (struct string *st, size_t min_capacity)
+{
+ if (min_capacity > st->capacity)
+ {
+ st->capacity *= 2;
+ if (st->capacity < min_capacity)
+ st->capacity = 2 * min_capacity;
+
+ st->ss.string = xrealloc (st->ss.string, st->capacity + 1);
+ }
+}
+
+/* Shrink ST to the minimum capacity need to contain its content. */
+void
+ds_shrink (struct string *st)
+{
+ if (st->capacity != st->ss.length)
+ {
+ st->capacity = st->ss.length;
+ st->ss.string = xrealloc (st->ss.string, st->capacity + 1);
+ }
+}
+
+/* Truncates ST to at most LENGTH bytes long. */
+void
+ds_truncate (struct string *st, size_t length)
+{
+ ss_truncate (&st->ss, length);
+}
+
+/* Removes trailing bytes in TRIM_SET from ST.
+ Returns number of bytes removed. */
+size_t
+ds_rtrim (struct string *st, struct substring trim_set)
+{
+ return ss_rtrim (&st->ss, trim_set);
+}
+
+/* Removes leading bytes in TRIM_SET from ST.
+ Returns number of bytes removed. */
+size_t
+ds_ltrim (struct string *st, struct substring trim_set)
+{
+ size_t cnt = ds_span (st, trim_set);
+ if (cnt > 0)
+ ds_assign_substring (st, ds_substr (st, cnt, SIZE_MAX));
+ return cnt;
+}
+
+/* Trims leading and trailing bytes in TRIM_SET from ST.
+ Returns number of bytes removed. */
+size_t
+ds_trim (struct string *st, struct substring trim_set)
+{
+ size_t cnt = ds_rtrim (st, trim_set);
+ return cnt + ds_ltrim (st, trim_set);
+}
+
+/* If the last byte in ST is C, removes it and returns true.
+ Otherwise, returns false without modifying ST. */
+bool
+ds_chomp_byte (struct string *st, char c)
+{
+ return ss_chomp_byte (&st->ss, c);
+}
+
+/* If ST ends with SUFFIX, removes it and returns true.
+ Otherwise, returns false without modifying ST. */
+bool
+ds_chomp (struct string *st, struct substring suffix)
+{
+ return ss_chomp (&st->ss, suffix);
+}
+
+/* Divides ST into tokens separated by any of the DELIMITERS.
+ Each call replaces TOKEN by the next token in ST, or by an
+ empty string if no tokens remain. Returns true if a token was
+ obtained, false otherwise.
+
+ Before the first call, initialize *SAVE_IDX to 0. Do not
+ modify *SAVE_IDX between calls.
+
+ ST divides into exactly one more tokens than it contains
+ delimiters. That is, a delimiter at the start or end of ST or
+ a pair of adjacent delimiters yields an empty token, and the
+ empty string contains a single token. */
+bool
+ds_separate (const struct string *st, struct substring delimiters,
+ size_t *save_idx, struct substring *token)
+{
+ return ss_separate (ds_ss (st), delimiters, save_idx, token);
+}
+
+/* Divides ST into tokens separated by any of the DELIMITERS,
+ merging adjacent delimiters so that the empty string is never
+ produced as a token. Each call replaces TOKEN by the next
+ token in ST, or by an empty string if no tokens remain.
+ Returns true if a token was obtained, false otherwise.
+
+ Before the first call, initialize *SAVE_IDX to 0. Do not
+ modify *SAVE_IDX between calls. */
+bool
+ds_tokenize (const struct string *st, struct substring delimiters,
+ size_t *save_idx, struct substring *token)
+{
+ return ss_tokenize (ds_ss (st), delimiters, save_idx, token);
+}
+
+/* Pad ST on the right with copies of PAD until ST is at least
+ LENGTH bytes in size. If ST is initially LENGTH
+ bytes or longer, this is a no-op. */
+void
+ds_rpad (struct string *st, size_t length, char pad)
+{
+ if (length > st->ss.length)
+ ds_put_byte_multiple (st, pad, length - st->ss.length);
+}
+
+/* Sets the length of ST to exactly NEW_LENGTH,
+ either by truncating bytes from the end,
+ or by padding on the right with PAD. */
+void
+ds_set_length (struct string *st, size_t new_length, char pad)
+{
+ if (st->ss.length < new_length)
+ ds_rpad (st, new_length, pad);
+ else
+ st->ss.length = new_length;
+}
+
+/* Removes N bytes from ST starting at offset START. */
+void
+ds_remove (struct string *st, size_t start, size_t n)
+{
+ if (n > 0 && start < st->ss.length)
+ {
+ if (st->ss.length - start <= n)
+ {
+ /* All bytes at or beyond START are deleted. */
+ st->ss.length = start;
+ }
+ else
+ {
+ /* Some bytes remain and must be shifted into
+ position. */
+ memmove (st->ss.string + st->ss.length,
+ st->ss.string + st->ss.length + n,
+ st->ss.length - start - n);
+ st->ss.length -= n;
+ }
+ }
+ else
+ {
+ /* There are no bytes to delete or no bytes at or
+ beyond START, hence deletion is a no-op. */
+ }
+}
+
+/* Returns true if ST is empty, false otherwise. */
+bool
+ds_is_empty (const struct string *st)
+{
+ return ss_is_empty (st->ss);
+}
+
+/* Returns the length of ST. */
+size_t
+ds_length (const struct string *st)
+{
+ return ss_length (ds_ss (st));
+}
+
+/* Returns the string data inside ST. */
+char *
+ds_data (const struct string *st)
+{
+ return ss_data (ds_ss (st));
+}
+
+/* Returns a pointer to the null terminator ST.
+ This might not be an actual null byte unless ds_c_str() has
+ been called since the last modification to ST. */
+char *
+ds_end (const struct string *st)
+{
+ return ss_end (ds_ss (st));
+}
+
+/* Returns the byte in position IDX in ST, as a value in the
+ range of unsigned char. Returns EOF if IDX is out of the
+ range of indexes for ST. */
+int
+ds_at (const struct string *st, size_t idx)