We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Collections, Text and First Iterators step 6 of 21
Zero-copy parsing: borrow, don't allocate
Parse an INI-style configuration into (section, key, value) triples. All
three fields must be borrowed slices of the input text, not newly allocated
Strings.
pub fn parse_ini(src: &str) -> Vec<(&str, &str, &str)>
For example, this input:
name = demo
[server]
host = example.com
; a comment
url = http://x?a=b
returns [("", "name", "demo"), ("server", "host", "example.com"), ("server", "url", "http://x?a=b")].
The rules
Process the input one line at a time, in order:
- Split
srcinto lines withstr::lines, which accepts both\nand\r\nline endings. Trim leading and trailing whitespace from each line withstr::trimbefore applying the rules below. - Skip a trimmed line that is empty or starts with
#or;. - A trimmed line that starts with
[and ends with]is a section header. The section name is the text between the brackets, trimmed, so[ s ]names section"s"and[]names section"". The header applies to every following pair until the next header. - Otherwise, if the line contains
=, split it at the first=. The key is the trimmed text before it and the value is the trimmed text after it. Either may be empty.url = http://x?a=bgives keyurland valuehttp://x?a=b. - Any other line, such as
junkor an unclosed[name, is ignored.
Pairs that appear before any header get the section "". Output follows
input order. Repeated keys and repeated section names are all kept; nothing
is merged or deduplicated. A # or ; later in a line does not start a
comment, so k = v # note has the value v # note.
The allocation budget
One hidden case generates a 10 000-line configuration containing 8 900
pairs. The harness counts calls to the global allocator's alloc and
realloc while parse_ini runs on that input, and the case fails if there
are more than 20.
The budget is sized for the output Vec to grow, plus a little slack; it
has no room for per-line or per-pair allocations. With the toolchain used to check this problem, an empty Vec that
grows by push reached 8 900 entries after 13 allocator calls. The standard
library does not promise a particular growth strategy, which is why the
budget has slack. Calling Vec::with_capacity with the number of lines is
also fine. Allocating one String per pair, even only for the section name,
makes thousands of calls and fails.
Why the return type can borrow
A &str is a pointer to UTF-8 bytes plus a length (16 bytes on 64-bit
targets). It does not own the bytes. The compiler checks that no such
reference outlives the data it points into.
The signature does not name a lifetime, so the lifetime elision rules supply one. When a function has exactly one reference parameter, every elided lifetime in the return type is tied to it. Written out in full:
pub fn parse_ini<'a>(src: &'a str) -> Vec<(&'a str, &'a str, &'a str)>
So every returned slice must come from src. Without any reference
parameter, as in fn parse_ini() -> Vec<(&str, &str, &str)>, there is
nothing for the output to borrow from, and the compiler reports
E0106, missing lifetime specifiers.
The errors in the starter
The starter already applies the rules above correctly. Its problem is that it
keeps the current section in an owned String, and it does not compile:
error[E0506]: cannot assign to `section` because it is borrowed
error[E0515]: cannot return value referencing local variable `section`
E0515 explains the underlying problem. section is a local variable, and its
heap buffer is freed when the function returns. Pushing section.as_str()
into out stores a reference to that buffer in the return value, so the
caller would receive references to freed memory. The compiler rejects the
function instead.
E0506 comes from the same borrow. Once out holds a reference into
section, assigning a new String to section would drop the buffer that
reference points at, so the assignment is rejected too.
The repair is to stop owning the section name. src.lines() yields &str
slices borrowed from src, and trimming or stripping brackets from one of
them yields a smaller slice of src. Store that slice. That takes three
small edits: start with let mut section = "";, assign the trimmed slice
without .to_string(), and push section without .as_str(). Changing only
the initializer is not enough: the .to_string() assignment then fails with
E0308 (mismatched types), and as_str on a &str is not available on
stable Rust (E0658).
Check your understanding
If the tuple held an owned section, out.push((section.clone(), k.trim(), v.trim())), would that compile?
Show answer
Hide answer
Only after changing the return type to Vec<(String, &str, &str)>. With
that change the function compiles and returns correct values, because each
tuple owns a copy of the section name and no longer borrows the local
String. It would not pass this problem: the harness calls the required
signature, and one clone per pair is far over the allocation budget.
The borrow checker rejects the version that would return dangling references. It does not reject a version that copies more than necessary.
Owned strings are the right choice when the parsed result must outlive the
input text, for example when src will be dropped while the configuration
is kept. Borrowed slices are the right choice when the input stays alive at
least as long as the result, as it does here.
Useful str methods
split_once(pat)returnsOption<(&str, &str)>: the text before the first match and the text after it, orNoneif there is no match. Clippy'smanual_split_oncelint flags somesplitn(2, ...)patterns that this method expresses directly.strip_prefix/strip_suffixreturnOption<&str>with the prefix or suffix removed, orNoneif it is absent. Chaining them recognizes[section]without manual index arithmetic.trimreturns a sub-slice of the same string with leading and trailing whitespace removed. It does not allocate.
Grading compiles your function with the harness, runs the test cases, and runs Clippy with warnings treated as errors.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.