diff options
Diffstat (limited to 'security/nss/cmd/signtool/sign.c')
-rw-r--r-- | security/nss/cmd/signtool/sign.c | 872 |
1 files changed, 872 insertions, 0 deletions
diff --git a/security/nss/cmd/signtool/sign.c b/security/nss/cmd/signtool/sign.c new file mode 100644 index 0000000000..168bb1b9ec --- /dev/null +++ b/security/nss/cmd/signtool/sign.c @@ -0,0 +1,872 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "signtool.h" +#include "zip.h" +#include "prmem.h" +#include "blapi.h" +#include "sechash.h" /* for HASH_GetHashObject() */ + +static int create_pk7(char *dir, char *keyName, int *keyType); +static int jar_find_key_type(CERTCertificate *cert); +static int manifesto(char *dirname, char *install_script, PRBool recurse); +static int manifesto_fn(char *relpath, char *basedir, char *reldir, + char *filename, void *arg); +static int manifesto_xpi_fn(char *relpath, char *basedir, char *reldir, + char *filename, void *arg); +static int sign_all_arc_fn(char *relpath, char *basedir, char *reldir, + char *filename, void *arg); +static int add_meta(FILE *fp, char *name); +static int SignFile(FILE *outFile, FILE *inFile, CERTCertificate *cert); +static int generate_SF_file(char *manifile, char *who); +static int calculate_MD5_range(FILE *fp, long r1, long r2, + JAR_Digest *dig); +static void SignOut(void *arg, const char *buf, unsigned long len); + +static char *metafile = NULL; +static int optimize = 0; +static FILE *mf; +static ZIPfile *zipfile = NULL; + +/* + * S i g n A r c h i v e + * + * Sign an individual archive tree. A directory + * called META-INF is created underneath this. + * + */ +int +SignArchive(char *tree, char *keyName, char *zip_file, int javascript, + char *meta_file, char *install_script, int _optimize, PRBool recurse) +{ + int status; + char tempfn[FNSIZE], fullfn[FNSIZE]; + int keyType = rsaKey; + int count; + + metafile = meta_file; + optimize = _optimize; + + /* To create XPI compatible Archive manifesto() must be run before + * the zipfile is opened. This is so the signed files are not added + * the archive before the crucial rsa/dsa file*/ + if (xpi_arc) { + manifesto(tree, install_script, recurse); + } + + if (zip_file) { + zipfile = JzipOpen(zip_file, NULL /*no comment*/); + } + + /*Sign and add files to the archive normally with manifesto()*/ + if (!xpi_arc) { + manifesto(tree, install_script, recurse); + } + + if (keyName) { + status = create_pk7(tree, keyName, &keyType); + if (status < 0) { + PR_fprintf(errorFD, "the tree \"%s\" was NOT SUCCESSFULLY SIGNED\n", + tree); + errorCount++; + exit(ERRX); + } + } + + /* Add the rsa/dsa file as the first file in the archive. This is crucial + * for a XPInstall compatible archive */ + if (xpi_arc) { + if (verbosity >= 0) { + PR_fprintf(outputFD, "%s \n", XPI_TEXT); + } + + /* rsa/dsa to zip */ + count = snprintf(tempfn, sizeof(tempfn), "META-INF/%s.%s", base, (keyType == dsaKey ? "dsa" : "rsa")); + if (count >= sizeof(tempfn)) { + PR_fprintf(errorFD, "unable to write key metadata\n"); + errorCount++; + exit(ERRX); + } + count = snprintf(fullfn, sizeof(fullfn), "%s/%s", tree, tempfn); + if (count >= sizeof(fullfn)) { + PR_fprintf(errorFD, "unable to write key metadata\n"); + errorCount++; + exit(ERRX); + } + JzipAdd(fullfn, tempfn, zipfile, compression_level); + + /* Loop through all files & subdirectories, add to archive */ + foreach (tree, "", manifesto_xpi_fn, recurse, PR_FALSE /*include dirs */, + (void *)NULL) + ; + } + /* mf to zip */ + strcpy(tempfn, "META-INF/manifest.mf"); + count = snprintf(fullfn, sizeof(fullfn), "%s/%s", tree, tempfn); + if (count >= sizeof(fullfn)) { + PR_fprintf(errorFD, "unable to write manifest\n"); + errorCount++; + exit(ERRX); + } + JzipAdd(fullfn, tempfn, zipfile, compression_level); + + /* sf to zip */ + count = snprintf(tempfn, sizeof(tempfn), "META-INF/%s.sf", base); + if (count >= sizeof(tempfn)) { + PR_fprintf(errorFD, "unable to write sf metadata\n"); + errorCount++; + exit(ERRX); + } + count = snprintf(fullfn, sizeof(fullfn), "%s/%s", tree, tempfn); + if (count >= sizeof(fullfn)) { + PR_fprintf(errorFD, "unable to write sf metadata\n"); + errorCount++; + exit(ERRX); + } + JzipAdd(fullfn, tempfn, zipfile, compression_level); + + /* Add the rsa/dsa file to the zip archive normally */ + if (!xpi_arc) { + /* rsa/dsa to zip */ + count = snprintf(tempfn, sizeof(tempfn), "META-INF/%s.%s", base, (keyType == dsaKey ? "dsa" : "rsa")); + if (count >= sizeof(tempfn)) { + PR_fprintf(errorFD, "unable to write key metadata\n"); + errorCount++; + exit(ERRX); + } + count = snprintf(fullfn, sizeof(fullfn), "%s/%s", tree, tempfn); + if (count >= sizeof(fullfn)) { + PR_fprintf(errorFD, "unable to write key metadata\n"); + errorCount++; + exit(ERRX); + } + JzipAdd(fullfn, tempfn, zipfile, compression_level); + } + + JzipClose(zipfile); + + if (verbosity >= 0) { + if (javascript) { + PR_fprintf(outputFD, "jarfile \"%s\" signed successfully\n", + zip_file); + } else { + PR_fprintf(outputFD, "tree \"%s\" signed successfully\n", + tree); + } + } + + return 0; +} + +typedef struct { + char *keyName; + int javascript; + char *metafile; + char *install_script; + int optimize; +} SignArcInfo; + +/* + * S i g n A l l A r c + * + * Javascript may generate multiple .arc directories, one + * for each jar archive needed. Sign them all. + * + */ +int +SignAllArc(char *jartree, char *keyName, int javascript, char *metafilename, + char *install_script, int optimize_level, PRBool recurse) +{ + SignArcInfo info; + + info.keyName = keyName; + info.javascript = javascript; + info.metafile = metafilename; + info.install_script = install_script; + info.optimize = optimize_level; + + return foreach (jartree, "", sign_all_arc_fn, recurse, + PR_TRUE /*include dirs*/, (void *)&info); +} + +static int +sign_all_arc_fn(char *relpath, char *basedir, char *reldir, char *filename, + void *arg) +{ + char *zipfilename = NULL; + char *arc = NULL, *archive = NULL; + int retval = 0; + SignArcInfo *infop = (SignArcInfo *)arg; + + /* Make sure there is one and only one ".arc" in the relative path, + * and that it is at the end of the path (don't sign .arcs within .arcs) */ + if ((PL_strcaserstr(relpath, ".arc") == relpath + strlen(relpath) - 4) && + (PL_strcasestr(relpath, ".arc") == relpath + strlen(relpath) - 4)) { + + if (!infop) { + PR_fprintf(errorFD, "%s: Internal failure\n", PROGRAM_NAME); + errorCount++; + retval = -1; + goto finish; + } + archive = PR_smprintf("%s/%s", basedir, relpath); + + zipfilename = PL_strdup(archive); + arc = PORT_Strrchr(zipfilename, '.'); + + if (arc == NULL) { + PR_fprintf(errorFD, "%s: Internal failure\n", PROGRAM_NAME); + errorCount++; + retval = -1; + goto finish; + } + + PL_strcpy(arc, ".jar"); + + if (verbosity >= 0) { + PR_fprintf(outputFD, "\nsigning: %s\n", zipfilename); + } + retval = SignArchive(archive, infop->keyName, zipfilename, + infop->javascript, infop->metafile, infop->install_script, + infop->optimize, PR_TRUE /* recurse */); + } +finish: + if (archive) + PR_Free(archive); + if (zipfilename) + PR_Free(zipfilename); + + return retval; +} + +/********************************************************************* + * + * c r e a t e _ p k 7 + */ +static int +create_pk7(char *dir, char *keyName, int *keyType) +{ + int status = 0; + char *file_ext; + + CERTCertificate *cert; + CERTCertDBHandle *db; + + FILE *in, *out; + + char sf_file[FNSIZE]; + char pk7_file[FNSIZE]; + + /* open cert database */ + db = CERT_GetDefaultCertDB(); + + if (db == NULL) + return -1; + + /* find cert */ + /*cert = CERT_FindCertByNicknameOrEmailAddr(db, keyName);*/ + cert = PK11_FindCertFromNickname(keyName, &pwdata); + + if (cert == NULL) { + SECU_PrintError(PROGRAM_NAME, + "Cannot find the cert \"%s\"", keyName); + return -1; + } + + /* determine the key type, which sets the extension for pkcs7 object */ + + *keyType = jar_find_key_type(cert); + file_ext = (*keyType == dsaKey) ? "dsa" : "rsa"; + + snprintf(sf_file, sizeof(sf_file), "%s/META-INF/%s.sf", dir, base); + snprintf(pk7_file, sizeof(pk7_file), "%s/META-INF/%s.%s", dir, base, file_ext); + + if ((in = fopen(sf_file, "rb")) == NULL) { + PR_fprintf(errorFD, "%s: Can't open %s for reading\n", PROGRAM_NAME, + sf_file); + errorCount++; + exit(ERRX); + } + + if ((out = fopen(pk7_file, "wb")) == NULL) { + PR_fprintf(errorFD, "%s: Can't open %s for writing\n", PROGRAM_NAME, + sf_file); + errorCount++; + exit(ERRX); + } + + status = SignFile(out, in, cert); + + CERT_DestroyCertificate(cert); + fclose(in); + fclose(out); + + if (status) { + PR_fprintf(errorFD, "%s: PROBLEM signing data (%s)\n", + PROGRAM_NAME, SECU_Strerror(PORT_GetError())); + errorCount++; + return -1; + } + + return 0; +} + +/* + * j a r _ f i n d _ k e y _ t y p e + * + * Determine the key type for a given cert, which + * should be rsaKey or dsaKey. Any error return 0. + * + */ +static int +jar_find_key_type(CERTCertificate *cert) +{ + SECKEYPrivateKey *privk = NULL; + KeyType keyType; + + /* determine its type */ + privk = PK11_FindKeyByAnyCert(cert, &pwdata); + if (privk == NULL) { + PR_fprintf(errorFD, "warning - can't find private key for this cert\n"); + warningCount++; + return 0; + } + + keyType = privk->keyType; + SECKEY_DestroyPrivateKey(privk); + return keyType; +} + +/* + * m a n i f e s t o + * + * Run once for every subdirectory in which a + * manifest is to be created -- usually exactly once. + * + */ +static int +manifesto(char *dirname, char *install_script, PRBool recurse) +{ + char metadir[FNSIZE], sfname[FNSIZE]; + + /* Create the META-INF directory to hold signing info */ + + if (PR_Access(dirname, PR_ACCESS_READ_OK)) { + PR_fprintf(errorFD, "%s: unable to read your directory: %s\n", + PROGRAM_NAME, dirname); + errorCount++; + perror(dirname); + exit(ERRX); + } + + if (PR_Access(dirname, PR_ACCESS_WRITE_OK)) { + PR_fprintf(errorFD, "%s: unable to write to your directory: %s\n", + PROGRAM_NAME, dirname); + errorCount++; + perror(dirname); + exit(ERRX); + } + + snprintf(metadir, sizeof(metadir), "%s/META-INF", dirname); + + strcpy(sfname, metadir); + + PR_MkDir(metadir, 0777); + + strcat(metadir, "/"); + strcat(metadir, MANIFEST); + + if ((mf = fopen(metadir, "wb")) == NULL) { + perror(MANIFEST); + PR_fprintf(errorFD, "%s: Probably, the directory you are trying to" + " sign has\n", + PROGRAM_NAME); + PR_fprintf(errorFD, "%s: permissions problems or may not exist.\n", + PROGRAM_NAME); + errorCount++; + exit(ERRX); + } + + if (verbosity >= 0) { + PR_fprintf(outputFD, "Generating %s file..\n", metadir); + } + + fprintf(mf, "Manifest-Version: 1.0\n"); + fprintf(mf, "Created-By: %s\n", CREATOR); + fprintf(mf, "Comments: %s\n", BREAKAGE); + + if (scriptdir) { + fprintf(mf, "Comments: --\n"); + fprintf(mf, "Comments: --\n"); + fprintf(mf, "Comments: -- This archive signs Javascripts which may not necessarily\n"); + fprintf(mf, "Comments: -- be included in the physical jar file.\n"); + fprintf(mf, "Comments: --\n"); + fprintf(mf, "Comments: --\n"); + } + + if (install_script) + fprintf(mf, "Install-Script: %s\n", install_script); + + if (metafile) + add_meta(mf, "+"); + + /* Loop through all files & subdirectories */ + foreach (dirname, "", manifesto_fn, recurse, PR_FALSE /*include dirs */, + (void *)NULL) + ; + + fclose(mf); + + strcat(sfname, "/"); + strcat(sfname, base); + strcat(sfname, ".sf"); + + if (verbosity >= 0) { + PR_fprintf(outputFD, "Generating %s.sf file..\n", base); + } + generate_SF_file(metadir, sfname); + + return 0; +} + +/* + * m a n i f e s t o _ x p i _ f n + * + * Called by pointer from SignArchive(), once for + * each file within the directory. This function + * is only used for adding to XPI compatible archive + * + */ +static int +manifesto_xpi_fn(char *relpath, char *basedir, char *reldir, char *filename, void *arg) +{ + char fullname[FNSIZE]; + int count; + + if (verbosity >= 0) { + PR_fprintf(outputFD, "--> %s\n", relpath); + } + + /* extension matching */ + if (extensionsGiven) { + char *ext = PL_strrchr(relpath, '.'); + if (!ext) + return 0; + if (!PL_HashTableLookup(extensions, ext)) + return 0; + } + count = snprintf(fullname, sizeof(fullname), "%s/%s", basedir, relpath); + if (count >= sizeof(fullname)) { + return 1; + } + JzipAdd(fullname, relpath, zipfile, compression_level); + + return 0; +} + +/* + * m a n i f e s t o _ f n + * + * Called by pointer from manifesto(), once for + * each file within the directory. + * + */ +static int +manifesto_fn(char *relpath, char *basedir, char *reldir, char *filename, void *arg) +{ + int use_js; + char *md5, *sha1; + + JAR_Digest dig; + char fullname[FNSIZE]; + + if (verbosity >= 0) { + PR_fprintf(outputFD, "--> %s\n", relpath); + } + + /* extension matching */ + if (extensionsGiven) { + char *ext = PL_strrchr(relpath, '.'); + if (!ext) + return 0; + if (!PL_HashTableLookup(extensions, ext)) + return 0; + } + + snprintf(fullname, sizeof(fullname), "%s/%s", basedir, relpath); + + fprintf(mf, "\n"); + + use_js = 0; + + if (scriptdir && !PORT_Strcmp(scriptdir, reldir)) + use_js++; + + /* sign non-.js files inside .arc directories using the javascript magic */ + + if ((PL_strcaserstr(filename, ".js") != filename + strlen(filename) - 3) && + (PL_strcaserstr(reldir, ".arc") == reldir + strlen(filename) - 4)) + use_js++; + + if (use_js) { + fprintf(mf, "Name: %s\n", filename); + fprintf(mf, "Magic: javascript\n"); + + if (optimize == 0) + fprintf(mf, "javascript.id: %s\n", filename); + + if (metafile) + add_meta(mf, filename); + } else { + fprintf(mf, "Name: %s\n", relpath); + if (metafile) + add_meta(mf, relpath); + } + + JAR_digest_file(fullname, &dig); + + if (optimize == 0) { + fprintf(mf, "Digest-Algorithms: MD5 SHA1\n"); + + md5 = BTOA_DataToAscii(dig.md5, MD5_LENGTH); + fprintf(mf, "MD5-Digest: %s\n", md5); + PORT_Free(md5); + } + + sha1 = BTOA_DataToAscii(dig.sha1, SHA1_LENGTH); + fprintf(mf, "SHA1-Digest: %s\n", sha1); + PORT_Free(sha1); + + if (!use_js) { + JzipAdd(fullname, relpath, zipfile, compression_level); + } + + return 0; +} + +/* + * a d d _ m e t a + * + * Parse the metainfo file, and add any details + * necessary to the manifest file. In most cases you + * should be using the -i option (ie, for SmartUpdate). + * + */ +static int +add_meta(FILE *fp, char *name) +{ + FILE *met; + char buf[BUFSIZ]; + + int place; + char *pattern, *meta; + + int num = 0; + + if ((met = fopen(metafile, "r")) != NULL) { + while (fgets(buf, BUFSIZ, met)) { + char *s; + + for (s = buf; *s && *s != '\n' && *s != '\r'; s++) + ; + *s = 0; + + if (*buf == 0) + continue; + + pattern = buf; + + /* skip to whitespace */ + for (s = buf; *s && *s != ' ' && *s != '\t'; s++) + ; + + /* terminate pattern */ + if (*s == ' ' || *s == '\t') + *s++ = 0; + + /* eat through whitespace */ + while (*s == ' ' || *s == '\t') + s++; + + meta = s; + + /* this will eventually be regexp matching */ + + place = 0; + if (!PORT_Strcmp(pattern, name)) + place = 1; + + if (place) { + num++; + if (verbosity >= 0) { + PR_fprintf(outputFD, "[%s] %s\n", name, meta); + } + fprintf(fp, "%s\n", meta); + } + } + fclose(met); + } else { + PR_fprintf(errorFD, "%s: can't open metafile: %s\n", PROGRAM_NAME, + metafile); + errorCount++; + exit(ERRX); + } + + return num; +} + +/********************************************************************** + * + * S i g n F i l e + */ +static int +SignFile(FILE *outFile, FILE *inFile, CERTCertificate *cert) +{ + int nb; + char ibuf[4096], digestdata[32]; + const SECHashObject *hashObj; + void *hashcx; + unsigned int len; + + SECItem digest; + SEC_PKCS7ContentInfo *cinfo; + SECStatus rv; + + if (outFile == NULL || inFile == NULL || cert == NULL) + return -1; + + /* XXX probably want to extend interface to allow other hash algorithms */ + hashObj = HASH_GetHashObject(HASH_AlgSHA1); + + hashcx = (*hashObj->create)(); + if (hashcx == NULL) + return -1; + + (*hashObj->begin)(hashcx); + + for (;;) { + if (feof(inFile)) + break; + nb = fread(ibuf, 1, sizeof(ibuf), inFile); + if (nb == 0) { + if (ferror(inFile)) { + PORT_SetError(SEC_ERROR_IO); + (*hashObj->destroy)(hashcx, PR_TRUE); + return -1; + } + /* eof */ + break; + } + (*hashObj->update)(hashcx, (unsigned char *)ibuf, nb); + } + + (*hashObj->end)(hashcx, (unsigned char *)digestdata, &len, 32); + (*hashObj->destroy)(hashcx, PR_TRUE); + + digest.data = (unsigned char *)digestdata; + digest.len = len; + + cinfo = SEC_PKCS7CreateSignedData(cert, certUsageObjectSigner, NULL, + SEC_OID_SHA1, &digest, NULL, NULL); + + if (cinfo == NULL) + return -1; + + rv = SEC_PKCS7IncludeCertChain(cinfo, NULL); + if (rv != SECSuccess) { + SEC_PKCS7DestroyContentInfo(cinfo); + return -1; + } + + if (no_time == 0) { + rv = SEC_PKCS7AddSigningTime(cinfo); + if (rv != SECSuccess) { + /* don't check error */ + } + } + + rv = SEC_PKCS7Encode(cinfo, SignOut, outFile, NULL, NULL, &pwdata); + + SEC_PKCS7DestroyContentInfo(cinfo); + + if (rv != SECSuccess) + return -1; + + return 0; +} + +/* + * g e n e r a t e _ S F _ f i l e + * + * From the supplied manifest file, calculates + * digests on the various sections, creating a .SF + * file in the process. + * + */ +static int +generate_SF_file(char *manifile, char *who) +{ + FILE *sfFile; + FILE *mfFile; + long r1, r2, r3; + char whofile[FNSIZE]; + char *buf, *name = NULL; + char *md5, *sha1; + JAR_Digest dig; + int line = 0; + + strcpy(whofile, who); + + if ((mfFile = fopen(manifile, "rb")) == NULL) { + perror(manifile); + exit(ERRX); + } + + if ((sfFile = fopen(whofile, "wb")) == NULL) { + perror(who); + exit(ERRX); + } + + buf = (char *)PORT_ZAlloc(BUFSIZ); + + if (buf) + name = (char *)PORT_ZAlloc(BUFSIZ); + + if (buf == NULL || name == NULL) + out_of_memory(); + + fprintf(sfFile, "Signature-Version: 1.0\n"); + fprintf(sfFile, "Created-By: %s\n", CREATOR); + fprintf(sfFile, "Comments: %s\n", BREAKAGE); + + if (fgets(buf, BUFSIZ, mfFile) == NULL) { + PR_fprintf(errorFD, "%s: empty manifest file!\n", PROGRAM_NAME); + errorCount++; + exit(ERRX); + } + + if (strncmp(buf, "Manifest-Version:", 17)) { + PR_fprintf(errorFD, "%s: not a manifest file!\n", PROGRAM_NAME); + errorCount++; + exit(ERRX); + } + + fseek(mfFile, 0L, SEEK_SET); + + /* Process blocks of headers, and calculate their hashen */ + + while (1) { + /* Beginning range */ + r1 = ftell(mfFile); + + if (fgets(name, BUFSIZ, mfFile) == NULL) + break; + + line++; + + if (r1 != 0 && strncmp(name, "Name:", 5)) { + PR_fprintf(errorFD, + "warning: unexpected input in manifest file \"%s\" at line %d:\n", + manifile, line); + PR_fprintf(errorFD, "%s\n", name); + warningCount++; + } + + r2 = r1; + while (fgets(buf, BUFSIZ, mfFile)) { + if (*buf == 0 || *buf == '\n' || *buf == '\r') + break; + + line++; + + /* Ending range for hashing */ + r2 = ftell(mfFile); + } + + r3 = ftell(mfFile); + + if (r1) { + fprintf(sfFile, "\n"); + fprintf(sfFile, "%s", name); + } + + calculate_MD5_range(mfFile, r1, r2, &dig); + + if (optimize == 0) { + fprintf(sfFile, "Digest-Algorithms: MD5 SHA1\n"); + + md5 = BTOA_DataToAscii(dig.md5, MD5_LENGTH); + fprintf(sfFile, "MD5-Digest: %s\n", md5); + PORT_Free(md5); + } + + sha1 = BTOA_DataToAscii(dig.sha1, SHA1_LENGTH); + fprintf(sfFile, "SHA1-Digest: %s\n", sha1); + PORT_Free(sha1); + + /* restore normalcy after changing offset position */ + fseek(mfFile, r3, SEEK_SET); + } + + PORT_Free(buf); + PORT_Free(name); + + fclose(sfFile); + fclose(mfFile); + + return 0; +} + +/* + * c a l c u l a t e _ M D 5 _ r a n g e + * + * Calculate the MD5 digest on a range of bytes in + * the specified fopen'd file. Returns base64. + * + */ +static int +calculate_MD5_range(FILE *fp, long r1, long r2, JAR_Digest *dig) +{ + int num; + int range; + unsigned char *buf; + SECStatus rv; + + range = r2 - r1; + + /* position to the beginning of range */ + fseek(fp, r1, SEEK_SET); + + buf = (unsigned char *)PORT_ZAlloc(range); + if (buf == NULL) + out_of_memory(); + + if ((num = fread(buf, 1, range, fp)) != range) { + PR_fprintf(errorFD, "%s: expected %d bytes, got %d\n", PROGRAM_NAME, + range, num); + errorCount++; + exit(ERRX); + } + + rv = PK11_HashBuf(SEC_OID_MD5, dig->md5, buf, range); + if (rv == SECSuccess) { + rv = PK11_HashBuf(SEC_OID_SHA1, dig->sha1, buf, range); + } + if (rv != SECSuccess) { + PR_fprintf(errorFD, "%s: can't generate digest context\n", + PROGRAM_NAME); + errorCount++; + exit(ERRX); + } + + PORT_Free(buf); + + return 0; +} + +static void +SignOut(void *arg, const char *buf, unsigned long len) +{ + fwrite(buf, len, 1, (FILE *)arg); +} |