--- parser3/src/classes/file.C 2017/05/17 14:22:11 1.263 +++ parser3/src/classes/file.C 2024/11/06 21:56:23 1.286 @@ -1,14 +1,15 @@ /** @file Parser: @b file parser class. - Copyright (c) 2001-2017 Art. Lebedev Studio (http://www.artlebedev.com) - Author: Alexandr Petrosian (http://paf.design.ru) + Copyright (c) 2001-2024 Art. Lebedev Studio (http://www.artlebedev.com) + Authors: Konstantin Morshnev , Alexandr Petrosian */ #include "pa_config_includes.h" #include "classes.h" #include "pa_vmethod_frame.h" +#include "pa_base64.h" #include "pa_request.h" #include "pa_vfile.h" @@ -18,6 +19,7 @@ #include "pa_vdate.h" #include "pa_dir.h" #include "pa_vtable.h" +#include "pa_varray.h" #include "pa_charset.h" #include "pa_charsets.h" #include "pa_sql_connection.h" @@ -25,7 +27,7 @@ #include "pa_vregex.h" #include "pa_version.h" -volatile const char * IDENT_FILE_C="$Id: file.C,v 1.263 2017/05/17 14:22:11 moko Exp $"; +volatile const char * IDENT_FILE_C="$Id: file.C,v 1.286 2024/11/06 21:56:23 moko Exp $"; // defines @@ -142,7 +144,7 @@ static void _save(Request& r, MethodPara } // save - GET_SELF(r, VFile).save(r.charsets, r.absolute(vfile_name.as_string()), is_text, asked_charset); + GET_SELF(r, VFile).save(r.charsets, r.full_disk_path(vfile_name.as_string()), is_text, asked_charset); } static void _delete(Request& r, MethodParams& params) { @@ -166,7 +168,7 @@ static void _delete(Request& r, MethodPa } // unlink - file_delete(r.absolute(file_name), fail_on_problem, keep_empty_dirs); + file_delete(r.full_disk_path(file_name), fail_on_problem, keep_empty_dirs); } static void _move(Request& r, MethodParams& params) { @@ -187,8 +189,8 @@ static void _move(Request& r, MethodPara // move file_move( - r.absolute(vfrom_file_name.as_string()), - r.absolute(vto_file_name.as_string()), + r.full_disk_path(vfrom_file_name.as_string()), + r.full_disk_path(vto_file_name.as_string()), keep_empty_dirs); } @@ -217,14 +219,28 @@ static void _copy(Request& r, MethodPara Value& vfrom_file_name=params.as_no_junction(0, "from file name must not be code"); Value& vto_file_name=params.as_no_junction(1, "to file name must not be code"); - String from_spec = r.absolute(vfrom_file_name.as_string()); - const String& to_spec = r.absolute(vto_file_name.as_string()); + bool append=false; + if(params.count()>2) + if(HashStringValue* options=params.as_hash(2)){ + int valid_options=0; + if(Value* vappend=options->get("append")){ + append=r.process(*vappend).as_bool(); + valid_options++; + } + if(valid_options != options->count()) + throw Exception(PARSER_RUNTIME, 0, CALLED_WITH_INVALID_OPTION); + } + + String from_spec = r.full_disk_path(vfrom_file_name.as_string()); + const String& to_spec = r.full_disk_path(vto_file_name.as_string()); file_write_action_under_lock( to_spec, "copy", copy_open_target, - &from_spec); + &from_spec, + false /*as text*/, + append); } static void _load_pass_param( @@ -236,7 +252,7 @@ static void _load_pass_param( static void _load(Request& r, MethodParams& params) { bool as_text=VFile::is_text_mode(params.as_string(0, MODE_MUST_NOT_BE_CODE)); - const String& lfile_name=r.absolute(params.as_string(1, FILE_NAME_MUST_NOT_BE_CODE)); + const String& lfile_name=r.full_disk_path(params.as_string(1, FILE_NAME_MUST_NOT_BE_CODE)); size_t param_index=params.count()-1; Value* param_value=param_index>1?¶ms.as_no_junction(param_index, "file name or options must not be code"):0; @@ -257,22 +273,7 @@ static void _load(Request& r, MethodPara if(!user_file_name) user_file_name=&lfile_name; - size_t offset=0; - size_t limit=0; - - if(options){ - options=new HashStringValue(*options); - if(Value *voffset=(Value *)options->get(sql_offset_name)){ - offset=r.process(*voffset).as_int(); - } - if(Value *vlimit=(Value *)options->get(sql_limit_name)){ - limit=r.process(*vlimit).as_int(); - } - // no check on options count here, see file_read - } - File_read_result file=file_load(r, lfile_name, - as_text, options, true, 0, offset, limit - ); + File_read_result file=file_load(r, lfile_name, as_text, options, true); Value* vcontent_type=0; if(file.headers){ @@ -345,7 +346,7 @@ static void _create(Request& r, MethodPa } if(Value* vcharset_name=options->get(PA_CHARSET_NAME)) { if(to_charset) - throw Exception(PARSER_RUNTIME, 0, "charset option can not be used with to-charset"); + throw Exception(PARSER_RUNTIME, 0, "'charset' option cannot be used together with 'to-charset' option"); to_charset=&pa_charsets.get(vcharset_name->as_string()); valid_options++; } @@ -365,7 +366,7 @@ static void _create(Request& r, MethodPa String::Body body=content_str->cstr_to_string_body_untaint(String::L_AS_IS, r.connection(false), &r.charsets); // explode content, honor tainting changes self.set(true/*tainted*/, is_text, body.cstrm(), body.length(), file_name, vcontent_type, &r); } else { - VFile& fcontent=*vcontent.as_vfile(String::L_AS_IS); // can't be null + VFile& fcontent=*vcontent.as_vfile(); // can't be null if(mode){ self.set(fcontent, &is_text, file_name, vcontent_type, &r); if(is_text && !fcontent.is_text_mode()) @@ -380,7 +381,7 @@ static void _create(Request& r, MethodPa if(is_text) self.transcode(from_charset ? *from_charset : r.charsets.source(), to_charset ? *to_charset : r.charsets.source()); else - throw Exception(PARSER_RUNTIME, 0, "charset options can not be used with binary content"); + throw Exception(PARSER_RUNTIME, 0, "charset options cannot be used with binary content"); } static void _stat(Request& r, MethodParams& params) { @@ -388,7 +389,7 @@ static void _stat(Request& r, MethodPara uint64_t size; time_t atime, mtime, ctime; - file_stat(r.absolute(lfile_name), size, atime, mtime, ctime); + file_stat(r.full_disk_path(lfile_name), size, atime, mtime, ctime); VFile& self=GET_SELF(r, VFile); @@ -465,8 +466,7 @@ static void pass_cgi_header_attribute( } static void append_to_argv(Request& r, ArrayString& argv, const String* str){ - if(!str->is_empty()) - argv+=new String(str->cstr_to_string_body_untaint(String::L_AS_IS, r.connection(false), &r.charsets), String::L_AS_IS); + argv+=new String(str->cstr_to_string_body_untaint(String::L_AS_IS, r.connection(false), &r.charsets), String::L_AS_IS); } /// @todo fix `` in perl - they produced flipping consoles and no output to perl @@ -482,7 +482,7 @@ static void _exec_cgi(Request& r, Method if(param_index>=params.count()) throw Exception(PARSER_RUNTIME, 0, FILE_NAME_MUST_BE_SPECIFIED); - const String& script_name=r.absolute(params.as_string(param_index++, FILE_NAME_MUST_NOT_BE_CODE)); + const String& script_name=r.full_disk_path(params.as_string(param_index++, FILE_NAME_MUST_NOT_BE_CODE)); HashStringString env; #define ECSTR(name, value_cstr) if(value_cstr) env.put(#name, value_cstr); @@ -500,7 +500,7 @@ static void _exec_cgi(Request& r, Method ECSTR(QUERY_STRING, r.request_info.query_string); ECSTR(REQUEST_URI, r.request_info.uri); ECSTR(CONTENT_TYPE, r.request_info.content_type); - ECSTR(CONTENT_LENGTH, format(r.request_info.content_length, "%u")); + ECSTR(CONTENT_LENGTH, pa_uitoa(r.request_info.content_length)); // SCRIPT_* env.put("SCRIPT_NAME", script_name); @@ -528,7 +528,7 @@ static void _exec_cgi(Request& r, Method // untaint stdin in = String::C(sstdin->cstr_to_string_body_untaint(String::L_AS_IS, r.connection(false), &r.charsets)); } else - if(VFile* vfile=static_cast(info.vstdin->as("file"))){ + if(VFile* vfile=dynamic_cast(info.vstdin)){ in = String::C((const char* )vfile->value_ptr(), vfile->value_size()); in_is_text_mode = vfile->is_text_mode(); } else @@ -545,18 +545,31 @@ static void _exec_cgi(Request& r, Method for(size_t i=param_index; icount(); j++) - append_to_argv(r, argv, table->get(j)->get(0)); - } else { - throw Exception(PARSER_RUNTIME, 0, "param must be string or table"); + if(const String *string=param.get_string()){ + append_to_argv(r, argv, string); + } else if(Table* table=param.get_table()){ + for(size_t j=0; jcount(); j++) + append_to_argv(r, argv, table->get(j)->get(0)); + } else if(VArray* array=dynamic_cast(¶m)){ + for(ArrayValue::Iterator i(array->array()); i; i.next()){ + if(i.value()){ + const String *string=i.value()->get_string(); + if(!string) + i.value()->bark("array element is '%s', it does not have string value"); + append_to_argv(r, argv, string); } } + } else { + throw Exception(PARSER_RUNTIME, 0, "param must be string or table or array of strings"); + } + } + + // remove trailing empty arguments for backward compatibility + for(ArrayString::ReverseIterator i(argv); i;){ + if(i.prev()->is_empty()){ // here for correct i.index() + argv.remove(i.index()); + } else { + break; } } } @@ -706,8 +719,8 @@ static void _list(Request& r, MethodPara vfilter=&voption; } if(vfilter) { - if(Value* value=vfilter->as(VREGEX_TYPE)) { - vregex=static_cast(value); + if(VRegex* value=dynamic_cast(vfilter)) { + vregex=value; } else if(vfilter->is_string()) { if(!vfilter->get_string()->trim().is_empty()) { vregex=new VRegex(r.charsets.source(), &vfilter->as_string(), 0/*options*/); @@ -721,7 +734,7 @@ static void _list(Request& r, MethodPara } } - const char* absolute_path_cstr=r.absolute(relative_path.as_string()).taint_cstr(String::L_FILE_SPEC); + const char* absolute_path_cstr=r.full_disk_path(relative_path.as_string()).taint_cstr(String::L_FILE_SPEC); Table::Action_options table_options; Table& table=*new Table(file_list_table_template, table_options); @@ -736,12 +749,12 @@ static void _list(Request& r, MethodPara if(!vregex || vregex->exec(file_name_cstr, file_name_size, ovector, ovector_size)>=0) { Table::element_type row(new ArrayString); *row+=new String(pa_strdup(file_name_cstr, file_name_size), String::L_TAINTED); - *row+=new String(String::Body::Format(ffblk.is_dir(stat) ? 1 : 0), String::L_CLEAN); + *row+=new String(ffblk.is_dir(stat) ? "1" : "0", String::L_CLEAN); if(stat) { *row+=VDouble(ffblk.size()).get_string(); - *row+=new String(String::Body::Format((int)ffblk.c_timestamp()), String::L_CLEAN); - *row+=new String(String::Body::Format((int)ffblk.m_timestamp()), String::L_CLEAN); - *row+=new String(String::Body::Format((int)ffblk.a_timestamp()), String::L_CLEAN); + *row+=new String(pa_uitoa(ffblk.c_timestamp()), String::L_CLEAN); + *row+=new String(pa_uitoa(ffblk.m_timestamp()), String::L_CLEAN); + *row+=new String(pa_uitoa(ffblk.a_timestamp()), String::L_CLEAN); } table+=row; } @@ -765,7 +778,7 @@ static void lock_execute_body(int , void } static void _lock(Request& r, MethodParams& params) { - const String& file_spec=r.absolute(params.as_string(0, FILE_NAME_MUST_BE_STRING)); + const String& file_spec=r.full_disk_path(params.as_string(0, FILE_NAME_MUST_BE_STRING)); Lock_execute_body_info info={ &r, ¶ms.as_junction(1, "body must be code") @@ -800,7 +813,7 @@ static void _find(Request& r, MethodPara file_spec=&r.relative(r.request_info.uri, file_name); // easy way - if(file_exist(r.absolute(*file_spec))) { + if(file_exist(r.full_disk_path(*file_spec))) { r.write(*file_spec); return; } @@ -818,7 +831,7 @@ static void _find(Request& r, MethodPara String test_name; test_name << dirname.mid(0, slash+1); test_name << basename; - if(file_exist(r.absolute(test_name))) { + if(file_exist(r.full_disk_path(test_name))) { r.write(test_name); return; } @@ -917,7 +930,7 @@ static void _fullpath(Request& r, Method result=&file_spec; else { // /some/page.html: ^file:fullpath[a.gif] => /some/a.gif - const String& full_disk_path=r.absolute(file_spec); + const String& full_disk_path=r.full_disk_path(file_spec); size_t document_root_length=strlen(r.request_info.document_root); if(document_root_length>0) { @@ -939,31 +952,37 @@ static void _sql_string(Request& r, Meth #ifndef DOXYGEN class File_sql_event_handlers: public SQL_Driver_query_event_handlers { - const String& statement_string; const char* statement_cstr; int got_columns; int got_cells; + bool got_row; public: String::C value; const String* user_file_name; const String* user_content_type; public: - File_sql_event_handlers( - const String& astatement_string, const char* astatement_cstr): - statement_string(astatement_string), statement_cstr(astatement_cstr), + File_sql_event_handlers(): got_columns(0), got_cells(0), + got_row(false), user_file_name(0), user_content_type(0) {} bool add_column(SQL_Error& error, const char* /*str*/, size_t /*length*/) { if(got_columns++==3) { - error=SQL_Error(PARSER_RUNTIME, "result must contain not more then 3 columns"); + error=SQL_Error("result must contain no more than 3 columns"); return true; } return false; } bool before_rows(SQL_Error& /*error*/ ) { /* ignore */ return false; } - bool add_row(SQL_Error& /*error*/) { /* ignore */ return false; } + bool add_row(SQL_Error& error) { + if(got_row) { + error=SQL_Error("result must contain no more than 1 row"); + return true; + } + got_row=true; + return false; + } bool add_row_cell(SQL_Error& error, const char* str, size_t length) { try { switch(got_cells++) { @@ -979,7 +998,7 @@ public: user_content_type=new String(str, String::L_TAINTED); break; default: - error=SQL_Error(PARSER_RUNTIME, "result must not contain more then one row, three columns"); + error=SQL_Error("result must contain no more than 1 row and 3 columns"); return true; } return false; @@ -996,7 +1015,7 @@ static void _sql(Request& r, MethodParam const String& statement_string=r.process_to_string(statement); const char* statement_cstr=statement_string.untaint_cstr(String::L_SQL, r.connection()); - File_sql_event_handlers handlers(statement_string, statement_cstr); + File_sql_event_handlers handlers; ulong limit=SQL_NO_LIMIT; ulong offset=0; @@ -1025,12 +1044,7 @@ static void _sql(Request& r, MethodParam } - r.connection()->query( - statement_cstr, - 0, 0, - offset, limit, - handlers, - statement_string); + r.connection()->query(statement_cstr, 0, 0, offset, limit, handlers, statement_string); if(!handlers.value.str) throw Exception(PARSER_RUNTIME, 0, "produced no result"); @@ -1042,44 +1056,61 @@ static void _sql(Request& r, MethodParam , &r); } +extern Base64Options base64_encode_options(Request& r, HashStringValue* options); + +Base64Options base64_decode_options(Request& r, HashStringValue* options, VString** vcontent_type) { + Base64Options result; + if(options) { + int valid_options=0; + for(HashStringValue::Iterator i(*options); i; i.next() ) { + String::Body key=i.key(); + Value* value=i.value(); + if(key == "pad") { + result.pad=r.process(*value).as_bool(); + valid_options++; + } else if(key == "strict") { + result.strict=r.process(*value).as_bool(); + valid_options++; + } else if(key == CONTENT_TYPE_NAME) { + *vcontent_type=new VString(value->as_string()); + valid_options++; + } else if(key == "url-safe") { + if(r.process(*value).as_bool()) + result.set_url_safe_abc(); + valid_options++; + } + } + + if(valid_options != options->count()) + throw Exception(PARSER_RUNTIME, 0, CALLED_WITH_INVALID_OPTION); + } + return result; +} + static void _base64(Request& r, MethodParams& params) { bool dynamic=!(&r.get_self() == file_class); if(dynamic) { VFile& self=GET_SELF(r, VFile); - if(params.count()) { + if(params.count()>1 || params.count()==1 && params[0].is_string()) { // decode: // ^file::base64[encoded] // backward // ^file::base64[mode;user-file-name;encoded[;$.content-type[...] $.strict(true|false)]] bool is_text=false; - bool strict=false; - VString* vcontent_type=0; const String* user_file_name=0; + VString* vcontent_type=0; + Base64Options options; + size_t param_index=0; if(params.count() > 1) { if(params.count() < 3) - throw Exception(PARSER_RUNTIME, - 0, - "constructor can not have less then 3 parameters (has %d parameters)", - params.count()); // actually it accepts 1 parameter (backward) + throw Exception(PARSER_RUNTIME, 0, "constructor cannot have less than 3 parameters (has %d parameters)", params.count()); // actually it accepts 1 parameter (backward) is_text=VFile::is_text_mode(params.as_string(0, MODE_MUST_NOT_BE_CODE)); user_file_name=¶ms.as_string(1, FILE_NAME_MUST_BE_STRING); if(params.count() == 4) - if(HashStringValue* options=params.as_hash(3)) { - int valid_options=0; - if(Value* value=options->get(CONTENT_TYPE_NAME)) { - vcontent_type=new VString(value->as_string()); - valid_options++; - } - if(Value* vstrict=options->get(BASE64_STRICT_OPTION_NAME)) { - strict=r.process(*vstrict).as_bool(); - valid_options++; - } - if(valid_options!=options->count()) - throw Exception(PARSER_RUNTIME, 0, CALLED_WITH_INVALID_OPTION); - } + options=base64_decode_options(r, params.as_hash(3), &vcontent_type); param_index=2; } @@ -1087,30 +1118,36 @@ static void _base64(Request& r, MethodPa const char* encoded=params.as_string(param_index, PARAMETER_MUST_BE_STRING).cstr(); char* decoded=0; - size_t length=0; - pa_base64_decode(encoded, strlen(encoded), decoded, length, strict); + size_t length=pa_base64_decode(encoded, strlen(encoded), decoded, options); self.set(true/*tainted*/, is_text, decoded, length, user_file_name, vcontent_type, &r); } else { - // encode: ^f.base64[] - const char* encoded=pa_base64_encode(self.value_ptr(), self.value_size()); - r.write(*new String(encoded, String::L_TAINTED/*once ?param=base64(something) was needed**/)); + // encode: ^f.base64[options] + Base64Options options = base64_encode_options(r, params.count() > 0 ? params.as_hash(0) : NULL); + const char* encoded=pa_base64_encode(self.value_ptr(), self.value_size(), options); + r.write(*new String(encoded, String::L_TAINTED /*once ?param=base64(something) was needed**/ )); } } else { - // encode: ^file:base64[filespec] - const String& file_spec=params.as_string(0, FILE_NAME_MUST_BE_STRING); - const char* encoded=pa_base64_encode(r.absolute(file_spec)); - r.write(*new String(encoded, String::L_TAINTED/*once ?param=base64(something) was needed*/)); + // encode: ^file:base64[filespec[;options]] + if(params.count() > 2) + throw Exception(PARSER_RUNTIME, 0, "accepts maximum 2 parameter(s) (has %d parameters)", params.count()); + + const String& file_spec = params.as_string(0, FILE_NAME_MUST_BE_STRING); + File_read_result data = file_read_binary(r.full_disk_path(file_spec), true /*fail on problem*/); + + Base64Options options = base64_encode_options(r, params.count() > 1 ? params.as_hash(1) : NULL); + const char* encoded = pa_base64_encode(data.str, data.length, options); + r.write(*new String(encoded, String::L_TAINTED /*once ?param=base64(something) was needed*/ )); } } static void _crc32(Request& r, MethodParams& params) { - unsigned long crc32 = 0; + uint crc32 = 0; if(&r.get_self() == file_class) { // ^file:crc32[file-name] if(params.count()) { const String& file_spec=params.as_string(0, FILE_NAME_MUST_BE_STRING); - crc32=pa_crc32(r.absolute(file_spec)); + crc32=pa_crc32(r.full_disk_path(file_spec)); } else { throw Exception(PARSER_RUNTIME, 0, FILE_NAME_MUST_BE_SPECIFIED); } @@ -1119,7 +1156,7 @@ static void _crc32(Request& r, MethodPar VFile& self=GET_SELF(r, VFile); crc32=pa_crc32(self.value_ptr(), self.value_size()); } - r.write(*new VInt(crc32)); + r.write(*new VDouble(crc32)); } @@ -1166,7 +1203,7 @@ static void _md5(Request& r, MethodParam // ^file:md5[file-name] if(params.count()) { const String& file_spec=params.as_string(0, FILE_NAME_MUST_BE_STRING); - md5=pa_md5(r.absolute(file_spec)); + md5=pa_md5(r.full_disk_path(file_spec)); } else { throw Exception(PARSER_RUNTIME, 0, FILE_NAME_MUST_BE_SPECIFIED); } @@ -1266,5 +1303,6 @@ MFile::MFile(): Methoded("file") { add_native_method("md5", Method::CT_ANY, _md5, 0, 1); // ^file:copy[from-file-name;to-file-name] - add_native_method("copy", Method::CT_STATIC, _copy, 2, 2); + // ^file:copy[from-file-name;to-file-name;$.append(false)] + add_native_method("copy", Method::CT_STATIC, _copy, 2, 3); }