diff --git a/app/build.gradle b/app/build.gradle index 66fac6844..458a70dac 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -71,6 +71,7 @@ dependencies { implementation 'com.github.seven332:android-resource:0.1.0' implementation 'com.github.seven332:animator:0.1.0' implementation 'com.github.seven332:beerbelly:0.1.4' + implementation 'com.github.seven332.commons-compress:android-lite:e4218388f6' implementation 'com.github.seven332:conaco:0.1.5-eh' implementation 'com.github.seven332:drawerlayout:0.2.1' implementation 'com.github.seven332:easyrecyclerview:0.1.1' @@ -79,12 +80,13 @@ dependencies { implementation 'com.github.seven332:glview-image:0.1.0' implementation 'com.github.seven332:hotspot:0.1.0' implementation 'com.github.seven332:image:0.1.12' + implementation 'com.github.seven332.junrar:android-lite:46ecc9630a' implementation 'com.github.seven332:okhttp:3.5.0' implementation 'com.github.seven332:refreshlayout:0.1.0' implementation 'com.github.seven332:ripple:0.1.2' implementation 'com.github.seven332:streampipe:0.1.0' implementation 'com.github.seven332:tuxiang:0.1.2' - implementation 'com.github.seven332:unifile:b59c14e47b' + implementation 'com.github.seven332:unifile:2de815c5ed' implementation 'com.github.seven332:yorozuya:0.1.2' implementation 'com.github.seven332:yorozuya-thread:0.1.1' implementation 'com.github.seven332:yorozuya-collect:0.1.4' @@ -105,5 +107,6 @@ configurations.all { force 'com.github.seven332:image:bbf0bbb0a0' exclude group: 'com.github.seven332', module: 'okhttp' + exclude group: 'com.squareup.okio', module: 'okio' } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 30336fb39..bb2eb8c8d 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -85,17 +85,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java new file mode 100644 index 000000000..82ff6701d --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java @@ -0,0 +1,218 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery; + +import android.content.Context; +import android.net.Uri; +import android.os.Process; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.hippo.ehviewer.GetText; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.gallery.archive.Archive; +import com.hippo.ehviewer.gallery.archive.ArchiveEntry; +import com.hippo.glgallery.GalleryPageView; +import com.hippo.image.Image; +import com.hippo.unifile.UniFile; +import com.hippo.unifile.UniRandomAccessFile; +import com.hippo.util.NaturalComparator; +import com.hippo.yorozuya.IOUtils; +import com.hippo.yorozuya.thread.PriorityThread; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Stack; +import java.util.concurrent.atomic.AtomicInteger; + +public class ArchiveGalleryProvider extends GalleryProvider2 implements Runnable { + + private static final String TAG = ZipGalleryProvider.class.getSimpleName(); + private static final AtomicInteger sIdGenerator = new AtomicInteger(); + + private final UniFile file; + private final Stack requests = new Stack<>(); + private final AtomicInteger decodingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX); + @Nullable + private Thread bgThread; + private volatile int size = STATE_WAIT; + private String error; + + public ArchiveGalleryProvider(Context context, Uri uri) { + file = UniFile.fromUri(context, uri); + } + + @Override + public void start() { + super.start(); + + bgThread = new PriorityThread(this, TAG + '-' + sIdGenerator.incrementAndGet(), + Process.THREAD_PRIORITY_BACKGROUND); + bgThread.start(); + } + + @Override + public void stop() { + super.stop(); + + if (bgThread != null) { + bgThread.interrupt(); + bgThread = null; + } + } + + @Override + public int size() { + return size; + } + + @Override + protected void onRequest(int index) { + synchronized (requests) { + if (!requests.contains(index) && index != decodingIndex.get()) { + requests.add(index); + requests.notify(); + } + } + notifyPageWait(index); + } + + @Override + protected void onForceRequest(int index) { + onRequest(index); + } + + @Override + protected void onCancelRequest(int index) { + synchronized (requests) { + requests.remove(Integer.valueOf(index)); + } + } + + @Override + public String getError() { + return error; + } + + @NonNull + @Override + public String getImageFilename(int index) { + // TODO + return Integer.toString(index); + } + + @Override + public boolean save(int index, @NonNull UniFile file) { + // TODO + return false; + } + + @Nullable + @Override + public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) { + // TODO + return null; + } + + @Override + public void run() { + UniRandomAccessFile uraf = null; + if (file != null) { + try { + uraf = file.createRandomAccessFile("r"); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (uraf == null) { + size = STATE_ERROR; + error = GetText.getString(R.string.error_reading_failed); + notifyDataChanged(); + return; + } + + Archive archive = null; + try { + archive = Archive.create(uraf); + } catch (IOException e) { + e.printStackTrace(); + } + if (archive == null) { + size = STATE_ERROR; + error = GetText.getString(R.string.error_invalid_zip_file); + notifyDataChanged(); + return; + } + + List entries = archive.getArchiveEntries(); + Collections.sort(entries, naturalComparator); + + // Update size and notify changed + size = entries.size(); + notifyDataChanged(); + + while (!Thread.currentThread().isInterrupted()) { + int index; + synchronized (requests) { + if (requests.isEmpty()) { + try { + requests.wait(); + } catch (InterruptedException e) { + // Interrupted + break; + } + continue; + } + index = requests.pop(); + decodingIndex.lazySet(index); + } + + // Check index valid + if (index < 0 || index >= entries.size()) { + decodingIndex.lazySet(GalleryPageView.INVALID_INDEX); + notifyPageFailed(index, GetText.getString(R.string.error_out_of_range)); + continue; + } + + try { + InputStream is = entries.get(index).getInputStream(); + Image image = Image.decode(is, true); + decodingIndex.lazySet(GalleryPageView.INVALID_INDEX); + if (image != null) { + notifyPageSucceed(index, image); + } else { + notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed)); + } + } catch (IOException e) { + decodingIndex.lazySet(GalleryPageView.INVALID_INDEX); + notifyPageFailed(index, GetText.getString(R.string.error_reading_failed)); + } + } + + // Clear + IOUtils.closeQuietly(archive); + } + + private static Comparator naturalComparator = new Comparator() { + private NaturalComparator comparator = new NaturalComparator(); + @Override + public int compare(ArchiveEntry o1, ArchiveEntry o2) { + return comparator.compare(o1.getName(), o2.getName()); + } + }; +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/archive/Archive.java b/app/src/main/java/com/hippo/ehviewer/gallery/archive/Archive.java new file mode 100644 index 000000000..03dcaaeab --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/archive/Archive.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.archive; + +import com.hippo.ehviewer.gallery.GalleryProvider2; +import com.hippo.unifile.UniRandomAccessFile; +import java.io.Closeable; +import java.io.IOException; +import java.util.List; + +public abstract class Archive implements Closeable { + + private static final byte[] FILE_SIGNATURE_SEVEN_Z = { 0x37, 0x7A, (byte) 0xBC, (byte) 0xAF, 0x27, 0x1C }; + private static final byte[] FILE_SIGNATURE_ZIP = { 0x50, 0x4B }; + private static final byte[] FILE_SIGNATURE_RAR = { 0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00 }; + + public abstract List getArchiveEntries(); + + public static Archive create(UniRandomAccessFile file) throws IOException { + byte[] bytes = new byte[8]; + readFully(file, bytes, 8); + file.seek(0); + + if (equals(bytes, FILE_SIGNATURE_SEVEN_Z, FILE_SIGNATURE_SEVEN_Z.length)) { + return SevenZArchive.create(file); + } else if (equals(bytes, FILE_SIGNATURE_ZIP, FILE_SIGNATURE_ZIP.length)) { + return ZipArchive.create(file); + } else if (equals(bytes, FILE_SIGNATURE_RAR, FILE_SIGNATURE_RAR.length)) { + return RarArchive.create(file); + } + + return null; + } + + static boolean isSupportedFilename(String name) { + for (String extension : GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS) { + if (name.endsWith(extension)) { + return true; + } + } + return false; + } + + private static int readFully(UniRandomAccessFile file, byte[] bytes, int count) throws IOException { + if (count <= 0) { + return 0; + } + + int read = 0; + do { + int l = file.read(bytes, read, count - read); + if (l < 0) + break; + read += l; + } while (read < count); + + if (read == 0) { + return -1; + } else { + return read; + } + } + + private static boolean equals(byte[] b1, byte[] b2, int size) { + if (b1.length < size || b2.length < size) { + return false; + } + + for (int i = 0; i < size; i++) { + if (b1[i] != b2[i]) { + return false; + } + } + + return true; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/archive/ArchiveEntry.java b/app/src/main/java/com/hippo/ehviewer/gallery/archive/ArchiveEntry.java new file mode 100644 index 000000000..250faa39e --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/archive/ArchiveEntry.java @@ -0,0 +1,27 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.archive; + +import java.io.IOException; +import java.io.InputStream; + +public abstract class ArchiveEntry { + + public abstract String getName(); + + public abstract InputStream getInputStream() throws IOException; +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/archive/RarArchive.java b/app/src/main/java/com/hippo/ehviewer/gallery/archive/RarArchive.java new file mode 100644 index 000000000..f1df4f9b7 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/archive/RarArchive.java @@ -0,0 +1,195 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.archive; + +import com.github.junrar.Volume; +import com.github.junrar.VolumeManager; +import com.github.junrar.exception.RarException; +import com.github.junrar.io.IReadOnlyAccess; +import com.github.junrar.rarfile.FileHeader; +import com.hippo.unifile.UniRandomAccessFile; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class RarArchive extends Archive { + + private com.github.junrar.Archive file; + + private RarArchive(com.github.junrar.Archive file) { + this.file = file; + } + + @Override + public List getArchiveEntries() { + List result = new ArrayList<>(); + + for (FileHeader fileHeader : file.getFileHeaders()) { + if (isSupportedFilename(fileHeader.getFileNameString().toLowerCase())) { + result.add(new RarArchiveEntry(file, fileHeader)); + } + } + + return result; + } + + @Override + public void close() throws IOException { + file.close(); + } + + public static Archive create(UniRandomAccessFile file) throws IOException { + VolumeManager volumeManager = new UniRandomAccessFileVolumeManager(file); + com.github.junrar.Archive archive; + try { + archive = new com.github.junrar.Archive(volumeManager); + } catch (RarException e) { + throw new IOException("Invalid rar archive", e); + } + return new RarArchive(archive); + } + + public static class RarArchiveEntry extends ArchiveEntry { + + private com.github.junrar.Archive file; + private FileHeader header; + + private RarArchiveEntry(com.github.junrar.Archive file, FileHeader header) { + this.file = file; + this.header = header; + } + + @Override + public String getName() { + return header.getFileNameString(); + } + + @Override + public InputStream getInputStream() throws IOException { + try { + return file.getInputStream(header); + } catch (RarException e) { + throw new IOException("Can't getInputStream from rar archive", e); + } + } + } + + private static class UniRandomAccessFileVolumeManager implements VolumeManager { + + private UniRandomAccessFile file; + + private UniRandomAccessFileVolumeManager(UniRandomAccessFile file) { + this.file = file; + } + + @Override + public Volume nextArchive(com.github.junrar.Archive archive, Volume lastVolume) { + if (lastVolume == null) { + return new UniRandomAccessFileVolume(archive, file); + } else { + return null; + } + } + } + + private static class UniRandomAccessFileVolume implements Volume { + + private com.github.junrar.Archive archive; + private UniRandomAccessFile file; + + private UniRandomAccessFileVolume(com.github.junrar.Archive archive, UniRandomAccessFile file) { + this.archive = archive; + this.file = file; + } + + @Override + public IReadOnlyAccess getReadOnlyAccess() { + return new UniReadOnlyAccessFile(file); + } + + @Override + public long getLength() throws IOException { + return file.length(); + } + + @Override + public com.github.junrar.Archive getArchive() { + return archive; + } + } + + private static class UniReadOnlyAccessFile implements IReadOnlyAccess { + + private UniRandomAccessFile file; + + private UniReadOnlyAccessFile(UniRandomAccessFile file) { + this.file = file; + } + + @Override + public long getPosition() throws IOException { + return file.getFilePointer(); + } + + @Override + public void setPosition(long pos) throws IOException { + file.seek(pos); + } + + @Override + public int read() throws IOException { + byte[] b = new byte[1]; + if (file.read(b) == -1) { + return -1; + } else { + return b[0]; + } + } + + @Override + public int read(byte[] buffer, int off, int count) throws IOException { + return file.read(buffer, off, count); + } + + @Override + public int readFully(byte[] buffer, int count) throws IOException { + if (count <= 0) { + return 0; + } + + int read = 0; + do { + int l = read(buffer, read, count - read); + if (l < 0) + break; + read += l; + } while (read < count); + + if (read == 0) { + return -1; + } else { + return read; + } + } + + @Override + public void close() throws IOException { + file.close(); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/archive/SevenZArchive.java b/app/src/main/java/com/hippo/ehviewer/gallery/archive/SevenZArchive.java new file mode 100644 index 000000000..d4b4dc6ee --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/archive/SevenZArchive.java @@ -0,0 +1,84 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.archive; + +import com.hippo.unifile.UniRandomAccessFile; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import okio.Store; +import okio.UniRandomAccessFileStore; +import org.apache.commons.compress.archivers.sevenz.SevenZFile; + +public class SevenZArchive extends Archive { + + private SevenZFile file; + + public SevenZArchive(SevenZFile file) { + this.file = file; + } + + @Override + public List getArchiveEntries() { + List result = new ArrayList<>(); + + for (org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry archive : file.getEntries()) { + if (isSupportedFilename(archive.getName().toLowerCase())) { + result.add(new SevenZArchiveEntry(file, archive)); + } + } + + return result; + } + + @Override + public void close() throws IOException { + file.close(); + } + + public static Archive create(UniRandomAccessFile file) throws IOException { + Store store = new UniRandomAccessFileStore(file); + SevenZFile sevenZFile = new SevenZFile(store); + return new SevenZArchive(sevenZFile); + } + + public static class SevenZArchiveEntry extends ArchiveEntry { + + private SevenZFile file; + private org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry; + + private SevenZArchiveEntry( + SevenZFile file, + org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry + ) { + this.file = file; + this.entry = entry; + } + + @Override + public String getName() { + return entry.getName(); + } + + @Override + public InputStream getInputStream() throws IOException { + file.setEntry(entry); + return file.getInputStreamForCurrentEntry(); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/archive/ZipArchive.java b/app/src/main/java/com/hippo/ehviewer/gallery/archive/ZipArchive.java new file mode 100644 index 000000000..23891bde4 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/archive/ZipArchive.java @@ -0,0 +1,86 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.archive; + +import com.hippo.unifile.UniRandomAccessFile; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import okio.Store; +import okio.UniRandomAccessFileStore; +import org.apache.commons.compress.archivers.zip.ZipFile; + +public class ZipArchive extends Archive { + + private ZipFile file; + + private ZipArchive(ZipFile file) { + this.file = file; + } + + @Override + public List getArchiveEntries() { + List result = new ArrayList<>(); + + Enumeration entries = file.getEntries(); + while (entries.hasMoreElements()) { + org.apache.commons.compress.archivers.zip.ZipArchiveEntry entry = entries.nextElement(); + if (isSupportedFilename(entry.getName().toLowerCase())) { + result.add(new ZipArchiveEntry(file, entry)); + } + } + + return result; + } + + @Override + public void close() throws IOException { + file.close(); + } + + public static Archive create(UniRandomAccessFile file) throws IOException { + Store store = new UniRandomAccessFileStore(file); + ZipFile zipFile = new ZipFile(store); + return new ZipArchive(zipFile); + } + + public static class ZipArchiveEntry extends ArchiveEntry { + + private ZipFile file; + private org.apache.commons.compress.archivers.zip.ZipArchiveEntry entry; + + private ZipArchiveEntry( + ZipFile file, + org.apache.commons.compress.archivers.zip.ZipArchiveEntry entry + ) { + this.file = file; + this.entry = entry; + } + + @Override + public String getName() { + return entry.getName(); + } + + @Override + public InputStream getInputStream() throws IOException { + return file.getInputStream(entry); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java index a3e1e5b21..6916dfc30 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java @@ -54,6 +54,7 @@ import com.hippo.ehviewer.BuildConfig; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.client.data.GalleryInfo; +import com.hippo.ehviewer.gallery.ArchiveGalleryProvider; import com.hippo.ehviewer.gallery.DirGalleryProvider; import com.hippo.ehviewer.gallery.EhGalleryProvider; import com.hippo.ehviewer.gallery.GalleryProvider2; @@ -214,7 +215,7 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan } else if (Intent.ACTION_VIEW.equals(mAction)) { if (mUri != null) { // Only support zip now - mGalleryProvider = new ZipGalleryProvider(new File(mUri.getPath())); + mGalleryProvider = new ArchiveGalleryProvider(this, mUri); } } } diff --git a/app/src/main/java/okio/UniRandomAccessFileStore.java b/app/src/main/java/okio/UniRandomAccessFileStore.java new file mode 100644 index 000000000..5afedd7de --- /dev/null +++ b/app/src/main/java/okio/UniRandomAccessFileStore.java @@ -0,0 +1,99 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package okio; + +import static okio.Util.checkOffsetAndCount; + +import com.hippo.unifile.UniRandomAccessFile; +import java.io.IOException; + +public class UniRandomAccessFileStore implements Store { + + private UniRandomAccessFile file; + private Timeout timeout; + + public UniRandomAccessFileStore(UniRandomAccessFile file) { + this(file, new Timeout()); + } + + public UniRandomAccessFileStore(UniRandomAccessFile file, Timeout timeout) { + this.file = file; + this.timeout = timeout; + } + + @Override + public void seek(long position) throws IOException { + file.seek(position); + } + + @Override + public long tell() throws IOException { + return file.getFilePointer(); + } + + @Override + public long size() throws IOException { + return file.length(); + } + + @Override + public long read(Buffer sink, long byteCount) throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (byteCount == 0) return 0; + timeout.throwIfReached(); + Segment tail = sink.writableSegment(1); + int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit); + int bytesRead = file.read(tail.data, tail.limit, maxToCopy); + if (bytesRead == -1) return -1; + tail.limit += bytesRead; + sink.size += bytesRead; + return bytesRead; + } + + @Override + public void write(Buffer source, long byteCount) throws IOException { + checkOffsetAndCount(source.size, 0, byteCount); + while (byteCount > 0) { + timeout.throwIfReached(); + Segment head = source.head; + int toCopy = (int) Math.min(byteCount, head.limit - head.pos); + file.write(head.data, head.pos, toCopy); + + head.pos += toCopy; + byteCount -= toCopy; + source.size -= toCopy; + + if (head.pos == head.limit) { + source.head = head.pop(); + SegmentPool.recycle(head); + } + } + } + + @Override + public void flush() { } + + @Override + public Timeout timeout() { + return timeout; + } + + @Override + public void close() throws IOException { + file.close(); + } +}