use std::{borrow::Cow, fmt::Write, mem, str::FromStr};
use super::{
LazyHeapSet, List, PyTrait, Type,
bytes::Bytes,
str::{allocate_string, allocate_string_no_interning},
};
use crate::{
args::ArgValues,
bytecode::{CallResult, VM},
exception_private::{ExcType, RunError, RunResult, SimpleException},
heap::{DropWithHeap, Heap, HeapData, HeapGuard, HeapId, HeapItem, HeapRead, HeapReadOutput},
intern::StaticStrings,
os::{MontyPath, OsFunctionCall, PathBytesDataArgs, PathStringDataArgs},
resource::ResourceTracker,
types::str::StringRepr,
value::{EitherStr, Value},
};
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub(crate) enum ReadSpec {
All,
Size(usize),
Line,
Lines,
Seek { offset: i64, whence: i64 },
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub(crate) enum PendingFileEffect {
BufferStore { file_id: HeapId },
WritePosition {
file_id: HeapId,
previous_position: u64,
previous_length: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum FileMode {
Read(bool),
ReadUpdate(bool),
Write(bool),
WriteUpdate(bool),
Append(bool),
AppendUpdate(bool),
}
impl FileMode {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Read(false) => "r",
Self::Read(true) => "rb",
Self::ReadUpdate(false) => "r+",
Self::ReadUpdate(true) => "rb+",
Self::Write(false) => "w",
Self::Write(true) => "wb",
Self::WriteUpdate(false) => "w+",
Self::WriteUpdate(true) => "wb+",
Self::Append(false) => "a",
Self::Append(true) => "ab",
Self::AppendUpdate(false) => "a+",
Self::AppendUpdate(true) => "ab+",
}
}
#[must_use]
pub fn is_binary(&self) -> bool {
let (Self::Read(b)
| Self::ReadUpdate(b)
| Self::Write(b)
| Self::WriteUpdate(b)
| Self::Append(b)
| Self::AppendUpdate(b)) = self;
*b
}
#[must_use]
pub fn readable(&self) -> bool {
matches!(
self,
Self::Read(_) | Self::ReadUpdate(_) | Self::WriteUpdate(_) | Self::AppendUpdate(_)
)
}
#[must_use]
pub fn writable(&self) -> bool {
matches!(
self,
Self::Write(_) | Self::WriteUpdate(_) | Self::Append(_) | Self::AppendUpdate(_) | Self::ReadUpdate(_)
)
}
#[must_use]
pub fn is_append(&self) -> bool {
matches!(self, Self::Append(_) | Self::AppendUpdate(_))
}
#[must_use]
pub fn truncate(&self) -> bool {
matches!(self, Self::Write(_) | Self::WriteUpdate(_))
}
#[must_use]
pub fn create(&self) -> bool {
matches!(
self,
Self::Write(_) | Self::WriteUpdate(_) | Self::Append(_) | Self::AppendUpdate(_)
)
}
#[must_use]
pub fn file_type(&self) -> Type {
match self {
_ if !self.is_binary() => Type::TextIOWrapper,
Self::ReadUpdate(_) | Self::WriteUpdate(_) | Self::AppendUpdate(_) => Type::BufferedRandom,
Self::Read(_) => Type::BufferedReader,
Self::Write(_) | Self::Append(_) => Type::BufferedWriter,
}
}
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
_ if !self.is_binary() => "TextIOWrapper",
Self::ReadUpdate(_) | Self::WriteUpdate(_) | Self::AppendUpdate(_) => "BufferedRandom",
Self::Read(_) => "BufferedReader",
Self::Write(_) | Self::Append(_) => "BufferedWriter",
}
}
}
impl FromStr for FileMode {
type Err = Cow<'static, str>;
fn from_str(mode: &str) -> Result<Self, Self::Err> {
if mode.is_empty() {
return Err("Must have exactly one of create/read/write/append mode and at most one plus".into());
}
let mut action = None;
let mut binary = false;
let mut text = false;
for ch in mode.chars() {
match ch {
'r' | 'w' | 'a' => {
if action.replace(ch).is_some() {
return Err("must have exactly one of create/read/write/append mode".into());
}
}
'x' => return Err("exclusive creation mode is not supported".into()),
'b' => {
if binary {
return Err("invalid mode: binary mode specified twice".into());
}
binary = true;
}
't' => {
if text {
return Err("invalid mode: text mode specified twice".into());
}
text = true;
}
'+' => return Err("update modes ('+') are not yet supported".into()),
_ => return Err(format!("invalid mode: {ch:?}").into()),
}
}
if binary && text {
return Err("can't have text and binary mode at once".into());
}
Ok(match action.unwrap_or('r') {
'w' => Self::Write(binary),
'a' => Self::Append(binary),
_ => Self::Read(binary),
})
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub(crate) struct OpenFile {
path: String,
mode: FileMode,
first_write_done: bool,
closed: bool,
position: u64,
buffer: Option<HeapId>,
#[serde(skip)]
buffer_meta: Option<BufferMeta>,
pending_read: Option<ReadSpec>,
eof: bool,
file_length: u64,
}
#[derive(Debug, Clone, Copy)]
struct BufferMeta {
byte_position: u64,
buffer_total: u64,
}
impl OpenFile {
#[must_use]
pub fn with_state(path: String, mode: FileMode, position: u64) -> Self {
Self {
path,
mode,
first_write_done: mode.truncate(),
closed: false,
position,
buffer: None,
buffer_meta: None,
pending_read: None,
eof: false,
file_length: position,
}
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
#[must_use]
pub fn mode(&self) -> &'static str {
self.mode.as_str()
}
#[must_use]
pub fn file_mode(&self) -> &FileMode {
&self.mode
}
#[must_use]
pub fn position(&self) -> u64 {
self.position
}
#[must_use]
pub(crate) fn buffer_id(&self) -> Option<HeapId> {
self.buffer
}
#[must_use]
pub fn file_type(&self) -> Type {
self.mode.file_type()
}
}
impl HeapItem for OpenFile {
fn py_estimate_size(&self) -> usize {
mem::size_of::<Self>() + self.path.len()
}
fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {
if let Some(buffer_id) = self.buffer.take() {
stack.push(buffer_id);
}
}
}
impl<'h> PyTrait<'h> for HeapRead<'h, OpenFile> {
fn py_type(&self, vm: &VM<'h, impl ResourceTracker>) -> Type {
self.get(vm.heap).file_type()
}
fn py_len(&self, _vm: &VM<'h, impl ResourceTracker>) -> Option<usize> {
None
}
fn py_eq_impl(&self, _other: &Value, _vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Option<bool>> {
Ok(None)
}
fn py_bool(&self, _vm: &mut VM<'h, impl ResourceTracker>) -> bool {
true
}
fn py_repr_fmt(
&self,
f: &mut impl Write,
vm: &mut VM<'h, impl ResourceTracker>,
_heap_ids: &mut LazyHeapSet,
) -> RunResult<()> {
let file = self.get(vm.heap);
write!(
f,
"<{} name={} mode={}>",
file.file_type(),
StringRepr(file.path()),
StringRepr(file.mode())
)?;
Ok(())
}
fn py_call_attr(
&mut self,
self_id: HeapId,
vm: &mut VM<'h, impl ResourceTracker>,
attr: &EitherStr,
args: ArgValues,
) -> RunResult<CallResult> {
let Some(method) = attr.static_string() else {
args.drop_with_heap(vm);
return Err(ExcType::attribute_error(
self.py_type(vm).name(vm.heap, vm.interns),
attr.as_str(vm.interns),
));
};
match method {
StaticStrings::Read => self.read(self_id, vm, args),
StaticStrings::Readline => self.readline(self_id, vm, args),
StaticStrings::Readlines => self.readlines(self_id, vm, args),
StaticStrings::Tell => self.tell(vm, args),
StaticStrings::Seek => self.seek(self_id, vm, args),
StaticStrings::Write => self.write(self_id, vm, args),
StaticStrings::Close => self.close(vm, args),
StaticStrings::Flush => self.flush(vm, args),
StaticStrings::Readable => self.readable(vm, args),
StaticStrings::Writable => self.writable(vm, args),
StaticStrings::Seekable => self.seekable(vm, args),
_ => {
args.drop_with_heap(vm);
Err(ExcType::attribute_error(
self.py_type(vm).name(vm.heap, vm.interns),
attr.as_str(vm.interns),
))
}
}
}
fn py_is_context_manager(&self, _vm: &VM<'h, impl ResourceTracker>) -> bool {
true
}
fn py_enter(&mut self, self_id: HeapId, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<CallResult> {
self.get(vm.heap).ensure_open()?;
vm.heap.inc_ref(self_id);
Ok(CallResult::Value(Value::Ref(self_id)))
}
fn py_exit(
&mut self,
_self_id: HeapId,
vm: &mut VM<'h, impl ResourceTracker>,
_exc: Option<HeapId>,
) -> RunResult<CallResult> {
self.get_mut(vm.heap).closed = true;
Ok(CallResult::Value(Value::None))
}
fn py_getattr(&self, attr: &EitherStr, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Option<CallResult>> {
let Some(method) = attr.static_string() else {
return Err(ExcType::attribute_error(
self.py_type(vm).name(vm.heap, vm.interns),
attr.as_str(vm.interns),
));
};
let file = self.get(vm.heap);
let value = match method {
StaticStrings::Name => allocate_string(file.path.clone(), vm.heap)?,
StaticStrings::Mode => allocate_string(file.mode.as_str().to_owned(), vm.heap)?,
StaticStrings::Closed => Value::Bool(file.closed),
StaticStrings::Encoding if !file.mode.is_binary() => allocate_string("utf-8", vm.heap)?,
_ => {
return Err(ExcType::attribute_error(
self.py_type(vm).name(vm.heap, vm.interns),
attr.as_str(vm.interns),
));
}
};
Ok(Some(CallResult::Value(value)))
}
}
impl<'h> HeapRead<'h, OpenFile> {
fn read(
&mut self,
self_id: HeapId,
vm: &mut VM<'h, impl ResourceTracker>,
args: ArgValues,
) -> RunResult<CallResult> {
let spec = parse_read_size_arg(args.get_zero_one_arg("read", vm.heap)?, vm)?;
if matches!(spec, ReadSpec::Size(0)) {
let binary = {
let file = self.get(vm.heap);
file.ensure_open()?;
if !file.mode.readable() {
return Err(unsupported_operation("not readable"));
}
file.mode.is_binary()
};
Ok(CallResult::Value(empty_result(binary, vm.heap)?))
} else {
self.read_with_spec(self_id, vm, spec)
}
}
fn readline(
&mut self,
self_id: HeapId,
vm: &mut VM<'h, impl ResourceTracker>,
args: ArgValues,
) -> RunResult<CallResult> {
args.check_zero_args("readline", vm.heap)?;
self.read_with_spec(self_id, vm, ReadSpec::Line)
}
fn readlines(
&mut self,
self_id: HeapId,
vm: &mut VM<'h, impl ResourceTracker>,
args: ArgValues,
) -> RunResult<CallResult> {
args.check_zero_args("readlines", vm.heap)?;
self.read_with_spec(self_id, vm, ReadSpec::Lines)
}
fn tell(&self, vm: &mut VM<'h, impl ResourceTracker>, args: ArgValues) -> RunResult<CallResult> {
args.check_zero_args("tell", vm.heap)?;
let file = self.get(vm.heap);
file.ensure_open()?;
let pos = i64::try_from(file.position).map_err(|_| ExcType::overflow_c_ssize_t())?;
Ok(CallResult::Value(Value::Int(pos)))
}
fn seek(
&mut self,
self_id: HeapId,
vm: &mut VM<'h, impl ResourceTracker>,
args: ArgValues,
) -> RunResult<CallResult> {
let (offset, whence) = parse_seek_args(args, vm)?;
if self.get(vm.heap).mode.readable() {
self.read_with_spec(self_id, vm, ReadSpec::Seek { offset, whence })
} else {
let (target, file_length) = {
let file = self.get(vm.heap);
file.ensure_open()?;
let position = i64::try_from(file.position).map_err(|_| ExcType::overflow_c_ssize_t())?;
let file_length = i64::try_from(file.file_length).map_err(|_| ExcType::overflow_c_ssize_t())?;
(
resolve_seek_target(offset, whence, position, file_length)?,
file.file_length,
)
};
let target_u64 = u64::try_from(target).map_err(|_| ExcType::overflow_c_ssize_t())?;
let file = self.get_mut(vm.heap);
file.position = target_u64;
file.eof = target_u64 >= file_length;
Ok(CallResult::Value(Value::Int(target)))
}
}
fn read_with_spec(
&mut self,
self_id: HeapId,
vm: &mut VM<'h, impl ResourceTracker>,
spec: ReadSpec,
) -> RunResult<CallResult> {
let (binary, buffer_loaded) = {
let file = self.get(vm.heap);
file.ensure_open()?;
if !file.mode.readable() {
return Err(unsupported_operation("not readable"));
}
debug_assert!(!file.eof || file.buffer.is_some());
(file.mode.is_binary(), file.buffer.is_some())
};
if buffer_loaded {
return compute_slice(self_id, spec, vm).map(CallResult::Value);
}
self.get_mut(vm.heap).pending_read = Some(spec);
let path = MontyPath::new(self.get(vm.heap).path().to_owned());
let call = if binary {
OsFunctionCall::ReadBytes(path)
} else {
OsFunctionCall::ReadText(path)
};
inc_ref_for_pending_oscall(vm, self_id);
Ok(CallResult::OsCallStoreBuffer { call, file_id: self_id })
}
fn write(
&mut self,
self_id: HeapId,
vm: &mut VM<'h, impl ResourceTracker>,
args: ArgValues,
) -> RunResult<CallResult> {
let data = args.get_one_arg("write", vm.heap)?;
let binary = self.get(vm.heap).mode.is_binary();
if let Err(err) = validate_write_data(&data, binary, vm) {
data.drop_with_heap(vm);
return Err(err);
}
if let Err(err) = self.get(vm.heap).ensure_open() {
data.drop_with_heap(vm);
return Err(err);
}
let (path, append, binary) = {
let file = self.get_mut(vm.heap);
if !file.mode.writable() {
let message = if file.mode.is_binary() { "write" } else { "not writable" };
data.drop_with_heap(vm);
return Err(unsupported_operation(message));
}
let append = file.mode.is_append() || file.first_write_done;
let binary = file.mode.is_binary();
let path = file.path().to_owned();
file.first_write_done = true;
(path, append, binary)
};
let path = MontyPath::new(path);
let call = if binary {
let bytes = extract_bytes_payload(&data, vm).expect("validate_write_data accepted a bytes-shaped value");
data.drop_with_heap(vm);
let args = PathBytesDataArgs { path, data: bytes };
if append {
OsFunctionCall::AppendBytes(args)
} else {
OsFunctionCall::WriteBytes(args)
}
} else {
let text = extract_str_payload(&data, vm).expect("validate_write_data accepted a str-shaped value");
data.drop_with_heap(vm);
let args = PathStringDataArgs { path, data: text };
if append {
OsFunctionCall::AppendText(args)
} else {
OsFunctionCall::WriteText(args)
}
};
inc_ref_for_pending_oscall(vm, self_id);
vm.pending_file_effect = Some(PendingFileEffect::WritePosition {
file_id: self_id,
previous_position: self.get(vm.heap).position,
previous_length: self.get(vm.heap).file_length,
});
Ok(CallResult::OsCall(call))
}
fn close(&mut self, vm: &mut VM<'h, impl ResourceTracker>, args: ArgValues) -> RunResult<CallResult> {
args.check_zero_args("close", vm.heap)?;
let buffer_id = {
let file = self.get_mut(vm.heap);
file.closed = true;
file.buffer_meta = None;
file.buffer.take()
};
if let Some(buffer_id) = buffer_id {
vm.heap.dec_ref(buffer_id);
}
Ok(CallResult::Value(Value::None))
}
fn flush(&mut self, vm: &mut VM<'h, impl ResourceTracker>, args: ArgValues) -> RunResult<CallResult> {
args.check_zero_args("flush", vm.heap)?;
self.get(vm.heap).ensure_open()?;
Ok(CallResult::Value(Value::None))
}
fn readable(&mut self, vm: &mut VM<'h, impl ResourceTracker>, args: ArgValues) -> RunResult<CallResult> {
args.check_zero_args("readable", vm.heap)?;
let file = self.get(vm.heap);
file.ensure_open()?;
Ok(CallResult::Value(Value::Bool(file.mode.readable())))
}
fn writable(&mut self, vm: &mut VM<'h, impl ResourceTracker>, args: ArgValues) -> RunResult<CallResult> {
args.check_zero_args("writable", vm.heap)?;
let file = self.get(vm.heap);
file.ensure_open()?;
Ok(CallResult::Value(Value::Bool(file.mode.writable())))
}
fn seekable(&mut self, vm: &mut VM<'h, impl ResourceTracker>, args: ArgValues) -> RunResult<CallResult> {
args.check_zero_args("seekable", vm.heap)?;
self.get(vm.heap).ensure_open()?;
Ok(CallResult::Value(Value::Bool(true)))
}
}
impl OpenFile {
fn ensure_open(&self) -> RunResult<()> {
if self.closed {
Err(SimpleException::new_msg(ExcType::ValueError, "I/O operation on closed file.").into())
} else {
Ok(())
}
}
pub(crate) fn clear_pending_read(&mut self) {
self.pending_read = None;
}
pub(crate) fn rollback_write_position(&mut self, previous_position: u64, previous_length: u64) {
self.position = previous_position;
self.file_length = previous_length;
self.eof = previous_position >= previous_length;
}
}
fn inc_ref_for_pending_oscall(vm: &VM<'_, impl ResourceTracker>, file_id: HeapId) {
vm.heap.inc_ref(file_id);
}
fn os_read_result_to_heap_id(result: Value, vm: &mut VM<'_, impl ResourceTracker>) -> RunResult<HeapId> {
let id = match &result {
Value::Ref(id) => {
vm.heap.inc_ref(*id);
*id
}
Value::InternString(string_id) => {
let s = vm.interns.get_str(*string_id).to_owned();
let v = allocate_string_no_interning(s, vm.heap)?;
let Value::Ref(new_id) = &v else {
unreachable!("allocate_string_no_interning returns Value::Ref");
};
let new_id = *new_id;
vm.heap.inc_ref(new_id);
v.drop_with_heap(vm);
new_id
}
Value::InternBytes(bytes_id) => {
let b = vm.interns.get_bytes(*bytes_id).to_vec();
vm.heap.allocate(HeapData::Bytes(Bytes::new(b)))?
}
_ => {
result.drop_with_heap(vm);
return Err(RunError::internal(
"os_read_result_to_heap_id: OS result must be a string or bytes value",
));
}
};
result.drop_with_heap(vm);
Ok(id)
}
pub(crate) fn apply_buffer_store(
file_id: HeapId,
result: Value,
vm: &mut VM<'_, impl ResourceTracker>,
) -> RunResult<Value> {
let mut pin = HeapGuard::new(Value::Ref(file_id), vm);
let (result, spec) = {
let (_, vm) = pin.as_parts_mut();
let mut result_guard = HeapGuard::new(result, vm);
let (_, vm) = result_guard.as_parts_mut();
let HeapReadOutput::OpenFile(mut file) = vm.heap.read(file_id) else {
return Err(RunError::internal(
"apply_buffer_store: file_id does not point to an OpenFile",
));
};
let spec = file.get_mut(vm.heap).pending_read.take();
drop(file);
let Some(spec) = spec else {
return Err(RunError::internal("apply_buffer_store: OpenFile has no pending_read"));
};
(result_guard.into_inner(), spec)
};
let (_, vm) = pin.as_parts_mut();
let result_id = os_read_result_to_heap_id(result, vm)?;
let dec_result = {
let HeapReadOutput::OpenFile(mut file) = vm.heap.read(file_id) else {
vm.heap.dec_ref(result_id);
return Err(RunError::internal(
"apply_buffer_store: file_id does not point to an OpenFile",
));
};
let f = file.get_mut(vm.heap);
if f.buffer.is_some() {
true
} else {
f.buffer = Some(result_id);
false
}
};
if dec_result {
vm.heap.dec_ref(result_id);
}
populate_buffer_meta(file_id, vm)?;
compute_slice(file_id, spec, vm)
}
pub(crate) fn apply_write_position(
file_id: HeapId,
result: Value,
vm: &mut VM<'_, impl ResourceTracker>,
) -> RunResult<Value> {
let mut pin = HeapGuard::new(Value::Ref(file_id), vm);
let (_, vm) = pin.as_parts_mut();
let mut result_guard = HeapGuard::new(result, vm);
let (result_ref, vm) = result_guard.as_parts_mut();
let written = result_ref.as_int(vm)?;
if written < 0 {
return Err(RunError::internal(
"apply_write_position: write count cannot be negative",
));
}
let written = u64::try_from(written).map_err(|_| ExcType::overflow_c_ssize_t())?;
let HeapReadOutput::OpenFile(mut file) = vm.heap.read(file_id) else {
return Err(RunError::internal(
"apply_write_position: file_id does not point to an OpenFile",
));
};
let f = file.get_mut(vm.heap);
let new_position = f
.position
.checked_add(written)
.ok_or_else(ExcType::overflow_c_ssize_t)?;
f.position = new_position;
f.file_length = f.file_length.max(new_position);
f.eof = new_position >= f.file_length;
drop(file);
Ok(result_guard.into_inner())
}
fn compute_slice(file_id: HeapId, spec: ReadSpec, vm: &mut VM<'_, impl ResourceTracker>) -> RunResult<Value> {
let needs_meta = {
let HeapReadOutput::OpenFile(file) = vm.heap.read(file_id) else {
return Err(RunError::internal("compute_slice: not an OpenFile"));
};
let f = file.get(vm.heap);
f.buffer.is_some() && f.buffer_meta.is_none()
};
if needs_meta {
populate_buffer_meta(file_id, vm)?;
}
let (binary, buffer_id, position, byte_position, buffer_total) = {
let HeapReadOutput::OpenFile(file) = vm.heap.read(file_id) else {
return Err(RunError::internal("compute_slice: not an OpenFile"));
};
let f = file.get(vm.heap);
let buffer = f
.buffer
.ok_or_else(|| RunError::internal("compute_slice: buffer must be loaded"))?;
let meta = f
.buffer_meta
.ok_or_else(|| RunError::internal("compute_slice: buffer_meta must be loaded"))?;
let position = usize::try_from(f.position).map_err(|_| ExcType::overflow_c_ssize_t())?;
let byte_position = usize::try_from(meta.byte_position).map_err(|_| ExcType::overflow_c_ssize_t())?;
let buffer_total = usize::try_from(meta.buffer_total).map_err(|_| ExcType::overflow_c_ssize_t())?;
(f.mode.is_binary(), buffer, position, byte_position, buffer_total)
};
if binary {
compute_slice_binary(file_id, buffer_id, position, buffer_total, spec, vm)
} else {
compute_slice_text(file_id, buffer_id, position, byte_position, buffer_total, spec, vm)
}
}
fn compute_slice_text(
file_id: HeapId,
buffer_id: HeapId,
position: usize,
byte_position: usize,
buffer_total: usize,
spec: ReadSpec,
vm: &mut VM<'_, impl ResourceTracker>,
) -> RunResult<Value> {
let (value, new_position, new_byte_position, eof) = {
let buffer = match vm.heap.get(buffer_id) {
HeapData::Str(s) => s.as_str(),
_ => return Err(RunError::internal("compute_slice_text: buffer is not a Str")),
};
debug_assert!(byte_position <= buffer.len());
let tail = &buffer[byte_position..];
match spec {
ReadSpec::All => {
let value = if byte_position == 0 {
vm.heap.inc_ref(buffer_id);
Value::Ref(buffer_id)
} else {
allocate_string(tail.to_owned(), vm.heap)?
};
(value, position.max(buffer_total), buffer.len(), true)
}
ReadSpec::Size(n) => {
let take = buffer_total.saturating_sub(position).min(n);
let bytes_taken = tail.char_indices().nth(take).map_or(tail.len(), |(i, _)| i);
let slice = &tail[..bytes_taken];
let value = allocate_string(slice.to_owned(), vm.heap)?;
let new_pos = position + take;
let new_byte_pos = byte_position + bytes_taken;
(value, new_pos, new_byte_pos, new_pos >= buffer_total)
}
ReadSpec::Line => {
let (slice, chars_consumed) = match tail.find('\n') {
Some(rel) => {
let line = &tail[..=rel];
(line, line.chars().count())
}
None => (tail, tail.chars().count()),
};
let value = allocate_string(slice.to_owned(), vm.heap)?;
let new_pos = position + chars_consumed;
let new_byte_pos = byte_position + slice.len();
(value, new_pos, new_byte_pos, new_pos >= buffer_total)
}
ReadSpec::Lines => {
let mut items: Vec<Value> = Vec::new();
let mut start = 0usize;
while start < tail.len() {
let rest = &tail[start..];
let end = rest.find('\n').map_or(rest.len(), |i| i + 1);
let line = &rest[..end];
items.push(allocate_string(line.to_owned(), vm.heap)?);
start += end;
}
let list_id = vm.heap.allocate(HeapData::List(List::new(items)))?;
(Value::Ref(list_id), position.max(buffer_total), buffer.len(), true)
}
ReadSpec::Seek { offset, whence } => {
let target = resolve_seek_target_usize(offset, whence, position, buffer_total)?;
let target_usize = usize::try_from(target).map_err(|_| ExcType::overflow_c_ssize_t())?;
let target_clamped = target_usize.min(buffer_total);
let new_byte_pos = nth_char_byte_offset(buffer, target_clamped);
(
Value::Int(target),
target_usize,
new_byte_pos,
target_usize >= buffer_total,
)
}
}
};
update_file_state(file_id, new_position, new_byte_position, eof, vm)?;
Ok(value)
}
fn compute_slice_binary(
file_id: HeapId,
buffer_id: HeapId,
position: usize,
buffer_total: usize,
spec: ReadSpec,
vm: &mut VM<'_, impl ResourceTracker>,
) -> RunResult<Value> {
let clamped_position = position.min(buffer_total);
let (value, new_position, eof) = {
let buffer = match vm.heap.get(buffer_id) {
HeapData::Bytes(b) => b.as_slice(),
_ => return Err(RunError::internal("compute_slice_binary: buffer is not Bytes")),
};
debug_assert_eq!(buffer.len(), buffer_total);
let tail = &buffer[clamped_position..];
match spec {
ReadSpec::All => {
let value = if clamped_position == 0 {
vm.heap.inc_ref(buffer_id);
Value::Ref(buffer_id)
} else {
let id = vm.heap.allocate(HeapData::Bytes(Bytes::new(tail.to_vec())))?;
Value::Ref(id)
};
(value, position.max(buffer_total), true)
}
ReadSpec::Size(n) => {
let take = tail.len().min(n);
let id = vm.heap.allocate(HeapData::Bytes(Bytes::new(tail[..take].to_vec())))?;
let new_pos = position + take;
(Value::Ref(id), new_pos, new_pos >= buffer_total)
}
ReadSpec::Line => {
let end = tail.iter().position(|b| *b == b'\n').map_or(tail.len(), |i| i + 1);
let id = vm.heap.allocate(HeapData::Bytes(Bytes::new(tail[..end].to_vec())))?;
let new_pos = position + end;
(Value::Ref(id), new_pos, new_pos >= buffer_total)
}
ReadSpec::Lines => {
let mut items: Vec<Value> = Vec::new();
let mut start = 0usize;
while start < tail.len() {
let rest = &tail[start..];
let end = rest.iter().position(|b| *b == b'\n').map_or(rest.len(), |i| i + 1);
let id = vm.heap.allocate(HeapData::Bytes(Bytes::new(rest[..end].to_vec())))?;
items.push(Value::Ref(id));
start += end;
}
let list_id = vm.heap.allocate(HeapData::List(List::new(items)))?;
(Value::Ref(list_id), position.max(buffer_total), true)
}
ReadSpec::Seek { offset, whence } => {
let target = resolve_seek_target_usize(offset, whence, position, buffer_total)?;
let target_usize = usize::try_from(target).map_err(|_| ExcType::overflow_c_ssize_t())?;
(Value::Int(target), target_usize, target_usize >= buffer_total)
}
}
};
update_file_state(file_id, new_position, new_position.min(buffer_total), eof, vm)?;
Ok(value)
}
fn resolve_seek_target_usize(offset: i64, whence: i64, position: usize, buffer_len: usize) -> RunResult<i64> {
let position = i64::try_from(position).map_err(|_| ExcType::overflow_c_ssize_t())?;
let buffer_len = i64::try_from(buffer_len).map_err(|_| ExcType::overflow_c_ssize_t())?;
resolve_seek_target(offset, whence, position, buffer_len)
}
fn resolve_seek_target(offset: i64, whence: i64, position: i64, buffer_len: i64) -> RunResult<i64> {
let target = match whence {
0 => offset,
1 => position.checked_add(offset).ok_or_else(ExcType::overflow_c_ssize_t)?,
2 => buffer_len.checked_add(offset).ok_or_else(ExcType::overflow_c_ssize_t)?,
_ => {
return Err(
SimpleException::new_msg(ExcType::ValueError, format!("whence value {whence} unsupported")).into(),
);
}
};
if target < 0 {
return Err(SimpleException::new_msg(ExcType::OSError, "[Errno 22] Invalid argument").into());
}
Ok(target)
}
fn nth_char_byte_offset(s: &str, nth: usize) -> usize {
s.char_indices().nth(nth).map_or(s.len(), |(i, _)| i)
}
fn update_file_state(
file_id: HeapId,
new_position: usize,
new_byte_position: usize,
eof: bool,
vm: &mut VM<'_, impl ResourceTracker>,
) -> RunResult<()> {
let new_position = u64::try_from(new_position).expect("usize fits in u64");
let new_byte_position = u64::try_from(new_byte_position).expect("usize fits in u64");
let HeapReadOutput::OpenFile(mut file) = vm.heap.read(file_id) else {
return Err(RunError::internal(
"update_file_state: file_id does not point to an OpenFile",
));
};
let f = file.get_mut(vm.heap);
f.position = new_position;
f.eof = eof;
let buffer_total = f.buffer_meta.as_ref().map_or(new_byte_position, |m| m.buffer_total);
f.buffer_meta = Some(BufferMeta {
byte_position: new_byte_position,
buffer_total,
});
f.file_length = buffer_total;
Ok(())
}
fn populate_buffer_meta(file_id: HeapId, vm: &mut VM<'_, impl ResourceTracker>) -> RunResult<()> {
let (buffer_id, position, binary, already_populated) = {
let HeapReadOutput::OpenFile(file) = vm.heap.read(file_id) else {
return Err(RunError::internal(
"populate_buffer_meta: file_id does not point to an OpenFile",
));
};
let f = file.get(vm.heap);
let Some(buffer_id) = f.buffer else {
return Err(RunError::internal("populate_buffer_meta: buffer must be loaded"));
};
(buffer_id, f.position, f.mode.is_binary(), f.buffer_meta.is_some())
};
if already_populated {
return Ok(());
}
let meta = match (vm.heap.get(buffer_id), binary) {
(HeapData::Bytes(b), true) => {
let len = b.as_slice().len() as u64;
BufferMeta {
byte_position: position.min(len),
buffer_total: len,
}
}
(HeapData::Str(s), false) => {
let s = s.as_str();
let char_count = s.chars().count();
let pos_clamped = usize::try_from(position).unwrap_or(usize::MAX).min(char_count);
BufferMeta {
byte_position: nth_char_byte_offset(s, pos_clamped) as u64,
buffer_total: char_count as u64,
}
}
_ => {
return Err(RunError::internal(
"populate_buffer_meta: buffer type does not match file mode",
));
}
};
let HeapReadOutput::OpenFile(mut file) = vm.heap.read(file_id) else {
return Err(RunError::internal(
"populate_buffer_meta: file_id does not point to an OpenFile",
));
};
file.get_mut(vm.heap).buffer_meta = Some(meta);
Ok(())
}
fn parse_seek_args(args: ArgValues, vm: &mut VM<'_, impl ResourceTracker>) -> RunResult<(i64, i64)> {
let (offset, maybe_whence) = args.get_one_two_args("seek", vm.heap)?;
let offset_int = offset.as_int(vm)?;
let whence_int = match maybe_whence {
Some(w) => w.as_int(vm)?,
None => 0,
};
Ok((offset_int, whence_int))
}
fn parse_read_size_arg(size_arg: Option<Value>, vm: &mut VM<'_, impl ResourceTracker>) -> RunResult<ReadSpec> {
let Some(size) = size_arg else {
return Ok(ReadSpec::All);
};
let spec = match &size {
Value::None => Ok(ReadSpec::All),
Value::Bool(false) => Ok(ReadSpec::Size(0)),
Value::Bool(true) => Ok(ReadSpec::Size(1)),
_ => match size.as_int(vm) {
Ok(n) if n < 0 => Ok(ReadSpec::All),
Ok(n) => usize::try_from(n)
.map(ReadSpec::Size)
.map_err(|_| ExcType::overflow_c_ssize_t()),
Err(err) => Err(err),
},
};
size.drop_with_heap(vm);
spec
}
fn empty_result(binary: bool, heap: &mut Heap<impl ResourceTracker>) -> RunResult<Value> {
if binary {
let id = heap.allocate(HeapData::Bytes(Bytes::new(Vec::new())))?;
Ok(Value::Ref(id))
} else {
Ok(Value::InternString(StaticStrings::EmptyString.into()))
}
}
fn validate_write_data(data: &Value, binary: bool, vm: &VM<'_, impl ResourceTracker>) -> RunResult<()> {
if binary {
if is_bytes(data, vm.heap) {
Ok(())
} else {
Err(ExcType::type_error(format!(
"a bytes-like object is required, not '{}'",
data.py_type_name(vm)
)))
}
} else if data.is_str(vm.heap) {
Ok(())
} else {
Err(ExcType::type_error(format!(
"write() argument must be str, not {}",
data.py_type_name(vm)
)))
}
}
fn extract_str_payload(data: &Value, vm: &VM<'_, impl ResourceTracker>) -> Option<String> {
match data {
Value::InternString(id) => Some(vm.interns.get_str(*id).to_owned()),
Value::Ref(id) => match vm.heap.get(*id) {
HeapData::Str(s) => Some(s.as_str().to_owned()),
_ => None,
},
_ => None,
}
}
fn extract_bytes_payload(data: &Value, vm: &VM<'_, impl ResourceTracker>) -> Option<Vec<u8>> {
match data {
Value::InternBytes(id) => Some(vm.interns.get_bytes(*id).to_owned()),
Value::Ref(id) => match vm.heap.get(*id) {
HeapData::Bytes(b) => Some(b.as_slice().to_owned()),
_ => None,
},
_ => None,
}
}
fn is_bytes(data: &Value, heap: &Heap<impl ResourceTracker>) -> bool {
match data {
Value::InternBytes(_) => true,
Value::Ref(id) => matches!(heap.get(*id), HeapData::Bytes(_)),
_ => false,
}
}
fn unsupported_operation(message: &'static str) -> RunError {
SimpleException::new_msg(ExcType::UnsupportedOperation, message).into()
}