use std::fs;
use std::io::Write;
use std::path::Path;
use std::time::Instant;
use anyhow::{Context, Result};
use crate::binary_detect;
use crate::checksum;
use crate::cli::{GlobalArgs, OutputFormat, ReadArgs};
use crate::error::AtomwriteError;
use crate::ndjson_types::{LineRange, MediaInfo, ReadOutput};
use crate::output::NdjsonWriter;
#[tracing::instrument(skip_all, fields(command = "read"))]
pub fn cmd_read(
args: &ReadArgs,
global: &GlobalArgs,
writer: &mut NdjsonWriter<impl Write>,
verb: &str,
) -> Result<()> {
let start = Instant::now();
if args.no_comments && !numbering_requested(args) {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"{verb} --no-comments {} so every line keeps the file's number; add --number",
crate::constants::NO_COMMENTS_NEEDS_NUMBER_REASON_MARKER
),
}
.into());
}
let workspace = global.resolve_workspace()?;
let targets = resolve_targets(&args.paths, &workspace)?;
if matches!(args.format, OutputFormat::Raw) {
return write_raw_targets(&targets, args, global, &workspace, writer, verb);
}
if let Some(pattern) = targets.empty_patterns.first() {
return Err(AtomwriteError::GlobNoMatches {
pattern: pattern.clone(),
}
.into());
}
if targets.paths.len() != 1 {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"{verb} takes one target per invocation, and {} were named",
targets.paths.len()
),
}
.into());
}
let path = crate::path_safety::validate_path(&targets.paths[0], &workspace)?;
if !path.exists() {
return Err(AtomwriteError::NotFound { path }.into());
}
if let Some(pattern) = args.grep.as_deref() {
regex::Regex::new(pattern).map_err(|e| {
crate::error::AtomwriteError::invalid(&format!("invalid --grep pattern '{pattern}'"), e)
})?;
}
let metadata =
fs::metadata(&path).with_context(|| format!("cannot stat {}", path.display()))?;
let max_size = global.effective_max_filesize();
let is_binary = detect_binary_prefix(&path)?;
let (hash, bytes) = checksum::hash_file_streaming(&path, max_size)?;
if let Some(ref expected) = args.verify_checksum
&& &hash != expected
{
return Err(AtomwriteError::ChecksumVerifyFailed {
path,
expected: expected.clone(),
}
.into());
}
if let Some(kind) = crate::media::classify(&path) {
return read_media(kind, &path, args, writer, &metadata, &hash, bytes);
}
if args.ocr {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"read: {} carries text, so --ocr has nothing to recognize; it applies to a \
picture or to a PDF page",
path.display()
),
}
.into());
}
if args.no_comments
&& !is_binary
&& crate::syntax_check::detect_language_name(&path, &[]).is_none()
{
crate::best_effort::best_effort(
writeln!(
std::io::stderr().lock(),
"atomwrite: {} has no known language, so --no-comments drops blank lines and lines starting with //, #, --, /* or *",
path.display()
),
"warn that --no-comments has no comment syntax to drop",
);
}
let permissions_str = format_permissions(&metadata);
let modified_str = format_modified(&metadata);
let mut cut = None;
let (content_str, lines_total) = if is_binary {
(None, 0u64)
} else if args.stat {
(None, count_lines_streaming(&path)?)
} else {
let scanned = scan_lines_streaming(&path, args, max_size)?;
cut = scanned.truncated.then(|| (true, scanned.next.clone()));
(Some(scanned.content), scanned.total)
};
let (truncated, next) = cut.unwrap_or((false, None));
let line_count = content_str
.as_deref()
.map(|s| s.lines().count() as u64)
.unwrap_or(lines_total);
let range = if is_binary {
None
} else {
parse_line_range(args, lines_total)
};
let mode = if args.stat {
"stat"
} else if args.line.is_some() {
"line"
} else if !args.lines.is_empty() {
"lines"
} else if args.head.is_some() {
"head"
} else if args.tail.is_some() {
"tail"
} else if args.grep.is_some() {
"grep"
} else {
"full"
};
let output = ReadOutput {
r#type: "read",
path: path.display().to_string(),
content: content_str,
lines: line_count,
lines_total: if lines_total != line_count {
Some(lines_total)
} else {
None
},
truncated: truncated.then_some(true),
next,
bytes,
checksum: hash,
permissions: permissions_str,
modified: modified_str,
kind: if is_binary {
"binary".into()
} else {
"text".into()
},
binary: is_binary,
range,
verified: args.verify_checksum.as_ref().map(|_| true),
mode: mode.to_string(),
media: None,
};
writer.write_event(&output)?;
tracing::debug!(path = %path.display(), elapsed_ms = start.elapsed().as_millis() as u64, "read complete");
Ok(())
}
fn read_media(
kind: crate::media::MediaKind,
path: &Path,
args: &ReadArgs,
writer: &mut NdjsonWriter<impl Write>,
metadata: &fs::Metadata,
hash: &str,
bytes: u64,
) -> Result<()> {
let request = media_request(args)?;
match kind {
crate::media::MediaKind::Image(format) => {
let facts = crate::media::image_uri::facts(path, format, &request)?;
if args.stat {
let output = ReadOutput {
r#type: "read",
path: path.display().to_string(),
content: None,
lines: 0,
lines_total: None,
truncated: None,
next: None,
bytes,
checksum: hash.to_owned(),
permissions: format_permissions(metadata),
modified: format_modified(metadata),
kind: "image".into(),
binary: true,
range: None,
verified: None,
mode: "stat".into(),
media: Some(MediaInfo {
kind: "image".into(),
width: Some(facts.width),
height: Some(facts.height),
format: Some(facts.format.to_owned()),
visual_tokens: Some(facts.visual_tokens),
frames: facts.frames,
orientation: facts.orientation,
pages: None,
needs_vision: None,
}),
};
writer.write_event(&output)?;
return Ok(());
}
let file = FileFacts {
path,
metadata,
hash,
bytes,
};
if args.ocr {
let picture = crate::media::image_uri::decode_for_ocr(path, &request)?;
return emit_recognized(&file, writer, "image", &picture, None);
}
if !args.force && crate::media::image_uri::delivered_in_session(hash) {
let output = ReadOutput {
r#type: "read",
path: path.display().to_string(),
content: None,
lines: 0,
lines_total: None,
truncated: None,
next: None,
bytes,
checksum: hash.to_owned(),
permissions: format_permissions(metadata),
modified: format_modified(metadata),
kind: "image".into(),
binary: true,
range: None,
verified: None,
mode: "delivered".into(),
media: Some(MediaInfo {
kind: "image".into(),
width: Some(facts.width),
height: Some(facts.height),
format: Some(facts.format.to_owned()),
visual_tokens: Some(facts.visual_tokens),
frames: facts.frames,
orientation: facts.orientation,
pages: None,
needs_vision: None,
}),
};
writer.write_event(&output)?;
return Ok(());
}
let (uri, facts) = crate::media::image_uri::to_data_uri(path, format, &request)?;
crate::media::image_uri::guard_size(&uri, crate::constants::IMAGE_MAX_DATA_URI_BYTES)?;
tracing::debug!(
path = %path.display(),
width = facts.width,
height = facts.height,
visual_tokens = facts.visual_tokens,
"picture encoded"
);
writer.flush()?;
let mut out = std::io::stdout().lock();
out.write_all(uri.as_bytes())?;
out.write_all(b"\n")?;
out.flush()?;
crate::media::image_uri::mark_delivered(hash);
Ok(())
}
crate::media::MediaKind::Heic => {
let (source_width, source_height) = crate::media::image_uri::heic_size(path)?;
let (max_edge, max_tokens) =
crate::media::token_rule::tier_limits(request.high_resolution);
let max_tokens = request.max_tokens.unwrap_or(max_tokens);
let (final_width, final_height) =
crate::media::token_rule::fit(source_width, source_height, max_edge, max_tokens);
if args.stat {
let output = ReadOutput {
r#type: "read",
path: path.display().to_string(),
content: None,
lines: 0,
lines_total: None,
truncated: None,
next: None,
bytes,
checksum: hash.to_owned(),
permissions: format_permissions(metadata),
modified: format_modified(metadata),
kind: "image".into(),
binary: true,
range: None,
verified: None,
mode: "stat".into(),
media: Some(MediaInfo {
kind: "image".into(),
width: Some(source_width),
height: Some(source_height),
format: Some("heic".into()),
visual_tokens: Some(crate::media::token_rule::visual_tokens(
final_width,
final_height,
)),
frames: None,
orientation: None,
pages: None,
needs_vision: None,
}),
};
writer.write_event(&output)?;
return Ok(());
}
if args.ocr {
let picture = crate::media::image_uri::decode_heic(path)?;
let file = FileFacts {
path,
metadata,
hash,
bytes,
};
return emit_recognized(&file, writer, "image", &picture, None);
}
if !args.force && crate::media::image_uri::delivered_in_session(hash) {
let output = ReadOutput {
r#type: "read",
path: path.display().to_string(),
content: None,
lines: 0,
lines_total: None,
truncated: None,
next: None,
bytes,
checksum: hash.to_owned(),
permissions: format_permissions(metadata),
modified: format_modified(metadata),
kind: "image".into(),
binary: true,
range: None,
verified: None,
mode: "delivered".into(),
media: Some(MediaInfo {
kind: "image".into(),
width: Some(final_width),
height: Some(final_height),
format: Some("jpeg".into()),
visual_tokens: Some(crate::media::token_rule::visual_tokens(
final_width,
final_height,
)),
frames: None,
orientation: None,
pages: None,
needs_vision: None,
}),
};
writer.write_event(&output)?;
return Ok(());
}
let picture = crate::media::image_uri::decode_heic(path)?;
let (uri, facts) = crate::media::image_uri::from_picture(picture, &request)?;
crate::media::image_uri::guard_size(&uri, crate::constants::IMAGE_MAX_DATA_URI_BYTES)?;
writer.flush()?;
let mut out = std::io::stdout().lock();
out.write_all(uri.as_bytes())?;
out.write_all(b"\n")?;
out.flush()?;
crate::media::image_uri::mark_delivered(hash);
tracing::debug!(
path = %path.display(),
width = facts.width,
height = facts.height,
visual_tokens = facts.visual_tokens,
"heic picture encoded"
);
Ok(())
}
crate::media::MediaKind::Pdf => {
if let Some(page) = args.page {
let file = FileFacts {
path,
metadata,
hash,
bytes,
};
return read_rendered_page(&file, page, args, writer, &request);
}
let total = crate::media::pdf_text::page_count(path)?;
if args.stat {
let output = ReadOutput {
r#type: "read",
path: path.display().to_string(),
content: None,
lines: 0,
lines_total: None,
truncated: None,
next: None,
bytes,
checksum: hash.to_owned(),
permissions: format_permissions(metadata),
modified: format_modified(metadata),
kind: "pdf".into(),
binary: true,
range: None,
verified: None,
mode: "stat".into(),
media: Some(MediaInfo {
kind: "pdf".into(),
width: None,
height: None,
format: Some("pdf".into()),
visual_tokens: None,
frames: None,
orientation: None,
pages: Some((1..=total).collect()),
needs_vision: None,
}),
};
writer.write_event(&output)?;
return Ok(());
}
let pages = requested_pages(&args.pages, total)?;
let extracted = crate::media::pdf_text::extract_pages(
path,
&pages,
crate::constants::PDF_MAX_DECOMPRESSED_BYTES,
)?;
let needs_vision: Vec<u32> = extracted
.iter()
.filter(|page| page.needs_vision())
.map(|page| page.page)
.collect();
let content = join_pages(&extracted);
let line_count = content.lines().count() as u64;
let output = ReadOutput {
r#type: "read",
path: path.display().to_string(),
content: Some(content),
lines: line_count,
lines_total: None,
truncated: None,
next: None,
bytes,
checksum: hash.to_owned(),
permissions: format_permissions(metadata),
modified: format_modified(metadata),
kind: "pdf".into(),
binary: true,
range: None,
verified: None,
mode: if args.stat { "stat" } else { "pages" }.into(),
media: Some(MediaInfo {
kind: "pdf".into(),
width: None,
height: None,
format: Some("pdf".into()),
visual_tokens: None,
frames: None,
orientation: None,
pages: Some(pages),
needs_vision: (!needs_vision.is_empty()).then_some(needs_vision),
}),
};
writer.write_event(&output)?;
Ok(())
}
}
}
fn media_request(args: &ReadArgs) -> Result<crate::media::image_uri::RenderRequest> {
let crop = match args.crop.as_deref() {
None => None,
Some(spec) => Some(parse_crop(spec)?),
};
Ok(crate::media::image_uri::RenderRequest {
high_resolution: args.high_res,
max_tokens: args.max_tokens,
crop,
})
}
fn parse_crop(spec: &str) -> Result<(u32, u32, u32, u32)> {
let parts: Vec<&str> = spec.split(',').map(str::trim).collect();
let [x, y, width, height] = parts.as_slice() else {
return Err(AtomwriteError::InvalidInput {
reason: format!("--crop needs X,Y,W,H and got {spec:?}"),
}
.into());
};
let parse = |value: &str, name: &str| -> Result<u32> {
value.parse::<u32>().map_err(|_| {
anyhow::Error::from(AtomwriteError::InvalidInput {
reason: format!("--crop {name} must be a non-negative integer, got {value:?}"),
})
})
};
let window = (
parse(x, "x")?,
parse(y, "y")?,
parse(width, "width")?,
parse(height, "height")?,
);
if window.2 == 0 || window.3 == 0 {
return Err(AtomwriteError::InvalidInput {
reason: "--crop width and height must be greater than zero".into(),
}
.into());
}
Ok(window)
}
fn requested_pages(specs: &[String], total: u32) -> Result<Vec<u32>> {
if specs.is_empty() {
return Ok((1..=total).collect());
}
let mut pages: Vec<u32> = Vec::new();
for spec in specs {
match spec.split_once(':') {
Some((first, last)) => {
let first =
first
.trim()
.parse::<u32>()
.map_err(|_| AtomwriteError::InvalidInput {
reason: format!(
"--pages range start must be a page number, got {first:?}"
),
})?;
let last =
last.trim()
.parse::<u32>()
.map_err(|_| AtomwriteError::InvalidInput {
reason: format!(
"--pages range end must be a page number, got {last:?}"
),
})?;
pages.extend(crate::media::pdf_text::page_range(first, last, total));
}
None => {
let page =
spec.trim()
.parse::<u32>()
.map_err(|_| AtomwriteError::InvalidInput {
reason: format!("--pages must be N or A:B, got {spec:?}"),
})?;
pages.push(page);
}
}
}
pages.sort_unstable();
pages.dedup();
Ok(pages)
}
struct FileFacts<'a> {
path: &'a Path,
metadata: &'a fs::Metadata,
hash: &'a str,
bytes: u64,
}
fn emit_recognized(
file: &FileFacts<'_>,
writer: &mut NdjsonWriter<impl Write>,
kind: &str,
picture: &image::DynamicImage,
pages: Option<Vec<u32>>,
) -> Result<()> {
let FileFacts {
path,
metadata,
hash,
bytes,
} = *file;
let text = crate::media::ocr::recognize(picture)?;
let lines = u64::try_from(text.lines().count()).unwrap_or(u64::MAX);
let output = ReadOutput {
r#type: "read",
path: path.display().to_string(),
content: Some(text),
lines,
lines_total: Some(lines),
truncated: None,
next: None,
bytes,
checksum: hash.to_owned(),
permissions: format_permissions(metadata),
modified: format_modified(metadata),
kind: kind.into(),
binary: true,
range: None,
verified: None,
mode: "ocr".into(),
media: Some(MediaInfo {
kind: kind.into(),
width: Some(picture.width()),
height: Some(picture.height()),
format: None,
visual_tokens: None,
frames: None,
orientation: None,
pages,
needs_vision: None,
}),
};
writer.write_event(&output)?;
tracing::debug!(
path = %path.display(),
lines,
"characters recognized"
);
Ok(())
}
fn read_rendered_page(
file: &FileFacts<'_>,
page: u32,
args: &ReadArgs,
writer: &mut NdjsonWriter<impl Write>,
request: &crate::media::image_uri::RenderRequest,
) -> Result<()> {
let FileFacts {
path,
metadata,
hash,
bytes,
} = *file;
let dpi = args.dpi.unwrap_or(if args.ocr {
crate::constants::PDF_OCR_RENDER_DPI
} else {
crate::constants::PDF_RENDER_DPI
});
if !crate::constants::PDF_RENDER_DPI_RANGE.contains(&dpi) {
return Err(crate::error::AtomwriteError::InvalidInput {
reason: format!(
"read: --dpi {dpi} is outside {} to {} dots per inch",
crate::constants::PDF_RENDER_DPI_RANGE.start(),
crate::constants::PDF_RENDER_DPI_RANGE.end()
),
}
.into());
}
let picture = crate::media::pdf_render::render_page(
path,
page,
dpi,
crate::constants::IMAGE_MAX_MEGAPIXELS.saturating_mul(1_000_000),
)?;
if args.ocr {
return emit_recognized(file, writer, "pdf", &picture, Some(vec![page]));
}
let (uri, facts) = crate::media::image_uri::from_picture(picture, request)?;
if args.stat {
let output = ReadOutput {
r#type: "read",
path: path.display().to_string(),
content: None,
lines: 0,
lines_total: None,
truncated: None,
next: None,
bytes,
checksum: hash.to_owned(),
permissions: format_permissions(metadata),
modified: format_modified(metadata),
kind: "pdf".into(),
binary: true,
range: None,
verified: None,
mode: "stat".into(),
media: Some(MediaInfo {
kind: "pdf".into(),
width: Some(facts.width),
height: Some(facts.height),
format: Some(facts.format.to_owned()),
visual_tokens: Some(facts.visual_tokens),
frames: None,
orientation: None,
pages: Some(vec![page]),
needs_vision: None,
}),
};
writer.write_event(&output)?;
return Ok(());
}
crate::media::image_uri::guard_size(&uri, crate::constants::IMAGE_MAX_DATA_URI_BYTES)?;
tracing::debug!(
path = %path.display(),
page,
width = facts.width,
height = facts.height,
visual_tokens = facts.visual_tokens,
"PDF page rendered"
);
writer.flush()?;
let mut out = std::io::stdout().lock();
out.write_all(uri.as_bytes())?;
out.write_all(b"\n")?;
out.flush()?;
Ok(())
}
fn join_pages(pages: &[crate::media::pdf_text::PageText]) -> String {
let mut out = String::new();
let several = pages.len() > 1;
for page in pages {
if several {
out.push_str(&format!("--- page {} ---\n", page.page));
}
out.push_str(&page.text);
if !out.ends_with('\n') {
out.push('\n');
}
}
out
}
fn write_raw(writer: &mut NdjsonWriter<impl Write>, data: &[u8], args: &ReadArgs) -> Result<()> {
writer.flush()?;
let inner = std::io::stdout();
let mut lock = inner.lock();
let (rendered, emittable, total_lines) = render_raw_lines(data, args);
let cap = if args.all {
usize::MAX
} else {
crate::config::read_max_output_bytes()
};
let (payload, truncated) = cap_lines(&rendered, cap);
let written = lock.write_all(payload);
if truncated {
let emitted = emittable
.iter()
.filter(|(_, end)| *end <= payload.len())
.count()
.max(1);
let last = emittable
.get(emitted.saturating_sub(1))
.map(|(line, _)| *line)
.unwrap_or(0);
let next = last.saturating_add(1);
crate::best_effort::best_effort(
writeln!(
std::io::stderr().lock(),
"atomwrite: showing {emitted} of {} selected lines up to {cap} bytes; next: --lines {next}:{total_lines} (use --all for everything)",
emittable.len()
),
"write read truncation notice on stderr",
);
}
written.and_then(|()| lock.flush()).map_err(|e| {
if e.kind() == std::io::ErrorKind::BrokenPipe {
crate::error::AtomwriteError::BrokenPipe.into()
} else {
anyhow::Error::from(e)
}
})
}
fn write_raw_targets(
targets: &ResolvedTargets,
args: &ReadArgs,
global: &GlobalArgs,
workspace: &std::path::Path,
writer: &mut NdjsonWriter<impl Write>,
verb: &str,
) -> Result<()> {
if args.stat && targets.paths.len() > 1 {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"{verb} --stat takes one target per invocation, and {} were named",
targets.paths.len()
),
}
.into());
}
if targets.paths.len() == 1 && targets.empty_patterns.is_empty() {
let path = crate::path_safety::validate_path(&targets.paths[0], workspace)?;
if !path.exists() {
return Err(AtomwriteError::NotFound { path }.into());
}
if args.symbol.is_none() && !args.enclosing {
return stream_one_raw(&path, args, global, writer, verb);
}
let raw_bytes = crate::file_io::read_file_bytes(&path, global.effective_max_filesize())?;
let head = &raw_bytes[..raw_bytes.len().min(crate::constants::BINARY_DETECT_SIZE)];
if binary_detect::is_binary(head) {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"{verb} --format raw refuses binary {}; drop --format raw: a picture then arrives as a data: URI and a PDF as page text",
path.display()
),
}
.into());
}
if let Some(ref expected) = args.verify_checksum {
let hash = checksum::hash_bytes(&raw_bytes);
if &hash != expected {
return Err(AtomwriteError::ChecksumVerifyFailed {
path,
expected: expected.clone(),
}
.into());
}
}
let mut raw_bytes = raw_bytes;
if let Some(name) = &args.symbol {
let spans = crate::commands::outline::symbol_spans(&path, &raw_bytes, None, name);
let span = crate::commands::outline::resolve_symbol_span(&spans, name, &path)
.map_err(anyhow::Error::from)?;
raw_bytes = raw_bytes[span.start..span.end].to_vec();
} else if args.enclosing {
let line = args.line.ok_or_else(|| AtomwriteError::InvalidInput {
reason: "--enclosing needs --line N".to_string(),
})?;
let (start, end) =
crate::commands::outline::enclosing_span(&path, &raw_bytes, None, line)
.ok_or_else(|| AtomwriteError::InvalidInput {
reason: format!(
"no structural block contains line {line} in {}",
path.display()
),
})?;
raw_bytes = raw_bytes[start..end].to_vec();
}
if !args.lines.is_empty() {
let total = line_spans(&raw_bytes).len();
let selected = args.lines.iter().any(|spec| {
spec.split(',')
.any(|part| parse_range_str(part.trim(), total).is_some_and(|(s, e)| s < e))
});
if !selected && total > 0 {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"--lines selects nothing in {}: the file has {total} lines",
path.display()
),
}
.into());
}
}
return write_raw(writer, &raw_bytes, args);
}
if !has_line_filters(args) {
return stream_raw_targets(targets, global, workspace, writer);
}
stream_filtered_targets(targets, args, global, workspace, writer, verb)
}
fn has_line_filters(args: &ReadArgs) -> bool {
args.head.is_some()
|| args.tail.is_some()
|| !args.lines.is_empty()
|| args.line.is_some()
|| args.symbol.is_some()
|| args.enclosing
|| args.grep.is_some()
|| args.number
|| args.no_comments
}
fn open_raw_reader(
path: &std::path::Path,
verb: &str,
) -> Result<std::io::BufReader<std::fs::File>> {
use std::io::BufRead;
let file = std::fs::File::open(path)?;
let mut reader = std::io::BufReader::with_capacity(crate::constants::RAW_COPY_BUF_BYTES, file);
let head = reader.fill_buf()?;
let len = head.len().min(crate::constants::BINARY_DETECT_SIZE);
if binary_detect::is_binary(&head[..len]) {
return Err(binary_raw_error(verb, path));
}
Ok(reader)
}
fn binary_raw_error(verb: &str, path: &std::path::Path) -> anyhow::Error {
AtomwriteError::InvalidInput {
reason: format!(
"{verb} --format raw refuses binary {}; drop --format raw: a picture then arrives as a data: URI and a PDF as page text",
path.display()
),
}
.into()
}
fn stream_one_raw(
path: &std::path::Path,
args: &ReadArgs,
global: &GlobalArgs,
writer: &mut NdjsonWriter<impl Write>,
verb: &str,
) -> Result<()> {
writer.flush()?;
let total_bytes = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
let max_size = global.effective_max_filesize();
if total_bytes > max_size {
return Err(AtomwriteError::FileTooLarge {
path: path.to_path_buf(),
size: total_bytes,
max_size,
}
.into());
}
let mut reader = open_raw_reader(path, verb)?;
if let Some(ref expected) = args.verify_checksum {
let (hash, _) = checksum::hash_file_streaming(path, global.effective_max_filesize())?;
if &hash != expected {
return Err(AtomwriteError::ChecksumVerifyFailed {
path: path.to_path_buf(),
expected: expected.clone(),
}
.into());
}
}
let mut out = std::io::stdout().lock();
if !has_line_filters(args) {
return copy_plain_raw(path, args, &mut reader, &mut out, total_bytes);
}
let cap = if args.all {
usize::MAX
} else {
crate::config::read_max_output_bytes()
};
let stats = stream_selected_into(path, args, &mut out, cap, verb)?;
if cap != usize::MAX && stats.truncated {
write_filtered_notice(path, &stats, cap);
}
out.flush().map_err(map_raw_io)
}
fn copy_plain_raw(
path: &std::path::Path,
args: &ReadArgs,
reader: &mut std::io::BufReader<std::fs::File>,
out: &mut impl Write,
total_bytes: u64,
) -> Result<()> {
use std::io::Read;
if args.all {
copy_raw_stream(reader, out)?;
return out.flush().map_err(map_raw_io);
}
let cap = crate::config::read_max_output_bytes();
let mut buf = Vec::with_capacity(cap.min(total_bytes as usize).saturating_add(1));
reader.by_ref().take(cap as u64 + 1).read_to_end(&mut buf)?;
let (payload, truncated) = cap_lines(&buf, cap);
let payload = if truncated {
align_raw_payload(path, payload)
} else {
payload
};
let written = out.write_all(payload).and_then(|()| out.flush());
if truncated {
let emitted = payload.iter().filter(|b| **b == b'\n').count().max(1);
let prefixed = buf.iter().filter(|b| **b == b'\n').count();
let mut chunk = [0u8; 65_536];
let mut remaining = 0usize;
loop {
let read = reader.read(&mut chunk)?;
if read == 0 {
break;
}
remaining += memchr::memchr_iter(b'\n', &chunk[..read]).count();
}
crate::best_effort::best_effort(
writeln!(
std::io::stderr().lock(),
"atomwrite: {} ({} bytes) has {} lines; showing {} up to {cap} bytes, next: --lines {}:{} (--all for everything)",
path.display(),
total_bytes,
prefixed + remaining,
emitted,
emitted + 1,
prefixed + remaining
),
"write read truncation notice on stderr",
);
}
written.map_err(map_raw_io)
}
struct SelectedStats {
total_lines: u64,
selected_lines: u64,
payload_indices: Vec<usize>,
truncated: bool,
}
fn stream_selected_into(
path: &std::path::Path,
args: &ReadArgs,
out: &mut impl Write,
cap: usize,
verb: &str,
) -> Result<SelectedStats> {
if cap == usize::MAX && !args.lines.is_empty() {
let total = count_lines_streaming(path)?;
ensure_ranges_select(path, total, args)?;
}
drop(open_raw_reader(path, verb)?);
let number = numbering_enabled(args);
let mut selector = LineSelector::new(args);
let mut payload: Vec<u8> = Vec::new();
let mut payload_indices: Vec<usize> = Vec::new();
let mut truncated = false;
let mut selected_lines: u64 = 0;
let mut io_error: Option<std::io::Error> = None;
let mut previous: Option<usize> = None;
let mut emit = |index: usize, raw: &[u8]| {
if io_error.is_some() {
return;
}
selected_lines += 1;
let gap = previous.is_some_and(|prev| index != prev + 1);
previous = Some(index);
if cap == usize::MAX {
if gap && let Err(error) = out.write_all(b"--\n") {
io_error = Some(error);
return;
}
if let Err(error) = write_numbered_raw(out, index, raw, number) {
io_error = Some(error);
}
return;
}
if truncated {
return;
}
let separator = usize::from(gap) * 3;
let mut chunk = Vec::with_capacity(separator + raw.len() + 16);
if gap {
chunk.extend_from_slice(b"--\n");
}
chunk.extend_from_slice(&numbered_chunk(index, raw, number));
if payload.len() + chunk.len() <= cap {
payload.extend_from_slice(&chunk);
payload_indices.push(index);
} else {
let remaining = cap - payload.len();
let cut = memchr::memrchr(b'\n', &chunk[..chunk.len().min(remaining)])
.map(|position| position + 1)
.unwrap_or(0);
payload.extend_from_slice(&chunk[..cut]);
if cut > separator {
payload_indices.push(index);
}
truncated = true;
}
};
let total = visit_raw_lines(path, |index, line| selector.push(index, line, &mut emit))?;
selector.finish(&mut emit);
if let Some(error) = io_error {
return Err(map_raw_io(error));
}
ensure_ranges_select(path, total, args)?;
if cap != usize::MAX {
out.write_all(&payload).map_err(map_raw_io)?;
}
Ok(SelectedStats {
total_lines: total,
selected_lines,
payload_indices,
truncated,
})
}
fn write_numbered_raw(
out: &mut impl Write,
index: usize,
raw: &[u8],
number: bool,
) -> std::io::Result<()> {
if number {
write!(out, "{}:", index + 1)?;
}
out.write_all(raw)?;
if number && !raw.ends_with(b"\n") {
out.write_all(b"\n")?;
}
Ok(())
}
fn numbered_chunk(index: usize, raw: &[u8], number: bool) -> Vec<u8> {
let mut chunk = Vec::with_capacity(raw.len() + 16);
if number {
chunk.extend_from_slice(format!("{}:", index + 1).as_bytes());
}
chunk.extend_from_slice(raw);
if number && !raw.ends_with(b"\n") {
chunk.push(b'\n');
}
chunk
}
fn write_filtered_notice(path: &std::path::Path, stats: &SelectedStats, cap: usize) {
let emitted = stats.payload_indices.len().max(1);
let last = stats.payload_indices.last().copied().unwrap_or(0);
crate::best_effort::best_effort(
writeln!(
std::io::stderr().lock(),
"atomwrite: {}: showing {emitted} of {} selected lines up to {cap} bytes; next: --lines {}:{} (use --all for everything)",
path.display(),
stats.selected_lines,
last.saturating_add(2),
stats.total_lines
),
"write read truncation notice on stderr",
);
}
fn map_raw_io(error: std::io::Error) -> anyhow::Error {
if error.kind() == std::io::ErrorKind::BrokenPipe {
crate::error::AtomwriteError::BrokenPipe.into()
} else {
anyhow::Error::from(error)
}
}
fn copy_raw_stream(reader: &mut impl std::io::Read, out: &mut impl Write) -> Result<()> {
let mut buf = vec![0u8; crate::constants::RAW_COPY_BUF_BYTES];
loop {
let read = reader.read(&mut buf)?;
if read == 0 {
return Ok(());
}
out.write_all(&buf[..read]).map_err(map_raw_io)?;
}
}
fn stream_filtered_targets(
targets: &ResolvedTargets,
args: &ReadArgs,
_global: &GlobalArgs,
workspace: &std::path::Path,
writer: &mut NdjsonWriter<impl Write>,
verb: &str,
) -> Result<()> {
writer.flush()?;
let mut out = std::io::stdout().lock();
let header = targets.paths.len() > 1;
let cap = if args.all {
usize::MAX
} else {
crate::config::read_max_output_bytes()
};
let mut first_error: Option<anyhow::Error> = None;
for raw_path in &targets.paths {
let path = match crate::path_safety::validate_path(raw_path, workspace) {
Ok(path) => path,
Err(error) => {
crate::best_effort::best_effort(
writeln!(
std::io::stderr().lock(),
"atomwrite: {}: {error}",
raw_path.display()
),
"report rejected target on stderr",
);
first_error.get_or_insert(error);
continue;
}
};
if !path.exists() {
let error = AtomwriteError::NotFound { path: path.clone() };
crate::best_effort::best_effort(
writeln!(
std::io::stderr().lock(),
"atomwrite: {}: not found",
path.display()
),
"report missing target on stderr",
);
first_error.get_or_insert(error.into());
continue;
}
open_raw_reader(&path, verb)?;
if header {
out.write_all(format!("==> {} <==\n", path.display()).as_bytes())
.map_err(map_raw_io)?;
}
match stream_selected_into(&path, args, &mut out, cap, verb) {
Ok(stats) => {
if cap != usize::MAX && stats.truncated {
write_filtered_notice(&path, &stats, cap);
}
}
Err(error) => {
let fatal = error
.downcast_ref::<AtomwriteError>()
.is_some_and(|failure| {
matches!(
failure,
AtomwriteError::InvalidInput { .. }
| AtomwriteError::ChecksumVerifyFailed { .. }
)
});
if fatal {
return Err(error);
}
crate::best_effort::best_effort(
writeln!(
std::io::stderr().lock(),
"atomwrite: {}: {error}",
path.display()
),
"report unreadable target on stderr",
);
first_error.get_or_insert(error);
}
}
}
out.flush().map_err(map_raw_io)?;
match first_error {
Some(error) => Err(error),
None => Ok(()),
}
}
fn stream_raw_targets(
targets: &ResolvedTargets,
global: &GlobalArgs,
workspace: &std::path::Path,
writer: &mut NdjsonWriter<impl Write>,
) -> Result<()> {
writer.flush()?;
let max_size = global.effective_max_filesize();
let shutdown = crate::signal::get_or_install_handlers()?;
let mut cancel = crate::signal::CancelCheck::for_bytes(shutdown.flag());
let mut out = std::io::stdout().lock();
let mut buf = vec![0u8; crate::constants::RAW_COPY_BUF_BYTES];
let mut first_error: Option<AtomwriteError> = None;
let mut bytes_total: u64 = 0;
for path in &targets.paths {
let checked = match crate::path_safety::validate_path(path, workspace) {
Ok(checked) => checked,
Err(e) => {
let failure = e.downcast::<AtomwriteError>().unwrap_or_else(|other| {
AtomwriteError::InternalError {
reason: format!("{other:#}"),
}
});
record_failure(failure, &mut first_error);
continue;
}
};
match copy_one_target(
&checked,
&mut out,
&mut buf,
max_size,
&mut cancel,
&mut bytes_total,
) {
Ok(()) => {}
Err(e) if is_fatal(&e) => return Err(e.into()),
Err(e) => record_failure(e, &mut first_error),
}
}
for pattern in &targets.empty_patterns {
record_failure(
AtomwriteError::GlobNoMatches {
pattern: pattern.clone(),
},
&mut first_error,
);
}
out.flush()
.map_err(|e| write_error_at(std::path::Path::new(""), e))?;
tracing::debug!(
targets = targets.paths.len(),
bytes = bytes_total,
"read raw concatenation complete"
);
match first_error {
Some(e) => Err(anyhow::Error::from(e).context(crate::output::VerdictPublished)),
None => Ok(()),
}
}
fn record_failure(e: AtomwriteError, first: &mut Option<AtomwriteError>) {
report_target_error(&e);
if first.is_none() {
*first = Some(e);
}
}
fn is_fatal(e: &AtomwriteError) -> bool {
matches!(e, AtomwriteError::BrokenPipe) || crate::signal::is_global_shutdown()
}
fn copy_one_target(
path: &std::path::Path,
out: &mut impl Write,
buf: &mut [u8],
max_size: u64,
cancel: &mut crate::signal::CancelCheck,
bytes_total: &mut u64,
) -> std::result::Result<(), AtomwriteError> {
use std::io::Read;
let mut file = match fs::File::open(path) {
Ok(file) => file,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(AtomwriteError::NotFound {
path: path.to_path_buf(),
});
}
Err(e) => return Err(AtomwriteError::from_io_at(path, e)),
};
let size = file
.metadata()
.map_err(|e| AtomwriteError::from_io_at(path, e))?
.len();
if max_size > 0 && size > max_size {
return Err(AtomwriteError::FileTooLarge {
path: path.to_path_buf(),
size,
max_size,
});
}
loop {
let read = file
.read(buf)
.map_err(|e| AtomwriteError::from_io_at(path, e))?;
if read == 0 {
return Ok(());
}
cancel.advance(read as u64)?;
out.write_all(&buf[..read])
.map_err(|e| write_error_at(path, e))?;
*bytes_total += read as u64;
}
}
fn write_error_at(path: &std::path::Path, e: std::io::Error) -> AtomwriteError {
if e.kind() == std::io::ErrorKind::BrokenPipe {
AtomwriteError::BrokenPipe
} else {
AtomwriteError::from_io_at(path, e)
}
}
fn report_target_error(e: &AtomwriteError) {
crate::best_effort::best_effort(
writeln!(std::io::stderr().lock(), "atomwrite: {e}"),
"report read batch error on stderr",
);
}
fn detect_binary_prefix(path: &std::path::Path) -> Result<bool> {
use std::io::Read;
let mut file =
fs::File::open(path).with_context(|| format!("cannot open {}", path.display()))?;
let mut head = [0u8; crate::constants::BINARY_DETECT_SIZE];
let read = file
.read(&mut head)
.with_context(|| format!("cannot read {}", path.display()))?;
Ok(binary_detect::is_binary(&head[..read]))
}
fn render_raw_lines(data: &[u8], args: &ReadArgs) -> (Vec<u8>, Vec<(usize, usize)>, usize) {
let ranges = line_spans(data);
let total = ranges.len();
let indices = select_line_indices(&ranges, data, args, total);
let numbered = numbering_enabled(args);
let mut out = Vec::new();
let mut emitted = Vec::with_capacity(indices.len());
let mut previous: Option<usize> = None;
for &index in &indices {
if let Some(prev) = previous
&& index != prev + 1
{
out.extend_from_slice(b"--\n");
}
let (start, end) = ranges[index];
if numbered {
out.extend_from_slice(format!("{}:", index + 1).as_bytes());
}
out.extend_from_slice(&data[start..end]);
if numbered && !out.ends_with(b"\n") {
out.push(b'\n');
}
emitted.push((index + 1, out.len()));
previous = Some(index);
}
(out, emitted, total)
}
fn select_line_indices(
ranges: &[(usize, usize)],
data: &[u8],
args: &ReadArgs,
total: usize,
) -> Vec<usize> {
let mut keep = vec![false; total];
let mut filtered = false;
if args.enclosing {
} else if !args.lines.is_empty() {
filtered = true;
for raw in &args.lines {
for part in raw.split(',') {
if let Some((start, end)) = parse_range_str(part.trim(), total) {
for slot in keep.iter_mut().take(end).skip(start) {
*slot = true;
}
}
}
}
} else if let Some(line_num) = args.line {
filtered = true;
let idx = line_num.saturating_sub(1);
if idx < total {
let ctx = args.context;
let start = idx.saturating_sub(ctx);
let end = (idx + ctx + 1).min(total);
for slot in keep.iter_mut().take(end).skip(start) {
*slot = true;
}
}
} else if let Some(n) = args.head {
filtered = true;
for slot in keep.iter_mut().take(n.min(total)) {
*slot = true;
}
} else if let Some(n) = args.tail {
filtered = true;
for slot in keep.iter_mut().skip(total.saturating_sub(n)) {
*slot = true;
}
} else if let Some(pattern) = &args.grep {
filtered = true;
if let Ok(re) = regex::Regex::new(pattern) {
for (index, (start, end)) in ranges.iter().enumerate() {
if re.is_match(&String::from_utf8_lossy(trim_line_ending(
&data[*start..*end],
))) {
let lo = index.saturating_sub(args.context);
let hi = (index + args.context + 1).min(total);
for slot in keep.iter_mut().take(hi).skip(lo) {
*slot = true;
}
}
}
}
}
let mut out = Vec::new();
for (index, (start, end)) in ranges.iter().enumerate() {
if filtered && !keep[index] {
continue;
}
if args.no_comments && is_comment_or_blank(&data[*start..*end]) {
continue;
}
out.push(index);
}
out
}
fn is_comment_or_blank(line: &[u8]) -> bool {
let text = String::from_utf8_lossy(line);
let trimmed = text.trim();
trimmed.is_empty()
|| trimmed.starts_with("//")
|| trimmed.starts_with('#')
|| trimmed.starts_with("--")
|| trimmed.starts_with("/*")
|| trimmed.starts_with('*')
}
fn align_raw_payload<'a>(path: &std::path::Path, payload: &'a [u8]) -> &'a [u8] {
let Ok(prefix) = std::str::from_utf8(payload) else {
return payload;
};
let last_line = payload.iter().filter(|b| **b == b'\n').count();
let Some(end_line) = align_cut_to_outline(path, prefix, last_line) else {
return payload;
};
if end_line == 0 || end_line >= last_line {
return payload;
}
let mut seen = 0usize;
for (index, byte) in payload.iter().enumerate() {
if *byte == b'\n' {
seen += 1;
if seen == end_line {
return &payload[..=index];
}
}
}
payload
}
fn cap_lines(payload: &[u8], cap: usize) -> (&[u8], bool) {
if payload.len() <= cap {
return (payload, false);
}
let cut = memchr::memrchr(b'\n', &payload[..cap])
.map(|index| index + 1)
.unwrap_or(0);
(&payload[..cut], true)
}
fn line_spans(data: &[u8]) -> Vec<(usize, usize)> {
let mut spans = Vec::new();
let mut start = 0;
for (index, byte) in data.iter().enumerate() {
if *byte == b'\n' {
spans.push((start, index + 1));
start = index + 1;
}
}
if start < data.len() {
spans.push((start, data.len()));
}
spans
}
fn parse_range_str(s: &str, total: usize) -> Option<(usize, usize)> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 2 {
return None;
}
let start = parts[0].parse::<usize>().ok()?.saturating_sub(1).min(total);
let end = parts[1].parse::<usize>().ok()?.min(total);
if start >= end {
return Some((0, 0));
}
Some((start, end))
}
fn parse_line_range(args: &ReadArgs, total: u64) -> Option<LineRange> {
let mut merged: Option<(usize, usize)> = None;
for raw in &args.lines {
for part in raw.split(',') {
if let Some((start, end)) = parse_range_str(part.trim(), total as usize) {
merged = Some(match merged {
Some((lo, hi)) => (lo.min(start), hi.max(end)),
None => (start, end),
});
}
}
}
if let Some((start, end)) = merged {
return Some(LineRange {
start: start + 1,
end,
});
}
if let Some(line_num) = args.line {
let ctx = args.context;
let start = line_num.saturating_sub(ctx);
let end = (line_num + ctx).min(total as usize);
return Some(LineRange { start, end });
}
None
}
fn format_permissions(metadata: &fs::Metadata) -> String {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
format!(
"0o{:o}",
metadata.permissions().mode() & crate::constants::UNIX_MODE_PERMISSION_MASK
)
}
#[cfg(not(unix))]
{
if metadata.permissions().readonly() {
"readonly".into()
} else {
"readwrite".into()
}
}
}
fn format_modified(metadata: &fs::Metadata) -> String {
match metadata.modified() {
Ok(time) => {
let secs = time
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let (year, month, day, hour, min, sec) = crate::atomic::epoch_to_utc(secs);
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}Z")
}
Err(_) => "unknown".into(),
}
}
include!("read_globs.inc.rs");
include!("read_lines.inc.rs");