Skip to content

← Collections, Text and First Iterators step 17 of 21

Medium Primitives

Cow: borrow until you have to own

Replace every ASCII control character with an underscore. When nothing needs changing, borrow the original string without allocating an output buffer.

pub fn sanitize(s: &str) -> Cow<'_, str>

Import Cow from std::borrow. Return Cow::Borrowed(s) exactly when the input has no ASCII control characters, including for empty input. Otherwise return Cow::Owned containing the changed text.

Which characters change?

Use char::is_ascii_control: replace U+0000 through U+001F and U+007F. This includes null, tab, newline, carriage return and DEL. Replace each one separately, so a carriage return followed by a newline becomes two underscores.

Preserve every other character in order. Non-ASCII controls such as U+0085 and U+009F stay unchanged, as do accented letters, emoji and ordinary spaces. Since each replaced ASCII character and its underscore occupy one byte, the output has the same UTF-8 byte length as the input.

Input Result Borrowed?
"clean text" "clean text" true
"a\tb" "a_b" false
"\r\n" "__" false
"" "" true
"héllo" "héllo" true

The harness checks the text and Cow variant. It does not count allocations: returning Borrowed is not proof that your implementation did no temporary allocation. Avoid allocating on the unchanged path as required by the task. The harness later converts the result to owned text for JSON output; that serialization work is separate from sanitize.

Why return Cow?

Cow<'a, str> can hold either Borrowed(&'a str) or Owned(String). It lets this function reuse the input or return newly built text through one return type.

Returning a String would require independently owned text even for unchanged nonempty input. Returning only a borrowed string slice would not let this function return a reference to a replacement String created locally: that buffer would be dropped when the function returned. Operations that only select an existing subslice, such as trimming, can often return &str instead.

Cow dereferences to str, so callers can read either variant using methods such as len and contains. They can request ownership only when needed.

Check before building

The starter always returns Owned, including for clean or empty input, so it fails the required variant checks even when its output text is correct.

First scan for an ASCII control character. If none exists, return Borrowed. If one exists, collect characters into a String, replacing controls with underscores, then return Owned. The initial search can stop at the first control; only the transformation needs to traverse all characters.

An alternative is to find the first control's byte index, copy the unchanged prefix into a new String, then transform the remaining suffix. The matching ASCII character gives a valid UTF-8 boundary for slicing. This avoids rechecking the prefix, but the prefix still needs to be copied into the output. Choose based on clarity and measured needs, not a universal speed rule.

str::replace returns String rather than Cow. Its return type alone does not establish an allocation count: an empty String, for example, can have no buffer. The requirement here is to preserve the borrowed path explicitly.

Requesting mutation or ownership

to_mut() returns a mutable String reference. For Borrowed text, it first creates an owned copy; for Owned text, it accesses the existing String. into_owned() consumes the Cow and returns a String, copying borrowed text or moving out the existing owned value.

This complete example shows the transition without changing the source:

use std::borrow::Cow;

fn main() {
    let source = String::from("hello");
    let mut text = Cow::Borrowed(source.as_str());
    assert!(matches!(text, Cow::Borrowed(_)));

    text.to_mut().push('!');
    assert!(matches!(text, Cow::Owned(_)));
    assert_eq!(source, "hello");
    assert_eq!(text.into_owned(), "hello!");
}

to_owned() on a Cow clones the Cow, which is different: a borrowed Cow stays borrowed, while an owned Cow clones its contents. Use into_owned when you need the String and no longer need the Cow.

The usual inner type for text is str. Cow<String> is legal, but its borrowed variant requires &String instead of accepting any string slice. Prefer Cow<str> for this API. If an API always returns independently owned text, returning String can be simpler; Cow's runtime layout and optimization costs are not a fixed number of branches at every use.

Submissions must compile, pass the text and variant checks, and pass Clippy with warnings treated as errors.