modules/io/src/main/java/net/multiphasicapps/io/MIMEFileDecoder.java
// -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
// ---------------------------------------------------------------------------
// SquirrelJME
// Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
// ---------------------------------------------------------------------------
// SquirrelJME is under the Mozilla Public License Version 2.0.
// See license.mkd for licensing and copyright information.
// ---------------------------------------------------------------------------
package net.multiphasicapps.io;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
/**
* This class is used to decode input streams which have been encoded in the
* MIME Base64 format. This file format is genearted by {@code uuencode -m}.
* This format usually begins with {@code begin-base64 mode filename} and
* ends with the padding sequence {@code ====}.
*
* This class is not thread safe.
*
* @since 2018/03/05
*/
public final class MIMEFileDecoder
extends InputStream
{
/** The input base64 data. */
protected Base64Decoder mime;
/** The read mode. */
private int _mode =
Integer.MIN_VALUE;
/** The read filename. */
private String _filename;
/**
* Initializes the MIME file decoder using the default encoding.
*
* @param __in The input source.
* @throws NullPointerException On null arguments.
* @since 2018/11/30
*/
public MIMEFileDecoder(InputStream __in)
throws NullPointerException
{
this(new InputStreamReader(__in));
}
/**
* Initializes the MIME file decoder using the given encoding.
*
* @param __in The input source.
* @param __enc The encoding used.
* @throws NullPointerException On null arguments.
* @throws UnsupportedEncodingException If the encoding is not supported.
* @since 2018/11/30
*/
public MIMEFileDecoder(InputStream __in, String __enc)
throws NullPointerException, UnsupportedEncodingException
{
this(new InputStreamReader(__in, __enc));
}
/**
* Initializes the MIME file decoder from the given set of characters.
*
* @param __in The input source.
* @throws NullPointerException On null arguments.
* @since 2018/03/05
*/
public MIMEFileDecoder(Reader __in)
throws NullPointerException
{
if (__in == null)
throw new NullPointerException("NARG");
// Directly wrap the reader with the MIME decoder which reads from
// a source reader that is internally maintained
this.mime = new Base64Decoder(new __SubReader__(__in));
}
/**
* {@inheritDoc}
* @since 2018/11/25
*/
@Override
public final int available()
throws IOException
{
return this.mime.available();
}
/**
* {@inheritDoc}
* @since 2018/03/05
*/
@Override
public final void close()
throws IOException
{
this.mime.close();
}
/**
* Returns the filename which was read.
*
* @return The read filename, {@code null} will be returned if it has not
* been read yet or has not been specified.
* @since 2018/03/05
*/
public final String filename()
{
return this._filename;
}
/**
* Returns the UNIX mode of the stream.
*
* @return The UNIX mode, a negative value will be returned if it has not
* been read yet.
* @since 2018/03/05
*/
public final int mode()
{
return this._mode;
}
/**
* {@inheritDoc}
* @since 2018/03/05
*/
@Override
public final int read()
throws IOException
{
return this.mime.read();
}
/**
* {@inheritDoc}
* @since 2018/11/25
*/
@Override
public final int read(byte[] __b)
throws IOException
{
return this.mime.read(__b);
}
/**
* {@inheritDoc}
* @since 2018/03/05
*/
@Override
public final int read(byte[] __b, int __o, int __l)
throws IndexOutOfBoundsException, IOException, NullPointerException
{
return this.mime.read(__b, __o, __l);
}
/**
* This is a sub-reader which handles parsing of the MIME header and
* otherwise just passing the data to the Base64Decoder instance.
*
* @since 2018/11/25
*/
private final class __SubReader__
extends Reader
{
/** The line-by-line reader for data. */
protected final BufferedReader in;
/** The input character buffer. */
private final StringBuilder _sb =
new StringBuilder(80);
/** The current read in the buffer. */
private int _at;
/** The current limit of the buffer. */
private int _limit;
/** Did we read the header? */
private boolean _didheader;
/** Did we read the footer? */
private boolean _didfooter;
/**
* Initializes the sub-reader for the MIME data.
*
* @param __in The source reader.
* @throws NullPointerException On null arguments.
* @since 2018/11/24
*/
__SubReader__(Reader __in)
throws NullPointerException
{
if (__in == null)
throw new NullPointerException("NARG");
this.in = new BufferedReader(__in, 80);
}
/**
* {@inheritDoc}
* @since 2018/11/25
*/
@Override
public void close()
throws IOException
{
this.in.close();
}
/**
* {@inheritDoc}
* @since 2018/11/25
*/
@Override
public int read()
throws IOException
{
// Read header?
if (!this._didheader)
this.__readHeader();
// If the footer was read, this means EOF
if (this._didfooter)
return -1;
// Need to read more from the buffer
int at = this._at,
limit = this._limit;
if (at >= limit)
{
// Read line next
if (!this.__readNext())
return -1;
// Re-read
at = this._at;
limit = this._limit;
}
// Read the next character
int rv = this._sb.charAt(at);
this._at = at + 1;
return rv;
}
/**
* {@inheritDoc}
* @since 2018/11/25
*/
@Override
public int read(char[] __c)
throws IOException
{
if (__c == null)
throw new NullPointerException("NARG");
return this.read(__c, 0, __c.length);
}
/**
* {@inheritDoc}
* @since 2018/11/25
*/
@Override
public int read(char[] __c, int __o, int __l)
throws IOException
{
if (__c == null)
throw new NullPointerException("NARG");
if (__o < 0 || __l < 0 || (__o + __l) < 0 || (__o + __l) > __c.length)
throw new IndexOutOfBoundsException("IOOB");
// Read header?
if (!this._didheader)
this.__readHeader();
// If the footer was read, this means EOF
if (this._didfooter)
return -1;
// Where to read from
StringBuilder sb = this._sb;
int at = this._at,
limit = this._limit;
// Read in all characters
int rv = 0;
while (rv < __l)
{
// Need to read more?
if (at >= limit)
{
// EOF?
if (!this.__readNext())
return (rv == 0 ? -1 : rv);
// Re-read
at = this._at;
limit = this._limit;
}
// Read the next character
__c[__o++] = sb.charAt(at++);
}
// Store new at position
this._at = at;
return rv;
}
/**
* Reads the header information.
*
* @throws IOException On read errors.
* @since 2018/11/25
*/
private void __readHeader()
throws IOException
{
BufferedReader in = this.in;
/* {@squirreljme.error BD1k Unexpected end of file while trying
to read MIME header.} */
String ln = in.readLine();
if (ln == null)
throw new IOException("BD1k");
// The header is in this format:
// begin-base64 <unixmode> <filename>
/* {@squirreljme.error BD1l MIME encoded does not start with
MIME header.} */
if (!ln.startsWith("begin-base64"))
throw new IOException("BD1l");
// UNIX mode?
int fs = ln.indexOf(' ');
if (fs >= 0)
{
int ss = ln.indexOf(' ', fs + 1);
// Decode octal mode bits
MIMEFileDecoder.this._mode = Integer.parseInt(
ln.substring(fs + 1, (ss < 0 ? ln.length() : ss)), 8);
// Filename?
if (ss >= 0)
MIMEFileDecoder.this._filename =
ln.substring(ss + 1);
}
// Set as read
this._didheader = true;
}
/**
* Reads the next line into the character.
*
* @return If a line was read.
* @throws IOException On read errors.
* @since 2018/11/25
*/
private boolean __readNext()
throws IOException
{
/* {@squirreljme.error BD1m Unexpected EOF while read the MIME
file data.} */
String ln = this.in.readLine();
if (ln == null)
throw new IOException("BD1m");
// End of MIME data?
if (ln.equals("===="))
{
// Footer was read, so EOF now
this._didfooter = true;
// Was EOF
return false;
}
// Fill buffer
StringBuilder sb = this._sb;
sb.setLength(0);
sb.append(ln);
// Set properties
this._at = 0;
this._limit = ln.length();
// Was not EOF
return true;
}
}
}