Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/*
* JARFILE
*
* Parsing of a Jar file
*/
#define JAR_SIZE 256
#include "jar.h"
#include "jarint.h"
#include "jarfile.h"
/* commercial compression */
#include "jzlib.h"
#if defined(XP_UNIX)
#include "sys/stat.h"
#endif
#include "sechash.h" /* for HASH_GetHashObject() */
PR_STATIC_ASSERT(46 == sizeof(struct ZipCentral));
PR_STATIC_ASSERT(30 == sizeof(struct ZipLocal));
PR_STATIC_ASSERT(22 == sizeof(struct ZipEnd));
PR_STATIC_ASSERT(512 == sizeof(union TarEntry));
/* extracting */
static int
jar_guess_jar(const char *filename, JAR_FILE fp);
static int
jar_inflate_memory(unsigned int method, long *length, long expected_out_len,
char **data);
static int
jar_physical_extraction(JAR_FILE fp, char *outpath, unsigned long offset,
unsigned long length);
static int
jar_physical_inflate(JAR_FILE fp, char *outpath, unsigned long offset,
unsigned long length, unsigned int method);
static int
jar_verify_extract(JAR *jar, char *path, char *physical_path);
static JAR_Physical *
jar_get_physical(JAR *jar, char *pathname);
static int
jar_extract_manifests(JAR *jar, jarArch format, JAR_FILE fp);
static int
jar_extract_mf(JAR *jar, jarArch format, JAR_FILE fp, char *ext);
/* indexing */
static int
jar_gen_index(JAR *jar, jarArch format, JAR_FILE fp);
static int
jar_listtar(JAR *jar, JAR_FILE fp);
static int
jar_listzip(JAR *jar, JAR_FILE fp);
/* conversions */
static int
dosdate(char *date, const char *s);
static int
dostime(char *time, const char *s);
#ifdef NSS_X86_OR_X64
/* The following macros throw up warnings. */
#if defined(__GNUC__) && !defined(NSS_NO_GCC48)
#pragma GCC diagnostic ignored "-Wstrict-aliasing"
#endif
#define x86ShortToUint32(ii) ((const PRUint32) * ((const PRUint16 *)(ii)))
#define x86LongToUint32(ii) (*(const PRUint32 *)(ii))
#else
static PRUint32
x86ShortToUint32(const void *ii);
static PRUint32
x86LongToUint32(const void *ll);
#endif
static long
octalToLong(const char *s);
/*
* J A R _ p a s s _ a r c h i v e
*
* For use by naive clients. Slam an entire archive file
* into this function. We extract manifests, parse, index
* the archive file, and do whatever nastiness.
*
*/
int
JAR_pass_archive(JAR *jar, jarArch format, char *filename, const char *url)
{
JAR_FILE fp;
int status = 0;
if (filename == NULL)
return JAR_ERR_GENERAL;
if ((fp = JAR_FOPEN(filename, "rb")) != NULL) {
if (format == jarArchGuess)
format = (jarArch)jar_guess_jar(filename, fp);
jar->format = format;
jar->url = url ? PORT_Strdup(url) : NULL;
jar->filename = PORT_Strdup(filename);
status = jar_gen_index(jar, format, fp);
if (status == 0)
status = jar_extract_manifests(jar, format, fp);
JAR_FCLOSE(fp);
if (status < 0)
return status;
/* people were expecting it this way */
return jar->valid;
}
/* file not found */
return JAR_ERR_FNF;
}
/*
* J A R _ p a s s _ a r c h i v e _ u n v e r i f i e d
*
* Same as JAR_pass_archive, but doesn't parse signatures.
*
*/
int
JAR_pass_archive_unverified(JAR *jar, jarArch format, char *filename,
const char *url)
{
JAR_FILE fp;
int status = 0;
if (filename == NULL) {
return JAR_ERR_GENERAL;
}
if ((fp = JAR_FOPEN(filename, "rb")) != NULL) {
if (format == jarArchGuess) {
format = (jarArch)jar_guess_jar(filename, fp);
}
jar->format = format;
jar->url = url ? PORT_Strdup(url) : NULL;
jar->filename = PORT_Strdup(filename);
status = jar_gen_index(jar, format, fp);
if (status == 0) {
status = jar_extract_mf(jar, format, fp, "mf");
}
JAR_FCLOSE(fp);
if (status < 0) {
return status;
}
/* people were expecting it this way */
return jar->valid;
}
/* file not found */
return JAR_ERR_FNF;
}
/*
* J A R _ v e r i f i e d _ e x t r a c t
*
* Optimization: keep a file descriptor open
* inside the JAR structure, so we don't have to
* open the file 25 times to run java.
*
*/
int
JAR_verified_extract(JAR *jar, char *path, char *outpath)
{
int status = JAR_extract(jar, path, outpath);
if (status >= 0)
return jar_verify_extract(jar, path, outpath);
return status;
}
int
JAR_extract(JAR *jar, char *path, char *outpath)
{
int result;
JAR_Physical *phy;
if (jar->fp == NULL && jar->filename) {
jar->fp = (FILE *)JAR_FOPEN(jar->filename, "rb");
}
if (jar->fp == NULL) {
/* file not found */
return JAR_ERR_FNF;
}
phy = jar_get_physical(jar, path);
if (phy) {
if (phy->compression == 0) {
result = jar_physical_extraction((PRFileDesc *)jar->fp, outpath, phy->offset, phy->length);
} else {
/* compression methods other than 8 are unsupported,
* but for historical reasons, jar_physical_inflate will be called for
* unsupported compression method constants too. */
result = jar_physical_inflate((PRFileDesc *)jar->fp, outpath,
phy->offset, phy->length,
(unsigned int)phy->compression);
}
#if defined(XP_UNIX)
if (phy->mode)
chmod(outpath, 0400 | (mode_t)phy->mode);
#endif
} else {
/* pathname not found in archive */
result = JAR_ERR_PNF;
}
return result;
}
/*
* p h y s i c a l _ e x t r a c t i o n
*
* This needs to be done in chunks of say 32k, instead of
* in one bulk calloc. (Necessary under Win16 platform.)
* This is done for uncompressed entries only.
*
*/
#define CHUNK 32768
static int
jar_physical_extraction(JAR_FILE fp, char *outpath, unsigned long offset,
unsigned long length)
{
JAR_FILE out;
char *buffer = (char *)PORT_ZAlloc(CHUNK);
int status = 0;
if (buffer == NULL)
return JAR_ERR_MEMORY;
if ((out = JAR_FOPEN(outpath, "wb")) != NULL) {
unsigned long at = 0;
JAR_FSEEK(fp, offset, (PRSeekWhence)0);
while (at < length) {
long chunk = (at + CHUNK <= length) ? CHUNK : length - at;
if (JAR_FREAD(fp, buffer, chunk) != chunk) {
status = JAR_ERR_DISK;
break;
}
at += chunk;
if (JAR_FWRITE(out, buffer, chunk) < chunk) {
/* most likely a disk full error */
status = JAR_ERR_DISK;
break;
}
}
JAR_FCLOSE(out);
} else {
/* error opening output file */
status = JAR_ERR_DISK;
}
PORT_Free(buffer);
return status;
}
/*
* j a r _ p h y s i c a l _ i n f l a t e
*
* Inflate a range of bytes in a file, writing the inflated
* result to "outpath". Chunk based.
*
*/
/* input and output chunks differ, assume 4x compression */
#define ICHUNK 8192
#define OCHUNK 32768
static int
jar_physical_inflate(JAR_FILE fp, char *outpath, unsigned long offset, unsigned long length,
unsigned int method)
{
char *inbuf, *outbuf;
int status = 0;
z_stream zs;
JAR_FILE out;
/* Raw inflate in zlib 1.1.4 needs an extra dummy byte at the end */
if ((inbuf = (char *)PORT_ZAlloc(ICHUNK + 1)) == NULL)
return JAR_ERR_MEMORY;
if ((outbuf = (char *)PORT_ZAlloc(OCHUNK)) == NULL) {
status = JAR_ERR_MEMORY;
goto loser;
}
PORT_Memset(&zs, 0, sizeof(zs));
status = inflateInit2(&zs, -MAX_WBITS);
if (status != Z_OK) {
status = JAR_ERR_GENERAL;
goto loser;
}
if ((out = JAR_FOPEN(outpath, "wb")) != NULL) {
int status2 = 0;
unsigned long at = 0;
JAR_FSEEK(fp, offset, (PRSeekWhence)0);
while (at < length) {
unsigned long chunk = (at + ICHUNK <= length) ? ICHUNK : length - at;
unsigned long tin;
if (JAR_FREAD(fp, inbuf, chunk) != chunk) {
/* incomplete read */
JAR_FCLOSE(out);
status = JAR_ERR_CORRUPT;
break;
}
at += chunk;
if (at == length) {
/* add an extra dummy byte at the end */
inbuf[chunk++] = 0xDD;
}
zs.next_in = (Bytef *)inbuf;
zs.avail_in = chunk;
zs.avail_out = OCHUNK;
tin = zs.total_in;
while ((zs.total_in - tin < chunk) || (zs.avail_out == 0)) {
unsigned long prev_total = zs.total_out;
unsigned long ochunk;
zs.next_out = (Bytef *)outbuf;
zs.avail_out = OCHUNK;
status = inflate(&zs, Z_NO_FLUSH);
if (status != Z_OK && status != Z_STREAM_END) {
/* error during decompression */
JAR_FCLOSE(out);
status = JAR_ERR_CORRUPT;
break;
}
ochunk = zs.total_out - prev_total;
if (JAR_FWRITE(out, outbuf, ochunk) < (long)ochunk) {
/* most likely a disk full error */
status = JAR_ERR_DISK;
break;
}
if (status == Z_STREAM_END)
break;
}
if (status != Z_OK) {
break;
}
}
JAR_FCLOSE(out);
status2 = inflateEnd(&zs);
if (status == Z_OK) {
status = status2;
}
} else {
/* error opening output file */
status = JAR_ERR_DISK;
}
loser:
if (inbuf) {
PORT_Free(inbuf);
}
if (outbuf) {
PORT_Free(outbuf);
}
return status;
}
/*
* j a r _ i n f l a t e _ m e m o r y
*
* Call zlib to inflate the given memory chunk. It is re-XP_ALLOC'd,
* and thus appears to operate inplace to the caller.
*
*/
static int
jar_inflate_memory(unsigned int method, long *length, long expected_out_len,
char **data)
{
char *inbuf = *data;
char *outbuf = (char *)PORT_ZAlloc(expected_out_len);
long insz = *length;
int status;
z_stream zs;
if (outbuf == NULL)
return JAR_ERR_MEMORY;
PORT_Memset(&zs, 0, sizeof zs);
status = inflateInit2(&zs, -MAX_WBITS);
if (status < 0) {
/* error initializing zlib stream */
PORT_Free(outbuf);
return JAR_ERR_GENERAL;
}
zs.next_in = (Bytef *)inbuf;
zs.next_out = (Bytef *)outbuf;
zs.avail_in = insz;
zs.avail_out = expected_out_len;
status = inflate(&zs, Z_FINISH);
if (status != Z_OK && status != Z_STREAM_END) {
/* error during deflation */
PORT_Free(outbuf);
return JAR_ERR_GENERAL;
}
status = inflateEnd(&zs);
if (status != Z_OK) {
/* error during deflation */
PORT_Free(outbuf);
return JAR_ERR_GENERAL;
}
PORT_Free(*data);
*data = outbuf;
*length = zs.total_out;
return 0;
}
/*
* v e r i f y _ e x t r a c t
*
* Validate signature on the freshly extracted file.
*
*/
static int
jar_verify_extract(JAR *jar, char *path, char *physical_path)
{
int status;
JAR_Digest dig;
PORT_Memset(&dig, 0, sizeof dig);
status = JAR_digest_file(physical_path, &dig);
if (!status)
status = JAR_verify_digest(jar, path, &dig);
return status;
}
/*
* g e t _ p h y s i c a l
*
* Let's get physical.
* Obtains the offset and length of this file in the jar file.
*
*/
static JAR_Physical *
jar_get_physical(JAR *jar, char *pathname)
{
ZZLink *link;
ZZList *list = jar->phy;
if (ZZ_ListEmpty(list))
return NULL;
for (link = ZZ_ListHead(list);
!ZZ_ListIterDone(list, link);
link = link->next) {
JAR_Item *it = link->thing;
if (it->type == jarTypePhy &&
it->pathname && !PORT_Strcmp(it->pathname, pathname)) {
JAR_Physical *phy = (JAR_Physical *)it->data;
return phy;
}
}
return NULL;
}
/*
* j a r _ e x t r a c t _ m a n i f e s t s
*
* Extract the manifest files and parse them,
* from an open archive file whose contents are known.
*
*/
static int
jar_extract_manifests(JAR *jar, jarArch format, JAR_FILE fp)
{
int status, signatures;
if (format != jarArchZip && format != jarArchTar)
return JAR_ERR_CORRUPT;
if ((status = jar_extract_mf(jar, format, fp, "mf")) < 0)
return status;
if (!status)
return JAR_ERR_ORDER;
if ((status = jar_extract_mf(jar, format, fp, "sf")) < 0)
return status;
if (!status)
return JAR_ERR_ORDER;
if ((status = jar_extract_mf(jar, format, fp, "rsa")) < 0)
return status;
signatures = status;
if ((status = jar_extract_mf(jar, format, fp, "dsa")) < 0)
return status;
if (!(signatures += status))
return JAR_ERR_SIG;
return 0;
}
/*
* j a r _ e x t r a c t _ m f
*
* Extracts manifest files based on an extension, which
* should be .MF, .SF, .RSA, etc. Order of the files is now no
* longer important when zipping jar files.
*
*/
static int
jar_extract_mf(JAR *jar, jarArch format, JAR_FILE fp, char *ext)
{
ZZLink *link;
ZZList *list = jar->phy;
int ret = 0;
if (ZZ_ListEmpty(list))
return JAR_ERR_PNF;
for (link = ZZ_ListHead(list);
ret >= 0 && !ZZ_ListIterDone(list, link);
link = link->next) {
JAR_Item *it = link->thing;
if (it->type == jarTypePhy &&
!PORT_Strncmp(it->pathname, "META-INF", 8)) {
JAR_Physical *phy = (JAR_Physical *)it->data;
char *fn = it->pathname + 8;
char *e;
char *manifest;
long length;
int num, status;
if (PORT_Strlen(it->pathname) < 8)
continue;
if (*fn == '/' || *fn == '\\')
fn++;
if (*fn == 0) {
/* just a directory entry */
continue;
}
/* skip to extension */
for (e = fn; *e && *e != '.'; e++)
/* yip */;
/* and skip dot */
if (*e == '.')
e++;
if (PORT_Strcasecmp(ext, e)) {
/* not the right extension */
continue;
}
if (phy->length == 0 || phy->length > 0xFFFF) {
/* manifest files cannot be zero length or too big! */
/* the 0xFFFF limit is per J2SE SDK */
return JAR_ERR_CORRUPT;
}
/* Read in the manifest and parse it */
/* Raw inflate in zlib 1.1.4 needs an extra dummy byte at the end */
manifest = (char *)PORT_ZAlloc(phy->length + 1);
if (!manifest)
return JAR_ERR_MEMORY;
JAR_FSEEK(fp, phy->offset, (PRSeekWhence)0);
num = JAR_FREAD(fp, manifest, phy->length);
if (num != phy->length) {
/* corrupt archive file */
PORT_Free(manifest);
return JAR_ERR_CORRUPT;
}
if (phy->compression == 8) {
length = phy->length;
/* add an extra dummy byte at the end */
manifest[length++] = 0xDD;
status = jar_inflate_memory((unsigned int)phy->compression,
&length,
phy->uncompressed_length,
&manifest);
if (status < 0) {
PORT_Free(manifest);
return status;
}
} else if (phy->compression) {
/* unsupported compression method */
PORT_Free(manifest);
return JAR_ERR_CORRUPT;
} else
length = phy->length;
status = JAR_parse_manifest(jar, manifest, length,
it->pathname, "url");
PORT_Free(manifest);
if (status < 0)
ret = status;
else
++ret;
} else if (it->type == jarTypePhy) {
/* ordinary file */
}
}
return ret;
}
/*
* j a r _ g e n _ i n d e x
*
* Generate an index for the various types of
* known archive files. Right now .ZIP and .TAR
*
*/
static int
jar_gen_index(JAR *jar, jarArch format, JAR_FILE fp)
{
int result = JAR_ERR_CORRUPT;
JAR_FSEEK(fp, 0, (PRSeekWhence)0);
switch (format) {
case jarArchZip:
result = jar_listzip(jar, fp);
break;
case jarArchTar:
result = jar_listtar(jar, fp);
break;
case jarArchGuess:
case jarArchNone:
return JAR_ERR_GENERAL;
}
JAR_FSEEK(fp, 0, (PRSeekWhence)0);
return result;
}
/*
* j a r _ l i s t z i p
*
* List the physical contents of a Phil Katz
* style .ZIP file into the JAR linked list.
*
*/
static int
jar_listzip(JAR *jar, JAR_FILE fp)
{
ZZLink *ent;
JAR_Item *it = NULL;
JAR_Physical *phy = NULL;
struct ZipLocal *Local = PORT_ZNew(struct ZipLocal);
struct ZipCentral *Central = PORT_ZNew(struct ZipCentral);
struct ZipEnd *End = PORT_ZNew(struct ZipEnd);
int err = 0;
long pos = 0L;
unsigned int compression;
unsigned int filename_len, extra_len;
char filename[JAR_SIZE];
char date[9], time[9];
char sig[4];
if (!Local || !Central || !End) {
/* out of memory */
err = JAR_ERR_MEMORY;
goto loser;
}
while (1) {
PRUint32 sigVal;
JAR_FSEEK(fp, pos, (PRSeekWhence)0);
if (JAR_FREAD(fp, sig, sizeof sig) != sizeof sig) {
/* zip file ends prematurely */
err = JAR_ERR_CORRUPT;
goto loser;
}
JAR_FSEEK(fp, pos, (PRSeekWhence)0);
sigVal = x86LongToUint32(sig);
if (sigVal == LSIG) {
JAR_FREAD(fp, Local, sizeof *Local);
filename_len = x86ShortToUint32(Local->filename_len);
extra_len = x86ShortToUint32(Local->extrafield_len);
if (filename_len >= JAR_SIZE) {
/* corrupt zip file */
err = JAR_ERR_CORRUPT;
goto loser;
}
if (JAR_FREAD(fp, filename, filename_len) != filename_len) {
/* truncated archive file */
err = JAR_ERR_CORRUPT;
goto loser;
}
filename[filename_len] = 0;
/* Add this to our jar chain */
phy = PORT_ZNew(JAR_Physical);
if (phy == NULL) {
err = JAR_ERR_MEMORY;
goto loser;
}
/* We will index any file that comes our way, but when it comes
to actually extraction, compression must be 0 or 8 */
compression = x86ShortToUint32(Local->method);
phy->compression = (compression <= 255) ? compression : 222;
/* XXX 222 is bad magic. */
phy->offset = pos + (sizeof *Local) + filename_len + extra_len;
phy->length = x86LongToUint32(Local->size);
phy->uncompressed_length = x86LongToUint32(Local->orglen);
dosdate(date, Local->date);
dostime(time, Local->time);
it = PORT_ZNew(JAR_Item);
if (it == NULL) {
err = JAR_ERR_MEMORY;
goto loser;
}
it->pathname = PORT_Strdup(filename);
it->type = jarTypePhy;
it->data = (unsigned char *)phy;
it->size = sizeof(JAR_Physical);
ent = ZZ_NewLink(it);
if (ent == NULL) {
err = JAR_ERR_MEMORY;
goto loser;
}
ZZ_AppendLink(jar->phy, ent);
pos = phy->offset + phy->length;
} else if (sigVal == CSIG) {
#if defined(XP_UNIX)
unsigned int attr = 0;
#endif
if (JAR_FREAD(fp, Central, sizeof *Central) != sizeof *Central) {
/* apparently truncated archive */
err = JAR_ERR_CORRUPT;
goto loser;
}
#if defined(XP_UNIX)
/* with unix we need to locate any bits from
the protection mask in the external attributes. */
attr = Central->external_attributes[2]; /* magic */
if (attr) {
/* we have to read the filename, again */
filename_len = x86ShortToUint32(Central->filename_len);
if (filename_len >= JAR_SIZE) {
/* corrupt in central directory */
err = JAR_ERR_CORRUPT;
goto loser;
}
if (JAR_FREAD(fp, filename, filename_len) != filename_len) {
/* truncated in central directory */
err = JAR_ERR_CORRUPT;
goto loser;
}
filename[filename_len] = 0;
/* look up this name again */
phy = jar_get_physical(jar, filename);
if (phy) {
/* always allow access by self */
phy->mode = 0400 | attr;
}
}
#endif
pos += sizeof(struct ZipCentral) +
x86ShortToUint32(Central->filename_len) +
x86ShortToUint32(Central->commentfield_len) +
x86ShortToUint32(Central->extrafield_len);
} else if (sigVal == ESIG) {
if (JAR_FREAD(fp, End, sizeof *End) != sizeof *End) {
err = JAR_ERR_CORRUPT;
goto loser;
}
break;
} else {
/* garbage in archive */
err = JAR_ERR_CORRUPT;
goto loser;
}
}
loser:
if (Local)
PORT_Free(Local);
if (phy && it == NULL)
PORT_Free(phy);
if (Central)
PORT_Free(Central);
if (End)
PORT_Free(End);
return err;
}
/*
* j a r _ l i s t t a r
*
* List the physical contents of a Unix
* .tar file into the JAR linked list.
*
*/
static int
jar_listtar(JAR *jar, JAR_FILE fp)
{
char *s;
JAR_Physical *phy;
long pos = 0L;
long sz;
union TarEntry tarball;
while (1) {
JAR_FSEEK(fp, pos, (PRSeekWhence)0);
if (JAR_FREAD(fp, &tarball, sizeof tarball) < sizeof tarball)
break;
if (!*tarball.val.filename)
break;
sz = octalToLong(tarball.val.size);
/* Tag the end of filename */
s = tarball.val.filename;
while (*s && *s != ' ')
s++;
*s = 0;
/* Add to our linked list */
phy = PORT_ZNew(JAR_Physical);
if (phy == NULL)
return JAR_ERR_MEMORY;
phy->compression = 0;
phy->offset = pos + sizeof tarball;
phy->length = sz;
ADDITEM(jar->phy, jarTypePhy, tarball.val.filename, phy,
sizeof *phy);
/* Advance to next file entry */
sz = PR_ROUNDUP(sz, sizeof tarball);
pos += sz + sizeof tarball;
}
return 0;
}
/*
* d o s d a t e
*
* Not used right now, but keep it in here because
* it will be needed.
*
*/
static int
dosdate(char *date, const char *s)
{
PRUint32 num = x86ShortToUint32(s);
PR_snprintf(date, 9, "%02d-%02d-%02d", ((num >> 5) & 0x0F), (num & 0x1F),
((num >> 9) + 80));
return 0;
}
/*
* d o s t i m e
*
* Not used right now, but keep it in here because
* it will be needed.
*
*/
static int
dostime(char *time, const char *s)
{
PRUint32 num = x86ShortToUint32(s);
PR_snprintf(time, 6, "%02d:%02d", ((num >> 11) & 0x1F),
((num >> 5) & 0x3F));
return 0;
}
#ifndef NSS_X86_OR_X64
/*
* Simulates an x86 (little endian, unaligned) ushort fetch from any address.
*/
static PRUint32
x86ShortToUint32(const void *v)
{
const unsigned char *ii = (const unsigned char *)v;
PRUint32 ret = (PRUint32)(ii[0]) | ((PRUint32)(ii[1]) << 8);
return ret;
}
/*
* Simulates an x86 (little endian, unaligned) uint fetch from any address.
*/
static PRUint32
x86LongToUint32(const void *v)
{
const unsigned char *ll = (const unsigned char *)v;
PRUint32 ret;
ret = ((((PRUint32)(ll[0])) << 0) |
(((PRUint32)(ll[1])) << 8) |
(((PRUint32)(ll[2])) << 16) |
(((PRUint32)(ll[3])) << 24));
return ret;
}
#endif
/*
* ASCII octal to binary long.
* Used for integer encoding inside tar files.
*
*/
static long
octalToLong(const char *s)
{
long num = 0L;
while (*s == ' ')
s++;
while (*s >= '0' && *s <= '7') {
num <<= 3;
num += *s++ - '0';
}
return num;
}
/*
* g u e s s _ j a r
*
* Try to guess what kind of JAR file this is.
* Maybe tar, maybe zip. Look in the file for magic
* or at its filename.
*
*/
static int
jar_guess_jar(const char *filename, JAR_FILE fp)
{
PRInt32 len = PORT_Strlen(filename);
const char *ext = filename + len - 4; /* 4 for ".tar" */
if (len >= 4 && !PL_strcasecmp(ext, ".tar"))
return jarArchTar;
return jarArchZip;
}