--- parser3/src/classes/file.C 2004/07/26 10:44:21 1.126 +++ parser3/src/classes/file.C 2007/02/07 15:50:32 1.151 @@ -1,11 +1,11 @@ /** @file Parser: @b file parser class. - Copyright (c) 2001-2004 ArtLebedev Group (http://www.artlebedev.com) + Copyright (c) 2001-2005 ArtLebedev Group (http://www.artlebedev.com) Author: Alexandr Petrosian (http://paf.design.ru) */ -static const char * const IDENT_FILE_C="$Date: 2004/07/26 10:44:21 $"; +static const char * const IDENT_FILE_C="$Date: 2007/02/07 15:50:32 $"; #include "pa_config_includes.h" @@ -25,8 +25,7 @@ static const char * const IDENT_FILE_C=" #include "pa_charset.h" #include "pa_charsets.h" #include "pa_sql_connection.h" -#include "pa_vresponse.h" -#include "pa_vcookie.h" +#include "pa_md5.h" // defines @@ -35,12 +34,19 @@ static const char * const IDENT_FILE_C=" #define STDIN_EXEC_PARAM_NAME "stdin" #define CHARSET_EXEC_PARAM_NAME "charset" +#define NAME_NAME "name" + +// externs + +extern String sql_limit_name; +extern String sql_offset_name; + // class class MFile: public Methoded { public: // VStateless_class - Value* create_new_value(Pool&) { return new VFile(); } + Value* create_new_value(Pool&, HashStringValue&) { return new VFile(); } public: // Methoded bool used_directly() { return true; } @@ -144,6 +150,47 @@ static void _move(Request& r, MethodPara r.absolute(vto_file_name.as_string())); } +static void copy_process_source( + struct stat& , + int from_file, + const String& , const char* /*fname*/, bool, + void *context) { + int& to_file=*static_cast(context); + + int nCount=0; + do { + unsigned char buffer[FILE_BUFFER_SIZE]; + nCount = file_block_read(from_file, buffer, sizeof(buffer)); + int written=write(to_file, buffer, nCount); + if( written < 0 ) + throw Exception(0, + 0, + "write failed: %s (%d)", strerror(errno), errno); + + } while(nCount > 0); +} + +static void copy_open_target(int f, void *from_spec) { + String& file_spec=*static_cast(from_spec); + file_read_action_under_lock(file_spec, "copy", copy_process_source, &f); +}; + +static void _copy(Request& r, MethodParams& params) { + 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()); + + create_dir_for_file(to_spec); + + file_write_action_under_lock( + to_spec, + "copy", + copy_open_target, + &from_spec); +} + static void _load_pass_param( HashStringValue::key_type key, HashStringValue::value_type value, @@ -160,9 +207,22 @@ static void _load(Request& r, MethodPara if(third_param_hash) alt_filename_param_index++; + HashStringValue* options=third_param_hash; + 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_to_value(*voffset).as_int(); + } + if(Value *vlimit=(Value *)options->get(sql_limit_name)) { + limit=r.process_to_value(*vlimit).as_int(); + } + // no check on options count here, see file_read + } File_read_result file=file_read(r.charsets, lfile_name, is_text_mode(vmode_name.as_string()), - third_param_hash + options, true, 0, offset, limit ); const char *user_file_name=params.count()>alt_filename_param_index? @@ -171,14 +231,36 @@ static void _load(Request& r, MethodPara Value* vcontent_type=0; if(file.headers) - vcontent_type=file.headers->get(content_type_name); + { + if(Value* remote_content_type=file.headers->get("CONTENT-TYPE")) + vcontent_type=new VString(*new String(remote_content_type->as_string().cstr())); + } if(!vcontent_type) vcontent_type=new VString(r.mime_type_of(user_file_name)); VFile& self=GET_SELF(r, VFile); self.set(true/*tainted*/, file.str, file.length, user_file_name, vcontent_type); if(file.headers) - file.headers->for_each(_load_pass_param, &self.fields()); + file.headers->for_each(_load_pass_param, &self.fields()); +} + +static void _create(Request& r, MethodParams& params) { + Value& vmode_name=params. as_no_junction(0, "mode must not be code"); + if(!is_text_mode(vmode_name.as_string())) + throw Exception("parser.runtime", + 0, + "only text mode is currently supported"); + + const char* user_file_name_cstr=r.absolute( + params.as_no_junction(1, "file name must not be code").as_string()).cstr(String::L_FILE_SPEC); + + const String& content=params.as_string(2, "content must be string"); + const char* content_cstr=content.cstr(String::L_UNSPECIFIED); // explode content, honor tainting changes + + VString* vcontent_type=new VString(r.mime_type_of(user_file_name_cstr)); + + VFile& self=GET_SELF(r, VFile); + self.set(true/*tainted*/, content_cstr, strlen(content_cstr), user_file_name_cstr, vcontent_type); } static void _stat(Request& r, MethodParams& params) { @@ -219,9 +301,9 @@ static bool is_safe_env_key(const char* } #ifndef DOXYGEN struct Append_env_pair_info { + Request_charsets* charsets; HashStringString* env; Value* vstdin; - Value* vcharset; }; #endif static void append_env_pair( @@ -231,13 +313,13 @@ static void append_env_pair( if(akey==STDIN_EXEC_PARAM_NAME) { info->vstdin=avalue; } else if(akey==CHARSET_EXEC_PARAM_NAME) { - info->vcharset=avalue; + // ignore, already processed } else { if(!is_safe_env_key(akey.cstr())) throw Exception("parser.runtime", new String(akey, String::L_TAINTED), "not safe environment variable"); - info->env->put(akey, avalue->as_string().cstr(String::L_UNSPECIFIED)); + info->env->put(akey, avalue->as_string().cstr_to_string_body(String::L_UNSPECIFIED, 0, info->charsets)); } } #ifndef DOXYGEN @@ -251,10 +333,10 @@ static void pass_cgi_header_attribute( ArrayString::element_type astring, Pass_cgi_header_attribute_info* info) { size_t colon_pos=astring->pos(':'); - if(colon_pos==STRING_NOT_FOUND) { + if(colon_pos!=STRING_NOT_FOUND) { const String& key=astring->mid(0, colon_pos).change_case( *info->charset, String::CC_UPPER); - Value* value=new VString(astring->mid(colon_pos+1, astring->length())); + Value* value=new VString(astring->mid(colon_pos+1, astring->length()).trim()); info->fields->put(key, value); if(key=="CONTENT-TYPE") info->content_type=value; @@ -308,8 +390,19 @@ static void _exec_cgi(Request& r, Method if(params.count()>1) { Value& venv=params.as_no_junction(1, "env must not be code"); if(HashStringValue* user_env=venv.get_hash()) { - Append_env_pair_info info={&env, 0, 0}; - user_env->for_each(append_env_pair, &info); + // $.charset [previewing to handle URI pieces] + if(Value* vcharset=user_env->get(CHARSET_EXEC_PARAM_NAME)) + charset=&charsets.get(vcharset->as_string() + .change_case(r.charsets.source(), String::CC_UPPER)); + + // $.others + Append_env_pair_info info={&r.charsets, &env, 0}; + { + // influence tainting + // main target -- $.QUERY_STRING -- URLencoding of tainted pieces to String::L_URI lang + Temp_client_charset temp(r.charsets, charset? *charset: r.charsets.source()); + user_env->for_each(append_env_pair, &info); + } // $.stdin if(info.vstdin) { stdin_specified=true; @@ -323,18 +416,21 @@ static void _exec_cgi(Request& r, Method 0, STDIN_EXEC_PARAM_NAME " parameter must be string or file"); } - // $.charset - if(info.vcharset) - charset=&charsets.get(info.vcharset->as_string() - .change_case(r.charsets.source(), String::CC_UPPER)); } } // argv from params ArrayString argv; if(params.count()>2) { - for(size_t i=2; i 0) { + argv+=new String(param.cstr_to_string_body(String::L_UNSPECIFIED, 0, &r.charsets), String::L_AS_IS); + } + } } // transcode if necessary @@ -548,7 +644,7 @@ static void _find(Request& r, MethodPara file_spec=&r.relative(r.request_info.uri, file_name); // easy way - if(file_readable(r.absolute(*file_spec))) { + if(file_exist(r.absolute(*file_spec))) { r.write_assign_lang(*file_spec); return; } @@ -563,7 +659,7 @@ static void _find(Request& r, MethodPara String test_name; test_name<<*(dirname=&dirname->mid(0, after_monkey_slash)); test_name<is_string()) - user_file_name=¶ms.get(0)->as_string(); - - Value& statement=params.as_junction(params.count()-1, "statement must be code"); + Value& statement=params.as_junction(0, "statement must be code"); Temp_lang temp_lang(r, String::L_SQL); const String& statement_string=r.process_to_string(statement); const char* statement_cstr= statement_string.cstr(String::L_UNSPECIFIED, r.connection()); File_sql_event_handlers handlers(statement_string, statement_cstr); + + if(params.count()>1) + if(HashStringValue* options= + params.as_no_junction(1, "param must not be code").get_hash()) { + int valid_options=0; + if(Value* vfilename=options->get(NAME_NAME)) { + valid_options++; + handlers.user_file_name=&vfilename->as_string(); + } + if(Value* vcontent_type=options->get(CONTENT_TYPE_NAME)) { + valid_options++; + handlers.user_content_type=&vcontent_type->as_string(); + } + if(valid_options!=options->count()) + throw Exception("parser.runtime", + 0, + "called with invalid option"); + } + + r.connection()->query( statement_cstr, 0, 0, @@ -711,304 +825,157 @@ static void _sql(Request& r, MethodParam 0, "produced no result"); - if(!user_file_name) -class send_attr_info -{ -public: - send_attr_info(Request *t) : r(t), add_content_type(true), add_last_modified(true), add_content_disposition(true) {} - Request *r; - bool add_content_type; - bool add_last_modified; - bool add_content_disposition; -}; + const char* user_file_name_cstr=handlers.user_file_name? handlers.user_file_name->cstr(): 0; -static void send_add_header_attribute( - HashStringValue::key_type aattribute, - HashStringValue::value_type ameaning, - send_attr_info *r) -{ - const char *a = aattribute.cstr(); - SAPI::add_header_attribute(r->r->sapi_info, - a, - attributed_meaning_to_string(*ameaning, String::L_HTTP_HEADER, false). - cstr(String::L_UNSPECIFIED)); - if(strcasecmp(a, "content-type")==0) - r->add_content_type = false; - else if(strcasecmp(a, "last-modified")==0) - r->add_last_modified = false; - else if(strcasecmp(a, "content-disposition")==0) - r->add_content_disposition = false; -} - -struct RANGE -{ - size_t start; - size_t end; -}; - -static void parse_range(const String* s, Array &ar) -{ - const char *p = s->cstr(); - if(s->starts_with("bytes=")) - p += 6; - RANGE r; - while(*p){ - r.start = (size_t)-1; - r.end = (size_t)-1; - if(*p >= '0' && *p <= '9'){ - r.start = atol(p); - while(*p>='0' && *p<='9') ++p; - } - if(*p++ != '-') break; - if(*p >= '0' && *p <= '9'){ - r.end = atol(p); - while(*p>='0' && *p<='9') ++p; - } - if(*p == ',') ++p; - ar += r; - } + VString* vcontent_type=handlers.user_content_type? + new VString(*handlers.user_content_type) + : user_file_name_cstr? + new VString(r.mime_type_of(user_file_name_cstr)) + : 0; + VFile& self=GET_SELF(r, VFile); + self.set(true/*tainted*/, handlers.value.str, handlers.value.length, user_file_name_cstr, vcontent_type); } -class auto_file -{ -protected: - FILE *f; -public: - auto_file(FILE *t){ - f = t; - } - ~auto_file(){ - if(f != 0){ - fclose(f); - f = 0; - } +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()) { + // decode + const char* cstr=params.as_string(0, "parameter must be string").cstr(); + char* decoded_cstr=0; + size_t decoded_size=0; + pa_base64_decode(cstr, strlen(cstr), decoded_cstr, decoded_size); + if(decoded_cstr && decoded_size) + self.set(true/*tainted*/, decoded_cstr, decoded_size); + } else { + // encode + const char* encoded=pa_base64_encode(self.value_ptr(), self.value_size()); + r.write_assign_lang(*new String(encoded, 0, true/*once ?param=base64(something) was needed*/)); } - operator FILE*(){ - return f; + } else { + // encode + 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_assign_lang(*new String(encoded, 0, true/*once ?param=base64(something) was needed*/)); } -}; +} -// ^file:send[filename] -// ^file:send[filename;options hash] -// ^file:send[local_filename;remote_filename] -// ^file:send[local_filename;remote_filename;options hash] -static void _send(Request& r, MethodParams& params) { - SAPI::add_header_attribute(r.sapi_info, "Accept-Ranges", "bytes"); - if(r.response.fields().get("ignore")!=0) throw Exception("parser.runtime", 0, "^file:send not allowed here"); - Value *to_file_name = 0; - Value *options = 0; - Value *from_file_name = params.get(0); - const char *c_from_file_name=0, *disposition=0; - if(!from_file_name->is("string")) throw Exception("parser.runtime", 0, "filename must be string"); - - size_t count = params.count(); - if(count > 1){ - to_file_name = params.get(1); - if(to_file_name->is("hash")){ - options = to_file_name; - to_file_name = 0; - }else if(count > 2){ - options = params.get(2); - if(!options->is("hash")) throw Exception("parser.runtime", 0, "options parameter must be hash"); +static void _crc32(Request& r, MethodParams& params) { + unsigned long 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)); + } else { + throw Exception("parser.runtime", + 0, + "file name must be defined"); } + } else { + // ^file.crc32[] + VFile& self=GET_SELF(r, VFile); + crc32=pa_crc32(self.value_ptr(), self.value_size()); } + r.write_no_lang(*new VInt(crc32)); +} - c_from_file_name=r.absolute(from_file_name->as_string()).cstr(); - - size_t offset = 0; - size_t limit = (size_t)-1; - send_attr_info info(&r); - VDate *date = 0; - - if(options){ - HashStringValue *opts = options->get_hash(); - if(opts == 0) - throw Exception("parser.runtime", 0, "options must be hash"); - Value *v; - int valid_options = 0; - if(v = opts->get("offset")){ - ++valid_options; - offset = v->as_int(); - } - if(v = opts->get("limit")){ - ++valid_options; - limit = v->as_int(); - } - if(v = opts->get("headers")){ - ++valid_options; - HashStringValue *headers = v->get_hash(); - if(headers == 0) - throw Exception("parser.runtime", 0, "headers must be hash"); - headers->for_each(send_add_header_attribute, &info); - } - if(v = opts->get("mdate")){ - ++valid_options; - if(Value* vdate=v->as(VDATE_TYPE, false)) - date=static_cast(vdate); - else throw Exception("parser.runtime", 0, "mdate must be a date"); - } - if(v = opts->get("disposition")){ - ++valid_options; - if(!v->is("string")) throw Exception("parser.runtime", 0, "disposition must be a string"); - disposition = v->get_string()->cstr(); - if(strcmp(disposition, "inline") && strcmp(disposition, "attachment")) throw Exception("parser.runtime", 0, "disposition can be only 'inline' or 'attachment'"); - } - if(valid_options != opts->count()) - throw Exception("parser.runtime", 0, "invalid option passed"); - } - auto_file f = fopen(c_from_file_name, "rb"); - if(f == 0) - throw Exception("parser.runtime", 0, "Can't open file"); - - if(fseek(f, 0, SEEK_END)!=0) - throw Exception("parser.runtime", 0, "Can't seek file"); - - size_t file_length = (size_t)ftell(f); - if(file_length == (size_t)-1) - throw Exception("parser.runtime", 0, "can't get file size"); - if(file_length <= offset) - throw Exception("parser.runtime", 0, "offset too big"); - - size_t content_length = file_length-offset; - if(limit != (size_t)-1) - content_length = limit ar; - parse_range(new String(range), ar); - size_t count = ar.count(); - if(count == 1){ - RANGE &rg = ar.get_ref(0); - if(rg.start == (size_t)-1 && rg.end == (size_t)-1){ - SAPI::add_header_attribute(r.sapi_info, "status", "416 Requested Range Not Satisfiable"); - return; - } - if(rg.start == (size_t)-1 && rg.end != (size_t)-1){ - rg.start = content_length - rg.end; - rg.end = content_length; - offset += rg.start; - part_length = rg.end-rg.start; - }else if(rg.start != (size_t)-1 && rg.end == (size_t)-1){ - rg.end = content_length-1; - offset += rg.start; - part_length -= rg.start; - } - if(part_length == 0){ - SAPI::add_header_attribute(r.sapi_info, "status", "204 No Content"); - return; +static void file_md5_file_action( + struct stat& finfo, + int f, + const String& , const char* /*fname*/, bool, + void *context) +{ + PA_MD5_CTX& md5context=*static_cast(context); + if(finfo.st_size) { + int nCount=0; + do { + unsigned char buffer[FILE_BUFFER_SIZE]; + nCount = file_block_read(f, buffer, sizeof(buffer)); + if ( nCount ){ + pa_MD5Update(&md5context, (const unsigned char*)buffer, nCount); } - SAPI::add_header_attribute(r.sapi_info, "status", "206 Partial Content"); - snprintf((char*)buf, BUFSIZE, "bytes %u-%u/%u", rg.start, rg.end, content_length); - SAPI::add_header_attribute(r.sapi_info, "Content-Range", (char*)buf); - }else if(count != 0){ - SAPI::add_header_attribute(r.sapi_info, "status", "501 Not Implemented"); - return; - } + } while(nCount > 0); } +} - fseek(f, offset, SEEK_SET); - snprintf((char*)buf, BUFSIZE, "%u", part_length); - SAPI::add_header_attribute(r.sapi_info, "Content-Length", (char*)buf); - - if(info.add_content_disposition && disposition){ - const char *fname = 0; - if(to_file_name){ - fname = to_file_name->as_string().cstr(); - }else{ - const char *fname = c_from_file_name; - const char *p1 = strrchr(fname, '/'); - const char *p2 = strrchr(fname, '\\'); - if(p1 || p2) - fname = max(p1, p2)+1; - } - - snprintf((char*)buf, BUFSIZE, "%s; filename=\"%s\"", disposition, fname); - SAPI::add_header_attribute(r.sapi_info, "Content-Disposition", (char*)buf); - } - if(info.add_content_type) - SAPI::add_header_attribute(r.sapi_info, "Content-Type", r.mime_type_of(c_from_file_name).cstr()); - if(info.add_last_modified){ - if(date == 0){ - struct stat st; - if(stat(c_from_file_name, &st)!=0) throw Exception("parser.runtime", 0, "can't get file stat"); - date = new VDate(st.st_mtime); - } - const String &s = attributed_meaning_to_string(*date, String::L_AS_IS, true); - SAPI::add_header_attribute(r.sapi_info, "Last-Modified", s.cstr()); - } - r.cookie.output_result(r.sapi_info); - SAPI::send_header(r.sapi_info); - - const char* request_method=getenv("REQUEST_METHOD"); - bool header_only=request_method && strcasecmp(request_method, "HEAD")==0; - size_t sent = 0; - if(!header_only){ - size_t to_read = 0; - size_t size = 0; - do{ - to_read = part_lengthcstr(): 0; +static void _md5(Request& r, MethodParams& params) { + const char* md5; + if(&r.get_self() == file_class) { + // ^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)); + } else { + throw Exception("parser.runtime", + 0, + "file name must be defined"); + } + } else { + // ^file.md5[] + VFile& self=GET_SELF(r, VFile); + md5=pa_md5(self.value_ptr(), self.value_size()); - VString* vcontent_type=handlers.user_content_type? - new VString(*handlers.user_content_type) - : user_file_name_cstr? - new VString(r.mime_type_of(user_file_name_cstr)) - : 0; - VFile& self=GET_SELF(r, VFile); - self.set(true/*tainted*/, handlers.value.str, handlers.value.length, user_file_name_cstr, vcontent_type); + } + r.write_no_lang(*new String(md5)); } // constructor MFile::MFile(): Methoded("file") { - // ^save[mode;file-name] + // ^file::create[text;user-name;string] + // ^file::create[binary;user-name;SOMEDAY SOMETHING] + add_native_method("create", Method::CT_DYNAMIC, _create, 3, 3); + + // ^file.save[mode;file-name] add_native_method("save", Method::CT_DYNAMIC, _save, 2, 2); - // ^delete[file-name] + // ^file:delete[file-name] add_native_method("delete", Method::CT_STATIC, _delete, 1, 1); - // ^move[from-file-name;to-file-name] + // ^file:move[from-file-name;to-file-name] add_native_method("move", Method::CT_STATIC, _move, 2, 2); - // ^load[mode;disk-name] - // ^load[mode;disk-name;user-name] + // ^file::load[mode;disk-name] + // ^file::load[mode;disk-name;user-name] add_native_method("load", Method::CT_DYNAMIC, _load, 2, 3); - // ^stat[disk-name] + // ^file::stat[disk-name] add_native_method("stat", Method::CT_DYNAMIC, _stat, 1, 1); - // ^cgi[file-name] - // ^cgi[file-name;env hash] - // ^cgi[file-name;env hash;1cmd;2line;3ar;4g;5s] - add_native_method("cgi", Method::CT_DYNAMIC, _cgi, 1, 2+10); - - // ^exec[file-name] - // ^exec[file-name;env hash] - // ^exec[file-name;env hash;1cmd;2line;3ar;4g;5s] - add_native_method("exec", Method::CT_DYNAMIC, _exec, 1, 2+10); + // ^file::cgi[file-name] + // ^file::cgi[file-name;env hash] + // ^file::cgi[file-name;env hash;1cmd;2line;3ar;4g;5s] + add_native_method("cgi", Method::CT_DYNAMIC, _cgi, 1, 2+50); + + // ^file::exec[file-name] + // ^file::exec[file-name;env hash] + // ^file::exec[file-name;env hash;1cmd;2line;3ar;4g;5s] + add_native_method("exec", Method::CT_DYNAMIC, _exec, 1, 2+50); // ^file:list[path] // ^file:list[path][regexp] @@ -1017,20 +984,14 @@ MFile::MFile(): Methoded("file") { // ^file:lock[path]{code} add_native_method("lock", Method::CT_STATIC, _lock, 2, 2); - // ^find[file-name] - // ^find[file-name]{when-not-found} + // ^file:find[file-name] + // ^file:find[file-name]{when-not-found} add_native_method("find", Method::CT_STATIC, _find, 1, 2); // ^file:dirname[/a/some.tar.gz]=/a // ^file:dirname[/a/b/]=/a add_native_method("dirname", Method::CT_STATIC, _dirname, 1, 1); // ^file:basename[/a/some.tar.gz]=some.tar.gz - - // ^file:send[filename] - // ^file:send[filename;options hash] - // ^file:send[filename;new_filename] - // ^file:send[filename;new_filename;options hash] - add_native_method("send", Method::CT_STATIC, _send, 1, 3); add_native_method("basename", Method::CT_STATIC, _basename, 1, 1); // ^file:justname[/a/some.tar.gz]=some.tar add_native_method("justname", Method::CT_STATIC, _justname, 1, 1); @@ -1044,4 +1005,20 @@ MFile::MFile(): Methoded("file") { // ^file::sql[[alt_name]]{} add_native_method("sql", Method::CT_DYNAMIC, _sql, 1, 2); + + // ^file::base64[string] << decode + // ^file.base64[] << encode + // ^file:base64[file-name] << encode + add_native_method("base64", Method::CT_ANY, _base64, 0, 1); + + // ^file.crc32[] + // ^file:crc32[file-name] + add_native_method("crc32", Method::CT_ANY, _crc32, 0, 1); + + // ^file.md5[] + // ^file:md5[file-name] + 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); }