2026-10-14 00:01:03 +03:00
/* tools.cpp — bash/read/write tools and oversized result offloading. */
#include "bantam.hpp"
#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 ---------------------------------- */
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 ;
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 ) {
/* child */
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 ());
execl ( "/bin/bash" , "bash" , "-c" , fcmd . c_str (), ( char * ) nullptr );
_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 ; }
int pr = poll ( & pfd , 1 , remain * 1000 );
if ( pr < 0 ) { if ( errno == EINTR ) continue ; break ; }
if ( pr == 0 ) { killed = true ; break ; }
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 ) {
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 )) {
std :: vector < std :: string > raw_names ;
{
DIR * dir = opendir ( p . c_str ());
if ( ! dir ) { * style = 31 ; return "[tool error: read (directory listing failed)]" ; }
struct dirent * de ;
while (( de = readdir ( dir ))) {
std :: string nm = de -> d_name ;
if ( nm == "." || nm == ".." ) continue ;
raw_names . push_back ( std :: move ( nm ));
}
closedir ( dir );
}
2026-10-14 08:36:39 +03:00
for ( size_t i = 1 ; i < raw_names . size (); i ++ ) {
for ( size_t j = i ; j > 0 && raw_names [ j - 1 ] > raw_names [ j ]; j -- ) {
std :: swap ( raw_names [ j - 1 ], raw_names [ j ]);
}
}
2026-10-14 00:01:03 +03:00
std :: string b ;
for ( auto & nm : raw_names ) {
std :: string full = p + "/" + nm ;
struct stat ds ;
bool isdir = ( stat ( full . c_str (), & ds ) == 0 && S_ISDIR ( ds . st_mode ));
b += nm ;
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 ---------------------------------- */
std :: string write_file ( std :: string_view path , int offset , int del_bytes ,
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]" ; }
if ( offset < 0 ) offset = 0 ;
if ( del_bytes < 0 ) del_bytes = 0 ;
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 ( "." );
}
if (( size_t ) offset > data . size ()) {
data . resize (( size_t ) offset , '\0' );
}
size_t end_del = ( size_t ) offset + ( size_t ) del_bytes ;
size_t prefix_len = ( size_t ) offset ;
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 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 ,
std :: vector < std :: string > & tmps ) {
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 ();
}