/* * Overchan Android (Meta Imageboard Client) * Copyright (C) 2014-2016 miku-nyan <https://github.com/miku-nyan> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package nya.miku.wishmaster.containers; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.EOFException; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import nya.miku.wishmaster.common.IOUtils; import nya.miku.wishmaster.lib.base64.Base64; import nya.miku.wishmaster.lib.base64.Base64InputStream; /** * Класс инкапсулирует работу с MHTML-файлом для чтения * @author miku-nyan * */ public class ReadableMHTML extends ReadableContainer { private final File file; private final Map<String, Long> positions; private static final int SEARCH_META_BUF_SIZE = 8192; @SuppressWarnings("resource") public ReadableMHTML(File file) throws IOException { this.file = file; this.positions = new HashMap<String, Long>(); byte[] metadataFilter = ("Content-Location: " + WriteableMHTML.BASE_URL + WriteableMHTML.METADATA_FILE).getBytes("UTF-8"); int buflen = SEARCH_META_BUF_SIZE + metadataFilter.length - 1; byte[] buf = new byte[buflen]; RandomAccessFile raf = null; try { raf = new RandomAccessFile(file, "r"); boolean found = false; boolean inhead = false; for (int tail = 1; !found; ++tail) { long position = raf.length() - ((long)SEARCH_META_BUF_SIZE * tail); if (position < 0) { if (inhead) throw new IOException("metadata not found"); position = 0; inhead = true; } raf.seek(position); raf.read(buf, 0, tail == 1 ? SEARCH_META_BUF_SIZE : buflen); int curpos = 0; for (int i=0; i<buflen; ++i) { if (buf[i] == metadataFilter[curpos]) ++curpos; else curpos = 0; if (curpos == metadataFilter.length) { raf.seek(position+i); found = true; break; } } } InputStream bis = new BufferedInputStream(new RAFInputStream(raf)); int r; boolean eol = false; while ((r = bis.read()) != -1) { if (r == '\n') { if (eol) break; eol = true; } else if (r != '\r') { eol = false; } } DataInputStream dataStream = new DataInputStream(new Base64InputStream(new InputStreamUntilClearLine(bis), Base64.NO_CLOSE)); if (dataStream.readLong() != WriteableMHTML.METADATA_MAGIC) throw new IOException("wrong metadata"); List<Long> metadata = new ArrayList<Long>(); while(true){ try { long meta = dataStream.readLong(); metadata.add(meta); } catch (EOFException e) { IOUtils.closeQuietly(dataStream); break; } } for (Long filePos : metadata) { raf.seek(filePos); bis = new BufferedInputStream(new RAFInputStream(raf)); byte[] fnfilter = ("Content-Location: " + WriteableMHTML.BASE_URL).getBytes("UTF-8"); int curpos = 0; while ((r = bis.read()) != -1) { if (r == fnfilter[curpos]) ++curpos; else curpos = 0; if (curpos == fnfilter.length) break; } ByteArrayOutputStream fnbuf = new ByteArrayOutputStream(); while ((r = bis.read()) != -1) { if (r == '\r' || r == '\n') break; else fnbuf.write(r); } positions.put(fnbuf.toString("UTF-8"), filePos); } } finally { if (raf != null) raf.close(); } } @Override public boolean hasFile(String filename) { return positions.containsKey(filename); } @Override public InputStream openStream(String filename) throws IOException { if (!positions.containsKey(filename)) throw new FileNotFoundException(); RandomAccessFile raf = new RandomAccessFile(file, "r"); raf.seek(positions.get(filename)); InputStream bif = new BufferedInputStream(new RAFInputStream(raf)); int r; boolean eol = false; while ((r = bif.read()) != -1) { if (r == '\n') { if (eol) break; eol = true; } else if (r != '\r') { eol = false; } } return new RAFClosingInputStream(new Base64InputStream(new InputStreamUntilClearLine(bif), Base64.NO_CLOSE), raf); } @Override public void close() throws IOException { //сам объект не содержит ресурсов, которые следует закрывать, файловые дескрипторы открываются в методе openStream } /** * Обёртка входного потока (InputStream), чтение до тех пор, пока не встретится пустая строка. * Читает по 1 байту, рекомендуется использовать с буфером ({@link BufferedInputStream}). * @author miku * */ private class InputStreamUntilClearLine extends InputStream { private boolean terminated = false; private boolean eol = false; private final InputStream in; public InputStreamUntilClearLine(InputStream in) { this.in = in; } @Override public int read() throws IOException { if (terminated) return -1; int r = in.read(); if (r == '\n') { if (eol) terminated = true; eol = true; } else if (r != '\r') { eol = false; } return r; } } /** * Входной поток (InputStream), оборачивается RandomAccessFile. * Метод {@link #close()} НЕ ЗАКРЫВАЕТ сам файл. * @author miku * */ private class RAFInputStream extends InputStream { private final RandomAccessFile raf; public RAFInputStream(RandomAccessFile raf) { this.raf = raf; } @Override public int read() throws IOException { return raf.read(); } @Override public int read(byte[] buffer) throws IOException { return raf.read(buffer); } @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { return raf.read(buffer, byteOffset, byteCount); } @Override public long skip(long byteCount) throws IOException { return raf.skipBytes((int) byteCount); } } /** * Обёртка входного потока (InputStream), который оборачивает RandomAccessFile. * При закрытии ({@link #close()}) соответствующий RandomAccessFile закрывается. * @author miku * */ private class RAFClosingInputStream extends InputStream { private final InputStream in; private final RandomAccessFile raf; public RAFClosingInputStream(InputStream in, RandomAccessFile raf) { this.in = in; this.raf = raf; } @Override public int read() throws IOException { return in.read(); } @Override public int read(byte[] buffer) throws IOException { return in.read(buffer); } @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { return in.read(buffer, byteOffset, byteCount); } @Override public int available() throws IOException { return in.available(); } @Override public long skip(long byteCount) throws IOException { return in.skip(byteCount); } @Override public void mark(int readlimit) { in.mark(readlimit); } @Override public boolean markSupported() { return in.markSupported(); } @Override public void close() throws IOException { try { in.close(); } finally { raf.close(); } } } }