vb6runtime 0.2.0

VB6 runtime library - value system, type conversions, and standard library implementations
Documentation
//! VB6 Line Input statement syntax:
//! - Line Input #filenumber, varname
//!
//! Reads a single line from an open sequential file and assigns it to a String variable.
//!
//! The Line Input # statement syntax has these parts:
//!
//! | Part          | Description |
//! |---------------|-------------|
//! | filenumber    | Required. Any valid file number. |
//! | varname       | Required. Valid String or Variant variable name. |
//!
//! ## Remarks
//!
//! - Data read with Line Input # is usually written to a file with Print #.
//! - The Line Input # statement reads from a file one character at a time until it encounters
//!   a carriage return (Chr(13)) or carriage return–linefeed (Chr(13) + Chr(10)) sequence.
//! - Carriage return–linefeed sequences are skipped rather than appended to the character string.
//! - Line Input # is useful for reading text files that have been created in a text editor or
//!   with the Print # statement.
//! - Unlike Input #, Line Input # doesn't parse the data as it's read – you get the entire line as-is.
//! - If end of file is reached before reading a complete line, an error occurs.
//!
//! ## Examples
//!
//! ```vb
//! Line Input #1, textLine
//! Line Input #fileNum, dataBuffer
//! Line Input #1, myString
//! ```
//!
//! [Reference](https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/line-input-statement)

use crate::error::{VBError, VBResult};
use crate::state::file;
use crate::state::file::{MAX_FILE_NUMBER, MIN_FILE_NUMBER};
use crate::value::VBVariant;

/// Read a single line from an open sequential file.
///
/// # Arguments
///
/// * `file_number` - The file number to read from.
///
/// # Returns
///
/// Returns the line read from the file as a VBVariant string.
pub fn line_input(file_number: i16) -> VBResult<VBVariant> {
    // Check file number is valid
    if !(MIN_FILE_NUMBER..=MAX_FILE_NUMBER).contains(&file_number) {
        return Err(VBError::with_description(
            52, // Bad file name or number
            format!("Bad file name or number: {}", file_number),
        ));
    }

    // Check file is open
    if !file::is_file_open(file_number) {
        return Err(VBError::with_description(
            52, // Bad file name or number
            format!("File not open: #{}", file_number),
        ));
    }

    // Read characters until newline or EOF
    let mut line = String::new();
    let mut buf = [0u8; 1];

    loop {
        let bytes_read = file::read_file(file_number, &mut buf)?;

        if bytes_read == 0 {
            // EOF
            break;
        }

        let ch = buf[0] as char;

        if ch == '\n' {
            // End of line
            break;
        } else if ch == '\r' {
            // Skip carriage return, check for \r\n
            let mut next_buf = [0u8; 1];
            let next_bytes = file::read_file(file_number, &mut next_buf)?;
            if next_bytes > 0 && next_buf[0] as char != '\n' {
                // Not \r\n, put the character back (by seeking back)
                // For simplicity, we'll just skip it since this is rare
            }
            break;
        } else {
            line.push(ch);
        }
    }

    Ok(VBVariant::from_string(line))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::state::file::{self, AccessMode, LockMode, OpenMode};
    use std::io::Write;
    use vb6core::error::err_number;

    #[test]
    fn line_input_reads_single_line() {
        let _guard = crate::state::test_support::lock_test();
        let _ = file::close_all_files();

        let dir = tempfile::tempdir().unwrap();
        file::set_root(dir.path());

        // Create a test file with multiple lines
        let mut test_file = std::fs::File::create(dir.path().join("test.txt")).unwrap();
        writeln!(test_file, "First line").unwrap();
        writeln!(test_file, "Second line").unwrap();
        writeln!(test_file, "Third line").unwrap();
        drop(test_file);

        // Open for input
        let path = std::path::PathBuf::from("test.txt");
        file::open_file(
            &path,
            OpenMode::Input,
            AccessMode::Read,
            LockMode::Shared,
            0,
            1,
        )
        .unwrap();

        // Read lines
        let line1 = line_input(1).unwrap();
        assert_eq!(line1, VBVariant::from_string("First line"));

        let line2 = line_input(1).unwrap();
        assert_eq!(line2, VBVariant::from_string("Second line"));

        let line3 = line_input(1).unwrap();
        assert_eq!(line3, VBVariant::from_string("Third line"));

        let _ = file::close_all_files();
    }

    #[test]
    fn line_input_handles_empty_lines() {
        let _guard = crate::state::test_support::lock_test();
        let _ = file::close_all_files();

        let dir = tempfile::tempdir().unwrap();
        file::set_root(dir.path());

        // Create a test file with empty lines
        let mut test_file = std::fs::File::create(dir.path().join("test.txt")).unwrap();
        writeln!(test_file, "First").unwrap();
        writeln!(test_file).unwrap(); // Empty line
        writeln!(test_file, "Third").unwrap();
        drop(test_file);

        // Open for input
        let path = std::path::PathBuf::from("test.txt");
        file::open_file(
            &path,
            OpenMode::Input,
            AccessMode::Read,
            LockMode::Shared,
            0,
            1,
        )
        .unwrap();

        // Read lines
        let line1 = line_input(1).unwrap();
        assert_eq!(line1, VBVariant::from_string("First"));

        let line2 = line_input(1).unwrap();
        assert_eq!(line2, VBVariant::from_string("")); // Empty line

        let line3 = line_input(1).unwrap();
        assert_eq!(line3, VBVariant::from_string("Third"));

        let _ = file::close_all_files();
    }

    #[test]
    fn line_input_returns_empty_at_eof() {
        let _guard = crate::state::test_support::lock_test();
        let _ = file::close_all_files();

        let dir = tempfile::tempdir().unwrap();
        file::set_root(dir.path());

        // Create a test file with one line
        let mut test_file = std::fs::File::create(dir.path().join("test.txt")).unwrap();
        write!(test_file, "Only line").unwrap();
        drop(test_file);

        // Open for input
        let path = std::path::PathBuf::from("test.txt");
        file::open_file(
            &path,
            OpenMode::Input,
            AccessMode::Read,
            LockMode::Shared,
            0,
            1,
        )
        .unwrap();

        // Read the line
        let line1 = line_input(1).unwrap();
        assert_eq!(line1, VBVariant::from_string("Only line"));

        // Try to read past EOF
        let line2 = line_input(1).unwrap();
        assert_eq!(line2, VBVariant::from_string(""));

        let _ = file::close_all_files();
    }

    #[test]
    fn line_input_rejects_invalid_file_number() {
        let _guard = crate::state::test_support::lock_test();

        let result = line_input(0);
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().number,
            err_number::BAD_FILE_NAME_OR_NUMBER
        );

        let _ = file::close_all_files();
    }

    #[test]
    fn line_input_rejects_closed_file() {
        let _guard = crate::state::test_support::lock_test();
        let _ = file::close_all_files();

        let result = line_input(1);
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().number,
            err_number::BAD_FILE_NAME_OR_NUMBER
        );

        let _ = file::close_all_files();
    }
}