d33tah
d33tah

Reputation: 11618

Is it possible to perform another read in the middle of stdin.lines()?

Consider the following code:

use std::io::{self, BufRead, Read};

fn main() {
    let mut stdin = io::stdin();

    let mut content_length = 0;
    for line_wrapped in stdin.lock().lines() {
        let line = line_wrapped.unwrap();
        if line == "" {
            let mut buf = vec![0u8; content_length];
            stdin.read_exact(&mut buf).unwrap();
            print!("{:?}", buf);
        }
        if line.starts_with("Content-Length: ") {
            content_length = line
                .split("Content-Length: ")
                .nth(1)
                .unwrap()
                .parse()
                .unwrap();
        }
    }
}

And the compiler output:

error[E0502]: cannot borrow `stdin` as mutable because it is also borrowed as immutable
  --> src/main.rs:11:13
   |
7  |     for line_wrapped in stdin.lock().lines() {
   |                         ----- immutable borrow occurs here
...
11 |             stdin.read_exact(&mut buf).unwrap();
   |             ^^^^^ mutable borrow occurs here
...
22 |     }
   |     - immutable borrow ends here

Is there a way I could fix the error while keeping a similar structure of the program (read within a .lines())?

Upvotes: 2

Views: 129

Answers (1)

trent
trent

Reputation: 28075

Alternating between buffered and non-buffered reads of the same stream can be quite tricky. If you didn't have to lock standard input in order to call lines(), the internal buffer used to implement StdinLock could consume beyond the \n of the empty line, and the subsequent read_exact call would not start at the right place.

So you have to lock it only once, and you have to call read_exact on the same buffered reader that gave you the Lines, to be sure no bytes are lost. At first glance this looks impossible: lines() takes self by value, so once you've called it, you can't call read_exact on the same object. But there's a bit of a trick you can use.

The documentation for BufRead contains this blanket impl:

impl<'a, B: BufRead + ?Sized> BufRead for &'a mut B

&mut references to things that implement BufRead also implement BufRead. So you can take a temporary &mut reference of your StdinLock, call lines() on that, discard the Lines in time to read_exact the payload into buf, and then start over again with another &mut reference and another Lines.

This approach necessitates adding another loop, with a flag has_header to indicate whether to break the outer loop. It's not very pretty, but maybe you can work with it.

let stdin = io::stdin();
let mut stdin_buf = stdin.lock();

'record: loop {
    let mut content_length = 0;
    let mut has_header = false;
    'header: for line_wrapped in (&mut stdin_buf).lines() {
        let line = line_wrapped.unwrap();
        if line.starts_with("Content-Length: ") {
            content_length = line["Content-Length: ".len()..].parse().unwrap();
        }
        if line.is_empty() {
            has_header = true;
            break 'header;
        }
    }
    if has_header {
        let mut buf = vec![0u8; content_length];
        stdin_buf.read_exact(&mut buf).unwrap();
        println!("{:?}", buf);
    } else {
        break 'record;
    }
}

A final note: It's unclear what should happen when the Content-Length header is absent. If your original code worked, it would reuse the previously defined value (whatever the last content length was, or 0 for the first record). My version just uses 0 all the time. It's idiomatic in Rust to use Option to represent possibly-uninitialized values such as content_length, so initializing it to None could be a better option if the absence of a Content-Length header is an error that needs to be detected.

Upvotes: 2

Related Questions