2026-10-14 00:01:03 +03:00
/* tools.cpp — bash/read/write tools and oversized result offloading. */
#include "bantam.hpp"
2026-10-14 15:30:12 +03:00
#include <algorithm>
2026-10-14 00:01:03 +03:00
#include <array>
#include <cerrno>
2026-10-14 08:36:39 +03:00
#include <cstdio>
#include <cstdlib>
#include <cstring>
2026-10-14 00:01:03 +03:00
#include <dirent.h>
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
/* ----------------------------- shell ---------------------------------- */
2026-10-14 10:58:08 +03:00
/* Resolve the Bourne-again shell from the system PATH. Returns a static
* buffer holding the absolute path to `bash`, or "/bin/sh" when bash cannot
* be located anywhere on PATH. Never returns a hardcoded bash path. */
static const char * resolve_shell () {
static char buf [ 4096 ];
2026-10-14 15:30:12 +03:00
static const char * cached = nullptr ;
if ( cached ) return cached ;
2026-10-14 10:58:08 +03:00
const char * path = getenv ( "PATH" );
if ( path && * path ) {
std :: string p ( path );
size_t start = 0 ;
while ( start <= p . size ()) {
size_t end = p . find ( ':' , start );
std :: string dir = ( end == std :: string :: npos ) ? p . substr ( start )
: p . substr ( start , end - start );
if ( ! dir . empty () && dir . back () != '/' ) dir += '/' ;
std :: string cand = dir + "bash" ;
struct stat st ;
if ( stat ( cand . c_str (), & st ) == 0 && S_ISREG ( st . st_mode ) &&
( st . st_mode & ( S_IXUSR | S_IXGRP | S_IXOTH )) != 0 ) {
snprintf ( buf , sizeof buf , "%s" , cand . c_str ());
2026-10-14 15:30:12 +03:00
cached = buf ;
return cached ;
2026-10-14 10:58:08 +03:00
}
if ( end == std :: string :: npos ) break ;
start = end + 1 ;
}
}
2026-10-14 15:30:12 +03:00
cached = "/bin/sh" ;
return cached ;
2026-10-14 10:58:08 +03:00
}
2026-10-14 00:01:03 +03:00
std :: string shell_with_workdir ( std :: string_view cmd , std :: string_view workdir ,
int timeout , volatile int * interrupted ) {
std :: string fcmd = filter_text ( cmd );
if ( timeout <= 0 ) timeout = 120 ;
2026-10-14 15:30:12 +03:00
const char * sh = resolve_shell ();
2026-10-14 00:01:03 +03:00
int pipefd [ 2 ];
if ( pipe ( pipefd ) != 0 ) return "[tool error: pipe failed]" ;
pid_t pid = fork ();
if ( pid < 0 ) {
close ( pipefd [ 0 ]);
close ( pipefd [ 1 ]);
return "[tool error: fork failed]" ;
}
if ( pid == 0 ) {
2026-10-14 15:30:12 +03:00
/* child: create a dedicated process group so timeout/SIGINT kills the
* entire process tree spawned by the shell. */
setpgid ( 0 , 0 );
2026-10-14 00:01:03 +03:00
dup2 ( pipefd [ 1 ], STDOUT_FILENO );
dup2 ( pipefd [ 1 ], STDERR_FILENO );
close ( pipefd [ 0 ]);
close ( pipefd [ 1 ]);
if ( ! workdir . empty ()) chdir ( std :: string ( workdir ). c_str ());
2026-10-14 10:58:08 +03:00
const char * shname = ( strrchr ( sh , '/' ) ? strrchr ( sh , '/' ) + 1 : sh );
execl ( sh , shname , "-c" , fcmd . c_str (), ( char * ) nullptr );
2026-10-14 00:01:03 +03:00
_exit ( 127 );
}
close ( pipefd [ 1 ]);
int flags = fcntl ( pipefd [ 0 ], F_GETFL , 0 );
fcntl ( pipefd [ 0 ], F_SETFL , flags | O_NONBLOCK );
std :: string out ;
struct pollfd pfd ; pfd . fd = pipefd [ 0 ]; pfd . events = POLLIN ;
time_t deadline = time ( nullptr ) + timeout ;
bool killed = false ;
while ( true ) {
if ( * interrupted ) break ;
int remain = ( int )( deadline - time ( nullptr ));
if ( remain <= 0 ) { killed = true ; break ; }
2026-10-14 15:30:12 +03:00
int wait_ms = remain * 1000 ;
if ( wait_ms > 200 ) wait_ms = 200 ;
int pr = poll ( & pfd , 1 , wait_ms );
2026-10-14 00:01:03 +03:00
if ( pr < 0 ) { if ( errno == EINTR ) continue ; break ; }
2026-10-14 15:30:12 +03:00
if ( pr == 0 ) continue ;
2026-10-14 00:01:03 +03:00
char buf [ 4096 ];
ssize_t n = read ( pipefd [ 0 ], buf , sizeof buf );
if ( n > 0 ) out . append ( buf , ( size_t ) n );
else if ( n == 0 ) break ; /* EOF: child exited */
else if ( errno == EAGAIN || errno == EWOULDBLOCK ) continue ;
else break ;
}
if ( * interrupted || killed ) {
2026-10-14 15:30:12 +03:00
kill ( - pid , SIGKILL );
2026-10-14 00:01:03 +03:00
kill ( pid , SIGKILL );
/* drain remaining output briefly */
struct pollfd dpfd ; dpfd . fd = pipefd [ 0 ]; dpfd . events = POLLIN ;
for ( int i = 0 ; i < 20 ; i ++ ) {
int pr = poll ( & dpfd , 1 , 50 );
if ( pr <= 0 ) break ;
char buf [ 4096 ];
ssize_t n = read ( pipefd [ 0 ], buf , sizeof buf );
if ( n > 0 ) out . append ( buf , ( size_t ) n );
else break ;
}
}
int status = 0 ;
while ( waitpid ( pid , & status , 0 ) < 0 && errno == EINTR ) {}
close ( pipefd [ 0 ]);
std :: string filtered = filter_text ( out );
/* trim trailing whitespace */
while ( ! filtered . empty () &&
( filtered . back () == '\n' || filtered . back () == '\r' ||
filtered . back () == ' ' || filtered . back () == '\t' ))
filtered . pop_back ();
if ( * interrupted ) {
return filtered + " \n\n exit: -1" ;
}
if ( killed ) {
return sfmt ( "%s \n\n [shell timeout after %ds] \n exit: -1" , filtered . c_str (), timeout );
}
int code = 0 ;
if ( WIFEXITED ( status )) code = WEXITSTATUS ( status );
else if ( WIFSIGNALED ( status )) code = - 1 ;
return sfmt ( "%s \n\n exit: %d" , filtered . c_str (), code );
}
/* ----------------------------- read ----------------------------------- */
std :: string read_file_or_dir ( std :: string_view path , int offset , int limit , int * style ) {
std :: string p ( path );
/* trim */
size_t s = 0 ;
while ( s < p . size () && ( p [ s ] == ' ' || p [ s ] == '\t' )) s ++ ;
p = p . substr ( s );
if ( p . empty ()) { * style = 31 ; return "[tool error: read requires 'filePath' parameter]" ; }
struct stat st ;
if ( stat ( p . c_str (), & st ) != 0 ) {
* style = 31 ;
return sfmt ( "[tool error: read %s: %s]" , p . c_str (), strerror ( errno ));
}
if ( S_ISDIR ( st . st_mode )) {
2026-10-14 15:30:12 +03:00
struct DirEnt {
std :: string name ;
int is_dir ; /* -1 = unknown, 0 = non-dir, 1 = dir */
};
std :: vector < DirEnt > entries ;
2026-10-14 00:01:03 +03:00
{
DIR * dir = opendir ( p . c_str ());
if ( ! dir ) { * style = 31 ; return "[tool error: read (directory listing failed)]" ; }
struct dirent * de ;
while (( de = readdir ( dir ))) {
2026-10-14 15:30:12 +03:00
if ( de -> d_name [ 0 ] == '.' &&
( de -> d_name [ 1 ] == '\0' || ( de -> d_name [ 1 ] == '.' && de -> d_name [ 2 ] == '\0' )))
continue ;
int isdir = - 1 ;
#if defined(DT_DIR) && defined(DT_UNKNOWN) && defined(DT_LNK)
if ( de -> d_type != DT_UNKNOWN && de -> d_type != DT_LNK ) {
isdir = ( de -> d_type == DT_DIR ) ? 1 : 0 ;
}
#endif
entries . push_back ( DirEnt { de -> d_name , isdir });
2026-10-14 00:01:03 +03:00
}
closedir ( dir );
}
2026-10-14 15:30:12 +03:00
std :: sort ( entries . begin (), entries . end (), []( const DirEnt & a , const DirEnt & b ) {
return a . name < b . name ;
});
2026-10-14 00:01:03 +03:00
std :: string b ;
2026-10-14 15:30:12 +03:00
for ( auto & ent : entries ) {
bool isdir = ( ent . is_dir == 1 );
if ( ent . is_dir < 0 ) {
std :: string full = p + "/" + ent . name ;
struct stat ds ;
isdir = ( stat ( full . c_str (), & ds ) == 0 && S_ISDIR ( ds . st_mode ));
}
b += ent . name ;
2026-10-14 00:01:03 +03:00
if ( isdir ) b += '/' ;
b += '\n' ;
}
if ( ! b . empty ()) b . pop_back (); /* trailing newline */
* style = 2 ;
return b ;
}
/* regular file */
2026-10-14 08:36:39 +03:00
FILE * f = std :: fopen ( p . c_str (), "rb" );
2026-10-14 00:01:03 +03:00
if ( ! f ) { * style = 31 ; return sfmt ( "[tool error: read %s: %s]" , p . c_str (), strerror ( errno )); }
if ( offset < 1 ) offset = 1 ;
if ( limit <= 0 ) limit = 2000 ;
std :: string b ;
2026-10-14 08:36:39 +03:00
char * line_buf = nullptr ;
size_t cap = 0 ;
ssize_t n ;
2026-10-14 00:01:03 +03:00
int line_num = 0 , read_count = 0 ;
2026-10-14 08:36:39 +03:00
while (( n = getline ( & line_buf , & cap , f )) >= 0 ) {
2026-10-14 00:01:03 +03:00
line_num ++ ;
if ( line_num < offset ) continue ;
read_count ++ ;
2026-10-14 08:36:39 +03:00
size_t len = ( size_t ) n ;
while ( len > 0 && ( line_buf [ len - 1 ] == '\n' || line_buf [ len - 1 ] == '\r' )) len -- ;
std :: string_view lview = utf8_truncate ( std :: string_view ( line_buf , len ), 2000 );
2026-10-14 00:01:03 +03:00
str_appendf ( b , "%d: " , line_num );
b . append ( lview );
b += '\n' ;
if ( read_count >= limit ) break ;
}
2026-10-14 08:36:39 +03:00
free ( line_buf );
std :: fclose ( f );
2026-10-14 00:01:03 +03:00
if ( ! b . empty ()) b . pop_back (); /* trailing newline */
* style = 2 ;
return b ;
}
/* ----------------------------- write ---------------------------------- */
2026-10-14 10:58:08 +03:00
/* Convert a 1-indexed line number to the byte offset of the start of that
* line within `data`. Line 1 begins at byte 0; a line number past the end of
* the file maps to data.size() (i.e. append at end of file). */
static size_t line_to_byte_offset ( std :: string_view data , long line_no ) {
if ( line_no <= 1 ) return 0 ;
size_t pos = 0 ;
long cur = 1 ;
while ( pos < data . size ()) {
if ( data [ pos ] == '\n' ) {
cur ++ ;
if ( cur == line_no ) return pos + 1 ;
}
pos ++ ;
}
return data . size ();
}
/* Byte length of `nlines` complete lines beginning at byte offset `start`.
* Counts the newlines terminating each line and stops at end of data. */
static size_t lines_to_byte_len ( std :: string_view data , size_t start , long nlines ) {
if ( nlines <= 0 || start > data . size ()) return 0 ;
size_t pos = start ;
long consumed = 0 ;
while ( pos < data . size () && consumed < nlines ) {
if ( data [ pos ] == '\n' ) consumed ++ ;
pos ++ ;
}
return pos - start ;
}
std :: string write_file ( std :: string_view path , long line_offset , long byte_offset ,
long del_lines , long del_bytes ,
2026-10-14 00:01:03 +03:00
std :: string_view content , int * err ) {
* err = 0 ;
std :: string p ( path );
size_t s = 0 ;
while ( s < p . size () && ( p [ s ] == ' ' || p [ s ] == '\t' )) s ++ ;
p = p . substr ( s );
if ( p . empty ()) { * err = 1 ; return "[tool error: path is required]" ; }
std :: string data ;
struct stat st ;
if ( stat ( p . c_str (), & st ) == 0 ) {
if ( S_ISDIR ( st . st_mode )) {
* err = 1 ;
return sfmt ( "[tool error: write %s: Is a directory]" , p . c_str ());
}
2026-10-14 08:36:39 +03:00
if ( ! read_file_str ( p , data )) {
* err = 1 ;
return sfmt ( "[tool error: write %s: %s]" , p . c_str (), strerror ( errno ));
}
2026-10-14 00:01:03 +03:00
} else {
auto slash = p . find_last_of ( '/' );
if ( slash != std :: string :: npos ) mkdir_p ( std :: string_view ( p ). substr ( 0 , slash ));
else mkdir_p ( "." );
}
2026-10-14 10:58:08 +03:00
/* Resolve insertion byte offset. Line-based takes precedence over byte-based
* ("first and foremost line-based writing"); if neither is given the whole
* file is overwritten by the caller. */
size_t ins_off = 0 ;
if ( line_offset >= 0 ) {
ins_off = line_to_byte_offset ( data , line_offset );
} else if ( byte_offset >= 0 ) {
ins_off = ( size_t ) byte_offset ;
if ( ins_off > data . size ()) data . resize ( ins_off , '\0' );
} else {
ins_off = 0 ;
2026-10-14 00:01:03 +03:00
}
2026-10-14 10:58:08 +03:00
/* Resolve deletion span. Line-based takes precedence over byte-based. */
size_t del_len = 0 ;
if ( del_lines >= 0 ) {
del_len = lines_to_byte_len ( data , ins_off , del_lines );
} else if ( del_bytes >= 0 ) {
del_len = ( size_t ) del_bytes ;
if ( ins_off + del_len > data . size ())
del_len = ( ins_off < data . size ()) ? ( data . size () - ins_off ) : 0 ;
}
size_t end_del = ins_off + del_len ;
size_t prefix_len = ins_off ;
2026-10-14 00:01:03 +03:00
size_t suffix_len = ( end_del < data . size ()) ? ( data . size () - end_del ) : 0 ;
size_t clen = content . size ();
size_t newlen = prefix_len + clen + suffix_len ;
std :: string nd ;
nd . reserve ( newlen );
nd . append ( data , 0 , prefix_len );
nd . append ( content );
if ( suffix_len ) nd . append ( data , end_del , suffix_len );
2026-10-14 12:19:11 +03:00
/* Keep a single trailing newline for line-based / whole-file text writes.
* Byte-based edits are exact byte operations and must not be mutated with an
* injected newline, so the guard is skipped whenever a byte offset or byte
* deletion is actually in effect. */
bool byte_based = ( line_offset < 0 && byte_offset >= 0 ) ||
( del_lines < 0 && del_bytes >= 0 );
if ( ! byte_based && ! nd . empty () && nd . back () != '\n' ) nd . push_back ( '\n' );
2026-10-14 08:36:39 +03:00
if ( ! write_file_str ( p , nd )) {
* err = 1 ;
return sfmt ( "[tool error: write %s: %s]" , p . c_str (), strerror ( errno ));
2026-10-14 00:01:03 +03:00
}
return sfmt ( "Successfully wrote %zu bytes to %s" , clen , p . c_str ());
}
/* ----------------------------- offload -------------------------------- */
std :: string offload_tool_result ( std :: string_view res , int max_res ,
2026-10-14 10:58:08 +03:00
std :: vector < std :: string > & tmps ) {
2026-10-14 00:01:03 +03:00
if ( max_res <= 0 ) max_res = 65536 ;
if ( res . size () <= ( size_t ) max_res ) return std :: string ( res );
std :: string dir = tool_res_dir ();
std :: string tpl = dir + "/toolres-XXXXXX" ;
int fd = mkstemp ( tpl . data ());
if ( fd < 0 ) return std :: string ( res );
size_t written = 0 ;
bool write_ok = true ;
while ( written < res . size ()) {
ssize_t w = write ( fd , res . data () + written , res . size () - written );
if ( w < 0 ) {
if ( errno == EINTR ) continue ;
write_ok = false ;
break ;
}
written += ( size_t ) w ;
}
close ( fd );
if ( ! write_ok ) { unlink ( tpl . c_str ()); return std :: string ( res ); }
tmps . push_back ( tpl );
return sfmt ( "[tool result too large for context: %zu bytes (limit %d) . The full output was saved to % s . Read it with the read tool ; to read it partially , use offset and limit . The file is % zu bytes long .] ",
res . size (), max_res , tpl . c_str (), res . size ());
}
void cleanup_temps ( std :: vector < std :: string > & tmps ) {
for ( auto & t : tmps ) unlink ( t . c_str ());
tmps . clear ();
}