diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-02-02 23:50:33 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-02-02 23:50:33 +0000 |
commit | 64c2294aa2c703b1878a4855cf2b2a85a2833b52 (patch) | |
tree | 954b79658f98919373377437244ebdf1eb589ed3 | |
parent | ff8f8d19acae9d45aa4ae92a1c99728acf54e2e4 (diff) | |
parent | 57b848e746336c2f4d4be8f3ddad0ad3132ccc74 (diff) | |
download | okio-simpleperf-release.tar.gz |
Snap for 11400057 from 57b848e746336c2f4d4be8f3ddad0ad3132ccc74 to simpleperf-releasesimpleperf-release
Change-Id: I45059b139f47c33e26ee7009d77ec7b8169249b7
336 files changed, 55173 insertions, 9940 deletions
diff --git a/.buildscript/prepare_mkdocs.sh b/.buildscript/prepare_mkdocs.sh index 5dcb42cd..b9bbebc2 100755 --- a/.buildscript/prepare_mkdocs.sh +++ b/.buildscript/prepare_mkdocs.sh @@ -9,7 +9,7 @@ set -ex # Generate the API docs -./gradlew okio:dokkaHtml +./gradlew dokkaHtml # Copy in special files that GitHub wants in the project root. cp CHANGELOG.md docs/changelog.md diff --git a/.buildscript/restore_v1_docs.sh b/.buildscript/restore_v1_docs.sh index 220fed82..e0fae1e7 100644..100755 --- a/.buildscript/restore_v1_docs.sh +++ b/.buildscript/restore_v1_docs.sh @@ -1,30 +1,34 @@ #!/bin/bash # Commit b3205fa199a19d6fbf13ee5c8e0c3d6d2b15b05f contains -# Javadoc for Okio 1.x. Those should be present on -# gh-pages and published along with the other website -# content, but if for some reason they have to be re-added +# Javadoc for Okio 1.x. Those should be present on +# gh-pages and published along with the other website +# content, but if for some reason they have to be re-added # to gh-pages - run this script locally. set -ex -REPO="git@github.com:square/okio.git" -DIR=temp-clone +DIR=temp-clone -# Delete any existing temporary website clone -rm -rf $DIR +# Delete any existing temporary website clone +rm -rf $DIR -# Clone the current repo into temp folder -git clone $REPO $DIR +# Clone the current repo into temp folder +git clone . $DIR -# Move working directory into temp folder +# Move working directory into temp folder cd $DIR -# Restore Javadocs from 1.x -git checkout gh-pages -git cherry-pick b3205fa199a19d6fbf13ee5c8e0c3d6d2b15b05f -git push +# Restore docs from 1.x +git checkout b3205fa199a19d6fbf13ee5c8e0c3d6d2b15b05f +mkdir -p ../site +mv ./1.x ../site/1.x + +# Restore docs from 2.x +git checkout 9235ff8faca96082aa8784e789448b5f4893af69 +mkdir -p ../site +mv ./2.x ../site/2.x -# Delete our temp folder -cd .. +# Delete our temp folder +cd .. rm -rf $DIR diff --git a/.editorconfig b/.editorconfig index 15d8bd6e..e4cef88f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,13 @@ -[*.kt] -indent_size = 2 +root = true -[*.gradle] -indent_size = 2 +[*] +insert_final_newline=true +end_of_line=lf +charset=utf-8 +indent_size=2 +trim_trailing_whitespace=true + +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma=true +ij_kotlin_allow_trailing_comma_on_call_site=true +ij_kotlin_imports_layout=* diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..27fc7c29 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto eol=lf + +*.bat text eol=crlf +*.jar binary
\ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26b26894..a4fba896 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,16 @@ name: build -on: [push, pull_request] +on: + pull_request: {} + workflow_dispatch: {} + push: + branches: + - 'master' + tags-ignore: + - '**' env: - GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" + GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx2g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" jobs: jvm: @@ -13,134 +20,147 @@ jobs: fail-fast: false matrix: java-version: - - 1.8 - - 9 - - 10 + - 8 - 11 - - 12 - - 13 - - 14 - - 15 + - 17 + - 19 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@v1 - name: Configure JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: ${{ matrix.java-version }} + distribution: 'zulu' + java-version: 19 - name: Test run: | - ./gradlew -Dkjs=false -Dknative=false build + ./gradlew -Dkjs=false -Dknative=false -Dkwasm=false -Dtest.java.version=${{ matrix.java-version }} build --stacktrace - - name: Upload Japicmp report - if: failure() - uses: actions/upload-artifact@master + emulator: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4.0.0 with: - name: japicmp-report - path: okio/jvm/japicmp/build/reports/japi.txt + distribution: 'zulu' + java-version: 19 + + - uses: gradle/gradle-build-action@v2 + + - uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 24 + script: ./gradlew :okio-assetfilesystem:connectedCheck + + loom: + runs-on: ubuntu-latest - multiplatform: - runs-on: macOS-latest + strategy: + fail-fast: false steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@v1 - name: Configure JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 14 + distribution: 'zulu' + java-version: 19 - name: Test run: | - ./gradlew build + ./gradlew -DloomEnabled=true build + + all-platforms: + runs-on: ${{ matrix.os }} - windows: - runs-on: windows-latest + strategy: + fail-fast: false + matrix: + os: [ macos-11, ubuntu-latest, windows-latest ] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@v1 - name: Configure JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 1.8 + distribution: 'zulu' + java-version: 19 - name: Test + if: matrix.os != 'windows-latest' run: | ./gradlew build - publish: - runs-on: macOS-latest - if: github.ref == 'refs/heads/master' - needs: [jvm, multiplatform, windows] - - steps: - - name: Checkout - uses: actions/checkout@v2 + - name: Test (No WASM) + if: matrix.os == 'windows-latest' + run: | + ./gradlew build -Dkwasm=false - - name: Configure JDK - uses: actions/setup-java@v1 + - name: Save Test Reports + if: failure() + uses: actions/upload-artifact@v4 with: - java-version: 14 + name: test-reports + path: '**/build/reports' - - name: Upload Artifacts - run: | - ./gradlew clean publish - env: - ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} - ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - - publish-windows: - runs-on: windows-latest - if: github.ref == 'refs/heads/master' - needs: [jvm, multiplatform, windows] + publish: + runs-on: macos-13 + if: github.repository == 'square/okio' && github.ref == 'refs/heads/master' + needs: [jvm, all-platforms, emulator] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Configure JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 1.8 + distribution: 'zulu' + java-version: 19 - name: Upload Artifacts run: | - ./gradlew clean publishMingwX64PublicationToMavenRepository + ./gradlew clean publish --stacktrace env: - ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} - ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }} publish-website: runs-on: ubuntu-latest - if: github.ref == 'refs/heads/master' - needs: [jvm, multiplatform] + if: github.repository == 'square/okio' && github.ref == 'refs/heads/master' + needs: [jvm, all-platforms, emulator] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Configure JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 14 + distribution: 'zulu' + java-version: 19 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: 3.8 @@ -149,9 +169,12 @@ jobs: - name: Build mkdocs run: | - pip3 install mkdocs-macros-plugin + pip3 install mkdocs-material mkdocs-macros-plugin mkdocs build + - name: Restore 1.x docs + run: .buildscript/restore_v1_docs.sh + - name: Deploy docs if: success() uses: JamesIves/github-pages-deploy-action@releases/v3 @@ -25,6 +25,6 @@ obj node_modules # Special Mkdocs files -docs/2.x +docs/3.x docs/changelog.md docs/contributing.md @@ -20,6 +20,8 @@ java_library { host_supported: true, srcs: [ "okio/src/jvmMain/**/*.kt", + ], + common_srcs: [ "okio/src/commonMain/**/*.kt", ], apex_available: [ diff --git a/CHANGELOG.md b/CHANGELOG.md index 48028d0a..ad2eaaa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,309 @@ Change Log ========== +## Version 3.7.0 + +_2023-12-16_ + + * New: `Timeout.cancel()` prevents a timeout from firing. + * Breaking: Drop the `watchosX86` Kotlin/Native target. From [the Kotlin blog][watchosX86], + _‘This is an obsolete simulator for Intel Macs. Use the watchosX64 target instead.’_ + * New: Add the `watchosDeviceArm64` Kotlin/Native target. + * New: `Timeout` APIs that accept `kotlin.time.Duration`. + * Upgrade: [Kotlin 1.9.21][kotlin_1_9_21]. + + +## Version 3.6.0 + +_2023-10-01_ + + * Fix: Don't leak file handles when using `metadata` functions on `ZipFileSystem`. We had a bug + where we were closing the `.zip` file, but not a stream inside of it. We would have prevented + this bug if only we’d used `FakeFileSystem.checkNoOpenFiles()` in our tests! + * Fix: Don't build an index of a class loader's resources in `ResourceFileSystem.read()`. This + operation doesn't need this index, and building it is potentially expensive. + * New: Experimentally support Linux on ARM64 for Kotlin/Native targets (`linuxArm64`). Note that + we haven't yet added CI test coverage for this platform. + * Upgrade: [Kotlin 1.9.10][kotlin_1_9_10]. + + +## Version 1.17.6 + +_2023-10-01_ + + * Fix: Don't crash decoding GZIP files when the optional extra data (`XLEN`) is 32 KiB or larger. + + +## Version 3.5.0 + +_2023-08-02_ + + * New: Support the WebAssembly (WASM) platform. Okio's support for WASM is experimental, but + improving, just like Kotlin's own support for WASM. + * New: Adapt WebAssembly System Interface (WASI) API's as an Okio FileSystem using + `WasiFileSystem`. This is in the new `okio-wasifilesystem` module. It requires the [preview1] + WASI API. We’ll make backwards-incompatible upgrades to new WASI API versions as they become + available. + * Fix: Return relative paths in the NIO adapter FileSystem when required. `FileSystem.list()` + had always returned absolute paths, even when the target directory was supplied as a relative + path. + * Fix: Don't crash when reading into an empty array using `FileHandle` on Kotlin/Native. + * Upgrade: [Kotlin 1.9.0][kotlin_1_9_0]. + + +## Version 3.4.0 + +_2023-07-07_ + + * New: Adapt a Java NIO FileSystem (`java.nio.file.FileSystem`) as an Okio FileSystem using + `fileSystem.asOkioFileSystem()`. + * New: Adapt Android’s `AssetManager` as an Okio FileSystem using `AssetFileSystem`. This is in the + new `okio-assetfilesystem` module. Android applications should prefer this over + `FileSystem.RESOURCES` as it’s faster to load. + * Fix: Don't crash decoding GZIP files when the optional extra data (`XLEN`) is 32 KiB or larger. + * Fix: Resolve symlinks in `FakeFileSystem.canonicalize()`. + * Fix: Report the correct `createdAtMillis` in `NodeJsFileSystem` file metadata. We were + incorrectly using `ctimeMs`, where `c` means _changed_, not _created_. + * Fix: `UnsafeCursor` is now `Closeable`. + + +## Version 3.3.0 + +_2023-01-07_ + + * Fix: Don't leak resources when `use {}` is used with a non-local return. We introduced this + performance and stability bug by not considering that non-local returns execute neither the + `return` nor `catch` control flows. + * Fix: Use a sealed interface for `BufferedSink` and `BufferedSource`. These were never intended + for end-users to implement, and we're happy that Kotlin now allows us to express that in our API. + * New: Change internal locks from `synchronized` to `ReentrantLock` and `Condition`. We expect this + to improve help when using Okio with Java virtual threads ([Project Loom][loom]). + * Upgrade: [Kotlin 1.8.0][kotlin_1_8_0]. + + +## Version 3.2.0 + +_2022-06-26_ + + * Fix: Configure the multiplatform artifact (`com.squareup.okio:okio:3.x.x`) to depend on the + JVM artifact (`com.squareup.okio:okio-jvm:3.x.x`) for Maven builds. This should work-around an + issue where Maven doesn't interpret Gradle metadata. + * Fix: Change `CipherSource` and `CipherSink` to recover if the cipher doesn't support streaming. + This should work around a crash with AES/GCM ciphers on Android. + * New: Enable compatibility with non-hierarchical projects. + + +## Version 3.1.0 + +_2022-04-19_ + + * Upgrade: [Kotlin 1.6.20][kotlin_1_6_20]. + * New: Support [Hierarchical project structure][hierarchical_projects]. If you're using Okio in a + multiplatform project please upgrade your project to Kotlin 1.6.20 (or newer) to take advantage + of this. With hierarchical projects it's easier to use properties like `FileSystem.SYSTEM` that + are available on most Okio platforms but not all of them. + * New: `ForwardingSource` is now available on all platforms. + * New: The `watchosX64` platform is now supported. + * Fix: Don't crash in `NSData.toByteString()' when the input is empty. + * Fix: Support empty ZIP files in `FileSystem.openZip()`. + * Fix: Throw in `canonicalize()` of ZIP file systems if the path doesn't exist. + * Fix: Don't require ZIP files start with a local file header. + * New: `okio.ProtocolException` is a new exception type for multiplatform users. (It is aliased to + `java.net.ProtocolException` on JVM platforms). + + +## Version 3.0.0 + +_2021-10-28_ + +This is the first stable release of Okio 3.x. This release is strongly backwards-compatible with +Okio 2.x, and the new major version signifies new capabilities more than it does backwards +incompatibility. + +Most users should be able to upgrade from 2.x by just changing the version. If you're using Okio +in a Kotlin Multiplatform project, you'll need to drop the `-multiplatform` suffix in your Gradle +dependencies. + + * New: Remove `@ExperimentalFileSystem`. This annotation is no longer necessary as the file system + is no longer experimental! + * New: Path no longer aggressively normalizes `..` segments. Use `Path.normalize()` to apply these + based on the content of the path, or `FileSystem.canonicalize()` to do it honoring any symlinks + on a particular file system. + * New: Publish a [bill of materials (BOM)][bom] for Okio. Depend on this from Gradle or Maven to + keep all of your Okio artifacts on the same version, even if they're declared via transitive + dependencies. You can even omit versions when declaring other Okio dependencies. + + ```kotlin + dependencies { + api(platform("com.squareup.okio:okio-bom:3.0.0")) + api("com.squareup.okio:okio") // No version! + api("com.squareup.okio:okio-fakefilesystem") // No version! + } + ``` + + * New: `FileSystem.delete()` silently succeeds when deleting a file that doesn't exist. Use + the new `mustExist` parameter to trigger an exception instead. + * New: `FileSystem.createDirectories()` silently succeeds when creating a directory that already + exists. Use the new `mustCreate` parameter to trigger an exception instead. + * New: `FileSystem` offers Java-language overloads where appropriate. Previously functions that + had default parameters were potentially awkward to invoke from Java. + * New: `Timeout.intersectWith()` returns a value instead of `Unit`. This is a binary-incompatible + change. We expect that this public API is very rarely used outside of Okio itself. + * Fix: Change `BufferedSource.readDecimalLong()` to fail if the input value is just `-`. Previously + Okio incorrectly returned `0` for this. + + +## Version 3.0.0-alpha.11 + +_2021-10-23_ + + * Upgrade: [Kotlin 1.5.31][kotlin_1_5_31]. + * Upgrade: [kotlinx-datetime 0.3.0][datetime_0_3_0]. (This is a dependency of `okio-fakefilesystem` + only.) + * New: Support creating and accessing symlinks. We were reluctant to include symlinks in our API + (to keep it small!) but decided that supporting them was essential to properly implement + recursive traversal. + * New: `FileMetadata.extras` can track metadata for custom `FileSystem` implementations. + * New: Support Apple Silicon Kotlin/Native targets (`macosArm64`, `iosSimulatorArm64`, + `tvosSimulatorArm64`, and `watchosSimulatorArm64`). + * New: `FileSystem.listRecursively()` returns a `Sequence` that includes all of a directory's + children, and all of their children recursively. The implementation does a lazy, depth-first + traversal. + * New: `Path.relativeTo()` computes how to get from one path to another. + * New: `Path.root` and `Path.segments`. These APIs decompose a path into its component parts. + * New: `FileSystem.listOrNull()` returns a directory's children, or null if the path doesn't + reference a readable directory. + * New: Option to fail if the file being updated doesn't already exist: `mustExist`. Use this to + avoid creating a new file when your intention is to update an existing file. + * New: Option to fail if a file being created already exists: `mustCreate`. Use this to avoid + updating an existing file when your intention is to create a new file. + * Fix: Restore support for Kotlin/JS on browser platforms. We were relying on NodeJS-only features + to fetch the local directory separator (`/` or `\`) and temporary directory. + * Fix: Don't ignore the caller's specified write offset running Okio on Kotlin/Native on Linux. + (`FileHandle.write()` was broken and always appended to the end of the file.) + + +## Version 3.0.0-alpha.10 + +_2021-09-09_ + +This release drops the `-multiplatform` suffix on Kotlin Multiplatform artifacts. All artifacts now +share the same name (like `com.squareup.okio:okio:3.0.0-alpha.10`) for both Kotlin/JVM and Kotlin +Multiplatform. + + * Fix: Don't crash in `ResourceFileSystem` when classpath `.jar` files have special characters in + their paths. + + +## Version 3.0.0-alpha.9 + +_2021-08-01_ + + * New: `ByteString.copyInto()` saves an allocation when extracting data from a `ByteString`. + * Fix: Create `FileHandle.protectedSize()` to match other abstract functions. + * Fix: Open files in binary mode on Windows. Without this, files that contain `0x1a` will be + truncated prematurely. + + +## Version 3.0.0-alpha.8 + +_2021-07-13_ + + * Fix: Don't crash on duplicate entries in a .zip file. + * Fix: Change `FileSystem.RESOURCES` to initialize itself lazily. + + +## Version 3.0.0-alpha.7 + +_2021-07-12_ + + * Fix: Change `ResourceFileSystem` to load roots eagerly. We had a bug where `list()` on the root + returned an empty list even if resources were present. + * New: `FileHandle.reposition()` can seek on a source or sink returned by that `FileHandle`. + * New: Move the system resources instance to `FileSystem.RESOURCES`. + * Upgrade: [Kotlin 1.5.20][kotlin_1_5_20]. + + +## Version 3.0.0-alpha.6 + +_2021-06-01_ + + * New: `FileHandle` supports random access reads, writes, and resizes on files. Create an instance + with `FileSystem.openReadOnly()` or `FileSystem.openReadWrite()`. + * New: Remove `Cursor` which is obsoleted by `FileHandle`. (`UnsafeCursor` is still around!) + * New: Add support for the new intermediate representation (IR) artifacts in Kotlin/JS. We still + support the legacy artifact format. + * New: Support tvOS (tvosArm64, tvosX64) in multiplatform. + * New: Change `ResourceFileSystem` to omit `.class` files when indexing `.zip` files. We expect + this to lower the memory footprint of `ResourceFileSystem`. + * Fix: Don't crash on background thread access in Kotlin/Native. We had to apply `@SharedImmutable` + and run our test suite on a background thread. + + +## Version 3.0.0-alpha.5 + +_2021-04-27_ + + * New: Promote the `ZipFileSystem` and `ResourceFileSystem` to the main Okio module. These are + currently JVM-only. The `okio-zipfilesystem` module is no longer published. + + +## Version 3.0.0-alpha.4 + +_2021-04-14_ + + * Fix: Rename internal classes to avoid name collisions. We were seeing problems due to having + multiple files named `-Platform.kt`. + + +## Version 3.0.0-alpha.3 + +_2021-04-06_ + + * New: Move `NodeJsFileSystem` into its own module. Having it built-in prevented Okio from working + in a browser where there's no synchronous file system API. This is in the `okio-nodefilesystem` + artifact. + + +## Version 3.0.0-alpha.2 + +_2021-03-24_ + + * New: Require Java 8+ for Okio 3.x. + * New: `Cursor` supports random access reads on a `Source`. + * New: `FileSystem.openZip(path)` returns a file system backed by a `.zip` file. This is in the + `okio-zipfilesystem` artifact. + + +## Version 3.0.0-alpha.1 + +_2021-01-07_ + +* New: Experimental file system API. The `Path`, `FileMetadata`, `FileSystem` and + `ForwardingFileSystem` types are subject to API changes in a future release. +* New: Experimental `okio-fakefilesystem` artifact. + + +## Version 2.10.0 + +_2021-01-07_ + +* New: Support Windows (mingwX64) in multiplatform. +* New: Support watchOS (watchosArm32, watchosArm64, watchosX86) in multiplatform. +* New: Support `HashingSource`, `HashingSink`, buffer hash functions, and `UnsafeCursor` on non-JVM + platforms. Previously these were all JVM-only. +* New: Implement `Closeable` on `Sink` and `Source` on non-JVM platforms. Okio now includes a + multiplatform `okio.Closeable` interface and corresponding `use {}` extension. Closing resources + when you're done with them shouldn't be JVM-only! +* New: `Sink.hashingSink` and `Source.hashingSource` functions that accept + `java.security.MessageDigest` and `javax.crypto.Mac` instances. Use these when your hash function + isn't built-in. +* Fix: Don't crash with a `ShortBufferException` in `CipherSink` and `CipherSource` on Android. + (Android may throw a `ShortBufferException` even if the buffer is not too short. We now + avoid this problem!) +* Upgrade: [Kotlin 1.4.20][kotlin_1_4_20]. + + ## Version 2.9.0 _2020-10-04_ @@ -34,10 +337,10 @@ _2020-07-07_ * New: `Pipe.cancel()` causes in-progress and future reads and writes on the pipe to immediately fail with an `IOException`. The streams may still be canceled normally. - + * New: Enlarge Okio's internal segment pool from a fixed 64 KiB total to 64 KiB per processor. For - example, on an Intel i9 8-core/16-thread machine the segment pool now uses up to 1 MiB of memory. - + example, on an Intel i9 8-core/16-thread machine the segment pool now uses up to 1 MiB of memory. + * New: Migrate from `synchronized` to lock-free when accessing the segment pool. Combined with the change above we saw throughput increase 3x on a synthetic benchmark designed to create contention. @@ -83,20 +386,20 @@ _2019-12-11_ in a crash when subsequent reads encountered an unexpected empty segment. -### Version 2.4.1 +## Version 2.4.1 _2019-10-04_ * Fix: Don't cache hash code and UTF-8 string in `ByteString` on Kotlin/Native which prevented freezing. -### Version 2.4.0 +## Version 2.4.0 _2019-08-26_ * New: Upgrade to Kotlin 1.3.50. -### Version 2.3.0 +## Version 2.3.0 _2019-07-29_ @@ -577,7 +880,21 @@ _2014-04-08_ * Imported from OkHttp. - [gradle_metadata]: https://blog.gradle.org/gradle-metadata-1.0 - [kotlin_1_4_10]: https://github.com/JetBrains/kotlin/releases/tag/v1.4.10 - [maven_provided]: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html - [xor_utf8]: https://github.com/square/okio/blob/bbb29c459e5ccf0f286e0b17ccdcacd7ac4bc2a9/okio/src/main/kotlin/okio/Utf8.kt#L302 +[bom]: https://docs.gradle.org/6.2/userguide/platforms.html#sub:bom_import +[datetime_0_3_0]: https://github.com/Kotlin/kotlinx-datetime/releases/tag/v0.3.0 +[gradle_metadata]: https://blog.gradle.org/gradle-metadata-1.0 +[hierarchical_projects]: https://kotlinlang.org/docs/multiplatform-hierarchy.html +[kotlin_1_4_10]: https://github.com/JetBrains/kotlin/releases/tag/v1.4.10 +[kotlin_1_4_20]: https://github.com/JetBrains/kotlin/releases/tag/v1.4.20 +[kotlin_1_5_20]: https://github.com/JetBrains/kotlin/releases/tag/v1.5.20 +[kotlin_1_5_31]: https://github.com/JetBrains/kotlin/releases/tag/v1.5.31 +[kotlin_1_6_20]: https://blog.jetbrains.com/kotlin/2022/04/kotlin-1-6-20-released/ +[kotlin_1_8_0]: https://kotlinlang.org/docs/whatsnew18.html +[kotlin_1_9_0]: https://kotlinlang.org/docs/whatsnew19.html +[kotlin_1_9_10]: https://github.com/JetBrains/kotlin/releases/tag/v1.9.10 +[kotlin_1_9_21]: https://github.com/JetBrains/kotlin/releases/tag/v1.9.21 +[loom]: https://wiki.openjdk.org/display/loom/Getting+started +[maven_provided]: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html +[preview1]: https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md +[watchosX86]: https://blog.jetbrains.com/kotlin/2023/02/update-regarding-kotlin-native-targets/ +[xor_utf8]: https://github.com/square/okio/blob/bbb29c459e5ccf0f286e0b17ccdcacd7ac4bc2a9/okio/src/main/kotlin/okio/Utf8.kt#L302 @@ -14,7 +14,7 @@ third_party { type: GIT value: "https://github.com/square/okio/" } - version: "47fb0ddcd0bcf768a897dff723a1699341eea10f" - last_upgrade_date { year: 2021 month: 4 day: 6 } + version: "3.7.0" + last_upgrade_date { year: 2024 month: 1 day: 3 } license_type: NOTICE } @@ -1,3 +1,3 @@ -file:platform/frameworks/base:/core/java/android/app/admin/WorkDeviceExperience_OWNERS +file:platform/external/lottie:OWNERS kirit@google.com olegsh@google.com diff --git a/android-test/build.gradle b/android-test/build.gradle deleted file mode 100644 index 56fcda76..00000000 --- a/android-test/build.gradle +++ /dev/null @@ -1,73 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' - -buildscript { - repositories { - mavenCentral() - gradlePluginPortal() - google() - } -} - -def isIDE = properties.containsKey('android.injected.invoked.from.ide') || - (System.getenv("XPC_SERVICE_NAME") ?: "").contains("intellij") || - System.getenv("IDEA_INITIAL_DIRECTORY") != null - -android { - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - coreLibraryDesugaringEnabled true - } - - kotlinOptions { - freeCompilerArgs += "-Xmulti-platform" - } - - compileSdkVersion 30 - - defaultConfig { - minSdkVersion 15 - targetSdkVersion 30 - versionCode 1 - versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - - // AndroidJUnitRunner wasn't finding tests in multidex artifacts when running on Android 4.0.3. - // Work around by adding all Okio classes to the keep list. That way they'll be in the main - // .dx file where TestRequestBuilder will find them. - multiDexEnabled true - multiDexKeepProguard file('multidex-config.pro') - } - - if (!isIDE) { - sourceSets { - named("androidTest") { - it.java.srcDirs += [ - project.file("../okio/src/commonMain/kotlin"), - project.file("../okio/src/commonTest/java"), - project.file("../okio/src/commonTest/kotlin"), - project.file("../okio/src/hashFunctions/kotlin"), - project.file("../okio/src/jvmMain/kotlin"), - project.file("../okio/src/jvmTest/java"), - project.file("../okio/src/jvmTest/kotlin"), - ] - } - } - } -} - - -dependencies { - coreLibraryDesugaring deps.android.desugarJdkLibs - androidTestImplementation deps.androidx.testExtJunit - androidTestImplementation deps.androidx.testRunner - androidTestImplementation deps.animalSniffer.annotations - androidTestImplementation deps.kotlin.stdLib.common - androidTestImplementation deps.kotlin.test.annotations - androidTestImplementation deps.kotlin.test.common - androidTestImplementation deps.kotlin.test.jdk - androidTestImplementation deps.kotlin.time - androidTestImplementation deps.test.assertj - androidTestImplementation deps.test.junit -} diff --git a/android-test/build.gradle.kts b/android-test/build.gradle.kts new file mode 100644 index 00000000..5f9957ba --- /dev/null +++ b/android-test/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +buildscript { + repositories { + mavenCentral() + gradlePluginPortal() + google() + } +} + +val isIDE = properties.containsKey("android.injected.invoked.from.ide") || + (System.getenv("XPC_SERVICE_NAME") ?: "").contains("intellij") || + System.getenv("IDEA_INITIAL_DIRECTORY") != null + +android { + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + isCoreLibraryDesugaringEnabled = true + } + + kotlinOptions { + freeCompilerArgs += "-Xmulti-platform" + } + + compileSdkVersion(33) + + defaultConfig { + minSdkVersion(15) + targetSdkVersion(33) + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + // AndroidJUnitRunner wasn't finding tests in multidex artifacts when running on Android 4.0.3. + // Work around by adding all Okio classes to the keep list. That way they'll be in the main + // .dx file where TestRequestBuilder will find them. + multiDexEnabled = true + multiDexKeepProguard = file("multidex-config.pro") + } + + if (!isIDE) { + sourceSets { + named("androidTest") { + java.srcDirs( + project.file("../okio-fakefilesystem/src/commonMain/kotlin"), + project.file("../okio/src/commonMain/kotlin"), + project.file("../okio/src/commonTest/java"), + project.file("../okio/src/commonTest/kotlin"), + project.file("../okio/src/hashFunctions/kotlin"), + project.file("../okio/src/jvmMain/kotlin"), + project.file("../okio/src/jvmTest/java"), + project.file("../okio/src/jvmTest/kotlin") + ) + } + } + } +} + +dependencies { + coreLibraryDesugaring(libs.android.desugar.jdk.libs) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.kotlin.test) + androidTestImplementation(libs.kotlin.time) + androidTestImplementation(libs.test.assertj) + androidTestImplementation(libs.test.junit) +} diff --git a/build-support/build.gradle.kts b/build-support/build.gradle.kts new file mode 100644 index 00000000..cd0c3df4 --- /dev/null +++ b/build-support/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + `kotlin-dsl` + `java-gradle-plugin` +} + +repositories { + mavenCentral() +} + +dependencies { + add("compileOnly", kotlin("gradle-plugin")) + add("compileOnly", kotlin("gradle-plugin-api")) +} + +gradlePlugin { + plugins { + create("build-support") { + id = "build-support" + implementationClass = "BuildSupport" + } + } +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21") +} diff --git a/build-support/settings.gradle.kts b/build-support/settings.gradle.kts new file mode 100644 index 00000000..2fcdac38 --- /dev/null +++ b/build-support/settings.gradle.kts @@ -0,0 +1 @@ +// empty. diff --git a/build-support/src/main/kotlin/BuildSupport.kt b/build-support/src/main/kotlin/BuildSupport.kt new file mode 100644 index 00000000..ad7ad8e4 --- /dev/null +++ b/build-support/src/main/kotlin/BuildSupport.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +import org.gradle.api.Plugin +import org.gradle.api.Project + +class BuildSupport : Plugin<Project> { + override fun apply(project: Project) { + // Do nothing. + } +} diff --git a/build-support/src/main/kotlin/bom.kt b/build-support/src/main/kotlin/bom.kt new file mode 100644 index 00000000..b647714c --- /dev/null +++ b/build-support/src/main/kotlin/bom.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +import java.util.Locale +import org.gradle.api.Project +import org.gradle.api.artifacts.dsl.DependencyConstraintHandler +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper +import org.jetbrains.kotlin.gradle.plugin.KotlinJsPluginWrapper +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMetadataTarget +import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget + +/** + * Collect all the root project's multiplatform targets and add them to the BOM. + * + * Only published subprojects are included. + * + * This supports Kotlin/Multiplatform and Kotlin/JS subprojects. + */ +fun Project.collectBomConstraints() { + val bomConstraints: DependencyConstraintHandler = dependencies.constraints + rootProject.subprojects { + val subproject = this + + subproject.plugins.withId("com.vanniktech.maven.publish.base") { + subproject.plugins.withType<KotlinAndroidPluginWrapper> { + bomConstraints.api(subproject) + } + + subproject.plugins.withType<KotlinJsPluginWrapper> { + bomConstraints.api(subproject) + } + + subproject.plugins.withType<KotlinMultiplatformPluginWrapper> { + subproject.extensions.getByType<KotlinMultiplatformExtension>().targets.all { + bomConstraints.api(dependencyConstraint(this)) + } + } + } + } +} + +/** Returns a string like "com.squareup.okio:okio-iosarm64:3.4.0" for this target. */ +private fun Project.dependencyConstraint(target: KotlinTarget): String { + val artifactId = when (target) { + is KotlinMetadataTarget -> name + is KotlinJsTarget -> "$name-js" + else -> "$name-${target.targetName.toLowerCase(Locale.ROOT)}" + } + return "$group:$artifactId:$version" +} + +private fun DependencyConstraintHandler.api(constraintNotation: Any) = + add("api", constraintNotation) diff --git a/build-support/src/main/kotlin/jvm.kt b/build-support/src/main/kotlin/jvm.kt new file mode 100644 index 00000000..3e409ef2 --- /dev/null +++ b/build-support/src/main/kotlin/jvm.kt @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +// If true - tests should run for a loom environment. +val loomEnabled = System.getProperty("loomEnabled", "false").toBoolean() diff --git a/build-support/src/main/kotlin/kmp.kt b/build-support/src/main/kotlin/kmp.kt new file mode 100644 index 00000000..50737df7 --- /dev/null +++ b/build-support/src/main/kotlin/kmp.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +// If false - JS targets will not be configured in multiplatform projects. +val kmpJsEnabled = System.getProperty("kjs", "true").toBoolean() + +// If false - Native targets will not be configured in multiplatform projects. +val kmpNativeEnabled = System.getProperty("knative", "true").toBoolean() + +// If false - WASM targets will not be configured in multiplatform projects. +val kmpWasmEnabled = System.getProperty("kwasm", "true").toBoolean() diff --git a/build-support/src/main/kotlin/platforms.kt b/build-support/src/main/kotlin/platforms.kt new file mode 100644 index 00000000..4cfb5ae5 --- /dev/null +++ b/build-support/src/main/kotlin/platforms.kt @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.Project +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.withType +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin +import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +fun KotlinMultiplatformExtension.configureOrCreateOkioPlatforms() { + jvm { + } + if (kmpJsEnabled) { + configureOrCreateJsPlatforms() + } + if (kmpNativeEnabled) { + configureOrCreateNativePlatforms() + } + if (kmpWasmEnabled) { + configureOrCreateWasmPlatform() + } +} + +fun KotlinMultiplatformExtension.configureOrCreateNativePlatforms() { + iosX64() + iosArm64() + iosSimulatorArm64() + tvosX64() + tvosArm64() + tvosSimulatorArm64() + watchosArm32() + watchosArm64() + watchosDeviceArm64() + watchosX64() + watchosSimulatorArm64() + // Required to generate tests tasks: https://youtrack.jetbrains.com/issue/KT-26547 + linuxX64() + linuxArm64() + macosX64() + macosArm64() + mingwX64() +} + +val appleTargets = listOf( + "iosArm64", + "iosX64", + "iosSimulatorArm64", + "macosX64", + "macosArm64", + "tvosArm64", + "tvosX64", + "tvosSimulatorArm64", + "watchosArm32", + "watchosArm64", + "watchosDeviceArm64", + "watchosX64", + "watchosSimulatorArm64", +) + +val mingwTargets = listOf( + "mingwX64", +) + +val linuxTargets = listOf( + "linuxX64", + "linuxArm64", +) + +val nativeTargets = appleTargets + linuxTargets + mingwTargets + +val wasmTargets = listOf( + "wasmJs", + "wasmWasi", +) + +/** + * Creates a source set for a directory that isn't already a built-in platform. Use this to create + * custom shared directories like `nonJvmMain` or `unixMain`. + */ +fun NamedDomainObjectContainer<KotlinSourceSet>.createSourceSet( + name: String, + parent: KotlinSourceSet? = null, + children: List<String> = listOf() +): KotlinSourceSet { + val result = create(name) + + if (parent != null) { + result.dependsOn(parent) + } + + val suffix = when { + name.endsWith("Main") -> "Main" + name.endsWith("Test") -> "Test" + else -> error("unexpected source set name: ${name}") + } + + for (childTarget in children) { + val childSourceSet = get("${childTarget}$suffix") + childSourceSet.dependsOn(result) + } + + return result +} + +fun KotlinMultiplatformExtension.configureOrCreateJsPlatforms() { + js { + compilations.all { + kotlinOptions { + moduleKind = "umd" + sourceMap = true + metaInfo = true + } + } + nodejs { + testTask { + useMocha { + timeout = "30s" + } + } + } + browser { + } + } +} + +fun KotlinMultiplatformExtension.configureOrCreateWasmPlatform( + js: Boolean = true, + wasi: Boolean = true, +) { + if (js) { + wasmJs { + nodejs() + } + } + if (wasi) { + wasmWasi { + nodejs() + } + } +} diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 4923686e..00000000 --- a/build.gradle +++ /dev/null @@ -1,120 +0,0 @@ -buildscript { - // If false - JS targets will not be configured in multiplatform projects. - ext.kmpJsEnabled = Boolean.parseBoolean(System.getProperty('kjs', 'true')) - - // If false - Native targets will not be configured in multiplatform projects. - ext.kmpNativeEnabled = Boolean.parseBoolean(System.getProperty('knative', 'true')) - - ext.versions = [ - 'kotlin': '1.4.20', - 'jmhPlugin': '0.5.0', - 'animalSnifferPlugin': '1.5.0', - 'dokka': '1.4.20', - 'jmh': '1.23', - 'animalSniffer': '1.16', - 'junit': '4.12', - 'assertj': '1.7.0', - 'shadowPlugin': '5.2.0', - 'spotless': '5.8.2', - 'ktlint': '0.40.0', - 'bndPlugin': '5.1.2' - ] - - ext.deps = [ - 'android': [ - 'gradlePlugin': "com.android.tools.build:gradle:4.1.1", - 'desugarJdkLibs': "com.android.tools:desugar_jdk_libs:1.1.1", - ], - 'androidx': [ - 'testExtJunit': "androidx.test.ext:junit:1.1.2", - 'testRunner': "androidx.test:runner:1.3.0", - ], - 'kotlin': [ - 'gradlePlugin': "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}", - 'stdLib': [ - 'common': "org.jetbrains.kotlin:kotlin-stdlib-common", - 'jdk8': "org.jetbrains.kotlin:kotlin-stdlib-jdk8", - 'jdk7': "org.jetbrains.kotlin:kotlin-stdlib-jdk7", - 'jdk6': "org.jetbrains.kotlin:kotlin-stdlib", - 'js': "org.jetbrains.kotlin:kotlin-stdlib-js", - ], - 'test': [ - 'common': "org.jetbrains.kotlin:kotlin-test-common", - 'annotations': "org.jetbrains.kotlin:kotlin-test-annotations-common", - 'jdk': "org.jetbrains.kotlin:kotlin-test-junit", - 'js': "org.jetbrains.kotlin:kotlin-test-js", - ], - 'time': 'org.jetbrains.kotlinx:kotlinx-datetime:0.1.1', - ], - 'jmh': [ - 'gradlePlugin': "me.champeau.gradle:jmh-gradle-plugin:${versions.jmhPlugin}", - 'core': "org.openjdk.jmh:jmh-core:${versions.jmh}", - 'generator': "org.openjdk.jmh:jmh-generator-annprocess:${versions.jmh}", - ], - 'animalSniffer': [ - 'gradlePlugin': "ru.vyarus:gradle-animalsniffer-plugin:${versions.animalSnifferPlugin}", - 'annotations': "org.codehaus.mojo:animal-sniffer-annotations:${versions.animalSniffer}", - ], - 'japicmp': 'me.champeau.gradle:japicmp-gradle-plugin:0.2.8', - 'dokka': "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}", - 'shadow': "com.github.jengelman.gradle.plugins:shadow:${versions.shadowPlugin}", - 'spotless': "com.diffplug.spotless:spotless-plugin-gradle:${versions.spotless}", - 'bnd': "biz.aQute.bnd:biz.aQute.bnd.gradle:${versions.bndPlugin}", - 'test': [ - 'junit': "junit:junit:${versions.junit}", - 'assertj': "org.assertj:assertj-core:${versions.assertj}", - ] - ] - - dependencies { - classpath deps.android.gradlePlugin - classpath deps.kotlin.gradlePlugin - classpath deps.animalSniffer.gradlePlugin - classpath deps.japicmp - classpath deps.dokka - classpath deps.shadow - classpath deps.jmh.gradlePlugin - classpath deps.spotless - classpath deps.bnd - // https://github.com/melix/japicmp-gradle-plugin/issues/36 - classpath 'com.google.guava:guava:28.2-jre' - } - - repositories { - mavenCentral() - gradlePluginPortal() - jcenter() - google() - maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } - maven { url 'https://kotlin.bintray.com/kotlinx/' } - } -} - -// when scripts are applied the buildscript classes are not accessible directly therefore we save the class here to make it accessible -ext.bndBundleTaskConventionClass = aQute.bnd.gradle.BundleTaskConvention - -allprojects { - group = GROUP - version = VERSION_NAME -} - -subprojects { - repositories { - mavenCentral() - jcenter() - google() - maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } - maven { url 'https://kotlin.bintray.com/kotlinx/' } - } - - apply plugin: "com.diffplug.spotless" - - spotless { - kotlin { - target("**/*.kt") - ktlint(versions.ktlint).userData([indent_size: '2']) - trimTrailingWhitespace() - endWithNewline() - } - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..f564d88a --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,248 @@ +import aQute.bnd.gradle.BundleTaskConvention +import com.diffplug.gradle.spotless.SpotlessExtension +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import com.vanniktech.maven.publish.SonatypeHost +import groovy.util.Node +import groovy.util.NodeList +import java.nio.charset.StandardCharsets +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED +import org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED +import org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED +import org.gradle.api.tasks.testing.logging.TestLogEvent.STARTED +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform +import org.jetbrains.dokka.gradle.DokkaTask +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin +import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("build-support").apply(false) +} + +buildscript { + dependencies { + classpath(libs.android.gradle.plugin) + classpath(libs.dokka) + classpath(libs.jmh.gradle.plugin) + classpath(libs.binaryCompatibilityValidator) + classpath(libs.spotless) + classpath(libs.bnd) + classpath(libs.vanniktech.publish.plugin) + } + + repositories { + mavenCentral() + gradlePluginPortal() + google() + } +} + +apply(plugin = "com.vanniktech.maven.publish.base") + +// When scripts are applied the buildscript classes are not accessible directly therefore we save +// the class here to make it accessible. +ext.set("bndBundleTaskConventionClass", BundleTaskConvention::class.java) + +allprojects { + group = project.property("GROUP") as String + version = project.property("VERSION_NAME") as String + + repositories { + mavenCentral() + google() + } + + tasks.withType<DokkaTask>().configureEach { + dokkaSourceSets.configureEach { + reportUndocumented.set(false) + skipDeprecated.set(true) + jdkVersion.set(8) + perPackageOption { + matchingRegex.set("com\\.squareup.okio.*") + suppress.set(true) + } + perPackageOption { + matchingRegex.set("okio\\.internal.*") + suppress.set(true) + } + } + + if (name == "dokkaHtml") { + outputDirectory.set(file("${rootDir}/docs/3.x/${project.name}")) + pluginsMapConfiguration.set( + mapOf( + "org.jetbrains.dokka.base.DokkaBase" to """ + { + "customStyleSheets": [ + "${rootDir.toString().replace('\\', '/')}/docs/css/dokka-logo.css" + ], + "customAssets" : [ + "${rootDir.toString().replace('\\', '/')}/docs/images/icon-square.png" + ] + } + """.trimIndent() + ) + ) + } + } + + plugins.withId("com.vanniktech.maven.publish.base") { + configure<PublishingExtension> { + repositories { + /** + * Want to push to an internal repository for testing? Set the following properties in + * `~/.gradle/gradle.properties`. + * + * internalMavenUrl=YOUR_INTERNAL_MAVEN_REPOSITORY_URL + * internalMavenUsername=YOUR_USERNAME + * internalMavenPassword=YOUR_PASSWORD + */ + val internalUrl = providers.gradleProperty("internalUrl") + if (internalUrl.isPresent) { + maven { + name = "internal" + setUrl(internalUrl) + credentials(PasswordCredentials::class) + } + } + } + } + val publishingExtension = extensions.getByType(PublishingExtension::class.java) + configure<MavenPublishBaseExtension> { + publishToMavenCentral(SonatypeHost.S01, automaticRelease = true) + signAllPublications() + pom { + description.set("A modern I/O library for Android, Java, and Kotlin Multiplatform.") + name.set(project.name) + url.set("https://github.com/square/okio/") + licenses { + license { + name.set("The Apache Software License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("repo") + } + } + scm { + url.set("https://github.com/square/okio/") + connection.set("scm:git:git://github.com/square/okio.git") + developerConnection.set("scm:git:ssh://git@github.com/square/okio.git") + } + developers { + developer { + id.set("square") + name.set("Square, Inc.") + } + } + } + + // Configure the kotlinMultiplatform artifact to depend on the JVM artifact in pom.xml only. + // This hack allows Maven users to continue using our original Okio artifact names (like + // com.squareup.okio:okio:3.x.y) even though we changed that artifact from JVM-only to Kotlin + // Multiplatform. Note that module.json doesn't need this hack. + val mavenPublications = publishingExtension.publications.withType<MavenPublication>() + mavenPublications.configureEach { + if (name != "jvm") return@configureEach + val jvmPublication = this + val kmpPublication = mavenPublications.getByName("kotlinMultiplatform") + kmpPublication.pom.withXml { + val root = asNode() + val dependencies = (root["dependencies"] as NodeList).firstOrNull() as Node? + ?: root.appendNode("dependencies") + for (child in dependencies.children().toList()) { + dependencies.remove(child as Node) + } + dependencies.appendNode("dependency").apply { + appendNode("groupId", jvmPublication.groupId) + appendNode("artifactId", jvmPublication.artifactId) + appendNode("version", jvmPublication.version) + appendNode("scope", "compile") + } + } + } + } + } +} + +subprojects { + apply(plugin = "com.diffplug.spotless") + configure<SpotlessExtension> { + kotlin { + target("**/*.kt") + ktlint(libs.versions.ktlint.get()) + } + } + + tasks.withType<KotlinCompile>().configureEach { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + @Suppress("SuspiciousCollectionReassignment") + freeCompilerArgs += "-Xjvm-default=all" + } + } + + tasks.withType<JavaCompile> { + options.encoding = StandardCharsets.UTF_8.toString() + sourceCompatibility = JavaVersion.VERSION_1_8.toString() + targetCompatibility = JavaVersion.VERSION_1_8.toString() + } + + val testJavaVersion = System.getProperty("test.java.version", "19").toInt() + tasks.withType<Test> { + val javaToolchains = project.extensions.getByType<JavaToolchainService>() + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(testJavaVersion)) + }) + + testLogging { + events(STARTED, PASSED, SKIPPED, FAILED) + exceptionFormat = TestExceptionFormat.FULL + showStandardStreams = false + } + + if (loomEnabled) { + jvmArgs = jvmArgs!! + listOf( + "-Djdk.tracePinnedThread=full", + "--enable-preview", + "-DloomEnabled=true" + ) + } + } + + tasks.withType<AbstractArchiveTask>().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + } + + normalization { + runtimeClasspath { + metaInf { + ignoreAttribute("Bnd-LastModified") + } + } + } +} + +/** + * Select a NodeJS version with WASI and WASM GC. + * https://github.com/Kotlin/kotlin-wasm-examples/blob/main/wasi-example/build.gradle.kts + */ +plugins.withType<NodeJsRootPlugin> { + extensions.getByType<NodeJsRootExtension>().apply { + if (DefaultNativePlatform.getCurrentOperatingSystem().isWindows) { + // We're waiting for a Windows build of NodeJS that can do WASM GC + WASI. + nodeVersion = "21.4.0" + } else { + nodeVersion = "21.0.0-v8-canary202309143a48826a08" + nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary" + } + } + // Suppress an error because yarn doesn't like our Node version string. + // warning You are using Node "21.0.0-v8-canary202309143a48826a08" which is not supported and + // may encounter bugs or unexpected behavior. + // error typescript@5.0.4: The engine "node" is incompatible with this module. + tasks.withType<KotlinNpmInstallTask>().all { + args += "--ignore-engines" + } +} diff --git a/docs/css/app.css b/docs/css/app.css index 48136b7e..2667de1e 100644 --- a/docs/css/app.css +++ b/docs/css/app.css @@ -27,7 +27,7 @@ body, input { font-family: cash-market,"Helvetica Neue",helvetica,sans-serif; line-height: normal; font-weight: bold; - color: #353535; + color: var(--md-default-fg-color); } button.dl { diff --git a/docs/css/dokka-logo.css b/docs/css/dokka-logo.css index ae3a99e6..d05d9701 100644 --- a/docs/css/dokka-logo.css +++ b/docs/css/dokka-logo.css @@ -1,6 +1,3 @@ #logo { - background-image: url(../images/logo-square.png); - background-size: auto; - padding-top: unset; - height: 60px; + display: none; } diff --git a/docs/file_system.md b/docs/file_system.md new file mode 100644 index 00000000..49207d9f --- /dev/null +++ b/docs/file_system.md @@ -0,0 +1,125 @@ +File System +=========== + +Okio's file system is designed to be easy, testable, multiplatform, and efficient. + +### Easy + +Reading and writing files is concise yet flexible. + +```kotlin +val path = "README.md".toPath() + +val readmeContent = FileSystem.SYSTEM.read(path) { + readUtf8() +} + +val updatedContent = readmeContent.replace("red", "blue") + +FileSystem.SYSTEM.write(path) { + writeUtf8(updatedContent) +} +``` + + +### Testable + +It's easy to swap out the real file system with a fake. This makes tests run faster and more +reliably. + +```kotlin +val fileSystem = FakeFileSystem() +val userHome = "/Users/sandy".toPath() +val gitConfig = userHome / ".gitconfig" + +fileSystem.createDirectories(userHome) +val original = """ + |[user] + | email = sandy@example.com + |""".trimMargin() +fileSystem.write(gitConfig) { writeUtf8(original) } + +GitConfigFixer(fileSystem).fix(userHome) + +val expected = """ + |[user] + | email = sandy@example.com + |[diff] + | renames = true + | indentHeuristic = on + """.trimIndent() +assertEquals(expected, fileSystem.read(gitConfig) { readUtf8() }) +``` + +With `ForwardingFileSystem` you can easily inject faults to confirm your program is graceful even +when the user's disk fills up. + + +### Multiplatform + +Okio’s `Path` class supports Windows-style (like `C:\autoexec.bat`) and UNIX-style paths +(like `/etc/passwd`). It supports manipulating Windows paths on UNIX, and UNIX paths on Windows. + +The system `FileSystem` abstracts over these platform APIs: + + * Android API levels <26: [java.io.File](https://developer.android.com/reference/java/io/File) + * Java and Android API level 26+: [java.nio.file](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/file/FileSystem.html) + * Linux: [man pages](https://www.kernel.org/doc/man-pages/) + * UNIX: [stdio.h](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdio.h.html) + * Windows: [fileapi.h](https://docs.microsoft.com/en-us/windows/win32/api/fileapi/) + * Node.js: [file system](https://nodejs.org/api/fs.html) + + +### Efficient + +Read and write operations integrate with Okio buffers to reduce the number of system calls. + +It exposes high-level operations like `atomicMove()` and `metadata` to get the OS to do all the work +when appropriate. + + +## Known Issues + + +Okio's implementation is constrained by the capabilities its underlying APIs. This page is an +overview of these limitations. + + +### All Platforms + + * There are no APIs for file permissions, watches, volume management, memory mapping, or locking. + * Paths that cannot be represented as UTF-8 strings are unsupported. The underlying APIs that Okio + calls through, including `java.io.File`, all treat paths as strings. + + +### Kotlin/JVM + +#### On Android, API level less than 26: + + * Creating and accessing symlinks is unsupported. + + +#### On Windows: + + * `FileSystem.atomicMove()` fails if the target file already exists. + + +### Kotlin/Native + + * FakeFileSystem does not support concurrent use. We are [holding off on this][fake_fs_concurrency] + until the upcoming memory model is released. + +#### On Windows: + + * Creating and accessing symlinks is unsupported. + + +### Kotlin/JS + + * NodeJsFileSystem's `source()` and `sink()` cannot access UNIX pipes. + * Instead of returning null, `NodeJsFileSystem.metadataOrNull()` throws `IOException` if the path + is invalid. (In the Node.js API there's no mechanism to differentiate between a failure to read + a valid path and a rejection of an invalid path.) + + +[fake_fs_concurrency]: https://github.com/square/okio/issues/950 diff --git a/docs/index.md b/docs/index.md index d69c409d..4fb96f5e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -77,1035 +77,44 @@ works and how Okio does it. [Ok Multiplatform!][ok_multiplatform_talk] ([slides][ok_multiplatform_slides]): How we changed Okio’s implementation language from Java to Kotlin. +[Nerding Out On Okio][apis_talk]: The story of the Okio APIs, their design and tradeoffs, as well +as implementation notes with animated marbles diagrams. + Requirements ------------ -Okio supports Android 4.0.3+ (API level 15+) and Java 7+. +Okio 2.x supports Android 4.0.3+ (API level 15+) and Java 7+. + +Okio 3.x supports Android 4.0.3+ (API level 15+) and Java 8+. Okio depends on the [Kotlin standard library][kotlin]. It is a small library with strong backward-compatibility. -Recipes -------- - -We've written some recipes that demonstrate how to solve common problems with -Okio. Read through them to learn about how everything works together. -Cut-and-paste these examples freely; that's what they're for. - -### Read a text file line-by-line ([Java][ReadFileLineByLine]/[Kotlin][ReadFileLineByLineKt]) - -Use `Okio.source(File)` to open a source stream to read a file. The returned -`Source` interface is very small and has limited uses. Instead we wrap the -source with a buffer. This has two benefits: - - * **It makes the API more powerful.** Instead of the basic methods offered by - `Source`, `BufferedSource` has dozens of methods to address most common - problems concisely. - - * **It makes your program run faster.** Buffering allows Okio to get more done - with fewer I/O operations. - -Each `Source` that is opened needs to be closed. The code that opens the stream -is responsible for making sure it is closed. - -=== "Java" - - Here we use Java's `try` blocks to close our sources automatically. - - ```java - public void readLines(File file) throws IOException { - try (Source fileSource = Okio.source(file); - BufferedSource bufferedSource = Okio.buffer(fileSource)) { - - while (true) { - String line = bufferedSource.readUtf8Line(); - if (line == null) break; - - if (line.contains("square")) { - System.out.println(line); - } - } - - } - } - ``` - -=== "Kotlin" - - Note that static `Okio` methods become extension functions (`Okio.source(file)` => - `file.source()`), and `use` is used to automatically close the streams: - - ```kotlin - @Throws(IOException::class) - fun readLines(file: File) { - file.source().use { fileSource -> - fileSource.buffer().use { bufferedFileSource -> - while (true) { - val line = bufferedFileSource.readUtf8Line() ?: break - if ("square" in line) { - println(line) - } - } - } - } - } - ``` - -The `readUtf8Line()` API reads all of the data until the next line delimiter – -either `\n`, `\r\n`, or the end of the file. It returns that data as a string, -omitting the delimiter at the end. When it encounters empty lines the method -will return an empty string. If there isn’t any more data to read it will -return null. - -The above program can be written more compactly by inlining the `fileSource` -variable and by using a fancy `for` loop instead of a `while`: - -```java -public void readLines(File file) throws IOException { - try (BufferedSource source = Okio.buffer(Okio.source(file))) { - for (String line; (line = source.readUtf8Line()) != null; ) { - if (line.contains("square")) { - System.out.println(line); - } - } - } -} -``` - -In Kotlin, we can wrap invocations of `source.readUtf8Line()` into the `generateSequence` builder to -create a sequence of lines that will end once null is returned. Plus, transforming streams is easy -thanks to the extension functions: - -```kotlin -@Throws(IOException::class) -fun readLines(file: File) { - file.source().buffer().use { source -> - generateSequence { source.readUtf8Line() } - .filter { line -> "square" in line } - .forEach(::println) - } -} -``` - -The `readUtf8Line()` method is suitable for parsing most files. For certain -use-cases you may also consider `readUtf8LineStrict()`. It is similar but it -requires that each line is terminated by `\n` or `\r\n`. If it encounters the -end of the file before that it will throw an `EOFException`. The strict variant -also permits a byte limit to defend against malformed input. - -```java -public void readLines(File file) throws IOException { - try (BufferedSource source = Okio.buffer(Okio.source(file))) { - while (!source.exhausted()) { - String line = source.readUtf8LineStrict(1024L); - if (line.contains("square")) { - System.out.println(line); - } - } - } -} -``` - -Here's a similar example written in Kotlin: - -```kotlin -@Throws(IOException::class) -fun readLines(file: File) { - file.source().buffer().use { source -> - while (!source.exhausted()) { - val line = source.readUtf8LineStrict(1024) - if ("square" in line) { - println(line) - } - } - } -} -``` - -### Write a text file ([Java][WriteFile]/[Kotlin][WriteFileKt]) - -Above we used a `Source` and a `BufferedSource` to read a file. To write, we use -a `Sink` and a `BufferedSink`. The advantages of buffering are the same: a more -capable API and better performance. - -```java -public void writeEnv(File file) throws IOException { - try (Sink fileSink = Okio.sink(file); - BufferedSink bufferedSink = Okio.buffer(fileSink)) { - - for (Map.Entry<String, String> entry : System.getenv().entrySet()) { - bufferedSink.writeUtf8(entry.getKey()); - bufferedSink.writeUtf8("="); - bufferedSink.writeUtf8(entry.getValue()); - bufferedSink.writeUtf8("\n"); - } - - } -} -``` - -There isn’t an API to write a line of input; instead we manually insert our own -newline character. Most programs should hardcode `"\n"` as the newline -character. In rare situations you may use `System.lineSeparator()` instead of -`"\n"`: it returns `"\r\n"` on Windows and `"\n"` everywhere else. - -We can write the above program more compactly by inlining the `fileSink` -variable and by taking advantage of method chaining: - -=== "Java" - - ```Java - public void writeEnv(File file) throws IOException { - try (BufferedSink sink = Okio.buffer(Okio.sink(file))) { - for (Map.Entry<String, String> entry : System.getenv().entrySet()) { - sink.writeUtf8(entry.getKey()) - .writeUtf8("=") - .writeUtf8(entry.getValue()) - .writeUtf8("\n"); - } - } - } - ``` - -=== "Kotlin" - - ```Kotlin - @Throws(IOException::class) - fun writeEnv(file: File) { - file.sink().buffer().use { sink -> - for ((key, value) in System.getenv()) { - sink.writeUtf8(key) - sink.writeUtf8("=") - sink.writeUtf8(value) - sink.writeUtf8("\n") - } - } - } - ``` - -In the above code we make four calls to `writeUtf8()`. Making four calls is -more efficient than the code below because the VM doesn’t have to create and -garbage collect a temporary string. - -```java -sink.writeUtf8(entry.getKey() + "=" + entry.getValue() + "\n"); // Slower! -``` - -### UTF-8 ([Java][ExploreCharsets]/[Kotlin][ExploreCharsetsKt]) - -In the above APIs you can see that Okio really likes UTF-8. Early computer -systems suffered many incompatible character encodings: ISO-8859-1, ShiftJIS, -ASCII, EBCDIC, etc. Writing software to support multiple character sets was -awful and we didn’t even have emoji! Today we're lucky that the world has -standardized on UTF-8 everywhere, with some rare uses of other charsets in -legacy systems. - -If you need another character set, `readString()` and `writeString()` are there -for you. These methods require that you specify a character set. Otherwise you -may accidentally create data that is only readable by the local computer. Most -programs should use the UTF-8 methods only. - -When encoding strings you need to be mindful of the different ways that strings -are represented and encoded. When a glyph has an accent or another adornment -it may be represented as a single complex code point (`é`) or as a simple code -point (`e`) followed by its modifiers (`´`). When the entire glyph is a single -code point that’s called [NFC][nfc]; when it’s multiple it’s [NFD][nfd]. - -Though we use UTF-8 whenever we read or write strings in I/O, when they are in -memory Java Strings use an obsolete character encoding called UTF-16. It is a -bad encoding because it uses a 16-bit `char` for most characters, but some don’t -fit. In particular, most emoji use two Java chars. This is problematic because -`String.length()` returns a surprising result: the number of UTF-16 chars and -not the natural number of glyphs. - -| | Café 🍩 | Café 🍩 | -| --------------------: | :---------------------------| :------------------------------| -| Form | [NFC][nfc] | [NFD][nfd] | -| Code Points | `c a f é ␣ 🍩 ` | `c a f e ´ ␣ 🍩 ` | -| UTF-8 bytes | `43 61 66 c3a9 20 f09f8da9` | `43 61 66 65 cc81 20 f09f8da9` | -| String.codePointCount | 6 | 7 | -| String.length | 7 | 8 | -| Utf8.size | 10 | 11 | - -For the most part Okio lets you ignore these problems and focus on your data. -But when you need them, there are convenient APIs for dealing with low-level -UTF-8 strings. - -Use `Utf8.size()` to count the number of bytes required to encode a string as -UTF-8 without actually encoding it. This is handy in length-prefixed encodings -like protocol buffers. - -Use `BufferedSource.readUtf8CodePoint()` to read a single variable-length code -point, and `BufferedSink.writeUtf8CodePoint()` to write one. - -### Golden Values ([Java][GoldenValue]/[Kotlin][GoldenValueKt]) - -Okio likes testing. The library itself is heavily tested, and it has features -that are often helpful when testing application code. One pattern we’ve found to -be quite useful is “golden value” testing. The goal of such tests is to confirm -that data encoded with earlier versions of a program can safely be decoded by -the current program. - -We’ll illustrate this by encoding a value using Java Serialization. Though we -must disclaim that Java Serialization is an awful encoding system and most -programs should prefer other formats like JSON or protobuf! In any case, here’s -a method that takes an object, serializes it, and returns the result as a -`ByteString`: - -=== "Java" - - ```Java - private ByteString serialize(Object o) throws IOException { - Buffer buffer = new Buffer(); - try (ObjectOutputStream objectOut = new ObjectOutputStream(buffer.outputStream())) { - objectOut.writeObject(o); - } - return buffer.readByteString(); - } - ``` - -=== "Kotlin" - - ```Kotlin - @Throws(IOException::class) - private fun serialize(o: Any?): ByteString { - val buffer = Buffer() - ObjectOutputStream(buffer.outputStream()).use { objectOut -> - objectOut.writeObject(o) - } - return buffer.readByteString() - } - ``` - -There’s a lot going on here. - -1. We create a buffer as a holding space for our serialized data. It’s a convenient - replacement for `ByteArrayOutputStream`. - -2. We ask the buffer for its output stream. Writes to a buffer or its output stream - always append data to the end of the buffer. - -3. We create an `ObjectOutputStream` (the encoding API for Java serialization) and - write our object. The try block takes care of closing the stream for us. Note - that closing a buffer has no effect. - -4. Finally we read a byte string from the buffer. The `readByteString()` method - allows us to specify how many bytes to read; here we don’t specify a count in - order to read the entire thing. Reads from a buffer always consume data from - the front of the buffer. - -With our `serialize()` method handy we are ready to compute and print a golden -value. - -=== "Java" - - ```Java - Point point = new Point(8.0, 15.0); - ByteString pointBytes = serialize(point); - System.out.println(pointBytes.base64()); - ``` - -=== "Kotlin" - - ```Kotlin - val point = Point(8.0, 15.0) - val pointBytes = serialize(point) - println(pointBytes.base64()) - ``` - -We print the `ByteString` as [base64][base64] because it’s a compact format -that’s suitable for embedding in a test case. The program prints this: - -``` -rO0ABXNyAB5va2lvLnNhbXBsZXMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA -``` - -That’s our golden value! We can embed it in our test case using base64 again -to convert it back into a `ByteString`: - -=== "Java" - - ```Java - ByteString goldenBytes = ByteString.decodeBase64("rO0ABXNyAB5va2lvLnNhbXBsZ" - + "XMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuA" - + "AAAAAAA"); - ``` - -=== "Kotlin" - - ```Kotlin - val goldenBytes = ("rO0ABXNyACRva2lvLnNhbXBsZXMuS290bGluR29sZGVuVmFsdWUkUG9pbnRF9yaY7cJ9EwIAA" + - "kQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA").decodeBase64() - ``` - -The next step is to deserialize the `ByteString` back into our value class. This -method reverses the `serialize()` method above: we append a byte string to a -buffer then consume it using an `ObjectInputStream`: - -=== "Java" - - ```Java - private Object deserialize(ByteString byteString) throws IOException, ClassNotFoundException { - Buffer buffer = new Buffer(); - buffer.write(byteString); - try (ObjectInputStream objectIn = new ObjectInputStream(buffer.inputStream())) { - return objectIn.readObject(); - } - } - ``` - -=== "Kotlin" - - ```Kotlin - @Throws(IOException::class, ClassNotFoundException::class) - private fun deserialize(byteString: ByteString): Any? { - val buffer = Buffer() - buffer.write(byteString) - ObjectInputStream(buffer.inputStream()).use { objectIn -> - return objectIn.readObject() - } - } - ``` - -Now we can test the decoder against the golden value: - -=== "Java" - - ```Java - ByteString goldenBytes = ByteString.decodeBase64("rO0ABXNyAB5va2lvLnNhbXBsZ" - + "XMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuA" - + "AAAAAAA"); - Point decoded = (Point) deserialize(goldenBytes); - assertEquals(new Point(8.0, 15.0), decoded); - ``` - -=== "Kotlin" - - ```Kotlin - val goldenBytes = ("rO0ABXNyACRva2lvLnNhbXBsZXMuS290bGluR29sZGVuVmFsdWUkUG9pbnRF9yaY7cJ9EwIAA" + - "kQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA").decodeBase64()!! - val decoded = deserialize(goldenBytes) as Point - assertEquals(point, decoded) - ``` - -With this test we can change the serialization of the `Point` class without -breaking compatibility. - - -### Write a binary file ([Java][BitmapEncoder]/[Kotlin][BitmapEncoderKt]) - -Encoding a binary file is not unlike encoding a text file. Okio uses the same -`BufferedSink` and `BufferedSource` bytes for both. This is handy for binary -formats that include both byte and character data. - -Writing binary data is more hazardous than text because if you make a mistake it -is often quite difficult to diagnose. Avoid such mistakes by being careful -around these traps: - - * **The width of each field.** This is the number of bytes used. Okio doesn't - include a mechanism to emit partial bytes. If you need that, you’ll need to - do your own bit shifting and masking before writing. - - * **The endianness of each field.** All fields that have more than one byte - have _endianness_: whether the bytes are ordered most-significant to least - (big endian) or least-significant to most (little endian). Okio uses the `Le` - suffix for little-endian methods; methods without a suffix are big-endian. - - * **Signed vs. Unsigned.** Java doesn’t have unsigned primitive types (except - for `char`!) so coping with this is often something that happens at the - application layer. To make this a little easier Okio accepts `int` types for - `writeByte()` and `writeShort()`. You can pass an “unsigned” byte like 255 - and Okio will do the right thing. - -| Method | Width | Endianness | Value | Encoded Value | -| :----------- | ----: | :--------- | --------------: | :------------------------ | -| writeByte | 1 | | 3 | `03` | -| writeShort | 2 | big | 3 | `00 03` | -| writeInt | 4 | big | 3 | `00 00 00 03` | -| writeLong | 8 | big | 3 | `00 00 00 00 00 00 00 03` | -| writeShortLe | 2 | little | 3 | `03 00` | -| writeIntLe | 4 | little | 3 | `03 00 00 00` | -| writeLongLe | 8 | little | 3 | `03 00 00 00 00 00 00 00` | -| writeByte | 1 | | Byte.MAX_VALUE | `7f` | -| writeShort | 2 | big | Short.MAX_VALUE | `7f ff` | -| writeInt | 4 | big | Int.MAX_VALUE | `7f ff ff ff` | -| writeLong | 8 | big | Long.MAX_VALUE | `7f ff ff ff ff ff ff ff` | -| writeShortLe | 2 | little | Short.MAX_VALUE | `ff 7f` | -| writeIntLe | 4 | little | Int.MAX_VALUE | `ff ff ff 7f` | -| writeLongLe | 8 | little | Long.MAX_VALUE | `ff ff ff ff ff ff ff 7f` | - -This code encodes a bitmap following the [BMP file format][bmp]. - -=== "Java" - - ```Java - void encode(Bitmap bitmap, BufferedSink sink) throws IOException { - int height = bitmap.height(); - int width = bitmap.width(); - - int bytesPerPixel = 3; - int rowByteCountWithoutPadding = (bytesPerPixel * width); - int rowByteCount = ((rowByteCountWithoutPadding + 3) / 4) * 4; - int pixelDataSize = rowByteCount * height; - int bmpHeaderSize = 14; - int dibHeaderSize = 40; - - // BMP Header - sink.writeUtf8("BM"); // ID. - sink.writeIntLe(bmpHeaderSize + dibHeaderSize + pixelDataSize); // File size. - sink.writeShortLe(0); // Unused. - sink.writeShortLe(0); // Unused. - sink.writeIntLe(bmpHeaderSize + dibHeaderSize); // Offset of pixel data. - - // DIB Header - sink.writeIntLe(dibHeaderSize); - sink.writeIntLe(width); - sink.writeIntLe(height); - sink.writeShortLe(1); // Color plane count. - sink.writeShortLe(bytesPerPixel * Byte.SIZE); - sink.writeIntLe(0); // No compression. - sink.writeIntLe(16); // Size of bitmap data including padding. - sink.writeIntLe(2835); // Horizontal print resolution in pixels/meter. (72 dpi). - sink.writeIntLe(2835); // Vertical print resolution in pixels/meter. (72 dpi). - sink.writeIntLe(0); // Palette color count. - sink.writeIntLe(0); // 0 important colors. - - // Pixel data. - for (int y = height - 1; y >= 0; y--) { - for (int x = 0; x < width; x++) { - sink.writeByte(bitmap.blue(x, y)); - sink.writeByte(bitmap.green(x, y)); - sink.writeByte(bitmap.red(x, y)); - } - - // Padding for 4-byte alignment. - for (int p = rowByteCountWithoutPadding; p < rowByteCount; p++) { - sink.writeByte(0); - } - } - } - ``` - -=== "Kotlin" - - ```Kotlin - @Throws(IOException::class) - fun encode(bitmap: Bitmap, sink: BufferedSink) { - val height = bitmap.height - val width = bitmap.width - val bytesPerPixel = 3 - val rowByteCountWithoutPadding = bytesPerPixel * width - val rowByteCount = (rowByteCountWithoutPadding + 3) / 4 * 4 - val pixelDataSize = rowByteCount * height - val bmpHeaderSize = 14 - val dibHeaderSize = 40 - - // BMP Header - sink.writeUtf8("BM") // ID. - sink.writeIntLe(bmpHeaderSize + dibHeaderSize + pixelDataSize) // File size. - sink.writeShortLe(0) // Unused. - sink.writeShortLe(0) // Unused. - sink.writeIntLe(bmpHeaderSize + dibHeaderSize) // Offset of pixel data. - - // DIB Header - sink.writeIntLe(dibHeaderSize) - sink.writeIntLe(width) - sink.writeIntLe(height) - sink.writeShortLe(1) // Color plane count. - sink.writeShortLe(bytesPerPixel * Byte.SIZE_BITS) - sink.writeIntLe(0) // No compression. - sink.writeIntLe(16) // Size of bitmap data including padding. - sink.writeIntLe(2835) // Horizontal print resolution in pixels/meter. (72 dpi). - sink.writeIntLe(2835) // Vertical print resolution in pixels/meter. (72 dpi). - sink.writeIntLe(0) // Palette color count. - sink.writeIntLe(0) // 0 important colors. - - // Pixel data. - for (y in height - 1 downTo 0) { - for (x in 0 until width) { - sink.writeByte(bitmap.blue(x, y)) - sink.writeByte(bitmap.green(x, y)) - sink.writeByte(bitmap.red(x, y)) - } - - // Padding for 4-byte alignment. - for (p in rowByteCountWithoutPadding until rowByteCount) { - sink.writeByte(0) - } - } - } - ``` - -The trickiest part of this program is the format’s required padding. The BMP -format expects each row to begin on a 4-byte boundary so it is necessary to add -zeros to maintain the alignment. - -Encoding other binary formats is usually quite similar. Some tips: - - * Write tests with golden values! Confirming that your program emits the - expected result can make debugging easier. - * Use `Utf8.size()` to compute the number of bytes of an encoded string. This - is essential for length-prefixed formats. - * Use `Float.floatToIntBits()` and `Double.doubleToLongBits()` to encode - floating point values. - - -### Communicate on a Socket ([Java][SocksProxyServer]/[Kotlin][SocksProxyServerKt]) - -Sending and receiving data over the network is a bit like writing and reading -files. We use `BufferedSink` to encode output and `BufferedSource` to decode -input. Like files, network protocols can be text, binary, or a mix of both. But -there are also some substantial differences between the network and the -file system. - -With a file you’re either reading or writing but with the network you can do -both! Some protocols handle this by taking turns: write a request, read a -response, repeat. You can implement this kind of protocol with a single thread. -In other protocols you may read and write simultaneously. Typically you’ll want -one dedicated thread for reading. For writing you can use either a dedicated -thread or use `synchronized` so that multiple threads can share a sink. Okio’s -streams are not safe for concurrent use. - -Sinks buffer outbound data to minimize I/O operations. This is efficient but it -means you must manually call `flush()` to transmit data. Typically -message-oriented protocols flush after each message. Note that Okio will -automatically flush when the buffered data exceeds some threshold. This is -intended to save memory and you shouldn’t rely on it for interactive protocols. - -Okio builds on `java.io.Socket` for connectivity. Create your socket as a server -or as a client, then use `Okio.source(Socket)` to read and `Okio.sink(Socket)` -to write. These APIs also work with `SSLSocket`. You should use SSL unless you -have a very good reason not to! - -Cancel a socket from any thread by calling `Socket.close()`; this will cause its -sources and sinks to immediately fail with an `IOException`. You can also -configure timeouts for all socket operations. You don’t need a reference to the -socket to adjust timeouts: `Source` and `Sink` expose timeouts directly. This -API works even if the streams are decorated. - -As a complete example of networking with Okio we wrote a [basic SOCKS -proxy][SocksProxyServer] server. Some highlights: - -=== "Java" - - ```Java - Socket fromSocket = ... - BufferedSource fromSource = Okio.buffer(Okio.source(fromSocket)); - BufferedSink fromSink = Okio.buffer(Okio.sink(fromSocket)); - ``` - -=== "Kotlin" - - ```Kotlin - val fromSocket: Socket = ... - val fromSource = fromSocket.source().buffer() - val fromSink = fromSocket.sink().buffer() - ``` - -Creating sources and sinks for sockets is the same as creating them for files. -Once you create a `Source` or `Sink` for a socket you must not use its -`InputStream` or `OutputStream`, respectively. - -=== "Java" - - ```Java - Buffer buffer = new Buffer(); - for (long byteCount; (byteCount = source.read(buffer, 8192L)) != -1; ) { - sink.write(buffer, byteCount); - sink.flush(); - } - ``` - -=== "Kotlin" - - ```Kotlin - val buffer = Buffer() - var byteCount: Long - while (source.read(buffer, 8192L).also { byteCount = it } != -1L) { - sink.write(buffer, byteCount) - sink.flush() - } - ``` - -The above loop copies data from the source to the sink, flushing after each -read. If we didn’t need the flushing we could replace this loop with a single -call to `BufferedSink.writeAll(Source)`. - -The `8192` argument to `read()` is the maximum number of bytes to read before -returning. We could have passed any value here, but we like 8 KiB because that’s -the largest value Okio can do in a single system call. Most of the time -application code doesn’t need to deal with such limits! - -=== "Java" - - ```Java - int addressType = fromSource.readByte() & 0xff; - int port = fromSource.readShort() & 0xffff; - ``` - -=== "Kotlin" - - ```Kotlin - val addressType = fromSource.readByte().toInt() and 0xff - val port = fromSource.readShort().toInt() and 0xffff - ``` - -Okio uses signed types like `byte` and `short`, but often protocols want -unsigned values. The bitwise `&` operator is Java’s preferred idiom to convert -a signed value into an unsigned value. Here’s a cheat sheet for bytes, shorts, -and ints: - -| Type | Signed Range | Unsigned Range | Signed to Unsigned | -| :---- | :---------------------------: | :--------------- | :-------------------------- | -| byte | -128..127 | 0..255 | `int u = s & 0xff;` | -| short | -32,768..32,767 | 0..65,535 | `int u = s & 0xffff;` | -| int | -2,147,483,648..2,147,483,647 | 0..4,294,967,295 | `long u = s & 0xffffffffL;` | - -Java has no primitive type that can represent unsigned longs. - - -### Hashing ([Java][Hashing]/[Kotlin][HashingKt]) - -We’re bombarded by hashing in our lives as Java programmers. Early on we're introduced to the -`hashCode()` method, something we know we need to override otherwise unforeseen bad things happen. -Later we’re shown `LinkedHashMap` and its friends. These build on that `hashCode()` method to -organize data for fast retrieval. - -Elsewhere we have cryptographic hash functions. These get used all over the place. HTTPS -certificates, Git commits, BitTorrent integrity checking, and Blockchain blocks all use -cryptographic hashes. Good use of hashes can improve the performance, privacy, security, and -simplicity of an application. - -Each cryptographic hash function accepts a variable-length stream of input bytes and produces a -fixed-length byte string value called the “hash”. Hash functions have these important qualities: - - * Deterministic: each input always produces the same output. - * Uniform: each output byte string is equally likely. It is very difficult to find or create pairs - of different inputs that yield the same output. This is called a “collision”. - * Non-reversible: knowing an output doesn't help you to find the input. Note that if you know some - possible inputs you can hash them to see if their hashes match. - * Well-known: the hash is implemented everywhere and rigorously understood. - -Good hash functions are very cheap to compute (dozens of microseconds) and expensive to reverse -(quintillions of millenia). Steady advances in computing and mathematics have caused once-great hash -functions to become inexpensive to reverse. When choosing a hash function, beware that not all are -created equal! Okio supports these well-known cryptographic hash functions: - - * **MD5**: a 128-bit (16 byte) cryptographic hash. It is both insecure and obsolete because it is - inexpensive to reverse! This hash is offered because it is popular and convenient for use in - legacy systems that are not security-sensitive. - * **SHA-1**: a 160-bit (20 byte) cryptographic hash. It was recently demonstrated that it is - feasible to create SHA-1 collisions. Consider upgrading from SHA-1 to SHA-256. - * **SHA-256**: a 256-bit (32 byte) cryptographic hash. SHA-256 is widely understood and expensive - to reverse. This is the hash most systems should use. - * **SHA-512**: a 512-bit (64 byte) cryptographic hash. It is expensive to reverse. - -Each hash creates a `ByteString` of the specified length. Use `hex()` to get the conventional -human-readable form. Or leave it as a `ByteString` because that’s a convenient model type! - -Okio can produce cryptographic hashes from byte strings: - -=== "Java" - - ```Java - ByteString byteString = readByteString(new File("README.md")); - System.out.println(" md5: " + byteString.md5().hex()); - System.out.println(" sha1: " + byteString.sha1().hex()); - System.out.println("sha256: " + byteString.sha256().hex()); - System.out.println("sha512: " + byteString.sha512().hex()); - ``` - -=== "Kotlin" - - ```Kotlin - val byteString = readByteString(File("README.md")) - println(" md5: " + byteString.md5().hex()) - println(" sha1: " + byteString.sha1().hex()) - println(" sha256: " + byteString.sha256().hex()) - println(" sha512: " + byteString.sha512().hex()) - ``` - -From buffers: - -=== "Java" - - ```Java - Buffer buffer = readBuffer(new File("README.md")); - System.out.println(" md5: " + buffer.md5().hex()); - System.out.println(" sha1: " + buffer.sha1().hex()); - System.out.println("sha256: " + buffer.sha256().hex()); - System.out.println("sha512: " + buffer.sha512().hex()); - ``` - -=== "Kotlin" - - ```Kotlin - val buffer = readBuffer(File("README.md")) - println(" md5: " + buffer.md5().hex()) - println(" sha1: " + buffer.sha1().hex()) - println(" sha256: " + buffer.sha256().hex()) - println(" sha512: " + buffer.sha512().hex()) - ``` - -While streaming from a source: - -=== "Java" - - ```Java - try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole()); - BufferedSource source = Okio.buffer(Okio.source(file))) { - source.readAll(hashingSink); - System.out.println("sha256: " + hashingSink.hash().hex()); - } - ``` - -=== "Kotlin" - - ```Kotlin - sha256(blackholeSink()).use { hashingSink -> - file.source().buffer().use { source -> - source.readAll(hashingSink) - println(" sha256: " + hashingSink.hash.hex()) - } - } - ``` - -While streaming to a sink: - -=== "Java" - - ```Java - try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole()); - BufferedSink sink = Okio.buffer(hashingSink); - Source source = Okio.source(file)) { - sink.writeAll(source); - sink.close(); // Emit anything buffered. - System.out.println("sha256: " + hashingSink.hash().hex()); - } - ``` - -=== "Kotlin" - - ```Kotlin - sha256(blackholeSink()).use { hashingSink -> - hashingSink.buffer().use { sink -> - file.source().use { source -> - sink.writeAll(source) - sink.close() // Emit anything buffered. - println(" sha256: " + hashingSink.hash.hex()) - } - } - } - ``` - -Okio also supports HMAC (Hash Message Authentication Code) which combines a secret and a hash. -Applications use HMAC for data integrity and authentication. - -=== "Java" - - ```Java - ByteString secret = ByteString.decodeHex("7065616e7574627574746572"); - System.out.println("hmacSha256: " + byteString.hmacSha256(secret).hex()); - ``` - -=== "Kotlin" - - ```Kotlin - val secret = "7065616e7574627574746572".decodeHex() - println("hmacSha256: " + byteString.hmacSha256(secret).hex()) - ``` - -As with hashing, you can generate an HMAC from a `ByteString`, `Buffer`, `HashingSource`, and -`HashingSink`. Note that Okio doesn’t implement HMAC for MD5. Okio uses Java’s -`java.security.MessageDigest` for cryptographic hashes and `javax.crypto.Mac` for HMAC. - -### Encryption and Decryption - -Use `Okio.cipherSink(Sink, Cipher)` or `Okio.cipherSource(Source, Cipher)` to encrypt or decrypt a -stream using a block cipher. - -Callers are responsible for the initialization of the encryption or decryption cipher with the -chosen algorithm, the key, and algorithm-specific additional parameters like the initialization -vector. The following example shows a typical usage with AES encryption, in which `key` and `iv` -parameters should both be 16 bytes long. - -```java -void encryptAes(ByteString bytes, File file, byte[] key, byte[] iv) - throws GeneralSecurityException, IOException { - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv)); - try (BufferedSink sink = Okio.buffer(Okio.cipherSink(Okio.sink(file), cipher))) { - sink.write(bytes); - } -} - -ByteString decryptAesToByteString(File file, byte[] key, byte[] iv) - throws GeneralSecurityException, IOException { - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv)); - try (BufferedSource source = Okio.buffer(Okio.cipherSource(Okio.source(file), cipher))) { - return source.readByteString(); - } -} -``` - -In Kotlin, these encryption and decryption methods are extensions on `Cipher`: - -```kotlin -fun encryptAes(bytes: ByteString, file: File, key: ByteArray, iv: ByteArray) { - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) - val cipherSink = file.sink().cipherSink(cipher) - cipherSink.buffer().use { - it.write(bytes) - } -} - -fun decryptAesToByteString(file: File, key: ByteArray, iv: ByteArray): ByteString { - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) - val cipherSource = file.source().cipherSource(cipher) - return cipherSource.buffer().use { - it.readByteString() - } -} -``` - -File System Examples --------------------- - -Okio's recently gained a multiplatform file system API. These examples work on JVM, native, and -Node.js platforms. In the examples below `fileSystem` is an instance of [FileSystem] such as -`FileSystem.SYSTEM` or `FakeFileSystem`. - -Read all of `readme.md` as a string: - -``` -val path = "readme.md".toPath() -val entireFileString = fileSystem.read(path) { - readUtf8() -} -``` - -Read all of `thumbnail.png` as a [ByteString][3]: - -``` -val path = "thumbnail.png".toPath() -val entireFileByteString = fileSystem.read(path) { - readByteString() -} -``` - -Read all lines of `/etc/hosts` into a `List<String>`: - -``` -val path = "/etc/hosts".toPath() -val allLines = fileSystem.read(path) { - generateSequence { readUtf8Line() }.toList() -} -``` - -Read the prefix of `index.html` that precedes the first `<html>` substring: - -``` -val path = "index.html".toPath() -val untilHtmlTag = fileSystem.read(path) { - val htmlTag = indexOf("<html>".encodeUtf8()) - if (htmlTag != -1L) readUtf8(htmlTag) else null -} -``` - -Write `readme.md` as a string: - -``` -val path = "readme.md".toPath() -fileSystem.write(path) { - writeUtf8( - """ - |Hello, World - |------------ - | - |This is a sample file. - |""".trimMargin() - ) -} -``` - -Write `data.bin` as a [ByteString][3]: - -``` -val path = "data.bin".toPath() -fileSystem.write(path) { - val byteString = "68656c6c6f20776f726c640a".decodeHex() - write(byteString) -} -``` - -Write `readme.md` from a `List<String>`: - -``` -val path = "readme.md".toPath() -val lines = listOf( - "Hello, World", - "------------", - "", - "This is a sample file.", - "" -) -fileSystem.write(path) { - for (line in lines) { - writeUtf8(line) - writeUtf8("\n") - } -} -``` - -Generate `binary.txt` programmatically: - -``` -val path = "binary.txt".toPath() -fileSystem.write(path) { - for (i in 1 until 100) { - writeUtf8("$i ${i.toString(2)}") - writeUtf8("\n") - } -} -``` - - Releases -------- Our [change log][changelog] has release history. ```kotlin -implementation("com.squareup.okio:okio:2.10.0") +implementation("com.squareup.okio:okio:3.7.0") ``` <details> <summary>Snapshot builds are also available</summary> - + ```kotlin repositories { - maven { - url = uri("https://oss.sonatype.org/content/repositories/snapshots/") - } + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") } - + dependencies { - implementation("com.squareup.okio:okio:2.10.0") + implementation("com.squareup.okio:okio:3.7.0") } -``` - -</details> - - -R8 / ProGuard --------- +``` -If you are using R8 or ProGuard add the options from [this file][proguard]. +</details> License @@ -1124,20 +133,16 @@ License 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. - + [1]: https://github.com/square/okhttp - [3]: https://square.github.io/okio/2.x/okio/okio/-byte-string/index.html - [4]: https://square.github.io/okio/2.x/okio/okio/-buffer/index.html - [5]: https://square.github.io/okio/2.x/okio/okio/-source/index.html - [6]: https://square.github.io/okio/2.x/okio/okio/-sink/index.html - [7]: https://square.github.io/okio/2.x/okio/okio/-buffered-source/index.html - [8]: https://square.github.io/okio/2.x/okio/okio/-buffered-sink/index.html + [3]: https://square.github.io/okio/3.x/okio/okio/okio/-byte-string/index.html + [4]: https://square.github.io/okio/3.x/okio/okio/okio/-buffer/index.html + [5]: https://square.github.io/okio/3.x/okio/okio/okio/-source/index.html + [6]: https://square.github.io/okio/3.x/okio/okio/okio/-sink/index.html + [7]: https://square.github.io/okio/3.x/okio/okio/okio/-buffered-source/index.html + [8]: https://square.github.io/okio/3.x/okio/okio/okio/-buffered-sink/index.html [changelog]: http://square.github.io/okio/changelog/ [javadoc]: https://square.github.io/okio/2.x/okio/okio/index.html - [nfd]: https://docs.oracle.com/javase/7/docs/api/java/text/Normalizer.Form.html#NFD - [nfc]: https://docs.oracle.com/javase/7/docs/api/java/text/Normalizer.Form.html#NFC - [base64]: https://tools.ietf.org/html/rfc4648#section-4 - [bmp]: https://en.wikipedia.org/wiki/BMP_file_format [kotlin]: https://kotlinlang.org/ [ok_libraries_talk]: https://www.youtube.com/watch?v=WvyScM_S88c [ok_libraries_slides]: https://speakerdeck.com/jakewharton/a-few-ok-libraries-droidcon-mtl-2015 @@ -1145,19 +150,4 @@ License [encoding_slides]: https://speakerdeck.com/swankjesse/decoding-the-secrets-of-binary-data-droidcon-nyc-2016 [ok_multiplatform_talk]: https://www.youtube.com/watch?v=Q8B4eDirgk0 [ok_multiplatform_slides]: https://speakerdeck.com/swankjesse/ok-multiplatform - [ReadFileLineByLine]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.java - [ReadFileLineByLineKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/ReadFileLineByLine.kt - [WriteFile]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/WriteFile.java - [WriteFileKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/WriteFile.kt - [ExploreCharsets]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/ExploreCharsets.java - [ExploreCharsetsKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/ExploreCharsets.kt - [FileSystem]: https://square.github.io/okio/2.x/okio/okio/-file-system/index.html - [GoldenValue]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/GoldenValue.java - [GoldenValueKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/GoldenValue.kt - [BitmapEncoder]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/BitmapEncoder.java - [BitmapEncoderKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/BitmapEncoder.kt - [SocksProxyServer]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/SocksProxyServer.java - [SocksProxyServerKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/SocksProxyServer.kt - [Hashing]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/Hashing.java - [HashingKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/Hashing.kt - [proguard]: https://github.com/square/okio/blob/master/okio/src/jvmMain/resources/META-INF/proguard/okio.pro + [apis_talk]: https://www.youtube.com/watch?v=Du7YXPAV1M8 diff --git a/docs/java_io_recipes.md b/docs/java_io_recipes.md new file mode 100644 index 00000000..4c44d9dc --- /dev/null +++ b/docs/java_io_recipes.md @@ -0,0 +1,98 @@ +java.io Recipes +=============== + +These recipes use Okio with `java.io.File` instead of Okio's own `Path` and `FileSystem` types. + + +Read a text file line-by-line ([Java][ReadJavaIoFileLineByLine]/[Kotlin][ReadJavaIoFileLineByLineKt]) +----------------------------------------------------------------------------------------------------- + +This is similar to the other [line-by-line example](recipes.md#read-a-text-file-line-by-line-javakotlin), but it uses `java.io.File` +instead of `okio.Path` and `okio.FileSystem`. + +=== "Java" + + ```java + public void readLines(File file) throws IOException { + try (Source fileSource = Okio.source(file); + BufferedSource bufferedFileSource = Okio.buffer(fileSource)) { + + while (true) { + String line = bufferedFileSource.readUtf8Line(); + if (line == null) break; + + if (line.contains("square")) { + System.out.println(line); + } + } + + } + } + ``` + +=== "Kotlin" + + Note that static `Okio` methods become extension functions (`Okio.source(file)` => + `file.source()`). + + ```kotlin + @Throws(IOException::class) + fun readLines(file: File) { + file.source().use { fileSource -> + fileSource.buffer().use { bufferedFileSource -> + while (true) { + val line = bufferedFileSource.readUtf8Line() ?: break + if ("square" in line) { + println(line) + } + } + } + } + } + ``` + +Write a text file ([Java][WriteJavaIoFile]/[Kotlin][WriteJavaIoFileKt]) +----------------------------------------------------------------------------------------------------- + +This is similar to the other [write example](recipes.md#write-a-text-file-javakotlin), but it uses +`java.io.File` instead of `okio.Path` and `okio.FileSystem`. + +=== "Java" + + ```java + public void writeEnv(File file) throws IOException { + try (Sink fileSink = Okio.sink(file); + BufferedSink bufferedSink = Okio.buffer(fileSink)) { + + for (Map.Entry<String, String> entry : System.getenv().entrySet()) { + bufferedSink.writeUtf8(entry.getKey()); + bufferedSink.writeUtf8("="); + bufferedSink.writeUtf8(entry.getValue()); + bufferedSink.writeUtf8("\n"); + } + + } + } + ``` + +=== "Kotlin" + + ```kotlin + @Throws(IOException::class) + fun writeEnv(file: File) { + file.sink().buffer().use { sink -> + for ((key, value) in System.getenv()) { + sink.writeUtf8(key) + sink.writeUtf8("=") + sink.writeUtf8(value) + sink.writeUtf8("\n") + } + } + } + ``` + + +[ReadJavaIoFileLineByLineKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/ReadJavaIoFileLineByLine.kt +[ReadJavaIoFileLineByLine]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/ReadJavaIoFileLineByLine.java +[WriteJavaIoFileKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/WriteJavaIoFile.kt +[WriteJavaIoFile]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/WriteJavaIoFile.java diff --git a/docs/multiplatform.md b/docs/multiplatform.md index cad68636..8943099a 100644 --- a/docs/multiplatform.md +++ b/docs/multiplatform.md @@ -38,3 +38,29 @@ Okio uses the built-in implementations of these functions on the JVM. [kotlin_multiplatform]: https://kotlinlang.org/docs/reference/multiplatform.html [mingw]: http://www.mingw.org/ [node_js]: https://nodejs.org/api/fs.html + +## Gradle configuration + +```kotlin +// build.gradle.kts +kotlin { + sourceSets { + val okioVersion = "3.XXX" + val commonMain by getting { + dependencies { + implementation("com.squareup.okio:okio:$okioVersion") + } + } + val jsMain by getting { + dependencies { + implementation("com.squareup.okio:okio-nodefilesystem:$okioVersion") + } + } + val commonTest by getting { + dependencies { + implementation("com.squareup.okio:okio-fakefilesystem:$okioVersion") + } + } + } +} +``` diff --git a/docs/recipes.md b/docs/recipes.md new file mode 100644 index 00000000..acc5dcc7 --- /dev/null +++ b/docs/recipes.md @@ -0,0 +1,942 @@ +Recipes +======= + +We've written some recipes that demonstrate how to solve common problems with Okio. Read through +them to learn about how everything works together. Cut-and-paste these examples freely; that's what +they're for. + +These recipes work on all platforms: Java, Android, Kotlin/Native, and Kotlin/JS. See +[java.io Recipes](java_io_recipes.md) for samples that integrate Java APIs. + + +Read a text file line-by-line ([Java][ReadFileLineByLine]/[Kotlin][ReadFileLineByLineKt]) +----------------------------------------------------------------------------------------- + +Use `FileSystem.source(Path)` to open a source stream to read a file. The returned `Source` +interface is very small and has limited uses. Instead we wrap the source with a buffer. This has two +benefits: + + * **It makes the API more powerful.** Instead of the basic methods offered by `Source`, + `BufferedSource` has dozens of methods to address most common problems concisely. + + * **It makes your program run faster.** Buffering allows Okio to get more done with fewer I/O + operations. + +Each `Source` that is opened needs to be closed. The code that opens the stream is responsible for +making sure it is closed. + +=== "Java" + + Here we use Java's `try` blocks to close our sources automatically. + + ```java + public void readLines(Path path) throws IOException { + try (Source fileSource = FileSystem.SYSTEM.source(path); + BufferedSource bufferedFileSource = Okio.buffer(fileSource)) { + + while (true) { + String line = bufferedFileSource.readUtf8Line(); + if (line == null) break; + + if (line.contains("square")) { + System.out.println(line); + } + } + + } + } + ``` + +=== "Kotlin" + + This uses `use` to automatically close the streams. This prevents resource leaks, even if an + exception is thrown. + + ```kotlin + fun readLines(path: Path) { + FileSystem.SYSTEM.source(path).use { fileSource -> + fileSource.buffer().use { bufferedFileSource -> + while (true) { + val line = bufferedFileSource.readUtf8Line() ?: break + if ("square" in line) { + println(line) + } + } + } + } + } + ``` + + +The `readUtf8Line()` API reads all of the data until the next line delimiter – either `\n`, `\r\n`, +or the end of the file. It returns that data as a string, omitting the delimiter at the end. When it +encounters empty lines the method will return an empty string. If there isn’t any more data to read +it will return null. + + +=== "Java" + + The above Java program can be written more compactly by inlining the `fileSource` variable and + by using a fancy `for` loop instead of a `while`: + + ```java + public void readLines(Path path) throws IOException { + try (BufferedSource source = Okio.buffer(FileSystem.SYSTEM.source(path))) { + for (String line; (line = source.readUtf8Line()) != null; ) { + if (line.contains("square")) { + System.out.println(line); + } + } + } + } + ``` + +=== "Kotlin" + + In Kotlin, we can use `FileSystem.read()` to buffer the source before our block and close the + source afterwards. In the body of the block, `this` is a `BufferedSource`. + + ```kotlin + @Throws(IOException::class) + fun readLines(path: Path) { + FileSystem.SYSTEM.read(path) { + while (true) { + val line = readUtf8Line() ?: break + if ("square" in line) { + println(line) + } + } + } + } + ``` + +The `readUtf8Line()` method is suitable for parsing most files. For certain use-cases you may also +consider `readUtf8LineStrict()`. It is similar but it requires that each line is terminated by `\n` +or `\r\n`. If it encounters the end of the file before that it will throw an `EOFException`. The +strict variant also permits a byte limit to defend against malformed input. + +=== "Java" + + ```java + public void readLines(Path path) throws IOException { + try (BufferedSource source = Okio.buffer(FileSystem.SYSTEM.source(path))) { + while (!source.exhausted()) { + String line = source.readUtf8LineStrict(1024L); + if (line.contains("square")) { + System.out.println(line); + } + } + } + } + ``` + +=== "Kotlin" + + ```kotlin + @Throws(IOException::class) + fun readLines(path: Path) { + FileSystem.SYSTEM.read(path) { + while (!source.exhausted()) { + val line = source.readUtf8LineStrict(1024) + if ("square" in line) { + println(line) + } + } + } + } + ``` + + +Write a text file ([Java][WriteFile]/[Kotlin][WriteFileKt]) +----------------------------------------------------------- + +Above we used a `Source` and a `BufferedSource` to read a file. To write, we use a `Sink` and a +`BufferedSink`. The advantages of buffering are the same: a more capable API and better performance. + +```java +public void writeEnv(Path path) throws IOException { + try (Sink fileSink = FileSystem.SYSTEM.sink(path); + BufferedSink bufferedSink = Okio.buffer(fileSink)) { + + for (Map.Entry<String, String> entry : System.getenv().entrySet()) { + bufferedSink.writeUtf8(entry.getKey()); + bufferedSink.writeUtf8("="); + bufferedSink.writeUtf8(entry.getValue()); + bufferedSink.writeUtf8("\n"); + } + + } +} +``` + +There isn’t an API to write a line of input; instead we manually insert our own newline character. +Most programs should hardcode `"\n"` as the newline character. In rare situations you may use +`System.lineSeparator()` instead of `"\n"`: it returns `"\r\n"` on Windows and `"\n"` everywhere +else. + +=== "Java" + + We can write the above program more compactly by inlining the `fileSink` variable and by taking + advantage of method chaining: + + ```java + public void writeEnv(Path path) throws IOException { + try (BufferedSink sink = Okio.buffer(FileSystem.SYSTEM.sink(path))) { + for (Map.Entry<String, String> entry : System.getenv().entrySet()) { + sink.writeUtf8(entry.getKey()) + .writeUtf8("=") + .writeUtf8(entry.getValue()) + .writeUtf8("\n"); + } + } + } + ``` + +=== "Kotlin" + + In Kotlin, we can use `FileSystem.write()` to buffer the sink before our block and close the + sink afterwards. In the body of the block, `this` is a `BufferedSink`. + + ```kotlin + @Throws(IOException::class) + fun writeEnv(path: Path) { + FileSystem.SYSTEM.write(path) { + for ((key, value) in System.getenv()) { + writeUtf8(key) + writeUtf8("=") + writeUtf8(value) + writeUtf8("\n") + } + } + } + ``` + +In the above code we make four calls to `writeUtf8()`. Making four calls is more efficient than the +code below because the VM doesn’t have to create and garbage collect a temporary string. + +```java +sink.writeUtf8(entry.getKey() + "=" + entry.getValue() + "\n"); // Slower! +``` + + +UTF-8 ([Java][ExploreCharsets]/[Kotlin][ExploreCharsetsKt]) +----------------------------------------------------------- + +In the above APIs you can see that Okio really likes UTF-8. Early computer systems suffered many +incompatible character encodings: ISO-8859-1, ShiftJIS, ASCII, EBCDIC, etc. Writing software to +support multiple character sets was awful and we didn’t even have emoji! Today we're lucky that the +world has standardized on UTF-8 everywhere, with some rare uses of other charsets in legacy systems. + +If you need another character set, `readString()` and `writeString()` are there for you. These + methods require that you specify a character set. Otherwise you may accidentally create data that + is only readable by the local computer. Most programs should use the UTF-8 methods only. + +When encoding strings you need to be mindful of the different ways that strings are represented and +encoded. When a glyph has an accent or another adornment it may be represented as a single complex + code point (`é`) or as a simple code point (`e`) followed by its modifiers (`´`). When the entire + glyph is a single code point that’s called [NFC][nfc]; when it’s multiple it’s [NFD][nfd]. + +Though we use UTF-8 whenever we read or write strings in I/O, when they are in memory Java Strings +use an obsolete character encoding called UTF-16. It is a bad encoding because it uses a 16-bit +`char` for most characters, but some don’t fit. In particular, most emoji use two Java chars. This +is problematic because `String.length()` returns a surprising result: the number of UTF-16 chars and +not the natural number of glyphs. + +| | Café 🍩 | Café 🍩 | +| --------------------: | :---------------------------| :------------------------------| +| Form | [NFC][nfc] | [NFD][nfd] | +| Code Points | `c a f é ␣ 🍩 ` | `c a f e ´ ␣ 🍩 ` | +| UTF-8 bytes | `43 61 66 c3a9 20 f09f8da9` | `43 61 66 65 cc81 20 f09f8da9` | +| String.codePointCount | 6 | 7 | +| String.length | 7 | 8 | +| Utf8.size | 10 | 11 | + +For the most part Okio lets you ignore these problems and focus on your data. But when you need +them, there are convenient APIs for dealing with low-level UTF-8 strings. + +Use `Utf8.size()` to count the number of bytes required to encode a string as UTF-8 without actually +encoding it. This is handy in length-prefixed encodings like protocol buffers. + +Use `BufferedSource.readUtf8CodePoint()` to read a single variable-length code point, and +`BufferedSink.writeUtf8CodePoint()` to write one. + + +=== "Java" + + ```java + public void dumpStringData(String s) throws IOException { + System.out.println(" " + s); + System.out.println(" String.length: " + s.length()); + System.out.println("String.codePointCount: " + s.codePointCount(0, s.length())); + System.out.println(" Utf8.size: " + Utf8.size(s)); + System.out.println(" UTF-8 bytes: " + ByteString.encodeUtf8(s).hex()); + System.out.println(); + } + ``` + +=== "Kotlin" + + ```kotlin + fun dumpStringData(s: String) { + println(" " + s) + println(" String.length: " + s.length) + println("String.codePointCount: " + s.codePointCount(0, s.length)) + println(" Utf8.size: " + s.utf8Size()) + println(" UTF-8 bytes: " + s.encodeUtf8().hex()) + println() + } + ``` + +Golden Values ([Java][GoldenValue]/[Kotlin][GoldenValueKt]) +----------------------------------------------------------- + +Okio likes testing. The library itself is heavily tested, and it has features that are often helpful +when testing application code. One pattern we’ve found to be quite useful is “golden value” testing. +The goal of such tests is to confirm that data encoded with earlier versions of a program can safely +be decoded by the current program. + +We’ll illustrate this by encoding a value using Java Serialization. Though we must disclaim that +Java Serialization is an awful encoding system and most programs should prefer other formats like +JSON or protobuf! In any case, here’s a method that takes an object, serializes it, and returns the +result as a `ByteString`: + +=== "Java" + + ```Java + private ByteString serialize(Object o) throws IOException { + Buffer buffer = new Buffer(); + try (ObjectOutputStream objectOut = new ObjectOutputStream(buffer.outputStream())) { + objectOut.writeObject(o); + } + return buffer.readByteString(); + } + ``` + +=== "Kotlin" + + ```Kotlin + @Throws(IOException::class) + private fun serialize(o: Any?): ByteString { + val buffer = Buffer() + ObjectOutputStream(buffer.outputStream()).use { objectOut -> + objectOut.writeObject(o) + } + return buffer.readByteString() + } + ``` + +There’s a lot going on here. + +1. We create a buffer as a holding space for our serialized data. It’s a convenient replacement for + `ByteArrayOutputStream`. + +2. We ask the buffer for its output stream. Writes to a buffer or its output stream always append + data to the end of the buffer. + +3. We create an `ObjectOutputStream` (the encoding API for Java serialization) and write our object. + The try block takes care of closing the stream for us. Note that closing a buffer has no effect. + +4. Finally we read a byte string from the buffer. The `readByteString()` method allows us to specify + how many bytes to read; here we don’t specify a count in order to read the entire thing. Reads + from a buffer always consume data from the front of the buffer. + +With our `serialize()` method handy we are ready to compute and print a golden value. + +=== "Java" + + ```Java + Point point = new Point(8.0, 15.0); + ByteString pointBytes = serialize(point); + System.out.println(pointBytes.base64()); + ``` + +=== "Kotlin" + + ```Kotlin + val point = Point(8.0, 15.0) + val pointBytes = serialize(point) + println(pointBytes.base64()) + ``` + +We print the `ByteString` as [base64][base64] because it’s a compact format that’s suitable for +embedding in a test case. The program prints this: + +``` +rO0ABXNyAB5va2lvLnNhbXBsZXMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA +``` + +That’s our golden value! We can embed it in our test case using base64 again to convert it back into +a `ByteString`: + +=== "Java" + + ```Java + ByteString goldenBytes = ByteString.decodeBase64("rO0ABXNyAB5va2lvLnNhbXBsZ" + + "XMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuA" + + "AAAAAAA"); + ``` + +=== "Kotlin" + + ```Kotlin + val goldenBytes = ("rO0ABXNyACRva2lvLnNhbXBsZXMuS290bGluR29sZGVuVmFsdWUkUG9pbnRF9yaY7cJ9EwIAA" + + "kQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA").decodeBase64() + ``` + +The next step is to deserialize the `ByteString` back into our value class. This method reverses the +`serialize()` method above: we append a byte string to a buffer then consume it using an +`ObjectInputStream`: + +=== "Java" + + ```Java + private Object deserialize(ByteString byteString) throws IOException, ClassNotFoundException { + Buffer buffer = new Buffer(); + buffer.write(byteString); + try (ObjectInputStream objectIn = new ObjectInputStream(buffer.inputStream())) { + return objectIn.readObject(); + } + } + ``` + +=== "Kotlin" + + ```Kotlin + @Throws(IOException::class, ClassNotFoundException::class) + private fun deserialize(byteString: ByteString): Any? { + val buffer = Buffer() + buffer.write(byteString) + ObjectInputStream(buffer.inputStream()).use { objectIn -> + return objectIn.readObject() + } + } + ``` + +Now we can test the decoder against the golden value: + +=== "Java" + + ```Java + ByteString goldenBytes = ByteString.decodeBase64("rO0ABXNyAB5va2lvLnNhbXBsZ" + + "XMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuA" + + "AAAAAAA"); + Point decoded = (Point) deserialize(goldenBytes); + assertEquals(new Point(8.0, 15.0), decoded); + ``` + +=== "Kotlin" + + ```Kotlin + val goldenBytes = ("rO0ABXNyACRva2lvLnNhbXBsZXMuS290bGluR29sZGVuVmFsdWUkUG9pbnRF9yaY7cJ9EwIAA" + + "kQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA").decodeBase64()!! + val decoded = deserialize(goldenBytes) as Point + assertEquals(point, decoded) + ``` + +With this test we can change the serialization of the `Point` class without +breaking compatibility. + + +Write a binary file ([Java][BitmapEncoder]/[Kotlin][BitmapEncoderKt]) +--------------------------------------------------------------------- + +Encoding a binary file is not unlike encoding a text file. Okio uses the same `BufferedSink` and +`BufferedSource` bytes for both. This is handy for binary formats that include both byte and +character data. + +Writing binary data is more hazardous than text because if you make a mistake it is often quite +difficult to diagnose. Avoid such mistakes by being careful around these traps: + + * **The width of each field.** This is the number of bytes used. Okio doesn't include a mechanism + to emit partial bytes. If you need that, you’ll need to do your own bit shifting and masking + before writing. + + * **The endianness of each field.** All fields that have more than one byte have _endianness_: + whether the bytes are ordered most-significant to least (big endian) or least-significant to most + (little endian). Okio uses the `Le` suffix for little-endian methods; methods without a suffix + are big-endian. + + * **Signed vs. Unsigned.** Java doesn’t have unsigned primitive types (except for `char`!) so + coping with this is often something that happens at the application layer. To make this a little + easier Okio accepts `int` types for `writeByte()` and `writeShort()`. You can pass an “unsigned” + byte like 255 and Okio will do the right thing. + +| Method | Width | Endianness | Value | Encoded Value | +| :----------- | ----: | :--------- | --------------: | :------------------------ | +| writeByte | 1 | | 3 | `03` | +| writeShort | 2 | big | 3 | `00 03` | +| writeInt | 4 | big | 3 | `00 00 00 03` | +| writeLong | 8 | big | 3 | `00 00 00 00 00 00 00 03` | +| writeShortLe | 2 | little | 3 | `03 00` | +| writeIntLe | 4 | little | 3 | `03 00 00 00` | +| writeLongLe | 8 | little | 3 | `03 00 00 00 00 00 00 00` | +| writeByte | 1 | | Byte.MAX_VALUE | `7f` | +| writeShort | 2 | big | Short.MAX_VALUE | `7f ff` | +| writeInt | 4 | big | Int.MAX_VALUE | `7f ff ff ff` | +| writeLong | 8 | big | Long.MAX_VALUE | `7f ff ff ff ff ff ff ff` | +| writeShortLe | 2 | little | Short.MAX_VALUE | `ff 7f` | +| writeIntLe | 4 | little | Int.MAX_VALUE | `ff ff ff 7f` | +| writeLongLe | 8 | little | Long.MAX_VALUE | `ff ff ff ff ff ff ff 7f` | + +This code encodes a bitmap following the [BMP file format][bmp]. + +=== "Java" + + ```Java + void encode(Bitmap bitmap, BufferedSink sink) throws IOException { + int height = bitmap.height(); + int width = bitmap.width(); + + int bytesPerPixel = 3; + int rowByteCountWithoutPadding = (bytesPerPixel * width); + int rowByteCount = ((rowByteCountWithoutPadding + 3) / 4) * 4; + int pixelDataSize = rowByteCount * height; + int bmpHeaderSize = 14; + int dibHeaderSize = 40; + + // BMP Header + sink.writeUtf8("BM"); // ID. + sink.writeIntLe(bmpHeaderSize + dibHeaderSize + pixelDataSize); // File size. + sink.writeShortLe(0); // Unused. + sink.writeShortLe(0); // Unused. + sink.writeIntLe(bmpHeaderSize + dibHeaderSize); // Offset of pixel data. + + // DIB Header + sink.writeIntLe(dibHeaderSize); + sink.writeIntLe(width); + sink.writeIntLe(height); + sink.writeShortLe(1); // Color plane count. + sink.writeShortLe(bytesPerPixel * Byte.SIZE); + sink.writeIntLe(0); // No compression. + sink.writeIntLe(16); // Size of bitmap data including padding. + sink.writeIntLe(2835); // Horizontal print resolution in pixels/meter. (72 dpi). + sink.writeIntLe(2835); // Vertical print resolution in pixels/meter. (72 dpi). + sink.writeIntLe(0); // Palette color count. + sink.writeIntLe(0); // 0 important colors. + + // Pixel data. + for (int y = height - 1; y >= 0; y--) { + for (int x = 0; x < width; x++) { + sink.writeByte(bitmap.blue(x, y)); + sink.writeByte(bitmap.green(x, y)); + sink.writeByte(bitmap.red(x, y)); + } + + // Padding for 4-byte alignment. + for (int p = rowByteCountWithoutPadding; p < rowByteCount; p++) { + sink.writeByte(0); + } + } + } + ``` + +=== "Kotlin" + + ```Kotlin + @Throws(IOException::class) + fun encode(bitmap: Bitmap, sink: BufferedSink) { + val height = bitmap.height + val width = bitmap.width + val bytesPerPixel = 3 + val rowByteCountWithoutPadding = bytesPerPixel * width + val rowByteCount = (rowByteCountWithoutPadding + 3) / 4 * 4 + val pixelDataSize = rowByteCount * height + val bmpHeaderSize = 14 + val dibHeaderSize = 40 + + // BMP Header + sink.writeUtf8("BM") // ID. + sink.writeIntLe(bmpHeaderSize + dibHeaderSize + pixelDataSize) // File size. + sink.writeShortLe(0) // Unused. + sink.writeShortLe(0) // Unused. + sink.writeIntLe(bmpHeaderSize + dibHeaderSize) // Offset of pixel data. + + // DIB Header + sink.writeIntLe(dibHeaderSize) + sink.writeIntLe(width) + sink.writeIntLe(height) + sink.writeShortLe(1) // Color plane count. + sink.writeShortLe(bytesPerPixel * Byte.SIZE_BITS) + sink.writeIntLe(0) // No compression. + sink.writeIntLe(16) // Size of bitmap data including padding. + sink.writeIntLe(2835) // Horizontal print resolution in pixels/meter. (72 dpi). + sink.writeIntLe(2835) // Vertical print resolution in pixels/meter. (72 dpi). + sink.writeIntLe(0) // Palette color count. + sink.writeIntLe(0) // 0 important colors. + + // Pixel data. + for (y in height - 1 downTo 0) { + for (x in 0 until width) { + sink.writeByte(bitmap.blue(x, y)) + sink.writeByte(bitmap.green(x, y)) + sink.writeByte(bitmap.red(x, y)) + } + + // Padding for 4-byte alignment. + for (p in rowByteCountWithoutPadding until rowByteCount) { + sink.writeByte(0) + } + } + } + ``` + +The trickiest part of this program is the format’s required padding. The BMP format expects each row +to begin on a 4-byte boundary so it is necessary to add zeros to maintain the alignment. + +Encoding other binary formats is usually quite similar. Some tips: + + * Write tests with golden values! Confirming that your program emits the expected result can make + debugging easier. + * Use `Utf8.size()` to compute the number of bytes of an encoded string. This is essential for + length-prefixed formats. + * Use `Float.floatToIntBits()` and `Double.doubleToLongBits()` to encode floating point values. + + +Communicate on a Socket ([Java][SocksProxyServer]/[Kotlin][SocksProxyServerKt]) +------------------------------------------------------------------------------- + +Note that Okio doesn't yet support sockets on Kotlin/Native or Kotlin/JS. + +Sending and receiving data over the network is a bit like writing and reading files. We use +`BufferedSink` to encode output and `BufferedSource` to decode input. Like files, network protocols +can be text, binary, or a mix of both. But there are also some substantial differences between the +network and the file system. + +With a file you’re either reading or writing but with the network you can do both! Some protocols +handle this by taking turns: write a request, read a response, repeat. You can implement this kind +of protocol with a single thread. In other protocols you may read and write simultaneously. +Typically you’ll want one dedicated thread for reading. For writing you can use either a dedicated +thread or use `synchronized` so that multiple threads can share a sink. Okio’s streams are not safe +for concurrent use. + +Sinks buffer outbound data to minimize I/O operations. This is efficient but it means you must +manually call `flush()` to transmit data. Typically message-oriented protocols flush after each +message. Note that Okio will automatically flush when the buffered data exceeds some threshold. This +is intended to save memory and you shouldn’t rely on it for interactive protocols. + +Okio builds on `java.io.Socket` for connectivity. Create your socket as a server or as a client, +then use `Okio.source(Socket)` to read and `Okio.sink(Socket)` to write. These APIs also work with +`SSLSocket`. You should use SSL unless you have a very good reason not to! + +Cancel a socket from any thread by calling `Socket.close()`; this will cause its sources and sinks +to immediately fail with an `IOException`. You can also configure timeouts for all socket +operations. You don’t need a reference to the socket to adjust timeouts: `Source` and `Sink` expose +timeouts directly. This API works even if the streams are decorated. + +As a complete example of networking with Okio we wrote a [basic SOCKS proxy][SocksProxyServer] +server. Some highlights: + +=== "Java" + + ```Java + Socket fromSocket = ... + BufferedSource fromSource = Okio.buffer(Okio.source(fromSocket)); + BufferedSink fromSink = Okio.buffer(Okio.sink(fromSocket)); + ``` + +=== "Kotlin" + + ```Kotlin + val fromSocket: Socket = ... + val fromSource = fromSocket.source().buffer() + val fromSink = fromSocket.sink().buffer() + ``` + +Creating sources and sinks for sockets is the same as creating them for files. Once you create a +`Source` or `Sink` for a socket you must not use its `InputStream` or `OutputStream`, respectively. + +=== "Java" + + ```Java + Buffer buffer = new Buffer(); + for (long byteCount; (byteCount = source.read(buffer, 8192L)) != -1; ) { + sink.write(buffer, byteCount); + sink.flush(); + } + ``` + +=== "Kotlin" + + ```Kotlin + val buffer = Buffer() + var byteCount: Long + while (source.read(buffer, 8192L).also { byteCount = it } != -1L) { + sink.write(buffer, byteCount) + sink.flush() + } + ``` + +The above loop copies data from the source to the sink, flushing after each read. If we didn’t need +the flushing we could replace this loop with a single call to `BufferedSink.writeAll(Source)`. + +The `8192` argument to `read()` is the maximum number of bytes to read before returning. We could +have passed any value here, but we like 8 KiB because that’s the largest value Okio can do in a +single system call. Most of the time application code doesn’t need to deal with such limits! + +=== "Java" + + ```Java + int addressType = fromSource.readByte() & 0xff; + int port = fromSource.readShort() & 0xffff; + ``` + +=== "Kotlin" + + ```Kotlin + val addressType = fromSource.readByte().toInt() and 0xff + val port = fromSource.readShort().toInt() and 0xffff + ``` + +Okio uses signed types like `byte` and `short`, but often protocols want unsigned values. The +bitwise `&` operator is Java’s preferred idiom to convert a signed value into an unsigned value. +Here’s a cheat sheet for bytes, shorts, and ints: + +| Type | Signed Range | Unsigned Range | Signed to Unsigned | +| :---- | :---------------------------: | :--------------- | :-------------------------- | +| byte | -128..127 | 0..255 | `int u = s & 0xff;` | +| short | -32,768..32,767 | 0..65,535 | `int u = s & 0xffff;` | +| int | -2,147,483,648..2,147,483,647 | 0..4,294,967,295 | `long u = s & 0xffffffffL;` | + +Java has no primitive type that can represent unsigned longs. + + +Hashing ([Java][Hashing]/[Kotlin][HashingKt]) +--------------------------------------------- + +We’re bombarded by hashing in our lives as Java programmers. Early on we're introduced to the +`hashCode()` method, something we know we need to override otherwise unforeseen bad things happen. +Later we’re shown `LinkedHashMap` and its friends. These build on that `hashCode()` method to +organize data for fast retrieval. + +Elsewhere we have cryptographic hash functions. These get used all over the place. HTTPS +certificates, Git commits, BitTorrent integrity checking, and Blockchain blocks all use +cryptographic hashes. Good use of hashes can improve the performance, privacy, security, and +simplicity of an application. + +Each cryptographic hash function accepts a variable-length stream of input bytes and produces a +fixed-length byte string value called the “hash”. Hash functions have these important qualities: + + * Deterministic: each input always produces the same output. + * Uniform: each output byte string is equally likely. It is very difficult to find or create pairs + of different inputs that yield the same output. This is called a “collision”. + * Non-reversible: knowing an output doesn't help you to find the input. Note that if you know some + possible inputs you can hash them to see if their hashes match. + * Well-known: the hash is implemented everywhere and rigorously understood. + +Good hash functions are very cheap to compute (dozens of microseconds) and expensive to reverse +(quintillions of millenia). Steady advances in computing and mathematics have caused once-great hash +functions to become inexpensive to reverse. When choosing a hash function, beware that not all are +created equal! Okio supports these well-known cryptographic hash functions: + + * **MD5**: a 128-bit (16 byte) cryptographic hash. It is both insecure and obsolete because it is + inexpensive to reverse! This hash is offered because it is popular and convenient for use in + legacy systems that are not security-sensitive. + * **SHA-1**: a 160-bit (20 byte) cryptographic hash. It was recently demonstrated that it is + feasible to create SHA-1 collisions. Consider upgrading from SHA-1 to SHA-256. + * **SHA-256**: a 256-bit (32 byte) cryptographic hash. SHA-256 is widely understood and expensive + to reverse. This is the hash most systems should use. + * **SHA-512**: a 512-bit (64 byte) cryptographic hash. It is expensive to reverse. + +Each hash creates a `ByteString` of the specified length. Use `hex()` to get the conventional +human-readable form. Or leave it as a `ByteString` because that’s a convenient model type! + +Okio can produce cryptographic hashes from byte strings: + +=== "Java" + + ```Java + ByteString byteString = readByteString(Path.get("README.md")); + System.out.println(" md5: " + byteString.md5().hex()); + System.out.println(" sha1: " + byteString.sha1().hex()); + System.out.println("sha256: " + byteString.sha256().hex()); + System.out.println("sha512: " + byteString.sha512().hex()); + ``` + +=== "Kotlin" + + ```Kotlin + val byteString = readByteString("README.md".toPath()) + println(" md5: " + byteString.md5().hex()) + println(" sha1: " + byteString.sha1().hex()) + println(" sha256: " + byteString.sha256().hex()) + println(" sha512: " + byteString.sha512().hex()) + ``` + +From buffers: + +=== "Java" + + ```Java + Buffer buffer = readBuffer(Path.get("README.md")); + System.out.println(" md5: " + buffer.md5().hex()); + System.out.println(" sha1: " + buffer.sha1().hex()); + System.out.println("sha256: " + buffer.sha256().hex()); + System.out.println("sha512: " + buffer.sha512().hex()); + ``` + +=== "Kotlin" + + ```Kotlin + val buffer = readBuffer("README.md".toPath()) + println(" md5: " + buffer.md5().hex()) + println(" sha1: " + buffer.sha1().hex()) + println(" sha256: " + buffer.sha256().hex()) + println(" sha512: " + buffer.sha512().hex()) + ``` + +While streaming from a source: + +=== "Java" + + ```Java + try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole()); + BufferedSource source = Okio.buffer(FileSystem.SYSTEM.source(path))) { + source.readAll(hashingSink); + System.out.println("sha256: " + hashingSink.hash().hex()); + } + ``` + +=== "Kotlin" + + ```Kotlin + sha256(blackholeSink()).use { hashingSink -> + FileSystem.SYSTEM.source(path).buffer().use { source -> + source.readAll(hashingSink) + println(" sha256: " + hashingSink.hash.hex()) + } + } + ``` + +While streaming to a sink: + +=== "Java" + + ```Java + try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole()); + BufferedSink sink = Okio.buffer(hashingSink); + Source source = FileSystem.SYSTEM.source(path)) { + sink.writeAll(source); + sink.close(); // Emit anything buffered. + System.out.println("sha256: " + hashingSink.hash().hex()); + } + ``` + +=== "Kotlin" + + ```Kotlin + sha256(blackholeSink()).use { hashingSink -> + hashingSink.buffer().use { sink -> + FileSystem.SYSTEM.source(path).use { source -> + sink.writeAll(source) + sink.close() // Emit anything buffered. + println(" sha256: " + hashingSink.hash.hex()) + } + } + } + ``` + +Okio also supports HMAC (Hash Message Authentication Code) which combines a secret and a hash. +Applications use HMAC for data integrity and authentication. + +=== "Java" + + ```Java + ByteString secret = ByteString.decodeHex("7065616e7574627574746572"); + System.out.println("hmacSha256: " + byteString.hmacSha256(secret).hex()); + ``` + +=== "Kotlin" + + ```Kotlin + val secret = "7065616e7574627574746572".decodeHex() + println("hmacSha256: " + byteString.hmacSha256(secret).hex()) + ``` + +As with hashing, you can generate an HMAC from a `ByteString`, `Buffer`, `HashingSource`, and +`HashingSink`. Note that Okio doesn’t implement HMAC for MD5. + +On Android and Java, Okio uses Java’s `java.security.MessageDigest` for cryptographic hashes and +`javax.crypto.Mac` for HMAC. On other platforms Okio uses its own optimized implementation of +these algorithms. + + +Encryption and Decryption +------------------------- + +On Android and Java it's easy to encrypt streams. + +Callers are responsible for the initialization of the encryption or decryption cipher with the +chosen algorithm, the key, and algorithm-specific additional parameters like the initialization +vector. The following example shows a typical usage with AES encryption, in which `key` and `iv` +parameters should both be 16 bytes long. + +=== "Java" + + Use `Okio.cipherSink(Sink, Cipher)` or `Okio.cipherSource(Source, Cipher)` to encrypt or decrypt + a stream using a block cipher. + + ```java + void encryptAes(ByteString bytes, Path path, byte[] key, byte[] iv) + throws GeneralSecurityException, IOException { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv)); + try (BufferedSink sink = Okio.buffer( + Okio.cipherSink(FileSystem.SYSTEM.sink(path), cipher))) { + sink.write(bytes); + } + } + + ByteString decryptAesToByteString(Path path, byte[] key, byte[] iv) + throws GeneralSecurityException, IOException { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv)); + try (BufferedSource source = Okio.buffer( + Okio.cipherSource(FileSystem.SYSTEM.source(path), cipher))) { + return source.readByteString(); + } + } + ``` + +=== "Kotlin" + + Encryption and decryption functions are extensions on `Cipher`: + + ```kotlin + fun encryptAes(bytes: ByteString, path: Path, key: ByteArray, iv: ByteArray) { + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + val cipherSink = FileSystem.SYSTEM.sink(path).cipherSink(cipher) + cipherSink.buffer().use { + it.write(bytes) + } + } + + fun decryptAesToByteString(path: Path, key: ByteArray, iv: ByteArray): ByteString { + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + val cipherSource = FileSystem.SYSTEM.source(path).cipherSource(cipher) + return cipherSource.buffer().use { + it.readByteString() + } + } + ``` + + +[base64]: https://tools.ietf.org/html/rfc4648#section-4 +[bmp]: https://en.wikipedia.org/wiki/BMP_file_format +[nfd]: https://docs.oracle.com/javase/7/docs/api/java/text/Normalizer.Form.html#NFD +[nfc]: https://docs.oracle.com/javase/7/docs/api/java/text/Normalizer.Form.html#NFC +[BitmapEncoderKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/BitmapEncoder.kt +[BitmapEncoder]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/BitmapEncoder.java +[ExploreCharsetsKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/ExploreCharsets.kt +[ExploreCharsets]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/ExploreCharsets.java +[GoldenValueKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/GoldenValue.kt +[GoldenValue]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/GoldenValue.java +[HashingKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/Hashing.kt +[Hashing]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/Hashing.java +[ReadFileLineByLine]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.java +[ReadFileLineByLineKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/ReadFileLineByLine.kt +[SocksProxyServerKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/SocksProxyServer.kt +[SocksProxyServer]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/SocksProxyServer.java +[WriteFile]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/WriteFile.java +[WriteFileKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/WriteFile.kt diff --git a/docs/releasing.md b/docs/releasing.md index 94a1a34c..1a32a260 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,63 +1,6 @@ Releasing ========= -### Prerequisite: Sonatype (Maven Central) Account - -Create an account on the [Sonatype issues site][sonatype_issues]. Ask an existing publisher to open -an issue requesting publishing permissions for `com.squareup` projects. - -### Prerequisite: GPG Keys - -Generate a GPG key (RSA, 4096 bit, 3650 day) expiry, or use an existing one. You should leave the -password empty for this key. - -``` -$ gpg --full-generate-key -``` - -Upload the GPG keys to public servers: - -``` -$ gpg --list-keys --keyid-format LONG -/Users/johnbarber/.gnupg/pubring.kbx ------------------------------- -pub rsa4096/XXXXXXXXXXXXXXXX 2019-07-16 [SC] [expires: 2029-07-13] - YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY -uid [ultimate] John Barber <jbarber@squareup.com> -sub rsa4096/ZZZZZZZZZZZZZZZZ 2019-07-16 [E] [expires: 2029-07-13] - -$ gpg --send-keys --keyserver keyserver.ubuntu.com XXXXXXXXXXXXXXXX -``` - -### Prerequisite: Gradle Properties - -Define publishing properties in `~/.gradle/gradle.properties`: - -``` -signing.keyId=1A2345F8 -signing.password= -signing.secretKeyRingFile=/Users/jbarber/.gnupg/secring.gpg -``` - -`signing.keyId` is the GPG key's ID. Get it with this: - - ``` - $ gpg --list-keys --keyid-format SHORT - ``` - -`signing.password` is the password for this key. This might be empty! - -`signing.secretKeyRingFile` is the absolute path for `secring.gpg`. You may need to export this -file manually with the following command where `XXXXXXXX` is the `keyId` above: - - ``` - $ gpg --keyring secring.gpg --export-secret-key XXXXXXXX > ~/.gnupg/secring.gpg - ``` - - -Cutting a Release ------------------ - 1. Update `CHANGELOG.md`. 2. Set versions: @@ -67,14 +10,7 @@ Cutting a Release export NEXT_VERSION=X.Y.Z-SNAPSHOT ``` -3. Set environment variables with your [Sonatype credentials][sonatype_issues]. - - ``` - export SONATYPE_NEXUS_USERNAME=johnbarber - export SONATYPE_NEXUS_PASSWORD=`pbpaste` - ``` - -4. Update, build, and upload: +3. Update versions, tag the release, and prepare for the next release. ``` sed -i "" \ @@ -82,24 +18,19 @@ Cutting a Release gradle.properties sed -i "" \ "s/\"com.squareup.okio:\([^\:]*\):[^\"]*\"/\"com.squareup.okio:\1:$RELEASE_VERSION\"/g" \ - `find . -name "README.md"` - ./gradlew clean publish - ``` - -5. Visit [Sonatype Nexus][sonatype_nexus] to promote (close then release) the artifact. Or drop it - if there is a problem! + `find . -name "index.md"` -6. Tag the release, prepare for the next one, and push to GitHub. - - ``` git commit -am "Prepare for release $RELEASE_VERSION." git tag -a parent-$RELEASE_VERSION -m "Version $RELEASE_VERSION" + sed -i "" \ "s/VERSION_NAME=.*/VERSION_NAME=$NEXT_VERSION/g" \ gradle.properties git commit -am "Prepare next development version." + git push && git push --tags ``` - [sonatype_issues]: https://issues.sonatype.org/ - [sonatype_nexus]: https://oss.sonatype.org/ +4. Wait for [GitHub Actions][github_actions] to build and promote the release. + +[github_actions]: https://github.com/square/okio/actions diff --git a/docs/security.md b/docs/security.md index 1eb81491..03997d30 100644 --- a/docs/security.md +++ b/docs/security.md @@ -5,6 +5,7 @@ Security Policy | Version | Supported | | ------- | ---------- | +| 3.x | ✅ | | 2.x | ✅ | | 1.x | ✅ | diff --git a/gradle.properties b/gradle.properties index 21f92f07..ed1f6d9a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,25 +1,15 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8' -android.enableJetifier=true + +android.enableJetifier=false android.useAndroidX=true +android.defaults.buildfeatures.buildconfig=false +android.defaults.buildfeatures.aidl=false +android.defaults.buildfeatures.renderscript=false +android.defaults.buildfeatures.resvalues=false +android.defaults.buildfeatures.shaders=false -# Publishing SHA 256 and 512 hashses of maven-metadata is not supported by Sonatype and Nexus. -# See https://github.com/gradle/gradle/issues/11308 and -# https://issues.sonatype.org/browse/NEXUS-21802 -systemProp.org.gradle.internal.publish.checksums.insecure=true +kotlin.mpp.stability.nowarn=true GROUP=com.squareup.okio -VERSION_NAME=2.11.0-SNAPSHOT - -POM_DESCRIPTION=A modern I/O API for Java - -POM_URL=https://github.com/square/okio/ -POM_SCM_URL=https://github.com/square/okio/ -POM_SCM_CONNECTION=scm:git:git://github.com/square/okio.git -POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/square/okio.git - -POM_LICENCE_NAME=The Apache Software License, Version 2.0 -POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt -POM_LICENCE_DIST=repo - -POM_DEVELOPER_ID=square -POM_DEVELOPER_NAME=Square, Inc. +VERSION_NAME=3.7.0 +kotlin.mpp.commonizerLogLevel=info diff --git a/gradle/gradle-mvn-mpp-push.gradle b/gradle/gradle-mvn-mpp-push.gradle deleted file mode 100644 index 7ec94292..00000000 --- a/gradle/gradle-mvn-mpp-push.gradle +++ /dev/null @@ -1,137 +0,0 @@ -apply plugin: 'maven-publish' -apply plugin: 'signing' -apply plugin: 'org.jetbrains.dokka' - -def dokkaConfiguration = { - outputDirectory.set(file("$rootDir/docs/2.x")) - - dokkaSourceSets { - configureEach { - reportUndocumented.set(false) - skipDeprecated.set(true) - jdkVersion.set(8) - perPackageOption { - matchingRegex.set("com\\.squareup.okio.*") - suppress.set(true) - } - perPackageOption { - matchingRegex.set("okio\\.internal.*") - suppress.set(true) - } - } - } -} - -dokkaGfm.configure(dokkaConfiguration) -dokkaHtml.configure(dokkaConfiguration) - -def rootRelativePath(path) { - return rootProject.file(path).toString().replace('\\', '/') -} - -dokkaHtml.pluginsMapConfiguration.set([ - "org.jetbrains.dokka.base.DokkaBase": """{ "customStyleSheets": ["${rootRelativePath("docs/css/dokka-logo.css")}"], "customAssets" : ["${rootRelativePath("docs/images/logo-square.png")}"]}""" -]) - -def isReleaseBuild() { - return VERSION_NAME.contains("SNAPSHOT") == false -} - -def getReleaseRepositoryUrl() { - return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL : - "https://oss.sonatype.org/service/local/staging/deploy/maven2/" -} - -def getSnapshotRepositoryUrl() { - return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL : - "https://oss.sonatype.org/content/repositories/snapshots/" -} - -def getRepositoryUsername() { - return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : "" -} - -def getRepositoryPassword() { - return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : "" -} - -task emptySourcesJar(type: Jar) { - classifier = 'sources' -} - -task javadocsJar(type: Jar, dependsOn: dokkaGfm) { - classifier = 'javadoc' - from dokkaGfm.outputDirectory -} - -signing { - required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } - sign(publishing.publications) -} - -publishing { - publications.all { - artifact javadocsJar - - pom.withXml { - def root = asNode() - - root.children().last() + { - resolveStrategy = Closure.DELEGATE_FIRST - - description POM_DESCRIPTION - name POM_NAME - url POM_URL - licenses { - license { - name POM_LICENCE_NAME - url POM_LICENCE_URL - distribution POM_LICENCE_DIST - } - } - scm { - url POM_SCM_URL - connection POM_SCM_CONNECTION - developerConnection POM_SCM_DEV_CONNECTION - } - developers { - developer { - id POM_DEVELOPER_ID - name POM_DEVELOPER_NAME - } - } - } - } - } - - // Use default artifact name for the JVM target - publications { - kotlinMultiplatform { - artifactId = POM_ARTIFACT_ID + '-multiplatform' - } - jvm { - artifactId = POM_ARTIFACT_ID - } - } - - afterEvaluate { - publications.getByName('kotlinMultiplatform') { - // Source jars are only created for platforms, not the common artifact. - artifact emptySourcesJar - } - } - - repositories { - maven { - url isReleaseBuild() ? getReleaseRepositoryUrl() : getSnapshotRepositoryUrl() - credentials { - username getRepositoryUsername() - password getRepositoryPassword() - } - } - maven { - name 'test' - url "file://${rootProject.buildDir}/localMaven" - } - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..ba610822 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,24 @@ +[versions] +jmh = "1.37" +ktlint = "0.48.2" + +[libraries] +android-gradle-plugin = { module = "com.android.tools.build:gradle", version = "7.4.2" } +android-desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version = "2.0.4" } +androidx-test-ext-junit = { module = "androidx.test.ext:junit", version = "1.1.5" } +androidx-test-runner = { module = "androidx.test:runner", version = "1.5.2" } +binaryCompatibilityValidator = { module = "org.jetbrains.kotlinx.binary-compatibility-validator:org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin", version = "0.13.2" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test" } +kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit" } +kotlin-time = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.5.0" } +jmh-gradle-plugin = { module = "me.champeau.jmh:jmh-gradle-plugin", version = "0.7.2" } +jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } +jmh-generator = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" } +dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "1.9.10" } +spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "6.23.3" } +bnd = { module = "biz.aQute.bnd:biz.aQute.bnd.gradle", version = "6.4.0" } +vanniktech-publish-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.25.3" } +test-junit = { module = "junit:junit", version = "4.13.2" } +test-assertj = { module = "org.assertj:assertj-core", version = "3.24.2" } +test-assertk = "com.willowtreeapps.assertk:assertk:0.28.0" +test-jimfs = "com.google.jimfs:jimfs:1.3.0" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differindex e708b1c0..7f93135c 100644 --- a/gradle/wrapper/gradle-wrapper.jar +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4d9ca164..3fa8f862 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,99 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +119,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f9..93e3f59f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,92 @@ -@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock new file mode 100644 index 00000000..c9f26541 --- /dev/null +++ b/kotlin-js-store/yarn.lock @@ -0,0 +1,1931 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== + +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + +"@types/cors@^2.8.12": + version "2.8.13" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.13.tgz#b8ade22ba455a1b8cb3b5d3f35910fd204f84f94" + integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA== + dependencies: + "@types/node" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.44.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.0.tgz#55818eabb376e2272f77fbf5c96c43137c3c1e53" + integrity sha512-gsF+c/0XOguWgaOgvFs+xnnRqt9GwgTvIks36WpE6ueeI4KCEHHd8K/CKHqhOqrJKsYH8m27kRzQEvWXAwXUTw== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" + integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + +"@types/json-schema@*", "@types/json-schema@^7.0.8": + version "7.0.12" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" + integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== + +"@types/node@*", "@types/node@>=10.0.0": + version "20.4.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.1.tgz#a6033a8718653c50ac4962977e14d0f984d9527d" + integrity sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg== + +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^2.1.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.1": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.3": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abab@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + +accepts@~1.3.4: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-assertions@^1.7.6: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== + +acorn@^8.7.1, acorn@^8.8.2: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +body-parser@^1.19.0: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserslist@^4.14.5: + version "4.21.9" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.9.tgz#e11bdd3c313d7e2a9e87e8b4b0c7872b13897635" + integrity sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg== + dependencies: + caniuse-lite "^1.0.30001503" + electron-to-chromium "^1.4.431" + node-releases "^2.0.12" + update-browserslist-db "^1.0.11" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001503: + version "1.0.30001513" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001513.tgz#382fe5fbfb0f7abbaf8c55ca3ac71a0307a752e9" + integrity sha512-pnjGJo7SOOjAGytZZ203Em95MRM8Cr6jhCXNF/FAXTpCTRTECnqQWLpiTRqrFtdYcth8hf4WECUpkezuYsMVww== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@3.5.3, chokidar@^3.5.1: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +cors@~2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== + +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4.3.4, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +dom-serialize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ== + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.431: + version "1.4.454" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.454.tgz#774dc7cb5e58576d0125939ec34a4182f3ccc87d" + integrity sha512-pmf1rbAStw8UEQ0sr2cdJtWl48ZMuPD9Sto8HVQOq9vx9j2WgDEN6lYoaqFvqEHYOmGA9oRGn7LqWI9ta0YugQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +engine.io-parser@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.1.0.tgz#d593d6372d7f79212df48f807b8cace1ea1cb1b8" + integrity sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w== + +engine.io@~6.5.0: + version "6.5.1" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.1.tgz#59725f8593ccc891abb47f1efcdc52a089525a56" + integrity sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.1.0" + ws "~8.11.0" + +enhanced-resolve@^5.13.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +ent@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== + +envinfo@^7.7.3: + version "7.10.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.10.0.tgz#55146e3909cc5fe63c22da63fb15b05aeac35b13" + integrity sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw== + +es-module-lexer@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" + integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +follow-redirects@^1.0.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +format-util@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.3, glob@^7.1.7: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.11.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" + integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isbinaryfile@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" + integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-yaml@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +karma-chrome-launcher@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" + integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== + dependencies: + which "^1.2.1" + +karma-mocha@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d" + integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ== + dependencies: + minimist "^1.2.3" + +karma-sourcemap-loader@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== + dependencies: + graceful-fs "^4.2.10" + +karma-webpack@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.0.tgz#2a2c7b80163fe7ffd1010f83f5507f95ef39f840" + integrity sha512-+54i/cd3/piZuP3dr54+NcFeKOPnys5QeM1IY+0SPASwrtHsliXUiCL50iW+K9WWA7RvamC4macvvQ86l3KtaA== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + webpack-merge "^4.1.5" + +karma@6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.2.tgz#a983f874cee6f35990c4b2dcc3d274653714de8e" + integrity sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ== + dependencies: + "@colors/colors" "1.5.0" + body-parser "^1.19.0" + braces "^3.0.2" + chokidar "^3.5.1" + connect "^3.7.0" + di "^0.0.1" + dom-serialize "^2.2.1" + glob "^7.1.7" + graceful-fs "^4.2.6" + http-proxy "^1.18.1" + isbinaryfile "^4.0.8" + lodash "^4.17.21" + log4js "^6.4.1" + mime "^2.5.2" + minimatch "^3.0.4" + mkdirp "^0.5.5" + qjobs "^1.2.0" + range-parser "^1.2.1" + rimraf "^3.0.2" + socket.io "^4.4.1" + source-map "^0.6.1" + tmp "^0.2.1" + ua-parser-js "^0.7.30" + yargs "^16.1.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log4js@^6.4.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.5: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" + integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + nanoid "3.3.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-releases@^2.0.12: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +punycode@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +qjobs@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" + integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== + dependencies: + is-core-module "^2.11.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + +rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^3.1.1, schema-utils@^3.1.2: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +socket.io-adapter@~2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12" + integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== + dependencies: + ws "~8.11.0" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.4.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.1.tgz#9009f31bf7be25478895145e92fbc972ad1db900" + integrity sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.3.2" + engine.io "~6.5.0" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-loader@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-4.0.1.tgz#72f00d05f5d1f90f80974eda781cbd7107c125f2" + integrity sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA== + dependencies: + abab "^2.0.6" + iconv-lite "^0.6.3" + source-map-js "^1.0.2" + +source-map-support@0.5.21, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@8.1.1, supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.7: + version "5.3.9" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" + integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.17" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.16.8" + +terser@^5.16.8: + version "5.18.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.18.2.tgz#ff3072a0faf21ffd38f99acc9a0ddf7b5f07b948" + integrity sha512-Ah19JS86ypbJzTzvUCX7KOsEIhDaRONungA4aYBjEP3JZRf4ocuDzTg4QWZnPn9DEMiMYGJPiSOy7aykoCc70w== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +tmp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + +ua-parser-js@^0.7.30: + version "0.7.35" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307" + integrity sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g== + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@^1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +webpack-cli@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.0.tgz#abc4b1f44b50250f2632d8b8b536cfe2f6257891" + integrity sha512-a7KRJnCxejFoDpYTOwzm5o21ZXMaNqtRlvS183XzGDUPRdVEzJNImcQokqYZ8BNTnk9DkKiuWxw75+DCCoZ26w== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^2.1.0" + "@webpack-cli/info" "^2.0.1" + "@webpack-cli/serve" "^2.0.3" + colorette "^2.0.14" + commander "^10.0.1" + cross-spawn "^7.0.3" + envinfo "^7.7.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^5.7.3" + +webpack-merge@^4.1.5: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-merge@^5.7.3: + version "5.9.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826" + integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg== + dependencies: + clone-deep "^4.0.1" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.82.0: + version "5.82.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.82.0.tgz#3c0d074dec79401db026b4ba0fb23d6333f88e7d" + integrity sha512-iGNA2fHhnDcV1bONdUu554eZx+XeldsaeQ8T67H6KKHl2nUSwX8Zm7cmzOA46ox/X1ARxf7Bjv8wQ/HsB5fxBg== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" + acorn "^8.7.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.13.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.2" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.7" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +which@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0, yargs@^16.1.1: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== @@ -1,4 +1,5 @@ site_name: Okio +site_url: https://square.github.io/okio repo_name: Okio repo_url: https://github.com/square/okio site_description: "A modern I/O library for Android, Kotlin, and Java." @@ -13,8 +14,22 @@ theme: favicon: images/icon-square.png logo: images/icon-square.png palette: - primary: 'deep purple' - accent: 'white' + # Palette toggle for light mode + - scheme: default + media: "(prefers-color-scheme: light)" + primary: 'deep purple' + accent: 'white' + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Palette toggle for dark mode + - scheme: slate + media: "(prefers-color-scheme: dark)" + primary: 'deep purple' + accent: 'white' + toggle: + icon: material/brightness-4 + name: Switch to light mode icon: repo: fontawesome/brands/github @@ -43,17 +58,23 @@ markdown_extensions: - pymdownx.magiclink - pymdownx.smartsymbols - pymdownx.superfences - - pymdownx.tabbed + - pymdownx.tabbed: + alternate_style: true - pymdownx.tilde - tables nav: - 'Overview': index.md - 'Stack Overflow ⏏': https://stackoverflow.com/questions/tagged/okio?sort=active - - '2.x API': 2.x/okio/okio/index.html + - 'Recipes': recipes.md + - 'java.io Recipes': java_io_recipes.md + - '3.x API': + - 'okio': 3.x/okio/okio/okio/index.html + - 'fakefilesystem': 3.x/okio-fakefilesystem/okio-fakefilesystem/okio.fakefilesystem/-fake-file-system/index.html + - 'nodefilesystem': 3.x/okio-nodefilesystem/okio-nodefilesystem/okio/-node-js-file-system/index.html - '1.x API ⏏': https://square.github.io/okio/1.x/okio/ - 'Change Log': changelog.md + - 'File System': file_system.md - 'Multiplatform': multiplatform.md - 'Contributing': contributing.md - 'Code of Conduct': code_of_conduct.md - diff --git a/okio-assetfilesystem/README.md b/okio-assetfilesystem/README.md new file mode 100644 index 00000000..9258ed28 --- /dev/null +++ b/okio-assetfilesystem/README.md @@ -0,0 +1,5 @@ +Okio Asset File System +---------------------- + +This module contains a `FileSystem` implementation for +an Android application's `assets/` contents. diff --git a/okio-assetfilesystem/api/okio-assetfilesystem.api b/okio-assetfilesystem/api/okio-assetfilesystem.api new file mode 100644 index 00000000..773a8e2c --- /dev/null +++ b/okio-assetfilesystem/api/okio-assetfilesystem.api @@ -0,0 +1,4 @@ +public final class okio/assetfilesystem/AssetFileSystemKt { + public static final fun asFileSystem (Landroid/content/res/AssetManager;)Lokio/FileSystem; +} + diff --git a/okio-assetfilesystem/build.gradle.kts b/okio-assetfilesystem/build.gradle.kts new file mode 100644 index 00000000..b323aea4 --- /dev/null +++ b/okio-assetfilesystem/build.gradle.kts @@ -0,0 +1,36 @@ +import com.vanniktech.maven.publish.AndroidSingleVariantLibrary +import com.vanniktech.maven.publish.MavenPublishBaseExtension + +plugins { + id("com.android.library") + kotlin("android") + id("org.jetbrains.dokka") + id("com.vanniktech.maven.publish.base") + id("binary-compatibility-validator") + id("build-support") +} + +android { + namespace = "okio.assetfilesystem" + compileSdk = 33 + + defaultConfig { + minSdk = 14 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + api(projects.okio) + + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.kotlin.test) + androidTestImplementation(libs.test.assertk) +} + +configure<MavenPublishBaseExtension> { + configure( + AndroidSingleVariantLibrary() + ) +} diff --git a/okio-assetfilesystem/src/androidTest/assets/dir/nested.txt b/okio-assetfilesystem/src/androidTest/assets/dir/nested.txt new file mode 100644 index 00000000..eefc327e --- /dev/null +++ b/okio-assetfilesystem/src/androidTest/assets/dir/nested.txt @@ -0,0 +1 @@ +Nested! diff --git a/okio-assetfilesystem/src/androidTest/assets/file.txt b/okio-assetfilesystem/src/androidTest/assets/file.txt new file mode 100644 index 00000000..9b72baa6 --- /dev/null +++ b/okio-assetfilesystem/src/androidTest/assets/file.txt @@ -0,0 +1 @@ +File! diff --git a/okio-assetfilesystem/src/androidTest/assets/moby10b.txt b/okio-assetfilesystem/src/androidTest/assets/moby10b.txt new file mode 100644 index 00000000..e052542e --- /dev/null +++ b/okio-assetfilesystem/src/androidTest/assets/moby10b.txt @@ -0,0 +1,23244 @@ +**The Project Gutenberg Etext of Moby Dick, by Herman Melville** +#3 in our series by Herman Melville + +This Project Gutenberg version of Moby Dick is based on a combination +of the etext from the ERIS project at Virginia Tech and another from +Project Gutenberg's archives, as compared to a public-domain hard copy. + +Copyright laws are changing all over the world, be sure to check +the copyright laws for your country before posting these files!! + +Please take a look at the important information in this header. +We encourage you to keep this file on your own disk, keeping an +electronic path open for the next readers. Do not remove this. + + +**Welcome To The World of Free Plain Vanilla Electronic Texts** + +**Etexts Readable By Both Humans and By Computers, Since 1971** + +*These Etexts Prepared By Hundreds of Volunteers and Donations* + +Information on contacting Project Gutenberg to get Etexts, and +further information is included below. We need your donations. + + +Title: Moby Dick; or The Whale + +Author: Herman Melville + +June, 2001 [Etext #2701] + +**The Project Gutenberg Etext of Moby Dick, by Herman Melville** +******This file should be named moby10b.txt or moby10b.zip****** + +Corrected EDITIONS of our etexts get a new NUMBER, moby11b.txt +VERSIONS based on separate sources get new LETTER, moby10c.txt + +This etext was prepared by Daniel Lazarus and Jonesey + +Project Gutenberg Etexts are usually created from multiple editions, +all of which are in the Public Domain in the United States, unless a +copyright notice is included. Therefore, we usually do NOT keep any +of these books in compliance with any particular paper edition. + + +We are now trying to release all our books one month in advance +of the official release dates, leaving time for better editing. + +Please note: neither this list nor its contents are final till +midnight of the last day of the month of any such announcement. +The official release date of all Project Gutenberg Etexts is at +Midnight, Central Time, of the last day of the stated month. A +preliminary version may often be posted for suggestion, comment +and editing by those who wish to do so. To be sure you have an +up to date first edition [xxxxx10x.xxx] please check file sizes +in the first week of the next month. Since our ftp program has +a bug in it that scrambles the date [tried to fix and failed] a +look at the file size will have to do, but we will try to see a +new copy has at least one byte more or less. + + +Information about Project Gutenberg (one page) + +We produce about two million dollars for each hour we work. The +time it takes us, a rather conservative estimate, is fifty hours +to get any etext selected, entered, proofread, edited, copyright +searched and analyzed, the copyright letters written, etc. This +projected audience is one hundred million readers. If our value +per text is nominally estimated at one dollar then we produce $2 +million dollars per hour this year as we release thirty-six text +files per month, or 432 more Etexts in 1999 for a total of 2000+ +If these reach just 10% of the computerized population, then the +total should reach over 200 billion Etexts given away this year. + +The Goal of Project Gutenberg is to Give Away One Trillion Etext +Files by December 31, 2001. [10,000 x 100,000,000 = 1 Trillion] +This is ten thousand titles each to one hundred million readers, +which is only ~5% of the present number of computer users. + +At our revised rates of production, we will reach only one-third +of that goal by the end of 2001, or about 3,333 Etexts unless we +manage to get some real funding; currently our funding is mostly +from Michael Hart's salary at Carnegie-Mellon University, and an +assortment of sporadic gifts; this salary is only good for a few +more years, so we are looking for something to replace it, as we +don't want Project Gutenberg to be so dependent on one person. + +We need your donations more than ever! + + +All donations should be made to "Project Gutenberg/CMU": and are +tax deductible to the extent allowable by law. (CMU = Carnegie- +Mellon University). + +For these and other matters, please mail to: + +Project Gutenberg +P. O. Box 2782 +Champaign, IL 61825 + +When all other email fails. . .try our Executive Director: +Michael S. Hart <hart@pobox.com> +hart@pobox.com forwards to hart@prairienet.org and archive.org +if your mail bounces from archive.org, I will still see it, if +it bounces from prairienet.org, better resend later on. . . . + +We would prefer to send you this information by email. + +****** + +To access Project Gutenberg etexts, use any Web browser +to view http://promo.net/pg. This site lists Etexts by +author and by title, and includes information about how +to get involved with Project Gutenberg. You could also +download our past Newsletters, or subscribe here. This +is one of our major sites, please email hart@pobox.com, +for a more complete list of our various sites. + +To go directly to the etext collections, use FTP or any +Web browser to visit a Project Gutenberg mirror (mirror +sites are available on 7 continents; mirrors are listed +at http://promo.net/pg). + +Mac users, do NOT point and click, typing works better. + +Example FTP session: + +ftp sunsite.unc.edu +login: anonymous +password: your@login +cd pub/docs/books/gutenberg +cd etext90 through etext99 +dir [to see files] +get or mget [to get files. . .set bin for zip files] +GET GUTINDEX.?? [to get a year's listing of books, e.g., GUTINDEX.99] +GET GUTINDEX.ALL [to get a listing of ALL books] + +*** + +**Information prepared by the Project Gutenberg legal advisor** + +(Three Pages) + + +***START**THE SMALL PRINT!**FOR PUBLIC DOMAIN ETEXTS**START*** +Why is this "Small Print!" statement here? You know: lawyers. +They tell us you might sue us if there is something wrong with +your copy of this etext, even if you got it for free from +someone other than us, and even if what's wrong is not our +fault. So, among other things, this "Small Print!" statement +disclaims most of our liability to you. It also tells you how +you can distribute copies of this etext if you want to. + +*BEFORE!* YOU USE OR READ THIS ETEXT +By using or reading any part of this PROJECT GUTENBERG-tm +etext, you indicate that you understand, agree to and accept +this "Small Print!" statement. If you do not, you can receive +a refund of the money (if any) you paid for this etext by +sending a request within 30 days of receiving it to the person +you got it from. If you received this etext on a physical +medium (such as a disk), you must return it with your request. + +ABOUT PROJECT GUTENBERG-TM ETEXTS +This PROJECT GUTENBERG-tm etext, like most PROJECT GUTENBERG- +tm etexts, is a "public domain" work distributed by Professor +Michael S. Hart through the Project Gutenberg Association at +Carnegie-Mellon University (the "Project"). Among other +things, this means that no one owns a United States copyright +on or for this work, so the Project (and you!) can copy and +distribute it in the United States without permission and +without paying copyright royalties. Special rules, set forth +below, apply if you wish to copy and distribute this etext +under the Project's "PROJECT GUTENBERG" trademark. + +To create these etexts, the Project expends considerable +efforts to identify, transcribe and proofread public domain +works. Despite these efforts, the Project's etexts and any +medium they may be on may contain "Defects". Among other +things, Defects may take the form of incomplete, inaccurate or +corrupt data, transcription errors, a copyright or other +intellectual property infringement, a defective or damaged +disk or other etext medium, a computer virus, or computer +codes that damage or cannot be read by your equipment. + +LIMITED WARRANTY; DISCLAIMER OF DAMAGES +But for the "Right of Replacement or Refund" described below, +[1] the Project (and any other party you may receive this +etext from as a PROJECT GUTENBERG-tm etext) disclaims all +liability to you for damages, costs and expenses, including +legal fees, and [2] YOU HAVE NO REMEDIES FOR NEGLIGENCE OR +UNDER STRICT LIABILITY, OR FOR BREACH OF WARRANTY OR CONTRACT, +INCLUDING BUT NOT LIMITED TO INDIRECT, CONSEQUENTIAL, PUNITIVE +OR INCIDENTAL DAMAGES, EVEN IF YOU GIVE NOTICE OF THE +POSSIBILITY OF SUCH DAMAGES. + +If you discover a Defect in this etext within 90 days of +receiving it, you can receive a refund of the money (if any) +you paid for it by sending an explanatory note within that +time to the person you received it from. If you received it +on a physical medium, you must return it with your note, and +such person may choose to alternatively give you a replacement +copy. If you received it electronically, such person may +choose to alternatively give you a second opportunity to +receive it electronically. + +THIS ETEXT IS OTHERWISE PROVIDED TO YOU "AS-IS". NO OTHER +WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, ARE MADE TO YOU AS +TO THE ETEXT OR ANY MEDIUM IT MAY BE ON, INCLUDING BUT NOT +LIMITED TO WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A +PARTICULAR PURPOSE. + +Some states do not allow disclaimers of implied warranties or +the exclusion or limitation of consequential damages, so the +above disclaimers and exclusions may not apply to you, and you +may have other legal rights. + +INDEMNITY +You will indemnify and hold the Project, its directors, +officers, members and agents harmless from all liability, cost +and expense, including legal fees, that arise directly or +indirectly from any of the following that you do or cause: +[1] distribution of this etext, [2] alteration, modification, +or addition to the etext, or [3] any Defect. + +DISTRIBUTION UNDER "PROJECT GUTENBERG-tm" +You may distribute copies of this etext electronically, or by +disk, book or any other medium if you either delete this +"Small Print!" and all other references to Project Gutenberg, +or: + +[1] Only give exact copies of it. Among other things, this + requires that you do not remove, alter or modify the + etext or this "small print!" statement. You may however, + if you wish, distribute this etext in machine readable + binary, compressed, mark-up, or proprietary form, + including any form resulting from conversion by word pro- + cessing or hypertext software, but only so long as + *EITHER*: + + [*] The etext, when displayed, is clearly readable, and + does *not* contain characters other than those + intended by the author of the work, although tilde + (~), asterisk (*) and underline (_) characters may + be used to convey punctuation intended by the + author, and additional characters may be used to + indicate hypertext links; OR + + [*] The etext may be readily converted by the reader at + no expense into plain ASCII, EBCDIC or equivalent + form by the program that displays the etext (as is + the case, for instance, with most word processors); + OR + + [*] You provide, or agree to also provide on request at + no additional cost, fee or expense, a copy of the + etext in its original plain ASCII form (or in EBCDIC + or other equivalent proprietary form). + +[2] Honor the etext refund and replacement provisions of this + "Small Print!" statement. + +[3] Pay a trademark license fee to the Project of 20% of the + net profits you derive calculated using the method you + already use to calculate your applicable taxes. If you + don't derive profits, no royalty is due. Royalties are + payable to "Project Gutenberg Association/Carnegie-Mellon + University" within the 60 days following each + date you prepare (or were legally required to prepare) + your annual (or equivalent periodic) tax return. + +WHAT IF YOU *WANT* TO SEND MONEY EVEN IF YOU DON'T HAVE TO? +The Project gratefully accepts contributions in money, time, +scanning machines, OCR software, public domain etexts, royalty +free copyright licenses, and every other sort of contribution +you can think of. Money should be paid to "Project Gutenberg +Association / Carnegie-Mellon University". + +We are planning on making some changes in our donation structure +in 2000, so you might want to email me, hart@pobox.com beforehand. + + + + +*END THE SMALL PRINT! FOR PUBLIC DOMAIN ETEXTS*Ver.04.29.93*END* + + + + + +This etext was prepared by Daniel Lazarus and Jonesey + + + + + +Notes on this etext of Moby Dick: + +This text is a combination of etexts, one from the now-defunct ERIS +project at Virginia Tech and one from Project Gutenberg's archives. +The proofreaders of this version are indebted to The University of +Adelaide Library for preserving the Virginia Tech version. The +resulting etext was compared with a public domain hard copy version of +the text. + +In chapters 24, 89, and 90, we substituted a capital L for the symbol +for the British pound, a unit of currency. + + + + + +MOBY DICK; OR THE WHALE + +by Herman Melville + + + + +ETYMOLOGY. + +(Supplied by a Late Consumptive Usher to a Grammar School) + +The pale Usher--threadbare in coat, heart, body, and brain; I see him +now. He was ever dusting his old lexicons and grammars, with a queer +handkerchief, mockingly embellished with all the gay flags of all the +known nations of the world. He loved to dust his old grammars; it +somehow mildly reminded him of his mortality. + +"While you take in hand to school others, and to teach them by what +name a whale-fish is to be called in our tongue leaving out, through +ignorance, the letter H, which almost alone maketh the signification +of the word, you deliver that which is not true." --HACKLUYT + +"WHALE. ... Sw. and Dan. HVAL. This animal is named from roundness +or rolling; for in Dan. HVALT is arched or vaulted." --WEBSTER'S +DICTIONARY + +"WHALE. ... It is more immediately from the Dut. and Ger. WALLEN; +A.S. WALW-IAN, to roll, to wallow." --RICHARDSON'S DICTIONARY + +KETOS, GREEK. +CETUS, LATIN. +WHOEL, ANGLO-SAXON. +HVALT, DANISH. +WAL, DUTCH. +HWAL, SWEDISH. +WHALE, ICELANDIC. +WHALE, ENGLISH. +BALEINE, FRENCH. +BALLENA, SPANISH. +PEKEE-NUEE-NUEE, FEGEE. +PEKEE-NUEE-NUEE, ERROMANGOAN. + + + + +EXTRACTS (Supplied by a Sub-Sub-Librarian). + +It will be seen that this mere painstaking burrower and grub-worm of +a poor devil of a Sub-Sub appears to have gone through the long +Vaticans and street-stalls of the earth, picking up whatever random +allusions to whales he could anyways find in any book whatsoever, +sacred or profane. Therefore you must not, in every case at least, +take the higgledy-piggledy whale statements, however authentic, in +these extracts, for veritable gospel cetology. Far from it. As +touching the ancient authors generally, as well as the poets here +appearing, these extracts are solely valuable or entertaining, as +affording a glancing bird's eye view of what has been promiscuously +said, thought, fancied, and sung of Leviathan, by many nations and +generations, including our own. + +So fare thee well, poor devil of a Sub-Sub, whose commentator I am. +Thou belongest to that hopeless, sallow tribe which no wine of this +world will ever warm; and for whom even Pale Sherry would be too +rosy-strong; but with whom one sometimes loves to sit, and feel +poor-devilish, too; and grow convivial upon tears; and say to them +bluntly, with full eyes and empty glasses, and in not altogether +unpleasant sadness--Give it up, Sub-Subs! For by how much the more +pains ye take to please the world, by so much the more shall ye for +ever go thankless! Would that I could clear out Hampton Court and +the Tuileries for ye! But gulp down your tears and hie aloft to the +royal-mast with your hearts; for your friends who have gone before +are clearing out the seven-storied heavens, and making refugees of +long-pampered Gabriel, Michael, and Raphael, against your coming. +Here ye strike but splintered hearts together--there, ye shall strike +unsplinterable glasses! + + +EXTRACTS. + +"And God created great whales." --GENESIS. + +"Leviathan maketh a path to shine after him; One would think the deep +to be hoary." --JOB. + +"Now the Lord had prepared a great fish to swallow up Jonah." +--JONAH. + +"There go the ships; there is that Leviathan whom thou hast made to +play therein." --PSALMS. + +"In that day, the Lord with his sore, and great, and strong sword, +shall punish Leviathan the piercing serpent, even Leviathan that +crooked serpent; and he shall slay the dragon that is in the sea." +--ISAIAH + +"And what thing soever besides cometh within the chaos of this +monster's mouth, be it beast, boat, or stone, down it goes all +incontinently that foul great swallow of his, and perisheth in the +bottomless gulf of his paunch." --HOLLAND'S PLUTARCH'S MORALS. + +"The Indian Sea breedeth the most and the biggest fishes that are: +among which the Whales and Whirlpooles called Balaene, take up as +much in length as four acres or arpens of land." --HOLLAND'S PLINY. + +"Scarcely had we proceeded two days on the sea, when about sunrise a +great many Whales and other monsters of the sea, appeared. Among the +former, one was of a most monstrous size. ... This came towards us, +open-mouthed, raising the waves on all sides, and beating the sea +before him into a foam." --TOOKE'S LUCIAN. "THE TRUE HISTORY." + +"He visited this country also with a view of catching horse-whales, +which had bones of very great value for their teeth, of which he +brought some to the king. ... The best whales were catched in his +own country, of which some were forty-eight, some fifty yards long. +He said that he was one of six who had killed sixty in two days." +--OTHER OR OCTHER'S VERBAL NARRATIVE TAKEN DOWN FROM HIS MOUTH BY +KING ALFRED, A.D. 890. + +"And whereas all the other things, whether beast or vessel, that +enter into the dreadful gulf of this monster's (whale's) mouth, are +immediately lost and swallowed up, the sea-gudgeon retires into it in +great security, and there sleeps." --MONTAIGNE. --APOLOGY FOR +RAIMOND SEBOND. + +"Let us fly, let us fly! Old Nick take me if is not Leviathan +described by the noble prophet Moses in the life of patient Job." +--RABELAIS. + +"This whale's liver was two cartloads." --STOWE'S ANNALS. + +"The great Leviathan that maketh the seas to seethe like boiling +pan." --LORD BACON'S VERSION OF THE PSALMS. + +"Touching that monstrous bulk of the whale or ork we have received +nothing certain. They grow exceeding fat, insomuch that an +incredible quantity of oil will be extracted out of one whale." +--IBID. "HISTORY OF LIFE AND DEATH." + +"The sovereignest thing on earth is parmacetti for an inward bruise." +--KING HENRY. + +"Very like a whale." --HAMLET. + +"Which to secure, no skill of leach's art +Mote him availle, but to returne againe +To his wound's worker, that with lowly dart, +Dinting his breast, had bred his restless paine, +Like as the wounded whale to shore flies thro' the maine." +--THE FAERIE QUEEN. + +"Immense as whales, the motion of whose vast bodies can in a peaceful +calm trouble the ocean til it boil." --SIR WILLIAM DAVENANT. PREFACE +TO GONDIBERT. + +"What spermacetti is, men might justly doubt, since the learned +Hosmannus in his work of thirty years, saith plainly, Nescio quid +sit." --SIR T. BROWNE. OF SPERMA CETI AND THE SPERMA CETI WHALE. +VIDE HIS V. E. + +"Like Spencer's Talus with his modern flail +He threatens ruin with his ponderous tail. +... +Their fixed jav'lins in his side he wears, +And on his back a grove of pikes appears." --WALLER'S BATTLE OF THE +SUMMER ISLANDS. + +"By art is created that great Leviathan, called a Commonwealth or +State--(in Latin, Civitas) which is but an artificial man." --OPENING +SENTENCE OF HOBBES'S LEVIATHAN. + +"Silly Mansoul swallowed it without chewing, as if it had been a +sprat in the mouth of a whale." --PILGRIM'S PROGRESS. + +"That sea beast +Leviathan, which God of all his works +Created hugest that swim the ocean stream." --PARADISE LOST. + +---"There Leviathan, +Hugest of living creatures, in the deep +Stretched like a promontory sleeps or swims, +And seems a moving land; and at his gills +Draws in, and at his breath spouts out a sea." --IBID. + +"The mighty whales which swim in a sea of water, and have a sea of +oil swimming in them." --FULLLER'S PROFANE AND HOLY STATE. + +"So close behind some promontory lie +The huge Leviathan to attend their prey, +And give no chance, but swallow in the fry, +Which through their gaping jaws mistake the way." +--DRYDEN'S ANNUS MIRABILIS. + +"While the whale is floating at the stern of the ship, they cut off +his head, and tow it with a boat as near the shore as it will come; +but it will be aground in twelve or thirteen feet water." --THOMAS +EDGE'S TEN VOYAGES TO SPITZBERGEN, IN PURCHAS. + +"In their way they saw many whales sporting in the ocean, and in +wantonness fuzzing up the water through their pipes and vents, which +nature has placed on their shoulders." --SIR T. HERBERT'S VOYAGES +INTO ASIA AND AFRICA. HARRIS COLL. + +"Here they saw such huge troops of whales, that they were forced to +proceed with a great deal of caution for fear they should run their +ship upon them." --SCHOUTEN'S SIXTH CIRCUMNAVIGATION. + +"We set sail from the Elbe, wind N.E. in the ship called The +Jonas-in-the-Whale. ... Some say the whale can't open his mouth, but +that is a fable. ... They frequently climb up the masts to see +whether they can see a whale, for the first discoverer has a ducat +for his pains. ... I was told of a whale taken near Shetland, that +had above a barrel of herrings in his belly. ... One of our +harpooneers told me that he caught once a whale in Spitzbergen that +was white all over." --A VOYAGE TO GREENLAND, A.D. 1671 HARRIS COLL. + +"Several whales have come in upon this coast (Fife) Anno 1652, one +eighty feet in length of the whale-bone kind came in, which (as I was +informed), besides a vast quantity of oil, did afford 500 weight of +baleen. The jaws of it stand for a gate in the garden of Pitferren." +--SIBBALD'S FIFE AND KINROSS. + +"Myself have agreed to try whether I can master and kill this +Sperma-ceti whale, for I could never hear of any of that sort that +was killed by any man, such is his fierceness and swiftness." +--RICHARD STRAFFORD'S LETTER FROM THE BERMUDAS. PHIL. TRANS. A.D. +1668. + +"Whales in the sea God's voice obey." --N. E. PRIMER. + +"We saw also abundance of large whales, there being more in those +southern seas, as I may say, by a hundred to one; than we have to the +northward of us." --CAPTAIN COWLEY'S VOYAGE ROUND THE GLOBE, A.D. +1729. + +"... and the breath of the whale is frequendy attended with such an +insupportable smell, as to bring on a disorder of the brain." +--ULLOA'S SOUTH AMERICA. + +"To fifty chosen sylphs of special note, +We trust the important charge, the petticoat. +Oft have we known that seven-fold fence to fail, +Tho' stuffed with hoops and armed with ribs of whale." --RAPE +OF THE LOCK. + +"If we compare land animals in respect to magnitude, with those that +take up their abode in the deep, we shall find they will appear +contemptible in the comparison. The whale is doubtless the largest +animal in creation." --GOLDSMITH, NAT. HIST. + +"If you should write a fable for little fishes, you would make them +speak like great wales." --GOLDSMITH TO JOHNSON. + +"In the afternoon we saw what was supposed to be a rock, but it was +found to be a dead whale, which some Asiatics had killed, and were +then towing ashore. They seemed to endeavor to conceal themselves +behind the whale, in order to avoid being seen by us." --COOK'S +VOYAGES. + +"The larger whales, they seldom venture to attack. They stand in so +great dread of some of them, that when out at sea they are afraid to +mention even their names, and carry dung, lime-stone, juniper-wood, +and some other articles of the same nature in their boats, in order +to terrify and prevent their too near approach." --UNO VON TROIL'S +LETTERS ON BANKS'S AND SOLANDER'S VOYAGE TO ICELAND IN 1772. + +"The Spermacetti Whale found by the Nantuckois, is an active, fierce +animal, and requires vast address and boldness in the fishermen." +--THOMAS JEFFERSON'S WHALE MEMORIAL TO THE FRENCH MINISTER IN 1778. + +"And pray, sir, what in the world is equal to it?" --EDMUND BURKE'S +REFERENCE IN PARLIAMENT TO THE NANTUCKET WHALE-FISHERY. + +"Spain--a great whale stranded on the shores of Europe." --EDMUND +BURKE. (SOMEWHERE.) + +"A tenth branch of the king's ordinary revenue, said to be grounded +on the consideration of his guarding and protecting the seas from +pirates and robbers, is the right to royal fish, which are whale and +sturgeon. And these, when either thrown ashore or caught near the +coast, are the property of the king." --BLACKSTONE. + +"Soon to the sport of death the crews repair: +Rodmond unerring o'er his head suspends +The barbed steel, and every turn attends." +--FALCONER'S SHIPWRECK. + +"Bright shone the roofs, the domes, the spires, +And rockets blew self driven, +To hang their momentary fire +Around the vault of heaven. + +"So fire with water to compare, +The ocean serves on high, +Up-spouted by a whale in air, +To express unwieldy joy." --COWPER, ON THE QUEEN'S +VISIT TO LONDON. + +"Ten or fifteen gallons of blood are thrown out of the heart at a +stroke, with immense velocity." --JOHN HUNTER'S ACCOUNT OF THE +DISSECTION OF A WHALE. (A SMALL SIZED ONE.) + +"The aorta of a whale is larger in the bore than the main pipe of the +water-works at London Bridge, and the water roaring in its passage +through that pipe is inferior in impetus and velocity to the blood +gushing from the whale's heart." --PALEY'S THEOLOGY. + +"The whale is a mammiferous animal without hind feet." --BARON +CUVIER. + +"In 40 degrees south, we saw Spermacetti Whales, but did not take any +till the first of May, the sea being then covered with them." +--COLNETT'S VOYAGE FOR THE PURPOSE OF EXTENDING THE SPERMACETI WHALE +FISHERY. + +"In the free element beneath me swam, +Floundered and dived, in play, in chace, in battle, +Fishes of every colour, form, and kind; +Which language cannot paint, and mariner +Had never seen; from dread Leviathan +To insect millions peopling every wave: +Gather'd in shoals immense, like floating islands, +Led by mysterious instincts through that waste +And trackless region, though on every side +Assaulted by voracious enemies, +Whales, sharks, and monsters, arm'd in front or jaw, +With swords, saws, spiral horns, or hooked fangs." +--MONTGOMERY'S WORLD BEFORE THE FLOOD. + +"Io! Paean! Io! sing. +To the finny people's king. +Not a mightier whale than this +In the vast Atlantic is; +Not a fatter fish than he, +Flounders round the Polar Sea." --CHARLES LAMB'S TRIUMPH OF THE +WHALE. + +"In the year 1690 some persons were on a high hill observing the +whales spouting and sporting with each other, when one observed: +there--pointing to the sea--is a green pasture where our children's +grand-children will go for bread." --OBED MACY'S HISTORY OF +NANTUCKET. + +"I built a cottage for Susan and myself and made a gateway in the +form of a Gothic Arch, by setting up a whale's jaw bones." +--HAWTHORNE'S TWICE TOLD TALES. + +"She came to bespeak a monument for her first love, who had been +killed by a whale in the Pacific ocean, no less than forty years +ago." --IBID. + +"No, Sir, 'tis a Right Whale," answered Tom; "I saw his sprout; he +threw up a pair of as pretty rainbows as a Christian would wish to +look at. He's a raal oil-butt, that fellow!" --COOPER'S PILOT. + +"The papers were brought in, and we saw in the Berlin Gazette that +whales had been introduced on the stage there." --ECKERMANN'S +CONVERSATIONS WITH GOETHE. + +"My God! Mr. Chace, what is the matter?" I answered, "we have been +stove by a whale." --"NARRATIVE OF THE SHIPWRECK OF THE WHALE SHIP +ESSEX OF NANTUCKET, WHICH WAS ATTACKED AND FINALLY DESTROYED BY A +LARGE SPERM WHALE IN THE PACIFIC OCEAN." BY OWEN CHACE OF NANTUCKET, +FIRST MATE OF SAID VESSEL. NEW YORK, 1821. + +"A mariner sat in the shrouds one night, +The wind was piping free; +Now bright, now dimmed, was the moonlight pale, +And the phospher gleamed in the wake of the whale, +As it floundered in the sea." --ELIZABETH OAKES SMITH. + +"The quantity of line withdrawn from the boats engaged in the capture +of this one whale, amounted altogether to 10,440 yards or nearly six +English miles. ... + +"Sometimes the whale shakes its tremendous tail in the air, which, +cracking like a whip, resounds to the distance of three or four +miles." --SCORESBY. + +"Mad with the agonies he endures from these fresh attacks, the +infuriated Sperm Whale rolls over and over; he rears his enormous +head, and with wide expanded jaws snaps at everything around him; he +rushes at the boats with his head; they are propelled before him with +vast swiftness, and sometimes utterly destroyed. ... It is a matter +of great astonishment that the consideration of the habits of so +interesting, and, in a commercial point of view, so important an +animal (as the Sperm Whale) should have been so entirely neglected, +or should have excited so little curiosity among the numerous, and +many of them competent observers, that of late years, must have +possessed the most abundant and the most convenient opportunities of +witnessing their habitudes." --THOMAS BEALE'S HISTORY OF THE SPERM +WHALE, 1839. + +"The Cachalot" (Sperm Whale) "is not only better armed than the True +Whale" (Greenland or Right Whale) "in possessing a formidable weapon +at either extremity of its body, but also more frequently displays a +disposition to employ these weapons offensively and in manner at once +so artful, bold, and mischievous, as to lead to its being regarded as +the most dangerous to attack of all the known species of the whale +tribe." --FREDERICK DEBELL BENNETT'S WHALING VOYAGE ROUND THE GLOBE, +1840. + +October 13. "There she blows," was sung out from the mast-head. +"Where away?" demanded the captain. +"Three points off the lee bow, sir." +"Raise up your wheel. Steady!" "Steady, sir." +"Mast-head ahoy! Do you see that whale now?" +"Ay ay, sir! A shoal of Sperm Whales! There she blows! There she +breaches!" +"Sing out! sing out every time!" +"Ay Ay, sir! There she blows! there--there--THAR she +blows--bowes--bo-o-os!" +"How far off?" +"Two miles and a half." +"Thunder and lightning! so near! Call all hands." --J. ROSS BROWNE'S +ETCHINGS OF A WHALING CRUIZE. 1846. + +"The Whale-ship Globe, on board of which vessel occurred the horrid +transactions we are about to relate, belonged to the island of +Nantucket." --"NARRATIVE OF THE GLOBE," BY LAY AND HUSSEY SURVIVORS. +A.D. 1828. + +Being once pursued by a whale which he had wounded, he parried the +assault for some time with a lance; but the furious monster at length +rushed on the boat; himself and comrades only being preserved by +leaping into the water when they saw the onset was inevitable." +--MISSIONARY JOURNAL OF TYERMAN AND BENNETT. + +"Nantucket itself," said Mr. Webster, "is a very striking and +peculiar portion of the National interest. There is a population of +eight or nine thousand persons living here in the sea, adding largely +every year to the National wealth by the boldest and most persevering +industry." --REPORT OF DANIEL WEBSTER'S SPEECH IN THE U. S. SENATE, +ON THE APPLICATION FOR THE ERECTION OF A BREAKWATER AT NANTUCKET. +1828. + +"The whale fell directly over him, and probably killed him in a +moment." --"THE WHALE AND HIS CAPTORS, OR THE WHALEMAN'S ADVENTURES +AND THE WHALE'S BIOGRAPHY, GATHERED ON THE HOMEWARD CRUISE OF THE +COMMODORE PREBLE." BY REV. HENRY T. CHEEVER. + +"If you make the least damn bit of noise," replied Samuel, "I will +send you to hell." --LIFE OF SAMUEL COMSTOCK (THE MUTINEER), BY HIS +BROTHER, WILLIAM COMSTOCK. ANOTHER VERSION OF THE WHALE-SHIP GLOBE +NARRATIVE. + +"The voyages of the Dutch and English to the Northern Ocean, in +order, if possible, to discover a passage through it to India, though +they failed of their main object, laid-open the haunts of the whale." +--MCCULLOCH'S COMMERCIAL DICTIONARY. + +"These things are reciprocal; the ball rebounds, only to bound +forward again; for now in laying open the haunts of the whale, the +whalemen seem to have indirectly hit upon new clews to that same +mystic North-West Passage." --FROM "SOMETHING" UNPUBLISHED. + +"It is impossible to meet a whale-ship on the ocean without being +struck by her near appearance. The vessel under short sail, with +look-outs at the mast-heads, eagerly scanning the wide expanse around +them, has a totally different air from those engaged in regular +voyage." --CURRENTS AND WHALING. U.S. EX. EX. + +"Pedestrians in the vicinity of London and elsewhere may recollect +having seen large curved bones set upright in the earth, either to +form arches over gateways, or entrances to alcoves, and they may +perhaps have been told that these were the ribs of whales." --TALES +OF A WHALE VOYAGER TO THE ARCTIC OCEAN. + +"It was not till the boats returned from the pursuit of these whales, +that the whites saw their ship in bloody possession of the savages +enrolled among the crew." --NEWSPAPER ACCOUNT OF THE TAKING AND +RETAKING OF THE WHALE-SHIP HOBOMACK. + +"It is generally well known that out of the crews of Whaling vessels +(American) few ever return in the ships on board of which they +departed." --CRUISE IN A WHALE BOAT. + +"Suddenly a mighty mass emerged from the water, and shot up +perpendicularly into the air. It was the while." --MIRIAM COFFIN OR +THE WHALE FISHERMAN. + +"The Whale is harpooned to be sure; but bethink you, how you would +manage a powerful unbroken colt, with the mere appliance of a rope +tied to the root of his tail." --A CHAPTER ON WHALING IN RIBS AND +TRUCKS. + +"On one occasion I saw two of these monsters (whales) probably male +and female, slowly swimming, one after the other, within less than a +stone's throw of the shore" (Terra Del Fuego), "over which the beech +tree extended its branches." --DARWIN'S VOYAGE OF A NATURALIST. + +"'Stern all!' exclaimed the mate, as upon turning his head, he saw +the distended jaws of a large Sperm Whale close to the head of the +boat, threatening it with instant destruction;--'Stern all, for your +lives!'" --WHARTON THE WHALE KILLER. + +"So be cheery, my lads, let your hearts never fail, +While the bold harpooneer is striking the whale!" --NANTUCKET SONG. + +"Oh, the rare old Whale, mid storm and gale +In his ocean home will be +A giant in might, where might is right, +And King of the boundless sea." --WHALE SONG. + + + +CHAPTER 1 + +Loomings. + + +Call me Ishmael. Some years ago--never mind how long +precisely--having little or no money in my purse, and nothing +particular to interest me on shore, I thought I would sail about a +little and see the watery part of the world. It is a way I have of +driving off the spleen and regulating the circulation. Whenever I +find myself growing grim about the mouth; whenever it is a damp, +drizzly November in my soul; whenever I find myself involuntarily +pausing before coffin warehouses, and bringing up the rear of every +funeral I meet; and especially whenever my hypos get such an upper +hand of me, that it requires a strong moral principle to prevent me +from deliberately stepping into the street, and methodically knocking +people's hats off--then, I account it high time to get to sea as soon +as I can. This is my substitute for pistol and ball. With a +philosophical flourish Cato throws himself upon his sword; I quietly +take to the ship. There is nothing surprising in this. If they but +knew it, almost all men in their degree, some time or other, cherish +very nearly the same feelings towards the ocean with me. + +There now is your insular city of the Manhattoes, belted round by +wharves as Indian isles by coral reefs--commerce surrounds it with +her surf. Right and left, the streets take you waterward. Its +extreme downtown is the battery, where that noble mole is washed by +waves, and cooled by breezes, which a few hours previous were out of +sight of land. Look at the crowds of water-gazers there. + +Circumambulate the city of a dreamy Sabbath afternoon. Go from +Corlears Hook to Coenties Slip, and from thence, by Whitehall, +northward. What do you see?--Posted like silent sentinels all around +the town, stand thousands upon thousands of mortal men fixed in ocean +reveries. Some leaning against the spiles; some seated upon the +pier-heads; some looking over the bulwarks of ships from China; some +high aloft in the rigging, as if striving to get a still better +seaward peep. But these are all landsmen; of week days pent up in +lath and plaster--tied to counters, nailed to benches, clinched to +desks. How then is this? Are the green fields gone? What do they +here? + +But look! here come more crowds, pacing straight for the water, and +seemingly bound for a dive. Strange! Nothing will content them but +the extremest limit of the land; loitering under the shady lee of +yonder warehouses will not suffice. No. They must get just as nigh +the water as they possibly can without falling in. And there they +stand--miles of them--leagues. Inlanders all, they come from lanes +and alleys, streets and avenues--north, east, south, and west. Yet +here they all unite. Tell me, does the magnetic virtue of the +needles of the compasses of all those ships attract them thither? + +Once more. Say you are in the country; in some high land of lakes. +Take almost any path you please, and ten to one it carries you down +in a dale, and leaves you there by a pool in the stream. There is +magic in it. Let the most absent-minded of men be plunged in his +deepest reveries--stand that man on his legs, set his feet a-going, +and he will infallibly lead you to water, if water there be in all +that region. Should you ever be athirst in the great American +desert, try this experiment, if your caravan happen to be supplied +with a metaphysical professor. Yes, as every one knows, meditation +and water are wedded for ever. + +But here is an artist. He desires to paint you the dreamiest, +shadiest, quietest, most enchanting bit of romantic landscape in all +the valley of the Saco. What is the chief element he employs? There +stand his trees, each with a hollow trunk, as if a hermit and a +crucifix were within; and here sleeps his meadow, and there sleep his +cattle; and up from yonder cottage goes a sleepy smoke. Deep into +distant woodlands winds a mazy way, reaching to overlapping spurs of +mountains bathed in their hill-side blue. But though the picture +lies thus tranced, and though this pine-tree shakes down its sighs +like leaves upon this shepherd's head, yet all were vain, unless the +shepherd's eye were fixed upon the magic stream before him. Go visit +the Prairies in June, when for scores on scores of miles you wade +knee-deep among Tiger-lilies--what is the one charm +wanting?--Water--there is not a drop of water there! Were Niagara +but a cataract of sand, would you travel your thousand miles to see +it? Why did the poor poet of Tennessee, upon suddenly receiving two +handfuls of silver, deliberate whether to buy him a coat, which he +sadly needed, or invest his money in a pedestrian trip to Rockaway +Beach? Why is almost every robust healthy boy with a robust healthy +soul in him, at some time or other crazy to go to sea? Why upon your +first voyage as a passenger, did you yourself feel such a mystical +vibration, when first told that you and your ship were now out of +sight of land? Why did the old Persians hold the sea holy? Why did +the Greeks give it a separate deity, and own brother of Jove? Surely +all this is not without meaning. And still deeper the meaning of +that story of Narcissus, who because he could not grasp the +tormenting, mild image he saw in the fountain, plunged into it and +was drowned. But that same image, we ourselves see in all rivers and +oceans. It is the image of the ungraspable phantom of life; and this +is the key to it all. + +Now, when I say that I am in the habit of going to sea whenever I +begin to grow hazy about the eyes, and begin to be over conscious of +my lungs, I do not mean to have it inferred that I ever go to sea as +a passenger. For to go as a passenger you must needs have a purse, +and a purse is but a rag unless you have something in it. Besides, +passengers get sea-sick--grow quarrelsome--don't sleep of nights--do +not enjoy themselves much, as a general thing;--no, I never go as a +passenger; nor, though I am something of a salt, do I ever go to sea +as a Commodore, or a Captain, or a Cook. I abandon the glory and +distinction of such offices to those who like them. For my part, I +abominate all honourable respectable toils, trials, and tribulations +of every kind whatsoever. It is quite as much as I can do to take +care of myself, without taking care of ships, barques, brigs, +schooners, and what not. And as for going as cook,--though I confess +there is considerable glory in that, a cook being a sort of officer +on ship-board--yet, somehow, I never fancied broiling fowls;--though +once broiled, judiciously buttered, and judgmatically salted and +peppered, there is no one who will speak more respectfully, not to +say reverentially, of a broiled fowl than I will. It is out of the +idolatrous dotings of the old Egyptians upon broiled ibis and roasted +river horse, that you see the mummies of those creatures in their +huge bake-houses the pyramids. + +No, when I go to sea, I go as a simple sailor, right before the mast, +plumb down into the forecastle, aloft there to the royal mast-head. +True, they rather order me about some, and make me jump from spar to +spar, like a grasshopper in a May meadow. And at first, this sort of +thing is unpleasant enough. It touches one's sense of honour, +particularly if you come of an old established family in the land, +the Van Rensselaers, or Randolphs, or Hardicanutes. And more than +all, if just previous to putting your hand into the tar-pot, you have +been lording it as a country schoolmaster, making the tallest boys +stand in awe of you. The transition is a keen one, I assure you, +from a schoolmaster to a sailor, and requires a strong decoction of +Seneca and the Stoics to enable you to grin and bear it. But even +this wears off in time. + +What of it, if some old hunks of a sea-captain orders me to get a +broom and sweep down the decks? What does that indignity amount to, +weighed, I mean, in the scales of the New Testament? Do you think +the archangel Gabriel thinks anything the less of me, because I +promptly and respectfully obey that old hunks in that particular +instance? Who ain't a slave? Tell me that. Well, then, however the +old sea-captains may order me about--however they may thump and punch +me about, I have the satisfaction of knowing that it is all right; +that everybody else is one way or other served in much the same +way--either in a physical or metaphysical point of view, that is; and +so the universal thump is passed round, and all hands should rub each +other's shoulder-blades, and be content. + +Again, I always go to sea as a sailor, because they make a point of +paying me for my trouble, whereas they never pay passengers a single +penny that I ever heard of. On the contrary, passengers themselves +must pay. And there is all the difference in the world between +paying and being paid. The act of paying is perhaps the most +uncomfortable infliction that the two orchard thieves entailed upon +us. But BEING PAID,--what will compare with it? The urbane activity +with which a man receives money is really marvellous, considering +that we so earnestly believe money to be the root of all earthly +ills, and that on no account can a monied man enter heaven. Ah! how +cheerfully we consign ourselves to perdition! + +Finally, I always go to sea as a sailor, because of the wholesome +exercise and pure air of the fore-castle deck. For as in this world, +head winds are far more prevalent than winds from astern (that is, if +you never violate the Pythagorean maxim), so for the most part the +Commodore on the quarter-deck gets his atmosphere at second hand from +the sailors on the forecastle. He thinks he breathes it first; but +not so. In much the same way do the commonalty lead their leaders in +many other things, at the same time that the leaders little suspect +it. But wherefore it was that after having repeatedly smelt the sea +as a merchant sailor, I should now take it into my head to go on a +whaling voyage; this the invisible police officer of the Fates, who +has the constant surveillance of me, and secretly dogs me, and +influences me in some unaccountable way--he can better answer than +any one else. And, doubtless, my going on this whaling voyage, +formed part of the grand programme of Providence that was drawn up a +long time ago. It came in as a sort of brief interlude and solo +between more extensive performances. I take it that this part of the +bill must have run something like this: + + +"GRAND CONTESTED ELECTION FOR THE PRESIDENCY OF THE UNITED STATES. +"WHALING VOYAGE BY ONE ISHMAEL. +"BLOODY BATTLE IN AFFGHANISTAN." + + +Though I cannot tell why it was exactly that those stage managers, +the Fates, put me down for this shabby part of a whaling voyage, when +others were set down for magnificent parts in high tragedies, and +short and easy parts in genteel comedies, and jolly parts in +farces--though I cannot tell why this was exactly; yet, now that I +recall all the circumstances, I think I can see a little into the +springs and motives which being cunningly presented to me under +various disguises, induced me to set about performing the part I did, +besides cajoling me into the delusion that it was a choice resulting +from my own unbiased freewill and discriminating judgment. + +Chief among these motives was the overwhelming idea of the great +whale himself. Such a portentous and mysterious monster roused all +my curiosity. Then the wild and distant seas where he rolled his +island bulk; the undeliverable, nameless perils of the whale; these, +with all the attending marvels of a thousand Patagonian sights and +sounds, helped to sway me to my wish. With other men, perhaps, such +things would not have been inducements; but as for me, I am tormented +with an everlasting itch for things remote. I love to sail forbidden +seas, and land on barbarous coasts. Not ignoring what is good, I am +quick to perceive a horror, and could still be social with it--would +they let me--since it is but well to be on friendly terms with all +the inmates of the place one lodges in. + +By reason of these things, then, the whaling voyage was welcome; the +great flood-gates of the wonder-world swung open, and in the wild +conceits that swayed me to my purpose, two and two there floated into +my inmost soul, endless processions of the whale, and, mid most of +them all, one grand hooded phantom, like a snow hill in the air. + + + +CHAPTER 2 + +The Carpet-Bag. + + +I stuffed a shirt or two into my old carpet-bag, tucked it under my +arm, and started for Cape Horn and the Pacific. Quitting the good +city of old Manhatto, I duly arrived in New Bedford. It was a +Saturday night in December. Much was I disappointed upon learning +that the little packet for Nantucket had already sailed, and that no +way of reaching that place would offer, till the following Monday. + +As most young candidates for the pains and penalties of whaling stop +at this same New Bedford, thence to embark on their voyage, it may as +well be related that I, for one, had no idea of so doing. For my +mind was made up to sail in no other than a Nantucket craft, because +there was a fine, boisterous something about everything connected +with that famous old island, which amazingly pleased me. Besides +though New Bedford has of late been gradually monopolising the +business of whaling, and though in this matter poor old Nantucket is +now much behind her, yet Nantucket was her great original--the Tyre +of this Carthage;--the place where the first dead American whale was +stranded. Where else but from Nantucket did those aboriginal +whalemen, the Red-Men, first sally out in canoes to give chase to the +Leviathan? And where but from Nantucket, too, did that first +adventurous little sloop put forth, partly laden with imported +cobblestones--so goes the story--to throw at the whales, in order to +discover when they were nigh enough to risk a harpoon from the +bowsprit? + +Now having a night, a day, and still another night following before +me in New Bedford, ere I could embark for my destined port, it +became a matter of concernment where I was to eat and sleep +meanwhile. It was a very dubious-looking, nay, a very dark and +dismal night, bitingly cold and cheerless. I knew no one in the +place. With anxious grapnels I had sounded my pocket, and only +brought up a few pieces of silver,--So, wherever you go, Ishmael, +said I to myself, as I stood in the middle of a dreary street +shouldering my bag, and comparing the gloom towards the north with +the darkness towards the south--wherever in your wisdom you may +conclude to lodge for the night, my dear Ishmael, be sure to inquire +the price, and don't be too particular. + +With halting steps I paced the streets, and passed the sign of "The +Crossed Harpoons"--but it looked too expensive and jolly there. +Further on, from the bright red windows of the "Sword-Fish Inn," +there came such fervent rays, that it seemed to have melted the +packed snow and ice from before the house, for everywhere else the +congealed frost lay ten inches thick in a hard, asphaltic +pavement,--rather weary for me, when I struck my foot against the +flinty projections, because from hard, remorseless service the soles +of my boots were in a most miserable plight. Too expensive and +jolly, again thought I, pausing one moment to watch the broad glare +in the street, and hear the sounds of the tinkling glasses within. +But go on, Ishmael, said I at last; don't you hear? get away from +before the door; your patched boots are stopping the way. So on I +went. I now by instinct followed the streets that took me waterward, +for there, doubtless, were the cheapest, if not the cheeriest inns. + +Such dreary streets! blocks of blackness, not houses, on either +hand, and here and there a candle, like a candle moving about in a +tomb. At this hour of the night, of the last day of the week, that +quarter of the town proved all but deserted. But presently I came to +a smoky light proceeding from a low, wide building, the door of which +stood invitingly open. It had a careless look, as if it were meant +for the uses of the public; so, entering, the first thing I did was +to stumble over an ash-box in the porch. Ha! thought I, ha, as the +flying particles almost choked me, are these ashes from that +destroyed city, Gomorrah? But "The Crossed Harpoons," and "The +Sword-Fish?"--this, then must needs be the sign of "The Trap." +However, I picked myself up and hearing a loud voice within, pushed +on and opened a second, interior door. + +It seemed the great Black Parliament sitting in Tophet. A hundred +black faces turned round in their rows to peer; and beyond, a black +Angel of Doom was beating a book in a pulpit. It was a negro church; +and the preacher's text was about the blackness of darkness, and the +weeping and wailing and teeth-gnashing there. Ha, Ishmael, muttered +I, backing out, Wretched entertainment at the sign of 'The Trap!' + +Moving on, I at last came to a dim sort of light not far from the +docks, and heard a forlorn creaking in the air; and looking up, saw a +swinging sign over the door with a white painting upon it, faintly +representing a tall straight jet of misty spray, and these words +underneath--"The Spouter Inn:--Peter Coffin." + +Coffin?--Spouter?--Rather ominous in that particular connexion, +thought I. But it is a common name in Nantucket, they say, and I +suppose this Peter here is an emigrant from there. As the light +looked so dim, and the place, for the time, looked quiet enough, and +the dilapidated little wooden house itself looked as if it might have +been carted here from the ruins of some burnt district, and as the +swinging sign had a poverty-stricken sort of creak to it, I thought +that here was the very spot for cheap lodgings, and the best of pea +coffee. + +It was a queer sort of place--a gable-ended old house, one side +palsied as it were, and leaning over sadly. It stood on a sharp +bleak corner, where that tempestuous wind Euroclydon kept up a worse +howling than ever it did about poor Paul's tossed craft. Euroclydon, +nevertheless, is a mighty pleasant zephyr to any one in-doors, with +his feet on the hob quietly toasting for bed. "In judging of that +tempestuous wind called Euroclydon," says an old writer--of whose +works I possess the only copy extant--"it maketh a marvellous +difference, whether thou lookest out at it from a glass window where +the frost is all on the outside, or whether thou observest it from +that sashless window, where the frost is on both sides, and of which +the wight Death is the only glazier." True enough, thought I, as +this passage occurred to my mind--old black-letter, thou reasonest +well. Yes, these eyes are windows, and this body of mine is the +house. What a pity they didn't stop up the chinks and the crannies +though, and thrust in a little lint here and there. But it's too +late to make any improvements now. The universe is finished; the +copestone is on, and the chips were carted off a million years ago. +Poor Lazarus there, chattering his teeth against the curbstone for +his pillow, and shaking off his tatters with his shiverings, he might +plug up both ears with rags, and put a corn-cob into his mouth, and +yet that would not keep out the tempestuous Euroclydon. Euroclydon! +says old Dives, in his red silken wrapper--(he had a redder one +afterwards) pooh, pooh! What a fine frosty night; how Orion +glitters; what northern lights! Let them talk of their oriental +summer climes of everlasting conservatories; give me the privilege of +making my own summer with my own coals. + +But what thinks Lazarus? Can he warm his blue hands by holding them +up to the grand northern lights? Would not Lazarus rather be in +Sumatra than here? Would he not far rather lay him down lengthwise +along the line of the equator; yea, ye gods! go down to the fiery pit +itself, in order to keep out this frost? + +Now, that Lazarus should lie stranded there on the curbstone before +the door of Dives, this is more wonderful than that an iceberg should +be moored to one of the Moluccas. Yet Dives himself, he too lives +like a Czar in an ice palace made of frozen sighs, and being a +president of a temperance society, he only drinks the tepid tears of +orphans. + +But no more of this blubbering now, we are going a-whaling, and there +is plenty of that yet to come. Let us scrape the ice from our +frosted feet, and see what sort of a place this "Spouter" may be. + + + +CHAPTER 3 + +The Spouter-Inn. + + +Entering that gable-ended Spouter-Inn, you found yourself in a wide, +low, straggling entry with old-fashioned wainscots, reminding one of +the bulwarks of some condemned old craft. On one side hung a very +large oilpainting so thoroughly besmoked, and every way defaced, +that in the unequal crosslights by which you viewed it, it was only +by diligent study and a series of systematic visits to it, and +careful inquiry of the neighbors, that you could any way arrive at an +understanding of its purpose. Such unaccountable masses of shades +and shadows, that at first you almost thought some ambitious young +artist, in the time of the New England hags, had endeavored to +delineate chaos bewitched. But by dint of much and earnest +contemplation, and oft repeated ponderings, and especially by +throwing open the little window towards the back of the entry, you at +last come to the conclusion that such an idea, however wild, might +not be altogether unwarranted. + +But what most puzzled and confounded you was a long, limber, +portentous, black mass of something hovering in the centre of the +picture over three blue, dim, perpendicular lines floating in a +nameless yeast. A boggy, soggy, squitchy picture truly, enough to +drive a nervous man distracted. Yet was there a sort of indefinite, +half-attained, unimaginable sublimity about it that fairly froze you +to it, till you involuntarily took an oath with yourself to find out +what that marvellous painting meant. Ever and anon a bright, but, +alas, deceptive idea would dart you through.--It's the Black Sea in a +midnight gale.--It's the unnatural combat of the four primal +elements.--It's a blasted heath.--It's a Hyperborean winter +scene.--It's the breaking-up of the icebound stream of Time. But at +last all these fancies yielded to that one portentous something in +the picture's midst. THAT once found out, and all the rest were +plain. But stop; does it not bear a faint resemblance to a gigantic +fish? even the great leviathan himself? + +In fact, the artist's design seemed this: a final theory of my own, +partly based upon the aggregated opinions of many aged persons with +whom I conversed upon the subject. The picture represents a +Cape-Horner in a great hurricane; the half-foundered ship weltering +there with its three dismantled masts alone visible; and an +exasperated whale, purposing to spring clean over the craft, is in +the enormous act of impaling himself upon the three mast-heads. + +The opposite wall of this entry was hung all over with a heathenish +array of monstrous clubs and spears. Some were thickly set with +glittering teeth resembling ivory saws; others were tufted with knots +of human hair; and one was sickle-shaped, with a vast handle sweeping +round like the segment made in the new-mown grass by a long-armed +mower. You shuddered as you gazed, and wondered what monstrous +cannibal and savage could ever have gone a death-harvesting with such +a hacking, horrifying implement. Mixed with these were rusty old +whaling lances and harpoons all broken and deformed. Some were +storied weapons. With this once long lance, now wildly elbowed, +fifty years ago did Nathan Swain kill fifteen whales between a +sunrise and a sunset. And that harpoon--so like a corkscrew now--was +flung in Javan seas, and run away with by a whale, years afterwards +slain off the Cape of Blanco. The original iron entered nigh the +tail, and, like a restless needle sojourning in the body of a man, +travelled full forty feet, and at last was found imbedded in the +hump. + +Crossing this dusky entry, and on through yon low-arched way--cut +through what in old times must have been a great central chimney with +fireplaces all round--you enter the public room. A still duskier +place is this, with such low ponderous beams above, and such old +wrinkled planks beneath, that you would almost fancy you trod some +old craft's cockpits, especially of such a howling night, when this +corner-anchored old ark rocked so furiously. On one side stood a +long, low, shelf-like table covered with cracked glass cases, filled +with dusty rarities gathered from this wide world's remotest nooks. +Projecting from the further angle of the room stands a dark-looking +den--the bar--a rude attempt at a right whale's head. Be that how it +may, there stands the vast arched bone of the whale's jaw, so wide, a +coach might almost drive beneath it. Within are shabby shelves, +ranged round with old decanters, bottles, flasks; and in those jaws +of swift destruction, like another cursed Jonah (by which name indeed +they called him), bustles a little withered old man, who, for their +money, dearly sells the sailors deliriums and death. + +Abominable are the tumblers into which he pours his poison. Though +true cylinders without--within, the villanous green goggling glasses +deceitfully tapered downwards to a cheating bottom. Parallel +meridians rudely pecked into the glass, surround these footpads' +goblets. Fill to THIS mark, and your charge is but a penny; to THIS +a penny more; and so on to the full glass--the Cape Horn measure, +which you may gulp down for a shilling. + +Upon entering the place I found a number of young seamen gathered +about a table, examining by a dim light divers specimens of +SKRIMSHANDER. I sought the landlord, and telling him I desired to be +accommodated with a room, received for answer that his house was +full--not a bed unoccupied. "But avast," he added, tapping his +forehead, "you haint no objections to sharing a harpooneer's blanket, +have ye? I s'pose you are goin' a-whalin', so you'd better get used +to that sort of thing." + +I told him that I never liked to sleep two in a bed; that if I should +ever do so, it would depend upon who the harpooneer might be, and +that if he (the landlord) really had no other place for me, and the +harpooneer was not decidedly objectionable, why rather than wander +further about a strange town on so bitter a night, I would put up +with the half of any decent man's blanket. + +"I thought so. All right; take a seat. Supper?--you want supper? +Supper'll be ready directly." + +I sat down on an old wooden settle, carved all over like a bench on +the Battery. At one end a ruminating tar was still further adorning +it with his jack-knife, stooping over and diligently working away at +the space between his legs. He was trying his hand at a ship under +full sail, but he didn't make much headway, I thought. + +At last some four or five of us were summoned to our meal in an +adjoining room. It was cold as Iceland--no fire at all--the landlord +said he couldn't afford it. Nothing but two dismal tallow candles, +each in a winding sheet. We were fain to button up our monkey +jackets, and hold to our lips cups of scalding tea with our half +frozen fingers. But the fare was of the most substantial kind--not +only meat and potatoes, but dumplings; good heavens! dumplings for +supper! One young fellow in a green box coat, addressed himself to +these dumplings in a most direful manner. + +"My boy," said the landlord, "you'll have the nightmare to a dead +sartainty." + +"Landlord," I whispered, "that aint the harpooneer is it?" + +"Oh, no," said he, looking a sort of diabolically funny, "the +harpooneer is a dark complexioned chap. He never eats dumplings, he +don't--he eats nothing but steaks, and he likes 'em rare." + +"The devil he does," says I. "Where is that harpooneer? Is he +here?" + +"He'll be here afore long," was the answer. + +I could not help it, but I began to feel suspicious of this "dark +complexioned" harpooneer. At any rate, I made up my mind that if it +so turned out that we should sleep together, he must undress and get +into bed before I did. + +Supper over, the company went back to the bar-room, when, knowing not +what else to do with myself, I resolved to spend the rest of the +evening as a looker on. + +Presently a rioting noise was heard without. Starting up, the +landlord cried, "That's the Grampus's crew. I seed her reported in +the offing this morning; a three years' voyage, and a full ship. +Hurrah, boys; now we'll have the latest news from the Feegees." + +A tramping of sea boots was heard in the entry; the door was flung +open, and in rolled a wild set of mariners enough. Enveloped in +their shaggy watch coats, and with their heads muffled in woollen +comforters, all bedarned and ragged, and their beards stiff with +icicles, they seemed an eruption of bears from Labrador. They had +just landed from their boat, and this was the first house they +entered. No wonder, then, that they made a straight wake for the +whale's mouth--the bar--when the wrinkled little old Jonah, there +officiating, soon poured them out brimmers all round. One complained +of a bad cold in his head, upon which Jonah mixed him a pitch-like +potion of gin and molasses, which he swore was a sovereign cure for +all colds and catarrhs whatsoever, never mind of how long standing, +or whether caught off the coast of Labrador, or on the weather side +of an ice-island. + +The liquor soon mounted into their heads, as it generally does even +with the arrantest topers newly landed from sea, and they began +capering about most obstreperously. + +I observed, however, that one of them held somewhat aloof, and though +he seemed desirous not to spoil the hilarity of his shipmates by his +own sober face, yet upon the whole he refrained from making as much +noise as the rest. This man interested me at once; and since the +sea-gods had ordained that he should soon become my shipmate (though +but a sleeping-partner one, so far as this narrative is concerned), +I will here venture upon a little description of him. He stood full +six feet in height, with noble shoulders, and a chest like a +coffer-dam. I have seldom seen such brawn in a man. His face was +deeply brown and burnt, making his white teeth dazzling by the +contrast; while in the deep shadows of his eyes floated some +reminiscences that did not seem to give him much joy. His voice at +once announced that he was a Southerner, and from his fine stature, I +thought he must be one of those tall mountaineers from the +Alleghanian Ridge in Virginia. When the revelry of his companions +had mounted to its height, this man slipped away unobserved, and I +saw no more of him till he became my comrade on the sea. In a few +minutes, however, he was missed by his shipmates, and being, it +seems, for some reason a huge favourite with them, they raised a cry +of "Bulkington! Bulkington! where's Bulkington?" and darted out of +the house in pursuit of him. + +It was now about nine o'clock, and the room seeming almost +supernaturally quiet after these orgies, I began to congratulate +myself upon a little plan that had occurred to me just previous to +the entrance of the seamen. + +No man prefers to sleep two in a bed. In fact, you would a good deal +rather not sleep with your own brother. I don't know how it is, but +people like to be private when they are sleeping. And when it comes +to sleeping with an unknown stranger, in a strange inn, in a strange +town, and that stranger a harpooneer, then your objections +indefinitely multiply. Nor was there any earthly reason why I as a +sailor should sleep two in a bed, more than anybody else; for sailors +no more sleep two in a bed at sea, than bachelor Kings do ashore. To +be sure they all sleep together in one apartment, but you have your +own hammock, and cover yourself with your own blanket, and sleep in +your own skin. + +The more I pondered over this harpooneer, the more I abominated the +thought of sleeping with him. It was fair to presume that being a +harpooneer, his linen or woollen, as the case might be, would not be +of the tidiest, certainly none of the finest. I began to twitch all +over. Besides, it was getting late, and my decent harpooneer ought +to be home and going bedwards. Suppose now, he should tumble in upon +me at midnight--how could I tell from what vile hole he had been +coming? + +"Landlord! I've changed my mind about that harpooneer.--I shan't +sleep with him. I'll try the bench here." + +"Just as you please; I'm sorry I cant spare ye a tablecloth for a +mattress, and it's a plaguy rough board here"--feeling of the knots +and notches. "But wait a bit, Skrimshander; I've got a carpenter's +plane there in the bar--wait, I say, and I'll make ye snug enough." +So saying he procured the plane; and with his old silk handkerchief +first dusting the bench, vigorously set to planing away at my bed, +the while grinning like an ape. The shavings flew right and left; +till at last the plane-iron came bump against an indestructible knot. +The landlord was near spraining his wrist, and I told him for +heaven's sake to quit--the bed was soft enough to suit me, and I did +not know how all the planing in the world could make eider down of a +pine plank. So gathering up the shavings with another grin, and +throwing them into the great stove in the middle of the room, he went +about his business, and left me in a brown study. + +I now took the measure of the bench, and found that it was a foot too +short; but that could be mended with a chair. But it was a foot too +narrow, and the other bench in the room was about four inches higher +than the planed one--so there was no yoking them. I then placed the +first bench lengthwise along the only clear space against the wall, +leaving a little interval between, for my back to settle down in. +But I soon found that there came such a draught of cold air over me +from under the sill of the window, that this plan would never do at +all, especially as another current from the rickety door met the one +from the window, and both together formed a series of small +whirlwinds in the immediate vicinity of the spot where I had thought +to spend the night. + +The devil fetch that harpooneer, thought I, but stop, couldn't I +steal a march on him--bolt his door inside, and jump into his bed, +not to be wakened by the most violent knockings? It seemed no bad +idea; but upon second thoughts I dismissed it. For who could tell +but what the next morning, so soon as I popped out of the room, the +harpooneer might be standing in the entry, all ready to knock me +down! + +Still, looking round me again, and seeing no possible chance of +spending a sufferable night unless in some other person's bed, I +began to think that after all I might be cherishing unwarrantable +prejudices against this unknown harpooneer. Thinks I, I'll wait +awhile; he must be dropping in before long. I'll have a good look at +him then, and perhaps we may become jolly good bedfellows after +all--there's no telling. + +But though the other boarders kept coming in by ones, twos, and +threes, and going to bed, yet no sign of my harpooneer. + +"Landlord! said I, "what sort of a chap is he--does he always keep +such late hours?" It was now hard upon twelve o'clock. + +The landlord chuckled again with his lean chuckle, and seemed to be +mightily tickled at something beyond my comprehension. "No," he +answered, "generally he's an early bird--airley to bed and airley to +rise--yes, he's the bird what catches the worm. But to-night he +went out a peddling, you see, and I don't see what on airth keeps him +so late, unless, may be, he can't sell his head." + +"Can't sell his head?--What sort of a bamboozingly story is this you +are telling me?" getting into a towering rage. "Do you pretend to +say, landlord, that this harpooneer is actually engaged this blessed +Saturday night, or rather Sunday morning, in peddling his head around +this town?" + +"That's precisely it," said the landlord, "and I told him he couldn't +sell it here, the market's overstocked." + +"With what?" shouted I. + +"With heads to be sure; ain't there too many heads in the world?" + +"I tell you what it is, landlord," said I quite calmly, "you'd better +stop spinning that yarn to me--I'm not green." + +"May be not," taking out a stick and whittling a toothpick, "but I +rayther guess you'll be done BROWN if that ere harpooneer hears you a +slanderin' his head." + +"I'll break it for him," said I, now flying into a passion again at +this unaccountable farrago of the landlord's. + +"It's broke a'ready," said he. + +"Broke," said I--"BROKE, do you mean?" + +"Sartain, and that's the very reason he can't sell it, I guess." + +"Landlord," said I, going up to him as cool as Mt. Hecla in a +snow-storm--"landlord, stop whittling. You and I must understand one +another, and that too without delay. I come to your house and want a +bed; you tell me you can only give me half a one; that the other half +belongs to a certain harpooneer. And about this harpooneer, whom I +have not yet seen, you persist in telling me the most mystifying and +exasperating stories tending to beget in me an uncomfortable feeling +towards the man whom you design for my bedfellow--a sort of +connexion, landlord, which is an intimate and confidential one in the +highest degree. I now demand of you to speak out and tell me who and +what this harpooneer is, and whether I shall be in all respects safe +to spend the night with him. And in the first place, you will be so +good as to unsay that story about selling his head, which if true I +take to be good evidence that this harpooneer is stark mad, and I've +no idea of sleeping with a madman; and you, sir, YOU I mean, +landlord, YOU, sir, by trying to induce me to do so knowingly, would +thereby render yourself liable to a criminal prosecution." + +"Wall," said the landlord, fetching a long breath, "that's a purty +long sarmon for a chap that rips a little now and then. But be easy, +be easy, this here harpooneer I have been tellin' you of has just +arrived from the south seas, where he bought up a lot of 'balmed New +Zealand heads (great curios, you know), and he's sold all on 'em but +one, and that one he's trying to sell to-night, cause to-morrow's +Sunday, and it would not do to be sellin' human heads about the +streets when folks is goin' to churches. He wanted to, last Sunday, +but I stopped him just as he was goin' out of the door with four +heads strung on a string, for all the airth like a string of inions." + +This account cleared up the otherwise unaccountable mystery, and +showed that the landlord, after all, had had no idea of fooling +me--but at the same time what could I think of a harpooneer who +stayed out of a Saturday night clean into the holy Sabbath, engaged +in such a cannibal business as selling the heads of dead idolators? + +"Depend upon it, landlord, that harpooneer is a dangerous man." + +"He pays reg'lar," was the rejoinder. "But come, it's getting +dreadful late, you had better be turning flukes--it's a nice bed; +Sal and me slept in that ere bed the night we were spliced. There's +plenty of room for two to kick about in that bed; it's an almighty +big bed that. Why, afore we give it up, Sal used to put our Sam and +little Johnny in the foot of it. But I got a dreaming and sprawling +about one night, and somehow, Sam got pitched on the floor, and came +near breaking his arm. Arter that, Sal said it wouldn't do. Come +along here, I'll give ye a glim in a jiffy;" and so saying he lighted +a candle and held it towards me, offering to lead the way. But I +stood irresolute; when looking at a clock in the corner, he exclaimed +"I vum it's Sunday--you won't see that harpooneer to-night; he's come +to anchor somewhere--come along then; DO come; WON'T ye come?" + +I considered the matter a moment, and then up stairs we went, and I +was ushered into a small room, cold as a clam, and furnished, sure +enough, with a prodigious bed, almost big enough indeed for any four +harpooneers to sleep abreast. + +"There," said the landlord, placing the candle on a crazy old sea +chest that did double duty as a wash-stand and centre table; "there, +make yourself comfortable now, and good night to ye." I turned +round from eyeing the bed, but he had disappeared. + +Folding back the counterpane, I stooped over the bed. Though none of +the most elegant, it yet stood the scrutiny tolerably well. I then +glanced round the room; and besides the bedstead and centre table, +could see no other furniture belonging to the place, but a rude +shelf, the four walls, and a papered fireboard representing a man +striking a whale. Of things not properly belonging to the room, +there was a hammock lashed up, and thrown upon the floor in one +corner; also a large seaman's bag, containing the harpooneer's +wardrobe, no doubt in lieu of a land trunk. Likewise, there was a +parcel of outlandish bone fish hooks on the shelf over the +fire-place, and a tall harpoon standing at the head of the bed. + +But what is this on the chest? I took it up, and held it close to +the light, and felt it, and smelt it, and tried every way possible to +arrive at some satisfactory conclusion concerning it. I can compare +it to nothing but a large door mat, ornamented at the edges with +little tinkling tags something like the stained porcupine quills +round an Indian moccasin. There was a hole or slit in the middle of +this mat, as you see the same in South American ponchos. But could +it be possible that any sober harpooneer would get into a door mat, +and parade the streets of any Christian town in that sort of guise? +I put it on, to try it, and it weighed me down like a hamper, being +uncommonly shaggy and thick, and I thought a little damp, as though +this mysterious harpooneer had been wearing it of a rainy day. I +went up in it to a bit of glass stuck against the wall, and I never +saw such a sight in my life. I tore myself out of it in such a hurry +that I gave myself a kink in the neck. + +I sat down on the side of the bed, and commenced thinking about this +head-peddling harpooneer, and his door mat. After thinking some time +on the bed-side, I got up and took off my monkey jacket, and then +stood in the middle of the room thinking. I then took off my coat, +and thought a little more in my shirt sleeves. But beginning to feel +very cold now, half undressed as I was, and remembering what the +landlord said about the harpooneer's not coming home at all that +night, it being so very late, I made no more ado, but jumped out of +my pantaloons and boots, and then blowing out the light tumbled into +bed, and commended myself to the care of heaven. + +Whether that mattress was stuffed with corn-cobs or broken crockery, +there is no telling, but I rolled about a good deal, and could not +sleep for a long time. At last I slid off into a light doze, and had +pretty nearly made a good offing towards the land of Nod, when I +heard a heavy footfall in the passage, and saw a glimmer of light +come into the room from under the door. + +Lord save me, thinks I, that must be the harpooneer, the infernal +head-peddler. But I lay perfectly still, and resolved not to say a +word till spoken to. Holding a light in one hand, and that identical +New Zealand head in the other, the stranger entered the room, and +without looking towards the bed, placed his candle a good way off +from me on the floor in one corner, and then began working away at +the knotted cords of the large bag I before spoke of as being in the +room. I was all eagerness to see his face, but he kept it averted +for some time while employed in unlacing the bag's mouth. This +accomplished, however, he turned round--when, good heavens! what a +sight! Such a face! It was of a dark, purplish, yellow colour, here +and there stuck over with large blackish looking squares. Yes, it's +just as I thought, he's a terrible bedfellow; he's been in a fight, +got dreadfully cut, and here he is, just from the surgeon. But at +that moment he chanced to turn his face so towards the light, that I +plainly saw they could not be sticking-plasters at all, those black +squares on his cheeks. They were stains of some sort or other. At +first I knew not what to make of this; but soon an inkling of the +truth occurred to me. I remembered a story of a white man--a +whaleman too--who, falling among the cannibals, had been tattooed by +them. I concluded that this harpooneer, in the course of his distant +voyages, must have met with a similar adventure. And what is it, +thought I, after all! It's only his outside; a man can be honest in +any sort of skin. But then, what to make of his unearthly +complexion, that part of it, I mean, lying round about, and +completely independent of the squares of tattooing. To be sure, it +might be nothing but a good coat of tropical tanning; but I never +heard of a hot sun's tanning a white man into a purplish yellow one. +However, I had never been in the South Seas; and perhaps the sun +there produced these extraordinary effects upon the skin. Now, while +all these ideas were passing through me like lightning, this +harpooneer never noticed me at all. But, after some difficulty +having opened his bag, he commenced fumbling in it, and presently +pulled out a sort of tomahawk, and a seal-skin wallet with the hair +on. Placing these on the old chest in the middle of the room, he +then took the New Zealand head--a ghastly thing enough--and crammed +it down into the bag. He now took off his hat--a new beaver +hat--when I came nigh singing out with fresh surprise. There was no +hair on his head--none to speak of at least--nothing but a small +scalp-knot twisted up on his forehead. His bald purplish head now +looked for all the world like a mildewed skull. Had not the stranger +stood between me and the door, I would have bolted out of it quicker +than ever I bolted a dinner. + +Even as it was, I thought something of slipping out of the window, +but it was the second floor back. I am no coward, but what to make +of this head-peddling purple rascal altogether passed my +comprehension. Ignorance is the parent of fear, and being completely +nonplussed and confounded about the stranger, I confess I was now as +much afraid of him as if it was the devil himself who had thus broken +into my room at the dead of night. In fact, I was so afraid of him +that I was not game enough just then to address him, and demand a +satisfactory answer concerning what seemed inexplicable in him. + +Meanwhile, he continued the business of undressing, and at last +showed his chest and arms. As I live, these covered parts of him +were checkered with the same squares as his face; his back, too, was +all over the same dark squares; he seemed to have been in a Thirty +Years' War, and just escaped from it with a sticking-plaster shirt. +Still more, his very legs were marked, as if a parcel of dark green +frogs were running up the trunks of young palms. It was now quite +plain that he must be some abominable savage or other shipped aboard +of a whaleman in the South Seas, and so landed in this Christian +country. I quaked to think of it. A peddler of heads too--perhaps +the heads of his own brothers. He might take a fancy to +mine--heavens! look at that tomahawk! + +But there was no time for shuddering, for now the savage went about +something that completely fascinated my attention, and convinced me +that he must indeed be a heathen. Going to his heavy grego, or +wrapall, or dreadnaught, which he had previously hung on a chair, he +fumbled in the pockets, and produced at length a curious little +deformed image with a hunch on its back, and exactly the colour of a +three days' old Congo baby. Remembering the embalmed head, at first +I almost thought that this black manikin was a real baby preserved +in some similar manner. But seeing that it was not at all limber, +and that it glistened a good deal like polished ebony, I concluded +that it must be nothing but a wooden idol, which indeed it proved to +be. For now the savage goes up to the empty fire-place, and removing +the papered fire-board, sets up this little hunch-backed image, like +a tenpin, between the andirons. The chimney jambs and all the bricks +inside were very sooty, so that I thought this fire-place made a very +appropriate little shrine or chapel for his Congo idol. + +I now screwed my eyes hard towards the half hidden image, feeling but +ill at ease meantime--to see what was next to follow. First he takes +about a double handful of shavings out of his grego pocket, and +places them carefully before the idol; then laying a bit of ship +biscuit on top and applying the flame from the lamp, he kindled the +shavings into a sacrificial blaze. Presently, after many hasty +snatches into the fire, and still hastier withdrawals of his fingers +(whereby he seemed to be scorching them badly), he at last succeeded +in drawing out the biscuit; then blowing off the heat and ashes a +little, he made a polite offer of it to the little negro. But the +little devil did not seem to fancy such dry sort of fare at all; he +never moved his lips. All these strange antics were accompanied by +still stranger guttural noises from the devotee, who seemed to be +praying in a sing-song or else singing some pagan psalmody or other, +during which his face twitched about in the most unnatural manner. +At last extinguishing the fire, he took the idol up very +unceremoniously, and bagged it again in his grego pocket as +carelessly as if he were a sportsman bagging a dead woodcock. + +All these queer proceedings increased my uncomfortableness, and +seeing him now exhibiting strong symptoms of concluding his business +operations, and jumping into bed with me, I thought it was high time, +now or never, before the light was put out, to break the spell in +which I had so long been bound. + +But the interval I spent in deliberating what to say, was a fatal +one. Taking up his tomahawk from the table, he examined the head of +it for an instant, and then holding it to the light, with his mouth +at the handle, he puffed out great clouds of tobacco smoke. The next +moment the light was extinguished, and this wild cannibal, tomahawk +between his teeth, sprang into bed with me. I sang out, I could not +help it now; and giving a sudden grunt of astonishment he began +feeling me. + +Stammering out something, I knew not what, I rolled away from him +against the wall, and then conjured him, whoever or whatever he might +be, to keep quiet, and let me get up and light the lamp again. But +his guttural responses satisfied me at once that he but ill +comprehended my meaning. + +"Who-e debel you?"--he at last said--"you no speak-e, dam-me, I +kill-e." And so saying the lighted tomahawk began flourishing about +me in the dark. + +"Landlord, for God's sake, Peter Coffin!" shouted I. "Landlord! +Watch! Coffin! Angels! save me!" + +"Speak-e! tell-ee me who-ee be, or dam-me, I kill-e!" again growled +the cannibal, while his horrid flourishings of the tomahawk scattered +the hot tobacco ashes about me till I thought my linen would get on +fire. But thank heaven, at that moment the landlord came into the +room light in hand, and leaping from the bed I ran up to him. + +"Don't be afraid now," said he, grinning again, "Queequeg here +wouldn't harm a hair of your head." + +"Stop your grinning," shouted I, "and why didn't you tell me that +that infernal harpooneer was a cannibal?" + +"I thought ye know'd it;--didn't I tell ye, he was a peddlin' heads +around town?--but turn flukes again and go to sleep. Queequeg, look +here--you sabbee me, I sabbee--you this man sleepe you--you sabbee?" + +"Me sabbee plenty"--grunted Queequeg, puffing away at his pipe and +sitting up in bed. + +"You gettee in," he added, motioning to me with his tomahawk, and +throwing the clothes to one side. He really did this in not only a +civil but a really kind and charitable way. I stood looking at him a +moment. For all his tattooings he was on the whole a clean, comely +looking cannibal. What's all this fuss I have been making about, +thought I to myself--the man's a human being just as I am: he has +just as much reason to fear me, as I have to be afraid of him. +Better sleep with a sober cannibal than a drunken Christian. + +"Landlord," said I, "tell him to stash his tomahawk there, or pipe, +or whatever you call it; tell him to stop smoking, in short, and I +will turn in with him. But I don't fancy having a man smoking in bed +with me. It's dangerous. Besides, I ain't insured." + +This being told to Queequeg, he at once complied, and again politely +motioned me to get into bed--rolling over to one side as much as to +say--I won't touch a leg of ye." + +"Good night, landlord," said I, "you may go." + +I turned in, and never slept better in my life. + + + +CHAPTER 4 + +The Counterpane. + + +Upon waking next morning about daylight, I found Queequeg's arm +thrown over me in the most loving and affectionate manner. You had +almost thought I had been his wife. The counterpane was of +patchwork, full of odd little parti-coloured squares and triangles; +and this arm of his tattooed all over with an interminable Cretan +labyrinth of a figure, no two parts of which were of one precise +shade--owing I suppose to his keeping his arm at sea unmethodically +in sun and shade, his shirt sleeves irregularly rolled up at various +times--this same arm of his, I say, looked for all the world like a +strip of that same patchwork quilt. Indeed, partly lying on it as +the arm did when I first awoke, I could hardly tell it from the +quilt, they so blended their hues together; and it was only by the +sense of weight and pressure that I could tell that Queequeg was +hugging me. + +My sensations were strange. Let me try to explain them. When I was +a child, I well remember a somewhat similar circumstance that befell +me; whether it was a reality or a dream, I never could entirely +settle. The circumstance was this. I had been cutting up some caper +or other--I think it was trying to crawl up the chimney, as I had +seen a little sweep do a few days previous; and my stepmother who, +somehow or other, was all the time whipping me, or sending me to bed +supperless,--my mother dragged me by the legs out of the chimney and +packed me off to bed, though it was only two o'clock in the afternoon +of the 21st June, the longest day in the year in our hemisphere. I +felt dreadfully. But there was no help for it, so up stairs I went +to my little room in the third floor, undressed myself as slowly as +possible so as to kill time, and with a bitter sigh got between the +sheets. + +I lay there dismally calculating that sixteen entire hours must +elapse before I could hope for a resurrection. Sixteen hours in bed! +the small of my back ached to think of it. And it was so light too; +the sun shining in at the window, and a great rattling of coaches in +the streets, and the sound of gay voices all over the house. I felt +worse and worse--at last I got up, dressed, and softly going down in +my stockinged feet, sought out my stepmother, and suddenly threw +myself at her feet, beseeching her as a particular favour to give me a +good slippering for my misbehaviour; anything indeed but condemning +me to lie abed such an unendurable length of time. But she was the +best and most conscientious of stepmothers, and back I had to go to +my room. For several hours I lay there broad awake, feeling a great +deal worse than I have ever done since, even from the greatest +subsequent misfortunes. At last I must have fallen into a troubled +nightmare of a doze; and slowly waking from it--half steeped in +dreams--I opened my eyes, and the before sun-lit room was now wrapped +in outer darkness. Instantly I felt a shock running through all my +frame; nothing was to be seen, and nothing was to be heard; but a +supernatural hand seemed placed in mine. My arm hung over the +counterpane, and the nameless, unimaginable, silent form or phantom, +to which the hand belonged, seemed closely seated by my bed-side. +For what seemed ages piled on ages, I lay there, frozen with the most +awful fears, not daring to drag away my hand; yet ever thinking that +if I could but stir it one single inch, the horrid spell would be +broken. I knew not how this consciousness at last glided away from +me; but waking in the morning, I shudderingly remembered it all, and +for days and weeks and months afterwards I lost myself in confounding +attempts to explain the mystery. Nay, to this very hour, I often +puzzle myself with it. + +Now, take away the awful fear, and my sensations at feeling the +supernatural hand in mine were very similar, in their strangeness, +to those which I experienced on waking up and seeing Queequeg's pagan +arm thrown round me. But at length all the past night's events +soberly recurred, one by one, in fixed reality, and then I lay only +alive to the comical predicament. For though I tried to move his +arm--unlock his bridegroom clasp--yet, sleeping as he was, he still +hugged me tightly, as though naught but death should part us twain. +I now strove to rouse him--"Queequeg!"--but his only answer was a +snore. I then rolled over, my neck feeling as if it were in a +horse-collar; and suddenly felt a slight scratch. Throwing aside the +counterpane, there lay the tomahawk sleeping by the savage's side, as +if it were a hatchet-faced baby. A pretty pickle, truly, thought I; +abed here in a strange house in the broad day, with a cannibal and a +tomahawk! "Queequeg!--in the name of goodness, Queequeg, wake!" At +length, by dint of much wriggling, and loud and incessant +expostulations upon the unbecomingness of his hugging a fellow male +in that matrimonial sort of style, I succeeded in extracting a grunt; +and presently, he drew back his arm, shook himself all over like a +Newfoundland dog just from the water, and sat up in bed, stiff as a +pike-staff, looking at me, and rubbing his eyes as if he did not +altogether remember how I came to be there, though a dim +consciousness of knowing something about me seemed slowly dawning +over him. Meanwhile, I lay quietly eyeing him, having no serious +misgivings now, and bent upon narrowly observing so curious a +creature. When, at last, his mind seemed made up touching the +character of his bedfellow, and he became, as it were, reconciled to +the fact; he jumped out upon the floor, and by certain signs and +sounds gave me to understand that, if it pleased me, he would dress +first and then leave me to dress afterwards, leaving the whole +apartment to myself. Thinks I, Queequeg, under the circumstances, +this is a very civilized overture; but, the truth is, these savages +have an innate sense of delicacy, say what you will; it is marvellous +how essentially polite they are. I pay this particular compliment to +Queequeg, because he treated me with so much civility and +consideration, while I was guilty of great rudeness; staring at him +from the bed, and watching all his toilette motions; for the time my +curiosity getting the better of my breeding. Nevertheless, a man +like Queequeg you don't see every day, he and his ways were well +worth unusual regarding. + +He commenced dressing at top by donning his beaver hat, a very tall +one, by the by, and then--still minus his trowsers--he hunted up his +boots. What under the heavens he did it for, I cannot tell, but his +next movement was to crush himself--boots in hand, and hat on--under +the bed; when, from sundry violent gaspings and strainings, I +inferred he was hard at work booting himself; though by no law of +propriety that I ever heard of, is any man required to be private +when putting on his boots. But Queequeg, do you see, was a creature +in the transition stage--neither caterpillar nor butterfly. He was +just enough civilized to show off his outlandishness in the strangest +possible manners. His education was not yet completed. He was an +undergraduate. If he had not been a small degree civilized, he very +probably would not have troubled himself with boots at all; but then, +if he had not been still a savage, he never would have dreamt of +getting under the bed to put them on. At last, he emerged with his +hat very much dented and crushed down over his eyes, and began +creaking and limping about the room, as if, not being much accustomed +to boots, his pair of damp, wrinkled cowhide ones--probably not made +to order either--rather pinched and tormented him at the first go off +of a bitter cold morning. + +Seeing, now, that there were no curtains to the window, and that the +street being very narrow, the house opposite commanded a plain view +into the room, and observing more and more the indecorous figure that +Queequeg made, staving about with little else but his hat and boots +on; I begged him as well as I could, to accelerate his toilet +somewhat, and particularly to get into his pantaloons as soon as +possible. He complied, and then proceeded to wash himself. At that +time in the morning any Christian would have washed his face; but +Queequeg, to my amazement, contented himself with restricting his +ablutions to his chest, arms, and hands. He then donned his +waistcoat, and taking up a piece of hard soap on the wash-stand +centre table, dipped it into water and commenced lathering his face. +I was watching to see where he kept his razor, when lo and behold, he +takes the harpoon from the bed corner, slips out the long wooden +stock, unsheathes the head, whets it a little on his boot, and +striding up to the bit of mirror against the wall, begins a vigorous +scraping, or rather harpooning of his cheeks. Thinks I, Queequeg, +this is using Rogers's best cutlery with a vengeance. Afterwards I +wondered the less at this operation when I came to know of what fine +steel the head of a harpoon is made, and how exceedingly sharp the +long straight edges are always kept. + +The rest of his toilet was soon achieved, and he proudly marched out +of the room, wrapped up in his great pilot monkey jacket, and +sporting his harpoon like a marshal's baton. + + + +CHAPTER 5 + +Breakfast. + + +I quickly followed suit, and descending into the bar-room accosted +the grinning landlord very pleasantly. I cherished no malice towards +him, though he had been skylarking with me not a little in the matter +of my bedfellow. + +However, a good laugh is a mighty good thing, and rather too scarce a +good thing; the more's the pity. So, if any one man, in his own +proper person, afford stuff for a good joke to anybody, let him not +be backward, but let him cheerfully allow himself to spend and be +spent in that way. And the man that has anything bountifully +laughable about him, be sure there is more in that man than you +perhaps think for. + +The bar-room was now full of the boarders who had been dropping in +the night previous, and whom I had not as yet had a good look at. +They were nearly all whalemen; chief mates, and second mates, and +third mates, and sea carpenters, and sea coopers, and sea +blacksmiths, and harpooneers, and ship keepers; a brown and brawny +company, with bosky beards; an unshorn, shaggy set, all wearing +monkey jackets for morning gowns. + +You could pretty plainly tell how long each one had been ashore. +This young fellow's healthy cheek is like a sun-toasted pear in hue, +and would seem to smell almost as musky; he cannot have been three +days landed from his Indian voyage. That man next him looks a few +shades lighter; you might say a touch of satin wood is in him. In +the complexion of a third still lingers a tropic tawn, but slightly +bleached withal; HE doubtless has tarried whole weeks ashore. But +who could show a cheek like Queequeg? which, barred with various +tints, seemed like the Andes' western slope, to show forth in one +array, contrasting climates, zone by zone. + +"Grub, ho!" now cried the landlord, flinging open a door, and in we +went to breakfast. + +They say that men who have seen the world, thereby become quite at +ease in manner, quite self-possessed in company. Not always, though: +Ledyard, the great New England traveller, and Mungo Park, the Scotch +one; of all men, they possessed the least assurance in the parlor. +But perhaps the mere crossing of Siberia in a sledge drawn by dogs as +Ledyard did, or the taking a long solitary walk on an empty stomach, +in the negro heart of Africa, which was the sum of poor Mungo's +performances--this kind of travel, I say, may not be the very best +mode of attaining a high social polish. Still, for the most part, +that sort of thing is to be had anywhere. + +These reflections just here are occasioned by the circumstance that +after we were all seated at the table, and I was preparing to hear +some good stories about whaling; to my no small surprise, nearly +every man maintained a profound silence. And not only that, but they +looked embarrassed. Yes, here were a set of sea-dogs, many of whom +without the slightest bashfulness had boarded great whales on the +high seas--entire strangers to them--and duelled them dead without +winking; and yet, here they sat at a social breakfast table--all of +the same calling, all of kindred tastes--looking round as sheepishly +at each other as though they had never been out of sight of some +sheepfold among the Green Mountains. A curious sight; these bashful +bears, these timid warrior whalemen! + +But as for Queequeg--why, Queequeg sat there among them--at the head +of the table, too, it so chanced; as cool as an icicle. To be sure I +cannot say much for his breeding. His greatest admirer could not +have cordially justified his bringing his harpoon into breakfast with +him, and using it there without ceremony; reaching over the table +with it, to the imminent jeopardy of many heads, and grappling the +beefsteaks towards him. But THAT was certainly very coolly done by +him, and every one knows that in most people's estimation, to do +anything coolly is to do it genteelly. + +We will not speak of all Queequeg's peculiarities here; how he +eschewed coffee and hot rolls, and applied his undivided attention to +beefsteaks, done rare. Enough, that when breakfast was over he +withdrew like the rest into the public room, lighted his +tomahawk-pipe, and was sitting there quietly digesting and smoking +with his inseparable hat on, when I sallied out for a stroll. + + + +CHAPTER 6 + +The Street. + + +If I had been astonished at first catching a glimpse of so outlandish +an individual as Queequeg circulating among the polite society of a +civilized town, that astonishment soon departed upon taking my first +daylight stroll through the streets of New Bedford. + +In thoroughfares nigh the docks, any considerable seaport will +frequently offer to view the queerest looking nondescripts from +foreign parts. Even in Broadway and Chestnut streets, Mediterranean +mariners will sometimes jostle the affrighted ladies. Regent Street +is not unknown to Lascars and Malays; and at Bombay, in the Apollo +Green, live Yankees have often scared the natives. But New Bedford +beats all Water Street and Wapping. In these last-mentioned haunts +you see only sailors; but in New Bedford, actual cannibals stand +chatting at street corners; savages outright; many of whom yet carry +on their bones unholy flesh. It makes a stranger stare. + +But, besides the Feegeeans, Tongatobooarrs, Erromanggoans, +Pannangians, and Brighggians, and, besides the wild specimens of the +whaling-craft which unheeded reel about the streets, you will see +other sights still more curious, certainly more comical. There +weekly arrive in this town scores of green Vermonters and New +Hampshire men, all athirst for gain and glory in the fishery. They +are mostly young, of stalwart frames; fellows who have felled +forests, and now seek to drop the axe and snatch the whale-lance. +Many are as green as the Green Mountains whence they came. In some +things you would think them but a few hours old. Look there! that +chap strutting round the corner. He wears a beaver hat and +swallow-tailed coat, girdled with a sailor-belt and sheath-knife. +Here comes another with a sou'-wester and a bombazine cloak. + +No town-bred dandy will compare with a country-bred one--I mean a +downright bumpkin dandy--a fellow that, in the dog-days, will mow his +two acres in buckskin gloves for fear of tanning his hands. Now when +a country dandy like this takes it into his head to make a +distinguished reputation, and joins the great whale-fishery, you +should see the comical things he does upon reaching the seaport. In +bespeaking his sea-outfit, he orders bell-buttons to his waistcoats; +straps to his canvas trowsers. Ah, poor Hay-Seed! how bitterly will +burst those straps in the first howling gale, when thou art driven, +straps, buttons, and all, down the throat of the tempest. + +But think not that this famous town has only harpooneers, cannibals, +and bumpkins to show her visitors. Not at all. Still New Bedford is +a queer place. Had it not been for us whalemen, that tract of land +would this day perhaps have been in as howling condition as the coast +of Labrador. As it is, parts of her back country are enough to +frighten one, they look so bony. The town itself is perhaps the +dearest place to live in, in all New England. It is a land of oil, +true enough: but not like Canaan; a land, also, of corn and wine. +The streets do not run with milk; nor in the spring-time do they pave +them with fresh eggs. Yet, in spite of this, nowhere in all America +will you find more patrician-like houses; parks and gardens more +opulent, than in New Bedford. Whence came they? how planted upon +this once scraggy scoria of a country? + +Go and gaze upon the iron emblematical harpoons round yonder lofty +mansion, and your question will be answered. Yes; all these brave +houses and flowery gardens came from the Atlantic, Pacific, and +Indian oceans. One and all, they were harpooned and dragged up +hither from the bottom of the sea. Can Herr Alexander perform a feat +like that? + +In New Bedford, fathers, they say, give whales for dowers to their +daughters, and portion off their nieces with a few porpoises a-piece. +You must go to New Bedford to see a brilliant wedding; for, they +say, they have reservoirs of oil in every house, and every night +recklessly burn their lengths in spermaceti candles. + +In summer time, the town is sweet to see; full of fine maples--long +avenues of green and gold. And in August, high in air, the beautiful +and bountiful horse-chestnuts, candelabra-wise, proffer the passer-by +their tapering upright cones of congregated blossoms. So omnipotent +is art; which in many a district of New Bedford has superinduced +bright terraces of flowers upon the barren refuse rocks thrown aside +at creation's final day. + +And the women of New Bedford, they bloom like their own red roses. +But roses only bloom in summer; whereas the fine carnation of their +cheeks is perennial as sunlight in the seventh heavens. Elsewhere +match that bloom of theirs, ye cannot, save in Salem, where they tell +me the young girls breathe such musk, their sailor sweethearts smell +them miles off shore, as though they were drawing nigh the odorous +Moluccas instead of the Puritanic sands. + + + +CHAPTER 7 + +The Chapel. + + +In this same New Bedford there stands a Whaleman's Chapel, and few +are the moody fishermen, shortly bound for the Indian Ocean or +Pacific, who fail to make a Sunday visit to the spot. I am sure that +I did not. + +Returning from my first morning stroll, I again sallied out upon this +special errand. The sky had changed from clear, sunny cold, to +driving sleet and mist. Wrapping myself in my shaggy jacket of the +cloth called bearskin, I fought my way against the stubborn storm. +Entering, I found a small scattered congregation of sailors, and +sailors' wives and widows. A muffled silence reigned, only broken at +times by the shrieks of the storm. Each silent worshipper seemed +purposely sitting apart from the other, as if each silent grief were +insular and incommunicable. The chaplain had not yet arrived; and +there these silent islands of men and women sat steadfastly eyeing +several marble tablets, with black borders, masoned into the wall on +either side the pulpit. Three of them ran something like the +following, but I do not pretend to quote:-- + +SACRED +TO THE MEMORY +OF +JOHN TALBOT, +Who, at the age of eighteen, was lost overboard, +Near the Isle of Desolation, off Patagonia, +November 1st, 1836. +THIS TABLET +Is erected to his Memory +BY HIS +SISTER. +_____________ + +SACRED +TO THE MEMORY +OF +ROBERT LONG, WILLIS ELLERY, +NATHAN COLEMAN, WALTER CANNY, SETH MACY, +AND SAMUEL GLEIG, +Forming one of the boats' crews +OF +THE SHIP ELIZA +Who were towed out of sight by a Whale, +On the Off-shore Ground in the +PACIFIC, +December 31st, 1839. +THIS MARBLE +Is here placed by their surviving +SHIPMATES. +_____________ + +SACRED +TO THE MEMORY +OF +The late +CAPTAIN EZEKIEL HARDY, +Who in the bows of his boat was killed by a +Sperm Whale on the coast of Japan, +AUGUST 3d, 1833. +THIS TABLET +Is erected to his Memory +BY +HIS WIDOW. + +Shaking off the sleet from my ice-glazed hat and jacket, I seated +myself near the door, and turning sideways was surprised to see +Queequeg near me. Affected by the solemnity of the scene, there was +a wondering gaze of incredulous curiosity in his countenance. This +savage was the only person present who seemed to notice my entrance; +because he was the only one who could not read, and, therefore, was +not reading those frigid inscriptions on the wall. Whether any of +the relatives of the seamen whose names appeared there were now among +the congregation, I knew not; but so many are the unrecorded +accidents in the fishery, and so plainly did several women present +wear the countenance if not the trappings of some unceasing grief, +that I feel sure that here before me were assembled those, in whose +unhealing hearts the sight of those bleak tablets sympathetically +caused the old wounds to bleed afresh. + +Oh! ye whose dead lie buried beneath the green grass; who standing +among flowers can say--here, HERE lies my beloved; ye know not the +desolation that broods in bosoms like these. What bitter blanks in +those black-bordered marbles which cover no ashes! What despair in +those immovable inscriptions! What deadly voids and unbidden +infidelities in the lines that seem to gnaw upon all Faith, and +refuse resurrections to the beings who have placelessly perished +without a grave. As well might those tablets stand in the cave of +Elephanta as here. + +In what census of living creatures, the dead of mankind are included; +why it is that a universal proverb says of them, that they tell no +tales, though containing more secrets than the Goodwin Sands; how it +is that to his name who yesterday departed for the other world, we +prefix so significant and infidel a word, and yet do not thus entitle +him, if he but embarks for the remotest Indies of this living earth; +why the Life Insurance Companies pay death-forfeitures upon +immortals; in what eternal, unstirring paralysis, and deadly, +hopeless trance, yet lies antique Adam who died sixty round centuries +ago; how it is that we still refuse to be comforted for those who we +nevertheless maintain are dwelling in unspeakable bliss; why all the +living so strive to hush all the dead; wherefore but the rumor of a +knocking in a tomb will terrify a whole city. All these things are +not without their meanings. + +But Faith, like a jackal, feeds among the tombs, and even from these +dead doubts she gathers her most vital hope. + +It needs scarcely to be told, with what feelings, on the eve of a +Nantucket voyage, I regarded those marble tablets, and by the murky +light of that darkened, doleful day read the fate of the whalemen who +had gone before me. Yes, Ishmael, the same fate may be thine. But +somehow I grew merry again. Delightful inducements to embark, fine +chance for promotion, it seems--aye, a stove boat will make me an +immortal by brevet. Yes, there is death in this business of +whaling--a speechlessly quick chaotic bundling of a man into +Eternity. But what then? Methinks we have hugely mistaken this +matter of Life and Death. Methinks that what they call my shadow +here on earth is my true substance. Methinks that in looking at +things spiritual, we are too much like oysters observing the sun +through the water, and thinking that thick water the thinnest of air. +Methinks my body is but the lees of my better being. In fact take +my body who will, take it I say, it is not me. And therefore three +cheers for Nantucket; and come a stove boat and stove body when they +will, for stave my soul, Jove himself cannot. + + + +CHAPTER 8 + +The Pulpit. + + +I had not been seated very long ere a man of a certain venerable +robustness entered; immediately as the storm-pelted door flew back +upon admitting him, a quick regardful eyeing of him by all the +congregation, sufficiently attested that this fine old man was the +chaplain. Yes, it was the famous Father Mapple, so called by the +whalemen, among whom he was a very great favourite. He had been a +sailor and a harpooneer in his youth, but for many years past had +dedicated his life to the ministry. At the time I now write of, +Father Mapple was in the hardy winter of a healthy old age; that sort +of old age which seems merging into a second flowering youth, for +among all the fissures of his wrinkles, there shone certain mild +gleams of a newly developing bloom--the spring verdure peeping forth +even beneath February's snow. No one having previously heard his +history, could for the first time behold Father Mapple without the +utmost interest, because there were certain engrafted clerical +peculiarities about him, imputable to that adventurous maritime life +he had led. When he entered I observed that he carried no umbrella, +and certainly had not come in his carriage, for his tarpaulin hat ran +down with melting sleet, and his great pilot cloth jacket seemed +almost to drag him to the floor with the weight of the water it had +absorbed. However, hat and coat and overshoes were one by one +removed, and hung up in a little space in an adjacent corner; when, +arrayed in a decent suit, he quietly approached the pulpit. + +Like most old fashioned pulpits, it was a very lofty one, and since a +regular stairs to such a height would, by its long angle with the +floor, seriously contract the already small area of the chapel, the +architect, it seemed, had acted upon the hint of Father Mapple, and +finished the pulpit without a stairs, substituting a perpendicular +side ladder, like those used in mounting a ship from a boat at sea. +The wife of a whaling captain had provided the chapel with a handsome +pair of red worsted man-ropes for this ladder, which, being itself +nicely headed, and stained with a mahogany colour, the whole +contrivance, considering what manner of chapel it was, seemed by no +means in bad taste. Halting for an instant at the foot of the +ladder, and with both hands grasping the ornamental knobs of the +man-ropes, Father Mapple cast a look upwards, and then with a truly +sailor-like but still reverential dexterity, hand over hand, mounted +the steps as if ascending the main-top of his vessel. + +The perpendicular parts of this side ladder, as is usually the case +with swinging ones, were of cloth-covered rope, only the rounds were +of wood, so that at every step there was a joint. At my first +glimpse of the pulpit, it had not escaped me that however convenient +for a ship, these joints in the present instance seemed unnecessary. +For I was not prepared to see Father Mapple after gaining the height, +slowly turn round, and stooping over the pulpit, deliberately drag up +the ladder step by step, till the whole was deposited within, leaving +him impregnable in his little Quebec. + +I pondered some time without fully comprehending the reason for this. +Father Mapple enjoyed such a wide reputation for sincerity and +sanctity, that I could not suspect him of courting notoriety by any +mere tricks of the stage. No, thought I, there must be some sober +reason for this thing; furthermore, it must symbolize something +unseen. Can it be, then, that by that act of physical isolation, he +signifies his spiritual withdrawal for the time, from all outward +worldly ties and connexions? Yes, for replenished with the meat and +wine of the word, to the faithful man of God, this pulpit, I see, is +a self-containing stronghold--a lofty Ehrenbreitstein, with a +perennial well of water within the walls. + +But the side ladder was not the only strange feature of the place, +borrowed from the chaplain's former sea-farings. Between the marble +cenotaphs on either hand of the pulpit, the wall which formed its +back was adorned with a large painting representing a gallant ship +beating against a terrible storm off a lee coast of black rocks and +snowy breakers. But high above the flying scud and dark-rolling +clouds, there floated a little isle of sunlight, from which beamed +forth an angel's face; and this bright face shed a distinct spot of +radiance upon the ship's tossed deck, something like that silver +plate now inserted into the Victory's plank where Nelson fell. "Ah, +noble ship," the angel seemed to say, "beat on, beat on, thou noble +ship, and bear a hardy helm; for lo! the sun is breaking through; the +clouds are rolling off--serenest azure is at hand." + +Nor was the pulpit itself without a trace of the same sea-taste that +had achieved the ladder and the picture. Its panelled front was in +the likeness of a ship's bluff bows, and the Holy Bible rested on a +projecting piece of scroll work, fashioned after a ship's +fiddle-headed beak. + +What could be more full of meaning?--for the pulpit is ever this +earth's foremost part; all the rest comes in its rear; the pulpit +leads the world. From thence it is the storm of God's quick wrath is +first descried, and the bow must bear the earliest brunt. From +thence it is the God of breezes fair or foul is first invoked for +favourable winds. Yes, the world's a ship on its passage out, and not +a voyage complete; and the pulpit is its prow. + + + +CHAPTER 9 + +The Sermon. + + +Father Mapple rose, and in a mild voice of unassuming authority +ordered the scattered people to condense. "Starboard gangway, +there! side away to larboard--larboard gangway to starboard! +Midships! midships!" + +There was a low rumbling of heavy sea-boots among the benches, and a +still slighter shuffling of women's shoes, and all was quiet again, +and every eye on the preacher. + +He paused a little; then kneeling in the pulpit's bows, folded his +large brown hands across his chest, uplifted his closed eyes, and +offered a prayer so deeply devout that he seemed kneeling and praying +at the bottom of the sea. + +This ended, in prolonged solemn tones, like the continual tolling of +a bell in a ship that is foundering at sea in a fog--in such tones he +commenced reading the following hymn; but changing his manner towards +the concluding stanzas, burst forth with a pealing exultation and +joy-- + +"The ribs and terrors in the whale, +Arched over me a dismal gloom, +While all God's sun-lit waves rolled by, +And lift me deepening down to doom. + +"I saw the opening maw of hell, +With endless pains and sorrows there; +Which none but they that feel can tell-- +Oh, I was plunging to despair. + +"In black distress, I called my God, +When I could scarce believe him mine, +He bowed his ear to my complaints-- +No more the whale did me confine. + +"With speed he flew to my relief, +As on a radiant dolphin borne; +Awful, yet bright, as lightning shone +The face of my Deliverer God. + +"My song for ever shall record +That terrible, that joyful hour; +I give the glory to my God, +His all the mercy and the power. + + +Nearly all joined in singing this hymn, which swelled high above the +howling of the storm. A brief pause ensued; the preacher slowly +turned over the leaves of the Bible, and at last, folding his hand +down upon the proper page, said: "Beloved shipmates, clinch the last +verse of the first chapter of Jonah--'And God had prepared a great +fish to swallow up Jonah.'" + +"Shipmates, this book, containing only four chapters--four yarns--is +one of the smallest strands in the mighty cable of the Scriptures. +Yet what depths of the soul does Jonah's deep sealine sound! what a +pregnant lesson to us is this prophet! What a noble thing is that +canticle in the fish's belly! How billow-like and boisterously +grand! We feel the floods surging over us; we sound with him to the +kelpy bottom of the waters; sea-weed and all the slime of the sea is +about us! But WHAT is this lesson that the book of Jonah teaches? +Shipmates, it is a two-stranded lesson; a lesson to us all as sinful +men, and a lesson to me as a pilot of the living God. As sinful men, +it is a lesson to us all, because it is a story of the sin, +hard-heartedness, suddenly awakened fears, the swift punishment, +repentance, prayers, and finally the deliverance and joy of Jonah. +As with all sinners among men, the sin of this son of Amittai was in +his wilful disobedience of the command of God--never mind now what +that command was, or how conveyed--which he found a hard command. +But all the things that God would have us do are hard for us to +do--remember that--and hence, he oftener commands us than endeavors +to persuade. And if we obey God, we must disobey ourselves; and it +is in this disobeying ourselves, wherein the hardness of obeying God +consists. + +"With this sin of disobedience in him, Jonah still further flouts at +God, by seeking to flee from Him. He thinks that a ship made by men +will carry him into countries where God does not reign, but only the +Captains of this earth. He skulks about the wharves of Joppa, and +seeks a ship that's bound for Tarshish. There lurks, perhaps, a +hitherto unheeded meaning here. By all accounts Tarshish could have +been no other city than the modern Cadiz. That's the opinion of +learned men. And where is Cadiz, shipmates? Cadiz is in Spain; as +far by water, from Joppa, as Jonah could possibly have sailed in +those ancient days, when the Atlantic was an almost unknown sea. +Because Joppa, the modern Jaffa, shipmates, is on the most easterly +coast of the Mediterranean, the Syrian; and Tarshish or Cadiz more +than two thousand miles to the westward from that, just outside the +Straits of Gibraltar. See ye not then, shipmates, that Jonah sought +to flee world-wide from God? Miserable man! Oh! most contemptible +and worthy of all scorn; with slouched hat and guilty eye, skulking +from his God; prowling among the shipping like a vile burglar +hastening to cross the seas. So disordered, self-condemning is his +look, that had there been policemen in those days, Jonah, on the mere +suspicion of something wrong, had been arrested ere he touched a +deck. How plainly he's a fugitive! no baggage, not a hat-box, +valise, or carpet-bag,--no friends accompany him to the wharf with +their adieux. At last, after much dodging search, he finds the +Tarshish ship receiving the last items of her cargo; and as he steps +on board to see its Captain in the cabin, all the sailors for the +moment desist from hoisting in the goods, to mark the stranger's evil +eye. Jonah sees this; but in vain he tries to look all ease and +confidence; in vain essays his wretched smile. Strong intuitions of +the man assure the mariners he can be no innocent. In their gamesome +but still serious way, one whispers to the other--"Jack, he's robbed +a widow;" or, "Joe, do you mark him; he's a bigamist;" or, "Harry +lad, I guess he's the adulterer that broke jail in old Gomorrah, or +belike, one of the missing murderers from Sodom." Another runs to +read the bill that's stuck against the spile upon the wharf to which +the ship is moored, offering five hundred gold coins for the +apprehension of a parricide, and containing a description of his +person. He reads, and looks from Jonah to the bill; while all his +sympathetic shipmates now crowd round Jonah, prepared to lay their +hands upon him. Frighted Jonah trembles, and summoning all his +boldness to his face, only looks so much the more a coward. He will +not confess himself suspected; but that itself is strong suspicion. +So he makes the best of it; and when the sailors find him not to be +the man that is advertised, they let him pass, and he descends into +the cabin. + +"'Who's there?' cries the Captain at his busy desk, hurriedly making +out his papers for the Customs--'Who's there?' Oh! how that harmless +question mangles Jonah! For the instant he almost turns to flee +again. But he rallies. 'I seek a passage in this ship to Tarshish; +how soon sail ye, sir?' Thus far the busy Captain had not looked up +to Jonah, though the man now stands before him; but no sooner does he +hear that hollow voice, than he darts a scrutinizing glance. 'We +sail with the next coming tide,' at last he slowly answered, still +intently eyeing him. 'No sooner, sir?'--'Soon enough for any honest +man that goes a passenger.' Ha! Jonah, that's another stab. But he +swiftly calls away the Captain from that scent. 'I'll sail with +ye,'--he says,--'the passage money how much is that?--I'll pay now.' +For it is particularly written, shipmates, as if it were a thing not +to be overlooked in this history, 'that he paid the fare thereof' ere +the craft did sail. And taken with the context, this is full of +meaning. + +"Now Jonah's Captain, shipmates, was one whose discernment detects +crime in any, but whose cupidity exposes it only in the penniless. +In this world, shipmates, sin that pays its way can travel freely, +and without a passport; whereas Virtue, if a pauper, is stopped at +all frontiers. So Jonah's Captain prepares to test the length of +Jonah's purse, ere he judge him openly. He charges him thrice the +usual sum; and it's assented to. Then the Captain knows that Jonah +is a fugitive; but at the same time resolves to help a flight that +paves its rear with gold. Yet when Jonah fairly takes out his purse, +prudent suspicions still molest the Captain. He rings every coin to +find a counterfeit. Not a forger, any way, he mutters; and Jonah is +put down for his passage. 'Point out my state-room, Sir,' says Jonah +now, 'I'm travel-weary; I need sleep.' 'Thou lookest like it,' says +the Captain, 'there's thy room.' Jonah enters, and would lock the +door, but the lock contains no key. Hearing him foolishly fumbling +there, the Captain laughs lowly to himself, and mutters something +about the doors of convicts' cells being never allowed to be locked +within. All dressed and dusty as he is, Jonah throws himself into +his berth, and finds the little state-room ceiling almost resting on +his forehead. The air is close, and Jonah gasps. Then, in that +contracted hole, sunk, too, beneath the ship's water-line, Jonah +feels the heralding presentiment of that stifling hour, when the +whale shall hold him in the smallest of his bowels' wards. + +"Screwed at its axis against the side, a swinging lamp slightly +oscillates in Jonah's room; and the ship, heeling over towards the +wharf with the weight of the last bales received, the lamp, flame and +all, though in slight motion, still maintains a permanent obliquity +with reference to the room; though, in truth, infallibly straight +itself, it but made obvious the false, lying levels among which it +hung. The lamp alarms and frightens Jonah; as lying in his berth his +tormented eyes roll round the place, and this thus far successful +fugitive finds no refuge for his restless glance. But that +contradiction in the lamp more and more appals him. The floor, the +ceiling, and the side, are all awry. 'Oh! so my conscience hangs in +me!' he groans, 'straight upwards, so it burns; but the chambers of +my soul are all in crookedness!' + +"Like one who after a night of drunken revelry hies to his bed, still +reeling, but with conscience yet pricking him, as the plungings of +the Roman race-horse but so much the more strike his steel tags into +him; as one who in that miserable plight still turns and turns in +giddy anguish, praying God for annihilation until the fit be passed; +and at last amid the whirl of woe he feels, a deep stupor steals over +him, as over the man who bleeds to death, for conscience is the +wound, and there's naught to staunch it; so, after sore wrestlings in +his berth, Jonah's prodigy of ponderous misery drags him drowning +down to sleep. + +"And now the time of tide has come; the ship casts off her cables; +and from the deserted wharf the uncheered ship for Tarshish, all +careening, glides to sea. That ship, my friends, was the first of +recorded smugglers! the contraband was Jonah. But the sea rebels; he +will not bear the wicked burden. A dreadful storm comes on, the +ship is like to break. But now when the boatswain calls all hands to +lighten her; when boxes, bales, and jars are clattering overboard; +when the wind is shrieking, and the men are yelling, and every plank +thunders with trampling feet right over Jonah's head; in all this +raging tumult, Jonah sleeps his hideous sleep. He sees no black sky +and raging sea, feels not the reeling timbers, and little hears he or +heeds he the far rush of the mighty whale, which even now with open +mouth is cleaving the seas after him. Aye, shipmates, Jonah was gone +down into the sides of the ship--a berth in the cabin as I have taken +it, and was fast asleep. But the frightened master comes to him, and +shrieks in his dead ear, 'What meanest thou, O, sleeper! arise!' +Startled from his lethargy by that direful cry, Jonah staggers to his +feet, and stumbling to the deck, grasps a shroud, to look out upon +the sea. But at that moment he is sprung upon by a panther billow +leaping over the bulwarks. Wave after wave thus leaps into the ship, +and finding no speedy vent runs roaring fore and aft, till the +mariners come nigh to drowning while yet afloat. And ever, as the +white moon shows her affrighted face from the steep gullies in the +blackness overhead, aghast Jonah sees the rearing bowsprit pointing +high upward, but soon beat downward again towards the tormented deep. + +"Terrors upon terrors run shouting through his soul. In all his +cringing attitudes, the God-fugitive is now too plainly known. The +sailors mark him; more and more certain grow their suspicions of him, +and at last, fully to test the truth, by referring the whole matter +to high Heaven, they fall to casting lots, to see for whose +cause this great tempest was upon them. The lot is Jonah's; that +discovered, then how furiously they mob him with their questions. +'What is thine occupation? Whence comest thou? Thy country? What +people? But mark now, my shipmates, the behavior of poor Jonah. The +eager mariners but ask him who he is, and where from; whereas, they +not only receive an answer to those questions, but likewise another +answer to a question not put by them, but the unsolicited answer is +forced from Jonah by the hard hand of God that is upon him. + +"'I am a Hebrew,' he cries--and then--'I fear the Lord the God of +Heaven who hath made the sea and the dry land!' Fear him, O Jonah? +Aye, well mightest thou fear the Lord God THEN! Straightway, he now +goes on to make a full confession; whereupon the mariners became more +and more appalled, but still are pitiful. For when Jonah, not yet +supplicating God for mercy, since he but too well knew the darkness +of his deserts,--when wretched Jonah cries out to them to take him +and cast him forth into the sea, for he knew that for HIS sake this +great tempest was upon them; they mercifully turn from him, and seek +by other means to save the ship. But all in vain; the indignant gale +howls louder; then, with one hand raised invokingly to God, with the +other they not unreluctantly lay hold of Jonah. + +"And now behold Jonah taken up as an anchor and dropped into the sea; +when instantly an oily calmness floats out from the east, and the sea +is still, as Jonah carries down the gale with him, leaving smooth +water behind. He goes down in the whirling heart of such a +masterless commotion that he scarce heeds the moment when he drops +seething into the yawning jaws awaiting him; and the whale shoots-to +all his ivory teeth, like so many white bolts, upon his prison. Then +Jonah prayed unto the Lord out of the fish's belly. But observe his +prayer, and learn a weighty lesson. For sinful as he is, Jonah does +not weep and wail for direct deliverance. He feels that his dreadful +punishment is just. He leaves all his deliverance to God, contenting +himself with this, that spite of all his pains and pangs, he will +still look towards His holy temple. And here, shipmates, is true and +faithful repentance; not clamorous for pardon, but grateful for +punishment. And how pleasing to God was this conduct in Jonah, is +shown in the eventual deliverance of him from the sea and the whale. +Shipmates, I do not place Jonah before you to be copied for his sin +but I do place him before you as a model for repentance. Sin not; +but if you do, take heed to repent of it like Jonah." + +While he was speaking these words, the howling of the shrieking, +slanting storm without seemed to add new power to the preacher, who, +when describing Jonah's sea-storm, seemed tossed by a storm himself. +His deep chest heaved as with a ground-swell; his tossed arms seemed +the warring elements at work; and the thunders that rolled away from +off his swarthy brow, and the light leaping from his eye, made all +his simple hearers look on him with a quick fear that was strange to +them. + +There now came a lull in his look, as he silently turned over the +leaves of the Book once more; and, at last, standing motionless, with +closed eyes, for the moment, seemed communing with God and himself. + +But again he leaned over towards the people, and bowing his head +lowly, with an aspect of the deepest yet manliest humility, he spake +these words: + +"Shipmates, God has laid but one hand upon you; both his hands press +upon me. I have read ye by what murky light may be mine the lesson +that Jonah teaches to all sinners; and therefore to ye, and still +more to me, for I am a greater sinner than ye. And now how gladly +would I come down from this mast-head and sit on the hatches there +where you sit, and listen as you listen, while some one of you reads +ME that other and more awful lesson which Jonah teaches to ME, as a +pilot of the living God. How being an anointed pilot-prophet, or +speaker of true things, and bidden by the Lord to sound those +unwelcome truths in the ears of a wicked Nineveh, Jonah, appalled at +the hostility he should raise, fled from his mission, and sought to +escape his duty and his God by taking ship at Joppa. But God is +everywhere; Tarshish he never reached. As we have seen, God came +upon him in the whale, and swallowed him down to living gulfs of +doom, and with swift slantings tore him along 'into the midst of the +seas,' where the eddying depths sucked him ten thousand fathoms down, +and 'the weeds were wrapped about his head,' and all the watery world +of woe bowled over him. Yet even then beyond the reach of any +plummet--'out of the belly of hell'--when the whale grounded upon the +ocean's utmost bones, even then, God heard the engulphed, repenting +prophet when he cried. Then God spake unto the fish; and from the +shuddering cold and blackness of the sea, the whale came breeching up +towards the warm and pleasant sun, and all the delights of air and +earth; and 'vomited out Jonah upon the dry land;' when the word of +the Lord came a second time; and Jonah, bruised and beaten--his ears, +like two sea-shells, still multitudinously murmuring of the +ocean--Jonah did the Almighty's bidding. And what was that, +shipmates? To preach the Truth to the face of Falsehood! That was +it! + +"This, shipmates, this is that other lesson; and woe to that pilot of +the living God who slights it. Woe to him whom this world charms +from Gospel duty! Woe to him who seeks to pour oil upon the waters +when God has brewed them into a gale! Woe to him who seeks to please +rather than to appal! Woe to him whose good name is more to him than +goodness! Woe to him who, in this world, courts not dishonour! Woe +to him who would not be true, even though to be false were salvation! +Yea, woe to him who, as the great Pilot Paul has it, while preaching +to others is himself a castaway!" + +He dropped and fell away from himself for a moment; then lifting his +face to them again, showed a deep joy in his eyes, as he cried out +with a heavenly enthusiasm,--"But oh! shipmates! on the starboard +hand of every woe, there is a sure delight; and higher the top of +that delight, than the bottom of the woe is deep. Is not the +main-truck higher than the kelson is low? Delight is to him--a far, +far upward, and inward delight--who against the proud gods and +commodores of this earth, ever stands forth his own inexorable self. +Delight is to him whose strong arms yet support him, when the ship of +this base treacherous world has gone down beneath him. Delight is to +him, who gives no quarter in the truth, and kills, burns, and +destroys all sin though he pluck it out from under the robes of +Senators and Judges. Delight,--top-gallant delight is to him, who +acknowledges no law or lord, but the Lord his God, and is only a +patriot to heaven. Delight is to him, whom all the waves of the +billows of the seas of the boisterous mob can never shake from this +sure Keel of the Ages. And eternal delight and deliciousness will be +his, who coming to lay him down, can say with his final breath--O +Father!--chiefly known to me by Thy rod--mortal or immortal, here I +die. I have striven to be Thine, more than to be this world's, or +mine own. Yet this is nothing: I leave eternity to Thee; for what +is man that he should live out the lifetime of his God?" + +He said no more, but slowly waving a benediction, covered his face +with his hands, and so remained kneeling, till all the people had +departed, and he was left alone in the place. + + + +CHAPTER 10 + +A Bosom Friend. + + +Returning to the Spouter-Inn from the Chapel, I found Queequeg there +quite alone; he having left the Chapel before the benediction some +time. He was sitting on a bench before the fire, with his feet on +the stove hearth, and in one hand was holding close up to his face +that little negro idol of his; peering hard into its face, and with a +jack-knife gently whittling away at its nose, meanwhile humming to +himself in his heathenish way. + +But being now interrupted, he put up the image; and pretty soon, +going to the table, took up a large book there, and placing it on his +lap began counting the pages with deliberate regularity; at every +fiftieth page--as I fancied--stopping a moment, looking vacantly +around him, and giving utterance to a long-drawn gurgling whistle of +astonishment. He would then begin again at the next fifty; seeming +to commence at number one each time, as though he could not count +more than fifty, and it was only by such a large number of fifties +being found together, that his astonishment at the multitude of pages +was excited. + +With much interest I sat watching him. Savage though he was, and +hideously marred about the face--at least to my taste--his +countenance yet had a something in it which was by no means +disagreeable. You cannot hide the soul. Through all his unearthly +tattooings, I thought I saw the traces of a simple honest heart; and +in his large, deep eyes, fiery black and bold, there seemed tokens of +a spirit that would dare a thousand devils. And besides all this, +there was a certain lofty bearing about the Pagan, which even his +uncouthness could not altogether maim. He looked like a man who had +never cringed and never had had a creditor. Whether it was, too, +that his head being shaved, his forehead was drawn out in freer and +brighter relief, and looked more expansive than it otherwise would, +this I will not venture to decide; but certain it was his head was +phrenologically an excellent one. It may seem ridiculous, but it +reminded me of General Washington's head, as seen in the popular +busts of him. It had the same long regularly graded retreating slope +from above the brows, which were likewise very projecting, like two +long promontories thickly wooded on top. Queequeg was George +Washington cannibalistically developed. + +Whilst I was thus closely scanning him, half-pretending meanwhile to +be looking out at the storm from the casement, he never heeded my +presence, never troubled himself with so much as a single glance; but +appeared wholly occupied with counting the pages of the marvellous +book. Considering how sociably we had been sleeping together the +night previous, and especially considering the affectionate arm I had +found thrown over me upon waking in the morning, I thought this +indifference of his very strange. But savages are strange beings; at +times you do not know exactly how to take them. At first they are +overawing; their calm self-collectedness of simplicity seems a +Socratic wisdom. I had noticed also that Queequeg never consorted at +all, or but very little, with the other seamen in the inn. He made +no advances whatever; appeared to have no desire to enlarge the +circle of his acquaintances. All this struck me as mighty singular; +yet, upon second thoughts, there was something almost sublime in it. +Here was a man some twenty thousand miles from home, by the way of +Cape Horn, that is--which was the only way he could get there--thrown +among people as strange to him as though he were in the planet +Jupiter; and yet he seemed entirely at his ease; preserving the +utmost serenity; content with his own companionship; always equal to +himself. Surely this was a touch of fine philosophy; though no doubt +he had never heard there was such a thing as that. But, perhaps, to +be true philosophers, we mortals should not be conscious of so living +or so striving. So soon as I hear that such or such a man gives +himself out for a philosopher, I conclude that, like the dyspeptic +old woman, he must have "broken his digester." + +As I sat there in that now lonely room; the fire burning low, in that +mild stage when, after its first intensity has warmed the air, it +then only glows to be looked at; the evening shades and phantoms +gathering round the casements, and peering in upon us silent, +solitary twain; the storm booming without in solemn swells; I began +to be sensible of strange feelings. I felt a melting in me. No more +my splintered heart and maddened hand were turned against the wolfish +world. This soothing savage had redeemed it. There he sat, his very +indifference speaking a nature in which there lurked no civilized +hypocrisies and bland deceits. Wild he was; a very sight of sights +to see; yet I began to feel myself mysteriously drawn towards him. +And those same things that would have repelled most others, they were +the very magnets that thus drew me. I'll try a pagan friend, thought +I, since Christian kindness has proved but hollow courtesy. I drew +my bench near him, and made some friendly signs and hints, doing my +best to talk with him meanwhile. At first he little noticed these +advances; but presently, upon my referring to his last night's +hospitalities, he made out to ask me whether we were again to be +bedfellows. I told him yes; whereat I thought he looked pleased, +perhaps a little complimented. + +We then turned over the book together, and I endeavored to explain to +him the purpose of the printing, and the meaning of the few pictures +that were in it. Thus I soon engaged his interest; and from that we +went to jabbering the best we could about the various outer sights to +be seen in this famous town. Soon I proposed a social smoke; and, +producing his pouch and tomahawk, he quietly offered me a puff. And +then we sat exchanging puffs from that wild pipe of his, and keeping +it regularly passing between us. + +If there yet lurked any ice of indifference towards me in the Pagan's +breast, this pleasant, genial smoke we had, soon thawed it out, and +left us cronies. He seemed to take to me quite as naturally and +unbiddenly as I to him; and when our smoke was over, he pressed his +forehead against mine, clasped me round the waist, and said that +henceforth we were married; meaning, in his country's phrase, that we +were bosom friends; he would gladly die for me, if need should be. +In a countryman, this sudden flame of friendship would have seemed +far too premature, a thing to be much distrusted; but in this simple +savage those old rules would not apply. + +After supper, and another social chat and smoke, we went to our room +together. He made me a present of his embalmed head; took out his +enormous tobacco wallet, and groping under the tobacco, drew out some +thirty dollars in silver; then spreading them on the table, and +mechanically dividing them into two equal portions, pushed one of +them towards me, and said it was mine. I was going to remonstrate; +but he silenced me by pouring them into my trowsers' pockets. I let +them stay. He then went about his evening prayers, took out his +idol, and removed the paper fireboard. By certain signs and +symptoms, I thought he seemed anxious for me to join him; but well +knowing what was to follow, I deliberated a moment whether, in case +he invited me, I would comply or otherwise. + +I was a good Christian; born and bred in the bosom of the infallible +Presbyterian Church. How then could I unite with this wild idolator +in worshipping his piece of wood? But what is worship? thought I. +Do you suppose now, Ishmael, that the magnanimous God of heaven and +earth--pagans and all included--can possibly be jealous of an +insignificant bit of black wood? Impossible! But what is +worship?--to do the will of God--THAT is worship. And what is the +will of God?--to do to my fellow man what I would have my fellow man +to do to me--THAT is the will of God. Now, Queequeg is my fellow +man. And what do I wish that this Queequeg would do to me? Why, +unite with me in my particular Presbyterian form of worship. +Consequently, I must then unite with him in his; ergo, I must turn +idolator. So I kindled the shavings; helped prop up the innocent +little idol; offered him burnt biscuit with Queequeg; salamed before +him twice or thrice; kissed his nose; and that done, we undressed and +went to bed, at peace with our own consciences and all the world. +But we did not go to sleep without some little chat. + +How it is I know not; but there is no place like a bed for +confidential disclosures between friends. Man and wife, they say, +there open the very bottom of their souls to each other; and some old +couples often lie and chat over old times till nearly morning. Thus, +then, in our hearts' honeymoon, lay I and Queequeg--a cosy, loving +pair. + + + +CHAPTER 11 + +Nightgown. + + +We had lain thus in bed, chatting and napping at short intervals, and +Queequeg now and then affectionately throwing his brown tattooed legs +over mine, and then drawing them back; so entirely sociable and free +and easy were we; when, at last, by reason of our confabulations, +what little nappishness remained in us altogether departed, and we +felt like getting up again, though day-break was yet some way down +the future. + +Yes, we became very wakeful; so much so that our recumbent position +began to grow wearisome, and by little and little we found ourselves +sitting up; the clothes well tucked around us, leaning against the +head-board with our four knees drawn up close together, and our two +noses bending over them, as if our kneepans were warming-pans. We +felt very nice and snug, the more so since it was so chilly out of +doors; indeed out of bed-clothes too, seeing that there was no fire +in the room. The more so, I say, because truly to enjoy bodily +warmth, some small part of you must be cold, for there is no quality +in this world that is not what it is merely by contrast. Nothing +exists in itself. If you flatter yourself that you are all over +comfortable, and have been so a long time, then you cannot be said to +be comfortable any more. But if, like Queequeg and me in the bed, +the tip of your nose or the crown of your head be slightly chilled, +why then, indeed, in the general consciousness you feel most +delightfully and unmistakably warm. For this reason a sleeping +apartment should never be furnished with a fire, which is one of the +luxurious discomforts of the rich. For the height of this sort of +deliciousness is to have nothing but the blanket between you and +your snugness and the cold of the outer air. Then there you lie like +the one warm spark in the heart of an arctic crystal. + +We had been sitting in this crouching manner for some time, when all +at once I thought I would open my eyes; for when between sheets, +whether by day or by night, and whether asleep or awake, I have a way +of always keeping my eyes shut, in order the more to concentrate the +snugness of being in bed. Because no man can ever feel his own +identity aright except his eyes be closed; as if darkness were +indeed the proper element of our essences, though light be more +congenial to our clayey part. Upon opening my eyes then, and coming +out of my own pleasant and self-created darkness into the imposed and +coarse outer gloom of the unilluminated twelve-o'clock-at-night, I +experienced a disagreeable revulsion. Nor did I at all object to the +hint from Queequeg that perhaps it were best to strike a light, +seeing that we were so wide awake; and besides he felt a strong +desire to have a few quiet puffs from his Tomahawk. Be it said, that +though I had felt such a strong repugnance to his smoking in the bed +the night before, yet see how elastic our stiff prejudices grow when +love once comes to bend them. For now I liked nothing better than +to have Queequeg smoking by me, even in bed, because he seemed to be +full of such serene household joy then. I no more felt unduly +concerned for the landlord's policy of insurance. I was only alive +to the condensed confidential comfortableness of sharing a pipe and a +blanket with a real friend. With our shaggy jackets drawn about our +shoulders, we now passed the Tomahawk from one to the other, till +slowly there grew over us a blue hanging tester of smoke, illuminated +by the flame of the new-lit lamp. + +Whether it was that this undulating tester rolled the savage away to +far distant scenes, I know not, but he now spoke of his native +island; and, eager to hear his history, I begged him to go on and +tell it. He gladly complied. Though at the time I but ill +comprehended not a few of his words, yet subsequent disclosures, when +I had become more familiar with his broken phraseology, now enable me +to present the whole story such as it may prove in the mere skeleton +I give. + + + +CHAPTER 12 + +Biographical. + + +Queequeg was a native of Rokovoko, an island far away to the West +and South. It is not down in any map; true places never are. + +When a new-hatched savage running wild about his native woodlands in +a grass clout, followed by the nibbling goats, as if he were a green +sapling; even then, in Queequeg's ambitious soul, lurked a strong +desire to see something more of Christendom than a specimen whaler or +two. His father was a High Chief, a King; his uncle a High Priest; +and on the maternal side he boasted aunts who were the wives of +unconquerable warriors. There was excellent blood in his +veins--royal stuff; though sadly vitiated, I fear, by the cannibal +propensity he nourished in his untutored youth. + +A Sag Harbor ship visited his father's bay, and Queequeg sought a +passage to Christian lands. But the ship, having her full complement +of seamen, spurned his suit; and not all the King his father's +influence could prevail. But Queequeg vowed a vow. Alone in his +canoe, he paddled off to a distant strait, which he knew the ship +must pass through when she quitted the island. On one side was a +coral reef; on the other a low tongue of land, covered with mangrove +thickets that grew out into the water. Hiding his canoe, still +afloat, among these thickets, with its prow seaward, he sat down in +the stern, paddle low in hand; and when the ship was gliding by, like +a flash he darted out; gained her side; with one backward dash of his +foot capsized and sank his canoe; climbed up the chains; and throwing +himself at full length upon the deck, grappled a ring-bolt there, and +swore not to let it go, though hacked in pieces. + +In vain the captain threatened to throw him overboard; suspended a +cutlass over his naked wrists; Queequeg was the son of a King, and +Queequeg budged not. Struck by his desperate dauntlessness, and his +wild desire to visit Christendom, the captain at last relented, and +told him he might make himself at home. But this fine young +savage--this sea Prince of Wales, never saw the Captain's cabin. +They put him down among the sailors, and made a whaleman of him. But +like Czar Peter content to toil in the shipyards of foreign cities, +Queequeg disdained no seeming ignominy, if thereby he might happily +gain the power of enlightening his untutored countrymen. For at +bottom--so he told me--he was actuated by a profound desire to learn +among the Christians, the arts whereby to make his people still +happier than they were; and more than that, still better than they +were. But, alas! the practices of whalemen soon convinced him that +even Christians could be both miserable and wicked; infinitely more +so, than all his father's heathens. Arrived at last in old Sag +Harbor; and seeing what the sailors did there; and then going on to +Nantucket, and seeing how they spent their wages in that place also, +poor Queequeg gave it up for lost. Thought he, it's a wicked world +in all meridians; I'll die a pagan. + +And thus an old idolator at heart, he yet lived among these +Christians, wore their clothes, and tried to talk their gibberish. +Hence the queer ways about him, though now some time from home. + +By hints, I asked him whether he did not propose going back, and +having a coronation; since he might now consider his father dead and +gone, he being very old and feeble at the last accounts. He answered +no, not yet; and added that he was fearful Christianity, or rather +Christians, had unfitted him for ascending the pure and undefiled +throne of thirty pagan Kings before him. But by and by, he said, he +would return,--as soon as he felt himself baptized again. For the +nonce, however, he proposed to sail about, and sow his wild oats in +all four oceans. They had made a harpooneer of him, and that barbed +iron was in lieu of a sceptre now. + +I asked him what might be his immediate purpose, touching his future +movements. He answered, to go to sea again, in his old vocation. +Upon this, I told him that whaling was my own design, and informed +him of my intention to sail out of Nantucket, as being the most +promising port for an adventurous whaleman to embark from. He at +once resolved to accompany me to that island, ship aboard the same +vessel, get into the same watch, the same boat, the same mess with +me, in short to share my every hap; with both my hands in his, boldly +dip into the Potluck of both worlds. To all this I joyously +assented; for besides the affection I now felt for Queequeg, he was +an experienced harpooneer, and as such, could not fail to be of great +usefulness to one, who, like me, was wholly ignorant of the mysteries +of whaling, though well acquainted with the sea, as known to merchant +seamen. + +His story being ended with his pipe's last dying puff, Queequeg +embraced me, pressed his forehead against mine, and blowing out the +light, we rolled over from each other, this way and that, and very +soon were sleeping. + + +CHAPTER 13 + +Wheelbarrow. + + +Next morning, Monday, after disposing of the embalmed head to a +barber, for a block, I settled my own and comrade's bill; using, +however, my comrade's money. The grinning landlord, as well as the +boarders, seemed amazingly tickled at the sudden friendship which had +sprung up between me and Queequeg--especially as Peter Coffin's cock +and bull stories about him had previously so much alarmed me +concerning the very person whom I now companied with. + +We borrowed a wheelbarrow, and embarking our things, including my own +poor carpet-bag, and Queequeg's canvas sack and hammock, away we went +down to "the Moss," the little Nantucket packet schooner moored at +the wharf. As we were going along the people stared; not at Queequeg +so much--for they were used to seeing cannibals like him in their +streets,--but at seeing him and me upon such confidential terms. But +we heeded them not, going along wheeling the barrow by turns, and +Queequeg now and then stopping to adjust the sheath on his harpoon +barbs. I asked him why he carried such a troublesome thing with him +ashore, and whether all whaling ships did not find their own +harpoons. To this, in substance, he replied, that though what I +hinted was true enough, yet he had a particular affection for his own +harpoon, because it was of assured stuff, well tried in many a mortal +combat, and deeply intimate with the hearts of whales. In short, +like many inland reapers and mowers, who go into the farmers' meadows +armed with their own scythes--though in no wise obliged to furnish +them--even so, Queequeg, for his own private reasons, preferred his +own harpoon. + +Shifting the barrow from my hand to his, he told me a funny story +about the first wheelbarrow he had ever seen. It was in Sag Harbor. +The owners of his ship, it seems, had lent him one, in which to carry +his heavy chest to his boarding house. Not to seem ignorant about +the thing--though in truth he was entirely so, concerning the precise +way in which to manage the barrow--Queequeg puts his chest upon it; +lashes it fast; and then shoulders the barrow and marches up the +wharf. "Why," said I, "Queequeg, you might have known better than +that, one would think. Didn't the people laugh?" + +Upon this, he told me another story. The people of his island of +Rokovoko, it seems, at their wedding feasts express the fragrant +water of young cocoanuts into a large stained calabash like a +punchbowl; and this punchbowl always forms the great central ornament +on the braided mat where the feast is held. Now a certain grand +merchant ship once touched at Rokovoko, and its commander--from all +accounts, a very stately punctilious gentleman, at least for a sea +captain--this commander was invited to the wedding feast of +Queequeg's sister, a pretty young princess just turned of ten. Well; +when all the wedding guests were assembled at the bride's bamboo +cottage, this Captain marches in, and being assigned the post of +honour, placed himself over against the punchbowl, and between the +High Priest and his majesty the King, Queequeg's father. Grace being +said,--for those people have their grace as well as we--though +Queequeg told me that unlike us, who at such times look downwards to +our platters, they, on the contrary, copying the ducks, glance +upwards to the great Giver of all feasts--Grace, I say, being said, +the High Priest opens the banquet by the immemorial ceremony of the +island; that is, dipping his consecrated and consecrating fingers +into the bowl before the blessed beverage circulates. Seeing himself +placed next the Priest, and noting the ceremony, and thinking +himself--being Captain of a ship--as having plain precedence over a +mere island King, especially in the King's own house--the Captain +coolly proceeds to wash his hands in the punchbowl;--taking it I +suppose for a huge finger-glass. "Now," said Queequeg, "what you +tink now?--Didn't our people laugh?" + +At last, passage paid, and luggage safe, we stood on board the +schooner. Hoisting sail, it glided down the Acushnet river. On one +side, New Bedford rose in terraces of streets, their ice-covered +trees all glittering in the clear, cold air. Huge hills and +mountains of casks on casks were piled upon her wharves, and side by +side the world-wandering whale ships lay silent and safely moored at +last; while from others came a sound of carpenters and coopers, with +blended noises of fires and forges to melt the pitch, all betokening +that new cruises were on the start; that one most perilous and long +voyage ended, only begins a second; and a second ended, only begins a +third, and so on, for ever and for aye. Such is the endlessness, +yea, the intolerableness of all earthly effort. + +Gaining the more open water, the bracing breeze waxed fresh; the +little Moss tossed the quick foam from her bows, as a young colt his +snortings. How I snuffed that Tartar air!--how I spurned that +turnpike earth!--that common highway all over dented with the marks +of slavish heels and hoofs; and turned me to admire the magnanimity +of the sea which will permit no records. + +At the same foam-fountain, Queequeg seemed to drink and reel with me. +His dusky nostrils swelled apart; he showed his filed and pointed +teeth. On, on we flew; and our offing gained, the Moss did homage to +the blast; ducked and dived her bows as a slave before the Sultan. +Sideways leaning, we sideways darted; every ropeyarn tingling like a +wire; the two tall masts buckling like Indian canes in land +tornadoes. So full of this reeling scene were we, as we stood by the +plunging bowsprit, that for some time we did not notice the jeering +glances of the passengers, a lubber-like assembly, who marvelled that +two fellow beings should be so companionable; as though a white man +were anything more dignified than a whitewashed negro. But there +were some boobies and bumpkins there, who, by their intense +greenness, must have come from the heart and centre of all verdure. +Queequeg caught one of these young saplings mimicking him behind his +back. I thought the bumpkin's hour of doom was come. Dropping his +harpoon, the brawny savage caught him in his arms, and by an almost +miraculous dexterity and strength, sent him high up bodily into the +air; then slightly tapping his stern in mid-somerset, the fellow +landed with bursting lungs upon his feet, while Queequeg, turning his +back upon him, lighted his tomahawk pipe and passed it to me for a +puff. + +"Capting! Capting! yelled the bumpkin, running towards that officer; +"Capting, Capting, here's the devil." + +"Hallo, YOU sir," cried the Captain, a gaunt rib of the sea, stalking +up to Queequeg, "what in thunder do you mean by that? Don't you know +you might have killed that chap?" + +"What him say?" said Queequeg, as he mildly turned to me. + +"He say," said I, "that you came near kill-e that man there," +pointing to the still shivering greenhorn. + +"Kill-e," cried Queequeg, twisting his tattooed face into an +unearthly expression of disdain, "ah! him bevy small-e fish-e; +Queequeg no kill-e so small-e fish-e; Queequeg kill-e big whale!" + +"Look you," roared the Captain, "I'll kill-e YOU, you cannibal, if +you try any more of your tricks aboard here; so mind your eye." + +But it so happened just then, that it was high time for the Captain +to mind his own eye. The prodigious strain upon the main-sail had +parted the weather-sheet, and the tremendous boom was now flying from +side to side, completely sweeping the entire after part of the deck. +The poor fellow whom Queequeg had handled so roughly, was swept +overboard; all hands were in a panic; and to attempt snatching at the +boom to stay it, seemed madness. It flew from right to left, and +back again, almost in one ticking of a watch, and every instant +seemed on the point of snapping into splinters. Nothing was done, +and nothing seemed capable of being done; those on deck rushed +towards the bows, and stood eyeing the boom as if it were the lower +jaw of an exasperated whale. In the midst of this consternation, +Queequeg dropped deftly to his knees, and crawling under the path of +the boom, whipped hold of a rope, secured one end to the bulwarks, +and then flinging the other like a lasso, caught it round the boom as +it swept over his head, and at the next jerk, the spar was that way +trapped, and all was safe. The schooner was run into the wind, and +while the hands were clearing away the stern boat, Queequeg, stripped +to the waist, darted from the side with a long living arc of a leap. +For three minutes or more he was seen swimming like a dog, throwing +his long arms straight out before him, and by turns revealing his +brawny shoulders through the freezing foam. I looked at the grand +and glorious fellow, but saw no one to be saved. The greenhorn had +gone down. Shooting himself perpendicularly from the water, +Queequeg, now took an instant's glance around him, and seeming to see +just how matters were, dived down and disappeared. A few minutes +more, and he rose again, one arm still striking out, and with the +other dragging a lifeless form. The boat soon picked them up. The +poor bumpkin was restored. All hands voted Queequeg a noble trump; +the captain begged his pardon. From that hour I clove to Queequeg +like a barnacle; yea, till poor Queequeg took his last long dive. + +Was there ever such unconsciousness? He did not seem to think that +he at all deserved a medal from the Humane and Magnanimous Societies. +He only asked for water--fresh water--something to wipe the brine +off; that done, he put on dry clothes, lighted his pipe, and leaning +against the bulwarks, and mildly eyeing those around him, seemed to +be saying to himself--"It's a mutual, joint-stock world, in all +meridians. We cannibals must help these Christians." + + + +CHAPTER 14 + +Nantucket. + + +Nothing more happened on the passage worthy the mentioning; so, after +a fine run, we safely arrived in Nantucket. + +Nantucket! Take out your map and look at it. See what a real corner +of the world it occupies; how it stands there, away off shore, more +lonely than the Eddystone lighthouse. Look at it--a mere hillock, +and elbow of sand; all beach, without a background. There is more +sand there than you would use in twenty years as a substitute for +blotting paper. Some gamesome wights will tell you that they have to +plant weeds there, they don't grow naturally; that they import Canada +thistles; that they have to send beyond seas for a spile to stop a +leak in an oil cask; that pieces of wood in Nantucket are carried +about like bits of the true cross in Rome; that people there plant +toadstools before their houses, to get under the shade in summer +time; that one blade of grass makes an oasis, three blades in a day's +walk a prairie; that they wear quicksand shoes, something like +Laplander snow-shoes; that they are so shut up, belted about, every +way inclosed, surrounded, and made an utter island of by the ocean, +that to their very chairs and tables small clams will sometimes be +found adhering, as to the backs of sea turtles. But these +extravaganzas only show that Nantucket is no Illinois. + +Look now at the wondrous traditional story of how this island was +settled by the red-men. Thus goes the legend. In olden times an +eagle swooped down upon the New England coast, and carried off an +infant Indian in his talons. With loud lament the parents saw their +child borne out of sight over the wide waters. They resolved to +follow in the same direction. Setting out in their canoes, after a +perilous passage they discovered the island, and there they found an +empty ivory casket,--the poor little Indian's skeleton. + +What wonder, then, that these Nantucketers, born on a beach, should +take to the sea for a livelihood! They first caught crabs and +quohogs in the sand; grown bolder, they waded out with nets for +mackerel; more experienced, they pushed off in boats and captured +cod; and at last, launching a navy of great ships on the sea, +explored this watery world; put an incessant belt of +circumnavigations round it; peeped in at Behring's Straits; and in +all seasons and all oceans declared everlasting war with the +mightiest animated mass that has survived the flood; most monstrous +and most mountainous! That Himmalehan, salt-sea Mastodon, clothed +with such portentousness of unconscious power, that his very panics +are more to be dreaded than his most fearless and malicious assaults! + +And thus have these naked Nantucketers, these sea hermits, issuing +from their ant-hill in the sea, overrun and conquered the watery +world like so many Alexanders; parcelling out among them the +Atlantic, Pacific, and Indian oceans, as the three pirate powers did +Poland. Let America add Mexico to Texas, and pile Cuba upon Canada; +let the English overswarm all India, and hang out their blazing +banner from the sun; two thirds of this terraqueous globe are the +Nantucketer's. For the sea is his; he owns it, as Emperors own +empires; other seamen having but a right of way through it. Merchant +ships are but extension bridges; armed ones but floating forts; even +pirates and privateers, though following the sea as highwaymen the +road, they but plunder other ships, other fragments of the land like +themselves, without seeking to draw their living from the bottomless +deep itself. The Nantucketer, he alone resides and riots on the sea; +he alone, in Bible language, goes down to it in ships; to and fro +ploughing it as his own special plantation. THERE is his home; THERE +lies his business, which a Noah's flood would not interrupt, though +it overwhelmed all the millions in China. He lives on the sea, as +prairie cocks in the prairie; he hides among the waves, he climbs +them as chamois hunters climb the Alps. For years he knows not the +land; so that when he comes to it at last, it smells like another +world, more strangely than the moon would to an Earthsman. With the +landless gull, that at sunset folds her wings and is rocked to sleep +between billows; so at nightfall, the Nantucketer, out of sight of +land, furls his sails, and lays him to his rest, while under his very +pillow rush herds of walruses and whales. + + + +CHAPTER 15 + +Chowder. + + +It was quite late in the evening when the little Moss came snugly to +anchor, and Queequeg and I went ashore; so we could attend to no +business that day, at least none but a supper and a bed. The +landlord of the Spouter-Inn had recommended us to his cousin Hosea +Hussey of the Try Pots, whom he asserted to be the proprietor of one +of the best kept hotels in all Nantucket, and moreover he had assured +us that Cousin Hosea, as he called him, was famous for his chowders. +In short, he plainly hinted that we could not possibly do better than +try pot-luck at the Try Pots. But the directions he had given us +about keeping a yellow warehouse on our starboard hand till we opened +a white church to the larboard, and then keeping that on the larboard +hand till we made a corner three points to the starboard, and that +done, then ask the first man we met where the place was: these +crooked directions of his very much puzzled us at first, especially +as, at the outset, Queequeg insisted that the yellow warehouse--our +first point of departure--must be left on the larboard hand, whereas +I had understood Peter Coffin to say it was on the starboard. +However, by dint of beating about a little in the dark, and now and +then knocking up a peaceable inhabitant to inquire the way, we at +last came to something which there was no mistaking. + +Two enormous wooden pots painted black, and suspended by asses' ears, +swung from the cross-trees of an old top-mast, planted in front of an +old doorway. The horns of the cross-trees were sawed off on the +other side, so that this old top-mast looked not a little like a +gallows. Perhaps I was over sensitive to such impressions at the +time, but I could not help staring at this gallows with a vague +misgiving. A sort of crick was in my neck as I gazed up to the two +remaining horns; yes, TWO of them, one for Queequeg, and one for me. +It's ominous, thinks I. A Coffin my Innkeeper upon landing in my +first whaling port; tombstones staring at me in the whalemen's +chapel; and here a gallows! and a pair of prodigious black pots too! +Are these last throwing out oblique hints touching Tophet? + +I was called from these reflections by the sight of a freckled woman +with yellow hair and a yellow gown, standing in the porch of the inn, +under a dull red lamp swinging there, that looked much like an +injured eye, and carrying on a brisk scolding with a man in a purple +woollen shirt. + +"Get along with ye," said she to the man, "or I'll be combing ye!" + +"Come on, Queequeg," said I, "all right. There's Mrs. Hussey." + +And so it turned out; Mr. Hosea Hussey being from home, but leaving +Mrs. Hussey entirely competent to attend to all his affairs. Upon +making known our desires for a supper and a bed, Mrs. Hussey, +postponing further scolding for the present, ushered us into a little +room, and seating us at a table spread with the relics of a recently +concluded repast, turned round to us and said--"Clam or Cod?" + +"What's that about Cods, ma'am?" said I, with much politeness. + +"Clam or Cod?" she repeated. + +"A clam for supper? a cold clam; is THAT what you mean, Mrs. Hussey?" +says I, "but that's a rather cold and clammy reception in the winter +time, ain't it, Mrs. Hussey?" + +But being in a great hurry to resume scolding the man in the purple +Shirt, who was waiting for it in the entry, and seeming to hear +nothing but the word "clam," Mrs. Hussey hurried towards an open door +leading to the kitchen, and bawling out "clam for two," disappeared. + +"Queequeg," said I, "do you think that we can make out a supper for +us both on one clam?" + +However, a warm savory steam from the kitchen served to belie the +apparently cheerless prospect before us. But when that smoking +chowder came in, the mystery was delightfully explained. Oh, sweet +friends! hearken to me. It was made of small juicy clams, scarcely +bigger than hazel nuts, mixed with pounded ship biscuit, and salted +pork cut up into little flakes; the whole enriched with butter, and +plentifully seasoned with pepper and salt. Our appetites being +sharpened by the frosty voyage, and in particular, Queequeg seeing +his favourite fishing food before him, and the chowder being +surpassingly excellent, we despatched it with great expedition: when +leaning back a moment and bethinking me of Mrs. Hussey's clam and cod +announcement, I thought I would try a little experiment. Stepping to +the kitchen door, I uttered the word "cod" with great emphasis, and +resumed my seat. In a few moments the savoury steam came forth +again, but with a different flavor, and in good time a fine +cod-chowder was placed before us. + +We resumed business; and while plying our spoons in the bowl, thinks +I to myself, I wonder now if this here has any effect on the head? +What's that stultifying saying about chowder-headed people? "But +look, Queequeg, ain't that a live eel in your bowl? Where's your +harpoon?" + +Fishiest of all fishy places was the Try Pots, which well deserved +its name; for the pots there were always boiling chowders. Chowder +for breakfast, and chowder for dinner, and chowder for supper, till +you began to look for fish-bones coming through your clothes. The +area before the house was paved with clam-shells. Mrs. Hussey wore a +polished necklace of codfish vertebra; and Hosea Hussey had his +account books bound in superior old shark-skin. There was a fishy +flavor to the milk, too, which I could not at all account for, till +one morning happening to take a stroll along the beach among some +fishermen's boats, I saw Hosea's brindled cow feeding on fish +remnants, and marching along the sand with each foot in a cod's +decapitated head, looking very slip-shod, I assure ye. + +Supper concluded, we received a lamp, and directions from Mrs. Hussey +concerning the nearest way to bed; but, as Queequeg was about to +precede me up the stairs, the lady reached forth her arm, and +demanded his harpoon; she allowed no harpoon in her chambers. "Why +not? said I; "every true whaleman sleeps with his harpoon--but why +not?" "Because it's dangerous," says she. "Ever since young Stiggs +coming from that unfort'nt v'y'ge of his, when he was gone four years +and a half, with only three barrels of ILE, was found dead in my +first floor back, with his harpoon in his side; ever since then I +allow no boarders to take sich dangerous weepons in their rooms at +night. So, Mr. Queequeg" (for she had learned his name), "I will +just take this here iron, and keep it for you till morning. But the +chowder; clam or cod to-morrow for breakfast, men?" + +"Both," says I; "and let's have a couple of smoked herring by way of +variety." + + + +CHAPTER 16 + +The Ship. + + +In bed we concocted our plans for the morrow. But to my surprise and +no small concern, Queequeg now gave me to understand, that he had +been diligently consulting Yojo--the name of his black little +god--and Yojo had told him two or three times over, and strongly +insisted upon it everyway, that instead of our going together among +the whaling-fleet in harbor, and in concert selecting our craft; +instead of this, I say, Yojo earnestly enjoined that the selection of +the ship should rest wholly with me, inasmuch as Yojo purposed +befriending us; and, in order to do so, had already pitched upon a +vessel, which, if left to myself, I, Ishmael, should infallibly light +upon, for all the world as though it had turned out by chance; and in +that vessel I must immediately ship myself, for the present +irrespective of Queequeg. + +I have forgotten to mention that, in many things, Queequeg placed +great confidence in the excellence of Yojo's judgment and surprising +forecast of things; and cherished Yojo with considerable esteem, as a +rather good sort of god, who perhaps meant well enough upon the +whole, but in all cases did not succeed in his benevolent designs. + +Now, this plan of Queequeg's, or rather Yojo's, touching the +selection of our craft; I did not like that plan at all. I had not a +little relied upon Queequeg's sagacity to point out the whaler best +fitted to carry us and our fortunes securely. But as all my +remonstrances produced no effect upon Queequeg, I was obliged to +acquiesce; and accordingly prepared to set about this business with a +determined rushing sort of energy and vigor, that should quickly +settle that trifling little affair. Next morning early, leaving +Queequeg shut up with Yojo in our little bedroom--for it seemed that +it was some sort of Lent or Ramadan, or day of fasting, humiliation, +and prayer with Queequeg and Yojo that day; HOW it was I never could +find out, for, though I applied myself to it several times, I never +could master his liturgies and XXXIX Articles--leaving Queequeg, +then, fasting on his tomahawk pipe, and Yojo warming himself at his +sacrificial fire of shavings, I sallied out among the shipping. +After much prolonged sauntering and many random inquiries, I learnt +that there were three ships up for three-years' voyages--The +Devil-dam, the Tit-bit, and the Pequod. DEVIL-DAM, I do not know +the origin of; TIT-BIT is obvious; PEQUOD, you will no doubt +remember, was the name of a celebrated tribe of Massachusetts +Indians; now extinct as the ancient Medes. I peered and pryed about +the Devil-dam; from her, hopped over to the Tit-bit; and finally, +going on board the Pequod, looked around her for a moment, and then +decided that this was the very ship for us. + +You may have seen many a quaint craft in your day, for aught I +know;--square-toed luggers; mountainous Japanese junks; butter-box +galliots, and what not; but take my word for it, you never saw such a +rare old craft as this same rare old Pequod. She was a ship of the +old school, rather small if anything; with an old-fashioned +claw-footed look about her. Long seasoned and weather-stained in the +typhoons and calms of all four oceans, her old hull's complexion was +darkened like a French grenadier's, who has alike fought in Egypt and +Siberia. Her venerable bows looked bearded. Her masts--cut +somewhere on the coast of Japan, where her original ones were lost +overboard in a gale--her masts stood stiffly up like the spines of +the three old kings of Cologne. Her ancient decks were worn and +wrinkled, like the pilgrim-worshipped flag-stone in Canterbury +Cathedral where Becket bled. But to all these her old antiquities, +were added new and marvellous features, pertaining to the wild +business that for more than half a century she had followed. Old +Captain Peleg, many years her chief-mate, before he commanded another +vessel of his own, and now a retired seaman, and one of the principal +owners of the Pequod,--this old Peleg, during the term of his +chief-mateship, had built upon her original grotesqueness, and inlaid +it, all over, with a quaintness both of material and device, +unmatched by anything except it be Thorkill-Hake's carved buckler or +bedstead. She was apparelled like any barbaric Ethiopian emperor, +his neck heavy with pendants of polished ivory. She was a thing of +trophies. A cannibal of a craft, tricking herself forth in the +chased bones of her enemies. All round, her unpanelled, open +bulwarks were garnished like one continuous jaw, with the long sharp +teeth of the sperm whale, inserted there for pins, to fasten her old +hempen thews and tendons to. Those thews ran not through base blocks +of land wood, but deftly travelled over sheaves of sea-ivory. +Scorning a turnstile wheel at her reverend helm, she sported there a +tiller; and that tiller was in one mass, curiously carved from the +long narrow lower jaw of her hereditary foe. The helmsman who +steered by that tiller in a tempest, felt like the Tartar, when he +holds back his fiery steed by clutching its jaw. A noble craft, but +somehow a most melancholy! All noble things are touched with that. + +Now when I looked about the quarter-deck, for some one having +authority, in order to propose myself as a candidate for the voyage, +at first I saw nobody; but I could not well overlook a strange sort +of tent, or rather wigwam, pitched a little behind the main-mast. It +seemed only a temporary erection used in port. It was of a conical +shape, some ten feet high; consisting of the long, huge slabs of +limber black bone taken from the middle and highest part of the jaws +of the right-whale. Planted with their broad ends on the deck, a +circle of these slabs laced together, mutually sloped towards each +other, and at the apex united in a tufted point, where the loose +hairy fibres waved to and fro like the top-knot on some old +Pottowottamie Sachem's head. A triangular opening faced towards the +bows of the ship, so that the insider commanded a complete view +forward. + +And half concealed in this queer tenement, I at length found one who +by his aspect seemed to have authority; and who, it being noon, and +the ship's work suspended, was now enjoying respite from the burden +of command. He was seated on an old-fashioned oaken chair, wriggling +all over with curious carving; and the bottom of which was formed of +a stout interlacing of the same elastic stuff of which the wigwam was +constructed. + +There was nothing so very particular, perhaps, about the appearance +of the elderly man I saw; he was brown and brawny, like most old +seamen, and heavily rolled up in blue pilot-cloth, cut in the Quaker +style; only there was a fine and almost microscopic net-work of the +minutest wrinkles interlacing round his eyes, which must have arisen +from his continual sailings in many hard gales, and always looking to +windward;--for this causes the muscles about the eyes to become +pursed together. Such eye-wrinkles are very effectual in a scowl. + +"Is this the Captain of the Pequod?" said I, advancing to the door of +the tent. + +"Supposing it be the captain of the Pequod, what dost thou want of +him?" he demanded. + +"I was thinking of shipping." + +"Thou wast, wast thou? I see thou art no Nantucketer--ever been in +a stove boat?" + +"No, Sir, I never have." + +"Dost know nothing at all about whaling, I dare say--eh? + +"Nothing, Sir; but I have no doubt I shall soon learn. I've been +several voyages in the merchant service, and I think that--" + +"Merchant service be damned. Talk not that lingo to me. Dost see +that leg?--I'll take that leg away from thy stern, if ever thou +talkest of the marchant service to me again. Marchant service +indeed! I suppose now ye feel considerable proud of having served in +those marchant ships. But flukes! man, what makes thee want to go a +whaling, eh?--it looks a little suspicious, don't it, eh?--Hast not +been a pirate, hast thou?--Didst not rob thy last Captain, didst +thou?--Dost not think of murdering the officers when thou gettest to +sea?" + +I protested my innocence of these things. I saw that under the mask +of these half humorous innuendoes, this old seaman, as an insulated +Quakerish Nantucketer, was full of his insular prejudices, and rather +distrustful of all aliens, unless they hailed from Cape Cod or the +Vineyard. + +"But what takes thee a-whaling? I want to know that before I think +of shipping ye." + +"Well, sir, I want to see what whaling is. I want to see the world." + +"Want to see what whaling is, eh? Have ye clapped eye on Captain +Ahab?" + +"Who is Captain Ahab, sir?" + +"Aye, aye, I thought so. Captain Ahab is the Captain of this ship." + +"I am mistaken then. I thought I was speaking to the Captain +himself." + +"Thou art speaking to Captain Peleg--that's who ye are speaking to, +young man. It belongs to me and Captain Bildad to see the Pequod +fitted out for the voyage, and supplied with all her needs, including +crew. We are part owners and agents. But as I was going to say, if +thou wantest to know what whaling is, as thou tellest ye do, I can +put ye in a way of finding it out before ye bind yourself to it, past +backing out. Clap eye on Captain Ahab, young man, and thou wilt find +that he has only one leg." + +"What do you mean, sir? Was the other one lost by a whale?" + +"Lost by a whale! Young man, come nearer to me: it was devoured, +chewed up, crunched by the monstrousest parmacetty that ever chipped +a boat!--ah, ah!" + +I was a little alarmed by his energy, perhaps also a little touched +at the hearty grief in his concluding exclamation, but said as calmly +as I could, "What you say is no doubt true enough, sir; but how could +I know there was any peculiar ferocity in that particular whale, +though indeed I might have inferred as much from the simple fact of +the accident." + +"Look ye now, young man, thy lungs are a sort of soft, d'ye see; thou +dost not talk shark a bit. SURE, ye've been to sea before now; sure +of that?" + +"Sir," said I, "I thought I told you that I had been four voyages in +the merchant--" + +"Hard down out of that! Mind what I said about the marchant +service--don't aggravate me--I won't have it. But let us understand +each other. I have given thee a hint about what whaling is; do ye +yet feel inclined for it?" + +"I do, sir." + +"Very good. Now, art thou the man to pitch a harpoon down a live +whale's throat, and then jump after it? Answer, quick!" + +"I am, sir, if it should be positively indispensable to do so; not to +be got rid of, that is; which I don't take to be the fact." + +"Good again. Now then, thou not only wantest to go a-whaling, to +find out by experience what whaling is, but ye also want to go in +order to see the world? Was not that what ye said? I thought so. +Well then, just step forward there, and take a peep over the +weather-bow, and then back to me and tell me what ye see there." + +For a moment I stood a little puzzled by this curious request, not +knowing exactly how to take it, whether humorously or in earnest. +But concentrating all his crow's feet into one scowl, Captain Peleg +started me on the errand. + +Going forward and glancing over the weather bow, I perceived that the +ship swinging to her anchor with the flood-tide, was now obliquely +pointing towards the open ocean. The prospect was unlimited, but +exceedingly monotonous and forbidding; not the slightest variety that +I could see. + +"Well, what's the report?" said Peleg when I came back; "what did ye +see?" + +"Not much," I replied--"nothing but water; considerable horizon +though, and there's a squall coming up, I think." + +"Well, what does thou think then of seeing the world? Do ye wish to +go round Cape Horn to see any more of it, eh? Can't ye see the world +where you stand?" + +I was a little staggered, but go a-whaling I must, and I would; and +the Pequod was as good a ship as any--I thought the best--and all +this I now repeated to Peleg. Seeing me so determined, he expressed +his willingness to ship me. + +"And thou mayest as well sign the papers right off," he added--"come +along with ye." And so saying, he led the way below deck into the +cabin. + +Seated on the transom was what seemed to me a most uncommon and +surprising figure. It turned out to be Captain Bildad, who along +with Captain Peleg was one of the largest owners of the vessel; the +other shares, as is sometimes the case in these ports, being held by +a crowd of old annuitants; widows, fatherless children, and chancery +wards; each owning about the value of a timber head, or a foot of +plank, or a nail or two in the ship. People in Nantucket invest +their money in whaling vessels, the same way that you do yours in +approved state stocks bringing in good interest. + +Now, Bildad, like Peleg, and indeed many other Nantucketers, was a +Quaker, the island having been originally settled by that sect; and +to this day its inhabitants in general retain in an uncommon measure +the peculiarities of the Quaker, only variously and anomalously +modified by things altogether alien and heterogeneous. For some of +these same Quakers are the most sanguinary of all sailors and +whale-hunters. They are fighting Quakers; they are Quakers with a +vengeance. + +So that there are instances among them of men, who, named with +Scripture names--a singularly common fashion on the island--and in +childhood naturally imbibing the stately dramatic thee and thou of +the Quaker idiom; still, from the audacious, daring, and boundless +adventure of their subsequent lives, strangely blend with these +unoutgrown peculiarities, a thousand bold dashes of character, not +unworthy a Scandinavian sea-king, or a poetical Pagan Roman. And +when these things unite in a man of greatly superior natural force, +with a globular brain and a ponderous heart; who has also by the +stillness and seclusion of many long night-watches in the remotest +waters, and beneath constellations never seen here at the north, been +led to think untraditionally and independently; receiving all +nature's sweet or savage impressions fresh from her own virgin +voluntary and confiding breast, and thereby chiefly, but with some +help from accidental advantages, to learn a bold and nervous lofty +language--that man makes one in a whole nation's census--a mighty +pageant creature, formed for noble tragedies. Nor will it at all +detract from him, dramatically regarded, if either by birth or other +circumstances, he have what seems a half wilful overruling morbidness +at the bottom of his nature. For all men tragically great are made +so through a certain morbidness. Be sure of this, O young ambition, +all mortal greatness is but disease. But, as yet we have not to do +with such an one, but with quite another; and still a man, who, if +indeed peculiar, it only results again from another phase of the +Quaker, modified by individual circumstances. + +Like Captain Peleg, Captain Bildad was a well-to-do, retired +whaleman. But unlike Captain Peleg--who cared not a rush for what +are called serious things, and indeed deemed those self-same serious +things the veriest of all trifles--Captain Bildad had not only been +originally educated according to the strictest sect of Nantucket +Quakerism, but all his subsequent ocean life, and the sight of many +unclad, lovely island creatures, round the Horn--all that had not +moved this native born Quaker one single jot, had not so much as +altered one angle of his vest. Still, for all this immutableness, +was there some lack of common consistency about worthy Captain +Peleg. Though refusing, from conscientious scruples, to bear arms +against land invaders, yet himself had illimitably invaded the +Atlantic and Pacific; and though a sworn foe to human bloodshed, yet +had he in his straight-bodied coat, spilled tuns upon tuns of +leviathan gore. How now in the contemplative evening of his days, +the pious Bildad reconciled these things in the reminiscence, I do +not know; but it did not seem to concern him much, and very probably +he had long since come to the sage and sensible conclusion that a +man's religion is one thing, and this practical world quite another. +This world pays dividends. Rising from a little cabin-boy in short +clothes of the drabbest drab, to a harpooneer in a broad shad-bellied +waistcoat; from that becoming boat-header, chief-mate, and captain, +and finally a ship owner; Bildad, as I hinted before, had concluded +his adventurous career by wholly retiring from active life at the +goodly age of sixty, and dedicating his remaining days to the quiet +receiving of his well-earned income. + +Now, Bildad, I am sorry to say, had the reputation of being an +incorrigible old hunks, and in his sea-going days, a bitter, hard +task-master. They told me in Nantucket, though it certainly seems a +curious story, that when he sailed the old Categut whaleman, his +crew, upon arriving home, were mostly all carried ashore to the +hospital, sore exhausted and worn out. For a pious man, especially +for a Quaker, he was certainly rather hard-hearted, to say the +least. He never used to swear, though, at his men, they said; but +somehow he got an inordinate quantity of cruel, unmitigated hard work +out of them. When Bildad was a chief-mate, to have his drab-coloured +eye intently looking at you, made you feel completely nervous, till +you could clutch something--a hammer or a marling-spike, and go to +work like mad, at something or other, never mind what. Indolence and +idleness perished before him. His own person was the exact +embodiment of his utilitarian character. On his long, gaunt body, he +carried no spare flesh, no superfluous beard, his chin having a soft, +economical nap to it, like the worn nap of his broad-brimmed hat. + +Such, then, was the person that I saw seated on the transom when I +followed Captain Peleg down into the cabin. The space between the +decks was small; and there, bolt-upright, sat old Bildad, who always +sat so, and never leaned, and this to save his coat tails. His +broad-brim was placed beside him; his legs were stiffly crossed; his +drab vesture was buttoned up to his chin; and spectacles on nose, he +seemed absorbed in reading from a ponderous volume. + +"Bildad," cried Captain Peleg, "at it again, Bildad, eh? Ye have +been studying those Scriptures, now, for the last thirty years, to my +certain knowledge. How far ye got, Bildad?" + +As if long habituated to such profane talk from his old shipmate, +Bildad, without noticing his present irreverence, quietly looked up, +and seeing me, glanced again inquiringly towards Peleg. + +"He says he's our man, Bildad," said Peleg, "he wants to ship." + +"Dost thee?" said Bildad, in a hollow tone, and turning round to me. + +"I dost," said I unconsciously, he was so intense a Quaker. + +"What do ye think of him, Bildad?" said Peleg. + +"He'll do," said Bildad, eyeing me, and then went on spelling away at +his book in a mumbling tone quite audible. + +I thought him the queerest old Quaker I ever saw, especially as +Peleg, his friend and old shipmate, seemed such a blusterer. But I +said nothing, only looking round me sharply. Peleg now threw open a +chest, and drawing forth the ship's articles, placed pen and ink +before him, and seated himself at a little table. I began to think +it was high time to settle with myself at what terms I would be +willing to engage for the voyage. I was already aware that in the +whaling business they paid no wages; but all hands, including the +captain, received certain shares of the profits called lays, and that +these lays were proportioned to the degree of importance pertaining +to the respective duties of the ship's company. I was also aware +that being a green hand at whaling, my own lay would not be very +large; but considering that I was used to the sea, could steer a +ship, splice a rope, and all that, I made no doubt that from all I +had heard I should be offered at least the 275th lay--that is, the +275th part of the clear net proceeds of the voyage, whatever that +might eventually amount to. And though the 275th lay was what they +call a rather LONG LAY, yet it was better than nothing; and if we had +a lucky voyage, might pretty nearly pay for the clothing I would wear +out on it, not to speak of my three years' beef and board, for which +I would not have to pay one stiver. + +It might be thought that this was a poor way to accumulate a princely +fortune--and so it was, a very poor way indeed. But I am one of +those that never take on about princely fortunes, and am quite +content if the world is ready to board and lodge me, while I am +putting up at this grim sign of the Thunder Cloud. Upon the whole, I +thought that the 275th lay would be about the fair thing, but would not +have been surprised had I been offered the 200th, considering I was +of a broad-shouldered make. + +But one thing, nevertheless, that made me a little distrustful about +receiving a generous share of the profits was this: Ashore, I had +heard something of both Captain Peleg and his unaccountable old crony +Bildad; how that they being the principal proprietors of the Pequod, +therefore the other and more inconsiderable and scattered owners, +left nearly the whole management of the ship's affairs to these two. +And I did not know but what the stingy old Bildad might have a mighty +deal to say about shipping hands, especially as I now found him on +board the Pequod, quite at home there in the cabin, and reading his +Bible as if at his own fireside. Now while Peleg was vainly trying +to mend a pen with his jack-knife, old Bildad, to my no small +surprise, considering that he was such an interested party in these +proceedings; Bildad never heeded us, but went on mumbling to himself +out of his book, "LAY not up for yourselves treasures upon earth, +where moth--" + +"Well, Captain Bildad," interrupted Peleg, "what d'ye say, what lay +shall we give this young man?" + +"Thou knowest best," was the sepulchral reply, "the seven hundred and +seventy-seventh wouldn't be too much, would it?--'where moth and rust +do corrupt, but LAY--'" + +LAY, indeed, thought I, and such a lay! the seven hundred and +seventy-seventh! Well, old Bildad, you are determined that I, for +one, shall not LAY up many LAYS here below, where moth and rust do +corrupt. It was an exceedingly LONG LAY that, indeed; and though +from the magnitude of the figure it might at first deceive a +landsman, yet the slightest consideration will show that though seven +hundred and seventy-seven is a pretty large number, yet, when you +come to make a TEENTH of it, you will then see, I say, that the seven +hundred and seventy-seventh part of a farthing is a good deal less +than seven hundred and seventy-seven gold doubloons; and so I thought +at the time. + +"Why, blast your eyes, Bildad," cried Peleg, "thou dost not want to +swindle this young man! he must have more than that." + +"Seven hundred and seventy-seventh," again said Bildad, without +lifting his eyes; and then went on mumbling--"for where your treasure +is, there will your heart be also." + +"I am going to put him down for the three hundredth," said Peleg, "do +ye hear that, Bildad! The three hundredth lay, I say." + +Bildad laid down his book, and turning solemnly towards him said, +"Captain Peleg, thou hast a generous heart; but thou must consider +the duty thou owest to the other owners of this ship--widows and +orphans, many of them--and that if we too abundantly reward the +labors of this young man, we may be taking the bread from those +widows and those orphans. The seven hundred and seventy-seventh lay, +Captain Peleg." + +"Thou Bildad!" roared Peleg, starting up and clattering about the +cabin. "Blast ye, Captain Bildad, if I had followed thy advice in +these matters, I would afore now had a conscience to lug about that +would be heavy enough to founder the largest ship that ever sailed +round Cape Horn." + +"Captain Peleg," said Bildad steadily, "thy conscience may be drawing +ten inches of water, or ten fathoms, I can't tell; but as thou art +still an impenitent man, Captain Peleg, I greatly fear lest thy +conscience be but a leaky one; and will in the end sink thee +foundering down to the fiery pit, Captain Peleg." + +"Fiery pit! fiery pit! ye insult me, man; past all natural bearing, +ye insult me. It's an all-fired outrage to tell any human creature +that he's bound to hell. Flukes and flames! Bildad, say that again +to me, and start my soul-bolts, but I'll--I'll--yes, I'll swallow a +live goat with all his hair and horns on. Out of the cabin, ye +canting, drab-coloured son of a wooden gun--a straight wake with ye!" + +As he thundered out this he made a rush at Bildad, but with a +marvellous oblique, sliding celerity, Bildad for that time eluded +him. + +Alarmed at this terrible outburst between the two principal and +responsible owners of the ship, and feeling half a mind to give up +all idea of sailing in a vessel so questionably owned and temporarily +commanded, I stepped aside from the door to give egress to Bildad, +who, I made no doubt, was all eagerness to vanish from before the +awakened wrath of Peleg. But to my astonishment, he sat down again +on the transom very quietly, and seemed to have not the slightest +intention of withdrawing. He seemed quite used to impenitent Peleg +and his ways. As for Peleg, after letting off his rage as he had, +there seemed no more left in him, and he, too, sat down like a lamb, +though he twitched a little as if still nervously agitated. "Whew!" +he whistled at last--"the squall's gone off to leeward, I think. +Bildad, thou used to be good at sharpening a lance, mend that pen, +will ye. My jack-knife here needs the grindstone. That's he; thank +ye, Bildad. Now then, my young man, Ishmael's thy name, didn't ye +say? Well then, down ye go here, Ishmael, for the three hundredth +lay." + +"Captain Peleg," said I, "I have a friend with me who wants to ship +too--shall I bring him down to-morrow?" + +"To be sure," said Peleg. "Fetch him along, and we'll look at him." + +"What lay does he want?" groaned Bildad, glancing up from the book +in which he had again been burying himself. + +"Oh! never thee mind about that, Bildad," said Peleg. "Has he ever +whaled it any?" turning to me. + +"Killed more whales than I can count, Captain Peleg." + +"Well, bring him along then." + +And, after signing the papers, off I went; nothing doubting but that +I had done a good morning's work, and that the Pequod was the +identical ship that Yojo had provided to carry Queequeg and me round +the Cape. + +But I had not proceeded far, when I began to bethink me that the +Captain with whom I was to sail yet remained unseen by me; though, +indeed, in many cases, a whale-ship will be completely fitted out, +and receive all her crew on board, ere the captain makes himself +visible by arriving to take command; for sometimes these voyages are +so prolonged, and the shore intervals at home so exceedingly brief, +that if the captain have a family, or any absorbing concernment of +that sort, he does not trouble himself much about his ship in port, +but leaves her to the owners till all is ready for sea. However, it +is always as well to have a look at him before irrevocably committing +yourself into his hands. Turning back I accosted Captain Peleg, +inquiring where Captain Ahab was to be found. + +"And what dost thou want of Captain Ahab? It's all right enough; +thou art shipped." + +"Yes, but I should like to see him." + +"But I don't think thou wilt be able to at present. I don't know +exactly what's the matter with him; but he keeps close inside the +house; a sort of sick, and yet he don't look so. In fact, he ain't +sick; but no, he isn't well either. Any how, young man, he won't +always see me, so I don't suppose he will thee. He's a queer man, +Captain Ahab--so some think--but a good one. Oh, thou'lt like him +well enough; no fear, no fear. He's a grand, ungodly, god-like man, +Captain Ahab; doesn't speak much; but, when he does speak, then you +may well listen. Mark ye, be forewarned; Ahab's above the common; +Ahab's been in colleges, as well as 'mong the cannibals; been used to +deeper wonders than the waves; fixed his fiery lance in mightier, +stranger foes than whales. His lance! aye, the keenest and the surest +that out of all our isle! Oh! he ain't Captain Bildad; no, and he +ain't Captain Peleg; HE'S AHAB, boy; and Ahab of old, thou knowest, +was a crowned king!" + +"And a very vile one. When that wicked king was slain, the dogs, did +they not lick his blood?" + +"Come hither to me--hither, hither," said Peleg, with a significance +in his eye that almost startled me. "Look ye, lad; never say that on +board the Pequod. Never say it anywhere. Captain Ahab did not name +himself. 'Twas a foolish, ignorant whim of his crazy, widowed +mother, who died when he was only a twelvemonth old. And yet the old +squaw Tistig, at Gayhead, said that the name would somehow prove +prophetic. And, perhaps, other fools like her may tell thee the +same. I wish to warn thee. It's a lie. I know Captain Ahab well; +I've sailed with him as mate years ago; I know what he is--a good +man--not a pious, good man, like Bildad, but a swearing good +man--something like me--only there's a good deal more of him. Aye, +aye, I know that he was never very jolly; and I know that on the +passage home, he was a little out of his mind for a spell; but it was +the sharp shooting pains in his bleeding stump that brought that +about, as any one might see. I know, too, that ever since he lost +his leg last voyage by that accursed whale, he's been a kind of +moody--desperate moody, and savage sometimes; but that will all pass +off. And once for all, let me tell thee and assure thee, young man, +it's better to sail with a moody good captain than a laughing bad +one. So good-bye to thee--and wrong not Captain Ahab, because he +happens to have a wicked name. Besides, my boy, he has a wife--not +three voyages wedded--a sweet, resigned girl. Think of that; by that +sweet girl that old man has a child: hold ye then there can be any +utter, hopeless harm in Ahab? No, no, my lad; stricken, blasted, if +he be, Ahab has his humanities!" + +As I walked away, I was full of thoughtfulness; what had been +incidentally revealed to me of Captain Ahab, filled me with a certain +wild vagueness of painfulness concerning him. And somehow, at the +time, I felt a sympathy and a sorrow for him, but for I don't know +what, unless it was the cruel loss of his leg. And yet I also felt a +strange awe of him; but that sort of awe, which I cannot at all +describe, was not exactly awe; I do not know what it was. But I felt +it; and it did not disincline me towards him; though I felt +impatience at what seemed like mystery in him, so imperfectly as he +was known to me then. However, my thoughts were at length carried in +other directions, so that for the present dark Ahab slipped my mind. + + + +CHAPTER 17 + +The Ramadan. + + +As Queequeg's Ramadan, or Fasting and Humiliation, was to continue +all day, I did not choose to disturb him till towards night-fall; for +I cherish the greatest respect towards everybody's religious +obligations, never mind how comical, and could not find it in my +heart to undervalue even a congregation of ants worshipping a +toad-stool; or those other creatures in certain parts of our earth, +who with a degree of footmanism quite unprecedented in other planets, +bow down before the torso of a deceased landed proprietor merely on +account of the inordinate possessions yet owned and rented in his +name. + +I say, we good Presbyterian Christians should be charitable in these +things, and not fancy ourselves so vastly superior to other mortals, +pagans and what not, because of their half-crazy conceits on these +subjects. There was Queequeg, now, certainly entertaining the most +absurd notions about Yojo and his Ramadan;--but what of that? +Queequeg thought he knew what he was about, I suppose; he seemed to +be content; and there let him rest. All our arguing with him would +not avail; let him be, I say: and Heaven have mercy on us +all--Presbyterians and Pagans alike--for we are all somehow +dreadfully cracked about the head, and sadly need mending. + +Towards evening, when I felt assured that all his performances and +rituals must be over, I went up to his room and knocked at the door; +but no answer. I tried to open it, but it was fastened inside. +"Queequeg," said I softly through the key-hole:--all silent. "I say, +Queequeg! why don't you speak? It's I--Ishmael." But all remained +still as before. I began to grow alarmed. I had allowed him such +abundant time; I thought he might have had an apoplectic fit. I +looked through the key-hole; but the door opening into an odd corner +of the room, the key-hole prospect was but a crooked and sinister +one. I could only see part of the foot-board of the bed and a line +of the wall, but nothing more. I was surprised to behold resting +against the wall the wooden shaft of Queequeg's harpoon, which the +landlady the evening previous had taken from him, before our mounting +to the chamber. That's strange, thought I; but at any rate, since +the harpoon stands yonder, and he seldom or never goes abroad without +it, therefore he must be inside here, and no possible mistake. + +"Queequeg!--Queequeg!"--all still. Something must have happened. +Apoplexy! I tried to burst open the door; but it stubbornly +resisted. Running down stairs, I quickly stated my suspicions to the +first person I met--the chamber-maid. "La! la!" she cried, "I +thought something must be the matter. I went to make the bed after +breakfast, and the door was locked; and not a mouse to be heard; and +it's been just so silent ever since. But I thought, may be, you had +both gone off and locked your baggage in for safe keeping. La! la, +ma'am!--Mistress! murder! Mrs. Hussey! apoplexy!"--and with these +cries, she ran towards the kitchen, I following. + +Mrs. Hussey soon appeared, with a mustard-pot in one hand and a +vinegar-cruet in the other, having just broken away from the +occupation of attending to the castors, and scolding her little black +boy meantime. + +"Wood-house!" cried I, "which way to it? Run for God's sake, and +fetch something to pry open the door--the axe!--the axe! he's had a +stroke; depend upon it!"--and so saying I was unmethodically rushing +up stairs again empty-handed, when Mrs. Hussey interposed the +mustard-pot and vinegar-cruet, and the entire castor of her +countenance. + +"What's the matter with you, young man?" + +"Get the axe! For God's sake, run for the doctor, some one, while I +pry it open!" + +"Look here," said the landlady, quickly putting down the +vinegar-cruet, so as to have one hand free; "look here; are you +talking about prying open any of my doors?"--and with that she seized +my arm. "What's the matter with you? What's the matter with you, +shipmate?" + +In as calm, but rapid a manner as possible, I gave her to understand +the whole case. Unconsciously clapping the vinegar-cruet to one side +of her nose, she ruminated for an instant; then exclaimed--"No! I +haven't seen it since I put it there." Running to a little closet +under the landing of the stairs, she glanced in, and returning, told +me that Queequeg's harpoon was missing. "He's killed himself," she +cried. "It's unfort'nate Stiggs done over again there goes another +counterpane--God pity his poor mother!--it will be the ruin of my +house. Has the poor lad a sister? Where's that girl?--there, Betty, +go to Snarles the Painter, and tell him to paint me a sign, with--"no +suicides permitted here, and no smoking in the parlor;"--might as +well kill both birds at once. Kill? The Lord be merciful to his +ghost! What's that noise there? You, young man, avast there!" + +And running up after me, she caught me as I was again trying to force +open the door. + +"I don't allow it; I won't have my premises spoiled. Go for the +locksmith, there's one about a mile from here. But avast!" putting +her hand in her side-pocket, "here's a key that'll fit, I guess; +let's see." And with that, she turned it in the lock; but, alas! +Queequeg's supplemental bolt remained unwithdrawn within. + +"Have to burst it open," said I, and was running down the entry a +little, for a good start, when the landlady caught at me, again +vowing I should not break down her premises; but I tore from her, and +with a sudden bodily rush dashed myself full against the mark. + +With a prodigious noise the door flew open, and the knob slamming +against the wall, sent the plaster to the ceiling; and there, good +heavens! there sat Queequeg, altogether cool and self-collected; +right in the middle of the room; squatting on his hams, and holding +Yojo on top of his head. He looked neither one way nor the other +way, but sat like a carved image with scarce a sign of active life. + +"Queequeg," said I, going up to him, "Queequeg, what's the matter +with you?" + +"He hain't been a sittin' so all day, has he?" said the landlady. + +But all we said, not a word could we drag out of him; I almost felt +like pushing him over, so as to change his position, for it was +almost intolerable, it seemed so painfully and unnaturally +constrained; especially, as in all probability he had been sitting so +for upwards of eight or ten hours, going too without his regular +meals. + +"Mrs. Hussey," said I, "he's ALIVE at all events; so leave us, if you +please, and I will see to this strange affair myself." + +Closing the door upon the landlady, I endeavored to prevail upon +Queequeg to take a chair; but in vain. There he sat; and all he +could do--for all my polite arts and blandishments--he would not move +a peg, nor say a single word, nor even look at me, nor notice my +presence in the slightest way. + +I wonder, thought I, if this can possibly be a part of his Ramadan; +do they fast on their hams that way in his native island. It must be +so; yes, it's part of his creed, I suppose; well, then, let him +rest; he'll get up sooner or later, no doubt. It can't last for +ever, thank God, and his Ramadan only comes once a year; and I don't +believe it's very punctual then. + +I went down to supper. After sitting a long time listening to the +long stories of some sailors who had just come from a plum-pudding +voyage, as they called it (that is, a short whaling-voyage in a +schooner or brig, confined to the north of the line, in the Atlantic +Ocean only); after listening to these plum-puddingers till nearly +eleven o'clock, I went up stairs to go to bed, feeling quite sure by +this time Queequeg must certainly have brought his Ramadan to a +termination. But no; there he was just where I had left him; he had +not stirred an inch. I began to grow vexed with him; it seemed so +downright senseless and insane to be sitting there all day and half +the night on his hams in a cold room, holding a piece of wood on his +head. + +"For heaven's sake, Queequeg, get up and shake yourself; get up and +have some supper. You'll starve; you'll kill yourself, Queequeg." +But not a word did he reply. + +Despairing of him, therefore, I determined to go to bed and to sleep; +and no doubt, before a great while, he would follow me. But previous +to turning in, I took my heavy bearskin jacket, and threw it over +him, as it promised to be a very cold night; and he had nothing but +his ordinary round jacket on. For some time, do all I would, I could +not get into the faintest doze. I had blown out the candle; and the +mere thought of Queequeg--not four feet off--sitting there in that +uneasy position, stark alone in the cold and dark; this made me +really wretched. Think of it; sleeping all night in the same room +with a wide awake pagan on his hams in this dreary, unaccountable +Ramadan! + +But somehow I dropped off at last, and knew nothing more till break +of day; when, looking over the bedside, there squatted Queequeg, as +if he had been screwed down to the floor. But as soon as the first +glimpse of sun entered the window, up he got, with stiff and grating +joints, but with a cheerful look; limped towards me where I lay; +pressed his forehead again against mine; and said his Ramadan was +over. + +Now, as I before hinted, I have no objection to any person's +religion, be it what it may, so long as that person does not kill or +insult any other person, because that other person don't believe it +also. But when a man's religion becomes really frantic; when it is a +positive torment to him; and, in fine, makes this earth of ours an +uncomfortable inn to lodge in; then I think it high time to take that +individual aside and argue the point with him. + +And just so I now did with Queequeg. "Queequeg," said I, "get into +bed now, and lie and listen to me." I then went on, beginning with +the rise and progress of the primitive religions, and coming down to +the various religions of the present time, during which time I +labored to show Queequeg that all these Lents, Ramadans, and +prolonged ham-squattings in cold, cheerless rooms were stark +nonsense; bad for the health; useless for the soul; opposed, in +short, to the obvious laws of Hygiene and common sense. I told him, +too, that he being in other things such an extremely sensible and +sagacious savage, it pained me, very badly pained me, to see him now +so deplorably foolish about this ridiculous Ramadan of his. Besides, +argued I, fasting makes the body cave in; hence the spirit caves in; +and all thoughts born of a fast must necessarily be half-starved. +This is the reason why most dyspeptic religionists cherish such +melancholy notions about their hereafters. In one word, Queequeg, +said I, rather digressively; hell is an idea first born on an +undigested apple-dumpling; and since then perpetuated through the +hereditary dyspepsias nurtured by Ramadans. + +I then asked Queequeg whether he himself was ever troubled with +dyspepsia; expressing the idea very plainly, so that he could take it +in. He said no; only upon one memorable occasion. It was after a +great feast given by his father the king, on the gaining of a great +battle wherein fifty of the enemy had been killed by about two +o'clock in the afternoon, and all cooked and eaten that very evening. + +"No more, Queequeg," said I, shuddering; "that will do;" for I knew +the inferences without his further hinting them. I had seen a sailor +who had visited that very island, and he told me that it was the +custom, when a great battle had been gained there, to barbecue all +the slain in the yard or garden of the victor; and then, one by one, +they were placed in great wooden trenchers, and garnished round like +a pilau, with breadfruit and cocoanuts; and with some parsley in +their mouths, were sent round with the victor's compliments to all +his friends, just as though these presents were so many Christmas +turkeys. + +After all, I do not think that my remarks about religion made much +impression upon Queequeg. Because, in the first place, he somehow +seemed dull of hearing on that important subject, unless considered +from his own point of view; and, in the second place, he did not more +than one third understand me, couch my ideas simply as I would; and, +finally, he no doubt thought he knew a good deal more about the true +religion than I did. He looked at me with a sort of condescending +concern and compassion, as though he thought it a great pity that +such a sensible young man should be so hopelessly lost to evangelical +pagan piety. + +At last we rose and dressed; and Queequeg, taking a prodigiously +hearty breakfast of chowders of all sorts, so that the landlady +should not make much profit by reason of his Ramadan, we sallied out +to board the Pequod, sauntering along, and picking our teeth with +halibut bones. + + + +CHAPTER 18 + +His Mark. + + +As we were walking down the end of the wharf towards the ship, +Queequeg carrying his harpoon, Captain Peleg in his gruff voice +loudly hailed us from his wigwam, saying he had not suspected my +friend was a cannibal, and furthermore announcing that he let no +cannibals on board that craft, unless they previously produced their +papers. + +"What do you mean by that, Captain Peleg?" said I, now jumping on the +bulwarks, and leaving my comrade standing on the wharf. + +"I mean," he replied, "he must show his papers." + +"Yes," said Captain Bildad in his hollow voice, sticking his head +from behind Peleg's, out of the wigwam. "He must show that he's +converted. Son of darkness," he added, turning to Queequeg, "art +thou at present in communion with any Christian church?" + +"Why," said I, "he's a member of the first Congregational Church." +Here be it said, that many tattooed savages sailing in Nantucket +ships at last come to be converted into the churches. + +"First Congregational Church," cried Bildad, "what! that worships in +Deacon Deuteronomy Coleman's meeting-house?" and so saying, taking +out his spectacles, he rubbed them with his great yellow bandana +handkerchief, and putting them on very carefully, came out of the +wigwam, and leaning stiffly over the bulwarks, took a good long look +at Queequeg. + +"How long hath he been a member?" he then said, turning to me; "not +very long, I rather guess, young man." + +"No," said Peleg, "and he hasn't been baptized right either, or it +would have washed some of that devil's blue off his face." + +"Do tell, now," cried Bildad, "is this Philistine a regular member of +Deacon Deuteronomy's meeting? I never saw him going there, and I +pass it every Lord's day." + +"I don't know anything about Deacon Deuteronomy or his meeting," said +I; "all I know is, that Queequeg here is a born member of the First +Congregational Church. He is a deacon himself, Queequeg is." + +"Young man," said Bildad sternly, "thou art skylarking with +me--explain thyself, thou young Hittite. What church dost thee mean? +answer me." + +Finding myself thus hard pushed, I replied. "I mean, sir, the same +ancient Catholic Church to which you and I, and Captain Peleg there, +and Queequeg here, and all of us, and every mother's son and soul of +us belong; the great and everlasting First Congregation of this whole +worshipping world; we all belong to that; only some of us cherish +some queer crotchets no ways touching the grand belief; in THAT we +all join hands." + +"Splice, thou mean'st SPLICE hands," cried Peleg, drawing nearer. +"Young man, you'd better ship for a missionary, instead of a +fore-mast hand; I never heard a better sermon. Deacon +Deuteronomy--why Father Mapple himself couldn't beat it, and he's +reckoned something. Come aboard, come aboard; never mind about the +papers. I say, tell Quohog there--what's that you call him? tell +Quohog to step along. By the great anchor, what a harpoon he's got +there! looks like good stuff that; and he handles it about right. I +say, Quohog, or whatever your name is, did you ever stand in the head +of a whale-boat? did you ever strike a fish?" + +Without saying a word, Queequeg, in his wild sort of way, jumped upon +the bulwarks, from thence into the bows of one of the whale-boats +hanging to the side; and then bracing his left knee, and poising his +harpoon, cried out in some such way as this:-- + +"Cap'ain, you see him small drop tar on water dere? You see him? +well, spose him one whale eye, well, den!" and taking sharp aim at +it, he darted the iron right over old Bildad's broad brim, clean +across the ship's decks, and struck the glistening tar spot out of +sight. + +"Now," said Queequeg, quietly hauling in the line, "spos-ee him +whale-e eye; why, dad whale dead." + +"Quick, Bildad," said Peleg, his partner, who, aghast at the close +vicinity of the flying harpoon, had retreated towards the cabin +gangway. "Quick, I say, you Bildad, and get the ship's papers. We +must have Hedgehog there, I mean Quohog, in one of our boats. Look +ye, Quohog, we'll give ye the ninetieth lay, and that's more than +ever was given a harpooneer yet out of Nantucket." + +So down we went into the cabin, and to my great joy Queequeg was soon +enrolled among the same ship's company to which I myself belonged. + +When all preliminaries were over and Peleg had got everything ready +for signing, he turned to me and said, "I guess, Quohog there don't +know how to write, does he? I say, Quohog, blast ye! dost thou sign +thy name or make thy mark? + +But at this question, Queequeg, who had twice or thrice before taken +part in similar ceremonies, looked no ways abashed; but taking the +offered pen, copied upon the paper, in the proper place, an exact +counterpart of a queer round figure which was tattooed upon his arm; +so that through Captain Peleg's obstinate mistake touching his +appellative, it stood something like this:-- + +Quohog. +his X mark. + +Meanwhile Captain Bildad sat earnestly and steadfastly eyeing +Queequeg, and at last rising solemnly and fumbling in the huge +pockets of his broad-skirted drab coat, took out a bundle of tracts, +and selecting one entitled "The Latter Day Coming; or No Time to +Lose," placed it in Queequeg's hands, and then grasping them and the +book with both his, looked earnestly into his eyes, and said, "Son of +darkness, I must do my duty by thee; I am part owner of this ship, +and feel concerned for the souls of all its crew; if thou still +clingest to thy Pagan ways, which I sadly fear, I beseech thee, +remain not for aye a Belial bondsman. Spurn the idol Bell, and the +hideous dragon; turn from the wrath to come; mind thine eye, I say; +oh! goodness gracious! steer clear of the fiery pit!" + +Something of the salt sea yet lingered in old Bildad's language, +heterogeneously mixed with Scriptural and domestic phrases. + +"Avast there, avast there, Bildad, avast now spoiling our +harpooneer," Peleg. "Pious harpooneers never make good voyagers--it +takes the shark out of 'em; no harpooneer is worth a straw who aint +pretty sharkish. There was young Nat Swaine, once the bravest +boat-header out of all Nantucket and the Vineyard; he joined the +meeting, and never came to good. He got so frightened about his +plaguy soul, that he shrinked and sheered away from whales, for fear +of after-claps, in case he got stove and went to Davy Jones." + +"Peleg! Peleg!" said Bildad, lifting his eyes and hands, "thou +thyself, as I myself, hast seen many a perilous time; thou knowest, +Peleg, what it is to have the fear of death; how, then, can'st thou +prate in this ungodly guise. Thou beliest thine own heart, Peleg. +Tell me, when this same Pequod here had her three masts overboard in +that typhoon on Japan, that same voyage when thou went mate with +Captain Ahab, did'st thou not think of Death and the Judgment then?" + +"Hear him, hear him now," cried Peleg, marching across the cabin, and +thrusting his hands far down into his pockets,--"hear him, all of ye. +Think of that! When every moment we thought the ship would sink! +Death and the Judgment then? What? With all three masts making such +an everlasting thundering against the side; and every sea breaking +over us, fore and aft. Think of Death and the Judgment then? No! +no time to think about Death then. Life was what Captain Ahab and I +was thinking of; and how to save all hands--how to rig +jury-masts--how to get into the nearest port; that was what I was +thinking of." + +Bildad said no more, but buttoning up his coat, stalked on deck, +where we followed him. There he stood, very quietly overlooking some +sailmakers who were mending a top-sail in the waist. Now and then he +stooped to pick up a patch, or save an end of tarred twine, which +otherwise might have been wasted. + + + +CHAPTER 19 + +The Prophet. + + +"Shipmates, have ye shipped in that ship?" + +Queequeg and I had just left the Pequod, and were sauntering away from +the water, for the moment each occupied with his own thoughts, when +the above words were put to us by a stranger, who, pausing before us, +levelled his massive forefinger at the vessel in question. He was +but shabbily apparelled in faded jacket and patched trowsers; a rag +of a black handkerchief investing his neck. A confluent small-pox +had in all directions flowed over his face, and left it like the +complicated ribbed bed of a torrent, when the rushing waters have +been dried up. + +"Have ye shipped in her?" he repeated. + +"You mean the ship Pequod, I suppose," said I, trying to gain a +little more time for an uninterrupted look at him. + +"Aye, the Pequod--that ship there," he said, drawing back his whole +arm, and then rapidly shoving it straight out from him, with the +fixed bayonet of his pointed finger darted full at the object. + +"Yes," said I, "we have just signed the articles." + +"Anything down there about your souls?" + +"About what?" + +"Oh, perhaps you hav'n't got any," he said quickly. "No matter +though, I know many chaps that hav'n't got any,--good luck to 'em; +and they are all the better off for it. A soul's a sort of a fifth +wheel to a wagon." + +"What are you jabbering about, shipmate?" said I. + +"HE'S got enough, though, to make up for all deficiencies of that +sort in other chaps," abruptly said the stranger, placing a nervous +emphasis upon the word HE. + +"Queequeg," said I, "let's go; this fellow has broken loose from +somewhere; he's talking about something and somebody we don't know." + +"Stop!" cried the stranger. "Ye said true--ye hav'n't seen Old +Thunder yet, have ye?" + +"Who's Old Thunder?" said I, again riveted with the insane +earnestness of his manner. + +"Captain Ahab." + +"What! the captain of our ship, the Pequod?" + +"Aye, among some of us old sailor chaps, he goes by that name. Ye +hav'n't seen him yet, have ye?" + +"No, we hav'n't. He's sick they say, but is getting better, and will +be all right again before long." + +"All right again before long!" laughed the stranger, with a solemnly +derisive sort of laugh. "Look ye; when Captain Ahab is all right, +then this left arm of mine will be all right; not before." + +"What do you know about him?" + +"What did they TELL you about him? Say that!" + +"They didn't tell much of anything about him; only I've heard that +he's a good whale-hunter, and a good captain to his crew." + +"That's true, that's true--yes, both true enough. But you must jump +when he gives an order. Step and growl; growl and go--that's the +word with Captain Ahab. But nothing about that thing that happened +to him off Cape Horn, long ago, when he lay like dead for three days +and nights; nothing about that deadly skrimmage with the Spaniard +afore the altar in Santa?--heard nothing about that, eh? Nothing +about the silver calabash he spat into? And nothing about his losing +his leg last voyage, according to the prophecy. Didn't ye hear a +word about them matters and something more, eh? No, I don't think ye +did; how could ye? Who knows it? Not all Nantucket, I guess. But +hows'ever, mayhap, ye've heard tell about the leg, and how he lost +it; aye, ye have heard of that, I dare say. Oh yes, THAT every one +knows a'most--I mean they know he's only one leg; and that a +parmacetti took the other off." + +"My friend," said I, "what all this gibberish of yours is about, I +don't know, and I don't much care; for it seems to me that you must +be a little damaged in the head. But if you are speaking of Captain +Ahab, of that ship there, the Pequod, then let me tell you, that I +know all about the loss of his leg." + +"ALL about it, eh--sure you do?--all?" + +"Pretty sure." + +With finger pointed and eye levelled at the Pequod, the beggar-like +stranger stood a moment, as if in a troubled reverie; then starting a +little, turned and said:--"Ye've shipped, have ye? Names down on the +papers? Well, well, what's signed, is signed; and what's to be, will +be; and then again, perhaps it won't be, after all. Anyhow, it's +all fixed and arranged a'ready; and some sailors or other must go +with him, I suppose; as well these as any other men, God pity 'em! +Morning to ye, shipmates, morning; the ineffable heavens bless ye; +I'm sorry I stopped ye." + +"Look here, friend," said I, "if you have anything important to tell +us, out with it; but if you are only trying to bamboozle us, you are +mistaken in your game; that's all I have to say." + +"And it's said very well, and I like to hear a chap talk up that way; +you are just the man for him--the likes of ye. Morning to ye, +shipmates, morning! Oh! when ye get there, tell 'em I've concluded +not to make one of 'em." + +"Ah, my dear fellow, you can't fool us that way--you can't fool us. +It is the easiest thing in the world for a man to look as if he had a +great secret in him." + +"Morning to ye, shipmates, morning." + +"Morning it is," said I. "Come along, Queequeg, let's leave this +crazy man. But stop, tell me your name, will you?" + +"Elijah." + +Elijah! thought I, and we walked away, both commenting, after each +other's fashion, upon this ragged old sailor; and agreed that he was +nothing but a humbug, trying to be a bugbear. But we had not gone +perhaps above a hundred yards, when chancing to turn a corner, and +looking back as I did so, who should be seen but Elijah following us, +though at a distance. Somehow, the sight of him struck me so, that I +said nothing to Queequeg of his being behind, but passed on with my +comrade, anxious to see whether the stranger would turn the same +corner that we did. He did; and then it seemed to me that he was +dogging us, but with what intent I could not for the life of me +imagine. This circumstance, coupled with his ambiguous, +half-hinting, half-revealing, shrouded sort of talk, now begat in me +all kinds of vague wonderments and half-apprehensions, and all +connected with the Pequod; and Captain Ahab; and the leg he had lost; +and the Cape Horn fit; and the silver calabash; and what Captain +Peleg had said of him, when I left the ship the day previous; and the +prediction of the squaw Tistig; and the voyage we had bound ourselves +to sail; and a hundred other shadowy things. + +I was resolved to satisfy myself whether this ragged Elijah was +really dogging us or not, and with that intent crossed the way with +Queequeg, and on that side of it retraced our steps. But Elijah +passed on, without seeming to notice us. This relieved me; and once +more, and finally as it seemed to me, I pronounced him in my heart, a +humbug. + + + +CHAPTER 20 + +All Astir. + + +A day or two passed, and there was great activity aboard the Pequod. +Not only were the old sails being mended, but new sails were coming +on board, and bolts of canvas, and coils of rigging; in short, +everything betokened that the ship's preparations were hurrying to a +close. Captain Peleg seldom or never went ashore, but sat in his +wigwam keeping a sharp look-out upon the hands: Bildad did all the +purchasing and providing at the stores; and the men employed in the +hold and on the rigging were working till long after night-fall. + +On the day following Queequeg's signing the articles, word was given +at all the inns where the ship's company were stopping, that their +chests must be on board before night, for there was no telling how +soon the vessel might be sailing. So Queequeg and I got down our +traps, resolving, however, to sleep ashore till the last. But it +seems they always give very long notice in these cases, and the ship +did not sail for several days. But no wonder; there was a good deal +to be done, and there is no telling how many things to be thought of, +before the Pequod was fully equipped. + +Every one knows what a multitude of things--beds, sauce-pans, knives +and forks, shovels and tongs, napkins, nut-crackers, and what not, +are indispensable to the business of housekeeping. Just so with +whaling, which necessitates a three-years' housekeeping upon the wide +ocean, far from all grocers, costermongers, doctors, bakers, and +bankers. And though this also holds true of merchant vessels, yet +not by any means to the same extent as with whalemen. For besides +the great length of the whaling voyage, the numerous articles +peculiar to the prosecution of the fishery, and the impossibility of +replacing them at the remote harbors usually frequented, it must be +remembered, that of all ships, whaling vessels are the most exposed +to accidents of all kinds, and especially to the destruction and loss +of the very things upon which the success of the voyage most depends. +Hence, the spare boats, spare spars, and spare lines and harpoons, +and spare everythings, almost, but a spare Captain and duplicate +ship. + +At the period of our arrival at the Island, the heaviest storage of +the Pequod had been almost completed; comprising her beef, bread, +water, fuel, and iron hoops and staves. But, as before hinted, for +some time there was a continual fetching and carrying on board of +divers odds and ends of things, both large and small. + +Chief among those who did this fetching and carrying was Captain +Bildad's sister, a lean old lady of a most determined and +indefatigable spirit, but withal very kindhearted, who seemed +resolved that, if SHE could help it, nothing should be found wanting +in the Pequod, after once fairly getting to sea. At one time she +would come on board with a jar of pickles for the steward's pantry; +another time with a bunch of quills for the chief mate's desk, where +he kept his log; a third time with a roll of flannel for the small of +some one's rheumatic back. Never did any woman better deserve her +name, which was Charity--Aunt Charity, as everybody called her. And +like a sister of charity did this charitable Aunt Charity bustle +about hither and thither, ready to turn her hand and heart to +anything that promised to yield safety, comfort, and consolation to +all on board a ship in which her beloved brother Bildad was +concerned, and in which she herself owned a score or two of +well-saved dollars. + +But it was startling to see this excellent hearted Quakeress coming +on board, as she did the last day, with a long oil-ladle in one hand, +and a still longer whaling lance in the other. Nor was Bildad himself +nor Captain Peleg at all backward. As for Bildad, he carried about +with him a long list of the articles needed, and at every fresh +arrival, down went his mark opposite that article upon the paper. +Every once in a while Peleg came hobbling out of his whalebone den, +roaring at the men down the hatchways, roaring up to the riggers at +the mast-head, and then concluded by roaring back into his wigwam. + +During these days of preparation, Queequeg and I often visited the +craft, and as often I asked about Captain Ahab, and how he was, and +when he was going to come on board his ship. To these questions they +would answer, that he was getting better and better, and was expected +aboard every day; meantime, the two captains, Peleg and Bildad, could +attend to everything necessary to fit the vessel for the voyage. If +I had been downright honest with myself, I would have seen very +plainly in my heart that I did but half fancy being committed this +way to so long a voyage, without once laying my eyes on the man who +was to be the absolute dictator of it, so soon as the ship sailed out +upon the open sea. But when a man suspects any wrong, it sometimes +happens that if he be already involved in the matter, he insensibly +strives to cover up his suspicions even from himself. And much this +way it was with me. I said nothing, and tried to think nothing. + +At last it was given out that some time next day the ship would +certainly sail. So next morning, Queequeg and I took a very early +start. + + + +CHAPTER 21 + +Going Aboard. + + +It was nearly six o'clock, but only grey imperfect misty dawn, when +we drew nigh the wharf. + +"There are some sailors running ahead there, if I see right," said I +to Queequeg, "it can't be shadows; she's off by sunrise, I guess; +come on!" + +"Avast!" cried a voice, whose owner at the same time coming close +behind us, laid a hand upon both our shoulders, and then insinuating +himself between us, stood stooping forward a little, in the uncertain +twilight, strangely peering from Queequeg to me. It was Elijah. + +"Going aboard?" + +"Hands off, will you," said I. + +"Lookee here," said Queequeg, shaking himself, "go 'way!" + +"Ain't going aboard, then?" + +"Yes, we are," said I, "but what business is that of yours? Do you +know, Mr. Elijah, that I consider you a little impertinent?" + +"No, no, no; I wasn't aware of that," said Elijah, slowly and +wonderingly looking from me to Queequeg, with the most unaccountable +glances. + +"Elijah," said I, "you will oblige my friend and me by withdrawing. +We are going to the Indian and Pacific Oceans, and would prefer not +to be detained." + +"Ye be, be ye? Coming back afore breakfast?" + +"He's cracked, Queequeg," said I, "come on." + +"Holloa!" cried stationary Elijah, hailing us when we had removed a +few paces. + +"Never mind him," said I, "Queequeg, come on." + +But he stole up to us again, and suddenly clapping his hand on my +shoulder, said--"Did ye see anything looking like men going towards +that ship a while ago?" + +Struck by this plain matter-of-fact question, I answered, saying, +"Yes, I thought I did see four or five men; but it was too dim to be +sure." + +"Very dim, very dim," said Elijah. "Morning to ye." + +Once more we quitted him; but once more he came softly after us; and +touching my shoulder again, said, "See if you can find 'em now, will +ye? + +"Find who?" + +"Morning to ye! morning to ye!" he rejoined, again moving off. "Oh! +I was going to warn ye against--but never mind, never mind--it's all +one, all in the family too;--sharp frost this morning, ain't it? +Good-bye to ye. Shan't see ye again very soon, I guess; unless it's +before the Grand Jury." And with these cracked words he finally +departed, leaving me, for the moment, in no small wonderment at his +frantic impudence. + +At last, stepping on board the Pequod, we found everything in +profound quiet, not a soul moving. The cabin entrance was locked +within; the hatches were all on, and lumbered with coils of rigging. +Going forward to the forecastle, we found the slide of the scuttle +open. Seeing a light, we went down, and found only an old rigger +there, wrapped in a tattered pea-jacket. He was thrown at whole +length upon two chests, his face downwards and inclosed in his folded +arms. The profoundest slumber slept upon him. + +"Those sailors we saw, Queequeg, where can they have gone to?" said +I, looking dubiously at the sleeper. But it seemed that, when on the +wharf, Queequeg had not at all noticed what I now alluded to; hence I +would have thought myself to have been optically deceived in that +matter, were it not for Elijah's otherwise inexplicable question. +But I beat the thing down; and again marking the sleeper, jocularly +hinted to Queequeg that perhaps we had best sit up with the body; +telling him to establish himself accordingly. He put his hand upon +the sleeper's rear, as though feeling if it was soft enough; and +then, without more ado, sat quietly down there. + +"Gracious! Queequeg, don't sit there," said I. + +"Oh! perry dood seat," said Queequeg, "my country way; won't hurt +him face." + +"Face!" said I, "call that his face? very benevolent countenance +then; but how hard he breathes, he's heaving himself; get off, +Queequeg, you are heavy, it's grinding the face of the poor. Get +off, Queequeg! Look, he'll twitch you off soon. I wonder he don't +wake." + +Queequeg removed himself to just beyond the head of the sleeper, and +lighted his tomahawk pipe. I sat at the feet. We kept the pipe +passing over the sleeper, from one to the other. Meanwhile, upon +questioning him in his broken fashion, Queequeg gave me to understand +that, in his land, owing to the absence of settees and sofas of all +sorts, the king, chiefs, and great people generally, were in the +custom of fattening some of the lower orders for ottomans; and to +furnish a house comfortably in that respect, you had only to buy up +eight or ten lazy fellows, and lay them round in the piers and +alcoves. Besides, it was very convenient on an excursion; much +better than those garden-chairs which are convertible into +walking-sticks; upon occasion, a chief calling his attendant, and +desiring him to make a settee of himself under a spreading tree, +perhaps in some damp marshy place. + +While narrating these things, every time Queequeg received the +tomahawk from me, he flourished the hatchet-side of it over the +sleeper's head. + +"What's that for, Queequeg?" + +"Perry easy, kill-e; oh! perry easy! + +He was going on with some wild reminiscences about his tomahawk-pipe, +which, it seemed, had in its two uses both brained his foes and +soothed his soul, when we were directly attracted to the sleeping +rigger. The strong vapour now completely filling the contracted hole, +it began to tell upon him. He breathed with a sort of muffledness; +then seemed troubled in the nose; then revolved over once or twice; +then sat up and rubbed his eyes. + +"Holloa!" he breathed at last, "who be ye smokers?" + +"Shipped men," answered I, "when does she sail?" + +"Aye, aye, ye are going in her, be ye? She sails to-day. The +Captain came aboard last night." + +"What Captain?--Ahab?" + +"Who but him indeed?" + +I was going to ask him some further questions concerning Ahab, when +we heard a noise on deck. + +"Holloa! Starbuck's astir," said the rigger. "He's a lively chief +mate, that; good man, and a pious; but all alive now, I must turn +to." And so saying he went on deck, and we followed. + +It was now clear sunrise. Soon the crew came on board in twos and +threes; the riggers bestirred themselves; the mates were actively +engaged; and several of the shore people were busy in bringing +various last things on board. Meanwhile Captain Ahab remained +invisibly enshrined within his cabin. + + + +CHAPTER 22 + +Merry Christmas. + + +At length, towards noon, upon the final dismissal of the ship's +riggers, and after the Pequod had been hauled out from the wharf, and +after the ever-thoughtful Charity had come off in a whale-boat, with +her last gift--a night-cap for Stubb, the second mate, her +brother-in-law, and a spare Bible for the steward--after all this, +the two Captains, Peleg and Bildad, issued from the cabin, and +turning to the chief mate, Peleg said: + +"Now, Mr. Starbuck, are you sure everything is right? Captain Ahab +is all ready--just spoke to him--nothing more to be got from shore, +eh? Well, call all hands, then. Muster 'em aft here--blast 'em!" + +"No need of profane words, however great the hurry, Peleg," said +Bildad, "but away with thee, friend Starbuck, and do our bidding." + +How now! Here upon the very point of starting for the voyage, +Captain Peleg and Captain Bildad were going it with a high hand on +the quarter-deck, just as if they were to be joint-commanders at sea, +as well as to all appearances in port. And, as for Captain Ahab, no +sign of him was yet to be seen; only, they said he was in the cabin. +But then, the idea was, that his presence was by no means necessary +in getting the ship under weigh, and steering her well out to sea. +Indeed, as that was not at all his proper business, but the pilot's; +and as he was not yet completely recovered--so they said--therefore, +Captain Ahab stayed below. And all this seemed natural enough; +especially as in the merchant service many captains never show +themselves on deck for a considerable time after heaving up the +anchor, but remain over the cabin table, having a farewell +merry-making with their shore friends, before they quit the ship for +good with the pilot. + +But there was not much chance to think over the matter, for Captain +Peleg was now all alive. He seemed to do most of the talking and +commanding, and not Bildad. + +"Aft here, ye sons of bachelors," he cried, as the sailors lingered +at the main-mast. "Mr. Starbuck, drive'em aft." + +"Strike the tent there!"--was the next order. As I hinted before, +this whalebone marquee was never pitched except in port; and on board +the Pequod, for thirty years, the order to strike the tent was well +known to be the next thing to heaving up the anchor. + +"Man the capstan! Blood and thunder!--jump!"--was the next command, +and the crew sprang for the handspikes. + +Now in getting under weigh, the station generally occupied by the +pilot is the forward part of the ship. And here Bildad, who, with +Peleg, be it known, in addition to his other officers, was one of the +licensed pilots of the port--he being suspected to have got himself +made a pilot in order to save the Nantucket pilot-fee to all the +ships he was concerned in, for he never piloted any other +craft--Bildad, I say, might now be seen actively engaged in looking +over the bows for the approaching anchor, and at intervals singing +what seemed a dismal stave of psalmody, to cheer the hands at the +windlass, who roared forth some sort of a chorus about the girls in +Booble Alley, with hearty good will. Nevertheless, not three days +previous, Bildad had told them that no profane songs would be allowed +on board the Pequod, particularly in getting under weigh; and +Charity, his sister, had placed a small choice copy of Watts in each +seaman's berth. + +Meantime, overseeing the other part of the ship, Captain Peleg ripped +and swore astern in the most frightful manner. I almost thought he +would sink the ship before the anchor could be got up; involuntarily +I paused on my handspike, and told Queequeg to do the same, thinking +of the perils we both ran, in starting on the voyage with such a +devil for a pilot. I was comforting myself, however, with the +thought that in pious Bildad might be found some salvation, spite of +his seven hundred and seventy-seventh lay; when I felt a sudden sharp +poke in my rear, and turning round, was horrified at the apparition +of Captain Peleg in the act of withdrawing his leg from my immediate +vicinity. That was my first kick. + +"Is that the way they heave in the marchant service?" he roared. +"Spring, thou sheep-head; spring, and break thy backbone! Why don't +ye spring, I say, all of ye--spring! Quohog! spring, thou chap with +the red whiskers; spring there, Scotch-cap; spring, thou green +pants. Spring, I say, all of ye, and spring your eyes out!" And so +saying, he moved along the windlass, here and there using his leg +very freely, while imperturbable Bildad kept leading off with his +psalmody. Thinks I, Captain Peleg must have been drinking something +to-day. + +At last the anchor was up, the sails were set, and off we glided. It +was a short, cold Christmas; and as the short northern day merged +into night, we found ourselves almost broad upon the wintry ocean, +whose freezing spray cased us in ice, as in polished armor. The long +rows of teeth on the bulwarks glistened in the moonlight; and like +the white ivory tusks of some huge elephant, vast curving icicles +depended from the bows. + +Lank Bildad, as pilot, headed the first watch, and ever and anon, as +the old craft deep dived into the green seas, and sent the shivering +frost all over her, and the winds howled, and the cordage rang, his +steady notes were heard,-- + +"Sweet fields beyond the swelling flood, +Stand dressed in living green. +So to the Jews old Canaan stood, +While Jordan rolled between." + + +Never did those sweet words sound more sweetly to me than then. They +were full of hope and fruition. Spite of this frigid winter night in +the boisterous Atlantic, spite of my wet feet and wetter jacket, +there was yet, it then seemed to me, many a pleasant haven in store; +and meads and glades so eternally vernal, that the grass shot up by +the spring, untrodden, unwilted, remains at midsummer. + +At last we gained such an offing, that the two pilots were needed no +longer. The stout sail-boat that had accompanied us began ranging +alongside. + +It was curious and not unpleasing, how Peleg and Bildad were affected +at this juncture, especially Captain Bildad. For loath to depart, +yet; very loath to leave, for good, a ship bound on so long and +perilous a voyage--beyond both stormy Capes; a ship in which some +thousands of his hard earned dollars were invested; a ship, in which +an old shipmate sailed as captain; a man almost as old as he, once +more starting to encounter all the terrors of the pitiless jaw; loath +to say good-bye to a thing so every way brimful of every interest to +him,--poor old Bildad lingered long; paced the deck with anxious +strides; ran down into the cabin to speak another farewell word +there; again came on deck, and looked to windward; looked towards the +wide and endless waters, only bounded by the far-off unseen Eastern +Continents; looked towards the land; looked aloft; looked right and +left; looked everywhere and nowhere; and at last, mechanically +coiling a rope upon its pin, convulsively grasped stout Peleg by the +hand, and holding up a lantern, for a moment stood gazing heroically +in his face, as much as to say, "Nevertheless, friend Peleg, I can +stand it; yes, I can." + +As for Peleg himself, he took it more like a philosopher; but for all +his philosophy, there was a tear twinkling in his eye, when the +lantern came too near. And he, too, did not a little run from cabin +to deck--now a word below, and now a word with Starbuck, the chief +mate. + +But, at last, he turned to his comrade, with a final sort of look +about him,--"Captain Bildad--come, old shipmate, we must go. Back +the main-yard there! Boat ahoy! Stand by to come close alongside, +now! Careful, careful!--come, Bildad, boy--say your last. Luck to +ye, Starbuck--luck to ye, Mr. Stubb--luck to ye, Mr. Flask--good-bye +and good luck to ye all--and this day three years I'll have a hot +supper smoking for ye in old Nantucket. Hurrah and away!" + +"God bless ye, and have ye in His holy keeping, men," murmured old +Bildad, almost incoherently. "I hope ye'll have fine weather now, so +that Captain Ahab may soon be moving among ye--a pleasant sun is all +he needs, and ye'll have plenty of them in the tropic voyage ye go. +Be careful in the hunt, ye mates. Don't stave the boats needlessly, +ye harpooneers; good white cedar plank is raised full three per cent. +within the year. Don't forget your prayers, either. Mr. Starbuck, +mind that cooper don't waste the spare staves. Oh! the sail-needles +are in the green locker! Don't whale it too much a' Lord's days, +men; but don't miss a fair chance either, that's rejecting Heaven's +good gifts. Have an eye to the molasses tierce, Mr. Stubb; it was a +little leaky, I thought. If ye touch at the islands, Mr. Flask, +beware of fornication. Good-bye, good-bye! Don't keep that cheese +too long down in the hold, Mr. Starbuck; it'll spoil. Be careful +with the butter--twenty cents the pound it was, and mind ye, if--" + +"Come, come, Captain Bildad; stop palavering,--away!" and with that, +Peleg hurried him over the side, and both dropt into the boat. + +Ship and boat diverged; the cold, damp night breeze blew between; a +screaming gull flew overhead; the two hulls wildly rolled; we gave +three heavy-hearted cheers, and blindly plunged like fate into the +lone Atlantic. + + + +CHAPTER 23 + +The Lee Shore. + + +Some chapters back, one Bulkington was spoken of, a tall, newlanded +mariner, encountered in New Bedford at the inn. + +When on that shivering winter's night, the Pequod thrust her +vindictive bows into the cold malicious waves, who should I see +standing at her helm but Bulkington! I looked with sympathetic awe +and fearfulness upon the man, who in mid-winter just landed from a +four years' dangerous voyage, could so unrestingly push off again for +still another tempestuous term. The land seemed scorching to his +feet. Wonderfullest things are ever the unmentionable; deep memories +yield no epitaphs; this six-inch chapter is the stoneless grave of +Bulkington. Let me only say that it fared with him as with the +storm-tossed ship, that miserably drives along the leeward land. The +port would fain give succor; the port is pitiful; in the port is +safety, comfort, hearthstone, supper, warm blankets, friends, all +that's kind to our mortalities. But in that gale, the port, the +land, is that ship's direst jeopardy; she must fly all hospitality; +one touch of land, though it but graze the keel, would make her +shudder through and through. With all her might she crowds all sail +off shore; in so doing, fights 'gainst the very winds that fain would +blow her homeward; seeks all the lashed sea's landlessness again; for +refuge's sake forlornly rushing into peril; her only friend her +bitterest foe! + +Know ye now, Bulkington? Glimpses do ye seem to see of that mortally +intolerable truth; that all deep, earnest thinking is but the +intrepid effort of the soul to keep the open independence of her sea; +while the wildest winds of heaven and earth conspire to cast her on +the treacherous, slavish shore? + +But as in landlessness alone resides highest truth, shoreless, +indefinite as God--so, better is it to perish in that howling +infinite, than be ingloriously dashed upon the lee, even if that were +safety! For worm-like, then, oh! who would craven crawl to land! +Terrors of the terrible! is all this agony so vain? Take heart, take +heart, O Bulkington! Bear thee grimly, demigod! Up from the spray +of thy ocean-perishing--straight up, leaps thy apotheosis! + + + +CHAPTER 24 + +The Advocate. + + +As Queequeg and I are now fairly embarked in this business of +whaling; and as this business of whaling has somehow come to be +regarded among landsmen as a rather unpoetical and disreputable +pursuit; therefore, I am all anxiety to convince ye, ye landsmen, of +the injustice hereby done to us hunters of whales. + +In the first place, it may be deemed almost superfluous to establish +the fact, that among people at large, the business of whaling is not +accounted on a level with what are called the liberal professions. +If a stranger were introduced into any miscellaneous metropolitan +society, it would but slightly advance the general opinion of his +merits, were he presented to the company as a harpooneer, say; and if +in emulation of the naval officers he should append the initials +S.W.F. (Sperm Whale Fishery) to his visiting card, such a procedure +would be deemed pre-eminently presuming and ridiculous. + +Doubtless one leading reason why the world declines honouring us +whalemen, is this: they think that, at best, our vocation amounts to +a butchering sort of business; and that when actively engaged +therein, we are surrounded by all manner of defilements. Butchers we +are, that is true. But butchers, also, and butchers of the bloodiest +badge have been all Martial Commanders whom the world invariably +delights to honour. And as for the matter of the alleged +uncleanliness of our business, ye shall soon be initiated into +certain facts hitherto pretty generally unknown, and which, upon the +whole, will triumphantly plant the sperm whale-ship at least among +the cleanliest things of this tidy earth. But even granting the +charge in question to be true; what disordered slippery decks of a +whale-ship are comparable to the unspeakable carrion of those +battle-fields from which so many soldiers return to drink in all +ladies' plaudits? And if the idea of peril so much enhances the +popular conceit of the soldier's profession; let me assure ye that +many a veteran who has freely marched up to a battery, would quickly +recoil at the apparition of the sperm whale's vast tail, fanning into +eddies the air over his head. For what are the comprehensible +terrors of man compared with the interlinked terrors and wonders of +God! + +But, though the world scouts at us whale hunters, yet does it +unwittingly pay us the profoundest homage; yea, an all-abounding +adoration! for almost all the tapers, lamps, and candles that burn +round the globe, burn, as before so many shrines, to our glory! + +But look at this matter in other lights; weigh it in all sorts of +scales; see what we whalemen are, and have been. + +Why did the Dutch in De Witt's time have admirals of their whaling +fleets? Why did Louis XVI. of France, at his own personal expense, +fit out whaling ships from Dunkirk, and politely invite to that town +some score or two of families from our own island of Nantucket? Why +did Britain between the years 1750 and 1788 pay to her whalemen in +bounties upwards of L1,000,000? And lastly, how comes it that we +whalemen of America now outnumber all the rest of the banded whalemen +in the world; sail a navy of upwards of seven hundred vessels; manned +by eighteen thousand men; yearly consuming 4,000,000 of dollars; the +ships worth, at the time of sailing, $20,000,000! and every year +importing into our harbors a well reaped harvest of $7,000,000. How +comes all this, if there be not something puissant in whaling? + +But this is not the half; look again. + +I freely assert, that the cosmopolite philosopher cannot, for his +life, point out one single peaceful influence, which within the last +sixty years has operated more potentially upon the whole broad world, +taken in one aggregate, than the high and mighty business of whaling. +One way and another, it has begotten events so remarkable in +themselves, and so continuously momentous in their sequential issues, +that whaling may well be regarded as that Egyptian mother, who bore +offspring themselves pregnant from her womb. It would be a hopeless, +endless task to catalogue all these things. Let a handful suffice. +For many years past the whale-ship has been the pioneer in ferreting +out the remotest and least known parts of the earth. She has +explored seas and archipelagoes which had no chart, where no Cook or +Vancouver had ever sailed. If American and European men-of-war +now peacefully ride in once savage harbors, let them fire salutes to +the honour and glory of the whale-ship, which originally showed them +the way, and first interpreted between them and the savages. They +may celebrate as they will the heroes of Exploring Expeditions, your +Cooks, your Krusensterns; but I say that scores of anonymous +Captains have sailed out of Nantucket, that were as great, and +greater than your Cook and your Krusenstern. For in their +succourless empty-handedness, they, in the heathenish sharked waters, +and by the beaches of unrecorded, javelin islands, battled with +virgin wonders and terrors that Cook with all his marines and +muskets would not willingly have dared. All that is made such a +flourish of in the old South Sea Voyages, those things were but the +life-time commonplaces of our heroic Nantucketers. Often, +adventures which Vancouver dedicates three chapters to, these men +accounted unworthy of being set down in the ship's common log. Ah, +the world! Oh, the world! + +Until the whale fishery rounded Cape Horn, no commerce but colonial, +scarcely any intercourse but colonial, was carried on between Europe +and the long line of the opulent Spanish provinces on the Pacific +coast. It was the whaleman who first broke through the jealous +policy of the Spanish crown, touching those colonies; and, if space +permitted, it might be distinctly shown how from those whalemen at +last eventuated the liberation of Peru, Chili, and Bolivia from the +yoke of Old Spain, and the establishment of the eternal democracy in +those parts. + +That great America on the other side of the sphere, Australia, was +given to the enlightened world by the whaleman. After its first +blunder-born discovery by a Dutchman, all other ships long shunned +those shores as pestiferously barbarous; but the whale-ship touched +there. The whale-ship is the true mother of that now mighty colony. +Moreover, in the infancy of the first Australian settlement, the +emigrants were several times saved from starvation by the benevolent +biscuit of the whale-ship luckily dropping an anchor in their waters. +The uncounted isles of all Polynesia confess the same truth, and do +commercial homage to the whale-ship, that cleared the way for the +missionary and the merchant, and in many cases carried the primitive +missionaries to their first destinations. If that double-bolted +land, Japan, is ever to become hospitable, it is the whale-ship alone +to whom the credit will be due; for already she is on the threshold. + +But if, in the face of all this, you still declare that whaling has +no aesthetically noble associations connected with it, then am I +ready to shiver fifty lances with you there, and unhorse you with a +split helmet every time. + +The whale has no famous author, and whaling no famous chronicler, you +will say. + +THE WHALE NO FAMOUS AUTHOR, AND WHALING NO FAMOUS CHRONICLER? Who +wrote the first account of our Leviathan? Who but mighty Job! And +who composed the first narrative of a whaling-voyage? Who, but no +less a prince than Alfred the Great, who, with his own royal pen, +took down the words from Other, the Norwegian whale-hunter of those +times! And who pronounced our glowing eulogy in Parliament? Who, +but Edmund Burke! + +True enough, but then whalemen themselves are poor devils; they have +no good blood in their veins. + +NO GOOD BLOOD IN THEIR VEINS? They have something better than royal +blood there. The grandmother of Benjamin Franklin was Mary Morrel; +afterwards, by marriage, Mary Folger, one of the old settlers of +Nantucket, and the ancestress to a long line of Folgers and +harpooneers--all kith and kin to noble Benjamin--this day darting the +barbed iron from one side of the world to the other. + +Good again; but then all confess that somehow whaling is not +respectable. + +WHALING NOT RESPECTABLE? Whaling is imperial! By old English +statutory law, the whale is declared "a royal fish."* + +Oh, that's only nominal! The whale himself has never figured in any +grand imposing way. + +THE WHALE NEVER FIGURED IN ANY GRAND IMPOSING WAY? In one of the +mighty triumphs given to a Roman general upon his entering the +world's capital, the bones of a whale, brought all the way from the +Syrian coast, were the most conspicuous object in the cymballed +procession.* + + +*See subsequent chapters for something more on this head. + + +Grant it, since you cite it; but, say what you will, there is no real +dignity in whaling. + +NO DIGNITY IN WHALING? The dignity of our calling the very heavens +attest. Cetus is a constellation in the South! No more! Drive +down your hat in presence of the Czar, and take it off to Queequeg! +No more! I know a man that, in his lifetime, has taken three hundred +and fifty whales. I account that man more honourable than that great +captain of antiquity who boasted of taking as many walled towns. + +And, as for me, if, by any possibility, there be any as yet +undiscovered prime thing in me; if I shall ever deserve any real +repute in that small but high hushed world which I might not be +unreasonably ambitious of; if hereafter I shall do anything that, upon +the whole, a man might rather have done than to have left undone; if, +at my death, my executors, or more properly my creditors, find any +precious MSS. in my desk, then here I prospectively ascribe all the +honour and the glory to whaling; for a whale-ship was my Yale College +and my Harvard. + + + +CHAPTER 25 + +Postscript. + + +In behalf of the dignity of whaling, I would fain advance naught but +substantiated facts. But after embattling his facts, an advocate who +should wholly suppress a not unreasonable surmise, which might tell +eloquently upon his cause--such an advocate, would he not be +blameworthy? + +It is well known that at the coronation of kings and queens, even +modern ones, a certain curious process of seasoning them for their +functions is gone through. There is a saltcellar of state, so +called, and there may be a castor of state. How they use the salt, +precisely--who knows? Certain I am, however, that a king's head is +solemnly oiled at his coronation, even as a head of salad. Can it +be, though, that they anoint it with a view of making its interior +run well, as they anoint machinery? Much might be ruminated here, +concerning the essential dignity of this regal process, because in +common life we esteem but meanly and contemptibly a fellow who +anoints his hair, and palpably smells of that anointing. In truth, a +mature man who uses hair-oil, unless medicinally, that man has +probably got a quoggy spot in him somewhere. As a general rule, he +can't amount to much in his totality. + +But the only thing to be considered here, is this--what kind of oil +is used at coronations? Certainly it cannot be olive oil, nor +macassar oil, nor castor oil, nor bear's oil, nor train oil, nor +cod-liver oil. What then can it possibly be, but sperm oil in +its unmanufactured, unpolluted state, the sweetest of all oils? + +Think of that, ye loyal Britons! we whalemen supply your kings and +queens with coronation stuff! + + + +CHAPTER 26 + +Knights and Squires. + + +The chief mate of the Pequod was Starbuck, a native of Nantucket, and +a Quaker by descent. He was a long, earnest man, and though born on +an icy coast, seemed well adapted to endure hot latitudes, his flesh +being hard as twice-baked biscuit. Transported to the Indies, his +live blood would not spoil like bottled ale. He must have been born +in some time of general drought and famine, or upon one of those fast +days for which his state is famous. Only some thirty arid summers +had he seen; those summers had dried up all his physical +superfluousness. But this, his thinness, so to speak, seemed no more +the token of wasting anxieties and cares, than it seemed the +indication of any bodily blight. It was merely the condensation of +the man. He was by no means ill-looking; quite the contrary. His +pure tight skin was an excellent fit; and closely wrapped up in it, +and embalmed with inner health and strength, like a revivified +Egyptian, this Starbuck seemed prepared to endure for long ages to +come, and to endure always, as now; for be it Polar snow or torrid +sun, like a patent chronometer, his interior vitality was warranted +to do well in all climates. Looking into his eyes, you seemed to +see there the yet lingering images of those thousand-fold perils he +had calmly confronted through life. A staid, steadfast man, whose +life for the most part was a telling pantomime of action, and not a +tame chapter of sounds. Yet, for all his hardy sobriety and +fortitude, there were certain qualities in him which at times +affected, and in some cases seemed well nigh to overbalance all the +rest. Uncommonly conscientious for a seaman, and endued with a deep +natural reverence, the wild watery loneliness of his life did +therefore strongly incline him to superstition; but to that sort of +superstition, which in some organizations seems rather to spring, +somehow, from intelligence than from ignorance. Outward portents and +inward presentiments were his. And if at times these things bent the +welded iron of his soul, much more did his far-away domestic memories +of his young Cape wife and child, tend to bend him still more from +the original ruggedness of his nature, and open him still further to +those latent influences which, in some honest-hearted men, restrain +the gush of dare-devil daring, so often evinced by others in the more +perilous vicissitudes of the fishery. "I will have no man in my +boat," said Starbuck, "who is not afraid of a whale." By this, he +seemed to mean, not only that the most reliable and useful courage +was that which arises from the fair estimation of the encountered +peril, but that an utterly fearless man is a far more dangerous +comrade than a coward. + +"Aye, aye," said Stubb, the second mate, "Starbuck, there, is as +careful a man as you'll find anywhere in this fishery." But we shall +ere long see what that word "careful" precisely means when used by a +man like Stubb, or almost any other whale hunter. + +Starbuck was no crusader after perils; in him courage was not a +sentiment; but a thing simply useful to him, and always at hand upon +all mortally practical occasions. Besides, he thought, perhaps, that +in this business of whaling, courage was one of the great staple +outfits of the ship, like her beef and her bread, and not to be +foolishly wasted. Wherefore he had no fancy for lowering for whales +after sun-down; nor for persisting in fighting a fish that too much +persisted in fighting him. For, thought Starbuck, I am here in this +critical ocean to kill whales for my living, and not to be killed by +them for theirs; and that hundreds of men had been so killed Starbuck +well knew. What doom was his own father's? Where, in the bottomless +deeps, could he find the torn limbs of his brother? + +With memories like these in him, and, moreover, given to a certain +superstitiousness, as has been said; the courage of this Starbuck +which could, nevertheless, still flourish, must indeed have been +extreme. But it was not in reasonable nature that a man so +organized, and with such terrible experiences and remembrances as he +had; it was not in nature that these things should fail in latently +engendering an element in him, which, under suitable circumstances, +would break out from its confinement, and burn all his courage up. +And brave as he might be, it was that sort of bravery chiefly, +visible in some intrepid men, which, while generally abiding firm in +the conflict with seas, or winds, or whales, or any of the ordinary +irrational horrors of the world, yet cannot withstand those more +terrific, because more spiritual terrors, which sometimes menace you +from the concentrating brow of an enraged and mighty man. + +But were the coming narrative to reveal in any instance, the complete +abasement of poor Starbuck's fortitude, scarce might I have the heart +to write it; for it is a thing most sorrowful, nay shocking, to +expose the fall of valour in the soul. Men may seem detestable as +joint stock-companies and nations; knaves, fools, and murderers there +may be; men may have mean and meagre faces; but man, in the ideal, +is so noble and so sparkling, such a grand and glowing creature, that +over any ignominious blemish in him all his fellows should run to +throw their costliest robes. That immaculate manliness we feel +within ourselves, so far within us, that it remains intact though all +the outer character seem gone; bleeds with keenest anguish at the +undraped spectacle of a valor-ruined man. Nor can piety itself, at +such a shameful sight, completely stifle her upbraidings against the +permitting stars. But this august dignity I treat of, is not the +dignity of kings and robes, but that abounding dignity which has no +robed investiture. Thou shalt see it shining in the arm that wields +a pick or drives a spike; that democratic dignity which, on all +hands, radiates without end from God; Himself! The great God +absolute! The centre and circumference of all democracy! His +omnipresence, our divine equality! + +If, then, to meanest mariners, and renegades and castaways, I shall +hereafter ascribe high qualities, though dark; weave round them +tragic graces; if even the most mournful, perchance the most abased, +among them all, shall at times lift himself to the exalted mounts; if +I shall touch that workman's arm with some ethereal light; if I shall +spread a rainbow over his disastrous set of sun; then against all +mortal critics bear me out in it, thou Just Spirit of Equality, +which hast spread one royal mantle of humanity over all my kind! +Bear me out in it, thou great democratic God! who didst not refuse to +the swart convict, Bunyan, the pale, poetic pearl; Thou who didst +clothe with doubly hammered leaves of finest gold, the stumped and +paupered arm of old Cervantes; Thou who didst pick up Andrew Jackson +from the pebbles; who didst hurl him upon a war-horse; who didst +thunder him higher than a throne! Thou who, in all Thy mighty, +earthly marchings, ever cullest Thy selectest champions from the +kingly commons; bear me out in it, O God! + + + +CHAPTER 27 + +Knights and Squires. + + +Stubb was the second mate. He was a native of Cape Cod; and hence, +according to local usage, was called a Cape-Cod-man. A +happy-go-lucky; neither craven nor valiant; taking perils as they +came with an indifferent air; and while engaged in the most imminent +crisis of the chase, toiling away, calm and collected as a journeyman +joiner engaged for the year. Good-humored, easy, and careless, he +presided over his whale-boat as if the most deadly encounter were but +a dinner, and his crew all invited guests. He was as particular +about the comfortable arrangement of his part of the boat, as an +old stage-driver is about the snugness of his box. When close to the +whale, in the very death-lock of the fight, he handled his unpitying +lance coolly and off-handedly, as a whistling tinker his hammer. He +would hum over his old rigadig tunes while flank and flank with the +most exasperated monster. Long usage had, for this Stubb, converted +the jaws of death into an easy chair. What he thought of death +itself, there is no telling. Whether he ever thought of it at all, +might be a question; but, if he ever did chance to cast his mind that +way after a comfortable dinner, no doubt, like a good sailor, he took +it to be a sort of call of the watch to tumble aloft, and bestir +themselves there, about something which he would find out when he +obeyed the order, and not sooner. + +What, perhaps, with other things, made Stubb such an easy-going, +unfearing man, so cheerily trudging off with the burden of life in a +world full of grave pedlars, all bowed to the ground with their +packs; what helped to bring about that almost impious good-humor of +his; that thing must have been his pipe. For, like his nose, his +short, black little pipe was one of the regular features of his face. +You would almost as soon have expected him to turn out of his bunk +without his nose as without his pipe. He kept a whole row of pipes +there ready loaded, stuck in a rack, within easy reach of his hand; +and, whenever he turned in, he smoked them all out in succession, +lighting one from the other to the end of the chapter; then loading +them again to be in readiness anew. For, when Stubb dressed, instead +of first putting his legs into his trowsers, he put his pipe into his +mouth. + +I say this continual smoking must have been one cause, at least, of +his peculiar disposition; for every one knows that this earthly air, +whether ashore or afloat, is terribly infected with the nameless +miseries of the numberless mortals who have died exhaling it; and as +in time of the cholera, some people go about with a camphorated +handkerchief to their mouths; so, likewise, against all mortal +tribulations, Stubb's tobacco smoke might have operated as a sort of +disinfecting agent. + +The third mate was Flask, a native of Tisbury, in Martha's Vineyard. +A short, stout, ruddy young fellow, very pugnacious concerning +whales, who somehow seemed to think that the great leviathans had +personally and hereditarily affronted him; and therefore it was a +sort of point of honour with him, to destroy them whenever +encountered. So utterly lost was he to all sense of reverence for +the many marvels of their majestic bulk and mystic ways; and so dead +to anything like an apprehension of any possible danger from +encountering them; that in his poor opinion, the wondrous whale was +but a species of magnified mouse, or at least water-rat, requiring +only a little circumvention and some small application of time and +trouble in order to kill and boil. This ignorant, unconscious +fearlessness of his made him a little waggish in the matter of +whales; he followed these fish for the fun of it; and a three years' +voyage round Cape Horn was only a jolly joke that lasted that length +of time. As a carpenter's nails are divided into wrought nails and +cut nails; so mankind may be similarly divided. Little Flask was one +of the wrought ones; made to clinch tight and last long. They called +him King-Post on board of the Pequod; because, in form, he could be +well likened to the short, square timber known by that name in Arctic +whalers; and which by the means of many radiating side timbers +inserted into it, serves to brace the ship against the icy +concussions of those battering seas. + +Now these three mates--Starbuck, Stubb, and Flask, were momentous +men. They it was who by universal prescription commanded three of the +Pequod's boats as headsmen. In that grand order of battle in which +Captain Ahab would probably marshal his forces to descend on the +whales, these three headsmen were as captains of companies. Or, +being armed with their long keen whaling spears, they were as a +picked trio of lancers; even as the harpooneers were flingers of +javelins. + +And since in this famous fishery, each mate or headsman, like a +Gothic Knight of old, is always accompanied by his boat-steerer or +harpooneer, who in certain conjunctures provides him with a fresh +lance, when the former one has been badly twisted, or elbowed in the +assault; and moreover, as there generally subsists between the two, a +close intimacy and friendliness; it is therefore but meet, that in +this place we set down who the Pequod's harpooneers were, and to what +headsman each of them belonged. + +First of all was Queequeg, whom Starbuck, the chief mate, had +selected for his squire. But Queequeg is already known. + +Next was Tashtego, an unmixed Indian from Gay Head, the most westerly +promontory of Martha's Vineyard, where there still exists the last +remnant of a village of red men, which has long supplied the +neighboring island of Nantucket with many of her most daring +harpooneers. In the fishery, they usually go by the generic name of +Gay-Headers. Tashtego's long, lean, sable hair, his high cheek +bones, and black rounding eyes--for an Indian, Oriental in their +largeness, but Antarctic in their glittering expression--all this +sufficiently proclaimed him an inheritor of the unvitiated blood of +those proud warrior hunters, who, in quest of the great New England +moose, had scoured, bow in hand, the aboriginal forests of the main. +But no longer snuffing in the trail of the wild beasts of the +woodland, Tashtego now hunted in the wake of the great whales of the +sea; the unerring harpoon of the son fitly replacing the infallible +arrow of the sires. To look at the tawny brawn of his lithe snaky +limbs, you would almost have credited the superstitions of some of +the earlier Puritans, and half-believed this wild Indian to be a son +of the Prince of the Powers of the Air. Tashtego was Stubb the +second mate's squire. + +Third among the harpooneers was Daggoo, a gigantic, coal-black +negro-savage, with a lion-like tread--an Ahasuerus to behold. +Suspended from his ears were two golden hoops, so large that the +sailors called them ring-bolts, and would talk of securing the +top-sail halyards to them. In his youth Daggoo had voluntarily +shipped on board of a whaler, lying in a lonely bay on his native +coast. And never having been anywhere in the world but in Africa, +Nantucket, and the pagan harbors most frequented by whalemen; and +having now led for many years the bold life of the fishery in the +ships of owners uncommonly heedful of what manner of men they +shipped; Daggoo retained all his barbaric virtues, and erect as a +giraffe, moved about the decks in all the pomp of six feet five in +his socks. There was a corporeal humility in looking up at him; and +a white man standing before him seemed a white flag come to beg truce +of a fortress. Curious to tell, this imperial negro, Ahasuerus +Daggoo, was the Squire of little Flask, who looked like a chess-man +beside him. As for the residue of the Pequod's company, be it said, +that at the present day not one in two of the many thousand men +before the mast employed in the American whale fishery, are Americans +born, though pretty nearly all the officers are. Herein it is the +same with the American whale fishery as with the American army and +military and merchant navies, and the engineering forces employed in +the construction of the American Canals and Railroads. The same, I +say, because in all these cases the native American liberally +provides the brains, the rest of the world as generously supplying +the muscles. No small number of these whaling seamen belong to the +Azores, where the outward bound Nantucket whalers frequently touch to +augment their crews from the hardy peasants of those rocky shores. +In like manner, the Greenland whalers sailing out of Hull or London, +put in at the Shetland Islands, to receive the full complement of +their crew. Upon the passage homewards, they drop them there again. +How it is, there is no telling, but Islanders seem to make the best +whalemen. They were nearly all Islanders in the Pequod, ISOLATOES +too, I call such, not acknowledging the common continent of men, but +each ISOLATO living on a separate continent of his own. Yet now, +federated along one keel, what a set these Isolatoes were! An +Anacharsis Clootz deputation from all the isles of the sea, and all +the ends of the earth, accompanying Old Ahab in the Pequod to lay the +world's grievances before that bar from which not very many of them +ever come back. Black Little Pip--he never did--oh, no! he went +before. Poor Alabama boy! On the grim Pequod's forecastle, ye shall +ere long see him, beating his tambourine; prelusive of the eternal +time, when sent for, to the great quarter-deck on high, he was bid +strike in with angels, and beat his tambourine in glory; called a +coward here, hailed a hero there! + + + +CHAPTER 28 + +Ahab. + + +For several days after leaving Nantucket, nothing above hatches was +seen of Captain Ahab. The mates regularly relieved each other at the +watches, and for aught that could be seen to the contrary, they +seemed to be the only commanders of the ship; only they sometimes +issued from the cabin with orders so sudden and peremptory, that +after all it was plain they but commanded vicariously. Yes, their +supreme lord and dictator was there, though hitherto unseen by any +eyes not permitted to penetrate into the now sacred retreat of the +cabin. + +Every time I ascended to the deck from my watches below, I instantly +gazed aft to mark if any strange face were visible; for my first +vague disquietude touching the unknown captain, now in the seclusion +of the sea, became almost a perturbation. This was strangely +heightened at times by the ragged Elijah's diabolical incoherences +uninvitedly recurring to me, with a subtle energy I could not have +before conceived of. But poorly could I withstand them, much as in +other moods I was almost ready to smile at the solemn whimsicalities +of that outlandish prophet of the wharves. But whatever it was of +apprehensiveness or uneasiness--to call it so--which I felt, yet +whenever I came to look about me in the ship, it seemed against all +warrantry to cherish such emotions. For though the harpooneers, with +the great body of the crew, were a far more barbaric, heathenish, and +motley set than any of the tame merchant-ship companies which my +previous experiences had made me acquainted with, still I ascribed +this--and rightly ascribed it--to the fierce uniqueness of the very +nature of that wild Scandinavian vocation in which I had so +abandonedly embarked. But it was especially the aspect of the three +chief officers of the ship, the mates, which was most forcibly +calculated to allay these colourless misgivings, and induce confidence +and cheerfulness in every presentment of the voyage. Three better, +more likely sea-officers and men, each in his own different way, +could not readily be found, and they were every one of them +Americans; a Nantucketer, a Vineyarder, a Cape man. Now, it being +Christmas when the ship shot from out her harbor, for a space we had +biting Polar weather, though all the time running away from it to the +southward; and by every degree and minute of latitude which we +sailed, gradually leaving that merciless winter, and all its +intolerable weather behind us. It was one of those less lowering, +but still grey and gloomy enough mornings of the transition, when +with a fair wind the ship was rushing through the water with a +vindictive sort of leaping and melancholy rapidity, that as I mounted +to the deck at the call of the forenoon watch, so soon as I levelled +my glance towards the taffrail, foreboding shivers ran over me. +Reality outran apprehension; Captain Ahab stood upon his +quarter-deck. + +There seemed no sign of common bodily illness about him, nor of the +recovery from any. He looked like a man cut away from the stake, +when the fire has overrunningly wasted all the limbs without +consuming them, or taking away one particle from their compacted aged +robustness. His whole high, broad form, seemed made of solid bronze, +and shaped in an unalterable mould, like Cellini's cast Perseus. +Threading its way out from among his grey hairs, and continuing right +down one side of his tawny scorched face and neck, till it +disappeared in his clothing, you saw a slender rod-like mark, lividly +whitish. It resembled that perpendicular seam sometimes made in the +straight, lofty trunk of a great tree, when the upper lightning +tearingly darts down it, and without wrenching a single twig, peels +and grooves out the bark from top to bottom, ere running off into the +soil, leaving the tree still greenly alive, but branded. Whether +that mark was born with him, or whether it was the scar left by some +desperate wound, no one could certainly say. By some tacit consent, +throughout the voyage little or no allusion was made to it, +especially by the mates. But once Tashtego's senior, an old Gay-Head +Indian among the crew, superstitiously asserted that not till he was +full forty years old did Ahab become that way branded, and then it +came upon him, not in the fury of any mortal fray, but in an +elemental strife at sea. Yet, this wild hint seemed inferentially +negatived, by what a grey Manxman insinuated, an old sepulchral man, +who, having never before sailed out of Nantucket, had never ere this +laid eye upon wild Ahab. Nevertheless, the old sea-traditions, the +immemorial credulities, popularly invested this old Manxman with +preternatural powers of discernment. So that no white sailor +seriously contradicted him when he said that if ever Captain Ahab +should be tranquilly laid out--which might hardly come to pass, so he +muttered--then, whoever should do that last office for the dead, +would find a birth-mark on him from crown to sole. + +So powerfully did the whole grim aspect of Ahab affect me, and the +livid brand which streaked it, that for the first few moments I +hardly noted that not a little of this overbearing grimness was owing +to the barbaric white leg upon which he partly stood. It had +previously come to me that this ivory leg had at sea been fashioned +from the polished bone of the sperm whale's jaw. "Aye, he was +dismasted off Japan," said the old Gay-Head Indian once; "but like +his dismasted craft, he shipped another mast without coming home for +it. He has a quiver of 'em." + +I was struck with the singular posture he maintained. Upon each side +of the Pequod's quarter deck, and pretty close to the mizzen shrouds, +there was an auger hole, bored about half an inch or so, into the +plank. His bone leg steadied in that hole; one arm elevated, and +holding by a shroud; Captain Ahab stood erect, looking straight out +beyond the ship's ever-pitching prow. There was an infinity of +firmest fortitude, a determinate, unsurrenderable wilfulness, in the +fixed and fearless, forward dedication of that glance. Not a word he +spoke; nor did his officers say aught to him; though by all their +minutest gestures and expressions, they plainly showed the uneasy, if +not painful, consciousness of being under a troubled master-eye. And +not only that, but moody stricken Ahab stood before them with a +crucifixion in his face; in all the nameless regal overbearing +dignity of some mighty woe. + +Ere long, from his first visit in the air, he withdrew into his +cabin. But after that morning, he was every day visible to the crew; +either standing in his pivot-hole, or seated upon an ivory stool he +had; or heavily walking the deck. As the sky grew less gloomy; +indeed, began to grow a little genial, he became still less and less +a recluse; as if, when the ship had sailed from home, nothing but the +dead wintry bleakness of the sea had then kept him so secluded. And, +by and by, it came to pass, that he was almost continually in the +air; but, as yet, for all that he said, or perceptibly did, on the at +last sunny deck, he seemed as unnecessary there as another mast. But +the Pequod was only making a passage now; not regularly cruising; +nearly all whaling preparatives needing supervision the mates were +fully competent to, so that there was little or nothing, out of +himself, to employ or excite Ahab, now; and thus chase away, for that +one interval, the clouds that layer upon layer were piled upon his +brow, as ever all clouds choose the loftiest peaks to pile themselves +upon. + +Nevertheless, ere long, the warm, warbling persuasiveness of the +pleasant, holiday weather we came to, seemed gradually to charm him +from his mood. For, as when the red-cheeked, dancing girls, April +and May, trip home to the wintry, misanthropic woods; even the +barest, ruggedest, most thunder-cloven old oak will at least send +forth some few green sprouts, to welcome such glad-hearted visitants; +so Ahab did, in the end, a little respond to the playful allurings of +that girlish air. More than once did he put forth the faint blossom +of a look, which, in any other man, would have soon flowered out in a +smile. + + + +CHAPTER 29 + +Enter Ahab; to Him, Stubb. + + +Some days elapsed, and ice and icebergs all astern, the Pequod now +went rolling through the bright Quito spring, which, at sea, almost +perpetually reigns on the threshold of the eternal August of the +Tropic. The warmly cool, clear, ringing, perfumed, overflowing, +redundant days, were as crystal goblets of Persian sherbet, heaped +up--flaked up, with rose-water snow. The starred and stately nights +seemed haughty dames in jewelled velvets, nursing at home in lonely +pride, the memory of their absent conquering Earls, the golden +helmeted suns! For sleeping man, 'twas hard to choose between such +winsome days and such seducing nights. But all the witcheries of +that unwaning weather did not merely lend new spells and potencies to +the outward world. Inward they turned upon the soul, especially when +the still mild hours of eve came on; then, memory shot her crystals +as the clear ice most forms of noiseless twilights. And all these +subtle agencies, more and more they wrought on Ahab's texture. + +Old age is always wakeful; as if, the longer linked with life, the +less man has to do with aught that looks like death. Among +sea-commanders, the old greybeards will oftenest leave their berths +to visit the night-cloaked deck. It was so with Ahab; only that now, +of late, he seemed so much to live in the open air, that truly +speaking, his visits were more to the cabin, than from the cabin to +the planks. "It feels like going down into one's tomb,"--he would +mutter to himself--"for an old captain like me to be descending this +narrow scuttle, to go to my grave-dug berth." + +So, almost every twenty-four hours, when the watches of the night +were set, and the band on deck sentinelled the slumbers of the band +below; and when if a rope was to be hauled upon the forecastle, the +sailors flung it not rudely down, as by day, but with some +cautiousness dropt it to its place for fear of disturbing their +slumbering shipmates; when this sort of steady quietude would begin +to prevail, habitually, the silent steersman would watch the +cabin-scuttle; and ere long the old man would emerge, gripping at the +iron banister, to help his crippled way. Some considering touch of +humanity was in him; for at times like these, he usually abstained +from patrolling the quarter-deck; because to his wearied mates, +seeking repose within six inches of his ivory heel, such would have +been the reverberating crack and din of that bony step, that their +dreams would have been on the crunching teeth of sharks. But once, +the mood was on him too deep for common regardings; and as with +heavy, lumber-like pace he was measuring the ship from taffrail to +mainmast, Stubb, the old second mate, came up from below, with a +certain unassured, deprecating humorousness, hinted that if Captain +Ahab was pleased to walk the planks, then, no one could say nay; but +there might be some way of muffling the noise; hinting something +indistinctly and hesitatingly about a globe of tow, and the insertion +into it, of the ivory heel. Ah! Stubb, thou didst not know Ahab +then. + +"Am I a cannon-ball, Stubb," said Ahab, "that thou wouldst wad me +that fashion? But go thy ways; I had forgot. Below to thy nightly +grave; where such as ye sleep between shrouds, to use ye to the +filling one at last.--Down, dog, and kennel!" + +Starting at the unforseen concluding exclamation of the so suddenly +scornful old man, Stubb was speechless a moment; then said excitedly, +"I am not used to be spoken to that way, sir; I do but less than half +like it, sir." + +"Avast! gritted Ahab between his set teeth, and violently moving +away, as if to avoid some passionate temptation. + +"No, sir; not yet," said Stubb, emboldened, "I will not tamely be +called a dog, sir." + +"Then be called ten times a donkey, and a mule, and an ass, and +begone, or I'll clear the world of thee!" + +As he said this, Ahab advanced upon him with such overbearing terrors +in his aspect, that Stubb involuntarily retreated. + +"I was never served so before without giving a hard blow for it," +muttered Stubb, as he found himself descending the cabin-scuttle. +"It's very queer. Stop, Stubb; somehow, now, I don't well know +whether to go back and strike him, or--what's that?--down here on my +knees and pray for him? Yes, that was the thought coming up in me; +but it would be the first time I ever DID pray. It's queer; very +queer; and he's queer too; aye, take him fore and aft, he's about the +queerest old man Stubb ever sailed with. How he flashed at me!--his +eyes like powder-pans! is he mad? Anyway there's something on his +mind, as sure as there must be something on a deck when it cracks. +He aint in his bed now, either, more than three hours out of the +twenty-four; and he don't sleep then. Didn't that Dough-Boy, the +steward, tell me that of a morning he always finds the old man's +hammock clothes all rumpled and tumbled, and the sheets down at the +foot, and the coverlid almost tied into knots, and the pillow a sort +of frightful hot, as though a baked brick had been on it? A hot old +man! I guess he's got what some folks ashore call a conscience; it's +a kind of Tic-Dolly-row they say--worse nor a toothache. Well, well; +I don't know what it is, but the Lord keep me from catching it. He's +full of riddles; I wonder what he goes into the after hold for, every +night, as Dough-Boy tells me he suspects; what's that for, I should +like to know? Who's made appointments with him in the hold? Ain't +that queer, now? But there's no telling, it's the old game--Here +goes for a snooze. Damn me, it's worth a fellow's while to be born +into the world, if only to fall right asleep. And now that I think +of it, that's about the first thing babies do, and that's a sort of +queer, too. Damn me, but all things are queer, come to think of 'em. +But that's against my principles. Think not, is my eleventh +commandment; and sleep when you can, is my twelfth--So here goes +again. But how's that? didn't he call me a dog? blazes! he called me +ten times a donkey, and piled a lot of jackasses on top of THAT! He +might as well have kicked me, and done with it. Maybe he DID kick +me, and I didn't observe it, I was so taken all aback with his brow, +somehow. It flashed like a bleached bone. What the devil's the +matter with me? I don't stand right on my legs. Coming afoul of +that old man has a sort of turned me wrong side out. By the Lord, I +must have been dreaming, though--How? how? how?--but the only way's +to stash it; so here goes to hammock again; and in the morning, I'll +see how this plaguey juggling thinks over by daylight." + + + +CHAPTER 30 + +The Pipe. + + +When Stubb had departed, Ahab stood for a while leaning over the +bulwarks; and then, as had been usual with him of late, calling a +sailor of the watch, he sent him below for his ivory stool, and also +his pipe. Lighting the pipe at the binnacle lamp and planting the +stool on the weather side of the deck, he sat and smoked. + +In old Norse times, the thrones of the sea-loving Danish kings were +fabricated, saith tradition, of the tusks of the narwhale. How could +one look at Ahab then, seated on that tripod of bones, without +bethinking him of the royalty it symbolized? For a Khan of the +plank, and a king of the sea, and a great lord of Leviathans was +Ahab. + +Some moments passed, during which the thick vapour came from his mouth +in quick and constant puffs, which blew back again into his face. +"How now," he soliloquized at last, withdrawing the tube, "this +smoking no longer soothes. Oh, my pipe! hard must it go with me if +thy charm be gone! Here have I been unconsciously toiling, not +pleasuring--aye, and ignorantly smoking to windward all the while; to +windward, and with such nervous whiffs, as if, like the dying whale, +my final jets were the strongest and fullest of trouble. What +business have I with this pipe? This thing that is meant for +sereneness, to send up mild white vapours among mild white hairs, not +among torn iron-grey locks like mine. I'll smoke no more--" + +He tossed the still lighted pipe into the sea. The fire hissed in +the waves; the same instant the ship shot by the bubble the sinking +pipe made. With slouched hat, Ahab lurchingly paced the planks. + + + +CHAPTER 31 + +Queen Mab. + + +Next morning Stubb accosted Flask. + +"Such a queer dream, King-Post, I never had. You know the old man's +ivory leg, well I dreamed he kicked me with it; and when I tried to +kick back, upon my soul, my little man, I kicked my leg right off! +And then, presto! Ahab seemed a pyramid, and I, like a blazing fool, +kept kicking at it. But what was still more curious, Flask--you know +how curious all dreams are--through all this rage that I was in, I +somehow seemed to be thinking to myself, that after all, it was not +much of an insult, that kick from Ahab. 'Why,' thinks I, 'what's the +row? It's not a real leg, only a false leg.' And there's a mighty +difference between a living thump and a dead thump. That's what +makes a blow from the hand, Flask, fifty times more savage to bear +than a blow from a cane. The living member--that makes the living +insult, my little man. And thinks I to myself all the while, mind, +while I was stubbing my silly toes against that cursed pyramid--so +confoundedly contradictory was it all, all the while, I say, I was +thinking to myself, 'what's his leg now, but a cane--a whalebone +cane. Yes,' thinks I, 'it was only a playful cudgelling--in fact, +only a whaleboning that he gave me--not a base kick. Besides,' +thinks I, 'look at it once; why, the end of it--the foot part--what a +small sort of end it is; whereas, if a broad footed farmer kicked me, +THERE'S a devilish broad insult. But this insult is whittled down to +a point only.' But now comes the greatest joke of the dream, Flask. +While I was battering away at the pyramid, a sort of badger-haired +old merman, with a hump on his back, takes me by the shoulders, and +slews me round. 'What are you 'bout?' says he. Slid! man, but I was +frightened. Such a phiz! But, somehow, next moment I was over the +fright. 'What am I about?' says I at last. 'And what business is +that of yours, I should like to know, Mr. Humpback? Do YOU want a +kick?' By the lord, Flask, I had no sooner said that, than he turned +round his stern to me, bent over, and dragging up a lot of seaweed he +had for a clout--what do you think, I saw?--why thunder alive, man, +his stern was stuck full of marlinspikes, with the points out. Says +I, on second thoughts, 'I guess I won't kick you, old fellow.' 'Wise +Stubb,' said he, 'wise Stubb;' and kept muttering it all the time, a +sort of eating of his own gums like a chimney hag. Seeing he wasn't +going to stop saying over his 'wise Stubb, wise Stubb,' I thought I +might as well fall to kicking the pyramid again. But I had only just +lifted my foot for it, when he roared out, 'Stop that kicking!' +'Halloa,' says I, 'what's the matter now, old fellow?' 'Look ye +here,' says he; 'let's argue the insult. Captain Ahab kicked ye, +didn't he?' 'Yes, he did,' says I--'right HERE it was.' 'Very +good,' says he--'he used his ivory leg, didn't he?' 'Yes, he did,' +says I. 'Well then,' says he, 'wise Stubb, what have you to complain +of? Didn't he kick with right good will? it wasn't a common pitch +pine leg he kicked with, was it? No, you were kicked by a great man, +and with a beautiful ivory leg, Stubb. It's an honour; I consider it +an honour. Listen, wise Stubb. In old England the greatest lords +think it great glory to be slapped by a queen, and made +garter-knights of; but, be YOUR boast, Stubb, that ye were kicked by +old Ahab, and made a wise man of. Remember what I say; BE kicked by +him; account his kicks honours; and on no account kick back; for you +can't help yourself, wise Stubb. Don't you see that pyramid?' With +that, he all of a sudden seemed somehow, in some queer fashion, to +swim off into the air. I snored; rolled over; and there I was in my +hammock! Now, what do you think of that dream, Flask?" + +"I don't know; it seems a sort of foolish to me, tho.'" + +"May be; may be. But it's made a wise man of me, Flask. D'ye see +Ahab standing there, sideways looking over the stern? Well, the best +thing you can do, Flask, is to let the old man alone; never speak to +him, whatever he says. Halloa! What's that he shouts? Hark!" + +"Mast-head, there! Look sharp, all of ye! There are whales +hereabouts! + +If ye see a white one, split your lungs for him! + +"What do you think of that now, Flask? ain't there a small drop of +something queer about that, eh? A white whale--did ye mark that, +man? Look ye--there's something special in the wind. Stand by for +it, Flask. Ahab has that that's bloody on his mind. But, mum; he +comes this way." + + + +CHAPTER 32 + +Cetology. + + +Already we are boldly launched upon the deep; but soon we shall be +lost in its unshored, harbourless immensities. Ere that come to pass; +ere the Pequod's weedy hull rolls side by side with the barnacled +hulls of the leviathan; at the outset it is but well to attend to a +matter almost indispensable to a thorough appreciative understanding +of the more special leviathanic revelations and allusions of all +sorts which are to follow. + +It is some systematized exhibition of the whale in his broad genera, +that I would now fain put before you. Yet is it no easy task. The +classification of the constituents of a chaos, nothing less is here +essayed. Listen to what the best and latest authorities have laid +down. + +"No branch of Zoology is so much involved as that which is entitled +Cetology," says Captain Scoresby, A.D. 1820. + +"It is not my intention, were it in my power, to enter into the +inquiry as to the true method of dividing the cetacea into groups and +families.... Utter confusion exists among the historians of this +animal" (sperm whale), says Surgeon Beale, A.D. 1839. + +"Unfitness to pursue our research in the unfathomable waters." +"Impenetrable veil covering our knowledge of the cetacea." "A field +strewn with thorns." "All these incomplete indications but serve to +torture us naturalists." + +Thus speak of the whale, the great Cuvier, and John Hunter, and +Lesson, those lights of zoology and anatomy. Nevertheless, though of +real knowledge there be little, yet of books there are a plenty; and +so in some small degree, with cetology, or the science of whales. +Many are the men, small and great, old and new, landsmen and seamen, +who have at large or in little, written of the whale. Run over a +few:--The Authors of the Bible; Aristotle; Pliny; Aldrovandi; Sir +Thomas Browne; Gesner; Ray; Linnaeus; Rondeletius; Willoughby; Green; +Artedi; Sibbald; Brisson; Marten; Lacepede; Bonneterre; Desmarest; +Baron Cuvier; Frederick Cuvier; John Hunter; Owen; Scoresby; Beale; +Bennett; J. Ross Browne; the Author of Miriam Coffin; Olmstead; and +the Rev. T. Cheever. But to what ultimate generalizing purpose all +these have written, the above cited extracts will show. + +Of the names in this list of whale authors, only those following Owen +ever saw living whales; and but one of them was a real professional +harpooneer and whaleman. I mean Captain Scoresby. On the separate +subject of the Greenland or right-whale, he is the best existing +authority. But Scoresby knew nothing and says nothing of the great +sperm whale, compared with which the Greenland whale is almost +unworthy mentioning. And here be it said, that the Greenland whale +is an usurper upon the throne of the seas. He is not even by any +means the largest of the whales. Yet, owing to the long priority of +his claims, and the profound ignorance which, till some seventy years +back, invested the then fabulous or utterly unknown sperm-whale, and +which ignorance to this present day still reigns in all but some few +scientific retreats and whale-ports; this usurpation has been every +way complete. Reference to nearly all the leviathanic allusions in +the great poets of past days, will satisfy you that the Greenland +whale, without one rival, was to them the monarch of the seas. But +the time has at last come for a new proclamation. This is Charing +Cross; hear ye! good people all,--the Greenland whale is +deposed,--the great sperm whale now reigneth! + +There are only two books in being which at all pretend to put the +living sperm whale before you, and at the same time, in the remotest +degree succeed in the attempt. Those books are Beale's and +Bennett's; both in their time surgeons to English South-Sea +whale-ships, and both exact and reliable men. The original matter +touching the sperm whale to be found in their volumes is necessarily +small; but so far as it goes, it is of excellent quality, though +mostly confined to scientific description. As yet, however, the +sperm whale, scientific or poetic, lives not complete in any +literature. Far above all other hunted whales, his is an unwritten +life. + +Now the various species of whales need some sort of popular +comprehensive classification, if only an easy outline one for the +present, hereafter to be filled in all its departments by subsequent +laborers. As no better man advances to take this matter in hand, I +hereupon offer my own poor endeavors. I promise nothing complete; +because any human thing supposed to be complete, must for that very +reason infallibly be faulty. I shall not pretend to a minute +anatomical description of the various species, or--in this place at +least--to much of any description. My object here is simply to +project the draught of a systematization of cetology. I am the +architect, not the builder. + +But it is a ponderous task; no ordinary letter-sorter in the +Post-Office is equal to it. To grope down into the bottom of the sea +after them; to have one's hands among the unspeakable foundations, +ribs, and very pelvis of the world; this is a fearful thing. What am +I that I should essay to hook the nose of this leviathan! The awful +tauntings in Job might well appal me. "Will he the (leviathan) make +a covenant with thee? Behold the hope of him is vain! But I have +swam through libraries and sailed through oceans; I have had to do +with whales with these visible hands; I am in earnest; and I will +try. There are some preliminaries to settle. + +First: The uncertain, unsettled condition of this science of Cetology +is in the very vestibule attested by the fact, that in some quarters +it still remains a moot point whether a whale be a fish. In his +System of Nature, A.D. 1776, Linnaeus declares, "I hereby separate +the whales from the fish." But of my own knowledge, I know that down +to the year 1850, sharks and shad, alewives and herring, against +Linnaeus's express edict, were still found dividing the possession of +the same seas with the Leviathan. + +The grounds upon which Linnaeus would fain have banished the whales +from the waters, he states as follows: "On account of their warm +bilocular heart, their lungs, their movable eyelids, their hollow +ears, penem intrantem feminam mammis lactantem," and finally, "ex +lege naturae jure meritoque." I submitted all this to my friends +Simeon Macey and Charley Coffin, of Nantucket, both messmates of mine +in a certain voyage, and they united in the opinion that the reasons +set forth were altogether insufficient. Charley profanely hinted +they were humbug. + +Be it known that, waiving all argument, I take the good old fashioned +ground that the whale is a fish, and call upon holy Jonah to back me. +This fundamental thing settled, the next point is, in what internal +respect does the whale differ from other fish. Above, Linnaeus has +given you those items. But in brief, they are these: lungs and warm +blood; whereas, all other fish are lungless and cold blooded. + +Next: how shall we define the whale, by his obvious externals, so as +conspicuously to label him for all time to come? To be short, then, +a whale is A SPOUTING FISH WITH A HORIZONTAL TAIL. There you have +him. However contracted, that definition is the result of expanded +meditation. A walrus spouts much like a whale, but the walrus is not +a fish, because he is amphibious. But the last term of the +definition is still more cogent, as coupled with the first. Almost +any one must have noticed that all the fish familiar to landsmen have +not a flat, but a vertical, or up-and-down tail. Whereas, among +spouting fish the tail, though it may be similarly shaped, invariably +assumes a horizontal position. + +By the above definition of what a whale is, I do by no means exclude +from the leviathanic brotherhood any sea creature hitherto identified +with the whale by the best informed Nantucketers; nor, on the other +hand, link with it any fish hitherto authoritatively regarded as +alien.* Hence, all the smaller, spouting, and horizontal tailed fish +must be included in this ground-plan of Cetology. Now, then, come +the grand divisions of the entire whale host. + + +*I am aware that down to the present time, the fish styled Lamatins +and Dugongs (Pig-fish and Sow-fish of the Coffins of Nantucket) are +included by many naturalists among the whales. But as these pig-fish +are a noisy, contemptible set, mostly lurking in the mouths of +rivers, and feeding on wet hay, and especially as they do not spout, +I deny their credentials as whales; and have presented them with +their passports to quit the Kingdom of Cetology. + + +First: According to magnitude I divide the whales into three primary +BOOKS (subdivisible into CHAPTERS), and these shall comprehend them +all, both small and large. + +I. THE FOLIO WHALE; II. the OCTAVO WHALE; III. the DUODECIMO WHALE. + +As the type of the FOLIO I present the SPERM WHALE; of the OCTAVO, +the GRAMPUS; of the DUODECIMO, the PORPOISE. + +FOLIOS. Among these I here include the following chapters:--I. The +SPERM WHALE; II. the RIGHT WHALE; III. the FIN-BACK WHALE; IV. the +HUMP-BACKED WHALE; V. the RAZOR-BACK WHALE; VI. the SULPHUR-BOTTOM +WHALE. + +BOOK I. (FOLIO), CHAPTER I. (SPERM WHALE).--This whale, among the +English of old vaguely known as the Trumpa whale, and the Physeter +whale, and the Anvil Headed whale, is the present Cachalot of the +French, and the Pottsfich of the Germans, and the Macrocephalus of +the Long Words. He is, without doubt, the largest inhabitant of the +globe; the most formidable of all whales to encounter; the most +majestic in aspect; and lastly, by far the most valuable in commerce; +he being the only creature from which that valuable substance, +spermaceti, is obtained. All his peculiarities will, in many other +places, be enlarged upon. It is chiefly with his name that I now +have to do. Philologically considered, it is absurd. Some centuries +ago, when the Sperm whale was almost wholly unknown in his own +proper individuality, and when his oil was only accidentally obtained +from the stranded fish; in those days spermaceti, it would seem, was +popularly supposed to be derived from a creature identical with the +one then known in England as the Greenland or Right Whale. It was +the idea also, that this same spermaceti was that quickening humor of +the Greenland Whale which the first syllable of the word literally +expresses. In those times, also, spermaceti was exceedingly scarce, +not being used for light, but only as an ointment and medicament. It +was only to be had from the druggists as you nowadays buy an ounce of +rhubarb. When, as I opine, in the course of time, the true nature of +spermaceti became known, its original name was still retained by the +dealers; no doubt to enhance its value by a notion so strangely +significant of its scarcity. And so the appellation must at last +have come to be bestowed upon the whale from which this spermaceti +was really derived. + +BOOK I. (FOLIO), CHAPTER II. (RIGHT WHALE).--In one respect this is +the most venerable of the leviathans, being the one first regularly +hunted by man. It yields the article commonly known as whalebone or +baleen; and the oil specially known as "whale oil," an inferior +article in commerce. Among the fishermen, he is indiscriminately +designated by all the following titles: The Whale; the Greenland +Whale; the Black Whale; the Great Whale; the True Whale; the Right +Whale. There is a deal of obscurity concerning the identity of the +species thus multitudinously baptised. What then is the whale, which +I include in the second species of my Folios? It is the Great +Mysticetus of the English naturalists; the Greenland Whale of the +English whalemen; the Baliene Ordinaire of the French whalemen; the +Growlands Walfish of the Swedes. It is the whale which for more than +two centuries past has been hunted by the Dutch and English in the +Arctic seas; it is the whale which the American fishermen have long +pursued in the Indian ocean, on the Brazil Banks, on the Nor' West +Coast, and various other parts of the world, designated by them Right +Whale Cruising Grounds. + +Some pretend to see a difference between the Greenland whale of the +English and the right whale of the Americans. But they precisely +agree in all their grand features; nor has there yet been presented a +single determinate fact upon which to ground a radical distinction. +It is by endless subdivisions based upon the most inconclusive +differences, that some departments of natural history become so +repellingly intricate. The right whale will be elsewhere treated of +at some length, with reference to elucidating the sperm whale. + +BOOK I. (FOLIO), CHAPTER III. (FIN-BACK).--Under this head I reckon a +monster which, by the various names of Fin-Back, Tall-Spout, and +Long-John, has been seen almost in every sea and is commonly the +whale whose distant jet is so often descried by passengers crossing +the Atlantic, in the New York packet-tracks. In the length he +attains, and in his baleen, the Fin-back resembles the right whale, +but is of a less portly girth, and a lighter colour, approaching to +olive. His great lips present a cable-like aspect, formed by the +intertwisting, slanting folds of large wrinkles. His grand +distinguishing feature, the fin, from which he derives his name, is +often a conspicuous object. This fin is some three or four feet +long, growing vertically from the hinder part of the back, of an +angular shape, and with a very sharp pointed end. Even if not the +slightest other part of the creature be visible, this isolated fin +will, at times, be seen plainly projecting from the surface. When +the sea is moderately calm, and slightly marked with spherical +ripples, and this gnomon-like fin stands up and casts shadows upon +the wrinkled surface, it may well be supposed that the watery circle +surrounding it somewhat resembles a dial, with its style and wavy +hour-lines graved on it. On that Ahaz-dial the shadow often goes +back. The Fin-Back is not gregarious. He seems a whale-hater, as +some men are man-haters. Very shy; always going solitary; +unexpectedly rising to the surface in the remotest and most sullen +waters; his straight and single lofty jet rising like a tall +misanthropic spear upon a barren plain; gifted with such wondrous +power and velocity in swimming, as to defy all present pursuit from +man; this leviathan seems the banished and unconquerable Cain of his +race, bearing for his mark that style upon his back. From having the +baleen in his mouth, the Fin-Back is sometimes included with the +right whale, among a theoretic species denominated WHALEBONE WHALES, +that is, whales with baleen. Of these so called Whalebone whales, +there would seem to be several varieties, most of which, however, are +little known. Broad-nosed whales and beaked whales; pike-headed +whales; bunched whales; under-jawed whales and rostrated whales, are +the fishermen's names for a few sorts. + +In connection with this appellative of "Whalebone whales," it is of +great importance to mention, that however such a nomenclature may be +convenient in facilitating allusions to some kind of whales, yet it +is in vain to attempt a clear classification of the Leviathan, +founded upon either his baleen, or hump, or fin, or teeth; +notwithstanding that those marked parts or features very obviously +seem better adapted to afford the basis for a regular system of +Cetology than any other detached bodily distinctions, which the +whale, in his kinds, presents. How then? The baleen, hump, +back-fin, and teeth; these are things whose peculiarities are +indiscriminately dispersed among all sorts of whales, without any +regard to what may be the nature of their structure in other and +more essential particulars. Thus, the sperm whale and the humpbacked +whale, each has a hump; but there the similitude ceases. Then, this +same humpbacked whale and the Greenland whale, each of these has +baleen; but there again the similitude ceases. And it is just the +same with the other parts above mentioned. In various sorts of +whales, they form such irregular combinations; or, in the case of any +one of them detached, such an irregular isolation; as utterly to defy +all general methodization formed upon such a basis. On this rock +every one of the whale-naturalists has split. + +But it may possibly be conceived that, in the internal parts of the +whale, in his anatomy--there, at least, we shall be able to hit the +right classification. Nay; what thing, for example, is there in the +Greenland whale's anatomy more striking than his baleen? Yet we have +seen that by his baleen it is impossible correctly to classify the +Greenland whale. And if you descend into the bowels of the various +leviathans, why there you will not find distinctions a fiftieth part +as available to the systematizer as those external ones already +enumerated. What then remains? nothing but to take hold of the +whales bodily, in their entire liberal volume, and boldly sort them +that way. And this is the Bibliographical system here adopted; and +it is the only one that can possibly succeed, for it alone is +practicable. To proceed. + +BOOK I. (FOLIO) CHAPTER IV. (HUMP-BACK).--This whale is often seen on +the northern American coast. He has been frequently captured there, +and towed into harbor. He has a great pack on him like a peddler; or +you might call him the Elephant and Castle whale. At any rate, the +popular name for him does not sufficiently distinguish him, since the +sperm whale also has a hump though a smaller one. His oil is not +very valuable. He has baleen. He is the most gamesome and +light-hearted of all the whales, making more gay foam and white water +generally than any other of them. + +BOOK I. (FOLIO), CHAPTER V. (RAZOR-BACK).--Of this whale little is +known but his name. I have seen him at a distance off Cape Horn. Of +a retiring nature, he eludes both hunters and philosophers. Though +no coward, he has never yet shown any part of him but his back, which +rises in a long sharp ridge. Let him go. I know little more of him, +nor does anybody else. + +BOOK I. (FOLIO), CHAPTER VI. (SULPHUR-BOTTOM).--Another retiring +gentleman, with a brimstone belly, doubtless got by scraping along +the Tartarian tiles in some of his profounder divings. He is seldom +seen; at least I have never seen him except in the remoter southern +seas, and then always at too great a distance to study his +countenance. He is never chased; he would run away with rope-walks +of line. Prodigies are told of him. Adieu, Sulphur Bottom! I can +say nothing more that is true of ye, nor can the oldest Nantucketer. + +Thus ends BOOK I. (FOLIO), and now begins BOOK II. (OCTAVO). + +OCTAVOES.*--These embrace the whales of middling magnitude, among +which present may be numbered:--I., the GRAMPUS; II., the BLACK FISH; +III., the NARWHALE; IV., the THRASHER; V., the KILLER. + + +*Why this book of whales is not denominated the Quarto is very plain. +Because, while the whales of this order, though smaller than those +of the former order, nevertheless retain a proportionate likeness to +them in figure, yet the bookbinder's Quarto volume in its dimensioned +form does not preserve the shape of the Folio volume, but the Octavo +volume does. + + +BOOK II. (OCTAVO), CHAPTER I. (GRAMPUS).--Though this fish, whose +loud sonorous breathing, or rather blowing, has furnished a proverb +to landsmen, is so well known a denizen of the deep, yet is he not +popularly classed among whales. But possessing all the grand +distinctive features of the leviathan, most naturalists have +recognised him for one. He is of moderate octavo size, varying from +fifteen to twenty-five feet in length, and of corresponding +dimensions round the waist. He swims in herds; he is never regularly +hunted, though his oil is considerable in quantity, and pretty good +for light. By some fishermen his approach is regarded as premonitory +of the advance of the great sperm whale. + +BOOK II. (OCTAVO), CHAPTER II. (BLACK FISH).--I give the popular +fishermen's names for all these fish, for generally they are the +best. Where any name happens to be vague or inexpressive, I shall +say so, and suggest another. I do so now, touching the Black Fish, +so-called, because blackness is the rule among almost all whales. +So, call him the Hyena Whale, if you please. His voracity is well +known, and from the circumstance that the inner angles of his lips +are curved upwards, he carries an everlasting Mephistophelean grin on +his face. This whale averages some sixteen or eighteen feet in +length. He is found in almost all latitudes. He has a peculiar way +of showing his dorsal hooked fin in swimming, which looks something +like a Roman nose. When not more profitably employed, the sperm +whale hunters sometimes capture the Hyena whale, to keep up the +supply of cheap oil for domestic employment--as some frugal +housekeepers, in the absence of company, and quite alone by +themselves, burn unsavory tallow instead of odorous wax. Though +their blubber is very thin, some of these whales will yield you +upwards of thirty gallons of oil. + +BOOK II. (OCTAVO), CHAPTER III. (NARWHALE), that is, NOSTRIL +WHALE.--Another instance of a curiously named whale, so named I +suppose from his peculiar horn being originally mistaken for a peaked +nose. The creature is some sixteen feet in length, while its horn +averages five feet, though some exceed ten, and even attain to +fifteen feet. Strictly speaking, this horn is but a lengthened tusk, +growing out from the jaw in a line a little depressed from the +horizontal. But it is only found on the sinister side, which has an +ill effect, giving its owner something analogous to the aspect of a +clumsy left-handed man. What precise purpose this ivory horn or +lance answers, it would be hard to say. It does not seem to be used +like the blade of the sword-fish and bill-fish; though some sailors +tell me that the Narwhale employs it for a rake in turning over the +bottom of the sea for food. Charley Coffin said it was used for an +ice-piercer; for the Narwhale, rising to the surface of the Polar +Sea, and finding it sheeted with ice, thrusts his horn up, and so +breaks through. But you cannot prove either of these surmises to be +correct. My own opinion is, that however this one-sided horn may +really be used by the Narwhale--however that may be--it would +certainly be very convenient to him for a folder in reading +pamphlets. The Narwhale I have heard called the Tusked whale, the +Horned whale, and the Unicorn whale. He is certainly a curious +example of the Unicornism to be found in almost every kingdom of +animated nature. From certain cloistered old authors I have gathered +that this same sea-unicorn's horn was in ancient days regarded as the +great antidote against poison, and as such, preparations of it +brought immense prices. It was also distilled to a volatile salts +for fainting ladies, the same way that the horns of the male deer are +manufactured into hartshorn. Originally it was in itself accounted +an object of great curiosity. Black Letter tells me that Sir Martin +Frobisher on his return from that voyage, when Queen Bess did +gallantly wave her jewelled hand to him from a window of Greenwich +Palace, as his bold ship sailed down the Thames; "when Sir Martin +returned from that voyage," saith Black Letter, "on bended knees he +presented to her highness a prodigious long horn of the Narwhale, +which for a long period after hung in the castle at Windsor." An +Irish author avers that the Earl of Leicester, on bended knees, did +likewise present to her highness another horn, pertaining to a land +beast of the unicorn nature. + +The Narwhale has a very picturesque, leopard-like look, being of a +milk-white ground colour, dotted with round and oblong spots of black. +His oil is very superior, clear and fine; but there is little of it, +and he is seldom hunted. He is mostly found in the circumpolar seas. + +BOOK II. (OCTAVO), CHAPTER IV. (KILLER).--Of this whale little is +precisely known to the Nantucketer, and nothing at all to the +professed naturalist. From what I have seen of him at a distance, +I should say that he was about the bigness of a grampus. He is very +savage--a sort of Feegee fish. He sometimes takes the great Folio +whales by the lip, and hangs there like a leech, till the mighty +brute is worried to death. The Killer is never hunted. I never +heard what sort of oil he has. Exception might be taken to the name +bestowed upon this whale, on the ground of its indistinctness. For +we are all killers, on land and on sea; Bonapartes and Sharks +included. + +BOOK II. (OCTAVO), CHAPTER V. (THRASHER).--This gentleman is famous +for his tail, which he uses for a ferule in thrashing his foes. He +mounts the Folio whale's back, and as he swims, he works his passage +by flogging him; as some schoolmasters get along in the world by a +similar process. Still less is known of the Thrasher than of the +Killer. Both are outlaws, even in the lawless seas. + +Thus ends BOOK II. (OCTAVO), and begins BOOK III. (DUODECIMO). + +DUODECIMOES.--These include the smaller whales. I. The Huzza +Porpoise. II. The Algerine Porpoise. III. The Mealy-mouthed +Porpoise. + +To those who have not chanced specially to study the subject, it may +possibly seem strange, that fishes not commonly exceeding four or +five feet should be marshalled among WHALES--a word, which, in the +popular sense, always conveys an idea of hugeness. But the creatures +set down above as Duodecimoes are infallibly whales, by the terms of +my definition of what a whale is--i.e. a spouting fish, with a +horizontal tail. + +BOOK III. (DUODECIMO), CHAPTER 1. (HUZZA PORPOISE).--This is the +common porpoise found almost all over the globe. The name is of my +own bestowal; for there are more than one sort of porpoises, and +something must be done to distinguish them. I call him thus, because +he always swims in hilarious shoals, which upon the broad sea keep +tossing themselves to heaven like caps in a Fourth-of-July crowd. +Their appearance is generally hailed with delight by the mariner. +Full of fine spirits, they invariably come from the breezy billows to +windward. They are the lads that always live before the wind. They +are accounted a lucky omen. If you yourself can withstand three +cheers at beholding these vivacious fish, then heaven help ye; the +spirit of godly gamesomeness is not in ye. A well-fed, plump Huzza +Porpoise will yield you one good gallon of good oil. But the fine +and delicate fluid extracted from his jaws is exceedingly valuable. +It is in request among jewellers and watchmakers. Sailors put it on +their hones. Porpoise meat is good eating, you know. It may never +have occurred to you that a porpoise spouts. Indeed, his spout is so +small that it is not very readily discernible. But the next time you +have a chance, watch him; and you will then see the great Sperm whale +himself in miniature. + +BOOK III. (DUODECIMO), CHAPTER II. (ALGERINE PORPOISE).--A pirate. +Very savage. He is only found, I think, in the Pacific. He is +somewhat larger than the Huzza Porpoise, but much of the same general +make. Provoke him, and he will buckle to a shark. I have lowered +for him many times, but never yet saw him captured. + +BOOK III. (DUODECIMO), CHAPTER III. (MEALY-MOUTHED PORPOISE).--The +largest kind of Porpoise; and only found in the Pacific, so far as it +is known. The only English name, by which he has hitherto been +designated, is that of the fishers--Right-Whale Porpoise, from the +circumstance that he is chiefly found in the vicinity of that Folio. +In shape, he differs in some degree from the Huzza Porpoise, being of +a less rotund and jolly girth; indeed, he is of quite a neat and +gentleman-like figure. He has no fins on his back (most other +porpoises have), he has a lovely tail, and sentimental Indian eyes of +a hazel hue. But his mealy-mouth spoils all. Though his entire +back down to his side fins is of a deep sable, yet a boundary line, +distinct as the mark in a ship's hull, called the "bright waist," +that line streaks him from stem to stern, with two separate colours, +black above and white below. The white comprises part of his head, +and the whole of his mouth, which makes him look as if he had just +escaped from a felonious visit to a meal-bag. A most mean and mealy +aspect! His oil is much like that of the common porpoise. + + +Beyond the DUODECIMO, this system does not proceed, inasmuch as the +Porpoise is the smallest of the whales. Above, you have all the +Leviathans of note. But there are a rabble of uncertain, fugitive, +half-fabulous whales, which, as an American whaleman, I know by +reputation, but not personally. I shall enumerate them by their +fore-castle appellations; for possibly such a list may be valuable to +future investigators, who may complete what I have here but begun. +If any of the following whales, shall hereafter be caught and marked, +then he can readily be incorporated into this System, according to +his Folio, Octavo, or Duodecimo magnitude:--The Bottle-Nose Whale; +the Junk Whale; the Pudding-Headed Whale; the Cape Whale; the Leading +Whale; the Cannon Whale; the Scragg Whale; the Coppered Whale; the +Elephant Whale; the Iceberg Whale; the Quog Whale; the Blue Whale; etc. +From Icelandic, Dutch, and old English authorities, there might +be quoted other lists of uncertain whales, blessed with all manner of +uncouth names. But I omit them as altogether obsolete; and can +hardly help suspecting them for mere sounds, full of Leviathanism, +but signifying nothing. + +Finally: It was stated at the outset, that this system would not be +here, and at once, perfected. You cannot but plainly see that I have +kept my word. But I now leave my cetological System standing thus +unfinished, even as the great Cathedral of Cologne was left, with the +crane still standing upon the top of the uncompleted tower. For +small erections may be finished by their first architects; grand +ones, true ones, ever leave the copestone to posterity. God keep me +from ever completing anything. This whole book is but a +draught--nay, but the draught of a draught. Oh, Time, Strength, +Cash, and Patience! + + + +CHAPTER 33 + +The Specksynder. + + +Concerning the officers of the whale-craft, this seems as good a +place as any to set down a little domestic peculiarity on ship-board, +arising from the existence of the harpooneer class of officers, a +class unknown of course in any other marine than the whale-fleet. + +The large importance attached to the harpooneer's vocation is evinced +by the fact, that originally in the old Dutch Fishery, two centuries +and more ago, the command of a whale ship was not wholly lodged in +the person now called the captain, but was divided between him and an +officer called the Specksynder. Literally this word means +Fat-Cutter; usage, however, in time made it equivalent to Chief +Harpooneer. In those days, the captain's authority was restricted to +the navigation and general management of the vessel; while over the +whale-hunting department and all its concerns, the Specksynder or +Chief Harpooneer reigned supreme. In the British Greenland Fishery, +under the corrupted title of Specksioneer, this old Dutch official is +still retained, but his former dignity is sadly abridged. At present +he ranks simply as senior Harpooneer; and as such, is but one of the +captain's more inferior subalterns. Nevertheless, as upon the good +conduct of the harpooneers the success of a whaling voyage largely +depends, and since in the American Fishery he is not only an +important officer in the boat, but under certain circumstances (night +watches on a whaling ground) the command of the ship's deck is also +his; therefore the grand political maxim of the sea demands, that he +should nominally live apart from the men before the mast, and be in +some way distinguished as their professional superior; though always, +by them, familiarly regarded as their social equal. + +Now, the grand distinction drawn between officer and man at sea, is +this--the first lives aft, the last forward. Hence, in whale-ships +and merchantmen alike, the mates have their quarters with the +captain; and so, too, in most of the American whalers the harpooneers +are lodged in the after part of the ship. That is to say, they take +their meals in the captain's cabin, and sleep in a place indirectly +communicating with it. + +Though the long period of a Southern whaling voyage (by far the +longest of all voyages now or ever made by man), the peculiar perils +of it, and the community of interest prevailing among a company, all +of whom, high or low, depend for their profits, not upon fixed wages, +but upon their common luck, together with their common vigilance, +intrepidity, and hard work; though all these things do in some cases +tend to beget a less rigorous discipline than in merchantmen +generally; yet, never mind how much like an old Mesopotamian family +these whalemen may, in some primitive instances, live together; for +all that, the punctilious externals, at least, of the quarter-deck +are seldom materially relaxed, and in no instance done away. Indeed, +many are the Nantucket ships in which you will see the skipper +parading his quarter-deck with an elated grandeur not surpassed in +any military navy; nay, extorting almost as much outward homage as if +he wore the imperial purple, and not the shabbiest of pilot-cloth. + +And though of all men the moody captain of the Pequod was the least +given to that sort of shallowest assumption; and though the only +homage he ever exacted, was implicit, instantaneous obedience; though +he required no man to remove the shoes from his feet ere stepping +upon the quarter-deck; and though there were times when, owing to +peculiar circumstances connected with events hereafter to be +detailed, he addressed them in unusual terms, whether of +condescension or IN TERROREM, or otherwise; yet even Captain Ahab was +by no means unobservant of the paramount forms and usages of the sea. + +Nor, perhaps, will it fail to be eventually perceived, that behind +those forms and usages, as it were, he sometimes masked himself; +incidentally making use of them for other and more private ends than +they were legitimately intended to subserve. That certain sultanism +of his brain, which had otherwise in a good degree remained +unmanifested; through those forms that same sultanism became +incarnate in an irresistible dictatorship. For be a man's +intellectual superiority what it will, it can never assume the +practical, available supremacy over other men, without the aid of +some sort of external arts and entrenchments, always, in themselves, +more or less paltry and base. This it is, that for ever keeps God's +true princes of the Empire from the world's hustings; and leaves the +highest honours that this air can give, to those men who become famous +more through their infinite inferiority to the choice hidden handful +of the Divine Inert, than through their undoubted superiority over +the dead level of the mass. Such large virtue lurks in these small +things when extreme political superstitions invest them, that in some +royal instances even to idiot imbecility they have imparted potency. +But when, as in the case of Nicholas the Czar, the ringed crown of +geographical empire encircles an imperial brain; then, the plebeian +herds crouch abased before the tremendous centralization. Nor, will +the tragic dramatist who would depict mortal indomitableness in its +fullest sweep and direct swing, ever forget a hint, incidentally so +important in his art, as the one now alluded to. + +But Ahab, my Captain, still moves before me in all his Nantucket +grimness and shagginess; and in this episode touching Emperors and +Kings, I must not conceal that I have only to do with a poor old +whale-hunter like him; and, therefore, all outward majestical +trappings and housings are denied me. Oh, Ahab! what shall be grand +in thee, it must needs be plucked at from the skies, and dived for in +the deep, and featured in the unbodied air! + + + +CHAPTER 34 + +The Cabin-Table. + + +It is noon; and Dough-Boy, the steward, thrusting his pale +loaf-of-bread face from the cabin-scuttle, announces dinner to his +lord and master; who, sitting in the lee quarter-boat, has just been +taking an observation of the sun; and is now mutely reckoning the +latitude on the smooth, medallion-shaped tablet, reserved for that +daily purpose on the upper part of his ivory leg. From his complete +inattention to the tidings, you would think that moody Ahab had not +heard his menial. But presently, catching hold of the mizen shrouds, +he swings himself to the deck, and in an even, unexhilarated voice, +saying, "Dinner, Mr. Starbuck," disappears into the cabin. + +When the last echo of his sultan's step has died away, and Starbuck, +the first Emir, has every reason to suppose that he is seated, then +Starbuck rouses from his quietude, takes a few turns along the +planks, and, after a grave peep into the binnacle, says, with some +touch of pleasantness, "Dinner, Mr. Stubb," and descends the scuttle. +The second Emir lounges about the rigging awhile, and then slightly +shaking the main brace, to see whether it will be all right with +that important rope, he likewise takes up the old burden, and with a +rapid "Dinner, Mr. Flask," follows after his predecessors. + +But the third Emir, now seeing himself all alone on the quarter-deck, +seems to feel relieved from some curious restraint; for, tipping all +sorts of knowing winks in all sorts of directions, and kicking off +his shoes, he strikes into a sharp but noiseless squall of a hornpipe +right over the Grand Turk's head; and then, by a dexterous sleight, +pitching his cap up into the mizentop for a shelf, he goes down +rollicking so far at least as he remains visible from the deck, +reversing all other processions, by bringing up the rear with music. +But ere stepping into the cabin doorway below, he pauses, ships a new +face altogether, and, then, independent, hilarious little Flask +enters King Ahab's presence, in the character of Abjectus, or the +Slave. + +It is not the least among the strange things bred by the intense +artificialness of sea-usages, that while in the open air of the deck +some officers will, upon provocation, bear themselves boldly and +defyingly enough towards their commander; yet, ten to one, let those +very officers the next moment go down to their customary dinner in +that same commander's cabin, and straightway their inoffensive, not +to say deprecatory and humble air towards him, as he sits at the head +of the table; this is marvellous, sometimes most comical. Wherefore +this difference? A problem? Perhaps not. To have been Belshazzar, +King of Babylon; and to have been Belshazzar, not haughtily but +courteously, therein certainly must have been some touch of mundane +grandeur. But he who in the rightly regal and intelligent spirit +presides over his own private dinner-table of invited guests, that +man's unchallenged power and dominion of individual influence for the +time; that man's royalty of state transcends Belshazzar's, for +Belshazzar was not the greatest. Who has but once dined his friends, +has tasted what it is to be Caesar. It is a witchery of social +czarship which there is no withstanding. Now, if to this +consideration you superadd the official supremacy of a ship-master, +then, by inference, you will derive the cause of that peculiarity of +sea-life just mentioned. + +Over his ivory-inlaid table, Ahab presided like a mute, maned +sea-lion on the white coral beach, surrounded by his warlike but +still deferential cubs. In his own proper turn, each officer waited +to be served. They were as little children before Ahab; and yet, in +Ahab, there seemed not to lurk the smallest social arrogance. With +one mind, their intent eyes all fastened upon the old man's knife, as +he carved the chief dish before him. I do not suppose that for the +world they would have profaned that moment with the slightest +observation, even upon so neutral a topic as the weather. No! And +when reaching out his knife and fork, between which the slice of beef +was locked, Ahab thereby motioned Starbuck's plate towards him, the +mate received his meat as though receiving alms; and cut it tenderly; +and a little started if, perchance, the knife grazed against the +plate; and chewed it noiselessly; and swallowed it, not without +circumspection. For, like the Coronation banquet at Frankfort, where +the German Emperor profoundly dines with the seven Imperial +Electors, so these cabin meals were somehow solemn meals, eaten in +awful silence; and yet at table old Ahab forbade not conversation; +only he himself was dumb. What a relief it was to choking Stubb, +when a rat made a sudden racket in the hold below. And poor little +Flask, he was the youngest son, and little boy of this weary family +party. His were the shinbones of the saline beef; his would have +been the drumsticks. For Flask to have presumed to help himself, +this must have seemed to him tantamount to larceny in the first +degree. Had he helped himself at that table, doubtless, never more +would he have been able to hold his head up in this honest world; +nevertheless, strange to say, Ahab never forbade him. And had Flask +helped himself, the chances were Ahab had never so much as noticed +it. Least of all, did Flask presume to help himself to butter. +Whether he thought the owners of the ship denied it to him, on +account of its clotting his clear, sunny complexion; or whether he +deemed that, on so long a voyage in such marketless waters, butter +was at a premium, and therefore was not for him, a subaltern; however +it was, Flask, alas! was a butterless man! + +Another thing. Flask was the last person down at the dinner, and +Flask is the first man up. Consider! For hereby Flask's dinner was +badly jammed in point of time. Starbuck and Stubb both had the start +of him; and yet they also have the privilege of lounging in the rear. +If Stubb even, who is but a peg higher than Flask, happens to have +but a small appetite, and soon shows symptoms of concluding his +repast, then Flask must bestir himself, he will not get more than +three mouthfuls that day; for it is against holy usage for Stubb to +precede Flask to the deck. Therefore it was that Flask once admitted +in private, that ever since he had arisen to the dignity of an +officer, from that moment he had never known what it was to be +otherwise than hungry, more or less. For what he ate did not so much +relieve his hunger, as keep it immortal in him. Peace and +satisfaction, thought Flask, have for ever departed from my stomach. +I am an officer; but, how I wish I could fish a bit of old-fashioned +beef in the forecastle, as I used to when I was before the mast. +There's the fruits of promotion now; there's the vanity of glory: +there's the insanity of life! Besides, if it were so that any mere +sailor of the Pequod had a grudge against Flask in Flask's official +capacity, all that sailor had to do, in order to obtain ample +vengeance, was to go aft at dinner-time, and get a peep at Flask +through the cabin sky-light, sitting silly and dumfoundered before +awful Ahab. + +Now, Ahab and his three mates formed what may be called the first +table in the Pequod's cabin. After their departure, taking place in +inverted order to their arrival, the canvas cloth was cleared, or +rather was restored to some hurried order by the pallid steward. And +then the three harpooneers were bidden to the feast, they being its +residuary legatees. They made a sort of temporary servants' hall of +the high and mighty cabin. + +In strange contrast to the hardly tolerable constraint and nameless +invisible domineerings of the captain's table, was the entire +care-free license and ease, the almost frantic democracy of those +inferior fellows the harpooneers. While their masters, the mates, +seemed afraid of the sound of the hinges of their own jaws, the +harpooneers chewed their food with such a relish that there was a +report to it. They dined like lords; they filled their bellies like +Indian ships all day loading with spices. Such portentous appetites +had Queequeg and Tashtego, that to fill out the vacancies made by the +previous repast, often the pale Dough-Boy was fain to bring on a +great baron of salt-junk, seemingly quarried out of the solid ox. +And if he were not lively about it, if he did not go with a nimble +hop-skip-and-jump, then Tashtego had an ungentlemanly way of +accelerating him by darting a fork at his back, harpoon-wise. And +once Daggoo, seized with a sudden humor, assisted Dough-Boy's memory +by snatching him up bodily, and thrusting his head into a great empty +wooden trencher, while Tashtego, knife in hand, began laying out the +circle preliminary to scalping him. He was naturally a very nervous, +shuddering sort of little fellow, this bread-faced steward; the +progeny of a bankrupt baker and a hospital nurse. And what with the +standing spectacle of the black terrific Ahab, and the periodical +tumultuous visitations of these three savages, Dough-Boy's whole life +was one continual lip-quiver. Commonly, after seeing the harpooneers +furnished with all things they demanded, he would escape from their +clutches into his little pantry adjoining, and fearfully peep out at +them through the blinds of its door, till all was over. + +It was a sight to see Queequeg seated over against Tashtego, opposing +his filed teeth to the Indian's: crosswise to them, Daggoo seated on +the floor, for a bench would have brought his hearse-plumed head to +the low carlines; at every motion of his colossal limbs, making the +low cabin framework to shake, as when an African elephant goes +passenger in a ship. But for all this, the great negro was +wonderfully abstemious, not to say dainty. It seemed hardly possible +that by such comparatively small mouthfuls he could keep up the +vitality diffused through so broad, baronial, and superb a person. +But, doubtless, this noble savage fed strong and drank deep of the +abounding element of air; and through his dilated nostrils snuffed in +the sublime life of the worlds. Not by beef or by bread, are giants +made or nourished. But Queequeg, he had a mortal, barbaric smack of +the lip in eating--an ugly sound enough--so much so, that the +trembling Dough-Boy almost looked to see whether any marks of teeth +lurked in his own lean arms. And when he would hear Tashtego singing +out for him to produce himself, that his bones might be picked, the +simple-witted steward all but shattered the crockery hanging round +him in the pantry, by his sudden fits of the palsy. Nor did the +whetstone which the harpooneers carried in their pockets, for their +lances and other weapons; and with which whetstones, at dinner, they +would ostentatiously sharpen their knives; that grating sound did not +at all tend to tranquillize poor Dough-Boy. How could he forget that +in his Island days, Queequeg, for one, must certainly have been +guilty of some murderous, convivial indiscretions. Alas! Dough-Boy! +hard fares the white waiter who waits upon cannibals. Not a napkin +should he carry on his arm, but a buckler. In good time, though, to +his great delight, the three salt-sea warriors would rise and depart; +to his credulous, fable-mongering ears, all their martial bones +jingling in them at every step, like Moorish scimetars in scabbards. + +But, though these barbarians dined in the cabin, and nominally lived +there; still, being anything but sedentary in their habits, they were +scarcely ever in it except at mealtimes, and just before +sleeping-time, when they passed through it to their own peculiar +quarters. + +In this one matter, Ahab seemed no exception to most American whale +captains, who, as a set, rather incline to the opinion that by rights +the ship's cabin belongs to them; and that it is by courtesy alone +that anybody else is, at any time, permitted there. So that, in real +truth, the mates and harpooneers of the Pequod might more properly be +said to have lived out of the cabin than in it. For when they did +enter it, it was something as a street-door enters a house; turning +inwards for a moment, only to be turned out the next; and, as a +permanent thing, residing in the open air. Nor did they lose much +hereby; in the cabin was no companionship; socially, Ahab was +inaccessible. Though nominally included in the census of +Christendom, he was still an alien to it. He lived in the world, as +the last of the Grisly Bears lived in settled Missouri. And as when +Spring and Summer had departed, that wild Logan of the woods, burying +himself in the hollow of a tree, lived out the winter there, sucking +his own paws; so, in his inclement, howling old age, Ahab's soul, +shut up in the caved trunk of his body, there fed upon the sullen +paws of its gloom! + + + +CHAPTER 35 + +The Mast-Head. + + +It was during the more pleasant weather, that in due rotation with +the other seamen my first mast-head came round. + +In most American whalemen the mast-heads are manned almost +simultaneously with the vessel's leaving her port; even though she +may have fifteen thousand miles, and more, to sail ere reaching her +proper cruising ground. And if, after a three, four, or five years' +voyage she is drawing nigh home with anything empty in her--say, an +empty vial even--then, her mast-heads are kept manned to the last; +and not till her skysail-poles sail in among the spires of the port, +does she altogether relinquish the hope of capturing one whale more. + +Now, as the business of standing mast-heads, ashore or afloat, is a +very ancient and interesting one, let us in some measure expatiate +here. I take it, that the earliest standers of mast-heads were the +old Egyptians; because, in all my researches, I find none prior to +them. For though their progenitors, the builders of Babel, must +doubtless, by their tower, have intended to rear the loftiest +mast-head in all Asia, or Africa either; yet (ere the final truck was +put to it) as that great stone mast of theirs may be said to have +gone by the board, in the dread gale of God's wrath; therefore, we +cannot give these Babel builders priority over the Egyptians. And +that the Egyptians were a nation of mast-head standers, is an +assertion based upon the general belief among archaeologists, that +the first pyramids were founded for astronomical purposes: a theory +singularly supported by the peculiar stair-like formation of all four +sides of those edifices; whereby, with prodigious long upliftings of +their legs, those old astronomers were wont to mount to the apex, and +sing out for new stars; even as the look-outs of a modern ship sing +out for a sail, or a whale just bearing in sight. In Saint Stylites, +the famous Christian hermit of old times, who built him a lofty stone +pillar in the desert and spent the whole latter portion of his life +on its summit, hoisting his food from the ground with a tackle; in +him we have a remarkable instance of a dauntless +stander-of-mast-heads; who was not to be driven from his place by +fogs or frosts, rain, hail, or sleet; but valiantly facing everything +out to the last, literally died at his post. Of modern +standers-of-mast-heads we have but a lifeless set; mere stone, iron, +and bronze men; who, though well capable of facing out a stiff gale, +are still entirely incompetent to the business of singing out upon +discovering any strange sight. There is Napoleon; who, upon the top +of the column of Vendome, stands with arms folded, some one hundred +and fifty feet in the air; careless, now, who rules the decks below; +whether Louis Philippe, Louis Blanc, or Louis the Devil. Great +Washington, too, stands high aloft on his towering main-mast in +Baltimore, and like one of Hercules' pillars, his column marks that +point of human grandeur beyond which few mortals will go. Admiral +Nelson, also, on a capstan of gun-metal, stands his mast-head in +Trafalgar Square; and ever when most obscured by that London smoke, +token is yet given that a hidden hero is there; for where there is +smoke, must be fire. But neither great Washington, nor Napoleon, nor +Nelson, will answer a single hail from below, however madly invoked +to befriend by their counsels the distracted decks upon which they +gaze; however it may be surmised, that their spirits penetrate +through the thick haze of the future, and descry what shoals and what +rocks must be shunned. + +It may seem unwarrantable to couple in any respect the mast-head +standers of the land with those of the sea; but that in truth it is +not so, is plainly evinced by an item for which Obed Macy, the sole +historian of Nantucket, stands accountable. The worthy Obed tells +us, that in the early times of the whale fishery, ere ships were +regularly launched in pursuit of the game, the people of that island +erected lofty spars along the sea-coast, to which the look-outs +ascended by means of nailed cleats, something as fowls go upstairs in +a hen-house. A few years ago this same plan was adopted by the Bay +whalemen of New Zealand, who, upon descrying the game, gave notice to +the ready-manned boats nigh the beach. But this custom has now +become obsolete; turn we then to the one proper mast-head, that of a +whale-ship at sea. The three mast-heads are kept manned from +sun-rise to sun-set; the seamen taking their regular turns (as at the +helm), and relieving each other every two hours. In the serene +weather of the tropics it is exceedingly pleasant the mast-head; nay, +to a dreamy meditative man it is delightful. There you stand, a +hundred feet above the silent decks, striding along the deep, as if +the masts were gigantic stilts, while beneath you and between your +legs, as it were, swim the hugest monsters of the sea, even as ships +once sailed between the boots of the famous Colossus at old Rhodes. +There you stand, lost in the infinite series of the sea, with nothing +ruffled but the waves. The tranced ship indolently rolls; the drowsy +trade winds blow; everything resolves you into languor. For the most +part, in this tropic whaling life, a sublime uneventfulness invests +you; you hear no news; read no gazettes; extras with startling +accounts of commonplaces never delude you into unnecessary +excitements; you hear of no domestic afflictions; bankrupt +securities; fall of stocks; are never troubled with the thought of +what you shall have for dinner--for all your meals for three years +and more are snugly stowed in casks, and your bill of fare is +immutable. + +In one of those southern whalesmen, on a long three or four years' +voyage, as often happens, the sum of the various hours you spend at +the mast-head would amount to several entire months. And it is much +to be deplored that the place to which you devote so considerable a +portion of the whole term of your natural life, should be so sadly +destitute of anything approaching to a cosy inhabitiveness, or +adapted to breed a comfortable localness of feeling, such as pertains +to a bed, a hammock, a hearse, a sentry box, a pulpit, a coach, or +any other of those small and snug contrivances in which men +temporarily isolate themselves. Your most usual point of perch is +the head of the t' gallant-mast, where you stand upon two thin +parallel sticks (almost peculiar to whalemen) called the t' gallant +cross-trees. Here, tossed about by the sea, the beginner feels about +as cosy as he would standing on a bull's horns. To be sure, in cold +weather you may carry your house aloft with you, in the shape of a +watch-coat; but properly speaking the thickest watch-coat is no more +of a house than the unclad body; for as the soul is glued inside of +its fleshy tabernacle, and cannot freely move about in it, nor even +move out of it, without running great risk of perishing (like an +ignorant pilgrim crossing the snowy Alps in winter); so a watch-coat +is not so much of a house as it is a mere envelope, or additional +skin encasing you. You cannot put a shelf or chest of drawers in +your body, and no more can you make a convenient closet of your +watch-coat. + +Concerning all this, it is much to be deplored that the mast-heads of +a southern whale ship are unprovided with those enviable little tents +or pulpits, called CROW'S-NESTS, in which the look-outs of a +Greenland whaler are protected from the inclement weather of the +frozen seas. In the fireside narrative of Captain Sleet, entitled +"A Voyage among the Icebergs, in quest of the Greenland Whale, and +incidentally for the re-discovery of the Lost Icelandic Colonies of +Old Greenland;" in this admirable volume, all standers of mast-heads +are furnished with a charmingly circumstantial account of the then +recently invented CROW'S-NEST of the Glacier, which was the name of +Captain Sleet's good craft. He called it the SLEET'S CROW'S-NEST, in +honour of himself; he being the original inventor and patentee, and +free from all ridiculous false delicacy, and holding that if we call +our own children after our own names (we fathers being the original +inventors and patentees), so likewise should we denominate after +ourselves any other apparatus we may beget. In shape, the Sleet's +crow's-nest is something like a large tierce or pipe; it is open +above, however, where it is furnished with a movable side-screen to +keep to windward of your head in a hard gale. Being fixed on the +summit of the mast, you ascend into it through a little trap-hatch in +the bottom. On the after side, or side next the stern of the ship, +is a comfortable seat, with a locker underneath for umbrellas, +comforters, and coats. In front is a leather rack, in which to keep +your speaking trumpet, pipe, telescope, and other nautical +conveniences. When Captain Sleet in person stood his mast-head in +this crow's-nest of his, he tells us that he always had a rifle with +him (also fixed in the rack), together with a powder flask and shot, +for the purpose of popping off the stray narwhales, or vagrant sea +unicorns infesting those waters; for you cannot successfully shoot at +them from the deck owing to the resistance of the water, but to shoot +down upon them is a very different thing. Now, it was plainly a +labor of love for Captain Sleet to describe, as he does, all the +little detailed conveniences of his crow's-nest; but though he so +enlarges upon many of these, and though he treats us to a very +scientific account of his experiments in this crow's-nest, with a +small compass he kept there for the purpose of counteracting the +errors resulting from what is called the "local attraction" of all +binnacle magnets; an error ascribable to the horizontal vicinity of +the iron in the ship's planks, and in the Glacier's case, perhaps, to +there having been so many broken-down blacksmiths among her crew; I +say, that though the Captain is very discreet and scientific here, +yet, for all his learned "binnacle deviations," "azimuth compass +observations," and "approximate errors," he knows very well, Captain +Sleet, that he was not so much immersed in those profound magnetic +meditations, as to fail being attracted occasionally towards that +well replenished little case-bottle, so nicely tucked in on one side +of his crow's nest, within easy reach of his hand. Though, upon the +whole, I greatly admire and even love the brave, the honest, and +learned Captain; yet I take it very ill of him that he should so +utterly ignore that case-bottle, seeing what a faithful friend and +comforter it must have been, while with mittened fingers and hooded +head he was studying the mathematics aloft there in that bird's nest +within three or four perches of the pole. + +But if we Southern whale-fishers are not so snugly housed aloft as +Captain Sleet and his Greenlandmen were; yet that disadvantage is +greatly counter-balanced by the widely contrasting serenity of those +seductive seas in which we South fishers mostly float. For one, I +used to lounge up the rigging very leisurely, resting in the top to +have a chat with Queequeg, or any one else off duty whom I might find +there; then ascending a little way further, and throwing a lazy leg +over the top-sail yard, take a preliminary view of the watery +pastures, and so at last mount to my ultimate destination. + +Let me make a clean breast of it here, and frankly admit that I kept +but sorry guard. With the problem of the universe revolving in me, +how could I--being left completely to myself at such a +thought-engendering altitude--how could I but lightly hold my +obligations to observe all whale-ships' standing orders, "Keep your +weather eye open, and sing out every time." + +And let me in this place movingly admonish you, ye ship-owners of +Nantucket! Beware of enlisting in your vigilant fisheries any lad +with lean brow and hollow eye; given to unseasonable meditativeness; +and who offers to ship with the Phaedon instead of Bowditch in his +head. Beware of such an one, I say; your whales must be seen before +they can be killed; and this sunken-eyed young Platonist will tow you +ten wakes round the world, and never make you one pint of sperm the +richer. Nor are these monitions at all unneeded. For nowadays, the +whale-fishery furnishes an asylum for many romantic, melancholy, and +absent-minded young men, disgusted with the carking cares of earth, +and seeking sentiment in tar and blubber. Childe Harold not +unfrequently perches himself upon the mast-head of some luckless +disappointed whale-ship, and in moody phrase ejaculates:-- + +"Roll on, thou deep and dark blue ocean, roll! Ten thousand +blubber-hunters sweep over thee in vain." + +Very often do the captains of such ships take those absent-minded +young philosophers to task, upbraiding them with not feeling +sufficient "interest" in the voyage; half-hinting that they are so +hopelessly lost to all honourable ambition, as that in their secret +souls they would rather not see whales than otherwise. But all in +vain; those young Platonists have a notion that their vision is +imperfect; they are short-sighted; what use, then, to strain the +visual nerve? They have left their opera-glasses at home. + +"Why, thou monkey," said a harpooneer to one of these lads, "we've +been cruising now hard upon three years, and thou hast not raised a +whale yet. Whales are scarce as hen's teeth whenever thou art up +here." Perhaps they were; or perhaps there might have been shoals of +them in the far horizon; but lulled into such an opium-like +listlessness of vacant, unconscious reverie is this absent-minded +youth by the blending cadence of waves with thoughts, that at last he +loses his identity; takes the mystic ocean at his feet for the +visible image of that deep, blue, bottomless soul, pervading mankind +and nature; and every strange, half-seen, gliding, beautiful thing +that eludes him; every dimly-discovered, uprising fin of some +undiscernible form, seems to him the embodiment of those elusive +thoughts that only people the soul by continually flitting through +it. In this enchanted mood, thy spirit ebbs away to whence it came; +becomes diffused through time and space; like Crammer's sprinkled +Pantheistic ashes, forming at last a part of every shore the round +globe over. + +There is no life in thee, now, except that rocking life imparted by a +gently rolling ship; by her, borrowed from the sea; by the sea, from +the inscrutable tides of God. But while this sleep, this dream is on +ye, move your foot or hand an inch; slip your hold at all; and your +identity comes back in horror. Over Descartian vortices you hover. +And perhaps, at mid-day, in the fairest weather, with one +half-throttled shriek you drop through that transparent air into the +summer sea, no more to rise for ever. Heed it well, ye Pantheists! + + + +CHAPTER 36 + +The Quarter-Deck. + + +(ENTER AHAB: THEN, ALL) + + +It was not a great while after the affair of the pipe, that one +morning shortly after breakfast, Ahab, as was his wont, ascended the +cabin-gangway to the deck. There most sea-captains usually walk at +that hour, as country gentlemen, after the same meal, take a few +turns in the garden. + +Soon his steady, ivory stride was heard, as to and fro he paced his +old rounds, upon planks so familiar to his tread, that they were all +over dented, like geological stones, with the peculiar mark of his +walk. Did you fixedly gaze, too, upon that ribbed and dented brow; +there also, you would see still stranger foot-prints--the foot-prints +of his one unsleeping, ever-pacing thought. + +But on the occasion in question, those dents looked deeper, even as +his nervous step that morning left a deeper mark. And, so full of +his thought was Ahab, that at every uniform turn that he made, now at +the main-mast and now at the binnacle, you could almost see that +thought turn in him as he turned, and pace in him as he paced; so +completely possessing him, indeed, that it all but seemed the inward +mould of every outer movement. + +"D'ye mark him, Flask?" whispered Stubb; "the chick that's in him +pecks the shell. 'Twill soon be out." + +The hours wore on;--Ahab now shut up within his cabin; anon, pacing +the deck, with the same intense bigotry of purpose in his aspect. + +It drew near the close of day. Suddenly he came to a halt by the +bulwarks, and inserting his bone leg into the auger-hole there, and +with one hand grasping a shroud, he ordered Starbuck to send +everybody aft. + +"Sir!" said the mate, astonished at an order seldom or never given on +ship-board except in some extraordinary case. + +"Send everybody aft," repeated Ahab. "Mast-heads, there! come down!" + +When the entire ship's company were assembled, and with curious and +not wholly unapprehensive faces, were eyeing him, for he looked not +unlike the weather horizon when a storm is coming up, Ahab, after +rapidly glancing over the bulwarks, and then darting his eyes among +the crew, started from his standpoint; and as though not a soul were +nigh him resumed his heavy turns upon the deck. With bent head and +half-slouched hat he continued to pace, unmindful of the wondering +whispering among the men; till Stubb cautiously whispered to Flask, +that Ahab must have summoned them there for the purpose of witnessing +a pedestrian feat. But this did not last long. Vehemently pausing, +he cried:-- + +"What do ye do when ye see a whale, men?" + +"Sing out for him!" was the impulsive rejoinder from a score of +clubbed voices. + +"Good!" cried Ahab, with a wild approval in his tones; observing the +hearty animation into which his unexpected question had so +magnetically thrown them. + +"And what do ye next, men?" + +"Lower away, and after him!" + +"And what tune is it ye pull to, men?" + +"A dead whale or a stove boat!" + +More and more strangely and fiercely glad and approving, grew the +countenance of the old man at every shout; while the mariners began +to gaze curiously at each other, as if marvelling how it was that +they themselves became so excited at such seemingly purposeless +questions. + +But, they were all eagerness again, as Ahab, now half-revolving in +his pivot-hole, with one hand reaching high up a shroud, and tightly, +almost convulsively grasping it, addressed them thus:-- + +"All ye mast-headers have before now heard me give orders about a +white whale. Look ye! d'ye see this Spanish ounce of gold?"--holding +up a broad bright coin to the sun--"it is a sixteen dollar piece, +men. D'ye see it? Mr. Starbuck, hand me yon top-maul." + +While the mate was getting the hammer, Ahab, without speaking, was +slowly rubbing the gold piece against the skirts of his jacket, as if +to heighten its lustre, and without using any words was meanwhile +lowly humming to himself, producing a sound so strangely muffled and +inarticulate that it seemed the mechanical humming of the wheels of +his vitality in him. + +Receiving the top-maul from Starbuck, he advanced towards the +main-mast with the hammer uplifted in one hand, exhibiting the gold +with the other, and with a high raised voice exclaiming: "Whosoever +of ye raises me a white-headed whale with a wrinkled brow and a +crooked jaw; whosoever of ye raises me that white-headed whale, with +three holes punctured in his starboard fluke--look ye, whosoever of +ye raises me that same white whale, he shall have this gold ounce, my +boys!" + +"Huzza! huzza!" cried the seamen, as with swinging tarpaulins they +hailed the act of nailing the gold to the mast. + +"It's a white whale, I say," resumed Ahab, as he threw down the +topmaul: "a white whale. Skin your eyes for him, men; look sharp for +white water; if ye see but a bubble, sing out." + +All this while Tashtego, Daggoo, and Queequeg had looked on with even +more intense interest and surprise than the rest, and at the mention +of the wrinkled brow and crooked jaw they had started as if each was +separately touched by some specific recollection. + +"Captain Ahab," said Tashtego, "that white whale must be the same +that some call Moby Dick." + +"Moby Dick?" shouted Ahab. "Do ye know the white whale then, Tash?" + +"Does he fan-tail a little curious, sir, before he goes down?" said +the Gay-Header deliberately. + +"And has he a curious spout, too," said Daggoo, "very bushy, even for +a parmacetty, and mighty quick, Captain Ahab?" + +"And he have one, two, three--oh! good many iron in him hide, too, +Captain," cried Queequeg disjointedly, "all twiske-tee be-twisk, like +him--him--" faltering hard for a word, and screwing his hand round +and round as though uncorking a bottle--"like him--him--" + +"Corkscrew!" cried Ahab, "aye, Queequeg, the harpoons lie all twisted +and wrenched in him; aye, Daggoo, his spout is a big one, like a +whole shock of wheat, and white as a pile of our Nantucket wool after +the great annual sheep-shearing; aye, Tashtego, and he fan-tails like +a split jib in a squall. Death and devils! men, it is Moby Dick ye +have seen--Moby Dick--Moby Dick!" + +"Captain Ahab," said Starbuck, who, with Stubb and Flask, had thus +far been eyeing his superior with increasing surprise, but at last +seemed struck with a thought which somewhat explained all the wonder. +"Captain Ahab, I have heard of Moby Dick--but it was not Moby Dick +that took off thy leg?" + +"Who told thee that?" cried Ahab; then pausing, "Aye, Starbuck; aye, +my hearties all round; it was Moby Dick that dismasted me; Moby Dick +that brought me to this dead stump I stand on now. Aye, aye," he +shouted with a terrific, loud, animal sob, like that of a +heart-stricken moose; "Aye, aye! it was that accursed white whale +that razeed me; made a poor pegging lubber of me for ever and a day!" +Then tossing both arms, with measureless imprecations he shouted +out: "Aye, aye! and I'll chase him round Good Hope, and round the +Horn, and round the Norway Maelstrom, and round perdition's flames +before I give him up. And this is what ye have shipped for, men! to +chase that white whale on both sides of land, and over all sides of +earth, till he spouts black blood and rolls fin out. What say ye, +men, will ye splice hands on it, now? I think ye do look brave." + +"Aye, aye!" shouted the harpooneers and seamen, running closer to the +excited old man: "A sharp eye for the white whale; a sharp lance for +Moby Dick!" + +"God bless ye," he seemed to half sob and half shout. "God bless ye, +men. Steward! go draw the great measure of grog. But what's this +long face about, Mr. Starbuck; wilt thou not chase the white whale? +art not game for Moby Dick?" + +"I am game for his crooked jaw, and for the jaws of Death too, +Captain Ahab, if it fairly comes in the way of the business we +follow; but I came here to hunt whales, not my commander's vengeance. +How many barrels will thy vengeance yield thee even if thou gettest +it, Captain Ahab? it will not fetch thee much in our Nantucket +market." + +"Nantucket market! Hoot! But come closer, Starbuck; thou requirest +a little lower layer. If money's to be the measurer, man, and the +accountants have computed their great counting-house the globe, by +girdling it with guineas, one to every three parts of an inch; then, +let me tell thee, that my vengeance will fetch a great premium HERE!" + +"He smites his chest," whispered Stubb, "what's that for? methinks it +rings most vast, but hollow." + +"Vengeance on a dumb brute!" cried Starbuck, "that simply smote thee +from blindest instinct! Madness! To be enraged with a dumb thing, +Captain Ahab, seems blasphemous." + +"Hark ye yet again--the little lower layer. All visible objects, +man, are but as pasteboard masks. But in each event--in the living +act, the undoubted deed--there, some unknown but still reasoning +thing puts forth the mouldings of its features from behind the +unreasoning mask. If man will strike, strike through the mask! How +can the prisoner reach outside except by thrusting through the wall? +To me, the white whale is that wall, shoved near to me. Sometimes I +think there's naught beyond. But 'tis enough. He tasks me; he heaps +me; I see in him outrageous strength, with an inscrutable malice +sinewing it. That inscrutable thing is chiefly what I hate; and be +the white whale agent, or be the white whale principal, I will wreak +that hate upon him. Talk not to me of blasphemy, man; I'd strike the +sun if it insulted me. For could the sun do that, then could I do +the other; since there is ever a sort of fair play herein, jealousy +presiding over all creations. But not my master, man, is even that +fair play. Who's over me? Truth hath no confines. Take off thine +eye! more intolerable than fiends' glarings is a doltish stare! So, +so; thou reddenest and palest; my heat has melted thee to anger-glow. +But look ye, Starbuck, what is said in heat, that thing unsays +itself. There are men from whom warm words are small indignity. I +meant not to incense thee. Let it go. Look! see yonder Turkish +cheeks of spotted tawn--living, breathing pictures painted by the +sun. The Pagan leopards--the unrecking and unworshipping things, +that live; and seek, and give no reasons for the torrid life they +feel! The crew, man, the crew! Are they not one and all with Ahab, +in this matter of the whale? See Stubb! he laughs! See yonder +Chilian! he snorts to think of it. Stand up amid the general +hurricane, thy one tost sapling cannot, Starbuck! And what is it? +Reckon it. 'Tis but to help strike a fin; no wondrous feat for +Starbuck. What is it more? From this one poor hunt, then, the best +lance out of all Nantucket, surely he will not hang back, when every +foremast-hand has clutched a whetstone? Ah! constrainings seize +thee; I see! the billow lifts thee! Speak, but speak!--Aye, aye! thy +silence, then, THAT voices thee. (ASIDE) Something shot from my +dilated nostrils, he has inhaled it in his lungs. Starbuck now is +mine; cannot oppose me now, without rebellion." + +"God keep me!--keep us all!" murmured Starbuck, lowly. + +But in his joy at the enchanted, tacit acquiescence of the mate, Ahab +did not hear his foreboding invocation; nor yet the low laugh from +the hold; nor yet the presaging vibrations of the winds in the +cordage; nor yet the hollow flap of the sails against the masts, as +for a moment their hearts sank in. For again Starbuck's downcast +eyes lighted up with the stubbornness of life; the subterranean laugh +died away; the winds blew on; the sails filled out; the ship heaved +and rolled as before. Ah, ye admonitions and warnings! why stay ye +not when ye come? But rather are ye predictions than warnings, ye +shadows! Yet not so much predictions from without, as verifications +of the foregoing things within. For with little external to +constrain us, the innermost necessities in our being, these still +drive us on. + +"The measure! the measure!" cried Ahab. + +Receiving the brimming pewter, and turning to the harpooneers, he +ordered them to produce their weapons. Then ranging them before him +near the capstan, with their harpoons in their hands, while his three +mates stood at his side with their lances, and the rest of the ship's +company formed a circle round the group; he stood for an instant +searchingly eyeing every man of his crew. But those wild eyes met +his, as the bloodshot eyes of the prairie wolves meet the eye of +their leader, ere he rushes on at their head in the trail of the +bison; but, alas! only to fall into the hidden snare of the Indian. + +"Drink and pass!" he cried, handing the heavy charged flagon to the +nearest seaman. "The crew alone now drink. Round with it, round! +Short draughts--long swallows, men; 'tis hot as Satan's hoof. So, +so; it goes round excellently. It spiralizes in ye; forks out at the +serpent-snapping eye. Well done; almost drained. That way it went, +this way it comes. Hand it me--here's a hollow! Men, ye seem the +years; so brimming life is gulped and gone. Steward, refill! + +"Attend now, my braves. I have mustered ye all round this capstan; +and ye mates, flank me with your lances; and ye harpooneers, stand +there with your irons; and ye, stout mariners, ring me in, that I may +in some sort revive a noble custom of my fisherman fathers before +me. O men, you will yet see that--Ha! boy, come back? bad pennies +come not sooner. Hand it me. Why, now, this pewter had run brimming +again, were't not thou St. Vitus' imp--away, thou ague! + +"Advance, ye mates! Cross your lances full before me. Well done! +Let me touch the axis." So saying, with extended arm, he grasped the +three level, radiating lances at their crossed centre; while so +doing, suddenly and nervously twitched them; meanwhile, glancing +intently from Starbuck to Stubb; from Stubb to Flask. It seemed as +though, by some nameless, interior volition, he would fain have +shocked into them the same fiery emotion accumulated within the +Leyden jar of his own magnetic life. The three mates quailed before +his strong, sustained, and mystic aspect. Stubb and Flask looked +sideways from him; the honest eye of Starbuck fell downright. + +"In vain!" cried Ahab; "but, maybe, 'tis well. For did ye three but +once take the full-forced shock, then mine own electric thing, THAT +had perhaps expired from out me. Perchance, too, it would have +dropped ye dead. Perchance ye need it not. Down lances! And now, +ye mates, I do appoint ye three cupbearers to my three pagan kinsmen +there--yon three most honourable gentlemen and noblemen, my valiant +harpooneers. Disdain the task? What, when the great Pope washes the +feet of beggars, using his tiara for ewer? Oh, my sweet cardinals! +your own condescension, THAT shall bend ye to it. I do not order ye; +ye will it. Cut your seizings and draw the poles, ye harpooneers!" + +Silently obeying the order, the three harpooneers now stood with the +detached iron part of their harpoons, some three feet long, held, +barbs up, before him. + +"Stab me not with that keen steel! Cant them; cant them over! know +ye not the goblet end? Turn up the socket! So, so; now, ye +cup-bearers, advance. The irons! take them; hold them while I fill!" +Forthwith, slowly going from one officer to the other, he brimmed +the harpoon sockets with the fiery waters from the pewter. + +"Now, three to three, ye stand. Commend the murderous chalices! +Bestow them, ye who are now made parties to this indissoluble league. +Ha! Starbuck! but the deed is done! Yon ratifying sun now waits to +sit upon it. Drink, ye harpooneers! drink and swear, ye men that man +the deathful whaleboat's bow--Death to Moby Dick! God hunt us all, +if we do not hunt Moby Dick to his death!" The long, barbed steel +goblets were lifted; and to cries and maledictions against the white +whale, the spirits were simultaneously quaffed down with a hiss. +Starbuck paled, and turned, and shivered. Once more, and finally, +the replenished pewter went the rounds among the frantic crew; when, +waving his free hand to them, they all dispersed; and Ahab retired +within his cabin. + + + +CHAPTER 37 + +Sunset. + + +THE CABIN; BY THE STERN WINDOWS; AHAB SITTING ALONE, AND GAZING OUT. + + +I leave a white and turbid wake; pale waters, paler cheeks, where'er +I sail. The envious billows sidelong swell to whelm my track; let +them; but first I pass. + +Yonder, by ever-brimming goblet's rim, the warm waves blush like +wine. The gold brow plumbs the blue. The diver sun--slow dived from +noon--goes down; my soul mounts up! she wearies with her endless +hill. Is, then, the crown too heavy that I wear? this Iron Crown of +Lombardy. Yet is it bright with many a gem; I the wearer, see not +its far flashings; but darkly feel that I wear that, that dazzlingly +confounds. 'Tis iron--that I know--not gold. 'Tis split, too--that +I feel; the jagged edge galls me so, my brain seems to beat against +the solid metal; aye, steel skull, mine; the sort that needs no +helmet in the most brain-battering fight! + +Dry heat upon my brow? Oh! time was, when as the sunrise nobly +spurred me, so the sunset soothed. No more. This lovely light, it +lights not me; all loveliness is anguish to me, since I can ne'er +enjoy. Gifted with the high perception, I lack the low, enjoying +power; damned, most subtly and most malignantly! damned in the midst +of Paradise! Good night--good night! (WAVING HIS HAND, HE MOVES FROM +THE WINDOW.) + +'Twas not so hard a task. I thought to find one stubborn, at the +least; but my one cogged circle fits into all their various wheels, +and they revolve. Or, if you will, like so many ant-hills of powder, +they all stand before me; and I their match. Oh, hard! that to fire +others, the match itself must needs be wasting! What I've dared, +I've willed; and what I've willed, I'll do! They think me +mad--Starbuck does; but I'm demoniac, I am madness maddened! That +wild madness that's only calm to comprehend itself! The prophecy was +that I should be dismembered; and--Aye! I lost this leg. I now +prophesy that I will dismember my dismemberer. Now, then, be the +prophet and the fulfiller one. That's more than ye, ye great gods, +ever were. I laugh and hoot at ye, ye cricket-players, ye pugilists, +ye deaf Burkes and blinded Bendigoes! I will not say as schoolboys +do to bullies--Take some one of your own size; don't pommel ME! No, +ye've knocked me down, and I am up again; but YE have run and hidden. +Come forth from behind your cotton bags! I have no long gun to +reach ye. Come, Ahab's compliments to ye; come and see if ye can +swerve me. Swerve me? ye cannot swerve me, else ye swerve +yourselves! man has ye there. Swerve me? The path to my fixed +purpose is laid with iron rails, whereon my soul is grooved to run. +Over unsounded gorges, through the rifled hearts of mountains, under +torrents' beds, unerringly I rush! Naught's an obstacle, naught's an +angle to the iron way! + + + +CHAPTER 38 + +Dusk. + + +BY THE MAINMAST; STARBUCK LEANING AGAINST IT. + + +My soul is more than matched; she's overmanned; and by a madman! +Insufferable sting, that sanity should ground arms on such a field! +But he drilled deep down, and blasted all my reason out of me! I +think I see his impious end; but feel that I must help him to it. +Will I, nill I, the ineffable thing has tied me to him; tows me with +a cable I have no knife to cut. Horrible old man! Who's over him, +he cries;--aye, he would be a democrat to all above; look, how he +lords it over all below! Oh! I plainly see my miserable office,--to +obey, rebelling; and worse yet, to hate with touch of pity! For in +his eyes I read some lurid woe would shrivel me up, had I it. Yet is +there hope. Time and tide flow wide. The hated whale has the round +watery world to swim in, as the small gold-fish has its glassy globe. +His heaven-insulting purpose, God may wedge aside. I would up +heart, were it not like lead. But my whole clock's run down; my +heart the all-controlling weight, I have no key to lift again. + + +[A BURST OF REVELRY FROM THE FORECASTLE.] + + +Oh, God! to sail with such a heathen crew that have small touch of +human mothers in them! Whelped somewhere by the sharkish sea. The +white whale is their demigorgon. Hark! the infernal orgies! that +revelry is forward! mark the unfaltering silence aft! Methinks it +pictures life. Foremost through the sparkling sea shoots on the gay, +embattled, bantering bow, but only to drag dark Ahab after it, where +he broods within his sternward cabin, builded over the dead water of +the wake, and further on, hunted by its wolfish gurglings. The long +howl thrills me through! Peace! ye revellers, and set the watch! +Oh, life! 'tis in an hour like this, with soul beat down and held to +knowledge,--as wild, untutored things are forced to feed--Oh, life! +'tis now that I do feel the latent horror in thee! but 'tis not me! +that horror's out of me! and with the soft feeling of the human in +me, yet will I try to fight ye, ye grim, phantom futures! Stand by +me, hold me, bind me, O ye blessed influences! + + + +CHAPTER 39 + +First Night Watch. + +Fore-Top. + +(STUBB SOLUS, AND MENDING A BRACE.) + + +Ha! ha! ha! ha! hem! clear my throat!--I've been thinking over it +ever since, and that ha, ha's the final consequence. Why so? +Because a laugh's the wisest, easiest answer to all that's queer; and +come what will, one comfort's always left--that unfailing comfort is, +it's all predestinated. I heard not all his talk with Starbuck; but +to my poor eye Starbuck then looked something as I the other evening +felt. Be sure the old Mogul has fixed him, too. I twigged it, knew +it; had had the gift, might readily have prophesied it--for when I +clapped my eye upon his skull I saw it. Well, Stubb, WISE +Stubb--that's my title--well, Stubb, what of it, Stubb? Here's a +carcase. I know not all that may be coming, but be it what it will, +I'll go to it laughing. Such a waggish leering as lurks in all your +horribles! I feel funny. Fa, la! lirra, skirra! What's my juicy +little pear at home doing now? Crying its eyes out?--Giving a party +to the last arrived harpooneers, I dare say, gay as a frigate's +pennant, and so am I--fa, la! lirra, skirra! Oh-- + +We'll drink to-night with hearts as light, +To love, as gay and fleeting +As bubbles that swim, on the beaker's brim, +And break on the lips while meeting. + + +A brave stave that--who calls? Mr. Starbuck? Aye, aye, sir--(ASIDE) +he's my superior, he has his too, if I'm not mistaken.--Aye, aye, +sir, just through with this job--coming. + + + +CHAPTER 40 + +Midnight, Forecastle. + +HARPOONEERS AND SAILORS. + +(FORESAIL RISES AND DISCOVERS THE WATCH STANDING, LOUNGING, LEANING, +AND LYING IN VARIOUS ATTITUDES, ALL SINGING IN CHORUS.) + +Farewell and adieu to you, Spanish ladies! +Farewell and adieu to you, ladies of Spain! +Our captain's commanded.-- + +1ST NANTUCKET SAILOR. +Oh, boys, don't be sentimental; it's bad for the digestion! Take a +tonic, follow me! +(SINGS, AND ALL FOLLOW) + +Our captain stood upon the deck, +A spy-glass in his hand, +A viewing of those gallant whales +That blew at every strand. +Oh, your tubs in your boats, my boys, +And by your braces stand, +And we'll have one of those fine whales, +Hand, boys, over hand! +So, be cheery, my lads! may your hearts never fail! +While the bold harpooner is striking the whale! + +MATE'S VOICE FROM THE QUARTER-DECK. +Eight bells there, forward! + +2ND NANTUCKET SAILOR. +Avast the chorus! Eight bells there! d'ye hear, bell-boy? Strike +the bell eight, thou Pip! thou blackling! and let me call the watch. +I've the sort of mouth for that--the hogshead mouth. So, so, +(THRUSTS HIS HEAD DOWN THE SCUTTLE,) Star-bo-l-e-e-n-s, a-h-o-y! +Eight bells there below! Tumble up! + +DUTCH SAILOR. +Grand snoozing to-night, maty; fat night for that. I mark this in +our old Mogul's wine; it's quite as deadening to some as filliping to +others. We sing; they sleep--aye, lie down there, like ground-tier +butts. At 'em again! There, take this copper-pump, and hail 'em +through it. Tell 'em to avast dreaming of their lasses. Tell 'em +it's the resurrection; they must kiss their last, and come to +judgment. That's the way--THAT'S it; thy throat ain't spoiled with +eating Amsterdam butter. + +FRENCH SAILOR. +Hist, boys! let's have a jig or two before we ride to anchor in +Blanket Bay. What say ye? There comes the other watch. Stand by +all legs! Pip! little Pip! hurrah with your tambourine! + +PIP. +(SULKY AND SLEEPY) +Don't know where it is. + +FRENCH SAILOR. +Beat thy belly, then, and wag thy ears. Jig it, men, I say; merry's +the word; hurrah! Damn me, won't you dance? Form, now, Indian-file, +and gallop into the double-shuffle? Throw yourselves! Legs! legs! + +ICELAND SAILOR. +I don't like your floor, maty; it's too springy to my taste. I'm +used to ice-floors. I'm sorry to throw cold water on the subject; +but excuse me. + +MALTESE SAILOR. +Me too; where's your girls? Who but a fool would take his left hand +by his right, and say to himself, how d'ye do? Partners! I must +have partners! + +SICILIAN SAILOR. +Aye; girls and a green!--then I'll hop with ye; yea, turn +grasshopper! + +LONG-ISLAND SAILOR. +Well, well, ye sulkies, there's plenty more of us. Hoe corn when you +may, say I. All legs go to harvest soon. Ah! here comes the music; +now for it! + +AZORE SAILOR. +(ASCENDING, AND PITCHING THE TAMBOURINE UP THE SCUTTLE.) +Here you are, Pip; and there's the windlass-bitts; up you mount! +Now, boys! +(THE HALF OF THEM DANCE TO THE TAMBOURINE; SOME GO BELOW; SOME SLEEP +OR LIE AMONG THE COILS OF RIGGING. OATHS A-PLENTY.) + +AZORE SAILOR. +(DANCING) +Go it, Pip! Bang it, bell-boy! Rig it, dig it, stig it, quig it, +bell-boy! Make fire-flies; break the jinglers! + +PIP. +Jinglers, you say?--there goes another, dropped off; I pound it so. + +CHINA SAILOR. +Rattle thy teeth, then, and pound away; make a pagoda of thyself. + + +FRENCH SAILOR. +Merry-mad! Hold up thy hoop, Pip, till I jump through it! Split +jibs! tear yourselves! + +TASHTEGO. +(QUIETLY SMOKING) +That's a white man; he calls that fun: humph! I save my sweat. + +OLD MANX SAILOR. +I wonder whether those jolly lads bethink them of what they are +dancing over. I'll dance over your grave, I will--that's the +bitterest threat of your night-women, that beat head-winds round +corners. O Christ! to think of the green navies and the +green-skulled crews! Well, well; belike the whole world's a ball, as +you scholars have it; and so 'tis right to make one ballroom of it. +Dance on, lads, you're young; I was once. + +3D NANTUCKET SAILOR. +Spell oh!--whew! this is worse than pulling after whales in a +calm--give us a whiff, Tash. + +(THEY CEASE DANCING, AND GATHER IN CLUSTERS. MEANTIME THE SKY +DARKENS--THE WIND RISES.) + +LASCAR SAILOR. +By Brahma! boys, it'll be douse sail soon. The sky-born, high-tide +Ganges turned to wind! Thou showest thy black brow, Seeva! + +MALTESE SAILOR. +(RECLINING AND SHAKING HIS CAP.) +It's the waves--the snow's caps turn to jig it now. They'll shake +their tassels soon. Now would all the waves were women, then I'd go +drown, and chassee with them evermore! There's naught so sweet on +earth--heaven may not match it!--as those swift glances of warm, wild +bosoms in the dance, when the over-arboring arms hide such ripe, +bursting grapes. + +SICILIAN SAILOR. +(RECLINING.) +Tell me not of it! Hark ye, lad--fleet interlacings of the +limbs--lithe swayings--coyings--flutterings! lip! heart! hip! all +graze: unceasing touch and go! not taste, observe ye, else come +satiety. Eh, Pagan? (NUDGING.) + +TAHITAN SAILOR. +(RECLINING ON A MAT.) +Hail, holy nakedness of our dancing girls!--the Heeva-Heeva! Ah! low +veiled, high palmed Tahiti! I still rest me on thy mat, but the soft +soil has slid! I saw thee woven in the wood, my mat! green the first +day I brought ye thence; now worn and wilted quite. Ah me!--not thou +nor I can bear the change! How then, if so be transplanted to yon +sky? Hear I the roaring streams from Pirohitee's peak of spears, +when they leap down the crags and drown the villages?--The blast! the +blast! Up, spine, and meet it! (LEAPS TO HIS FEET.) + +PORTUGUESE SAILOR. +How the sea rolls swashing 'gainst the side! Stand by for reefing, +hearties! the winds are just crossing swords, pell-mell they'll go +lunging presently. + +DANISH SAILOR. +Crack, crack, old ship! so long as thou crackest, thou holdest! Well +done! The mate there holds ye to it stiffly. He's no more afraid +than the isle fort at Cattegat, put there to fight the Baltic with +storm-lashed guns, on which the sea-salt cakes! + +4TH NANTUCKET SAILOR. +He has his orders, mind ye that. I heard old Ahab tell him he must +always kill a squall, something as they burst a waterspout with a +pistol--fire your ship right into it! + +ENGLISH SAILOR. +Blood! but that old man's a grand old cove! We are the lads to hunt +him up his whale! + +ALL. +Aye! aye! + +OLD MANX SAILOR. +How the three pines shake! Pines are the hardest sort of tree to +live when shifted to any other soil, and here there's none but the +crew's cursed clay. Steady, helmsman! steady. This is the sort of +weather when brave hearts snap ashore, and keeled hulls split at sea. +Our captain has his birthmark; look yonder, boys, there's another in +the sky--lurid-like, ye see, all else pitch black. + +DAGGOO. +What of that? Who's afraid of black's afraid of me! I'm quarried +out of it! + +SPANISH SAILOR. +(ASIDE.) He wants to bully, ah!--the old grudge makes me touchy +(ADVANCING.) Aye, harpooneer, thy race is the undeniable dark side of +mankind--devilish dark at that. No offence. + +DAGGOO (GRIMLY). +None. + +ST. JAGO'S SAILOR. +That Spaniard's mad or drunk. But that can't be, or else in his one +case our old Mogul's fire-waters are somewhat long in working. + +5TH NANTUCKET SAILOR. +What's that I saw--lightning? Yes. + +SPANISH SAILOR. +No; Daggoo showing his teeth. + +DAGGOO (SPRINGING). +Swallow thine, mannikin! White skin, white liver! + +SPANISH SAILOR (MEETING HIM). +Knife thee heartily! big frame, small spirit! + +ALL. +A row! a row! a row! + +TASHTEGO (WITH A WHIFF). +A row a'low, and a row aloft--Gods and men--both brawlers! Humph! + +BELFAST SAILOR. +A row! arrah a row! The Virgin be blessed, a row! Plunge in with +ye! + +ENGLISH SAILOR. +Fair play! Snatch the Spaniard's knife! A ring, a ring! + +OLD MANX SAILOR. +Ready formed. There! the ringed horizon. In that ring Cain struck +Abel. Sweet work, right work! No? Why then, God, mad'st thou the +ring? + +MATE'S VOICE FROM THE QUARTER-DECK. +Hands by the halyards! in top-gallant sails! Stand by to reef +topsails! + +ALL. +The squall! the squall! jump, my jollies! (THEY SCATTER.) + + +PIP (SHRINKING UNDER THE WINDLASS). +Jollies? Lord help such jollies! Crish, crash! there goes the +jib-stay! Blang-whang! God! Duck lower, Pip, here comes the royal +yard! It's worse than being in the whirled woods, the last day of +the year! Who'd go climbing after chestnuts now? But there they +go, all cursing, and here I don't. Fine prospects to 'em; they're on +the road to heaven. Hold on hard! Jimmini, what a squall! But +those chaps there are worse yet--they are your white squalls, they. +White squalls? white whale, shirr! shirr! Here have I heard all +their chat just now, and the white whale--shirr! shirr!--but spoken +of once! and only this evening--it makes me jingle all over like my +tambourine--that anaconda of an old man swore 'em in to hunt him! +Oh, thou big white God aloft there somewhere in yon darkness, have +mercy on this small black boy down here; preserve him from all men +that have no bowels to feel fear! + + + +CHAPTER 41 + +Moby Dick. + + +I, Ishmael, was one of that crew; my shouts had gone up with the +rest; my oath had been welded with theirs; and stronger I shouted, +and more did I hammer and clinch my oath, because of the dread in my +soul. A wild, mystical, sympathetical feeling was in me; Ahab's +quenchless feud seemed mine. With greedy ears I learned the history +of that murderous monster against whom I and all the others had taken +our oaths of violence and revenge. + +For some time past, though at intervals only, the unaccompanied, +secluded White Whale had haunted those uncivilized seas mostly +frequented by the Sperm Whale fishermen. But not all of them knew of +his existence; only a few of them, comparatively, had knowingly seen +him; while the number who as yet had actually and knowingly given +battle to him, was small indeed. For, owing to the large number of +whale-cruisers; the disorderly way they were sprinkled over the +entire watery circumference, many of them adventurously pushing their +quest along solitary latitudes, so as seldom or never for a whole +twelvemonth or more on a stretch, to encounter a single news-telling +sail of any sort; the inordinate length of each separate voyage; the +irregularity of the times of sailing from home; all these, with other +circumstances, direct and indirect, long obstructed the spread +through the whole world-wide whaling-fleet of the special +individualizing tidings concerning Moby Dick. It was hardly to be +doubted, that several vessels reported to have encountered, at such +or such a time, or on such or such a meridian, a Sperm Whale of +uncommon magnitude and malignity, which whale, after doing great +mischief to his assailants, had completely escaped them; to some +minds it was not an unfair presumption, I say, that the whale in +question must have been no other than Moby Dick. Yet as of late the +Sperm Whale fishery had been marked by various and not unfrequent +instances of great ferocity, cunning, and malice in the monster +attacked; therefore it was, that those who by accident ignorantly +gave battle to Moby Dick; such hunters, perhaps, for the most part, +were content to ascribe the peculiar terror he bred, more, as it +were, to the perils of the Sperm Whale fishery at large, than to the +individual cause. In that way, mostly, the disastrous encounter +between Ahab and the whale had hitherto been popularly regarded. + +And as for those who, previously hearing of the White Whale, by +chance caught sight of him; in the beginning of the thing they had +every one of them, almost, as boldly and fearlessly lowered for him, +as for any other whale of that species. But at length, such +calamities did ensue in these assaults--not restricted to sprained +wrists and ankles, broken limbs, or devouring amputations--but fatal +to the last degree of fatality; those repeated disastrous repulses, +all accumulating and piling their terrors upon Moby Dick; those +things had gone far to shake the fortitude of many brave hunters, to +whom the story of the White Whale had eventually come. + +Nor did wild rumors of all sorts fail to exaggerate, and still the +more horrify the true histories of these deadly encounters. For not +only do fabulous rumors naturally grow out of the very body of all +surprising terrible events,--as the smitten tree gives birth to its +fungi; but, in maritime life, far more than in that of terra firma, +wild rumors abound, wherever there is any adequate reality for them +to cling to. And as the sea surpasses the land in this matter, so +the whale fishery surpasses every other sort of maritime life, in the +wonderfulness and fearfulness of the rumors which sometimes circulate +there. For not only are whalemen as a body unexempt from that +ignorance and superstitiousness hereditary to all sailors; but of all +sailors, they are by all odds the most directly brought into contact +with whatever is appallingly astonishing in the sea; face to face +they not only eye its greatest marvels, but, hand to jaw, give battle +to them. Alone, in such remotest waters, that though you sailed a +thousand miles, and passed a thousand shores, you would not come to +any chiseled hearth-stone, or aught hospitable beneath that part of +the sun; in such latitudes and longitudes, pursuing too such a +calling as he does, the whaleman is wrapped by influences all tending +to make his fancy pregnant with many a mighty birth. + +No wonder, then, that ever gathering volume from the mere transit +over the widest watery spaces, the outblown rumors of the White Whale +did in the end incorporate with themselves all manner of morbid +hints, and half-formed foetal suggestions of supernatural agencies, +which eventually invested Moby Dick with new terrors unborrowed from +anything that visibly appears. So that in many cases such a panic +did he finally strike, that few who by those rumors, at least, had +heard of the White Whale, few of those hunters were willing to +encounter the perils of his jaw. + +But there were still other and more vital practical influences at +work. Not even at the present day has the original prestige of the +Sperm Whale, as fearfully distinguished from all other species of the +leviathan, died out of the minds of the whalemen as a body. There +are those this day among them, who, though intelligent and courageous +enough in offering battle to the Greenland or Right whale, would +perhaps--either from professional inexperience, or incompetency, or +timidity, decline a contest with the Sperm Whale; at any rate, there +are plenty of whalemen, especially among those whaling nations not +sailing under the American flag, who have never hostilely encountered +the Sperm Whale, but whose sole knowledge of the leviathan is +restricted to the ignoble monster primitively pursued in the North; +seated on their hatches, these men will hearken with a childish +fireside interest and awe, to the wild, strange tales of Southern +whaling. Nor is the pre-eminent tremendousness of the great Sperm +Whale anywhere more feelingly comprehended, than on board of those +prows which stem him. + +And as if the now tested reality of his might had in former legendary +times thrown its shadow before it; we find some book +naturalists--Olassen and Povelson--declaring the Sperm Whale not only +to be a consternation to every other creature in the sea, but also to +be so incredibly ferocious as continually to be athirst for human +blood. Nor even down to so late a time as Cuvier's, were these or +almost similar impressions effaced. For in his Natural History, the +Baron himself affirms that at sight of the Sperm Whale, all fish +(sharks included) are "struck with the most lively terrors," and +"often in the precipitancy of their flight dash themselves against +the rocks with such violence as to cause instantaneous death." And +however the general experiences in the fishery may amend such reports +as these; yet in their full terribleness, even to the bloodthirsty +item of Povelson, the superstitious belief in them is, in some +vicissitudes of their vocation, revived in the minds of the hunters. + +So that overawed by the rumors and portents concerning him, not a few +of the fishermen recalled, in reference to Moby Dick, the earlier +days of the Sperm Whale fishery, when it was oftentimes hard to +induce long practised Right whalemen to embark in the perils of this +new and daring warfare; such men protesting that although other +leviathans might be hopefully pursued, yet to chase and point lance +at such an apparition as the Sperm Whale was not for mortal man. +That to attempt it, would be inevitably to be torn into a quick +eternity. On this head, there are some remarkable documents that may +be consulted. + +Nevertheless, some there were, who even in the face of these things +were ready to give chase to Moby Dick; and a still greater number +who, chancing only to hear of him distantly and vaguely, without the +specific details of any certain calamity, and without superstitious +accompaniments, were sufficiently hardy not to flee from the battle +if offered. + +One of the wild suggestions referred to, as at last coming to be +linked with the White Whale in the minds of the superstitiously +inclined, was the unearthly conceit that Moby Dick was ubiquitous; +that he had actually been encountered in opposite latitudes at one +and the same instant of time. + +Nor, credulous as such minds must have been, was this conceit +altogether without some faint show of superstitious probability. For +as the secrets of the currents in the seas have never yet been +divulged, even to the most erudite research; so the hidden ways of +the Sperm Whale when beneath the surface remain, in great part, +unaccountable to his pursuers; and from time to time have originated +the most curious and contradictory speculations regarding them, +especially concerning the mystic modes whereby, after sounding to a +great depth, he transports himself with such vast swiftness to the +most widely distant points. + +It is a thing well known to both American and English whale-ships, +and as well a thing placed upon authoritative record years ago by +Scoresby, that some whales have been captured far north in the +Pacific, in whose bodies have been found the barbs of harpoons darted +in the Greenland seas. Nor is it to be gainsaid, that in some of +these instances it has been declared that the interval of time +between the two assaults could not have exceeded very many days. +Hence, by inference, it has been believed by some whalemen, that the +Nor' West Passage, so long a problem to man, was never a problem to +the whale. So that here, in the real living experience of living +men, the prodigies related in old times of the inland Strello +mountain in Portugal (near whose top there was said to be a lake in +which the wrecks of ships floated up to the surface); and that still +more wonderful story of the Arethusa fountain near Syracuse (whose +waters were believed to have come from the Holy Land by an +underground passage); these fabulous narrations are almost fully +equalled by the realities of the whalemen. + +Forced into familiarity, then, with such prodigies as these; and +knowing that after repeated, intrepid assaults, the White Whale had +escaped alive; it cannot be much matter of surprise that some +whalemen should go still further in their superstitions; declaring +Moby Dick not only ubiquitous, but immortal (for immortality is but +ubiquity in time); that though groves of spears should be planted in +his flanks, he would still swim away unharmed; or if indeed he should +ever be made to spout thick blood, such a sight would be but a +ghastly deception; for again in unensanguined billows hundreds of +leagues away, his unsullied jet would once more be seen. + +But even stripped of these supernatural surmisings, there was enough +in the earthly make and incontestable character of the monster to +strike the imagination with unwonted power. For, it was not so much +his uncommon bulk that so much distinguished him from other sperm +whales, but, as was elsewhere thrown out--a peculiar snow-white +wrinkled forehead, and a high, pyramidical white hump. These were +his prominent features; the tokens whereby, even in the limitless, +uncharted seas, he revealed his identity, at a long distance, to +those who knew him. + +The rest of his body was so streaked, and spotted, and marbled with +the same shrouded hue, that, in the end, he had gained his +distinctive appellation of the White Whale; a name, indeed, literally +justified by his vivid aspect, when seen gliding at high noon through +a dark blue sea, leaving a milky-way wake of creamy foam, all +spangled with golden gleamings. + +Nor was it his unwonted magnitude, nor his remarkable hue, nor yet +his deformed lower jaw, that so much invested the whale with natural +terror, as that unexampled, intelligent malignity which, according to +specific accounts, he had over and over again evinced in his +assaults. More than all, his treacherous retreats struck more of +dismay than perhaps aught else. For, when swimming before his +exulting pursuers, with every apparent symptom of alarm, he had +several times been known to turn round suddenly, and, bearing down +upon them, either stave their boats to splinters, or drive them back +in consternation to their ship. + +Already several fatalities had attended his chase. But though +similar disasters, however little bruited ashore, were by no means +unusual in the fishery; yet, in most instances, such seemed the White +Whale's infernal aforethought of ferocity, that every dismembering or +death that he caused, was not wholly regarded as having been +inflicted by an unintelligent agent. + +Judge, then, to what pitches of inflamed, distracted fury the minds +of his more desperate hunters were impelled, when amid the chips of +chewed boats, and the sinking limbs of torn comrades, they swam out +of the white curds of the whale's direful wrath into the serene, +exasperating sunlight, that smiled on, as if at a birth or a bridal. + +His three boats stove around him, and oars and men both whirling in +the eddies; one captain, seizing the line-knife from his broken prow, +had dashed at the whale, as an Arkansas duellist at his foe, blindly +seeking with a six inch blade to reach the fathom-deep life of the +whale. That captain was Ahab. And then it was, that suddenly +sweeping his sickle-shaped lower jaw beneath him, Moby Dick had +reaped away Ahab's leg, as a mower a blade of grass in the field. No +turbaned Turk, no hired Venetian or Malay, could have smote him with +more seeming malice. Small reason was there to doubt, then, that +ever since that almost fatal encounter, Ahab had cherished a wild +vindictiveness against the whale, all the more fell for that in his +frantic morbidness he at last came to identify with him, not only all +his bodily woes, but all his intellectual and spiritual +exasperations. The White Whale swam before him as the monomaniac +incarnation of all those malicious agencies which some deep men feel +eating in them, till they are left living on with half a heart and +half a lung. That intangible malignity which has been from the +beginning; to whose dominion even the modern Christians ascribe +one-half of the worlds; which the ancient Ophites of the east +reverenced in their statue devil;--Ahab did not fall down and worship +it like them; but deliriously transferring its idea to the abhorred +white whale, he pitted himself, all mutilated, against it. All that +most maddens and torments; all that stirs up the lees of things; all +truth with malice in it; all that cracks the sinews and cakes the +brain; all the subtle demonisms of life and thought; all evil, to +crazy Ahab, were visibly personified, and made practically assailable +in Moby Dick. He piled upon the whale's white hump the sum of all +the general rage and hate felt by his whole race from Adam down; and +then, as if his chest had been a mortar, he burst his hot heart's +shell upon it. + +It is not probable that this monomania in him took its instant rise +at the precise time of his bodily dismemberment. Then, in darting at +the monster, knife in hand, he had but given loose to a sudden, +passionate, corporal animosity; and when he received the stroke that +tore him, he probably but felt the agonizing bodily laceration, but +nothing more. Yet, when by this collision forced to turn towards +home, and for long months of days and weeks, Ahab and anguish lay +stretched together in one hammock, rounding in mid winter that +dreary, howling Patagonian Cape; then it was, that his torn body and +gashed soul bled into one another; and so interfusing, made him mad. +That it was only then, on the homeward voyage, after the encounter, +that the final monomania seized him, seems all but certain from the +fact that, at intervals during the passage, he was a raving lunatic; +and, though unlimbed of a leg, yet such vital strength yet lurked in +his Egyptian chest, and was moreover intensified by his delirium, +that his mates were forced to lace him fast, even there, as he +sailed, raving in his hammock. In a strait-jacket, he swung to the +mad rockings of the gales. And, when running into more sufferable +latitudes, the ship, with mild stun'sails spread, floated across the +tranquil tropics, and, to all appearances, the old man's delirium +seemed left behind him with the Cape Horn swells, and he came forth +from his dark den into the blessed light and air; even then, when he +bore that firm, collected front, however pale, and issued his calm +orders once again; and his mates thanked God the direful madness was +now gone; even then, Ahab, in his hidden self, raved on. Human +madness is oftentimes a cunning and most feline thing. When you +think it fled, it may have but become transfigured into some still +subtler form. Ahab's full lunacy subsided not, but deepeningly +contracted; like the unabated Hudson, when that noble Northman flows +narrowly, but unfathomably through the Highland gorge. But, as in +his narrow-flowing monomania, not one jot of Ahab's broad madness had +been left behind; so in that broad madness, not one jot of his great +natural intellect had perished. That before living agent, now became +the living instrument. If such a furious trope may stand, his +special lunacy stormed his general sanity, and carried it, and turned +all its concentred cannon upon its own mad mark; so that far from +having lost his strength, Ahab, to that one end, did now possess a +thousand fold more potency than ever he had sanely brought to bear +upon any one reasonable object. + +This is much; yet Ahab's larger, darker, deeper part remains +unhinted. But vain to popularize profundities, and all truth is +profound. Winding far down from within the very heart of this spiked +Hotel de Cluny where we here stand--however grand and wonderful, now +quit it;--and take your way, ye nobler, sadder souls, to those vast +Roman halls of Thermes; where far beneath the fantastic towers of +man's upper earth, his root of grandeur, his whole awful essence sits +in bearded state; an antique buried beneath antiquities, and throned +on torsoes! So with a broken throne, the great gods mock that +captive king; so like a Caryatid, he patient sits, upholding on his +frozen brow the piled entablatures of ages. Wind ye down there, ye +prouder, sadder souls! question that proud, sad king! A family +likeness! aye, he did beget ye, ye young exiled royalties; and from +your grim sire only will the old State-secret come. + +Now, in his heart, Ahab had some glimpse of this, namely: all my +means are sane, my motive and my object mad. Yet without power to +kill, or change, or shun the fact; he likewise knew that to mankind +he did long dissemble; in some sort, did still. But that thing of +his dissembling was only subject to his perceptibility, not to his +will determinate. Nevertheless, so well did he succeed in that +dissembling, that when with ivory leg he stepped ashore at last, no +Nantucketer thought him otherwise than but naturally grieved, and +that to the quick, with the terrible casualty which had overtaken +him. + +The report of his undeniable delirium at sea was likewise popularly +ascribed to a kindred cause. And so too, all the added moodiness +which always afterwards, to the very day of sailing in the Pequod on +the present voyage, sat brooding on his brow. Nor is it so very +unlikely, that far from distrusting his fitness for another whaling +voyage, on account of such dark symptoms, the calculating people of +that prudent isle were inclined to harbor the conceit, that for those +very reasons he was all the better qualified and set on edge, for a +pursuit so full of rage and wildness as the bloody hunt of whales. +Gnawed within and scorched without, with the infixed, unrelenting +fangs of some incurable idea; such an one, could he be found, would +seem the very man to dart his iron and lift his lance against the +most appalling of all brutes. Or, if for any reason thought to be +corporeally incapacitated for that, yet such an one would seem +superlatively competent to cheer and howl on his underlings to the +attack. But be all this as it may, certain it is, that with the mad +secret of his unabated rage bolted up and keyed in him, Ahab had +purposely sailed upon the present voyage with the one only and +all-engrossing object of hunting the White Whale. Had any one of his +old acquaintances on shore but half dreamed of what was lurking in +him then, how soon would their aghast and righteous souls have +wrenched the ship from such a fiendish man! They were bent on +profitable cruises, the profit to be counted down in dollars from the +mint. He was intent on an audacious, immitigable, and supernatural +revenge. + +Here, then, was this grey-headed, ungodly old man, chasing with +curses a Job's whale round the world, at the head of a crew, too, +chiefly made up of mongrel renegades, and castaways, and +cannibals--morally enfeebled also, by the incompetence of mere +unaided virtue or right-mindedness in Starbuck, the invunerable +jollity of indifference and recklessness in Stubb, and the pervading +mediocrity in Flask. Such a crew, so officered, seemed specially +picked and packed by some infernal fatality to help him to his +monomaniac revenge. How it was that they so aboundingly responded to +the old man's ire--by what evil magic their souls were possessed, +that at times his hate seemed almost theirs; the White Whale as much +their insufferable foe as his; how all this came to be--what the +White Whale was to them, or how to their unconscious understandings, +also, in some dim, unsuspected way, he might have seemed the gliding +great demon of the seas of life,--all this to explain, would be to +dive deeper than Ishmael can go. The subterranean miner that works +in us all, how can one tell whither leads his shaft by the ever +shifting, muffled sound of his pick? Who does not feel the +irresistible arm drag? What skiff in tow of a seventy-four can stand +still? For one, I gave myself up to the abandonment of the time and +the place; but while yet all a-rush to encounter the whale, could see +naught in that brute but the deadliest ill. + + + +CHAPTER 42 + +The Whiteness of The Whale. + + +What the white whale was to Ahab, has been hinted; what, at times, he +was to me, as yet remains unsaid. + +Aside from those more obvious considerations touching Moby Dick, +which could not but occasionally awaken in any man's soul some alarm, +there was another thought, or rather vague, nameless horror +concerning him, which at times by its intensity completely +overpowered all the rest; and yet so mystical and well nigh ineffable +was it, that I almost despair of putting it in a comprehensible form. +It was the whiteness of the whale that above all things appalled me. +But how can I hope to explain myself here; and yet, in some dim, +random way, explain myself I must, else all these chapters might be +naught. + +Though in many natural objects, whiteness refiningly enhances beauty, +as if imparting some special virtue of its own, as in marbles, +japonicas, and pearls; and though various nations have in some way +recognised a certain royal preeminence in this hue; even the +barbaric, grand old kings of Pegu placing the title "Lord of the +White Elephants" above all their other magniloquent ascriptions of +dominion; and the modern kings of Siam unfurling the same snow-white +quadruped in the royal standard; and the Hanoverian flag bearing the +one figure of a snow-white charger; and the great Austrian Empire, +Caesarian, heir to overlording Rome, having for the imperial colour +the same imperial hue; and though this pre-eminence in it applies to +the human race itself, giving the white man ideal mastership over +every dusky tribe; and though, besides, all this, whiteness has been +even made significant of gladness, for among the Romans a white stone +marked a joyful day; and though in other mortal sympathies and +symbolizings, this same hue is made the emblem of many touching, +noble things--the innocence of brides, the benignity of age; though +among the Red Men of America the giving of the white belt of wampum +was the deepest pledge of honour; though in many climes, whiteness +typifies the majesty of Justice in the ermine of the Judge, and +contributes to the daily state of kings and queens drawn by +milk-white steeds; though even in the higher mysteries of the most +august religions it has been made the symbol of the divine +spotlessness and power; by the Persian fire worshippers, the white +forked flame being held the holiest on the altar; and in the Greek +mythologies, Great Jove himself being made incarnate in a snow-white +bull; and though to the noble Iroquois, the midwinter sacrifice of +the sacred White Dog was by far the holiest festival of their +theology, that spotless, faithful creature being held the purest +envoy they could send to the Great Spirit with the annual tidings of +their own fidelity; and though directly from the Latin word for +white, all Christian priests derive the name of one part of their +sacred vesture, the alb or tunic, worn beneath the cassock; and +though among the holy pomps of the Romish faith, white is specially +employed in the celebration of the Passion of our Lord; though in the +Vision of St. John, white robes are given to the redeemed, and the +four-and-twenty elders stand clothed in white before the great-white +throne, and the Holy One that sitteth there white like wool; yet for +all these accumulated associations, with whatever is sweet, and +honourable, and sublime, there yet lurks an elusive something in the +innermost idea of this hue, which strikes more of panic to the soul +than that redness which affrights in blood. + +This elusive quality it is, which causes the thought of whiteness, +when divorced from more kindly associations, and coupled with any +object terrible in itself, to heighten that terror to the furthest +bounds. Witness the white bear of the poles, and the white shark of +the tropics; what but their smooth, flaky whiteness makes them the +transcendent horrors they are? That ghastly whiteness it is which +imparts such an abhorrent mildness, even more loathsome than +terrific, to the dumb gloating of their aspect. So that not the +fierce-fanged tiger in his heraldic coat can so stagger courage as +the white-shrouded bear or shark.* + + +*With reference to the Polar bear, it may possibly be urged by him +who would fain go still deeper into this matter, that it is not the +whiteness, separately regarded, which heightens the intolerable +hideousness of that brute; for, analysed, that heightened +hideousness, it might be said, only rises from the circumstance, that +the irresponsible ferociousness of the creature stands invested in +the fleece of celestial innocence and love; and hence, by bringing +together two such opposite emotions in our minds, the Polar bear +frightens us with so unnatural a contrast. But even assuming all +this to be true; yet, were it not for the whiteness, you would not +have that intensified terror. + +As for the white shark, the white gliding ghostliness of repose in +that creature, when beheld in his ordinary moods, strangely tallies +with the same quality in the Polar quadruped. This peculiarity is +most vividly hit by the French in the name they bestow upon that +fish. The Romish mass for the dead begins with "Requiem eternam" +(eternal rest), whence REQUIEM denominating the mass itself, and any +other funeral music. Now, in allusion to the white, silent stillness +of death in this shark, and the mild deadliness of his habits, the +French call him REQUIN. + + +Bethink thee of the albatross, whence come those clouds of spiritual +wonderment and pale dread, in which that white phantom sails in all +imaginations? Not Coleridge first threw that spell; but God's great, +unflattering laureate, Nature.* + + +*I remember the first albatross I ever saw. It was during a +prolonged gale, in waters hard upon the Antarctic seas. From my +forenoon watch below, I ascended to the overclouded deck; and there, +dashed upon the main hatches, I saw a regal, feathery thing of +unspotted whiteness, and with a hooked, Roman bill sublime. At +intervals, it arched forth its vast archangel wings, as if to embrace +some holy ark. Wondrous flutterings and throbbings shook it. Though +bodily unharmed, it uttered cries, as some king's ghost in +supernatural distress. Through its inexpressible, strange eyes, +methought I peeped to secrets which took hold of God. As Abraham +before the angels, I bowed myself; the white thing was so white, its +wings so wide, and in those for ever exiled waters, I had lost the +miserable warping memories of traditions and of towns. Long I gazed +at that prodigy of plumage. I cannot tell, can only hint, the things +that darted through me then. But at last I awoke; and turning, asked +a sailor what bird was this. A goney, he replied. Goney! never had +heard that name before; is it conceivable that this glorious thing is +utterly unknown to men ashore! never! But some time after, I learned +that goney was some seaman's name for albatross. So that by no +possibility could Coleridge's wild Rhyme have had aught to do with +those mystical impressions which were mine, when I saw that bird upon +our deck. For neither had I then read the Rhyme, nor knew the bird +to be an albatross. Yet, in saying this, I do but indirectly burnish +a little brighter the noble merit of the poem and the poet. + +I assert, then, that in the wondrous bodily whiteness of the bird +chiefly lurks the secret of the spell; a truth the more evinced in +this, that by a solecism of terms there are birds called grey +albatrosses; and these I have frequently seen, but never with such +emotions as when I beheld the Antarctic fowl. + +But how had the mystic thing been caught? Whisper it not, and I will +tell; with a treacherous hook and line, as the fowl floated on the +sea. At last the Captain made a postman of it; tying a lettered, +leathern tally round its neck, with the ship's time and place; and +then letting it escape. But I doubt not, that leathern tally, meant +for man, was taken off in Heaven, when the white fowl flew to join +the wing-folding, the invoking, and adoring cherubim! + + +Most famous in our Western annals and Indian traditions is that of +the White Steed of the Prairies; a magnificent milk-white charger, +large-eyed, small-headed, bluff-chested, and with the dignity of a +thousand monarchs in his lofty, overscorning carriage. He was the +elected Xerxes of vast herds of wild horses, whose pastures in those +days were only fenced by the Rocky Mountains and the Alleghanies. At +their flaming head he westward trooped it like that chosen star which +every evening leads on the hosts of light. The flashing cascade of +his mane, the curving comet of his tail, invested him with housings +more resplendent than gold and silver-beaters could have furnished +him. A most imperial and archangelical apparition of that unfallen, +western world, which to the eyes of the old trappers and hunters +revived the glories of those primeval times when Adam walked majestic +as a god, bluff-browed and fearless as this mighty steed. Whether +marching amid his aides and marshals in the van of countless cohorts +that endlessly streamed it over the plains, like an Ohio; or whether +with his circumambient subjects browsing all around at the horizon, +the White Steed gallopingly reviewed them with warm nostrils +reddening through his cool milkiness; in whatever aspect he presented +himself, always to the bravest Indians he was the object of trembling +reverence and awe. Nor can it be questioned from what stands on +legendary record of this noble horse, that it was his spiritual +whiteness chiefly, which so clothed him with divineness; and that +this divineness had that in it which, though commanding worship, at +the same time enforced a certain nameless terror. + +But there are other instances where this whiteness loses all that +accessory and strange glory which invests it in the White Steed and +Albatross. + +What is it that in the Albino man so peculiarly repels and often +shocks the eye, as that sometimes he is loathed by his own kith and +kin! It is that whiteness which invests him, a thing expressed by +the name he bears. The Albino is as well made as other men--has no +substantive deformity--and yet this mere aspect of all-pervading +whiteness makes him more strangely hideous than the ugliest abortion. +Why should this be so? + +Nor, in quite other aspects, does Nature in her least palpable but +not the less malicious agencies, fail to enlist among her forces this +crowning attribute of the terrible. From its snowy aspect, the +gauntleted ghost of the Southern Seas has been denominated the White +Squall. Nor, in some historic instances, has the art of human malice +omitted so potent an auxiliary. How wildly it heightens the effect +of that passage in Froissart, when, masked in the snowy symbol of +their faction, the desperate White Hoods of Ghent murder their +bailiff in the market-place! + +Nor, in some things, does the common, hereditary experience of all +mankind fail to bear witness to the supernaturalism of this hue. It +cannot well be doubted, that the one visible quality in the aspect of +the dead which most appals the gazer, is the marble pallor lingering +there; as if indeed that pallor were as much like the badge of +consternation in the other world, as of mortal trepidation here. And +from that pallor of the dead, we borrow the expressive hue of the +shroud in which we wrap them. Nor even in our superstitions do we +fail to throw the same snowy mantle round our phantoms; all ghosts +rising in a milk-white fog--Yea, while these terrors seize us, let us +add, that even the king of terrors, when personified by the +evangelist, rides on his pallid horse. + +Therefore, in his other moods, symbolize whatever grand or gracious +thing he will by whiteness, no man can deny that in its profoundest +idealized significance it calls up a peculiar apparition to the soul. + +But though without dissent this point be fixed, how is mortal man to +account for it? To analyse it, would seem impossible. Can we, +then, by the citation of some of those instances wherein this thing +of whiteness--though for the time either wholly or in great part +stripped of all direct associations calculated to impart to it aught +fearful, but nevertheless, is found to exert over us the same +sorcery, however modified;--can we thus hope to light upon some +chance clue to conduct us to the hidden cause we seek? + +Let us try. But in a matter like this, subtlety appeals to subtlety, +and without imagination no man can follow another into these halls. +And though, doubtless, some at least of the imaginative impressions +about to be presented may have been shared by most men, yet few +perhaps were entirely conscious of them at the time, and therefore +may not be able to recall them now. + +Why to the man of untutored ideality, who happens to be but loosely +acquainted with the peculiar character of the day, does the bare +mention of Whitsuntide marshal in the fancy such long, dreary, +speechless processions of slow-pacing pilgrims, down-cast and hooded +with new-fallen snow? Or, to the unread, unsophisticated Protestant +of the Middle American States, why does the passing mention of a +White Friar or a White Nun, evoke such an eyeless statue in the soul? + +Or what is there apart from the traditions of dungeoned warriors and +kings (which will not wholly account for it) that makes the White +Tower of London tell so much more strongly on the imagination of an +untravelled American, than those other storied structures, its +neighbors--the Byward Tower, or even the Bloody? And those sublimer +towers, the White Mountains of New Hampshire, whence, in peculiar +moods, comes that gigantic ghostliness over the soul at the bare +mention of that name, while the thought of Virginia's Blue Ridge is +full of a soft, dewy, distant dreaminess? Or why, irrespective of +all latitudes and longitudes, does the name of the White Sea exert +such a spectralness over the fancy, while that of the Yellow Sea +lulls us with mortal thoughts of long lacquered mild afternoons on +the waves, followed by the gaudiest and yet sleepiest of sunsets? +Or, to choose a wholly unsubstantial instance, purely addressed to +the fancy, why, in reading the old fairy tales of Central Europe, +does "the tall pale man" of the Hartz forests, whose changeless +pallor unrustlingly glides through the green of the groves--why is +this phantom more terrible than all the whooping imps of the +Blocksburg? + +Nor is it, altogether, the remembrance of her cathedral-toppling +earthquakes; nor the stampedoes of her frantic seas; nor the +tearlessness of arid skies that never rain; nor the sight of her +wide field of leaning spires, wrenched cope-stones, and crosses all +adroop (like canted yards of anchored fleets); and her suburban +avenues of house-walls lying over upon each other, as a tossed pack +of cards;--it is not these things alone which make tearless Lima, the +strangest, saddest city thou can'st see. For Lima has taken the +white veil; and there is a higher horror in this whiteness of her +woe. Old as Pizarro, this whiteness keeps her ruins for ever new; +admits not the cheerful greenness of complete decay; spreads over her +broken ramparts the rigid pallor of an apoplexy that fixes its own +distortions. + +I know that, to the common apprehension, this phenomenon of whiteness +is not confessed to be the prime agent in exaggerating the terror of +objects otherwise terrible; nor to the unimaginative mind is there +aught of terror in those appearances whose awfulness to another mind +almost solely consists in this one phenomenon, especially when +exhibited under any form at all approaching to muteness or +universality. What I mean by these two statements may perhaps be +respectively elucidated by the following examples. + +First: The mariner, when drawing nigh the coasts of foreign lands, if +by night he hear the roar of breakers, starts to vigilance, and feels +just enough of trepidation to sharpen all his faculties; but under +precisely similar circumstances, let him be called from his hammock +to view his ship sailing through a midnight sea of milky +whiteness--as if from encircling headlands shoals of combed white +bears were swimming round him, then he feels a silent, superstitious +dread; the shrouded phantom of the whitened waters is horrible to him +as a real ghost; in vain the lead assures him he is still off +soundings; heart and helm they both go down; he never rests till blue +water is under him again. Yet where is the mariner who will tell +thee, "Sir, it was not so much the fear of striking hidden rocks, as +the fear of that hideous whiteness that so stirred me?" + +Second: To the native Indian of Peru, the continual sight of the +snowhowdahed Andes conveys naught of dread, except, perhaps, in the +mere fancying of the eternal frosted desolateness reigning at such +vast altitudes, and the natural conceit of what a fearfulness it +would be to lose oneself in such inhuman solitudes. Much the same is +it with the backwoodsman of the West, who with comparative +indifference views an unbounded prairie sheeted with driven snow, no +shadow of tree or twig to break the fixed trance of whiteness. Not +so the sailor, beholding the scenery of the Antarctic seas; where at +times, by some infernal trick of legerdemain in the powers of frost +and air, he, shivering and half shipwrecked, instead of rainbows +speaking hope and solace to his misery, views what seems a boundless +churchyard grinning upon him with its lean ice monuments and +splintered crosses. + +But thou sayest, methinks that white-lead chapter about whiteness is +but a white flag hung out from a craven soul; thou surrenderest to a +hypo, Ishmael. + +Tell me, why this strong young colt, foaled in some peaceful valley +of Vermont, far removed from all beasts of prey--why is it that upon +the sunniest day, if you but shake a fresh buffalo robe behind him, +so that he cannot even see it, but only smells its wild animal +muskiness--why will he start, snort, and with bursting eyes paw the +ground in phrensies of affright? There is no remembrance in him of +any gorings of wild creatures in his green northern home, so that the +strange muskiness he smells cannot recall to him anything associated +with the experience of former perils; for what knows he, this New +England colt, of the black bisons of distant Oregon? + +No; but here thou beholdest even in a dumb brute, the instinct of the +knowledge of the demonism in the world. Though thousands of miles +from Oregon, still when he smells that savage musk, the rending, +goring bison herds are as present as to the deserted wild foal of the +prairies, which this instant they may be trampling into dust. + +Thus, then, the muffled rollings of a milky sea; the bleak rustlings +of the festooned frosts of mountains; the desolate shiftings of the +windrowed snows of prairies; all these, to Ishmael, are as the +shaking of that buffalo robe to the frightened colt! + +Though neither knows where lie the nameless things of which the +mystic sign gives forth such hints; yet with me, as with the colt, +somewhere those things must exist. Though in many of its aspects +this visible world seems formed in love, the invisible spheres were +formed in fright. + +But not yet have we solved the incantation of this whiteness, and +learned why it appeals with such power to the soul; and more strange +and far more portentous--why, as we have seen, it is at once the most +meaning symbol of spiritual things, nay, the very veil of the +Christian's Deity; and yet should be as it is, the intensifying agent +in things the most appalling to mankind. + +Is it that by its indefiniteness it shadows forth the heartless voids +and immensities of the universe, and thus stabs us from behind with +the thought of annihilation, when beholding the white depths of the +milky way? Or is it, that as in essence whiteness is not so much a +colour as the visible absence of colour; and at the same time the +concrete of all colours; is it for these reasons that there is such a +dumb blankness, full of meaning, in a wide landscape of snows--a +colourless, all-colour of atheism from which we shrink? And when we +consider that other theory of the natural philosophers, that all +other earthly hues--every stately or lovely emblazoning--the sweet +tinges of sunset skies and woods; yea, and the gilded velvets of +butterflies, and the butterfly cheeks of young girls; all these are +but subtile deceits, not actually inherent in substances, but only +laid on from without; so that all deified Nature absolutely paints +like the harlot, whose allurements cover nothing but the +charnel-house within; and when we proceed further, and consider that +the mystical cosmetic which produces every one of her hues, the great +principle of light, for ever remains white or colourless in itself, +and if operating without medium upon matter, would touch all objects, +even tulips and roses, with its own blank tinge--pondering all this, +the palsied universe lies before us a leper; and like wilful +travellers in Lapland, who refuse to wear coloured and colouring +glasses upon their eyes, so the wretched infidel gazes himself blind +at the monumental white shroud that wraps all the prospect around +him. And of all these things the Albino whale was the symbol. +Wonder ye then at the fiery hunt? + + + +CHAPTER 43 + +Hark! + + +"HIST! Did you hear that noise, Cabaco? + +It was the middle-watch; a fair moonlight; the seamen were standing +in a cordon, extending from one of the fresh-water butts in the +waist, to the scuttle-butt near the taffrail. In this manner, they +passed the buckets to fill the scuttle-butt. Standing, for the most +part, on the hallowed precincts of the quarter-deck, they were +careful not to speak or rustle their feet. From hand to hand, the +buckets went in the deepest silence, only broken by the occasional +flap of a sail, and the steady hum of the unceasingly advancing keel. + +It was in the midst of this repose, that Archy, one of the cordon, +whose post was near the after-hatches, whispered to his neighbor, a +Cholo, the words above. + +"Hist! did you hear that noise, Cabaco?" + +"Take the bucket, will ye, Archy? what noise d'ye mean?" + +"There it is again--under the hatches--don't you hear it--a cough--it +sounded like a cough." + +"Cough be damned! Pass along that return bucket." + +"There again--there it is!--it sounds like two or three sleepers +turning over, now!" + +"Caramba! have done, shipmate, will ye? It's the three soaked +biscuits ye eat for supper turning over inside of ye--nothing else. +Look to the bucket!" + +"Say what ye will, shipmate; I've sharp ears." + +"Aye, you are the chap, ain't ye, that heard the hum of the old +Quakeress's knitting-needles fifty miles at sea from Nantucket; +you're the chap." + +"Grin away; we'll see what turns up. Hark ye, Cabaco, there is +somebody down in the after-hold that has not yet been seen on deck; +and I suspect our old Mogul knows something of it too. I heard Stubb +tell Flask, one morning watch, that there was something of that sort +in the wind." + +"Tish! the bucket!" + + + +CHAPTER 44 + +The Chart. + + +Had you followed Captain Ahab down into his cabin after the squall +that took place on the night succeeding that wild ratification of his +purpose with his crew, you would have seen him go to a locker in the +transom, and bringing out a large wrinkled roll of yellowish sea +charts, spread them before him on his screwed-down table. Then +seating himself before it, you would have seen him intently study the +various lines and shadings which there met his eye; and with slow but +steady pencil trace additional courses over spaces that before were +blank. At intervals, he would refer to piles of old log-books beside +him, wherein were set down the seasons and places in which, on +various former voyages of various ships, sperm whales had been +captured or seen. + +While thus employed, the heavy pewter lamp suspended in chains over +his head, continually rocked with the motion of the ship, and for +ever threw shifting gleams and shadows of lines upon his wrinkled +brow, till it almost seemed that while he himself was marking out +lines and courses on the wrinkled charts, some invisible pencil was +also tracing lines and courses upon the deeply marked chart of his +forehead. + +But it was not this night in particular that, in the solitude of his +cabin, Ahab thus pondered over his charts. Almost every night they +were brought out; almost every night some pencil marks were effaced, +and others were substituted. For with the charts of all four oceans +before him, Ahab was threading a maze of currents and eddies, with a +view to the more certain accomplishment of that monomaniac thought of +his soul. + +Now, to any one not fully acquainted with the ways of the leviathans, +it might seem an absurdly hopeless task thus to seek out one solitary +creature in the unhooped oceans of this planet. But not so did it +seem to Ahab, who knew the sets of all tides and currents; and +thereby calculating the driftings of the sperm whale's food; and, +also, calling to mind the regular, ascertained seasons for hunting +him in particular latitudes; could arrive at reasonable surmises, +almost approaching to certainties, concerning the timeliest day to be +upon this or that ground in search of his prey. + +So assured, indeed, is the fact concerning the periodicalness of the +sperm whale's resorting to given waters, that many hunters believe +that, could he be closely observed and studied throughout the world; +were the logs for one voyage of the entire whale fleet carefully +collated, then the migrations of the sperm whale would be found to +correspond in invariability to those of the herring-shoals or the +flights of swallows. On this hint, attempts have been made to +construct elaborate migratory charts of the sperm whale.* + + +*Since the above was written, the statement is happily borne out by +an official circular, issued by Lieutenant Maury, of the National +Observatory, Washington, April 16th, 1851. By that circular, it +appears that precisely such a chart is in course of completion; and +portions of it are presented in the circular. "This chart divides +the ocean into districts of five degrees of latitude by five degrees +of longitude; perpendicularly through each of which districts are +twelve columns for the twelve months; and horizontally through each +of which districts are three lines; one to show the number of days +that have been spent in each month in every district, and the two +others to show the number of days in which whales, sperm or right, +have been seen." + + +Besides, when making a passage from one feeding-ground to another, +the sperm whales, guided by some infallible instinct--say, rather, +secret intelligence from the Deity--mostly swim in VEINS, as they are +called; continuing their way along a given ocean-line with such +undeviating exactitude, that no ship ever sailed her course, by any +chart, with one tithe of such marvellous precision. Though, in these +cases, the direction taken by any one whale be straight as a +surveyor's parallel, and though the line of advance be strictly +confined to its own unavoidable, straight wake, yet the arbitrary +VEIN in which at these times he is said to swim, generally embraces +some few miles in width (more or less, as the vein is presumed to +expand or contract); but never exceeds the visual sweep from the +whale-ship's mast-heads, when circumspectly gliding along this magic +zone. The sum is, that at particular seasons within that breadth and +along that path, migrating whales may with great confidence be looked +for. + +And hence not only at substantiated times, upon well known separate +feeding-grounds, could Ahab hope to encounter his prey; but in +crossing the widest expanses of water between those grounds he could, +by his art, so place and time himself on his way, as even then not to +be wholly without prospect of a meeting. + +There was a circumstance which at first sight seemed to entangle his +delirious but still methodical scheme. But not so in the reality, +perhaps. Though the gregarious sperm whales have their regular +seasons for particular grounds, yet in general you cannot conclude +that the herds which haunted such and such a latitude or longitude +this year, say, will turn out to be identically the same with those +that were found there the preceding season; though there are peculiar +and unquestionable instances where the contrary of this has proved +true. In general, the same remark, only within a less wide limit, +applies to the solitaries and hermits among the matured, aged sperm +whales. So that though Moby Dick had in a former year been seen, for +example, on what is called the Seychelle ground in the Indian ocean, +or Volcano Bay on the Japanese Coast; yet it did not follow, that +were the Pequod to visit either of those spots at any subsequent +corresponding season, she would infallibly encounter him there. So, +too, with some other feeding grounds, where he had at times revealed +himself. But all these seemed only his casual stopping-places and +ocean-inns, so to speak, not his places of prolonged abode. And +where Ahab's chances of accomplishing his object have hitherto been +spoken of, allusion has only been made to whatever way-side, +antecedent, extra prospects were his, ere a particular set time or +place were attained, when all possibilities would become +probabilities, and, as Ahab fondly thought, every possibility the +next thing to a certainty. That particular set time and place were +conjoined in the one technical phrase--the Season-on-the-Line. For +there and then, for several consecutive years, Moby Dick had been +periodically descried, lingering in those waters for awhile, as the +sun, in its annual round, loiters for a predicted interval in any one +sign of the Zodiac. There it was, too, that most of the deadly +encounters with the white whale had taken place; there the waves were +storied with his deeds; there also was that tragic spot where the +monomaniac old man had found the awful motive to his vengeance. But +in the cautious comprehensiveness and unloitering vigilance with +which Ahab threw his brooding soul into this unfaltering hunt, he +would not permit himself to rest all his hopes upon the one crowning +fact above mentioned, however flattering it might be to those hopes; +nor in the sleeplessness of his vow could he so tranquillize his +unquiet heart as to postpone all intervening quest. + +Now, the Pequod had sailed from Nantucket at the very beginning of +the Season-on-the-Line. No possible endeavor then could enable her +commander to make the great passage southwards, double Cape Horn, and +then running down sixty degrees of latitude arrive in the equatorial +Pacific in time to cruise there. Therefore, he must wait for the +next ensuing season. Yet the premature hour of the Pequod's sailing +had, perhaps, been correctly selected by Ahab, with a view to this +very complexion of things. Because, an interval of three hundred and +sixty-five days and nights was before him; an interval which, instead +of impatiently enduring ashore, he would spend in a miscellaneous +hunt; if by chance the White Whale, spending his vacation in seas far +remote from his periodical feeding-grounds, should turn up his +wrinkled brow off the Persian Gulf, or in the Bengal Bay, or China +Seas, or in any other waters haunted by his race. So that Monsoons, +Pampas, Nor'-Westers, Harmattans, Trades; any wind but the Levanter +and Simoon, might blow Moby Dick into the devious zig-zag +world-circle of the Pequod's circumnavigating wake. + +But granting all this; yet, regarded discreetly and coolly, seems it +not but a mad idea, this; that in the broad boundless ocean, one +solitary whale, even if encountered, should be thought capable of +individual recognition from his hunter, even as a white-bearded Mufti +in the thronged thoroughfares of Constantinople? Yes. For the +peculiar snow-white brow of Moby Dick, and his snow-white hump, could +not but be unmistakable. And have I not tallied the whale, Ahab +would mutter to himself, as after poring over his charts till long +after midnight he would throw himself back in reveries--tallied him, +and shall he escape? His broad fins are bored, and scalloped out +like a lost sheep's ear! And here, his mad mind would run on in a +breathless race; till a weariness and faintness of pondering came +over him; and in the open air of the deck he would seek to recover +his strength. Ah, God! what trances of torments does that man endure +who is consumed with one unachieved revengeful desire. He sleeps +with clenched hands; and wakes with his own bloody nails in his +palms. + +Often, when forced from his hammock by exhausting and intolerably +vivid dreams of the night, which, resuming his own intense thoughts +through the day, carried them on amid a clashing of phrensies, and +whirled them round and round and round in his blazing brain, till +the very throbbing of his life-spot became insufferable anguish; and +when, as was sometimes the case, these spiritual throes in him heaved +his being up from its base, and a chasm seemed opening in him, from +which forked flames and lightnings shot up, and accursed fiends +beckoned him to leap down among them; when this hell in himself +yawned beneath him, a wild cry would be heard through the ship; and +with glaring eyes Ahab would burst from his state room, as though +escaping from a bed that was on fire. Yet these, perhaps, instead of +being the unsuppressable symptoms of some latent weakness, or fright +at his own resolve, were but the plainest tokens of its intensity. +For, at such times, crazy Ahab, the scheming, unappeasedly steadfast +hunter of the white whale; this Ahab that had gone to his hammock, +was not the agent that so caused him to burst from it in horror +again. The latter was the eternal, living principle or soul in him; +and in sleep, being for the time dissociated from the characterizing +mind, which at other times employed it for its outer vehicle or +agent, it spontaneously sought escape from the scorching contiguity +of the frantic thing, of which, for the time, it was no longer an +integral. But as the mind does not exist unless leagued with the +soul, therefore it must have been that, in Ahab's case, yielding up +all his thoughts and fancies to his one supreme purpose; that +purpose, by its own sheer inveteracy of will, forced itself against +gods and devils into a kind of self-assumed, independent being of its +own. Nay, could grimly live and burn, while the common vitality to +which it was conjoined, fled horror-stricken from the unbidden and +unfathered birth. Therefore, the tormented spirit that glared out of +bodily eyes, when what seemed Ahab rushed from his room, was for the +time but a vacated thing, a formless somnambulistic being, a ray of +living light, to be sure, but without an object to colour, and +therefore a blankness in itself. God help thee, old man, thy +thoughts have created a creature in thee; and he whose intense +thinking thus makes him a Prometheus; a vulture feeds upon that heart +for ever; that vulture the very creature he creates. + + + +CHAPTER 45 + +The Affidavit. + + +So far as what there may be of a narrative in this book; and, indeed, +as indirectly touching one or two very interesting and curious +particulars in the habits of sperm whales, the foregoing chapter, in +its earlier part, is as important a one as will be found in this +volume; but the leading matter of it requires to be still further and +more familiarly enlarged upon, in order to be adequately understood, +and moreover to take away any incredulity which a profound ignorance +of the entire subject may induce in some minds, as to the natural +verity of the main points of this affair. + +I care not to perform this part of my task methodically; but shall be +content to produce the desired impression by separate citations of +items, practically or reliably known to me as a whaleman; and from +these citations, I take it--the conclusion aimed at will naturally +follow of itself. + +First: I have personally known three instances where a whale, after +receiving a harpoon, has effected a complete escape; and, after an +interval (in one instance of three years), has been again struck by +the same hand, and slain; when the two irons, both marked by the same +private cypher, have been taken from the body. In the instance where +three years intervened between the flinging of the two harpoons; and +I think it may have been something more than that; the man who darted +them happening, in the interval, to go in a trading ship on a voyage +to Africa, went ashore there, joined a discovery party, and +penetrated far into the interior, where he travelled for a period of +nearly two years, often endangered by serpents, savages, tigers, +poisonous miasmas, with all the other common perils incident to +wandering in the heart of unknown regions. Meanwhile, the whale he +had struck must also have been on its travels; no doubt it had thrice +circumnavigated the globe, brushing with its flanks all the coasts of +Africa; but to no purpose. This man and this whale again came +together, and the one vanquished the other. I say I, myself, have +known three instances similar to this; that is in two of them I saw +the whales struck; and, upon the second attack, saw the two irons +with the respective marks cut in them, afterwards taken from the dead +fish. In the three-year instance, it so fell out that I was in the +boat both times, first and last, and the last time distinctly +recognised a peculiar sort of huge mole under the whale's eye, which +I had observed there three years previous. I say three years, but I +am pretty sure it was more than that. Here are three instances, +then, which I personally know the truth of; but I have heard of many +other instances from persons whose veracity in the matter there is no +good ground to impeach. + +Secondly: It is well known in the Sperm Whale Fishery, however +ignorant the world ashore may be of it, that there have been several +memorable historical instances where a particular whale in the ocean +has been at distant times and places popularly cognisable. Why such +a whale became thus marked was not altogether and originally owing to +his bodily peculiarities as distinguished from other whales; for +however peculiar in that respect any chance whale may be, they soon +put an end to his peculiarities by killing him, and boiling him down +into a peculiarly valuable oil. No: the reason was this: that from +the fatal experiences of the fishery there hung a terrible prestige +of perilousness about such a whale as there did about Rinaldo +Rinaldini, insomuch that most fishermen were content to recognise him +by merely touching their tarpaulins when he would be discovered +lounging by them on the sea, without seeking to cultivate a more +intimate acquaintance. Like some poor devils ashore that happen to +know an irascible great man, they make distant unobtrusive +salutations to him in the street, lest if they pursued the +acquaintance further, they might receive a summary thump for their +presumption. + +But not only did each of these famous whales enjoy great individual +celebrity--Nay, you may call it an ocean-wide renown; not only was he +famous in life and now is immortal in forecastle stories after death, +but he was admitted into all the rights, privileges, and distinctions +of a name; had as much a name indeed as Cambyses or Caesar. Was it +not so, O Timor Tom! thou famed leviathan, scarred like an iceberg, +who so long did'st lurk in the Oriental straits of that name, whose +spout was oft seen from the palmy beach of Ombay? Was it not so, O +New Zealand Jack! thou terror of all cruisers that crossed their +wakes in the vicinity of the Tattoo Land? Was it not so, O Morquan! +King of Japan, whose lofty jet they say at times assumed the +semblance of a snow-white cross against the sky? Was it not so, O +Don Miguel! thou Chilian whale, marked like an old tortoise with +mystic hieroglyphics upon the back! In plain prose, here are four +whales as well known to the students of Cetacean History as Marius or +Sylla to the classic scholar. + +But this is not all. New Zealand Tom and Don Miguel, after at +various times creating great havoc among the boats of different +vessels, were finally gone in quest of, systematically hunted out, +chased and killed by valiant whaling captains, who heaved up their +anchors with that express object as much in view, as in setting out +through the Narragansett Woods, Captain Butler of old had it in his +mind to capture that notorious murderous savage Annawon, the headmost +warrior of the Indian King Philip. + +I do not know where I can find a better place than just here, to make +mention of one or two other things, which to me seem important, as in +printed form establishing in all respects the reasonableness of the +whole story of the White Whale, more especially the catastrophe. For +this is one of those disheartening instances where truth requires +full as much bolstering as error. So ignorant are most landsmen of +some of the plainest and most palpable wonders of the world, that +without some hints touching the plain facts, historical and +otherwise, of the fishery, they might scout at Moby Dick as a +monstrous fable, or still worse and more detestable, a hideous and +intolerable allegory. + +First: Though most men have some vague flitting ideas of the general +perils of the grand fishery, yet they have nothing like a fixed, +vivid conception of those perils, and the frequency with which they +recur. One reason perhaps is, that not one in fifty of the actual +disasters and deaths by casualties in the fishery, ever finds a +public record at home, however transient and immediately forgotten +that record. Do you suppose that that poor fellow there, who this +moment perhaps caught by the whale-line off the coast of New Guinea, +is being carried down to the bottom of the sea by the sounding +leviathan--do you suppose that that poor fellow's name will appear in +the newspaper obituary you will read to-morrow at your breakfast? +No: because the mails are very irregular between here and New Guinea. +In fact, did you ever hear what might be called regular news direct +or indirect from New Guinea? Yet I tell you that upon one particular +voyage which I made to the Pacific, among many others we spoke thirty +different ships, every one of which had had a death by a whale, some +of them more than one, and three that had each lost a boat's crew. +For God's sake, be economical with your lamps and candles! not a +gallon you burn, but at least one drop of man's blood was spilled for +it. + +Secondly: People ashore have indeed some indefinite idea that a whale +is an enormous creature of enormous power; but I have ever found that +when narrating to them some specific example of this two-fold +enormousness, they have significantly complimented me upon my +facetiousness; when, I declare upon my soul, I had no more idea of +being facetious than Moses, when he wrote the history of the plagues +of Egypt. + +But fortunately the special point I here seek can be established upon +testimony entirely independent of my own. That point is this: The +Sperm Whale is in some cases sufficiently powerful, knowing, and +judiciously malicious, as with direct aforethought to stave in, +utterly destroy, and sink a large ship; and what is more, the Sperm +Whale HAS done it. + +First: In the year 1820 the ship Essex, Captain Pollard, of +Nantucket, was cruising in the Pacific Ocean. One day she saw +spouts, lowered her boats, and gave chase to a shoal of sperm whales. +Ere long, several of the whales were wounded; when, suddenly, a very +large whale escaping from the boats, issued from the shoal, and bore +directly down upon the ship. Dashing his forehead against her hull, +he so stove her in, that in less than "ten minutes" she settled down +and fell over. Not a surviving plank of her has been seen since. +After the severest exposure, part of the crew reached the land in +their boats. Being returned home at last, Captain Pollard once more +sailed for the Pacific in command of another ship, but the gods +shipwrecked him again upon unknown rocks and breakers; for the second +time his ship was utterly lost, and forthwith forswearing the sea, he +has never tempted it since. At this day Captain Pollard is a +resident of Nantucket. I have seen Owen Chace, who was chief mate of +the Essex at the time of the tragedy; I have read his plain and +faithful narrative; I have conversed with his son; and all this +within a few miles of the scene of the catastrophe.* + + +*The following are extracts from Chace's narrative: "Every fact +seemed to warrant me in concluding that it was anything but chance +which directed his operations; he made two several attacks upon the +ship, at a short interval between them, both of which, according to +their direction, were calculated to do us the most injury, by being +made ahead, and thereby combining the speed of the two objects for +the shock; to effect which, the exact manoeuvres which he made were +necessary. His aspect was most horrible, and such as indicated +resentment and fury. He came directly from the shoal which we had +just before entered, and in which we had struck three of his +companions, as if fired with revenge for their sufferings." Again: +"At all events, the whole circumstances taken together, all happening +before my own eyes, and producing, at the time, impressions in my +mind of decided, calculating mischief, on the part of the whale (many +of which impressions I cannot now recall), induce me to be satisfied +that I am correct in my opinion." + +Here are his reflections some time after quitting the ship, during a +black night an open boat, when almost despairing of reaching any +hospitable shore. "The dark ocean and swelling waters were nothing; +the fears of being swallowed up by some dreadful tempest, or dashed +upon hidden rocks, with all the other ordinary subjects of fearful +contemplation, seemed scarcely entitled to a moment's thought; the +dismal looking wreck, and THE HORRID ASPECT AND REVENGE OF THE WHALE, +wholly engrossed my reflections, until day again made its +appearance." + +In another place--p. 45,--he speaks of "THE MYSTERIOUS AND MORTAL +ATTACK OF THE ANIMAL." + + +Secondly: The ship Union, also of Nantucket, was in the year 1807 +totally lost off the Azores by a similar onset, but the authentic +particulars of this catastrophe I have never chanced to encounter, +though from the whale hunters I have now and then heard casual +allusions to it. + +Thirdly: Some eighteen or twenty years ago Commodore J---, then +commanding an American sloop-of-war of the first class, happened to +be dining with a party of whaling captains, on board a Nantucket ship +in the harbor of Oahu, Sandwich Islands. Conversation turning upon +whales, the Commodore was pleased to be sceptical touching the +amazing strength ascribed to them by the professional gentlemen +present. He peremptorily denied for example, that any whale could so +smite his stout sloop-of-war as to cause her to leak so much as a +thimbleful. Very good; but there is more coming. Some weeks after, +the Commodore set sail in this impregnable craft for Valparaiso. But +he was stopped on the way by a portly sperm whale, that begged a few +moments' confidential business with him. That business consisted in +fetching the Commodore's craft such a thwack, that with all his pumps +going he made straight for the nearest port to heave down and repair. +I am not superstitious, but I consider the Commodore's interview +with that whale as providential. Was not Saul of Tarsus converted +from unbelief by a similar fright? I tell you, the sperm whale will +stand no nonsense. + +I will now refer you to Langsdorff's Voyages for a little +circumstance in point, peculiarly interesting to the writer hereof. +Langsdorff, you must know by the way, was attached to the Russian +Admiral Krusenstern's famous Discovery Expedition in the beginning of +the present century. Captain Langsdorff thus begins his seventeenth +chapter: + +"By the thirteenth of May our ship was ready to sail, and the next +day we were out in the open sea, on our way to Ochotsh. The weather +was very clear and fine, but so intolerably cold that we were obliged +to keep on our fur clothing. For some days we had very little wind; +it was not till the nineteenth that a brisk gale from the northwest +sprang up. An uncommon large whale, the body of which was larger +than the ship itself, lay almost at the surface of the water, but was +not perceived by any one on board till the moment when the ship, +which was in full sail, was almost upon him, so that it was +impossible to prevent its striking against him. We were thus placed +in the most imminent danger, as this gigantic creature, setting up +its back, raised the ship three feet at least out of the water. The +masts reeled, and the sails fell altogether, while we who were below +all sprang instantly upon the deck, concluding that we had struck +upon some rock; instead of this we saw the monster sailing off with +the utmost gravity and solemnity. Captain D'Wolf applied immediately +to the pumps to examine whether or not the vessel had received any +damage from the shock, but we found that very happily it had escaped +entirely uninjured." + +Now, the Captain D'Wolf here alluded to as commanding the ship in +question, is a New Englander, who, after a long life of unusual +adventures as a sea-captain, this day resides in the village of +Dorchester near Boston. I have the honour of being a nephew of his. +I have particularly questioned him concerning this passage in +Langsdorff. He substantiates every word. The ship, however, was by +no means a large one: a Russian craft built on the Siberian coast, +and purchased by my uncle after bartering away the vessel in which he +sailed from home. + +In that up and down manly book of old-fashioned adventure, so full, +too, of honest wonders--the voyage of Lionel Wafer, one of ancient +Dampier's old chums--I found a little matter set down so like that +just quoted from Langsdorff, that I cannot forbear inserting it here +for a corroborative example, if such be needed. + +Lionel, it seems, was on his way to "John Ferdinando," as he calls +the modern Juan Fernandes. "In our way thither," he says, "about +four o'clock in the morning, when we were about one hundred and fifty +leagues from the Main of America, our ship felt a terrible shock, +which put our men in such consternation that they could hardly tell +where they were or what to think; but every one began to prepare for +death. And, indeed, the shock was so sudden and violent, that we +took it for granted the ship had struck against a rock; but when the +amazement was a little over, we cast the lead, and sounded, but found +no ground. .... The suddenness of the shock made the guns leap in +their carriages, and several of the men were shaken out of their +hammocks. Captain Davis, who lay with his head on a gun, was thrown +out of his cabin!" Lionel then goes on to impute the shock to an +earthquake, and seems to substantiate the imputation by stating that +a great earthquake, somewhere about that time, did actually do great +mischief along the Spanish land. But I should not much wonder if, in +the darkness of that early hour of the morning, the shock was after +all caused by an unseen whale vertically bumping the hull from +beneath. + +I might proceed with several more examples, one way or another known +to me, of the great power and malice at times of the sperm whale. In +more than one instance, he has been known, not only to chase the +assailing boats back to their ships, but to pursue the ship itself, +and long withstand all the lances hurled at him from its decks. The +English ship Pusie Hall can tell a story on that head; and, as for +his strength, let me say, that there have been examples where the +lines attached to a running sperm whale have, in a calm, been +transferred to the ship, and secured there; the whale towing her +great hull through the water, as a horse walks off with a cart. +Again, it is very often observed that, if the sperm whale, once +struck, is allowed time to rally, he then acts, not so often with +blind rage, as with wilful, deliberate designs of destruction to his +pursuers; nor is it without conveying some eloquent indication of his +character, that upon being attacked he will frequently open his +mouth, and retain it in that dread expansion for several consecutive +minutes. But I must be content with only one more and a concluding +illustration; a remarkable and most significant one, by which you +will not fail to see, that not only is the most marvellous event in +this book corroborated by plain facts of the present day, but that +these marvels (like all marvels) are mere repetitions of the ages; so +that for the millionth time we say amen with Solomon--Verily there is +nothing new under the sun. + +In the sixth Christian century lived Procopius, a Christian +magistrate of Constantinople, in the days when Justinian was Emperor +and Belisarius general. As many know, he wrote the history of his +own times, a work every way of uncommon value. By the best +authorities, he has always been considered a most trustworthy and +unexaggerating historian, except in some one or two particulars, not +at all affecting the matter presently to be mentioned. + +Now, in this history of his, Procopius mentions that, during the term +of his prefecture at Constantinople, a great sea-monster was captured +in the neighboring Propontis, or Sea of Marmora, after having +destroyed vessels at intervals in those waters for a period of more +than fifty years. A fact thus set down in substantial history cannot +easily be gainsaid. Nor is there any reason it should be. Of what +precise species this sea-monster was, is not mentioned. But as he +destroyed ships, as well as for other reasons, he must have been a +whale; and I am strongly inclined to think a sperm whale. And I will +tell you why. For a long time I fancied that the sperm whale had +been always unknown in the Mediterranean and the deep waters +connecting with it. Even now I am certain that those seas are not, +and perhaps never can be, in the present constitution of things, a +place for his habitual gregarious resort. But further investigations +have recently proved to me, that in modern times there have been +isolated instances of the presence of the sperm whale in the +Mediterranean. I am told, on good authority, that on the Barbary +coast, a Commodore Davis of the British navy found the skeleton of a +sperm whale. Now, as a vessel of war readily passes through the +Dardanelles, hence a sperm whale could, by the same route, pass out +of the Mediterranean into the Propontis. + +In the Propontis, as far as I can learn, none of that peculiar +substance called BRIT is to be found, the aliment of the right whale. +But I have every reason to believe that the food of the sperm +whale--squid or cuttle-fish--lurks at the bottom of that sea, because +large creatures, but by no means the largest of that sort, have been +found at its surface. If, then, you properly put these statements +together, and reason upon them a bit, you will clearly perceive that, +according to all human reasoning, Procopius's sea-monster, that for +half a century stove the ships of a Roman Emperor, must in all +probability have been a sperm whale. + + + +CHAPTER 46 + +Surmises. + + +Though, consumed with the hot fire of his purpose, Ahab in all his +thoughts and actions ever had in view the ultimate capture of Moby +Dick; though he seemed ready to sacrifice all mortal interests to +that one passion; nevertheless it may have been that he was by nature +and long habituation far too wedded to a fiery whaleman's ways, +altogether to abandon the collateral prosecution of the voyage. Or +at least if this were otherwise, there were not wanting other motives +much more influential with him. It would be refining too much, +perhaps, even considering his monomania, to hint that his +vindictiveness towards the White Whale might have possibly extended +itself in some degree to all sperm whales, and that the more monsters +he slew by so much the more he multiplied the chances that each +subsequently encountered whale would prove to be the hated one he +hunted. But if such an hypothesis be indeed exceptionable, there +were still additional considerations which, though not so strictly +according with the wildness of his ruling passion, yet were by no +means incapable of swaying him. + +To accomplish his object Ahab must use tools; and of all tools used +in the shadow of the moon, men are most apt to get out of order. He +knew, for example, that however magnetic his ascendency in some +respects was over Starbuck, yet that ascendency did not cover the +complete spiritual man any more than mere corporeal superiority +involves intellectual mastership; for to the purely spiritual, the +intellectual but stand in a sort of corporeal relation. Starbuck's +body and Starbuck's coerced will were Ahab's, so long as Ahab kept +his magnet at Starbuck's brain; still he knew that for all this the +chief mate, in his soul, abhorred his captain's quest, and could he, +would joyfully disintegrate himself from it, or even frustrate it. +It might be that a long interval would elapse ere the White Whale was +seen. During that long interval Starbuck would ever be apt to fall +into open relapses of rebellion against his captain's leadership, +unless some ordinary, prudential, circumstantial influences were +brought to bear upon him. Not only that, but the subtle insanity of +Ahab respecting Moby Dick was noways more significantly manifested +than in his superlative sense and shrewdness in foreseeing that, for +the present, the hunt should in some way be stripped of that strange +imaginative impiousness which naturally invested it; that the full +terror of the voyage must be kept withdrawn into the obscure +background (for few men's courage is proof against protracted +meditation unrelieved by action); that when they stood their long +night watches, his officers and men must have some nearer things to +think of than Moby Dick. For however eagerly and impetuously the +savage crew had hailed the announcement of his quest; yet all sailors +of all sorts are more or less capricious and unreliable--they live in +the varying outer weather, and they inhale its fickleness--and when +retained for any object remote and blank in the pursuit, however +promissory of life and passion in the end, it is above all things +requisite that temporary interests and employments should intervene +and hold them healthily suspended for the final dash. + +Nor was Ahab unmindful of another thing. In times of strong emotion +mankind disdain all base considerations; but such times are +evanescent. The permanent constitutional condition of the +manufactured man, thought Ahab, is sordidness. Granting that the +White Whale fully incites the hearts of this my savage crew, and +playing round their savageness even breeds a certain generous +knight-errantism in them, still, while for the love of it they give +chase to Moby Dick, they must also have food for their more common, +daily appetites. For even the high lifted and chivalric Crusaders of +old times were not content to traverse two thousand miles of land to +fight for their holy sepulchre, without committing burglaries, +picking pockets, and gaining other pious perquisites by the way. Had +they been strictly held to their one final and romantic object--that +final and romantic object, too many would have turned from in +disgust. I will not strip these men, thought Ahab, of all hopes of +cash--aye, cash. They may scorn cash now; but let some months go by, +and no perspective promise of it to them, and then this same +quiescent cash all at once mutinying in them, this same cash would +soon cashier Ahab. + +Nor was there wanting still another precautionary motive more related +to Ahab personally. Having impulsively, it is probable, and perhaps +somewhat prematurely revealed the prime but private purpose of the +Pequod's voyage, Ahab was now entirely conscious that, in so doing, +he had indirectly laid himself open to the unanswerable charge of +usurpation; and with perfect impunity, both moral and legal, his crew +if so disposed, and to that end competent, could refuse all further +obedience to him, and even violently wrest from him the command. +From even the barely hinted imputation of usurpation, and the +possible consequences of such a suppressed impression gaining ground, +Ahab must of course have been most anxious to protect himself. That +protection could only consist in his own predominating brain and +heart and hand, backed by a heedful, closely calculating attention to +every minute atmospheric influence which it was possible for his crew +to be subjected to. + +For all these reasons then, and others perhaps too analytic to be +verbally developed here, Ahab plainly saw that he must still in a +good degree continue true to the natural, nominal purpose of the +Pequod's voyage; observe all customary usages; and not only that, but +force himself to evince all his well known passionate interest in the +general pursuit of his profession. + +Be all this as it may, his voice was now often heard hailing the +three mast-heads and admonishing them to keep a bright look-out, and +not omit reporting even a porpoise. This vigilance was not long +without reward. + + + +CHAPTER 47 + +The Mat-Maker. + + +It was a cloudy, sultry afternoon; the seamen were lazily lounging +about the decks, or vacantly gazing over into the lead-coloured +waters. Queequeg and I were mildly employed weaving what is called a +sword-mat, for an additional lashing to our boat. So still and +subdued and yet somehow preluding was all the scene, and such an +incantation of reverie lurked in the air, that each silent sailor +seemed resolved into his own invisible self. + +I was the attendant or page of Queequeg, while busy at the mat. As I +kept passing and repassing the filling or woof of marline between the +long yarns of the warp, using my own hand for the shuttle, and as +Queequeg, standing sideways, ever and anon slid his heavy oaken sword +between the threads, and idly looking off upon the water, carelessly +and unthinkingly drove home every yarn: I say so strange a +dreaminess did there then reign all over the ship and all over the +sea, only broken by the intermitting dull sound of the sword, that it +seemed as if this were the Loom of Time, and I myself were a shuttle +mechanically weaving and weaving away at the Fates. There lay the +fixed threads of the warp subject to but one single, ever returning, +unchanging vibration, and that vibration merely enough to admit of +the crosswise interblending of other threads with its own. This warp +seemed necessity; and here, thought I, with my own hand I ply my own +shuttle and weave my own destiny into these unalterable threads. +Meantime, Queequeg's impulsive, indifferent sword, sometimes hitting +the woof slantingly, or crookedly, or strongly, or weakly, as the +case might be; and by this difference in the concluding blow +producing a corresponding contrast in the final aspect of the +completed fabric; this savage's sword, thought I, which thus finally +shapes and fashions both warp and woof; this easy, indifferent sword +must be chance--aye, chance, free will, and necessity--nowise +incompatible--all interweavingly working together. The straight warp +of necessity, not to be swerved from its ultimate course--its every +alternating vibration, indeed, only tending to that; free will still +free to ply her shuttle between given threads; and chance, though +restrained in its play within the right lines of necessity, and +sideways in its motions directed by free will, though thus prescribed +to by both, chance by turns rules either, and has the last featuring +blow at events. + + +Thus we were weaving and weaving away when I started at a sound so +strange, long drawn, and musically wild and unearthly, that the ball +of free will dropped from my hand, and I stood gazing up at the +clouds whence that voice dropped like a wing. High aloft in the +cross-trees was that mad Gay-Header, Tashtego. His body was reaching +eagerly forward, his hand stretched out like a wand, and at brief +sudden intervals he continued his cries. To be sure the same sound +was that very moment perhaps being heard all over the seas, from +hundreds of whalemen's look-outs perched as high in the air; but from +few of those lungs could that accustomed old cry have derived such a +marvellous cadence as from Tashtego the Indian's. + +As he stood hovering over you half suspended in air, so wildly and +eagerly peering towards the horizon, you would have thought him some +prophet or seer beholding the shadows of Fate, and by those wild +cries announcing their coming. + +"There she blows! there! there! there! she blows! she blows!" + +"Where-away?" + +"On the lee-beam, about two miles off! a school of them!" + +Instantly all was commotion. + +The Sperm Whale blows as a clock ticks, with the same undeviating and +reliable uniformity. And thereby whalemen distinguish this fish from +other tribes of his genus. + +"There go flukes!" was now the cry from Tashtego; and the whales +disappeared. + +"Quick, steward!" cried Ahab. "Time! time!" + +Dough-Boy hurried below, glanced at the watch, and reported the exact +minute to Ahab. + +The ship was now kept away from the wind, and she went gently rolling +before it. Tashtego reporting that the whales had gone down heading +to leeward, we confidently looked to see them again directly in +advance of our bows. For that singular craft at times evinced by the +Sperm Whale when, sounding with his head in one direction, he +nevertheless, while concealed beneath the surface, mills round, and +swiftly swims off in the opposite quarter--this deceitfulness of his +could not now be in action; for there was no reason to suppose that +the fish seen by Tashtego had been in any way alarmed, or indeed knew +at all of our vicinity. One of the men selected for +shipkeepers--that is, those not appointed to the boats, by this time +relieved the Indian at the main-mast head. The sailors at the fore +and mizzen had come down; the line tubs were fixed in their places; +the cranes were thrust out; the mainyard was backed, and the three +boats swung over the sea like three samphire baskets over high +cliffs. Outside of the bulwarks their eager crews with one hand +clung to the rail, while one foot was expectantly poised on the +gunwale. So look the long line of man-of-war's men about to throw +themselves on board an enemy's ship. + +But at this critical instant a sudden exclamation was heard that took +every eye from the whale. With a start all glared at dark Ahab, who +was surrounded by five dusky phantoms that seemed fresh formed out of +air. + + + +CHAPTER 48 + +The First Lowering. + + +The phantoms, for so they then seemed, were flitting on the other +side of the deck, and, with a noiseless celerity, were casting loose +the tackles and bands of the boat which swung there. This boat had +always been deemed one of the spare boats, though technically called +the captain's, on account of its hanging from the starboard quarter. +The figure that now stood by its bows was tall and swart, with one +white tooth evilly protruding from its steel-like lips. A rumpled +Chinese jacket of black cotton funereally invested him, with wide +black trowsers of the same dark stuff. But strangely crowning this +ebonness was a glistening white plaited turban, the living hair +braided and coiled round and round upon his head. Less swart in +aspect, the companions of this figure were of that vivid, +tiger-yellow complexion peculiar to some of the aboriginal natives of +the Manillas;--a race notorious for a certain diabolism of subtilty, +and by some honest white mariners supposed to be the paid spies and +secret confidential agents on the water of the devil, their lord, +whose counting-room they suppose to be elsewhere. + +While yet the wondering ship's company were gazing upon these +strangers, Ahab cried out to the white-turbaned old man at their +head, "All ready there, Fedallah?" + +"Ready," was the half-hissed reply. + +"Lower away then; d'ye hear?" shouting across the deck. "Lower away +there, I say." + +Such was the thunder of his voice, that spite of their amazement the +men sprang over the rail; the sheaves whirled round in the blocks; +with a wallow, the three boats dropped into the sea; while, with a +dexterous, off-handed daring, unknown in any other vocation, the +sailors, goat-like, leaped down the rolling ship's side into the +tossed boats below. + +Hardly had they pulled out from under the ship's lee, when a fourth +keel, coming from the windward side, pulled round under the stern, +and showed the five strangers rowing Ahab, who, standing erect in the +stern, loudly hailed Starbuck, Stubb, and Flask, to spread themselves +widely, so as to cover a large expanse of water. But with all their +eyes again riveted upon the swart Fedallah and his crew, the inmates +of the other boats obeyed not the command. + +"Captain Ahab?--" said Starbuck. + +"Spread yourselves," cried Ahab; "give way, all four boats. Thou, +Flask, pull out more to leeward!" + +"Aye, aye, sir," cheerily cried little King-Post, sweeping round his +great steering oar. "Lay back!" addressing his crew. +"There!--there!--there again! There she blows right ahead, +boys!--lay back!" + +"Never heed yonder yellow boys, Archy." + +"Oh, I don't mind'em, sir," said Archy; "I knew it all before now. +Didn't I hear 'em in the hold? And didn't I tell Cabaco here of it? +What say ye, Cabaco? They are stowaways, Mr. Flask." + +"Pull, pull, my fine hearts-alive; pull, my children; pull, my little +ones," drawlingly and soothingly sighed Stubb to his crew, some of +whom still showed signs of uneasiness. "Why don't you break your +backbones, my boys? What is it you stare at? Those chaps in yonder +boat? Tut! They are only five more hands come to help us--never +mind from where--the more the merrier. Pull, then, do pull; never +mind the brimstone--devils are good fellows enough. So, so; there +you are now; that's the stroke for a thousand pounds; that's the +stroke to sweep the stakes! Hurrah for the gold cup of sperm oil, my +heroes! Three cheers, men--all hearts alive! Easy, easy; don't be +in a hurry--don't be in a hurry. Why don't you snap your oars, you +rascals? Bite something, you dogs! So, so, so, then:--softly, +softly! That's it--that's it! long and strong. Give way there, give +way! The devil fetch ye, ye ragamuffin rapscallions; ye are all +asleep. Stop snoring, ye sleepers, and pull. Pull, will ye? pull, +can't ye? pull, won't ye? Why in the name of gudgeons and +ginger-cakes don't ye pull?--pull and break something! pull, and +start your eyes out! Here!" whipping out the sharp knife from his +girdle; "every mother's son of ye draw his knife, and pull with the +blade between his teeth. That's it--that's it. Now ye do something; +that looks like it, my steel-bits. Start her--start her, my +silver-spoons! Start her, marling-spikes!" + +Stubb's exordium to his crew is given here at large, because he had +rather a peculiar way of talking to them in general, and especially +in inculcating the religion of rowing. But you must not suppose from +this specimen of his sermonizings that he ever flew into downright +passions with his congregation. Not at all; and therein consisted +his chief peculiarity. He would say the most terrific things to his +crew, in a tone so strangely compounded of fun and fury, and the fury +seemed so calculated merely as a spice to the fun, that no oarsman +could hear such queer invocations without pulling for dear life, and +yet pulling for the mere joke of the thing. Besides he all the time +looked so easy and indolent himself, so loungingly managed his +steering-oar, and so broadly gaped--open-mouthed at times--that the +mere sight of such a yawning commander, by sheer force of contrast, +acted like a charm upon the crew. Then again, Stubb was one of those +odd sort of humorists, whose jollity is sometimes so curiously +ambiguous, as to put all inferiors on their guard in the matter of +obeying them. + +In obedience to a sign from Ahab, Starbuck was now pulling obliquely +across Stubb's bow; and when for a minute or so the two boats were +pretty near to each other, Stubb hailed the mate. + +"Mr. Starbuck! larboard boat there, ahoy! a word with ye, sir, if ye +please!" + +"Halloa!" returned Starbuck, turning round not a single inch as he +spoke; still earnestly but whisperingly urging his crew; his face set +like a flint from Stubb's. + +"What think ye of those yellow boys, sir! + +"Smuggled on board, somehow, before the ship sailed. (Strong, strong, +boys!)" in a whisper to his crew, then speaking out loud again: "A +sad business, Mr. Stubb! (seethe her, seethe her, my lads!) but never +mind, Mr. Stubb, all for the best. Let all your crew pull strong, +come what will. (Spring, my men, spring!) There's hogsheads of sperm +ahead, Mr. Stubb, and that's what ye came for. (Pull, my boys!) +Sperm, sperm's the play! This at least is duty; duty and profit hand +in hand." + +"Aye, aye, I thought as much," soliloquized Stubb, when the boats +diverged, "as soon as I clapt eye on 'em, I thought so. Aye, and +that's what he went into the after hold for, so often, as Dough-Boy +long suspected. They were hidden down there. The White Whale's at +the bottom of it. Well, well, so be it! Can't be helped! All +right! Give way, men! It ain't the White Whale to-day! Give way!" + +Now the advent of these outlandish strangers at such a critical +instant as the lowering of the boats from the deck, this had not +unreasonably awakened a sort of superstitious amazement in some of +the ship's company; but Archy's fancied discovery having some time +previous got abroad among them, though indeed not credited then, this +had in some small measure prepared them for the event. It took off +the extreme edge of their wonder; and so what with all this and +Stubb's confident way of accounting for their appearance, they were +for the time freed from superstitious surmisings; though the affair +still left abundant room for all manner of wild conjectures as to +dark Ahab's precise agency in the matter from the beginning. For me, +I silently recalled the mysterious shadows I had seen creeping on +board the Pequod during the dim Nantucket dawn, as well as the +enigmatical hintings of the unaccountable Elijah. + +Meantime, Ahab, out of hearing of his officers, having sided the +furthest to windward, was still ranging ahead of the other boats; a +circumstance bespeaking how potent a crew was pulling him. Those +tiger yellow creatures of his seemed all steel and whalebone; like +five trip-hammers they rose and fell with regular strokes of +strength, which periodically started the boat along the water like a +horizontal burst boiler out of a Mississippi steamer. As for +Fedallah, who was seen pulling the harpooneer oar, he had thrown +aside his black jacket, and displayed his naked chest with the whole +part of his body above the gunwale, clearly cut against the +alternating depressions of the watery horizon; while at the other end +of the boat Ahab, with one arm, like a fencer's, thrown half backward +into the air, as if to counterbalance any tendency to trip; Ahab was +seen steadily managing his steering oar as in a thousand boat +lowerings ere the White Whale had torn him. All at once the +outstretched arm gave a peculiar motion and then remained fixed, +while the boat's five oars were seen simultaneously peaked. Boat and +crew sat motionless on the sea. Instantly the three spread boats in +the rear paused on their way. The whales had irregularly settled +bodily down into the blue, thus giving no distantly discernible token +of the movement, though from his closer vicinity Ahab had observed +it. + +"Every man look out along his oars!" cried Starbuck. "Thou, +Queequeg, stand up!" + +Nimbly springing up on the triangular raised box in the bow, the +savage stood erect there, and with intensely eager eyes gazed off +towards the spot where the chase had last been descried. Likewise +upon the extreme stern of the boat where it was also triangularly +platformed level with the gunwale, Starbuck himself was seen coolly +and adroitly balancing himself to the jerking tossings of his chip of +a craft, and silently eyeing the vast blue eye of the sea. + +Not very far distant Flask's boat was also lying breathlessly still; +its commander recklessly standing upon the top of the loggerhead, a +stout sort of post rooted in the keel, and rising some two feet above +the level of the stern platform. It is used for catching turns with +the whale line. Its top is not more spacious than the palm of a +man's hand, and standing upon such a base as that, Flask seemed +perched at the mast-head of some ship which had sunk to all but her +trucks. But little King-Post was small and short, and at the same +time little King-Post was full of a large and tall ambition, so that +this loggerhead stand-point of his did by no means satisfy King-Post. + +"I can't see three seas off; tip us up an oar there, and let me on to +that." + +Upon this, Daggoo, with either hand upon the gunwale to steady his +way, swiftly slid aft, and then erecting himself volunteered his +lofty shoulders for a pedestal. + +"Good a mast-head as any, sir. Will you mount?" + +"That I will, and thank ye very much, my fine fellow; only I wish you +fifty feet taller." + +Whereupon planting his feet firmly against two opposite planks of the +boat, the gigantic negro, stooping a little, presented his flat palm +to Flask's foot, and then putting Flask's hand on his hearse-plumed +head and bidding him spring as he himself should toss, with one +dexterous fling landed the little man high and dry on his shoulders. +And here was Flask now standing, Daggoo with one lifted arm +furnishing him with a breastband to lean against and steady himself +by. + +At any time it is a strange sight to the tyro to see with what +wondrous habitude of unconscious skill the whaleman will maintain an +erect posture in his boat, even when pitched about by the most +riotously perverse and cross-running seas. Still more strange to see +him giddily perched upon the loggerhead itself, under such +circumstances. But the sight of little Flask mounted upon gigantic +Daggoo was yet more curious; for sustaining himself with a cool, +indifferent, easy, unthought of, barbaric majesty, the noble negro to +every roll of the sea harmoniously rolled his fine form. On his +broad back, flaxen-haired Flask seemed a snow-flake. The bearer +looked nobler than the rider. Though truly vivacious, tumultuous, +ostentatious little Flask would now and then stamp with impatience; +but not one added heave did he thereby give to the negro's lordly +chest. So have I seen Passion and Vanity stamping the living +magnanimous earth, but the earth did not alter her tides and her +seasons for that. + +Meanwhile Stubb, the third mate, betrayed no such far-gazing +solicitudes. The whales might have made one of their regular +soundings, not a temporary dive from mere fright; and if that were +the case, Stubb, as his wont in such cases, it seems, was resolved to +solace the languishing interval with his pipe. He withdrew it from +his hatband, where he always wore it aslant like a feather. He +loaded it, and rammed home the loading with his thumb-end; but hardly +had he ignited his match across the rough sandpaper of his hand, +when Tashtego, his harpooneer, whose eyes had been setting to +windward like two fixed stars, suddenly dropped like light from his +erect attitude to his seat, crying out in a quick phrensy of hurry, +"Down, down all, and give way!--there they are!" + +To a landsman, no whale, nor any sign of a herring, would have been +visible at that moment; nothing but a troubled bit of greenish white +water, and thin scattered puffs of vapour hovering over it, and +suffusingly blowing off to leeward, like the confused scud from white +rolling billows. The air around suddenly vibrated and tingled, as it +were, like the air over intensely heated plates of iron. Beneath +this atmospheric waving and curling, and partially beneath a thin +layer of water, also, the whales were swimming. Seen in advance of +all the other indications, the puffs of vapour they spouted, seemed +their forerunning couriers and detached flying outriders. + +All four boats were now in keen pursuit of that one spot of troubled +water and air. But it bade fair to outstrip them; it flew on and on, +as a mass of interblending bubbles borne down a rapid stream from the +hills. + +"Pull, pull, my good boys," said Starbuck, in the lowest possible but +intensest concentrated whisper to his men; while the sharp fixed +glance from his eyes darted straight ahead of the bow, almost seemed +as two visible needles in two unerring binnacle compasses. He did +not say much to his crew, though, nor did his crew say anything to +him. Only the silence of the boat was at intervals startlingly +pierced by one of his peculiar whispers, now harsh with command, now +soft with entreaty. + +How different the loud little King-Post. "Sing out and say +something, my hearties. Roar and pull, my thunderbolts! Beach me, +beach me on their black backs, boys; only do that for me, and I'll +sign over to you my Martha's Vineyard plantation, boys; including +wife and children, boys. Lay me on--lay me on! O Lord, Lord! but I +shall go stark, staring mad! See! see that white water!" And so +shouting, he pulled his hat from his head, and stamped up and down on +it; then picking it up, flirted it far off upon the sea; and finally +fell to rearing and plunging in the boat's stern like a crazed colt +from the prairie. + +"Look at that chap now," philosophically drawled Stubb, who, with his +unlighted short pipe, mechanically retained between his teeth, at a +short distance, followed after--"He's got fits, that Flask has. +Fits? yes, give him fits--that's the very word--pitch fits into 'em. +Merrily, merrily, hearts-alive. Pudding for supper, you +know;--merry's the word. Pull, babes--pull, sucklings--pull, all. +But what the devil are you hurrying about? Softly, softly, and +steadily, my men. Only pull, and keep pulling; nothing more. Crack +all your backbones, and bite your knives in two--that's all. Take it +easy--why don't ye take it easy, I say, and burst all your livers and +lungs!" + +But what it was that inscrutable Ahab said to that tiger-yellow crew +of his--these were words best omitted here; for you live under the +blessed light of the evangelical land. Only the infidel sharks in +the audacious seas may give ear to such words, when, with tornado +brow, and eyes of red murder, and foam-glued lips, Ahab leaped after +his prey. + +Meanwhile, all the boats tore on. The repeated specific allusions of +Flask to "that whale," as he called the fictitious monster which he +declared to be incessantly tantalizing his boat's bow with its +tail--these allusions of his were at times so vivid and life-like, +that they would cause some one or two of his men to snatch a fearful +look over the shoulder. But this was against all rule; for the +oarsmen must put out their eyes, and ram a skewer through their +necks; usage pronouncing that they must have no organs but ears, and +no limbs but arms, in these critical moments. + +It was a sight full of quick wonder and awe! The vast swells of the +omnipotent sea; the surging, hollow roar they made, as they rolled +along the eight gunwales, like gigantic bowls in a boundless +bowling-green; the brief suspended agony of the boat, as it would tip +for an instant on the knife-like edge of the sharper waves, that +almost seemed threatening to cut it in two; the sudden profound dip +into the watery glens and hollows; the keen spurrings and goadings to +gain the top of the opposite hill; the headlong, sled-like slide down +its other side;--all these, with the cries of the headsmen and +harpooneers, and the shuddering gasps of the oarsmen, with the +wondrous sight of the ivory Pequod bearing down upon her boats with +outstretched sails, like a wild hen after her screaming brood;--all +this was thrilling. + +Not the raw recruit, marching from the bosom of his wife into the +fever heat of his first battle; not the dead man's ghost encountering +the first unknown phantom in the other world;--neither of these can +feel stranger and stronger emotions than that man does, who for the +first time finds himself pulling into the charmed, churned circle of +the hunted sperm whale. + +The dancing white water made by the chase was now becoming more and +more visible, owing to the increasing darkness of the dun +cloud-shadows flung upon the sea. The jets of vapour no longer +blended, but tilted everywhere to right and left; the whales seemed +separating their wakes. The boats were pulled more apart; Starbuck +giving chase to three whales running dead to leeward. Our sail was +now set, and, with the still rising wind, we rushed along; the boat +going with such madness through the water, that the lee oars could +scarcely be worked rapidly enough to escape being torn from the +row-locks. + +Soon we were running through a suffusing wide veil of mist; neither +ship nor boat to be seen. + +"Give way, men," whispered Starbuck, drawing still further aft the +sheet of his sail; "there is time to kill a fish yet before the +squall comes. There's white water again!--close to! Spring!" + +Soon after, two cries in quick succession on each side of us denoted +that the other boats had got fast; but hardly were they overheard, +when with a lightning-like hurtling whisper Starbuck said: "Stand +up!" and Queequeg, harpoon in hand, sprang to his feet. + +Though not one of the oarsmen was then facing the life and death +peril so close to them ahead, yet with their eyes on the intense +countenance of the mate in the stern of the boat, they knew that the +imminent instant had come; they heard, too, an enormous wallowing +sound as of fifty elephants stirring in their litter. Meanwhile the +boat was still booming through the mist, the waves curling and +hissing around us like the erected crests of enraged serpents. + +"That's his hump. THERE, THERE, give it to him!" whispered Starbuck. + +A short rushing sound leaped out of the boat; it was the darted iron +of Queequeg. Then all in one welded commotion came an invisible push +from astern, while forward the boat seemed striking on a ledge; the +sail collapsed and exploded; a gush of scalding vapour shot up near +by; something rolled and tumbled like an earthquake beneath us. The +whole crew were half suffocated as they were tossed helter-skelter +into the white curdling cream of the squall. Squall, whale, and +harpoon had all blended together; and the whale, merely grazed by the +iron, escaped. + +Though completely swamped, the boat was nearly unharmed. Swimming +round it we picked up the floating oars, and lashing them across the +gunwale, tumbled back to our places. There we sat up to our knees in +the sea, the water covering every rib and plank, so that to our +downward gazing eyes the suspended craft seemed a coral boat grown up +to us from the bottom of the ocean. + +The wind increased to a howl; the waves dashed their bucklers +together; the whole squall roared, forked, and crackled around us +like a white fire upon the prairie, in which, unconsumed, we were +burning; immortal in these jaws of death! In vain we hailed the +other boats; as well roar to the live coals down the chimney of a +flaming furnace as hail those boats in that storm. Meanwhile the +driving scud, rack, and mist, grew darker with the shadows of night; +no sign of the ship could be seen. The rising sea forbade all +attempts to bale out the boat. The oars were useless as propellers, +performing now the office of life-preservers. So, cutting the +lashing of the waterproof match keg, after many failures Starbuck +contrived to ignite the lamp in the lantern; then stretching it on a +waif pole, handed it to Queequeg as the standard-bearer of this +forlorn hope. There, then, he sat, holding up that imbecile candle +in the heart of that almighty forlornness. There, then, he sat, the +sign and symbol of a man without faith, hopelessly holding up hope in +the midst of despair. + +Wet, drenched through, and shivering cold, despairing of ship or +boat, we lifted up our eyes as the dawn came on. The mist still +spread over the sea, the empty lantern lay crushed in the bottom of +the boat. Suddenly Queequeg started to his feet, hollowing his hand +to his ear. We all heard a faint creaking, as of ropes and yards +hitherto muffled by the storm. The sound came nearer and nearer; the +thick mists were dimly parted by a huge, vague form. Affrighted, we +all sprang into the sea as the ship at last loomed into view, bearing +right down upon us within a distance of not much more than its +length. + +Floating on the waves we saw the abandoned boat, as for one instant +it tossed and gaped beneath the ship's bows like a chip at the base +of a cataract; and then the vast hull rolled over it, and it was seen +no more till it came up weltering astern. Again we swam for it, were +dashed against it by the seas, and were at last taken up and safely +landed on board. Ere the squall came close to, the other boats had +cut loose from their fish and returned to the ship in good time. The +ship had given us up, but was still cruising, if haply it might light +upon some token of our perishing,--an oar or a lance pole. + + + +CHAPTER 49 + +The Hyena. + + +There are certain queer times and occasions in this strange mixed +affair we call life when a man takes this whole universe for a vast +practical joke, though the wit thereof he but dimly discerns, and +more than suspects that the joke is at nobody's expense but his own. +However, nothing dispirits, and nothing seems worth while disputing. +He bolts down all events, all creeds, and beliefs, and persuasions, +all hard things visible and invisible, never mind how knobby; as an +ostrich of potent digestion gobbles down bullets and gun flints. And +as for small difficulties and worryings, prospects of sudden +disaster, peril of life and limb; all these, and death itself, seem +to him only sly, good-natured hits, and jolly punches in the side +bestowed by the unseen and unaccountable old joker. That odd sort of +wayward mood I am speaking of, comes over a man only in some time of +extreme tribulation; it comes in the very midst of his earnestness, +so that what just before might have seemed to him a thing most +momentous, now seems but a part of the general joke. There is +nothing like the perils of whaling to breed this free and easy sort +of genial, desperado philosophy; and with it I now regarded this +whole voyage of the Pequod, and the great White Whale its object. + +"Queequeg," said I, when they had dragged me, the last man, to the +deck, and I was still shaking myself in my jacket to fling off the +water; "Queequeg, my fine friend, does this sort of thing often +happen?" Without much emotion, though soaked through just like me, +he gave me to understand that such things did often happen. + +"Mr. Stubb," said I, turning to that worthy, who, buttoned up in his +oil-jacket, was now calmly smoking his pipe in the rain; "Mr. Stubb, +I think I have heard you say that of all whalemen you ever met, our +chief mate, Mr. Starbuck, is by far the most careful and prudent. I +suppose then, that going plump on a flying whale with your sail set +in a foggy squall is the height of a whaleman's discretion?" + +"Certain. I've lowered for whales from a leaking ship in a gale off +Cape Horn." + +"Mr. Flask," said I, turning to little King-Post, who was standing +close by; "you are experienced in these things, and I am not. Will +you tell me whether it is an unalterable law in this fishery, Mr. +Flask, for an oarsman to break his own back pulling himself +back-foremost into death's jaws?" + +"Can't you twist that smaller?" said Flask. "Yes, that's the law. I +should like to see a boat's crew backing water up to a whale face +foremost. Ha, ha! the whale would give them squint for squint, mind +that!" + +Here then, from three impartial witnesses, I had a deliberate +statement of the entire case. Considering, therefore, that squalls +and capsizings in the water and consequent bivouacks on the deep, +were matters of common occurrence in this kind of life; considering +that at the superlatively critical instant of going on to the whale I +must resign my life into the hands of him who steered the +boat--oftentimes a fellow who at that very moment is in his +impetuousness upon the point of scuttling the craft with his own +frantic stampings; considering that the particular disaster to our +own particular boat was chiefly to be imputed to Starbuck's driving +on to his whale almost in the teeth of a squall, and considering that +Starbuck, notwithstanding, was famous for his great heedfulness in +the fishery; considering that I belonged to this uncommonly prudent +Starbuck's boat; and finally considering in what a devil's chase I +was implicated, touching the White Whale: taking all things together, +I say, I thought I might as well go below and make a rough draft of +my will. "Queequeg," said I, "come along, you shall be my lawyer, +executor, and legatee." + +It may seem strange that of all men sailors should be tinkering at +their last wills and testaments, but there are no people in the world +more fond of that diversion. This was the fourth time in my nautical +life that I had done the same thing. After the ceremony was +concluded upon the present occasion, I felt all the easier; a stone +was rolled away from my heart. Besides, all the days I should now +live would be as good as the days that Lazarus lived after his +resurrection; a supplementary clean gain of so many months or weeks +as the case might be. I survived myself; my death and burial were +locked up in my chest. I looked round me tranquilly and contentedly, +like a quiet ghost with a clean conscience sitting inside the bars of +a snug family vault. + +Now then, thought I, unconsciously rolling up the sleeves of my +frock, here goes for a cool, collected dive at death and destruction, +and the devil fetch the hindmost. + + + +CHAPTER 50 + +Ahab's Boat and Crew. Fedallah. + + +"Who would have thought it, Flask!" cried Stubb; "if I had but one +leg you would not catch me in a boat, unless maybe to stop the +plug-hole with my timber toe. Oh! he's a wonderful old man!" + +"I don't think it so strange, after all, on that account," said +Flask. "If his leg were off at the hip, now, it would be a different +thing. That would disable him; but he has one knee, and good part of +the other left, you know." + +"I don't know that, my little man; I never yet saw him kneel." + + +Among whale-wise people it has often been argued whether, considering +the paramount importance of his life to the success of the voyage, it +is right for a whaling captain to jeopardize that life in the active +perils of the chase. So Tamerlane's soldiers often argued with tears +in their eyes, whether that invaluable life of his ought to be +carried into the thickest of the fight. + +But with Ahab the question assumed a modified aspect. Considering +that with two legs man is but a hobbling wight in all times of +danger; considering that the pursuit of whales is always under great +and extraordinary difficulties; that every individual moment, indeed, +then comprises a peril; under these circumstances is it wise for any +maimed man to enter a whale-boat in the hunt? As a general thing, +the joint-owners of the Pequod must have plainly thought not. + +Ahab well knew that although his friends at home would think little +of his entering a boat in certain comparatively harmless vicissitudes +of the chase, for the sake of being near the scene of action and +giving his orders in person, yet for Captain Ahab to have a boat +actually apportioned to him as a regular headsman in the hunt--above +all for Captain Ahab to be supplied with five extra men, as that same +boat's crew, he well knew that such generous conceits never entered the +heads of the owners of the Pequod. Therefore he had not solicited a +boat's crew from them, nor had he in any way hinted his desires on +that head. Nevertheless he had taken private measures of his own +touching all that matter. Until Cabaco's published discovery, the +sailors had little foreseen it, though to be sure when, after being a +little while out of port, all hands had concluded the customary +business of fitting the whaleboats for service; when some time after +this Ahab was now and then found bestirring himself in the matter of +making thole-pins with his own hands for what was thought to be one +of the spare boats, and even solicitously cutting the small wooden +skewers, which when the line is running out are pinned over the +groove in the bow: when all this was observed in him, and +particularly his solicitude in having an extra coat of sheathing in +the bottom of the boat, as if to make it better withstand the pointed +pressure of his ivory limb; and also the anxiety he evinced in +exactly shaping the thigh board, or clumsy cleat, as it is sometimes +called, the horizontal piece in the boat's bow for bracing the knee +against in darting or stabbing at the whale; when it was observed how +often he stood up in that boat with his solitary knee fixed in the +semi-circular depression in the cleat, and with the carpenter's +chisel gouged out a little here and straightened it a little there; +all these things, I say, had awakened much interest and curiosity at +the time. But almost everybody supposed that this particular +preparative heedfulness in Ahab must only be with a view to the +ultimate chase of Moby Dick; for he had already revealed his +intention to hunt that mortal monster in person. But such a +supposition did by no means involve the remotest suspicion as to any +boat's crew being assigned to that boat. + +Now, with the subordinate phantoms, what wonder remained soon waned +away; for in a whaler wonders soon wane. Besides, now and then such +unaccountable odds and ends of strange nations come up from the +unknown nooks and ash-holes of the earth to man these floating +outlaws of whalers; and the ships themselves often pick up such queer +castaway creatures found tossing about the open sea on planks, bits +of wreck, oars, whaleboats, canoes, blown-off Japanese junks, and +what not; that Beelzebub himself might climb up the side and step +down into the cabin to chat with the captain, and it would not create +any unsubduable excitement in the forecastle. + +But be all this as it may, certain it is that while the subordinate +phantoms soon found their place among the crew, though still as it +were somehow distinct from them, yet that hair-turbaned Fedallah +remained a muffled mystery to the last. Whence he came in a mannerly +world like this, by what sort of unaccountable tie he soon evinced +himself to be linked with Ahab's peculiar fortunes; nay, so far as to +have some sort of a half-hinted influence; Heaven knows, but it might +have been even authority over him; all this none knew. But one +cannot sustain an indifferent air concerning Fedallah. He was such a +creature as civilized, domestic people in the temperate zone only see +in their dreams, and that but dimly; but the like of whom now and +then glide among the unchanging Asiatic communities, especially the +Oriental isles to the east of the continent--those insulated, +immemorial, unalterable countries, which even in these modern days +still preserve much of the ghostly aboriginalness of earth's primal +generations, when the memory of the first man was a distinct +recollection, and all men his descendants, unknowing whence he came, +eyed each other as real phantoms, and asked of the sun and the moon +why they were created and to what end; when though, according to +Genesis, the angels indeed consorted with the daughters of men, the +devils also, add the uncanonical Rabbins, indulged in mundane amours. + + + +CHAPTER 51 + +The Spirit-Spout. + + +Days, weeks passed, and under easy sail, the ivory Pequod had slowly +swept across four several cruising-grounds; that off the Azores; off +the Cape de Verdes; on the Plate (so called), being off the mouth of +the Rio de la Plata; and the Carrol Ground, an unstaked, watery +locality, southerly from St. Helena. + +It was while gliding through these latter waters that one serene and +moonlight night, when all the waves rolled by like scrolls of silver; +and, by their soft, suffusing seethings, made what seemed a silvery +silence, not a solitude; on such a silent night a silvery jet was +seen far in advance of the white bubbles at the bow. Lit up by the +moon, it looked celestial; seemed some plumed and glittering god +uprising from the sea. Fedallah first descried this jet. For of +these moonlight nights, it was his wont to mount to the main-mast +head, and stand a look-out there, with the same precision as if it +had been day. And yet, though herds of whales were seen by night, +not one whaleman in a hundred would venture a lowering for them. You +may think with what emotions, then, the seamen beheld this old +Oriental perched aloft at such unusual hours; his turban and the +moon, companions in one sky. But when, after spending his uniform +interval there for several successive nights without uttering a +single sound; when, after all this silence, his unearthly voice was +heard announcing that silvery, moon-lit jet, every reclining mariner +started to his feet as if some winged spirit had lighted in the +rigging, and hailed the mortal crew. "There she blows!" Had the +trump of judgment blown, they could not have quivered more; yet still +they felt no terror; rather pleasure. For though it was a most +unwonted hour, yet so impressive was the cry, and so deliriously +exciting, that almost every soul on board instinctively desired a +lowering. + +Walking the deck with quick, side-lunging strides, Ahab commanded the +t'gallant sails and royals to be set, and every stunsail spread. The +best man in the ship must take the helm. Then, with every mast-head +manned, the piled-up craft rolled down before the wind. The strange, +upheaving, lifting tendency of the taffrail breeze filling the +hollows of so many sails, made the buoyant, hovering deck to feel +like air beneath the feet; while still she rushed along, as if two +antagonistic influences were struggling in her--one to mount direct +to heaven, the other to drive yawingly to some horizontal goal. And +had you watched Ahab's face that night, you would have thought that +in him also two different things were warring. While his one live +leg made lively echoes along the deck, every stroke of his dead limb +sounded like a coffin-tap. On life and death this old man walked. +But though the ship so swiftly sped, and though from every eye, like +arrows, the eager glances shot, yet the silvery jet was no more seen +that night. Every sailor swore he saw it once, but not a second +time. + +This midnight-spout had almost grown a forgotten thing, when, some +days after, lo! at the same silent hour, it was again announced: +again it was descried by all; but upon making sail to overtake it, +once more it disappeared as if it had never been. And so it served +us night after night, till no one heeded it but to wonder at it. +Mysteriously jetted into the clear moonlight, or starlight, as the +case might be; disappearing again for one whole day, or two days, or +three; and somehow seeming at every distinct repetition to be +advancing still further and further in our van, this solitary jet +seemed for ever alluring us on. + +Nor with the immemorial superstition of their race, and in accordance +with the preternaturalness, as it seemed, which in many things +invested the Pequod, were there wanting some of the seamen who swore +that whenever and wherever descried; at however remote times, or in +however far apart latitudes and longitudes, that unnearable spout was +cast by one self-same whale; and that whale, Moby Dick. For a time, +there reigned, too, a sense of peculiar dread at this flitting +apparition, as if it were treacherously beckoning us on and on, in +order that the monster might turn round upon us, and rend us at last +in the remotest and most savage seas. + +These temporary apprehensions, so vague but so awful, derived a +wondrous potency from the contrasting serenity of the weather, in +which, beneath all its blue blandness, some thought there lurked a +devilish charm, as for days and days we voyaged along, through seas +so wearily, lonesomely mild, that all space, in repugnance to our +vengeful errand, seemed vacating itself of life before our urn-like +prow. + +But, at last, when turning to the eastward, the Cape winds began +howling around us, and we rose and fell upon the long, troubled seas +that are there; when the ivory-tusked Pequod sharply bowed to the +blast, and gored the dark waves in her madness, till, like showers of +silver chips, the foam-flakes flew over her bulwarks; then all this +desolate vacuity of life went away, but gave place to sights more +dismal than before. + +Close to our bows, strange forms in the water darted hither and +thither before us; while thick in our rear flew the inscrutable +sea-ravens. And every morning, perched on our stays, rows of these +birds were seen; and spite of our hootings, for a long time +obstinately clung to the hemp, as though they deemed our ship some +drifting, uninhabited craft; a thing appointed to desolation, and +therefore fit roosting-place for their homeless selves. And heaved +and heaved, still unrestingly heaved the black sea, as if its vast +tides were a conscience; and the great mundane soul were in anguish +and remorse for the long sin and suffering it had bred. + +Cape of Good Hope, do they call ye? Rather Cape Tormentoto, as +called of yore; for long allured by the perfidious silences that +before had attended us, we found ourselves launched into this +tormented sea, where guilty beings transformed into those fowls and +these fish, seemed condemned to swim on everlastingly without any +haven in store, or beat that black air without any horizon. But +calm, snow-white, and unvarying; still directing its fountain of +feathers to the sky; still beckoning us on from before, the solitary +jet would at times be descried. + +During all this blackness of the elements, Ahab, though assuming for +the time the almost continual command of the drenched and dangerous +deck, manifested the gloomiest reserve; and more seldom than ever +addressed his mates. In tempestuous times like these, after +everything above and aloft has been secured, nothing more can be done +but passively to await the issue of the gale. Then Captain and crew +become practical fatalists. So, with his ivory leg inserted into its +accustomed hole, and with one hand firmly grasping a shroud, Ahab for +hours and hours would stand gazing dead to windward, while an +occasional squall of sleet or snow would all but congeal his very +eyelashes together. Meantime, the crew driven from the forward part +of the ship by the perilous seas that burstingly broke over its bows, +stood in a line along the bulwarks in the waist; and the better to +guard against the leaping waves, each man had slipped himself into a +sort of bowline secured to the rail, in which he swung as in a +loosened belt. Few or no words were spoken; and the silent ship, as +if manned by painted sailors in wax, day after day tore on through +all the swift madness and gladness of the demoniac waves. By night +the same muteness of humanity before the shrieks of the ocean +prevailed; still in silence the men swung in the bowlines; still +wordless Ahab stood up to the blast. Even when wearied nature seemed +demanding repose he would not seek that repose in his hammock. +Never could Starbuck forget the old man's aspect, when one night +going down into the cabin to mark how the barometer stood, he saw him +with closed eyes sitting straight in his floor-screwed chair; the +rain and half-melted sleet of the storm from which he had some time +before emerged, still slowly dripping from the unremoved hat and +coat. On the table beside him lay unrolled one of those charts of +tides and currents which have previously been spoken of. His lantern +swung from his tightly clenched hand. Though the body was erect, the +head was thrown back so that the closed eyes were pointed towards the +needle of the tell-tale that swung from a beam in the ceiling.* + + +*The cabin-compass is called the tell-tale, because without going to +the compass at the helm, the Captain, while below, can inform himself +of the course of the ship. + + +Terrible old man! thought Starbuck with a shudder, sleeping in this +gale, still thou steadfastly eyest thy purpose. + + + +CHAPTER 52 + +The Albatross. + + +South-eastward from the Cape, off the distant Crozetts, a good +cruising ground for Right Whalemen, a sail loomed ahead, the Goney +(Albatross) by name. As she slowly drew nigh, from my lofty perch at +the fore-mast-head, I had a good view of that sight so remarkable to +a tyro in the far ocean fisheries--a whaler at sea, and long absent +from home. + +As if the waves had been fullers, this craft was bleached like the +skeleton of a stranded walrus. All down her sides, this spectral +appearance was traced with long channels of reddened rust, while all +her spars and her rigging were like the thick branches of trees +furred over with hoar-frost. Only her lower sails were set. A wild +sight it was to see her long-bearded look-outs at those three +mast-heads. They seemed clad in the skins of beasts, so torn and +bepatched the raiment that had survived nearly four years of +cruising. Standing in iron hoops nailed to the mast, they swayed and +swung over a fathomless sea; and though, when the ship slowly glided +close under our stern, we six men in the air came so nigh to each +other that we might almost have leaped from the mast-heads of one +ship to those of the other; yet, those forlorn-looking fishermen, +mildly eyeing us as they passed, said not one word to our own +look-outs, while the quarter-deck hail was being heard from below. + +"Ship ahoy! Have ye seen the White Whale?" + +But as the strange captain, leaning over the pallid bulwarks, was in +the act of putting his trumpet to his mouth, it somehow fell from his +hand into the sea; and the wind now rising amain, he in vain strove +to make himself heard without it. Meantime his ship was still +increasing the distance between. While in various silent ways +the seamen of the Pequod were evincing their observance of this +ominous incident at the first mere mention of the White Whale's name +to another ship, Ahab for a moment paused; it almost seemed as though +he would have lowered a boat to board the stranger, had not the +threatening wind forbade. But taking advantage of his windward +position, he again seized his trumpet, and knowing by her aspect that +the stranger vessel was a Nantucketer and shortly bound home, he +loudly hailed--"Ahoy there! This is the Pequod, bound round the +world! Tell them to address all future letters to the Pacific ocean! +and this time three years, if I am not at home, tell them to address +them to--" + +At that moment the two wakes were fairly crossed, and instantly, +then, in accordance with their singular ways, shoals of small +harmless fish, that for some days before had been placidly swimming +by our side, darted away with what seemed shuddering fins, and ranged +themselves fore and aft with the stranger's flanks. Though in the +course of his continual voyagings Ahab must often before have noticed +a similar sight, yet, to any monomaniac man, the veriest trifles +capriciously carry meanings. + +"Swim away from me, do ye?" murmured Ahab, gazing over into the +water. There seemed but little in the words, but the tone conveyed +more of deep helpless sadness than the insane old man had ever before +evinced. But turning to the steersman, who thus far had been holding +the ship in the wind to diminish her headway, he cried out in his old +lion voice,--"Up helm! Keep her off round the world!" + +Round the world! There is much in that sound to inspire proud +feelings; but whereto does all that circumnavigation conduct? Only +through numberless perils to the very point whence we started, where +those that we left behind secure, were all the time before us. + +Were this world an endless plain, and by sailing eastward we could +for ever reach new distances, and discover sights more sweet and +strange than any Cyclades or Islands of King Solomon, then there were +promise in the voyage. But in pursuit of those far mysteries we +dream of, or in tormented chase of that demon phantom that, some time +or other, swims before all human hearts; while chasing such over this +round globe, they either lead us on in barren mazes or midway leave +us whelmed. + + + +CHAPTER 53 + +The Gam. + + +The ostensible reason why Ahab did not go on board of the whaler we +had spoken was this: the wind and sea betokened storms. But even had +this not been the case, he would not after all, perhaps, have boarded +her--judging by his subsequent conduct on similar occasions--if so it +had been that, by the process of hailing, he had obtained a negative +answer to the question he put. For, as it eventually turned out, he +cared not to consort, even for five minutes, with any stranger +captain, except he could contribute some of that information he so +absorbingly sought. But all this might remain inadequately +estimated, were not something said here of the peculiar usages of +whaling-vessels when meeting each other in foreign seas, and +especially on a common cruising-ground. + +If two strangers crossing the Pine Barrens in New York State, or the +equally desolate Salisbury Plain in England; if casually encountering +each other in such inhospitable wilds, these twain, for the life of +them, cannot well avoid a mutual salutation; and stopping for a +moment to interchange the news; and, perhaps, sitting down for a +while and resting in concert: then, how much more natural that upon +the illimitable Pine Barrens and Salisbury Plains of the sea, two +whaling vessels descrying each other at the ends of the earth--off +lone Fanning's Island, or the far away King's Mills; how much more +natural, I say, that under such circumstances these ships should not +only interchange hails, but come into still closer, more friendly and +sociable contact. And especially would this seem to be a matter of +course, in the case of vessels owned in one seaport, and whose +captains, officers, and not a few of the men are personally known to +each other; and consequently, have all sorts of dear domestic things +to talk about. + +For the long absent ship, the outward-bounder, perhaps, has letters +on board; at any rate, she will be sure to let her have some papers +of a date a year or two later than the last one on her blurred and +thumb-worn files. And in return for that courtesy, the outward-bound +ship would receive the latest whaling intelligence from the +cruising-ground to which she may be destined, a thing of the utmost +importance to her. And in degree, all this will hold true concerning +whaling vessels crossing each other's track on the cruising-ground +itself, even though they are equally long absent from home. For one +of them may have received a transfer of letters from some third, and +now far remote vessel; and some of those letters may be for the +people of the ship she now meets. Besides, they would exchange the +whaling news, and have an agreeable chat. For not only would they +meet with all the sympathies of sailors, but likewise with all the +peculiar congenialities arising from a common pursuit and mutually +shared privations and perils. + +Nor would difference of country make any very essential difference; +that is, so long as both parties speak one language, as is the case +with Americans and English. Though, to be sure, from the small +number of English whalers, such meetings do not very often occur, and +when they do occur there is too apt to be a sort of shyness between +them; for your Englishman is rather reserved, and your Yankee, he +does not fancy that sort of thing in anybody but himself. Besides, +the English whalers sometimes affect a kind of metropolitan +superiority over the American whalers; regarding the long, lean +Nantucketer, with his nondescript provincialisms, as a sort of +sea-peasant. But where this superiority in the English whalemen +does really consist, it would be hard to say, seeing that the Yankees +in one day, collectively, kill more whales than all the English, +collectively, in ten years. But this is a harmless little foible in +the English whale-hunters, which the Nantucketer does not take much +to heart; probably, because he knows that he has a few foibles +himself. + +So, then, we see that of all ships separately sailing the sea, the +whalers have most reason to be sociable--and they are so. Whereas, +some merchant ships crossing each other's wake in the mid-Atlantic, +will oftentimes pass on without so much as a single word of +recognition, mutually cutting each other on the high seas, like a +brace of dandies in Broadway; and all the time indulging, perhaps, in +finical criticism upon each other's rig. As for Men-of-War, when +they chance to meet at sea, they first go through such a string of +silly bowings and scrapings, such a ducking of ensigns, that there +does not seem to be much right-down hearty good-will and brotherly +love about it at all. As touching Slave-ships meeting, why, they are +in such a prodigious hurry, they run away from each other as soon as +possible. And as for Pirates, when they chance to cross each other's +cross-bones, the first hail is--"How many skulls?"--the same way that +whalers hail--"How many barrels?" And that question once answered, +pirates straightway steer apart, for they are infernal villains on +both sides, and don't like to see overmuch of each other's villanous +likenesses. + +But look at the godly, honest, unostentatious, hospitable, sociable, +free-and-easy whaler! What does the whaler do when she meets another +whaler in any sort of decent weather? She has a "GAM," a thing so +utterly unknown to all other ships that they never heard of the name +even; and if by chance they should hear of it, they only grin at it, +and repeat gamesome stuff about "spouters" and "blubber-boilers," and +such like pretty exclamations. Why it is that all Merchant-seamen, +and also all Pirates and Man-of-War's men, and Slave-ship sailors, +cherish such a scornful feeling towards Whale-ships; this is a +question it would be hard to answer. Because, in the case of +pirates, say, I should like to know whether that profession of theirs +has any peculiar glory about it. It sometimes ends in uncommon +elevation, indeed; but only at the gallows. And besides, when a man +is elevated in that odd fashion, he has no proper foundation for his +superior altitude. Hence, I conclude, that in boasting himself to be +high lifted above a whaleman, in that assertion the pirate has no +solid basis to stand on. + +But what is a GAM? You might wear out your index-finger running up +and down the columns of dictionaries, and never find the word. Dr. +Johnson never attained to that erudition; Noah Webster's ark does not +hold it. Nevertheless, this same expressive word has now for many +years been in constant use among some fifteen thousand true born +Yankees. Certainly, it needs a definition, and should be +incorporated into the Lexicon. With that view, let me learnedly +define it. + +GAM. NOUN--A SOCIAL MEETING OF TWO (OR MORE) WHALESHIPS, GENERALLY +ON A CRUISING-GROUND; WHEN, AFTER EXCHANGING HAILS, THEY EXCHANGE +VISITS BY BOATS' CREWS; THE TWO CAPTAINS REMAINING, FOR THE TIME, ON +BOARD OF ONE SHIP, AND THE TWO CHIEF MATES ON THE OTHER. + +There is another little item about Gamming which must not be +forgotten here. All professions have their own little peculiarities +of detail; so has the whale fishery. In a pirate, man-of-war, or +slave ship, when the captain is rowed anywhere in his boat, he always +sits in the stern sheets on a comfortable, sometimes cushioned seat +there, and often steers himself with a pretty little milliner's +tiller decorated with gay cords and ribbons. But the whale-boat has +no seat astern, no sofa of that sort whatever, and no tiller at all. +High times indeed, if whaling captains were wheeled about the water +on castors like gouty old aldermen in patent chairs. And as for a +tiller, the whale-boat never admits of any such effeminacy; and +therefore as in gamming a complete boat's crew must leave the ship, +and hence as the boat steerer or harpooneer is of the number, that +subordinate is the steersman upon the occasion, and the captain, +having no place to sit in, is pulled off to his visit all standing +like a pine tree. And often you will notice that being conscious of +the eyes of the whole visible world resting on him from the sides of +the two ships, this standing captain is all alive to the importance +of sustaining his dignity by maintaining his legs. Nor is this any +very easy matter; for in his rear is the immense projecting steering +oar hitting him now and then in the small of his back, the after-oar +reciprocating by rapping his knees in front. He is thus completely +wedged before and behind, and can only expand himself sideways by +settling down on his stretched legs; but a sudden, violent pitch of +the boat will often go far to topple him, because length of +foundation is nothing without corresponding breadth. Merely make a +spread angle of two poles, and you cannot stand them up. Then, +again, it would never do in plain sight of the world's riveted eyes, +it would never do, I say, for this straddling captain to be seen +steadying himself the slightest particle by catching hold of anything +with his hands; indeed, as token of his entire, buoyant self-command, +he generally carries his hands in his trowsers' pockets; but perhaps +being generally very large, heavy hands, he carries them there for +ballast. Nevertheless there have occurred instances, well +authenticated ones too, where the captain has been known for an +uncommonly critical moment or two, in a sudden squall say--to seize +hold of the nearest oarsman's hair, and hold on there like grim +death. + + + +CHAPTER 54 + +The Town-Ho's Story. + + +(AS TOLD AT THE GOLDEN INN) + + +The Cape of Good Hope, and all the watery region round about there, +is much like some noted four corners of a great highway, where you +meet more travellers than in any other part. + +It was not very long after speaking the Goney that another +homeward-bound whaleman, the Town-Ho,* was encountered. She was +manned almost wholly by Polynesians. In the short gam that ensued +she gave us strong news of Moby Dick. To some the general interest +in the White Whale was now wildly heightened by a circumstance of the +Town-Ho's story, which seemed obscurely to involve with the whale a +certain wondrous, inverted visitation of one of those so called +judgments of God which at times are said to overtake some men. This +latter circumstance, with its own particular accompaniments, forming +what may be called the secret part of the tragedy about to be +narrated, never reached the ears of Captain Ahab or his mates. For +that secret part of the story was unknown to the captain of the +Town-Ho himself. It was the private property of three confederate +white seamen of that ship, one of whom, it seems, communicated it to +Tashtego with Romish injunctions of secrecy, but the following night +Tashtego rambled in his sleep, and revealed so much of it in that +way, that when he was wakened he could not well withhold the rest. +Nevertheless, so potent an influence did this thing have on those +seamen in the Pequod who came to the full knowledge of it, and by +such a strange delicacy, to call it so, were they governed in this +matter, that they kept the secret among themselves so that it never +transpired abaft the Pequod's main-mast. Interweaving in its proper +place this darker thread with the story as publicly narrated on the +ship, the whole of this strange affair I now proceed to put on +lasting record. + + +*The ancient whale-cry upon first sighting a whale from the +mast-head, still used by whalemen in hunting the famous Gallipagos +terrapin. + + +For my humor's sake, I shall preserve the style in which I once +narrated it at Lima, to a lounging circle of my Spanish friends, one +saint's eve, smoking upon the thick-gilt tiled piazza of the Golden +Inn. Of those fine cavaliers, the young Dons, Pedro and Sebastian, +were on the closer terms with me; and hence the interluding questions +they occasionally put, and which are duly answered at the time. + +"Some two years prior to my first learning the events which I am +about rehearsing to you, gentlemen, the Town-Ho, Sperm Whaler of +Nantucket, was cruising in your Pacific here, not very many days' +sail eastward from the eaves of this good Golden Inn. She was +somewhere to the northward of the Line. One morning upon handling +the pumps, according to daily usage, it was observed that she made +more water in her hold than common. They supposed a sword-fish had +stabbed her, gentlemen. But the captain, having some unusual reason +for believing that rare good luck awaited him in those latitudes; and +therefore being very averse to quit them, and the leak not being then +considered at all dangerous, though, indeed, they could not find it +after searching the hold as low down as was possible in rather heavy +weather, the ship still continued her cruisings, the mariners working +at the pumps at wide and easy intervals; but no good luck came; more +days went by, and not only was the leak yet undiscovered, but it +sensibly increased. So much so, that now taking some alarm, the +captain, making all sail, stood away for the nearest harbor among the +islands, there to have his hull hove out and repaired. + +"Though no small passage was before her, yet, if the commonest chance +favoured, he did not at all fear that his ship would founder by the +way, because his pumps were of the best, and being periodically +relieved at them, those six-and-thirty men of his could easily keep +the ship free; never mind if the leak should double on her. In +truth, well nigh the whole of this passage being attended by very +prosperous breezes, the Town-Ho had all but certainly arrived in +perfect safety at her port without the occurrence of the least +fatality, had it not been for the brutal overbearing of Radney, the +mate, a Vineyarder, and the bitterly provoked vengeance of Steelkilt, +a Lakeman and desperado from Buffalo. + +"'Lakeman!--Buffalo! Pray, what is a Lakeman, and where is Buffalo?' +said Don Sebastian, rising in his swinging mat of grass. + +"On the eastern shore of our Lake Erie, Don; but--I crave your +courtesy--may be, you shall soon hear further of all that. Now, +gentlemen, in square-sail brigs and three-masted ships, well-nigh as +large and stout as any that ever sailed out of your old Callao to far +Manilla; this Lakeman, in the land-locked heart of our America, had +yet been nurtured by all those agrarian freebooting impressions +popularly connected with the open ocean. For in their interflowing +aggregate, those grand fresh-water seas of ours,--Erie, and Ontario, +and Huron, and Superior, and Michigan,--possess an ocean-like +expansiveness, with many of the ocean's noblest traits; with many of +its rimmed varieties of races and of climes. They contain round +archipelagoes of romantic isles, even as the Polynesian waters do; in +large part, are shored by two great contrasting nations, as the +Atlantic is; they furnish long maritime approaches to our numerous +territorial colonies from the East, dotted all round their banks; +here and there are frowned upon by batteries, and by the goat-like +craggy guns of lofty Mackinaw; they have heard the fleet thunderings +of naval victories; at intervals, they yield their beaches to wild +barbarians, whose red painted faces flash from out their peltry +wigwams; for leagues and leagues are flanked by ancient and unentered +forests, where the gaunt pines stand like serried lines of kings in +Gothic genealogies; those same woods harboring wild Afric beasts of +prey, and silken creatures whose exported furs give robes to Tartar +Emperors; they mirror the paved capitals of Buffalo and Cleveland, as +well as Winnebago villages; they float alike the full-rigged merchant +ship, the armed cruiser of the State, the steamer, and the beech +canoe; they are swept by Borean and dismasting blasts as direful as +any that lash the salted wave; they know what shipwrecks are, for out +of sight of land, however inland, they have drowned full many a +midnight ship with all its shrieking crew. Thus, gentlemen, though +an inlander, Steelkilt was wild-ocean born, and wild-ocean nurtured; +as much of an audacious mariner as any. And for Radney, though in +his infancy he may have laid him down on the lone Nantucket beach, to +nurse at his maternal sea; though in after life he had long followed +our austere Atlantic and your contemplative Pacific; yet was he quite +as vengeful and full of social quarrel as the backwoods seaman, fresh +from the latitudes of buck-horn handled bowie-knives. Yet was this +Nantucketer a man with some good-hearted traits; and this Lakeman, a +mariner, who though a sort of devil indeed, might yet by inflexible +firmness, only tempered by that common decency of human recognition +which is the meanest slave's right; thus treated, this Steelkilt had +long been retained harmless and docile. At all events, he had proved +so thus far; but Radney was doomed and made mad, and Steelkilt--but, +gentlemen, you shall hear. + +"It was not more than a day or two at the furthest after pointing her +prow for her island haven, that the Town-Ho's leak seemed again +increasing, but only so as to require an hour or more at the pumps +every day. You must know that in a settled and civilized ocean like +our Atlantic, for example, some skippers think little of pumping +their whole way across it; though of a still, sleepy night, should +the officer of the deck happen to forget his duty in that respect, +the probability would be that he and his shipmates would never again +remember it, on account of all hands gently subsiding to the bottom. +Nor in the solitary and savage seas far from you to the westward, +gentlemen, is it altogether unusual for ships to keep clanging at +their pump-handles in full chorus even for a voyage of considerable +length; that is, if it lie along a tolerably accessible coast, or if +any other reasonable retreat is afforded them. It is only when a +leaky vessel is in some very out of the way part of those waters, +some really landless latitude, that her captain begins to feel a +little anxious. + +"Much this way had it been with the Town-Ho; so when her leak was +found gaining once more, there was in truth some small concern +manifested by several of her company; especially by Radney the mate. +He commanded the upper sails to be well hoisted, sheeted home anew, +and every way expanded to the breeze. Now this Radney, I suppose, +was as little of a coward, and as little inclined to any sort of +nervous apprehensiveness touching his own person as any fearless, +unthinking creature on land or on sea that you can conveniently +imagine, gentlemen. Therefore when he betrayed this solicitude about +the safety of the ship, some of the seamen declared that it was only +on account of his being a part owner in her. So when they were +working that evening at the pumps, there was on this head no small +gamesomeness slily going on among them, as they stood with their feet +continually overflowed by the rippling clear water; clear as any +mountain spring, gentlemen--that bubbling from the pumps ran across +the deck, and poured itself out in steady spouts at the lee +scupper-holes. + +"Now, as you well know, it is not seldom the case in this +conventional world of ours--watery or otherwise; that when a person +placed in command over his fellow-men finds one of them to be very +significantly his superior in general pride of manhood, straightway +against that man he conceives an unconquerable dislike and +bitterness; and if he have a chance he will pull down and pulverize +that subaltern's tower, and make a little heap of dust of it. Be +this conceit of mine as it may, gentlemen, at all events Steelkilt +was a tall and noble animal with a head like a Roman, and a flowing +golden beard like the tasseled housings of your last viceroy's +snorting charger; and a brain, and a heart, and a soul in him, +gentlemen, which had made Steelkilt Charlemagne, had he been born son +to Charlemagne's father. But Radney, the mate, was ugly as a mule; +yet as hardy, as stubborn, as malicious. He did not love Steelkilt, +and Steelkilt knew it. + +"Espying the mate drawing near as he was toiling at the pump with the +rest, the Lakeman affected not to notice him, but unawed, went on +with his gay banterings. + +"'Aye, aye, my merry lads, it's a lively leak this; hold a cannikin, +one of ye, and let's have a taste. By the Lord, it's worth bottling! +I tell ye what, men, old Rad's investment must go for it! he had +best cut away his part of the hull and tow it home. The fact is, +boys, that sword-fish only began the job; he's come back again with a +gang of ship-carpenters, saw-fish, and file-fish, and what not; and +the whole posse of 'em are now hard at work cutting and slashing at +the bottom; making improvements, I suppose. If old Rad were here +now, I'd tell him to jump overboard and scatter 'em. They're playing +the devil with his estate, I can tell him. But he's a simple old +soul,--Rad, and a beauty too. Boys, they say the rest of his +property is invested in looking-glasses. I wonder if he'd give a +poor devil like me the model of his nose.' + +"'Damn your eyes! what's that pump stopping for?' roared Radney, +pretending not to have heard the sailors' talk. 'Thunder away at +it!' + +'Aye, aye, sir,' said Steelkilt, merry as a cricket. 'Lively, boys, +lively, now!' And with that the pump clanged like fifty +fire-engines; the men tossed their hats off to it, and ere long that +peculiar gasping of the lungs was heard which denotes the fullest +tension of life's utmost energies. + +"Quitting the pump at last, with the rest of his band, the Lakeman +went forward all panting, and sat himself down on the windlass; his +face fiery red, his eyes bloodshot, and wiping the profuse sweat from +his brow. Now what cozening fiend it was, gentlemen, that possessed +Radney to meddle with such a man in that corporeally exasperated +state, I know not; but so it happened. Intolerably striding along +the deck, the mate commanded him to get a broom and sweep down the +planks, and also a shovel, and remove some offensive matters +consequent upon allowing a pig to run at large. + +"Now, gentlemen, sweeping a ship's deck at sea is a piece of +household work which in all times but raging gales is regularly +attended to every evening; it has been known to be done in the case +of ships actually foundering at the time. Such, gentlemen, is the +inflexibility of sea-usages and the instinctive love of neatness in +seamen; some of whom would not willingly drown without first washing +their faces. But in all vessels this broom business is the +prescriptive province of the boys, if boys there be aboard. Besides, +it was the stronger men in the Town-Ho that had been divided into +gangs, taking turns at the pumps; and being the most athletic seaman +of them all, Steelkilt had been regularly assigned captain of one of +the gangs; consequently he should have been freed from any trivial +business not connected with truly nautical duties, such being the +case with his comrades. I mention all these particulars so that you +may understand exactly how this affair stood between the two men. + +"But there was more than this: the order about the shovel was almost +as plainly meant to sting and insult Steelkilt, as though Radney had +spat in his face. Any man who has gone sailor in a whale-ship will +understand this; and all this and doubtless much more, the Lakeman +fully comprehended when the mate uttered his command. But as he sat +still for a moment, and as he steadfastly looked into the mate's +malignant eye and perceived the stacks of powder-casks heaped up in +him and the slow-match silently burning along towards them; as he +instinctively saw all this, that strange forbearance and +unwillingness to stir up the deeper passionateness in any already +ireful being--a repugnance most felt, when felt at all, by really +valiant men even when aggrieved--this nameless phantom feeling, +gentlemen, stole over Steelkilt. + +"Therefore, in his ordinary tone, only a little broken by the bodily +exhaustion he was temporarily in, he answered him saying that +sweeping the deck was not his business, and he would not do it. And +then, without at all alluding to the shovel, he pointed to three +lads as the customary sweepers; who, not being billeted at the +pumps, had done little or nothing all day. To this, Radney replied +with an oath, in a most domineering and outrageous manner +unconditionally reiterating his command; meanwhile advancing upon the +still seated Lakeman, with an uplifted cooper's club hammer which he +had snatched from a cask near by. + +"Heated and irritated as he was by his spasmodic toil at the pumps, +for all his first nameless feeling of forbearance the sweating +Steelkilt could but ill brook this bearing in the mate; but somehow +still smothering the conflagration within him, without speaking he +remained doggedly rooted to his seat, till at last the incensed +Radney shook the hammer within a few inches of his face, furiously +commanding him to do his bidding. + +"Steelkilt rose, and slowly retreating round the windlass, steadily +followed by the mate with his menacing hammer, deliberately repeated +his intention not to obey. Seeing, however, that his forbearance had +not the slightest effect, by an awful and unspeakable intimation with +his twisted hand he warned off the foolish and infatuated man; but it +was to no purpose. And in this way the two went once slowly round +the windlass; when, resolved at last no longer to retreat, bethinking +him that he had now forborne as much as comported with his humor, the +Lakeman paused on the hatches and thus spoke to the officer: + +"'Mr. Radney, I will not obey you. Take that hammer away, or look to +yourself.' But the predestinated mate coming still closer to him, +where the Lakeman stood fixed, now shook the heavy hammer within an +inch of his teeth; meanwhile repeating a string of insufferable +maledictions. Retreating not the thousandth part of an inch; +stabbing him in the eye with the unflinching poniard of his glance, +Steelkilt, clenching his right hand behind him and creepingly drawing +it back, told his persecutor that if the hammer but grazed his cheek +he (Steelkilt) would murder him. But, gentlemen, the fool had been +branded for the slaughter by the gods. Immediately the hammer +touched the cheek; the next instant the lower jaw of the mate was +stove in his head; he fell on the hatch spouting blood like a whale. + +"Ere the cry could go aft Steelkilt was shaking one of the backstays +leading far aloft to where two of his comrades were standing their +mastheads. They were both Canallers. + +"'Canallers!' cried Don Pedro. 'We have seen many whale-ships in our +harbours, but never heard of your Canallers. Pardon: who and what are +they?' + +"'Canallers, Don, are the boatmen belonging to our grand Erie Canal. +You must have heard of it.' + +"'Nay, Senor; hereabouts in this dull, warm, most lazy, and +hereditary land, we know but little of your vigorous North.' + +"'Aye? Well then, Don, refill my cup. Your chicha's very fine; and +ere proceeding further I will tell ye what our Canallers are; for +such information may throw side-light upon my story.' + +"For three hundred and sixty miles, gentlemen, through the entire +breadth of the state of New York; through numerous populous cities +and most thriving villages; through long, dismal, uninhabited swamps, +and affluent, cultivated fields, unrivalled for fertility; by +billiard-room and bar-room; through the holy-of-holies of great +forests; on Roman arches over Indian rivers; through sun and shade; +by happy hearts or broken; through all the wide contrasting scenery +of those noble Mohawk counties; and especially, by rows of snow-white +chapels, whose spires stand almost like milestones, flows one +continual stream of Venetianly corrupt and often lawless life. +There's your true Ashantee, gentlemen; there howl your pagans; where +you ever find them, next door to you; under the long-flung shadow, +and the snug patronising lee of churches. For by some curious +fatality, as it is often noted of your metropolitan freebooters that +they ever encamp around the halls of justice, so sinners, gentlemen, +most abound in holiest vicinities. + +"'Is that a friar passing?' said Don Pedro, looking downwards into +the crowded plazza, with humorous concern. + +"'Well for our northern friend, Dame Isabella's Inquisition wanes in +Lima,' laughed Don Sebastian. 'Proceed, Senor.' + +"'A moment! Pardon!' cried another of the company. 'In the name of +all us Limeese, I but desire to express to you, sir sailor, that we +have by no means overlooked your delicacy in not substituting present +Lima for distant Venice in your corrupt comparison. Oh! do not bow +and look surprised; you know the proverb all along this +coast--"Corrupt as Lima." It but bears out your saying, too; +churches more plentiful than billiard-tables, and for ever open--and +"Corrupt as Lima." So, too, Venice; I have been there; the holy city +of the blessed evangelist, St. Mark!--St. Dominic, purge it! Your +cup! Thanks: here I refill; now, you pour out again.' + +"Freely depicted in his own vocation, gentlemen, the Canaller would +make a fine dramatic hero, so abundantly and picturesquely wicked is +he. Like Mark Antony, for days and days along his green-turfed, +flowery Nile, he indolently floats, openly toying with his +red-cheeked Cleopatra, ripening his apricot thigh upon the sunny +deck. But ashore, all this effeminacy is dashed. The brigandish +guise which the Canaller so proudly sports; his slouched and +gaily-ribboned hat betoken his grand features. A terror to the +smiling innocence of the villages through which he floats; his swart +visage and bold swagger are not unshunned in cities. Once a vagabond +on his own canal, I have received good turns from one of these +Canallers; I thank him heartily; would fain be not ungrateful; but it +is often one of the prime redeeming qualities of your man of +violence, that at times he has as stiff an arm to back a poor +stranger in a strait, as to plunder a wealthy one. In sum, +gentlemen, what the wildness of this canal life is, is emphatically +evinced by this; that our wild whale-fishery contains so many of its +most finished graduates, and that scarce any race of mankind, except +Sydney men, are so much distrusted by our whaling captains. Nor does +it at all diminish the curiousness of this matter, that to many +thousands of our rural boys and young men born along its line, the +probationary life of the Grand Canal furnishes the sole transition +between quietly reaping in a Christian corn-field, and recklessly +ploughing the waters of the most barbaric seas. + +"'I see! I see!' impetuously exclaimed Don Pedro, spilling his +chicha upon his silvery ruffles. 'No need to travel! The world's +one Lima. I had thought, now, that at your temperate North the +generations were cold and holy as the hills.--But the story.' + +"I left off, gentlemen, where the Lakeman shook the backstay. +Hardly had he done so, when he was surrounded by the three junior +mates and the four harpooneers, who all crowded him to the deck. But +sliding down the ropes like baleful comets, the two Canallers rushed +into the uproar, and sought to drag their man out of it towards the +forecastle. Others of the sailors joined with them in this attempt, +and a twisted turmoil ensued; while standing out of harm's way, the +valiant captain danced up and down with a whale-pike, calling upon +his officers to manhandle that atrocious scoundrel, and smoke him +along to the quarter-deck. At intervals, he ran close up to the +revolving border of the confusion, and prying into the heart of it +with his pike, sought to prick out the object of his resentment. But +Steelkilt and his desperadoes were too much for them all; they +succeeded in gaining the forecastle deck, where, hastily slewing +about three or four large casks in a line with the windlass, these +sea-Parisians entrenched themselves behind the barricade. + +"'Come out of that, ye pirates!' roared the captain, now menacing +them with a pistol in each hand, just brought to him by the steward. +'Come out of that, ye cut-throats!' + +"Steelkilt leaped on the barricade, and striding up and down there, +defied the worst the pistols could do; but gave the captain to +understand distinctly, that his (Steelkilt's) death would be the +signal for a murderous mutiny on the part of all hands. Fearing in +his heart lest this might prove but too true, the captain a little +desisted, but still commanded the insurgents instantly to return to +their duty. + +"'Will you promise not to touch us, if we do?' demanded their +ringleader. + +"'Turn to! turn to!--I make no promise;--to your duty! Do you want +to sink the ship, by knocking off at a time like this? Turn to!' and +he once more raised a pistol. + +"'Sink the ship?' cried Steelkilt. 'Aye, let her sink. Not a man of +us turns to, unless you swear not to raise a rope-yarn against us. +What say ye, men?' turning to his comrades. A fierce cheer was their +response. + +"The Lakeman now patrolled the barricade, all the while keeping his +eye on the Captain, and jerking out such sentences as these:--'It's +not our fault; we didn't want it; I told him to take his hammer away; +it was boy's business; he might have known me before this; I told him +not to prick the buffalo; I believe I have broken a finger here +against his cursed jaw; ain't those mincing knives down in the +forecastle there, men? look to those handspikes, my hearties. +Captain, by God, look to yourself; say the word; don't be a fool; +forget it all; we are ready to turn to; treat us decently, and we're +your men; but we won't be flogged.' + +"'Turn to! I make no promises, turn to, I say!' + +"'Look ye, now,' cried the Lakeman, flinging out his arm towards him, +'there are a few of us here (and I am one of them) who have shipped +for the cruise, d'ye see; now as you well know, sir, we can claim our +discharge as soon as the anchor is down; so we don't want a row; it's +not our interest; we want to be peaceable; we are ready to work, but +we won't be flogged.' + +"'Turn to!' roared the Captain. + +"Steelkilt glanced round him a moment, and then said:--'I tell you +what it is now, Captain, rather than kill ye, and be hung for such a +shabby rascal, we won't lift a hand against ye unless ye attack us; +but till you say the word about not flogging us, we don't do a hand's +turn.' + +"'Down into the forecastle then, down with ye, I'll keep ye there +till ye're sick of it. Down ye go.' + +"'Shall we?' cried the ringleader to his men. Most of them were +against it; but at length, in obedience to Steelkilt, they preceded +him down into their dark den, growlingly disappearing, like bears +into a cave. + +"As the Lakeman's bare head was just level with the planks, the +Captain and his posse leaped the barricade, and rapidly drawing over +the slide of the scuttle, planted their group of hands upon it, and +loudly called for the steward to bring the heavy brass padlock +belonging to the companionway. + +Then opening the slide a little, the Captain whispered something down +the crack, closed it, and turned the key upon them--ten in +number--leaving on deck some twenty or more, who thus far had +remained neutral. + +"All night a wide-awake watch was kept by all the officers, forward +and aft, especially about the forecastle scuttle and fore hatchway; +at which last place it was feared the insurgents might emerge, after +breaking through the bulkhead below. But the hours of darkness +passed in peace; the men who still remained at their duty toiling +hard at the pumps, whose clinking and clanking at intervals through +the dreary night dismally resounded through the ship. + +"At sunrise the Captain went forward, and knocking on the deck, +summoned the prisoners to work; but with a yell they refused. Water +was then lowered down to them, and a couple of handfuls of biscuit +were tossed after it; when again turning the key upon them and +pocketing it, the Captain returned to the quarter-deck. Twice every +day for three days this was repeated; but on the fourth morning a +confused wrangling, and then a scuffling was heard, as the customary +summons was delivered; and suddenly four men burst up from the +forecastle, saying they were ready to turn to. The fetid closeness +of the air, and a famishing diet, united perhaps to some fears of +ultimate retribution, had constrained them to surrender at +discretion. Emboldened by this, the Captain reiterated his demand to +the rest, but Steelkilt shouted up to him a terrific hint to stop his +babbling and betake himself where he belonged. On the fifth morning +three others of the mutineers bolted up into the air from the +desperate arms below that sought to restrain them. Only three were +left. + +"'Better turn to, now?' said the Captain with a heartless jeer. + +"'Shut us up again, will ye!' cried Steelkilt. + +"'Oh certainly,' the Captain, and the key clicked. + +"It was at this point, gentlemen, that enraged by the defection of +seven of his former associates, and stung by the mocking voice that +had last hailed him, and maddened by his long entombment in a place +as black as the bowels of despair; it was then that Steelkilt +proposed to the two Canallers, thus far apparently of one mind with +him, to burst out of their hole at the next summoning of the +garrison; and armed with their keen mincing knives (long, crescentic, +heavy implements with a handle at each end) run amuck from the +bowsprit to the taffrail; and if by any devilishness of desperation +possible, seize the ship. For himself, he would do this, he said, +whether they joined him or not. That was the last night he should +spend in that den. But the scheme met with no opposition on the part +of the other two; they swore they were ready for that, or for any +other mad thing, for anything in short but a surrender. And what was +more, they each insisted upon being the first man on deck, when the +time to make the rush should come. But to this their leader as +fiercely objected, reserving that priority for himself; particularly +as his two comrades would not yield, the one to the other, in the +matter; and both of them could not be first, for the ladder would but +admit one man at a time. And here, gentlemen, the foul play of these +miscreants must come out. + +"Upon hearing the frantic project of their leader, each in his own +separate soul had suddenly lighted, it would seem, upon the same +piece of treachery, namely: to be foremost in breaking out, in +order to be the first of the three, though the last of the ten, to +surrender; and thereby secure whatever small chance of pardon such +conduct might merit. But when Steelkilt made known his determination +still to lead them to the last, they in some way, by some subtle +chemistry of villany, mixed their before secret treacheries together; +and when their leader fell into a doze, verbally opened their souls +to each other in three sentences; and bound the sleeper with cords, +and gagged him with cords; and shrieked out for the Captain at +midnight. + +"Thinking murder at hand, and smelling in the dark for the blood, he +and all his armed mates and harpooneers rushed for the forecastle. +In a few minutes the scuttle was opened, and, bound hand and foot, +the still struggling ringleader was shoved up into the air by his +perfidious allies, who at once claimed the honour of securing a man +who had been fully ripe for murder. But all these were collared, and +dragged along the deck like dead cattle; and, side by side, were +seized up into the mizzen rigging, like three quarters of meat, and +there they hung till morning. 'Damn ye,' cried the Captain, pacing +to and fro before them, 'the vultures would not touch ye, ye +villains!' + +"At sunrise he summoned all hands; and separating those who had +rebelled from those who had taken no part in the mutiny, he told the +former that he had a good mind to flog them all round--thought, upon +the whole, he would do so--he ought to--justice demanded it; but for +the present, considering their timely surrender, he would let them go +with a reprimand, which he accordingly administered in the vernacular. + +"'But as for you, ye carrion rogues,' turning to the three men in the +rigging--'for you, I mean to mince ye up for the try-pots;' and, +seizing a rope, he applied it with all his might to the backs of the +two traitors, till they yelled no more, but lifelessly hung their +heads sideways, as the two crucified thieves are drawn. + +"'My wrist is sprained with ye!' he cried, at last; 'but there is +still rope enough left for you, my fine bantam, that wouldn't give +up. Take that gag from his mouth, and let us hear what he can say +for himself.' + +"For a moment the exhausted mutineer made a tremulous motion of his +cramped jaws, and then painfully twisting round his head, said in a +sort of hiss, 'What I say is this--and mind it well--if you flog me, +I murder you!' + +"'Say ye so? then see how ye frighten me'--and the Captain drew off +with the rope to strike. + +"'Best not,' hissed the Lakeman. + +"'But I must,'--and the rope was once more drawn back for the stroke. + +"Steelkilt here hissed out something, inaudible to all but the +Captain; who, to the amazement of all hands, started back, paced the +deck rapidly two or three times, and then suddenly throwing down his +rope, said, 'I won't do it--let him go--cut him down: d'ye hear?' + +But as the junior mates were hurrying to execute the order, a pale +man, with a bandaged head, arrested them--Radney the chief mate. +Ever since the blow, he had lain in his berth; but that morning, +hearing the tumult on the deck, he had crept out, and thus far had +watched the whole scene. Such was the state of his mouth, that he +could hardly speak; but mumbling something about his being willing +and able to do what the captain dared not attempt, he snatched the +rope and advanced to his pinioned foe. + +"'You are a coward!' hissed the Lakeman. + +"'So I am, but take that.' The mate was in the very act of striking, +when another hiss stayed his uplifted arm. He paused: and then +pausing no more, made good his word, spite of Steelkilt's threat, +whatever that might have been. The three men were then cut down, all +hands were turned to, and, sullenly worked by the moody seamen, the +iron pumps clanged as before. + +"Just after dark that day, when one watch had retired below, a clamor +was heard in the forecastle; and the two trembling traitors running +up, besieged the cabin door, saying they durst not consort with the +crew. Entreaties, cuffs, and kicks could not drive them back, so at +their own instance they were put down in the ship's run for +salvation. Still, no sign of mutiny reappeared among the rest. On +the contrary, it seemed, that mainly at Steelkilt's instigation, they +had resolved to maintain the strictest peacefulness, obey all orders +to the last, and, when the ship reached port, desert her in a body. +But in order to insure the speediest end to the voyage, they all +agreed to another thing--namely, not to sing out for whales, in case +any should be discovered. For, spite of her leak, and spite of all her +other perils, the Town-Ho still maintained her mast-heads, and her +captain was just as willing to lower for a fish that moment, as on +the day his craft first struck the cruising ground; and Radney the mate +was quite as ready to change his berth for a boat, and with his +bandaged mouth seek to gag in death the vital jaw of the whale. + +"But though the Lakeman had induced the seamen to adopt this sort of +passiveness in their conduct, he kept his own counsel (at least till +all was over) concerning his own proper and private revenge upon the +man who had stung him in the ventricles of his heart. He was in +Radney the chief mate's watch; and as if the infatuated man sought to +run more than half way to meet his doom, after the scene at the +rigging, he insisted, against the express counsel of the captain, +upon resuming the head of his watch at night. Upon this, and one or +two other circumstances, Steelkilt systematically built the plan of +his revenge. + +"During the night, Radney had an unseamanlike way of sitting on the +bulwarks of the quarter-deck, and leaning his arm upon the gunwale of +the boat which was hoisted up there, a little above the ship's side. +In this attitude, it was well known, he sometimes dozed. There was a +considerable vacancy between the boat and the ship, and down between +this was the sea. Steelkilt calculated his time, and found that his +next trick at the helm would come round at two o'clock, in the +morning of the third day from that in which he had been betrayed. At +his leisure, he employed the interval in braiding something very +carefully in his watches below. + +"'What are you making there?' said a shipmate. + +"'What do you think? what does it look like?' + +"'Like a lanyard for your bag; but it's an odd one, seems to me.' + +'Yes, rather oddish,' said the Lakeman, holding it at arm's length +before him; 'but I think it will answer. Shipmate, I haven't enough +twine,--have you any?' + +"But there was none in the forecastle. + +"'Then I must get some from old Rad;' and he rose to go aft. + +"'You don't mean to go a begging to HIM!' said a sailor. + +"'Why not? Do you think he won't do me a turn, when it's to help +himself in the end, shipmate?' and going to the mate, he looked at +him quietly, and asked him for some twine to mend his hammock. It +was given him--neither twine nor lanyard were seen again; but the +next night an iron ball, closely netted, partly rolled from the +pocket of the Lakeman's monkey jacket, as he was tucking the coat +into his hammock for a pillow. Twenty-four hours after, his trick at +the silent helm--nigh to the man who was apt to doze over the grave +always ready dug to the seaman's hand--that fatal hour was then to +come; and in the fore-ordaining soul of Steelkilt, the mate was +already stark and stretched as a corpse, with his forehead crushed +in. + +"But, gentlemen, a fool saved the would-be murderer from the bloody +deed he had planned. Yet complete revenge he had, and without being +the avenger. For by a mysterious fatality, Heaven itself seemed to +step in to take out of his hands into its own the damning thing he +would have done. + +"It was just between daybreak and sunrise of the morning of the +second day, when they were washing down the decks, that a stupid +Teneriffe man, drawing water in the main-chains, all at once shouted +out, 'There she rolls! there she rolls!' Jesu, what a whale! It was +Moby Dick. + +"'Moby Dick!' cried Don Sebastian; 'St. Dominic! Sir sailor, but do +whales have christenings? Whom call you Moby Dick?' + +"'A very white, and famous, and most deadly immortal monster, +Don;--but that would be too long a story.' + +"'How? how?' cried all the young Spaniards, crowding. + +"'Nay, Dons, Dons--nay, nay! I cannot rehearse that now. Let me get +more into the air, Sirs.' + +"'The chicha! the chicha!' cried Don Pedro; 'our vigorous friend looks +faint;--fill up his empty glass!' + +"No need, gentlemen; one moment, and I proceed.--Now, gentlemen, so +suddenly perceiving the snowy whale within fifty yards of the +ship--forgetful of the compact among the crew--in the excitement of +the moment, the Teneriffe man had instinctively and involuntarily +lifted his voice for the monster, though for some little time past it +had been plainly beheld from the three sullen mast-heads. All was +now a phrensy. 'The White Whale--the White Whale!' was the cry from +captain, mates, and harpooneers, who, undeterred by fearful rumours, +were all anxious to capture so famous and precious a fish; while the +dogged crew eyed askance, and with curses, the appalling beauty of +the vast milky mass, that lit up by a horizontal spangling sun, +shifted and glistened like a living opal in the blue morning sea. +Gentlemen, a strange fatality pervades the whole career of these +events, as if verily mapped out before the world itself was charted. +The mutineer was the bowsman of the mate, and when fast to a fish, it +was his duty to sit next him, while Radney stood up with his lance in +the prow, and haul in or slacken the line, at the word of command. +Moreover, when the four boats were lowered, the mate's got the start; +and none howled more fiercely with delight than did Steelkilt, as he +strained at his oar. After a stiff pull, their harpooneer got fast, +and, spear in hand, Radney sprang to the bow. He was always a +furious man, it seems, in a boat. And now his bandaged cry was, to +beach him on the whale's topmost back. Nothing loath, his bowsman +hauled him up and up, through a blinding foam that blent two +whitenesses together; till of a sudden the boat struck as against a +sunken ledge, and keeling over, spilled out the standing mate. That +instant, as he fell on the whale's slippery back, the boat righted, +and was dashed aside by the swell, while Radney was tossed over into +the sea, on the other flank of the whale. He struck out through the +spray, and, for an instant, was dimly seen through that veil, wildly +seeking to remove himself from the eye of Moby Dick. But the whale +rushed round in a sudden maelstrom; seized the swimmer between his +jaws; and rearing high up with him, plunged headlong again, and went +down. + +"Meantime, at the first tap of the boat's bottom, the Lakeman had +slackened the line, so as to drop astern from the whirlpool; calmly +looking on, he thought his own thoughts. But a sudden, terrific, +downward jerking of the boat, quickly brought his knife to the line. +He cut it; and the whale was free. But, at some distance, Moby Dick +rose again, with some tatters of Radney's red woollen shirt, caught +in the teeth that had destroyed him. All four boats gave chase +again; but the whale eluded them, and finally wholly disappeared. + +"In good time, the Town-Ho reached her port--a savage, solitary +place--where no civilized creature resided. There, headed by the +Lakeman, all but five or six of the foremastmen deliberately +deserted among the palms; eventually, as it turned out, seizing a +large double war-canoe of the savages, and setting sail for some +other harbor. + +"The ship's company being reduced to but a handful, the captain +called upon the Islanders to assist him in the laborious business of +heaving down the ship to stop the leak. But to such unresting +vigilance over their dangerous allies was this small band of whites +necessitated, both by night and by day, and so extreme was the hard +work they underwent, that upon the vessel being ready again for sea, +they were in such a weakened condition that the captain durst not put +off with them in so heavy a vessel. After taking counsel with his +officers, he anchored the ship as far off shore as possible; loaded +and ran out his two cannon from the bows; stacked his muskets on the +poop; and warning the Islanders not to approach the ship at their +peril, took one man with him, and setting the sail of his best +whale-boat, steered straight before the wind for Tahiti, five hundred +miles distant, to procure a reinforcement to his crew. + +"On the fourth day of the sail, a large canoe was descried, which +seemed to have touched at a low isle of corals. He steered away from +it; but the savage craft bore down on him; and soon the voice of +Steelkilt hailed him to heave to, or he would run him under water. +The captain presented a pistol. With one foot on each prow of the +yoked war-canoes, the Lakeman laughed him to scorn; assuring him that +if the pistol so much as clicked in the lock, he would bury him in +bubbles and foam. + +"'What do you want of me?' cried the captain. + +"'Where are you bound? and for what are you bound?' demanded +Steelkilt; 'no lies.' + +"'I am bound to Tahiti for more men.' + +"'Very good. Let me board you a moment--I come in peace.' With that +he leaped from the canoe, swam to the boat; and climbing the gunwale, +stood face to face with the captain. + +"'Cross your arms, sir; throw back your head. Now, repeat after me. +As soon as Steelkilt leaves me, I swear to beach this boat on yonder +island, and remain there six days. If I do not, may lightning strike +me!' + +"'A pretty scholar,' laughed the Lakeman. 'Adios, Senor!' and +leaping into the sea, he swam back to his comrades. + +"Watching the boat till it was fairly beached, and drawn up to the +roots of the cocoa-nut trees, Steelkilt made sail again, and in due +time arrived at Tahiti, his own place of destination. There, luck +befriended him; two ships were about to sail for France, and were +providentially in want of precisely that number of men which the +sailor headed. They embarked; and so for ever got the start of +their former captain, had he been at all minded to work them legal +retribution. + +"Some ten days after the French ships sailed, the whale-boat arrived, +and the captain was forced to enlist some of the more civilized +Tahitians, who had been somewhat used to the sea. Chartering a small +native schooner, he returned with them to his vessel; and finding all +right there, again resumed his cruisings. + +"Where Steelkilt now is, gentlemen, none know; but upon the island of +Nantucket, the widow of Radney still turns to the sea which refuses +to give up its dead; still in dreams sees the awful white whale that +destroyed him. + +"'Are you through?' said Don Sebastian, quietly. + +"'I am, Don.' + +"'Then I entreat you, tell me if to the best of your own convictions, +this your story is in substance really true? It is so passing +wonderful! Did you get it from an unquestionable source? Bear with +me if I seem to press.' + +"'Also bear with all of us, sir sailor; for we all join in Don +Sebastian's suit,' cried the company, with exceeding interest. + +"'Is there a copy of the Holy Evangelists in the Golden Inn, +gentlemen?' + +"'Nay,' said Don Sebastian; 'but I know a worthy priest near by, who +will quickly procure one for me. I go for it; but are you well +advised? this may grow too serious.' + +"'Will you be so good as to bring the priest also, Don?' + +"'Though there are no Auto-da-Fe's in Lima now,' said one of the +company to another; 'I fear our sailor friend runs risk of the +archiepiscopacy. Let us withdraw more out of the moonlight. I see +no need of this.' + +"'Excuse me for running after you, Don Sebastian; but may I also beg +that you will be particular in procuring the largest sized +Evangelists you can.' + + +'This is the priest, he brings you the Evangelists,' said Don +Sebastian, gravely, returning with a tall and solemn figure. + +"'Let me remove my hat. Now, venerable priest, further into the +light, and hold the Holy Book before me that I may touch it. + +"'So help me Heaven, and on my honour the story I have told ye, +gentlemen, is in substance and its great items, true. I know it to +be true; it happened on this ball; I trod the ship; I knew the crew; +I have seen and talked with Steelkilt since the death of Radney.'" + + + +CHAPTER 55 + +Of the Monstrous Pictures of Whales. + + +I shall ere long paint to you as well as one can without canvas, +something like the true form of the whale as he actually appears to +the eye of the whaleman when in his own absolute body the whale is +moored alongside the whale-ship so that he can be fairly stepped upon +there. It may be worth while, therefore, previously to advert to +those curious imaginary portraits of him which even down to the +present day confidently challenge the faith of the landsman. It is +time to set the world right in this matter, by proving such pictures +of the whale all wrong. + +It may be that the primal source of all those pictorial delusions +will be found among the oldest Hindoo, Egyptian, and Grecian +sculptures. For ever since those inventive but unscrupulous times +when on the marble panellings of temples, the pedestals of statues, +and on shields, medallions, cups, and coins, the dolphin was drawn in +scales of chain-armor like Saladin's, and a helmeted head like St. +George's; ever since then has something of the same sort of license +prevailed, not only in most popular pictures of the whale, but in +many scientific presentations of him. + +Now, by all odds, the most ancient extant portrait anyways purporting +to be the whale's, is to be found in the famous cavern-pagoda of +Elephanta, in India. The Brahmins maintain that in the almost +endless sculptures of that immemorial pagoda, all the trades and +pursuits, every conceivable avocation of man, were prefigured ages +before any of them actually came into being. No wonder then, that in +some sort our noble profession of whaling should have been there +shadowed forth. The Hindoo whale referred to, occurs in a separate +department of the wall, depicting the incarnation of Vishnu in the +form of leviathan, learnedly known as the Matse Avatar. But though +this sculpture is half man and half whale, so as only to give the +tail of the latter, yet that small section of him is all wrong. It +looks more like the tapering tail of an anaconda, than the broad palms +of the true whale's majestic flukes. + +But go to the old Galleries, and look now at a great Christian +painter's portrait of this fish; for he succeeds no better than the +antediluvian Hindoo. It is Guido's picture of Perseus rescuing +Andromeda from the sea-monster or whale. Where did Guido get the +model of such a strange creature as that? Nor does Hogarth, in +painting the same scene in his own "Perseus Descending," make out one +whit better. The huge corpulence of that Hogarthian monster +undulates on the surface, scarcely drawing one inch of water. It has +a sort of howdah on its back, and its distended tusked mouth into +which the billows are rolling, might be taken for the Traitors' Gate +leading from the Thames by water into the Tower. Then, there are the +Prodromus whales of old Scotch Sibbald, and Jonah's whale, as +depicted in the prints of old Bibles and the cuts of old primers. +What shall be said of these? As for the book-binder's whale winding +like a vine-stalk round the stock of a descending anchor--as stamped +and gilded on the backs and title-pages of many books both old and +new--that is a very picturesque but purely fabulous creature, +imitated, I take it, from the like figures on antique vases. Though +universally denominated a dolphin, I nevertheless call this +book-binder's fish an attempt at a whale; because it was so intended +when the device was first introduced. It was introduced by an old +Italian publisher somewhere about the 15th century, during the +Revival of Learning; and in those days, and even down to a +comparatively late period, dolphins were popularly supposed to be a +species of the Leviathan. + +In the vignettes and other embellishments of some ancient books you +will at times meet with very curious touches at the whale, where all +manner of spouts, jets d'eau, hot springs and cold, Saratoga and +Baden-Baden, come bubbling up from his unexhausted brain. In the +title-page of the original edition of the "Advancement of Learning" +you will find some curious whales. + +But quitting all these unprofessional attempts, let us glance at +those pictures of leviathan purporting to be sober, scientific +delineations, by those who know. In old Harris's collection of +voyages there are some plates of whales extracted from a Dutch book +of voyages, A.D. 1671, entitled "A Whaling Voyage to Spitzbergen in +the ship Jonas in the Whale, Peter Peterson of Friesland, master." +In one of those plates the whales, like great rafts of logs, are +represented lying among ice-isles, with white bears running over +their living backs. In another plate, the prodigious blunder is made +of representing the whale with perpendicular flukes. + +Then again, there is an imposing quarto, written by one Captain +Colnett, a Post Captain in the English navy, entitled "A Voyage round +Cape Horn into the South Seas, for the purpose of extending the +Spermaceti Whale Fisheries." In this book is an outline purporting +to be a "Picture of a Physeter or Spermaceti whale, drawn by scale +from one killed on the coast of Mexico, August, 1793, and hoisted on +deck." I doubt not the captain had this veracious picture taken for +the benefit of his marines. To mention but one thing about it, let +me say that it has an eye which applied, according to the +accompanying scale, to a full grown sperm whale, would make the eye +of that whale a bow-window some five feet long. Ah, my gallant +captain, why did ye not give us Jonah looking out of that eye! + +Nor are the most conscientious compilations of Natural History for +the benefit of the young and tender, free from the same heinousness +of mistake. Look at that popular work "Goldsmith's Animated Nature." +In the abridged London edition of 1807, there are plates of an +alleged "whale" and a "narwhale." I do not wish to seem inelegant, +but this unsightly whale looks much like an amputated sow; and, as +for the narwhale, one glimpse at it is enough to amaze one, that in +this nineteenth century such a hippogriff could be palmed for genuine +upon any intelligent public of schoolboys. + +Then, again, in 1825, Bernard Germain, Count de Lacepede, a great +naturalist, published a scientific systemized whale book, wherein are +several pictures of the different species of the Leviathan. All +these are not only incorrect, but the picture of the Mysticetus or +Greenland whale (that is to say, the Right whale), even Scoresby, a +long experienced man as touching that species, declares not to have +its counterpart in nature. + +But the placing of the cap-sheaf to all this blundering business was +reserved for the scientific Frederick Cuvier, brother to the famous +Baron. In 1836, he published a Natural History of Whales, in which +he gives what he calls a picture of the Sperm Whale. Before showing +that picture to any Nantucketer, you had best provide for your +summary retreat from Nantucket. In a word, Frederick Cuvier's Sperm +Whale is not a Sperm Whale, but a squash. Of course, he never had +the benefit of a whaling voyage (such men seldom have), but whence he +derived that picture, who can tell? Perhaps he got it as his +scientific predecessor in the same field, Desmarest, got one of his +authentic abortions; that is, from a Chinese drawing. And what sort +of lively lads with the pencil those Chinese are, many queer cups and +saucers inform us. + +As for the sign-painters' whales seen in the streets hanging over the +shops of oil-dealers, what shall be said of them? They are generally +Richard III. whales, with dromedary humps, and very savage; +breakfasting on three or four sailor tarts, that is whaleboats full +of mariners: their deformities floundering in seas of blood and blue +paint. + +But these manifold mistakes in depicting the whale are not so very +surprising after all. Consider! Most of the scientific drawings +have been taken from the stranded fish; and these are about as +correct as a drawing of a wrecked ship, with broken back, would +correctly represent the noble animal itself in all its undashed pride +of hull and spars. Though elephants have stood for their +full-lengths, the living Leviathan has never yet fairly floated +himself for his portrait. The living whale, in his full majesty and +significance, is only to be seen at sea in unfathomable waters; and +afloat the vast bulk of him is out of sight, like a launched +line-of-battle ship; and out of that element it is a thing eternally +impossible for mortal man to hoist him bodily into the air, so as to +preserve all his mighty swells and undulations. And, not to speak of +the highly presumable difference of contour between a young sucking +whale and a full-grown Platonian Leviathan; yet, even in the case of +one of those young sucking whales hoisted to a ship's deck, such is +then the outlandish, eel-like, limbered, varying shape of him, that +his precise expression the devil himself could not catch. + +But it may be fancied, that from the naked skeleton of the stranded +whale, accurate hints may be derived touching his true form. Not at +all. For it is one of the more curious things about this Leviathan, +that his skeleton gives very little idea of his general shape. +Though Jeremy Bentham's skeleton, which hangs for candelabra in the +library of one of his executors, correctly conveys the idea of a +burly-browed utilitarian old gentleman, with all Jeremy's other +leading personal characteristics; yet nothing of this kind could be +inferred from any leviathan's articulated bones. In fact, as the +great Hunter says, the mere skeleton of the whale bears the same +relation to the fully invested and padded animal as the insect does +to the chrysalis that so roundingly envelopes it. This peculiarity +is strikingly evinced in the head, as in some part of this book will +be incidentally shown. It is also very curiously displayed in the +side fin, the bones of which almost exactly answer to the bones of the +human hand, minus only the thumb. This fin has four regular +bone-fingers, the index, middle, ring, and little finger. But all +these are permanently lodged in their fleshy covering, as the human +fingers in an artificial covering. "However recklessly the whale may +sometimes serve us," said humorous Stubb one day, "he can never be +truly said to handle us without mittens." + +For all these reasons, then, any way you may look at it, you must +needs conclude that the great Leviathan is that one creature in the +world which must remain unpainted to the last. True, one portrait +may hit the mark much nearer than another, but none can hit it with +any very considerable degree of exactness. So there is no earthly +way of finding out precisely what the whale really looks like. And +the only mode in which you can derive even a tolerable idea of his +living contour, is by going a whaling yourself; but by so doing, you +run no small risk of being eternally stove and sunk by him. +Wherefore, it seems to me you had best not be too fastidious in your +curiosity touching this Leviathan. + + + +CHAPTER 56 + +Of the Less Erroneous Pictures of Whales, and the True Pictures of +Whaling Scenes. + + +In connexion with the monstrous pictures of whales, I am strongly +tempted here to enter upon those still more monstrous stories of them +which are to be found in certain books, both ancient and modern, +especially in Pliny, Purchas, Hackluyt, Harris, Cuvier, etc. But I +pass that matter by. + +I know of only four published outlines of the great Sperm Whale; +Colnett's, Huggins's, Frederick Cuvier's, and Beale's. In the +previous chapter Colnett and Cuvier have been referred to. Huggins's +is far better than theirs; but, by great odds, Beale's is the best. +All Beale's drawings of this whale are good, excepting the middle +figure in the picture of three whales in various attitudes, capping +his second chapter. His frontispiece, boats attacking Sperm Whales, +though no doubt calculated to excite the civil scepticism of some +parlor men, is admirably correct and life-like in its general effect. +Some of the Sperm Whale drawings in J. Ross Browne are pretty +correct in contour; but they are wretchedly engraved. That is not +his fault though. + +Of the Right Whale, the best outline pictures are in Scoresby; but +they are drawn on too small a scale to convey a desirable impression. +He has but one picture of whaling scenes, and this is a sad +deficiency, because it is by such pictures only, when at all well +done, that you can derive anything like a truthful idea of the living +whale as seen by his living hunters. + +But, taken for all in all, by far the finest, though in some details +not the most correct, presentations of whales and whaling scenes to +be anywhere found, are two large French engravings, well executed, +and taken from paintings by one Garnery. Respectively, they +represent attacks on the Sperm and Right Whale. In the first +engraving a noble Sperm Whale is depicted in full majesty of might, +just risen beneath the boat from the profundities of the ocean, and +bearing high in the air upon his back the terrific wreck of the +stoven planks. The prow of the boat is partially unbroken, and is +drawn just balancing upon the monster's spine; and standing in that +prow, for that one single incomputable flash of time, you behold an +oarsman, half shrouded by the incensed boiling spout of the whale, +and in the act of leaping, as if from a precipice. The action of the +whole thing is wonderfully good and true. The half-emptied line-tub +floats on the whitened sea; the wooden poles of the spilled harpoons +obliquely bob in it; the heads of the swimming crew are scattered +about the whale in contrasting expressions of affright; while in the +black stormy distance the ship is bearing down upon the scene. +Serious fault might be found with the anatomical details of this +whale, but let that pass; since, for the life of me, I could not draw +so good a one. + +In the second engraving, the boat is in the act of drawing alongside +the barnacled flank of a large running Right Whale, that rolls his +black weedy bulk in the sea like some mossy rock-slide from the +Patagonian cliffs. His jets are erect, full, and black like soot; so +that from so abounding a smoke in the chimney, you would think there +must be a brave supper cooking in the great bowels below. Sea fowls +are pecking at the small crabs, shell-fish, and other sea candies and +maccaroni, which the Right Whale sometimes carries on his pestilent +back. And all the while the thick-lipped leviathan is rushing +through the deep, leaving tons of tumultuous white curds in his wake, +and causing the slight boat to rock in the swells like a skiff caught +nigh the paddle-wheels of an ocean steamer. Thus, the foreground is +all raging commotion; but behind, in admirable artistic contrast, is +the glassy level of a sea becalmed, the drooping unstarched sails of +the powerless ship, and the inert mass of a dead whale, a conquered +fortress, with the flag of capture lazily hanging from the whale-pole +inserted into his spout-hole. + +Who Garnery the painter is, or was, I know not. But my life for it +he was either practically conversant with his subject, or else +marvellously tutored by some experienced whaleman. The French are +the lads for painting action. Go and gaze upon all the paintings of +Europe, and where will you find such a gallery of living and +breathing commotion on canvas, as in that triumphal hall at +Versailles; where the beholder fights his way, pell-mell, through the +consecutive great battles of France; where every sword seems a flash +of the Northern Lights, and the successive armed kings and Emperors +dash by, like a charge of crowned centaurs? Not wholly unworthy of a +place in that gallery, are these sea battle-pieces of Garnery. + +The natural aptitude of the French for seizing the picturesqueness of +things seems to be peculiarly evinced in what paintings and +engravings they have of their whaling scenes. With not one tenth of +England's experience in the fishery, and not the thousandth part of +that of the Americans, they have nevertheless furnished both nations +with the only finished sketches at all capable of conveying the real +spirit of the whale hunt. For the most part, the English and +American whale draughtsmen seem entirely content with presenting the +mechanical outline of things, such as the vacant profile of the +whale; which, so far as picturesqueness of effect is concerned, is +about tantamount to sketching the profile of a pyramid. Even +Scoresby, the justly renowned Right whaleman, after giving us a stiff +full length of the Greenland whale, and three or four delicate +miniatures of narwhales and porpoises, treats us to a series of +classical engravings of boat hooks, chopping knives, and grapnels; +and with the microscopic diligence of a Leuwenhoeck submits to the +inspection of a shivering world ninety-six fac-similes of magnified +Arctic snow crystals. I mean no disparagement to the excellent +voyager (I honour him for a veteran), but in so important a matter it +was certainly an oversight not to have procured for every crystal a +sworn affidavit taken before a Greenland Justice of the Peace. + +In addition to those fine engravings from Garnery, there are two +other French engravings worthy of note, by some one who subscribes +himself "H. Durand." One of them, though not precisely adapted to +our present purpose, nevertheless deserves mention on other accounts. +It is a quiet noon-scene among the isles of the Pacific; a French +whaler anchored, inshore, in a calm, and lazily taking water on +board; the loosened sails of the ship, and the long leaves of the +palms in the background, both drooping together in the breezeless +air. The effect is very fine, when considered with reference to its +presenting the hardy fishermen under one of their few aspects of +oriental repose. The other engraving is quite a different affair: +the ship hove-to upon the open sea, and in the very heart of the +Leviathanic life, with a Right Whale alongside; the vessel (in the +act of cutting-in) hove over to the monster as if to a quay; and a +boat, hurriedly pushing off from this scene of activity, is about +giving chase to whales in the distance. The harpoons and lances lie +levelled for use; three oarsmen are just setting the mast in its +hole; while from a sudden roll of the sea, the little craft stands +half-erect out of the water, like a rearing horse. From the ship, +the smoke of the torments of the boiling whale is going up like the +smoke over a village of smithies; and to windward, a black cloud, +rising up with earnest of squalls and rains, seems to quicken the +activity of the excited seamen. + + + +CHAPTER 57 + +Of Whales in Paint; in Teeth; in Wood; in Sheet-Iron; in Stone; in +Mountains; in Stars. + + +On Tower-hill, as you go down to the London docks, you may have seen +a crippled beggar (or KEDGER, as the sailors say) holding a painted +board before him, representing the tragic scene in which he lost his +leg. There are three whales and three boats; and one of the boats +(presumed to contain the missing leg in all its original integrity) +is being crunched by the jaws of the foremost whale. Any time these +ten years, they tell me, has that man held up that picture, and +exhibited that stump to an incredulous world. But the time of his +justification has now come. His three whales are as good whales as +were ever published in Wapping, at any rate; and his stump as +unquestionable a stump as any you will find in the western clearings. +But, though for ever mounted on that stump, never a stump-speech +does the poor whaleman make; but, with downcast eyes, stands ruefully +contemplating his own amputation. + +Throughout the Pacific, and also in Nantucket, and New Bedford, and +Sag Harbor, you will come across lively sketches of whales and +whaling-scenes, graven by the fishermen themselves on Sperm +Whale-teeth, or ladies' busks wrought out of the Right Whale-bone, +and other like skrimshander articles, as the whalemen call the +numerous little ingenious contrivances they elaborately carve out of +the rough material, in their hours of ocean leisure. Some of them +have little boxes of dentistical-looking implements, specially +intended for the skrimshandering business. But, in general, they +toil with their jack-knives alone; and, with that almost omnipotent +tool of the sailor, they will turn you out anything you please, in +the way of a mariner's fancy. + +Long exile from Christendom and civilization inevitably restores a +man to that condition in which God placed him, i.e. what is called +savagery. Your true whale-hunter is as much a savage as an Iroquois. +I myself am a savage, owning no allegiance but to the King of the +Cannibals; and ready at any moment to rebel against him. + +Now, one of the peculiar characteristics of the savage in his +domestic hours, is his wonderful patience of industry. An ancient +Hawaiian war-club or spear-paddle, in its full multiplicity and +elaboration of carving, is as great a trophy of human perseverance as +a Latin lexicon. For, with but a bit of broken sea-shell or a +shark's tooth, that miraculous intricacy of wooden net-work has been +achieved; and it has cost steady years of steady application. + +As with the Hawaiian savage, so with the white sailor-savage. With +the same marvellous patience, and with the same single shark's tooth, +of his one poor jack-knife, he will carve you a bit of bone +sculpture, not quite as workmanlike, but as close packed in its +maziness of design, as the Greek savage, Achilles's shield; and full +of barbaric spirit and suggestiveness, as the prints of that fine old +Dutch savage, Albert Durer. + +Wooden whales, or whales cut in profile out of the small dark slabs +of the noble South Sea war-wood, are frequently met with in the +forecastles of American whalers. Some of them are done with much +accuracy. + +At some old gable-roofed country houses you will see brass whales +hung by the tail for knockers to the road-side door. When the porter +is sleepy, the anvil-headed whale would be best. But these knocking +whales are seldom remarkable as faithful essays. On the spires of +some old-fashioned churches you will see sheet-iron whales placed +there for weather-cocks; but they are so elevated, and besides that +are to all intents and purposes so labelled with "HANDS OFF!" you +cannot examine them closely enough to decide upon their merit. + +In bony, ribby regions of the earth, where at the base of high broken +cliffs masses of rock lie strewn in fantastic groupings upon the +plain, you will often discover images as of the petrified forms of +the Leviathan partly merged in grass, which of a windy day breaks +against them in a surf of green surges. + +Then, again, in mountainous countries where the traveller is +continually girdled by amphitheatrical heights; here and there from +some lucky point of view you will catch passing glimpses of the +profiles of whales defined along the undulating ridges. But you must +be a thorough whaleman, to see these sights; and not only that, but +if you wish to return to such a sight again, you must be sure and +take the exact intersecting latitude and longitude of your first +stand-point, else so chance-like are such observations of the hills, +that your precise, previous stand-point would require a laborious +re-discovery; like the Soloma Islands, which still remain incognita, +though once high-ruffed Mendanna trod them and old Figuera +chronicled them. + +Nor when expandingly lifted by your subject, can you fail to trace +out great whales in the starry heavens, and boats in pursuit of them; +as when long filled with thoughts of war the Eastern nations saw +armies locked in battle among the clouds. Thus at the North have I +chased Leviathan round and round the Pole with the revolutions of the +bright points that first defined him to me. And beneath the +effulgent Antarctic skies I have boarded the Argo-Navis, and joined +the chase against the starry Cetus far beyond the utmost stretch of +Hydrus and the Flying Fish. + +With a frigate's anchors for my bridle-bitts and fasces of harpoons +for spurs, would I could mount that whale and leap the topmost skies, +to see whether the fabled heavens with all their countless tents +really lie encamped beyond my mortal sight! + + + +CHAPTER 58 + +Brit. + + +Steering north-eastward from the Crozetts, we fell in with vast +meadows of brit, the minute, yellow substance, upon which the Right +Whale largely feeds. For leagues and leagues it undulated round us, +so that we seemed to be sailing through boundless fields of ripe and +golden wheat. + +On the second day, numbers of Right Whales were seen, who, secure +from the attack of a Sperm Whaler like the Pequod, with open jaws +sluggishly swam through the brit, which, adhering to the fringing +fibres of that wondrous Venetian blind in their mouths, was in that +manner separated from the water that escaped at the lip. + +As morning mowers, who side by side slowly and seethingly advance +their scythes through the long wet grass of marshy meads; even so +these monsters swam, making a strange, grassy, cutting sound; and +leaving behind them endless swaths of blue upon the yellow sea.* + + +*That part of the sea known among whalemen as the "Brazil Banks" does +not bear that name as the Banks of Newfoundland do, because of there +being shallows and soundings there, but because of this remarkable +meadow-like appearance, caused by the vast drifts of brit continually +floating in those latitudes, where the Right Whale is often chased. + + +But it was only the sound they made as they parted the brit which at +all reminded one of mowers. Seen from the mast-heads, especially +when they paused and were stationary for a while, their vast black +forms looked more like lifeless masses of rock than anything else. +And as in the great hunting countries of India, the stranger at a +distance will sometimes pass on the plains recumbent elephants +without knowing them to be such, taking them for bare, blackened +elevations of the soil; even so, often, with him, who for the first +time beholds this species of the leviathans of the sea. And even +when recognised at last, their immense magnitude renders it very +hard really to believe that such bulky masses of overgrowth can +possibly be instinct, in all parts, with the same sort of life that +lives in a dog or a horse. + +Indeed, in other respects, you can hardly regard any creatures of the +deep with the same feelings that you do those of the shore. For +though some old naturalists have maintained that all creatures of the +land are of their kind in the sea; and though taking a broad general +view of the thing, this may very well be; yet coming to specialties, +where, for example, does the ocean furnish any fish that in +disposition answers to the sagacious kindness of the dog? The +accursed shark alone can in any generic respect be said to bear +comparative analogy to him. + +But though, to landsmen in general, the native inhabitants of the +seas have ever been regarded with emotions unspeakably unsocial and +repelling; though we know the sea to be an everlasting terra +incognita, so that Columbus sailed over numberless unknown worlds to +discover his one superficial western one; though, by vast odds, the +most terrific of all mortal disasters have immemorially and +indiscriminately befallen tens and hundreds of thousands of those who +have gone upon the waters; though but a moment's consideration will +teach, that however baby man may brag of his science and skill, and +however much, in a flattering future, that science and skill may +augment; yet for ever and for ever, to the crack of doom, the sea +will insult and murder him, and pulverize the stateliest, stiffest +frigate he can make; nevertheless, by the continual repetition of +these very impressions, man has lost that sense of the full awfulness +of the sea which aboriginally belongs to it. + +The first boat we read of, floated on an ocean, that with Portuguese +vengeance had whelmed a whole world without leaving so much as a +widow. That same ocean rolls now; that same ocean destroyed the +wrecked ships of last year. Yea, foolish mortals, Noah's flood is +not yet subsided; two thirds of the fair world it yet covers. + +Wherein differ the sea and the land, that a miracle upon one is not a +miracle upon the other? Preternatural terrors rested upon the +Hebrews, when under the feet of Korah and his company the live ground +opened and swallowed them up for ever; yet not a modern sun ever +sets, but in precisely the same manner the live sea swallows up ships +and crews. + +But not only is the sea such a foe to man who is an alien to it, but +it is also a fiend to its own off-spring; worse than the Persian host +who murdered his own guests; sparing not the creatures which itself +hath spawned. Like a savage tigress that tossing in the jungle +overlays her own cubs, so the sea dashes even the mightiest whales +against the rocks, and leaves them there side by side with the split +wrecks of ships. No mercy, no power but its own controls it. +Panting and snorting like a mad battle steed that has lost its rider, +the masterless ocean overruns the globe. + +Consider the subtleness of the sea; how its most dreaded creatures +glide under water, unapparent for the most part, and treacherously +hidden beneath the loveliest tints of azure. Consider also the +devilish brilliance and beauty of many of its most remorseless +tribes, as the dainty embellished shape of many species of sharks. +Consider, once more, the universal cannibalism of the sea; all whose +creatures prey upon each other, carrying on eternal war since the +world began. + +Consider all this; and then turn to this green, gentle, and most +docile earth; consider them both, the sea and the land; and do you +not find a strange analogy to something in yourself? For as this +appalling ocean surrounds the verdant land, so in the soul of man +there lies one insular Tahiti, full of peace and joy, but encompassed +by all the horrors of the half known life. God keep thee! Push not +off from that isle, thou canst never return! + + +CHAPTER 59 + +Squid. + + +Slowly wading through the meadows of brit, the Pequod still held on +her way north-eastward towards the island of Java; a gentle air +impelling her keel, so that in the surrounding serenity her three +tall tapering masts mildly waved to that languid breeze, as three +mild palms on a plain. And still, at wide intervals in the silvery +night, the lonely, alluring jet would be seen. + +But one transparent blue morning, when a stillness almost +preternatural spread over the sea, however unattended with any +stagnant calm; when the long burnished sun-glade on the waters seemed +a golden finger laid across them, enjoining some secrecy; when the +slippered waves whispered together as they softly ran on; in this +profound hush of the visible sphere a strange spectre was seen by +Daggoo from the main-mast-head. + +In the distance, a great white mass lazily rose, and rising higher +and higher, and disentangling itself from the azure, at last gleamed +before our prow like a snow-slide, new slid from the hills. Thus +glistening for a moment, as slowly it subsided, and sank. Then once +more arose, and silently gleamed. It seemed not a whale; and yet is +this Moby Dick? thought Daggoo. Again the phantom went down, but on +re-appearing once more, with a stiletto-like cry that startled every +man from his nod, the negro yelled out--"There! there again! there +she breaches! right ahead! The White Whale, the White Whale!" + +Upon this, the seamen rushed to the yard-arms, as in swarming-time +the bees rush to the boughs. Bare-headed in the sultry sun, Ahab +stood on the bowsprit, and with one hand pushed far behind in +readiness to wave his orders to the helmsman, cast his eager glance +in the direction indicated aloft by the outstretched motionless arm +of Daggoo. + +Whether the flitting attendance of the one still and solitary jet had +gradually worked upon Ahab, so that he was now prepared to connect +the ideas of mildness and repose with the first sight of the +particular whale he pursued; however this was, or whether his +eagerness betrayed him; whichever way it might have been, no sooner +did he distinctly perceive the white mass, than with a quick +intensity he instantly gave orders for lowering. + +The four boats were soon on the water; Ahab's in advance, and all +swiftly pulling towards their prey. Soon it went down, and while, +with oars suspended, we were awaiting its reappearance, lo! in the +same spot where it sank, once more it slowly rose. Almost forgetting +for the moment all thoughts of Moby Dick, we now gazed at the most +wondrous phenomenon which the secret seas have hitherto revealed to +mankind. A vast pulpy mass, furlongs in length and breadth, of a +glancing cream-colour, lay floating on the water, innumerable long +arms radiating from its centre, and curling and twisting like a nest +of anacondas, as if blindly to clutch at any hapless object within +reach. No perceptible face or front did it have; no conceivable +token of either sensation or instinct; but undulated there on the +billows, an unearthly, formless, chance-like apparition of life. + +As with a low sucking sound it slowly disappeared again, Starbuck +still gazing at the agitated waters where it had sunk, with a wild +voice exclaimed--"Almost rather had I seen Moby Dick and fought him, +than to have seen thee, thou white ghost!" + +"What was it, Sir?" said Flask. + +"The great live squid, which, they say, few whale-ships ever beheld, +and returned to their ports to tell of it." + +But Ahab said nothing; turning his boat, he sailed back to the +vessel; the rest as silently following. + +Whatever superstitions the sperm whalemen in general have connected +with the sight of this object, certain it is, that a glimpse of it +being so very unusual, that circumstance has gone far to invest it +with portentousness. So rarely is it beheld, that though one and all +of them declare it to be the largest animated thing in the ocean, yet +very few of them have any but the most vague ideas concerning its +true nature and form; notwithstanding, they believe it to furnish to +the sperm whale his only food. For though other species of whales +find their food above water, and may be seen by man in the act of +feeding, the spermaceti whale obtains his whole food in unknown zones +below the surface; and only by inference is it that any one can tell +of what, precisely, that food consists. At times, when closely +pursued, he will disgorge what are supposed to be the detached arms +of the squid; some of them thus exhibited exceeding twenty and thirty +feet in length. They fancy that the monster to which these arms +belonged ordinarily clings by them to the bed of the ocean; and that +the sperm whale, unlike other species, is supplied with teeth in +order to attack and tear it. + +There seems some ground to imagine that the great Kraken of Bishop +Pontoppodan may ultimately resolve itself into Squid. The manner in +which the Bishop describes it, as alternately rising and sinking, +with some other particulars he narrates, in all this the two +correspond. But much abatement is necessary with respect to the +incredible bulk he assigns it. + +By some naturalists who have vaguely heard rumors of the mysterious +creature, here spoken of, it is included among the class of +cuttle-fish, to which, indeed, in certain external respects it would +seem to belong, but only as the Anak of the tribe. + + + +CHAPTER 60 + +The Line. + + +With reference to the whaling scene shortly to be described, as well +as for the better understanding of all similar scenes elsewhere +presented, I have here to speak of the magical, sometimes horrible +whale-line. + +The line originally used in the fishery was of the best hemp, +slightly vapoured with tar, not impregnated with it, as in the case of +ordinary ropes; for while tar, as ordinarily used, makes the hemp +more pliable to the rope-maker, and also renders the rope itself more +convenient to the sailor for common ship use; yet, not only would the +ordinary quantity too much stiffen the whale-line for the close +coiling to which it must be subjected; but as most seamen are +beginning to learn, tar in general by no means adds to the rope's +durability or strength, however much it may give it compactness and +gloss. + +Of late years the Manilla rope has in the American fishery almost +entirely superseded hemp as a material for whale-lines; for, though +not so durable as hemp, it is stronger, and far more soft and +elastic; and I will add (since there is an aesthetics in all things), +is much more handsome and becoming to the boat, than hemp. Hemp is a +dusky, dark fellow, a sort of Indian; but Manilla is as a +golden-haired Circassian to behold. + +The whale-line is only two-thirds of an inch in thickness. At first +sight, you would not think it so strong as it really is. By +experiment its one and fifty yarns will each suspend a weight of one +hundred and twenty pounds; so that the whole rope will bear a strain +nearly equal to three tons. In length, the common sperm whale-line +measures something over two hundred fathoms. Towards the stern of +the boat it is spirally coiled away in the tub, not like the +worm-pipe of a still though, but so as to form one round, +cheese-shaped mass of densely bedded "sheaves," or layers of +concentric spiralizations, without any hollow but the "heart," or +minute vertical tube formed at the axis of the cheese. As the least +tangle or kink in the coiling would, in running out, infallibly take +somebody's arm, leg, or entire body off, the utmost precaution is +used in stowing the line in its tub. Some harpooneers will consume +almost an entire morning in this business, carrying the line high +aloft and then reeving it downwards through a block towards the tub, +so as in the act of coiling to free it from all possible wrinkles and +twists. + +In the English boats two tubs are used instead of one; the same line +being continuously coiled in both tubs. There is some advantage in +this; because these twin-tubs being so small they fit more readily +into the boat, and do not strain it so much; whereas, the American +tub, nearly three feet in diameter and of proportionate depth, makes +a rather bulky freight for a craft whose planks are but one half-inch +in thickness; for the bottom of the whale-boat is like critical ice, +which will bear up a considerable distributed weight, but not very +much of a concentrated one. When the painted canvas cover is clapped +on the American line-tub, the boat looks as if it were pulling off +with a prodigious great wedding-cake to present to the whales. + +Both ends of the line are exposed; the lower end terminating in an +eye-splice or loop coming up from the bottom against the side of the +tub, and hanging over its edge completely disengaged from everything. +This arrangement of the lower end is necessary on two accounts. +First: In order to facilitate the fastening to it of an additional +line from a neighboring boat, in case the stricken whale should sound +so deep as to threaten to carry off the entire line originally +attached to the harpoon. In these instances, the whale of course is +shifted like a mug of ale, as it were, from the one boat to the +other; though the first boat always hovers at hand to assist its +consort. Second: This arrangement is indispensable for common +safety's sake; for were the lower end of the line in any way attached +to the boat, and were the whale then to run the line out to the end +almost in a single, smoking minute as he sometimes does, he would not +stop there, for the doomed boat would infallibly be dragged down +after him into the profundity of the sea; and in that case no +town-crier would ever find her again. + +Before lowering the boat for the chase, the upper end of the line is +taken aft from the tub, and passing round the loggerhead there, is +again carried forward the entire length of the boat, resting +crosswise upon the loom or handle of every man's oar, so that it jogs +against his wrist in rowing; and also passing between the men, as +they alternately sit at the opposite gunwales, to the leaded chocks +or grooves in the extreme pointed prow of the boat, where a wooden +pin or skewer the size of a common quill, prevents it from slipping +out. From the chocks it hangs in a slight festoon over the bows, and +is then passed inside the boat again; and some ten or twenty fathoms +(called box-line) being coiled upon the box in the bows, it continues +its way to the gunwale still a little further aft, and is then +attached to the short-warp--the rope which is immediately connected +with the harpoon; but previous to that connexion, the short-warp goes +through sundry mystifications too tedious to detail. + +Thus the whale-line folds the whole boat in its complicated coils, +twisting and writhing around it in almost every direction. All the +oarsmen are involved in its perilous contortions; so that to the +timid eye of the landsman, they seem as Indian jugglers, with the +deadliest snakes sportively festooning their limbs. Nor can any son +of mortal woman, for the first time, seat himself amid those hempen +intricacies, and while straining his utmost at the oar, bethink him +that at any unknown instant the harpoon may be darted, and all these +horrible contortions be put in play like ringed lightnings; he cannot +be thus circumstanced without a shudder that makes the very marrow in +his bones to quiver in him like a shaken jelly. Yet habit--strange +thing! what cannot habit accomplish?--Gayer sallies, more merry +mirth, better jokes, and brighter repartees, you never heard over +your mahogany, than you will hear over the half-inch white cedar of +the whale-boat, when thus hung in hangman's nooses; and, like the six +burghers of Calais before King Edward, the six men composing the crew +pull into the jaws of death, with a halter around every neck, as you +may say. + +Perhaps a very little thought will now enable you to account for +those repeated whaling disasters--some few of which are casually +chronicled--of this man or that man being taken out of the boat by +the line, and lost. For, when the line is darting out, to be seated +then in the boat, is like being seated in the midst of the manifold +whizzings of a steam-engine in full play, when every flying beam, and +shaft, and wheel, is grazing you. It is worse; for you cannot sit +motionless in the heart of these perils, because the boat is rocking +like a cradle, and you are pitched one way and the other, without the +slightest warning; and only by a certain self-adjusting buoyancy and +simultaneousness of volition and action, can you escape being made a +Mazeppa of, and run away with where the all-seeing sun himself could +never pierce you out. + +Again: as the profound calm which only apparently precedes and +prophesies of the storm, is perhaps more awful than the storm itself; +for, indeed, the calm is but the wrapper and envelope of the storm; +and contains it in itself, as the seemingly harmless rifle holds the +fatal powder, and the ball, and the explosion; so the graceful repose +of the line, as it silently serpentines about the oarsmen before +being brought into actual play--this is a thing which carries more of +true terror than any other aspect of this dangerous affair. But why +say more? All men live enveloped in whale-lines. All are born with +halters round their necks; but it is only when caught in the swift, +sudden turn of death, that mortals realize the silent, subtle, +ever-present perils of life. And if you be a philosopher, though +seated in the whale-boat, you would not at heart feel one whit more +of terror, than though seated before your evening fire with a poker, +and not a harpoon, by your side. + + + +CHAPTER 61 + +Stubb Kills a Whale. + + +If to Starbuck the apparition of the Squid was a thing of portents, +to Queequeg it was quite a different object. + +"When you see him 'quid," said the savage, honing his harpoon in the +bow of his hoisted boat, "then you quick see him 'parm whale." + +The next day was exceedingly still and sultry, and with nothing +special to engage them, the Pequod's crew could hardly resist the +spell of sleep induced by such a vacant sea. For this part of the +Indian Ocean through which we then were voyaging is not what whalemen +call a lively ground; that is, it affords fewer glimpses of +porpoises, dolphins, flying-fish, and other vivacious denizens of +more stirring waters, than those off the Rio de la Plata, or the +in-shore ground off Peru. + +It was my turn to stand at the foremast-head; and with my shoulders +leaning against the slackened royal shrouds, to and fro I idly swayed +in what seemed an enchanted air. No resolution could withstand it; +in that dreamy mood losing all consciousness, at last my soul went +out of my body; though my body still continued to sway as a pendulum +will, long after the power which first moved it is withdrawn. + +Ere forgetfulness altogether came over me, I had noticed that the +seamen at the main and mizzen-mast-heads were already drowsy. So +that at last all three of us lifelessly swung from the spars, and for +every swing that we made there was a nod from below from the +slumbering helmsman. The waves, too, nodded their indolent crests; +and across the wide trance of the sea, east nodded to west, and the +sun over all. + +Suddenly bubbles seemed bursting beneath my closed eyes; like vices +my hands grasped the shrouds; some invisible, gracious agency +preserved me; with a shock I came back to life. And lo! close under +our lee, not forty fathoms off, a gigantic Sperm Whale lay rolling in +the water like the capsized hull of a frigate, his broad, glossy +back, of an Ethiopian hue, glistening in the sun's rays like a +mirror. But lazily undulating in the trough of the sea, and ever and +anon tranquilly spouting his vapoury jet, the whale looked like a +portly burgher smoking his pipe of a warm afternoon. But that pipe, +poor whale, was thy last. As if struck by some enchanter's wand, the +sleepy ship and every sleeper in it all at once started into +wakefulness; and more than a score of voices from all parts of the +vessel, simultaneously with the three notes from aloft, shouted forth +the accustomed cry, as the great fish slowly and regularly spouted +the sparkling brine into the air. + +"Clear away the boats! Luff!" cried Ahab. And obeying his own +order, he dashed the helm down before the helmsman could handle the +spokes. + +The sudden exclamations of the crew must have alarmed the whale; and +ere the boats were down, majestically turning, he swam away to the +leeward, but with such a steady tranquillity, and making so few +ripples as he swam, that thinking after all he might not as yet be +alarmed, Ahab gave orders that not an oar should be used, and no man +must speak but in whispers. So seated like Ontario Indians on the +gunwales of the boats, we swiftly but silently paddled along; the +calm not admitting of the noiseless sails being set. Presently, as +we thus glided in chase, the monster perpendicularly flitted his tail +forty feet into the air, and then sank out of sight like a tower +swallowed up. + +"There go flukes!" was the cry, an announcement immediately followed +by Stubb's producing his match and igniting his pipe, for now a +respite was granted. After the full interval of his sounding had +elapsed, the whale rose again, and being now in advance of the +smoker's boat, and much nearer to it than to any of the others, Stubb +counted upon the honour of the capture. It was obvious, now, that the +whale had at length become aware of his pursuers. All silence of +cautiousness was therefore no longer of use. Paddles were dropped, +and oars came loudly into play. And still puffing at his pipe, Stubb +cheered on his crew to the assault. + +Yes, a mighty change had come over the fish. All alive to his +jeopardy, he was going "head out"; that part obliquely projecting +from the mad yeast which he brewed.* + + +*It will be seen in some other place of what a very light substance +the entire interior of the sperm whale's enormous head consists. +Though apparently the most massive, it is by far the most buoyant +part about him. So that with ease he elevates it in the air, and +invariably does so when going at his utmost speed. Besides, such is +the breadth of the upper part of the front of his head, and such the +tapering cut-water formation of the lower part, that by obliquely +elevating his head, he thereby may be said to transform himself from +a bluff-bowed sluggish galliot into a sharppointed New York +pilot-boat. + + +"Start her, start her, my men! Don't hurry yourselves; take plenty +of time--but start her; start her like thunder-claps, that's all," +cried Stubb, spluttering out the smoke as he spoke. "Start her, now; +give 'em the long and strong stroke, Tashtego. Start her, Tash, my +boy--start her, all; but keep cool, keep cool--cucumbers is the +word--easy, easy--only start her like grim death and grinning devils, +and raise the buried dead perpendicular out of their graves, +boys--that's all. Start her!" + +"Woo-hoo! Wa-hee!" screamed the Gay-Header in reply, raising some +old war-whoop to the skies; as every oarsman in the strained boat +involuntarily bounced forward with the one tremendous leading stroke +which the eager Indian gave. + +But his wild screams were answered by others quite as wild. +"Kee-hee! Kee-hee!" yelled Daggoo, straining forwards and backwards +on his seat, like a pacing tiger in his cage. + +"Ka-la! Koo-loo!" howled Queequeg, as if smacking his lips over a +mouthful of Grenadier's steak. And thus with oars and yells the +keels cut the sea. Meanwhile, Stubb retaining his place in the +van, still encouraged his men to the onset, all the while puffing the +smoke from his mouth. Like desperadoes they tugged and they +strained, till the welcome cry was heard--"Stand up, Tashtego!--give +it to him!" The harpoon was hurled. "Stern all!" The oarsmen +backed water; the same moment something went hot and hissing along +every one of their wrists. It was the magical line. An instant +before, Stubb had swiftly caught two additional turns with it round +the loggerhead, whence, by reason of its increased rapid circlings, a +hempen blue smoke now jetted up and mingled with the steady fumes +from his pipe. As the line passed round and round the loggerhead; so +also, just before reaching that point, it blisteringly passed through +and through both of Stubb's hands, from which the hand-cloths, or +squares of quilted canvas sometimes worn at these times, had +accidentally dropped. It was like holding an enemy's sharp two-edged +sword by the blade, and that enemy all the time striving to wrest it +out of your clutch. + +"Wet the line! wet the line!" cried Stubb to the tub oarsman (him +seated by the tub) who, snatching off his hat, dashed sea-water into +it.* More turns were taken, so that the line began holding its place. +The boat now flew through the boiling water like a shark all fins. +Stubb and Tashtego here changed places--stem for stern--a staggering +business truly in that rocking commotion. + + +*Partly to show the indispensableness of this act, it may here be +stated, that, in the old Dutch fishery, a mop was used to dash the +running line with water; in many other ships, a wooden piggin, or +bailer, is set apart for that purpose. Your hat, however, is the +most convenient. + + +From the vibrating line extending the entire length of the upper part +of the boat, and from its now being more tight than a harpstring, you +would have thought the craft had two keels--one cleaving the water, +the other the air--as the boat churned on through both opposing +elements at once. A continual cascade played at the bows; a +ceaseless whirling eddy in her wake; and, at the slightest motion +from within, even but of a little finger, the vibrating, cracking +craft canted over her spasmodic gunwale into the sea. Thus they +rushed; each man with might and main clinging to his seat, to prevent +being tossed to the foam; and the tall form of Tashtego at the +steering oar crouching almost double, in order to bring down his +centre of gravity. Whole Atlantics and Pacifics seemed passed as +they shot on their way, till at length the whale somewhat slackened +his flight. + +"Haul in--haul in!" cried Stubb to the bowsman! and, facing round +towards the whale, all hands began pulling the boat up to him, while +yet the boat was being towed on. Soon ranging up by his flank, +Stubb, firmly planting his knee in the clumsy cleat, darted dart +after dart into the flying fish; at the word of command, the boat +alternately sterning out of the way of the whale's horrible wallow, +and then ranging up for another fling. + +The red tide now poured from all sides of the monster like brooks +down a hill. His tormented body rolled not in brine but in blood, +which bubbled and seethed for furlongs behind in their wake. The +slanting sun playing upon this crimson pond in the sea, sent back +its reflection into every face, so that they all glowed to each other +like red men. And all the while, jet after jet of white smoke was +agonizingly shot from the spiracle of the whale, and vehement puff +after puff from the mouth of the excited headsman; as at every dart, +hauling in upon his crooked lance (by the line attached to it), Stubb +straightened it again and again, by a few rapid blows against the +gunwale, then again and again sent it into the whale. + +"Pull up--pull up!" he now cried to the bowsman, as the waning whale +relaxed in his wrath. "Pull up!--close to!" and the boat ranged +along the fish's flank. When reaching far over the bow, Stubb slowly +churned his long sharp lance into the fish, and kept it there, +carefully churning and churning, as if cautiously seeking to feel +after some gold watch that the whale might have swallowed, and which +he was fearful of breaking ere he could hook it out. But that gold +watch he sought was the innermost life of the fish. And now it is +struck; for, starting from his trance into that unspeakable thing +called his "flurry," the monster horribly wallowed in his blood, +overwrapped himself in impenetrable, mad, boiling spray, so that the +imperilled craft, instantly dropping astern, had much ado blindly to +struggle out from that phrensied twilight into the clear air of the +day. + +And now abating in his flurry, the whale once more rolled out into +view; surging from side to side; spasmodically dilating and +contracting his spout-hole, with sharp, cracking, agonized +respirations. At last, gush after gush of clotted red gore, as if it +had been the purple lees of red wine, shot into the frighted air; and +falling back again, ran dripping down his motionless flanks into +the sea. His heart had burst! + +"He's dead, Mr. Stubb," said Daggoo. + +"Yes; both pipes smoked out!" and withdrawing his own from his mouth, +Stubb scattered the dead ashes over the water; and, for a moment, +stood thoughtfully eyeing the vast corpse he had made. + + + +CHAPTER 62 + +The Dart. + + +A word concerning an incident in the last chapter. + +According to the invariable usage of the fishery, the whale-boat +pushes off from the ship, with the headsman or whale-killer as +temporary steersman, and the harpooneer or whale-fastener pulling the +foremost oar, the one known as the harpooneer-oar. Now it needs a +strong, nervous arm to strike the first iron into the fish; for +often, in what is called a long dart, the heavy implement has to be +flung to the distance of twenty or thirty feet. But however +prolonged and exhausting the chase, the harpooneer is expected to +pull his oar meanwhile to the uttermost; indeed, he is expected to +set an example of superhuman activity to the rest, not only by +incredible rowing, but by repeated loud and intrepid exclamations; +and what it is to keep shouting at the top of one's compass, while +all the other muscles are strained and half started--what that is +none know but those who have tried it. For one, I cannot bawl very +heartily and work very recklessly at one and the same time. In this +straining, bawling state, then, with his back to the fish, all at +once the exhausted harpooneer hears the exciting cry--"Stand up, and +give it to him!" He now has to drop and secure his oar, turn round +on his centre half way, seize his harpoon from the crotch, and with +what little strength may remain, he essays to pitch it somehow into +the whale. No wonder, taking the whole fleet of whalemen in a body, +that out of fifty fair chances for a dart, not five are successful; +no wonder that so many hapless harpooneers are madly cursed and +disrated; no wonder that some of them actually burst their +blood-vessels in the boat; no wonder that some sperm whalemen are +absent four years with four barrels; no wonder that to many ship +owners, whaling is but a losing concern; for it is the harpooneer +that makes the voyage, and if you take the breath out of his body how +can you expect to find it there when most wanted! + +Again, if the dart be successful, then at the second critical +instant, that is, when the whale starts to run, the boatheader and +harpooneer likewise start to running fore and aft, to the imminent +jeopardy of themselves and every one else. It is then they change +places; and the headsman, the chief officer of the little craft, +takes his proper station in the bows of the boat. + +Now, I care not who maintains the contrary, but all this is both +foolish and unnecessary. The headsman should stay in the bows from +first to last; he should both dart the harpoon and the lance, and no +rowing whatever should be expected of him, except under circumstances +obvious to any fisherman. I know that this would sometimes involve a +slight loss of speed in the chase; but long experience in various +whalemen of more than one nation has convinced me that in the vast +majority of failures in the fishery, it has not by any means been so +much the speed of the whale as the before described exhaustion of the +harpooneer that has caused them. + +To insure the greatest efficiency in the dart, the harpooneers of +this world must start to their feet from out of idleness, and not +from out of toil. + + + +CHAPTER 63 + +The Crotch. + + +Out of the trunk, the branches grow; out of them, the twigs. So, in +productive subjects, grow the chapters. + +The crotch alluded to on a previous page deserves independent +mention. It is a notched stick of a peculiar form, some two feet in +length, which is perpendicularly inserted into the starboard gunwale +near the bow, for the purpose of furnishing a rest for the wooden +extremity of the harpoon, whose other naked, barbed end slopingly +projects from the prow. Thereby the weapon is instantly at hand to +its hurler, who snatches it up as readily from its rest as a +backwoodsman swings his rifle from the wall. It is customary to have +two harpoons reposing in the crotch, respectively called the first +and second irons. + +But these two harpoons, each by its own cord, are both connected with +the line; the object being this: to dart them both, if possible, one +instantly after the other into the same whale; so that if, in the +coming drag, one should draw out, the other may still retain a hold. +It is a doubling of the chances. But it very often happens that +owing to the instantaneous, violent, convulsive running of the whale +upon receiving the first iron, it becomes impossible for the +harpooneer, however lightning-like in his movements, to pitch the +second iron into him. Nevertheless, as the second iron is already +connected with the line, and the line is running, hence that weapon +must, at all events, be anticipatingly tossed out of the boat, +somehow and somewhere; else the most terrible jeopardy would involve +all hands. Tumbled into the water, it accordingly is in such cases; +the spare coils of box line (mentioned in a preceding chapter) making +this feat, in most instances, prudently practicable. But this +critical act is not always unattended with the saddest and most fatal +casualties. + +Furthermore: you must know that when the second iron is thrown +overboard, it thenceforth becomes a dangling, sharp-edged terror, +skittishly curvetting about both boat and whale, entangling the +lines, or cutting them, and making a prodigious sensation in all +directions. Nor, in general, is it possible to secure it again until +the whale is fairly captured and a corpse. + +Consider, now, how it must be in the case of four boats all engaging +one unusually strong, active, and knowing whale; when owing to these +qualities in him, as well as to the thousand concurring accidents of +such an audacious enterprise, eight or ten loose second irons may be +simultaneously dangling about him. For, of course, each boat is +supplied with several harpoons to bend on to the line should the +first one be ineffectually darted without recovery. All these +particulars are faithfully narrated here, as they will not fail to +elucidate several most important, however intricate passages, in +scenes hereafter to be painted. + + + +CHAPTER 64 + +Stubb's Supper. + + +Stubb's whale had been killed some distance from the ship. It was a +calm; so, forming a tandem of three boats, we commenced the slow +business of towing the trophy to the Pequod. And now, as we eighteen +men with our thirty-six arms, and one hundred and eighty thumbs and +fingers, slowly toiled hour after hour upon that inert, sluggish +corpse in the sea; and it seemed hardly to budge at all, except at +long intervals; good evidence was hereby furnished of the +enormousness of the mass we moved. For, upon the great canal of +Hang-Ho, or whatever they call it, in China, four or five laborers on +the foot-path will draw a bulky freighted junk at the rate of a mile +an hour; but this grand argosy we towed heavily forged along, as if +laden with pig-lead in bulk. + +Darkness came on; but three lights up and down in the Pequod's +main-rigging dimly guided our way; till drawing nearer we saw Ahab +dropping one of several more lanterns over the bulwarks. Vacantly +eyeing the heaving whale for a moment, he issued the usual orders for +securing it for the night, and then handing his lantern to a seaman, +went his way into the cabin, and did not come forward again until +morning. + +Though, in overseeing the pursuit of this whale, Captain Ahab had +evinced his customary activity, to call it so; yet now that the +creature was dead, some vague dissatisfaction, or impatience, or +despair, seemed working in him; as if the sight of that dead body +reminded him that Moby Dick was yet to be slain; and though a +thousand other whales were brought to his ship, all that would not +one jot advance his grand, monomaniac object. Very soon you would +have thought from the sound on the Pequod's decks, that all hands +were preparing to cast anchor in the deep; for heavy chains are being +dragged along the deck, and thrust rattling out of the port-holes. +But by those clanking links, the vast corpse itself, not the ship, is +to be moored. Tied by the head to the stern, and by the tail to the +bows, the whale now lies with its black hull close to the vessel's +and seen through the darkness of the night, which obscured the spars +and rigging aloft, the two--ship and whale, seemed yoked together +like colossal bullocks, whereof one reclines while the other remains +standing.* + + +*A little item may as well be related here. The strongest and most +reliable hold which the ship has upon the whale when moored +alongside, is by the flukes or tail; and as from its greater density +that part is relatively heavier than any other (excepting the +side-fins), its flexibility even in death, causes it to sink low +beneath the surface; so that with the hand you cannot get at it from +the boat, in order to put the chain round it. But this difficulty is +ingeniously overcome: a small, strong line is prepared with a wooden +float at its outer end, and a weight in its middle, while the other +end is secured to the ship. By adroit management the wooden float is +made to rise on the other side of the mass, so that now having +girdled the whale, the chain is readily made to follow suit; and +being slipped along the body, is at last locked fast round the +smallest part of the tail, at the point of junction with its broad +flukes or lobes. + + +If moody Ahab was now all quiescence, at least so far as could be +known on deck, Stubb, his second mate, flushed with conquest, +betrayed an unusual but still good-natured excitement. Such an +unwonted bustle was he in that the staid Starbuck, his official +superior, quietly resigned to him for the time the sole management of +affairs. One small, helping cause of all this liveliness in Stubb, +was soon made strangely manifest. Stubb was a high liver; he was +somewhat intemperately fond of the whale as a flavorish thing to his +palate. + +"A steak, a steak, ere I sleep! You, Daggoo! overboard you go, and +cut me one from his small!" + +Here be it known, that though these wild fishermen do not, as a +general thing, and according to the great military maxim, make the +enemy defray the current expenses of the war (at least before +realizing the proceeds of the voyage), yet now and then you find some +of these Nantucketers who have a genuine relish for that particular +part of the Sperm Whale designated by Stubb; comprising the tapering +extremity of the body. + +About midnight that steak was cut and cooked; and lighted by two +lanterns of sperm oil, Stubb stoutly stood up to his spermaceti +supper at the capstan-head, as if that capstan were a sideboard. Nor +was Stubb the only banqueter on whale's flesh that night. Mingling +their mumblings with his own mastications, thousands on thousands of +sharks, swarming round the dead leviathan, smackingly feasted on its +fatness. The few sleepers below in their bunks were often startled +by the sharp slapping of their tails against the hull, within a few +inches of the sleepers' hearts. Peering over the side you could just +see them (as before you heard them) wallowing in the sullen, black +waters, and turning over on their backs as they scooped out huge +globular pieces of the whale of the bigness of a human head. This +particular feat of the shark seems all but miraculous. How at such +an apparently unassailable surface, they contrive to gouge out such +symmetrical mouthfuls, remains a part of the universal problem of all +things. The mark they thus leave on the whale, may best be likened +to the hollow made by a carpenter in countersinking for a screw. + +Though amid all the smoking horror and diabolism of a sea-fight, +sharks will be seen longingly gazing up to the ship's decks, like +hungry dogs round a table where red meat is being carved, ready to +bolt down every killed man that is tossed to them; and though, while +the valiant butchers over the deck-table are thus cannibally carving +each other's live meat with carving-knives all gilded and tasselled, +the sharks, also, with their jewel-hilted mouths, are quarrelsomely +carving away under the table at the dead meat; and though, were you +to turn the whole affair upside down, it would still be pretty much +the same thing, that is to say, a shocking sharkish business enough +for all parties; and though sharks also are the invariable outriders +of all slave ships crossing the Atlantic, systematically trotting +alongside, to be handy in case a parcel is to be carried anywhere, or +a dead slave to be decently buried; and though one or two other like +instances might be set down, touching the set terms, places, and +occasions, when sharks do most socially congregate, and most +hilariously feast; yet is there no conceivable time or occasion when +you will find them in such countless numbers, and in gayer or more +jovial spirits, than around a dead sperm whale, moored by night to a +whaleship at sea. If you have never seen that sight, then suspend +your decision about the propriety of devil-worship, and the +expediency of conciliating the devil. + +But, as yet, Stubb heeded not the mumblings of the banquet that was +going on so nigh him, no more than the sharks heeded the smacking of +his own epicurean lips. + +"Cook, cook!--where's that old Fleece?" he cried at length, widening +his legs still further, as if to form a more secure base for his +supper; and, at the same time darting his fork into the dish, as if +stabbing with his lance; "cook, you cook!--sail this way, cook!" + +The old black, not in any very high glee at having been previously +roused from his warm hammock at a most unseasonable hour, came +shambling along from his galley, for, like many old blacks, there was +something the matter with his knee-pans, which he did not keep well +scoured like his other pans; this old Fleece, as they called him, +came shuffling and limping along, assisting his step with his tongs, +which, after a clumsy fashion, were made of straightened iron hoops; +this old Ebony floundered along, and in obedience to the word of +command, came to a dead stop on the opposite side of Stubb's +sideboard; when, with both hands folded before him, and resting on +his two-legged cane, he bowed his arched back still further over, at +the same time sideways inclining his head, so as to bring his best +ear into play. + +"Cook," said Stubb, rapidly lifting a rather reddish morsel to his +mouth, "don't you think this steak is rather overdone? You've been +beating this steak too much, cook; it's too tender. Don't I always +say that to be good, a whale-steak must be tough? There are those +sharks now over the side, don't you see they prefer it tough and +rare? What a shindy they are kicking up! Cook, go and talk to 'em; +tell 'em they are welcome to help themselves civilly, and in +moderation, but they must keep quiet. Blast me, if I can hear my own +voice. Away, cook, and deliver my message. Here, take this +lantern," snatching one from his sideboard; "now then, go and preach +to 'em!" + +Sullenly taking the offered lantern, old Fleece limped across the +deck to the bulwarks; and then, with one hand dropping his light low +over the sea, so as to get a good view of his congregation, with the +other hand he solemnly flourished his tongs, and leaning far over the +side in a mumbling voice began addressing the sharks, while Stubb, +softly crawling behind, overheard all that was said. + +"Fellow-critters: I'se ordered here to say dat you must stop dat dam +noise dare. You hear? Stop dat dam smackin' ob de lips! Massa +Stubb say dat you can fill your dam bellies up to de hatchings, but +by Gor! you must stop dat dam racket!" + +"Cook," here interposed Stubb, accompanying the word with a sudden +slap on the shoulder,--"Cook! why, damn your eyes, you mustn't swear +that way when you're preaching. That's no way to convert sinners, +cook!" + +"Who dat? Den preach to him yourself," sullenly turning to go. + +"No, cook; go on, go on." + +"Well, den, Belubed fellow-critters:"- + +"Right!" exclaimed Stubb, approvingly, "coax 'em to it; try that," +and Fleece continued. + +"Do you is all sharks, and by natur wery woracious, yet I zay to you, +fellow-critters, dat dat woraciousness--'top dat dam slappin' ob de +tail! How you tink to hear, spose you keep up such a dam slappin' +and bitin' dare?" + +"Cook," cried Stubb, collaring him, "I won't have that swearing. +Talk to 'em gentlemanly." + +Once more the sermon proceeded. + +"Your woraciousness, fellow-critters, I don't blame ye so much for; +dat is natur, and can't be helped; but to gobern dat wicked natur, +dat is de pint. You is sharks, sartin; but if you gobern de shark in +you, why den you be angel; for all angel is not'ing more dan de shark +well goberned. Now, look here, bred'ren, just try wonst to be cibil, +a helping yourselbs from dat whale. Don't be tearin' de blubber out +your neighbour's mout, I say. Is not one shark dood right as toder +to dat whale? And, by Gor, none on you has de right to dat whale; +dat whale belong to some one else. I know some o' you has berry brig +mout, brigger dan oders; but den de brig mouts sometimes has de +small bellies; so dat de brigness of de mout is not to swaller wid, +but to bit off de blubber for de small fry ob sharks, dat can't get +into de scrouge to help demselves." + +"Well done, old Fleece!" cried Stubb, "that's Christianity; go on." + +"No use goin' on; de dam willains will keep a scougin' and slappin' +each oder, Massa Stubb; dey don't hear one word; no use a-preaching +to such dam g'uttons as you call 'em, till dare bellies is full, and +dare bellies is bottomless; and when dey do get 'em full, dey wont +hear you den; for den dey sink in the sea, go fast to sleep on de +coral, and can't hear noting at all, no more, for eber and eber." + +"Upon my soul, I am about of the same opinion; so give the +benediction, Fleece, and I'll away to my supper." + +Upon this, Fleece, holding both hands over the fishy mob, raised his +shrill voice, and cried-- + +"Cussed fellow-critters! Kick up de damndest row as ever you can; +fill your dam bellies 'till dey bust--and den die." + +"Now, cook," said Stubb, resuming his supper at the capstan; "stand +just where you stood before, there, over against me, and pay +particular attention." + +"All 'dention," said Fleece, again stooping over upon his tongs in +the desired position. + +"Well," said Stubb, helping himself freely meanwhile; "I shall now go +back to the subject of this steak. In the first place, how old are +you, cook?" + +"What dat do wid de 'teak," said the old black, testily. + +"Silence! How old are you, cook?" + +"'Bout ninety, dey say," he gloomily muttered. + +"And you have lived in this world hard upon one hundred years, cook, +and don't know yet how to cook a whale-steak?" rapidly bolting +another mouthful at the last word, so that morsel seemed a +continuation of the question. "Where were you born, cook?" + +"'Hind de hatchway, in ferry-boat, goin' ober de Roanoke." + +"Born in a ferry-boat! That's queer, too. But I want to know what +country you were born in, cook!" + +"Didn't I say de Roanoke country?" he cried sharply. + +"No, you didn't, cook; but I'll tell you what I'm coming to, cook. +You must go home and be born over again; you don't know how to cook a +whale-steak yet." + +"Bress my soul, if I cook noder one," he growled, angrily, turning +round to depart. + +"Come back here, cook;--here, hand me those tongs;--now take that bit +of steak there, and tell me if you think that steak cooked as it +should be? Take it, I say"--holding the tongs towards him--"take it, +and taste it." + +Faintly smacking his withered lips over it for a moment, the old +negro muttered, "Best cooked 'teak I eber taste; joosy, berry joosy." + +"Cook," said Stubb, squaring himself once more; "do you belong to the +church?" + +"Passed one once in Cape-Down," said the old man sullenly. + +"And you have once in your life passed a holy church in Cape-Town, +where you doubtless overheard a holy parson addressing his hearers as +his beloved fellow-creatures, have you, cook! And yet you come here, +and tell me such a dreadful lie as you did just now, eh?" said Stubb. +"Where do you expect to go to, cook?" + +"Go to bed berry soon," he mumbled, half-turning as he spoke. + +"Avast! heave to! I mean when you die, cook. It's an awful +question. Now what's your answer?" + +"When dis old brack man dies," said the negro slowly, changing his +whole air and demeanor, "he hisself won't go nowhere; but some +bressed angel will come and fetch him." + +"Fetch him? How? In a coach and four, as they fetched Elijah? And +fetch him where?" + +"Up dere," said Fleece, holding his tongs straight over his head, and +keeping it there very solemnly. + +"So, then, you expect to go up into our main-top, do you, cook, when +you are dead? But don't you know the higher you climb, the colder it +gets? Main-top, eh?" + +"Didn't say dat t'all," said Fleece, again in the sulks. + +"You said up there, didn't you? and now look yourself, and see where +your tongs are pointing. But, perhaps you expect to get into heaven +by crawling through the lubber's hole, cook; but, no, no, cook, you +don't get there, except you go the regular way, round by the rigging. +It's a ticklish business, but must be done, or else it's no go. But +none of us are in heaven yet. Drop your tongs, cook, and hear my +orders. Do ye hear? Hold your hat in one hand, and clap t'other +a'top of your heart, when I'm giving my orders, cook. What! that +your heart, there?--that's your gizzard! Aloft! aloft!--that's +it--now you have it. Hold it there now, and pay attention." + +"All 'dention," said the old black, with both hands placed as +desired, vainly wriggling his grizzled head, as if to get both ears +in front at one and the same time. + +"Well then, cook, you see this whale-steak of yours was so very bad, +that I have put it out of sight as soon as possible; you see that, +don't you? Well, for the future, when you cook another whale-steak +for my private table here, the capstan, I'll tell you what to do so +as not to spoil it by overdoing. Hold the steak in one hand, and +show a live coal to it with the other; that done, dish it; d'ye hear? +And now to-morrow, cook, when we are cutting in the fish, be sure +you stand by to get the tips of his fins; have them put in pickle. +As for the ends of the flukes, have them soused, cook. There, now ye +may go." + +But Fleece had hardly got three paces off, when he was recalled. + +"Cook, give me cutlets for supper to-morrow night in the mid-watch. +D'ye hear? away you sail, then.--Halloa! stop! make a bow before you +go.--Avast heaving again! Whale-balls for breakfast--don't forget." + +"Wish, by gor! whale eat him, 'stead of him eat whale. I'm bressed +if he ain't more of shark dan Massa Shark hisself," muttered the old +man, limping away; with which sage ejaculation he went to his +hammock. + + + +CHAPTER 65 + +The Whale as a Dish. + + +That mortal man should feed upon the creature that feeds his lamp, +and, like Stubb, eat him by his own light, as you may say; this seems +so outlandish a thing that one must needs go a little into the +history and philosophy of it. + +It is upon record, that three centuries ago the tongue of the Right +Whale was esteemed a great delicacy in France, and commanded large +prices there. Also, that in Henry VIIIth's time, a certain cook of +the court obtained a handsome reward for inventing an admirable sauce +to be eaten with barbacued porpoises, which, you remember, are a +species of whale. Porpoises, indeed, are to this day considered fine +eating. The meat is made into balls about the size of billiard +balls, and being well seasoned and spiced might be taken for +turtle-balls or veal balls. The old monks of Dunfermline were very +fond of them. They had a great porpoise grant from the crown. + +The fact is, that among his hunters at least, the whale would by all +hands be considered a noble dish, were there not so much of him; but +when you come to sit down before a meat-pie nearly one hundred feet +long, it takes away your appetite. Only the most unprejudiced of men +like Stubb, nowadays partake of cooked whales; but the Esquimaux are +not so fastidious. We all know how they live upon whales, and have +rare old vintages of prime old train oil. Zogranda, one of their +most famous doctors, recommends strips of blubber for infants, as +being exceedingly juicy and nourishing. And this reminds me that +certain Englishmen, who long ago were accidentally left in Greenland +by a whaling vessel--that these men actually lived for several months +on the mouldy scraps of whales which had been left ashore after +trying out the blubber. Among the Dutch whalemen these scraps are +called "fritters"; which, indeed, they greatly resemble, being brown +and crisp, and smelling something like old Amsterdam housewives' +dough-nuts or oly-cooks, when fresh. They have such an eatable look +that the most self-denying stranger can hardly keep his hands off. + +But what further depreciates the whale as a civilized dish, is his +exceeding richness. He is the great prize ox of the sea, too fat to +be delicately good. Look at his hump, which would be as fine eating +as the buffalo's (which is esteemed a rare dish), were it not such a +solid pyramid of fat. But the spermaceti itself, how bland and +creamy that is; like the transparent, half-jellied, white meat of a +cocoanut in the third month of its growth, yet far too rich to supply +a substitute for butter. Nevertheless, many whalemen have a method +of absorbing it into some other substance, and then partaking of it. +In the long try watches of the night it is a common thing for the +seamen to dip their ship-biscuit into the huge oil-pots and let them +fry there awhile. Many a good supper have I thus made. + +In the case of a small Sperm Whale the brains are accounted a fine +dish. The casket of the skull is broken into with an axe, and the +two plump, whitish lobes being withdrawn (precisely resembling two +large puddings), they are then mixed with flour, and cooked into a +most delectable mess, in flavor somewhat resembling calves' head, +which is quite a dish among some epicures; and every one knows that +some young bucks among the epicures, by continually dining upon +calves' brains, by and by get to have a little brains of their own, +so as to be able to tell a calf's head from their own heads; which, +indeed, requires uncommon discrimination. And that is the reason why +a young buck with an intelligent looking calf's head before him, is +somehow one of the saddest sights you can see. The head looks a sort +of reproachfully at him, with an "Et tu Brute!" expression. + +It is not, perhaps, entirely because the whale is so excessively +unctuous that landsmen seem to regard the eating of him with +abhorrence; that appears to result, in some way, from the +consideration before mentioned: i.e. that a man should eat a newly +murdered thing of the sea, and eat it too by its own light. But no +doubt the first man that ever murdered an ox was regarded as a +murderer; perhaps he was hung; and if he had been put on his trial by +oxen, he certainly would have been; and he certainly deserved it if +any murderer does. Go to the meat-market of a Saturday night and see +the crowds of live bipeds staring up at the long rows of dead +quadrupeds. Does not that sight take a tooth out of the cannibal's +jaw? Cannibals? who is not a cannibal? I tell you it will be more +tolerable for the Fejee that salted down a lean missionary in his +cellar against a coming famine; it will be more tolerable for that +provident Fejee, I say, in the day of judgment, than for thee, +civilized and enlightened gourmand, who nailest geese to the ground +and feastest on their bloated livers in thy pate-de-foie-gras. + +But Stubb, he eats the whale by its own light, does he? and that is +adding insult to injury, is it? Look at your knife-handle, there, my +civilized and enlightened gourmand dining off that roast beef, what +is that handle made of?--what but the bones of the brother of the +very ox you are eating? And what do you pick your teeth with, after +devouring that fat goose? With a feather of the same fowl. And with +what quill did the Secretary of the Society for the Suppression of +Cruelty to Ganders formally indite his circulars? It is only within +the last month or two that that society passed a resolution to +patronise nothing but steel pens. + + + +CHAPTER 66 + +The Shark Massacre. + + +When in the Southern Fishery, a captured Sperm Whale, after long and +weary toil, is brought alongside late at night, it is not, as a +general thing at least, customary to proceed at once to the business +of cutting him in. For that business is an exceedingly laborious +one; is not very soon completed; and requires all hands to set about +it. Therefore, the common usage is to take in all sail; lash the +helm a'lee; and then send every one below to his hammock till +daylight, with the reservation that, until that time, anchor-watches +shall be kept; that is, two and two for an hour, each couple, the +crew in rotation shall mount the deck to see that all goes well. + +But sometimes, especially upon the Line in the Pacific, this plan +will not answer at all; because such incalculable hosts of sharks +gather round the moored carcase, that were he left so for six hours, +say, on a stretch, little more than the skeleton would be visible by +morning. In most other parts of the ocean, however, where these fish +do not so largely abound, their wondrous voracity can be at times +considerably diminished, by vigorously stirring them up with sharp +whaling-spades, a procedure notwithstanding, which, in some +instances, only seems to tickle them into still greater activity. +But it was not thus in the present case with the Pequod's sharks; +though, to be sure, any man unaccustomed to such sights, to have +looked over her side that night, would have almost thought the whole +round sea was one huge cheese, and those sharks the maggots in it. + +Nevertheless, upon Stubb setting the anchor-watch after his supper +was concluded; and when, accordingly, Queequeg and a forecastle +seaman came on deck, no small excitement was created among the +sharks; for immediately suspending the cutting stages over the side, +and lowering three lanterns, so that they cast long gleams of light +over the turbid sea, these two mariners, darting their long +whaling-spades, kept up an incessant murdering of the sharks,* by +striking the keen steel deep into their skulls, seemingly their only +vital part. But in the foamy confusion of their mixed and struggling +hosts, the marksmen could not always hit their mark; and this brought +about new revelations of the incredible ferocity of the foe. They +viciously snapped, not only at each other's disembowelments, but like +flexible bows, bent round, and bit their own; till those entrails +seemed swallowed over and over again by the same mouth, to be +oppositely voided by the gaping wound. Nor was this all. It was +unsafe to meddle with the corpses and ghosts of these creatures. A +sort of generic or Pantheistic vitality seemed to lurk in their very +joints and bones, after what might be called the individual life had +departed. Killed and hoisted on deck for the sake of his skin, one +of these sharks almost took poor Queequeg's hand off, when he tried +to shut down the dead lid of his murderous jaw. + + +*The whaling-spade used for cutting-in is made of the very best +steel; is about the bigness of a man's spread hand; and in general +shape, corresponds to the garden implement after which it is named; +only its sides are perfectly flat, and its upper end considerably +narrower than the lower. This weapon is always kept as sharp as +possible; and when being used is occasionally honed, just like a +razor. In its socket, a stiff pole, from twenty to thirty feet long, +is inserted for a handle. + + +"Queequeg no care what god made him shark," said the savage, +agonizingly lifting his hand up and down; "wedder Fejee god or +Nantucket god; but de god wat made shark must be one dam Ingin." + + + +CHAPTER 67 + +Cutting In. + + +It was a Saturday night, and such a Sabbath as followed! Ex officio +professors of Sabbath breaking are all whalemen. The ivory Pequod +was turned into what seemed a shamble; every sailor a butcher. You +would have thought we were offering up ten thousand red oxen to the +sea gods. + +In the first place, the enormous cutting tackles, among other +ponderous things comprising a cluster of blocks generally painted +green, and which no single man can possibly lift--this vast bunch of +grapes was swayed up to the main-top and firmly lashed to the lower +mast-head, the strongest point anywhere above a ship's deck. The end +of the hawser-like rope winding through these intricacies, was then +conducted to the windlass, and the huge lower block of the tackles +was swung over the whale; to this block the great blubber hook, +weighing some one hundred pounds, was attached. And now suspended in +stages over the side, Starbuck and Stubb, the mates, armed with their +long spades, began cutting a hole in the body for the insertion of +the hook just above the nearest of the two side-fins. This done, a +broad, semicircular line is cut round the hole, the hook is inserted, +and the main body of the crew striking up a wild chorus, now commence +heaving in one dense crowd at the windlass. When instantly, the +entire ship careens over on her side; every bolt in her starts like +the nail-heads of an old house in frosty weather; she trembles, +quivers, and nods her frighted mast-heads to the sky. More and more +she leans over to the whale, while every gasping heave of the +windlass is answered by a helping heave from the billows; till at +last, a swift, startling snap is heard; with a great swash the ship +rolls upwards and backwards from the whale, and the triumphant tackle +rises into sight dragging after it the disengaged semicircular end of +the first strip of blubber. Now as the blubber envelopes the whale +precisely as the rind does an orange, so is it stripped off from the +body precisely as an orange is sometimes stripped by spiralizing it. +For the strain constantly kept up by the windlass continually keeps +the whale rolling over and over in the water, and as the blubber in +one strip uniformly peels off along the line called the "scarf," +simultaneously cut by the spades of Starbuck and Stubb, the mates; +and just as fast as it is thus peeled off, and indeed by that very +act itself, it is all the time being hoisted higher and higher aloft +till its upper end grazes the main-top; the men at the windlass then +cease heaving, and for a moment or two the prodigious blood-dripping +mass sways to and fro as if let down from the sky, and every one +present must take good heed to dodge it when it swings, else it may +box his ears and pitch him headlong overboard. + +One of the attending harpooneers now advances with a long, keen +weapon called a boarding-sword, and watching his chance he +dexterously slices out a considerable hole in the lower part of the +swaying mass. Into this hole, the end of the second alternating +great tackle is then hooked so as to retain a hold upon the blubber, +in order to prepare for what follows. Whereupon, this accomplished +swordsman, warning all hands to stand off, once more makes a +scientific dash at the mass, and with a few sidelong, desperate, +lunging slicings, severs it completely in twain; so that while the +short lower part is still fast, the long upper strip, called a +blanket-piece, swings clear, and is all ready for lowering. The +heavers forward now resume their song, and while the one tackle is +peeling and hoisting a second strip from the whale, the other is +slowly slackened away, and down goes the first strip through the main +hatchway right beneath, into an unfurnished parlor called the +blubber-room. Into this twilight apartment sundry nimble hands keep +coiling away the long blanket-piece as if it were a great live mass +of plaited serpents. And thus the work proceeds; the two tackles +hoisting and lowering simultaneously; both whale and windlass +heaving, the heavers singing, the blubber-room gentlemen coiling, the +mates scarfing, the ship straining, and all hands swearing +occasionally, by way of assuaging the general friction. + + + +CHAPTER 68 + +The Blanket. + + +I have given no small attention to that not unvexed subject, the skin +of the whale. I have had controversies about it with experienced +whalemen afloat, and learned naturalists ashore. My original opinion +remains unchanged; but it is only an opinion. + +The question is, what and where is the skin of the whale? Already +you know what his blubber is. That blubber is something of the +consistence of firm, close-grained beef, but tougher, more elastic +and compact, and ranges from eight or ten to twelve and fifteen +inches in thickness. + +Now, however preposterous it may at first seem to talk of any +creature's skin as being of that sort of consistence and thickness, +yet in point of fact these are no arguments against such a +presumption; because you cannot raise any other dense enveloping +layer from the whale's body but that same blubber; and the outermost +enveloping layer of any animal, if reasonably dense, what can that be +but the skin? True, from the unmarred dead body of the whale, you +may scrape off with your hand an infinitely thin, transparent +substance, somewhat resembling the thinnest shreds of isinglass, only +it is almost as flexible and soft as satin; that is, previous to +being dried, when it not only contracts and thickens, but becomes +rather hard and brittle. I have several such dried bits, which I use +for marks in my whale-books. It is transparent, as I said before; +and being laid upon the printed page, I have sometimes pleased myself +with fancying it exerted a magnifying influence. At any rate, it is +pleasant to read about whales through their own spectacles, as you +may say. But what I am driving at here is this. That same +infinitely thin, isinglass substance, which, I admit, invests the +entire body of the whale, is not so much to be regarded as the skin +of the creature, as the skin of the skin, so to speak; for it were +simply ridiculous to say, that the proper skin of the tremendous +whale is thinner and more tender than the skin of a new-born child. +But no more of this. + +Assuming the blubber to be the skin of the whale; then, when this +skin, as in the case of a very large Sperm Whale, will yield the bulk +of one hundred barrels of oil; and, when it is considered that, in +quantity, or rather weight, that oil, in its expressed state, is only +three fourths, and not the entire substance of the coat; some idea +may hence be had of the enormousness of that animated mass, a mere +part of whose mere integument yields such a lake of liquid as that. +Reckoning ten barrels to the ton, you have ten tons for the net +weight of only three quarters of the stuff of the whale's skin. + +In life, the visible surface of the Sperm Whale is not the least +among the many marvels he presents. Almost invariably it is all over +obliquely crossed and re-crossed with numberless straight marks in +thick array, something like those in the finest Italian line +engravings. But these marks do not seem to be impressed upon the +isinglass substance above mentioned, but seem to be seen through it, +as if they were engraved upon the body itself. Nor is this all. In +some instances, to the quick, observant eye, those linear marks, as +in a veritable engraving, but afford the ground for far other +delineations. These are hieroglyphical; that is, if you call those +mysterious cyphers on the walls of pyramids hieroglyphics, then that +is the proper word to use in the present connexion. By my retentive +memory of the hieroglyphics upon one Sperm Whale in particular, I was +much struck with a plate representing the old Indian characters +chiselled on the famous hieroglyphic palisades on the banks of the +Upper Mississippi. Like those mystic rocks, too, the mystic-marked +whale remains undecipherable. This allusion to the Indian rocks +reminds me of another thing. Besides all the other phenomena which +the exterior of the Sperm Whale presents, he not seldom displays the +back, and more especially his flanks, effaced in great part of the +regular linear appearance, by reason of numerous rude scratches, +altogether of an irregular, random aspect. I should say that those +New England rocks on the sea-coast, which Agassiz imagines to bear +the marks of violent scraping contact with vast floating icebergs--I +should say, that those rocks must not a little resemble the Sperm +Whale in this particular. It also seems to me that such scratches in +the whale are probably made by hostile contact with other whales; for +I have most remarked them in the large, full-grown bulls of the +species. + +A word or two more concerning this matter of the skin or blubber of +the whale. It has already been said, that it is stript from him in +long pieces, called blanket-pieces. Like most sea-terms, this one is +very happy and significant. For the whale is indeed wrapt up in his +blubber as in a real blanket or counterpane; or, still better, an +Indian poncho slipt over his head, and skirting his extremity. It is +by reason of this cosy blanketing of his body, that the whale is +enabled to keep himself comfortable in all weathers, in all seas, +times, and tides. What would become of a Greenland whale, say, in +those shuddering, icy seas of the North, if unsupplied with his cosy +surtout? True, other fish are found exceedingly brisk in those +Hyperborean waters; but these, be it observed, are your cold-blooded, +lungless fish, whose very bellies are refrigerators; creatures, that +warm themselves under the lee of an iceberg, as a traveller in winter +would bask before an inn fire; whereas, like man, the whale has lungs +and warm blood. Freeze his blood, and he dies. How wonderful is it +then--except after explanation--that this great monster, to whom +corporeal warmth is as indispensable as it is to man; how wonderful +that he should be found at home, immersed to his lips for life in +those Arctic waters! where, when seamen fall overboard, they are +sometimes found, months afterwards, perpendicularly frozen into the +hearts of fields of ice, as a fly is found glued in amber. But more +surprising is it to know, as has been proved by experiment, that the +blood of a Polar whale is warmer than that of a Borneo negro in +summer. + +It does seem to me, that herein we see the rare virtue of a strong +individual vitality, and the rare virtue of thick walls, and the rare +virtue of interior spaciousness. Oh, man! admire and model thyself +after the whale! Do thou, too, remain warm among ice. Do thou, too, +live in this world without being of it. Be cool at the equator; keep +thy blood fluid at the Pole. Like the great dome of St. Peter's, and +like the great whale, retain, O man! in all seasons a temperature of +thine own. + +But how easy and how hopeless to teach these fine things! Of +erections, how few are domed like St. Peter's! of creatures, how few +vast as the whale! + + + +CHAPTER 69 + +The Funeral. + + +Haul in the chains! Let the carcase go astern! + +The vast tackles have now done their duty. The peeled white body of +the beheaded whale flashes like a marble sepulchre; though changed in +hue, it has not perceptibly lost anything in bulk. It is still +colossal. Slowly it floats more and more away, the water round it +torn and splashed by the insatiate sharks, and the air above vexed +with rapacious flights of screaming fowls, whose beaks are like so +many insulting poniards in the whale. The vast white headless +phantom floats further and further from the ship, and every rod that +it so floats, what seem square roods of sharks and cubic roods of +fowls, augment the murderous din. For hours and hours from the +almost stationary ship that hideous sight is seen. Beneath the +unclouded and mild azure sky, upon the fair face of the pleasant sea, +wafted by the joyous breezes, that great mass of death floats on and +on, till lost in infinite perspectives. + +There's a most doleful and most mocking funeral! The sea-vultures +all in pious mourning, the air-sharks all punctiliously in black or +speckled. In life but few of them would have helped the whale, I +ween, if peradventure he had needed it; but upon the banquet of his +funeral they most piously do pounce. Oh, horrible vultureism of +earth! from which not the mightiest whale is free. + +Nor is this the end. Desecrated as the body is, a vengeful ghost +survives and hovers over it to scare. Espied by some timid +man-of-war or blundering discovery-vessel from afar, when the +distance obscuring the swarming fowls, nevertheless still shows the +white mass floating in the sun, and the white spray heaving high +against it; straightway the whale's unharming corpse, with trembling +fingers is set down in the log--SHOALS, ROCKS, AND BREAKERS +HEREABOUTS: BEWARE! And for years afterwards, perhaps, ships shun +the place; leaping over it as silly sheep leap over a vacuum, because +their leader originally leaped there when a stick was held. There's +your law of precedents; there's your utility of traditions; there's +the story of your obstinate survival of old beliefs never bottomed on +the earth, and now not even hovering in the air! There's orthodoxy! + +Thus, while in life the great whale's body may have been a real +terror to his foes, in his death his ghost becomes a powerless panic +to a world. + +Are you a believer in ghosts, my friend? There are other ghosts than +the Cock-Lane one, and far deeper men than Doctor Johnson who believe +in them. + + + +CHAPTER 70 + +The Sphynx. + + +It should not have been omitted that previous to completely stripping +the body of the leviathan, he was beheaded. Now, the beheading of +the Sperm Whale is a scientific anatomical feat, upon which +experienced whale surgeons very much pride themselves: and not +without reason. + +Consider that the whale has nothing that can properly be called a +neck; on the contrary, where his head and body seem to join, there, +in that very place, is the thickest part of him. Remember, also, +that the surgeon must operate from above, some eight or ten feet +intervening between him and his subject, and that subject almost +hidden in a discoloured, rolling, and oftentimes tumultuous and +bursting sea. Bear in mind, too, that under these untoward +circumstances he has to cut many feet deep in the flesh; and in that +subterraneous manner, without so much as getting one single peep into +the ever-contracting gash thus made, he must skilfully steer clear +of all adjacent, interdicted parts, and exactly divide the spine at a +critical point hard by its insertion into the skull. Do you not +marvel, then, at Stubb's boast, that he demanded but ten minutes to +behead a sperm whale? + +When first severed, the head is dropped astern and held there by a +cable till the body is stripped. That done, if it belong to a small +whale it is hoisted on deck to be deliberately disposed of. But, +with a full grown leviathan this is impossible; for the sperm whale's +head embraces nearly one third of his entire bulk, and completely to +suspend such a burden as that, even by the immense tackles of a +whaler, this were as vain a thing as to attempt weighing a Dutch barn +in jewellers' scales. + +The Pequod's whale being decapitated and the body stripped, the head +was hoisted against the ship's side--about half way out of the sea, +so that it might yet in great part be buoyed up by its native +element. And there with the strained craft steeply leaning over to it, +by reason of the enormous downward drag from the lower mast-head, and +every yard-arm on that side projecting like a crane over the waves; +there, that blood-dripping head hung to the Pequod's waist like the +giant Holofernes's from the girdle of Judith. + +When this last task was accomplished it was noon, and the seamen went +below to their dinner. Silence reigned over the before tumultuous +but now deserted deck. An intense copper calm, like a universal +yellow lotus, was more and more unfolding its noiseless measureless +leaves upon the sea. + +A short space elapsed, and up into this noiselessness came Ahab alone +from his cabin. Taking a few turns on the quarter-deck, he paused to +gaze over the side, then slowly getting into the main-chains he took +Stubb's long spade--still remaining there after the whale's +Decapitation--and striking it into the lower part of the +half-suspended mass, placed its other end crutch-wise under one arm, +and so stood leaning over with eyes attentively fixed on this head. + +It was a black and hooded head; and hanging there in the midst of so +intense a calm, it seemed the Sphynx's in the desert. "Speak, thou +vast and venerable head," muttered Ahab, "which, though ungarnished +with a beard, yet here and there lookest hoary with mosses; speak, +mighty head, and tell us the secret thing that is in thee. Of all +divers, thou hast dived the deepest. That head upon which the upper +sun now gleams, has moved amid this world's foundations. Where +unrecorded names and navies rust, and untold hopes and anchors rot; +where in her murderous hold this frigate earth is ballasted with +bones of millions of the drowned; there, in that awful water-land, +there was thy most familiar home. Thou hast been where bell or diver +never went; hast slept by many a sailor's side, where sleepless +mothers would give their lives to lay them down. Thou saw'st the +locked lovers when leaping from their flaming ship; heart to heart +they sank beneath the exulting wave; true to each other, when heaven +seemed false to them. Thou saw'st the murdered mate when tossed by +pirates from the midnight deck; for hours he fell into the deeper +midnight of the insatiate maw; and his murderers still sailed on +unharmed--while swift lightnings shivered the neighboring ship that +would have borne a righteous husband to outstretched, longing arms. +O head! thou hast seen enough to split the planets and make an +infidel of Abraham, and not one syllable is thine!" + +"Sail ho!" cried a triumphant voice from the main-mast-head. + +"Aye? Well, now, that's cheering," cried Ahab, suddenly erecting +himself, while whole thunder-clouds swept aside from his brow. "That +lively cry upon this deadly calm might almost convert a better +man.--Where away?" + +"Three points on the starboard bow, sir, and bringing down her breeze +to us! + +"Better and better, man. Would now St. Paul would come along that +way, and to my breezelessness bring his breeze! O Nature, and O soul +of man! how far beyond all utterance are your linked analogies! not +the smallest atom stirs or lives on matter, but has its cunning +duplicate in mind." + + + +CHAPTER 71 + +The Jeroboam's Story. + + +Hand in hand, ship and breeze blew on; but the breeze came faster +than the ship, and soon the Pequod began to rock. + +By and by, through the glass the stranger's boats and manned +mast-heads proved her a whale-ship. But as she was so far to +windward, and shooting by, apparently making a passage to some other +ground, the Pequod could not hope to reach her. So the signal was +set to see what response would be made. + +Here be it said, that like the vessels of military marines, the ships +of the American Whale Fleet have each a private signal; all which +signals being collected in a book with the names of the respective +vessels attached, every captain is provided with it. Thereby, the +whale commanders are enabled to recognise each other upon the ocean, +even at considerable distances and with no small facility. + +The Pequod's signal was at last responded to by the stranger's +setting her own; which proved the ship to be the Jeroboam of +Nantucket. Squaring her yards, she bore down, ranged abeam under the +Pequod's lee, and lowered a boat; it soon drew nigh; but, as the +side-ladder was being rigged by Starbuck's order to accommodate the +visiting captain, the stranger in question waved his hand from his +boat's stern in token of that proceeding being entirely unnecessary. +It turned out that the Jeroboam had a malignant epidemic on board, +and that Mayhew, her captain, was fearful of infecting the Pequod's +company. For, though himself and boat's crew remained untainted, and +though his ship was half a rifle-shot off, and an incorruptible sea +and air rolling and flowing between; yet conscientiously adhering to +the timid quarantine of the land, he peremptorily refused to come +into direct contact with the Pequod. + +But this did by no means prevent all communications. Preserving an +interval of some few yards between itself and the ship, the +Jeroboam's boat by the occasional use of its oars contrived to keep +parallel to the Pequod, as she heavily forged through the sea (for by +this time it blew very fresh), with her main-topsail aback; though, +indeed, at times by the sudden onset of a large rolling wave, the +boat would be pushed some way ahead; but would be soon skilfully +brought to her proper bearings again. Subject to this, and other the +like interruptions now and then, a conversation was sustained between +the two parties; but at intervals not without still another +interruption of a very different sort. + +Pulling an oar in the Jeroboam's boat, was a man of a singular +appearance, even in that wild whaling life where individual +notabilities make up all totalities. He was a small, short, youngish +man, sprinkled all over his face with freckles, and wearing redundant +yellow hair. A long-skirted, cabalistically-cut coat of a faded +walnut tinge enveloped him; the overlapping sleeves of which were +rolled up on his wrists. A deep, settled, fanatic delirium was in +his eyes. + +So soon as this figure had been first descried, Stubb had +exclaimed--"That's he! that's he!--the long-togged scaramouch the +Town-Ho's company told us of!" Stubb here alluded to a strange story +told of the Jeroboam, and a certain man among her crew, some time +previous when the Pequod spoke the Town-Ho. According to this +account and what was subsequently learned, it seemed that the +scaramouch in question had gained a wonderful ascendency over almost +everybody in the Jeroboam. His story was this: + +He had been originally nurtured among the crazy society of Neskyeuna +Shakers, where he had been a great prophet; in their cracked, secret +meetings having several times descended from heaven by the way of a +trap-door, announcing the speedy opening of the seventh vial, which +he carried in his vest-pocket; but, which, instead of containing +gunpowder, was supposed to be charged with laudanum. A strange, +apostolic whim having seized him, he had left Neskyeuna for +Nantucket, where, with that cunning peculiar to craziness, he assumed +a steady, common-sense exterior, and offered himself as a green-hand +candidate for the Jeroboam's whaling voyage. They engaged him; but +straightway upon the ship's getting out of sight of land, his +insanity broke out in a freshet. He announced himself as the +archangel Gabriel, and commanded the captain to jump overboard. He +published his manifesto, whereby he set himself forth as the +deliverer of the isles of the sea and vicar-general of all Oceanica. +The unflinching earnestness with which he declared these things;--the +dark, daring play of his sleepless, excited imagination, and all the +preternatural terrors of real delirium, united to invest this Gabriel +in the minds of the majority of the ignorant crew, with an atmosphere +of sacredness. Moreover, they were afraid of him. As such a man, +however, was not of much practical use in the ship, especially as he +refused to work except when he pleased, the incredulous captain would +fain have been rid of him; but apprised that that individual's +intention was to land him in the first convenient port, the archangel +forthwith opened all his seals and vials--devoting the ship and all +hands to unconditional perdition, in case this intention was carried +out. So strongly did he work upon his disciples among the crew, that +at last in a body they went to the captain and told him if Gabriel +was sent from the ship, not a man of them would remain. He was +therefore forced to relinquish his plan. Nor would they permit +Gabriel to be any way maltreated, say or do what he would; so that it +came to pass that Gabriel had the complete freedom of the ship. The +consequence of all this was, that the archangel cared little or +nothing for the captain and mates; and since the epidemic had broken +out, he carried a higher hand than ever; declaring that the plague, +as he called it, was at his sole command; nor should it be stayed but +according to his good pleasure. The sailors, mostly poor devils, +cringed, and some of them fawned before him; in obedience to his +instructions, sometimes rendering him personal homage, as to a god. +Such things may seem incredible; but, however wondrous, they are +true. Nor is the history of fanatics half so striking in respect to +the measureless self-deception of the fanatic himself, as his +measureless power of deceiving and bedevilling so many others. But +it is time to return to the Pequod. + +"I fear not thy epidemic, man," said Ahab from the bulwarks, to +Captain Mayhew, who stood in the boat's stern; "come on board." + +But now Gabriel started to his feet. + +"Think, think of the fevers, yellow and bilious! Beware of the +horrible plague!" + +"Gabriel! Gabriel!" cried Captain Mayhew; "thou must either--" But +that instant a headlong wave shot the boat far ahead, and its +seethings drowned all speech. + +"Hast thou seen the White Whale?" demanded Ahab, when the boat +drifted back. + +"Think, think of thy whale-boat, stoven and sunk! Beware of the +horrible tail!" + +"I tell thee again, Gabriel, that--" But again the boat tore ahead +as if dragged by fiends. Nothing was said for some moments, while a +succession of riotous waves rolled by, which by one of those +occasional caprices of the seas were tumbling, not heaving it. +Meantime, the hoisted sperm whale's head jogged about very violently, +and Gabriel was seen eyeing it with rather more apprehensiveness than +his archangel nature seemed to warrant. + +When this interlude was over, Captain Mayhew began a dark story +concerning Moby Dick; not, however, without frequent interruptions +from Gabriel, whenever his name was mentioned, and the crazy sea that +seemed leagued with him. + +It seemed that the Jeroboam had not long left home, when upon +speaking a whale-ship, her people were reliably apprised of the +existence of Moby Dick, and the havoc he had made. Greedily sucking +in this intelligence, Gabriel solemnly warned the captain against +attacking the White Whale, in case the monster should be seen; in his +gibbering insanity, pronouncing the White Whale to be no less a being +than the Shaker God incarnated; the Shakers receiving the Bible. But +when, some year or two afterwards, Moby Dick was fairly sighted from +the mast-heads, Macey, the chief mate, burned with ardour to encounter +him; and the captain himself being not unwilling to let him have the +opportunity, despite all the archangel's denunciations and +forewarnings, Macey succeeded in persuading five men to man his boat. +With them he pushed off; and, after much weary pulling, and many +perilous, unsuccessful onsets, he at last succeeded in getting one +iron fast. Meantime, Gabriel, ascending to the main-royal mast-head, +was tossing one arm in frantic gestures, and hurling forth prophecies +of speedy doom to the sacrilegious assailants of his divinity. Now, +while Macey, the mate, was standing up in his boat's bow, and with +all the reckless energy of his tribe was venting his wild +exclamations upon the whale, and essaying to get a fair chance for +his poised lance, lo! a broad white shadow rose from the sea; by its +quick, fanning motion, temporarily taking the breath out of the +bodies of the oarsmen. Next instant, the luckless mate, so full of +furious life, was smitten bodily into the air, and making a long arc +in his descent, fell into the sea at the distance of about fifty +yards. Not a chip of the boat was harmed, nor a hair of any +oarsman's head; but the mate for ever sank. + +It is well to parenthesize here, that of the fatal accidents in the +Sperm-Whale Fishery, this kind is perhaps almost as frequent as any. +Sometimes, nothing is injured but the man who is thus annihilated; +oftener the boat's bow is knocked off, or the thigh-board, in which +the headsman stands, is torn from its place and accompanies the body. +But strangest of all is the circumstance, that in more instances +than one, when the body has been recovered, not a single mark of +violence is discernible; the man being stark dead. + +The whole calamity, with the falling form of Macey, was plainly +descried from the ship. Raising a piercing shriek--"The vial! the +vial!" Gabriel called off the terror-stricken crew from the further +hunting of the whale. This terrible event clothed the archangel with +added influence; because his credulous disciples believed that he had +specifically fore-announced it, instead of only making a general +prophecy, which any one might have done, and so have chanced to hit +one of many marks in the wide margin allowed. He became a nameless +terror to the ship. + +Mayhew having concluded his narration, Ahab put such questions to +him, that the stranger captain could not forbear inquiring whether he +intended to hunt the White Whale, if opportunity should offer. To +which Ahab answered--"Aye." Straightway, then, Gabriel once more +started to his feet, glaring upon the old man, and vehemently +exclaimed, with downward pointed finger--"Think, think of the +blasphemer--dead, and down there!--beware of the blasphemer's end!" + +Ahab stolidly turned aside; then said to Mayhew, "Captain, I have +just bethought me of my letter-bag; there is a letter for one of thy +officers, if I mistake not. Starbuck, look over the bag." + +Every whale-ship takes out a goodly number of letters for various +ships, whose delivery to the persons to whom they may be addressed, +depends upon the mere chance of encountering them in the four oceans. +Thus, most letters never reach their mark; and many are only +received after attaining an age of two or three years or more. + +Soon Starbuck returned with a letter in his hand. It was sorely +tumbled, damp, and covered with a dull, spotted, green mould, in +consequence of being kept in a dark locker of the cabin. Of such a +letter, Death himself might well have been the post-boy. + +"Can'st not read it?" cried Ahab. "Give it me, man. Aye, aye, it's +but a dim scrawl;--what's this?" As he was studying it out, Starbuck +took a long cutting-spade pole, and with his knife slightly split the +end, to insert the letter there, and in that way, hand it to the +boat, without its coming any closer to the ship. + +Meantime, Ahab holding the letter, muttered, "Mr. Har--yes, Mr. +Harry--(a woman's pinny hand,--the man's wife, I'll wager)--Aye--Mr. +Harry Macey, Ship Jeroboam;--why it's Macey, and he's dead!" + +"Poor fellow! poor fellow! and from his wife," sighed Mayhew; "but +let me have it." + +"Nay, keep it thyself," cried Gabriel to Ahab; "thou art soon going +that way." + +"Curses throttle thee!" yelled Ahab. "Captain Mayhew, stand by now +to receive it"; and taking the fatal missive from Starbuck's hands, +he caught it in the slit of the pole, and reached it over towards the +boat. But as he did so, the oarsmen expectantly desisted from +rowing; the boat drifted a little towards the ship's stern; so that, +as if by magic, the letter suddenly ranged along with Gabriel's eager +hand. He clutched it in an instant, seized the boat-knife, and +impaling the letter on it, sent it thus loaded back into the ship. +It fell at Ahab's feet. Then Gabriel shrieked out to his comrades to +give way with their oars, and in that manner the mutinous boat +rapidly shot away from the Pequod. + +As, after this interlude, the seamen resumed their work upon the +jacket of the whale, many strange things were hinted in reference to +this wild affair. + + + +CHAPTER 72 + +The Monkey-Rope. + + +In the tumultuous business of cutting-in and attending to a whale, +there is much running backwards and forwards among the crew. Now +hands are wanted here, and then again hands are wanted there. There +is no staying in any one place; for at one and the same time +everything has to be done everywhere. It is much the same with him +who endeavors the description of the scene. We must now retrace our +way a little. It was mentioned that upon first breaking ground in +the whale's back, the blubber-hook was inserted into the original +hole there cut by the spades of the mates. But how did so clumsy and +weighty a mass as that same hook get fixed in that hole? It was +inserted there by my particular friend Queequeg, whose duty it was, +as harpooneer, to descend upon the monster's back for the special +purpose referred to. But in very many cases, circumstances require +that the harpooneer shall remain on the whale till the whole tensing +or stripping operation is concluded. The whale, be it observed, lies +almost entirely submerged, excepting the immediate parts operated +upon. So down there, some ten feet below the level of the deck, the +poor harpooneer flounders about, half on the whale and half in the +water, as the vast mass revolves like a tread-mill beneath him. On +the occasion in question, Queequeg figured in the Highland costume--a +shirt and socks--in which to my eyes, at least, he appeared to +uncommon advantage; and no one had a better chance to observe him, as +will presently be seen. + +Being the savage's bowsman, that is, the person who pulled the +bow-oar in his boat (the second one from forward), it was my cheerful +duty to attend upon him while taking that hard-scrabble scramble upon +the dead whale's back. You have seen Italian organ-boys holding a +dancing-ape by a long cord. Just so, from the ship's steep side, did +I hold Queequeg down there in the sea, by what is technically called +in the fishery a monkey-rope, attached to a strong strip of canvas +belted round his waist. + +It was a humorously perilous business for both of us. For, before we +proceed further, it must be said that the monkey-rope was fast at +both ends; fast to Queequeg's broad canvas belt, and fast to my +narrow leather one. So that for better or for worse, we two, for the +time, were wedded; and should poor Queequeg sink to rise no more, +then both usage and honour demanded, that instead of cutting the cord, +it should drag me down in his wake. So, then, an elongated Siamese +ligature united us. Queequeg was my own inseparable twin brother; +nor could I any way get rid of the dangerous liabilities which the +hempen bond entailed. + +So strongly and metaphysically did I conceive of my situation then, +that while earnestly watching his motions, I seemed distinctly to +perceive that my own individuality was now merged in a joint stock +company of two; that my free will had received a mortal wound; and +that another's mistake or misfortune might plunge innocent me into +unmerited disaster and death. Therefore, I saw that here was a sort +of interregnum in Providence; for its even-handed equity never could +have so gross an injustice. And yet still further pondering--while I +jerked him now and then from between the whale and ship, which would +threaten to jam him--still further pondering, I say, I saw that this +situation of mine was the precise situation of every mortal that +breathes; only, in most cases, he, one way or other, has this Siamese +connexion with a plurality of other mortals. If your banker breaks, +you snap; if your apothecary by mistake sends you poison in your +pills, you die. True, you may say that, by exceeding caution, you +may possibly escape these and the multitudinous other evil chances of +life. But handle Queequeg's monkey-rope heedfully as I would, +sometimes he jerked it so, that I came very near sliding overboard. +Nor could I possibly forget that, do what I would, I only had the +management of one end of it.* + + +*The monkey-rope is found in all whalers; but it was only in the +Pequod that the monkey and his holder were ever tied together. This +improvement upon the original usage was introduced by no less a man +than Stubb, in order to afford the imperilled harpooneer the strongest +possible guarantee for the faithfulness and vigilance of his +monkey-rope holder. + + +I have hinted that I would often jerk poor Queequeg from between the +whale and the ship--where he would occasionally fall, from the +incessant rolling and swaying of both. But this was not the only +jamming jeopardy he was exposed to. Unappalled by the massacre made +upon them during the night, the sharks now freshly and more keenly +allured by the before pent blood which began to flow from the +carcass--the rabid creatures swarmed round it like bees in a beehive. + +And right in among those sharks was Queequeg; who often pushed them +aside with his floundering feet. A thing altogether incredible were +it not that attracted by such prey as a dead whale, the otherwise +miscellaneously carnivorous shark will seldom touch a man. + +Nevertheless, it may well be believed that since they have such a +ravenous finger in the pie, it is deemed but wise to look sharp to +them. Accordingly, besides the monkey-rope, with which I now and +then jerked the poor fellow from too close a vicinity to the maw of +what seemed a peculiarly ferocious shark--he was provided with still +another protection. Suspended over the side in one of the stages, +Tashtego and Daggoo continually flourished over his head a couple of +keen whale-spades, wherewith they slaughtered as many sharks as they +could reach. This procedure of theirs, to be sure, was very +disinterested and benevolent of them. They meant Queequeg's best +happiness, I admit; but in their hasty zeal to befriend him, and from +the circumstance that both he and the sharks were at times half +hidden by the blood-muddled water, those indiscreet spades of theirs +would come nearer amputating a leg than a tall. But poor Queequeg, I +suppose, straining and gasping there with that great iron hook--poor +Queequeg, I suppose, only prayed to his Yojo, and gave up his life +into the hands of his gods. + +Well, well, my dear comrade and twin-brother, thought I, as I drew in +and then slacked off the rope to every swell of the sea--what matters +it, after all? Are you not the precious image of each and all of us +men in this whaling world? That unsounded ocean you gasp in, is +Life; those sharks, your foes; those spades, your friends; and what +between sharks and spades you are in a sad pickle and peril, poor +lad. + +But courage! there is good cheer in store for you, Queequeg. For +now, as with blue lips and blood-shot eyes the exhausted savage at +last climbs up the chains and stands all dripping and involuntarily +trembling over the side; the steward advances, and with a benevolent, +consolatory glance hands him--what? Some hot Cognac? No! hands him, +ye gods! hands him a cup of tepid ginger and water! + +"Ginger? Do I smell ginger?" suspiciously asked Stubb, coming near. +"Yes, this must be ginger," peering into the as yet untasted cup. +Then standing as if incredulous for a while, he calmly walked towards +the astonished steward slowly saying, "Ginger? ginger? and will you +have the goodness to tell me, Mr. Dough-Boy, where lies the virtue of +ginger? Ginger! is ginger the sort of fuel you use, Dough-boy, to +kindle a fire in this shivering cannibal? Ginger!--what the devil is +ginger?--sea-coal? firewood?--lucifer +matches?--tinder?--gunpowder?--what the devil is ginger, I say, that +you offer this cup to our poor Queequeg here." + +"There is some sneaking Temperance Society movement about this +business," he suddenly added, now approaching Starbuck, who had just +come from forward. "Will you look at that kannakin, sir; smell of +it, if you please." Then watching the mate's countenance, he added, +"The steward, Mr. Starbuck, had the face to offer that calomel and +jalap to Queequeg, there, this instant off the whale. Is the steward +an apothecary, sir? and may I ask whether this is the sort of bitters +by which he blows back the life into a half-drowned man?" + +"I trust not," said Starbuck, "it is poor stuff enough." + +"Aye, aye, steward," cried Stubb, "we'll teach you to drug it +harpooneer; none of your apothecary's medicine here; you want to +poison us, do ye? You have got out insurances on our lives and want +to murder us all, and pocket the proceeds, do ye?" + +"It was not me," cried Dough-Boy, "it was Aunt Charity that brought +the ginger on board; and bade me never give the harpooneers any +spirits, but only this ginger-jub--so she called it." + +"Ginger-jub! you gingerly rascal! take that! and run along with ye to +the lockers, and get something better. I hope I do no wrong, Mr. +Starbuck. It is the captain's orders--grog for the harpooneer on a +whale." + +"Enough," replied Starbuck, "only don't hit him again, but--" + +"Oh, I never hurt when I hit, except when I hit a whale or something +of that sort; and this fellow's a weazel. What were you about +saying, sir?" + +"Only this: go down with him, and get what thou wantest thyself." + +When Stubb reappeared, he came with a dark flask in one hand, and a +sort of tea-caddy in the other. The first contained strong spirits, +and was handed to Queequeg; the second was Aunt Charity's gift, and +that was freely given to the waves. + + + +CHAPTER 73 + +Stubb and Flask Kill a Right Whale; and Then Have a Talk Over Him. + + +It must be borne in mind that all this time we have a Sperm Whale's +prodigious head hanging to the Pequod's side. But we must let it +continue hanging there a while till we can get a chance to attend to +it. For the present other matters press, and the best we can do now +for the head, is to pray heaven the tackles may hold. + +Now, during the past night and forenoon, the Pequod had gradually +drifted into a sea, which, by its occasional patches of yellow brit, +gave unusual tokens of the vicinity of Right Whales, a species of the +Leviathan that but few supposed to be at this particular time lurking +anywhere near. And though all hands commonly disdained the capture +of those inferior creatures; and though the Pequod was not +commissioned to cruise for them at all, and though she had passed +numbers of them near the Crozetts without lowering a boat; yet now +that a Sperm Whale had been brought alongside and beheaded, to the +surprise of all, the announcement was made that a Right Whale should +be captured that day, if opportunity offered. + +Nor was this long wanting. Tall spouts were seen to leeward; and two +boats, Stubb's and Flask's, were detached in pursuit. Pulling +further and further away, they at last became almost invisible to the +men at the mast-head. But suddenly in the distance, they saw a great +heap of tumultuous white water, and soon after news came from aloft +that one or both the boats must be fast. An interval passed and the +boats were in plain sight, in the act of being dragged right towards +the ship by the towing whale. So close did the monster come to the +hull, that at first it seemed as if he meant it malice; but suddenly +going down in a maelstrom, within three rods of the planks, he wholly +disappeared from view, as if diving under the keel. "Cut, cut!" was +the cry from the ship to the boats, which, for one instant, seemed on +the point of being brought with a deadly dash against the vessel's +side. But having plenty of line yet in the tubs, and the whale not +sounding very rapidly, they paid out abundance of rope, and at the +same time pulled with all their might so as to get ahead of the ship. +For a few minutes the struggle was intensely critical; for while +they still slacked out the tightened line in one direction, and still +plied their oars in another, the contending strain threatened to take +them under. But it was only a few feet advance they sought to gain. +And they stuck to it till they did gain it; when instantly, a swift +tremor was felt running like lightning along the keel, as the +strained line, scraping beneath the ship, suddenly rose to view under +her bows, snapping and quivering; and so flinging off its drippings, +that the drops fell like bits of broken glass on the water, while the +whale beyond also rose to sight, and once more the boats were free to +fly. But the fagged whale abated his speed, and blindly altering his +course, went round the stern of the ship towing the two boats after +him, so that they performed a complete circuit. + +Meantime, they hauled more and more upon their lines, till close +flanking him on both sides, Stubb answered Flask with lance for +lance; and thus round and round the Pequod the battle went, while the +multitudes of sharks that had before swum round the Sperm Whale's +body, rushed to the fresh blood that was spilled, thirstily drinking +at every new gash, as the eager Israelites did at the new bursting +fountains that poured from the smitten rock. + +At last his spout grew thick, and with a frightful roll and vomit, he +turned upon his back a corpse. + +While the two headsmen were engaged in making fast cords to his +flukes, and in other ways getting the mass in readiness for towing, +some conversation ensued between them. + +"I wonder what the old man wants with this lump of foul lard," said +Stubb, not without some disgust at the thought of having to do with +so ignoble a leviathan. + +"Wants with it?" said Flask, coiling some spare line in the boat's +bow, "did you never hear that the ship which but once has a Sperm +Whale's head hoisted on her starboard side, and at the same time a +Right Whale's on the larboard; did you never hear, Stubb, that that +ship can never afterwards capsize?" + +"Why not? + +"I don't know, but I heard that gamboge ghost of a Fedallah saying +so, and he seems to know all about ships' charms. But I sometimes +think he'll charm the ship to no good at last. I don't half like +that chap, Stubb. Did you ever notice how that tusk of his is a sort +of carved into a snake's head, Stubb?" + +"Sink him! I never look at him at all; but if ever I get a chance of +a dark night, and he standing hard by the bulwarks, and no one by; +look down there, Flask"--pointing into the sea with a peculiar motion +of both hands--"Aye, will I! Flask, I take that Fedallah to be the +devil in disguise. Do you believe that cock and bull story about his +having been stowed away on board ship? He's the devil, I say. The +reason why you don't see his tail, is because he tucks it up out of +sight; he carries it coiled away in his pocket, I guess. Blast him! +now that I think of it, he's always wanting oakum to stuff into the +toes of his boots." + +"He sleeps in his boots, don't he? He hasn't got any hammock; but +I've seen him lay of nights in a coil of rigging." + +"No doubt, and it's because of his cursed tail; he coils it down, do +ye see, in the eye of the rigging." + +"What's the old man have so much to do with him for?" + +"Striking up a swap or a bargain, I suppose." + +"Bargain?--about what?" + +"Why, do ye see, the old man is hard bent after that White Whale, and +the devil there is trying to come round him, and get him to swap away +his silver watch, or his soul, or something of that sort, and then +he'll surrender Moby Dick." + +"Pooh! Stubb, you are skylarking; how can Fedallah do that?" + +"I don't know, Flask, but the devil is a curious chap, and a wicked +one, I tell ye. Why, they say as how he went a sauntering into the +old flag-ship once, switching his tail about devilish easy and +gentlemanlike, and inquiring if the old governor was at home. Well, +he was at home, and asked the devil what he wanted. The devil, +switching his hoofs, up and says, 'I want John.' 'What for?' says +the old governor. 'What business is that of yours,' says the devil, +getting mad,--'I want to use him.' 'Take him,' says the +governor--and by the Lord, Flask, if the devil didn't give John the +Asiatic cholera before he got through with him, I'll eat this whale +in one mouthful. But look sharp--ain't you all ready there? Well, +then, pull ahead, and let's get the whale alongside." + +"I think I remember some such story as you were telling," said Flask, +when at last the two boats were slowly advancing with their burden +towards the ship, "but I can't remember where." + +"Three Spaniards? Adventures of those three bloody-minded soladoes? +Did ye read it there, Flask? I guess ye did?" + +"No: never saw such a book; heard of it, though. But now, tell me, +Stubb, do you suppose that that devil you was speaking of just now, +was the same you say is now on board the Pequod?" + +"Am I the same man that helped kill this whale? Doesn't the devil +live for ever; who ever heard that the devil was dead? Did you ever +see any parson a wearing mourning for the devil? And if the devil +has a latch-key to get into the admiral's cabin, don't you suppose he +can crawl into a porthole? Tell me that, Mr. Flask?" + +"How old do you suppose Fedallah is, Stubb?" + +"Do you see that mainmast there?" pointing to the ship; "well, that's +the figure one; now take all the hoops in the Pequod's hold, and +string along in a row with that mast, for oughts, do you see; well, +that wouldn't begin to be Fedallah's age. Nor all the coopers in +creation couldn't show hoops enough to make oughts enough." + +"But see here, Stubb, I thought you a little boasted just now, that +you meant to give Fedallah a sea-toss, if you got a good chance. +Now, if he's so old as all those hoops of yours come to, and if he is +going to live for ever, what good will it do to pitch him +overboard--tell me that? + +"Give him a good ducking, anyhow." + +"But he'd crawl back." + +"Duck him again; and keep ducking him." + +"Suppose he should take it into his head to duck you, though--yes, +and drown you--what then?" + +"I should like to see him try it; I'd give him such a pair of black +eyes that he wouldn't dare to show his face in the admiral's cabin +again for a long while, let alone down in the orlop there, where he +lives, and hereabouts on the upper decks where he sneaks so much. +Damn the devil, Flask; so you suppose I'm afraid of the devil? Who's +afraid of him, except the old governor who daresn't catch him and put +him in double-darbies, as he deserves, but lets him go about +kidnapping people; aye, and signed a bond with him, that all the +people the devil kidnapped, he'd roast for him? There's a governor!" + +"Do you suppose Fedallah wants to kidnap Captain Ahab?" + +"Do I suppose it? You'll know it before long, Flask. But I am going +now to keep a sharp look-out on him; and if I see anything very +suspicious going on, I'll just take him by the nape of his neck, and +say--Look here, Beelzebub, you don't do it; and if he makes any fuss, +by the Lord I'll make a grab into his pocket for his tail, take it to +the capstan, and give him such a wrenching and heaving, that his tail +will come short off at the stump--do you see; and then, I rather +guess when he finds himself docked in that queer fashion, he'll sneak +off without the poor satisfaction of feeling his tail between his +legs." + +"And what will you do with the tail, Stubb?" + +"Do with it? Sell it for an ox whip when we get home;--what else?" + +"Now, do you mean what you say, and have been saying all along, +Stubb?" + +"Mean or not mean, here we are at the ship." + +The boats were here hailed, to tow the whale on the larboard side, +where fluke chains and other necessaries were already prepared for +securing him. + +"Didn't I tell you so?" said Flask; "yes, you'll soon see this right +whale's head hoisted up opposite that parmacetti's." + +In good time, Flask's saying proved true. As before, the Pequod +steeply leaned over towards the sperm whale's head, now, by the +counterpoise of both heads, she regained her even keel; though sorely +strained, you may well believe. So, when on one side you hoist in +Locke's head, you go over that way; but now, on the other side, hoist +in Kant's and you come back again; but in very poor plight. Thus, +some minds for ever keep trimming boat. Oh, ye foolish! throw all +these thunder-heads overboard, and then you will float light and +right. + +In disposing of the body of a right whale, when brought alongside the +ship, the same preliminary proceedings commonly take place as in the +case of a sperm whale; only, in the latter instance, the head is cut +off whole, but in the former the lips and tongue are separately +removed and hoisted on deck, with all the well known black bone +attached to what is called the crown-piece. But nothing like this, +in the present case, had been done. The carcases of both whales had +dropped astern; and the head-laden ship not a little resembled a mule +carrying a pair of overburdening panniers. + +Meantime, Fedallah was calmly eyeing the right whale's head, and ever +and anon glancing from the deep wrinkles there to the lines in his +own hand. And Ahab chanced so to stand, that the Parsee occupied his +shadow; while, if the Parsee's shadow was there at all it seemed only +to blend with, and lengthen Ahab's. As the crew toiled on, +Laplandish speculations were bandied among them, concerning all these +passing things. + + + +CHAPTER 74 + +The Sperm Whale's Head--Contrasted View. + + +Here, now, are two great whales, laying their heads together; let us +join them, and lay together our own. + +Of the grand order of folio leviathans, the Sperm Whale and the Right +Whale are by far the most noteworthy. They are the only whales +regularly hunted by man. To the Nantucketer, they present the two +extremes of all the known varieties of the whale. As the external +difference between them is mainly observable in their heads; and as a +head of each is this moment hanging from the Pequod's side; and as we +may freely go from one to the other, by merely stepping across the +deck:--where, I should like to know, will you obtain a better chance +to study practical cetology than here? + +In the first place, you are struck by the general contrast between +these heads. Both are massive enough in all conscience; but there +is a certain mathematical symmetry in the Sperm Whale's which the +Right Whale's sadly lacks. There is more character in the Sperm +Whale's head. As you behold it, you involuntarily yield the immense +superiority to him, in point of pervading dignity. In the present +instance, too, this dignity is heightened by the pepper and salt +colour of his head at the summit, giving token of advanced age and +large experience. In short, he is what the fishermen technically +call a "grey-headed whale." + +Let us now note what is least dissimilar in these heads--namely, the +two most important organs, the eye and the ear. Far back on the side +of the head, and low down, near the angle of either whale's jaw, if +you narrowly search, you will at last see a lashless eye, which you +would fancy to be a young colt's eye; so out of all proportion is it +to the magnitude of the head. + +Now, from this peculiar sideway position of the whale's eyes, it is +plain that he can never see an object which is exactly ahead, no more +than he can one exactly astern. In a word, the position of the +whale's eyes corresponds to that of a man's ears; and you may fancy, +for yourself, how it would fare with you, did you sideways survey +objects through your ears. You would find that you could only +command some thirty degrees of vision in advance of the straight +side-line of sight; and about thirty more behind it. If your +bitterest foe were walking straight towards you, with dagger uplifted +in broad day, you would not be able to see him, any more than if he +were stealing upon you from behind. In a word, you would have two +backs, so to speak; but, at the same time, also, two fronts (side +fronts): for what is it that makes the front of a man--what, indeed, +but his eyes? + +Moreover, while in most other animals that I can now think of, the +eyes are so planted as imperceptibly to blend their visual power, so +as to produce one picture and not two to the brain; the peculiar +position of the whale's eyes, effectually divided as they are by many +cubic feet of solid head, which towers between them like a great +mountain separating two lakes in valleys; this, of course, must +wholly separate the impressions which each independent organ imparts. +The whale, therefore, must see one distinct picture on this side, +and another distinct picture on that side; while all between must be +profound darkness and nothingness to him. Man may, in effect, be +said to look out on the world from a sentry-box with two joined +sashes for his window. But with the whale, these two sashes are +separately inserted, making two distinct windows, but sadly impairing +the view. This peculiarity of the whale's eyes is a thing always to +be borne in mind in the fishery; and to be remembered by the reader +in some subsequent scenes. + +A curious and most puzzling question might be started concerning this +visual matter as touching the Leviathan. But I must be content with +a hint. So long as a man's eyes are open in the light, the act of +seeing is involuntary; that is, he cannot then help mechanically +seeing whatever objects are before him. Nevertheless, any one's +experience will teach him, that though he can take in an +undiscriminating sweep of things at one glance, it is quite +impossible for him, attentively, and completely, to examine any two +things--however large or however small--at one and the same instant +of time; never mind if they lie side by side and touch each other. +But if you now come to separate these two objects, and surround each +by a circle of profound darkness; then, in order to see one of them, +in such a manner as to bring your mind to bear on it, the other will +be utterly excluded from your contemporary consciousness. How is it, +then, with the whale? True, both his eyes, in themselves, must +simultaneously act; but is his brain so much more comprehensive, +combining, and subtle than man's, that he can at the same moment of +time attentively examine two distinct prospects, one on one side of +him, and the other in an exactly opposite direction? If he can, then +is it as marvellous a thing in him, as if a man were able +simultaneously to go through the demonstrations of two distinct +problems in Euclid. Nor, strictly investigated, is there any +incongruity in this comparison. + +It may be but an idle whim, but it has always seemed to me, that the +extraordinary vacillations of movement displayed by some whales when +beset by three or four boats; the timidity and liability to queer +frights, so common to such whales; I think that all this indirectly +proceeds from the helpless perplexity of volition, in which their +divided and diametrically opposite powers of vision must involve +them. + +But the ear of the whale is full as curious as the eye. If you are +an entire stranger to their race, you might hunt over these two heads +for hours, and never discover that organ. The ear has no external +leaf whatever; and into the hole itself you can hardly insert a +quill, so wondrously minute is it. It is lodged a little behind the +eye. With respect to their ears, this important difference is to be +observed between the sperm whale and the right. While the ear of +the former has an external opening, that of the latter is entirely +and evenly covered over with a membrane, so as to be quite +imperceptible from without. + +Is it not curious, that so vast a being as the whale should see the +world through so small an eye, and hear the thunder through an ear +which is smaller than a hare's? But if his eyes were broad as the +lens of Herschel's great telescope; and his ears capacious as the +porches of cathedrals; would that make him any longer of sight, or +sharper of hearing? Not at all.--Why then do you try to "enlarge" +your mind? Subtilize it. + +Let us now with whatever levers and steam-engines we have at hand, +cant over the sperm whale's head, that it may lie bottom up; +then, ascending by a ladder to the summit, have a peep down the +mouth; and were it not that the body is now completely separated from +it, with a lantern we might descend into the great Kentucky Mammoth +Cave of his stomach. But let us hold on here by this tooth, and look +about us where we are. What a really beautiful and chaste-looking +mouth! from floor to ceiling, lined, or rather papered with a +glistening white membrane, glossy as bridal satins. + +But come out now, and look at this portentous lower jaw, which seems +like the long narrow lid of an immense snuff-box, with the hinge at +one end, instead of one side. If you pry it up, so as to get it +overhead, and expose its rows of teeth, it seems a terrific +portcullis; and such, alas! it proves to many a poor wight in the +fishery, upon whom these spikes fall with impaling force. But far +more terrible is it to behold, when fathoms down in the sea, you see +some sulky whale, floating there suspended, with his prodigious jaw, +some fifteen feet long, hanging straight down at right-angles with +his body, for all the world like a ship's jib-boom. This whale is +not dead; he is only dispirited; out of sorts, perhaps; +hypochondriac; and so supine, that the hinges of his jaw have +relaxed, leaving him there in that ungainly sort of plight, a +reproach to all his tribe, who must, no doubt, imprecate lock-jaws +upon him. + +In most cases this lower jaw--being easily unhinged by a practised +artist--is disengaged and hoisted on deck for the purpose of +extracting the ivory teeth, and furnishing a supply of that hard +white whalebone with which the fishermen fashion all sorts of curious +articles, including canes, umbrella-stocks, and handles to +riding-whips. + +With a long, weary hoist the jaw is dragged on board, as if it were +an anchor; and when the proper time comes--some few days after the +other work--Queequeg, Daggoo, and Tashtego, being all accomplished +dentists, are set to drawing teeth. With a keen cutting-spade, +Queequeg lances the gums; then the jaw is lashed down to ringbolts, +and a tackle being rigged from aloft, they drag out these teeth, as +Michigan oxen drag stumps of old oaks out of wild wood lands. There +are generally forty-two teeth in all; in old whales, much worn down, +but undecayed; nor filled after our artificial fashion. The jaw is +afterwards sawn into slabs, and piled away like joists for building +houses. + + + +CHAPTER 75 + +The Right Whale's Head--Contrasted View. + + +Crossing the deck, let us now have a good long look at the Right +Whale's head. + +As in general shape the noble Sperm Whale's head may be compared to a +Roman war-chariot (especially in front, where it is so broadly +rounded); so, at a broad view, the Right Whale's head bears a rather +inelegant resemblance to a gigantic galliot-toed shoe. Two hundred +years ago an old Dutch voyager likened its shape to that of a +shoemaker's last. And in this same last or shoe, that old woman of +the nursery tale, with the swarming brood, might very comfortably be +lodged, she and all her progeny. + +But as you come nearer to this great head it begins to assume +different aspects, according to your point of view. If you stand on +its summit and look at these two F-shaped spoutholes, you would take +the whole head for an enormous bass-viol, and these spiracles, the +apertures in its sounding-board. Then, again, if you fix your eye +upon this strange, crested, comb-like incrustation on the top of the +mass--this green, barnacled thing, which the Greenlanders call the +"crown," and the Southern fishers the "bonnet" of the Right Whale; +fixing your eyes solely on this, you would take the head for the +trunk of some huge oak, with a bird's nest in its crotch. At any +rate, when you watch those live crabs that nestle here on this +bonnet, such an idea will be almost sure to occur to you; unless, +indeed, your fancy has been fixed by the technical term "crown" also +bestowed upon it; in which case you will take great interest in +thinking how this mighty monster is actually a diademed king of the +sea, whose green crown has been put together for him in this +marvellous manner. But if this whale be a king, he is a very sulky +looking fellow to grace a diadem. Look at that hanging lower lip! +what a huge sulk and pout is there! a sulk and pout, by carpenter's +measurement, about twenty feet long and five feet deep; a sulk and +pout that will yield you some 500 gallons of oil and more. + +A great pity, now, that this unfortunate whale should be hare-lipped. +The fissure is about a foot across. Probably the mother during an +important interval was sailing down the Peruvian coast, when +earthquakes caused the beach to gape. Over this lip, as over a +slippery threshold, we now slide into the mouth. Upon my word were I +at Mackinaw, I should take this to be the inside of an Indian wigwam. +Good Lord! is this the road that Jonah went? The roof is about +twelve feet high, and runs to a pretty sharp angle, as if there were +a regular ridge-pole there; while these ribbed, arched, hairy sides, +present us with those wondrous, half vertical, scimetar-shaped slats +of whalebone, say three hundred on a side, which depending from the +upper part of the head or crown bone, form those Venetian blinds +which have elsewhere been cursorily mentioned. The edges of these +bones are fringed with hairy fibres, through which the Right Whale +strains the water, and in whose intricacies he retains the small +fish, when openmouthed he goes through the seas of brit in feeding +time. In the central blinds of bone, as they stand in their natural +order, there are certain curious marks, curves, hollows, and ridges, +whereby some whalemen calculate the creature's age, as the age of an +oak by its circular rings. Though the certainty of this criterion is +far from demonstrable, yet it has the savor of analogical +probability. At any rate, if we yield to it, we must grant a far +greater age to the Right Whale than at first glance will seem +reasonable. + +In old times, there seem to have prevailed the most curious fancies +concerning these blinds. One voyager in Purchas calls them the +wondrous "whiskers" inside of the whale's mouth;* another, "hogs' +bristles"; a third old gentleman in Hackluyt uses the following +elegant language: "There are about two hundred and fifty fins growing +on each side of his upper CHOP, which arch over his tongue on each +side of his mouth." + + +*This reminds us that the Right Whale really has a sort of whisker, +or rather a moustache, consisting of a few scattered white hairs on +the upper part of the outer end of the lower jaw. Sometimes these +tufts impart a rather brigandish expression to his otherwise solemn +countenance. + + +As every one knows, these same "hogs' bristles," "fins," "whiskers," +"blinds," or whatever you please, furnish to the ladies their busks +and other stiffening contrivances. But in this particular, the +demand has long been on the decline. It was in Queen Anne's time +that the bone was in its glory, the farthingale being then all the +fashion. And as those ancient dames moved about gaily, though in the +jaws of the whale, as you may say; even so, in a shower, with the +like thoughtlessness, do we nowadays fly under the same jaws for +protection; the umbrella being a tent spread over the same bone. + +But now forget all about blinds and whiskers for a moment, and, +standing in the Right Whale's mouth, look around you afresh. Seeing +all these colonnades of bone so methodically ranged about, would you +not think you were inside of the great Haarlem organ, and gazing +upon its thousand pipes? For a carpet to the organ we have a rug of +the softest Turkey--the tongue, which is glued, as it were, to the +floor of the mouth. It is very fat and tender, and apt to tear in +pieces in hoisting it on deck. This particular tongue now before us; +at a passing glance I should say it was a six-barreler; that is, it +will yield you about that amount of oil. + +Ere this, you must have plainly seen the truth of what I started +with--that the Sperm Whale and the Right Whale have almost entirely +different heads. To sum up, then: in the Right Whale's there is no +great well of sperm; no ivory teeth at all; no long, slender mandible +of a lower jaw, like the Sperm Whale's. Nor in the Sperm Whale are +there any of those blinds of bone; no huge lower lip; and scarcely +anything of a tongue. Again, the Right Whale has two external +spout-holes, the Sperm Whale only one. + +Look your last, now, on these venerable hooded heads, while they yet +lie together; for one will soon sink, unrecorded, in the sea; the +other will not be very long in following. + +Can you catch the expression of the Sperm Whale's there? It is the +same he died with, only some of the longer wrinkles in the forehead +seem now faded away. I think his broad brow to be full of a +prairie-like placidity, born of a speculative indifference as to +death. But mark the other head's expression. See that amazing lower +lip, pressed by accident against the vessel's side, so as firmly to +embrace the jaw. Does not this whole head seem to speak of an +enormous practical resolution in facing death? This Right Whale I +take to have been a Stoic; the Sperm Whale, a Platonian, who might +have taken up Spinoza in his latter years. + + + +CHAPTER 76 + +The Battering-Ram. + + +Ere quitting, for the nonce, the Sperm Whale's head, I would have +you, as a sensible physiologist, simply--particularly remark its +front aspect, in all its compacted collectedness. I would have you +investigate it now with the sole view of forming to yourself some +unexaggerated, intelligent estimate of whatever battering-ram power +may be lodged there. Here is a vital point; for you must either +satisfactorily settle this matter with yourself, or for ever remain +an infidel as to one of the most appalling, but not the less true +events, perhaps anywhere to be found in all recorded history. + +You observe that in the ordinary swimming position of the Sperm +Whale, the front of his head presents an almost wholly vertical plane +to the water; you observe that the lower part of that front slopes +considerably backwards, so as to furnish more of a retreat for the +long socket which receives the boom-like lower jaw; you observe that +the mouth is entirely under the head, much in the same way, indeed, +as though your own mouth were entirely under your chin. Moreover you +observe that the whale has no external nose; and that what nose he +has--his spout hole--is on the top of his head; you observe that his +eyes and ears are at the sides of his head, nearly one third of his +entire length from the front. Wherefore, you must now have perceived +that the front of the Sperm Whale's head is a dead, blind wall, +without a single organ or tender prominence of any sort whatsoever. +Furthermore, you are now to consider that only in the extreme, lower, +backward sloping part of the front of the head, is there the +slightest vestige of bone; and not till you get near twenty feet from +the forehead do you come to the full cranial development. So that +this whole enormous boneless mass is as one wad. Finally, though, as +will soon be revealed, its contents partly comprise the most delicate +oil; yet, you are now to be apprised of the nature of the substance +which so impregnably invests all that apparent effeminacy. In some +previous place I have described to you how the blubber wraps the body +of the whale, as the rind wraps an orange. Just so with the head; +but with this difference: about the head this envelope, though not so +thick, is of a boneless toughness, inestimable by any man who has not +handled it. The severest pointed harpoon, the sharpest lance darted +by the strongest human arm, impotently rebounds from it. It is as +though the forehead of the Sperm Whale were paved with horses' hoofs. +I do not think that any sensation lurks in it. + +Bethink yourself also of another thing. When two large, loaded +Indiamen chance to crowd and crush towards each other in the +docks, what do the sailors do? They do not suspend between them, at +the point of coming contact, any merely hard substance, like iron or +wood. No, they hold there a large, round wad of tow and cork, +enveloped in the thickest and toughest of ox-hide. That bravely and +uninjured takes the jam which would have snapped all their oaken +handspikes and iron crow-bars. By itself this sufficiently +illustrates the obvious fact I drive at. But supplementary to this, +it has hypothetically occurred to me, that as ordinary fish possess +what is called a swimming bladder in them, capable, at will, of +distension or contraction; and as the Sperm Whale, as far as I know, +has no such provision in him; considering, too, the otherwise +inexplicable manner in which he now depresses his head altogether +beneath the surface, and anon swims with it high elevated out of the +water; considering the unobstructed elasticity of its envelope; +considering the unique interior of his head; it has hypothetically +occurred to me, I say, that those mystical lung-celled honeycombs +there may possibly have some hitherto unknown and unsuspected +connexion with the outer air, so as to be susceptible to atmospheric +distension and contraction. If this be so, fancy the +irresistibleness of that might, to which the most impalpable and +destructive of all elements contributes. + +Now, mark. Unerringly impelling this dead, impregnable, uninjurable +wall, and this most buoyant thing within; there swims behind it all a +mass of tremendous life, only to be adequately estimated as piled +wood is--by the cord; and all obedient to one volition, as the +smallest insect. So that when I shall hereafter detail to you all +the specialities and concentrations of potency everywhere lurking in +this expansive monster; when I shall show you some of his more +inconsiderable braining feats; I trust you will have renounced all +ignorant incredulity, and be ready to abide by this; that though the +Sperm Whale stove a passage through the Isthmus of Darien, and mixed +the Atlantic with the Pacific, you would not elevate one hair of your +eye-brow. For unless you own the whale, you are but a provincial and +sentimentalist in Truth. But clear Truth is a thing for salamander +giants only to encounter; how small the chances for the provincials +then? What befell the weakling youth lifting the dread goddess's +veil at Lais? + + + +CHAPTER 77 + +The Great Heidelburgh Tun. + + +Now comes the Baling of the Case. But to comprehend it aright, you +must know something of the curious internal structure of the thing +operated upon. + +Regarding the Sperm Whale's head as a solid oblong, you may, on an +inclined plane, sideways divide it into two quoins,* whereof the +lower is the bony structure, forming the cranium and jaws, and the +upper an unctuous mass wholly free from bones; its broad forward end +forming the expanded vertical apparent forehead of the whale. At the +middle of the forehead horizontally subdivide this upper quoin, and +then you have two almost equal parts, which before were naturally +divided by an internal wall of a thick tendinous substance. + + +*Quoin is not a Euclidean term. It belongs to the pure nautical +mathematics. I know not that it has been defined before. A quoin is +a solid which differs from a wedge in having its sharp end formed by +the steep inclination of one side, instead of the mutual tapering of +both sides. + + +The lower subdivided part, called the junk, is one immense honeycomb +of oil, formed by the crossing and recrossing, into ten thousand +infiltrated cells, of tough elastic white fibres throughout its whole +extent. The upper part, known as the Case, may be regarded as the +great Heidelburgh Tun of the Sperm Whale. And as that famous great +tierce is mystically carved in front, so the whale's vast plaited +forehead forms innumerable strange devices for the emblematical +adornment of his wondrous tun. Moreover, as that of Heidelburgh was +always replenished with the most excellent of the wines of the +Rhenish valleys, so the tun of the whale contains by far the most +precious of all his oily vintages; namely, the highly-prized +spermaceti, in its absolutely pure, limpid, and odoriferous state. +Nor is this precious substance found unalloyed in any other part of +the creature. Though in life it remains perfectly fluid, yet, upon +exposure to the air, after death, it soon begins to concrete; sending +forth beautiful crystalline shoots, as when the first thin delicate +ice is just forming in water. A large whale's case generally yields +about five hundred gallons of sperm, though from unavoidable +circumstances, considerable of it is spilled, leaks, and dribbles +away, or is otherwise irrevocably lost in the ticklish business of +securing what you can. + +I know not with what fine and costly material the Heidelburgh Tun was +coated within, but in superlative richness that coating could not +possibly have compared with the silken pearl-coloured membrane, like +the lining of a fine pelisse, forming the inner surface of the Sperm +Whale's case. + +It will have been seen that the Heidelburgh Tun of the Sperm Whale +embraces the entire length of the entire top of the head; and +since--as has been elsewhere set forth--the head embraces one third +of the whole length of the creature, then setting that length down at +eighty feet for a good sized whale, you have more than twenty-six +feet for the depth of the tun, when it is lengthwise hoisted up and +down against a ship's side. + +As in decapitating the whale, the operator's instrument is brought +close to the spot where an entrance is subsequently forced into the +spermaceti magazine; he has, therefore, to be uncommonly heedful, +lest a careless, untimely stroke should invade the sanctuary and +wastingly let out its invaluable contents. It is this decapitated +end of the head, also, which is at last elevated out of the water, +and retained in that position by the enormous cutting tackles, whose +hempen combinations, on one side, make quite a wilderness of ropes in +that quarter. + +Thus much being said, attend now, I pray you, to that marvellous +and--in this particular instance--almost fatal operation whereby the +Sperm Whale's great Heidelburgh Tun is tapped. + + + +CHAPTER 78 + +Cistern and Buckets. + + +Nimble as a cat, Tashtego mounts aloft; and without altering his +erect posture, runs straight out upon the overhanging mainyard-arm, +to the part where it exactly projects over the hoisted Tun. He has +carried with him a light tackle called a whip, consisting of only two +parts, travelling through a single-sheaved block. Securing this +block, so that it hangs down from the yard-arm, he swings one end of +the rope, till it is caught and firmly held by a hand on deck. +Then, hand-over-hand, down the other part, the Indian drops through +the air, till dexterously he lands on the summit of the head. +There--still high elevated above the rest of the company, to whom he +vivaciously cries--he seems some Turkish Muezzin calling the good +people to prayers from the top of a tower. A short-handled sharp +spade being sent up to him, he diligently searches for the proper +place to begin breaking into the Tun. In this business he proceeds +very heedfully, like a treasure-hunter in some old house, sounding +the walls to find where the gold is masoned in. By the time this +cautious search is over, a stout iron-bound bucket, precisely like a +well-bucket, has been attached to one end of the whip; while the +other end, being stretched across the deck, is there held by two or +three alert hands. These last now hoist the bucket within grasp of +the Indian, to whom another person has reached up a very long pole. +Inserting this pole into the bucket, Tashtego downward guides the +bucket into the Tun, till it entirely disappears; then giving the +word to the seamen at the whip, up comes the bucket again, all +bubbling like a dairy-maid's pail of new milk. Carefully lowered +from its height, the full-freighted vessel is caught by an appointed +hand, and quickly emptied into a large tub. Then remounting aloft, +it again goes through the same round until the deep cistern will +yield no more. Towards the end, Tashtego has to ram his long pole +harder and harder, and deeper and deeper into the Tun, until some +twenty feet of the pole have gone down. + +Now, the people of the Pequod had been baling some time in this way; +several tubs had been filled with the fragrant sperm; when all at +once a queer accident happened. Whether it was that Tashtego, that +wild Indian, was so heedless and reckless as to let go for a moment +his one-handed hold on the great cabled tackles suspending the head; +or whether the place where he stood was so treacherous and oozy; or +whether the Evil One himself would have it to fall out so, without +stating his particular reasons; how it was exactly, there is no +telling now; but, on a sudden, as the eightieth or ninetieth bucket +came suckingly up--my God! poor Tashtego--like the twin reciprocating +bucket in a veritable well, dropped head-foremost down into this +great Tun of Heidelburgh, and with a horrible oily gurgling, went +clean out of sight! + +"Man overboard!" cried Daggoo, who amid the general consternation +first came to his senses. "Swing the bucket this way!" and putting +one foot into it, so as the better to secure his slippery hand-hold +on the whip itself, the hoisters ran him high up to the top of the +head, almost before Tashtego could have reached its interior bottom. +Meantime, there was a terrible tumult. Looking over the side, they +saw the before lifeless head throbbing and heaving just below the +surface of the sea, as if that moment seized with some momentous +idea; whereas it was only the poor Indian unconsciously revealing by +those struggles the perilous depth to which he had sunk. + +At this instant, while Daggoo, on the summit of the head, was +clearing the whip--which had somehow got foul of the great cutting +tackles--a sharp cracking noise was heard; and to the unspeakable +horror of all, one of the two enormous hooks suspending the head tore +out, and with a vast vibration the enormous mass sideways swung, till +the drunk ship reeled and shook as if smitten by an iceberg. The one +remaining hook, upon which the entire strain now depended, seemed +every instant to be on the point of giving way; an event still more +likely from the violent motions of the head. + +"Come down, come down!" yelled the seamen to Daggoo, but with one +hand holding on to the heavy tackles, so that if the head should +drop, he would still remain suspended; the negro having cleared the +foul line, rammed down the bucket into the now collapsed well, +meaning that the buried harpooneer should grasp it, and so be hoisted +out. + +"In heaven's name, man," cried Stubb, "are you ramming home a +cartridge there?--Avast! How will that help him; jamming that +iron-bound bucket on top of his head? Avast, will ye!" + +"Stand clear of the tackle!" cried a voice like the bursting of a +rocket. + +Almost in the same instant, with a thunder-boom, the enormous mass +dropped into the sea, like Niagara's Table-Rock into the whirlpool; +the suddenly relieved hull rolled away from it, to far down her +glittering copper; and all caught their breath, as half swinging--now +over the sailors' heads, and now over the water--Daggoo, through a +thick mist of spray, was dimly beheld clinging to the pendulous +tackles, while poor, buried-alive Tashtego was sinking utterly down +to the bottom of the sea! But hardly had the blinding vapour cleared +away, when a naked figure with a boarding-sword in his hand, was for +one swift moment seen hovering over the bulwarks. The next, a loud +splash announced that my brave Queequeg had dived to the rescue. One +packed rush was made to the side, and every eye counted every ripple, +as moment followed moment, and no sign of either the sinker or the +diver could be seen. Some hands now jumped into a boat alongside, +and pushed a little off from the ship. + +"Ha! ha!" cried Daggoo, all at once, from his now quiet, swinging +perch overhead; and looking further off from the side, we saw an arm +thrust upright from the blue waves; a sight strange to see, as an arm +thrust forth from the grass over a grave. + +"Both! both!--it is both!"--cried Daggoo again with a joyful shout; +and soon after, Queequeg was seen boldly striking out with one hand, +and with the other clutching the long hair of the Indian. Drawn into +the waiting boat, they were quickly brought to the deck; but Tashtego +was long in coming to, and Queequeg did not look very brisk. + +Now, how had this noble rescue been accomplished? Why, diving after +the slowly descending head, Queequeg with his keen sword had made +side lunges near its bottom, so as to scuttle a large hole there; +then dropping his sword, had thrust his long arm far inwards and +upwards, and so hauled out poor Tash by the head. He averred, that +upon first thrusting in for him, a leg was presented; but well +knowing that that was not as it ought to be, and might occasion great +trouble;--he had thrust back the leg, and by a dexterous heave and +toss, had wrought a somerset upon the Indian; so that with the next +trial, he came forth in the good old way--head foremost. As for the +great head itself, that was doing as well as could be expected. + +And thus, through the courage and great skill in obstetrics of +Queequeg, the deliverance, or rather, delivery of Tashtego, was +successfully accomplished, in the teeth, too, of the most untoward +and apparently hopeless impediments; which is a lesson by no means to +be forgotten. Midwifery should be taught in the same course with +fencing and boxing, riding and rowing. + +I know that this queer adventure of the Gay-Header's will be sure to +seem incredible to some landsmen, though they themselves may have +either seen or heard of some one's falling into a cistern ashore; an +accident which not seldom happens, and with much less reason too than +the Indian's, considering the exceeding slipperiness of the curb of +the Sperm Whale's well. + +But, peradventure, it may be sagaciously urged, how is this? We +thought the tissued, infiltrated head of the Sperm Whale, was the +lightest and most corky part about him; and yet thou makest it sink +in an element of a far greater specific gravity than itself. We have +thee there. Not at all, but I have ye; for at the time poor Tash +fell in, the case had been nearly emptied of its lighter contents, +leaving little but the dense tendinous wall of the well--a double +welded, hammered substance, as I have before said, much heavier than +the sea water, and a lump of which sinks in it like lead almost. But +the tendency to rapid sinking in this substance was in the present +instance materially counteracted by the other parts of the head +remaining undetached from it, so that it sank very slowly and +deliberately indeed, affording Queequeg a fair chance for performing +his agile obstetrics on the run, as you may say. Yes, it was a +running delivery, so it was. + +Now, had Tashtego perished in that head, it had been a very precious +perishing; smothered in the very whitest and daintiest of fragrant +spermaceti; coffined, hearsed, and tombed in the secret inner chamber +and sanctum sanctorum of the whale. Only one sweeter end can readily +be recalled--the delicious death of an Ohio honey-hunter, who seeking +honey in the crotch of a hollow tree, found such exceeding store of +it, that leaning too far over, it sucked him in, so that he died +embalmed. How many, think ye, have likewise fallen into Plato's +honey head, and sweetly perished there? + + + +CHAPTER 79 + +The Prairie. + + +To scan the lines of his face, or feel the bumps on the head of this +Leviathan; this is a thing which no Physiognomist or Phrenologist has +as yet undertaken. Such an enterprise would seem almost as hopeful +as for Lavater to have scrutinized the wrinkles on the Rock of +Gibraltar, or for Gall to have mounted a ladder and manipulated the +Dome of the Pantheon. Still, in that famous work of his, Lavater +not only treats of the various faces of men, but also attentively +studies the faces of horses, birds, serpents, and fish; and dwells in +detail upon the modifications of expression discernible therein. Nor +have Gall and his disciple Spurzheim failed to throw out some hints +touching the phrenological characteristics of other beings than man. +Therefore, though I am but ill qualified for a pioneer, in the +application of these two semi-sciences to the whale, I will do my +endeavor. I try all things; I achieve what I can. + +Physiognomically regarded, the Sperm Whale is an anomalous creature. +He has no proper nose. And since the nose is the central and most +conspicuous of the features; and since it perhaps most modifies and +finally controls their combined expression; hence it would seem that +its entire absence, as an external appendage, must very largely +affect the countenance of the whale. For as in landscape gardening, +a spire, cupola, monument, or tower of some sort, is deemed almost +indispensable to the completion of the scene; so no face can be +physiognomically in keeping without the elevated open-work belfry of +the nose. Dash the nose from Phidias's marble Jove, and what a sorry +remainder! Nevertheless, Leviathan is of so mighty a magnitude, all +his proportions are so stately, that the same deficiency which in the +sculptured Jove were hideous, in him is no blemish at all. Nay, it +is an added grandeur. A nose to the whale would have been +impertinent. As on your physiognomical voyage you sail round his +vast head in your jolly-boat, your noble conceptions of him are never +insulted by the reflection that he has a nose to be pulled. A +pestilent conceit, which so often will insist upon obtruding even +when beholding the mightiest royal beadle on his throne. + +In some particulars, perhaps the most imposing physiognomical view +to be had of the Sperm Whale, is that of the full front of his head. +This aspect is sublime. + +In thought, a fine human brow is like the East when troubled with +the morning. In the repose of the pasture, the curled brow of the +bull has a touch of the grand in it. Pushing heavy cannon up +mountain defiles, the elephant's brow is majestic. Human or animal, +the mystical brow is as that great golden seal affixed by the German +Emperors to their decrees. It signifies--"God: done this day by my +hand." But in most creatures, nay in man himself, very often the +brow is but a mere strip of alpine land lying along the snow line. +Few are the foreheads which like Shakespeare's or Melancthon's rise +so high, and descend so low, that the eyes themselves seem clear, +eternal, tideless mountain lakes; and all above them in the forehead's +wrinkles, you seem to track the antlered thoughts descending there to +drink, as the Highland hunters track the snow prints of the deer. +But in the great Sperm Whale, this high and mighty god-like dignity +inherent in the brow is so immensely amplified, that gazing on it, in +that full front view, you feel the Deity and the dread powers more +forcibly than in beholding any other object in living nature. For +you see no one point precisely; not one distinct feature is revealed; +no nose, eyes, ears, or mouth; no face; he has none, proper; nothing +but that one broad firmament of a forehead, pleated with riddles; +dumbly lowering with the doom of boats, and ships, and men. Nor, in +profile, does this wondrous brow diminish; though that way viewed its +grandeur does not domineer upon you so. In profile, you plainly +perceive that horizontal, semi-crescentic depression in the +forehead's middle, which, in man, is Lavater's mark of genius. + +But how? Genius in the Sperm Whale? Has the Sperm Whale ever +written a book, spoken a speech? No, his great genius is declared in +his doing nothing particular to prove it. It is moreover declared in +his pyramidical silence. And this reminds me that had the great +Sperm Whale been known to the young Orient World, he would have been +deified by their child-magian thoughts. They deified the crocodile +of the Nile, because the crocodile is tongueless; and the Sperm Whale +has no tongue, or at least it is so exceedingly small, as to be +incapable of protrusion. If hereafter any highly cultured, poetical +nation shall lure back to their birth-right, the merry May-day gods +of old; and livingly enthrone them again in the now egotistical sky; +in the now unhaunted hill; then be sure, exalted to Jove's high seat, +the great Sperm Whale shall lord it. + +Champollion deciphered the wrinkled granite hieroglyphics. But there +is no Champollion to decipher the Egypt of every man's and every +being's face. Physiognomy, like every other human science, is but a +passing fable. If then, Sir William Jones, who read in thirty +languages, could not read the simplest peasant's face in its +profounder and more subtle meanings, how may unlettered Ishmael hope +to read the awful Chaldee of the Sperm Whale's brow? I but put that +brow before you. Read it if you can. + + + +CHAPTER 80 + +The Nut. + + +If the Sperm Whale be physiognomically a Sphinx, to the phrenologist +his brain seems that geometrical circle which it is impossible to +square. + +In the full-grown creature the skull will measure at least twenty +feet in length. Unhinge the lower jaw, and the side view of this +skull is as the side of a moderately inclined plane resting +throughout on a level base. But in life--as we have elsewhere +seen--this inclined plane is angularly filled up, and almost squared +by the enormous superincumbent mass of the junk and sperm. At the +high end the skull forms a crater to bed that part of the mass; while +under the long floor of this crater--in another cavity seldom +exceeding ten inches in length and as many in depth--reposes the +mere handful of this monster's brain. The brain is at least twenty +feet from his apparent forehead in life; it is hidden away behind its +vast outworks, like the innermost citadel within the amplified +fortifications of Quebec. So like a choice casket is it secreted in +him, that I have known some whalemen who peremptorily deny that the +Sperm Whale has any other brain than that palpable semblance of one +formed by the cubic-yards of his sperm magazine. Lying in strange +folds, courses, and convolutions, to their apprehensions, it seems +more in keeping with the idea of his general might to regard that +mystic part of him as the seat of his intelligence. + +It is plain, then, that phrenologically the head of this Leviathan, +in the creature's living intact state, is an entire delusion. As for +his true brain, you can then see no indications of it, nor feel any. +The whale, like all things that are mighty, wears a false brow to the +common world. + +If you unload his skull of its spermy heaps and then take a rear view +of its rear end, which is the high end, you will be struck by its +resemblance to the human skull, beheld in the same situation, and +from the same point of view. Indeed, place this reversed skull +(scaled down to the human magnitude) among a plate of men's skulls, +and you would involuntarily confound it with them; and remarking the +depressions on one part of its summit, in phrenological phrase you +would say--This man had no self-esteem, and no veneration. And by +those negations, considered along with the affirmative fact of his +prodigious bulk and power, you can best form to yourself the truest, +though not the most exhilarating conception of what the most exalted +potency is. + +But if from the comparative dimensions of the whale's proper brain, +you deem it incapable of being adequately charted, then I have +another idea for you. If you attentively regard almost any +quadruped's spine, you will be struck with the resemblance of its +vertebrae to a strung necklace of dwarfed skulls, all bearing +rudimental resemblance to the skull proper. It is a German conceit, +that the vertebrae are absolutely undeveloped skulls. But the +curious external resemblance, I take it the Germans were not the +first men to perceive. A foreign friend once pointed it out to me, +in the skeleton of a foe he had slain, and with the vertebrae of +which he was inlaying, in a sort of basso-relievo, the beaked prow +of his canoe. Now, I consider that the phrenologists have omitted an +important thing in not pushing their investigations from the +cerebellum through the spinal canal. For I believe that much of a +man's character will be found betokened in his backbone. I would +rather feel your spine than your skull, whoever you are. A thin +joist of a spine never yet upheld a full and noble soul. I rejoice +in my spine, as in the firm audacious staff of that flag which I +fling half out to the world. + +Apply this spinal branch of phrenology to the Sperm Whale. His +cranial cavity is continuous with the first neck-vertebra; and in +that vertebra the bottom of the spinal canal will measure ten inches +across, being eight in height, and of a triangular figure with the +base downwards. As it passes through the remaining vertebrae the +canal tapers in size, but for a considerable distance remains of +large capacity. Now, of course, this canal is filled with much the +same strangely fibrous substance--the spinal cord--as the brain; and +directly communicates with the brain. And what is still more, for +many feet after emerging from the brain's cavity, the spinal cord +remains of an undecreasing girth, almost equal to that of the brain. +Under all these circumstances, would it be unreasonable to survey and +map out the whale's spine phrenologically? For, viewed in this +light, the wonderful comparative smallness of his brain proper is +more than compensated by the wonderful comparative magnitude of his +spinal cord. + +But leaving this hint to operate as it may with the phrenologists, I +would merely assume the spinal theory for a moment, in reference to +the Sperm Whale's hump. This august hump, if I mistake not, rises +over one of the larger vertebrae, and is, therefore, in some sort, +the outer convex mould of it. From its relative situation then, I +should call this high hump the organ of firmness or indomitableness +in the Sperm Whale. And that the great monster is indomitable, you +will yet have reason to know. + + + +CHAPTER 81 + +The Pequod Meets The Virgin. + + +The predestinated day arrived, and we duly met the ship Jungfrau, +Derick De Deer, master, of Bremen. + +At one time the greatest whaling people in the world, the Dutch and +Germans are now among the least; but here and there at very wide +intervals of latitude and longitude, you still occasionally meet with +their flag in the Pacific. + +For some reason, the Jungfrau seemed quite eager to pay her respects. +While yet some distance from the Pequod, she rounded to, and +dropping a boat, her captain was impelled towards us, impatiently +standing in the bows instead of the stern. + +"What has he in his hand there?" cried Starbuck, pointing to +something wavingly held by the German. "Impossible!--a lamp-feeder!" + +"Not that," said Stubb, "no, no, it's a coffee-pot, Mr. Starbuck; +he's coming off to make us our coffee, is the Yarman; don't you see +that big tin can there alongside of him?--that's his boiling water. +Oh! he's all right, is the Yarman." + +"Go along with you," cried Flask, "it's a lamp-feeder and an oil-can. +He's out of oil, and has come a-begging." + +However curious it may seem for an oil-ship to be borrowing oil on +the whale-ground, and however much it may invertedly contradict the +old proverb about carrying coals to Newcastle, yet sometimes such a +thing really happens; and in the present case Captain Derick De Deer +did indubitably conduct a lamp-feeder as Flask did declare. + +As he mounted the deck, Ahab abruptly accosted him, without at all +heeding what he had in his hand; but in his broken lingo, the German +soon evinced his complete ignorance of the White Whale; immediately +turning the conversation to his lamp-feeder and oil can, with some +remarks touching his having to turn into his hammock at night in +profound darkness--his last drop of Bremen oil being gone, and not a +single flying-fish yet captured to supply the deficiency; concluding +by hinting that his ship was indeed what in the Fishery is +technically called a CLEAN one (that is, an empty one), well +deserving the name of Jungfrau or the Virgin. + +His necessities supplied, Derick departed; but he had not gained his +ship's side, when whales were almost simultaneously raised from the +mast-heads of both vessels; and so eager for the chase was Derick, +that without pausing to put his oil-can and lamp-feeder aboard, he +slewed round his boat and made after the leviathan lamp-feeders. + +Now, the game having risen to leeward, he and the other three German +boats that soon followed him, had considerably the start of the +Pequod's keels. There were eight whales, an average pod. Aware of +their danger, they were going all abreast with great speed straight +before the wind, rubbing their flanks as closely as so many spans of +horses in harness. They left a great, wide wake, as though +continually unrolling a great wide parchment upon the sea. + +Full in this rapid wake, and many fathoms in the rear, swam a huge, +humped old bull, which by his comparatively slow progress, as well as +by the unusual yellowish incrustations overgrowing him, seemed +afflicted with the jaundice, or some other infirmity. Whether this +whale belonged to the pod in advance, seemed questionable; for it is +not customary for such venerable leviathans to be at all social. +Nevertheless, he stuck to their wake, though indeed their back water +must have retarded him, because the white-bone or swell at his broad +muzzle was a dashed one, like the swell formed when two hostile +currents meet. His spout was short, slow, and laborious; coming +forth with a choking sort of gush, and spending itself in torn +shreds, followed by strange subterranean commotions in him, which +seemed to have egress at his other buried extremity, causing the +waters behind him to upbubble. + +"Who's got some paregoric?" said Stubb, "he has the stomach-ache, I'm +afraid. Lord, think of having half an acre of stomach-ache! Adverse +winds are holding mad Christmas in him, boys. It's the first foul +wind I ever knew to blow from astern; but look, did ever whale yaw +so before? it must be, he's lost his tiller." + +As an overladen Indiaman bearing down the Hindostan coast with a deck +load of frightened horses, careens, buries, rolls, and wallows on her +way; so did this old whale heave his aged bulk, and now and then +partly turning over on his cumbrous rib-ends, expose the cause of his +devious wake in the unnatural stump of his starboard fin. Whether he +had lost that fin in battle, or had been born without it, it were +hard to say. + +"Only wait a bit, old chap, and I'll give ye a sling for that wounded +arm," cried cruel Flask, pointing to the whale-line near him. + +"Mind he don't sling thee with it," cried Starbuck. "Give way, or +the German will have him." + +With one intent all the combined rival boats were pointed for this +one fish, because not only was he the largest, and therefore the most +valuable whale, but he was nearest to them, and the other whales were +going with such great velocity, moreover, as almost to defy pursuit +for the time. At this juncture the Pequod's keels had shot by the +three German boats last lowered; but from the great start he had had, +Derick's boat still led the chase, though every moment neared by his +foreign rivals. The only thing they feared, was, that from being +already so nigh to his mark, he would be enabled to dart his iron +before they could completely overtake and pass him. As for Derick, +he seemed quite confident that this would be the case, and +occasionally with a deriding gesture shook his lamp-feeder at the +other boats. + +"The ungracious and ungrateful dog!" cried Starbuck; "he mocks and +dares me with the very poor-box I filled for him not five minutes +ago!"--then in his old intense whisper--"Give way, greyhounds! Dog +to it!" + +"I tell ye what it is, men"--cried Stubb to his crew--"it's against +my religion to get mad; but I'd like to eat that villainous +Yarman--Pull--won't ye? Are ye going to let that rascal beat ye? Do +ye love brandy? A hogshead of brandy, then, to the best man. Come, +why don't some of ye burst a blood-vessel? Who's that been dropping +an anchor overboard--we don't budge an inch--we're becalmed. Halloo, +here's grass growing in the boat's bottom--and by the Lord, the mast +there's budding. This won't do, boys. Look at that Yarman! The +short and long of it is, men, will ye spit fire or not?" + +"Oh! see the suds he makes!" cried Flask, dancing up and down--"What +a hump--Oh, DO pile on the beef--lays like a log! Oh! my lads, DO +spring--slap-jacks and quahogs for supper, you know, my lads--baked +clams and muffins--oh, DO, DO, spring,--he's a hundred barreller--don't +lose him now--don't oh, DON'T!--see that Yarman--Oh, +won't ye pull for your duff, my lads--such a sog! such a sogger! +Don't ye love sperm? There goes three thousand dollars, men!--a +bank!--a whole bank! The bank of England!--Oh, DO, DO, DO!--What's +that Yarman about now?" + +At this moment Derick was in the act of pitching his lamp-feeder at +the advancing boats, and also his oil-can; perhaps with the double +view of retarding his rivals' way, and at the same time economically +accelerating his own by the momentary impetus of the backward toss. + +"The unmannerly Dutch dogger!" cried Stubb. "Pull now, men, like +fifty thousand line-of-battle-ship loads of red-haired devils. What +d'ye say, Tashtego; are you the man to snap your spine in +two-and-twenty pieces for the honour of old Gayhead? What d'ye say?" + +"I say, pull like god-dam,"--cried the Indian. + +Fiercely, but evenly incited by the taunts of the German, the +Pequod's three boats now began ranging almost abreast; and, so +disposed, momentarily neared him. In that fine, loose, chivalrous +attitude of the headsman when drawing near to his prey, the three +mates stood up proudly, occasionally backing the after oarsman with +an exhilarating cry of, "There she slides, now! Hurrah for the +white-ash breeze! Down with the Yarman! Sail over him!" + +But so decided an original start had Derick had, that spite of all +their gallantry, he would have proved the victor in this race, had +not a righteous judgment descended upon him in a crab which caught +the blade of his midship oarsman. While this clumsy lubber was +striving to free his white-ash, and while, in consequence, Derick's +boat was nigh to capsizing, and he thundering away at his men in a +mighty rage;--that was a good time for Starbuck, Stubb, and Flask. +With a shout, they took a mortal start forwards, and slantingly +ranged up on the German's quarter. An instant more, and all four +boats were diagonically in the whale's immediate wake, while +stretching from them, on both sides, was the foaming swell that he +made. + +It was a terrific, most pitiable, and maddening sight. The whale was +now going head out, and sending his spout before him in a continual +tormented jet; while his one poor fin beat his side in an agony of +fright. Now to this hand, now to that, he yawed in his faltering +flight, and still at every billow that he broke, he spasmodically +sank in the sea, or sideways rolled towards the sky his one beating +fin. So have I seen a bird with clipped wing making affrighted +broken circles in the air, vainly striving to escape the piratical +hawks. But the bird has a voice, and with plaintive cries will make +known her fear; but the fear of this vast dumb brute of the sea, was +chained up and enchanted in him; he had no voice, save that choking +respiration through his spiracle, and this made the sight of him +unspeakably pitiable; while still, in his amazing bulk, portcullis +jaw, and omnipotent tail, there was enough to appal the stoutest man +who so pitied. + +Seeing now that but a very few moments more would give the Pequod's +boats the advantage, and rather than be thus foiled of his game, +Derick chose to hazard what to him must have seemed a most unusually +long dart, ere the last chance would for ever escape. + +But no sooner did his harpooneer stand up for the stroke, than all +three tigers--Queequeg, Tashtego, Daggoo--instinctively sprang to +their feet, and standing in a diagonal row, simultaneously pointed +their barbs; and darted over the head of the German harpooneer, their +three Nantucket irons entered the whale. Blinding vapours of foam and +white-fire! The three boats, in the first fury of the whale's +headlong rush, bumped the German's aside with such force, that both +Derick and his baffled harpooneer were spilled out, and sailed over +by the three flying keels. + +"Don't be afraid, my butter-boxes," cried Stubb, casting a passing +glance upon them as he shot by; "ye'll be picked up presently--all +right--I saw some sharks astern--St. Bernard's dogs, you +know--relieve distressed travellers. Hurrah! this is the way to sail +now. Every keel a sunbeam! Hurrah!--Here we go like three tin +kettles at the tail of a mad cougar! This puts me in mind of +fastening to an elephant in a tilbury on a plain--makes the +wheel-spokes fly, boys, when you fasten to him that way; and there's +danger of being pitched out too, when you strike a hill. Hurrah! +this is the way a fellow feels when he's going to Davy Jones--all a +rush down an endless inclined plane! Hurrah! this whale carries the +everlasting mail!" + +But the monster's run was a brief one. Giving a sudden gasp, he +tumultuously sounded. With a grating rush, the three lines flew +round the loggerheads with such a force as to gouge deep grooves in +them; while so fearful were the harpooneers that this rapid sounding +would soon exhaust the lines, that using all their dexterous might, +they caught repeated smoking turns with the rope to hold on; till at +last--owing to the perpendicular strain from the lead-lined chocks of +the boats, whence the three ropes went straight down into the +blue--the gunwales of the bows were almost even with the water, while +the three sterns tilted high in the air. And the whale soon ceasing +to sound, for some time they remained in that attitude, fearful of +expending more line, though the position was a little ticklish. But +though boats have been taken down and lost in this way, yet it is +this "holding on," as it is called; this hooking up by the sharp +barbs of his live flesh from the back; this it is that often torments +the Leviathan into soon rising again to meet the sharp lance of his +foes. Yet not to speak of the peril of the thing, it is to be +doubted whether this course is always the best; for it is but +reasonable to presume, that the longer the stricken whale stays under +water, the more he is exhausted. Because, owing to the enormous +surface of him--in a full grown sperm whale something less than 2000 +square feet--the pressure of the water is immense. We all know what +an astonishing atmospheric weight we ourselves stand up under; even +here, above-ground, in the air; how vast, then, the burden of a +whale, bearing on his back a column of two hundred fathoms of ocean! +It must at least equal the weight of fifty atmospheres. One whaleman +has estimated it at the weight of twenty line-of-battle ships, with +all their guns, and stores, and men on board. + +As the three boats lay there on that gently rolling sea, gazing down +into its eternal blue noon; and as not a single groan or cry of any +sort, nay, not so much as a ripple or a bubble came up from its +depths; what landsman would have thought, that beneath all that +silence and placidity, the utmost monster of the seas was writhing +and wrenching in agony! Not eight inches of perpendicular rope were +visible at the bows. Seems it credible that by three such thin +threads the great Leviathan was suspended like the big weight to an +eight day clock. Suspended? and to what? To three bits of board. +Is this the creature of whom it was once so triumphantly said--"Canst +thou fill his skin with barbed irons? or his head with fish-spears? +The sword of him that layeth at him cannot hold, the spear, the dart, +nor the habergeon: he esteemeth iron as straw; the arrow cannot make +him flee; darts are counted as stubble; he laugheth at the shaking of +a spear!" This the creature? this he? Oh! that unfulfilments should +follow the prophets. For with the strength of a thousand thighs in +his tail, Leviathan had run his head under the mountains of the sea, +to hide him from the Pequod's fish-spears! + +In that sloping afternoon sunlight, the shadows that the three boats +sent down beneath the surface, must have been long enough and broad +enough to shade half Xerxes' army. Who can tell how appalling to the +wounded whale must have been such huge phantoms flitting over his +head! + +"Stand by, men; he stirs," cried Starbuck, as the three lines +suddenly vibrated in the water, distinctly conducting upwards to +them, as by magnetic wires, the life and death throbs of the whale, +so that every oarsman felt them in his seat. The next moment, +relieved in great part from the downward strain at the bows, the +boats gave a sudden bounce upwards, as a small icefield will, when a +dense herd of white bears are scared from it into the sea. + +"Haul in! Haul in!" cried Starbuck again; "he's rising." + +The lines, of which, hardly an instant before, not one hand's breadth +could have been gained, were now in long quick coils flung back all +dripping into the boats, and soon the whale broke water within two +ship's lengths of the hunters. + +His motions plainly denoted his extreme exhaustion. In most land +animals there are certain valves or flood-gates in many of their +veins, whereby when wounded, the blood is in some degree at least +instantly shut off in certain directions. Not so with the whale; one +of whose peculiarities it is to have an entire non-valvular structure +of the blood-vessels, so that when pierced even by so small a point +as a harpoon, a deadly drain is at once begun upon his whole +arterial system; and when this is heightened by the extraordinary +pressure of water at a great distance below the surface, his life may +be said to pour from him in incessant streams. Yet so vast is the +quantity of blood in him, and so distant and numerous its interior +fountains, that he will keep thus bleeding and bleeding for a +considerable period; even as in a drought a river will flow, whose +source is in the well-springs of far-off and undiscernible hills. +Even now, when the boats pulled upon this whale, and perilously drew +over his swaying flukes, and the lances were darted into him, they +were followed by steady jets from the new made wound, which kept +continually playing, while the natural spout-hole in his head was +only at intervals, however rapid, sending its affrighted moisture +into the air. From this last vent no blood yet came, because no +vital part of him had thus far been struck. His life, as they +significantly call it, was untouched. + +As the boats now more closely surrounded him, the whole upper part of +his form, with much of it that is ordinarily submerged, was plainly +revealed. His eyes, or rather the places where his eyes had been, +were beheld. As strange misgrown masses gather in the knot-holes of +the noblest oaks when prostrate, so from the points which the whale's +eyes had once occupied, now protruded blind bulbs, horribly pitiable +to see. But pity there was none. For all his old age, and his one +arm, and his blind eyes, he must die the death and be murdered, in +order to light the gay bridals and other merry-makings of men, and +also to illuminate the solemn churches that preach unconditional +inoffensiveness by all to all. Still rolling in his blood, at last +he partially disclosed a strangely discoloured bunch or protuberance, +the size of a bushel, low down on the flank. + +"A nice spot," cried Flask; "just let me prick him there once." + +"Avast!" cried Starbuck, "there's no need of that!" + +But humane Starbuck was too late. At the instant of the dart an +ulcerous jet shot from this cruel wound, and goaded by it into more +than sufferable anguish, the whale now spouting thick blood, with +swift fury blindly darted at the craft, bespattering them and their +glorying crews all over with showers of gore, capsizing Flask's boat +and marring the bows. It was his death stroke. For, by this time, +so spent was he by loss of blood, that he helplessly rolled away from +the wreck he had made; lay panting on his side, impotently flapped +with his stumped fin, then over and over slowly revolved like a +waning world; turned up the white secrets of his belly; lay like a +log, and died. It was most piteous, that last expiring spout. As +when by unseen hands the water is gradually drawn off from some +mighty fountain, and with half-stifled melancholy gurglings the +spray-column lowers and lowers to the ground--so the last long dying +spout of the whale. + +Soon, while the crews were awaiting the arrival of the ship, the body +showed symptoms of sinking with all its treasures unrifled. +Immediately, by Starbuck's orders, lines were secured to it at +different points, so that ere long every boat was a buoy; the sunken +whale being suspended a few inches beneath them by the cords. By +very heedful management, when the ship drew nigh, the whale was +transferred to her side, and was strongly secured there by the +stiffest fluke-chains, for it was plain that unless artificially +upheld, the body would at once sink to the bottom. + +It so chanced that almost upon first cutting into him with the +spade, the entire length of a corroded harpoon was found imbedded in +his flesh, on the lower part of the bunch before described. But as +the stumps of harpoons are frequently found in the dead bodies of +captured whales, with the flesh perfectly healed around them, and no +prominence of any kind to denote their place; therefore, there must +needs have been some other unknown reason in the present case fully +to account for the ulceration alluded to. But still more curious was +the fact of a lance-head of stone being found in him, not far from +the buried iron, the flesh perfectly firm about it. Who had darted +that stone lance? And when? It might have been darted by some Nor' +West Indian long before America was discovered. + +What other marvels might have been rummaged out of this monstrous +cabinet there is no telling. But a sudden stop was put to further +discoveries, by the ship's being unprecedentedly dragged over +sideways to the sea, owing to the body's immensely increasing +tendency to sink. However, Starbuck, who had the ordering of +affairs, hung on to it to the last; hung on to it so resolutely, +indeed, that when at length the ship would have been capsized, if +still persisting in locking arms with the body; then, when the +command was given to break clear from it, such was the immovable +strain upon the timber-heads to which the fluke-chains and cables +were fastened, that it was impossible to cast them off. Meantime +everything in the Pequod was aslant. To cross to the other side of +the deck was like walking up the steep gabled roof of a house. The +ship groaned and gasped. Many of the ivory inlayings of her bulwarks +and cabins were started from their places, by the unnatural +dislocation. In vain handspikes and crows were brought to bear upon +the immovable fluke-chains, to pry them adrift from the timberheads; +and so low had the whale now settled that the submerged ends could +not be at all approached, while every moment whole tons of +ponderosity seemed added to the sinking bulk, and the ship seemed on +the point of going over. + +"Hold on, hold on, won't ye?" cried Stubb to the body, "don't be in +such a devil of a hurry to sink! By thunder, men, we must do +something or go for it. No use prying there; avast, I say with your +handspikes, and run one of ye for a prayer book and a pen-knife, and +cut the big chains." + +"Knife? Aye, aye," cried Queequeg, and seizing the carpenter's heavy +hatchet, he leaned out of a porthole, and steel to iron, began +slashing at the largest fluke-chains. But a few strokes, full of +sparks, were given, when the exceeding strain effected the rest. +With a terrific snap, every fastening went adrift; the ship righted, +the carcase sank. + +Now, this occasional inevitable sinking of the recently killed Sperm +Whale is a very curious thing; nor has any fisherman yet adequately +accounted for it. Usually the dead Sperm Whale floats with great +buoyancy, with its side or belly considerably elevated above the +surface. If the only whales that thus sank were old, meagre, and +broken-hearted creatures, their pads of lard diminished and all their +bones heavy and rheumatic; then you might with some reason assert +that this sinking is caused by an uncommon specific gravity in the +fish so sinking, consequent upon this absence of buoyant matter in +him. But it is not so. For young whales, in the highest health, and +swelling with noble aspirations, prematurely cut off in the warm +flush and May of life, with all their panting lard about them; even +these brawny, buoyant heroes do sometimes sink. + +Be it said, however, that the Sperm Whale is far less liable to this +accident than any other species. Where one of that sort go down, +twenty Right Whales do. This difference in the species is no doubt +imputable in no small degree to the greater quantity of bone in the +Right Whale; his Venetian blinds alone sometimes weighing more than a +ton; from this incumbrance the Sperm Whale is wholly free. But there +are instances where, after the lapse of many hours or several days, +the sunken whale again rises, more buoyant than in life. But the +reason of this is obvious. Gases are generated in him; he swells to +a prodigious magnitude; becomes a sort of animal balloon. A +line-of-battle ship could hardly keep him under then. In the Shore +Whaling, on soundings, among the Bays of New Zealand, when a Right +Whale gives token of sinking, they fasten buoys to him, with plenty +of rope; so that when the body has gone down, they know where to look +for it when it shall have ascended again. + +It was not long after the sinking of the body that a cry was heard +from the Pequod's mast-heads, announcing that the Jungfrau was again +lowering her boats; though the only spout in sight was that of a +Fin-Back, belonging to the species of uncapturable whales, because of +its incredible power of swimming. Nevertheless, the Fin-Back's spout +is so similar to the Sperm Whale's, that by unskilful fishermen it is +often mistaken for it. And consequently Derick and all his host were +now in valiant chase of this unnearable brute. The Virgin crowding +all sail, made after her four young keels, and thus they all +disappeared far to leeward, still in bold, hopeful chase. + +Oh! many are the Fin-Backs, and many are the Dericks, my friend. + + + +CHAPTER 82 + +The Honour and Glory of Whaling. + + +There are some enterprises in which a careful disorderliness is the +true method. + +The more I dive into this matter of whaling, and push my researches +up to the very spring-head of it so much the more am I impressed with +its great honourableness and antiquity; and especially when I find so +many great demi-gods and heroes, prophets of all sorts, who one way +or other have shed distinction upon it, I am transported with the +reflection that I myself belong, though but subordinately, to so +emblazoned a fraternity. + +The gallant Perseus, a son of Jupiter, was the first whaleman; and to +the eternal honour of our calling be it said, that the first whale +attacked by our brotherhood was not killed with any sordid intent. +Those were the knightly days of our profession, when we only bore +arms to succor the distressed, and not to fill men's lamp-feeders. +Every one knows the fine story of Perseus and Andromeda; how the +lovely Andromeda, the daughter of a king, was tied to a rock on the +sea-coast, and as Leviathan was in the very act of carrying her off, +Perseus, the prince of whalemen, intrepidly advancing, harpooned the +monster, and delivered and married the maid. It was an admirable +artistic exploit, rarely achieved by the best harpooneers of the +present day; inasmuch as this Leviathan was slain at the very first +dart. And let no man doubt this Arkite story; for in the ancient +Joppa, now Jaffa, on the Syrian coast, in one of the Pagan temples, +there stood for many ages the vast skeleton of a whale, which the +city's legends and all the inhabitants asserted to be the identical +bones of the monster that Perseus slew. When the Romans took Joppa, +the same skeleton was carried to Italy in triumph. What seems most +singular and suggestively important in this story, is this: it was +from Joppa that Jonah set sail. + +Akin to the adventure of Perseus and Andromeda--indeed, by some +supposed to be indirectly derived from it--is that famous story of +St. George and the Dragon; which dragon I maintain to have been a +whale; for in many old chronicles whales and dragons are strangely +jumbled together, and often stand for each other. "Thou art as a +lion of the waters, and as a dragon of the sea," saith Ezekiel; +hereby, plainly meaning a whale; in truth, some versions of the Bible +use that word itself. Besides, it would much subtract from the glory +of the exploit had St. George but encountered a crawling reptile of +the land, instead of doing battle with the great monster of the deep. +Any man may kill a snake, but only a Perseus, a St. George, a +Coffin, have the heart in them to march boldly up to a whale. + +Let not the modern paintings of this scene mislead us; for though the +creature encountered by that valiant whaleman of old is vaguely +represented of a griffin-like shape, and though the battle is +depicted on land and the saint on horseback, yet considering the +great ignorance of those times, when the true form of the whale was +unknown to artists; and considering that as in Perseus' case, St. +George's whale might have crawled up out of the sea on the beach; and +considering that the animal ridden by St. George might have been only +a large seal, or sea-horse; bearing all this in mind, it will not +appear altogether incompatible with the sacred legend and the +ancientest draughts of the scene, to hold this so-called dragon no +other than the great Leviathan himself. In fact, placed before the +strict and piercing truth, this whole story will fare like that fish, +flesh, and fowl idol of the Philistines, Dagon by name; who being +planted before the ark of Israel, his horse's head and both the palms +of his hands fell off from him, and only the stump or fishy part of +him remained. Thus, then, one of our own noble stamp, even a +whaleman, is the tutelary guardian of England; and by good rights, we +harpooneers of Nantucket should be enrolled in the most noble order +of St. George. And therefore, let not the knights of that honourable +company (none of whom, I venture to say, have ever had to do with a +whale like their great patron), let them never eye a Nantucketer with +disdain, since even in our woollen frocks and tarred trowsers we are +much better entitled to St. George's decoration than they. + +Whether to admit Hercules among us or not, concerning this I long +remained dubious: for though according to the Greek mythologies, that +antique Crockett and Kit Carson--that brawny doer of rejoicing good +deeds, was swallowed down and thrown up by a whale; still, whether +that strictly makes a whaleman of him, that might be mooted. It +nowhere appears that he ever actually harpooned his fish, unless, +indeed, from the inside. Nevertheless, he may be deemed a sort of +involuntary whaleman; at any rate the whale caught him, if he did not +the whale. I claim him for one of our clan. + +But, by the best contradictory authorities, this Grecian story of +Hercules and the whale is considered to be derived from the still +more ancient Hebrew story of Jonah and the whale; and vice versa; +certainly they are very similar. If I claim the demigod then, why +not the prophet? + +Nor do heroes, saints, demigods, and prophets alone comprise the +whole roll of our order. Our grand master is still to be named; for +like royal kings of old times, we find the head waters of our +fraternity in nothing short of the great gods themselves. That +wondrous oriental story is now to be rehearsed from the Shaster, +which gives us the dread Vishnoo, one of the three persons in the +godhead of the Hindoos; gives us this divine Vishnoo himself for our +Lord;--Vishnoo, who, by the first of his ten earthly incarnations, +has for ever set apart and sanctified the whale. When Brahma, or the +God of Gods, saith the Shaster, resolved to recreate the world after +one of its periodical dissolutions, he gave birth to Vishnoo, to +preside over the work; but the Vedas, or mystical books, whose +perusal would seem to have been indispensable to Vishnoo before +beginning the creation, and which therefore must have contained +something in the shape of practical hints to young architects, these +Vedas were lying at the bottom of the waters; so Vishnoo became +incarnate in a whale, and sounding down in him to the uttermost +depths, rescued the sacred volumes. Was not this Vishnoo a whaleman, +then? even as a man who rides a horse is called a horseman? + +Perseus, St. George, Hercules, Jonah, and Vishnoo! there's a +member-roll for you! What club but the whaleman's can head off like +that? + + + +CHAPTER 83 + +Jonah Historically Regarded. + + +Reference was made to the historical story of Jonah and the whale in +the preceding chapter. Now some Nantucketers rather distrust this +historical story of Jonah and the whale. But then there were some +sceptical Greeks and Romans, who, standing out from the orthodox +pagans of their times, equally doubted the story of Hercules and the +whale, and Arion and the dolphin; and yet their doubting those +traditions did not make those traditions one whit the less facts, for +all that. + +One old Sag-Harbor whaleman's chief reason for questioning the Hebrew +story was this:--He had one of those quaint old-fashioned Bibles, +embellished with curious, unscientific plates; one of which +represented Jonah's whale with two spouts in his head--a peculiarity +only true with respect to a species of the Leviathan (the Right +Whale, and the varieties of that order), concerning which the +fishermen have this saying, "A penny roll would choke him"; his +swallow is so very small. But, to this, Bishop Jebb's anticipative +answer is ready. It is not necessary, hints the Bishop, that we +consider Jonah as tombed in the whale's belly, but as temporarily +lodged in some part of his mouth. And this seems reasonable enough +in the good Bishop. For truly, the Right Whale's mouth would +accommodate a couple of whist-tables, and comfortably seat all the +players. Possibly, too, Jonah might have ensconced himself in a +hollow tooth; but, on second thoughts, the Right Whale is toothless. + +Another reason which Sag-Harbor (he went by that name) urged for his +want of faith in this matter of the prophet, was something obscurely +in reference to his incarcerated body and the whale's gastric juices. +But this objection likewise falls to the ground, because a German +exegetist supposes that Jonah must have taken refuge in the floating +body of a DEAD whale--even as the French soldiers in the Russian +campaign turned their dead horses into tents, and crawled into them. +Besides, it has been divined by other continental commentators, that +when Jonah was thrown overboard from the Joppa ship, he straightway +effected his escape to another vessel near by, some vessel with a +whale for a figure-head; and, I would add, possibly called "The +Whale," as some craft are nowadays christened the "Shark," the +"Gull," the "Eagle." Nor have there been wanting learned exegetists +who have opined that the whale mentioned in the book of Jonah merely +meant a life-preserver--an inflated bag of wind--which the endangered +prophet swam to, and so was saved from a watery doom. Poor +Sag-Harbor, therefore, seems worsted all round. But he had still +another reason for his want of faith. It was this, if I remember +right: Jonah was swallowed by the whale in the Mediterranean Sea, and +after three days he was vomited up somewhere within three days' +journey of Nineveh, a city on the Tigris, very much more than three +days' journey across from the nearest point of the Mediterranean +coast. How is that? + +But was there no other way for the whale to land the prophet within +that short distance of Nineveh? Yes. He might have carried him +round by the way of the Cape of Good Hope. But not to speak of the +passage through the whole length of the Mediterranean, and another +passage up the Persian Gulf and Red Sea, such a supposition would +involve the complete circumnavigation of all Africa in three days, +not to speak of the Tigris waters, near the site of Nineveh, being +too shallow for any whale to swim in. Besides, this idea of Jonah's +weathering the Cape of Good Hope at so early a day would wrest the +honour of the discovery of that great headland from Bartholomew Diaz, +its reputed discoverer, and so make modern history a liar. + +But all these foolish arguments of old Sag-Harbor only evinced his +foolish pride of reason--a thing still more reprehensible in him, +seeing that he had but little learning except what he had picked up +from the sun and the sea. I say it only shows his foolish, impious +pride, and abominable, devilish rebellion against the reverend +clergy. For by a Portuguese Catholic priest, this very idea of +Jonah's going to Nineveh via the Cape of Good Hope was advanced as a +signal magnification of the general miracle. And so it was. +Besides, to this day, the highly enlightened Turks devoutly believe +in the historical story of Jonah. And some three centuries ago, an +English traveller in old Harris's Voyages, speaks of a Turkish Mosque +built in honour of Jonah, in which Mosque was a miraculous lamp that +burnt without any oil. + + + +CHAPTER 84 + +Pitchpoling. + + +To make them run easily and swiftly, the axles of carriages are +anointed; and for much the same purpose, some whalers perform an +analogous operation upon their boat; they grease the bottom. Nor is +it to be doubted that as such a procedure can do no harm, it may +possibly be of no contemptible advantage; considering that oil and +water are hostile; that oil is a sliding thing, and that the object +in view is to make the boat slide bravely. Queequeg believed +strongly in anointing his boat, and one morning not long after the +German ship Jungfrau disappeared, took more than customary pains in +that occupation; crawling under its bottom, where it hung over the +side, and rubbing in the unctuousness as though diligently seeking to +insure a crop of hair from the craft's bald keel. He seemed to be +working in obedience to some particular presentiment. Nor did it +remain unwarranted by the event. + +Towards noon whales were raised; but so soon as the ship sailed down +to them, they turned and fled with swift precipitancy; a disordered +flight, as of Cleopatra's barges from Actium. + +Nevertheless, the boats pursued, and Stubb's was foremost. By great +exertion, Tashtego at last succeeded in planting one iron; but the +stricken whale, without at all sounding, still continued his +horizontal flight, with added fleetness. Such unintermitted +strainings upon the planted iron must sooner or later inevitably +extract it. It became imperative to lance the flying whale, or be +content to lose him. But to haul the boat up to his flank was +impossible, he swam so fast and furious. What then remained? + +Of all the wondrous devices and dexterities, the sleights of hand and +countless subtleties, to which the veteran whaleman is so often +forced, none exceed that fine manoeuvre with the lance called +pitchpoling. Small sword, or broad sword, in all its exercises +boasts nothing like it. It is only indispensable with an inveterate +running whale; its grand fact and feature is the wonderful distance +to which the long lance is accurately darted from a violently +rocking, jerking boat, under extreme headway. Steel and wood +included, the entire spear is some ten or twelve feet in length; the +staff is much slighter than that of the harpoon, and also of a +lighter material--pine. It is furnished with a small rope called a +warp, of considerable length, by which it can be hauled back to the +hand after darting. + +But before going further, it is important to mention here, that +though the harpoon may be pitchpoled in the same way with the lance, +yet it is seldom done; and when done, is still less frequently +successful, on account of the greater weight and inferior length of +the harpoon as compared with the lance, which in effect become +serious drawbacks. As a general thing, therefore, you must first +get fast to a whale, before any pitchpoling comes into play. + +Look now at Stubb; a man who from his humorous, deliberate coolness +and equanimity in the direst emergencies, was specially qualified to +excel in pitchpoling. Look at him; he stands upright in the tossed +bow of the flying boat; wrapt in fleecy foam, the towing whale is +forty feet ahead. Handling the long lance lightly, glancing twice or +thrice along its length to see if it be exactly straight, Stubb +whistlingly gathers up the coil of the warp in one hand, so as to +secure its free end in his grasp, leaving the rest unobstructed. +Then holding the lance full before his waistband's middle, he levels +it at the whale; when, covering him with it, he steadily depresses +the butt-end in his hand, thereby elevating the point till the weapon +stands fairly balanced upon his palm, fifteen feet in the air. He +minds you somewhat of a juggler, balancing a long staff on his chin. +Next moment with a rapid, nameless impulse, in a superb lofty arch the +bright steel spans the foaming distance, and quivers in the life spot +of the whale. Instead of sparkling water, he now spouts red blood. + +"That drove the spigot out of him!" cried Stubb. "'Tis July's +immortal Fourth; all fountains must run wine today! Would now, it +were old Orleans whiskey, or old Ohio, or unspeakable old +Monongahela! Then, Tashtego, lad, I'd have ye hold a canakin to the +jet, and we'd drink round it! Yea, verily, hearts alive, we'd brew +choice punch in the spread of his spout-hole there, and from that +live punch-bowl quaff the living stuff." + +Again and again to such gamesome talk, the dexterous dart is +repeated, the spear returning to its master like a greyhound held in +skilful leash. The agonized whale goes into his flurry; the tow-line +is slackened, and the pitchpoler dropping astern, folds his hands, +and mutely watches the monster die. + + + +CHAPTER 85 + +The Fountain. + + +That for six thousand years--and no one knows how many millions of +ages before--the great whales should have been spouting all over the +sea, and sprinkling and mistifying the gardens of the deep, as with +so many sprinkling or mistifying pots; and that for some centuries +back, thousands of hunters should have been close by the fountain of +the whale, watching these sprinklings and spoutings--that all this +should be, and yet, that down to this blessed minute (fifteen and a +quarter minutes past one o'clock P.M. of this sixteenth day of +December, A.D. 1851), it should still remain a problem, whether these +spoutings are, after all, really water, or nothing but vapour--this is +surely a noteworthy thing. + +Let us, then, look at this matter, along with some interesting items +contingent. Every one knows that by the peculiar cunning of their +gills, the finny tribes in general breathe the air which at all times +is combined with the element in which they swim; hence, a herring or +a cod might live a century, and never once raise its head above the +surface. But owing to his marked internal structure which gives him +regular lungs, like a human being's, the whale can only live by +inhaling the disengaged air in the open atmosphere. Wherefore the +necessity for his periodical visits to the upper world. But he +cannot in any degree breathe through his mouth, for, in his ordinary +attitude, the Sperm Whale's mouth is buried at least eight feet +beneath the surface; and what is still more, his windpipe has no +connexion with his mouth. No, he breathes through his spiracle +alone; and this is on the top of his head. + +If I say, that in any creature breathing is only a function +indispensable to vitality, inasmuch as it withdraws from the air a +certain element, which being subsequently brought into contact with +the blood imparts to the blood its vivifying principle, I do not +think I shall err; though I may possibly use some superfluous +scientific words. Assume it, and it follows that if all the blood in +a man could be aerated with one breath, he might then seal up his +nostrils and not fetch another for a considerable time. That is to +say, he would then live without breathing. Anomalous as it may seem, +this is precisely the case with the whale, who systematically lives, +by intervals, his full hour and more (when at the bottom) without +drawing a single breath, or so much as in any way inhaling a particle +of air; for, remember, he has no gills. How is this? Between his +ribs and on each side of his spine he is supplied with a remarkable +involved Cretan labyrinth of vermicelli-like vessels, which vessels, +when he quits the surface, are completely distended with oxygenated +blood. So that for an hour or more, a thousand fathoms in the sea, +he carries a surplus stock of vitality in him, just as the camel +crossing the waterless desert carries a surplus supply of drink for +future use in its four supplementary stomachs. The anatomical fact +of this labyrinth is indisputable; and that the supposition founded +upon it is reasonable and true, seems the more cogent to me, when I +consider the otherwise inexplicable obstinacy of that leviathan in +HAVING HIS SPOUTINGS OUT, as the fishermen phrase it. This is what I +mean. If unmolested, upon rising to the surface, the Sperm Whale +will continue there for a period of time exactly uniform with all his +other unmolested risings. Say he stays eleven minutes, and jets +seventy times, that is, respires seventy breaths; then whenever he +rises again, he will be sure to have his seventy breaths over again, +to a minute. Now, if after he fetches a few breaths you alarm him, +so that he sounds, he will be always dodging up again to make good +his regular allowance of air. And not till those seventy breaths are +told, will he finally go down to stay out his full term below. +Remark, however, that in different individuals these rates are +different; but in any one they are alike. Now, why should the whale +thus insist upon having his spoutings out, unless it be to replenish +his reservoir of air, ere descending for good? How obvious is it, +too, that this necessity for the whale's rising exposes him to all +the fatal hazards of the chase. For not by hook or by net could +this vast leviathan be caught, when sailing a thousand fathoms +beneath the sunlight. Not so much thy skill, then, O hunter, as the +great necessities that strike the victory to thee! + +In man, breathing is incessantly going on--one breath only serving +for two or three pulsations; so that whatever other business he has +to attend to, waking or sleeping, breathe he must, or die he will. +But the Sperm Whale only breathes about one seventh or Sunday of his +time. + +It has been said that the whale only breathes through his spout-hole; +if it could truthfully be added that his spouts are mixed with water, +then I opine we should be furnished with the reason why his sense of +smell seems obliterated in him; for the only thing about him that at +all answers to his nose is that identical spout-hole; and being so +clogged with two elements, it could not be expected to have the power +of smelling. But owing to the mystery of the spout--whether it be +water or whether it be vapour--no absolute certainty can as yet be +arrived at on this head. Sure it is, nevertheless, that the Sperm +Whale has no proper olfactories. But what does he want of them? No +roses, no violets, no Cologne-water in the sea. + +Furthermore, as his windpipe solely opens into the tube of his +spouting canal, and as that long canal--like the grand Erie Canal--is +furnished with a sort of locks (that open and shut) for the downward +retention of air or the upward exclusion of water, therefore the +whale has no voice; unless you insult him by saying, that when he so +strangely rumbles, he talks through his nose. But then again, what +has the whale to say? Seldom have I known any profound being that +had anything to say to this world, unless forced to stammer out +something by way of getting a living. Oh! happy that the world is +such an excellent listener! + +Now, the spouting canal of the Sperm Whale, chiefly intended as it is +for the conveyance of air, and for several feet laid along, +horizontally, just beneath the upper surface of his head, and a +little to one side; this curious canal is very much like a gas-pipe +laid down in a city on one side of a street. But the question +returns whether this gas-pipe is also a water-pipe; in other words, +whether the spout of the Sperm Whale is the mere vapour of the exhaled +breath, or whether that exhaled breath is mixed with water taken in +at the mouth, and discharged through the spiracle. It is certain +that the mouth indirectly communicates with the spouting canal; but +it cannot be proved that this is for the purpose of discharging water +through the spiracle. Because the greatest necessity for so doing +would seem to be, when in feeding he accidentally takes in water. +But the Sperm Whale's food is far beneath the surface, and there he +cannot spout even if he would. Besides, if you regard him very +closely, and time him with your watch, you will find that when +unmolested, there is an undeviating rhyme between the periods of his +jets and the ordinary periods of respiration. + +But why pester one with all this reasoning on the subject? Speak +out! You have seen him spout; then declare what the spout is; can +you not tell water from air? My dear sir, in this world it is not so +easy to settle these plain things. I have ever found your plain +things the knottiest of all. And as for this whale spout, you might +almost stand in it, and yet be undecided as to what it is precisely. + +The central body of it is hidden in the snowy sparkling mist +enveloping it; and how can you certainly tell whether any water falls +from it, when, always, when you are close enough to a whale to get a +close view of his spout, he is in a prodigious commotion, the water +cascading all around him. And if at such times you should think that +you really perceived drops of moisture in the spout, how do you know +that they are not merely condensed from its vapour; or how do you know +that they are not those identical drops superficially lodged in the +spout-hole fissure, which is countersunk into the summit of the +whale's head? For even when tranquilly swimming through the mid-day +sea in a calm, with his elevated hump sun-dried as a dromedary's in +the desert; even then, the whale always carries a small basin of +water on his head, as under a blazing sun you will sometimes see a +cavity in a rock filled up with rain. + +Nor is it at all prudent for the hunter to be over curious touching +the precise nature of the whale spout. It will not do for him to be +peering into it, and putting his face in it. You cannot go with your +pitcher to this fountain and fill it, and bring it away. For even +when coming into slight contact with the outer, vapoury shreds of the +jet, which will often happen, your skin will feverishly smart, from +the acridness of the thing so touching it. And I know one, who +coming into still closer contact with the spout, whether with some +scientific object in view, or otherwise, I cannot say, the skin +peeled off from his cheek and arm. Wherefore, among whalemen, the +spout is deemed poisonous; they try to evade it. Another thing; I +have heard it said, and I do not much doubt it, that if the jet is +fairly spouted into your eyes, it will blind you. The wisest thing +the investigator can do then, it seems to me, is to let this deadly +spout alone. + +Still, we can hypothesize, even if we cannot prove and establish. My +hypothesis is this: that the spout is nothing but mist. And besides +other reasons, to this conclusion I am impelled, by considerations +touching the great inherent dignity and sublimity of the Sperm Whale; +I account him no common, shallow being, inasmuch as it is an +undisputed fact that he is never found on soundings, or near shores; +all other whales sometimes are. He is both ponderous and profound. +And I am convinced that from the heads of all ponderous profound +beings, such as Plato, Pyrrho, the Devil, Jupiter, Dante, and so on, +there always goes up a certain semi-visible steam, while in the act +of thinking deep thoughts. While composing a little treatise on +Eternity, I had the curiosity to place a mirror before me; and ere +long saw reflected there, a curious involved worming and undulation +in the atmosphere over my head. The invariable moisture of my hair, +while plunged in deep thought, after six cups of hot tea in my thin +shingled attic, of an August noon; this seems an additional argument +for the above supposition. + +And how nobly it raises our conceit of the mighty, misty monster, to +behold him solemnly sailing through a calm tropical sea; his vast, +mild head overhung by a canopy of vapour, engendered by his +incommunicable contemplations, and that vapour--as you will sometimes +see it--glorified by a rainbow, as if Heaven itself had put its seal +upon his thoughts. For, d'ye see, rainbows do not visit the clear +air; they only irradiate vapour. And so, through all the thick mists +of the dim doubts in my mind, divine intuitions now and then shoot, +enkindling my fog with a heavenly ray. And for this I thank God; for +all have doubts; many deny; but doubts or denials, few along with +them, have intuitions. Doubts of all things earthly, and intuitions +of some things heavenly; this combination makes neither believer nor +infidel, but makes a man who regards them both with equal eye. + + + +CHAPTER 86 + +The Tail. + + +Other poets have warbled the praises of the soft eye of the antelope, +and the lovely plumage of the bird that never alights; less +celestial, I celebrate a tail. + +Reckoning the largest sized Sperm Whale's tail to begin at that point +of the trunk where it tapers to about the girth of a man, it +comprises upon its upper surface alone, an area of at least fifty +square feet. The compact round body of its root expands into two +broad, firm, flat palms or flukes, gradually shoaling away to less +than an inch in thickness. At the crotch or junction, these flukes +slightly overlap, then sideways recede from each other like wings, +leaving a wide vacancy between. In no living thing are the lines of +beauty more exquisitely defined than in the crescentic borders of +these flukes. At its utmost expansion in the full grown whale, the +tail will considerably exceed twenty feet across. + +The entire member seems a dense webbed bed of welded sinews; but cut +into it, and you find that three distinct strata compose it:--upper, +middle, and lower. The fibres in the upper and lower layers, are +long and horizontal; those of the middle one, very short, and running +crosswise between the outside layers. This triune structure, as much +as anything else, imparts power to the tail. To the student of old +Roman walls, the middle layer will furnish a curious parallel to the +thin course of tiles always alternating with the stone in those +wonderful relics of the antique, and which undoubtedly contribute so +much to the great strength of the masonry. + +But as if this vast local power in the tendinous tail were not +enough, the whole bulk of the leviathan is knit over with a warp and +woof of muscular fibres and filaments, which passing on either side +the loins and running down into the flukes, insensibly blend with +them, and largely contribute to their might; so that in the tail the +confluent measureless force of the whole whale seems concentrated to +a point. Could annihilation occur to matter, this were the thing to +do it. + +Nor does this--its amazing strength, at all tend to cripple the +graceful flexion of its motions; where infantileness of ease +undulates through a Titanism of power. On the contrary, those +motions derive their most appalling beauty from it. Real strength +never impairs beauty or harmony, but it often bestows it; and in +everything imposingly beautiful, strength has much to do with the +magic. Take away the tied tendons that all over seem bursting from +the marble in the carved Hercules, and its charm would be gone. As +devout Eckerman lifted the linen sheet from the naked corpse of +Goethe, he was overwhelmed with the massive chest of the man, that +seemed as a Roman triumphal arch. When Angelo paints even God the +Father in human form, mark what robustness is there. And whatever +they may reveal of the divine love in the Son, the soft, curled, +hermaphroditical Italian pictures, in which his idea has been most +successfully embodied; these pictures, so destitute as they are of +all brawniness, hint nothing of any power, but the mere negative, +feminine one of submission and endurance, which on all hands it is +conceded, form the peculiar practical virtues of his teachings. + +Such is the subtle elasticity of the organ I treat of, that whether +wielded in sport, or in earnest, or in anger, whatever be the mood it +be in, its flexions are invariably marked by exceeding grace. +Therein no fairy's arm can transcend it. + +Five great motions are peculiar to it. First, when used as a fin for +progression; Second, when used as a mace in battle; Third, in +sweeping; Fourth, in lobtailing; Fifth, in peaking flukes. + +First: Being horizontal in its position, the Leviathan's tail acts in +a different manner from the tails of all other sea creatures. It +never wriggles. In man or fish, wriggling is a sign of inferiority. +To the whale, his tail is the sole means of propulsion. Scroll-wise +coiled forwards beneath the body, and then rapidly sprung backwards, +it is this which gives that singular darting, leaping motion to the +monster when furiously swimming. His side-fins only serve to steer +by. + +Second: It is a little significant, that while one sperm whale only +fights another sperm whale with his head and jaw, nevertheless, in +his conflicts with man, he chiefly and contemptuously uses his tail. +In striking at a boat, he swiftly curves away his flukes from it, and +the blow is only inflicted by the recoil. If it be made in the +unobstructed air, especially if it descend to its mark, the stroke is +then simply irresistible. No ribs of man or boat can withstand it. +Your only salvation lies in eluding it; but if it comes sideways +through the opposing water, then partly owing to the light buoyancy +of the whale boat, and the elasticity of its materials, a cracked +rib or a dashed plank or two, a sort of stitch in the side, is +generally the most serious result. These submerged side blows are so +often received in the fishery, that they are accounted mere child's +play. Some one strips off a frock, and the hole is stopped. + +Third: I cannot demonstrate it, but it seems to me, that in the whale +the sense of touch is concentrated in the tail; for in this respect +there is a delicacy in it only equalled by the daintiness of the +elephant's trunk. This delicacy is chiefly evinced in the action of +sweeping, when in maidenly gentleness the whale with a certain soft +slowness moves his immense flukes from side to side upon the surface of +the sea; and if he feel but a sailor's whisker, woe to that sailor, +whiskers and all. What tenderness there is in that preliminary +touch! Had this tail any prehensile power, I should straightway +bethink me of Darmonodes' elephant that so frequented the +flower-market, and with low salutations presented nosegays to +damsels, and then caressed their zones. On more accounts than one, a +pity it is that the whale does not possess this prehensile virtue in +his tail; for I have heard of yet another elephant, that when wounded +in the fight, curved round his trunk and extracted the dart. + +Fourth: Stealing unawares upon the whale in the fancied security of +the middle of solitary seas, you find him unbent from the vast +corpulence of his dignity, and kitten-like, he plays on the ocean as +if it were a hearth. But still you see his power in his play. The +broad palms of his tail are flirted high into the air; then smiting +the surface, the thunderous concussion resounds for miles. You would +almost think a great gun had been discharged; and if you noticed the +light wreath of vapour from the spiracle at his other extremity, you +would think that that was the smoke from the touch-hole. + +Fifth: As in the ordinary floating posture of the leviathan the +flukes lie considerably below the level of his back, they are then +completely out of sight beneath the surface; but when he is about to +plunge into the deeps, his entire flukes with at least thirty feet of +his body are tossed erect in the air, and so remain vibrating a +moment, till they downwards shoot out of view. Excepting the sublime +BREACH--somewhere else to be described--this peaking of the whale's +flukes is perhaps the grandest sight to be seen in all animated +nature. Out of the bottomless profundities the gigantic tail seems +spasmodically snatching at the highest heaven. So in dreams, have I +seen majestic Satan thrusting forth his tormented colossal claw from +the flame Baltic of Hell. But in gazing at such scenes, it is all in +all what mood you are in; if in the Dantean, the devils will occur to +you; if in that of Isaiah, the archangels. Standing at the mast-head +of my ship during a sunrise that crimsoned sky and sea, I once saw a +large herd of whales in the east, all heading towards the sun, and +for a moment vibrating in concert with peaked flukes. As it seemed +to me at the time, such a grand embodiment of adoration of the gods +was never beheld, even in Persia, the home of the fire worshippers. +As Ptolemy Philopater testified of the African elephant, I then +testified of the whale, pronouncing him the most devout of all +beings. For according to King Juba, the military elephants of +antiquity often hailed the morning with their trunks uplifted in the +profoundest silence. + +The chance comparison in this chapter, between the whale and the +elephant, so far as some aspects of the tail of the one and the trunk +of the other are concerned, should not tend to place those two +opposite organs on an equality, much less the creatures to which they +respectively belong. For as the mightiest elephant is but a terrier +to Leviathan, so, compared with Leviathan's tail, his trunk is but +the stalk of a lily. The most direful blow from the elephant's trunk +were as the playful tap of a fan, compared with the measureless crush +and crash of the sperm whale's ponderous flukes, which in repeated +instances have one after the other hurled entire boats with all their +oars and crews into the air, very much as an Indian juggler tosses +his balls.* + + +*Though all comparison in the way of general bulk between the whale +and the elephant is preposterous, inasmuch as in that particular the +elephant stands in much the same respect to the whale that a dog does +to the elephant; nevertheless, there are not wanting some points of +curious similitude; among these is the spout. It is well known that +the elephant will often draw up water or dust in his trunk, and then +elevating it, jet it forth in a stream. + + +The more I consider this mighty tail, the more do I deplore my +inability to express it. At times there are gestures in it, which, +though they would well grace the hand of man, remain wholly +inexplicable. In an extensive herd, so remarkable, occasionally, are +these mystic gestures, that I have heard hunters who have declared +them akin to Free-Mason signs and symbols; that the whale, indeed, by +these methods intelligently conversed with the world. Nor are there +wanting other motions of the whale in his general body, full of +strangeness, and unaccountable to his most experienced assailant. +Dissect him how I may, then, I but go skin deep; I know him not, +and never will. But if I know not even the tail of this whale, how +understand his head? much more, how comprehend his face, when face he +has none? Thou shalt see my back parts, my tail, he seems to say, +but my face shall not be seen. But I cannot completely make out his +back parts; and hint what he will about his face, I say again he has +no face. + + + +CHAPTER 87 + +The Grand Armada. + + +The long and narrow peninsula of Malacca, extending south-eastward +from the territories of Birmah, forms the most southerly point of all +Asia. In a continuous line from that peninsula stretch the long +islands of Sumatra, Java, Bally, and Timor; which, with many others, +form a vast mole, or rampart, lengthwise connecting Asia with +Australia, and dividing the long unbroken Indian ocean from the +thickly studded oriental archipelagoes. This rampart is pierced by +several sally-ports for the convenience of ships and whales; +conspicuous among which are the straits of Sunda and Malacca. By the +straits of Sunda, chiefly, vessels bound to China from the west, +emerge into the China seas. + +Those narrow straits of Sunda divide Sumatra from Java; and standing +midway in that vast rampart of islands, buttressed by that bold green +promontory, known to seamen as Java Head; they not a little +correspond to the central gateway opening into some vast walled +empire: and considering the inexhaustible wealth of spices, and +silks, and jewels, and gold, and ivory, with which the thousand +islands of that oriental sea are enriched, it seems a significant +provision of nature, that such treasures, by the very formation of +the land, should at least bear the appearance, however ineffectual, +of being guarded from the all-grasping western world. The shores of +the Straits of Sunda are unsupplied with those domineering fortresses +which guard the entrances to the Mediterranean, the Baltic, and the +Propontis. Unlike the Danes, these Orientals do not demand the +obsequious homage of lowered top-sails from the endless procession of +ships before the wind, which for centuries past, by night and by day, +have passed between the islands of Sumatra and Java, freighted with +the costliest cargoes of the east. But while they freely waive a +ceremonial like this, they do by no means renounce their claim to +more solid tribute. + +Time out of mind the piratical proas of the Malays, lurking among the +low shaded coves and islets of Sumatra, have sallied out upon the +vessels sailing through the straits, fiercely demanding tribute at +the point of their spears. Though by the repeated bloody +chastisements they have received at the hands of European cruisers, +the audacity of these corsairs has of late been somewhat repressed; +yet, even at the present day, we occasionally hear of English and +American vessels, which, in those waters, have been remorselessly +boarded and pillaged. + +With a fair, fresh wind, the Pequod was now drawing nigh to these +straits; Ahab purposing to pass through them into the Javan sea, and +thence, cruising northwards, over waters known to be frequented here +and there by the Sperm Whale, sweep inshore by the Philippine +Islands, and gain the far coast of Japan, in time for the great +whaling season there. By these means, the circumnavigating Pequod +would sweep almost all the known Sperm Whale cruising grounds of the +world, previous to descending upon the Line in the Pacific; where +Ahab, though everywhere else foiled in his pursuit, firmly counted +upon giving battle to Moby Dick, in the sea he was most known to +frequent; and at a season when he might most reasonably be presumed +to be haunting it. + +But how now? in this zoned quest, does Ahab touch no land? does his +crew drink air? Surely, he will stop for water. Nay. For a long +time, now, the circus-running sun has raced within his fiery ring, +and needs no sustenance but what's in himself. So Ahab. Mark this, +too, in the whaler. While other hulls are loaded down with alien +stuff, to be transferred to foreign wharves; the world-wandering +whale-ship carries no cargo but herself and crew, their weapons and +their wants. She has a whole lake's contents bottled in her ample +hold. She is ballasted with utilities; not altogether with unusable +pig-lead and kentledge. She carries years' water in her. Clear old +prime Nantucket water; which, when three years afloat, the +Nantucketer, in the Pacific, prefers to drink before the brackish +fluid, but yesterday rafted off in casks, from the Peruvian or Indian +streams. Hence it is, that, while other ships may have gone to China +from New York, and back again, touching at a score of ports, the +whale-ship, in all that interval, may not have sighted one grain of +soil; her crew having seen no man but floating seamen like +themselves. So that did you carry them the news that another flood +had come; they would only answer--"Well, boys, here's the ark!" + +Now, as many Sperm Whales had been captured off the western coast of +Java, in the near vicinity of the Straits of Sunda; indeed, as most +of the ground, roundabout, was generally recognised by the fishermen +as an excellent spot for cruising; therefore, as the Pequod gained +more and more upon Java Head, the look-outs were repeatedly hailed, +and admonished to keep wide awake. But though the green palmy cliffs +of the land soon loomed on the starboard bow, and with delighted +nostrils the fresh cinnamon was snuffed in the air, yet not a single +jet was descried. Almost renouncing all thought of falling in with +any game hereabouts, the ship had well nigh entered the straits, when +the customary cheering cry was heard from aloft, and ere long a +spectacle of singular magnificence saluted us. + +But here be it premised, that owing to the unwearied activity with +which of late they have been hunted over all four oceans, the Sperm +Whales, instead of almost invariably sailing in small detached +companies, as in former times, are now frequently met with in +extensive herds, sometimes embracing so great a multitude, that it +would almost seem as if numerous nations of them had sworn solemn +league and covenant for mutual assistance and protection. To this +aggregation of the Sperm Whale into such immense caravans, may be +imputed the circumstance that even in the best cruising grounds, you +may now sometimes sail for weeks and months together, without being +greeted by a single spout; and then be suddenly saluted by what +sometimes seems thousands on thousands. + +Broad on both bows, at the distance of some two or three miles, and +forming a great semicircle, embracing one half of the level horizon, +a continuous chain of whale-jets were up-playing and sparkling in the +noon-day air. Unlike the straight perpendicular twin-jets of the +Right Whale, which, dividing at top, fall over in two branches, like +the cleft drooping boughs of a willow, the single forward-slanting +spout of the Sperm Whale presents a thick curled bush of white mist, +continually rising and falling away to leeward. + +Seen from the Pequod's deck, then, as she would rise on a high hill +of the sea, this host of vapoury spouts, individually curling up into +the air, and beheld through a blending atmosphere of bluish haze, +showed like the thousand cheerful chimneys of some dense metropolis, +descried of a balmy autumnal morning, by some horseman on a height. + +As marching armies approaching an unfriendly defile in the mountains, +accelerate their march, all eagerness to place that perilous passage +in their rear, and once more expand in comparative security upon the +plain; even so did this vast fleet of whales now seem hurrying +forward through the straits; gradually contracting the wings of their +semicircle, and swimming on, in one solid, but still crescentic +centre. + +Crowding all sail the Pequod pressed after them; the harpooneers +handling their weapons, and loudly cheering from the heads of their +yet suspended boats. If the wind only held, little doubt had they, +that chased through these Straits of Sunda, the vast host would only +deploy into the Oriental seas to witness the capture of not a few of +their number. And who could tell whether, in that congregated +caravan, Moby Dick himself might not temporarily be swimming, like +the worshipped white-elephant in the coronation procession of the +Siamese! So with stun-sail piled on stun-sail, we sailed along, +driving these leviathans before us; when, of a sudden, the voice of +Tashtego was heard, loudly directing attention to something in our +wake. + +Corresponding to the crescent in our van, we beheld another in our +rear. It seemed formed of detached white vapours, rising and falling +something like the spouts of the whales; only they did not so +completely come and go; for they constantly hovered, without finally +disappearing. Levelling his glass at this sight, Ahab quickly +revolved in his pivot-hole, crying, "Aloft there, and rig whips and +buckets to wet the sails;--Malays, sir, and after us!" + +As if too long lurking behind the headlands, till the Pequod should +fairly have entered the straits, these rascally Asiatics were now in +hot pursuit, to make up for their over-cautious delay. But when the +swift Pequod, with a fresh leading wind, was herself in hot chase; +how very kind of these tawny philanthropists to assist in speeding +her on to her own chosen pursuit,--mere riding-whips and rowels to +her, that they were. As with glass under arm, Ahab to-and-fro paced +the deck; in his forward turn beholding the monsters he chased, and +in the after one the bloodthirsty pirates chasing him; some such +fancy as the above seemed his. And when he glanced upon the green +walls of the watery defile in which the ship was then sailing, and +bethought him that through that gate lay the route to his vengeance, +and beheld, how that through that same gate he was now both chasing +and being chased to his deadly end; and not only that, but a herd of +remorseless wild pirates and inhuman atheistical devils were +infernally cheering him on with their curses;--when all these +conceits had passed through his brain, Ahab's brow was left gaunt and +ribbed, like the black sand beach after some stormy tide has been +gnawing it, without being able to drag the firm thing from its place. + +But thoughts like these troubled very few of the reckless crew; and +when, after steadily dropping and dropping the pirates astern, the +Pequod at last shot by the vivid green Cockatoo Point on the Sumatra +side, emerging at last upon the broad waters beyond; then, the +harpooneers seemed more to grieve that the swift whales had been +gaining upon the ship, than to rejoice that the ship had so +victoriously gained upon the Malays. But still driving on in the +wake of the whales, at length they seemed abating their speed; +gradually the ship neared them; and the wind now dying away, word was +passed to spring to the boats. But no sooner did the herd, by some +presumed wonderful instinct of the Sperm Whale, become notified of +the three keels that were after them,--though as yet a mile in their +rear,--than they rallied again, and forming in close ranks and +battalions, so that their spouts all looked like flashing lines of +stacked bayonets, moved on with redoubled velocity. + +Stripped to our shirts and drawers, we sprang to the white-ash, and +after several hours' pulling were almost disposed to renounce the +chase, when a general pausing commotion among the whales gave +animating token that they were now at last under the influence of +that strange perplexity of inert irresolution, which, when the +fishermen perceive it in the whale, they say he is gallied. The +compact martial columns in which they had been hitherto rapidly and +steadily swimming, were now broken up in one measureless rout; and +like King Porus' elephants in the Indian battle with Alexander, they +seemed going mad with consternation. In all directions expanding in +vast irregular circles, and aimlessly swimming hither and thither, by +their short thick spoutings, they plainly betrayed their distraction +of panic. This was still more strangely evinced by those of their +number, who, completely paralysed as it were, helplessly floated like +water-logged dismantled ships on the sea. Had these Leviathans been +but a flock of simple sheep, pursued over the pasture by three fierce +wolves, they could not possibly have evinced such excessive dismay. +But this occasional timidity is characteristic of almost all herding +creatures. Though banding together in tens of thousands, the +lion-maned buffaloes of the West have fled before a solitary +horseman. Witness, too, all human beings, how when herded together +in the sheepfold of a theatre's pit, they will, at the slightest +alarm of fire, rush helter-skelter for the outlets, crowding, +trampling, jamming, and remorselessly dashing each other to death. +Best, therefore, withhold any amazement at the strangely gallied +whales before us, for there is no folly of the beasts of the earth +which is not infinitely outdone by the madness of men. + +Though many of the whales, as has been said, were in violent motion, +yet it is to be observed that as a whole the herd neither advanced +nor retreated, but collectively remained in one place. As is +customary in those cases, the boats at once separated, each making +for some one lone whale on the outskirts of the shoal. In about +three minutes' time, Queequeg's harpoon was flung; the stricken fish +darted blinding spray in our faces, and then running away with us like +light, steered straight for the heart of the herd. Though such a +movement on the part of the whale struck under such circumstances, is +in no wise unprecedented; and indeed is almost always more or less +anticipated; yet does it present one of the more perilous +vicissitudes of the fishery. For as the swift monster drags you +deeper and deeper into the frantic shoal, you bid adieu to +circumspect life and only exist in a delirious throb. + +As, blind and deaf, the whale plunged forward, as if by sheer power +of speed to rid himself of the iron leech that had fastened to him; +as we thus tore a white gash in the sea, on all sides menaced as we +flew, by the crazed creatures to and fro rushing about us; our beset +boat was like a ship mobbed by ice-isles in a tempest, and striving +to steer through their complicated channels and straits, knowing not at +what moment it may be locked in and crushed. + +But not a bit daunted, Queequeg steered us manfully; now sheering off +from this monster directly across our route in advance; now edging +away from that, whose colossal flukes were suspended overhead, while +all the time, Starbuck stood up in the bows, lance in hand, pricking +out of our way whatever whales he could reach by short darts, for +there was no time to make long ones. Nor were the oarsmen quite +idle, though their wonted duty was now altogether dispensed with. +They chiefly attended to the shouting part of the business. "Out of +the way, Commodore!" cried one, to a great dromedary that of a sudden +rose bodily to the surface, and for an instant threatened to swamp +us. "Hard down with your tail, there!" cried a second to another, +which, close to our gunwale, seemed calmly cooling himself with his +own fan-like extremity. + +All whaleboats carry certain curious contrivances, originally +invented by the Nantucket Indians, called druggs. Two thick squares +of wood of equal size are stoutly clenched together, so that they +cross each other's grain at right angles; a line of considerable +length is then attached to the middle of this block, and the other +end of the line being looped, it can in a moment be fastened to a +harpoon. It is chiefly among gallied whales that this drugg is used. +For then, more whales are close round you than you can possibly +chase at one time. But sperm whales are not every day encountered; +while you may, then, you must kill all you can. And if you cannot +kill them all at once, you must wing them, so that they can be +afterwards killed at your leisure. Hence it is, that at times like +these the drugg, comes into requisition. Our boat was furnished with +three of them. The first and second were successfully darted, and we +saw the whales staggeringly running off, fettered by the enormous +sidelong resistance of the towing drugg. They were cramped like +malefactors with the chain and ball. But upon flinging the third, in +the act of tossing overboard the clumsy wooden block, it caught under +one of the seats of the boat, and in an instant tore it out and +carried it away, dropping the oarsman in the boat's bottom as the +seat slid from under him. On both sides the sea came in at the +wounded planks, but we stuffed two or three drawers and shirts in, +and so stopped the leaks for the time. + +It had been next to impossible to dart these drugged-harpoons, were +it not that as we advanced into the herd, our whale's way greatly +diminished; moreover, that as we went still further and further from +the circumference of commotion, the direful disorders seemed waning. +So that when at last the jerking harpoon drew out, and the towing +whale sideways vanished; then, with the tapering force of his parting +momentum, we glided between two whales into the innermost heart of +the shoal, as if from some mountain torrent we had slid into a serene +valley lake. Here the storms in the roaring glens between the +outermost whales, were heard but not felt. In this central expanse +the sea presented that smooth satin-like surface, called a sleek, +produced by the subtle moisture thrown off by the whale in his more +quiet moods. Yes, we were now in that enchanted calm which they say +lurks at the heart of every commotion. And still in the distracted +distance we beheld the tumults of the outer concentric circles, and +saw successive pods of whales, eight or ten in each, swiftly going +round and round, like multiplied spans of horses in a ring; and so +closely shoulder to shoulder, that a Titanic circus-rider might +easily have over-arched the middle ones, and so have gone round on +their backs. Owing to the density of the crowd of reposing whales, +more immediately surrounding the embayed axis of the herd, no +possible chance of escape was at present afforded us. We must watch +for a breach in the living wall that hemmed us in; the wall that had +only admitted us in order to shut us up. Keeping at the centre of +the lake, we were occasionally visited by small tame cows and calves; +the women and children of this routed host. + +Now, inclusive of the occasional wide intervals between the revolving +outer circles, and inclusive of the spaces between the various pods +in any one of those circles, the entire area at this juncture, +embraced by the whole multitude, must have contained at least two or +three square miles. At any rate--though indeed such a test at such a +time might be deceptive--spoutings might be discovered from our low +boat that seemed playing up almost from the rim of the horizon. I +mention this circumstance, because, as if the cows and calves had +been purposely locked up in this innermost fold; and as if the wide +extent of the herd had hitherto prevented them from learning the +precise cause of its stopping; or, possibly, being so young, +unsophisticated, and every way innocent and inexperienced; however it +may have been, these smaller whales--now and then visiting our +becalmed boat from the margin of the lake--evinced a wondrous +fearlessness and confidence, or else a still becharmed panic which it +was impossible not to marvel at. Like household dogs they came +snuffling round us, right up to our gunwales, and touching them; till +it almost seemed that some spell had suddenly domesticated them. +Queequeg patted their foreheads; Starbuck scratched their backs with +his lance; but fearful of the consequences, for the time refrained +from darting it. + +But far beneath this wondrous world upon the surface, another and +still stranger world met our eyes as we gazed over the side. For, +suspended in those watery vaults, floated the forms of the nursing +mothers of the whales, and those that by their enormous girth seemed +shortly to become mothers. The lake, as I have hinted, was to a +considerable depth exceedingly transparent; and as human infants +while suckling will calmly and fixedly gaze away from the breast, as +if leading two different lives at the time; and while yet drawing +mortal nourishment, be still spiritually feasting upon some unearthly +reminiscence;--even so did the young of these whales seem looking up +towards us, but not at us, as if we were but a bit of Gulfweed in +their new-born sight. Floating on their sides, the mothers also +seemed quietly eyeing us. One of these little infants, that from +certain queer tokens seemed hardly a day old, might have measured +some fourteen feet in length, and some six feet in girth. He was a +little frisky; though as yet his body seemed scarce yet recovered +from that irksome position it had so lately occupied in the maternal +reticule; where, tail to head, and all ready for the final spring, +the unborn whale lies bent like a Tartar's bow. The delicate +side-fins, and the palms of his flukes, still freshly retained the +plaited crumpled appearance of a baby's ears newly arrived from +foreign parts. + +"Line! line!" cried Queequeg, looking over the gunwale; "him fast! +him fast!--Who line him! Who struck?--Two whale; one big, one +little!" + +"What ails ye, man?" cried Starbuck. + +"Look-e here," said Queequeg, pointing down. + +As when the stricken whale, that from the tub has reeled out hundreds +of fathoms of rope; as, after deep sounding, he floats up again, and +shows the slackened curling line buoyantly rising and spiralling +towards the air; so now, Starbuck saw long coils of the umbilical +cord of Madame Leviathan, by which the young cub seemed still +tethered to its dam. Not seldom in the rapid vicissitudes of the +chase, this natural line, with the maternal end loose, becomes +entangled with the hempen one, so that the cub is thereby trapped. +Some of the subtlest secrets of the seas seemed divulged to us in +this enchanted pond. We saw young Leviathan amours in the deep.* + + +*The sperm whale, as with all other species of the Leviathan, but +unlike most other fish, breeds indifferently at all seasons; after a +gestation which may probably be set down at nine months, producing +but one at a time; though in some few known instances giving birth to +an Esau and Jacob:--a contingency provided for in suckling by two +teats, curiously situated, one on each side of the anus; but the +breasts themselves extend upwards from that. When by chance these +precious parts in a nursing whale are cut by the hunter's lance, the +mother's pouring milk and blood rivallingly discolour the sea for +rods. The milk is very sweet and rich; it has been tasted by man; it +might do well with strawberries. When overflowing with mutual +esteem, the whales salute MORE HOMINUM. + + +And thus, though surrounded by circle upon circle of consternations +and affrights, did these inscrutable creatures at the centre freely +and fearlessly indulge in all peaceful concernments; yea, serenely +revelled in dalliance and delight. But even so, amid the tornadoed +Atlantic of my being, do I myself still for ever centrally disport in +mute calm; and while ponderous planets of unwaning woe revolve round +me, deep down and deep inland there I still bathe me in eternal +mildness of joy. + +Meanwhile, as we thus lay entranced, the occasional sudden frantic +spectacles in the distance evinced the activity of the other boats, +still engaged in drugging the whales on the frontier of the host; or +possibly carrying on the war within the first circle, where abundance +of room and some convenient retreats were afforded them. But the +sight of the enraged drugged whales now and then blindly darting to +and fro across the circles, was nothing to what at last met our eyes. +It is sometimes the custom when fast to a whale more than commonly +powerful and alert, to seek to hamstring him, as it were, by +sundering or maiming his gigantic tail-tendon. It is done by darting +a short-handled cutting-spade, to which is attached a rope for +hauling it back again. A whale wounded (as we afterwards learned) in +this part, but not effectually, as it seemed, had broken away from +the boat, carrying along with him half of the harpoon line; and in +the extraordinary agony of the wound, he was now dashing among the +revolving circles like the lone mounted desperado Arnold, at the +battle of Saratoga, carrying dismay wherever he went. + +But agonizing as was the wound of this whale, and an appalling +spectacle enough, any way; yet the peculiar horror with which he +seemed to inspire the rest of the herd, was owing to a cause which at +first the intervening distance obscured from us. But at length we +perceived that by one of the unimaginable accidents of the fishery, +this whale had become entangled in the harpoon-line that he towed; he +had also run away with the cutting-spade in him; and while the free +end of the rope attached to that weapon, had permanently caught in +the coils of the harpoon-line round his tail, the cutting-spade +itself had worked loose from his flesh. So that tormented to +madness, he was now churning through the water, violently flailing +with his flexible tail, and tossing the keen spade about him, +wounding and murdering his own comrades. + +This terrific object seemed to recall the whole herd from their +stationary fright. First, the whales forming the margin of our lake +began to crowd a little, and tumble against each other, as if lifted +by half spent billows from afar; then the lake itself began faintly +to heave and swell; the submarine bridal-chambers and nurseries +vanished; in more and more contracting orbits the whales in the more +central circles began to swim in thickening clusters. Yes, the long +calm was departing. A low advancing hum was soon heard; and then +like to the tumultuous masses of block-ice when the great river +Hudson breaks up in Spring, the entire host of whales came tumbling +upon their inner centre, as if to pile themselves up in one common +mountain. Instantly Starbuck and Queequeg changed places; Starbuck +taking the stern. + +"Oars! Oars!" he intensely whispered, seizing the helm--"gripe your +oars, and clutch your souls, now! My God, men, stand by! Shove him +off, you Queequeg--the whale there!--prick him!--hit him! Stand +up--stand up, and stay so! Spring, men--pull, men; never mind their +backs--scrape them!--scrape away!" + +The boat was now all but jammed between two vast black bulks, leaving +a narrow Dardanelles between their long lengths. But by desperate +endeavor we at last shot into a temporary opening; then giving way +rapidly, and at the same time earnestly watching for another outlet. +After many similar hair-breadth escapes, we at last swiftly glided +into what had just been one of the outer circles, but now crossed by +random whales, all violently making for one centre. This lucky +salvation was cheaply purchased by the loss of Queequeg's hat, who, +while standing in the bows to prick the fugitive whales, had his hat +taken clean from his head by the air-eddy made by the sudden tossing +of a pair of broad flukes close by. + +Riotous and disordered as the universal commotion now was, it soon +resolved itself into what seemed a systematic movement; for having +clumped together at last in one dense body, they then renewed their +onward flight with augmented fleetness. Further pursuit was useless; +but the boats still lingered in their wake to pick up what drugged +whales might be dropped astern, and likewise to secure one which +Flask had killed and waifed. The waif is a pennoned pole, two or +three of which are carried by every boat; and which, when additional +game is at hand, are inserted upright into the floating body of a +dead whale, both to mark its place on the sea, and also as token of +prior possession, should the boats of any other ship draw near. + +The result of this lowering was somewhat illustrative of that +sagacious saying in the Fishery,--the more whales the less fish. Of +all the drugged whales only one was captured. The rest contrived to +escape for the time, but only to be taken, as will hereafter be seen, +by some other craft than the Pequod. + + + +CHAPTER 88 + +Schools and Schoolmasters. + + +The previous chapter gave account of an immense body or herd of Sperm +Whales, and there was also then given the probable cause inducing +those vast aggregations. + +Now, though such great bodies are at times encountered, yet, as must +have been seen, even at the present day, small detached bands are +occasionally observed, embracing from twenty to fifty individuals +each. Such bands are known as schools. They generally are of two +sorts; those composed almost entirely of females, and those mustering +none but young vigorous males, or bulls, as they are familiarly +designated. + +In cavalier attendance upon the school of females, you invariably see +a male of full grown magnitude, but not old; who, upon any alarm, +evinces his gallantry by falling in the rear and covering the flight +of his ladies. In truth, this gentleman is a luxurious Ottoman, +swimming about over the watery world, surroundingly accompanied by +all the solaces and endearments of the harem. The contrast between +this Ottoman and his concubines is striking; because, while he is +always of the largest leviathanic proportions, the ladies, even at +full growth, are not more than one-third of the bulk of an +average-sized male. They are comparatively delicate, indeed; I dare +say, not to exceed half a dozen yards round the waist. Nevertheless, +it cannot be denied, that upon the whole they are hereditarily +entitled to EMBONPOINT. + +It is very curious to watch this harem and its lord in their indolent +ramblings. Like fashionables, they are for ever on the move in +leisurely search of variety. You meet them on the Line in time for +the full flower of the Equatorial feeding season, having just +returned, perhaps, from spending the summer in the Northern seas, and +so cheating summer of all unpleasant weariness and warmth. By the +time they have lounged up and down the promenade of the Equator +awhile, they start for the Oriental waters in anticipation of the +cool season there, and so evade the other excessive temperature of +the year. + +When serenely advancing on one of these journeys, if any strange +suspicious sights are seen, my lord whale keeps a wary eye on his +interesting family. Should any unwarrantably pert young Leviathan +coming that way, presume to draw confidentially close to one of the +ladies, with what prodigious fury the Bashaw assails him, and chases +him away! High times, indeed, if unprincipled young rakes like him +are to be permitted to invade the sanctity of domestic bliss; though +do what the Bashaw will, he cannot keep the most notorious Lothario +out of his bed; for, alas! all fish bed in common. As ashore, the +ladies often cause the most terrible duels among their rival +admirers; just so with the whales, who sometimes come to deadly +battle, and all for love. They fence with their long lower jaws, +sometimes locking them together, and so striving for the supremacy +like elks that warringly interweave their antlers. Not a few are +captured having the deep scars of these encounters,--furrowed heads, +broken teeth, scolloped fins; and in some instances, wrenched and +dislocated mouths. + +But supposing the invader of domestic bliss to betake himself away at +the first rush of the harem's lord, then is it very diverting to +watch that lord. Gently he insinuates his vast bulk among them again +and revels there awhile, still in tantalizing vicinity to young +Lothario, like pious Solomon devoutly worshipping among his thousand +concubines. Granting other whales to be in sight, the fishermen +will seldom give chase to one of these Grand Turks; for these Grand +Turks are too lavish of their strength, and hence their unctuousness +is small. As for the sons and the daughters they beget, why, those sons +and daughters must take care of themselves; at least, with only the +maternal help. For like certain other omnivorous roving lovers that +might be named, my Lord Whale has no taste for the nursery, however +much for the bower; and so, being a great traveller, he leaves his +anonymous babies all over the world; every baby an exotic. In good +time, nevertheless, as the ardour of youth declines; as years and +dumps increase; as reflection lends her solemn pauses; in short, as a +general lassitude overtakes the sated Turk; then a love of ease and +virtue supplants the love for maidens; our Ottoman enters upon the +impotent, repentant, admonitory stage of life, forswears, disbands +the harem, and grown to an exemplary, sulky old soul, goes about all +alone among the meridians and parallels saying his prayers, and +warning each young Leviathan from his amorous errors. + +Now, as the harem of whales is called by the fishermen a school, so +is the lord and master of that school technically known as the +schoolmaster. It is therefore not in strict character, however +admirably satirical, that after going to school himself, he should +then go abroad inculcating not what he learned there, but the folly +of it. His title, schoolmaster, would very naturally seem derived +from the name bestowed upon the harem itself, but some have surmised +that the man who first thus entitled this sort of Ottoman whale, must +have read the memoirs of Vidocq, and informed himself what sort of a +country-schoolmaster that famous Frenchman was in his younger days, +and what was the nature of those occult lessons he inculcated into +some of his pupils. + +The same secludedness and isolation to which the schoolmaster whale +betakes himself in his advancing years, is true of all aged Sperm +Whales. Almost universally, a lone whale--as a solitary Leviathan is +called--proves an ancient one. Like venerable moss-bearded Daniel +Boone, he will have no one near him but Nature herself; and her he +takes to wife in the wilderness of waters, and the best of wives she +is, though she keeps so many moody secrets. + +The schools composing none but young and vigorous males, previously +mentioned, offer a strong contrast to the harem schools. For while +those female whales are characteristically timid, the young males, or +forty-barrel-bulls, as they call them, are by far the most pugnacious +of all Leviathans, and proverbially the most dangerous to encounter; +excepting those wondrous grey-headed, grizzled whales, sometimes met, +and these will fight you like grim fiends exasperated by a penal +gout. + +The Forty-barrel-bull schools are larger than the harem schools. +Like a mob of young collegians, they are full of fight, fun, and +wickedness, tumbling round the world at such a reckless, rollicking +rate, that no prudent underwriter would insure them any more than he +would a riotous lad at Yale or Harvard. They soon relinquish this +turbulence though, and when about three-fourths grown, break up, and +separately go about in quest of settlements, that is, harems. + +Another point of difference between the male and female schools is +still more characteristic of the sexes. Say you strike a +Forty-barrel-bull--poor devil! all his comrades quit him. But strike +a member of the harem school, and her companions swim around her with +every token of concern, sometimes lingering so near her and so long, +as themselves to fall a prey. + + + +CHAPTER 89 + +Fast-Fish and Loose-Fish. + + +The allusion to the waif and waif-poles in the last chapter but one, +necessitates some account of the laws and regulations of the whale +fishery, of which the waif may be deemed the grand symbol and badge. + +It frequently happens that when several ships are cruising in +company, a whale may be struck by one vessel, then escape, and be +finally killed and captured by another vessel; and herein are +indirectly comprised many minor contingencies, all partaking of this +one grand feature. For example,--after a weary and perilous chase +and capture of a whale, the body may get loose from the ship by +reason of a violent storm; and drifting far away to leeward, be +retaken by a second whaler, who, in a calm, snugly tows it alongside, +without risk of life or line. Thus the most vexatious and violent +disputes would often arise between the fishermen, were there not some +written or unwritten, universal, undisputed law applicable to all +cases. + +Perhaps the only formal whaling code authorized by legislative +enactment, was that of Holland. It was decreed by the States-General +in A.D. 1695. But though no other nation has ever had any written +whaling law, yet the American fishermen have been their own +legislators and lawyers in this matter. They have provided a system +which for terse comprehensiveness surpasses Justinian's Pandects and +the By-laws of the Chinese Society for the Suppression of Meddling +with other People's Business. Yes; these laws might be engraven on a +Queen Anne's forthing, or the barb of a harpoon, and worn round the +neck, so small are they. + +I. A Fast-Fish belongs to the party fast to it. + +II. A Loose-Fish is fair game for anybody who can soonest catch it. + +But what plays the mischief with this masterly code is the admirable +brevity of it, which necessitates a vast volume of commentaries to +expound it. + +First: What is a Fast-Fish? Alive or dead a fish is technically +fast, when it is connected with an occupied ship or boat, by any +medium at all controllable by the occupant or occupants,--a mast, an +oar, a nine-inch cable, a telegraph wire, or a strand of cobweb, it +is all the same. Likewise a fish is technically fast when it bears a +waif, or any other recognised symbol of possession; so long as the +party waifing it plainly evince their ability at any time to take it +alongside, as well as their intention so to do. + +These are scientific commentaries; but the commentaries of the +whalemen themselves sometimes consist in hard words and harder +knocks--the Coke-upon-Littleton of the fist. True, among the more +upright and honourable whalemen allowances are always made for +peculiar cases, where it would be an outrageous moral injustice for +one party to claim possession of a whale previously chased or killed +by another party. But others are by no means so scrupulous. + +Some fifty years ago there was a curious case of whale-trover +litigated in England, wherein the plaintiffs set forth that after a +hard chase of a whale in the Northern seas; and when indeed they (the +plaintiffs) had succeeded in harpooning the fish; they were at last, +through peril of their lives, obliged to forsake not only their +lines, but their boat itself. Ultimately the defendants (the crew of +another ship) came up with the whale, struck, killed, seized, and +finally appropriated it before the very eyes of the plaintiffs. And +when those defendants were remonstrated with, their captain snapped +his fingers in the plaintiffs' teeth, and assured them that by way of +doxology to the deed he had done, he would now retain their line, +harpoons, and boat, which had remained attached to the whale at the +time of the seizure. Wherefore the plaintiffs now sued for the +recovery of the value of their whale, line, harpoons, and boat. + +Mr. Erskine was counsel for the defendants; Lord Ellenborough was the +judge. In the course of the defence, the witty Erskine went on to +illustrate his position, by alluding to a recent crim. con. case, +wherein a gentleman, after in vain trying to bridle his wife's +viciousness, had at last abandoned her upon the seas of life; but in +the course of years, repenting of that step, he instituted an action +to recover possession of her. Erskine was on the other side; and he +then supported it by saying, that though the gentleman had originally +harpooned the lady, and had once had her fast, and only by reason of +the great stress of her plunging viciousness, had at last abandoned +her; yet abandon her he did, so that she became a loose-fish; and +therefore when a subsequent gentleman re-harpooned her, the lady then +became that subsequent gentleman's property, along with whatever +harpoon might have been found sticking in her. + +Now in the present case Erskine contended that the examples of the +whale and the lady were reciprocally illustrative of each other. + +These pleadings, and the counter pleadings, being duly heard, the +very learned Judge in set terms decided, to wit,--That as for the +boat, he awarded it to the plaintiffs, because they had merely +abandoned it to save their lives; but that with regard to the +controverted whale, harpoons, and line, they belonged to the +defendants; the whale, because it was a Loose-Fish at the time of the +final capture; and the harpoons and line because when the fish made +off with them, it (the fish) acquired a property in those articles; +and hence anybody who afterwards took the fish had a right to them. +Now the defendants afterwards took the fish; ergo, the aforesaid +articles were theirs. + +A common man looking at this decision of the very learned Judge, +might possibly object to it. But ploughed up to the primary rock of +the matter, the two great principles laid down in the twin whaling +laws previously quoted, and applied and elucidated by Lord +Ellenborough in the above cited case; these two laws touching +Fast-Fish and Loose-Fish, I say, will, on reflection, be found the +fundamentals of all human jurisprudence; for notwithstanding its +complicated tracery of sculpture, the Temple of the Law, like the +Temple of the Philistines, has but two props to stand on. + +Is it not a saying in every one's mouth, Possession is half of the +law: that is, regardless of how the thing came into possession? But +often possession is the whole of the law. What are the sinews and +souls of Russian serfs and Republican slaves but Fast-Fish, whereof +possession is the whole of the law? What to the rapacious landlord +is the widow's last mite but a Fast-Fish? What is yonder undetected +villain's marble mansion with a door-plate for a waif; what is that +but a Fast-Fish? What is the ruinous discount which Mordecai, the +broker, gets from poor Woebegone, the bankrupt, on a loan to +keep Woebegone's family from starvation; what is that ruinous +discount but a Fast-Fish? What is the Archbishop of Savesoul's +income of L100,000 seized from the scant bread and cheese of +hundreds of thousands of broken-backed laborers (all sure of heaven +without any of Savesoul's help) what is that globular L100,000 but a +Fast-Fish? What are the Duke of Dunder's hereditary towns and +hamlets but Fast-Fish? What to that redoubted harpooneer, John Bull, +is poor Ireland, but a Fast-Fish? What to that apostolic lancer, +Brother Jonathan, is Texas but a Fast-Fish? And concerning all +these, is not Possession the whole of the law? + +But if the doctrine of Fast-Fish be pretty generally applicable, the +kindred doctrine of Loose-Fish is still more widely so. That is +internationally and universally applicable. + +What was America in 1492 but a Loose-Fish, in which Columbus struck +the Spanish standard by way of waifing it for his royal master and +mistress? What was Poland to the Czar? What Greece to the Turk? +What India to England? What at last will Mexico be to the United +States? All Loose-Fish. + +What are the Rights of Man and the Liberties of the World but +Loose-Fish? What all men's minds and opinions but Loose-Fish? What +is the principle of religious belief in them but a Loose-Fish? What +to the ostentatious smuggling verbalists are the thoughts of thinkers +but Loose-Fish? What is the great globe itself but a Loose-Fish? +And what are you, reader, but a Loose-Fish and a Fast-Fish, too? + + + +CHAPTER 90 + +Heads or Tails. + + +"De balena vero sufficit, si rex habeat caput, et regina caudam." +BRACTON, L. 3, C. 3. + + +Latin from the books of the Laws of England, which taken along with +the context, means, that of all whales captured by anybody on the +coast of that land, the King, as Honourary Grand Harpooneer, must have +the head, and the Queen be respectfully presented with the tail. A +division which, in the whale, is much like halving an apple; there is +no intermediate remainder. Now as this law, under a modified form, +is to this day in force in England; and as it offers in various +respects a strange anomaly touching the general law of Fast and +Loose-Fish, it is here treated of in a separate chapter, on the same +courteous principle that prompts the English railways to be at the +expense of a separate car, specially reserved for the accommodation +of royalty. In the first place, in curious proof of the fact that +the above-mentioned law is still in force, I proceed to lay before +you a circumstance that happened within the last two years. + +It seems that some honest mariners of Dover, or Sandwich, or some one +of the Cinque Ports, had after a hard chase succeeded in killing and +beaching a fine whale which they had originally descried afar off +from the shore. Now the Cinque Ports are partially or somehow under +the jurisdiction of a sort of policeman or beadle, called a Lord +Warden. Holding the office directly from the crown, I believe, all +the royal emoluments incident to the Cinque Port territories become +by assignment his. By some writers this office is called a sinecure. +But not so. Because the Lord Warden is busily employed at times in +fobbing his perquisites; which are his chiefly by virtue of that same +fobbing of them. + +Now when these poor sun-burnt mariners, bare-footed, and with their +trowsers rolled high up on their eely legs, had wearily hauled their +fat fish high and dry, promising themselves a good L150 from the +precious oil and bone; and in fantasy sipping rare tea with their +wives, and good ale with their cronies, upon the strength of their +respective shares; up steps a very learned and most Christian and +charitable gentleman, with a copy of Blackstone under his arm; and +laying it upon the whale's head, he says--"Hands off! this fish, my +masters, is a Fast-Fish. I seize it as the Lord Warden's." Upon +this the poor mariners in their respectful consternation--so truly +English--knowing not what to say, fall to vigorously scratching their +heads all round; meanwhile ruefully glancing from the whale to the +stranger. But that did in nowise mend the matter, or at all soften +the hard heart of the learned gentleman with the copy of Blackstone. +At length one of them, after long scratching about for his ideas, +made bold to speak, + +"Please, sir, who is the Lord Warden?" + +"The Duke." + +"But the duke had nothing to do with taking this fish?" + +"It is his." + +"We have been at great trouble, and peril, and some expense, and is +all that to go to the Duke's benefit; we getting nothing at all for +our pains but our blisters?" + +"It is his." + +"Is the Duke so very poor as to be forced to this desperate mode of +getting a livelihood?" + +"It is his." + +"I thought to relieve my old bed-ridden mother by part of my share of +this whale." + +"It is his." + +"Won't the Duke be content with a quarter or a half?" + +"It is his." + +In a word, the whale was seized and sold, and his Grace the Duke of +Wellington received the money. Thinking that viewed in some +particular lights, the case might by a bare possibility in some small +degree be deemed, under the circumstances, a rather hard one, an +honest clergyman of the town respectfully addressed a note to his +Grace, begging him to take the case of those unfortunate mariners +into full consideration. To which my Lord Duke in substance replied +(both letters were published) that he had already done so, and +received the money, and would be obliged to the reverend gentleman if +for the future he (the reverend gentleman) would decline meddling +with other people's business. Is this the still militant old man, +standing at the corners of the three kingdoms, on all hands coercing +alms of beggars? + +It will readily be seen that in this case the alleged right of the +Duke to the whale was a delegated one from the Sovereign. We must +needs inquire then on what principle the Sovereign is originally +invested with that right. The law itself has already been set forth. +But Plowdon gives us the reason for it. Says Plowdon, the whale so +caught belongs to the King and Queen, "because of its superior +excellence." And by the soundest commentators this has ever been +held a cogent argument in such matters. + +But why should the King have the head, and the Queen the tail? A +reason for that, ye lawyers! + +In his treatise on "Queen-Gold," or Queen-pinmoney, an old King's +Bench author, one William Prynne, thus discourseth: "Ye tail is ye +Queen's, that ye Queen's wardrobe may be supplied with ye whalebone." +Now this was written at a time when the black limber bone of the +Greenland or Right whale was largely used in ladies' bodices. But +this same bone is not in the tail; it is in the head, which is a sad +mistake for a sagacious lawyer like Prynne. But is the Queen a +mermaid, to be presented with a tail? An allegorical meaning may +lurk here. + +There are two royal fish so styled by the English law writers--the +whale and the sturgeon; both royal property under certain +limitations, and nominally supplying the tenth branch of the crown's +ordinary revenue. I know not that any other author has hinted of the +matter; but by inference it seems to me that the sturgeon must be +divided in the same way as the whale, the King receiving the highly +dense and elastic head peculiar to that fish, which, symbolically +regarded, may possibly be humorously grounded upon some presumed +congeniality. And thus there seems a reason in all things, even in +law. + + + +CHAPTER 91 + +The Pequod Meets The Rose-Bud. + + +"In vain it was to rake for Ambergriese in the paunch of this +Leviathan, insufferable fetor denying not inquiry." +SIR T. BROWNE, V.E. + + +It was a week or two after the last whaling scene recounted, and when +we were slowly sailing over a sleepy, vapoury, mid-day sea, that the +many noses on the Pequod's deck proved more vigilant discoverers than +the three pairs of eyes aloft. A peculiar and not very pleasant +smell was smelt in the sea. + +"I will bet something now," said Stubb, "that somewhere hereabouts +are some of those drugged whales we tickled the other day. I thought +they would keel up before long." + +Presently, the vapours in advance slid aside; and there in the +distance lay a ship, whose furled sails betokened that some sort of +whale must be alongside. As we glided nearer, the stranger showed +French colours from his peak; and by the eddying cloud of vulture +sea-fowl that circled, and hovered, and swooped around him, it was +plain that the whale alongside must be what the fishermen call a +blasted whale, that is, a whale that has died unmolested on the sea, +and so floated an unappropriated corpse. It may well be conceived, +what an unsavory odor such a mass must exhale; worse than an Assyrian +city in the plague, when the living are incompetent to bury the +departed. So intolerable indeed is it regarded by some, that no +cupidity could persuade them to moor alongside of it. Yet are there +those who will still do it; notwithstanding the fact that the oil +obtained from such subjects is of a very inferior quality, and by no +means of the nature of attar-of-rose. + +Coming still nearer with the expiring breeze, we saw that the +Frenchman had a second whale alongside; and this second whale seemed +even more of a nosegay than the first. In truth, it turned out to be +one of those problematical whales that seem to dry up and die with a +sort of prodigious dyspepsia, or indigestion; leaving their defunct +bodies almost entirely bankrupt of anything like oil. Nevertheless, +in the proper place we shall see that no knowing fisherman will ever +turn up his nose at such a whale as this, however much he may shun +blasted whales in general. + +The Pequod had now swept so nigh to the stranger, that Stubb vowed he +recognised his cutting spade-pole entangled in the lines that were +knotted round the tail of one of these whales. + +"There's a pretty fellow, now," he banteringly laughed, standing in +the ship's bows, "there's a jackal for ye! I well know that these +Crappoes of Frenchmen are but poor devils in the fishery; sometimes +lowering their boats for breakers, mistaking them for Sperm Whale +spouts; yes, and sometimes sailing from their port with their hold +full of boxes of tallow candles, and cases of snuffers, foreseeing +that all the oil they will get won't be enough to dip the Captain's +wick into; aye, we all know these things; but look ye, here's a +Crappo that is content with our leavings, the drugged whale there, I +mean; aye, and is content too with scraping the dry bones of that +other precious fish he has there. Poor devil! I say, pass round a +hat, some one, and let's make him a present of a little oil for dear +charity's sake. For what oil he'll get from that drugged whale +there, wouldn't be fit to burn in a jail; no, not in a condemned +cell. And as for the other whale, why, I'll agree to get more oil by +chopping up and trying out these three masts of ours, than he'll get +from that bundle of bones; though, now that I think of it, it may +contain something worth a good deal more than oil; yes, ambergris. I +wonder now if our old man has thought of that. It's worth trying. +Yes, I'm for it;" and so saying he started for the quarter-deck. + +By this time the faint air had become a complete calm; so that +whether or no, the Pequod was now fairly entrapped in the smell, with +no hope of escaping except by its breezing up again. Issuing from +the cabin, Stubb now called his boat's crew, and pulled off for the +stranger. Drawing across her bow, he perceived that in accordance +with the fanciful French taste, the upper part of her stem-piece was +carved in the likeness of a huge drooping stalk, was painted green, +and for thorns had copper spikes projecting from it here and there; +the whole terminating in a symmetrical folded bulb of a bright red +colour. Upon her head boards, in large gilt letters, he read "Bouton +de Rose,"--Rose-button, or Rose-bud; and this was the romantic name +of this aromatic ship. + +Though Stubb did not understand the BOUTON part of the inscription, +yet the word ROSE, and the bulbous figure-head put together, +sufficiently explained the whole to him. + +"A wooden rose-bud, eh?" he cried with his hand to his nose, "that +will do very well; but how like all creation it smells!" + +Now in order to hold direct communication with the people on deck, he +had to pull round the bows to the starboard side, and thus come close +to the blasted whale; and so talk over it. + +Arrived then at this spot, with one hand still to his nose, he +bawled--"Bouton-de-Rose, ahoy! are there any of you Bouton-de-Roses +that speak English?" + +"Yes," rejoined a Guernsey-man from the bulwarks, who turned out to +be the chief-mate. + +"Well, then, my Bouton-de-Rose-bud, have you seen the White Whale?" + +"WHAT whale?" + +"The WHITE Whale--a Sperm Whale--Moby Dick, have ye seen him? + +"Never heard of such a whale. Cachalot Blanche! White Whale--no." + +"Very good, then; good bye now, and I'll call again in a minute." + +Then rapidly pulling back towards the Pequod, and seeing Ahab leaning +over the quarter-deck rail awaiting his report, he moulded his two +hands into a trumpet and shouted--"No, Sir! No!" Upon which Ahab +retired, and Stubb returned to the Frenchman. + +He now perceived that the Guernsey-man, who had just got into the +chains, and was using a cutting-spade, had slung his nose in a sort +of bag. + +"What's the matter with your nose, there?" said Stubb. "Broke it?" + +"I wish it was broken, or that I didn't have any nose at all!" +answered the Guernsey-man, who did not seem to relish the job he was +at very much. "But what are you holding YOURS for?" + +"Oh, nothing! It's a wax nose; I have to hold it on. Fine day, +ain't it? Air rather gardenny, I should say; throw us a bunch of +posies, will ye, Bouton-de-Rose?" + +"What in the devil's name do you want here?" roared the Guernseyman, +flying into a sudden passion. + +"Oh! keep cool--cool? yes, that's the word! why don't you pack those +whales in ice while you're working at 'em? But joking aside, though; +do you know, Rose-bud, that it's all nonsense trying to get any oil +out of such whales? As for that dried up one, there, he hasn't a +gill in his whole carcase." + +"I know that well enough; but, d'ye see, the Captain here won't +believe it; this is his first voyage; he was a Cologne manufacturer +before. But come aboard, and mayhap he'll believe you, if he won't +me; and so I'll get out of this dirty scrape." + +"Anything to oblige ye, my sweet and pleasant fellow," rejoined +Stubb, and with that he soon mounted to the deck. There a queer +scene presented itself. The sailors, in tasselled caps of red +worsted, were getting the heavy tackles in readiness for the whales. +But they worked rather slow and talked very fast, and seemed in +anything but a good humor. All their noses upwardly projected from +their faces like so many jib-booms. Now and then pairs of them would +drop their work, and run up to the mast-head to get some fresh air. +Some thinking they would catch the plague, dipped oakum in coal-tar, +and at intervals held it to their nostrils. Others having broken the +stems of their pipes almost short off at the bowl, were vigorously +puffing tobacco-smoke, so that it constantly filled their +olfactories. + +Stubb was struck by a shower of outcries and anathemas proceeding +from the Captain's round-house abaft; and looking in that direction +saw a fiery face thrust from behind the door, which was held ajar +from within. This was the tormented surgeon, who, after in vain +remonstrating against the proceedings of the day, had betaken himself +to the Captain's round-house (CABINET he called it) to avoid the +pest; but still, could not help yelling out his entreaties and +indignations at times. + +Marking all this, Stubb argued well for his scheme, and turning to +the Guernsey-man had a little chat with him, during which the +stranger mate expressed his detestation of his Captain as a conceited +ignoramus, who had brought them all into so unsavory and unprofitable +a pickle. Sounding him carefully, Stubb further perceived that the +Guernsey-man had not the slightest suspicion concerning the +ambergris. He therefore held his peace on that head, but otherwise +was quite frank and confidential with him, so that the two quickly +concocted a little plan for both circumventing and satirizing the +Captain, without his at all dreaming of distrusting their sincerity. +According to this little plan of theirs, the Guernsey-man, under +cover of an interpreter's office, was to tell the Captain what he +pleased, but as coming from Stubb; and as for Stubb, he was to utter +any nonsense that should come uppermost in him during the interview. + +By this time their destined victim appeared from his cabin. He was a +small and dark, but rather delicate looking man for a sea-captain, +with large whiskers and moustache, however; and wore a red cotton +velvet vest with watch-seals at his side. To this gentleman, Stubb +was now politely introduced by the Guernsey-man, who at once +ostentatiously put on the aspect of interpreting between them. + +"What shall I say to him first?" said he. + +"Why," said Stubb, eyeing the velvet vest and the watch and seals, +"you may as well begin by telling him that he looks a sort of babyish +to me, though I don't pretend to be a judge." + +"He says, Monsieur," said the Guernsey-man, in French, turning to his +captain, "that only yesterday his ship spoke a vessel, whose captain +and chief-mate, with six sailors, had all died of a fever caught from +a blasted whale they had brought alongside." + +Upon this the captain started, and eagerly desired to know more. + +"What now?" said the Guernsey-man to Stubb. + +"Why, since he takes it so easy, tell him that now I have eyed him +carefully, I'm quite certain that he's no more fit to command a +whale-ship than a St. Jago monkey. In fact, tell him from me he's a +baboon." + +"He vows and declares, Monsieur, that the other whale, the dried one, +is far more deadly than the blasted one; in fine, Monsieur, he +conjures us, as we value our lives, to cut loose from these fish." + +Instantly the captain ran forward, and in a loud voice commanded his +crew to desist from hoisting the cutting-tackles, and at once cast +loose the cables and chains confining the whales to the ship. + +"What now?" said the Guernsey-man, when the Captain had returned to +them. + +"Why, let me see; yes, you may as well tell him now that--that--in +fact, tell him I've diddled him, and (aside to himself) perhaps +somebody else." + +"He says, Monsieur, that he's very happy to have been of any service +to us." + +Hearing this, the captain vowed that they were the grateful parties +(meaning himself and mate) and concluded by inviting Stubb down +into his cabin to drink a bottle of Bordeaux. + +"He wants you to take a glass of wine with him," said the +interpreter. + +"Thank him heartily; but tell him it's against my principles to drink +with the man I've diddled. In fact, tell him I must go." + +"He says, Monsieur, that his principles won't admit of his drinking; +but that if Monsieur wants to live another day to drink, then +Monsieur had best drop all four boats, and pull the ship away from +these whales, for it's so calm they won't drift." + +By this time Stubb was over the side, and getting into his boat, +hailed the Guernsey-man to this effect,--that having a long tow-line +in his boat, he would do what he could to help them, by pulling out +the lighter whale of the two from the ship's side. While the +Frenchman's boats, then, were engaged in towing the ship one way, +Stubb benevolently towed away at his whale the other way, +ostentatiously slacking out a most unusually long tow-line. + +Presently a breeze sprang up; Stubb feigned to cast off from the +whale; hoisting his boats, the Frenchman soon increased his distance, +while the Pequod slid in between him and Stubb's whale. Whereupon +Stubb quickly pulled to the floating body, and hailing the Pequod to +give notice of his intentions, at once proceeded to reap the fruit of +his unrighteous cunning. Seizing his sharp boat-spade, he commenced +an excavation in the body, a little behind the side fin. You would +almost have thought he was digging a cellar there in the sea; and +when at length his spade struck against the gaunt ribs, it was like +turning up old Roman tiles and pottery buried in fat English loam. +His boat's crew were all in high excitement, eagerly helping their +chief, and looking as anxious as gold-hunters. + +And all the time numberless fowls were diving, and ducking, and +screaming, and yelling, and fighting around them. Stubb was +beginning to look disappointed, especially as the horrible nosegay +increased, when suddenly from out the very heart of this plague, +there stole a faint stream of perfume, which flowed through the tide +of bad smells without being absorbed by it, as one river will flow +into and then along with another, without at all blending with it for +a time. + +"I have it, I have it," cried Stubb, with delight, striking something +in the subterranean regions, "a purse! a purse!" + +Dropping his spade, he thrust both hands in, and drew out handfuls of +something that looked like ripe Windsor soap, or rich mottled old +cheese; very unctuous and savory withal. You might easily dent it +with your thumb; it is of a hue between yellow and ash colour. And +this, good friends, is ambergris, worth a gold guinea an ounce to any +druggist. Some six handfuls were obtained; but more was unavoidably +lost in the sea, and still more, perhaps, might have been secured +were it not for impatient Ahab's loud command to Stubb to desist, and +come on board, else the ship would bid them good bye. + + + +CHAPTER 92 + +Ambergris. + + +Now this ambergris is a very curious substance, and so important as +an article of commerce, that in 1791 a certain Nantucket-born Captain +Coffin was examined at the bar of the English House of Commons on +that subject. For at that time, and indeed until a comparatively +late day, the precise origin of ambergris remained, like amber +itself, a problem to the learned. Though the word ambergris is but +the French compound for grey amber, yet the two substances are quite +distinct. For amber, though at times found on the sea-coast, is also +dug up in some far inland soils, whereas ambergris is never found +except upon the sea. Besides, amber is a hard, transparent, brittle, +odorless substance, used for mouth-pieces to pipes, for beads and +ornaments; but ambergris is soft, waxy, and so highly fragrant and +spicy, that it is largely used in perfumery, in pastiles, precious +candles, hair-powders, and pomatum. The Turks use it in cooking, and +also carry it to Mecca, for the same purpose that frankincense is +carried to St. Peter's in Rome. Some wine merchants drop a few +grains into claret, to flavor it. + +Who would think, then, that such fine ladies and gentlemen should +regale themselves with an essence found in the inglorious bowels of a +sick whale! Yet so it is. By some, ambergris is supposed to be the +cause, and by others the effect, of the dyspepsia in the whale. How +to cure such a dyspepsia it were hard to say, unless by administering +three or four boat loads of Brandreth's pills, and then running out +of harm's way, as laborers do in blasting rocks. + +I have forgotten to say that there were found in this ambergris, +certain hard, round, bony plates, which at first Stubb thought might +be sailors' trowsers buttons; but it afterwards turned out that they +were nothing more than pieces of small squid bones embalmed in that +manner. + +Now that the incorruption of this most fragrant ambergris should be +found in the heart of such decay; is this nothing? Bethink thee of +that saying of St. Paul in Corinthians, about corruption and +incorruption; how that we are sown in dishonour, but raised in glory. +And likewise call to mind that saying of Paracelsus about what it is +that maketh the best musk. Also forget not the strange fact that of +all things of ill-savor, Cologne-water, in its rudimental +manufacturing stages, is the worst. + +I should like to conclude the chapter with the above appeal, but +cannot, owing to my anxiety to repel a charge often made against +whalemen, and which, in the estimation of some already biased minds, +might be considered as indirectly substantiated by what has been said +of the Frenchman's two whales. Elsewhere in this volume the +slanderous aspersion has been disproved, that the vocation of whaling +is throughout a slatternly, untidy business. But there is another +thing to rebut. They hint that all whales always smell bad. Now how +did this odious stigma originate? + +I opine, that it is plainly traceable to the first arrival of the +Greenland whaling ships in London, more than two centuries ago. +Because those whalemen did not then, and do not now, try out their +oil at sea as the Southern ships have always done; but cutting up the +fresh blubber in small bits, thrust it through the bung holes of +large casks, and carry it home in that manner; the shortness of the +season in those Icy Seas, and the sudden and violent storms to which +they are exposed, forbidding any other course. The consequence is, +that upon breaking into the hold, and unloading one of these whale +cemeteries, in the Greenland dock, a savor is given forth somewhat +similar to that arising from excavating an old city grave-yard, for +the foundations of a Lying-in-Hospital. + +I partly surmise also, that this wicked charge against whalers may be +likewise imputed to the existence on the coast of Greenland, in +former times, of a Dutch village called Schmerenburgh or Smeerenberg, +which latter name is the one used by the learned Fogo Von Slack, in +his great work on Smells, a text-book on that subject. As its name +imports (smeer, fat; berg, to put up), this village was founded in +order to afford a place for the blubber of the Dutch whale fleet to +be tried out, without being taken home to Holland for that purpose. +It was a collection of furnaces, fat-kettles, and oil sheds; and when +the works were in full operation certainly gave forth no very +pleasant savor. But all this is quite different with a South Sea +Sperm Whaler; which in a voyage of four years perhaps, after +completely filling her hold with oil, does not, perhaps, consume +fifty days in the business of boiling out; and in the state that it +is casked, the oil is nearly scentless. The truth is, that living or +dead, if but decently treated, whales as a species are by no means +creatures of ill odor; nor can whalemen be recognised, as the people +of the middle ages affected to detect a Jew in the company, by the +nose. Nor indeed can the whale possibly be otherwise than fragrant, +when, as a general thing, he enjoys such high health; taking +abundance of exercise; always out of doors; though, it is true, +seldom in the open air. I say, that the motion of a Sperm Whale's +flukes above water dispenses a perfume, as when a musk-scented lady +rustles her dress in a warm parlor. What then shall I liken the +Sperm Whale to for fragrance, considering his magnitude? Must it not +be to that famous elephant, with jewelled tusks, and redolent with +myrrh, which was led out of an Indian town to do honour to Alexander +the Great? + + + +CHAPTER 93 + +The Castaway. + + +It was but some few days after encountering the Frenchman, that a +most significant event befell the most insignificant of the Pequod's +crew; an event most lamentable; and which ended in providing the +sometimes madly merry and predestinated craft with a living and ever +accompanying prophecy of whatever shattered sequel might prove her +own. + +Now, in the whale ship, it is not every one that goes in the boats. +Some few hands are reserved called ship-keepers, whose province it is +to work the vessel while the boats are pursuing the whale. As a +general thing, these ship-keepers are as hardy fellows as the men +comprising the boats' crews. But if there happen to be an unduly +slender, clumsy, or timorous wight in the ship, that wight is certain +to be made a ship-keeper. It was so in the Pequod with the little +negro Pippin by nick-name, Pip by abbreviation. Poor Pip! ye have +heard of him before; ye must remember his tambourine on that dramatic +midnight, so gloomy-jolly. + +In outer aspect, Pip and Dough-Boy made a match, like a black pony +and a white one, of equal developments, though of dissimilar colour, +driven in one eccentric span. But while hapless Dough-Boy was by +nature dull and torpid in his intellects, Pip, though over +tender-hearted, was at bottom very bright, with that pleasant, +genial, jolly brightness peculiar to his tribe; a tribe, which ever +enjoy all holidays and festivities with finer, freer relish than any +other race. For blacks, the year's calendar should show naught but +three hundred and sixty-five Fourth of Julys and New Year's Days. +Nor smile so, while I write that this little black was brilliant, for +even blackness has its brilliancy; behold yon lustrous ebony, +panelled in king's cabinets. But Pip loved life, and all life's +peaceable securities; so that the panic-striking business in which he +had somehow unaccountably become entrapped, had most sadly blurred +his brightness; though, as ere long will be seen, what was thus +temporarily subdued in him, in the end was destined to be luridly +illumined by strange wild fires, that fictitiously showed him off to +ten times the natural lustre with which in his native Tolland County +in Connecticut, he had once enlivened many a fiddler's frolic on the +green; and at melodious even-tide, with his gay ha-ha! had turned the +round horizon into one star-belled tambourine. So, though in the +clear air of day, suspended against a blue-veined neck, the +pure-watered diamond drop will healthful glow; yet, when the cunning +jeweller would show you the diamond in its most impressive lustre, he +lays it against a gloomy ground, and then lights it up, not by the +sun, but by some unnatural gases. Then come out those fiery +effulgences, infernally superb; then the evil-blazing diamond, once +the divinest symbol of the crystal skies, looks like some crown-jewel +stolen from the King of Hell. But let us to the story. + +It came to pass, that in the ambergris affair Stubb's after-oarsman +chanced so to sprain his hand, as for a time to become quite maimed; +and, temporarily, Pip was put into his place. + +The first time Stubb lowered with him, Pip evinced much nervousness; +but happily, for that time, escaped close contact with the whale; and +therefore came off not altogether discreditably; though Stubb +observing him, took care, afterwards, to exhort him to cherish his +courageousness to the utmost, for he might often find it needful. + +Now upon the second lowering, the boat paddled upon the whale; and as +the fish received the darted iron, it gave its customary rap, which +happened, in this instance, to be right under poor Pip's seat. The +involuntary consternation of the moment caused him to leap, paddle in +hand, out of the boat; and in such a way, that part of the slack +whale line coming against his chest, he breasted it overboard with +him, so as to become entangled in it, when at last plumping into the +water. That instant the stricken whale started on a fierce run, the +line swiftly straightened; and presto! poor Pip came all foaming up +to the chocks of the boat, remorselessly dragged there by the line, +which had taken several turns around his chest and neck. + +Tashtego stood in the bows. He was full of the fire of the hunt. He +hated Pip for a poltroon. Snatching the boat-knife from its sheath, +he suspended its sharp edge over the line, and turning towards Stubb, +exclaimed interrogatively, "Cut?" Meantime Pip's blue, choked face +plainly looked, Do, for God's sake! All passed in a flash. In less +than half a minute, this entire thing happened. + +"Damn him, cut!" roared Stubb; and so the whale was lost and Pip was +saved. + +So soon as he recovered himself, the poor little negro was assailed +by yells and execrations from the crew. Tranquilly permitting these +irregular cursings to evaporate, Stubb then in a plain, +business-like, but still half humorous manner, cursed Pip officially; +and that done, unofficially gave him much wholesome advice. The +substance was, Never jump from a boat, Pip, except--but all the rest +was indefinite, as the soundest advice ever is. Now, in general, +STICK TO THE BOAT, is your true motto in whaling; but cases will +sometimes happen when LEAP FROM THE BOAT, is still better. Moreover, +as if perceiving at last that if he should give undiluted +conscientious advice to Pip, he would be leaving him too wide a +margin to jump in for the future; Stubb suddenly dropped all advice, +and concluded with a peremptory command, "Stick to the boat, Pip, or +by the Lord, I won't pick you up if you jump; mind that. We can't +afford to lose whales by the likes of you; a whale would sell for +thirty times what you would, Pip, in Alabama. Bear that in mind, and +don't jump any more." Hereby perhaps Stubb indirectly hinted, that +though man loved his fellow, yet man is a money-making animal, which +propensity too often interferes with his benevolence. + +But we are all in the hands of the Gods; and Pip jumped again. It +was under very similar circumstances to the first performance; but +this time he did not breast out the line; and hence, when the whale +started to run, Pip was left behind on the sea, like a hurried +traveller's trunk. Alas! Stubb was but too true to his word. It +was a beautiful, bounteous, blue day; the spangled sea calm and +cool, and flatly stretching away, all round, to the horizon, like +gold-beater's skin hammered out to the extremest. Bobbing up and +down in that sea, Pip's ebon head showed like a head of cloves. No +boat-knife was lifted when he fell so rapidly astern. Stubb's +inexorable back was turned upon him; and the whale was winged. In +three minutes, a whole mile of shoreless ocean was between Pip and +Stubb. Out from the centre of the sea, poor Pip turned his crisp, +curling, black head to the sun, another lonely castaway, though the +loftiest and the brightest. + +Now, in calm weather, to swim in the open ocean is as easy to the +practised swimmer as to ride in a spring-carriage ashore. But the +awful lonesomeness is intolerable. The intense concentration of self +in the middle of such a heartless immensity, my God! who can tell it? +Mark, how when sailors in a dead calm bathe in the open sea--mark +how closely they hug their ship and only coast along her sides. + +But had Stubb really abandoned the poor little negro to his fate? +No; he did not mean to, at least. Because there were two boats in +his wake, and he supposed, no doubt, that they would of course come +up to Pip very quickly, and pick him up; though, indeed, such +considerations towards oarsmen jeopardized through their own +timidity, is not always manifested by the hunters in all similar +instances; and such instances not unfrequently occur; almost +invariably in the fishery, a coward, so called, is marked with the +same ruthless detestation peculiar to military navies and armies. + +But it so happened, that those boats, without seeing Pip, suddenly +spying whales close to them on one side, turned, and gave chase; and +Stubb's boat was now so far away, and he and all his crew so intent +upon his fish, that Pip's ringed horizon began to expand around him +miserably. By the merest chance the ship itself at last rescued him; +but from that hour the little negro went about the deck an idiot; +such, at least, they said he was. The sea had jeeringly kept his +finite body up, but drowned the infinite of his soul. Not drowned +entirely, though. Rather carried down alive to wondrous depths, +where strange shapes of the unwarped primal world glided to and fro +before his passive eyes; and the miser-merman, Wisdom, revealed his +hoarded heaps; and among the joyous, heartless, ever-juvenile +eternities, Pip saw the multitudinous, God-omnipresent, coral +insects, that out of the firmament of waters heaved the colossal +orbs. He saw God's foot upon the treadle of the loom, and spoke it; +and therefore his shipmates called him mad. So man's insanity is +heaven's sense; and wandering from all mortal reason, man comes at +last to that celestial thought, which, to reason, is absurd and +frantic; and weal or woe, feels then uncompromised, indifferent as +his God. + +For the rest, blame not Stubb too hardly. The thing is common in +that fishery; and in the sequel of the narrative, it will then be +seen what like abandonment befell myself. + + + +CHAPTER 94 + +A Squeeze of the Hand. + + +That whale of Stubb's, so dearly purchased, was duly brought to the +Pequod's side, where all those cutting and hoisting operations +previously detailed, were regularly gone through, even to the baling +of the Heidelburgh Tun, or Case. + +While some were occupied with this latter duty, others were employed +in dragging away the larger tubs, so soon as filled with the sperm; +and when the proper time arrived, this same sperm was carefully +manipulated ere going to the try-works, of which anon. + +It had cooled and crystallized to such a degree, that when, with +several others, I sat down before a large Constantine's bath of it, I +found it strangely concreted into lumps, here and there rolling about +in the liquid part. It was our business to squeeze these lumps back +into fluid. A sweet and unctuous duty! No wonder that in old times +this sperm was such a favourite cosmetic. Such a clearer! such a +sweetener! such a softener! such a delicious molifier! After +having my hands in it for only a few minutes, my fingers felt like +eels, and began, as it were, to serpentine and spiralise. + +As I sat there at my ease, cross-legged on the deck; after the bitter +exertion at the windlass; under a blue tranquil sky; the ship under +indolent sail, and gliding so serenely along; as I bathed my hands +among those soft, gentle globules of infiltrated tissues, woven +almost within the hour; as they richly broke to my fingers, and +discharged all their opulence, like fully ripe grapes their wine; as +I snuffed up that uncontaminated aroma,--literally and truly, like +the smell of spring violets; I declare to you, that for the time I +lived as in a musky meadow; I forgot all about our horrible oath; in +that inexpressible sperm, I washed my hands and my heart of it; I +almost began to credit the old Paracelsan superstition that sperm is +of rare virtue in allaying the heat of anger; while bathing in that +bath, I felt divinely free from all ill-will, or petulance, or +malice, of any sort whatsoever. + +Squeeze! squeeze! squeeze! all the morning long; I squeezed that +sperm till I myself almost melted into it; I squeezed that sperm till +a strange sort of insanity came over me; and I found myself +unwittingly squeezing my co-laborers' hands in it, mistaking their +hands for the gentle globules. Such an abounding, affectionate, +friendly, loving feeling did this avocation beget; that at last I was +continually squeezing their hands, and looking up into their eyes +sentimentally; as much as to say,--Oh! my dear fellow beings, why +should we longer cherish any social acerbities, or know the slightest +ill-humor or envy! Come; let us squeeze hands all round; nay, let us +all squeeze ourselves into each other; let us squeeze ourselves +universally into the very milk and sperm of kindness. + +Would that I could keep squeezing that sperm for ever! For now, +since by many prolonged, repeated experiences, I have perceived that +in all cases man must eventually lower, or at least shift, his +conceit of attainable felicity; not placing it anywhere in the +intellect or the fancy; but in the wife, the heart, the bed, the +table, the saddle, the fireside, the country; now that I have +perceived all this, I am ready to squeeze case eternally. In +thoughts of the visions of the night, I saw long rows of angels in +paradise, each with his hands in a jar of spermaceti. + +Now, while discoursing of sperm, it behooves to speak of other things +akin to it, in the business of preparing the sperm whale for the +try-works. + +First comes white-horse, so called, which is obtained from the +tapering part of the fish, and also from the thicker portions of his +flukes. It is tough with congealed tendons--a wad of muscle--but +still contains some oil. After being severed from the whale, the +white-horse is first cut into portable oblongs ere going to the +mincer. They look much like blocks of Berkshire marble. + +Plum-pudding is the term bestowed upon certain fragmentary parts of +the whale's flesh, here and there adhering to the blanket of blubber, +and often participating to a considerable degree in its unctuousness. +It is a most refreshing, convivial, beautiful object to behold. As +its name imports, it is of an exceedingly rich, mottled tint, with a +bestreaked snowy and golden ground, dotted with spots of the deepest +crimson and purple. It is plums of rubies, in pictures of citron. +Spite of reason, it is hard to keep yourself from eating it. I +confess, that once I stole behind the foremast to try it. It tasted +something as I should conceive a royal cutlet from the thigh of Louis +le Gros might have tasted, supposing him to have been killed the +first day after the venison season, and that particular venison +season contemporary with an unusually fine vintage of the vineyards +of Champagne. + +There is another substance, and a very singular one, which turns up +in the course of this business, but which I feel it to be very +puzzling adequately to describe. It is called slobgollion; an +appellation original with the whalemen, and even so is the nature of +the substance. It is an ineffably oozy, stringy affair, most +frequently found in the tubs of sperm, after a prolonged squeezing, +and subsequent decanting. I hold it to be the wondrously thin, +ruptured membranes of the case, coalescing. + +Gurry, so called, is a term properly belonging to right whalemen, but +sometimes incidentally used by the sperm fishermen. It designates +the dark, glutinous substance which is scraped off the back of the +Greenland or right whale, and much of which covers the decks of those +inferior souls who hunt that ignoble Leviathan. + +Nippers. Strictly this word is not indigenous to the whale's +vocabulary. But as applied by whalemen, it becomes so. A whaleman's +nipper is a short firm strip of tendinous stuff cut from the tapering +part of Leviathan's tail: it averages an inch in thickness, and for +the rest, is about the size of the iron part of a hoe. Edgewise +moved along the oily deck, it operates like a leathern squilgee; and +by nameless blandishments, as of magic, allures along with it all +impurities. + +But to learn all about these recondite matters, your best way is at +once to descend into the blubber-room, and have a long talk with its +inmates. This place has previously been mentioned as the receptacle +for the blanket-pieces, when stript and hoisted from the whale. When +the proper time arrives for cutting up its contents, this apartment +is a scene of terror to all tyros, especially by night. On one side, +lit by a dull lantern, a space has been left clear for the workmen. +They generally go in pairs,--a pike-and-gaffman and a spade-man. +The whaling-pike is similar to a frigate's boarding-weapon of the +same name. The gaff is something like a boat-hook. With his gaff, +the gaffman hooks on to a sheet of blubber, and strives to hold it +from slipping, as the ship pitches and lurches about. Meanwhile, the +spade-man stands on the sheet itself, perpendicularly chopping it +into the portable horse-pieces. This spade is sharp as hone can make +it; the spademan's feet are shoeless; the thing he stands on will +sometimes irresistibly slide away from him, like a sledge. If he +cuts off one of his own toes, or one of his assistants', would you be +very much astonished? Toes are scarce among veteran blubber-room +men. + + + +CHAPTER 95 + +The Cassock. + + +Had you stepped on board the Pequod at a certain juncture of this +post-mortemizing of the whale; and had you strolled forward nigh the +windlass, pretty sure am I that you would have scanned with no small +curiosity a very strange, enigmatical object, which you would have +seen there, lying along lengthwise in the lee scuppers. Not the +wondrous cistern in the whale's huge head; not the prodigy of his +unhinged lower jaw; not the miracle of his symmetrical tail; none of +these would so surprise you, as half a glimpse of that unaccountable +cone,--longer than a Kentuckian is tall, nigh a foot in diameter at +the base, and jet-black as Yojo, the ebony idol of Queequeg. And an +idol, indeed, it is; or, rather, in old times, its likeness was. +Such an idol as that found in the secret groves of Queen Maachah in +Judea; and for worshipping which, King Asa, her son, did depose her, +and destroyed the idol, and burnt it for an abomination at the brook +Kedron, as darkly set forth in the 15th chapter of the First Book of +Kings. + +Look at the sailor, called the mincer, who now comes along, and +assisted by two allies, heavily backs the grandissimus, as the +mariners call it, and with bowed shoulders, staggers off with it as +if he were a grenadier carrying a dead comrade from the field. +Extending it upon the forecastle deck, he now proceeds cylindrically +to remove its dark pelt, as an African hunter the pelt of a boa. +This done he turns the pelt inside out, like a pantaloon leg; gives +it a good stretching, so as almost to double its diameter; and at +last hangs it, well spread, in the rigging, to dry. Ere long, it is +taken down; when removing some three feet of it, towards the pointed +extremity, and then cutting two slits for arm-holes at the other end, +he lengthwise slips himself bodily into it. The mincer now stands +before you invested in the full canonicals of his calling. +Immemorial to all his order, this investiture alone will adequately +protect him, while employed in the peculiar functions of his office. + +That office consists in mincing the horse-pieces of blubber for the +pots; an operation which is conducted at a curious wooden horse, +planted endwise against the bulwarks, and with a capacious tub +beneath it, into which the minced pieces drop, fast as the sheets +from a rapt orator's desk. Arrayed in decent black; occupying a +conspicuous pulpit; intent on bible leaves; what a candidate for an +archbishopric, what a lad for a Pope were this mincer!* + + +*Bible leaves! Bible leaves! This is the invariable cry from the +mates to the mincer. It enjoins him to be careful, and cut his work +into as thin slices as possible, inasmuch as by so doing the business +of boiling out the oil is much accelerated, and its quantity +considerably increased, besides perhaps improving it in quality. + + + +CHAPTER 96 + +The Try-Works. + + +Besides her hoisted boats, an American whaler is outwardly +distinguished by her try-works. She presents the curious anomaly of +the most solid masonry joining with oak and hemp in constituting the +completed ship. It is as if from the open field a brick-kiln were +transported to her planks. + +The try-works are planted between the foremast and mainmast, the +most roomy part of the deck. The timbers beneath are of a peculiar +strength, fitted to sustain the weight of an almost solid mass of +brick and mortar, some ten feet by eight square, and five in height. +The foundation does not penetrate the deck, but the masonry is firmly +secured to the surface by ponderous knees of iron bracing it on all +sides, and screwing it down to the timbers. On the flanks it is +cased with wood, and at top completely covered by a large, sloping, +battened hatchway. Removing this hatch we expose the great try-pots, +two in number, and each of several barrels' capacity. When not in +use, they are kept remarkably clean. Sometimes they are polished +with soapstone and sand, till they shine within like silver +punch-bowls. During the night-watches some cynical old sailors will +crawl into them and coil themselves away there for a nap. While +employed in polishing them--one man in each pot, side by side--many +confidential communications are carried on, over the iron lips. It +is a place also for profound mathematical meditation. It was in the +left hand try-pot of the Pequod, with the soapstone diligently +circling round me, that I was first indirectly struck by the +remarkable fact, that in geometry all bodies gliding along the +cycloid, my soapstone for example, will descend from any point in +precisely the same time. + +Removing the fire-board from the front of the try-works, the bare +masonry of that side is exposed, penetrated by the two iron mouths of +the furnaces, directly underneath the pots. These mouths are fitted +with heavy doors of iron. The intense heat of the fire is prevented +from communicating itself to the deck, by means of a shallow +reservoir extending under the entire inclosed surface of the works. +By a tunnel inserted at the rear, this reservoir is kept replenished +with water as fast as it evaporates. There are no external chimneys; +they open direct from the rear wall. And here let us go back for a +moment. + +It was about nine o'clock at night that the Pequod's try-works were +first started on this present voyage. It belonged to Stubb to +oversee the business. + +"All ready there? Off hatch, then, and start her. You cook, fire +the works." This was an easy thing, for the carpenter had been +thrusting his shavings into the furnace throughout the passage. Here +be it said that in a whaling voyage the first fire in the try-works has +to be fed for a time with wood. After that no wood is used, except +as a means of quick ignition to the staple fuel. In a word, after +being tried out, the crisp, shrivelled blubber, now called scraps or +fritters, still contains considerable of its unctuous properties. +These fritters feed the flames. Like a plethoric burning martyr, or +a self-consuming misanthrope, once ignited, the whale supplies his +own fuel and burns by his own body. Would that he consumed his own +smoke! for his smoke is horrible to inhale, and inhale it you must, +and not only that, but you must live in it for the time. It has an +unspeakable, wild, Hindoo odor about it, such as may lurk in the +vicinity of funereal pyres. It smells like the left wing of the day +of judgment; it is an argument for the pit. + +By midnight the works were in full operation. We were clear from the +carcase; sail had been made; the wind was freshening; the wild ocean +darkness was intense. But that darkness was licked up by the fierce +flames, which at intervals forked forth from the sooty flues, and +illuminated every lofty rope in the rigging, as with the famed Greek +fire. The burning ship drove on, as if remorselessly commissioned to +some vengeful deed. So the pitch and sulphur-freighted brigs of the +bold Hydriote, Canaris, issuing from their midnight harbors, with +broad sheets of flame for sails, bore down upon the Turkish frigates, +and folded them in conflagrations. + +The hatch, removed from the top of the works, now afforded a wide +hearth in front of them. Standing on this were the Tartarean shapes +of the pagan harpooneers, always the whale-ship's stokers. With huge +pronged poles they pitched hissing masses of blubber into the +scalding pots, or stirred up the fires beneath, till the snaky flames +darted, curling, out of the doors to catch them by the feet. The +smoke rolled away in sullen heaps. To every pitch of the ship there +was a pitch of the boiling oil, which seemed all eagerness to leap +into their faces. Opposite the mouth of the works, on the further +side of the wide wooden hearth, was the windlass. This served for a +sea-sofa. Here lounged the watch, when not otherwise employed, +looking into the red heat of the fire, till their eyes felt scorched +in their heads. Their tawny features, now all begrimed with smoke +and sweat, their matted beards, and the contrasting barbaric +brilliancy of their teeth, all these were strangely revealed in the +capricious emblazonings of the works. As they narrated to each other +their unholy adventures, their tales of terror told in words of +mirth; as their uncivilized laughter forked upwards out of them, like +the flames from the furnace; as to and fro, in their front, the +harpooneers wildly gesticulated with their huge pronged forks and +dippers; as the wind howled on, and the sea leaped, and the ship +groaned and dived, and yet steadfastly shot her red hell further and +further into the blackness of the sea and the night, and scornfully +champed the white bone in her mouth, and viciously spat round her on +all sides; then the rushing Pequod, freighted with savages, and laden +with fire, and burning a corpse, and plunging into that blackness of +darkness, seemed the material counterpart of her monomaniac +commander's soul. + +So seemed it to me, as I stood at her helm, and for long hours +silently guided the way of this fire-ship on the sea. Wrapped, for +that interval, in darkness myself, I but the better saw the redness, +the madness, the ghastliness of others. The continual sight of the +fiend shapes before me, capering half in smoke and half in fire, +these at last begat kindred visions in my soul, so soon as I began to +yield to that unaccountable drowsiness which ever would come over me +at a midnight helm. + +But that night, in particular, a strange (and ever since +inexplicable) thing occurred to me. Starting from a brief standing +sleep, I was horribly conscious of something fatally wrong. The +jaw-bone tiller smote my side, which leaned against it; in my ears +was the low hum of sails, just beginning to shake in the wind; I +thought my eyes were open; I was half conscious of putting my fingers +to the lids and mechanically stretching them still further apart. +But, spite of all this, I could see no compass before me to steer by; +though it seemed but a minute since I had been watching the card, by +the steady binnacle lamp illuminating it. Nothing seemed before me +but a jet gloom, now and then made ghastly by flashes of redness. +Uppermost was the impression, that whatever swift, rushing thing I +stood on was not so much bound to any haven ahead as rushing from all +havens astern. A stark, bewildered feeling, as of death, came over +me. Convulsively my hands grasped the tiller, but with the crazy +conceit that the tiller was, somehow, in some enchanted way, +inverted. My God! what is the matter with me? thought I. Lo! in my +brief sleep I had turned myself about, and was fronting the ship's +stern, with my back to her prow and the compass. In an instant I +faced back, just in time to prevent the vessel from flying up into +the wind, and very probably capsizing her. How glad and how grateful +the relief from this unnatural hallucination of the night, and the +fatal contingency of being brought by the lee! + +Look not too long in the face of the fire, O man! Never dream with +thy hand on the helm! Turn not thy back to the compass; accept the +first hint of the hitching tiller; believe not the artificial fire, +when its redness makes all things look ghastly. To-morrow, in the +natural sun, the skies will be bright; those who glared like devils +in the forking flames, the morn will show in far other, at least +gentler, relief; the glorious, golden, glad sun, the only true +lamp--all others but liars! + +Nevertheless the sun hides not Virginia's Dismal Swamp, nor Rome's +accursed Campagna, nor wide Sahara, nor all the millions of miles of +deserts and of griefs beneath the moon. The sun hides not the ocean, +which is the dark side of this earth, and which is two thirds of this +earth. So, therefore, that mortal man who hath more of joy than +sorrow in him, that mortal man cannot be true--not true, or +undeveloped. With books the same. The truest of all men was the Man +of Sorrows, and the truest of all books is Solomon's, and +Ecclesiastes is the fine hammered steel of woe. "All is vanity." +ALL. This wilful world hath not got hold of unchristian Solomon's +wisdom yet. But he who dodges hospitals and jails, and walks fast +crossing graveyards, and would rather talk of operas than hell; +calls Cowper, Young, Pascal, Rousseau, poor devils all of sick men; +and throughout a care-free lifetime swears by Rabelais as passing +wise, and therefore jolly;--not that man is fitted to sit down on +tomb-stones, and break the green damp mould with unfathomably +wondrous Solomon. + +But even Solomon, he says, "the man that wandereth out of the way of +understanding shall remain" (I.E., even while living) "in the +congregation of the dead." Give not thyself up, then, to fire, lest +it invert thee, deaden thee; as for the time it did me. There is a +wisdom that is woe; but there is a woe that is madness. And there is +a Catskill eagle in some souls that can alike dive down into the +blackest gorges, and soar out of them again and become invisible in +the sunny spaces. And even if he for ever flies within the gorge, +that gorge is in the mountains; so that even in his lowest swoop the +mountain eagle is still higher than other birds upon the plain, even +though they soar. + + + +CHAPTER 97 + +The Lamp. + + +Had you descended from the Pequod's try-works to the Pequod's +forecastle, where the off duty watch were sleeping, for one single +moment you would have almost thought you were standing in some +illuminated shrine of canonized kings and counsellors. There they +lay in their triangular oaken vaults, each mariner a chiselled +muteness; a score of lamps flashing upon his hooded eyes. + +In merchantmen, oil for the sailor is more scarce than the milk of +queens. To dress in the dark, and eat in the dark, and stumble in +darkness to his pallet, this is his usual lot. But the whaleman, as +he seeks the food of light, so he lives in light. He makes his berth +an Aladdin's lamp, and lays him down in it; so that in the pitchiest +night the ship's black hull still houses an illumination. + +See with what entire freedom the whaleman takes his handful of +lamps--often but old bottles and vials, though--to the copper cooler +at the try-works, and replenishes them there, as mugs of ale at a +vat. He burns, too, the purest of oil, in its unmanufactured, and, +therefore, unvitiated state; a fluid unknown to solar, lunar, or +astral contrivances ashore. It is sweet as early grass butter in +April. He goes and hunts for his oil, so as to be sure of its +freshness and genuineness, even as the traveller on the prairie hunts +up his own supper of game. + + + +CHAPTER 98 + +Stowing Down and Clearing Up. + + +Already has it been related how the great leviathan is afar off +descried from the mast-head; how he is chased over the watery moors, +and slaughtered in the valleys of the deep; how he is then towed +alongside and beheaded; and how (on the principle which entitled the +headsman of old to the garments in which the beheaded was killed) his +great padded surtout becomes the property of his executioner; how, in +due time, he is condemned to the pots, and, like Shadrach, Meshach, +and Abednego, his spermaceti, oil, and bone pass unscathed through +the fire;--but now it remains to conclude the last chapter of this +part of the description by rehearsing--singing, if I may--the +romantic proceeding of decanting off his oil into the casks and +striking them down into the hold, where once again leviathan returns +to his native profundities, sliding along beneath the surface as +before; but, alas! never more to rise and blow. + +While still warm, the oil, like hot punch, is received into the +six-barrel casks; and while, perhaps, the ship is pitching and +rolling this way and that in the midnight sea, the enormous casks are +slewed round and headed over, end for end, and sometimes perilously +scoot across the slippery deck, like so many land slides, till at +last man-handled and stayed in their course; and all round the hoops, +rap, rap, go as many hammers as can play upon them, for now, EX +OFFICIO, every sailor is a cooper. + +At length, when the last pint is casked, and all is cool, then the +great hatchways are unsealed, the bowels of the ship are thrown open, +and down go the casks to their final rest in the sea. This done, the +hatches are replaced, and hermetically closed, like a closet walled +up. + +In the sperm fishery, this is perhaps one of the most remarkable +incidents in all the business of whaling. One day the planks stream +with freshets of blood and oil; on the sacred quarter-deck enormous +masses of the whale's head are profanely piled; great rusty casks lie +about, as in a brewery yard; the smoke from the try-works has +besooted all the bulwarks; the mariners go about suffused with +unctuousness; the entire ship seems great leviathan himself; while on +all hands the din is deafening. + +But a day or two after, you look about you, and prick your ears in +this self-same ship; and were it not for the tell-tale boats and +try-works, you would all but swear you trod some silent merchant +vessel, with a most scrupulously neat commander. The unmanufactured +sperm oil possesses a singularly cleansing virtue. This is the +reason why the decks never look so white as just after what they call +an affair of oil. Besides, from the ashes of the burned scraps of +the whale, a potent lye is readily made; and whenever any +adhesiveness from the back of the whale remains clinging to the side, +that lye quickly exterminates it. Hands go diligently along the +bulwarks, and with buckets of water and rags restore them to their +full tidiness. The soot is brushed from the lower rigging. All the +numerous implements which have been in use are likewise faithfully +cleansed and put away. The great hatch is scrubbed and placed upon +the try-works, completely hiding the pots; every cask is out of +sight; all tackles are coiled in unseen nooks; and when by the +combined and simultaneous industry of almost the entire ship's +company, the whole of this conscientious duty is at last concluded, +then the crew themselves proceed to their own ablutions; shift +themselves from top to toe; and finally issue to the immaculate deck, +fresh and all aglow, as bridegrooms new-leaped from out the daintiest +Holland. + +Now, with elated step, they pace the planks in twos and threes, and +humorously discourse of parlors, sofas, carpets, and fine cambrics; +propose to mat the deck; think of having hanging to the top; object +not to taking tea by moonlight on the piazza of the forecastle. To +hint to such musked mariners of oil, and bone, and blubber, were +little short of audacity. They know not the thing you distantly +allude to. Away, and bring us napkins! + +But mark: aloft there, at the three mast heads, stand three men +intent on spying out more whales, which, if caught, infallibly will +again soil the old oaken furniture, and drop at least one small +grease-spot somewhere. Yes; and many is the time, when, after the +severest uninterrupted labors, which know no night; continuing +straight through for ninety-six hours; when from the boat, where they +have swelled their wrists with all day rowing on the Line,--they only +step to the deck to carry vast chains, and heave the heavy windlass, +and cut and slash, yea, and in their very sweatings to be smoked and +burned anew by the combined fires of the equatorial sun and the +equatorial try-works; when, on the heel of all this, they have +finally bestirred themselves to cleanse the ship, and make a spotless +dairy room of it; many is the time the poor fellows, just buttoning +the necks of their clean frocks, are startled by the cry of "There +she blows!" and away they fly to fight another whale, and go through +the whole weary thing again. Oh! my friends, but this is +man-killing! Yet this is life. For hardly have we mortals by long +toilings extracted from this world's vast bulk its small but +valuable sperm; and then, with weary patience, cleansed ourselves +from its defilements, and learned to live here in clean tabernacles +of the soul; hardly is this done, when--THERE SHE BLOWS!--the ghost +is spouted up, and away we sail to fight some other world, and go +through young life's old routine again. + +Oh! the metempsychosis! Oh! Pythagoras, that in bright Greece, two +thousand years ago, did die, so good, so wise, so mild; I sailed with +thee along the Peruvian coast last voyage--and, foolish as I am, +taught thee, a green simple boy, how to splice a rope! + + + +CHAPTER 99 + +The Doubloon. + + +Ere now it has been related how Ahab was wont to pace his +quarter-deck, taking regular turns at either limit, the binnacle and +mainmast; but in the multiplicity of other things requiring narration +it has not been added how that sometimes in these walks, when most +plunged in his mood, he was wont to pause in turn at each spot, and +stand there strangely eyeing the particular object before him. When +he halted before the binnacle, with his glance fastened on the +pointed needle in the compass, that glance shot like a javelin with +the pointed intensity of his purpose; and when resuming his walk he +again paused before the mainmast, then, as the same riveted glance +fastened upon the riveted gold coin there, he still wore the same +aspect of nailed firmness, only dashed with a certain wild longing, +if not hopefulness. + +But one morning, turning to pass the doubloon, he seemed to be newly +attracted by the strange figures and inscriptions stamped on it, as +though now for the first time beginning to interpret for himself in +some monomaniac way whatever significance might lurk in them. And +some certain significance lurks in all things, else all things are +little worth, and the round world itself but an empty cipher, except +to sell by the cartload, as they do hills about Boston, to fill up +some morass in the Milky Way. + +Now this doubloon was of purest, virgin gold, raked somewhere out of +the heart of gorgeous hills, whence, east and west, over golden +sands, the head-waters of many a Pactolus flows. And though now +nailed amidst all the rustiness of iron bolts and the verdigris of +copper spikes, yet, untouchable and immaculate to any foulness, it +still preserved its Quito glow. Nor, though placed amongst a +ruthless crew and every hour passed by ruthless hands, and through +the livelong nights shrouded with thick darkness which might cover +any pilfering approach, nevertheless every sunrise found the doubloon +where the sunset left it last. For it was set apart and sanctified +to one awe-striking end; and however wanton in their sailor ways, one +and all, the mariners revered it as the white whale's talisman. +Sometimes they talked it over in the weary watch by night, wondering +whose it was to be at last, and whether he would ever live to spend +it. + +Now those noble golden coins of South America are as medals of the +sun and tropic token-pieces. Here palms, alpacas, and volcanoes; +sun's disks and stars; ecliptics, horns-of-plenty, and rich banners +waving, are in luxuriant profusion stamped; so that the precious gold +seems almost to derive an added preciousness and enhancing glories, +by passing through those fancy mints, so Spanishly poetic. + +It so chanced that the doubloon of the Pequod was a most wealthy +example of these things. On its round border it bore the letters, +REPUBLICA DEL ECUADOR: QUITO. So this bright coin came from a +country planted in the middle of the world, and beneath the great +equator, and named after it; and it had been cast midway up the +Andes, in the unwaning clime that knows no autumn. Zoned by those +letters you saw the likeness of three Andes' summits; from one a +flame; a tower on another; on the third a crowing cock; while arching +over all was a segment of the partitioned zodiac, the signs all +marked with their usual cabalistics, and the keystone sun entering +the equinoctial point at Libra. + +Before this equatorial coin, Ahab, not unobserved by others, was now +pausing. + +"There's something ever egotistical in mountain-tops and towers, and +all other grand and lofty things; look here,--three peaks as proud as +Lucifer. The firm tower, that is Ahab; the volcano, that is Ahab; +the courageous, the undaunted, and victorious fowl, that, too, is +Ahab; all are Ahab; and this round gold is but the image of the +rounder globe, which, like a magician's glass, to each and every man +in turn but mirrors back his own mysterious self. Great pains, small +gains for those who ask the world to solve them; it cannot solve +itself. Methinks now this coined sun wears a ruddy face; but see! +aye, he enters the sign of storms, the equinox! and but six months +before he wheeled out of a former equinox at Aries! From storm to +storm! So be it, then. Born in throes, 't is fit that man should +live in pains and die in pangs! So be it, then! Here's stout stuff +for woe to work on. So be it, then." + +"No fairy fingers can have pressed the gold, but devil's claws must have +left their mouldings there since yesterday," murmured Starbuck to +himself, leaning against the bulwarks. "The old man seems to read +Belshazzar's awful writing. I have never marked the coin +inspectingly. He goes below; let me read. A dark valley between +three mighty, heaven-abiding peaks, that almost seem the Trinity, in +some faint earthly symbol. So in this vale of Death, God girds us +round; and over all our gloom, the sun of Righteousness still shines +a beacon and a hope. If we bend down our eyes, the dark vale shows +her mouldy soil; but if we lift them, the bright sun meets our glance +half way, to cheer. Yet, oh, the great sun is no fixture; and if, at +midnight, we would fain snatch some sweet solace from him, we gaze +for him in vain! This coin speaks wisely, mildly, truly, but still +sadly to me. I will quit it, lest Truth shake me falsely." + +"There now's the old Mogul," soliloquized Stubb by the try-works, +"he's been twigging it; and there goes Starbuck from the same, and +both with faces which I should say might be somewhere within nine +fathoms long. And all from looking at a piece of gold, which did I +have it now on Negro Hill or in Corlaer's Hook, I'd not look at it +very long ere spending it. Humph! in my poor, insignificant opinion, +I regard this as queer. I have seen doubloons before now in my +voyagings; your doubloons of old Spain, your doubloons of Peru, your +doubloons of Chili, your doubloons of Bolivia, your doubloons of +Popayan; with plenty of gold moidores and pistoles, and joes, and +half joes, and quarter joes. What then should there be in this +doubloon of the Equator that is so killing wonderful? By Golconda! +let me read it once. Halloa! here's signs and wonders truly! That, +now, is what old Bowditch in his Epitome calls the zodiac, and what +my almanac below calls ditto. I'll get the almanac and as I have +heard devils can be raised with Daboll's arithmetic, I'll try my hand +at raising a meaning out of these queer curvicues here with the +Massachusetts calendar. Here's the book. Let's see now. Signs and +wonders; and the sun, he's always among 'em. Hem, hem, hem; here +they are--here they go--all alive:--Aries, or the Ram; Taurus, or the +Bull and Jimimi! here's Gemini himself, or the Twins. Well; the sun +he wheels among 'em. Aye, here on the coin he's just crossing the +threshold between two of twelve sitting-rooms all in a ring. Book! +you lie there; the fact is, you books must know your places. You'll +do to give us the bare words and facts, but we come in to supply the +thoughts. That's my small experience, so far as the Massachusetts +calendar, and Bowditch's navigator, and Daboll's arithmetic go. +Signs and wonders, eh? Pity if there is nothing wonderful in signs, +and significant in wonders! There's a clue somewhere; wait a bit; +hist--hark! By Jove, I have it! Look you, Doubloon, your zodiac +here is the life of man in one round chapter; and now I'll read it +off, straight out of the book. Come, Almanack! To begin: there's +Aries, or the Ram--lecherous dog, he begets us; then, Taurus, or the +Bull--he bumps us the first thing; then Gemini, or the Twins--that +is, Virtue and Vice; we try to reach Virtue, when lo! comes Cancer +the Crab, and drags us back; and here, going from Virtue, Leo, a +roaring Lion, lies in the path--he gives a few fierce bites and surly +dabs with his paw; we escape, and hail Virgo, the Virgin! that's our +first love; we marry and think to be happy for aye, when pop comes +Libra, or the Scales--happiness weighed and found wanting; and while +we are very sad about that, Lord! how we suddenly jump, as Scorpio, +or the Scorpion, stings us in the rear; we are curing the wound, when +whang come the arrows all round; Sagittarius, or the Archer, is +amusing himself. As we pluck out the shafts, stand aside! here's +the battering-ram, Capricornus, or the Goat; full tilt, he comes +rushing, and headlong we are tossed; when Aquarius, or the +Water-bearer, pours out his whole deluge and drowns us; and to wind +up with Pisces, or the Fishes, we sleep. There's a sermon now, writ +in high heaven, and the sun goes through it every year, and yet comes +out of it all alive and hearty. Jollily he, aloft there, wheels +through toil and trouble; and so, alow here, does jolly Stubb. Oh, +jolly's the word for aye! Adieu, Doubloon! But stop; here comes +little King-Post; dodge round the try-works, now, and let's hear what +he'll have to say. There; he's before it; he'll out with something +presently. So, so; he's beginning." + +"I see nothing here, but a round thing made of gold, and whoever +raises a certain whale, this round thing belongs to him. So, what's +all this staring been about? It is worth sixteen dollars, that's +true; and at two cents the cigar, that's nine hundred and sixty +cigars. I won't smoke dirty pipes like Stubb, but I like cigars, and +here's nine hundred and sixty of them; so here goes Flask aloft to +spy 'em out." + +"Shall I call that wise or foolish, now; if it be really wise it has +a foolish look to it; yet, if it be really foolish, then has it a +sort of wiseish look to it. But, avast; here comes our old +Manxman--the old hearse-driver, he must have been, that is, before he +took to the sea. He luffs up before the doubloon; halloa, and goes +round on the other side of the mast; why, there's a horse-shoe nailed +on that side; and now he's back again; what does that mean? Hark! +he's muttering--voice like an old worn-out coffee-mill. Prick ears, +and listen!" + +"If the White Whale be raised, it must be in a month and a day, when +the sun stands in some one of these signs. I've studied signs, and +know their marks; they were taught me two score years ago, by the old +witch in Copenhagen. Now, in what sign will the sun then be? The +horse-shoe sign; for there it is, right opposite the gold. And +what's the horse-shoe sign? The lion is the horse-shoe sign--the +roaring and devouring lion. Ship, old ship! my old head shakes to +think of thee." + +"There's another rendering now; but still one text. All sorts of men +in one kind of world, you see. Dodge again! here comes Queequeg--all +tattooing--looks like the signs of the Zodiac himself. What says the +Cannibal? As I live he's comparing notes; looking at his thigh bone; +thinks the sun is in the thigh, or in the calf, or in the bowels, I +suppose, as the old women talk Surgeon's Astronomy in the back +country. And by Jove, he's found something there in the vicinity of +his thigh--I guess it's Sagittarius, or the Archer. No: he don't +know what to make of the doubloon; he takes it for an old button off +some king's trowsers. But, aside again! here comes that ghost-devil, +Fedallah; tail coiled out of sight as usual, oakum in the toes of his +pumps as usual. What does he say, with that look of his? Ah, only +makes a sign to the sign and bows himself; there is a sun on the +coin--fire worshipper, depend upon it. Ho! more and more. This way +comes Pip--poor boy! would he had died, or I; he's half horrible to +me. He too has been watching all of these interpreters--myself +included--and look now, he comes to read, with that unearthly idiot +face. Stand away again and hear him. Hark!" + +"I look, you look, he looks; we look, ye look, they look." + +"Upon my soul, he's been studying Murray's Grammar! Improving his +mind, poor fellow! But what's that he says now--hist!" + +"I look, you look, he looks; we look, ye look, they look." + +"Why, he's getting it by heart--hist! again." + +"I look, you look, he looks; we look, ye look, they look." + +"Well, that's funny." + +"And I, you, and he; and we, ye, and they, are all bats; and I'm a +crow, especially when I stand a'top of this pine tree here. Caw! +caw! caw! caw! caw! caw! Ain't I a crow? And where's the +scare-crow? There he stands; two bones stuck into a pair of old +trowsers, and two more poked into the sleeves of an old jacket." + +"Wonder if he means me?--complimentary!--poor lad!--I could go hang +myself. Any way, for the present, I'll quit Pip's vicinity. I can +stand the rest, for they have plain wits; but he's too crazy-witty +for my sanity. So, so, I leave him muttering." + +"Here's the ship's navel, this doubloon here, and they are all on +fire to unscrew it. But, unscrew your navel, and what's the +consequence? Then again, if it stays here, that is ugly, too, for +when aught's nailed to the mast it's a sign that things grow +desperate. Ha, ha! old Ahab! the White Whale; he'll nail ye! This +is a pine tree. My father, in old Tolland county, cut down a pine +tree once, and found a silver ring grown over in it; some old +darkey's wedding ring. How did it get there? And so they'll say in +the resurrection, when they come to fish up this old mast, and find a +doubloon lodged in it, with bedded oysters for the shaggy bark. Oh, +the gold! the precious, precious, gold! the green miser'll hoard ye +soon! Hish! hish! God goes 'mong the worlds blackberrying. Cook! +ho, cook! and cook us! Jenny! hey, hey, hey, hey, hey, Jenny, Jenny! +and get your hoe-cake done!" + + + +CHAPTER 100 + +Leg and Arm. + +The Pequod, of Nantucket, Meets the Samuel Enderby, of London. + + +"Ship, ahoy! Hast seen the White Whale?" + +So cried Ahab, once more hailing a ship showing English colours, +bearing down under the stern. Trumpet to mouth, the old man was +standing in his hoisted quarter-boat, his ivory leg plainly revealed +to the stranger captain, who was carelessly reclining in his own +boat's bow. He was a darkly-tanned, burly, good-natured, +fine-looking man, of sixty or thereabouts, dressed in a spacious +roundabout, that hung round him in festoons of blue pilot-cloth; and +one empty arm of this jacket streamed behind him like the broidered +arm of a hussar's surcoat. + +"Hast seen the White Whale!" + +"See you this?" and withdrawing it from the folds that had hidden +it, he held up a white arm of sperm whale bone, terminating in a +wooden head like a mallet. + +"Man my boat!" cried Ahab, impetuously, and tossing about the oars +near him--"Stand by to lower!" + +In less than a minute, without quitting his little craft, he and his +crew were dropped to the water, and were soon alongside of the +stranger. But here a curious difficulty presented itself. In the +excitement of the moment, Ahab had forgotten that since the loss of +his leg he had never once stepped on board of any vessel at sea but +his own, and then it was always by an ingenious and very handy +mechanical contrivance peculiar to the Pequod, and a thing not to be +rigged and shipped in any other vessel at a moment's warning. Now, +it is no very easy matter for anybody--except those who are almost +hourly used to it, like whalemen--to clamber up a ship's side from a +boat on the open sea; for the great swells now lift the boat high up +towards the bulwarks, and then instantaneously drop it half way down +to the kelson. So, deprived of one leg, and the strange ship of +course being altogether unsupplied with the kindly invention, Ahab +now found himself abjectly reduced to a clumsy landsman again; +hopelessly eyeing the uncertain changeful height he could hardly hope +to attain. + +It has before been hinted, perhaps, that every little untoward +circumstance that befell him, and which indirectly sprang from his +luckless mishap, almost invariably irritated or exasperated Ahab. +And in the present instance, all this was heightened by the sight of +the two officers of the strange ship, leaning over the side, by the +perpendicular ladder of nailed cleets there, and swinging towards him +a pair of tastefully-ornamented man-ropes; for at first they did not +seem to bethink them that a one-legged man must be too much of a +cripple to use their sea bannisters. But this awkwardness only +lasted a minute, because the strange captain, observing at a glance +how affairs stood, cried out, "I see, I see!--avast heaving there! +Jump, boys, and swing over the cutting-tackle." + +As good luck would have it, they had had a whale alongside a day or +two previous, and the great tackles were still aloft, and the massive +curved blubber-hook, now clean and dry, was still attached to the +end. This was quickly lowered to Ahab, who at once comprehending it +all, slid his solitary thigh into the curve of the hook (it was like +sitting in the fluke of an anchor, or the crotch of an apple tree), +and then giving the word, held himself fast, and at the same time +also helped to hoist his own weight, by pulling hand-over-hand upon +one of the running parts of the tackle. Soon he was carefully swung +inside the high bulwarks, and gently landed upon the capstan head. +With his ivory arm frankly thrust forth in welcome, the other captain +advanced, and Ahab, putting out his ivory leg, and crossing the ivory +arm (like two sword-fish blades) cried out in his walrus way, "Aye, +aye, hearty! let us shake bones together!--an arm and a leg!--an arm +that never can shrink, d'ye see; and a leg that never can run. Where +did'st thou see the White Whale?--how long ago?" + +"The White Whale," said the Englishman, pointing his ivory arm +towards the East, and taking a rueful sight along it, as if it had +been a telescope; "there I saw him, on the Line, last season." + +"And he took that arm off, did he?" asked Ahab, now sliding down from +the capstan, and resting on the Englishman's shoulder, as he did so. + +"Aye, he was the cause of it, at least; and that leg, too?" + +"Spin me the yarn," said Ahab; "how was it?" + +"It was the first time in my life that I ever cruised on the Line," +began the Englishman. "I was ignorant of the White Whale at that +time. Well, one day we lowered for a pod of four or five whales, and +my boat fastened to one of them; a regular circus horse he was, too, +that went milling and milling round so, that my boat's crew could +only trim dish, by sitting all their sterns on the outer gunwale. +Presently up breaches from the bottom of the sea a bouncing great +whale, with a milky-white head and hump, all crows' feet and +wrinkles." + +"It was he, it was he!" cried Ahab, suddenly letting out his +suspended breath. + +"And harpoons sticking in near his starboard fin." + +"Aye, aye--they were mine--MY irons," cried Ahab, exultingly--"but +on!" + +"Give me a chance, then," said the Englishman, good-humoredly. +"Well, this old great-grandfather, with the white head and hump, runs +all afoam into the pod, and goes to snapping furiously at my +fast-line! + +"Aye, I see!--wanted to part it; free the fast-fish--an old trick--I +know him." + +"How it was exactly," continued the one-armed commander, "I do not +know; but in biting the line, it got foul of his teeth, caught there +somehow; but we didn't know it then; so that when we afterwards +pulled on the line, bounce we came plump on to his hump! instead of +the other whale's; that went off to windward, all fluking. Seeing +how matters stood, and what a noble great whale it was--the noblest +and biggest I ever saw, sir, in my life--I resolved to capture him, +spite of the boiling rage he seemed to be in. And thinking the +hap-hazard line would get loose, or the tooth it was tangled to +might draw (for I have a devil of a boat's crew for a pull on a +whale-line); seeing all this, I say, I jumped into my first mate's +boat--Mr. Mounttop's here (by the way, Captain--Mounttop; +Mounttop--the captain);--as I was saying, I jumped into Mounttop's +boat, which, d'ye see, was gunwale and gunwale with mine, then; and +snatching the first harpoon, let this old great-grandfather have it. +But, Lord, look you, sir--hearts and souls alive, man--the next +instant, in a jiff, I was blind as a bat--both eyes out--all befogged +and bedeadened with black foam--the whale's tail looming straight up +out of it, perpendicular in the air, like a marble steeple. No use +sterning all, then; but as I was groping at midday, with a blinding +sun, all crown-jewels; as I was groping, I say, after the second +iron, to toss it overboard--down comes the tail like a Lima tower, +cutting my boat in two, leaving each half in splinters; and, flukes +first, the white hump backed through the wreck, as though it was all +chips. We all struck out. To escape his terrible flailings, I +seized hold of my harpoon-pole sticking in him, and for a moment +clung to that like a sucking fish. But a combing sea dashed me off, +and at the same instant, the fish, taking one good dart forwards, +went down like a flash; and the barb of that cursed second iron +towing along near me caught me here" (clapping his hand just below +his shoulder); "yes, caught me just here, I say, and bore me down to +Hell's flames, I was thinking; when, when, all of a sudden, thank the +good God, the barb ript its way along the flesh--clear along the +whole length of my arm--came out nigh my wrist, and up I +floated;--and that gentleman there will tell you the rest (by the +way, captain--Dr. Bunger, ship's surgeon: Bunger, my lad,--the +captain). Now, Bunger boy, spin your part of the yarn." + +The professional gentleman thus familiarly pointed out, had been all +the time standing near them, with nothing specific visible, to denote +his gentlemanly rank on board. His face was an exceedingly round but +sober one; he was dressed in a faded blue woollen frock or shirt, and +patched trowsers; and had thus far been dividing his attention +between a marlingspike he held in one hand, and a pill-box held in +the other, occasionally casting a critical glance at the ivory limbs +of the two crippled captains. But, at his superior's introduction of +him to Ahab, he politely bowed, and straightway went on to do his +captain's bidding. + +"It was a shocking bad wound," began the whale-surgeon; "and, taking +my advice, Captain Boomer here, stood our old Sammy--" + +"Samuel Enderby is the name of my ship," interrupted the one-armed +captain, addressing Ahab; "go on, boy." + +"Stood our old Sammy off to the northward, to get out of the blazing +hot weather there on the Line. But it was no use--I did all I could; +sat up with him nights; was very severe with him in the matter of +diet--" + +"Oh, very severe!" chimed in the patient himself; then suddenly +altering his voice, "Drinking hot rum toddies with me every night, +till he couldn't see to put on the bandages; and sending me to bed, +half seas over, about three o'clock in the morning. Oh, ye stars! he +sat up with me indeed, and was very severe in my diet. Oh! a great +watcher, and very dietetically severe, is Dr. Bunger. (Bunger, you +dog, laugh out! why don't ye? You know you're a precious jolly +rascal.) But, heave ahead, boy, I'd rather be killed by you than kept +alive by any other man." + +"My captain, you must have ere this perceived, respected sir"--said +the imperturbable godly-looking Bunger, slightly bowing to Ahab--"is +apt to be facetious at times; he spins us many clever things of that +sort. But I may as well say--en passant, as the French remark--that +I myself--that is to say, Jack Bunger, late of the reverend +clergy--am a strict total abstinence man; I never drink--" + +"Water!" cried the captain; "he never drinks it; it's a sort of fits +to him; fresh water throws him into the hydrophobia; but go on--go on +with the arm story." + +"Yes, I may as well," said the surgeon, coolly. "I was about +observing, sir, before Captain Boomer's facetious interruption, that +spite of my best and severest endeavors, the wound kept getting worse +and worse; the truth was, sir, it was as ugly gaping wound as surgeon +ever saw; more than two feet and several inches long. I measured it +with the lead line. In short, it grew black; I knew what was +threatened, and off it came. But I had no hand in shipping that +ivory arm there; that thing is against all rule"--pointing at it with +the marlingspike--"that is the captain's work, not mine; he ordered +the carpenter to make it; he had that club-hammer there put to the +end, to knock some one's brains out with, I suppose, as he tried mine +once. He flies into diabolical passions sometimes. Do ye see this +dent, sir"--removing his hat, and brushing aside his hair, and +exposing a bowl-like cavity in his skull, but which bore not the +slightest scarry trace, or any token of ever having been a +wound--"Well, the captain there will tell you how that came here; +he knows." + +"No, I don't," said the captain, "but his mother did; he was born +with it. Oh, you solemn rogue, you--you Bunger! was there ever such +another Bunger in the watery world? Bunger, when you die, you ought +to die in pickle, you dog; you should be preserved to future ages, +you rascal." + +"What became of the White Whale?" now cried Ahab, who thus far had +been impatiently listening to this by-play between the two +Englishmen. + +"Oh!" cried the one-armed captain, "oh, yes! Well; after he sounded, +we didn't see him again for some time; in fact, as I before hinted, I +didn't then know what whale it was that had served me such a trick, +till some time afterwards, when coming back to the Line, we heard +about Moby Dick--as some call him--and then I knew it was he." + +"Did'st thou cross his wake again?" + +"Twice." + +"But could not fasten?" + +"Didn't want to try to: ain't one limb enough? What should I do +without this other arm? And I'm thinking Moby Dick doesn't bite so +much as he swallows." + +"Well, then," interrupted Bunger, "give him your left arm for bait to +get the right. Do you know, gentlemen"--very gravely and +mathematically bowing to each Captain in succession--"Do you know, +gentlemen, that the digestive organs of the whale are so inscrutably +constructed by Divine Providence, that it is quite impossible for him +to completely digest even a man's arm? And he knows it too. So that +what you take for the White Whale's malice is only his awkwardness. +For he never means to swallow a single limb; he only thinks to +terrify by feints. But sometimes he is like the old juggling fellow, +formerly a patient of mine in Ceylon, that making believe swallow +jack-knives, once upon a time let one drop into him in good earnest, +and there it stayed for a twelvemonth or more; when I gave him an +emetic, and he heaved it up in small tacks, d'ye see. No possible +way for him to digest that jack-knife, and fully incorporate it into +his general bodily system. Yes, Captain Boomer, if you are quick +enough about it, and have a mind to pawn one arm for the sake of the +privilege of giving decent burial to the other, why in that case +the arm is yours; only let the whale have another chance at you +shortly, that's all." + +"No, thank ye, Bunger," said the English Captain, "he's welcome to +the arm he has, since I can't help it, and didn't know him then; but +not to another one. No more White Whales for me; I've lowered for +him once, and that has satisfied me. There would be great glory in +killing him, I know that; and there is a ship-load of precious sperm +in him, but, hark ye, he's best let alone; don't you think so, +Captain?"--glancing at the ivory leg. + +"He is. But he will still be hunted, for all that. What is best let +alone, that accursed thing is not always what least allures. He's +all a magnet! How long since thou saw'st him last? Which way +heading?" + +"Bless my soul, and curse the foul fiend's," cried Bunger, stoopingly +walking round Ahab, and like a dog, strangely snuffing; "this man's +blood--bring the thermometer!--it's at the boiling point!--his pulse +makes these planks beat!--sir!"--taking a lancet from his pocket, and +drawing near to Ahab's arm. + +"Avast!" roared Ahab, dashing him against the bulwarks--"Man the +boat! Which way heading?" + +"Good God!" cried the English Captain, to whom the question was put. +"What's the matter? He was heading east, I think.--Is your Captain +crazy?" whispering Fedallah. + +But Fedallah, putting a finger on his lip, slid over the bulwarks to +take the boat's steering oar, and Ahab, swinging the cutting-tackle +towards him, commanded the ship's sailors to stand by to lower. + +In a moment he was standing in the boat's stern, and the Manilla men +were springing to their oars. In vain the English Captain hailed +him. With back to the stranger ship, and face set like a flint to +his own, Ahab stood upright till alongside of the Pequod. + + + +CHAPTER 101 + +The Decanter. + + +Ere the English ship fades from sight, be it set down here, that she +hailed from London, and was named after the late Samuel Enderby, +merchant of that city, the original of the famous whaling house of +Enderby & Sons; a house which in my poor whaleman's opinion, comes +not far behind the united royal houses of the Tudors and Bourbons, in +point of real historical interest. How long, prior to the year of +our Lord 1775, this great whaling house was in existence, my numerous +fish-documents do not make plain; but in that year (1775) it fitted +out the first English ships that ever regularly hunted the Sperm +Whale; though for some score of years previous (ever since 1726) our +valiant Coffins and Maceys of Nantucket and the Vineyard had in large +fleets pursued that Leviathan, but only in the North and South +Atlantic: not elsewhere. Be it distinctly recorded here, that the +Nantucketers were the first among mankind to harpoon with civilized +steel the great Sperm Whale; and that for half a century they were +the only people of the whole globe who so harpooned him. + +In 1778, a fine ship, the Amelia, fitted out for the express purpose, +and at the sole charge of the vigorous Enderbys, boldly rounded Cape +Horn, and was the first among the nations to lower a whale-boat of +any sort in the great South Sea. The voyage was a skilful and lucky +one; and returning to her berth with her hold full of the precious +sperm, the Amelia's example was soon followed by other ships, English +and American, and thus the vast Sperm Whale grounds of the Pacific +were thrown open. But not content with this good deed, the +indefatigable house again bestirred itself: Samuel and all his +Sons--how many, their mother only knows--and under their immediate +auspices, and partly, I think, at their expense, the British +government was induced to send the sloop-of-war Rattler on a whaling +voyage of discovery into the South Sea. Commanded by a naval +Post-Captain, the Rattler made a rattling voyage of it, and did some +service; how much does not appear. But this is not all. In 1819, +the same house fitted out a discovery whale ship of their own, to go +on a tasting cruise to the remote waters of Japan. That ship--well +called the "Syren"--made a noble experimental cruise; and it was thus +that the great Japanese Whaling Ground first became generally known. +The Syren in this famous voyage was commanded by a Captain Coffin, a +Nantucketer. + +All honour to the Enderbies, therefore, whose house, I think, exists +to the present day; though doubtless the original Samuel must long +ago have slipped his cable for the great South Sea of the other +world. + +The ship named after him was worthy of the honour, being a very fast +sailer and a noble craft every way. I boarded her once at midnight +somewhere off the Patagonian coast, and drank good flip down in the +forecastle. It was a fine gam we had, and they were all +trumps--every soul on board. A short life to them, and a jolly +death. And that fine gam I had--long, very long after old Ahab +touched her planks with his ivory heel--it minds me of the noble, +solid, Saxon hospitality of that ship; and may my parson forget me, +and the devil remember me, if I ever lose sight of it. Flip? Did I +say we had flip? Yes, and we flipped it at the rate of ten gallons +the hour; and when the squall came (for it's squally off there by +Patagonia), and all hands--visitors and all--were called to reef +topsails, we were so top-heavy that we had to swing each other aloft +in bowlines; and we ignorantly furled the skirts of our jackets into +the sails, so that we hung there, reefed fast in the howling gale, a +warning example to all drunken tars. However, the masts did not go +overboard; and by and by we scrambled down, so sober, that we had to +pass the flip again, though the savage salt spray bursting down the +forecastle scuttle, rather too much diluted and pickled it to my +taste. + +The beef was fine--tough, but with body in it. They said it was +bull-beef; others, that it was dromedary beef; but I do not know, for +certain, how that was. They had dumplings too; small, but +substantial, symmetrically globular, and indestructible dumplings. I +fancied that you could feel them, and roll them about in you after +they were swallowed. If you stooped over too far forward, you risked +their pitching out of you like billiard-balls. The bread--but that +couldn't be helped; besides, it was an anti-scorbutic; in short, the +bread contained the only fresh fare they had. But the forecastle was +not very light, and it was very easy to step over into a dark corner +when you ate it. But all in all, taking her from truck to helm, +considering the dimensions of the cook's boilers, including his own +live parchment boilers; fore and aft, I say, the Samuel Enderby was a +jolly ship; of good fare and plenty; fine flip and strong; crack +fellows all, and capital from boot heels to hat-band. + +But why was it, think ye, that the Samuel Enderby, and some other +English whalers I know of--not all though--were such famous, +hospitable ships; that passed round the beef, and the bread, and the +can, and the joke; and were not soon weary of eating, and drinking, +and laughing? I will tell you. The abounding good cheer of these +English whalers is matter for historical research. Nor have I been +at all sparing of historical whale research, when it has seemed +needed. + +The English were preceded in the whale fishery by the Hollanders, +Zealanders, and Danes; from whom they derived many terms still extant +in the fishery; and what is yet more, their fat old fashions, +touching plenty to eat and drink. For, as a general thing, the +English merchant-ship scrimps her crew; but not so the English +whaler. Hence, in the English, this thing of whaling good cheer is +not normal and natural, but incidental and particular; and, +therefore, must have some special origin, which is here pointed out, +and will be still further elucidated. + +During my researches in the Leviathanic histories, I stumbled upon an +ancient Dutch volume, which, by the musty whaling smell of it, I knew +must be about whalers. The title was, "Dan Coopman," wherefore I +concluded that this must be the invaluable memoirs of some Amsterdam +cooper in the fishery, as every whale ship must carry its cooper. I +was reinforced in this opinion by seeing that it was the production +of one "Fitz Swackhammer." But my friend Dr. Snodhead, a very +learned man, professor of Low Dutch and High German in the college of +Santa Claus and St. Pott's, to whom I handed the work for +translation, giving him a box of sperm candles for his trouble--this +same Dr. Snodhead, so soon as he spied the book, assured me that "Dan +Coopman" did not mean "The Cooper," but "The Merchant." In short, +this ancient and learned Low Dutch book treated of the commerce of +Holland; and, among other subjects, contained a very interesting +account of its whale fishery. And in this chapter it was, headed, +"Smeer," or "Fat," that I found a long detailed list of the outfits +for the larders and cellars of 180 sail of Dutch whalemen; from which +list, as translated by Dr. Snodhead, I transcribe the following: + +400,000 lbs. of beef. +60,000 lbs. Friesland pork. +150,000 lbs. of stock fish. +550,000 lbs. of biscuit. +72,000 lbs. of soft bread. +2,800 firkins of butter. +20,000 lbs. Texel & Leyden cheese. +144,000 lbs. cheese (probably an inferior article). +550 ankers of Geneva. +10,800 barrels of beer. + +Most statistical tables are parchingly dry in the reading; not so in +the present case, however, where the reader is flooded with whole +pipes, barrels, quarts, and gills of good gin and good cheer. + +At the time, I devoted three days to the studious digesting of all +this beer, beef, and bread, during which many profound thoughts were +incidentally suggested to me, capable of a transcendental and +Platonic application; and, furthermore, I compiled supplementary +tables of my own, touching the probable quantity of stock-fish, etc., +consumed by every Low Dutch harpooneer in that ancient Greenland and +Spitzbergen whale fishery. In the first place, the amount of butter, +and Texel and Leyden cheese consumed, seems amazing. I impute it, +though, to their naturally unctuous natures, being rendered still +more unctuous by the nature of their vocation, and especially by +their pursuing their game in those frigid Polar Seas, on the very +coasts of that Esquimaux country where the convivial natives pledge +each other in bumpers of train oil. + +The quantity of beer, too, is very large, 10,800 barrels. Now, +as those polar fisheries could only be prosecuted in the short summer +of that climate, so that the whole cruise of one of these Dutch +whalemen, including the short voyage to and from the Spitzbergen sea, +did not much exceed three months, say, and reckoning 30 men to each +of their fleet of 180 sail, we have 5,400 Low Dutch seamen in all; +therefore, I say, we have precisely two barrels of beer per man, for +a twelve weeks' allowance, exclusive of his fair proportion of that +550 ankers of gin. Now, whether these gin and beer harpooneers, so +fuddled as one might fancy them to have been, were the right sort of +men to stand up in a boat's head, and take good aim at flying whales; +this would seem somewhat improbable. Yet they did aim at them, and +hit them too. But this was very far North, be it remembered, where +beer agrees well with the constitution; upon the Equator, in our +southern fishery, beer would be apt to make the harpooneer sleepy at +the mast-head and boozy in his boat; and grievous loss might ensue to +Nantucket and New Bedford. + +But no more; enough has been said to show that the old Dutch whalers +of two or three centuries ago were high livers; and that the English +whalers have not neglected so excellent an example. For, say they, +when cruising in an empty ship, if you can get nothing better out of +the world, get a good dinner out of it, at least. And this empties +the decanter. + + + +CHAPTER 102 + +A Bower in the Arsacides. + + +Hitherto, in descriptively treating of the Sperm Whale, I have +chiefly dwelt upon the marvels of his outer aspect; or separately and +in detail upon some few interior structural features. But to a large +and thorough sweeping comprehension of him, it behooves me now to +unbutton him still further, and untagging the points of his hose, +unbuckling his garters, and casting loose the hooks and the eyes of +the joints of his innermost bones, set him before you in his +ultimatum; that is to say, in his unconditional skeleton. + +But how now, Ishmael? How is it, that you, a mere oarsman in the +fishery, pretend to know aught about the subterranean parts of the +whale? Did erudite Stubb, mounted upon your capstan, deliver +lectures on the anatomy of the Cetacea; and by help of the windlass, +hold up a specimen rib for exhibition? Explain thyself, Ishmael. +Can you land a full-grown whale on your deck for examination, as a +cook dishes a roast-pig? Surely not. A veritable witness have you +hitherto been, Ishmael; but have a care how you seize the privilege +of Jonah alone; the privilege of discoursing upon the joists and +beams; the rafters, ridge-pole, sleepers, and under-pinnings, making +up the frame-work of leviathan; and belike of the tallow-vats, +dairy-rooms, butteries, and cheeseries in his bowels. + +I confess, that since Jonah, few whalemen have penetrated very far +beneath the skin of the adult whale; nevertheless, I have been +blessed with an opportunity to dissect him in miniature. In a ship I +belonged to, a small cub Sperm Whale was once bodily hoisted to the +deck for his poke or bag, to make sheaths for the barbs of the +harpoons, and for the heads of the lances. Think you I let that +chance go, without using my boat-hatchet and jack-knife, and breaking +the seal and reading all the contents of that young cub? + +And as for my exact knowledge of the bones of the leviathan in their +gigantic, full grown development, for that rare knowledge I am +indebted to my late royal friend Tranquo, king of Tranque, one of +the Arsacides. For being at Tranque, years ago, when attached to the +trading-ship Dey of Algiers, I was invited to spend part of the +Arsacidean holidays with the lord of Tranque, at his retired palm +villa at Pupella; a sea-side glen not very far distant from what our +sailors called Bamboo-Town, his capital. + +Among many other fine qualities, my royal friend Tranquo, being +gifted with a devout love for all matters of barbaric vertu, had +brought together in Pupella whatever rare things the more ingenious +of his people could invent; chiefly carved woods of wonderful +devices, chiselled shells, inlaid spears, costly paddles, aromatic +canoes; and all these distributed among whatever natural wonders, the +wonder-freighted, tribute-rendering waves had cast upon his shores. + +Chief among these latter was a great Sperm Whale, which, after an +unusually long raging gale, had been found dead and stranded, with +his head against a cocoa-nut tree, whose plumage-like, tufted +droopings seemed his verdant jet. When the vast body had at last +been stripped of its fathom-deep enfoldings, and the bones become +dust dry in the sun, then the skeleton was carefully transported up +the Pupella glen, where a grand temple of lordly palms now sheltered +it. + +The ribs were hung with trophies; the vertebrae were carved with +Arsacidean annals, in strange hieroglyphics; in the skull, the +priests kept up an unextinguished aromatic flame, so that the mystic +head again sent forth its vapoury spout; while, suspended from a +bough, the terrific lower jaw vibrated over all the devotees, like +the hair-hung sword that so affrighted Damocles. + +It was a wondrous sight. The wood was green as mosses of the Icy +Glen; the trees stood high and haughty, feeling their living sap; the +industrious earth beneath was as a weaver's loom, with a gorgeous +carpet on it, whereof the ground-vine tendrils formed the warp and +woof, and the living flowers the figures. All the trees, with all +their laden branches; all the shrubs, and ferns, and grasses; the +message-carrying air; all these unceasingly were active. Through the +lacings of the leaves, the great sun seemed a flying shuttle weaving +the unwearied verdure. Oh, busy weaver! unseen weaver!--pause!--one +word!--whither flows the fabric? what palace may it deck? wherefore +all these ceaseless toilings? Speak, weaver!--stay thy hand!--but +one single word with thee! Nay--the shuttle flies--the figures float +from forth the loom; the freshet-rushing carpet for ever slides +away. The weaver-god, he weaves; and by that weaving is he deafened, +that he hears no mortal voice; and by that humming, we, too, who look +on the loom are deafened; and only when we escape it shall we hear +the thousand voices that speak through it. For even so it is in all +material factories. The spoken words that are inaudible among the +flying spindles; those same words are plainly heard without the +walls, bursting from the opened casements. Thereby have villainies +been detected. Ah, mortal! then, be heedful; for so, in all this din +of the great world's loom, thy subtlest thinkings may be overheard +afar. + +Now, amid the green, life-restless loom of that Arsacidean wood, the +great, white, worshipped skeleton lay lounging--a gigantic idler! +Yet, as the ever-woven verdant warp and woof intermixed and hummed +around him, the mighty idler seemed the cunning weaver; himself all +woven over with the vines; every month assuming greener, fresher +verdure; but himself a skeleton. Life folded Death; Death trellised +Life; the grim god wived with youthful Life, and begat him +curly-headed glories. + +Now, when with royal Tranquo I visited this wondrous whale, and saw +the skull an altar, and the artificial smoke ascending from where the +real jet had issued, I marvelled that the king should regard a chapel +as an object of vertu. He laughed. But more I marvelled that the +priests should swear that smoky jet of his was genuine. To and fro I +paced before this skeleton--brushed the vines aside--broke through +the ribs--and with a ball of Arsacidean twine, wandered, eddied long +amid its many winding, shaded colonnades and arbours. But soon my +line was out; and following it back, I emerged from the opening where I +entered. I saw no living thing within; naught was there but bones. + +Cutting me a green measuring-rod, I once more dived within the +skeleton. From their arrow-slit in the skull, the priests perceived +me taking the altitude of the final rib, "How now!" they shouted; +"Dar'st thou measure this our god! That's for us." "Aye, +priests--well, how long do ye make him, then?" But hereupon a fierce +contest rose among them, concerning feet and inches; they cracked +each other's sconces with their yard-sticks--the great skull +echoed--and seizing that lucky chance, I quickly concluded my own +admeasurements. + +These admeasurements I now propose to set before you. But first, be +it recorded, that, in this matter, I am not free to utter any fancied +measurement I please. Because there are skeleton authorities you +can refer to, to test my accuracy. There is a Leviathanic Museum, +they tell me, in Hull, England, one of the whaling ports of that +country, where they have some fine specimens of fin-backs and other +whales. Likewise, I have heard that in the museum of Manchester, in +New Hampshire, they have what the proprietors call "the only perfect +specimen of a Greenland or River Whale in the United States." +Moreover, at a place in Yorkshire, England, Burton Constable by name, +a certain Sir Clifford Constable has in his possession the skeleton +of a Sperm Whale, but of moderate size, by no means of the full-grown +magnitude of my friend King Tranquo's. + +In both cases, the stranded whales to which these two skeletons +belonged, were originally claimed by their proprietors upon similar +grounds. King Tranquo seizing his because he wanted it; and Sir +Clifford, because he was lord of the seignories of those parts. Sir +Clifford's whale has been articulated throughout; so that, like a +great chest of drawers, you can open and shut him, in all his bony +cavities--spread out his ribs like a gigantic fan--and swing all day +upon his lower jaw. Locks are to be put upon some of his trap-doors +and shutters; and a footman will show round future visitors with a +bunch of keys at his side. Sir Clifford thinks of charging twopence +for a peep at the whispering gallery in the spinal column; threepence +to hear the echo in the hollow of his cerebellum; and sixpence for +the unrivalled view from his forehead. + +The skeleton dimensions I shall now proceed to set down are copied +verbatim from my right arm, where I had them tattooed; as in my wild +wanderings at that period, there was no other secure way of +preserving such valuable statistics. But as I was crowded for space, +and wished the other parts of my body to remain a blank page for a +poem I was then composing--at least, what untattooed parts might +remain--I did not trouble myself with the odd inches; nor, indeed, +should inches at all enter into a congenial admeasurement of the +whale. + + + +CHAPTER 103 + +Measurement of The Whale's Skeleton. + + +In the first place, I wish to lay before you a particular, plain +statement, touching the living bulk of this leviathan, whose skeleton +we are briefly to exhibit. Such a statement may prove useful here. + +According to a careful calculation I have made, and which I partly +base upon Captain Scoresby's estimate, of seventy tons for the +largest sized Greenland whale of sixty feet in length; according to +my careful calculation, I say, a Sperm Whale of the largest +magnitude, between eighty-five and ninety feet in length, and +something less than forty feet in its fullest circumference, such a +whale will weigh at least ninety tons; so that, reckoning thirteen +men to a ton, he would considerably outweigh the combined population +of a whole village of one thousand one hundred inhabitants. + +Think you not then that brains, like yoked cattle, should be put to +this leviathan, to make him at all budge to any landsman's +imagination? + +Having already in various ways put before you his skull, spout-hole, +jaw, teeth, tail, forehead, fins, and divers other parts, I shall now +simply point out what is most interesting in the general bulk of his +unobstructed bones. But as the colossal skull embraces so very large +a proportion of the entire extent of the skeleton; as it is by far +the most complicated part; and as nothing is to be repeated +concerning it in this chapter, you must not fail to carry it in your +mind, or under your arm, as we proceed, otherwise you will not gain a +complete notion of the general structure we are about to view. + +In length, the Sperm Whale's skeleton at Tranque measured seventy-two +Feet; so that when fully invested and extended in life, he must have +been ninety feet long; for in the whale, the skeleton loses about one +fifth in length compared with the living body. Of this seventy-two +feet, his skull and jaw comprised some twenty feet, leaving some +fifty feet of plain back-bone. Attached to this back-bone, for +something less than a third of its length, was the mighty circular +basket of ribs which once enclosed his vitals. + +To me this vast ivory-ribbed chest, with the long, unrelieved spine, +extending far away from it in a straight line, not a little resembled +the hull of a great ship new-laid upon the stocks, when only some +twenty of her naked bow-ribs are inserted, and the keel is otherwise, +for the time, but a long, disconnected timber. + +The ribs were ten on a side. The first, to begin from the neck, was +nearly six feet long; the second, third, and fourth were each +successively longer, till you came to the climax of the fifth, or one +of the middle ribs, which measured eight feet and some inches. From +that part, the remaining ribs diminished, till the tenth and last +only spanned five feet and some inches. In general thickness, they +all bore a seemly correspondence to their length. The middle ribs +were the most arched. In some of the Arsacides they are used for +beams whereon to lay footpath bridges over small streams. + +In considering these ribs, I could not but be struck anew with the +circumstance, so variously repeated in this book, that the skeleton +of the whale is by no means the mould of his invested form. The +largest of the Tranque ribs, one of the middle ones, occupied that +part of the fish which, in life, is greatest in depth. Now, the +greatest depth of the invested body of this particular whale must +have been at least sixteen feet; whereas, the corresponding rib +measured but little more than eight feet. So that this rib only +conveyed half of the true notion of the living magnitude of that +part. Besides, for some way, where I now saw but a naked spine, all +that had been once wrapped round with tons of added bulk in flesh, +muscle, blood, and bowels. Still more, for the ample fins, I here +saw but a few disordered joints; and in place of the weighty and +majestic, but boneless flukes, an utter blank! + +How vain and foolish, then, thought I, for timid untravelled man to +try to comprehend aright this wondrous whale, by merely poring over +his dead attenuated skeleton, stretched in this peaceful wood. No. +Only in the heart of quickest perils; only when within the eddyings +of his angry flukes; only on the profound unbounded sea, can the +fully invested whale be truly and livingly found out. + +But the spine. For that, the best way we can consider it is, with a +crane, to pile its bones high up on end. No speedy enterprise. But +now it's done, it looks much like Pompey's Pillar. + +There are forty and odd vertebrae in all, which in the skeleton are +not locked together. They mostly lie like the great knobbed blocks +on a Gothic spire, forming solid courses of heavy masonry. The +largest, a middle one, is in width something less than three feet, +and in depth more than four. The smallest, where the spine tapers +away into the tail, is only two inches in width, and looks something +like a white billiard-ball. I was told that there were still smaller +ones, but they had been lost by some little cannibal urchins, the +priest's children, who had stolen them to play marbles with. Thus we +see how that the spine of even the hugest of living things tapers off +at last into simple child's play. + + + +CHAPTER 104 + +The Fossil Whale. + + +From his mighty bulk the whale affords a most congenial theme whereon +to enlarge, amplify, and generally expatiate. Would you, you could +not compress him. By good rights he should only be treated of in +imperial folio. Not to tell over again his furlongs from spiracle to +tail, and the yards he measures about the waist; only think of the +gigantic involutions of his intestines, where they lie in him like +great cables and hawsers coiled away in the subterranean orlop-deck +of a line-of-battle-ship. + +Since I have undertaken to manhandle this Leviathan, it behooves me +to approve myself omnisciently exhaustive in the enterprise; not +overlooking the minutest seminal germs of his blood, and spinning him +out to the uttermost coil of his bowels. Having already described +him in most of his present habitatory and anatomical peculiarities, +it now remains to magnify him in an archaeological, fossiliferous, +and antediluvian point of view. Applied to any other creature than +the Leviathan--to an ant or a flea--such portly terms might justly be +deemed unwarrantably grandiloquent. But when Leviathan is the text, +the case is altered. Fain am I to stagger to this emprise under +the weightiest words of the dictionary. And here be it said, that +whenever it has been convenient to consult one in the course of these +dissertations, I have invariably used a huge quarto edition of +Johnson, expressly purchased for that purpose; because that famous +lexicographer's uncommon personal bulk more fitted him to compile a +lexicon to be used by a whale author like me. + +One often hears of writers that rise and swell with their subject, +though it may seem but an ordinary one. How, then, with me, writing +of this Leviathan? Unconsciously my chirography expands into placard +capitals. Give me a condor's quill! Give me Vesuvius' crater for an +inkstand! Friends, hold my arms! For in the mere act of penning my +thoughts of this Leviathan, they weary me, and make me faint with +their outreaching comprehensiveness of sweep, as if to include the +whole circle of the sciences, and all the generations of whales, and +men, and mastodons, past, present, and to come, with all the +revolving panoramas of empire on earth, and throughout the whole +universe, not excluding its suburbs. Such, and so magnifying, is the +virtue of a large and liberal theme! We expand to its bulk. To +produce a mighty book, you must choose a mighty theme. No great and +enduring volume can ever be written on the flea, though many there be +who have tried it. + +Ere entering upon the subject of Fossil Whales, I present my +credentials as a geologist, by stating that in my miscellaneous time +I have been a stone-mason, and also a great digger of ditches, +canals and wells, wine-vaults, cellars, and cisterns of all sorts. +Likewise, by way of preliminary, I desire to remind the reader, that +while in the earlier geological strata there are found the fossils of +monsters now almost completely extinct; the subsequent relics +discovered in what are called the Tertiary formations seem the +connecting, or at any rate intercepted links, between the +antichronical creatures, and those whose remote posterity are said to +have entered the Ark; all the Fossil Whales hitherto discovered +belong to the Tertiary period, which is the last preceding the +superficial formations. And though none of them precisely answer to +any known species of the present time, they are yet sufficiently akin +to them in general respects, to justify their taking rank as +Cetacean fossils. + +Detached broken fossils of pre-adamite whales, fragments of their +bones and skeletons, have within thirty years past, at various +intervals, been found at the base of the Alps, in Lombardy, in +France, in England, in Scotland, and in the States of Louisiana, +Mississippi, and Alabama. Among the more curious of such remains is +part of a skull, which in the year 1779 was disinterred in the Rue +Dauphine in Paris, a short street opening almost directly upon the +palace of the Tuileries; and bones disinterred in excavating the +great docks of Antwerp, in Napoleon's time. Cuvier pronounced these +fragments to have belonged to some utterly unknown Leviathanic +species. + +But by far the most wonderful of all Cetacean relics was the almost +complete vast skeleton of an extinct monster, found in the year 1842, +on the plantation of Judge Creagh, in Alabama. The awe-stricken +credulous slaves in the vicinity took it for the bones of one of the +fallen angels. The Alabama doctors declared it a huge reptile, and +bestowed upon it the name of Basilosaurus. But some specimen bones +of it being taken across the sea to Owen, the English Anatomist, it +turned out that this alleged reptile was a whale, though of a +departed species. A significant illustration of the fact, again and +again repeated in this book, that the skeleton of the whale furnishes +but little clue to the shape of his fully invested body. So Owen +rechristened the monster Zeuglodon; and in his paper read before the +London Geological Society, pronounced it, in substance, one of the +most extraordinary creatures which the mutations of the globe have +blotted out of existence. + +When I stand among these mighty Leviathan skeletons, skulls, tusks, +jaws, ribs, and vertebrae, all characterized by partial resemblances +to the existing breeds of sea-monsters; but at the same time bearing +on the other hand similar affinities to the annihilated antichronical +Leviathans, their incalculable seniors; I am, by a flood, borne back +to that wondrous period, ere time itself can be said to have begun; +for time began with man. Here Saturn's grey chaos rolls over me, and +I obtain dim, shuddering glimpses into those Polar eternities; when +wedged bastions of ice pressed hard upon what are now the Tropics; +and in all the 25,000 miles of this world's circumference, not an +inhabitable hand's breadth of land was visible. Then the whole world +was the whale's; and, king of creation, he left his wake along the +present lines of the Andes and the Himmalehs. Who can show a +pedigree like Leviathan? Ahab's harpoon had shed older blood than +the Pharaoh's. Methuselah seems a school-boy. I look round to shake +hands with Shem. I am horror-struck at this antemosaic, unsourced +existence of the unspeakable terrors of the whale, which, having been +before all time, must needs exist after all humane ages are over. + +But not alone has this Leviathan left his pre-adamite traces in the +stereotype plates of nature, and in limestone and marl bequeathed his +ancient bust; but upon Egyptian tablets, whose antiquity seems to +claim for them an almost fossiliferous character, we find the +unmistakable print of his fin. In an apartment of the great temple +of Denderah, some fifty years ago, there was discovered upon the +granite ceiling a sculptured and painted planisphere, abounding in +centaurs, griffins, and dolphins, similar to the grotesque figures +on the celestial globe of the moderns. Gliding among them, old +Leviathan swam as of yore; was there swimming in that planisphere, +centuries before Solomon was cradled. + +Nor must there be omitted another strange attestation of the +antiquity of the whale, in his own osseous post-diluvian reality, as +set down by the venerable John Leo, the old Barbary traveller. + +"Not far from the Sea-side, they have a Temple, the Rafters and Beams +of which are made of Whale-Bones; for Whales of a monstrous size are +oftentimes cast up dead upon that shore. The Common People imagine, +that by a secret Power bestowed by God upon the temple, no Whale can +pass it without immediate death. But the truth of the Matter is, +that on either side of the Temple, there are Rocks that shoot two +Miles into the Sea, and wound the Whales when they light upon 'em. +They keep a Whale's Rib of an incredible length for a Miracle, which +lying upon the Ground with its convex part uppermost, makes an Arch, +the Head of which cannot be reached by a Man upon a Camel's Back. +This Rib (says John Leo) is said to have layn there a hundred Years +before I saw it. Their Historians affirm, that a Prophet who +prophesy'd of Mahomet, came from this Temple, and some do not stand +to assert, that the Prophet Jonas was cast forth by the Whale at the +Base of the Temple." + +In this Afric Temple of the Whale I leave you, reader, and if you be +a Nantucketer, and a whaleman, you will silently worship there. + + + +CHAPTER 105 + +Does the Whale's Magnitude Diminish?--Will He Perish? + + +Inasmuch, then, as this Leviathan comes floundering down upon us from +the head-waters of the Eternities, it may be fitly inquired, whether, +in the long course of his generations, he has not degenerated from +the original bulk of his sires. + +But upon investigation we find, that not only are the whales of the +present day superior in magnitude to those whose fossil remains are +found in the Tertiary system (embracing a distinct geological period +prior to man), but of the whales found in that Tertiary system, those +belonging to its latter formations exceed in size those of its +earlier ones. + +Of all the pre-adamite whales yet exhumed, by far the largest is the +Alabama one mentioned in the last chapter, and that was less than +seventy feet in length in the skeleton. Whereas, we have already +seen, that the tape-measure gives seventy-two feet for the skeleton +of a large sized modern whale. And I have heard, on whalemen's +authority, that Sperm Whales have been captured near a hundred feet +long at the time of capture. + +But may it not be, that while the whales of the present hour are an +advance in magnitude upon those of all previous geological periods; +may it not be, that since Adam's time they have degenerated? + +Assuredly, we must conclude so, if we are to credit the accounts of +such gentlemen as Pliny, and the ancient naturalists generally. For +Pliny tells us of Whales that embraced acres of living bulk, and +Aldrovandus of others which measured eight hundred feet in +length--Rope Walks and Thames Tunnels of Whales! And even in the +days of Banks and Solander, Cooke's naturalists, we find a Danish +member of the Academy of Sciences setting down certain Iceland Whales +(reydan-siskur, or Wrinkled Bellies) at one hundred and twenty yards; +that is, three hundred and sixty feet. And Lacepede, the French +naturalist, in his elaborate history of whales, in the very beginning +of his work (page 3), sets down the Right Whale at one hundred +metres, three hundred and twenty-eight feet. And this work was +published so late as A.D. 1825. + +But will any whaleman believe these stories? No. The whale of +to-day is as big as his ancestors in Pliny's time. And if ever I go +where Pliny is, I, a whaleman (more than he was), will make bold to +tell him so. Because I cannot understand how it is, that while the +Egyptian mummies that were buried thousands of years before even +Pliny was born, do not measure so much in their coffins as a modern +Kentuckian in his socks; and while the cattle and other animals +sculptured on the oldest Egyptian and Nineveh tablets, by the +relative proportions in which they are drawn, just as plainly prove +that the high-bred, stall-fed, prize cattle of Smithfield, not only +equal, but far exceed in magnitude the fattest of Pharaoh's fat kine; +in the face of all this, I will not admit that of all animals the +whale alone should have degenerated. + +But still another inquiry remains; one often agitated by the more +recondite Nantucketers. Whether owing to the almost omniscient +look-outs at the mast-heads of the whaleships, now penetrating even +through Behring's straits, and into the remotest secret drawers and +lockers of the world; and the thousand harpoons and lances darted +along all continental coasts; the moot point is, whether Leviathan +can long endure so wide a chase, and so remorseless a havoc; whether +he must not at last be exterminated from the waters, and the last +whale, like the last man, smoke his last pipe, and then himself +evaporate in the final puff. + +Comparing the humped herds of whales with the humped herds of +buffalo, which, not forty years ago, overspread by tens of thousands +the prairies of Illinois and Missouri, and shook their iron manes and +scowled with their thunder-clotted brows upon the sites of populous +river-capitals, where now the polite broker sells you land at a +dollar an inch; in such a comparison an irresistible argument would +seem furnished, to show that the hunted whale cannot now escape +speedy extinction. + +But you must look at this matter in every light. Though so short a +period ago--not a good lifetime--the census of the buffalo in +Illinois exceeded the census of men now in London, and though at the +present day not one horn or hoof of them remains in all that region; +and though the cause of this wondrous extermination was the spear of +man; yet the far different nature of the whale-hunt peremptorily +forbids so inglorious an end to the Leviathan. Forty men in one ship +hunting the Sperm Whales for forty-eight months think they have done +extremely well, and thank God, if at last they carry home the oil of +forty fish. Whereas, in the days of the old Canadian and Indian +hunters and trappers of the West, when the far west (in whose sunset +suns still rise) was a wilderness and a virgin, the same number of +moccasined men, for the same number of months, mounted on horse +instead of sailing in ships, would have slain not forty, but forty +thousand and more buffaloes; a fact that, if need were, could be +statistically stated. + +Nor, considered aright, does it seem any argument in favour of the +gradual extinction of the Sperm Whale, for example, that in former +years (the latter part of the last century, say) these Leviathans, in +small pods, were encountered much oftener than at present, and, in +consequence, the voyages were not so prolonged, and were also much +more remunerative. Because, as has been elsewhere noticed, those +whales, influenced by some views to safety, now swim the seas in +immense caravans, so that to a large degree the scattered solitaries, +yokes, and pods, and schools of other days are now aggregated into +vast but widely separated, unfrequent armies. That is all. And +equally fallacious seems the conceit, that because the so-called +whale-bone whales no longer haunt many grounds in former years +abounding with them, hence that species also is declining. For they +are only being driven from promontory to cape; and if one coast is no +longer enlivened with their jets, then, be sure, some other and +remoter strand has been very recently startled by the unfamiliar +spectacle. + +Furthermore: concerning these last mentioned Leviathans, they have +two firm fortresses, which, in all human probability, will for ever +remain impregnable. And as upon the invasion of their valleys, the +frosty Swiss have retreated to their mountains; so, hunted from the +savannas and glades of the middle seas, the whale-bone whales can at +last resort to their Polar citadels, and diving under the ultimate +glassy barriers and walls there, come up among icy fields and floes; +and in a charmed circle of everlasting December, bid defiance to all +pursuit from man. + +But as perhaps fifty of these whale-bone whales are harpooned for one +cachalot, some philosophers of the forecastle have concluded that +this positive havoc has already very seriously diminished their +battalions. But though for some time past a number of these whales, +not less than 13,000, have been annually slain on the nor'-west +coast by the Americans alone; yet there are considerations which +render even this circumstance of little or no account as an opposing +argument in this matter. + +Natural as it is to be somewhat incredulous concerning the +populousness of the more enormous creatures of the globe, yet what +shall we say to Harto, the historian of Goa, when he tells us that at +one hunting the King of Siam took 4,000 elephants; that in those +regions elephants are numerous as droves of cattle in the temperate +climes. And there seems no reason to doubt that if these elephants, +which have now been hunted for thousands of years, by Semiramis, by +Porus, by Hannibal, and by all the successive monarchs of the +East--if they still survive there in great numbers, much more may the +great whale outlast all hunting, since he has a pasture to expatiate +in, which is precisely twice as large as all Asia, both Americas, +Europe and Africa, New Holland, and all the Isles of the sea +combined. + +Moreover: we are to consider, that from the presumed great longevity +of whales, their probably attaining the age of a century and more, +therefore at any one period of time, several distinct adult +generations must be contemporary. And what that is, we may soon +gain some idea of, by imagining all the grave-yards, cemeteries, and +family vaults of creation yielding up the live bodies of all the men, +women, and children who were alive seventy-five years ago; and adding +this countless host to the present human population of the globe. + +Wherefore, for all these things, we account the whale immortal in his +species, however perishable in his individuality. He swam the seas +before the continents broke water; he once swam over the site of the +Tuileries, and Windsor Castle, and the Kremlin. In Noah's flood he +despised Noah's Ark; and if ever the world is to be again flooded, +like the Netherlands, to kill off its rats, then the eternal whale +will still survive, and rearing upon the topmost crest of the +equatorial flood, spout his frothed defiance to the skies. + + + +CHAPTER 106 + +Ahab's Leg. + + +The precipitating manner in which Captain Ahab had quitted the Samuel +Enderby of London, had not been unattended with some small violence +to his own person. He had lighted with such energy upon a thwart of +his boat that his ivory leg had received a half-splintering shock. +And when after gaining his own deck, and his own pivot-hole there, he +so vehemently wheeled round with an urgent command to the steersman +(it was, as ever, something about his not steering inflexibly +enough); then, the already shaken ivory received such an additional +twist and wrench, that though it still remained entire, and to all +appearances lusty, yet Ahab did not deem it entirely trustworthy. + +And, indeed, it seemed small matter for wonder, that for all his +pervading, mad recklessness, Ahab did at times give careful heed to +the condition of that dead bone upon which he partly stood. For it +had not been very long prior to the Pequod's sailing from Nantucket, +that he had been found one night lying prone upon the ground, and +insensible; by some unknown, and seemingly inexplicable, unimaginable +casualty, his ivory limb having been so violently displaced, that it +had stake-wise smitten, and all but pierced his groin; nor was it +without extreme difficulty that the agonizing wound was entirely +cured. + +Nor, at the time, had it failed to enter his monomaniac mind, that +all the anguish of that then present suffering was but the direct +issue of a former woe; and he too plainly seemed to see, that as the +most poisonous reptile of the marsh perpetuates his kind as +inevitably as the sweetest songster of the grove; so, equally with +every felicity, all miserable events do naturally beget their like. +Yea, more than equally, thought Ahab; since both the ancestry and +posterity of Grief go further than the ancestry and posterity of Joy. +For, not to hint of this: that it is an inference from certain +canonic teachings, that while some natural enjoyments here shall have +no children born to them for the other world, but, on the contrary, +shall be followed by the joy-childlessness of all hell's despair; +whereas, some guilty mortal miseries shall still fertilely beget to +themselves an eternally progressive progeny of griefs beyond the +grave; not at all to hint of this, there still seems an inequality in +the deeper analysis of the thing. For, thought Ahab, while even the +highest earthly felicities ever have a certain unsignifying pettiness +lurking in them, but, at bottom, all heartwoes, a mystic +significance, and, in some men, an archangelic grandeur; so do their +diligent tracings-out not belie the obvious deduction. To trail the +genealogies of these high mortal miseries, carries us at last among +the sourceless primogenitures of the gods; so that, in the face of +all the glad, hay-making suns, and soft cymballing, round +harvest-moons, we must needs give in to this: that the gods +themselves are not for ever glad. The ineffaceable, sad birth-mark +in the brow of man, is but the stamp of sorrow in the signers. + +Unwittingly here a secret has been divulged, which perhaps might more +properly, in set way, have been disclosed before. With many other +particulars concerning Ahab, always had it remained a mystery to +some, why it was, that for a certain period, both before and after +the sailing of the Pequod, he had hidden himself away with such +Grand-Lama-like exclusiveness; and, for that one interval, sought +speechless refuge, as it were, among the marble senate of the dead. +Captain Peleg's bruited reason for this thing appeared by no means +adequate; though, indeed, as touching all Ahab's deeper part, every +revelation partook more of significant darkness than of explanatory +light. But, in the end, it all came out; this one matter did, at +least. That direful mishap was at the bottom of his temporary +recluseness. And not only this, but to that ever-contracting, +dropping circle ashore, who, for any reason, possessed the privilege +of a less banned approach to him; to that timid circle the above +hinted casualty--remaining, as it did, moodily unaccounted for by +Ahab--invested itself with terrors, not entirely underived from the +land of spirits and of wails. So that, through their zeal for him, +they had all conspired, so far as in them lay, to muffle up the +knowledge of this thing from others; and hence it was, that not till +a considerable interval had elapsed, did it transpire upon the +Pequod's decks. + +But be all this as it may; let the unseen, ambiguous synod in the +air, or the vindictive princes and potentates of fire, have to do or +not with earthly Ahab, yet, in this present matter of his leg, he +took plain practical procedures;--he called the carpenter. + +And when that functionary appeared before him, he bade him without +delay set about making a new leg, and directed the mates to see him +supplied with all the studs and joists of jaw-ivory (Sperm Whale) +which had thus far been accumulated on the voyage, in order that a +careful selection of the stoutest, clearest-grained stuff might be +secured. This done, the carpenter received orders to have the leg +completed that night; and to provide all the fittings for it, +independent of those pertaining to the distrusted one in use. +Moreover, the ship's forge was ordered to be hoisted out of its +temporary idleness in the hold; and, to accelerate the affair, the +blacksmith was commanded to proceed at once to the forging of +whatever iron contrivances might be needed. + + + +CHAPTER 107 + +The Carpenter. + + +Seat thyself sultanically among the moons of Saturn, and take high +abstracted man alone; and he seems a wonder, a grandeur, and a woe. +But from the same point, take mankind in mass, and for the most part, +they seem a mob of unnecessary duplicates, both contemporary and +hereditary. But most humble though he was, and far from furnishing +an example of the high, humane abstraction; the Pequod's carpenter +was no duplicate; hence, he now comes in person on this stage. + +Like all sea-going ship carpenters, and more especially those +belonging to whaling vessels, he was, to a certain off-handed, +practical extent, alike experienced in numerous trades and callings +collateral to his own; the carpenter's pursuit being the ancient and +outbranching trunk of all those numerous handicrafts which more or +less have to do with wood as an auxiliary material. But, besides the +application to him of the generic remark above, this carpenter of the +Pequod was singularly efficient in those thousand nameless mechanical +emergencies continually recurring in a large ship, upon a three or +four years' voyage, in uncivilized and far-distant seas. For not to +speak of his readiness in ordinary duties:--repairing stove boats, +sprung spars, reforming the shape of clumsy-bladed oars, inserting +bull's eyes in the deck, or new tree-nails in the side planks, and +other miscellaneous matters more directly pertaining to his special +business; he was moreover unhesitatingly expert in all manner of +conflicting aptitudes, both useful and capricious. + +The one grand stage where he enacted all his various parts so +manifold, was his vice-bench; a long rude ponderous table furnished +with several vices, of different sizes, and both of iron and of wood. +At all times except when whales were alongside, this bench was +securely lashed athwartships against the rear of the Try-works. + +A belaying pin is found too large to be easily inserted into its +hole: the carpenter claps it into one of his ever-ready vices, and +straightway files it smaller. A lost land-bird of strange plumage +strays on board, and is made a captive: out of clean shaved rods of +right-whale bone, and cross-beams of sperm whale ivory, the carpenter +makes a pagoda-looking cage for it. An oarsman sprains his wrist: +the carpenter concocts a soothing lotion. Stubb longed for +vermillion stars to be painted upon the blade of his every oar; +screwing each oar in his big vice of wood, the carpenter +symmetrically supplies the constellation. A sailor takes a fancy to +wear shark-bone ear-rings: the carpenter drills his ears. Another +has the toothache: the carpenter out pincers, and clapping one hand +upon his bench bids him be seated there; but the poor fellow +unmanageably winces under the unconcluded operation; whirling round +the handle of his wooden vice, the carpenter signs him to clap his +jaw in that, if he would have him draw the tooth. + +Thus, this carpenter was prepared at all points, and alike +indifferent and without respect in all. Teeth he accounted bits of +ivory; heads he deemed but top-blocks; men themselves he lightly held +for capstans. But while now upon so wide a field thus variously +accomplished and with such liveliness of expertness in him, too; all +this would seem to argue some uncommon vivacity of intelligence. But +not precisely so. For nothing was this man more remarkable, than for +a certain impersonal stolidity as it were; impersonal, I say; for it +so shaded off into the surrounding infinite of things, that it seemed +one with the general stolidity discernible in the whole visible +world; which while pauselessly active in uncounted modes, still +eternally holds its peace, and ignores you, though you dig +foundations for cathedrals. Yet was this half-horrible stolidity in +him, involving, too, as it appeared, an all-ramifying +heartlessness;--yet was it oddly dashed at times, with an old, +crutch-like, antediluvian, wheezing humorousness, not unstreaked now +and then with a certain grizzled wittiness; such as might have served +to pass the time during the midnight watch on the bearded forecastle +of Noah's ark. Was it that this old carpenter had been a life-long +wanderer, whose much rolling, to and fro, not only had gathered no +moss; but what is more, had rubbed off whatever small outward +clingings might have originally pertained to him? He was a stript +abstract; an unfractioned integral; uncompromised as a new-born babe; +living without premeditated reference to this world or the next. You +might almost say, that this strange uncompromisedness in him involved +a sort of unintelligence; for in his numerous trades, he did not seem +to work so much by reason or by instinct, or simply because he had +been tutored to it, or by any intermixture of all these, even or +uneven; but merely by a kind of deaf and dumb, spontaneous literal +process. He was a pure manipulator; his brain, if he had ever had +one, must have early oozed along into the muscles of his fingers. He +was like one of those unreasoning but still highly useful, MULTUM IN +PARVO, Sheffield contrivances, assuming the exterior--though a little +swelled--of a common pocket knife; but containing, not only blades of +various sizes, but also screw-drivers, cork-screws, tweezers, awls, +pens, rulers, nail-filers, countersinkers. So, if his superiors +wanted to use the carpenter for a screw-driver, all they had to do +was to open that part of him, and the screw was fast: or if for +tweezers, take him up by the legs, and there they were. + +Yet, as previously hinted, this omnitooled, open-and-shut carpenter, +was, after all, no mere machine of an automaton. If he did not have +a common soul in him, he had a subtle something that somehow +anomalously did its duty. What that was, whether essence of +quicksilver, or a few drops of hartshorn, there is no telling. But +there it was; and there it had abided for now some sixty years or +more. And this it was, this same unaccountable, cunning +life-principle in him; this it was, that kept him a great part of the +time soliloquizing; but only like an unreasoning wheel, which also +hummingly soliloquizes; or rather, his body was a sentry-box and this +soliloquizer on guard there, and talking all the time to keep himself +awake. + + + +CHAPTER 108 + +Ahab and the Carpenter. + +The Deck--First Night Watch. + + +(CARPENTER STANDING BEFORE HIS VICE-BENCH, AND BY THE LIGHT OF TWO +LANTERNS BUSILY FILING THE IVORY JOIST FOR THE LEG, WHICH JOIST IS +FIRMLY FIXED IN THE VICE. SLABS OF IVORY, LEATHER STRAPS, PADS, +SCREWS, AND VARIOUS TOOLS OF ALL SORTS LYING ABOUT THE BENCH. +FORWARD, THE RED FLAME OF THE FORGE IS SEEN, WHERE THE BLACKSMITH IS +AT WORK.) + + +Drat the file, and drat the bone! That is hard which should be soft, +and that is soft which should be hard. So we go, who file old jaws +and shinbones. Let's try another. Aye, now, this works better +(SNEEZES). Halloa, this bone dust is (SNEEZES)--why it's +(SNEEZES)--yes it's (SNEEZES)--bless my soul, it won't let me speak! +This is what an old fellow gets now for working in dead lumber. Saw +a live tree, and you don't get this dust; amputate a live bone, and +you don't get it (SNEEZES). Come, come, you old Smut, there, bear a +hand, and let's have that ferule and buckle-screw; I'll be ready +for them presently. Lucky now (SNEEZES) there's no knee-joint to +make; that might puzzle a little; but a mere shinbone--why it's +easy as making hop-poles; only I should like to put a good finish on. +Time, time; if I but only had the time, I could turn him out as neat +a leg now as ever (SNEEZES) scraped to a lady in a parlor. Those +buckskin legs and calves of legs I've seen in shop windows wouldn't +compare at all. They soak water, they do; and of course get +rheumatic, and have to be doctored (SNEEZES) with washes and lotions, +just like live legs. There; before I saw it off, now, I must call his +old Mogulship, and see whether the length will be all right; too +short, if anything, I guess. Ha! that's the heel; we are in luck; +here he comes, or it's somebody else, that's certain. + +AHAB (ADVANCING) + +(DURING THE ENSUING SCENE, THE CARPENTER CONTINUES SNEEZING AT TIMES) + + +Well, manmaker! + +Just in time, sir. If the captain pleases, I will now mark the +length. Let me measure, sir. + +Measured for a leg! good. Well, it's not the first time. About it! +There; keep thy finger on it. This is a cogent vice thou hast here, +carpenter; let me feel its grip once. So, so; it does pinch some. + +Oh, sir, it will break bones--beware, beware! + +No fear; I like a good grip; I like to feel something in this +slippery world that can hold, man. What's Prometheus about +there?--the blacksmith, I mean--what's he about? + +He must be forging the buckle-screw, sir, now. + +Right. It's a partnership; he supplies the muscle part. He makes a +fierce red flame there! + +Aye, sir; he must have the white heat for this kind of fine work. + +Um-m. So he must. I do deem it now a most meaning thing, that that +old Greek, Prometheus, who made men, they say, should have been a +blacksmith, and animated them with fire; for what's made in fire must +properly belong to fire; and so hell's probable. How the soot flies! +This must be the remainder the Greek made the Africans of. +Carpenter, when he's through with that buckle, tell him to forge a +pair of steel shoulder-blades; there's a pedlar aboard with a +crushing pack. + +Sir? + +Hold; while Prometheus is about it, I'll order a complete man after a +desirable pattern. Imprimis, fifty feet high in his socks; then, +chest modelled after the Thames Tunnel; then, legs with roots to 'em, +to stay in one place; then, arms three feet through the wrist; no +heart at all, brass forehead, and about a quarter of an acre of fine +brains; and let me see--shall I order eyes to see outwards? No, but +put a sky-light on top of his head to illuminate inwards. There, +take the order, and away. + +Now, what's he speaking about, and who's he speaking to, I should +like to know? Shall I keep standing here? (ASIDE). + +'Tis but indifferent architecture to make a blind dome; here's one. +No, no, no; I must have a lantern. + +Ho, ho! That's it, hey? Here are two, sir; one will serve my turn. + +What art thou thrusting that thief-catcher into my face for, man? +Thrusted light is worse than presented pistols. + +I thought, sir, that you spoke to carpenter. + + +Carpenter? why that's--but no;--a very tidy, and, I may say, an +extremely gentlemanlike sort of business thou art in here, +carpenter;--or would'st thou rather work in clay? + +Sir?--Clay? clay, sir? That's mud; we leave clay to ditchers, sir. + +The fellow's impious! What art thou sneezing about? + +Bone is rather dusty, sir. + +Take the hint, then; and when thou art dead, never bury thyself under +living people's noses. + +Sir?--oh! ah!--I guess so;--yes--dear! + +Look ye, carpenter, I dare say thou callest thyself a right good +workmanlike workman, eh? Well, then, will it speak thoroughly well +for thy work, if, when I come to mount this leg thou makest, I shall +nevertheless feel another leg in the same identical place with it; +that is, carpenter, my old lost leg; the flesh and blood one, I mean. +Canst thou not drive that old Adam away? + +Truly, sir, I begin to understand somewhat now. Yes, I have heard +something curious on that score, sir; how that a dismasted man never +entirely loses the feeling of his old spar, but it will be still +pricking him at times. May I humbly ask if it be really so, sir? + +It is, man. Look, put thy live leg here in the place where mine once +was; so, now, here is only one distinct leg to the eye, yet two to +the soul. Where thou feelest tingling life; there, exactly there, +there to a hair, do I. Is't a riddle? + +I should humbly call it a poser, sir. + +Hist, then. How dost thou know that some entire, living, thinking +thing may not be invisibly and uninterpenetratingly standing +precisely where thou now standest; aye, and standing there in thy +spite? In thy most solitary hours, then, dost thou not fear +eavesdroppers? Hold, don't speak! And if I still feel the smart of +my crushed leg, though it be now so long dissolved; then, why mayst +not thou, carpenter, feel the fiery pains of hell for ever, and +without a body? Hah! + +Good Lord! Truly, sir, if it comes to that, I must calculate over +again; I think I didn't carry a small figure, sir. + +Look ye, pudding-heads should never grant premises.--How long before +the leg is done? + +Perhaps an hour, sir. + +Bungle away at it then, and bring it to me (TURNS TO GO). Oh, Life! +Here I am, proud as Greek god, and yet standing debtor to this +blockhead for a bone to stand on! Cursed be that mortal +inter-indebtedness which will not do away with ledgers. I would be +free as air; and I'm down in the whole world's books. I am so rich, +I could have given bid for bid with the wealthiest Praetorians at the +auction of the Roman empire (which was the world's); and yet I owe +for the flesh in the tongue I brag with. By heavens! I'll get a +crucible, and into it, and dissolve myself down to one small, +compendious vertebra. So. + +CARPENTER (RESUMING HIS WORK). + + +Well, well, well! Stubb knows him best of all, and Stubb always says +he's queer; says nothing but that one sufficient little word queer; +he's queer, says Stubb; he's queer--queer, queer; and keeps dinning +it into Mr. Starbuck all the time--queer--sir--queer, queer, very +queer. And here's his leg! Yes, now that I think of it, here's his +bedfellow! has a stick of whale's jaw-bone for a wife! And this is +his leg; he'll stand on this. What was that now about one leg +standing in three places, and all three places standing in one +hell--how was that? Oh! I don't wonder he looked so scornful at me! +I'm a sort of strange-thoughted sometimes, they say; but that's only +haphazard-like. Then, a short, little old body like me, should never +undertake to wade out into deep waters with tall, heron-built +captains; the water chucks you under the chin pretty quick, and +there's a great cry for life-boats. And here's the heron's leg! long +and slim, sure enough! Now, for most folks one pair of legs lasts a +lifetime, and that must be because they use them mercifully, as a +tender-hearted old lady uses her roly-poly old coach-horses. But +Ahab; oh he's a hard driver. Look, driven one leg to death, and +spavined the other for life, and now wears out bone legs by the cord. +Halloa, there, you Smut! bear a hand there with those screws, and +let's finish it before the resurrection fellow comes a-calling with +his horn for all legs, true or false, as brewery-men go round +collecting old beer barrels, to fill 'em up again. What a leg this +is! It looks like a real live leg, filed down to nothing but the +core; he'll be standing on this to-morrow; he'll be taking altitudes +on it. Halloa! I almost forgot the little oval slate, smoothed +ivory, where he figures up the latitude. So, so; chisel, file, and +sand-paper, now! + + + +CHAPTER 109 + +Ahab and Starbuck in the Cabin. + + +According to usage they were pumping the ship next morning; and lo! +no inconsiderable oil came up with the water; the casks below must +have sprung a bad leak. Much concern was shown; and Starbuck went +down into the cabin to report this unfavourable affair.* + + +*In Sperm-whalemen with any considerable quantity of oil on board, it +is a regular semiweekly duty to conduct a hose into the hold, and +drench the casks with sea-water; which afterwards, at varying +intervals, is removed by the ship's pumps. Hereby the casks are +sought to be kept damply tight; while by the changed character of the +withdrawn water, the mariners readily detect any serious leakage in +the precious cargo. + + +Now, from the South and West the Pequod was drawing nigh to Formosa +and the Bashee Isles, between which lies one of the tropical outlets +from the China waters into the Pacific. And so Starbuck found Ahab +with a general chart of the oriental archipelagoes spread before him; +and another separate one representing the long eastern coasts of the +Japanese islands--Niphon, Matsmai, and Sikoke. With his snow-white +new ivory leg braced against the screwed leg of his table, and with a +long pruning-hook of a jack-knife in his hand, the wondrous old man, +with his back to the gangway door, was wrinkling his brow, and +tracing his old courses again. + +"Who's there?" hearing the footstep at the door, but not turning +round to it. "On deck! Begone!" + +"Captain Ahab mistakes; it is I. The oil in the hold is leaking, +sir. We must up Burtons and break out." + +"Up Burtons and break out? Now that we are nearing Japan; heave-to +here for a week to tinker a parcel of old hoops?" + +"Either do that, sir, or waste in one day more oil than we may make +good in a year. What we come twenty thousand miles to get is worth +saving, sir." + +"So it is, so it is; if we get it." + +"I was speaking of the oil in the hold, sir." + +"And I was not speaking or thinking of that at all. Begone! Let it +leak! I'm all aleak myself. Aye! leaks in leaks! not only full of +leaky casks, but those leaky casks are in a leaky ship; and that's a +far worse plight than the Pequod's, man. Yet I don't stop to plug my +leak; for who can find it in the deep-loaded hull; or how hope to +plug it, even if found, in this life's howling gale? Starbuck! +I'll not have the Burtons hoisted." + +"What will the owners say, sir?" + +"Let the owners stand on Nantucket beach and outyell the Typhoons. +What cares Ahab? Owners, owners? Thou art always prating to me, +Starbuck, about those miserly owners, as if the owners were my +conscience. But look ye, the only real owner of anything is its +commander; and hark ye, my conscience is in this ship's keel.--On +deck!" + +"Captain Ahab," said the reddening mate, moving further into the +cabin, with a daring so strangely respectful and cautious that it +almost seemed not only every way seeking to avoid the slightest +outward manifestation of itself, but within also seemed more than +half distrustful of itself; "A better man than I might well pass over +in thee what he would quickly enough resent in a younger man; aye, +and in a happier, Captain Ahab." + +"Devils! Dost thou then so much as dare to critically think of +me?--On deck!" + +"Nay, sir, not yet; I do entreat. And I do dare, sir--to be +forbearing! Shall we not understand each other better than hitherto, +Captain Ahab?" + +Ahab seized a loaded musket from the rack (forming part of most +South-Sea-men's cabin furniture), and pointing it towards Starbuck, +exclaimed: "There is one God that is Lord over the earth, and one +Captain that is lord over the Pequod.--On deck!" + +For an instant in the flashing eyes of the mate, and his fiery +cheeks, you would have almost thought that he had really received the +blaze of the levelled tube. But, mastering his emotion, he half +calmly rose, and as he quitted the cabin, paused for an instant and +said: "Thou hast outraged, not insulted me, sir; but for that I ask +thee not to beware of Starbuck; thou wouldst but laugh; but let Ahab +beware of Ahab; beware of thyself, old man." + +"He waxes brave, but nevertheless obeys; most careful bravery that!" +murmured Ahab, as Starbuck disappeared. "What's that he said--Ahab +beware of Ahab--there's something there!" Then unconsciously using +the musket for a staff, with an iron brow he paced to and fro in the +little cabin; but presently the thick plaits of his forehead relaxed, +and returning the gun to the rack, he went to the deck. + +"Thou art but too good a fellow, Starbuck," he said lowly to the +mate; then raising his voice to the crew: "Furl the t'gallant-sails, +and close-reef the top-sails, fore and aft; back the main-yard; up +Burton, and break out in the main-hold." + +It were perhaps vain to surmise exactly why it was, that as +respecting Starbuck, Ahab thus acted. It may have been a flash of +honesty in him; or mere prudential policy which, under the +circumstance, imperiously forbade the slightest symptom of open +disaffection, however transient, in the important chief officer of +his ship. However it was, his orders were executed; and the Burtons +were hoisted. + + + +CHAPTER 110 + +Queequeg in His Coffin. + + +Upon searching, it was found that the casks last struck into the hold +were perfectly sound, and that the leak must be further off. So, it +being calm weather, they broke out deeper and deeper, disturbing the +slumbers of the huge ground-tier butts; and from that black midnight +sending those gigantic moles into the daylight above. So deep did +they go; and so ancient, and corroded, and weedy the aspect of the +lowermost puncheons, that you almost looked next for some mouldy +corner-stone cask containing coins of Captain Noah, with copies of +the posted placards, vainly warning the infatuated old world from the +flood. Tierce after tierce, too, of water, and bread, and beef, and +shooks of staves, and iron bundles of hoops, were hoisted out, till +at last the piled decks were hard to get about; and the hollow hull +echoed under foot, as if you were treading over empty catacombs, and +reeled and rolled in the sea like an air-freighted demijohn. +Top-heavy was the ship as a dinnerless student with all Aristotle in +his head. Well was it that the Typhoons did not visit them then. + +Now, at this time it was that my poor pagan companion, and fast +bosom-friend, Queequeg, was seized with a fever, which brought him +nigh to his endless end. + +Be it said, that in this vocation of whaling, sinecures are unknown; +dignity and danger go hand in hand; till you get to be Captain, the +higher you rise the harder you toil. So with poor Queequeg, who, as +harpooneer, must not only face all the rage of the living whale, +but--as we have elsewhere seen--mount his dead back in a rolling sea; +and finally descend into the gloom of the hold, and bitterly sweating +all day in that subterraneous confinement, resolutely manhandle the +clumsiest casks and see to their stowage. To be short, among +whalemen, the harpooneers are the holders, so called. + +Poor Queequeg! when the ship was about half disembowelled, you should +have stooped over the hatchway, and peered down upon him there; +where, stripped to his woollen drawers, the tattooed savage was +crawling about amid that dampness and slime, like a green spotted +lizard at the bottom of a well. And a well, or an ice-house, it +somehow proved to him, poor pagan; where, strange to say, for all the +heat of his sweatings, he caught a terrible chill which lapsed into a +fever; and at last, after some days' suffering, laid him in his +hammock, close to the very sill of the door of death. How he wasted +and wasted away in those few long-lingering days, till there seemed +but little left of him but his frame and tattooing. But as all else +in him thinned, and his cheek-bones grew sharper, his eyes, +nevertheless, seemed growing fuller and fuller; they became of a +strange softness of lustre; and mildly but deeply looked out at you +there from his sickness, a wondrous testimony to that immortal health +in him which could not die, or be weakened. And like circles on the +water, which, as they grow fainter, expand; so his eyes seemed +rounding and rounding, like the rings of Eternity. An awe that +cannot be named would steal over you as you sat by the side of this +waning savage, and saw as strange things in his face, as any beheld +who were bystanders when Zoroaster died. For whatever is truly +wondrous and fearful in man, never yet was put into words or books. +And the drawing near of Death, which alike levels all, alike +impresses all with a last revelation, which only an author from the +dead could adequately tell. So that--let us say it again--no dying +Chaldee or Greek had higher and holier thoughts than those, whose +mysterious shades you saw creeping over the face of poor Queequeg, as +he quietly lay in his swaying hammock, and the rolling sea seemed +gently rocking him to his final rest, and the ocean's invisible +flood-tide lifted him higher and higher towards his destined heaven. + +Not a man of the crew but gave him up; and, as for Queequeg himself, +what he thought of his case was forcibly shown by a curious favour he +asked. He called one to him in the grey morning watch, when the day +was just breaking, and taking his hand, said that while in Nantucket +he had chanced to see certain little canoes of dark wood, like the +rich war-wood of his native isle; and upon inquiry, he had learned +that all whalemen who died in Nantucket, were laid in those same dark +canoes, and that the fancy of being so laid had much pleased him; for +it was not unlike the custom of his own race, who, after embalming a +dead warrior, stretched him out in his canoe, and so left him to be +floated away to the starry archipelagoes; for not only do they +believe that the stars are isles, but that far beyond all visible +horizons, their own mild, uncontinented seas, interflow with the blue +heavens; and so form the white breakers of the milky way. He added, +that he shuddered at the thought of being buried in his hammock, +according to the usual sea-custom, tossed like something vile to the +death-devouring sharks. No: he desired a canoe like those of +Nantucket, all the more congenial to him, being a whaleman, that like +a whale-boat these coffin-canoes were without a keel; though that +involved but uncertain steering, and much lee-way adown the dim ages. + +Now, when this strange circumstance was made known aft, the carpenter +was at once commanded to do Queequeg's bidding, whatever it might +include. There was some heathenish, coffin-coloured old lumber +aboard, which, upon a long previous voyage, had been cut from the +aboriginal groves of the Lackaday islands, and from these dark planks +the coffin was recommended to be made. No sooner was the carpenter +apprised of the order, than taking his rule, he forthwith with all +the indifferent promptitude of his character, proceeded into the +forecastle and took Queequeg's measure with great accuracy, regularly +chalking Queequeg's person as he shifted the rule. + +"Ah! poor fellow! he'll have to die now," ejaculated the Long Island +sailor. + +Going to his vice-bench, the carpenter for convenience sake and +general reference, now transferringly measured on it the exact length +the coffin was to be, and then made the transfer permanent by cutting +two notches at its extremities. This done, he marshalled the planks +and his tools, and to work. + +When the last nail was driven, and the lid duly planed and fitted, he +lightly shouldered the coffin and went forward with it, inquiring +whether they were ready for it yet in that direction. + +Overhearing the indignant but half-humorous cries with which the +people on deck began to drive the coffin away, Queequeg, to every +one's consternation, commanded that the thing should be instantly +brought to him, nor was there any denying him; seeing that, of all +mortals, some dying men are the most tyrannical; and certainly, since +they will shortly trouble us so little for evermore, the poor fellows +ought to be indulged. + +Leaning over in his hammock, Queequeg long regarded the coffin with +an attentive eye. He then called for his harpoon, had the wooden +stock drawn from it, and then had the iron part placed in the coffin +along with one of the paddles of his boat. All by his own request, +also, biscuits were then ranged round the sides within: a flask of +fresh water was placed at the head, and a small bag of woody earth +scraped up in the hold at the foot; and a piece of sail-cloth being +rolled up for a pillow, Queequeg now entreated to be lifted into his +final bed, that he might make trial of its comforts, if any it had. +He lay without moving a few minutes, then told one to go to his bag +and bring out his little god, Yojo. Then crossing his arms on his +breast with Yojo between, he called for the coffin lid (hatch he +called it) to be placed over him. The head part turned over with a +leather hinge, and there lay Queequeg in his coffin with little but +his composed countenance in view. "Rarmai" (it will do; it is easy), +he murmured at last, and signed to be replaced in his hammock. + +But ere this was done, Pip, who had been slily hovering near by all +this while, drew nigh to him where he lay, and with soft sobbings, +took him by the hand; in the other, holding his tambourine. + +"Poor rover! will ye never have done with all this weary roving? +where go ye now? But if the currents carry ye to those sweet +Antilles where the beaches are only beat with water-lilies, will ye +do one little errand for me? Seek out one Pip, who's now been +missing long: I think he's in those far Antilles. If ye find him, +then comfort him; for he must be very sad; for look! he's left his +tambourine behind;--I found it. Rig-a-dig, dig, dig! Now, Queequeg, +die; and I'll beat ye your dying march." + +"I have heard," murmured Starbuck, gazing down the scuttle, "that in +violent fevers, men, all ignorance, have talked in ancient tongues; +and that when the mystery is probed, it turns out always that in +their wholly forgotten childhood those ancient tongues had been +really spoken in their hearing by some lofty scholars. So, to my +fond faith, poor Pip, in this strange sweetness of his lunacy, brings +heavenly vouchers of all our heavenly homes. Where learned he that, +but there?--Hark! he speaks again: but more wildly now." + +"Form two and two! Let's make a General of him! Ho, where's his +harpoon? Lay it across here.--Rig-a-dig, dig, dig! huzza! Oh for a +game cock now to sit upon his head and crow! Queequeg dies +game!--mind ye that; Queequeg dies game!--take ye good heed of that; +Queequeg dies game! I say; game, game, game! but base little Pip, he +died a coward; died all a'shiver;--out upon Pip! Hark ye; if ye find +Pip, tell all the Antilles he's a runaway; a coward, a coward, a +coward! Tell them he jumped from a whale-boat! I'd never beat my +tambourine over base Pip, and hail him General, if he were once more +dying here. No, no! shame upon all cowards--shame upon them! Let 'em +go drown like Pip, that jumped from a whale-boat. Shame! shame!" + +During all this, Queequeg lay with closed eyes, as if in a dream. +Pip was led away, and the sick man was replaced in his hammock. + +But now that he had apparently made every preparation for death; now +that his coffin was proved a good fit, Queequeg suddenly rallied; +soon there seemed no need of the carpenter's box: and thereupon, +when some expressed their delighted surprise, he, in substance, said, +that the cause of his sudden convalescence was this;--at a critical +moment, he had just recalled a little duty ashore, which he was +leaving undone; and therefore had changed his mind about dying: he +could not die yet, he averred. They asked him, then, whether to live +or die was a matter of his own sovereign will and pleasure. He +answered, certainly. In a word, it was Queequeg's conceit, that if a +man made up his mind to live, mere sickness could not kill him: +nothing but a whale, or a gale, or some violent, ungovernable, +unintelligent destroyer of that sort. + +Now, there is this noteworthy difference between savage and +civilized; that while a sick, civilized man may be six months +convalescing, generally speaking, a sick savage is almost half-well +again in a day. So, in good time my Queequeg gained strength; and at +length after sitting on the windlass for a few indolent days (but +eating with a vigorous appetite) he suddenly leaped to his feet, +threw out his arms and legs, gave himself a good stretching, yawned +a little bit, and then springing into the head of his hoisted boat, +and poising a harpoon, pronounced himself fit for a fight. + +With a wild whimsiness, he now used his coffin for a sea-chest; and +emptying into it his canvas bag of clothes, set them in order there. +Many spare hours he spent, in carving the lid with all manner of +grotesque figures and drawings; and it seemed that hereby he was +striving, in his rude way, to copy parts of the twisted tattooing on +his body. And this tattooing had been the work of a departed +prophet and seer of his island, who, by those hieroglyphic marks, had +written out on his body a complete theory of the heavens and the +earth, and a mystical treatise on the art of attaining truth; so that +Queequeg in his own proper person was a riddle to unfold; a wondrous +work in one volume; but whose mysteries not even himself could read, +though his own live heart beat against them; and these mysteries were +therefore destined in the end to moulder away with the living +parchment whereon they were inscribed, and so be unsolved to the +last. And this thought it must have been which suggested to Ahab +that wild exclamation of his, when one morning turning away from +surveying poor Queequeg--"Oh, devilish tantalization of the gods!" + + + +CHAPTER 111 + +The Pacific. + + +When gliding by the Bashee isles we emerged at last upon the great +South Sea; were it not for other things, I could have greeted my dear +Pacific with uncounted thanks, for now the long supplication of my +youth was answered; that serene ocean rolled eastwards from me a +thousand leagues of blue. + +There is, one knows not what sweet mystery about this sea, whose +gently awful stirrings seem to speak of some hidden soul beneath; +like those fabled undulations of the Ephesian sod over the buried +Evangelist St. John. And meet it is, that over these sea-pastures, +wide-rolling watery prairies and Potters' Fields of all four +continents, the waves should rise and fall, and ebb and flow +unceasingly; for here, millions of mixed shades and shadows, drowned +dreams, somnambulisms, reveries; all that we call lives and souls, +lie dreaming, dreaming, still; tossing like slumberers in their beds; +the ever-rolling waves but made so by their restlessness. + +To any meditative Magian rover, this serene Pacific, once beheld, +must ever after be the sea of his adoption. It rolls the midmost +waters of the world, the Indian ocean and Atlantic being but its +arms. The same waves wash the moles of the new-built Californian +towns, but yesterday planted by the recentest race of men, and lave +the faded but still gorgeous skirts of Asiatic lands, older than +Abraham; while all between float milky-ways of coral isles, and +low-lying, endless, unknown Archipelagoes, and impenetrable Japans. +Thus this mysterious, divine Pacific zones the world's whole bulk +about; makes all coasts one bay to it; seems the tide-beating heart +of earth. Lifted by those eternal swells, you needs must own the +seductive god, bowing your head to Pan. + +But few thoughts of Pan stirred Ahab's brain, as standing like an +iron statue at his accustomed place beside the mizen rigging, with +one nostril he unthinkingly snuffed the sugary musk from the Bashee +isles (in whose sweet woods mild lovers must be walking), and with +the other consciously inhaled the salt breath of the new found sea; +that sea in which the hated White Whale must even then be swimming. +Launched at length upon these almost final waters, and gliding +towards the Japanese cruising-ground, the old man's purpose +intensified itself. His firm lips met like the lips of a vice; the +Delta of his forehead's veins swelled like overladen brooks; in his +very sleep, his ringing cry ran through the vaulted hull, "Stern all! +the White Whale spouts thick blood!" + + + +CHAPTER 112 + +The Blacksmith. + + +Availing himself of the mild, summer-cool weather that now reigned +in these latitudes, and in preparation for the peculiarly active +pursuits shortly to be anticipated, Perth, the begrimed, blistered +old blacksmith, had not removed his portable forge to the hold again, +after concluding his contributory work for Ahab's leg, but still +retained it on deck, fast lashed to ringbolts by the foremast; being +now almost incessantly invoked by the headsmen, and harpooneers, and +bowsmen to do some little job for them; altering, or repairing, or +new shaping their various weapons and boat furniture. Often he would +be surrounded by an eager circle, all waiting to be served; holding +boat-spades, pike-heads, harpoons, and lances, and jealously watching +his every sooty movement, as he toiled. Nevertheless, this old man's +was a patient hammer wielded by a patient arm. No murmur, no +impatience, no petulance did come from him. Silent, slow, and +solemn; bowing over still further his chronically broken back, he +toiled away, as if toil were life itself, and the heavy beating of +his hammer the heavy beating of his heart. And so it was.--Most +miserable! + +A peculiar walk in this old man, a certain slight but painful +appearing yawing in his gait, had at an early period of the voyage +excited the curiosity of the mariners. And to the importunity of +their persisted questionings he had finally given in; and so it came +to pass that every one now knew the shameful story of his wretched +fate. + +Belated, and not innocently, one bitter winter's midnight, on the +road running between two country towns, the blacksmith half-stupidly +felt the deadly numbness stealing over him, and sought refuge in a +leaning, dilapidated barn. The issue was, the loss of the +extremities of both feet. Out of this revelation, part by part, at +last came out the four acts of the gladness, and the one long, and as +yet uncatastrophied fifth act of the grief of his life's drama. + +He was an old man, who, at the age of nearly sixty, had postponedly +encountered that thing in sorrow's technicals called ruin. He had +been an artisan of famed excellence, and with plenty to do; owned a +house and garden; embraced a youthful, daughter-like, loving wife, +and three blithe, ruddy children; every Sunday went to a +cheerful-looking church, planted in a grove. But one night, under +cover of darkness, and further concealed in a most cunning +disguisement, a desperate burglar slid into his happy home, and +robbed them all of everything. And darker yet to tell, the +blacksmith himself did ignorantly conduct this burglar into his +family's heart. It was the Bottle Conjuror! Upon the opening of +that fatal cork, forth flew the fiend, and shrivelled up his home. +Now, for prudent, most wise, and economic reasons, the blacksmith's +shop was in the basement of his dwelling, but with a separate +entrance to it; so that always had the young and loving healthy wife +listened with no unhappy nervousness, but with vigorous pleasure, to +the stout ringing of her young-armed old husband's hammer; whose +reverberations, muffled by passing through the floors and walls, came +up to her, not unsweetly, in her nursery; and so, to stout Labor's +iron lullaby, the blacksmith's infants were rocked to slumber. + +Oh, woe on woe! Oh, Death, why canst thou not sometimes be timely? +Hadst thou taken this old blacksmith to thyself ere his full ruin +came upon him, then had the young widow had a delicious grief, and +her orphans a truly venerable, legendary sire to dream of in their +after years; and all of them a care-killing competency. But Death +plucked down some virtuous elder brother, on whose whistling daily +toil solely hung the responsibilities of some other family, and left +the worse than useless old man standing, till the hideous rot of life +should make him easier to harvest. + +Why tell the whole? The blows of the basement hammer every day grew +more and more between; and each blow every day grew fainter than the +last; the wife sat frozen at the window, with tearless eyes, +glitteringly gazing into the weeping faces of her children; the +bellows fell; the forge choked up with cinders; the house was sold; +the mother dived down into the long church-yard grass; her children +twice followed her thither; and the houseless, familyless old man +staggered off a vagabond in crape; his every woe unreverenced; his +grey head a scorn to flaxen curls! + +Death seems the only desirable sequel for a career like this; but +Death is only a launching into the region of the strange Untried; it +is but the first salutation to the possibilities of the immense +Remote, the Wild, the Watery, the Unshored; therefore, to the +death-longing eyes of such men, who still have left in them some +interior compunctions against suicide, does the all-contributed and +all-receptive ocean alluringly spread forth his whole plain of +unimaginable, taking terrors, and wonderful, new-life adventures; and +from the hearts of infinite Pacifics, the thousand mermaids sing to +them--"Come hither, broken-hearted; here is another life without the +guilt of intermediate death; here are wonders supernatural, without +dying for them. Come hither! bury thyself in a life which, to your +now equally abhorred and abhorring, landed world, is more oblivious +than death. Come hither! put up THY gravestone, too, within the +churchyard, and come hither, till we marry thee!" + +Hearkening to these voices, East and West, by early sunrise, and by +fall of eve, the blacksmith's soul responded, Aye, I come! And so +Perth went a-whaling. + + + +CHAPTER 113 + +The Forge. + + +With matted beard, and swathed in a bristling shark-skin apron, about +mid-day, Perth was standing between his forge and anvil, the latter +placed upon an iron-wood log, with one hand holding a pike-head in +the coals, and with the other at his forge's lungs, when Captain Ahab +came along, carrying in his hand a small rusty-looking leathern bag. +While yet a little distance from the forge, moody Ahab paused; till +at last, Perth, withdrawing his iron from the fire, began hammering +it upon the anvil--the red mass sending off the sparks in thick +hovering flights, some of which flew close to Ahab. + +"Are these thy Mother Carey's chickens, Perth? they are always flying +in thy wake; birds of good omen, too, but not to all;--look here, +they burn; but thou--thou liv'st among them without a scorch." + +"Because I am scorched all over, Captain Ahab," answered Perth, +resting for a moment on his hammer; "I am past scorching; not easily +can'st thou scorch a scar." + +"Well, well; no more. Thy shrunk voice sounds too calmly, sanely +woeful to me. In no Paradise myself, I am impatient of all misery in +others that is not mad. Thou should'st go mad, blacksmith; say, why +dost thou not go mad? How can'st thou endure without being mad? Do +the heavens yet hate thee, that thou can'st not go mad?--What wert +thou making there?" + +"Welding an old pike-head, sir; there were seams and dents in it." + +"And can'st thou make it all smooth again, blacksmith, after such +hard usage as it had?" + +"I think so, sir." + +"And I suppose thou can'st smoothe almost any seams and dents; never +mind how hard the metal, blacksmith?" + +"Aye, sir, I think I can; all seams and dents but one." + +"Look ye here, then," cried Ahab, passionately advancing, and leaning +with both hands on Perth's shoulders; "look ye here--HERE--can ye +smoothe out a seam like this, blacksmith," sweeping one hand across +his ribbed brow; "if thou could'st, blacksmith, glad enough would I +lay my head upon thy anvil, and feel thy heaviest hammer between my +eyes. Answer! Can'st thou smoothe this seam?" + +"Oh! that is the one, sir! Said I not all seams and dents but one?" + +"Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for +though thou only see'st it here in my flesh, it has worked down into +the bone of my skull--THAT is all wrinkles! But, away with child's +play; no more gaffs and pikes to-day. Look ye here!" jingling the +leathern bag, as if it were full of gold coins. "I, too, want a +harpoon made; one that a thousand yoke of fiends could not part, +Perth; something that will stick in a whale like his own fin-bone. +There's the stuff," flinging the pouch upon the anvil. "Look ye, +blacksmith, these are the gathered nail-stubbs of the steel shoes of +racing horses." + +"Horse-shoe stubbs, sir? Why, Captain Ahab, thou hast here, then, +the best and stubbornest stuff we blacksmiths ever work." + +"I know it, old man; these stubbs will weld together like glue from +the melted bones of murderers. Quick! forge me the harpoon. And +forge me first, twelve rods for its shank; then wind, and twist, and +hammer these twelve together like the yarns and strands of a +tow-line. Quick! I'll blow the fire." + +When at last the twelve rods were made, Ahab tried them, one by one, +by spiralling them, with his own hand, round a long, heavy iron bolt. +"A flaw!" rejecting the last one. "Work that over again, Perth." + +This done, Perth was about to begin welding the twelve into one, when +Ahab stayed his hand, and said he would weld his own iron. As, then, +with regular, gasping hems, he hammered on the anvil, Perth passing +to him the glowing rods, one after the other, and the hard pressed +forge shooting up its intense straight flame, the Parsee passed +silently, and bowing over his head towards the fire, seemed invoking +some curse or some blessing on the toil. But, as Ahab looked up, he +slid aside. + +"What's that bunch of lucifers dodging about there for?" muttered +Stubb, looking on from the forecastle. "That Parsee smells fire like +a fusee; and smells of it himself, like a hot musket's powder-pan." + +At last the shank, in one complete rod, received its final heat; and +as Perth, to temper it, plunged it all hissing into the cask of water +near by, the scalding steam shot up into Ahab's bent face. + +"Would'st thou brand me, Perth?" wincing for a moment with the pain; +"have I been but forging my own branding-iron, then?" + +"Pray God, not that; yet I fear something, Captain Ahab. Is not this +harpoon for the White Whale?" + +"For the white fiend! But now for the barbs; thou must make them +thyself, man. Here are my razors--the best of steel; here, and make +the barbs sharp as the needle-sleet of the Icy Sea." + +For a moment, the old blacksmith eyed the razors as though he would +fain not use them. + +"Take them, man, I have no need for them; for I now neither shave, +sup, nor pray till--but here--to work!" + +Fashioned at last into an arrowy shape, and welded by Perth to the +shank, the steel soon pointed the end of the iron; and as the +blacksmith was about giving the barbs their final heat, prior to +tempering them, he cried to Ahab to place the water-cask near. + +"No, no--no water for that; I want it of the true death-temper. +Ahoy, there! Tashtego, Queequeg, Daggoo! What say ye, pagans! Will +ye give me as much blood as will cover this barb?" holding it high +up. A cluster of dark nods replied, Yes. Three punctures were made +in the heathen flesh, and the White Whale's barbs were then tempered. + +"Ego non baptizo te in nomine patris, sed in nomine diaboli!" +deliriously howled Ahab, as the malignant iron scorchingly devoured +the baptismal blood. + +Now, mustering the spare poles from below, and selecting one of +hickory, with the bark still investing it, Ahab fitted the end to the +socket of the iron. A coil of new tow-line was then unwound, and +some fathoms of it taken to the windlass, and stretched to a great +tension. Pressing his foot upon it, till the rope hummed like a +harp-string, then eagerly bending over it, and seeing no strandings, +Ahab exclaimed, "Good! and now for the seizings." + +At one extremity the rope was unstranded, and the separate spread +yarns were all braided and woven round the socket of the harpoon; the +pole was then driven hard up into the socket; from the lower end the +rope was traced half-way along the pole's length, and firmly secured +so, with intertwistings of twine. This done, pole, iron, and +rope--like the Three Fates--remained inseparable, and Ahab moodily +stalked away with the weapon; the sound of his ivory leg, and the +sound of the hickory pole, both hollowly ringing along every plank. +But ere he entered his cabin, light, unnatural, half-bantering, yet +most piteous sound was heard. Oh, Pip! thy wretched laugh, thy +idle but unresting eye; all thy strange mummeries not unmeaningly +blended with the black tragedy of the melancholy ship, and mocked it! + + + +CHAPTER 114 + +The Gilder. + + +Penetrating further and further into the heart of the Japanese +cruising ground, the Pequod was soon all astir in the fishery. +Often, in mild, pleasant weather, for twelve, fifteen, eighteen, and +twenty hours on the stretch, they were engaged in the boats, steadily +pulling, or sailing, or paddling after the whales, or for an +interlude of sixty or seventy minutes calmly awaiting their uprising; +though with but small success for their pains. + +At such times, under an abated sun; afloat all day upon smooth, slow +heaving swells; seated in his boat, light as a birch canoe; and so +sociably mixing with the soft waves themselves, that like +hearth-stone cats they purr against the gunwale; these are the times +of dreamy quietude, when beholding the tranquil beauty and brilliancy +of the ocean's skin, one forgets the tiger heart that pants beneath +it; and would not willingly remember, that this velvet paw but +conceals a remorseless fang. + +These are the times, when in his whale-boat the rover softly feels a +certain filial, confident, land-like feeling towards the sea; that he +regards it as so much flowery earth; and the distant ship revealing +only the tops of her masts, seems struggling forward, not through +high rolling waves, but through the tall grass of a rolling prairie: +as when the western emigrants' horses only show their erected ears, +while their hidden bodies widely wade through the amazing verdure. + +The long-drawn virgin vales; the mild blue hill-sides; as over these +there steals the hush, the hum; you almost swear that play-wearied +children lie sleeping in these solitudes, in some glad May-time, when +the flowers of the woods are plucked. And all this mixes with your +most mystic mood; so that fact and fancy, half-way meeting, +interpenetrate, and form one seamless whole. + +Nor did such soothing scenes, however temporary, fail of at least as +temporary an effect on Ahab. But if these secret golden keys did +seem to open in him his own secret golden treasuries, yet did his +breath upon them prove but tarnishing. + +Oh, grassy glades! oh, ever vernal endless landscapes in the soul; in +ye,--though long parched by the dead drought of the earthy +life,--in ye, men yet may roll, like young horses in new morning +clover; and for some few fleeting moments, feel the cool dew of the +life immortal on them. Would to God these blessed calms would last. +But the mingled, mingling threads of life are woven by warp and woof: +calms crossed by storms, a storm for every calm. There is no steady +unretracing progress in this life; we do not advance through fixed +gradations, and at the last one pause:--through infancy's unconscious +spell, boyhood's thoughtless faith, adolescence' doubt (the common +doom), then scepticism, then disbelief, resting at last in manhood's +pondering repose of If. But once gone through, we trace the round +again; and are infants, boys, and men, and Ifs eternally. Where lies +the final harbor, whence we unmoor no more? In what rapt ether sails +the world, of which the weariest will never weary? Where is the +foundling's father hidden? Our souls are like those orphans whose +unwedded mothers die in bearing them: the secret of our paternity +lies in their grave, and we must there to learn it. + +And that same day, too, gazing far down from his boat's side into +that same golden sea, Starbuck lowly murmured:-- + +"Loveliness unfathomable, as ever lover saw in his young bride's +eye!--Tell me not of thy teeth-tiered sharks, and thy kidnapping +cannibal ways. Let faith oust fact; let fancy oust memory; I look +deep down and do believe." + +And Stubb, fish-like, with sparkling scales, leaped up in that same +golden light:-- + +"I am Stubb, and Stubb has his history; but here Stubb takes oaths +that he has always been jolly!" + + + +CHAPTER 115 + +The Pequod Meets The Bachelor. + + +And jolly enough were the sights and the sounds that came bearing +down before the wind, some few weeks after Ahab's harpoon had been +welded. + +It was a Nantucket ship, the Bachelor, which had just wedged in her +last cask of oil, and bolted down her bursting hatches; and now, in +glad holiday apparel, was joyously, though somewhat vain-gloriously, +sailing round among the widely-separated ships on the ground, +previous to pointing her prow for home. + +The three men at her mast-head wore long streamers of narrow red +bunting at their hats; from the stern, a whale-boat was suspended, +bottom down; and hanging captive from the bowsprit was seen the long +lower jaw of the last whale they had slain. Signals, ensigns, and +jacks of all colours were flying from her rigging, on every side. +Sideways lashed in each of her three basketed tops were two barrels +of sperm; above which, in her top-mast cross-trees, you saw slender +breakers of the same precious fluid; and nailed to her main truck was +a brazen lamp. + +As was afterwards learned, the Bachelor had met with the most +surprising success; all the more wonderful, for that while cruising +in the same seas numerous other vessels had gone entire months +without securing a single fish. Not only had barrels of beef and +bread been given away to make room for the far more valuable sperm, +but additional supplemental casks had been bartered for, from the +ships she had met; and these were stowed along the deck, and in the +captain's and officers' state-rooms. Even the cabin table itself +had been knocked into kindling-wood; and the cabin mess dined off the +broad head of an oil-butt, lashed down to the floor for a +centrepiece. In the forecastle, the sailors had actually caulked +and pitched their chests, and filled them; it was humorously added, +that the cook had clapped a head on his largest boiler, and filled +it; that the steward had plugged his spare coffee-pot and filled it; +that the harpooneers had headed the sockets of their irons and filled +them; that indeed everything was filled with sperm, except the +captain's pantaloons pockets, and those he reserved to thrust his +hands into, in self-complacent testimony of his entire satisfaction. + +As this glad ship of good luck bore down upon the moody Pequod, the +barbarian sound of enormous drums came from her forecastle; and +drawing still nearer, a crowd of her men were seen standing round her +huge try-pots, which, covered with the parchment-like POKE or stomach +skin of the black fish, gave forth a loud roar to every stroke of the +clenched hands of the crew. On the quarter-deck, the mates and +harpooneers were dancing with the olive-hued girls who had eloped +with them from the Polynesian Isles; while suspended in an +ornamented boat, firmly secured aloft between the foremast and +mainmast, three Long Island negroes, with glittering fiddle-bows of +whale ivory, were presiding over the hilarious jig. Meanwhile, +others of the ship's company were tumultuously busy at the masonry of +the try-works, from which the huge pots had been removed. You would +have almost thought they were pulling down the cursed Bastille, such +wild cries they raised, as the now useless brick and mortar were +being hurled into the sea. + +Lord and master over all this scene, the captain stood erect on the +ship's elevated quarter-deck, so that the whole rejoicing drama was +full before him, and seemed merely contrived for his own individual +diversion. + +And Ahab, he too was standing on his quarter-deck, shaggy and black, +with a stubborn gloom; and as the two ships crossed each other's +wakes--one all jubilations for things passed, the other all +forebodings as to things to come--their two captains in themselves +impersonated the whole striking contrast of the scene. + +"Come aboard, come aboard!" cried the gay Bachelor's commander, +lifting a glass and a bottle in the air. + +"Hast seen the White Whale?" gritted Ahab in reply. + +"No; only heard of him; but don't believe in him at all," said the +other good-humoredly. "Come aboard!" + +"Thou art too damned jolly. Sail on. Hast lost any men?" + +"Not enough to speak of--two islanders, that's all;--but come aboard, +old hearty, come along. I'll soon take that black from your brow. +Come along, will ye (merry's the play); a full ship and +homeward-bound." + +"How wondrous familiar is a fool!" muttered Ahab; then aloud, "Thou +art a full ship and homeward bound, thou sayst; well, then, call me +an empty ship, and outward-bound. So go thy ways, and I will mine. +Forward there! Set all sail, and keep her to the wind!" + +And thus, while the one ship went cheerily before the breeze, the +other stubbornly fought against it; and so the two vessels parted; +the crew of the Pequod looking with grave, lingering glances towards +the receding Bachelor; but the Bachelor's men never heeding their +gaze for the lively revelry they were in. And as Ahab, leaning over +the taffrail, eyed the homewardbound craft, he took from his pocket a +small vial of sand, and then looking from the ship to the vial, +seemed thereby bringing two remote associations together, for that +vial was filled with Nantucket soundings. + + + +CHAPTER 116 + +The Dying Whale. + + +Not seldom in this life, when, on the right side, fortune's favourites +sail close by us, we, though all adroop before, catch somewhat of the +rushing breeze, and joyfully feel our bagging sails fill out. So +seemed it with the Pequod. For next day after encountering the gay +Bachelor, whales were seen and four were slain; and one of them by +Ahab. + +It was far down the afternoon; and when all the spearings of the +crimson fight were done: and floating in the lovely sunset sea and +sky, sun and whale both stilly died together; then, such a sweetness +and such plaintiveness, such inwreathing orisons curled up in that +rosy air, that it almost seemed as if far over from the deep green +convent valleys of the Manilla isles, the Spanish land-breeze, +wantonly turned sailor, had gone to sea, freighted with these vesper +hymns. + +Soothed again, but only soothed to deeper gloom, Ahab, who had +sterned off from the whale, sat intently watching his final wanings +from the now tranquil boat. For that strange spectacle observable in +all sperm whales dying--the turning sunwards of the head, and so +expiring--that strange spectacle, beheld of such a placid evening, +somehow to Ahab conveyed a wondrousness unknown before. + +"He turns and turns him to it,--how slowly, but how steadfastly, his +homage-rendering and invoking brow, with his last dying motions. He +too worships fire; most faithful, broad, baronial vassal of the +sun!--Oh that these too-favouring eyes should see these too-favouring +sights. Look! here, far water-locked; beyond all hum of human weal +or woe; in these most candid and impartial seas; where to traditions +no rocks furnish tablets; where for long Chinese ages, the billows +have still rolled on speechless and unspoken to, as stars that shine +upon the Niger's unknown source; here, too, life dies sunwards full +of faith; but see! no sooner dead, than death whirls round the +corpse, and it heads some other way. + +"Oh, thou dark Hindoo half of nature, who of drowned bones hast +builded thy separate throne somewhere in the heart of these +unverdured seas; thou art an infidel, thou queen, and too truly +speakest to me in the wide-slaughtering Typhoon, and the hushed +burial of its after calm. Nor has this thy whale sunwards turned his +dying head, and then gone round again, without a lesson to me. + +"Oh, trebly hooped and welded hip of power! Oh, high aspiring, +rainbowed jet!--that one strivest, this one jettest all in vain! In +vain, oh whale, dost thou seek intercedings with yon all-quickening +sun, that only calls forth life, but gives it not again. Yet dost +thou, darker half, rock me with a prouder, if a darker faith. All +thy unnamable imminglings float beneath me here; I am buoyed by +breaths of once living things, exhaled as air, but water now. + +"Then hail, for ever hail, O sea, in whose eternal tossings the wild +fowl finds his only rest. Born of earth, yet suckled by the sea; +though hill and valley mothered me, ye billows are my +foster-brothers!" + + + +CHAPTER 117 + +The Whale Watch. + + +The four whales slain that evening had died wide apart; one, far to +windward; one, less distant, to leeward; one ahead; one astern. +These last three were brought alongside ere nightfall; but the +windward one could not be reached till morning; and the boat that had +killed it lay by its side all night; and that boat was Ahab's. + +The waif-pole was thrust upright into the dead whale's spout-hole; +and the lantern hanging from its top, cast a troubled flickering +glare upon the black, glossy back, and far out upon the midnight +waves, which gently chafed the whale's broad flank, like soft surf +upon a beach. + +Ahab and all his boat's crew seemed asleep but the Parsee; who +crouching in the bow, sat watching the sharks, that spectrally played +round the whale, and tapped the light cedar planks with their tails. +A sound like the moaning in squadrons over Asphaltites of unforgiven +ghosts of Gomorrah, ran shuddering through the air. + +Started from his slumbers, Ahab, face to face, saw the Parsee; and +hooped round by the gloom of the night they seemed the last men in a +flooded world. "I have dreamed it again," said he. + +"Of the hearses? Have I not said, old man, that neither hearse nor +coffin can be thine?" + +"And who are hearsed that die on the sea?" + +"But I said, old man, that ere thou couldst die on this voyage, two +hearses must verily be seen by thee on the sea; the first not made by +mortal hands; and the visible wood of the last one must be grown in +America." + +"Aye, aye! a strange sight that, Parsee:--a hearse and its plumes +floating over the ocean with the waves for the pall-bearers. Ha! +Such a sight we shall not soon see." + +"Believe it or not, thou canst not die till it be seen, old man." + +"And what was that saying about thyself?" + +"Though it come to the last, I shall still go before thee thy pilot." + +"And when thou art so gone before--if that ever befall--then ere I +can follow, thou must still appear to me, to pilot me still?--Was it +not so? Well, then, did I believe all ye say, oh my pilot! I have +here two pledges that I shall yet slay Moby Dick and survive it." + +"Take another pledge, old man," said the Parsee, as his eyes lighted +up like fire-flies in the gloom--"Hemp only can kill thee." + +"The gallows, ye mean.--I am immortal then, on land and on sea," +cried Ahab, with a laugh of derision;--"Immortal on land and on sea!" + +Both were silent again, as one man. The grey dawn came on, and the +slumbering crew arose from the boat's bottom, and ere noon the dead +whale was brought to the ship. + + + +CHAPTER 118 + +The Quadrant. + + +The season for the Line at length drew near; and every day when Ahab, +coming from his cabin, cast his eyes aloft, the vigilant helmsman +would ostentatiously handle his spokes, and the eager mariners +quickly run to the braces, and would stand there with all their eyes +centrally fixed on the nailed doubloon; impatient for the order to +point the ship's prow for the equator. In good time the order came. +It was hard upon high noon; and Ahab, seated in the bows of his +high-hoisted boat, was about taking his wonted daily observation of +the sun to determine his latitude. + +Now, in that Japanese sea, the days in summer are as freshets of +effulgences. That unblinkingly vivid Japanese sun seems the blazing +focus of the glassy ocean's immeasurable burning-glass. The sky +looks lacquered; clouds there are none; the horizon floats; and this +nakedness of unrelieved radiance is as the insufferable splendors of +God's throne. Well that Ahab's quadrant was furnished with coloured +glasses, through which to take sight of that solar fire. So, +swinging his seated form to the roll of the ship, and with his +astrological-looking instrument placed to his eye, he remained in +that posture for some moments to catch the precise instant when the +sun should gain its precise meridian. Meantime while his whole +attention was absorbed, the Parsee was kneeling beneath him on the +ship's deck, and with face thrown up like Ahab's, was eyeing the same +sun with him; only the lids of his eyes half hooded their orbs, and +his wild face was subdued to an earthly passionlessness. At length +the desired observation was taken; and with his pencil upon his ivory +leg, Ahab soon calculated what his latitude must be at that precise +instant. Then falling into a moment's revery, he again looked up +towards the sun and murmured to himself: "Thou sea-mark! thou high +and mighty Pilot! thou tellest me truly where I AM--but canst thou +cast the least hint where I SHALL be? Or canst thou tell where some +other thing besides me is this moment living? Where is Moby Dick? +This instant thou must be eyeing him. These eyes of mine look into +the very eye that is even now beholding him; aye, and into the eye +that is even now equally beholding the objects on the unknown, +thither side of thee, thou sun!" + +Then gazing at his quadrant, and handling, one after the other, its +numerous cabalistical contrivances, he pondered again, and muttered: +"Foolish toy! babies' plaything of haughty Admirals, and Commodores, +and Captains; the world brags of thee, of thy cunning and might; but +what after all canst thou do, but tell the poor, pitiful point, where +thou thyself happenest to be on this wide planet, and the hand that +holds thee: no! not one jot more! Thou canst not tell where one drop +of water or one grain of sand will be to-morrow noon; and yet with +thy impotence thou insultest the sun! Science! Curse thee, thou +vain toy; and cursed be all the things that cast man's eyes aloft to +that heaven, whose live vividness but scorches him, as these old eyes +are even now scorched with thy light, O sun! Level by nature to this +earth's horizon are the glances of man's eyes; not shot from the +crown of his head, as if God had meant him to gaze on his firmament. +Curse thee, thou quadrant!" dashing it to the deck, "no longer will I +guide my earthly way by thee; the level ship's compass, and the level +deadreckoning, by log and by line; THESE shall conduct me, and show +me my place on the sea. Aye," lighting from the boat to the deck, +"thus I trample on thee, thou paltry thing that feebly pointest on +high; thus I split and destroy thee!" + +As the frantic old man thus spoke and thus trampled with his live and +dead feet, a sneering triumph that seemed meant for Ahab, and a +fatalistic despair that seemed meant for himself--these passed over +the mute, motionless Parsee's face. Unobserved he rose and glided +away; while, awestruck by the aspect of their commander, the seamen +clustered together on the forecastle, till Ahab, troubledly pacing +the deck, shouted out--"To the braces! Up helm!--square in!" + +In an instant the yards swung round; and as the ship half-wheeled +upon her heel, her three firm-seated graceful masts erectly poised +upon her long, ribbed hull, seemed as the three Horatii pirouetting +on one sufficient steed. + +Standing between the knight-heads, Starbuck watched the Pequod's +tumultuous way, and Ahab's also, as he went lurching along the deck. + +"I have sat before the dense coal fire and watched it all aglow, full +of its tormented flaming life; and I have seen it wane at last, down, +down, to dumbest dust. Old man of oceans! of all this fiery life of +thine, what will at length remain but one little heap of ashes!" + +"Aye," cried Stubb, "but sea-coal ashes--mind ye that, Mr. +Starbuck--sea-coal, not your common charcoal. Well, well; I heard +Ahab mutter, 'Here some one thrusts these cards into these old hands +of mine; swears that I must play them, and no others.' And damn me, +Ahab, but thou actest right; live in the game, and die in it!" + + + +CHAPTER 119 + +The Candles. + + +Warmest climes but nurse the cruellest fangs: the tiger of Bengal +crouches in spiced groves of ceaseless verdure. Skies the most +effulgent but basket the deadliest thunders: gorgeous Cuba knows +tornadoes that never swept tame northern lands. So, too, it is, that +in these resplendent Japanese seas the mariner encounters the direst +of all storms, the Typhoon. It will sometimes burst from out that +cloudless sky, like an exploding bomb upon a dazed and sleepy town. + +Towards evening of that day, the Pequod was torn of her canvas, and +bare-poled was left to fight a Typhoon which had struck her directly +ahead. When darkness came on, sky and sea roared and split with the +thunder, and blazed with the lightning, that showed the disabled +masts fluttering here and there with the rags which the first fury of +the tempest had left for its after sport. + +Holding by a shroud, Starbuck was standing on the quarter-deck; at +every flash of the lightning glancing aloft, to see what additional +disaster might have befallen the intricate hamper there; while Stubb +and Flask were directing the men in the higher hoisting and firmer +lashing of the boats. But all their pains seemed naught. Though +lifted to the very top of the cranes, the windward quarter boat +(Ahab's) did not escape. A great rolling sea, dashing high up +against the reeling ship's high teetering side, stove in the boat's +bottom at the stern, and left it again, all dripping through like a +sieve. + +"Bad work, bad work! Mr. Starbuck," said Stubb, regarding the wreck, +"but the sea will have its way. Stubb, for one, can't fight it. You +see, Mr. Starbuck, a wave has such a great long start before it +leaps, all round the world it runs, and then comes the spring! But +as for me, all the start I have to meet it, is just across the deck +here. But never mind; it's all in fun: so the old song +says;"--(SINGS.) + +Oh! jolly is the gale, +And a joker is the whale, +A' flourishin' his tail,-- +Such a funny, sporty, gamy, jesty, joky, hoky-poky lad, is the Ocean, oh! + +The scud all a flyin', +That's his flip only foamin'; +When he stirs in the spicin',-- +Such a funny, sporty, gamy, jesty, joky, hoky-poky lad, is the Ocean, oh! + +Thunder splits the ships, +But he only smacks his lips, +A tastin' of this flip,-- +Such a funny, sporty, gamy, jesty, joky, hoky-poky lad, is the Ocean, oh! + + +"Avast Stubb," cried Starbuck, "let the Typhoon sing, and strike his +harp here in our rigging; but if thou art a brave man thou wilt hold +thy peace." + +"But I am not a brave man; never said I was a brave man; I am a +coward; and I sing to keep up my spirits. And I tell you what it is, +Mr. Starbuck, there's no way to stop my singing in this world but to +cut my throat. And when that's done, ten to one I sing ye the +doxology for a wind-up." + +"Madman! look through my eyes if thou hast none of thine own." + +"What! how can you see better of a dark night than anybody else, +never mind how foolish?" + +"Here!" cried Starbuck, seizing Stubb by the shoulder, and pointing +his hand towards the weather bow, "markest thou not that the gale +comes from the eastward, the very course Ahab is to run for Moby +Dick? the very course he swung to this day noon? now mark his boat +there; where is that stove? In the stern-sheets, man; where he is +wont to stand--his stand-point is stove, man! Now jump overboard, +and sing away, if thou must! + +"I don't half understand ye: what's in the wind?" + +"Yes, yes, round the Cape of Good Hope is the shortest way to +Nantucket," soliloquized Starbuck suddenly, heedless of Stubb's +question. "The gale that now hammers at us to stave us, we can turn +it into a fair wind that will drive us towards home. Yonder, to +windward, all is blackness of doom; but to leeward, homeward--I see +it lightens up there; but not with the lightning." + +At that moment in one of the intervals of profound darkness, +following the flashes, a voice was heard at his side; and almost at +the same instant a volley of thunder peals rolled overhead. + +"Who's there?" + +"Old Thunder!" said Ahab, groping his way along the bulwarks to his +pivot-hole; but suddenly finding his path made plain to him by +elbowed lances of fire. + +Now, as the lightning rod to a spire on shore is intended to carry +off the perilous fluid into the soil; so the kindred rod which at sea +some ships carry to each mast, is intended to conduct it into the +water. But as this conductor must descend to considerable depth, +that its end may avoid all contact with the hull; and as moreover, if +kept constantly towing there, it would be liable to many mishaps, +besides interfering not a little with some of the rigging, and more +or less impeding the vessel's way in the water; because of all this, +the lower parts of a ship's lightning-rods are not always overboard; +but are generally made in long slender links, so as to be the more +readily hauled up into the chains outside, or thrown down into the +sea, as occasion may require. + +"The rods! the rods!" cried Starbuck to the crew, suddenly admonished +to vigilance by the vivid lightning that had just been darting +flambeaux, to light Ahab to his post. "Are they overboard? drop them +over, fore and aft. Quick!" + +"Avast!" cried Ahab; "let's have fair play here, though we be the +weaker side. Yet I'll contribute to raise rods on the Himmalehs and +Andes, that all the world may be secured; but out on privileges! Let +them be, sir." + +"Look aloft!" cried Starbuck. "The corpusants! the corpusants! + +All the yard-arms were tipped with a pallid fire; and touched at each +tri-pointed lightning-rod-end with three tapering white flames, each +of the three tall masts was silently burning in that sulphurous air, +like three gigantic wax tapers before an altar. + +"Blast the boat! let it go!" cried Stubb at this instant, as a +swashing sea heaved up under his own little craft, so that its +gunwale violently jammed his hand, as he was passing a lashing. +"Blast it!"--but slipping backward on the deck, his uplifted eyes +caught the flames; and immediately shifting his tone he cried--"The +corpusants have mercy on us all!" + +To sailors, oaths are household words; they will swear in the trance +of the calm, and in the teeth of the tempest; they will imprecate +curses from the topsail-yard-arms, when most they teeter over to a +seething sea; but in all my voyagings, seldom have I heard a common +oath when God's burning finger has been laid on the ship; when His +"Mene, Mene, Tekel Upharsin" has been woven into the shrouds and the +cordage. + +While this pallidness was burning aloft, few words were heard from +the enchanted crew; who in one thick cluster stood on the forecastle, +all their eyes gleaming in that pale phosphorescence, like a far away +constellation of stars. Relieved against the ghostly light, the +gigantic jet negro, Daggoo, loomed up to thrice his real stature, and +seemed the black cloud from which the thunder had come. The parted +mouth of Tashtego revealed his shark-white teeth, which strangely +gleamed as if they too had been tipped by corpusants; while lit up by +the preternatural light, Queequeg's tattooing burned like Satanic +blue flames on his body. + +The tableau all waned at last with the pallidness aloft; and once +more the Pequod and every soul on her decks were wrapped in a pall. +A moment or two passed, when Starbuck, going forward, pushed against +some one. It was Stubb. "What thinkest thou now, man; I heard thy +cry; it was not the same in the song." + +"No, no, it wasn't; I said the corpusants have mercy on us all; and I +hope they will, still. But do they only have mercy on long +faces?--have they no bowels for a laugh? And look ye, Mr. +Starbuck--but it's too dark to look. Hear me, then: I take that +mast-head flame we saw for a sign of good luck; for those masts are +rooted in a hold that is going to be chock a' block with sperm-oil, +d'ye see; and so, all that sperm will work up into the masts, like +sap in a tree. Yes, our three masts will yet be as three spermaceti +candles--that's the good promise we saw." + +At that moment Starbuck caught sight of Stubb's face slowly beginning +to glimmer into sight. Glancing upwards, he cried: "See! see!" and +once more the high tapering flames were beheld with what seemed +redoubled supernaturalness in their pallor. + +"The corpusants have mercy on us all," cried Stubb, again. + +At the base of the mainmast, full beneath the doubloon and the +flame, the Parsee was kneeling in Ahab's front, but with his head +bowed away from him; while near by, from the arched and overhanging +rigging, where they had just been engaged securing a spar, a number +of the seamen, arrested by the glare, now cohered together, and hung +pendulous, like a knot of numbed wasps from a drooping, orchard twig. +In various enchanted attitudes, like the standing, or stepping, or +running skeletons in Herculaneum, others remained rooted to the deck; +but all their eyes upcast. + +"Aye, aye, men!" cried Ahab. "Look up at it; mark it well; the white +flame but lights the way to the White Whale! Hand me those mainmast +links there; I would fain feel this pulse, and let mine beat against +it; blood against fire! So." + +Then turning--the last link held fast in his left hand, he put his +foot upon the Parsee; and with fixed upward eye, and high-flung right +arm, he stood erect before the lofty tri-pointed trinity of flames. + +"Oh! thou clear spirit of clear fire, whom on these seas I as Persian +once did worship, till in the sacramental act so burned by thee, that +to this hour I bear the scar; I now know thee, thou clear spirit, and +I now know that thy right worship is defiance. To neither love nor +reverence wilt thou be kind; and e'en for hate thou canst but kill; +and all are killed. No fearless fool now fronts thee. I own thy +speechless, placeless power; but to the last gasp of my earthquake +life will dispute its unconditional, unintegral mastery in me. In the +midst of the personified impersonal, a personality stands here. +Though but a point at best; whencesoe'er I came; wheresoe'er I go; +yet while I earthly live, the queenly personality lives in me, and +feels her royal rights. But war is pain, and hate is woe. Come in +thy lowest form of love, and I will kneel and kiss thee; but at thy +highest, come as mere supernal power; and though thou launchest +navies of full-freighted worlds, there's that in here that still +remains indifferent. Oh, thou clear spirit, of thy fire thou madest +me, and like a true child of fire, I breathe it back to thee." + +[SUDDEN, REPEATED FLASHES OF LIGHTNING; THE NINE FLAMES LEAP +LENGTHWISE TO THRICE THEIR PREVIOUS HEIGHT; AHAB, WITH THE REST, +CLOSES HIS EYES, HIS RIGHT HAND PRESSED HARD UPON THEM.] + +"I own thy speechless, placeless power; said I not so? Nor was it +wrung from me; nor do I now drop these links. Thou canst blind; but +I can then grope. Thou canst consume; but I can then be ashes. Take +the homage of these poor eyes, and shutter-hands. I would not take +it. The lightning flashes through my skull; mine eye-balls ache and +ache; my whole beaten brain seems as beheaded, and rolling on some +stunning ground. Oh, oh! Yet blindfold, yet will I talk to thee. +Light though thou be, thou leapest out of darkness; but I am darkness +leaping out of light, leaping out of thee! The javelins cease; open +eyes; see, or not? There burn the flames! Oh, thou magnanimous! now +I do glory in my genealogy. But thou art but my fiery father; my +sweet mother, I know not. Oh, cruel! what hast thou done with her? +There lies my puzzle; but thine is greater. Thou knowest not how +came ye, hence callest thyself unbegotten; certainly knowest not thy +beginning, hence callest thyself unbegun. I know that of me, which +thou knowest not of thyself, oh, thou omnipotent. There is some +unsuffusing thing beyond thee, thou clear spirit, to whom all thy +eternity is but time, all thy creativeness mechanical. Through thee, +thy flaming self, my scorched eyes do dimly see it. Oh, thou +foundling fire, thou hermit immemorial, thou too hast thy +incommunicable riddle, thy unparticipated grief. Here again with +haughty agony, I read my sire. Leap! leap up, and lick the sky! I +leap with thee; I burn with thee; would fain be welded with thee; +defyingly I worship thee!" + +"The boat! the boat!" cried Starbuck, "look at thy boat, old man!" + +Ahab's harpoon, the one forged at Perth's fire, remained firmly +lashed in its conspicuous crotch, so that it projected beyond his +whale-boat's bow; but the sea that had stove its bottom had caused +the loose leather sheath to drop off; and from the keen steel barb +there now came a levelled flame of pale, forked fire. As the silent +harpoon burned there like a serpent's tongue, Starbuck grasped Ahab +by the arm--"God, God is against thee, old man; forbear! 'tis an +ill voyage! ill begun, ill continued; let me square the yards, while +we may, old man, and make a fair wind of it homewards, to go on a +better voyage than this." + +Overhearing Starbuck, the panic-stricken crew instantly ran to the +braces--though not a sail was left aloft. For the moment all the +aghast mate's thoughts seemed theirs; they raised a half mutinous +cry. But dashing the rattling lightning links to the deck, and +snatching the burning harpoon, Ahab waved it like a torch among them; +swearing to transfix with it the first sailor that but cast loose a +rope's end. Petrified by his aspect, and still more shrinking from +the fiery dart that he held, the men fell back in dismay, and Ahab +again spoke:-- + +"All your oaths to hunt the White Whale are as binding as mine; and +heart, soul, and body, lungs and life, old Ahab is bound. And that +ye may know to what tune this heart beats; look ye here; thus I blow +out the last fear!" And with one blast of his breath he extinguished +the flame. + +As in the hurricane that sweeps the plain, men fly the neighborhood +of some lone, gigantic elm, whose very height and strength but render +it so much the more unsafe, because so much the more a mark for +thunderbolts; so at those last words of Ahab's many of the mariners +did run from him in a terror of dismay. + + + +CHAPTER 120 + +The Deck Towards the End of the First Night Watch. + +AHAB STANDING BY THE HELM. STARBUCK APPROACHING HIM. + + +We must send down the main-top-sail yard, sir. The band is working +loose and the lee lift is half-stranded. Shall I strike it, sir?" + +"Strike nothing; lash it. If I had sky-sail poles, I'd sway them up +now." + +"Sir!--in God's name!--sir?" + +"Well." + +"The anchors are working, sir. Shall I get them inboard?" + +"Strike nothing, and stir nothing, but lash everything. The wind +rises, but it has not got up to my table-lands yet. Quick, and see +to it.--By masts and keels! he takes me for the hunch-backed skipper +of some coasting smack. Send down my main-top-sail yard! Ho, +gluepots! Loftiest trucks were made for wildest winds, and this +brain-truck of mine now sails amid the cloud-scud. Shall I strike +that? Oh, none but cowards send down their brain-trucks in tempest +time. What a hooroosh aloft there! I would e'en take it for +sublime, did I not know that the colic is a noisy malady. Oh, take +medicine, take medicine!" + + + +CHAPTER 121 + +Midnight.--The Forecastle Bulwarks. + + +STUBB AND FLASK MOUNTED ON THEM, AND PASSING ADDITIONAL LASHINGS OVER +THE ANCHORS THERE HANGING. + + +No, Stubb; you may pound that knot there as much as you please, but +you will never pound into me what you were just now saying. And how +long ago is it since you said the very contrary? Didn't you once say +that whatever ship Ahab sails in, that ship should pay something +extra on its insurance policy, just as though it were loaded with +powder barrels aft and boxes of lucifers forward? Stop, now; didn't +you say so?" + +"Well, suppose I did? What then? I've part changed my flesh since +that time, why not my mind? Besides, supposing we ARE loaded with +powder barrels aft and lucifers forward; how the devil could the +lucifers get afire in this drenching spray here? Why, my little man, +you have pretty red hair, but you couldn't get afire now. Shake +yourself; you're Aquarius, or the water-bearer, Flask; might fill +pitchers at your coat collar. Don't you see, then, that for these +extra risks the Marine Insurance companies have extra guarantees? +Here are hydrants, Flask. But hark, again, and I'll answer ye the +other thing. First take your leg off from the crown of the anchor +here, though, so I can pass the rope; now listen. What's the mighty +difference between holding a mast's lightning-rod in the storm, and +standing close by a mast that hasn't got any lightning-rod at all in +a storm? Don't you see, you timber-head, that no harm can come to +the holder of the rod, unless the mast is first struck? What are you +talking about, then? Not one ship in a hundred carries rods, and +Ahab,--aye, man, and all of us,--were in no more danger then, in my +poor opinion, than all the crews in ten thousand ships now sailing +the seas. Why, you King-Post, you, I suppose you would have every +man in the world go about with a small lightning-rod running up the +corner of his hat, like a militia officer's skewered feather, and +trailing behind like his sash. Why don't ye be sensible, Flask? it's +easy to be sensible; why don't ye, then? any man with half an eye can +be sensible." + +"I don't know that, Stubb. You sometimes find it rather hard." + +"Yes, when a fellow's soaked through, it's hard to be sensible, +that's a fact. And I am about drenched with this spray. Never mind; +catch the turn there, and pass it. Seems to me we are lashing down +these anchors now as if they were never going to be used again. +Tying these two anchors here, Flask, seems like tying a man's hands +behind him. And what big generous hands they are, to be sure. These +are your iron fists, hey? What a hold they have, too! I wonder, +Flask, whether the world is anchored anywhere; if she is, she swings +with an uncommon long cable, though. There, hammer that knot down, +and we've done. So; next to touching land, lighting on deck is the +most satisfactory. I say, just wring out my jacket skirts, will ye? +Thank ye. They laugh at long-togs so, Flask; but seems to me, a +Long tailed coat ought always to be worn in all storms afloat. The +tails tapering down that way, serve to carry off the water, d'ye see. +Same with cocked hats; the cocks form gable-end eave-troughs, Flask. +No more monkey-jackets and tarpaulins for me; I must mount a +swallow-tail, and drive down a beaver; so. Halloa! whew! there goes +my tarpaulin overboard; Lord, Lord, that the winds that come from +heaven should be so unmannerly! This is a nasty night, lad." + + + +CHAPTER 122 + +Midnight Aloft.--Thunder and Lightning. + + +THE MAIN-TOP-SAIL YARD.--TASHTEGO PASSING NEW LASHINGS AROUND IT. + + +"Um, um, um. Stop that thunder! Plenty too much thunder up here. +What's the use of thunder? Um, um, um. We don't want thunder; we +want rum; give us a glass of rum. Um, um, um!" + + + +CHAPTER 123 + +The Musket. + + +During the most violent shocks of the Typhoon, the man at the +Pequod's jaw-bone tiller had several times been reelingly hurled to +the deck by its spasmodic motions, even though preventer tackles had +been attached to it--for they were slack--because some play to the +tiller was indispensable. + +In a severe gale like this, while the ship is but a tossed +shuttlecock to the blast, it is by no means uncommon to see the +needles in the compasses, at intervals, go round and round. It was +thus with the Pequod's; at almost every shock the helmsman had not +failed to notice the whirling velocity with which they revolved upon +the cards; it is a sight that hardly anyone can behold without some +sort of unwonted emotion. + +Some hours after midnight, the Typhoon abated so much, that through +the strenuous exertions of Starbuck and Stubb--one engaged forward +and the other aft--the shivered remnants of the jib and fore and +main-top-sails were cut adrift from the spars, and went eddying away +to leeward, like the feathers of an albatross, which sometimes are +cast to the winds when that storm-tossed bird is on the wing. + +The three corresponding new sails were now bent and reefed, and a +storm-trysail was set further aft; so that the ship soon went through +the water with some precision again; and the course--for the present, +East-south-east--which he was to steer, if practicable, was once more +given to the helmsman. For during the violence of the gale, he had +only steered according to its vicissitudes. But as he was now +bringing the ship as near her course as possible, watching the +compass meanwhile, lo! a good sign! the wind seemed coming round +astern; aye, the foul breeze became fair! + +Instantly the yards were squared, to the lively song of "HO! THE FAIR +WIND! OH-YE-HO, CHEERLY MEN!" the crew singing for joy, that so +promising an event should so soon have falsified the evil portents +preceding it. + +In compliance with the standing order of his commander--to report +immediately, and at any one of the twenty-four hours, any decided +change in the affairs of the deck,--Starbuck had no sooner trimmed +the yards to the breeze--however reluctantly and gloomily,--than he +mechanically went below to apprise Captain Ahab of the circumstance. + +Ere knocking at his state-room, he involuntarily paused before it a +moment. The cabin lamp--taking long swings this way and that--was +burning fitfully, and casting fitful shadows upon the old man's +bolted door,--a thin one, with fixed blinds inserted, in place of +upper panels. The isolated subterraneousness of the cabin made a +certain humming silence to reign there, though it was hooped round by +all the roar of the elements. The loaded muskets in the rack were +shiningly revealed, as they stood upright against the forward +bulkhead. Starbuck was an honest, upright man; but out of Starbuck's +heart, at that instant when he saw the muskets, there strangely +evolved an evil thought; but so blent with its neutral or good +accompaniments that for the instant he hardly knew it for itself. + +"He would have shot me once," he murmured, "yes, there's the very +musket that he pointed at me;--that one with the studded stock; let +me touch it--lift it. Strange, that I, who have handled so many +deadly lances, strange, that I should shake so now. Loaded? I must +see. Aye, aye; and powder in the pan;--that's not good. Best spill +it?--wait. I'll cure myself of this. I'll hold the musket boldly +while I think.--I come to report a fair wind to him. But how fair? +Fair for death and doom,--THAT'S fair for Moby Dick. It's a fair +wind that's only fair for that accursed fish.--The very tube he +pointed at me!--the very one; THIS one--I hold it here; he would have +killed me with the very thing I handle now.--Aye and he would fain +kill all his crew. Does he not say he will not strike his spars to +any gale? Has he not dashed his heavenly quadrant? and in these same +perilous seas, gropes he not his way by mere dead reckoning of the +error-abounding log? and in this very Typhoon, did he not swear that +he would have no lightning-rods? But shall this crazed old man be +tamely suffered to drag a whole ship's company down to doom with +him?--Yes, it would make him the wilful murderer of thirty men and +more, if this ship come to any deadly harm; and come to deadly harm, +my soul swears this ship will, if Ahab have his way. If, then, he +were this instant--put aside, that crime would not be his. Ha! is he +muttering in his sleep? Yes, just there,--in there, he's sleeping. +Sleeping? aye, but still alive, and soon awake again. I can't +withstand thee, then, old man. Not reasoning; not remonstrance; not +entreaty wilt thou hearken to; all this thou scornest. Flat +obedience to thy own flat commands, this is all thou breathest. Aye, +and say'st the men have vow'd thy vow; say'st all of us are Ahabs. +Great God forbid!--But is there no other way? no lawful way?--Make +him a prisoner to be taken home? What! hope to wrest this old man's +living power from his own living hands? Only a fool would try it. +Say he were pinioned even; knotted all over with ropes and hawsers; +chained down to ring-bolts on this cabin floor; he would be more +hideous than a caged tiger, then. I could not endure the sight; +could not possibly fly his howlings; all comfort, sleep itself, +inestimable reason would leave me on the long intolerable voyage. +What, then, remains? The land is hundreds of leagues away, and +locked Japan the nearest. I stand alone here upon an open sea, with +two oceans and a whole continent between me and law.--Aye, aye, 'tis +so.--Is heaven a murderer when its lightning strikes a would-be +murderer in his bed, tindering sheets and skin together?--And would I +be a murderer, then, if"--and slowly, stealthily, and half sideways +looking, he placed the loaded musket's end against the door. + +"On this level, Ahab's hammock swings within; his head this way. A +touch, and Starbuck may survive to hug his wife and child again.--Oh +Mary! Mary!--boy! boy! boy!--But if I wake thee not to death, old +man, who can tell to what unsounded deeps Starbuck's body this day +week may sink, with all the crew! Great God, where art Thou? Shall +I? shall I?--The wind has gone down and shifted, sir; the fore and +main topsails are reefed and set; she heads her course." + +"Stern all! Oh Moby Dick, I clutch thy heart at last!" + +Such were the sounds that now came hurtling from out the old man's +tormented sleep, as if Starbuck's voice had caused the long dumb +dream to speak. + +The yet levelled musket shook like a drunkard's arm against the +panel; Starbuck seemed wrestling with an angel; but turning from the +door, he placed the death-tube in its rack, and left the place. + +"He's too sound asleep, Mr. Stubb; go thou down, and wake him, and +tell him. I must see to the deck here. Thou know'st what to say." + + + +CHAPTER 124 + +The Needle. + + +Next morning the not-yet-subsided sea rolled in long slow billows of +mighty bulk, and striving in the Pequod's gurgling track, pushed her +on like giants' palms outspread. The strong, unstaggering breeze +abounded so, that sky and air seemed vast outbellying sails; the +whole world boomed before the wind. Muffled in the full morning +light, the invisible sun was only known by the spread intensity of +his place; where his bayonet rays moved on in stacks. Emblazonings, +as of crowned Babylonian kings and queens, reigned over everything. +The sea was as a crucible of molten gold, that bubblingly leaps with +light and heat. + +Long maintaining an enchanted silence, Ahab stood apart; and every +time the tetering ship loweringly pitched down her bowsprit, he +turned to eye the bright sun's rays produced ahead; and when she +profoundly settled by the stern, he turned behind, and saw the sun's +rearward place, and how the same yellow rays were blending with his +undeviating wake. + +"Ha, ha, my ship! thou mightest well be taken now for the sea-chariot +of the sun. Ho, ho! all ye nations before my prow, I bring the sun +to ye! Yoke on the further billows; hallo! a tandem, I drive the +sea!" + +But suddenly reined back by some counter thought, he hurried towards +the helm, huskily demanding how the ship was heading. + +"East-sou-east, sir," said the frightened steersman. + +"Thou liest!" smiting him with his clenched fist. "Heading East at +this hour in the morning, and the sun astern?" + +Upon this every soul was confounded; for the phenomenon just then +observed by Ahab had unaccountably escaped every one else; but its +very blinding palpableness must have been the cause. + +Thrusting his head half way into the binnacle, Ahab caught one +glimpse of the compasses; his uplifted arm slowly fell; for a moment +he almost seemed to stagger. Standing behind him Starbuck looked, +and lo! the two compasses pointed East, and the Pequod was as +infallibly going West. + +But ere the first wild alarm could get out abroad among the crew, the +old man with a rigid laugh exclaimed, "I have it! It has happened +before. Mr. Starbuck, last night's thunder turned our +compasses--that's all. Thou hast before now heard of such a thing, I +take it." + +"Aye; but never before has it happened to me, sir," said the pale +mate, gloomily. + +Here, it must needs be said, that accidents like this have in more +than one case occurred to ships in violent storms. The magnetic +energy, as developed in the mariner's needle, is, as all know, +essentially one with the electricity beheld in heaven; hence it is +not to be much marvelled at, that such things should be. Instances +where the lightning has actually struck the vessel, so as to smite +down some of the spars and rigging, the effect upon the needle has at +times been still more fatal; all its loadstone virtue being +annihilated, so that the before magnetic steel was of no more use +than an old wife's knitting needle. But in either case, the needle +never again, of itself, recovers the original virtue thus marred or +lost; and if the binnacle compasses be affected, the same fate +reaches all the others that may be in the ship; even were the +lowermost one inserted into the kelson. + +Deliberately standing before the binnacle, and eyeing the +transpointed compasses, the old man, with the sharp of his extended +hand, now took the precise bearing of the sun, and satisfied that the +needles were exactly inverted, shouted out his orders for the ship's +course to be changed accordingly. The yards were hard up; and once +more the Pequod thrust her undaunted bows into the opposing wind, for +the supposed fair one had only been juggling her. + +Meanwhile, whatever were his own secret thoughts, Starbuck said +nothing, but quietly he issued all requisite orders; while Stubb and +Flask--who in some small degree seemed then to be sharing his +feelings--likewise unmurmuringly acquiesced. As for the men, though +some of them lowly rumbled, their fear of Ahab was greater than their +fear of Fate. But as ever before, the pagan harpooneers remained +almost wholly unimpressed; or if impressed, it was only with a +certain magnetism shot into their congenial hearts from inflexible +Ahab's. + +For a space the old man walked the deck in rolling reveries. But +chancing to slip with his ivory heel, he saw the crushed copper +sight-tubes of the quadrant he had the day before dashed to the deck. + +"Thou poor, proud heaven-gazer and sun's pilot! yesterday I wrecked +thee, and to-day the compasses would fain have wrecked me. So, so. +But Ahab is lord over the level loadstone yet. Mr. Starbuck--a lance +without a pole; a top-maul, and the smallest of the sail-maker's +needles. Quick!" + +Accessory, perhaps, to the impulse dictating the thing he was now +about to do, were certain prudential motives, whose object might have +been to revive the spirits of his crew by a stroke of his subtile +skill, in a matter so wondrous as that of the inverted compasses. +Besides, the old man well knew that to steer by transpointed needles, +though clumsily practicable, was not a thing to be passed over by +superstitious sailors, without some shudderings and evil portents. + +"Men," said he, steadily turning upon the crew, as the mate handed +him the things he had demanded, "my men, the thunder turned old +Ahab's needles; but out of this bit of steel Ahab can make one of his +own, that will point as true as any." + +Abashed glances of servile wonder were exchanged by the sailors, as +this was said; and with fascinated eyes they awaited whatever magic +might follow. But Starbuck looked away. + +With a blow from the top-maul Ahab knocked off the steel head of the +lance, and then handing to the mate the long iron rod remaining, bade +him hold it upright, without its touching the deck. Then, with the +maul, after repeatedly smiting the upper end of this iron rod, he +placed the blunted needle endwise on the top of it, and less strongly +hammered that, several times, the mate still holding the rod as +before. Then going through some small strange motions with +it--whether indispensable to the magnetizing of the steel, or merely +intended to augment the awe of the crew, is uncertain--he called for +linen thread; and moving to the binnacle, slipped out the two +reversed needles there, and horizontally suspended the sail-needle by +its middle, over one of the compass-cards. At first, the steel went +round and round, quivering and vibrating at either end; but at last +it settled to its place, when Ahab, who had been intently watching +for this result, stepped frankly back from the binnacle, and pointing +his stretched arm towards it, exclaimed,--"Look ye, for yourselves, +if Ahab be not lord of the level loadstone! The sun is East, and +that compass swears it!" + +One after another they peered in, for nothing but their own eyes +could persuade such ignorance as theirs, and one after another they +slunk away. + +In his fiery eyes of scorn and triumph, you then saw Ahab in all his +fatal pride. + + + +CHAPTER 125 + +The Log and Line. + + +While now the fated Pequod had been so long afloat this voyage, the +log and line had but very seldom been in use. Owing to a confident +reliance upon other means of determining the vessel's place, some +merchantmen, and many whalemen, especially when cruising, wholly +neglect to heave the log; though at the same time, and frequently +more for form's sake than anything else, regularly putting down upon +the customary slate the course steered by the ship, as well as the +presumed average rate of progression every hour. It had been thus +with the Pequod. The wooden reel and angular log attached hung, long +untouched, just beneath the railing of the after bulwarks. Rains and +spray had damped it; sun and wind had warped it; all the elements +had combined to rot a thing that hung so idly. But heedless of all +this, his mood seized Ahab, as he happened to glance upon the reel, +not many hours after the magnet scene, and he remembered how his +quadrant was no more, and recalled his frantic oath about the level +log and line. The ship was sailing plungingly; astern the billows +rolled in riots. + +"Forward, there! Heave the log!" + +Two seamen came. The golden-hued Tahitian and the grizzly Manxman. +"Take the reel, one of ye, I'll heave." + +They went towards the extreme stern, on the ship's lee side, where +the deck, with the oblique energy of the wind, was now almost dipping +into the creamy, sidelong-rushing sea. + +The Manxman took the reel, and holding it high up, by the projecting +handle-ends of the spindle, round which the spool of line revolved, +so stood with the angular log hanging downwards, till Ahab advanced +to him. + +Ahab stood before him, and was lightly unwinding some thirty or forty +turns to form a preliminary hand-coil to toss overboard, when the old +Manxman, who was intently eyeing both him and the line, made bold to +speak. + +"Sir, I mistrust it; this line looks far gone, long heat and wet have +spoiled it." + +"'Twill hold, old gentleman. Long heat and wet, have they spoiled +thee? Thou seem'st to hold. Or, truer perhaps, life holds thee; +not thou it." + +"I hold the spool, sir. But just as my captain says. With these +grey hairs of mine 'tis not worth while disputing, 'specially with a +superior, who'll ne'er confess." + +"What's that? There now's a patched professor in Queen Nature's +granite-founded College; but methinks he's too subservient. Where +wert thou born?" + +"In the little rocky Isle of Man, sir." + +"Excellent! Thou'st hit the world by that." + +"I know not, sir, but I was born there." + +"In the Isle of Man, hey? Well, the other way, it's good. Here's a +man from Man; a man born in once independent Man, and now unmanned of +Man; which is sucked in--by what? Up with the reel! The dead, blind +wall butts all inquiring heads at last. Up with it! So." + +The log was heaved. The loose coils rapidly straightened out in a +long dragging line astern, and then, instantly, the reel began to +whirl. In turn, jerkingly raised and lowered by the rolling billows, +the towing resistance of the log caused the old reelman to stagger +strangely. + +"Hold hard!" + +Snap! the overstrained line sagged down in one long festoon; the +tugging log was gone. + +"I crush the quadrant, the thunder turns the needles, and now the mad +sea parts the log-line. But Ahab can mend all. Haul in here, +Tahitian; reel up, Manxman. And look ye, let the carpenter make +another log, and mend thou the line. See to it." + +"There he goes now; to him nothing's happened; but to me, the skewer +seems loosening out of the middle of the world. Haul in, haul in, +Tahitian! These lines run whole, and whirling out: come in broken, +and dragging slow. Ha, Pip? come to help; eh, Pip?" + +"Pip? whom call ye Pip? Pip jumped from the whale-boat. Pip's +missing. Let's see now if ye haven't fished him up here, fisherman. +It drags hard; I guess he's holding on. Jerk him, Tahiti! Jerk him +off; we haul in no cowards here. Ho! there's his arm just breaking +water. A hatchet! a hatchet! cut it off--we haul in no cowards here. +Captain Ahab! sir, sir! here's Pip, trying to get on board again." + +"Peace, thou crazy loon," cried the Manxman, seizing him by the arm. +"Away from the quarter-deck!" + +"The greater idiot ever scolds the lesser," muttered Ahab, advancing. +"Hands off from that holiness! Where sayest thou Pip was, boy? + +"Astern there, sir, astern! Lo! lo!" + +"And who art thou, boy? I see not my reflection in the vacant pupils +of thy eyes. Oh God! that man should be a thing for immortal souls +to sieve through! Who art thou, boy?" + +"Bell-boy, sir; ship's-crier; ding, dong, ding! Pip! Pip! Pip! One +hundred pounds of clay reward for Pip; five feet high--looks +cowardly--quickest known by that! Ding, dong, ding! Who's seen Pip +the coward?" + +"There can be no hearts above the snow-line. Oh, ye frozen heavens! +look down here. Ye did beget this luckless child, and have abandoned +him, ye creative libertines. Here, boy; Ahab's cabin shall be Pip's +home henceforth, while Ahab lives. Thou touchest my inmost centre, +boy; thou art tied to me by cords woven of my heart-strings. Come, +let's down." + +"What's this? here's velvet shark-skin," intently gazing at Ahab's +hand, and feeling it. "Ah, now, had poor Pip but felt so kind a +thing as this, perhaps he had ne'er been lost! This seems to me, +sir, as a man-rope; something that weak souls may hold by. Oh, sir, +let old Perth now come and rivet these two hands together; the black +one with the white, for I will not let this go." + +"Oh, boy, nor will I thee, unless I should thereby drag thee to worse +horrors than are here. Come, then, to my cabin. Lo! ye believers in +gods all goodness, and in man all ill, lo you! see the omniscient +gods oblivious of suffering man; and man, though idiotic, and knowing +not what he does, yet full of the sweet things of love and gratitude. +Come! I feel prouder leading thee by thy black hand, than though I +grasped an Emperor's!" + +"There go two daft ones now," muttered the old Manxman. "One daft +with strength, the other daft with weakness. But here's the end of +the rotten line--all dripping, too. Mend it, eh? I think we had +best have a new line altogether. I'll see Mr. Stubb about it." + + + +CHAPTER 126 + +The Life-Buoy. + + +Steering now south-eastward by Ahab's levelled steel, and her +progress solely determined by Ahab's level log and line; the Pequod +held on her path towards the Equator. Making so long a passage +through such unfrequented waters, descrying no ships, and ere long, +sideways impelled by unvarying trade winds, over waves monotonously +mild; all these seemed the strange calm things preluding some riotous +and desperate scene. + +At last, when the ship drew near to the outskirts, as it were, of the +Equatorial fishing-ground, and in the deep darkness that goes before +the dawn, was sailing by a cluster of rocky islets; the watch--then +headed by Flask--was startled by a cry so plaintively wild and +unearthly--like half-articulated wailings of the ghosts of all +Herod's murdered Innocents--that one and all, they started from their +reveries, and for the space of some moments stood, or sat, or leaned +all transfixedly listening, like the carved Roman slave, while that +wild cry remained within hearing. The Christian or civilized part of +the crew said it was mermaids, and shuddered; but the pagan +harpooneers remained unappalled. Yet the grey Manxman--the oldest +mariner of all--declared that the wild thrilling sounds that were +heard, were the voices of newly drowned men in the sea. + +Below in his hammock, Ahab did not hear of this till grey dawn, when +he came to the deck; it was then recounted to him by Flask, not +unaccompanied with hinted dark meanings. He hollowly laughed, and +thus explained the wonder. + +Those rocky islands the ship had passed were the resort of great +numbers of seals, and some young seals that had lost their dams, or +some dams that had lost their cubs, must have risen nigh the ship and +kept company with her, crying and sobbing with their human sort of +wail. But this only the more affected some of them, because most +mariners cherish a very superstitious feeling about seals, arising +not only from their peculiar tones when in distress, but also from +the human look of their round heads and semi-intelligent faces, seen +peeringly uprising from the water alongside. In the sea, under +certain circumstances, seals have more than once been mistaken for +men. + +But the bodings of the crew were destined to receive a most plausible +confirmation in the fate of one of their number that morning. At +sun-rise this man went from his hammock to his mast-head at the fore; +and whether it was that he was not yet half waked from his sleep (for +sailors sometimes go aloft in a transition state), whether it was +thus with the man, there is now no telling; but, be that as it may, +he had not been long at his perch, when a cry was heard--a cry and a +rushing--and looking up, they saw a falling phantom in the air; and +looking down, a little tossed heap of white bubbles in the blue of +the sea. + +The life-buoy--a long slender cask--was dropped from the stern, where +it always hung obedient to a cunning spring; but no hand rose to +seize it, and the sun having long beat upon this cask it had +shrunken, so that it slowly filled, and that parched wood also +filled at its every pore; and the studded iron-bound cask followed +the sailor to the bottom, as if to yield him his pillow, though in +sooth but a hard one. + +And thus the first man of the Pequod that mounted the mast to look +out for the White Whale, on the White Whale's own peculiar ground; +that man was swallowed up in the deep. But few, perhaps, thought of +that at the time. Indeed, in some sort, they were not grieved at +this event, at least as a portent; for they regarded it, not as a +foreshadowing of evil in the future, but as the fulfilment of an +evil already presaged. They declared that now they knew the reason +of those wild shrieks they had heard the night before. But again the +old Manxman said nay. + +The lost life-buoy was now to be replaced; Starbuck was directed to +see to it; but as no cask of sufficient lightness could be found, and +as in the feverish eagerness of what seemed the approaching crisis of +the voyage, all hands were impatient of any toil but what was +directly connected with its final end, whatever that might prove to +be; therefore, they were going to leave the ship's stern unprovided +with a buoy, when by certain strange signs and inuendoes Queequeg +hinted a hint concerning his coffin. + +"A life-buoy of a coffin!" cried Starbuck, starting. + +"Rather queer, that, I should say," said Stubb. + +"It will make a good enough one," said Flask, "the carpenter here can +arrange it easily." + +"Bring it up; there's nothing else for it," said Starbuck, after a +melancholy pause. "Rig it, carpenter; do not look at me so--the +coffin, I mean. Dost thou hear me? Rig it." + +"And shall I nail down the lid, sir?" moving his hand as with a +hammer. + +"Aye." + +"And shall I caulk the seams, sir?" moving his hand as with a +caulking-iron. + +"Aye." + +"And shall I then pay over the same with pitch, sir?" moving his hand +as with a pitch-pot. + +"Away! what possesses thee to this? Make a life-buoy of the coffin, +and no more.--Mr. Stubb, Mr. Flask, come forward with me." + +"He goes off in a huff. The whole he can endure; at the parts he +baulks. Now I don't like this. I make a leg for Captain Ahab, and +he wears it like a gentleman; but I make a bandbox for Queequeg, and +he won't put his head into it. Are all my pains to go for nothing +with that coffin? And now I'm ordered to make a life-buoy of it. +It's like turning an old coat; going to bring the flesh on the other +side now. I don't like this cobbling sort of business--I don't like +it at all; it's undignified; it's not my place. Let tinkers' brats +do tinkerings; we are their betters. I like to take in hand none but +clean, virgin, fair-and-square mathematical jobs, something that +regularly begins at the beginning, and is at the middle when midway, +and comes to an end at the conclusion; not a cobbler's job, that's at +an end in the middle, and at the beginning at the end. It's the old +woman's tricks to be giving cobbling jobs. Lord! what an affection +all old women have for tinkers. I know an old woman of sixty-five +who ran away with a bald-headed young tinker once. And that's the +reason I never would work for lonely widow old women ashore, when I +kept my job-shop in the Vineyard; they might have taken it into their +lonely old heads to run off with me. But heigh-ho! there are no caps +at sea but snow-caps. Let me see. Nail down the lid; caulk the +seams; pay over the same with pitch; batten them down tight, and hang +it with the snap-spring over the ship's stern. Were ever such things +done before with a coffin? Some superstitious old carpenters, now, +would be tied up in the rigging, ere they would do the job. But I'm +made of knotty Aroostook hemlock; I don't budge. Cruppered with a +coffin! Sailing about with a grave-yard tray! But never mind. We +workers in woods make bridal-bedsteads and card-tables, as well as +coffins and hearses. We work by the month, or by the job, or by the +profit; not for us to ask the why and wherefore of our work, unless +it be too confounded cobbling, and then we stash it if we can. Hem! +I'll do the job, now, tenderly. I'll have me--let's see--how many in +the ship's company, all told? But I've forgotten. Any way, I'll +have me thirty separate, Turk's-headed life-lines, each three feet +long hanging all round to the coffin. Then, if the hull go down, +there'll be thirty lively fellows all fighting for one coffin, a +sight not seen very often beneath the sun! Come hammer, +caulking-iron, pitch-pot, and marling-spike! Let's to it." + + + +CHAPTER 127 + +The Deck. + + +THE COFFIN LAID UPON TWO LINE-TUBS, BETWEEN THE VICE-BENCH AND THE +OPEN HATCHWAY; THE CARPENTER CAULKING ITS SEAMS; THE STRING OF +TWISTED OAKUM SLOWLY UNWINDING FROM A LARGE ROLL OF IT PLACED IN THE +BOSOM OF HIS FROCK.--AHAB COMES SLOWLY FROM THE CABIN-GANGWAY, AND +HEARS PIP FOLLOWING HIM. + + +Back, lad; I will be with ye again presently. He goes! Not this +hand complies with my humor more genially than that boy.--Middle +aisle of a church! What's here?" + +"Life-buoy, sir. Mr. Starbuck's orders. Oh, look, sir! Beware the +hatchway!" + +"Thank ye, man. Thy coffin lies handy to the vault." + +"Sir? The hatchway? oh! So it does, sir, so it does." + +"Art not thou the leg-maker? Look, did not this stump come from thy +shop?" + +"I believe it did, sir; does the ferrule stand, sir?" + +"Well enough. But art thou not also the undertaker?" + +"Aye, sir; I patched up this thing here as a coffin for Queequeg; but +they've set me now to turning it into something else." + +"Then tell me; art thou not an arrant, all-grasping, intermeddling, +monopolising, heathenish old scamp, to be one day making legs, and +the next day coffins to clap them in, and yet again life-buoys out of +those same coffins? Thou art as unprincipled as the gods, and as +much of a jack-of-all-trades." + +"But I do not mean anything, sir. I do as I do." + +"The gods again. Hark ye, dost thou not ever sing working about a +coffin? The Titans, they say, hummed snatches when chipping out the +craters for volcanoes; and the grave-digger in the play sings, spade +in hand. Dost thou never?" + +"Sing, sir? Do I sing? Oh, I'm indifferent enough, sir, for that; +but the reason why the grave-digger made music must have been because +there was none in his spade, sir. But the caulking mallet is full of +it. Hark to it." + +"Aye, and that's because the lid there's a sounding-board; and what +in all things makes the sounding-board is this--there's naught +beneath. And yet, a coffin with a body in it rings pretty much the +same, Carpenter. Hast thou ever helped carry a bier, and heard the +coffin knock against the churchyard gate, going in? + +"Faith, sir, I've--" + +"Faith? What's that?" + +"Why, faith, sir, it's only a sort of exclamation-like--that's all, +sir." + +"Um, um; go on." + +"I was about to say, sir, that--" + +"Art thou a silk-worm? Dost thou spin thy own shroud out of thyself? +Look at thy bosom! Despatch! and get these traps out of sight." + +"He goes aft. That was sudden, now; but squalls come sudden in hot +latitudes. I've heard that the Isle of Albemarle, one of the +Gallipagos, is cut by the Equator right in the middle. Seems to me +some sort of Equator cuts yon old man, too, right in his middle. +He's always under the Line--fiery hot, I tell ye! He's looking this +way--come, oakum; quick. Here we go again. This wooden mallet is +the cork, and I'm the professor of musical glasses--tap, tap!" + +(AHAB TO HIMSELF.) + +"There's a sight! There's a sound! The grey-headed woodpecker +tapping the hollow tree! Blind and dumb might well be envied now. +See! that thing rests on two line-tubs, full of tow-lines. A most +malicious wag, that fellow. Rat-tat! So man's seconds tick! Oh! +how immaterial are all materials! What things real are there, but +imponderable thoughts? Here now's the very dreaded symbol of grim +death, by a mere hap, made the expressive sign of the help and hope +of most endangered life. A life-buoy of a coffin! Does it go +further? Can it be that in some spiritual sense the coffin is, after +all, but an immortality-preserver! I'll think of that. But no. So +far gone am I in the dark side of earth, that its other side, the +theoretic bright one, seems but uncertain twilight to me. Will ye +never have done, Carpenter, with that accursed sound? I go below; +let me not see that thing here when I return again. Now, then, Pip, +we'll talk this over; I do suck most wondrous philosophies from thee! +Some unknown conduits from the unknown worlds must empty into thee!" + + + +CHAPTER 128 + +The Pequod Meets The Rachel. + + +Next day, a large ship, the Rachel, was descried, bearing directly +down upon the Pequod, all her spars thickly clustering with men. At +the time the Pequod was making good speed through the water; but as +the broad-winged windward stranger shot nigh to her, the boastful +sails all fell together as blank bladders that are burst, and all +life fled from the smitten hull. + +"Bad news; she brings bad news," muttered the old Manxman. But ere +her commander, who, with trumpet to mouth, stood up in his boat; ere +he could hopefully hail, Ahab's voice was heard. + +"Hast seen the White Whale?" + +"Aye, yesterday. Have ye seen a whale-boat adrift?" + +Throttling his joy, Ahab negatively answered this unexpected +question; and would then have fain boarded the stranger, when the +stranger captain himself, having stopped his vessel's way, was seen +descending her side. A few keen pulls, and his boat-hook soon +clinched the Pequod's main-chains, and he sprang to the deck. +Immediately he was recognised by Ahab for a Nantucketer he knew. But +no formal salutation was exchanged. + +"Where was he?--not killed!--not killed!" cried Ahab, closely +advancing. "How was it?" + +It seemed that somewhat late on the afternoon of the day previous, +while three of the stranger's boats were engaged with a shoal of +whales, which had led them some four or five miles from the ship; and +while they were yet in swift chase to windward, the white hump and +head of Moby Dick had suddenly loomed up out of the water, not very +far to leeward; whereupon, the fourth rigged boat--a reserved +one--had been instantly lowered in chase. After a keen sail before +the wind, this fourth boat--the swiftest keeled of all--seemed to +have succeeded in fastening--at least, as well as the man at the +mast-head could tell anything about it. In the distance he saw the +diminished dotted boat; and then a swift gleam of bubbling white +water; and after that nothing more; whence it was concluded that the +stricken whale must have indefinitely run away with his pursuers, as +often happens. There was some apprehension, but no positive alarm, +as yet. The recall signals were placed in the rigging; darkness came +on; and forced to pick up her three far to windward boats--ere going +in quest of the fourth one in the precisely opposite direction--the +ship had not only been necessitated to leave that boat to its fate +till near midnight, but, for the time, to increase her distance from +it. But the rest of her crew being at last safe aboard, she crowded +all sail--stunsail on stunsail--after the missing boat; kindling a +fire in her try-pots for a beacon; and every other man aloft on the +look-out. But though when she had thus sailed a sufficient distance +to gain the presumed place of the absent ones when last seen; though +she then paused to lower her spare boats to pull all around her; and +not finding anything, had again dashed on; again paused, and lowered +her boats; and though she had thus continued doing till daylight; +yet not the least glimpse of the missing keel had been seen. + +The story told, the stranger Captain immediately went on to reveal +his object in boarding the Pequod. He desired that ship to unite +with his own in the search; by sailing over the sea some four or five +miles apart, on parallel lines, and so sweeping a double horizon, as +it were. + +"I will wager something now," whispered Stubb to Flask, "that some +one in that missing boat wore off that Captain's best coat; mayhap, +his watch--he's so cursed anxious to get it back. Who ever heard of +two pious whale-ships cruising after one missing whale-boat in the +height of the whaling season? See, Flask, only see how pale he +looks--pale in the very buttons of his eyes--look--it wasn't the +coat--it must have been the--" + +"My boy, my own boy is among them. For God's sake--I beg, I +conjure"--here exclaimed the stranger Captain to Ahab, who thus far +had but icily received his petition. "For eight-and-forty hours let +me charter your ship--I will gladly pay for it, and roundly pay for +it--if there be no other way--for eight-and-forty hours only--only +that--you must, oh, you must, and you SHALL do this thing." + +"His son!" cried Stubb, "oh, it's his son he's lost! I take back the +coat and watch--what says Ahab? We must save that boy." + +"He's drowned with the rest on 'em, last night," said the old Manx +sailor standing behind them; "I heard; all of ye heard their +spirits." + +Now, as it shortly turned out, what made this incident of the +Rachel's the more melancholy, was the circumstance, that not only was +one of the Captain's sons among the number of the missing boat's +crew; but among the number of the other boat's crews, at the same +time, but on the other hand, separated from the ship during the dark +vicissitudes of the chase, there had been still another son; as that +for a time, the wretched father was plunged to the bottom of the +cruellest perplexity; which was only solved for him by his chief +mate's instinctively adopting the ordinary procedure of a whale-ship +in such emergencies, that is, when placed between jeopardized but +divided boats, always to pick up the majority first. But the +captain, for some unknown constitutional reason, had refrained from +mentioning all this, and not till forced to it by Ahab's iciness did +he allude to his one yet missing boy; a little lad, but twelve years +old, whose father with the earnest but unmisgiving hardihood of a +Nantucketer's paternal love, had thus early sought to initiate him in +the perils and wonders of a vocation almost immemorially the destiny +of all his race. Nor does it unfrequently occur, that Nantucket +captains will send a son of such tender age away from them, for a +protracted three or four years' voyage in some other ship than their +own; so that their first knowledge of a whaleman's career shall be +unenervated by any chance display of a father's natural but untimely +partiality, or undue apprehensiveness and concern. + +Meantime, now the stranger was still beseeching his poor boon of +Ahab; and Ahab still stood like an anvil, receiving every shock, but +without the least quivering of his own. + +"I will not go," said the stranger, "till you say aye to me. Do to +me as you would have me do to you in the like case. For YOU too have +a boy, Captain Ahab--though but a child, and nestling safely at home +now--a child of your old age too--Yes, yes, you relent; I see +it--run, run, men, now, and stand by to square in the yards." + +"Avast," cried Ahab--"touch not a rope-yarn"; then in a voice that +prolongingly moulded every word--"Captain Gardiner, I will not do it. +Even now I lose time. Good-bye, good-bye. God bless ye, man, and +may I forgive myself, but I must go. Mr. Starbuck, look at the +binnacle watch, and in three minutes from this present instant warn +off all strangers: then brace forward again, and let the ship sail +as before." + +Hurriedly turning, with averted face, he descended into his cabin, +leaving the strange captain transfixed at this unconditional and +utter rejection of his so earnest suit. But starting from his +enchantment, Gardiner silently hurried to the side; more fell than +stepped into his boat, and returned to his ship. + +Soon the two ships diverged their wakes; and long as the strange +vessel was in view, she was seen to yaw hither and thither at every +dark spot, however small, on the sea. This way and that her yards +were swung round; starboard and larboard, she continued to tack; +now she beat against a head sea; and again it pushed her before it; +while all the while, her masts and yards were thickly clustered with +men, as three tall cherry trees, when the boys are cherrying among +the boughs. + +But by her still halting course and winding, woeful way, you plainly +saw that this ship that so wept with spray, still remained without +comfort. She was Rachel, weeping for her children, because they were +not. + + + +CHAPTER 129 + +The Cabin. + + +(AHAB MOVING TO GO ON DECK; PIP CATCHES HIM BY THE HAND TO FOLLOW.) + +Lad, lad, I tell thee thou must not follow Ahab now. The hour is +coming when Ahab would not scare thee from him, yet would not have +thee by him. There is that in thee, poor lad, which I feel too +curing to my malady. Like cures like; and for this hunt, my malady +becomes my most desired health. Do thou abide below here, where they +shall serve thee, as if thou wert the captain. Aye, lad, thou shalt +sit here in my own screwed chair; another screw to it, thou must be." + +"No, no, no! ye have not a whole body, sir; do ye but use poor me for +your one lost leg; only tread upon me, sir; I ask no more, so I +remain a part of ye." + +"Oh! spite of million villains, this makes me a bigot in the fadeless +fidelity of man!--and a black! and crazy!--but methinks +like-cures-like applies to him too; he grows so sane again." + +"They tell me, sir, that Stubb did once desert poor little Pip, whose +drowned bones now show white, for all the blackness of his living +skin. But I will never desert ye, sir, as Stubb did him. Sir, I +must go with ye." + +"If thou speakest thus to me much more, Ahab's purpose keels up in +him. I tell thee no; it cannot be." + +"Oh good master, master, master! + +"Weep so, and I will murder thee! have a care, for Ahab too is mad. +Listen, and thou wilt often hear my ivory foot upon the deck, and +still know that I am there. And now I quit thee. Thy hand!--Met! +True art thou, lad, as the circumference to its centre. So: God for +ever bless thee; and if it come to that,--God for ever save thee, let +what will befall." + +(AHAB GOES; PIP STEPS ONE STEP FORWARD.) + + +"Here he this instant stood; I stand in his air,--but I'm alone. +Now were even poor Pip here I could endure it, but he's missing. +Pip! Pip! Ding, dong, ding! Who's seen Pip? He must be up here; +let's try the door. What? neither lock, nor bolt, nor bar; and yet +there's no opening it. It must be the spell; he told me to stay +here: Aye, and told me this screwed chair was mine. Here, then, I'll +seat me, against the transom, in the ship's full middle, all her keel +and her three masts before me. Here, our old sailors say, in their +black seventy-fours great admirals sometimes sit at table, and lord +it over rows of captains and lieutenants. Ha! what's this? epaulets! +epaulets! the epaulets all come crowding! Pass round the decanters; +glad to see ye; fill up, monsieurs! What an odd feeling, now, when a +black boy's host to white men with gold lace upon their +coats!--Monsieurs, have ye seen one Pip?--a little negro lad, five +feet high, hang-dog look, and cowardly! Jumped from a whale-boat +once;--seen him? No! Well then, fill up again, captains, and let's +drink shame upon all cowards! I name no names. Shame upon them! +Put one foot upon the table. Shame upon all cowards.--Hist! above +there, I hear ivory--Oh, master! master! I am indeed down-hearted +when you walk over me. But here I'll stay, though this stern +strikes rocks; and they bulge through; and oysters come to join me." + + + +CHAPTER 130 + +The Hat. + + +And now that at the proper time and place, after so long and wide a +preliminary cruise, Ahab,--all other whaling waters swept--seemed to +have chased his foe into an ocean-fold, to slay him the more securely +there; now, that he found himself hard by the very latitude and +longitude where his tormenting wound had been inflicted; now that a +vessel had been spoken which on the very day preceding had actually +encountered Moby Dick;--and now that all his successive meetings with +various ships contrastingly concurred to show the demoniac +indifference with which the white whale tore his hunters, whether +sinning or sinned against; now it was that there lurked a something +in the old man's eyes, which it was hardly sufferable for feeble +souls to see. As the unsetting polar star, which through the +livelong, arctic, six months' night sustains its piercing, steady, +central gaze; so Ahab's purpose now fixedly gleamed down upon the +constant midnight of the gloomy crew. It domineered above them so, +that all their bodings, doubts, misgivings, fears, were fain to hide +beneath their souls, and not sprout forth a single spear or leaf. + +In this foreshadowing interval too, all humor, forced or natural, +vanished. Stubb no more strove to raise a smile; Starbuck no more +strove to check one. Alike, joy and sorrow, hope and fear, seemed +ground to finest dust, and powdered, for the time, in the clamped +mortar of Ahab's iron soul. Like machines, they dumbly moved about +the deck, ever conscious that the old man's despot eye was on them. + +But did you deeply scan him in his more secret confidential hours; +when he thought no glance but one was on him; then you would have +seen that even as Ahab's eyes so awed the crew's, the inscrutable +Parsee's glance awed his; or somehow, at least, in some wild way, at +times affected it. Such an added, gliding strangeness began to +invest the thin Fedallah now; such ceaseless shudderings shook him; +that the men looked dubious at him; half uncertain, as it seemed, +whether indeed he were a mortal substance, or else a tremulous shadow +cast upon the deck by some unseen being's body. And that shadow was +always hovering there. For not by night, even, had Fedallah ever +certainly been known to slumber, or go below. He would stand still +for hours: but never sat or leaned; his wan but wondrous eyes did +plainly say--We two watchmen never rest. + +Nor, at any time, by night or day could the mariners now step upon +the deck, unless Ahab was before them; either standing in his +pivot-hole, or exactly pacing the planks between two undeviating +limits,--the main-mast and the mizen; or else they saw him standing +in the cabin-scuttle,--his living foot advanced upon the deck, as if +to step; his hat slouched heavily over his eyes; so that however +motionless he stood, however the days and nights were added on, that +he had not swung in his hammock; yet hidden beneath that slouching +hat, they could never tell unerringly whether, for all this, his eyes +were really closed at times; or whether he was still intently +scanning them; no matter, though he stood so in the scuttle for a +whole hour on the stretch, and the unheeded night-damp gathered in +beads of dew upon that stone-carved coat and hat. The clothes that +the night had wet, the next day's sunshine dried upon him; and so, +day after day, and night after night; he went no more beneath the +planks; whatever he wanted from the cabin that thing he sent for. + +He ate in the same open air; that is, his two only meals,--breakfast +and dinner: supper he never touched; nor reaped his beard; which +darkly grew all gnarled, as unearthed roots of trees blown over, +which still grow idly on at naked base, though perished in the upper +verdure. But though his whole life was now become one watch on deck; +and though the Parsee's mystic watch was without intermission as his +own; yet these two never seemed to speak--one man to the +other--unless at long intervals some passing unmomentous matter made +it necessary. Though such a potent spell seemed secretly to join the +twain; openly, and to the awe-struck crew, they seemed pole-like +asunder. If by day they chanced to speak one word; by night, dumb +men were both, so far as concerned the slightest verbal interchange. +At times, for longest hours, without a single hail, they stood far +parted in the starlight; Ahab in his scuttle, the Parsee by the +mainmast; but still fixedly gazing upon each other; as if in the +Parsee Ahab saw his forethrown shadow, in Ahab the Parsee his +abandoned substance. + +And yet, somehow, did Ahab--in his own proper self, as daily, hourly, +and every instant, commandingly revealed to his subordinates,--Ahab +seemed an independent lord; the Parsee but his slave. Still again +both seemed yoked together, and an unseen tyrant driving them; the +lean shade siding the solid rib. For be this Parsee what he may, all +rib and keel was solid Ahab. + +At the first faintest glimmering of the dawn, his iron voice was +heard from aft,--"Man the mast-heads!"--and all through the day, +till after sunset and after twilight, the same voice every hour, at +the striking of the helmsman's bell, was heard--"What d'ye +see?--sharp! sharp!" + +But when three or four days had slided by, after meeting the +children-seeking Rachel; and no spout had yet been seen; the +monomaniac old man seemed distrustful of his crew's fidelity; at +least, of nearly all except the Pagan harpooneers; he seemed to +doubt, even, whether Stubb and Flask might not willingly overlook the +sight he sought. But if these suspicions were really his, he +sagaciously refrained from verbally expressing them, however his +actions might seem to hint them. + +"I will have the first sight of the whale myself,"--he said. "Aye! +Ahab must have the doubloon! and with his own hands he rigged a nest +of basketed bowlines; and sending a hand aloft, with a single sheaved +block, to secure to the main-mast head, he received the two ends of +the downward-reeved rope; and attaching one to his basket prepared a +pin for the other end, in order to fasten it at the rail. This done, +with that end yet in his hand and standing beside the pin, he looked +round upon his crew, sweeping from one to the other; pausing his +glance long upon Daggoo, Queequeg, Tashtego; but shunning Fedallah; +and then settling his firm relying eye upon the chief mate, +said,--"Take the rope, sir--I give it into thy hands, Starbuck." +Then arranging his person in the basket, he gave the word for them to +hoist him to his perch, Starbuck being the one who secured the rope +at last; and afterwards stood near it. And thus, with one hand +clinging round the royal mast, Ahab gazed abroad upon the sea for +miles and miles,--ahead, astern, this side, and that,--within the +wide expanded circle commanded at so great a height. + +When in working with his hands at some lofty almost isolated place in +the rigging, which chances to afford no foothold, the sailor at sea +is hoisted up to that spot, and sustained there by the rope; under +these circumstances, its fastened end on deck is always given in +strict charge to some one man who has the special watch of it. +Because in such a wilderness of running rigging, whose various +different relations aloft cannot always be infallibly discerned by +what is seen of them at the deck; and when the deck-ends of these +ropes are being every few minutes cast down from the fastenings, it +would be but a natural fatality, if, unprovided with a constant +watchman, the hoisted sailor should by some carelessness of the crew +be cast adrift and fall all swooping to the sea. So Ahab's +proceedings in this matter were not unusual; the only strange thing +about them seemed to be, that Starbuck, almost the one only man who +had ever ventured to oppose him with anything in the slightest degree +approaching to decision--one of those too, whose faithfulness on the +look-out he had seemed to doubt somewhat;--it was strange, that this +was the very man he should select for his watchman; freely giving his +whole life into such an otherwise distrusted person's hands. + +Now, the first time Ahab was perched aloft; ere he had been there ten +minutes; one of those red-billed savage sea-hawks which so often fly +incommodiously close round the manned mast-heads of whalemen in these +latitudes; one of these birds came wheeling and screaming round his +head in a maze of untrackably swift circlings. Then it darted a +thousand feet straight up into the air; then spiralized downwards, +and went eddying again round his head. + +But with his gaze fixed upon the dim and distant horizon, Ahab seemed +not to mark this wild bird; nor, indeed, would any one else have +marked it much, it being no uncommon circumstance; only now almost +the least heedful eye seemed to see some sort of cunning meaning in +almost every sight. + +"Your hat, your hat, sir!" suddenly cried the Sicilian seaman, who +being posted at the mizen-mast-head, stood directly behind Ahab, +though somewhat lower than his level, and with a deep gulf of air +dividing them. + +But already the sable wing was before the old man's eyes; the long +hooked bill at his head: with a scream, the black hawk darted away +with his prize. + +An eagle flew thrice round Tarquin's head, removing his cap to +replace it, and thereupon Tanaquil, his wife, declared that Tarquin +would be king of Rome. But only by the replacing of the cap was that +omen accounted good. Ahab's hat was never restored; the wild hawk +flew on and on with it; far in advance of the prow: and at last +disappeared; while from the point of that disappearance, a minute +black spot was dimly discerned, falling from that vast height into +the sea. + + + +CHAPTER 131 + +The Pequod Meets The Delight. + + +The intense Pequod sailed on; the rolling waves and days went by; the +life-buoy-coffin still lightly swung; and another ship, most +miserably misnamed the Delight, was descried. As she drew nigh, all +eyes were fixed upon her broad beams, called shears, which, in some +whaling-ships, cross the quarter-deck at the height of eight or nine +feet; serving to carry the spare, unrigged, or disabled boats. + +Upon the stranger's shears were beheld the shattered, white ribs, and +some few splintered planks, of what had once been a whale-boat; but +you now saw through this wreck, as plainly as you see through the +peeled, half-unhinged, and bleaching skeleton of a horse. + +"Hast seen the White Whale?" + +"Look!" replied the hollow-cheeked captain from his taffrail; and +with his trumpet he pointed to the wreck. + +"Hast killed him?" + +"The harpoon is not yet forged that ever will do that," answered the +other, sadly glancing upon a rounded hammock on the deck, whose +gathered sides some noiseless sailors were busy in sewing together. + +"Not forged!" and snatching Perth's levelled iron from the crotch, +Ahab held it out, exclaiming--"Look ye, Nantucketer; here in this +hand I hold his death! Tempered in blood, and tempered by lightning +are these barbs; and I swear to temper them triply in that hot place +behind the fin, where the White Whale most feels his accursed life!" + +"Then God keep thee, old man--see'st thou that"--pointing to the +hammock--"I bury but one of five stout men, who were alive only +yesterday; but were dead ere night. Only THAT one I bury; the rest +were buried before they died; you sail upon their tomb." Then +turning to his crew--"Are ye ready there? place the plank then on the +rail, and lift the body; so, then--Oh! God"--advancing towards the +hammock with uplifted hands--"may the resurrection and the life--" + +"Brace forward! Up helm!" cried Ahab like lightning to his men. + +But the suddenly started Pequod was not quick enough to escape the +sound of the splash that the corpse soon made as it struck the sea; +not so quick, indeed, but that some of the flying bubbles might have +sprinkled her hull with their ghostly baptism. + +As Ahab now glided from the dejected Delight, the strange life-buoy +hanging at the Pequod's stern came into conspicuous relief. + +"Ha! yonder! look yonder, men!" cried a foreboding voice in her wake. +"In vain, oh, ye strangers, ye fly our sad burial; ye but turn us +your taffrail to show us your coffin!" + + + +CHAPTER 132 + +The Symphony. + + +It was a clear steel-blue day. The firmaments of air and sea were +hardly separable in that all-pervading azure; only, the pensive air +was transparently pure and soft, with a woman's look, and the robust +and man-like sea heaved with long, strong, lingering swells, as +Samson's chest in his sleep. + +Hither, and thither, on high, glided the snow-white wings of small, +unspeckled birds; these were the gentle thoughts of the feminine air; +but to and fro in the deeps, far down in the bottomless blue, rushed +mighty leviathans, sword-fish, and sharks; and these were the strong, +troubled, murderous thinkings of the masculine sea. + +But though thus contrasting within, the contrast was only in shades +and shadows without; those two seemed one; it was only the sex, as it +were, that distinguished them. + +Aloft, like a royal czar and king, the sun seemed giving this gentle +air to this bold and rolling sea; even as bride to groom. And at the +girdling line of the horizon, a soft and tremulous motion--most seen +here at the Equator--denoted the fond, throbbing trust, the loving +alarms, with which the poor bride gave her bosom away. + +Tied up and twisted; gnarled and knotted with wrinkles; haggardly +firm and unyielding; his eyes glowing like coals, that still glow in +the ashes of ruin; untottering Ahab stood forth in the clearness of +the morn; lifting his splintered helmet of a brow to the fair girl's +forehead of heaven. + +Oh, immortal infancy, and innocency of the azure! Invisible winged +creatures that frolic all round us! Sweet childhood of air and sky! +how oblivious were ye of old Ahab's close-coiled woe! But so have I +seen little Miriam and Martha, laughing-eyed elves, heedlessly gambol +around their old sire; sporting with the circle of singed locks which +grew on the marge of that burnt-out crater of his brain. + +Slowly crossing the deck from the scuttle, Ahab leaned over the side +and watched how his shadow in the water sank and sank to his gaze, +the more and the more that he strove to pierce the profundity. But +the lovely aromas in that enchanted air did at last seem to dispel, +for a moment, the cankerous thing in his soul. That glad, happy air, +that winsome sky, did at last stroke and caress him; the step-mother +world, so long cruel--forbidding--now threw affectionate arms round +his stubborn neck, and did seem to joyously sob over him, as if over +one, that however wilful and erring, she could yet find it in her +heart to save and to bless. From beneath his slouched hat Ahab +dropped a tear into the sea; nor did all the Pacific contain such +wealth as that one wee drop. + +Starbuck saw the old man; saw him, how he heavily leaned over the +side; and he seemed to hear in his own true heart the measureless +sobbing that stole out of the centre of the serenity around. Careful +not to touch him, or be noticed by him, he yet drew near to him, and +stood there. + +Ahab turned. + +"Starbuck!" + +"Sir." + +"Oh, Starbuck! it is a mild, mild wind, and a mild looking sky. On +such a day--very much such a sweetness as this--I struck my first +whale--a boy-harpooneer of eighteen! Forty--forty--forty years +ago!--ago! Forty years of continual whaling! forty years of +privation, and peril, and storm-time! forty years on the pitiless +sea! for forty years has Ahab forsaken the peaceful land, for forty +years to make war on the horrors of the deep! Aye and yes, Starbuck, +out of those forty years I have not spent three ashore. When I think +of this life I have led; the desolation of solitude it has been; the +masoned, walled-town of a Captain's exclusiveness, which admits but +small entrance to any sympathy from the green country without--oh, +weariness! heaviness! Guinea-coast slavery of solitary +command!--when I think of all this; only half-suspected, not so +keenly known to me before--and how for forty years I have fed upon +dry salted fare--fit emblem of the dry nourishment of my soil!--when +the poorest landsman has had fresh fruit to his daily hand, and +broken the world's fresh bread to my mouldy crusts--away, whole +oceans away, from that young girl-wife I wedded past fifty, and +sailed for Cape Horn the next day, leaving but one dent in my +marriage pillow--wife? wife?--rather a widow with her husband alive! +Aye, I widowed that poor girl when I married her, Starbuck; and +then, the madness, the frenzy, the boiling blood and the smoking +brow, with which, for a thousand lowerings old Ahab has furiously, +foamingly chased his prey--more a demon than a man!--aye, aye! what a +forty years' fool--fool--old fool, has old Ahab been! Why this +strife of the chase? why weary, and palsy the arm at the oar, and the +iron, and the lance? how the richer or better is Ahab now? Behold. +Oh, Starbuck! is it not hard, that with this weary load I bear, one +poor leg should have been snatched from under me? Here, brush this +old hair aside; it blinds me, that I seem to weep. Locks so grey did +never grow but from out some ashes! But do I look very old, so very, +very old, Starbuck? I feel deadly faint, bowed, and humped, as +though I were Adam, staggering beneath the piled centuries since +Paradise. God! God! God!--crack my heart!--stave my +brain!--mockery! mockery! bitter, biting mockery of grey hairs, have +I lived enough joy to wear ye; and seem and feel thus intolerably +old? Close! stand close to me, Starbuck; let me look into a human +eye; it is better than to gaze into sea or sky; better than to gaze +upon God. By the green land; by the bright hearth-stone! this is the +magic glass, man; I see my wife and my child in thine eye. No, no; +stay on board, on board!--lower not when I do; when branded Ahab +gives chase to Moby Dick. That hazard shall not be thine. No, no! +not with the far away home I see in that eye!" + +"Oh, my Captain! my Captain! noble soul! grand old heart, after all! +why should any one give chase to that hated fish! Away with me! let +us fly these deadly waters! let us home! Wife and child, too, are +Starbuck's--wife and child of his brotherly, sisterly, play-fellow +youth; even as thine, sir, are the wife and child of thy loving, +longing, paternal old age! Away! let us away!--this instant let me +alter the course! How cheerily, how hilariously, O my Captain, would +we bowl on our way to see old Nantucket again! I think, sir, they +have some such mild blue days, even as this, in Nantucket." + +"They have, they have. I have seen them--some summer days in the +morning. About this time--yes, it is his noon nap now--the boy +vivaciously wakes; sits up in bed; and his mother tells him of me, of +cannibal old me; how I am abroad upon the deep, but will yet come +back to dance him again." + +"'Tis my Mary, my Mary herself! She promised that my boy, every +morning, should be carried to the hill to catch the first glimpse of +his father's sail! Yes, yes! no more! it is done! we head for +Nantucket! Come, my Captain, study out the course, and let us away! +See, see! the boy's face from the window! the boy's hand on the +hill!" + +But Ahab's glance was averted; like a blighted fruit tree he shook, +and cast his last, cindered apple to the soil. + +"What is it, what nameless, inscrutable, unearthly thing is it; what +cozening, hidden lord and master, and cruel, remorseless emperor +commands me; that against all natural lovings and longings, I so keep +pushing, and crowding, and jamming myself on all the time; recklessly +making me ready to do what in my own proper, natural heart, I durst +not so much as dare? Is Ahab, Ahab? Is it I, God, or who, that +lifts this arm? But if the great sun move not of himself; but is as an +errand-boy in heaven; nor one single star can revolve, but by some +invisible power; how then can this one small heart beat; this one +small brain think thoughts; unless God does that beating, does that +thinking, does that living, and not I. By heaven, man, we are turned +round and round in this world, like yonder windlass, and Fate is the +handspike. And all the time, lo! that smiling sky, and this +unsounded sea! Look! see yon Albicore! who put it into him to chase +and fang that flying-fish? Where do murderers go, man! Who's to +doom, when the judge himself is dragged to the bar? But it is a +mild, mild wind, and a mild looking sky; and the air smells now, as +if it blew from a far-away meadow; they have been making hay +somewhere under the slopes of the Andes, Starbuck, and the mowers are +sleeping among the new-mown hay. Sleeping? Aye, toil we how we may, +we all sleep at last on the field. Sleep? Aye, and rust amid +greenness; as last year's scythes flung down, and left in the half-cut +swaths--Starbuck!" + +But blanched to a corpse's hue with despair, the Mate had stolen +away. + +Ahab crossed the deck to gaze over on the other side; but started at +two reflected, fixed eyes in the water there. Fedallah was +motionlessly leaning over the same rail. + + + +CHAPTER 133 + +The Chase--First Day. + + +That night, in the mid-watch, when the old man--as his wont at +intervals--stepped forth from the scuttle in which he leaned, and +went to his pivot-hole, he suddenly thrust out his face fiercely, +snuffing up the sea air as a sagacious ship's dog will, in drawing +nigh to some barbarous isle. He declared that a whale must be near. +Soon that peculiar odor, sometimes to a great distance given forth by +the living sperm whale, was palpable to all the watch; nor was any +mariner surprised when, after inspecting the compass, and then the +dog-vane, and then ascertaining the precise bearing of the odor as +nearly as possible, Ahab rapidly ordered the ship's course to be +slightly altered, and the sail to be shortened. + +The acute policy dictating these movements was sufficiently +vindicated at daybreak, by the sight of a long sleek on the sea +directly and lengthwise ahead, smooth as oil, and resembling in the +pleated watery wrinkles bordering it, the polished metallic-like +marks of some swift tide-rip, at the mouth of a deep, rapid stream. + +"Man the mast-heads! Call all hands!" + +Thundering with the butts of three clubbed handspikes on the +forecastle deck, Daggoo roused the sleepers with such judgment claps +that they seemed to exhale from the scuttle, so instantaneously did +they appear with their clothes in their hands. + +"What d'ye see?" cried Ahab, flattening his face to the sky. + +"Nothing, nothing sir!" was the sound hailing down in reply. + +"T'gallant sails!--stunsails! alow and aloft, and on both sides!" + +All sail being set, he now cast loose the life-line, reserved for +swaying him to the main royal-mast head; and in a few moments they +were hoisting him thither, when, while but two thirds of the way +aloft, and while peering ahead through the horizontal vacancy between +the main-top-sail and top-gallant-sail, he raised a gull-like cry in +the air. "There she blows!--there she blows! A hump like a +snow-hill! It is Moby Dick!" + +Fired by the cry which seemed simultaneously taken up by the three +look-outs, the men on deck rushed to the rigging to behold the famous +whale they had so long been pursuing. Ahab had now gained his final +perch, some feet above the other look-outs, Tashtego standing just +beneath him on the cap of the top-gallant-mast, so that the Indian's +head was almost on a level with Ahab's heel. From this height the +whale was now seen some mile or so ahead, at every roll of the sea +revealing his high sparkling hump, and regularly jetting his silent +spout into the air. To the credulous mariners it seemed the same +silent spout they had so long ago beheld in the moonlit Atlantic and +Indian Oceans. + +"And did none of ye see it before?" cried Ahab, hailing the perched +men all around him. + +"I saw him almost that same instant, sir, that Captain Ahab did, and +I cried out," said Tashtego. + +"Not the same instant; not the same--no, the doubloon is mine, Fate +reserved the doubloon for me. I only; none of ye could have raised +the White Whale first. There she blows!--there she blows!--there +she blows! There again!--there again!" he cried, in long-drawn, +lingering, methodic tones, attuned to the gradual prolongings of the +whale's visible jets. "He's going to sound! In stunsails! Down +top-gallant-sails! Stand by three boats. Mr. Starbuck, remember, +stay on board, and keep the ship. Helm there! Luff, luff a point! +So; steady, man, steady! There go flukes! No, no; only black water! +All ready the boats there? Stand by, stand by! Lower me, Mr. +Starbuck; lower, lower,--quick, quicker!" and he slid through the air +to the deck. + +"He is heading straight to leeward, sir," cried Stubb, "right away +from us; cannot have seen the ship yet." + +"Be dumb, man! Stand by the braces! Hard down the helm!--brace up! +Shiver her!--shiver her!--So; well that! Boats, boats!" + +Soon all the boats but Starbuck's were dropped; all the boat-sails +set--all the paddles plying; with rippling swiftness, shooting to +leeward; and Ahab heading the onset. A pale, death-glimmer lit up +Fedallah's sunken eyes; a hideous motion gnawed his mouth. + +Like noiseless nautilus shells, their light prows sped through the +sea; but only slowly they neared the foe. As they neared him, the +ocean grew still more smooth; seemed drawing a carpet over its waves; +seemed a noon-meadow, so serenely it spread. At length the +breathless hunter came so nigh his seemingly unsuspecting prey, that his +entire dazzling hump was distinctly visible, sliding along the sea as +if an isolated thing, and continually set in a revolving ring of +finest, fleecy, greenish foam. He saw the vast, involved wrinkles of +the slightly projecting head beyond. Before it, far out on the soft +Turkish-rugged waters, went the glistening white shadow from his +broad, milky forehead, a musical rippling playfully accompanying the +shade; and behind, the blue waters interchangeably flowed over into +the moving valley of his steady wake; and on either hand bright +bubbles arose and danced by his side. But these were broken again by +the light toes of hundreds of gay fowl softly feathering the sea, +alternate with their fitful flight; and like to some flag-staff +rising from the painted hull of an argosy, the tall but shattered +pole of a recent lance projected from the white whale's back; and at +intervals one of the cloud of soft-toed fowls hovering, and to and +fro skimming like a canopy over the fish, silently perched and rocked +on this pole, the long tail feathers streaming like pennons. + +A gentle joyousness--a mighty mildness of repose in swiftness, +invested the gliding whale. Not the white bull Jupiter swimming away +with ravished Europa clinging to his graceful horns; his lovely, +leering eyes sideways intent upon the maid; with smooth bewitching +fleetness, rippling straight for the nuptial bower in Crete; not +Jove, not that great majesty Supreme! did surpass the glorified White +Whale as he so divinely swam. + +On each soft side--coincident with the parted swell, that but once +leaving him, then flowed so wide away--on each bright side, the whale +shed off enticings. No wonder there had been some among the hunters +who namelessly transported and allured by all this serenity, had +ventured to assail it; but had fatally found that quietude but the +vesture of tornadoes. Yet calm, enticing calm, oh, whale! thou +glidest on, to all who for the first time eye thee, no matter how +many in that same way thou may'st have bejuggled and destroyed +before. + +And thus, through the serene tranquillities of the tropical sea, +among waves whose hand-clappings were suspended by exceeding rapture, +Moby Dick moved on, still withholding from sight the full terrors of +his submerged trunk, entirely hiding the wrenched hideousness of his +jaw. But soon the fore part of him slowly rose from the water; for +an instant his whole marbleized body formed a high arch, like +Virginia's Natural Bridge, and warningly waving his bannered flukes +in the air, the grand god revealed himself, sounded, and went out of +sight. Hoveringly halting, and dipping on the wing, the white +sea-fowls longingly lingered over the agitated pool that he left. + +With oars apeak, and paddles down, the sheets of their sails adrift, +the three boats now stilly floated, awaiting Moby Dick's +reappearance. + +"An hour," said Ahab, standing rooted in his boat's stern; and he +gazed beyond the whale's place, towards the dim blue spaces and wide +wooing vacancies to leeward. It was only an instant; for again his +eyes seemed whirling round in his head as he swept the watery circle. +The breeze now freshened; the sea began to swell. + +"The birds!--the birds!" cried Tashtego. + +In long Indian file, as when herons take wing, the white birds were +now all flying towards Ahab's boat; and when within a few yards began +fluttering over the water there, wheeling round and round, with +joyous, expectant cries. Their vision was keener than man's; Ahab +could discover no sign in the sea. But suddenly as he peered down +and down into its depths, he profoundly saw a white living spot no +bigger than a white weasel, with wonderful celerity uprising, and +magnifying as it rose, till it turned, and then there were plainly +revealed two long crooked rows of white, glistening teeth, floating +up from the undiscoverable bottom. It was Moby Dick's open mouth and +scrolled jaw; his vast, shadowed bulk still half blending with the +blue of the sea. The glittering mouth yawned beneath the boat like +an open-doored marble tomb; and giving one sidelong sweep with his +steering oar, Ahab whirled the craft aside from this tremendous +apparition. Then, calling upon Fedallah to change places with him, +went forward to the bows, and seizing Perth's harpoon, commanded his +crew to grasp their oars and stand by to stern. + +Now, by reason of this timely spinning round the boat upon its axis, +its bow, by anticipation, was made to face the whale's head while yet +under water. But as if perceiving this stratagem, Moby Dick, with +that malicious intelligence ascribed to him, sidelingly transplanted +himself, as it were, in an instant, shooting his pleated head +lengthwise beneath the boat. + +Through and through; through every plank and each rib, it thrilled +for an instant, the whale obliquely lying on his back, in the manner +of a biting shark, slowly and feelingly taking its bows full within +his mouth, so that the long, narrow, scrolled lower jaw curled high +up into the open air, and one of the teeth caught in a row-lock. The +bluish pearl-white of the inside of the jaw was within six inches of +Ahab's head, and reached higher than that. In this attitude the +White Whale now shook the slight cedar as a mildly cruel cat her +mouse. With unastonished eyes Fedallah gazed, and crossed his arms; +but the tiger-yellow crew were tumbling over each other's heads to +gain the uttermost stern. + +And now, while both elastic gunwales were springing in and out, as +the whale dallied with the doomed craft in this devilish way; and +from his body being submerged beneath the boat, he could not be +darted at from the bows, for the bows were almost inside of him, as +it were; and while the other boats involuntarily paused, as before a +quick crisis impossible to withstand, then it was that monomaniac +Ahab, furious with this tantalizing vicinity of his foe, which placed +him all alive and helpless in the very jaws he hated; frenzied with +all this, he seized the long bone with his naked hands, and wildly +strove to wrench it from its gripe. As now he thus vainly strove, +the jaw slipped from him; the frail gunwales bent in, collapsed, and +snapped, as both jaws, like an enormous shears, sliding further aft, +bit the craft completely in twain, and locked themselves fast again +in the sea, midway between the two floating wrecks. These floated +aside, the broken ends drooping, the crew at the stern-wreck clinging +to the gunwales, and striving to hold fast to the oars to lash them +across. + +At that preluding moment, ere the boat was yet snapped, Ahab, the +first to perceive the whale's intent, by the crafty upraising of his +head, a movement that loosed his hold for the time; at that moment +his hand had made one final effort to push the boat out of the bite. +But only slipping further into the whale's mouth, and tilting over +sideways as it slipped, the boat had shaken off his hold on the jaw; +spilled him out of it, as he leaned to the push; and so he fell +flat-faced upon the sea. + +Ripplingly withdrawing from his prey, Moby Dick now lay at a little +distance, vertically thrusting his oblong white head up and down in +the billows; and at the same time slowly revolving his whole spindled +body; so that when his vast wrinkled forehead rose--some twenty or +more feet out of the water--the now rising swells, with all their +confluent waves, dazzlingly broke against it; vindictively tossing +their shivered spray still higher into the air.* So, in a gale, the +but half baffled Channel billows only recoil from the base of the +Eddystone, triumphantly to overleap its summit with their scud. + + +*This motion is peculiar to the sperm whale. It receives its +designation (pitchpoling) from its being likened to that preliminary +up-and-down poise of the whale-lance, in the exercise called +pitchpoling, previously described. By this motion the whale must +best and most comprehensively view whatever objects may be encircling +him. + + +But soon resuming his horizontal attitude, Moby Dick swam swiftly +round and round the wrecked crew; sideways churning the water in his +vengeful wake, as if lashing himself up to still another and more +deadly assault. The sight of the splintered boat seemed to madden +him, as the blood of grapes and mulberries cast before Antiochus's +elephants in the book of Maccabees. Meanwhile Ahab half smothered in +the foam of the whale's insolent tail, and too much of a cripple to +swim,--though he could still keep afloat, even in the heart of such a +whirlpool as that; helpless Ahab's head was seen, like a tossed +bubble which the least chance shock might burst. From the boat's +fragmentary stern, Fedallah incuriously and mildly eyed him; the +clinging crew, at the other drifting end, could not succor him; more +than enough was it for them to look to themselves. For so +revolvingly appalling was the White Whale's aspect, and so +planetarily swift the ever-contracting circles he made, that he +seemed horizontally swooping upon them. And though the other boats, +unharmed, still hovered hard by; still they dared not pull into the +eddy to strike, lest that should be the signal for the instant +destruction of the jeopardized castaways, Ahab and all; nor in that +case could they themselves hope to escape. With straining eyes, +then, they remained on the outer edge of the direful zone, whose +centre had now become the old man's head. + +Meantime, from the beginning all this had been descried from the +ship's mast heads; and squaring her yards, she had borne down upon +the scene; and was now so nigh, that Ahab in the water hailed +her!--"Sail on the"--but that moment a breaking sea dashed on him +from Moby Dick, and whelmed him for the time. But struggling out of +it again, and chancing to rise on a towering crest, he +shouted,--"Sail on the whale!--Drive him off!" + +The Pequod's prows were pointed; and breaking up the charmed circle, +she effectually parted the white whale from his victim. As he +sullenly swam off, the boats flew to the rescue. + +Dragged into Stubb's boat with blood-shot, blinded eyes, the white +brine caking in his wrinkles; the long tension of Ahab's bodily +strength did crack, and helplessly he yielded to his body's doom: for +a time, lying all crushed in the bottom of Stubb's boat, like one +trodden under foot of herds of elephants. Far inland, nameless wails +came from him, as desolate sounds from out ravines. + +But this intensity of his physical prostration did but so much the +more abbreviate it. In an instant's compass, great hearts sometimes +condense to one deep pang, the sum total of those shallow pains +kindly diffused through feebler men's whole lives. And so, such +hearts, though summary in each one suffering; still, if the gods +decree it, in their life-time aggregate a whole age of woe, wholly +made up of instantaneous intensities; for even in their pointless +centres, those noble natures contain the entire circumferences of +inferior souls. + +"The harpoon," said Ahab, half way rising, and draggingly leaning on +one bended arm--"is it safe?" + +"Aye, sir, for it was not darted; this is it," said Stubb, showing +it. + +"Lay it before me;--any missing men?" + +"One, two, three, four, five;--there were five oars, sir, and here +are five men." + +"That's good.--Help me, man; I wish to stand. So, so, I see him! +there! there! going to leeward still; what a leaping spout!--Hands +off from me! The eternal sap runs up in Ahab's bones again! Set the +sail; out oars; the helm!" + +It is often the case that when a boat is stove, its crew, being +picked up by another boat, help to work that second boat; and the +chase is thus continued with what is called double-banked oars. It +was thus now. But the added power of the boat did not equal the +added power of the whale, for he seemed to have treble-banked his +every fin; swimming with a velocity which plainly showed, that if +now, under these circumstances, pushed on, the chase would prove an +indefinitely prolonged, if not a hopeless one; nor could any crew +endure for so long a period, such an unintermitted, intense straining +at the oar; a thing barely tolerable only in some one brief +vicissitude. The ship itself, then, as it sometimes happens, offered +the most promising intermediate means of overtaking the chase. +Accordingly, the boats now made for her, and were soon swayed up to +their cranes--the two parts of the wrecked boat having been +previously secured by her--and then hoisting everything to her side, +and stacking her canvas high up, and sideways outstretching it with +stun-sails, like the double-jointed wings of an albatross; the Pequod +bore down in the leeward wake of Moby-Dick. At the well known, +methodic intervals, the whale's glittering spout was regularly +announced from the manned mast-heads; and when he would be reported +as just gone down, Ahab would take the time, and then pacing the +deck, binnacle-watch in hand, so soon as the last second of the +allotted hour expired, his voice was heard.--"Whose is the doubloon +now? D'ye see him?" and if the reply was, No, sir! straightway he +commanded them to lift him to his perch. In this way the day wore +on; Ahab, now aloft and motionless; anon, unrestingly pacing the +planks. + +As he was thus walking, uttering no sound, except to hail the men +aloft, or to bid them hoist a sail still higher, or to spread one to +a still greater breadth--thus to and fro pacing, beneath his slouched +hat, at every turn he passed his own wrecked boat, which had been +dropped upon the quarter-deck, and lay there reversed; broken bow to +shattered stern. At last he paused before it; and as in an already +over-clouded sky fresh troops of clouds will sometimes sail across, +so over the old man's face there now stole some such added gloom as +this. + +Stubb saw him pause; and perhaps intending, not vainly, though, to +evince his own unabated fortitude, and thus keep up a valiant place +in his Captain's mind, he advanced, and eyeing the wreck +exclaimed--"The thistle the ass refused; it pricked his mouth too +keenly, sir; ha! ha!" + +"What soulless thing is this that laughs before a wreck? Man, man! +did I not know thee brave as fearless fire (and as mechanical) I +could swear thou wert a poltroon. Groan nor laugh should be heard +before a wreck." + +"Aye, sir," said Starbuck drawing near, "'tis a solemn sight; an +omen, and an ill one." + +"Omen? omen?--the dictionary! If the gods think to speak outright to +man, they will honourably speak outright; not shake their heads, and +give an old wives' darkling hint.--Begone! Ye two are the opposite +poles of one thing; Starbuck is Stubb reversed, and Stubb is +Starbuck; and ye two are all mankind; and Ahab stands alone among the +millions of the peopled earth, nor gods nor men his neighbors! Cold, +cold--I shiver!--How now? Aloft there! D'ye see him? Sing out for +every spout, though he spout ten times a second!" + +The day was nearly done; only the hem of his golden robe was +rustling. Soon, it was almost dark, but the look-out men still +remained unset. + +"Can't see the spout now, sir;--too dark"--cried a voice from the +air. + +"How heading when last seen?" + +"As before, sir,--straight to leeward." + +"Good! he will travel slower now 'tis night. Down royals and +top-gallant stun-sails, Mr. Starbuck. We must not run over him +before morning; he's making a passage now, and may heave-to a while. +Helm there! keep her full before the wind!--Aloft! come down!--Mr. +Stubb, send a fresh hand to the fore-mast head, and see it manned +till morning."--Then advancing towards the doubloon in the +main-mast--"Men, this gold is mine, for I earned it; but I shall let +it abide here till the White Whale is dead; and then, whosoever of ye +first raises him, upon the day he shall be killed, this gold is that +man's; and if on that day I shall again raise him, then, ten times +its sum shall be divided among all of ye! Away now!--the deck is +thine, sir!" + +And so saying, he placed himself half way within the scuttle, and +slouching his hat, stood there till dawn, except when at intervals +rousing himself to see how the night wore on. + + + +CHAPTER 134 + +The Chase--Second Day. + + +At day-break, the three mast-heads were punctually manned afresh. + +"D'ye see him?" cried Ahab after allowing a little space for the +light to spread. + +"See nothing, sir." + +"Turn up all hands and make sail! he travels faster than I thought +for;--the top-gallant sails!--aye, they should have been kept on her +all night. But no matter--'tis but resting for the rush." + +Here be it said, that this pertinacious pursuit of one particular +whale, continued through day into night, and through night into day, +is a thing by no means unprecedented in the South sea fishery. For +such is the wonderful skill, prescience of experience, and invincible +confidence acquired by some great natural geniuses among the +Nantucket commanders; that from the simple observation of a whale +when last descried, they will, under certain given circumstances, +pretty accurately foretell both the direction in which he will +continue to swim for a time, while out of sight, as well as his +probable rate of progression during that period. And, in these +cases, somewhat as a pilot, when about losing sight of a coast, whose +general trending he well knows, and which he desires shortly to +return to again, but at some further point; like as this pilot stands +by his compass, and takes the precise bearing of the cape at present +visible, in order the more certainly to hit aright the remote, unseen +headland, eventually to be visited: so does the fisherman, at his +compass, with the whale; for after being chased, and diligently +marked, through several hours of daylight, then, when night obscures +the fish, the creature's future wake through the darkness is almost +as established to the sagacious mind of the hunter, as the pilot's +coast is to him. So that to this hunter's wondrous skill, the +proverbial evanescence of a thing writ in water, a wake, is to all +desired purposes well nigh as reliable as the steadfast land. And as +the mighty iron Leviathan of the modern railway is so familiarly +known in its every pace, that, with watches in their hands, men time +his rate as doctors that of a baby's pulse; and lightly say of it, +the up train or the down train will reach such or such a spot, at +such or such an hour; even so, almost, there are occasions when these +Nantucketers time that other Leviathan of the deep, according to the +observed humor of his speed; and say to themselves, so many hours +hence this whale will have gone two hundred miles, will have about +reached this or that degree of latitude or longitude. But to render +this acuteness at all successful in the end, the wind and the sea +must be the whaleman's allies; for of what present avail to the +becalmed or windbound mariner is the skill that assures him he is +exactly ninety-three leagues and a quarter from his port? Inferable +from these statements, are many collateral subtile matters touching +the chase of whales. + +The ship tore on; leaving such a furrow in the sea as when a +cannon-ball, missent, becomes a plough-share and turns up the level +field. + +"By salt and hemp!" cried Stubb, "but this swift motion of the deck +creeps up one's legs and tingles at the heart. This ship and I are +two brave fellows!--Ha, ha! Some one take me up, and launch me, +spine-wise, on the sea,--for by live-oaks! my spine's a keel. Ha, +ha! we go the gait that leaves no dust behind!" + +"There she blows--she blows!--she blows!--right ahead!" was now the +mast-head cry. + +"Aye, aye!" cried Stubb, "I knew it--ye can't escape--blow on and +split your spout, O whale! the mad fiend himself is after ye! blow +your trump--blister your lungs!--Ahab will dam off your blood, as a +miller shuts his watergate upon the stream!" + +And Stubb did but speak out for well nigh all that crew. The +frenzies of the chase had by this time worked them bubblingly up, +like old wine worked anew. Whatever pale fears and forebodings some +of them might have felt before; these were not only now kept out of +sight through the growing awe of Ahab, but they were broken up, and +on all sides routed, as timid prairie hares that scatter before the +bounding bison. The hand of Fate had snatched all their souls; and +by the stirring perils of the previous day; the rack of the past +night's suspense; the fixed, unfearing, blind, reckless way in which +their wild craft went plunging towards its flying mark; by all these +things, their hearts were bowled along. The wind that made great +bellies of their sails, and rushed the vessel on by arms invisible as +irresistible; this seemed the symbol of that unseen agency which so +enslaved them to the race. + +They were one man, not thirty. For as the one ship that held them +all; though it was put together of all contrasting things--oak, and +maple, and pine wood; iron, and pitch, and hemp--yet all these ran +into each other in the one concrete hull, which shot on its way, both +balanced and directed by the long central keel; even so, all the +individualities of the crew, this man's valor, that man's fear; guilt +and guiltiness, all varieties were welded into oneness, and were all +directed to that fatal goal which Ahab their one lord and keel did +point to. + +The rigging lived. The mast-heads, like the tops of tall palms, were +outspreadingly tufted with arms and legs. Clinging to a spar with +one hand, some reached forth the other with impatient wavings; +others, shading their eyes from the vivid sunlight, sat far out on +the rocking yards; all the spars in full bearing of mortals, ready +and ripe for their fate. Ah! how they still strove through that +infinite blueness to seek out the thing that might destroy them! + +"Why sing ye not out for him, if ye see him?" cried Ahab, when, after +the lapse of some minutes since the first cry, no more had been +heard. "Sway me up, men; ye have been deceived; not Moby Dick casts +one odd jet that way, and then disappears." + +It was even so; in their headlong eagerness, the men had mistaken +some other thing for the whale-spout, as the event itself soon +proved; for hardly had Ahab reached his perch; hardly was the rope +belayed to its pin on deck, when he struck the key-note to an +orchestra, that made the air vibrate as with the combined discharges +of rifles. The triumphant halloo of thirty buckskin lungs was heard, +as--much nearer to the ship than the place of the imaginary jet, less +than a mile ahead--Moby Dick bodily burst into view! For not by any +calm and indolent spoutings; not by the peaceable gush of that mystic +fountain in his head, did the White Whale now reveal his vicinity; +but by the far more wondrous phenomenon of breaching. Rising with +his utmost velocity from the furthest depths, the Sperm Whale thus +booms his entire bulk into the pure element of air, and piling up a +mountain of dazzling foam, shows his place to the distance of seven +miles and more. In those moments, the torn, enraged waves he shakes +off, seem his mane; in some cases, this breaching is his act of +defiance. + +"There she breaches! there she breaches!" was the cry, as in his +immeasurable bravadoes the White Whale tossed himself salmon-like to +Heaven. So suddenly seen in the blue plain of the sea, and relieved +against the still bluer margin of the sky, the spray that he raised, +for the moment, intolerably glittered and glared like a glacier; and +stood there gradually fading and fading away from its first sparkling +intensity, to the dim mistiness of an advancing shower in a vale. + +"Aye, breach your last to the sun, Moby Dick!" cried Ahab, "thy hour +and thy harpoon are at hand!--Down! down all of ye, but one man at +the fore. The boats!--stand by!" + +Unmindful of the tedious rope-ladders of the shrouds, the men, like +shooting stars, slid to the deck, by the isolated backstays and +halyards; while Ahab, less dartingly, but still rapidly was dropped +from his perch. + +"Lower away," he cried, so soon as he had reached his boat--a spare +one, rigged the afternoon previous. "Mr. Starbuck, the ship is +thine--keep away from the boats, but keep near them. Lower, all!" + +As if to strike a quick terror into them, by this time being the +first assailant himself, Moby Dick had turned, and was now coming for +the three crews. Ahab's boat was central; and cheering his men, he +told them he would take the whale head-and-head,--that is, pull +straight up to his forehead,--a not uncommon thing; for when within a +certain limit, such a course excludes the coming onset from the +whale's sidelong vision. But ere that close limit was gained, and +while yet all three boats were plain as the ship's three masts to his +eye; the White Whale churning himself into furious speed, almost in +an instant as it were, rushing among the boats with open jaws, and a +lashing tail, offered appalling battle on every side; and heedless of +the irons darted at him from every boat, seemed only intent on +annihilating each separate plank of which those boats were made. But +skilfully manoeuvred, incessantly wheeling like trained chargers in +the field; the boats for a while eluded him; though, at times, but by +a plank's breadth; while all the time, Ahab's unearthly slogan tore +every other cry but his to shreds. + +But at last in his untraceable evolutions, the White Whale so crossed +and recrossed, and in a thousand ways entangled the slack of the +three lines now fast to him, that they foreshortened, and, of +themselves, warped the devoted boats towards the planted irons in +him; though now for a moment the whale drew aside a little, as if to +rally for a more tremendous charge. Seizing that opportunity, Ahab +first paid out more line: and then was rapidly hauling and jerking +in upon it again--hoping that way to disencumber it of some +snarls--when lo!--a sight more savage than the embattled teeth of +sharks! + +Caught and twisted--corkscrewed in the mazes of the line, loose +harpoons and lances, with all their bristling barbs and points, came +flashing and dripping up to the chocks in the bows of Ahab's boat. +Only one thing could be done. Seizing the boat-knife, he critically +reached within--through--and then, without--the rays of steel; +dragged in the line beyond, passed it, inboard, to the bowsman, and +then, twice sundering the rope near the chocks--dropped the +intercepted fagot of steel into the sea; and was all fast again. +That instant, the White Whale made a sudden rush among the remaining +tangles of the other lines; by so doing, irresistibly dragged the +more involved boats of Stubb and Flask towards his flukes; dashed +them together like two rolling husks on a surf-beaten beach, and +then, diving down into the sea, disappeared in a boiling maelstrom, +in which, for a space, the odorous cedar chips of the wrecks danced +round and round, like the grated nutmeg in a swiftly stirred bowl of +punch. + +While the two crews were yet circling in the waters, reaching out +after the revolving line-tubs, oars, and other floating furniture, +while aslope little Flask bobbed up and down like an empty vial, +twitching his legs upwards to escape the dreaded jaws of sharks; and +Stubb was lustily singing out for some one to ladle him up; and while +the old man's line--now parting--admitted of his pulling into the +creamy pool to rescue whom he could;--in that wild simultaneousness +of a thousand concreted perils,--Ahab's yet unstricken boat seemed +drawn up towards Heaven by invisible wires,--as, arrow-like, shooting +perpendicularly from the sea, the White Whale dashed his broad +forehead against its bottom, and sent it, turning over and over, into +the air; till it fell again--gunwale downwards--and Ahab and his men +struggled out from under it, like seals from a sea-side cave. + +The first uprising momentum of the whale--modifying its direction as +he struck the surface--involuntarily launched him along it, to a +little distance from the centre of the destruction he had made; and +with his back to it, he now lay for a moment slowly feeling with his +flukes from side to side; and whenever a stray oar, bit of plank, the +least chip or crumb of the boats touched his skin, his tail swiftly +drew back, and came sideways smiting the sea. But soon, as if +satisfied that his work for that time was done, he pushed his pleated +forehead through the ocean, and trailing after him the intertangled +lines, continued his leeward way at a traveller's methodic pace. + +As before, the attentive ship having descried the whole fight, again +came bearing down to the rescue, and dropping a boat, picked up the +floating mariners, tubs, oars, and whatever else could be caught at, +and safely landed them on her decks. Some sprained shoulders, +wrists, and ankles; livid contusions; wrenched harpoons and lances; +inextricable intricacies of rope; shattered oars and planks; all +these were there; but no fatal or even serious ill seemed to have +befallen any one. As with Fedallah the day before, so Ahab was now +found grimly clinging to his boat's broken half, which afforded a +comparatively easy float; nor did it so exhaust him as the previous +day's mishap. + +But when he was helped to the deck, all eyes were fastened upon him; +as instead of standing by himself he still half-hung upon the +shoulder of Starbuck, who had thus far been the foremost to assist +him. His ivory leg had been snapped off, leaving but one short sharp +splinter. + +"Aye, aye, Starbuck, 'tis sweet to lean sometimes, be the leaner who +he will; and would old Ahab had leaned oftener than he has." + +"The ferrule has not stood, sir," said the carpenter, now coming up; +"I put good work into that leg." + +"But no bones broken, sir, I hope," said Stubb with true concern. + +"Aye! and all splintered to pieces, Stubb!--d'ye see it.--But even +with a broken bone, old Ahab is untouched; and I account no living +bone of mine one jot more me, than this dead one that's lost. Nor +white whale, nor man, nor fiend, can so much as graze old Ahab in his +own proper and inaccessible being. Can any lead touch yonder floor, +any mast scrape yonder roof?--Aloft there! which way?" + +"Dead to leeward, sir." + +"Up helm, then; pile on the sail again, ship keepers! down the rest +of the spare boats and rig them--Mr. Starbuck away, and muster the +boat's crews." + +"Let me first help thee towards the bulwarks, sir." + +"Oh, oh, oh! how this splinter gores me now! Accursed fate! that the +unconquerable captain in the soul should have such a craven mate!" + +"Sir?" + +"My body, man, not thee. Give me something for a cane--there, that +shivered lance will do. Muster the men. Surely I have not seen him +yet. By heaven it cannot be!--missing?--quick! call them all." + +The old man's hinted thought was true. Upon mustering the company, +the Parsee was not there. + +"The Parsee!" cried Stubb--"he must have been caught in--" + +"The black vomit wrench thee!--run all of ye above, alow, cabin, +forecastle--find him--not gone--not gone!" + +But quickly they returned to him with the tidings that the Parsee was +nowhere to be found. + +"Aye, sir," said Stubb--"caught among the tangles of your line--I +thought I saw him dragging under." + +"MY line! MY line? Gone?--gone? What means that little word?--What +death-knell rings in it, that old Ahab shakes as if he were the +belfry. The harpoon, too!--toss over the litter there,--d'ye see +it?--the forged iron, men, the white whale's--no, no, no,--blistered +fool! this hand did dart it!--'tis in the fish!--Aloft there! Keep +him nailed--Quick!--all hands to the rigging of the boats--collect +the oars--harpooneers! the irons, the irons!--hoist the royals higher--a +pull on all the sheets!--helm there! steady, steady for your life! +I'll ten times girdle the unmeasured globe; yea and dive straight +through it, but I'll slay him yet! + +"Great God! but for one single instant show thyself," cried Starbuck; +"never, never wilt thou capture him, old man--In Jesus' name no more +of this, that's worse than devil's madness. Two days chased; twice +stove to splinters; thy very leg once more snatched from under thee; +thy evil shadow gone--all good angels mobbing thee with warnings:-- +what more wouldst thou have?--Shall we keep chasing this murderous +fish till he swamps the last man? Shall we be dragged by him to the +bottom of the sea? Shall we be towed by him to the infernal world? +Oh, oh,--Impiety and blasphemy to hunt him more!" + +"Starbuck, of late I've felt strangely moved to thee; ever since that +hour we both saw--thou know'st what, in one another's eyes. But in +this matter of the whale, be the front of thy face to me as the palm +of this hand--a lipless, unfeatured blank. Ahab is for ever Ahab, +man. This whole act's immutably decreed. 'Twas rehearsed by thee +and me a billion years before this ocean rolled. Fool! I am the +Fates' lieutenant; I act under orders. Look thou, underling! that +thou obeyest mine.--Stand round me, men. Ye see an old man cut down +to the stump; leaning on a shivered lance; propped up on a lonely +foot. 'Tis Ahab--his body's part; but Ahab's soul's a centipede, +that moves upon a hundred legs. I feel strained, half stranded, as +ropes that tow dismasted frigates in a gale; and I may look so. But +ere I break, yell hear me crack; and till ye hear THAT, know that +Ahab's hawser tows his purpose yet. Believe ye, men, in the things +called omens? Then laugh aloud, and cry encore! For ere they drown, +drowning things will twice rise to the surface; then rise again, to +sink for evermore. So with Moby Dick--two days he's floated--tomorrow +will be the third. Aye, men, he'll rise once more,--but only to +spout his last! D'ye feel brave men, brave?" + +"As fearless fire," cried Stubb. + +"And as mechanical," muttered Ahab. Then as the men went forward, he +muttered on: "The things called omens! And yesterday I talked the +same to Starbuck there, concerning my broken boat. Oh! how valiantly +I seek to drive out of others' hearts what's clinched so fast in +mine!--The Parsee--the Parsee!--gone, gone? and he was to go +before:--but still was to be seen again ere I could perish--How's +that?--There's a riddle now might baffle all the lawyers backed by +the ghosts of the whole line of judges:--like a hawk's beak it pecks +my brain. I'LL, I'LL solve it, though!" + +When dusk descended, the whale was still in sight to leeward. + +So once more the sail was shortened, and everything passed nearly as +on the previous night; only, the sound of hammers, and the hum of the +grindstone was heard till nearly daylight, as the men toiled by +lanterns in the complete and careful rigging of the spare boats and +sharpening their fresh weapons for the morrow. Meantime, of the +broken keel of Ahab's wrecked craft the carpenter made him another +leg; while still as on the night before, slouched Ahab stood fixed +within his scuttle; his hid, heliotrope glance anticipatingly gone +backward on its dial; sat due eastward for the earliest sun. + + + +CHAPTER 135 + +The Chase.--Third Day. + + +The morning of the third day dawned fair and fresh, and once more the +solitary night-man at the fore-mast-head was relieved by crowds of +the daylight look-outs, who dotted every mast and almost every spar. + +"D'ye see him?" cried Ahab; but the whale was not yet in sight. + +"In his infallible wake, though; but follow that wake, that's all. +Helm there; steady, as thou goest, and hast been going. What a +lovely day again! were it a new-made world, and made for a +summer-house to the angels, and this morning the first of its +throwing open to them, a fairer day could not dawn upon that world. +Here's food for thought, had Ahab time to think; but Ahab never +thinks; he only feels, feels, feels; THAT'S tingling enough for +mortal man! to think's audacity. God only has that right and +privilege. Thinking is, or ought to be, a coolness and a calmness; +and our poor hearts throb, and our poor brains beat too much for +that. And yet, I've sometimes thought my brain was very calm--frozen +calm, this old skull cracks so, like a glass in which the contents +turned to ice, and shiver it. And still this hair is growing now; +this moment growing, and heat must breed it; but no, it's like that +sort of common grass that will grow anywhere, between the earthy +clefts of Greenland ice or in Vesuvius lava. How the wild winds blow +it; they whip it about me as the torn shreds of split sails lash the +tossed ship they cling to. A vile wind that has no doubt blown ere +this through prison corridors and cells, and wards of hospitals, and +ventilated them, and now comes blowing hither as innocent as fleeces. +Out upon it!--it's tainted. Were I the wind, I'd blow no more on +such a wicked, miserable world. I'd crawl somewhere to a cave, and +slink there. And yet, 'tis a noble and heroic thing, the wind! who +ever conquered it? In every fight it has the last and bitterest +blow. Run tilting at it, and you but run through it. Ha! a coward +wind that strikes stark naked men, but will not stand to receive a +single blow. Even Ahab is a braver thing--a nobler thing than THAT. +Would now the wind but had a body; but all the things that most +exasperate and outrage mortal man, all these things are bodiless, but +only bodiless as objects, not as agents. There's a most special, a +most cunning, oh, a most malicious difference! And yet, I say again, +and swear it now, that there's something all glorious and gracious in +the wind. These warm Trade Winds, at least, that in the clear +heavens blow straight on, in strong and steadfast, vigorous mildness; +and veer not from their mark, however the baser currents of the sea +may turn and tack, and mightiest Mississippies of the land swift and +swerve about, uncertain where to go at last. And by the eternal +Poles! these same Trades that so directly blow my good ship on; these +Trades, or something like them--something so unchangeable, and full +as strong, blow my keeled soul along! To it! Aloft there! What +d'ye see?" + +"Nothing, sir." + +"Nothing! and noon at hand! The doubloon goes a-begging! See the +sun! Aye, aye, it must be so. I've oversailed him. How, got the +start? Aye, he's chasing ME now; not I, HIM--that's bad; I might +have known it, too. Fool! the lines--the harpoons he's towing. Aye, +aye, I have run him by last night. About! about! Come down, all of +ye, but the regular look outs! Man the braces!" + +Steering as she had done, the wind had been somewhat on the Pequod's +quarter, so that now being pointed in the reverse direction, the +braced ship sailed hard upon the breeze as she rechurned the cream in +her own white wake. + +"Against the wind he now steers for the open jaw," murmured Starbuck +to himself, as he coiled the new-hauled main-brace upon the rail. +"God keep us, but already my bones feel damp within me, and from the +inside wet my flesh. I misdoubt me that I disobey my God in obeying +him!" + +"Stand by to sway me up!" cried Ahab, advancing to the hempen basket. +"We should meet him soon." + +"Aye, aye, sir," and straightway Starbuck did Ahab's bidding, and +once more Ahab swung on high. + +A whole hour now passed; gold-beaten out to ages. Time itself now +held long breaths with keen suspense. But at last, some three points +off the weather bow, Ahab descried the spout again, and instantly +from the three mast-heads three shrieks went up as if the tongues of +fire had voiced it. + +"Forehead to forehead I meet thee, this third time, Moby Dick! On +deck there!--brace sharper up; crowd her into the wind's eye. He's +too far off to lower yet, Mr. Starbuck. The sails shake! Stand over +that helmsman with a top-maul! So, so; he travels fast, and I must +down. But let me have one more good round look aloft here at the +sea; there's time for that. An old, old sight, and yet somehow so +young; aye, and not changed a wink since I first saw it, a boy, from +the sand-hills of Nantucket! The same!--the same!--the same to Noah +as to me. There's a soft shower to leeward. Such lovely +leewardings! They must lead somewhere--to something else than common +land, more palmy than the palms. Leeward! the white whale goes that +way; look to windward, then; the better if the bitterer quarter. But +good bye, good bye, old mast-head! What's this?--green? aye, tiny +mosses in these warped cracks. No such green weather stains on +Ahab's head! There's the difference now between man's old age and +matter's. But aye, old mast, we both grow old together; sound in our +hulls, though, are we not, my ship? Aye, minus a leg, that's all. +By heaven this dead wood has the better of my live flesh every way. +I can't compare with it; and I've known some ships made of dead trees +outlast the lives of men made of the most vital stuff of vital +fathers. What's that he said? he should still go before me, my +pilot; and yet to be seen again? But where? Will I have eyes at the +bottom of the sea, supposing I descend those endless stairs? and all +night I've been sailing from him, wherever he did sink to. Aye, aye, +like many more thou told'st direful truth as touching thyself, O +Parsee; but, Ahab, there thy shot fell short. Good-bye, +mast-head--keep a good eye upon the whale, the while I'm gone. We'll +talk to-morrow, nay, to-night, when the white whale lies down there, +tied by head and tail." + +He gave the word; and still gazing round him, was steadily lowered +through the cloven blue air to the deck. + +In due time the boats were lowered; but as standing in his shallop's +stern, Ahab just hovered upon the point of the descent, he waved to +the mate,--who held one of the tackle-ropes on deck--and bade him +pause. + +"Starbuck!" + +"Sir?" + +"For the third time my soul's ship starts upon this voyage, +Starbuck." + +"Aye, sir, thou wilt have it so." + +"Some ships sail from their ports, and ever afterwards are missing, +Starbuck!" + +"Truth, sir: saddest truth." + +"Some men die at ebb tide; some at low water; some at the full of the +flood;--and I feel now like a billow that's all one crested comb, +Starbuck. I am old;--shake hands with me, man." + +Their hands met; their eyes fastened; Starbuck's tears the glue. + +"Oh, my captain, my captain!--noble heart--go not--go not!--see, it's +a brave man that weeps; how great the agony of the persuasion then!" + +"Lower away!"--cried Ahab, tossing the mate's arm from him. "Stand +by the crew!" + +In an instant the boat was pulling round close under the stern. + +"The sharks! the sharks!" cried a voice from the low cabin-window +there; "O master, my master, come back!" + +But Ahab heard nothing; for his own voice was high-lifted then; and +the boat leaped on. + +Yet the voice spake true; for scarce had he pushed from the ship, +when numbers of sharks, seemingly rising from out the dark waters +beneath the hull, maliciously snapped at the blades of the oars, +every time they dipped in the water; and in this way accompanied the +boat with their bites. It is a thing not uncommonly happening to the +whale-boats in those swarming seas; the sharks at times apparently +following them in the same prescient way that vultures hover over the +banners of marching regiments in the east. But these were the first +sharks that had been observed by the Pequod since the White Whale had +been first descried; and whether it was that Ahab's crew were all +such tiger-yellow barbarians, and therefore their flesh more musky to +the senses of the sharks--a matter sometimes well known to affect +them,--however it was, they seemed to follow that one boat without +molesting the others. + +"Heart of wrought steel!" murmured Starbuck gazing over the side, and +following with his eyes the receding boat--"canst thou yet ring +boldly to that sight?--lowering thy keel among ravening sharks, and +followed by them, open-mouthed to the chase; and this the critical +third day?--For when three days flow together in one continuous +intense pursuit; be sure the first is the morning, the second the +noon, and the third the evening and the end of that thing--be that +end what it may. Oh! my God! what is this that shoots through me, +and leaves me so deadly calm, yet expectant,--fixed at the top of a +shudder! Future things swim before me, as in empty outlines and +skeletons; all the past is somehow grown dim. Mary, girl! thou +fadest in pale glories behind me; boy! I seem to see but thy eyes +grown wondrous blue. Strangest problems of life seem clearing; but +clouds sweep between--Is my journey's end coming? My legs feel +faint; like his who has footed it all day. Feel thy heart,--beats +it yet? Stir thyself, Starbuck!--stave it off--move, move! speak +aloud!--Mast-head there! See ye my boy's hand on the +hill?--Crazed;--aloft there!--keep thy keenest eye upon the boats:-- +mark well the whale!--Ho! again!--drive off that hawk! see! he +pecks--he tears the vane"--pointing to the red flag flying at the +main-truck--"Ha! he soars away with it!--Where's the old man now? +see'st thou that sight, oh Ahab!--shudder, shudder!" + +The boats had not gone very far, when by a signal from the +mast-heads--a downward pointed arm, Ahab knew that the whale had +sounded; but intending to be near him at the next rising, he held on +his way a little sideways from the vessel; the becharmed crew +maintaining the profoundest silence, as the head-beat waves hammered +and hammered against the opposing bow. + +"Drive, drive in your nails, oh ye waves! to their uttermost heads +drive them in! ye but strike a thing without a lid; and no coffin and +no hearse can be mine:--and hemp only can kill me! Ha! ha!" + +Suddenly the waters around them slowly swelled in broad circles; then +quickly upheaved, as if sideways sliding from a submerged berg of +ice, swiftly rising to the surface. A low rumbling sound was heard; +a subterraneous hum; and then all held their breaths; as bedraggled +with trailing ropes, and harpoons, and lances, a vast form shot +lengthwise, but obliquely from the sea. Shrouded in a thin drooping +veil of mist, it hovered for a moment in the rainbowed air; and then +fell swamping back into the deep. Crushed thirty feet upwards, the +waters flashed for an instant like heaps of fountains, then brokenly +sank in a shower of flakes, leaving the circling surface creamed like +new milk round the marble trunk of the whale. + +"Give way!" cried Ahab to the oarsmen, and the boats darted forward +to the attack; but maddened by yesterday's fresh irons that corroded +in him, Moby Dick seemed combinedly possessed by all the angels that +fell from heaven. The wide tiers of welded tendons overspreading his +broad white forehead, beneath the transparent skin, looked knitted +together; as head on, he came churning his tail among the boats; and +once more flailed them apart; spilling out the irons and lances from +the two mates' boats, and dashing in one side of the upper part of +their bows, but leaving Ahab's almost without a scar. + +While Daggoo and Queequeg were stopping the strained planks; and as +the whale swimming out from them, turned, and showed one entire flank +as he shot by them again; at that moment a quick cry went up. Lashed +round and round to the fish's back; pinioned in the turns upon turns +in which, during the past night, the whale had reeled the involutions +of the lines around him, the half torn body of the Parsee was seen; +his sable raiment frayed to shreds; his distended eyes turned full +upon old Ahab. + +The harpoon dropped from his hand. + +"Befooled, befooled!"--drawing in a long lean breath--"Aye, Parsee! +I see thee again.--Aye, and thou goest before; and this, THIS then is +the hearse that thou didst promise. But I hold thee to the last +letter of thy word. Where is the second hearse? Away, mates, to the +ship! those boats are useless now; repair them if ye can in time, and +return to me; if not, Ahab is enough to die--Down, men! the first +thing that but offers to jump from this boat I stand in, that thing I +harpoon. Ye are not other men, but my arms and my legs; and so obey +me.--Where's the whale? gone down again?" + +But he looked too nigh the boat; for as if bent upon escaping with +the corpse he bore, and as if the particular place of the last +encounter had been but a stage in his leeward voyage, Moby Dick was +now again steadily swimming forward; and had almost passed the +ship,--which thus far had been sailing in the contrary direction to +him, though for the present her headway had been stopped. He seemed +swimming with his utmost velocity, and now only intent upon pursuing +his own straight path in the sea. + +"Oh! Ahab," cried Starbuck, "not too late is it, even now, the third +day, to desist. See! Moby Dick seeks thee not. It is thou, thou, +that madly seekest him!" + +Setting sail to the rising wind, the lonely boat was swiftly impelled +to leeward, by both oars and canvas. And at last when Ahab was +sliding by the vessel, so near as plainly to distinguish Starbuck's +face as he leaned over the rail, he hailed him to turn the vessel +about, and follow him, not too swiftly, at a judicious interval. +Glancing upwards, he saw Tashtego, Queequeg, and Daggoo, eagerly +mounting to the three mast-heads; while the oarsmen were rocking in +the two staved boats which had but just been hoisted to the side, and +were busily at work in repairing them. One after the other, through +the port-holes, as he sped, he also caught flying glimpses of Stubb +and Flask, busying themselves on deck among bundles of new irons and +lances. As he saw all this; as he heard the hammers in the broken +boats; far other hammers seemed driving a nail into his heart. But +he rallied. And now marking that the vane or flag was gone from the +main-mast-head, he shouted to Tashtego, who had just gained that +perch, to descend again for another flag, and a hammer and nails, and +so nail it to the mast. + +Whether fagged by the three days' running chase, and the resistance +to his swimming in the knotted hamper he bore; or whether it was some +latent deceitfulness and malice in him: whichever was true, the White +Whale's way now began to abate, as it seemed, from the boat so +rapidly nearing him once more; though indeed the whale's last start +had not been so long a one as before. And still as Ahab glided over +the waves the unpitying sharks accompanied him; and so pertinaciously +stuck to the boat; and so continually bit at the plying oars, that +the blades became jagged and crunched, and left small splinters in +the sea, at almost every dip. + +"Heed them not! those teeth but give new rowlocks to your oars. Pull +on! 'tis the better rest, the shark's jaw than the yielding water." + +"But at every bite, sir, the thin blades grow smaller and smaller!" + +"They will last long enough! pull on!--But who can tell"--he +muttered--"whether these sharks swim to feast on the whale or on +Ahab?--But pull on! Aye, all alive, now--we near him. The helm! +take the helm! let me pass,"--and so saying two of the oarsmen helped +him forward to the bows of the still flying boat. + +At length as the craft was cast to one side, and ran ranging along +with the White Whale's flank, he seemed strangely oblivious of its +advance--as the whale sometimes will--and Ahab was fairly within the +smoky mountain mist, which, thrown off from the whale's spout, curled +round his great, Monadnock hump; he was even thus close to him; when, +with body arched back, and both arms lengthwise high-lifted to the +poise, he darted his fierce iron, and his far fiercer curse into the +hated whale. As both steel and curse sank to the socket, as if +sucked into a morass, Moby Dick sideways writhed; spasmodically +rolled his nigh flank against the bow, and, without staving a hole in +it, so suddenly canted the boat over, that had it not been for the +elevated part of the gunwale to which he then clung, Ahab would once +more have been tossed into the sea. As it was, three of the +oarsmen--who foreknew not the precise instant of the dart, and were +therefore unprepared for its effects--these were flung out; but so +fell, that, in an instant two of them clutched the gunwale again, and +rising to its level on a combing wave, hurled themselves bodily +inboard again; the third man helplessly dropping astern, but still +afloat and swimming. + +Almost simultaneously, with a mighty volition of ungraduated, +instantaneous swiftness, the White Whale darted through the weltering +sea. But when Ahab cried out to the steersman to take new turns with +the line, and hold it so; and commanded the crew to turn round on +their seats, and tow the boat up to the mark; the moment the +treacherous line felt that double strain and tug, it snapped in the +empty air! + +"What breaks in me? Some sinew cracks!--'tis whole again; oars! +oars! Burst in upon him!" + +Hearing the tremendous rush of the sea-crashing boat, the whale +wheeled round to present his blank forehead at bay; but in that +evolution, catching sight of the nearing black hull of the ship; +seemingly seeing in it the source of all his persecutions; bethinking +it--it may be--a larger and nobler foe; of a sudden, he bore down +upon its advancing prow, smiting his jaws amid fiery showers of foam. + +Ahab staggered; his hand smote his forehead. "I grow blind; hands! +stretch out before me that I may yet grope my way. Is't night?" + +"The whale! The ship!" cried the cringing oarsmen. + +"Oars! oars! Slope downwards to thy depths, O sea, that ere it be +for ever too late, Ahab may slide this last, last time upon his +mark! I see: the ship! the ship! Dash on, my men! Will ye not +save my ship?" + +But as the oarsmen violently forced their boat through the +sledge-hammering seas, the before whale-smitten bow-ends of two +planks burst through, and in an instant almost, the temporarily +disabled boat lay nearly level with the waves; its half-wading, +splashing crew, trying hard to stop the gap and bale out the pouring +water. + +Meantime, for that one beholding instant, Tashtego's mast-head hammer +remained suspended in his hand; and the red flag, half-wrapping him +as with a plaid, then streamed itself straight out from him, as his +own forward-flowing heart; while Starbuck and Stubb, standing upon +the bowsprit beneath, caught sight of the down-coming monster just as +soon as he. + +"The whale, the whale! Up helm, up helm! Oh, all ye sweet powers of +air, now hug me close! Let not Starbuck die, if die he must, in a +woman's fainting fit. Up helm, I say--ye fools, the jaw! the jaw! +Is this the end of all my bursting prayers? all my life-long +fidelities? Oh, Ahab, Ahab, lo, thy work. Steady! helmsman, steady. +Nay, nay! Up helm again! He turns to meet us! Oh, his +unappeasable brow drives on towards one, whose duty tells him he +cannot depart. My God, stand by me now!" + +"Stand not by me, but stand under me, whoever you are that will now +help Stubb; for Stubb, too, sticks here. I grin at thee, thou +grinning whale! Who ever helped Stubb, or kept Stubb awake, but +Stubb's own unwinking eye? And now poor Stubb goes to bed upon a +mattrass that is all too soft; would it were stuffed with brushwood! +I grin at thee, thou grinning whale! Look ye, sun, moon, and stars! +I call ye assassins of as good a fellow as ever spouted up his ghost. +For all that, I would yet ring glasses with ye, would ye but hand +the cup! Oh, oh! oh, oh! thou grinning whale, but there'll be plenty +of gulping soon! Why fly ye not, O Ahab! For me, off shoes and +jacket to it; let Stubb die in his drawers! A most mouldy and over +salted death, though;--cherries! cherries! cherries! Oh, Flask, for +one red cherry ere we die!" + +"Cherries? I only wish that we were where they grow. Oh, Stubb, I +hope my poor mother's drawn my part-pay ere this; if not, few coppers +will now come to her, for the voyage is up." + +From the ship's bows, nearly all the seamen now hung inactive; +hammers, bits of plank, lances, and harpoons, mechanically retained +in their hands, just as they had darted from their various +employments; all their enchanted eyes intent upon the whale, which +from side to side strangely vibrating his predestinating head, sent a +broad band of overspreading semicircular foam before him as he +rushed. Retribution, swift vengeance, eternal malice were in his +whole aspect, and spite of all that mortal man could do, the solid +white buttress of his forehead smote the ship's starboard bow, till +men and timbers reeled. Some fell flat upon their faces. Like +dislodged trucks, the heads of the harpooneers aloft shook on their +bull-like necks. Through the breach, they heard the waters pour, as +mountain torrents down a flume. + +"The ship! The hearse!--the second hearse!" cried Ahab from the +boat; "its wood could only be American!" + +Diving beneath the settling ship, the whale ran quivering along its +keel; but turning under water, swiftly shot to the surface again, far +off the other bow, but within a few yards of Ahab's boat, where, for +a time, he lay quiescent. + +"I turn my body from the sun. What ho, Tashtego! let me hear thy +hammer. Oh! ye three unsurrendered spires of mine; thou uncracked +keel; and only god-bullied hull; thou firm deck, and haughty helm, +and Pole-pointed prow,--death-glorious ship! must ye then perish, +and without me? Am I cut off from the last fond pride of meanest +shipwrecked captains? Oh, lonely death on lonely life! Oh, now I +feel my topmost greatness lies in my topmost grief. Ho, ho! from all +your furthest bounds, pour ye now in, ye bold billows of my whole +foregone life, and top this one piled comber of my death! Towards +thee I roll, thou all-destroying but unconquering whale; to the last +I grapple with thee; from hell's heart I stab at thee; for hate's +sake I spit my last breath at thee. Sink all coffins and all hearses +to one common pool! and since neither can be mine, let me then tow to +pieces, while still chasing thee, though tied to thee, thou damned +whale! THUS, I give up the spear!" + +The harpoon was darted; the stricken whale flew forward; with +igniting velocity the line ran through the grooves;--ran foul. Ahab +stooped to clear it; he did clear it; but the flying turn caught him +round the neck, and voicelessly as Turkish mutes bowstring their +victim, he was shot out of the boat, ere the crew knew he was gone. +Next instant, the heavy eye-splice in the rope's final end flew out +of the stark-empty tub, knocked down an oarsman, and smiting the sea, +disappeared in its depths. + +For an instant, the tranced boat's crew stood still; then turned. +"The ship? Great God, where is the ship?" Soon they through dim, +bewildering mediums saw her sidelong fading phantom, as in the +gaseous Fata Morgana; only the uppermost masts out of water; while +fixed by infatuation, or fidelity, or fate, to their once lofty +perches, the pagan harpooneers still maintained their sinking +lookouts on the sea. And now, concentric circles seized the lone +boat itself, and all its crew, and each floating oar, and every +lance-pole, and spinning, animate and inanimate, all round and round +in one vortex, carried the smallest chip of the Pequod out of sight. + +But as the last whelmings intermixingly poured themselves over the +sunken head of the Indian at the mainmast, leaving a few inches of +the erect spar yet visible, together with long streaming yards of the +flag, which calmly undulated, with ironical coincidings, over the +destroying billows they almost touched;--at that instant, a red arm +and a hammer hovered backwardly uplifted in the open air, in the act +of nailing the flag faster and yet faster to the subsiding spar. A +sky-hawk that tauntingly had followed the main-truck downwards from +its natural home among the stars, pecking at the flag, and +incommoding Tashtego there; this bird now chanced to intercept its +broad fluttering wing between the hammer and the wood; and +simultaneously feeling that etherial thrill, the submerged savage +beneath, in his death-gasp, kept his hammer frozen there; and so the +bird of heaven, with archangelic shrieks, and his imperial beak +thrust upwards, and his whole captive form folded in the flag of +Ahab, went down with his ship, which, like Satan, would not sink to +hell till she had dragged a living part of heaven along with her, and +helmeted herself with it. + +Now small fowls flew screaming over the yet yawning gulf; a sullen +white surf beat against its steep sides; then all collapsed, and the +great shroud of the sea rolled on as it rolled five thousand years +ago. + + + +Epilogue + +"AND I ONLY AM ESCAPED ALONE TO TELL THEE" +Job. + +The drama's done. Why then here does any one step forth?--Because +one did survive the wreck. + +It so chanced, that after the Parsee's disappearance, I was he whom +the Fates ordained to take the place of Ahab's bowsman, when that +bowsman assumed the vacant post; the same, who, when on the last day +the three men were tossed from out of the rocking boat, was dropped +astern. So, floating on the margin of the ensuing scene, and in full +sight of it, when the halfspent suction of the sunk ship reached me, +I was then, but slowly, drawn towards the closing vortex. When I +reached it, it had subsided to a creamy pool. Round and round, then, +and ever contracting towards the button-like black bubble at the axis +of that slowly wheeling circle, like another Ixion I did revolve. +Till, gaining that vital centre, the black bubble upward burst; and +now, liberated by reason of its cunning spring, and, owing to its +great buoyancy, rising with great force, the coffin life-buoy shot +lengthwise from the sea, fell over, and floated by my side. Buoyed +up by that coffin, for almost one whole day and night, I floated on a +soft and dirgelike main. The unharming sharks, they glided by as if +with padlocks on their mouths; the savage sea-hawks sailed with +sheathed beaks. On the second day, a sail drew near, nearer, and +picked me up at last. It was the devious-cruising Rachel, that in +her retracing search after her missing children, only found another +orphan. + + + + +End of this Project Gutenberg etext of Moby Dick, by Herman Melville + diff --git a/okio-assetfilesystem/src/androidTest/kotlin/okio/assetfilesystem/AssetFileSystemTest.kt b/okio-assetfilesystem/src/androidTest/kotlin/okio/assetfilesystem/AssetFileSystemTest.kt new file mode 100644 index 00000000..719cbe4b --- /dev/null +++ b/okio-assetfilesystem/src/androidTest/kotlin/okio/assetfilesystem/AssetFileSystemTest.kt @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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.assetfilesystem + +import androidx.test.platform.app.InstrumentationRegistry +import assertk.assertThat +import assertk.assertions.containsAll +import assertk.assertions.containsExactly +import assertk.assertions.hasMessage +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import assertk.assertions.isTrue +import kotlin.test.assertFailsWith +import okio.Buffer +import okio.BufferedSource +import okio.FileHandle +import okio.FileNotFoundException +import okio.IOException +import okio.Path.Companion.toPath +import okio.buffer +import org.junit.Test + +class AssetFileSystemTest { + private val context = InstrumentationRegistry.getInstrumentation().context + private val fs = context.assets.asFileSystem() + + @Test fun canonicalizeValid() { + assertThat(fs.canonicalize("/".toPath())).isEqualTo("/".toPath()) + assertThat(fs.canonicalize(".".toPath())).isEqualTo("/".toPath()) + assertThat(fs.canonicalize("not/a/path/../../..".toPath())).isEqualTo("/".toPath()) + assertThat(fs.canonicalize("file.txt".toPath())).isEqualTo("/file.txt".toPath()) + assertThat(fs.canonicalize("stuff/../file.txt".toPath())).isEqualTo("/file.txt".toPath()) + assertThat(fs.canonicalize("dir".toPath())).isEqualTo("/dir".toPath()) + assertThat(fs.canonicalize("dir/whevs/..".toPath())).isEqualTo("/dir".toPath()) + assertThat(fs.canonicalize("dir/nested.txt".toPath())).isEqualTo("/dir/nested.txt".toPath()) + assertThat(fs.canonicalize("dir/whevs/../nested.txt".toPath())).isEqualTo("/dir/nested.txt".toPath()) + } + + @Test fun canonicalizeInvalidThrows() { + assertFailsWith<FileNotFoundException> { + fs.canonicalize("not/a/path".toPath()) + } + } + + @Test fun listRoot() { + val list = fs.list("/".toPath()) + assertThat(list).containsAll( + "dir".toPath(), + "file.txt".toPath(), + "moby10b.txt".toPath(), + ) + } + + @Test fun listRootCanonicalizes() { + val list = fs.list("foo/bar/../..".toPath()) + assertThat(list).containsAll( + "dir".toPath(), + "file.txt".toPath(), + "moby10b.txt".toPath(), + ) + } + + @Test fun listDirectory() { + val list = fs.list("dir".toPath()) + assertThat(list).containsExactly("nested.txt".toPath()) + } + + @Test fun listDirectoryCanonicalizes() { + val list = fs.list("dir/not/real/../..".toPath()) + assertThat(list).containsExactly("nested.txt".toPath()) + } + + @Test fun listNonExistentDirectoryThrows() { + assertFailsWith<FileNotFoundException> { + fs.list("nope/".toPath()) + } + } + + @Test fun listFileThrows() { + assertFailsWith<FileNotFoundException> { + fs.list("dir/nested.txt".toPath()) + } + } + + @Test fun listOrNullDirectory() { + val list = fs.listOrNull("dir".toPath()) + assertThat(list).isNotNull().containsExactly("nested.txt".toPath()) + } + + @Test fun listOrNullDirectoryCanonicalizes() { + val list = fs.listOrNull("dir/not/real/../..".toPath()) + assertThat(list).isNotNull().containsExactly("nested.txt".toPath()) + } + + @Test fun listOrNullNonExistentDirectory() { + val list = fs.listOrNull("nope".toPath()) + assertThat(list).isNull() + } + + @Test fun listOrNullFile() { + val list = fs.listOrNull("dir/nested.txt".toPath()) + assertThat(list).isNull() + } + + @Test fun metadataFile() { + val metadata = fs.metadataOrNull("file.txt".toPath())!! + + // Data we can get: + assertThat(metadata.isRegularFile).isTrue() + assertThat(metadata.isDirectory).isFalse() + + // Data we cannot get or is impossible: + assertThat(metadata.size).isNull() + assertThat(metadata.symlinkTarget).isNull() + assertThat(metadata.createdAtMillis).isNull() + assertThat(metadata.lastModifiedAtMillis).isNull() + assertThat(metadata.lastAccessedAtMillis).isNull() + assertThat(metadata.extras).isEmpty() + } + + @Test fun metadataDirectory() { + val metadata = fs.metadataOrNull("dir".toPath())!! + + // Data we can get: + assertThat(metadata.isRegularFile).isFalse() + assertThat(metadata.isDirectory).isTrue() + + // Data we cannot get or is impossible: + assertThat(metadata.symlinkTarget).isNull() + assertThat(metadata.size).isNull() + assertThat(metadata.createdAtMillis).isNull() + assertThat(metadata.lastModifiedAtMillis).isNull() + assertThat(metadata.lastAccessedAtMillis).isNull() + assertThat(metadata.extras).isEmpty() + } + + @Test fun metadataDirectoryCanonicalizes() { + val metadata = fs.metadataOrNull("dir/not/real/../..".toPath())!! + assertThat(metadata.isDirectory).isTrue() + } + + @Test fun metadataNonExistentPath() { + val metadata = fs.metadataOrNull("not/a/path".toPath()) + assertThat(metadata).isNull() + } + + @Test fun sourceFile() { + val file = fs.source("file.txt".toPath()).buffer().use(BufferedSource::readUtf8) + assertThat(file).isEqualTo("File!\n") + val nested = fs.source("dir/nested.txt".toPath()).buffer().use(BufferedSource::readUtf8) + assertThat(nested).isEqualTo("Nested!\n") + } + + @Test fun sourceDirectory() { + assertFailsWith<FileNotFoundException> { + fs.source("dir".toPath()) + } + } + + @Test fun sourceNonExistent() { + assertFailsWith<FileNotFoundException> { + fs.source("not/a/path".toPath()) + } + } + + @Test fun openReadOnlyInvalidThrows() { + assertFailsWith<FileNotFoundException> { + fs.openReadOnly("not/a/path".toPath()) + } + } + + @Test fun openReadOnlyDirectoryThrows() { + assertFailsWith<FileNotFoundException> { + fs.openReadOnly("dir".toPath()) + } + } + + @Test fun openReadOnlySize() { + val smallSize = fs.openReadOnly("file.txt".toPath()).use(FileHandle::size) + assertThat(smallSize).isEqualTo(6) + + val mobySize = fs.openReadOnly("moby10b.txt".toPath()).use(FileHandle::size) + assertThat(mobySize).isEqualTo(1232923) + } + + @Test fun openReadOnlyRandomAccessForward() { + fs.openReadOnly("moby10b.txt".toPath()).use { handle -> + val buffer = Buffer() + handle.read(34251, buffer, 16) + assertThat(buffer.readUtf8()).isEqualTo("Call me Ishmael.") + handle.read(148605, buffer, 49) + assertThat(buffer.readUtf8()).isEqualTo("It is not down in any map; true places never are.") + handle.read(1051694, buffer, 50) + assertThat(buffer.readUtf8()).isEqualTo("his forehead's veins swelled like overladen brooks") + handle.read(148605, buffer, 49) + assertThat(buffer.readUtf8()).isEqualTo("It is not down in any map; true places never are.") + handle.read(34251, buffer, 16) + assertThat(buffer.readUtf8()).isEqualTo("Call me Ishmael.") + } + } + + @Test fun openReadOnlyRandomAccessThenSize() { + fs.openReadOnly("moby10b.txt".toPath()).use { handle -> + val buffer = Buffer() + handle.read(34251, buffer, 16) + assertThat(buffer.readUtf8()).isEqualTo("Call me Ishmael.") + + assertThat(handle.size()).isEqualTo(1232923) + } + } + + @Test fun openReadOnlyFlushThrows() { + fs.openReadOnly("file.txt".toPath()).use { handle -> + val t = assertFailsWith<IllegalStateException> { + handle.flush() + } + assertThat(t).hasMessage("file handle is read-only") + } + } + + @Test fun openReadOnlyResizeThrows() { + fs.openReadOnly("file.txt".toPath()).use { handle -> + val t = assertFailsWith<IllegalStateException> { + handle.resize(10L) + } + assertThat(t).hasMessage("file handle is read-only") + } + } + + @Test fun openReadOnlyWriteThrows() { + fs.openReadOnly("file.txt".toPath()).use { handle -> + val t = assertFailsWith<IllegalStateException> { + handle.write(0, Buffer().writeUtf8("Sup"), 3) + } + assertThat(t).hasMessage("file handle is read-only") + } + } + + @Test fun sinkThrows() { + val t = assertFailsWith<IOException> { + fs.sink("file.txt".toPath()) + } + assertThat(t).hasMessage("asset file systems are read-only") + } + + @Test fun appendingSinkThrows() { + val t = assertFailsWith<IOException> { + fs.appendingSink("file.txt".toPath()) + } + assertThat(t).hasMessage("asset file systems are read-only") + } + + @Test fun createDirectoryThrows() { + val t = assertFailsWith<IOException> { + fs.createDirectory("new-dir".toPath(), mustCreate = true) + } + assertThat(t).hasMessage("asset file systems are read-only") + } + + @Test fun atomicMoveThrows() { + val t = assertFailsWith<IOException> { + fs.atomicMove("file.txt".toPath(), "new-file.txt".toPath()) + } + assertThat(t).hasMessage("asset file systems are read-only") + } + + @Test fun deleteThrows() { + val t = assertFailsWith<IOException> { + fs.delete("file.txt".toPath()) + } + assertThat(t).hasMessage("asset file systems are read-only") + } + + @Test fun createSymlinkThrows() { + val t = assertFailsWith<IOException> { + fs.createSymlink("file.txt".toPath(), "new-file.txt".toPath()) + } + assertThat(t).hasMessage("asset file systems are read-only") + } + + @Test fun openReadWriteThrows() { + val t = assertFailsWith<IOException> { + fs.openReadWrite("file.txt".toPath()) + } + assertThat(t).hasMessage("asset file systems are read-only") + } +} diff --git a/okio-assetfilesystem/src/main/kotlin/okio/assetfilesystem/AssetFileSystem.kt b/okio-assetfilesystem/src/main/kotlin/okio/assetfilesystem/AssetFileSystem.kt new file mode 100644 index 00000000..b63dca20 --- /dev/null +++ b/okio-assetfilesystem/src/main/kotlin/okio/assetfilesystem/AssetFileSystem.kt @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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.assetfilesystem + +import android.content.res.AssetManager +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import okio.FileHandle +import okio.FileMetadata +import okio.FileSystem +import okio.Path +import okio.Path.Companion.toPath +import okio.Sink +import okio.Source +import okio.source + +/** + * Expose this [AssetManager] as an Okio [FileSystem]. + * + * Note: Assets are a read-only view on a file system and so any attempt to mutate + * will throw an [IOException]. + */ +fun AssetManager.asFileSystem(): FileSystem = AssetFileSystem(this) + +private class AssetFileSystem( + private val assets: AssetManager, +) : FileSystem() { + override fun canonicalize(path: Path): Path { + val canonical = canonicalizeInternal(path) + if (canonical.existsInternal()) { + return canonical + } + throw FileNotFoundException("$path") + } + + private fun canonicalizeInternal(path: Path) = ROOT.resolve(path, normalize = true) + + private fun Path.toAssetRelativePathString(): String { + return toString().removePrefix("/") + } + + /** + * Determine if [this] is a valid path to a file or directory. + * + * If this function returns true, a call to [AssetManager.open] will either return successfully + * or throw [FileNotFoundException] based on whether [this] is a file or directory, respectively. + */ + private fun Path.existsInternal(): Boolean { + if (this == ROOT) { + return true + } + + // Both non-existent paths and paths to existing files return an empty array when listing. + // Determine if a path exists by checking if its name is present in the parent's list. + val parent = checkNotNull(parent) { "Path has no parent. Did you canonicalize? $this" } + val children = assets.list(parent.toAssetRelativePathString()).orEmpty() + return name in children + } + + override fun metadataOrNull(path: Path): FileMetadata? { + val canonical = canonicalizeInternal(path) + if (canonical.existsInternal()) { + val pathString = canonical.toAssetRelativePathString() + return try { + assets.open(pathString).close() + FileMetadata( + isRegularFile = true, + isDirectory = false, + ) + } catch (_: FileNotFoundException) { + FileMetadata( + isRegularFile = false, + isDirectory = true, + ) + } + } + return null + } + + override fun list(dir: Path): List<Path> { + val canonical = canonicalizeInternal(dir) + if (canonical.existsInternal()) { + val pathString = canonical.toAssetRelativePathString() + try { + // This will throw if the path points to a file. + assets.open(pathString).close() + } catch (_: FileNotFoundException) { + return assets.list(pathString) + ?.map { it.toPath() } + .orEmpty() + } + } + throw FileNotFoundException("$dir") + } + + override fun listOrNull(dir: Path): List<Path>? { + return try { + list(dir) + } catch (_: IOException) { + null + } + } + + override fun openReadOnly(file: Path): FileHandle { + val pathString = canonicalizeInternal(file).toAssetRelativePathString() + val inputStream = assets.open(pathString) + return AssetFileHandle(assets, pathString, inputStream) + } + + override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { + throw IOException("asset file systems are read-only") + } + + override fun source(file: Path): Source { + return assets.open(canonicalizeInternal(file).toAssetRelativePathString()).source() + } + + override fun sink(file: Path, mustCreate: Boolean): Sink { + throw IOException("asset file systems are read-only") + } + + override fun appendingSink(file: Path, mustExist: Boolean): Sink { + throw IOException("asset file systems are read-only") + } + + override fun createDirectory(dir: Path, mustCreate: Boolean) { + throw IOException("asset file systems are read-only") + } + + override fun atomicMove(source: Path, target: Path) { + throw IOException("asset file systems are read-only") + } + + override fun delete(path: Path, mustExist: Boolean) { + throw IOException("asset file systems are read-only") + } + + override fun createSymlink(source: Path, target: Path) { + throw IOException("asset file systems are read-only") + } + + private companion object { + val ROOT = "/".toPath() + } +} + +private class AssetFileHandle( + private val assets: AssetManager, + private val pathString: String, + private var inputStream: InputStream, +) : FileHandle(false) { + private var currentOffset = 0 + private var size = -1 + + override fun protectedRead( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ): Int { + // If we need to jump backwards or have reached the end of the file, + // close the existing stream and open a new one. + if (currentOffset > fileOffset || currentOffset == size) { + inputStream.close() + inputStream = assets.open(pathString) + currentOffset = 0 + } + + while (true) { + val skip = fileOffset - currentOffset + if (skip == 0L) break + val skipped = inputStream.skip(skip).toInt() + if (skipped == 0) { + // Since we know skip is never negative, a skip of 0 means EOF. + // Record this as the file size to trigger stream recreation. + size = currentOffset + throw IllegalArgumentException("fileOffset $fileOffset > size $size") + } + currentOffset += skipped + } + + val read = inputStream.read(array, arrayOffset, byteCount) + if (read == -1) { + // A read of -1 means EOF. Record this as the file size to trigger stream recreation. + size = currentOffset + } else { + currentOffset += read + } + return read + } + + override fun protectedSize(): Long { + if (size == -1) { + while (true) { + val skipped = inputStream.skip(1024 * 1024).toInt() + if (skipped == 0) { + size = currentOffset + break + } + currentOffset += skipped + } + } + return size.toLong() + } + + override fun protectedClose() { + inputStream.close() + } + + override fun protectedWrite( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ) { + throw AssertionError() + } + + override fun protectedFlush() { + throw AssertionError() + } + + override fun protectedResize(size: Long) { + throw AssertionError() + } +} diff --git a/okio-bom/build.gradle.kts b/okio-bom/build.gradle.kts new file mode 100644 index 00000000..8417d761 --- /dev/null +++ b/okio-bom/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("com.vanniktech.maven.publish.base") + id("java-platform") +} + +collectBomConstraints() + +extensions.configure<PublishingExtension> { + publications.create("maven", MavenPublication::class) { + from(project.components.getByName("javaPlatform")) + } +} diff --git a/okio-fakefilesystem/README.md b/okio-fakefilesystem/README.md new file mode 100644 index 00000000..5460c503 --- /dev/null +++ b/okio-fakefilesystem/README.md @@ -0,0 +1,4 @@ +Okio Fake File System +--------------------- + +This module contains an in-memory file system. diff --git a/okio-fakefilesystem/api/okio-fakefilesystem.api b/okio-fakefilesystem/api/okio-fakefilesystem.api new file mode 100644 index 00000000..1d319e43 --- /dev/null +++ b/okio-fakefilesystem/api/okio-fakefilesystem.api @@ -0,0 +1,41 @@ +public final class okio/fakefilesystem/FakeFileSystem : okio/FileSystem { + public final field clock Lkotlinx/datetime/Clock; + public fun <init> ()V + public fun <init> (Lkotlinx/datetime/Clock;)V + public synthetic fun <init> (Lkotlinx/datetime/Clock;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun allPaths ()Ljava/util/Set; + public fun appendingSink (Lokio/Path;Z)Lokio/Sink; + public fun atomicMove (Lokio/Path;Lokio/Path;)V + public fun canonicalize (Lokio/Path;)Lokio/Path; + public final fun checkNoOpenFiles ()V + public fun createDirectory (Lokio/Path;Z)V + public fun createSymlink (Lokio/Path;Lokio/Path;)V + public fun delete (Lokio/Path;Z)V + public final fun emulateUnix ()V + public final fun emulateWindows ()V + public final fun getAllowClobberingEmptyDirectories ()Z + public final fun getAllowDeletingOpenFiles ()Z + public final fun getAllowMovingOpenFiles ()Z + public final fun getAllowReadsWhileWriting ()Z + public final fun getAllowSymlinks ()Z + public final fun getAllowWritesWhileWriting ()Z + public final fun getWorkingDirectory ()Lokio/Path; + public fun list (Lokio/Path;)Ljava/util/List; + public fun listOrNull (Lokio/Path;)Ljava/util/List; + public fun metadataOrNull (Lokio/Path;)Lokio/FileMetadata; + public final fun openPaths ()Ljava/util/List; + public fun openReadOnly (Lokio/Path;)Lokio/FileHandle; + public fun openReadWrite (Lokio/Path;ZZ)Lokio/FileHandle; + public final fun setAllowClobberingEmptyDirectories (Z)V + public final fun setAllowDeletingOpenFiles (Z)V + public final fun setAllowMovingOpenFiles (Z)V + public final fun setAllowReadsWhileWriting (Z)V + public final fun setAllowSymlinks (Z)V + public final fun setAllowWritesWhileWriting (Z)V + public final fun setExtra (Lokio/Path;Lkotlin/reflect/KClass;Ljava/lang/Object;)V + public final fun setWorkingDirectory (Lokio/Path;)V + public fun sink (Lokio/Path;Z)Lokio/Sink; + public fun source (Lokio/Path;)Lokio/Source; + public fun toString ()Ljava/lang/String; +} + diff --git a/okio-fakefilesystem/build.gradle.kts b/okio-fakefilesystem/build.gradle.kts new file mode 100644 index 00000000..afebff16 --- /dev/null +++ b/okio-fakefilesystem/build.gradle.kts @@ -0,0 +1,79 @@ +import com.vanniktech.maven.publish.JavadocJar.Dokka +import com.vanniktech.maven.publish.KotlinMultiplatform +import com.vanniktech.maven.publish.MavenPublishBaseExtension + +plugins { + kotlin("multiplatform") + id("org.jetbrains.dokka") + id("com.vanniktech.maven.publish.base") + id("binary-compatibility-validator") + id("build-support") +} + +kotlin { + jvm { + } + if (kmpJsEnabled) { + js { + compilations.all { + kotlinOptions { + moduleKind = "umd" + sourceMap = true + metaInfo = true + } + } + nodejs { + testTask { + useMocha { + timeout = "30s" + } + } + } + browser { + } + } + } + if (kmpNativeEnabled) { + configureOrCreateNativePlatforms() + } + sourceSets { + val commonMain by getting { + dependencies { + api(libs.kotlin.time) + api(projects.okio) + } + } + val commonTest by getting + if (kmpWasmEnabled) { + // Add support for wasmWasi when https://github.com/Kotlin/kotlinx-datetime/issues/324 is resolved. + configureOrCreateWasmPlatform(wasi = false) + createSourceSet("wasmMain", parent = commonMain, children = listOf("wasmJs")) + createSourceSet("wasmTest", parent = commonTest, children = listOf("wasmJs")) + } + } +} + +tasks { + val jvmJar by getting(Jar::class) { + // BundleTaskConvention() crashes unless there's a 'main' source set. + sourceSets.create(SourceSet.MAIN_SOURCE_SET_NAME) + val bndConvention = aQute.bnd.gradle.BundleTaskConvention(this) + bndConvention.setBnd( + """ + Export-Package: okio.fakefilesystem + Automatic-Module-Name: okio.fakefilesystem + Bundle-SymbolicName: com.squareup.okio.fakefilesystem + """ + ) + // Call the convention when the task has finished to modify the jar to contain OSGi metadata. + doLast { + bndConvention.buildBundle() + } + } +} + +configure<MavenPublishBaseExtension> { + configure( + KotlinMultiplatform(javadocJar = Dokka("dokkaGfm")) + ) +} diff --git a/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FakeFileSystem.kt b/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FakeFileSystem.kt new file mode 100644 index 00000000..fb2bd655 --- /dev/null +++ b/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FakeFileSystem.kt @@ -0,0 +1,768 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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.fakefilesystem + +import kotlin.jvm.JvmField +import kotlin.jvm.JvmName +import kotlin.reflect.KClass +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import okio.ArrayIndexOutOfBoundsException +import okio.Buffer +import okio.ByteString +import okio.FileHandle +import okio.FileMetadata +import okio.FileNotFoundException +import okio.FileSystem +import okio.IOException +import okio.Path +import okio.Path.Companion.toPath +import okio.Sink +import okio.Source +import okio.fakefilesystem.FakeFileSystem.Element.Directory +import okio.fakefilesystem.FakeFileSystem.Element.File +import okio.fakefilesystem.FakeFileSystem.Element.Symlink +import okio.fakefilesystem.FakeFileSystem.Operation.READ +import okio.fakefilesystem.FakeFileSystem.Operation.WRITE + +/** + * A fully in-memory file system useful for testing. It includes features to support writing + * better tests. + * + * Use [openPaths] to see which paths have been opened for read or write, but not yet closed. Tests + * should call [checkNoOpenFiles] in `tearDown()` to confirm that no file streams were leaked. + * + * Strict By Default + * ----------------- + * + * These actions are not allowed and throw an [IOException] if attempted: + * + * * Moving a file that is currently open for reading or writing. + * * Deleting a file that is currently open for reading or writing. + * * Moving a file to a path that currently resolves to an empty directory. + * * Reading and writing the same file at the same time. + * * Opening a file for writing that is already open for writing. + * + * Programs that do not attempt any of the above operations should work fine on both UNIX and + * Windows systems. Relax these constraints individually or call [emulateWindows] or [emulateUnix]; + * to apply the constraints of a particular operating system. + */ +class FakeFileSystem( + @JvmField + val clock: Clock = Clock.System, +) : FileSystem() { + + /** File system roots. Each element is a Directory and is created on-demand. */ + private val roots = mutableMapOf<Path, Directory>() + + /** Files that are currently open and need to be closed to avoid resource leaks. */ + private val openFiles = mutableListOf<OpenFile>() + + /** + * An absolute path with this file system's current working directory. Relative paths will be + * resolved against this directory when they are used. + */ + var workingDirectory: Path = "/".toPath() + set(value) { + require(value.isAbsolute) { + "expected an absolute path but was $value" + } + field = value + } + + /** + * True to allow files to be moved even if they're currently open for read or write. UNIX file + * systems typically allow open files to be moved; Windows file systems do not. + */ + var allowMovingOpenFiles = false + + /** + * True to allow files to be deleted even if they're currently open for read or write. UNIX file + * systems typically allow open files to be deleted; Windows file systems do not. + */ + var allowDeletingOpenFiles = false + + /** + * True to allow the target of an [atomicMove] operation to be an empty directory. Windows file + * systems typically allow files to replace empty directories; UNIX file systems do not. + */ + var allowClobberingEmptyDirectories = false + + /** + * True to permit a file to have multiple [sinks][sink] open at the same time. Both Windows and + * UNIX file systems permit this but the result may be undefined. + */ + var allowWritesWhileWriting = false + + /** + * True to permit a file to have a [source] and [sink] open at the same time. Both Windows and + * UNIX file systems permit this but the result may be undefined. + */ + var allowReadsWhileWriting = false + + /** + * True to allow symlinks to be created. UNIX file systems typically allow symlinks; Windows file + * systems do not. Setting this to false after creating a symlink does not prevent that symlink + * from being returned or used. + */ + var allowSymlinks = false + + /** + * Canonical paths for every file and directory in this file system. This omits file system roots + * like `C:\` and `/`. + */ + @get:JvmName("allPaths") + val allPaths: Set<Path> + get() { + val result = mutableListOf<Path>() + for (path in roots.keys) { + result += listRecursively(path) + } + result.sort() + return result.toSet() + } + + /** + * Canonical paths currently opened for reading or writing in the order they were opened. This may + * contain duplicates if a single path is open by multiple readers. + * + * Note that this may contain paths not present in [allPaths]. This occurs if a file is deleted + * while it is still open. + * + * The returned list is ordered by the order that the paths were opened. + */ + @get:JvmName("openPaths") + val openPaths: List<Path> + get() = openFiles.map { it.canonicalPath } + + /** + * Confirm that all files that have been opened on this file system (with [source], [sink], and + * [appendingSink]) have since been closed. Call this in your test's `tearDown()` function to + * confirm that your program hasn't leaked any open files. + * + * Forgetting to close a file on a real file system is a severe error that may lead to a program + * crash. The operating system enforces a limit on how many files may be open simultaneously. On + * Linux this is [getrlimit] and is commonly adjusted with the `ulimit` command. + * + * [getrlimit]: https://man7.org/linux/man-pages/man2/getrlimit.2.html + * + * @throws IllegalStateException if any files are open when this function is called. + */ + fun checkNoOpenFiles() { + val firstOpenFile = openFiles.firstOrNull() ?: return + throw IllegalStateException( + """ + |expected 0 open files, but found: + | ${openFiles.joinToString(separator = "\n ") { it.canonicalPath.toString() }} + """.trimMargin(), + firstOpenFile.backtrace, + ) + } + + /** + * Configure this file system to use a Windows-like working directory (`F:\`, unless the working + * directory is already Windows-like) and to follow a Windows-like policy on what operations + * are permitted. + */ + fun emulateWindows() { + if ("\\" !in workingDirectory.toString()) { + workingDirectory = "F:\\".toPath() + } + allowMovingOpenFiles = false + allowDeletingOpenFiles = false + allowClobberingEmptyDirectories = true + allowWritesWhileWriting = true + allowReadsWhileWriting = true + } + + /** + * Configure this file system to use a UNIX-like working directory (`/`, unless the working + * directory is already UNIX-like) and to follow a UNIX-like policy on what operations are + * permitted. + */ + fun emulateUnix() { + if ("/" !in workingDirectory.toString()) { + workingDirectory = "/".toPath() + } + allowMovingOpenFiles = true + allowDeletingOpenFiles = true + allowClobberingEmptyDirectories = false + allowWritesWhileWriting = true + allowReadsWhileWriting = true + allowSymlinks = true + } + + override fun canonicalize(path: Path): Path { + val canonicalPath = canonicalizeInternal(path) + + val lookupResult = lookupPath(canonicalPath) + if (lookupResult?.element == null) { + throw FileNotFoundException("no such file: $path") + } + + return lookupResult.path + } + + /** Don't throw [FileNotFoundException] if the path doesn't identify a file. */ + private fun canonicalizeInternal(path: Path): Path { + return workingDirectory.resolve(path, normalize = true) + } + + /** + * Sets the metadata of type [type] on [path] to [value]. If [value] is null this clears that + * metadata. + * + * Extras are not copied by [copy] but they are moved with [atomicMove]. + * + * @throws IOException if [path] does not exist. + */ + @Throws(IOException::class) + fun <T : Any> setExtra(path: Path, type: KClass<out T>, value: T?) { + val canonicalPath = canonicalizeInternal(path) + val lookupResult = lookupPath( + canonicalPath = canonicalPath, + createRootOnDemand = canonicalPath.isRoot, + resolveLastSymlink = false, + ) + val element = lookupResult?.element ?: throw FileNotFoundException("no such file: $path") + if (value == null) { + element.extras.remove(type) + } else { + element.extras[type] = value + } + } + + override fun metadataOrNull(path: Path): FileMetadata? { + val canonicalPath = canonicalizeInternal(path) + val lookupResult = lookupPath( + canonicalPath = canonicalPath, + createRootOnDemand = canonicalPath.isRoot, + resolveLastSymlink = false, + ) + return lookupResult?.element?.metadata + } + + override fun list(dir: Path): List<Path> = list(dir, throwOnFailure = true)!! + + override fun listOrNull(dir: Path): List<Path>? = list(dir, throwOnFailure = false) + + private fun list(dir: Path, throwOnFailure: Boolean): List<Path>? { + val canonicalPath = canonicalizeInternal(dir) + val lookupResult = lookupPath(canonicalPath) + if (lookupResult?.element == null) { + if (throwOnFailure) throw FileNotFoundException("no such directory: $dir") else return null + } + val element = lookupResult.element as? Directory + ?: if (throwOnFailure) throw IOException("not a directory: $dir") else return null + + element.access(now = clock.now()) + return element.children.keys.map { dir / it }.sorted() + } + + override fun source(file: Path): Source { + val fileHandle = openReadOnly(file) + return fileHandle.source() + .also { fileHandle.close() } + } + + override fun sink(file: Path, mustCreate: Boolean): Sink { + val fileHandle = open(file, readWrite = true, mustCreate = mustCreate) + fileHandle.resize(0L) // If the file already has data, get rid of it. + return fileHandle.sink() + .also { fileHandle.close() } + } + + override fun appendingSink(file: Path, mustExist: Boolean): Sink { + val fileHandle = open(file, readWrite = true, mustExist = mustExist) + return fileHandle.appendingSink() + .also { fileHandle.close() } + } + + override fun openReadOnly(file: Path): FileHandle { + return open(file, readWrite = false) + } + + override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { + return open(file, readWrite = true, mustCreate = mustCreate, mustExist = mustExist) + } + + private fun open( + file: Path, + readWrite: Boolean, + mustCreate: Boolean = false, + mustExist: Boolean = false, + ): FileHandle { + require(!mustCreate || !mustExist) { + "Cannot require mustCreate and mustExist at the same time." + } + + val canonicalPath = canonicalizeInternal(file) + val lookupResult = lookupPath(canonicalPath, createRootOnDemand = readWrite) + val now = clock.now() + val element: File + val operation: Operation + + if (lookupResult?.element == null && mustExist) { + throw IOException("$file doesn't exist.") + } + if (lookupResult?.element != null && mustCreate) { + throw IOException("$file already exists.") + } + + if (readWrite) { + // Note that this case is used for both write and read/write. + if (lookupResult?.element is Directory) { + throw IOException("destination is a directory: $file") + } + if (!allowWritesWhileWriting) { + findOpenFile(canonicalPath, operation = WRITE)?.let { + throw IOException("file is already open for writing $file", it.backtrace) + } + } + if (!allowReadsWhileWriting) { + findOpenFile(canonicalPath, operation = READ)?.let { + throw IOException("file is already open for reading $file", it.backtrace) + } + } + + val parent = lookupResult?.parent + ?: throw FileNotFoundException("parent directory does not exist") + parent.access(now, true) + + val existing = lookupResult.element + element = File(createdAt = existing?.createdAt ?: now) + parent.children[lookupResult.segment!!] = element + operation = WRITE + + if (existing is File) { + element.data = existing.data + } + } else { + val existing = lookupResult?.element + ?: throw FileNotFoundException("no such file: $file") + element = existing as? File ?: throw IOException("not a file: $file") + operation = READ + + if (!allowReadsWhileWriting) { + findOpenFile(canonicalPath, operation = WRITE)?.let { + throw IOException("file is already open for writing $file", it.backtrace) + } + } + } + + element.access(now = clock.now(), modified = readWrite) + + val openFile = OpenFile(canonicalPath, operation, Exception("file opened for $operation here")) + openFiles += openFile + + return FakeFileHandle( + readWrite = readWrite, + openFile = openFile, + file = element, + ) + } + + override fun createDirectory(dir: Path, mustCreate: Boolean) { + val canonicalPath = canonicalizeInternal(dir) + + val lookupResult = lookupPath(canonicalPath, createRootOnDemand = true) + + if (canonicalPath.isRoot) { + // Looking it up was sufficient. Don't crash when creating roots that already exist. + return + } + + if (mustCreate && lookupResult?.element != null) { + throw IOException("already exists: $dir") + } + + val parentDirectory = lookupResult.requireParent() + parentDirectory.children[canonicalPath.nameBytes] = Directory(createdAt = clock.now()) + } + + override fun atomicMove( + source: Path, + target: Path, + ) { + val canonicalSource = canonicalizeInternal(source) + val canonicalTarget = canonicalizeInternal(target) + + val targetLookupResult = lookupPath(canonicalTarget, createRootOnDemand = true) + val sourceLookupResult = lookupPath(canonicalSource, resolveLastSymlink = false) + + // Universal constraints. + if (targetLookupResult?.element is Directory) { + throw IOException("target is a directory: $target") + } + val targetParent = targetLookupResult.requireParent() + if (!allowMovingOpenFiles) { + findOpenFile(canonicalSource)?.let { + throw IOException("source is open $source", it.backtrace) + } + findOpenFile(canonicalTarget)?.let { + throw IOException("target is open $target", it.backtrace) + } + } + if (!allowClobberingEmptyDirectories) { + if (sourceLookupResult?.element is Directory && targetLookupResult?.element is File) { + throw IOException("source is a directory and target is a file") + } + } + + val sourceParent = sourceLookupResult.requireParent() + val removed = sourceParent.children.remove(canonicalSource.nameBytes) + ?: throw FileNotFoundException("source doesn't exist: $source") + targetParent.children[canonicalTarget.nameBytes] = removed + } + + override fun delete(path: Path, mustExist: Boolean) { + val canonicalPath = canonicalizeInternal(path) + + val lookupResult = lookupPath( + canonicalPath = canonicalPath, + createRootOnDemand = true, + resolveLastSymlink = false, + ) + + if (lookupResult?.element == null) { + if (mustExist) { + throw FileNotFoundException("no such file: $path") + } else { + return + } + } + + if (lookupResult.element is Directory && lookupResult.element.children.isNotEmpty()) { + throw IOException("non-empty directory") + } + + if (!allowDeletingOpenFiles) { + findOpenFile(canonicalPath)?.let { + throw IOException("file is open $path", it.backtrace) + } + } + + val directory = lookupResult.requireParent() + directory.children.remove(canonicalPath.nameBytes) + } + + override fun createSymlink( + source: Path, + target: Path, + ) { + val canonicalSource = canonicalizeInternal(source) + + val existingLookupResult = lookupPath(canonicalSource, createRootOnDemand = true) + if (existingLookupResult?.element != null) { + throw IOException("already exists: $source") + } + val parent = existingLookupResult.requireParent() + + if (!allowSymlinks) { + throw IOException("symlinks are not supported") + } + + parent.children[canonicalSource.nameBytes] = Symlink(createdAt = clock.now(), target) + } + + /** + * Walks the file system looking for [canonicalPath], following symlinks encountered along the + * way. This function is designed to be used both when looking up existing files and when creating + * new files into an existing directory. + * + * It returns either: + * + * * a path lookup result with an element if that file or directory or symlink exists. This is + * useful when reading or writing an existing fie. + * + * * a path lookup result that only got as far as the canonical path's parent, if the parent + * exists but the child file does not. This is useful when creating a new file. + * + * * null, if not even the parent directory exists. A file cannot yet be created with this path + * because there is no parent to attach it to. + * + * This will create the root of the returned path if it does not exist. + * + * @param canonicalPath a normalized path, typically the result of [FakeFileSystem.canonicalizeInternal]. + * @param recurseCount used internally to detect cycles. + * @param resolveLastSymlink true if the result's element must not itself be a symlink. Use this + * for looking up metadata, or operations that apply to the path like delete and move. We + * always follow symlinks for enclosing directories. + * @param createRootOnDemand true to create a root directory like `C:\` or `/` if it doesn't + * exist. Pass true for mutating operations. + */ + private fun lookupPath( + canonicalPath: Path, + recurseCount: Int = 0, + resolveLastSymlink: Boolean = true, + createRootOnDemand: Boolean = false, + ): PathLookupResult? { + // 40 is chosen for consistency with the Linux kernel (which previously used 8). + if (recurseCount > 40) { + throw IOException("symlink cycle?") + } + + val rootPath = canonicalPath.root!! + var root = roots[rootPath] + + // If the path is a root, create it on demand. + if (root == null) { + if (!createRootOnDemand) return null + root = Directory(createdAt = clock.now()) + roots[rootPath] = root + } + + var parent: Directory? = null + var lastSegment: ByteString? = null + var current: Element = root + var currentPath: Path = rootPath + + var segmentsTraversed = 0 + val segments = canonicalPath.segmentsBytes + for (segment in segments) { + lastSegment = segment + + // Push the newest segment. + if (current !is Directory) { + throw IOException("not a directory: $currentPath") + } + parent = current + current = current.children[segment] ?: break + currentPath /= segment + segmentsTraversed++ + + // If it's a symlink, recurse to follow it. + val isLastSegment = segmentsTraversed == segments.size + val followSymlinks = !isLastSegment || resolveLastSymlink + if (current is Symlink && followSymlinks) { + current.access(now = clock.now()) + // We wanna normalize it in case the target is relative and starts with `..`. + currentPath = currentPath.parent!!.resolve(current.target, normalize = true) + val symlinkLookupResult = lookupPath( + canonicalPath = currentPath, + recurseCount = recurseCount + 1, + createRootOnDemand = createRootOnDemand, + ) ?: break + parent = symlinkLookupResult.parent + lastSegment = symlinkLookupResult.segment + current = symlinkLookupResult.element ?: break + currentPath = symlinkLookupResult.path + } + } + + return when (segmentsTraversed) { + segments.size -> { + PathLookupResult(currentPath, parent, lastSegment, current) // The file. + } + segments.size - 1 -> { + PathLookupResult(currentPath, parent, lastSegment, null) // The enclosing directory. + } + else -> null // We found nothing. + } + } + + private class PathLookupResult( + /** The canonical path for the looked up path or its enclosing directory. */ + val path: Path, + /** Only null if the looked up path is a root. */ + val parent: Directory?, + /** Only null if the looked up path is a root. */ + val segment: ByteString?, + /** Non-null if this is a root. Also not null if this file exists. */ + val element: Element?, + ) + + private fun PathLookupResult?.requireParent(): Directory { + return this?.parent ?: throw IOException("parent directory does not exist") + } + + private sealed class Element( + val createdAt: Instant, + ) { + var lastModifiedAt: Instant = createdAt + var lastAccessedAt: Instant = createdAt + val extras = mutableMapOf<KClass<*>, Any>() + + class File(createdAt: Instant) : Element(createdAt) { + var data: ByteString = ByteString.EMPTY + + override val metadata: FileMetadata + get() = FileMetadata( + isRegularFile = true, + size = data.size.toLong(), + createdAt = createdAt, + lastModifiedAt = lastModifiedAt, + lastAccessedAt = lastAccessedAt, + extras = extras, + ) + } + + class Directory(createdAt: Instant) : Element(createdAt) { + /** Keys are path segments. */ + val children = mutableMapOf<ByteString, Element>() + + override val metadata: FileMetadata + get() = FileMetadata( + isDirectory = true, + createdAt = createdAt, + lastModifiedAt = lastModifiedAt, + lastAccessedAt = lastAccessedAt, + extras = extras, + ) + } + + class Symlink( + createdAt: Instant, + /** This may be an absolute or relative path. */ + val target: Path, + ) : Element(createdAt) { + override val metadata: FileMetadata + get() = FileMetadata( + symlinkTarget = target, + createdAt = createdAt, + lastModifiedAt = lastModifiedAt, + lastAccessedAt = lastAccessedAt, + extras = extras, + ) + } + + fun access( + now: Instant, + modified: Boolean = false, + ) { + lastAccessedAt = now + if (modified) { + lastModifiedAt = now + } + } + + abstract val metadata: FileMetadata + } + + private fun findOpenFile( + canonicalPath: Path, + operation: Operation? = null, + ): OpenFile? { + return openFiles.firstOrNull { + it.canonicalPath == canonicalPath && (operation == null || operation == it.operation) + } + } + + private fun checkOffsetAndCount( + size: Long, + offset: Long, + byteCount: Long, + ) { + if (offset or byteCount < 0 || offset > size || size - offset < byteCount) { + throw ArrayIndexOutOfBoundsException("size=$size offset=$offset byteCount=$byteCount") + } + } + + private class OpenFile( + val canonicalPath: Path, + val operation: Operation, + val backtrace: Throwable, + ) + + private enum class Operation { + READ, + WRITE, + } + + private inner class FakeFileHandle( + readWrite: Boolean, + private val openFile: OpenFile, + private val file: File, + ) : FileHandle(readWrite) { + private var closed = false + + override fun protectedResize(size: Long) { + check(!closed) { "closed" } + + val delta = size - file.data.size + if (delta > 0) { + file.data = Buffer() + .write(file.data) + .write(ByteArray(delta.toInt())) + .readByteString() + } else { + file.data = file.data.substring(0, size.toInt()) + } + + file.access(now = clock.now(), modified = true) + } + + override fun protectedSize(): Long { + check(!closed) { "closed" } + return file.data.size.toLong() + } + + override fun protectedRead( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ): Int { + check(!closed) { "closed" } + checkOffsetAndCount(array.size.toLong(), arrayOffset.toLong(), byteCount.toLong()) + + val fileOffsetInt = fileOffset.toInt() + val toCopy = minOf(file.data.size - fileOffsetInt, byteCount) + if (toCopy <= 0) return -1 + for (i in 0 until toCopy) { + array[i + arrayOffset] = file.data[i + fileOffsetInt] + } + return toCopy + } + + override fun protectedWrite( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ) { + check(!closed) { "closed" } + checkOffsetAndCount(array.size.toLong(), arrayOffset.toLong(), byteCount.toLong()) + + val buffer = Buffer() + buffer.write(file.data, 0, minOf(fileOffset.toInt(), file.data.size)) + while (buffer.size < fileOffset) { + buffer.writeByte(0) + } + buffer.write(array, arrayOffset, byteCount) + if (buffer.size < file.data.size) { + buffer.write(file.data, buffer.size.toInt(), file.data.size - buffer.size.toInt()) + } + file.data = buffer.snapshot() + file.access(now = clock.now(), modified = true) + } + + override fun protectedFlush() { + check(!closed) { "closed" } + } + + override fun protectedClose() { + if (closed) return + closed = true + file.access(now = clock.now(), modified = readWrite) + openFiles -= openFile + } + + override fun toString() = "FileHandler(${openFile.canonicalPath})" + } + + override fun toString() = "FakeFileSystem" +} diff --git a/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FileMetadataCommon.kt b/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FileMetadataCommon.kt new file mode 100644 index 00000000..303e12a9 --- /dev/null +++ b/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FileMetadataCommon.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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. + */ +@file:JvmName("-Time") + +package okio.fakefilesystem + +import kotlin.jvm.JvmName +import kotlin.reflect.KClass +import kotlinx.datetime.Instant +import okio.FileMetadata +import okio.Path + +@JvmName("newFileMetadata") +internal fun FileMetadata( + isRegularFile: Boolean = false, + isDirectory: Boolean = false, + symlinkTarget: Path? = null, + size: Long? = null, + createdAt: Instant? = null, + lastModifiedAt: Instant? = null, + lastAccessedAt: Instant? = null, + extras: Map<KClass<*>, Any> = mapOf(), +): FileMetadata { + return FileMetadata( + isRegularFile = isRegularFile, + isDirectory = isDirectory, + symlinkTarget = symlinkTarget, + size = size, + createdAtMillis = createdAt?.toEpochMilliseconds(), + lastModifiedAtMillis = lastModifiedAt?.toEpochMilliseconds(), + lastAccessedAtMillis = lastAccessedAt?.toEpochMilliseconds(), + extras = extras, + ) +} diff --git a/okio-nodefilesystem/build.gradle.kts b/okio-nodefilesystem/build.gradle.kts new file mode 100644 index 00000000..0509e4e5 --- /dev/null +++ b/okio-nodefilesystem/build.gradle.kts @@ -0,0 +1,63 @@ +import com.vanniktech.maven.publish.JavadocJar.Dokka +import com.vanniktech.maven.publish.KotlinJs +import com.vanniktech.maven.publish.MavenPublishBaseExtension + +plugins { + kotlin("js") + id("org.jetbrains.dokka") + id("com.vanniktech.maven.publish.base") + id("binary-compatibility-validator") +} + +kotlin { + js { + configure(listOf(compilations.getByName("main"), compilations.getByName("test"))) { + tasks.getByName(compileKotlinTaskName) { + kotlinOptions { + moduleKind = "umd" + sourceMap = true + metaInfo = true + } + } + } + nodejs { + testTask { + useMocha { + timeout = "30s" + } + } + } + } + sourceSets { + all { + languageSettings.optIn("kotlin.RequiresOptIn") + } + matching { it.name.endsWith("Test") }.all { + languageSettings { + optIn("kotlin.time.ExperimentalTime") + } + } + val main by getting { + dependencies { + implementation(projects.okio) + // Uncomment this to generate fs.fs.module_node.kt. Use it when updating fs.kt. + // implementation(npm("@types/node", "14.14.16", true)) + } + } + val test by getting { + dependencies { + implementation(libs.kotlin.test) + implementation(libs.kotlin.time) + + implementation(projects.okioFakefilesystem) + implementation(projects.okioTestingSupport) + } + } + } +} + +configure<MavenPublishBaseExtension> { + configure( + KotlinJs(javadocJar = Dokka("dokkaGfm")) + ) +} diff --git a/okio-nodefilesystem/src/main/kotlin/okio/FileSink.kt b/okio-nodefilesystem/src/main/kotlin/okio/FileSink.kt new file mode 100644 index 00000000..904adc63 --- /dev/null +++ b/okio-nodefilesystem/src/main/kotlin/okio/FileSink.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 + +internal class FileSink( + private val fd: Number, +) : Sink { + private var closed = false + + override fun write(source: Buffer, byteCount: Long) { + require(byteCount >= 0L) { "byteCount < 0: $byteCount" } + require(source.size >= byteCount) { "source.size=${source.size} < byteCount=$byteCount" } + check(!closed) { "closed" } + + val data = source.readByteArray(byteCount) + val writtenByteCount = writeSync(fd, data) + if (writtenByteCount.toLong() != byteCount) { + throw IOException("expected $byteCount but was $writtenByteCount") + } + } + + override fun flush() { + } + + override fun timeout(): Timeout { + return Timeout.NONE + } + + override fun close() { + if (closed) return + closed = true + closeSync(fd) + } +} diff --git a/okio-nodefilesystem/src/main/kotlin/okio/FileSource.kt b/okio-nodefilesystem/src/main/kotlin/okio/FileSource.kt new file mode 100644 index 00000000..a363a9cc --- /dev/null +++ b/okio-nodefilesystem/src/main/kotlin/okio/FileSource.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 + +internal class FileSource( + private val fd: Number, +) : Source { + private var position_ = 0L + private var closed = false + + override fun read(sink: Buffer, byteCount: Long): Long { + require(byteCount >= 0L) { "byteCount < 0: $byteCount" } + check(!closed) { "closed" } + + val data = ByteArray(byteCount.toInt()) + val readByteCount = readSync( + fd = fd, + buffer = data, + length = byteCount.toDouble(), + offset = 0.0, + position = position_.toDouble(), + ).toInt() + + if (readByteCount == 0) return -1L + + position_ += readByteCount + + sink.write(data, offset = 0, byteCount = readByteCount) + + return readByteCount.toLong() + } + + override fun timeout(): Timeout = Timeout.NONE + + override fun close() { + if (closed) return + closed = true + closeSync(fd) + } +} diff --git a/okio-nodefilesystem/src/main/kotlin/okio/FsJs.kt b/okio-nodefilesystem/src/main/kotlin/okio/FsJs.kt new file mode 100644 index 00000000..2a308f06 --- /dev/null +++ b/okio-nodefilesystem/src/main/kotlin/okio/FsJs.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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. + */ + +/** + * This class declares the subset of Node.js file system APIs that we need in Okio. + * + * + * Why not Dukat? + * -------------- + * + * This file does manually what ideally [Dukat] would do automatically. + * + * Dukat's generated stubs need awkward call sites to disambiguate overloads. For example, to call + * `mkdirSync()` we must specify an options parameter even though we just want the default: + * + * mkdirSync(dir.toString(), options = undefined as MakeDirectoryOptions?) + * + * By defining our own externals, we can omit the unwanted optional parameter from the declaration. + * This leads to nicer calling code! + * + * mkdirSync(dir.toString()) + * + * Dukat also gets the nullability wrong for `Dirent.readSync()`. + * + * + * Why not Kotlinx-nodejs? + * ----------------------- + * + * Even better than using Dukat directly would be to use the [official artifact][kotlinx_nodejs], + * itself generated with Dukat. We also don't use the official Node.js artifact for the reasons + * above, and also because it has an unstable API. + * + * + * Updating this file + * ------------------ + * + * To declare new external APIs, run Dukat to generate a full set of Node stubs. The easiest way to + * do this is to add an NPM dependency on `@types/node` in `jsMain`, like this: + * + * ``` + * jsMain { + * ... + * dependencies { + * implementation(npm("@types/node", "14.14.16", true)) + * ... + * } + * } + * ``` + * + * This will create a file with a full set of APIs to copy-paste from. + * + * ``` + * okio/build/externals/okio-parent-okio/src/fs.fs.module_node.kt + * ``` + * + * [Dukat]: https://github.com/kotlin/dukat + * [kotlinx_nodejs]: https://github.com/Kotlin/kotlinx-nodejs + */ +@file:JsModule("fs") +@file:JsNonModule + +package okio + +import kotlin.js.Date + +internal external fun closeSync(fd: Number) + +internal external fun mkdirSync(path: String): String? + +internal external fun openSync(path: String, flags: String): Double + +internal external fun opendirSync(path: String): Dir + +internal external fun readlinkSync(path: String): String + +internal external fun readSync(fd: Number, buffer: ByteArray, offset: Double, length: Double, position: Double?): Double + +internal external fun realpathSync(path: String): String + +internal external fun renameSync(oldPath: String, newPath: String) + +internal external fun rmdirSync(path: String) + +internal external fun lstatSync(path: String): Stats + +internal external fun fstatSync(fd: Number): Stats + +internal external fun unlinkSync(path: String) + +internal external fun writeSync(fd: Number, buffer: ByteArray): Double + +internal external fun writeSync(fd: Number, buffer: ByteArray, offset: Double, length: Double, position: Double): Double + +internal external fun ftruncateSync(fd: Number, len: Double) + +internal external fun symlinkSync(target: String, path: String) + +internal open external class Dir { + open var path: String + open fun closeSync() + + // Note that dukat's signature of readSync() returns a non-nullable Dirent; that's incorrect. + open fun readSync(): Dirent? +} + +internal open external class Dirent { + open fun isFile(): Boolean + open fun isDirectory(): Boolean + open fun isBlockDevice(): Boolean + open fun isCharacterDevice(): Boolean + open fun isSymbolicLink(): Boolean + open fun isFIFO(): Boolean + open fun isSocket(): Boolean + open var name: String +} + +internal external interface StatsBase<T> { + fun isFile(): Boolean + fun isDirectory(): Boolean + fun isBlockDevice(): Boolean + fun isCharacterDevice(): Boolean + fun isSymbolicLink(): Boolean + fun isFIFO(): Boolean + fun isSocket(): Boolean + var dev: T + var ino: T + var mode: T + var nlink: T + var uid: T + var gid: T + var rdev: T + var size: T + var blksize: T + var blocks: T + var atimeMs: T + var mtimeMs: T + var ctimeMs: T + var birthtimeMs: T + var atime: Date + var mtime: Date + var ctime: Date + var birthtime: Date +} + +internal open external class Stats : StatsBase<Number> { + override fun isFile(): Boolean + override fun isDirectory(): Boolean + override fun isBlockDevice(): Boolean + override fun isCharacterDevice(): Boolean + override fun isSymbolicLink(): Boolean + override fun isFIFO(): Boolean + override fun isSocket(): Boolean + override var dev: Number + override var ino: Number + override var mode: Number + override var nlink: Number + override var uid: Number + override var gid: Number + override var rdev: Number + override var size: Number + override var blksize: Number + override var blocks: Number + override var atimeMs: Number + override var mtimeMs: Number + override var ctimeMs: Number + override var birthtimeMs: Number + override var atime: Date + override var mtime: Date + override var ctime: Date + override var birthtime: Date +} diff --git a/okio-nodefilesystem/src/main/kotlin/okio/NodeJsFileHandle.kt b/okio-nodefilesystem/src/main/kotlin/okio/NodeJsFileHandle.kt new file mode 100644 index 00000000..4b40d41b --- /dev/null +++ b/okio-nodefilesystem/src/main/kotlin/okio/NodeJsFileHandle.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + +internal class NodeJsFileHandle( + private val fd: Number, + readWrite: Boolean, +) : FileHandle(readWrite) { + override fun protectedSize(): Long { + val stats = fstatSync(fd) + return stats.size.toLong() + } + + override fun protectedRead( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ): Int { + val readByteCount = readSync( + fd = fd, + buffer = array, + length = byteCount.toDouble(), + offset = arrayOffset.toDouble(), + position = fileOffset.toDouble(), + ).toInt() + + if (readByteCount == 0) return -1 + + return readByteCount + } + + override fun protectedWrite( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ) { + val writtenByteCount = writeSync( + fd = fd, + buffer = array, + offset = arrayOffset.toDouble(), + length = byteCount.toDouble(), + position = fileOffset.toDouble(), + ) + + if (writtenByteCount.toInt() != byteCount) { + throw IOException("expected $byteCount but was $writtenByteCount") + } + } + + override fun protectedFlush() { + } + + override fun protectedResize(size: Long) { + ftruncateSync(fd, size.toDouble()) + } + + override fun protectedClose() { + closeSync(fd) + } +} diff --git a/okio-nodefilesystem/src/main/kotlin/okio/NodeJsFileSystem.kt b/okio-nodefilesystem/src/main/kotlin/okio/NodeJsFileSystem.kt new file mode 100644 index 00000000..2c7cf418 --- /dev/null +++ b/okio-nodefilesystem/src/main/kotlin/okio/NodeJsFileSystem.kt @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 okio.Path.Companion.toPath + +/** + * Use [Node.js APIs][node_fs] to implement the Okio file system interface. + * + * This class needs to make calls to some fs APIs that have multiple competing overloads. To + * unambiguously select an overload this passes `undefined` as the target type to some functions. + * + * [node_fs]: https://nodejs.org/dist/latest-v14.x/docs/api/fs.html + */ +object NodeJsFileSystem : FileSystem() { + private var S_IFMT = 0xf000 // fs.constants.S_IFMT + private var S_IFREG = 0x8000 // fs.constants.S_IFREG + private var S_IFDIR = 0x4000 // fs.constants.S_IFDIR + private var S_IFLNK = 0xa000 // fs.constants.S_IFLNK + + override fun canonicalize(path: Path): Path { + try { + val canonicalPath = realpathSync(path.toString()) + return canonicalPath.toPath() + } catch (e: Throwable) { + throw e.toIOException() + } + } + + override fun metadataOrNull(path: Path): FileMetadata? { + val pathString = path.toString() + val stat = try { + lstatSync(pathString) + } catch (e: Throwable) { + if (e.errorCode == "ENOENT") return null // "No such file or directory". + throw IOException(e.message) + } + + var symlinkTarget: Path? = null + if ((stat.mode.toInt() and S_IFMT) == S_IFLNK) { + try { + symlinkTarget = readlinkSync(pathString).toPath() + } catch (e: Throwable) { + throw e.toIOException() + } + } + + return FileMetadata( + isRegularFile = (stat.mode.toInt() and S_IFMT) == S_IFREG, + isDirectory = (stat.mode.toInt() and S_IFMT) == S_IFDIR, + symlinkTarget = symlinkTarget, + size = stat.size.toLong(), + createdAtMillis = stat.birthtimeMs.toLong(), + lastModifiedAtMillis = stat.mtimeMs.toLong(), + lastAccessedAtMillis = stat.atimeMs.toLong(), + ) + } + + /** + * Returns the error code on this `SystemError`. This uses `asDynamic()` because our JS bindings + * don't (yet) include the `SystemError` type. + * + * https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_systemerror + * https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_common_system_errors + */ + private val Throwable.errorCode + get() = asDynamic().code + + override fun list(dir: Path): List<Path> = list(dir, throwOnFailure = true)!! + + override fun listOrNull(dir: Path): List<Path>? = list(dir, throwOnFailure = false) + + private fun list(dir: Path, throwOnFailure: Boolean): List<Path>? { + try { + val opendir = opendirSync(dir.toString()) + try { + val result = mutableListOf<Path>() + while (true) { + val dirent = opendir.readSync() ?: break + result += dir / dirent.name + } + result.sort() + return result + } finally { + opendir.closeSync() + } + } catch (e: Throwable) { + if (throwOnFailure) { + throw e.toIOException() + } else { + return null + } + } + } + + override fun openReadOnly(file: Path): FileHandle { + val fd = openFd(file, flags = "r") + return NodeJsFileHandle(fd, readWrite = false) + } + + override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { + require(!mustCreate || !mustExist) { + "Cannot require mustCreate and mustExist at the same time." + } + val fd = if (Path.DIRECTORY_SEPARATOR == "\\") { + // On NodeJS on Windows there's no file system flag that does all of the following: + // - open a file for reading, writing, seeking, and resizing + // - create it doesn't exist + // - do not truncate it if it does exist + // Work around this by attempting to open a file that does exist (r+), falling back to + // creating a file that does not exist (wx+) if that throws. This is not atomic. + // https://nodejs.org/api/fs.html#fs_file_system_flags + try { + if (mustCreate && exists(file)) throw IOException("$file already exists.") + openFd(file, "r+") + } catch (e: FileNotFoundException) { + if (mustExist) throw IOException("$file doesn't exist.") + openFd(file, "wx+") + } + } else { + // Note that on Linux, positional writes don't work when the file is opened in append mode, so + // we don't want to use the `a` flag, + val flags = when { + mustCreate -> "wx+" + mustExist || exists(file) -> "r+" + else -> "w+" + } + openFd(file, flags) + } + return NodeJsFileHandle(fd, readWrite = true) + } + + override fun source(file: Path): Source { + val fd = openFd(file, flags = "r") + return FileSource(fd) + } + + override fun sink(file: Path, mustCreate: Boolean): Sink { + val fd = openFd(file, flags = if (mustCreate) "wx" else "w") + return FileSink(fd) + } + + override fun appendingSink(file: Path, mustExist: Boolean): Sink { + // There is a `r+` flag which we could have used to force existence of [file] but this flag + // doesn't allow opening for appending, and we don't currently have a way to move the cursor to + // the end of the file. We are then forcing existence non-atomically. + if (mustExist && !exists(file)) throw IOException("$file doesn't exist.") + val fd = openFd(file, flags = "a") + return FileSink(fd) + } + + private fun openFd(file: Path, flags: String): Double { + try { + return openSync(file.toString(), flags = flags) + } catch (e: Throwable) { + throw e.toIOException() + } + } + + override fun createDirectory(dir: Path, mustCreate: Boolean) { + try { + mkdirSync(dir.toString()) + } catch (e: Throwable) { + val alreadyExist = metadataOrNull(dir)?.isDirectory == true + if (alreadyExist) { + if (mustCreate) { + throw IOException("$dir already exist.") + } else { + return + } + } + + throw e.toIOException() + } + } + + override fun atomicMove(source: Path, target: Path) { + try { + renameSync(source.toString(), target.toString()) + } catch (e: Throwable) { + throw e.toIOException() + } + } + + /** + * We don't know if [path] is a file or a directory, but we don't (yet) have an API to delete + * either type. Just try each in sequence. + * + * TODO(jwilson): switch to fs.rmSync() when our minimum requirements are Node 14.14.0. + */ + override fun delete(path: Path, mustExist: Boolean) { + try { + unlinkSync(path.toString()) + return + } catch (e: Throwable) { + } + try { + rmdirSync(path.toString()) + } catch (e: Throwable) { + if (e.errorCode == "ENOENT") { + if (mustExist) { + throw FileNotFoundException("no such file: $path") + } else { + return + } + } + throw e.toIOException() + } + } + + override fun createSymlink(source: Path, target: Path) { + if (source.parent == null || !exists(source.parent!!)) { + throw IOException("parent directory does not exist: ${source.parent}") + } + + if (exists(source)) { + throw IOException("already exists: $source") + } + + symlinkSync(target.toString(), source.toString()) + } + + private fun Throwable.toIOException(): IOException { + return when (errorCode) { + "ENOENT" -> FileNotFoundException(message) + else -> IOException(message) + } + } + + override fun toString() = "NodeJsSystemFileSystem" +} diff --git a/okio-nodefilesystem/src/test/kotlin/okio/NodeJsFileSystemTest.kt b/okio-nodefilesystem/src/test/kotlin/okio/NodeJsFileSystemTest.kt new file mode 100644 index 00000000..3afc3ed6 --- /dev/null +++ b/okio-nodefilesystem/src/test/kotlin/okio/NodeJsFileSystemTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlinx.datetime.Clock + +class NodeJsFileSystemTest : AbstractFileSystemTest( + clock = Clock.System, + fileSystem = NodeJsFileSystem, + windowsLimitations = Path.DIRECTORY_SEPARATOR == "\\", + allowClobberingEmptyDirectories = Path.DIRECTORY_SEPARATOR == "\\", + allowAtomicMoveFromFileToDirectory = false, + temporaryDirectory = FileSystem.SYSTEM_TEMPORARY_DIRECTORY, +) diff --git a/okio-testing-support/README.md b/okio-testing-support/README.md new file mode 100644 index 00000000..f5146211 --- /dev/null +++ b/okio-testing-support/README.md @@ -0,0 +1,5 @@ +Okio Testing Support +==================== + +This module offers utilities and support for testing Okio itself. It's not intended for use by +other projects or consumers of the Okio library. diff --git a/okio-testing-support/build.gradle.kts b/okio-testing-support/build.gradle.kts new file mode 100644 index 00000000..bdf3506f --- /dev/null +++ b/okio-testing-support/build.gradle.kts @@ -0,0 +1,61 @@ +plugins { + kotlin("multiplatform") + id("build-support") +} + +kotlin { + configureOrCreateOkioPlatforms() + + sourceSets { + all { + languageSettings.apply { + optIn("kotlin.time.ExperimentalTime") + } + } + + val commonMain by getting { + dependencies { + api(projects.okio) + api(libs.kotlin.test) + } + } + + val nonWasmMain by creating { + dependsOn(commonMain) + dependencies { + api(libs.kotlin.time) + implementation(projects.okioFakefilesystem) + } + } + + if (kmpJsEnabled) { + val jsMain by getting { + dependsOn(nonWasmMain) + } + } + + val jvmMain by getting { + dependsOn(nonWasmMain) + dependencies { + // On the JVM the kotlin-test library resolves to one of three implementations based on + // which testing framework is in use. JUnit is used downstream, but Gradle can't know that + // here and thus fails to select a variant automatically. Declare it manually instead. + api(libs.kotlin.test.junit) + } + } + + if (kmpNativeEnabled) { + createSourceSet("nativeMain", children = nativeTargets) + .also { nativeMain -> + nativeMain.dependsOn(nonWasmMain) + } + } + + if (kmpWasmEnabled) { + createSourceSet("wasmMain", children = wasmTargets) + .also { wasmMain -> + wasmMain.dependsOn(commonMain) + } + } + } +} diff --git a/okio-testing-support/src/commonMain/kotlin/okio/AbstractFileSystemTest.kt b/okio-testing-support/src/commonMain/kotlin/okio/AbstractFileSystemTest.kt new file mode 100644 index 00000000..31ec6cba --- /dev/null +++ b/okio-testing-support/src/commonMain/kotlin/okio/AbstractFileSystemTest.kt @@ -0,0 +1,2651 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.test.fail +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import okio.ByteString.Companion.encodeUtf8 +import okio.ByteString.Companion.toByteString +import okio.Path.Companion.toPath + +/** This test assumes that okio-files/ is the current working directory when executed. */ +abstract class AbstractFileSystemTest( + val clock: Clock, + val fileSystem: FileSystem, + val windowsLimitations: Boolean, + val allowClobberingEmptyDirectories: Boolean, + val allowAtomicMoveFromFileToDirectory: Boolean, + val allowRenameWhenTargetIsOpen: Boolean = !windowsLimitations, + temporaryDirectory: Path, +) { + val base: Path = temporaryDirectory / "${this::class.simpleName}-${randomToken(16)}" + private val isNodeJsFileSystem = fileSystem::class.simpleName?.startsWith("NodeJs") ?: false + private val isWasiFileSystem = fileSystem::class.simpleName?.startsWith("Wasi") ?: false + private val isWrappingJimFileSystem = this::class.simpleName?.contains("JimFileSystem") ?: false + + @BeforeTest + fun setUp() { + fileSystem.createDirectories(base) + } + + @Test + fun doesNotExistsWithInvalidPathDoesNotThrow() { + if (isNodeJsFileSystemOnWindows()) return + + val slash = Path.DIRECTORY_SEPARATOR + // We are testing: `\\127.0.0.1\..\localhost\c$\Windows`. + val file = + "${slash}${slash}127.0.0.1$slash..${slash}localhost${slash}c\$${slash}Windows".toPath() + + assertFalse(fileSystem.exists(file)) + } + + @Test + fun canonicalizeDotReturnsCurrentWorkingDirectory() { + if (fileSystem.isFakeFileSystem || fileSystem is ForwardingFileSystem) return + val cwd = fileSystem.canonicalize(".".toPath()) + val cwdString = cwd.toString() + val slash = Path.DIRECTORY_SEPARATOR + assertTrue(cwdString) { + if (isWrappingJimFileSystem) { + cwdString.endsWith("work") + } else if (isWasiFileSystem) { + cwdString.endsWith("/tmp") + } else { + cwdString.endsWith("okio${slash}okio") || + cwdString.endsWith("${slash}okio-parent-okio-nodefilesystem-test") || + cwdString.contains("/CoreSimulator/Devices/") || // iOS simulator. + cwdString == "/" // Android emulator. + } + } + } + + @Test + fun currentWorkingDirectoryIsADirectory() { + val metadata = fileSystem.metadata(".".toPath()) + assertTrue(metadata.isDirectory) + assertFalse(metadata.isRegularFile) + } + + @Test + fun canonicalizeAbsolutePathNoSuchFile() { + assertFailsWith<FileNotFoundException> { + fileSystem.canonicalize(base / "no-such-file") + } + } + + @Test + fun canonicalizeRelativePathNoSuchFile() { + assertFailsWith<FileNotFoundException> { + fileSystem.canonicalize("no-such-file".toPath()) + } + } + + @Test + fun canonicalizeFollowsSymlinkDirectories() { + if (!supportsSymlink()) return + val base = fileSystem.canonicalize(base) + + fileSystem.createDirectory(base / "real-directory") + + val expected = base / "real-directory" / "real-file.txt" + expected.writeUtf8("hello") + + fileSystem.createSymlink(base / "symlink-directory", base / "real-directory") + + val canonicalPath = fileSystem.canonicalize(base / "symlink-directory" / "real-file.txt") + assertEquals(expected, canonicalPath) + } + + @Test + fun canonicalizeFollowsSymlinkFiles() { + if (!supportsSymlink()) return + val base = fileSystem.canonicalize(base) + + fileSystem.createDirectory(base / "real-directory") + + val expected = base / "real-directory" / "real-file.txt" + expected.writeUtf8("hello") + + fileSystem.createSymlink( + base / "real-directory" / "symlink-file.txt", + expected, + ) + + val canonicalPath = fileSystem.canonicalize(base / "real-directory" / "symlink-file.txt") + assertEquals(expected, canonicalPath) + } + + @Test + fun canonicalizeFollowsMultipleDirectoriesAndMultipleFiles() { + if (!supportsSymlink()) return + val base = fileSystem.canonicalize(base) + + fileSystem.createDirectory(base / "real-directory") + + val expected = base / "real-directory" / "real-file.txt" + expected.writeUtf8("hello") + + fileSystem.createSymlink( + base / "real-directory" / "one-symlink-file.txt", + expected, + ) + + fileSystem.createSymlink( + base / "real-directory" / "two-symlink-file.txt", + base / "real-directory" / "one-symlink-file.txt", + ) + + fileSystem.createSymlink( + base / "one-symlink-directory", + base / "real-directory", + ) + + fileSystem.createSymlink( + base / "two-symlink-directory", + base / "one-symlink-directory", + ) + + assertEquals( + expected, + fileSystem.canonicalize(base / "two-symlink-directory" / "two-symlink-file.txt"), + ) + assertEquals( + expected, + fileSystem.canonicalize(base / "two-symlink-directory" / "one-symlink-file.txt"), + ) + assertEquals( + expected, + fileSystem.canonicalize(base / "two-symlink-directory" / "real-file.txt"), + ) + + assertEquals( + expected, + fileSystem.canonicalize(base / "one-symlink-directory" / "two-symlink-file.txt"), + ) + assertEquals( + expected, + fileSystem.canonicalize(base / "one-symlink-directory" / "one-symlink-file.txt"), + ) + assertEquals( + expected, + fileSystem.canonicalize(base / "one-symlink-directory" / "real-file.txt"), + ) + + assertEquals( + expected, + fileSystem.canonicalize(base / "real-directory" / "two-symlink-file.txt"), + ) + assertEquals( + expected, + fileSystem.canonicalize(base / "real-directory" / "one-symlink-file.txt"), + ) + assertEquals( + expected, + fileSystem.canonicalize(expected), + ) + } + + @Test + fun canonicalizeReturnsDeeperPath() { + if (!supportsSymlink()) return + val base = fileSystem.canonicalize(base) + + fileSystem.createDirectories(base / "a" / "b" / "c") + + val expected = base / "a" / "b" / "c" / "d.txt" + expected.writeUtf8("hello") + + fileSystem.createSymlink( + base / "e.txt", + "a".toPath() / "b" / "c" / "d.txt", + ) + + assertEquals( + expected, + fileSystem.canonicalize(base / "e.txt"), + ) + } + + @Test + fun canonicalizeReturnsShallowerPath() { + if (!supportsSymlink()) return + val base = fileSystem.canonicalize(base) + + val expected = base / "a.txt" + expected.writeUtf8("hello") + + fileSystem.createDirectories(base / "b" / "c" / "d") + fileSystem.createSymlink( + base / "b" / "c" / "d" / "e.txt", + "../".toPath() / ".." / ".." / "a.txt", + ) + + assertEquals( + expected, + fileSystem.canonicalize(base / "b" / "c" / "d" / "e.txt"), + ) + } + + @Test + fun list() { + val target = base / "list" + target.writeUtf8("hello, world!") + val entries = fileSystem.list(base) + assertTrue(entries.toString()) { target in entries } + } + + @Test + fun listOnRelativePathReturnsRelativePaths() { + // Make sure there's always at least one file so our assertion is useful. + if (fileSystem.isFakeFileSystem) { + val workingDirectory = "/directory".toPath() + fileSystem.createDirectory(workingDirectory) + fileSystem.workingDirectory = workingDirectory + fileSystem.write("a.txt".toPath()) { + writeUtf8("hello, world!") + } + } else if (isWrappingJimFileSystem || isWasiFileSystem) { + fileSystem.write("a.txt".toPath()) { + writeUtf8("hello, world!") + } + } + + val entries = fileSystem.list(".".toPath()) + assertTrue(entries.toString()) { entries.isNotEmpty() && entries.all { it.isRelative } } + } + + @Test + fun listOnRelativePathWhichIsNotDotReturnsRelativePaths() { + if (isNodeJsFileSystem) return + + // Make sure there's always at least one file so our assertion is useful. We copy the first 2 + // entries of the real working directory of the JVM to validate the results on all environment. + if ( + fileSystem.isFakeFileSystem || + fileSystem is ForwardingFileSystem && fileSystem.delegate.isFakeFileSystem + ) { + val workingDirectory = "/directory".toPath() + fileSystem.createDirectory(workingDirectory) + fileSystem.workingDirectory = workingDirectory + val apiDir = "api".toPath() + fileSystem.createDirectory(apiDir) + fileSystem.write(apiDir / "okio.api".toPath()) { + writeUtf8("hello, world!") + } + } else if (isWrappingJimFileSystem || isWasiFileSystem) { + val apiDir = "api".toPath() + fileSystem.createDirectory(apiDir) + fileSystem.write(apiDir / "okio.api".toPath()) { + writeUtf8("hello, world!") + } + } + + try { + assertEquals( + listOf("api".toPath() / "okio.api".toPath()), + fileSystem.list("api".toPath()), + // List some entries to help debugging. + fileSystem.listRecursively(".".toPath()).take(5).toList().joinToString(), + ) + } catch (e: Throwable) { + if (e !is AssertionError && e !is FileNotFoundException) { throw e } + + // Non JVM environments. + val firstChild = fileSystem.list("Library".toPath()).first() + assertTrue( + // List some entries to help debugging. + fileSystem.listRecursively(".".toPath()).take(5).toList().joinToString(), + ) { + // To avoid relying too much on the environment we check that the path contains its parent + // once and that it's relative. + firstChild.isRelative && + firstChild.toString().startsWith("Library") && + firstChild.toString().split("Library").size == 2 + } + } + } + + @Test + fun listOrNullOnRelativePathWhichIsNotDotReturnsRelativePaths() { + if (isNodeJsFileSystem) return + + // Make sure there's always at least one file so our assertion is useful. We copy the first 2 + // entries of the real working directory of the JVM to validate the results on all environment. + if ( + fileSystem.isFakeFileSystem || + fileSystem is ForwardingFileSystem && fileSystem.delegate.isFakeFileSystem + ) { + val workingDirectory = "/directory".toPath() + fileSystem.createDirectory(workingDirectory) + fileSystem.workingDirectory = workingDirectory + val apiDir = "api".toPath() + fileSystem.createDirectory(apiDir) + fileSystem.write(apiDir / "okio.api".toPath()) { + writeUtf8("hello, world!") + } + } else if (isWrappingJimFileSystem) { + val apiDir = "api".toPath() + fileSystem.createDirectory(apiDir) + fileSystem.write(apiDir / "okio.api".toPath()) { + writeUtf8("hello, world!") + } + } + + try { + assertEquals( + listOf("api".toPath() / "okio.api".toPath()), + fileSystem.listOrNull("api".toPath()), + // List some entries to help debugging. + fileSystem.listRecursively(".".toPath()).take(5).toList().joinToString(), + ) + } catch (e: Throwable) { + if (e !is AssertionError && e !is FileNotFoundException) { throw e } + + // Non JVM environments. + val firstChild = fileSystem.list("Library".toPath()).first() + assertTrue( + // List some entries to help debugging. + fileSystem.listRecursively(".".toPath()).take(5).toList().joinToString(), + ) { + // To avoid relying too much on the environment we check that the path contains its parent + // once and that it's relative. + firstChild.isRelative && + firstChild.toString().startsWith("Library") && + firstChild.toString().split("Library").size == 2 + } + } + } + + @Test + fun listResultsAreSorted() { + val fileA = base / "a" + val fileB = base / "b" + val fileC = base / "c" + val fileD = base / "d" + + // Create files in a different order than the sorted order, so a file system that returns files + // in creation-order or reverse-creation order won't pass by accident. + fileD.writeUtf8("fileD") + fileB.writeUtf8("fileB") + fileC.writeUtf8("fileC") + fileA.writeUtf8("fileA") + + val entries = fileSystem.list(base) + assertEquals(entries, listOf(fileA, fileB, fileC, fileD)) + } + + @Test + fun listNoSuchDirectory() { + assertFailsWith<FileNotFoundException> { + fileSystem.list(base / "no-such-directory") + } + } + + @Test + fun listFile() { + val target = base / "list" + target.writeUtf8("hello, world!") + val exception = assertFailsWith<IOException> { + fileSystem.list(target) + } + assertTrue(exception !is FileNotFoundException) + } + + @Test + fun listOrNull() { + val target = base / "list" + target.writeUtf8("hello, world!") + val entries = fileSystem.listOrNull(base)!! + assertTrue(entries.toString()) { target in entries } + } + + @Test + fun listOrNullOnRelativePathReturnsRelativePaths() { + // Make sure there's always at least one file so our assertion is useful. + if (fileSystem.isFakeFileSystem) { + val workingDirectory = "/directory".toPath() + fileSystem.createDirectory(workingDirectory) + fileSystem.workingDirectory = workingDirectory + fileSystem.write("a.txt".toPath()) { + writeUtf8("hello, world!") + } + } else if (isWrappingJimFileSystem) { + fileSystem.write("a.txt".toPath()) { + writeUtf8("hello, world!") + } + } + + val entries = fileSystem.listOrNull(".".toPath()) + assertTrue(entries.toString()) { entries!!.isNotEmpty() && entries.all { it.isRelative } } + } + + @Test + fun listOrNullResultsAreSorted() { + val fileA = base / "a" + val fileB = base / "b" + val fileC = base / "c" + val fileD = base / "d" + + // Create files in a different order than the sorted order, so a file system that returns files + // in creation-order or reverse-creation order won't pass by accident. + fileD.writeUtf8("fileD") + fileB.writeUtf8("fileB") + fileC.writeUtf8("fileC") + fileA.writeUtf8("fileA") + + val entries = fileSystem.listOrNull(base) + assertEquals(entries, listOf(fileA, fileB, fileC, fileD)) + } + + @Test + fun listOrNullNoSuchDirectory() { + assertNull(fileSystem.listOrNull(base / "no-such-directory")) + } + + @Test + fun listOrNullFile() { + val target = base / "list" + target.writeUtf8("hello, world!") + assertNull(fileSystem.listOrNull(target)) + } + + @Test + fun listRecursivelyReturnsEmpty() { + val entries = fileSystem.listRecursively(base) + assertEquals(entries.toList(), listOf()) + } + + @Test + fun listRecursivelyReturnsSingleFile() { + val baseA = base / "a" + baseA.writeUtf8("a") + val entries = fileSystem.listRecursively(base) + assertEquals(entries.toList(), listOf(baseA)) + } + + @Test + fun listRecursivelyRecurses() { + val baseA = base / "a" + val baseAB = baseA / "b" + baseA.createDirectory() + baseAB.writeUtf8("ab") + val entries = fileSystem.listRecursively(base) + assertEquals(listOf(baseA, baseAB), entries.toList()) + } + + @Test + fun listRecursivelyNoSuchFile() { + val baseA = base / "a" + val sequence = fileSystem.listRecursively(baseA) + assertFailsWith<FileNotFoundException> { + sequence.first() + } + } + + /** + * Note that this is different from `Files.walk` in java.nio which returns the argument even if + * it is not a directory. + */ + @Test + fun listRecursivelyNotADirectory() { + val baseA = base / "a" + baseA.writeUtf8("a") + val sequence = fileSystem.listRecursively(baseA) + val exception = assertFailsWith<IOException> { + sequence.first() + } + assertTrue(exception !is FileNotFoundException) + } + + @Test + fun listRecursivelyIsDepthFirst() { + val baseA = base / "a" + val baseB = base / "b" + val baseA1 = baseA / "1" + val baseA2 = baseA / "2" + val baseB1 = baseB / "1" + val baseB2 = baseB / "2" + baseA.createDirectory() + baseB.createDirectory() + baseA1.writeUtf8("a1") + baseA2.writeUtf8("a2") + baseB1.writeUtf8("b1") + baseB2.writeUtf8("b2") + val entries = fileSystem.listRecursively(base) + assertEquals(listOf(baseA, baseA1, baseA2, baseB, baseB1, baseB2), entries.toList()) + } + + @Test + fun listRecursivelyIsLazy() { + val baseA = base / "a" + val baseB = base / "b" + baseA.createDirectory() + baseB.createDirectory() + val entries = fileSystem.listRecursively(base).iterator() + + // This call will enqueue up the children of base, baseA and baseB. + assertEquals(baseA, entries.next()) + val baseA1 = baseA / "1" + val baseA2 = baseA / "2" + baseA1.writeUtf8("a1") + baseA2.writeUtf8("a2") + + // This call will enqueue the children of baseA, baseA1 and baseA2. + assertEquals(baseA1, entries.next()) + assertEquals(baseA2, entries.next()) + assertEquals(baseB, entries.next()) + + // This call will enqueue the children of baseB, baseB1 and baseB2. + val baseB1 = baseB / "1" + val baseB2 = baseB / "2" + baseB1.writeUtf8("b1") + baseB2.writeUtf8("b2") + assertEquals(baseB1, entries.next()) + assertEquals(baseB2, entries.next()) + assertFalse(entries.hasNext()) + } + + /** + * This test creates directories that should be listed lazily, and then deletes them! The test + * wants to confirm that the sequence is resilient to such changes. + */ + @Test + fun listRecursivelySilentlyIgnoresListFailures() { + val baseA = base / "a" + val baseB = base / "b" + baseA.createDirectory() + baseB.createDirectory() + val entries = fileSystem.listRecursively(base).iterator() + assertEquals(baseA, entries.next()) + assertEquals(baseB, entries.next()) + fileSystem.delete(baseA) + fileSystem.delete(baseB) + assertFalse(entries.hasNext()) + } + + @Test + fun listRecursivelySequenceIterationsAreIndependent() { + val sequence = fileSystem.listRecursively(base) + val iterator1 = sequence.iterator() + assertFalse(iterator1.hasNext()) + val baseA = base / "a" + baseA.writeUtf8("a") + val iterator2 = sequence.iterator() + assertEquals(baseA, iterator2.next()) + assertFalse(iterator2.hasNext()) + } + + @Test + fun listRecursivelyFollowsSymlinks() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseAA = baseA / "a" + val baseB = base / "b" + val baseBA = baseB / "a" + baseA.createDirectory() + baseAA.writeUtf8("aa") + fileSystem.createSymlink(baseB, baseA) + + val sequence = fileSystem.listRecursively(base, followSymlinks = true) + assertEquals(listOf(baseA, baseAA, baseB, baseBA), sequence.toList()) + } + + @Test + fun listRecursivelyDoesNotFollowSymlinks() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseAA = baseA / "a" + val baseB = base / "b" + baseA.createDirectory() + baseAA.writeUtf8("aa") + fileSystem.createSymlink(baseB, baseA) + + val sequence = fileSystem.listRecursively(base, followSymlinks = false) + assertEquals(listOf(baseA, baseAA, baseB), sequence.toList()) + } + + @Test + fun listRecursivelyOnSymlink() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseAA = baseA / "a" + val baseB = base / "b" + val baseBA = baseB / "a" + + baseA.createDirectory() + baseAA.writeUtf8("aa") + fileSystem.createSymlink(baseB, baseA) + + val sequence = fileSystem.listRecursively(baseB, followSymlinks = false) + assertEquals(listOf(baseBA), sequence.toList()) + } + + @Test + fun listRecursiveWithSpecialCharacterNamedFiles() { + val baseA = base / "ä" + val baseASuperSaiyan = baseA / "超サイヤ人" + val baseB = base / "ß" + val baseBIliad = baseB / "Ἰλιάς" + + baseA.createDirectory() + baseASuperSaiyan.writeUtf8("カカロットよ!") + baseB.createDirectory() + baseBIliad.writeUtf8("μῆνιν ἄειδε θεὰ Πηληϊάδεω Ἀχιλῆος") + + val sequence = fileSystem.listRecursively(base) + assertEquals(listOf(baseB, baseBIliad, baseA, baseASuperSaiyan), sequence.toList()) + } + + @Test + fun listRecursiveOnSymlinkWithSpecialCharacterNamedFiles() { + if (!supportsSymlink()) return + + val baseA = base / "ä" + val baseASuperSaiyan = baseA / "超サイヤ人" + val baseB = base / "ß" + val baseBSuperSaiyan = baseB / "超サイヤ人" + + baseA.createDirectory() + baseASuperSaiyan.writeUtf8("aa") + fileSystem.createSymlink(baseB, baseA) + + val sequence = fileSystem.listRecursively(baseB, followSymlinks = false) + assertEquals(listOf(baseBSuperSaiyan), sequence.toList()) + } + + @Test + fun listRecursivelyOnSymlinkCycleThrows() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseAB = baseA / "b" + val baseAC = baseA / "c" + + baseA.createDirectory() + baseAB.writeUtf8("ab") + fileSystem.createSymlink(baseAC, baseA) + + val iterator = fileSystem.listRecursively(base, followSymlinks = true).iterator() + assertEquals(baseA, iterator.next()) + assertEquals(baseAB, iterator.next()) + assertEquals(baseAC, iterator.next()) + val exception = assertFailsWith<IOException> { + iterator.next() // This would fail because 'c' refers to a path we've already visited. + } + assertEquals("symlink cycle at $baseAC", exception.message) + } + + @Test + fun listRecursivelyDoesNotFollowRelativeSymlink() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseAA = baseA / "a" + val baseB = base / "b" + baseA.createDirectory() + baseAA.writeUtf8("aa") + fileSystem.createSymlink(baseB, ".".toPath()) // Symlink to enclosing directory! + + val iterator = fileSystem.listRecursively(base, followSymlinks = true).iterator() + assertEquals(baseA, iterator.next()) + assertEquals(baseAA, iterator.next()) + assertEquals(baseB, iterator.next()) + val exception = assertFailsWith<IOException> { + iterator.next() + } + assertEquals("symlink cycle at $baseB", exception.message) + } + + @Test + fun fileSourceNoSuchDirectory() { + assertFailsWith<FileNotFoundException> { + fileSystem.source(base / "no-such-directory" / "file") + } + } + + @Test + fun fileSource() { + val path = base / "file-source" + path.writeUtf8("hello, world!") + + val source = fileSystem.source(path) + val buffer = Buffer() + assertTrue(source.read(buffer, 100L) == 13L) + assertEquals(-1L, source.read(buffer, 100L)) + assertEquals("hello, world!", buffer.readUtf8()) + source.close() + } + + @Test + fun readPath() { + val path = base / "read-path" + val string = "hello, read with a Path" + path.writeUtf8(string) + + val result = fileSystem.read(path) { + assertEquals("hello", readUtf8(5)) + assertEquals(", read with ", readUtf8(12)) + assertEquals("a Path", readUtf8()) + return@read "success" + } + assertEquals("success", result) + } + + @Test + fun fileSink() { + val path = base / "file-sink" + val sink = fileSystem.sink(path) + val buffer = Buffer().writeUtf8("hello, world!") + sink.write(buffer, buffer.size) + sink.close() + assertTrue(path in fileSystem.list(base)) + assertEquals(0, buffer.size) + assertEquals("hello, world!", path.readUtf8()) + } + + @Test + fun fileSinkWithSpecialCharacterNamedFiles() { + val path = base / "Ἰλιάς" + val sink = fileSystem.sink(path) + val buffer = Buffer().writeUtf8("μῆνιν ἄειδε θεὰ Πηληϊάδεω Ἀχιλῆος") + sink.write(buffer, buffer.size) + sink.close() + assertTrue(path in fileSystem.list(base)) + assertEquals(0, buffer.size) + assertEquals("μῆνιν ἄειδε θεὰ Πηληϊάδεω Ἀχιλῆος", path.readUtf8()) + } + + @Test + fun fileSinkMustCreate() { + val path = base / "file-sink" + val sink = fileSystem.sink(path, mustCreate = true) + val buffer = Buffer().writeUtf8("hello, world!") + sink.write(buffer, buffer.size) + sink.close() + assertTrue(path in fileSystem.list(base)) + assertEquals(0, buffer.size) + assertEquals("hello, world!", path.readUtf8()) + } + + @Test + fun fileSinkMustCreateThrowsIfAlreadyExists() { + val path = base / "file-sink" + path.writeUtf8("First!") + assertFailsWith<IOException> { + fileSystem.sink(path, mustCreate = true) + } + } + + /** + * Write a file by concatenating three mechanisms, then read it in its entirety using three other + * mechanisms. This is attempting to defend against unwanted use of Windows text mode. + * + * https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/fopen-wfopen?view=msvc-160 + */ + @Test + fun fileSinkSpecialCharacters() { + val path = base / "file-sink-special-characters" + val content = "[ctrl-z: \u001A][newline: \n][crlf: \r\n]".encodeUtf8() + + fileSystem.write(path) { + writeUtf8("FileSystem.write()\n") + write(content) + } + + fileSystem.openReadWrite(path).use { handle -> + handle.sink(fileOffset = handle.size()).buffer().use { sink -> + sink.writeUtf8("FileSystem.openReadWrite()\n") + sink.write(content) + } + } + + fileSystem.appendingSink(path).buffer().use { sink -> + sink.writeUtf8("FileSystem.appendingSink()\n") + sink.write(content) + } + + fileSystem.read(path) { + assertEquals("FileSystem.write()", readUtf8LineStrict()) + assertEquals(content, readByteString(content.size.toLong())) + assertEquals("FileSystem.openReadWrite()", readUtf8LineStrict()) + assertEquals(content, readByteString(content.size.toLong())) + assertEquals("FileSystem.appendingSink()", readUtf8LineStrict()) + assertEquals(content, readByteString(content.size.toLong())) + assertTrue(exhausted()) + } + + fileSystem.openReadWrite(path).use { handle -> + handle.source().buffer().use { source -> + assertEquals("FileSystem.write()", source.readUtf8LineStrict()) + assertEquals(content, source.readByteString(content.size.toLong())) + assertEquals("FileSystem.openReadWrite()", source.readUtf8LineStrict()) + assertEquals(content, source.readByteString(content.size.toLong())) + assertEquals("FileSystem.appendingSink()", source.readUtf8LineStrict()) + assertEquals(content, source.readByteString(content.size.toLong())) + assertTrue(source.exhausted()) + } + } + + fileSystem.openReadOnly(path).use { handle -> + handle.source().buffer().use { source -> + assertEquals("FileSystem.write()", source.readUtf8LineStrict()) + assertEquals(content, source.readByteString(content.size.toLong())) + assertEquals("FileSystem.openReadWrite()", source.readUtf8LineStrict()) + assertEquals(content, source.readByteString(content.size.toLong())) + assertEquals("FileSystem.appendingSink()", source.readUtf8LineStrict()) + assertEquals(content, source.readByteString(content.size.toLong())) + assertTrue(source.exhausted()) + } + } + } + + @Test + fun writePath() { + val path = base / "write-path" + val content = fileSystem.write(path) { + val string = "hello, write with a Path" + writeUtf8(string) + return@write string + } + assertTrue(path in fileSystem.list(base)) + assertEquals(content, path.readUtf8()) + } + + @Test + fun writePathMustCreate() { + val path = base / "write-path" + val content = fileSystem.write(path, mustCreate = true) { + val string = "hello, write with a Path" + writeUtf8(string) + return@write string + } + assertTrue(path in fileSystem.list(base)) + assertEquals(content, path.readUtf8()) + } + + @Test + fun writePathMustCreateThrowsIfAlreadyExists() { + val path = base / "write-path" + path.writeUtf8("First!") + assertFailsWith<IOException> { + fileSystem.write(path, mustCreate = true) {} + } + } + + @Test + fun appendingSinkAppendsToExistingFile() { + val path = base / "appending-sink-appends-to-existing-file" + path.writeUtf8("hello, world!\n") + val sink = fileSystem.appendingSink(path) + val buffer = Buffer().writeUtf8("this is added later!") + sink.write(buffer, buffer.size) + sink.close() + assertTrue(path in fileSystem.list(base)) + assertEquals("hello, world!\nthis is added later!", path.readUtf8()) + } + + @Test + fun appendingSinkDoesNotImpactExistingFile() { + if (fileSystem.isFakeFileSystem && !fileSystem.allowReadsWhileWriting) return + + val path = base / "appending-sink-does-not-impact-existing-file" + path.writeUtf8("hello, world!\n") + val sink = fileSystem.appendingSink(path) + assertEquals("hello, world!\n", path.readUtf8()) + sink.close() + assertEquals("hello, world!\n", path.readUtf8()) + } + + @Test + fun appendingSinkCreatesNewFile() { + val path = base / "appending-sink-creates-new-file" + val sink = fileSystem.appendingSink(path) + val buffer = Buffer().writeUtf8("this is all there is!") + sink.write(buffer, buffer.size) + sink.close() + assertTrue(path in fileSystem.list(base)) + assertEquals("this is all there is!", path.readUtf8()) + } + + @Test + fun appendingSinkExistingFileMustExist() { + val path = base / "appending-sink-creates-new-file" + path.writeUtf8("Hey, ") + + val sink = fileSystem.appendingSink(path, mustExist = true) + val buffer = Buffer().writeUtf8("this is all there is!") + sink.write(buffer, buffer.size) + sink.close() + assertTrue(path in fileSystem.list(base)) + assertEquals("Hey, this is all there is!", path.readUtf8()) + } + + @Test + fun appendingSinkMustExistThrowsIfAbsent() { + val path = base / "appending-sink-creates-new-file" + assertFailsWith<IOException> { + fileSystem.appendingSink(path, mustExist = true) + } + } + + @Test + fun fileSinkFlush() { + if (fileSystem.isFakeFileSystem && !fileSystem.allowReadsWhileWriting) return + + val path = base / "file-sink" + val sink = fileSystem.sink(path) + + val buffer = Buffer().writeUtf8("hello,") + sink.write(buffer, buffer.size) + sink.flush() + assertEquals("hello,", path.readUtf8()) + + buffer.writeUtf8(" world!") + sink.write(buffer, buffer.size) + sink.close() + assertEquals("hello, world!", path.readUtf8()) + } + + @Test + fun fileSinkNoSuchDirectory() { + assertFailsWith<FileNotFoundException> { + fileSystem.sink(base / "no-such-directory" / "file") + } + } + + @Test + fun createDirectory() { + val path = base / "create-directory" + fileSystem.createDirectory(path) + assertTrue(path in fileSystem.list(base)) + } + + @Test + fun createDirectoryMustCreate() { + val path = base / "create-directory" + fileSystem.createDirectory(path, mustCreate = true) + assertTrue(path in fileSystem.list(base)) + } + + @Test + fun createDirectoryAlreadyExists() { + val path = base / "already-exists" + fileSystem.createDirectory(path) + fileSystem.createDirectory(path) + } + + @Test + fun createDirectoryAlreadyExistsMustCreateThrows() { + val path = base / "already-exists" + fileSystem.createDirectory(path) + val exception = assertFailsWith<IOException> { + fileSystem.createDirectory(path, mustCreate = true) + } + assertTrue(exception !is FileNotFoundException) + } + + @Test + fun createDirectoryParentDirectoryDoesNotExist() { + val path = base / "no-such-directory" / "created" + assertFailsWith<IOException> { + fileSystem.createDirectory(path) + } + } + + @Test + fun createDirectoriesSingle() { + val path = base / "create-directories-single" + fileSystem.createDirectories(path) + assertTrue(path in fileSystem.list(base)) + assertTrue(fileSystem.metadata(path).isDirectory) + } + + @Test + fun createDirectoriesAlreadyExists() { + val path = base / "already-exists" + fileSystem.createDirectory(path) + fileSystem.createDirectories(path) + assertTrue(fileSystem.metadata(path).isDirectory) + } + + @Test + fun createDirectoriesAlreadyExistsMustCreateThrows() { + val path = base / "already-exists" + fileSystem.createDirectory(path) + val exception = assertFailsWith<IOException> { + fileSystem.createDirectories(path, mustCreate = true) + } + assertTrue(exception !is FileNotFoundException) + assertTrue(fileSystem.metadata(path).isDirectory) + } + + @Test + fun createDirectoriesParentDirectoryDoesNotExist() { + fileSystem.createDirectories(base / "a" / "b" / "c") + assertTrue(base / "a" in fileSystem.list(base)) + assertTrue(base / "a" / "b" in fileSystem.list(base / "a")) + assertTrue(base / "a" / "b" / "c" in fileSystem.list(base / "a" / "b")) + assertTrue(fileSystem.metadata(base / "a" / "b" / "c").isDirectory) + } + + @Test + fun createDirectoriesParentIsFile() { + val file = base / "simple-file" + file.writeUtf8("just a file") + assertFailsWith<IOException> { + fileSystem.createDirectories(file / "child") + } + } + + @Test + fun atomicMoveFile() { + val source = base / "source" + source.writeUtf8("hello, world!") + val target = base / "target" + fileSystem.atomicMove(source, target) + assertEquals("hello, world!", target.readUtf8()) + assertTrue(source !in fileSystem.list(base)) + assertTrue(target in fileSystem.list(base)) + } + + @Test + fun atomicMoveDirectory() { + val source = base / "source" + fileSystem.createDirectory(source) + val target = base / "target" + fileSystem.atomicMove(source, target) + assertTrue(source !in fileSystem.list(base)) + assertTrue(target in fileSystem.list(base)) + } + + @Test + fun atomicMoveSourceIsTarget() { + val source = base / "source" + source.writeUtf8("hello, world!") + fileSystem.atomicMove(source, source) + assertEquals("hello, world!", source.readUtf8()) + assertTrue(source in fileSystem.list(base)) + } + + @Test + fun atomicMoveClobberExistingFile() { + // `java.io` on Windows doesn't allow file renaming if the target already exists. + if (isJvmFileSystemOnWindows()) return + + val source = base / "source" + source.writeUtf8("hello, world!") + val target = base / "target" + target.writeUtf8("this file will be clobbered!") + fileSystem.atomicMove(source, target) + assertEquals("hello, world!", target.readUtf8()) + assertTrue(source !in fileSystem.list(base)) + assertTrue(target in fileSystem.list(base)) + } + + @Test + fun atomicMoveSourceDoesNotExist() { + val source = base / "source" + val target = base / "target" + if (fileSystem::class.simpleName == "JvmSystemFileSystem") { + assertFailsWith<IOException> { + fileSystem.atomicMove(source, target) + } + } else { + assertFailsWith<FileNotFoundException> { + fileSystem.atomicMove(source, target) + } + } + } + + @Test + fun atomicMoveSourceIsFileAndTargetIsDirectory() { + val source = base / "source" + source.writeUtf8("hello, world!") + val target = base / "target" + fileSystem.createDirectory(target) + + if (allowAtomicMoveFromFileToDirectory) { + fileSystem.atomicMove(source, target) + assertEquals("hello, world!", target.readUtf8()) + } else { + val exception = assertFailsWith<IOException> { + fileSystem.atomicMove(source, target) + } + assertTrue(exception !is FileNotFoundException) + } + } + + @Test + fun atomicMoveSourceIsDirectoryAndTargetIsFile() { + // `java.io` on Windows doesn't allow file renaming if the target already exists. + if (isJvmFileSystemOnWindows()) return + + val source = base / "source" + fileSystem.createDirectory(source) + val target = base / "target" + target.writeUtf8("hello, world!") + try { + fileSystem.atomicMove(source, target) + assertTrue(allowClobberingEmptyDirectories) + } catch (e: IOException) { + assertFalse(allowClobberingEmptyDirectories) + } + } + + @Test + fun copyFile() { + val source = base / "source" + source.writeUtf8("hello, world!") + val target = base / "target" + fileSystem.copy(source, target) + assertTrue(target in fileSystem.list(base)) + assertEquals("hello, world!", source.readUtf8()) + assertEquals("hello, world!", target.readUtf8()) + } + + @Test + fun copySourceDoesNotExist() { + val source = base / "source" + val target = base / "target" + assertFailsWith<FileNotFoundException> { + fileSystem.copy(source, target) + } + assertFalse(target in fileSystem.list(base)) + } + + @Test + fun copyTargetIsClobbered() { + val source = base / "source" + source.writeUtf8("hello, world!") + val target = base / "target" + target.writeUtf8("this file will be clobbered!") + fileSystem.copy(source, target) + assertTrue(target in fileSystem.list(base)) + assertEquals("hello, world!", target.readUtf8()) + } + + @Test + fun deleteFile() { + val path = base / "delete-file" + path.writeUtf8("delete me") + fileSystem.delete(path) + assertTrue(path !in fileSystem.list(base)) + } + + @Test + fun deleteFileMustExist() { + val path = base / "delete-file" + path.writeUtf8("delete me") + fileSystem.delete(path, mustExist = true) + assertTrue(path !in fileSystem.list(base)) + } + + @Test + fun deleteEmptyDirectory() { + val path = base / "delete-empty-directory" + fileSystem.createDirectory(path) + fileSystem.delete(path) + assertTrue(path !in fileSystem.list(base)) + } + + @Test + fun deleteEmptyDirectoryMustExist() { + val path = base / "delete-empty-directory" + fileSystem.createDirectory(path) + fileSystem.delete(path, mustExist = true) + assertTrue(path !in fileSystem.list(base)) + } + + @Test + fun deleteDoesNotExist() { + val path = base / "no-such-file" + fileSystem.delete(path) + } + + @Test + fun deleteFailsOnNoSuchFileIfMustExist() { + val path = base / "no-such-file" + assertFailsWith<FileNotFoundException> { + fileSystem.delete(path, mustExist = true) + } + } + + @Test + fun deleteFailsOnNonEmptyDirectory() { + val path = base / "non-empty-directory" + fileSystem.createDirectory(path) + (path / "file.txt").writeUtf8("inside directory") + val exception = assertFailsWith<IOException> { + fileSystem.delete(path) + } + assertTrue(exception !is FileNotFoundException) + } + + @Test + fun deleteFailsOnNonEmptyDirectoryMustExist() { + val path = base / "non-empty-directory" + fileSystem.createDirectory(path) + (path / "file.txt").writeUtf8("inside directory") + val exception = assertFailsWith<IOException> { + fileSystem.delete(path, mustExist = true) + } + assertTrue(exception !is FileNotFoundException) + } + + @Test + fun deleteRecursivelyFile() { + val path = base / "delete-recursively-file" + path.writeUtf8("delete me") + fileSystem.deleteRecursively(path) + assertTrue(path !in fileSystem.list(base)) + } + + @Test + fun deleteRecursivelyFileMustExist() { + val path = base / "delete-recursively-file" + path.writeUtf8("delete me") + fileSystem.deleteRecursively(path, mustExist = true) + assertTrue(path !in fileSystem.list(base)) + } + + @Test + fun deleteRecursivelyEmptyDirectory() { + val path = base / "delete-recursively-empty-directory" + fileSystem.createDirectory(path) + fileSystem.deleteRecursively(path) + assertTrue(path !in fileSystem.list(base)) + } + + @Test + fun deleteRecursivelyEmptyDirectoryMustExist() { + val path = base / "delete-recursively-empty-directory" + fileSystem.createDirectory(path) + fileSystem.deleteRecursively(path, mustExist = true) + assertTrue(path !in fileSystem.list(base)) + } + + @Test + fun deleteRecursivelyNoSuchFile() { + val path = base / "no-such-file" + fileSystem.deleteRecursively(path) + } + + @Test + fun deleteRecursivelyMustExistFailsOnNoSuchFile() { + val path = base / "no-such-file" + assertFailsWith<IOException> { + fileSystem.deleteRecursively(path, mustExist = true) + } + } + + @Test + fun deleteRecursivelyNonEmptyDirectory() { + val path = base / "delete-recursively-non-empty-directory" + fileSystem.createDirectory(path) + (path / "file.txt").writeUtf8("inside directory") + fileSystem.deleteRecursively(path) + assertTrue(path !in fileSystem.list(base)) + assertTrue((path / "file.txt") !in fileSystem.list(base)) + } + + @Test + fun deleteRecursivelyNonEmptyDirectoryMustExist() { + val path = base / "delete-recursively-non-empty-directory" + fileSystem.createDirectory(path) + (path / "file.txt").writeUtf8("inside directory") + fileSystem.deleteRecursively(path, mustExist = true) + assertTrue(path !in fileSystem.list(base)) + assertTrue((path / "file.txt") !in fileSystem.list(base)) + } + + @Test + fun deleteRecursivelyDeepHierarchy() { + fileSystem.createDirectory(base / "a") + fileSystem.createDirectory(base / "a" / "b") + fileSystem.createDirectory(base / "a" / "b" / "c") + (base / "a" / "b" / "c" / "d.txt").writeUtf8("inside deep hierarchy") + fileSystem.deleteRecursively(base / "a") + assertEquals(listOf(), fileSystem.list(base)) + } + + @Test + fun deleteRecursivelyDeepHierarchyMustExist() { + fileSystem.createDirectory(base / "a") + fileSystem.createDirectory(base / "a" / "b") + fileSystem.createDirectory(base / "a" / "b" / "c") + (base / "a" / "b" / "c" / "d.txt").writeUtf8("inside deep hierarchy") + fileSystem.deleteRecursively(base / "a", mustExist = true) + assertEquals(listOf(), fileSystem.list(base)) + } + + @Test + fun deleteRecursivelyOnSymlinkToFileDeletesOnlyThatSymlink() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseB = base / "b" + baseB.writeUtf8("b") + fileSystem.createSymlink(baseA, baseB) + fileSystem.deleteRecursively(baseA) + assertEquals("b", baseB.readUtf8()) + } + + @Test + fun deleteRecursivelyOnSymlinkToFileDeletesOnlyThatSymlinkMustExist() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseB = base / "b" + baseB.writeUtf8("b") + fileSystem.createSymlink(baseA, baseB) + fileSystem.deleteRecursively(baseA, mustExist = true) + assertEquals("b", baseB.readUtf8()) + } + + @Test + fun deleteRecursivelyOnSymlinkToDirectoryDeletesOnlyThatSymlink() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseB = base / "b" + val baseBC = base / "b" / "c" + fileSystem.createDirectory(baseB) + baseBC.writeUtf8("c") + fileSystem.createSymlink(baseA, baseB) + fileSystem.deleteRecursively(baseA) + assertEquals("c", baseBC.readUtf8()) + } + + @Test + fun deleteRecursivelyOnSymlinkToDirectoryDeletesOnlyThatSymlinkMustExist() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseB = base / "b" + val baseBC = base / "b" / "c" + fileSystem.createDirectory(baseB) + baseBC.writeUtf8("c") + fileSystem.createSymlink(baseA, baseB) + fileSystem.deleteRecursively(baseA, mustExist = true) + assertEquals("c", baseBC.readUtf8()) + } + + @Test + fun deleteRecursivelyOnSymlinkCycleSucceeds() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseAB = baseA / "b" + val baseAC = baseA / "c" + + baseA.createDirectory() + baseAB.writeUtf8("ab") + fileSystem.createSymlink(baseAC, baseA) + + fileSystem.deleteRecursively(base) + assertFalse(fileSystem.exists(base)) + } + + @Test + fun deleteRecursivelyOnSymlinkCycleSucceedsMustExist() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseAB = baseA / "b" + val baseAC = baseA / "c" + + baseA.createDirectory() + baseAB.writeUtf8("ab") + fileSystem.createSymlink(baseAC, baseA) + + fileSystem.deleteRecursively(base, mustExist = true) + assertFalse(fileSystem.exists(base)) + } + + @Test + fun deleteRecursivelyOnSymlinkToEnclosingDirectorySucceeds() { + if (!supportsSymlink()) return + + val baseA = base / "a" + fileSystem.createSymlink(baseA, ".".toPath()) + + fileSystem.deleteRecursively(baseA) + assertFalse(fileSystem.exists(baseA)) + assertTrue(fileSystem.exists(base)) + } + + @Test + fun deleteRecursivelyOnSymlinkToEnclosingDirectorySucceedsMustExist() { + if (!supportsSymlink()) return + + val baseA = base / "a" + fileSystem.createSymlink(baseA, ".".toPath()) + + fileSystem.deleteRecursively(baseA, mustExist = true) + assertFalse(fileSystem.exists(baseA)) + assertTrue(fileSystem.exists(base)) + } + + @Test + fun fileMetadata() { + val minTime = clock.now() + val path = base / "file-metadata" + path.writeUtf8("hello, world!") + val maxTime = clock.now() + + val metadata = fileSystem.metadata(path) + assertTrue(metadata.isRegularFile) + assertFalse(metadata.isDirectory) + assertEquals(13, metadata.size) + assertInRange(metadata.createdAt, minTime, maxTime) + assertInRange(metadata.lastModifiedAt, minTime, maxTime) + assertInRange(metadata.lastAccessedAt, minTime, maxTime) + } + + @Test + fun directoryMetadata() { + val minTime = clock.now() + val path = base / "directory-metadata" + fileSystem.createDirectory(path) + val maxTime = clock.now() + + val metadata = fileSystem.metadata(path) + assertFalse(metadata.isRegularFile) + assertTrue(metadata.isDirectory) + // Note that the size check is omitted; we'd expect null but the JVM returns values like 64. + assertInRange(metadata.createdAt, minTime, maxTime) + assertInRange(metadata.lastModifiedAt, minTime, maxTime) + assertInRange(metadata.lastAccessedAt, minTime, maxTime) + } + + @Test + fun fileMetadataWithSpecialCharacterNamedFiles() { + val minTime = clock.now() + val path = base / "超サイヤ人" + path.writeUtf8("カカロットよ!") + val maxTime = clock.now() + + val metadata = fileSystem.metadata(path) + assertTrue(metadata.isRegularFile) + assertFalse(metadata.isDirectory) + assertEquals(21, metadata.size) + assertInRange(metadata.createdAt, minTime, maxTime) + assertInRange(metadata.lastModifiedAt, minTime, maxTime) + assertInRange(metadata.lastAccessedAt, minTime, maxTime) + } + + @Test + fun directoryMetadataWithSpecialCharacterNamedFiles() { + val minTime = clock.now() + val path = base / "Ἰλιάς" + fileSystem.createDirectory(path) + val maxTime = clock.now() + + val metadata = fileSystem.metadata(path) + assertFalse(metadata.isRegularFile) + assertTrue(metadata.isDirectory) + // Note that the size check is omitted; we'd expect null but the JVM returns values like 64. + assertInRange(metadata.createdAt, minTime, maxTime) + assertInRange(metadata.lastModifiedAt, minTime, maxTime) + assertInRange(metadata.lastAccessedAt, minTime, maxTime) + } + + @Test + fun absentMetadataOrNull() { + val path = base / "no-such-file" + assertNull(fileSystem.metadataOrNull(path)) + } + + @Test + fun absentParentDirectoryMetadataOrNull() { + val path = base / "no-such-directory" / "no-such-file" + assertNull(fileSystem.metadataOrNull(path)) + } + + @Test + fun parentDirectoryIsFileMetadataOrNull() { + val parent = base / "regular-file" + val path = parent / "no-such-file" + parent.writeUtf8("just a regular file") + // This returns null on Windows and throws IOException on other platforms. + try { + assertNull(fileSystem.metadataOrNull(path)) + } catch (e: IOException) { + // Also okay. + } + } + + @Test + @Ignore + fun inaccessibleMetadata() { + // TODO(swankjesse): configure a test directory in CI that exists, but that this process doesn't + // have permission to read metadata of. Perhaps a file in another user's /home directory? + } + + @Test + fun absentMetadata() { + val path = base / "no-such-file" + assertFailsWith<FileNotFoundException> { + fileSystem.metadata(path) + } + } + + @Test + fun fileExists() { + val path = base / "file-exists" + assertFalse(fileSystem.exists(path)) + path.writeUtf8("hello, world!") + assertTrue(fileSystem.exists(path)) + } + + @Test + fun directoryExists() { + val path = base / "directory-exists" + assertFalse(fileSystem.exists(path)) + fileSystem.createDirectory(path) + assertTrue(fileSystem.exists(path)) + } + + @Test + fun deleteOpenForWritingFailsOnWindows() { + val file = base / "file.txt" + expectIOExceptionOnWindows(exceptJs = true) { + fileSystem.sink(file).use { + fileSystem.delete(file) + } + } + } + + @Test + fun deleteOpenForReadingFailsOnWindows() { + val file = base / "file.txt" + file.writeUtf8("abc") + expectIOExceptionOnWindows(exceptJs = true) { + fileSystem.source(file).use { + fileSystem.delete(file) + } + } + } + + @Test + fun renameSourceIsOpenFailsOnWindows() { + val from = base / "from.txt" + val to = base / "to.txt" + from.writeUtf8("source file") + to.writeUtf8("target file") + expectIOExceptionOnWindows(exceptJs = true) { + fileSystem.source(from).use { + fileSystem.atomicMove(from, to) + } + } + } + + @Test + fun renameTargetIsOpenFailsOnWindows() { + val from = base / "from.txt" + val to = base / "to.txt" + from.writeUtf8("source file") + to.writeUtf8("target file") + + val expectCrash = !allowRenameWhenTargetIsOpen + try { + fileSystem.source(to).use { + fileSystem.atomicMove(from, to) + } + assertFalse(expectCrash) + } catch (_: IOException) { + assertTrue(expectCrash) + } + } + + @Test + fun deleteContentsOfParentOfFileOpenForReadingFailsOnWindows() { + val parentA = (base / "a") + fileSystem.createDirectory(parentA) + val parentAB = parentA / "b" + fileSystem.createDirectory(parentAB) + val parentABC = parentAB / "c" + fileSystem.createDirectory(parentABC) + val file = parentABC / "file.txt" + file.writeUtf8("child file") + expectIOExceptionOnWindows { + fileSystem.source(file).use { + fileSystem.delete(file) + fileSystem.delete(parentABC) + fileSystem.delete(parentAB) + fileSystem.delete(parentA) + } + } + } + + @Test fun fileHandleWriteAndRead() { + val path = base / "file-handle-write-and-read" + fileSystem.openReadWrite(path).use { handle -> + + handle.sink().buffer().use { sink -> + sink.writeUtf8("abcdefghijklmnop") + } + + handle.source().buffer().use { source -> + assertEquals("abcde", source.readUtf8(5)) + assertEquals("fghijklmnop", source.readUtf8()) + } + } + } + + @Test fun fileHandleWriteAndOverwrite() { + val path = base / "file-handle-write-and-overwrite" + fileSystem.openReadWrite(path).use { handle -> + + handle.sink().buffer().use { sink -> + sink.writeUtf8("abcdefghij") + } + + handle.sink(fileOffset = handle.size() - 3).buffer().use { sink -> + sink.writeUtf8("HIJKLMNOP") + } + + handle.source().buffer().use { source -> + assertEquals("abcdefgHIJKLMNOP", source.readUtf8()) + } + } + } + + @Test fun fileHandleWriteBeyondEnd() { + val path = base / "file-handle-write-beyond-end" + fileSystem.openReadWrite(path).use { handle -> + + handle.sink(fileOffset = 10).buffer().use { sink -> + sink.writeUtf8("klmnop") + } + + handle.source().buffer().use { source -> + assertEquals("00000000000000000000", source.readByteString(10).hex()) + assertEquals("klmnop", source.readUtf8()) + } + } + } + + @Test fun fileHandleResizeSmaller() { + val path = base / "file-handle-resize-smaller" + fileSystem.openReadWrite(path).use { handle -> + + handle.sink().buffer().use { sink -> + sink.writeUtf8("abcdefghijklmnop") + } + + handle.resize(10) + + handle.source().buffer().use { source -> + assertEquals("abcdefghij", source.readUtf8()) + } + } + } + + @Test fun fileHandleResizeLarger() { + val path = base / "file-handle-resize-larger" + fileSystem.openReadWrite(path).use { handle -> + + handle.sink().buffer().use { sink -> + sink.writeUtf8("abcde") + } + + handle.resize(15) + + handle.source().buffer().use { source -> + assertEquals("abcde", source.readUtf8(5)) + assertEquals("00000000000000000000", source.readByteString().hex()) + } + } + } + + @Test fun fileHandleFlush() { + if (windowsLimitations) return // Open for reading and writing simultaneously. + + val path = base / "file-handle-flush" + fileSystem.openReadWrite(path).use { handleA -> + handleA.sink().buffer().use { sink -> + sink.writeUtf8("abcde") + } + handleA.flush() + + fileSystem.openReadWrite(path).use { handleB -> + handleB.source().buffer().use { source -> + assertEquals("abcde", source.readUtf8()) + } + } + } + } + + @Test fun fileHandleLargeBufferedWriteAndRead() { + if (isBrowser()) return // This test errors on browsers in CI. + + val data = randomBytes(1024 * 1024 * 8) + + val path = base / "file-handle-large-buffered-write-and-read" + fileSystem.openReadWrite(path).use { handle -> + handle.sink().buffer().use { sink -> + sink.write(data) + } + } + + fileSystem.openReadWrite(path).use { handle -> + handle.source().buffer().use { source -> + assertEquals(data, source.readByteString()) + } + } + } + + @Test fun fileHandleLargeArrayWriteAndRead() { + if (isBrowser()) return // This test errors on browsers in CI. + + val path = base / "file-handle-large-array-write-and-read" + + val writtenBytes = randomBytes(1024 * 1024 * 8) + fileSystem.openReadWrite(path).use { handle -> + handle.write(0, writtenBytes.toByteArray(), 0, writtenBytes.size) + } + + val readBytes = fileSystem.openReadWrite(path).use { handle -> + val byteArray = ByteArray(writtenBytes.size) + handle.read(0, byteArray, 0, byteArray.size) + return@use byteArray.toByteString(0, byteArray.size) // Parameters necessary for issue 910. + } + + assertEquals(writtenBytes, readBytes) + } + + @Test fun fileHandleEmptyArrayWriteAndRead() { + val path = base / "file-handle-empty-array-write-and-read" + + val writtenBytes = ByteArray(0) + fileSystem.openReadWrite(path).use { handle -> + handle.write(0, writtenBytes, 0, writtenBytes.size) + } + + val readBytes = fileSystem.openReadWrite(path).use { handle -> + val byteArray = ByteArray(writtenBytes.size) + handle.read(0, byteArray, 0, byteArray.size) + return@use byteArray + } + + assertContentEquals(writtenBytes, readBytes) + } + + @Test fun fileHandleSinkPosition() { + val path = base / "file-handle-sink-position" + + fileSystem.openReadWrite(path).use { handle -> + handle.sink().use { sink -> + sink.write(Buffer().writeUtf8("abcde"), 5) + assertEquals(5, handle.position(sink)) + sink.write(Buffer().writeUtf8("fghijklmno"), 10) + assertEquals(15, handle.position(sink)) + } + + handle.sink(200).use { sink -> + sink.write(Buffer().writeUtf8("abcde"), 5) + assertEquals(205, handle.position(sink)) + sink.write(Buffer().writeUtf8("fghijklmno"), 10) + assertEquals(215, handle.position(sink)) + } + } + } + + @Test fun fileHandleBufferedSinkPosition() { + val path = base / "file-handle-buffered-sink-position" + + fileSystem.openReadWrite(path).use { handle -> + handle.sink().buffer().use { sink -> + sink.writeUtf8("abcde") + assertEquals(5, handle.position(sink)) + sink.writeUtf8("fghijklmno") + assertEquals(15, handle.position(sink)) + } + + handle.sink(200).buffer().use { sink -> + sink.writeUtf8("abcde") + assertEquals(205, handle.position(sink)) + sink.writeUtf8("fghijklmno") + assertEquals(215, handle.position(sink)) + } + } + } + + @Test fun fileHandleSinkReposition() { + val path = base / "file-handle-sink-reposition" + + fileSystem.openReadWrite(path).use { handle -> + handle.sink().use { sink -> + sink.write(Buffer().writeUtf8("abcdefghij"), 10) + handle.reposition(sink, 5) + assertEquals(5, handle.position(sink)) + sink.write(Buffer().writeUtf8("KLM"), 3) + assertEquals(8, handle.position(sink)) + + handle.reposition(sink, 200) + sink.write(Buffer().writeUtf8("ABCDEFGHIJ"), 10) + handle.reposition(sink, 205) + assertEquals(205, handle.position(sink)) + sink.write(Buffer().writeUtf8("klm"), 3) + assertEquals(208, handle.position(sink)) + } + + Buffer().also { + handle.read(fileOffset = 0, sink = it, byteCount = 10) + assertEquals("abcdeKLMij", it.readUtf8()) + } + + Buffer().also { + handle.read(fileOffset = 200, sink = it, byteCount = 15) + assertEquals("ABCDEklmIJ", it.readUtf8()) + } + } + } + + @Test fun fileHandleBufferedSinkReposition() { + val path = base / "file-handle-buffered-sink-reposition" + + fileSystem.openReadWrite(path).use { handle -> + handle.sink().buffer().use { sink -> + sink.write(Buffer().writeUtf8("abcdefghij"), 10) + handle.reposition(sink, 5) + assertEquals(5, handle.position(sink)) + sink.write(Buffer().writeUtf8("KLM"), 3) + assertEquals(8, handle.position(sink)) + + handle.reposition(sink, 200) + sink.write(Buffer().writeUtf8("ABCDEFGHIJ"), 10) + handle.reposition(sink, 205) + assertEquals(205, handle.position(sink)) + sink.write(Buffer().writeUtf8("klm"), 3) + assertEquals(208, handle.position(sink)) + } + + Buffer().also { + handle.read(fileOffset = 0, sink = it, byteCount = 10) + assertEquals("abcdeKLMij", it.readUtf8()) + } + + Buffer().also { + handle.read(fileOffset = 200, sink = it, byteCount = 15) + assertEquals("ABCDEklmIJ", it.readUtf8()) + } + } + } + + @Test fun fileHandleSourceHappyPath() { + val path = base / "file-handle-source" + fileSystem.write(path) { + writeUtf8("abcdefghijklmnop") + } + + fileSystem.openReadOnly(path).use { handle -> + assertEquals(16L, handle.size()) + val buffer = Buffer() + + handle.source().use { source -> + assertEquals(0L, handle.position(source)) + assertEquals(4L, source.read(buffer, 4L)) + assertEquals("abcd", buffer.readUtf8()) + assertEquals(4L, handle.position(source)) + } + + handle.source(fileOffset = 8L).use { source -> + assertEquals(8L, handle.position(source)) + assertEquals(4L, source.read(buffer, 4L)) + assertEquals("ijkl", buffer.readUtf8()) + assertEquals(12L, handle.position(source)) + } + + handle.source(fileOffset = 16L).use { source -> + assertEquals(16L, handle.position(source)) + assertEquals(-1L, source.read(buffer, 4L)) + assertEquals("", buffer.readUtf8()) + assertEquals(16L, handle.position(source)) + } + } + } + + @Test fun fileHandleSourceReposition() { + val path = base / "file-handle-source-reposition" + fileSystem.write(path) { + writeUtf8("abcdefghijklmnop") + } + + fileSystem.openReadOnly(path).use { handle -> + assertEquals(16L, handle.size()) + val buffer = Buffer() + + handle.source().use { source -> + handle.reposition(source, 12L) + assertEquals(12L, handle.position(source)) + assertEquals(4L, source.read(buffer, 4L)) + assertEquals("mnop", buffer.readUtf8()) + assertEquals(-1L, source.read(buffer, 4L)) + assertEquals("", buffer.readUtf8()) + assertEquals(16L, handle.position(source)) + + handle.reposition(source, 0L) + assertEquals(0L, handle.position(source)) + assertEquals(4L, source.read(buffer, 4L)) + assertEquals("abcd", buffer.readUtf8()) + assertEquals(4L, handle.position(source)) + + handle.reposition(source, 8L) + assertEquals(8L, handle.position(source)) + assertEquals(4L, source.read(buffer, 4L)) + assertEquals("ijkl", buffer.readUtf8()) + assertEquals(12L, handle.position(source)) + + handle.reposition(source, 16L) + assertEquals(16L, handle.position(source)) + assertEquals(-1L, source.read(buffer, 4L)) + assertEquals("", buffer.readUtf8()) + assertEquals(16L, handle.position(source)) + } + } + } + + @Test fun fileHandleBufferedSourceReposition() { + val path = base / "file-handle-buffered-source-reposition" + fileSystem.write(path) { + writeUtf8("abcdefghijklmnop") + } + + fileSystem.openReadOnly(path).use { handle -> + assertEquals(16L, handle.size()) + val buffer = Buffer() + + handle.source().buffer().use { source -> + handle.reposition(source, 12L) + assertEquals(0L, source.buffer.size) + assertEquals(12L, handle.position(source)) + assertEquals(4L, source.read(buffer, 4L)) + assertEquals(0L, source.buffer.size) + assertEquals("mnop", buffer.readUtf8()) + assertEquals(-1L, source.read(buffer, 4L)) + assertEquals("", buffer.readUtf8()) + assertEquals(16L, handle.position(source)) + + handle.reposition(source, 0L) + assertEquals(0L, source.buffer.size) + assertEquals(0L, handle.position(source)) + assertEquals(4L, source.read(buffer, 4L)) + assertEquals(12L, source.buffer.size) // Buffered bytes accumulated. + assertEquals("abcd", buffer.readUtf8()) + assertEquals(4L, handle.position(source)) + + handle.reposition(source, 8L) + assertEquals(8L, source.buffer.size) // Buffered bytes preserved. + assertEquals(8L, handle.position(source)) + assertEquals(4L, source.read(buffer, 4L)) + assertEquals(4L, source.buffer.size) + assertEquals("ijkl", buffer.readUtf8()) + assertEquals(12L, handle.position(source)) + + handle.reposition(source, 16L) + assertEquals(0L, source.buffer.size) + assertEquals(16L, handle.position(source)) + assertEquals(-1L, source.read(buffer, 4L)) + assertEquals(0L, source.buffer.size) + assertEquals("", buffer.readUtf8()) + assertEquals(16L, handle.position(source)) + } + } + } + + @Test fun fileHandleSourceSeekBackwards() { + val path = base / "file-handle-source-backwards" + fileSystem.write(path) { + writeUtf8("abcdefghijklmnop") + } + fileSystem.openReadOnly(path).use { handle -> + assertEquals(16L, handle.size()) + val buffer = Buffer() + + handle.source().use { source -> + assertEquals(0L, handle.position(source)) + assertEquals(16L, source.read(buffer, 16L)) + assertEquals("abcdefghijklmnop", buffer.readUtf8()) + assertEquals(16L, handle.position(source)) + } + + handle.source(0L).use { source -> + assertEquals(0L, handle.position(source)) + assertEquals(16L, source.read(buffer, 16L)) + assertEquals("abcdefghijklmnop", buffer.readUtf8()) + assertEquals(16L, handle.position(source)) + } + } + } + + @Test fun bufferedFileHandleSourceHappyPath() { + val path = base / "file-handle-source" + fileSystem.write(path) { + writeUtf8("abcdefghijklmnop") + } + + fileSystem.openReadOnly(path).use { handle -> + assertEquals(16L, handle.size()) + val buffer = Buffer() + + handle.source().buffer().use { source -> + assertEquals(0L, handle.position(source)) + assertEquals(4L, source.read(buffer, 4L)) + assertEquals("abcd", buffer.readUtf8()) + assertEquals(4L, handle.position(source)) + } + + handle.source(fileOffset = 8L).buffer().use { source -> + assertEquals(8L, handle.position(source)) + assertEquals(4L, source.read(buffer, 4L)) + assertEquals("ijkl", buffer.readUtf8()) + assertEquals(12L, handle.position(source)) + } + + handle.source(fileOffset = 16L).buffer().use { source -> + assertEquals(16L, handle.position(source)) + assertEquals(-1L, source.read(buffer, 4L)) + assertEquals("", buffer.readUtf8()) + assertEquals(16L, handle.position(source)) + } + } + } + + @Test fun bufferedFileHandleSourceSeekBackwards() { + val path = base / "file-handle-source-backwards" + fileSystem.write(path) { + writeUtf8("abcdefghijklmnop") + } + fileSystem.openReadOnly(path).use { handle -> + assertEquals(16L, handle.size()) + val buffer = Buffer() + + handle.source().buffer().use { source -> + assertEquals(0L, handle.position(source)) + assertEquals(16L, source.read(buffer, 16L)) + assertEquals("abcdefghijklmnop", buffer.readUtf8()) + assertEquals(16L, handle.position(source)) + } + + handle.source(0L).buffer().use { source -> + assertEquals(0L, handle.position(source)) + assertEquals(16L, source.read(buffer, 16L)) + assertEquals("abcdefghijklmnop", buffer.readUtf8()) + assertEquals(16L, handle.position(source)) + } + } + } + + @Test fun openReadOnlyThrowsOnAttemptToWrite() { + val path = base / "file-handle-source" + fileSystem.write(path) { + writeUtf8("abcdefghijklmnop") + } + + fileSystem.openReadOnly(path).use { handle -> + try { + handle.sink() + fail() + } catch (_: IllegalStateException) { + } + + try { + handle.flush() + fail() + } catch (_: IllegalStateException) { + } + + try { + handle.resize(0L) + fail() + } catch (_: IllegalStateException) { + } + + try { + handle.write(0L, Buffer().writeUtf8("hello"), 5L) + fail() + } catch (_: IllegalStateException) { + } + } + } + + @Test fun openReadOnlyFailsOnAbsentFile() { + val path = base / "file-handle-source" + + try { + fileSystem.openReadOnly(path) + fail() + } catch (_: IOException) { + } + } + + @Test fun openReadWriteCreatesAbsentFile() { + val path = base / "file-handle-source" + + fileSystem.openReadWrite(path).use { + } + + assertEquals("", path.readUtf8()) + } + + @Test fun openReadWriteCreatesAbsentFileMustCreate() { + val path = base / "file-handle-source" + + fileSystem.openReadWrite(path, mustCreate = true).use { + } + + assertEquals("", path.readUtf8()) + } + + @Test fun openReadWriteMustCreateThrowsIfAlreadyExists() { + val path = base / "file-handle-source" + path.writeUtf8("First!") + + assertFailsWith<IOException> { + fileSystem.openReadWrite(path, mustCreate = true).use {} + } + } + + @Test fun openReadWriteMustExist() { + val path = base / "file-handle-source" + path.writeUtf8("one") + + fileSystem.openReadWrite(path, mustExist = true).use { handle -> + handle.write(3L, Buffer().writeUtf8(" two"), 4L) + } + + assertEquals("one two", path.readUtf8()) + } + + @Test fun openReadWriteMustExistThrowsIfAbsent() { + val path = base / "file-handle-source" + + assertFailsWith<IOException> { + fileSystem.openReadWrite(path, mustExist = true).use {} + } + } + + @Test fun openReadWriteThrowsIfBothMustCreateAndMustExist() { + val path = base / "file-handle-source" + + assertFailsWith<IllegalArgumentException> { + fileSystem.openReadWrite(path, mustCreate = true, mustExist = true).use {} + } + } + + @Test fun sinkPositionFailsAfterClose() { + val path = base / "sink-position-fails-after-close" + + fileSystem.openReadWrite(path).use { handle -> + val sink = handle.sink() + sink.close() + try { + handle.position(sink) + fail() + } catch (_: IllegalStateException) { + } + try { + handle.position(sink.buffer()) + fail() + } catch (_: IllegalStateException) { + } + } + } + + @Test fun sinkRepositionFailsAfterClose() { + val path = base / "sink-reposition-fails-after-close" + + fileSystem.openReadWrite(path).use { handle -> + val sink = handle.sink() + sink.close() + try { + handle.reposition(sink, 1L) + fail() + } catch (_: IllegalStateException) { + } + try { + handle.reposition(sink.buffer(), 1L) + fail() + } catch (_: IllegalStateException) { + } + } + } + + @Test fun sourcePositionFailsAfterClose() { + val path = base / "source-position-fails-after-close" + + fileSystem.openReadWrite(path).use { handle -> + val source = handle.source() + source.close() + try { + handle.position(source) + fail() + } catch (_: IllegalStateException) { + } + try { + handle.position(source.buffer()) + fail() + } catch (_: IllegalStateException) { + } + } + } + + @Test fun sourceRepositionFailsAfterClose() { + val path = base / "source-reposition-fails-after-close" + + fileSystem.openReadWrite(path).use { handle -> + val source = handle.source() + source.close() + try { + handle.reposition(source, 1L) + fail() + } catch (_: IllegalStateException) { + } + try { + handle.reposition(source.buffer(), 1L) + fail() + } catch (_: IllegalStateException) { + } + } + } + + @Test fun sizeFailsAfterClose() { + val path = base / "size-fails-after-close" + + val handle = fileSystem.openReadWrite(path) + handle.close() + try { + handle.size() + fail() + } catch (_: IllegalStateException) { + } + } + + @Test + fun absoluteSymlinkMetadata() { + if (!supportsSymlink()) return + + val target = base / "symlink-target" + val source = base / "symlink-source" + + val minTime = clock.now() + fileSystem.createSymlink(source, target) + val maxTime = clock.now() + + val sourceMetadata = fileSystem.metadata(source) + // Okio's WasiFileSystem only creates relative symlinks. + assertEquals( + when { + isWasiFileSystem -> target.relativeTo(source.parent!!) + else -> target + }, + sourceMetadata.symlinkTarget, + ) + assertInRange(sourceMetadata.createdAt, minTime, maxTime) + } + + @Test + fun relativeSymlinkMetadata() { + if (!supportsSymlink()) return + + val target = "symlink-target".toPath() + val source = base / "symlink-source" + + val minTime = clock.now() + fileSystem.createSymlink(source, target) + val maxTime = clock.now() + + val sourceMetadata = fileSystem.metadata(source) + assertEquals(target, sourceMetadata.symlinkTarget) + assertInRange(sourceMetadata.createdAt, minTime, maxTime) + } + + @Test + fun createSymlinkSourceAlreadyExists() { + if (!supportsSymlink()) return + + val target = base / "symlink-target" + val source = base / "symlink-source" + source.writeUtf8("hello") + val exception = assertFailsWith<IOException> { + fileSystem.createSymlink(source, target) + } + assertTrue(exception !is FileNotFoundException) + } + + @Test + fun createSymlinkParentDirectoryDoesNotExist() { + if (!supportsSymlink()) return + + val source = base / "no-such-directory" / "source" + val target = base / "target" + val e = assertFailsWith<IOException> { + fileSystem.createSymlink(source, target) + } + assertTrue(e !is FileNotFoundException) + } + + @Test + fun openSymlinkSource() { + if (!supportsSymlink()) return + + val target = base / "symlink-target" + val source = base / "symlink-source" + fileSystem.createSymlink(source, target) + target.writeUtf8("I am the target file") + val sourceContent = fileSystem.source(source).buffer().use { it.readUtf8() } + assertEquals("I am the target file", sourceContent) + } + + @Test + fun openSymlinkSink() { + if (!supportsSymlink()) return + if (isJimFileSystem()) return + + val target = base / "symlink-target" + val source = base / "symlink-source" + fileSystem.createSymlink(source, target) + fileSystem.sink(source).buffer().use { + it.writeUtf8("This writes to the the source file") + } + assertEquals("This writes to the the source file", target.readUtf8()) + } + + @Test + fun openFileWithDirectorySymlink() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseAA = base / "a" / "a" + val baseB = base / "b" + val baseBA = base / "b" / "a" + fileSystem.createDirectory(baseA) + baseAA.writeUtf8("aa") + fileSystem.createSymlink(baseB, baseA) + assertEquals("aa", baseAA.readUtf8()) + assertEquals("aa", baseBA.readUtf8()) + } + + @Test + fun openSymlinkFileHandle() { + if (!supportsSymlink()) return + + val target = base / "symlink-target" + val source = base / "symlink-source" + fileSystem.createSymlink(source, target) + target.writeUtf8("I am the target file") + val sourceContent = fileSystem.openReadOnly(source).use { fileHandle -> + fileHandle.source().buffer().use { it.readUtf8() } + } + assertEquals("I am the target file", sourceContent) + } + + @Test + fun listSymlinkDirectory() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseAA = base / "a" / "a" + val baseAB = base / "a" / "b" + val baseB = base / "b" + val baseBA = base / "b" / "a" + val baseBB = base / "b" / "b" + fileSystem.createDirectory(baseA) + baseAA.writeUtf8("aa") + baseAB.writeUtf8("ab") + fileSystem.createSymlink(baseB, baseA) + assertEquals(listOf(baseBA, baseBB), fileSystem.list(baseB)) + } + + @Test + fun symlinkFileLastAccessedAt() { + if (!supportsSymlink()) return + + val target = base / "symlink-target" + val source = base / "symlink-source" + target.writeUtf8("a") + fileSystem.createSymlink(source, target) + tryAdvanceTime() + val minTime = clock.now() + assertEquals("a", source.readUtf8()) + val maxTime = clock.now() + assertInRange(fileSystem.metadata(source).lastAccessedAt, minTime, maxTime) + } + + @Test + fun symlinkDirectoryLastAccessedAt() { + if (!supportsSymlink()) return + + val baseA = base / "a" + val baseAA = base / "a" / "a" + val baseB = base / "b" + val baseBA = base / "b" / "a" + fileSystem.createDirectory(baseA) + baseAA.writeUtf8("aa") + fileSystem.createSymlink(baseB, baseA) + tryAdvanceTime() + val minTime = clock.now() + assertEquals("aa", baseBA.readUtf8()) + val maxTime = clock.now() + assertInRange(fileSystem.metadata(baseB).lastAccessedAt, minTime, maxTime) + } + + @Test + fun deleteSymlinkDoesntDeleteTargetFile() { + if (!supportsSymlink()) return + + val target = base / "symlink-target" + val source = base / "symlink-source" + target.writeUtf8("I am the target file") + fileSystem.createSymlink(source, target) + fileSystem.delete(source) + assertEquals("I am the target file", target.readUtf8()) + } + + @Test + fun moveSymlinkDoesntMoveTargetFile() { + if (!supportsSymlink()) return + + val target = base / "symlink-target" + val source1 = base / "symlink-source-1" + val source2 = base / "symlink-source-2" + target.writeUtf8("I am the target file") + fileSystem.createSymlink(source1, target) + fileSystem.atomicMove(source1, source2) + assertEquals("I am the target file", target.readUtf8()) + assertEquals("I am the target file", source2.readUtf8()) + // Okio's WasiFileSystem only creates relative symlinks. + assertEquals( + when { + isWasiFileSystem -> target.relativeTo(source1.parent!!) + else -> target + }, + fileSystem.metadata(source2).symlinkTarget, + ) + } + + @Test + fun symlinkCanBeRelative() { + if (!supportsSymlink()) return + + val relativeTarget = "symlink-target".toPath() + val absoluteTarget = base / relativeTarget + val source = base / "symlink-source" + absoluteTarget.writeUtf8("I am the target file") + fileSystem.createSymlink(source, relativeTarget) + assertEquals("I am the target file", source.readUtf8()) + } + + @Test + fun symlinkCanBeRelativeWithDotDots() { + if (!supportsSymlink()) return + + val relativeTarget = "../b/symlink-target".toPath() + val absoluteTarget = base / "b" / "symlink-target" + val absoluteSource = base / "a" / "symlink-source" + fileSystem.createDirectory(absoluteSource.parent!!) + fileSystem.createDirectory(absoluteTarget.parent!!) + absoluteTarget.writeUtf8("I am the target file") + fileSystem.createSymlink(absoluteSource, relativeTarget) + assertEquals("I am the target file", absoluteSource.readUtf8()) + } + + @Test + fun followingRecursiveSymlinksIsOkay() { + if (!supportsSymlink()) return + + val pathA = base / "symlink-a" + val pathB = base / "symlink-b" + val pathC = base / "symlink-c" + val target = base / "symlink-target" + fileSystem.createSymlink(pathA, pathB) + fileSystem.createSymlink(pathB, pathC) + fileSystem.createSymlink(pathC, target) + target.writeUtf8("I am the target file") + assertEquals("I am the target file", pathC.readUtf8()) + assertEquals("I am the target file", pathB.readUtf8()) + assertEquals("I am the target file", pathA.readUtf8()) + } + + @Test + fun symlinkCycle() { + if (!supportsSymlink()) return + + val pathA = base / "symlink-a" + val pathB = base / "symlink-b" + fileSystem.createSymlink(pathA, pathB) + fileSystem.createSymlink(pathB, pathA) + assertFailsWith<IOException> { + pathB.writeUtf8("This should not work") + } + assertFailsWith<IOException> { + pathB.readUtf8() + } + } + + protected fun supportsSymlink(): Boolean { + if (fileSystem.isFakeFileSystem) return fileSystem.allowSymlinks + if (windowsLimitations) return false + return when (fileSystem::class.simpleName) { + "JvmSystemFileSystem", + -> false + else -> true + } + } + + private fun expectIOExceptionOnWindows( + exceptJs: Boolean = false, + block: () -> Unit, + ) { + val expectCrash = windowsLimitations && (!isNodeJsFileSystem || !exceptJs) + try { + block() + assertFalse(expectCrash) + } catch (_: IOException) { + assertTrue(expectCrash) + } + } + + fun Path.readUtf8(): String { + return fileSystem.source(this).buffer().use { + it.readUtf8() + } + } + + fun Path.writeUtf8(string: String) { + fileSystem.sink(this).buffer().use { + it.writeUtf8(string) + } + } + + fun Path.createDirectory() { + fileSystem.createDirectory(this) + } + + /** + * Returns the earliest file system time that could be recorded for an event occurring at this + * instant. This truncates fractional seconds because most host file systems do not use precise + * timestamps for file metadata. + * + * It also pads the result by 200 milliseconds because the host and file system may use different + * clocks, allowing the time on the CPU to drift ahead of the time on the file system. + */ + private fun Instant.minFileSystemTime(): Instant { + val paddedInstant = minus(200.milliseconds) + return fromEpochSeconds(paddedInstant.epochSeconds) + } + + /** + * Returns the latest file system time that could be recorded for an event occurring at this + * instant. This adds 2 seconds and truncates fractional seconds because file systems may defer + * assigning the timestamp. + * + * It also pads the result by 200 milliseconds because the host and file system may use different + * clocks, allowing the time on the CPU to drift behind the time on the file system. + * + * https://docs.microsoft.com/en-us/windows/win32/sysinfo/file-times + */ + private fun Instant.maxFileSystemTime(): Instant { + val paddedInstant = plus(200.milliseconds) + return fromEpochSeconds(paddedInstant.plus(2.seconds).epochSeconds) + } + + /** + * Attempt to advance the clock so that any already-issued timestamps will not collide with + * timestamps yet to be issued. + */ + private fun tryAdvanceTime() { + if (clock is FakeClock) clock.sleep(1.minutes) + } + + private fun assertInRange(sampled: Instant?, minTime: Instant, maxTime: Instant) { + if (sampled == null) return + val minFsTime = minTime.minFileSystemTime() + val maxFsTime = maxTime.maxFileSystemTime() + assertTrue("expected $sampled in $minFsTime..$maxFsTime (relaxed from $minTime..$maxTime)") { + sampled in minFsTime..maxFsTime + } + } + + private fun isJvmFileSystemOnWindows(): Boolean { + return windowsLimitations && fileSystem::class.simpleName == "JvmSystemFileSystem" + } + + private fun isJimFileSystem(): Boolean { + return "JimfsFileSystem" in fileSystem.toString() + } + + private fun isNodeJsFileSystemOnWindows(): Boolean { + return windowsLimitations && fileSystem::class.simpleName == "NodeJsFileSystem" + } +} diff --git a/okio/src/commonTest/kotlin/okio/FakeClock.kt b/okio-testing-support/src/commonMain/kotlin/okio/FakeClock.kt index 31cf5503..9667090d 100644 --- a/okio/src/commonTest/kotlin/okio/FakeClock.kt +++ b/okio-testing-support/src/commonMain/kotlin/okio/FakeClock.kt @@ -15,14 +15,10 @@ */ package okio -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant import kotlin.time.Duration -import kotlin.time.ExperimentalTime -@ExperimentalTime -internal class FakeClock : Clock { - var time = Instant.parse("2021-01-01T00:00:00Z") +class FakeClock : Clock { + var time = fromEpochSeconds(1609459200L) // 2021-01-01T00:00:00Z override fun now() = time diff --git a/okio-testing-support/src/commonMain/kotlin/okio/TestingCommon.kt b/okio-testing-support/src/commonMain/kotlin/okio/TestingCommon.kt new file mode 100644 index 00000000..d84642ff --- /dev/null +++ b/okio-testing-support/src/commonMain/kotlin/okio/TestingCommon.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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 kotlin.random.Random +import kotlin.test.assertEquals +import kotlin.time.Duration +import okio.ByteString.Companion.toByteString + +fun Char.repeat(count: Int): String { + return toString().repeat(count) +} + +fun assertArrayEquals(a: ByteArray, b: ByteArray) { + assertEquals(a.contentToString(), b.contentToString()) +} + +fun randomBytes(length: Int): ByteString { + val random = Random(0) + val randomBytes = ByteArray(length) + random.nextBytes(randomBytes) + return ByteString.of(*randomBytes) +} + +fun randomToken(length: Int) = Random.nextBytes(length).toByteString(0, length).hex() + +expect fun isBrowser(): Boolean + +expect fun isWasm(): Boolean + +val FileMetadata.createdAt: Instant? + get() { + val createdAt = createdAtMillis ?: return null + return fromEpochMilliseconds(createdAt) + } + +val FileMetadata.lastModifiedAt: Instant? + get() { + val lastModifiedAt = lastModifiedAtMillis ?: return null + return fromEpochMilliseconds(lastModifiedAt) + } + +val FileMetadata.lastAccessedAt: Instant? + get() { + val lastAccessedAt = lastAccessedAtMillis ?: return null + return fromEpochMilliseconds(lastAccessedAt) + } + +/* + * This file contains some declarations from kotlinx.datetime used by [AbstractFileSystemTest], but + * that we can't use because that library isn't yet available for WASM. We should delete these when + * WASM is supported in kotlinx.datetime. + */ + +expect interface Clock { + fun now(): Instant +} + +expect class Instant : Comparable<Instant> { + val epochSeconds: Long + + operator fun plus(duration: Duration): Instant + + operator fun minus(duration: Duration): Instant +} + +expect fun fromEpochSeconds( + epochSeconds: Long, +): Instant + +expect fun fromEpochMilliseconds(epochMilliseconds: Long): Instant + +expect val FileSystem.isFakeFileSystem: Boolean + +expect val FileSystem.allowSymlinks: Boolean + +expect val FileSystem.allowReadsWhileWriting: Boolean + +expect var FileSystem.workingDirectory: Path diff --git a/okio-testing-support/src/jsMain/kotlin/okio/TestingJs.kt b/okio-testing-support/src/jsMain/kotlin/okio/TestingJs.kt new file mode 100644 index 00000000..21442a5c --- /dev/null +++ b/okio-testing-support/src/jsMain/kotlin/okio/TestingJs.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + +actual fun isBrowser(): Boolean { + return js("""(globalThis.window || null)""") != null +} + +actual fun isWasm() = false diff --git a/okio-testing-support/src/jvmMain/kotlin/okio/TestingExecutors.kt b/okio-testing-support/src/jvmMain/kotlin/okio/TestingExecutors.kt new file mode 100644 index 00000000..4561240f --- /dev/null +++ b/okio-testing-support/src/jvmMain/kotlin/okio/TestingExecutors.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 Block, Inc. + * + * 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 java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ThreadFactory + +object TestingExecutors { + val isLoom = System.getProperty("loomEnabled").toBoolean() + + fun newScheduledExecutorService(corePoolSize: Int = 0): ScheduledExecutorService = if (isLoom) { + Executors.newScheduledThreadPool(corePoolSize, newVirtualThreadFactory()) + } else { + Executors.newScheduledThreadPool(corePoolSize) + } + + fun newExecutorService(corePoolSize: Int = 0): ExecutorService = if (isLoom) { + Executors.newScheduledThreadPool(corePoolSize, newVirtualThreadFactory()) + } else { + Executors.newScheduledThreadPool(corePoolSize) + } + + fun newVirtualThreadFactory(): ThreadFactory { + val threadBuilder = Thread::class.java.getMethod("ofVirtual").invoke(null) + return Class.forName("java.lang.Thread\$Builder").getMethod("factory").invoke(threadBuilder) as ThreadFactory + } + + fun newVirtualThreadPerTaskExecutor(): ExecutorService { + return Executors::class.java.getMethod("newVirtualThreadPerTaskExecutor").invoke(null) as ExecutorService + } +} diff --git a/okio-testing-support/src/jvmMain/kotlin/okio/TestingJvm.kt b/okio-testing-support/src/jvmMain/kotlin/okio/TestingJvm.kt new file mode 100644 index 00000000..d6e274db --- /dev/null +++ b/okio-testing-support/src/jvmMain/kotlin/okio/TestingJvm.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + +actual fun isBrowser() = false + +actual fun isWasm() = false diff --git a/okio-testing-support/src/nativeMain/kotlin/okio/TestingNative.kt b/okio-testing-support/src/nativeMain/kotlin/okio/TestingNative.kt new file mode 100644 index 00000000..d6e274db --- /dev/null +++ b/okio-testing-support/src/nativeMain/kotlin/okio/TestingNative.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + +actual fun isBrowser() = false + +actual fun isWasm() = false diff --git a/okio-testing-support/src/nonWasmMain/kotlin/okio/TestingNonWasm.kt b/okio-testing-support/src/nonWasmMain/kotlin/okio/TestingNonWasm.kt new file mode 100644 index 00000000..a331ce7d --- /dev/null +++ b/okio-testing-support/src/nonWasmMain/kotlin/okio/TestingNonWasm.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 okio.fakefilesystem.FakeFileSystem + +actual typealias Clock = kotlinx.datetime.Clock + +actual typealias Instant = kotlinx.datetime.Instant + +actual fun fromEpochSeconds( + epochSeconds: Long, +) = Instant.fromEpochSeconds(epochSeconds) + +actual fun fromEpochMilliseconds( + epochMilliseconds: Long, +) = Instant.fromEpochMilliseconds(epochMilliseconds) + +actual val FileSystem.isFakeFileSystem: Boolean + get() = this is FakeFileSystem + +actual val FileSystem.allowSymlinks: Boolean + get() = (this as? FakeFileSystem)?.allowSymlinks == true + +actual val FileSystem.allowReadsWhileWriting: Boolean + get() = (this as? FakeFileSystem)?.allowReadsWhileWriting == true + +actual var FileSystem.workingDirectory: Path + get() { + return when (this) { + is FakeFileSystem -> workingDirectory + is ForwardingFileSystem -> delegate.workingDirectory + else -> error("cannot get working directory: $this") + } + } + set(value) { + when (this) { + is FakeFileSystem -> workingDirectory = value + is ForwardingFileSystem -> delegate.workingDirectory = value + else -> error("cannot set working directory: $this") + } + } diff --git a/okio-testing-support/src/wasmMain/kotlin/okio/TestingWasm.kt b/okio-testing-support/src/wasmMain/kotlin/okio/TestingWasm.kt new file mode 100644 index 00000000..cb841783 --- /dev/null +++ b/okio-testing-support/src/wasmMain/kotlin/okio/TestingWasm.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 kotlin.time.Duration + +actual fun isBrowser() = false + +actual fun isWasm() = true + +actual interface Clock { + actual fun now(): Instant +} + +actual class Instant( + private val epochMilliseconds: Long, +) : Comparable<Instant> { + actual val epochSeconds: Long + get() = epochMilliseconds / 1_000L + + actual operator fun plus(duration: Duration) = + Instant(epochMilliseconds + duration.inWholeMilliseconds) + + actual operator fun minus(duration: Duration) = + Instant(epochMilliseconds - duration.inWholeMilliseconds) + + override fun compareTo(other: Instant) = + epochMilliseconds.compareTo(other.epochMilliseconds) +} + +actual fun fromEpochSeconds(epochSeconds: Long) = + Instant(epochSeconds * 1_000L) + +actual fun fromEpochMilliseconds(epochMilliseconds: Long) = + Instant(epochMilliseconds) + +actual val FileSystem.isFakeFileSystem: Boolean + get() = false + +actual val FileSystem.allowSymlinks: Boolean + get() = error("unexpected call") + +actual val FileSystem.allowReadsWhileWriting: Boolean + get() = error("unexpected call") + +actual var FileSystem.workingDirectory: Path + get() = error("unexpected call") + set(_) = error("unexpected call") diff --git a/okio-testing-support/src/wasmWasiMain/kotlin/okio/WasiClock.kt b/okio-testing-support/src/wasmWasiMain/kotlin/okio/WasiClock.kt new file mode 100644 index 00000000..101a075b --- /dev/null +++ b/okio-testing-support/src/wasmWasiMain/kotlin/okio/WasiClock.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 kotlin.wasm.unsafe.UnsafeWasmMemoryApi +import kotlin.wasm.unsafe.withScopedMemoryAllocator +import okio.internal.preview1.clock_time_get +import okio.internal.preview1.clockid_realtime + +object WasiClock : Clock { + @OptIn(UnsafeWasmMemoryApi::class) + override fun now(): Instant { + withScopedMemoryAllocator { allocator -> + val returnPointer = allocator.allocate(8) // timestamp is u64. + val errno = clock_time_get( + id = clockid_realtime, + precision = 1_000_000_000L, // 1-second precision. + returnPointer.address.toInt(), + ) + if (errno != 0) throw IllegalStateException("failed to get now: $errno") + + val nanos = returnPointer.loadLong() + return Instant(epochMilliseconds = nanos / 1_000_000L) + } + } +} diff --git a/okio-testing-support/src/wasmWasiMain/kotlin/okio/internal/preview1/Clockid.kt b/okio-testing-support/src/wasmWasiMain/kotlin/okio/internal/preview1/Clockid.kt new file mode 100644 index 00000000..e6e9148f --- /dev/null +++ b/okio-testing-support/src/wasmWasiMain/kotlin/okio/internal/preview1/Clockid.kt @@ -0,0 +1,30 @@ +// Copyright 2019-2023 the Contributors to the WASI Specification +// This file is adapted from the WASI preview1 spec here: +// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md +package okio.internal.preview1 + +internal typealias clockid = Int + +/** + * The clock measuring real time. Time value zero corresponds with + * 1970-01-01T00:00:00Z. + */ +internal val clockid_realtime = 0 + +/** + * The store-wide monotonic clock, which is defined as a clock measuring + * real time, whose value cannot be adjusted and which cannot have negative + * clock jumps. The epoch of this clock is undefined. The absolute time + * value of this clock therefore has no meaning. + */ +internal val clockid_monotonic = 1 + +/** + * The CPU-time clock associated with the current process. + */ +internal val clockid_process_cputime_id = 2 + +/** + * The CPU-time clock associated with the current thread. + */ +internal val clockid_thread_cputime_id = 3 diff --git a/okio-testing-support/src/wasmWasiMain/kotlin/okio/internal/preview1/Preview1Clock.kt b/okio-testing-support/src/wasmWasiMain/kotlin/okio/internal/preview1/Preview1Clock.kt new file mode 100644 index 00000000..50726af1 --- /dev/null +++ b/okio-testing-support/src/wasmWasiMain/kotlin/okio/internal/preview1/Preview1Clock.kt @@ -0,0 +1,19 @@ +// Copyright 2019-2023 the Contributors to the WASI Specification +// This file is adapted from the WASI preview1 spec here: +// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md +package okio.internal.preview1 + +import kotlin.wasm.WasmImport + +/** + * clock_time_get(id: clockid, precision: timestamp) -> Result<timestamp, errno> + * + * Return the time value of a clock. + * Note: This is similar to `clock_gettime` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "clock_time_get") +internal external fun clock_time_get( + id: clockid, + precision: Long, + returnPointer: Int, +): Int // should be Short?? diff --git a/okio-wasifilesystem/README.md b/okio-wasifilesystem/README.md new file mode 100644 index 00000000..5ff87d4c --- /dev/null +++ b/okio-wasifilesystem/README.md @@ -0,0 +1,12 @@ +WASI FileSystem +=============== + +⚠️ This is a work in progress ⚠️ + +This module implements Okio's FileSystem API using the [WebAssembly System Interface (WASI)][wasi]. + +It currently uses the WASI [preview1] APIs and is tested on NodeJS with the +`--experimental-wasi-unstable-preview1` option. + +[wasi]: https://wasi.dev/ +[preview1]: https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md diff --git a/okio-wasifilesystem/build.gradle.kts b/okio-wasifilesystem/build.gradle.kts new file mode 100644 index 00000000..aaf43faa --- /dev/null +++ b/okio-wasifilesystem/build.gradle.kts @@ -0,0 +1,111 @@ +import com.vanniktech.maven.publish.JavadocJar.Empty +import com.vanniktech.maven.publish.KotlinMultiplatform +import com.vanniktech.maven.publish.MavenPublishBaseExtension + +plugins { + kotlin("multiplatform") + // TODO: Restore Dokka once this issue is resolved. + // https://github.com/Kotlin/dokka/issues/3038 + // id("org.jetbrains.dokka") + id("com.vanniktech.maven.publish.base") + id("build-support") + id("binary-compatibility-validator") +} + +kotlin { + configureOrCreateWasmPlatform( + js = false, + wasi = true, + ) + sourceSets { + all { + languageSettings.optIn("kotlin.wasm.unsafe.UnsafeWasmMemoryApi") + } + val wasmWasiMain by getting { + dependencies { + implementation(projects.okio) + } + } + val wasmWasiTest by getting { + dependencies { + implementation(projects.okioTestingSupport) + implementation(libs.kotlin.test) + } + } + } +} + +configure<MavenPublishBaseExtension> { + // TODO: switch from 'Empty' to 'Dokka' once this issue is resolved. + // https://github.com/Kotlin/dokka/issues/3038 + configure( + KotlinMultiplatform(javadocJar = Empty()), + ) +} + +/** + * Inspired by runner.mjs in kowasm, this rewrites the JavaScript bootstrap script to set up WASI. + * + * See also: + * * https://github.com/kowasm/kowasm + * * https://github.com/nodejs/node/blob/main/doc/api/wasi.md + * + * This task overwrites the output of `compileTestDevelopmentExecutableKotlinWasmWasi` and must run + * after that task. It must also run before the WASM test execution tasks that read this script. + * + * Note that this includes which file paths are exposed to the WASI sandbox. + */ +val injectWasiInit by tasks.creating { + dependsOn("compileTestDevelopmentExecutableKotlinWasmWasi") + val moduleName = "${rootProject.name}-${project.name}-wasm-wasi-test" + + val entryPointMjs = File( + buildDir, + "compileSync/wasmWasi/test/testDevelopmentExecutable/kotlin/$moduleName.mjs" + ) + + outputs.file(entryPointMjs) + + doLast { + val base = File(System.getProperty("java.io.tmpdir"), "okio-wasifilesystem-test") + val baseA = File(base, "a") + val baseB = File(base, "b") + base.mkdirs() + baseA.mkdirs() + baseB.mkdirs() + + entryPointMjs.writeText( + """ + import { WASI } from 'wasi'; + import { argv, env } from 'node:process'; + + export const wasi = new WASI({ + version: 'preview1', + preopens: { + '/tmp': '$base', + '/a': '$baseA', + '/b': '$baseB' + } + }); + + const module = await import(/* webpackIgnore: true */'node:module'); + const require = module.default.createRequire(import.meta.url); + const fs = require('fs'); + const path = require('path'); + const url = require('url'); + const filepath = url.fileURLToPath(import.meta.url); + const dirpath = path.dirname(filepath); + const wasmBuffer = fs.readFileSync(path.resolve(dirpath, './$moduleName.wasm')); + const wasmModule = new WebAssembly.Module(wasmBuffer); + const wasmInstance = new WebAssembly.Instance(wasmModule, wasi.getImportObject()); + + wasi.initialize(wasmInstance); + + export default wasmInstance.exports; + """.trimIndent() + ) + } +} +tasks.named("wasmWasiNodeTest").configure { + dependsOn(injectWasiInit) +} diff --git a/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/FileSink.kt b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/FileSink.kt new file mode 100644 index 00000000..f3b69a11 --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/FileSink.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 kotlin.wasm.unsafe.withScopedMemoryAllocator +import okio.internal.ErrnoException +import okio.internal.fdClose +import okio.internal.preview1.fd +import okio.internal.preview1.fd_sync +import okio.internal.preview1.fd_write +import okio.internal.preview1.size +import okio.internal.write + +internal class FileSink( + private val fd: fd, +) : Sink { + private var closed = false + private val cursor = Buffer.UnsafeCursor() + + override fun write(source: Buffer, byteCount: Long) { + require(byteCount >= 0L) { "byteCount < 0: $byteCount" } + require(source.size >= byteCount) { "source.size=${source.size} < byteCount=$byteCount" } + check(!closed) { "closed" } + + var bytesRemaining = byteCount + source.readAndWriteUnsafe(cursor) + try { + while (bytesRemaining > 0L) { + check(cursor.next() != -1) + + val count = minOf(bytesRemaining, cursor.end.toLong() - cursor.start).toInt() + if (fdWrite(cursor.data!!, cursor.start, count) != count) { + throw IOException("write failed") + } + bytesRemaining -= count + } + } finally { + cursor.close() + source.skip(byteCount) + } + } + + private fun fdWrite(data: ByteArray, offset: Int, count: Int): size { + withScopedMemoryAllocator { allocator -> + val dataPointer = allocator.write(data, offset, count) + + val iovec = allocator.allocate(8) + iovec.storeInt(dataPointer.address.toInt()) + (iovec + 4).storeInt(count) + + val returnPointer = allocator.allocate(4) // `size` is u32, 4 bytes. + val errno = fd_write( + fd = fd, + iovs = iovec.address.toInt(), + iovsSize = 1, + returnPointer = returnPointer.address.toInt(), + ) + if (errno != 0) throw ErrnoException(errno.toShort()) + + return returnPointer.loadInt() + } + } + + override fun flush() { + val errno = fd_sync(fd) + if (errno != 0) throw ErrnoException(errno.toShort()) + } + + override fun timeout() = Timeout.NONE + + override fun close() { + if (closed) return + closed = true + fdClose(fd) + } +} diff --git a/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/FileSource.kt b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/FileSource.kt new file mode 100644 index 00000000..d9d98666 --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/FileSource.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 kotlin.wasm.unsafe.withScopedMemoryAllocator +import okio.internal.ErrnoException +import okio.internal.fdClose +import okio.internal.preview1.fd +import okio.internal.preview1.fd_read +import okio.internal.preview1.size +import okio.internal.read + +internal class FileSource( + private val fd: fd, +) : Source { + private val unsafeCursor = Buffer.UnsafeCursor() + private var closed = false + + override fun read(sink: Buffer, byteCount: Long): Long { + require(byteCount >= 0L) { "byteCount < 0: $byteCount" } + check(!closed) { "closed" } + val sinkInitialSize = sink.size + + // Request a writable segment in `sink`. We request at least 1024 bytes, unless the request is + // for smaller than that, in which case we request only that many bytes. + val cursor = sink.readAndWriteUnsafe(unsafeCursor) + val addedCapacityCount = cursor.expandBuffer(minByteCount = minOf(byteCount, 1024L).toInt()) + + // Now that we have a writable segment, figure out how many bytes to read. This is the smaller + // of the user's requested byte count, and the segment's writable capacity. + val attemptCount = minOf(byteCount, addedCapacityCount).toInt() + + // Copy bytes from the file to the segment. + val bytesRead = fdRead(cursor.data!!, cursor.start, attemptCount) + + // Remove new capacity that was added but not used. + cursor.resizeBuffer(sinkInitialSize + bytesRead) + cursor.close() + + return when { + bytesRead == attemptCount -> bytesRead.toLong() + else -> if (bytesRead == 0) -1L else bytesRead.toLong() + } + } + + private fun fdRead(data: ByteArray, offset: Int, count: Int): size { + withScopedMemoryAllocator { allocator -> + val dataPointer = allocator.allocate(count) + + val iovec = allocator.allocate(8) + iovec.storeInt(dataPointer.address.toInt()) + (iovec + 4).storeInt(count) + + val returnPointer = allocator.allocate(4) // `size` is u32, 4 bytes. + val errno = fd_read( + fd = fd, + iovs = iovec.address.toInt(), + iovsSize = 1, + returnPointer = returnPointer.address.toInt(), + ) + if (errno != 0) throw ErrnoException(errno.toShort()) + + val byteCount = returnPointer.loadInt() + if (byteCount != -1) { + dataPointer.read(data, offset, byteCount) + } + + return byteCount + } + } + + override fun timeout(): Timeout = Timeout.NONE + + override fun close() { + if (closed) return + closed = true + fdClose(fd) + } +} diff --git a/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/WasiFileHandle.kt b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/WasiFileHandle.kt new file mode 100644 index 00000000..48271943 --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/WasiFileHandle.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 kotlin.wasm.unsafe.Pointer +import kotlin.wasm.unsafe.withScopedMemoryAllocator +import okio.internal.ErrnoException +import okio.internal.fdClose +import okio.internal.preview1.fd +import okio.internal.preview1.fd_filestat_get +import okio.internal.preview1.fd_filestat_set_size +import okio.internal.preview1.fd_pread +import okio.internal.preview1.fd_pwrite +import okio.internal.preview1.fd_sync +import okio.internal.read +import okio.internal.write + +internal class WasiFileHandle( + private val fd: fd, + readWrite: Boolean, +) : FileHandle(readWrite) { + override fun protectedSize(): Long { + withScopedMemoryAllocator { allocator -> + val returnPointer: Pointer = allocator.allocate(64) // filestat is 64 bytes. + val errno = fd_filestat_get(fd, returnPointer.address.toInt()) + if (errno != 0) throw ErrnoException(errno.toShort()) + + return (returnPointer + 32).loadLong() + } + } + + override fun protectedRead( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ): Int { + withScopedMemoryAllocator { allocator -> + val dataPointer = allocator.allocate(byteCount) + + val iovec = allocator.allocate(8) + iovec.storeInt(dataPointer.address.toInt()) + (iovec + 4).storeInt(byteCount) + + val returnPointer = allocator.allocate(4) // `size` is u32, 4 bytes. + val errno = fd_pread( + fd = fd, + iovs = iovec.address.toInt(), + iovsSize = 1, + offset = fileOffset, + returnPointer = returnPointer.address.toInt(), + ) + if (errno != 0) throw ErrnoException(errno.toShort()) + + val readByteCount = returnPointer.loadInt() + if (byteCount != -1) { + dataPointer.read(array, arrayOffset, readByteCount) + } + + if (readByteCount == 0) return -1 + return readByteCount + } + } + + override fun protectedWrite( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ) { + withScopedMemoryAllocator { allocator -> + val dataPointer = allocator.write(array, arrayOffset, byteCount) + + val iovec = allocator.allocate(8) + iovec.storeInt(dataPointer.address.toInt()) + (iovec + 4).storeInt(byteCount) + + val returnPointer = allocator.allocate(4) // `size` is u32, 4 bytes. + val errno = fd_pwrite( + fd = fd, + iovs = iovec.address.toInt(), + iovsSize = 1, + offset = fileOffset, + returnPointer = returnPointer.address.toInt(), + ) + if (errno != 0) throw ErrnoException(errno.toShort()) + + val writtenByteCount = returnPointer.loadInt() + if (writtenByteCount != byteCount) { + throw IOException("expected $byteCount but was $writtenByteCount") + } + } + } + + override fun protectedFlush() { + val errno = fd_sync(fd) + if (errno != 0) throw ErrnoException(errno.toShort()) + } + + override fun protectedResize(size: Long) { + val errno = fd_filestat_set_size(fd, size) + if (errno != 0) throw ErrnoException(errno.toShort()) + } + + override fun protectedClose() { + fdClose(fd) + } +} diff --git a/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/WasiFileSystem.kt b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/WasiFileSystem.kt new file mode 100644 index 00000000..9e820215 --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/WasiFileSystem.kt @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 kotlin.wasm.unsafe.Pointer +import kotlin.wasm.unsafe.withScopedMemoryAllocator +import okio.Path.Companion.toPath +import okio.internal.ErrnoException +import okio.internal.fdClose +import okio.internal.preview1.Errno +import okio.internal.preview1.dirnamelen +import okio.internal.preview1.fd +import okio.internal.preview1.fd_prestat_dir_name +import okio.internal.preview1.fd_prestat_get +import okio.internal.preview1.fd_readdir +import okio.internal.preview1.fdflags +import okio.internal.preview1.fdflags_append +import okio.internal.preview1.filetype +import okio.internal.preview1.filetype_directory +import okio.internal.preview1.filetype_regular_file +import okio.internal.preview1.filetype_symbolic_link +import okio.internal.preview1.oflag_creat +import okio.internal.preview1.oflag_directory +import okio.internal.preview1.oflag_excl +import okio.internal.preview1.oflag_trunc +import okio.internal.preview1.oflags +import okio.internal.preview1.path_create_directory +import okio.internal.preview1.path_filestat_get +import okio.internal.preview1.path_open +import okio.internal.preview1.path_readlink +import okio.internal.preview1.path_remove_directory +import okio.internal.preview1.path_rename +import okio.internal.preview1.path_symlink +import okio.internal.preview1.path_unlink_file +import okio.internal.preview1.right_fd_filestat_get +import okio.internal.preview1.right_fd_filestat_set_size +import okio.internal.preview1.right_fd_read +import okio.internal.preview1.right_fd_readdir +import okio.internal.preview1.right_fd_seek +import okio.internal.preview1.right_fd_sync +import okio.internal.preview1.right_fd_write +import okio.internal.preview1.rights +import okio.internal.readString +import okio.internal.write + +/** + * Use [WASI] to implement the Okio file system interface. + * + * [WASI]: https://wasi.dev/ + */ +object WasiFileSystem : FileSystem() { + private val preopens: List<Preopen> = buildList { + // File descriptor of the first preopen in the `WASI` instance's configured `preopens` property. + // This is 3 by default, assuming `stdin` is 0, `stdout` is 1, and `stderr` is 2. Other preopens + // are assigned sequentially starting at this value. + val firstPreopen = 3 + + withScopedMemoryAllocator { allocator -> + val bufSize = 2048 + val bufPointer = allocator.allocate(bufSize) + + for (fd in firstPreopen..Int.MAX_VALUE) { + val getReturnPointer = allocator.allocate(12) + + val getErrno = fd_prestat_get(fd, getReturnPointer.address.toInt()) + if (getErrno == Errno.badf.ordinal) break // No more preopens. + if (getErrno != 0) throw ErrnoException(getErrno.toShort()) + + val size = (getReturnPointer + 4).loadInt() + require(size + 1 < bufSize) { "unexpected preopen size: $size" } + val dirNameErrno = fd_prestat_dir_name(fd, bufPointer.address.toInt(), size + 1) + if (dirNameErrno != 0) throw ErrnoException(dirNameErrno.toShort()) + val dirName = bufPointer.readString(size) + val dirNamePath = dirName.toPath() + add(Preopen(dirNamePath, dirNamePath.segmentsBytes, fd)) + } + } + } + + private val relativePathPreopen: Preopen = preopens.firstOrNull() + ?: throw IllegalStateException("no preopens") + + override fun canonicalize(path: Path): Path { + val absolutePath = when { + path.isAbsolute -> path + else -> relativePathPreopen.path.resolve(path, normalize = true) + } + + // There's no APIs in preview1 to canonicalize a path. We give it a best effort by resolving + // all symlinks, but this could result in a relative path. + val result = resolveSymlinks(absolutePath, 0).normalized() + + check(result.isAbsolute) { + "Canonicalize $path returned non-absolute path: $result" + } + + return result + } + + private fun resolveSymlinks( + path: Path, + recurseCount: Int = 0, + ): Path { + // 40 is chosen for consistency with the Linux kernel (which previously used 8). + if (recurseCount > 40) throw IOException("symlink cycle?") + + val parent = path.parent + val resolvedParent = when { + parent != null -> resolveSymlinks(parent, recurseCount + 1) + else -> null + } + val pathWithResolvedParent = when { + resolvedParent != null -> resolvedParent.resolve(path.name) + else -> path + } + + val symlinkTarget = metadata(pathWithResolvedParent).symlinkTarget + ?: return pathWithResolvedParent + + val resolvedSymlinkTarget = when { + symlinkTarget.isAbsolute -> symlinkTarget + resolvedParent != null -> resolvedParent.resolve(symlinkTarget) + else -> symlinkTarget + } + + return resolveSymlinks(resolvedSymlinkTarget, recurseCount + 1) + } + + override fun metadataOrNull(path: Path): FileMetadata? { + withScopedMemoryAllocator { allocator -> + val returnPointer = allocator.allocate(64) + val preopen = preopenForPath(path) ?: return null + val (pathAddress, pathSize) = allocator.write(path.toString()) + + val errno = path_filestat_get( + fd = preopen.fd, + flags = 0, + path = pathAddress.address.toInt(), + pathSize = pathSize, + returnPointer = returnPointer.address.toInt(), + ) + + when (errno) { + // 'notcapable' means our preopens don't cover this path. This will happen for paths + // like '/' that are an ancestor of our preopens. + Errno.notcapable.ordinal -> return FileMetadata(isDirectory = true) + Errno.noent.ordinal -> return null + } + + if (errno != 0) throw ErrnoException(errno.toShort()) + + // Skip device, offset 0. + // Skip ino, offset 8. + val filetype: filetype = (returnPointer + 16).loadByte() + // Skip nlink, offset 24. + val filesize: Long = (returnPointer + 32).loadLong() + val atim: Long = (returnPointer + 40).loadLong() // Access time, Nanoseconds. + val mtim: Long = (returnPointer + 48).loadLong() // Modification time, Nanoseconds. + val ctim: Long = (returnPointer + 56).loadLong() // Status change time, Nanoseconds. + + val symlinkTarget: Path? = when (filetype) { + filetype_symbolic_link -> { + val bufLen = filesize.toInt() + 1 + val bufPointer = allocator.allocate(bufLen) + val readlinkReturnPointer = allocator.allocate(4) // `size` is u32, 4 bytes. + val readlinkErrno = path_readlink( + fd = preopen.fd, + path = pathAddress.address.toInt(), + pathSize = pathSize, + buf = bufPointer.address.toInt(), + buf_len = bufLen, + returnPointer = readlinkReturnPointer.address.toInt(), + ) + if (readlinkErrno != 0) throw ErrnoException(readlinkErrno.toShort()) + val symlinkSize = readlinkReturnPointer.loadInt() + val symlink = bufPointer.readString(symlinkSize) + symlink.toPath() + } + + else -> null + } + + return FileMetadata( + isRegularFile = filetype == filetype_regular_file, + isDirectory = filetype == filetype_directory, + symlinkTarget = symlinkTarget, + size = filesize, + createdAtMillis = ctim / 1_000_000L, // Nanos to millis. + lastModifiedAtMillis = mtim / 1_000_000L, // Nanos to millis. + lastAccessedAtMillis = atim / 1_000_000L, // Nanos to millis. + ) + } + } + + override fun list(dir: Path): List<Path> { + val fd = pathOpen( + path = dir, + oflags = oflag_directory, + rightsBase = right_fd_readdir, + ) + try { + return list(dir, fd) + } finally { + fdClose(fd) + } + } + + override fun listOrNull(dir: Path): List<Path>? { + // TODO: stop using exceptions for flow control. + try { + return list(dir) + } catch (e: FileNotFoundException) { + return null + } catch (e: ErrnoException) { + if (e.errno == Errno.notdir) return null + throw e + } + } + + private fun list(dir: Path, fd: fd): List<Path> { + withScopedMemoryAllocator { allocator -> + // In theory, fd_readdir uses a 'cookie' field to page through results. In practice the + // NodeJS implementation doesn't honor the cookie and directories with large file names + // don't progress. Instead, just grow the buffer until the entire directory fits. + var bufSize = 2048 + var bufPointer = allocator.allocate(bufSize) + val returnPointer = allocator.allocate(4) // `size` is u32, 4 bytes. + var pageSize: Int + while (true) { + val errno = fd_readdir( + fd = fd, + buf = bufPointer.address.toInt(), + buf_len = bufSize, + cookie = 0L, // Don't bother with dircookie, it doesn't work for large file names. + returnPointer = returnPointer.address.toInt(), + ) + + if (errno != 0) throw ErrnoException(errno.toShort()) + pageSize = returnPointer.loadInt() + + if (pageSize < bufSize) break + + bufSize *= 4 + bufPointer = allocator.allocate(bufSize) + } + + // Parse dirent records from the buffer. + var pos = bufPointer + val limit = bufPointer + pageSize + val result = mutableListOf<Path>() + while (pos.address < limit.address) { + pos += 8 // Skip dircookie. + pos += 8 // Skip inode. + val d_namelen: dirnamelen = pos.loadInt() + pos += 4 // Consume d_namelen. + pos += 4 // Skip d_type. + + val name = pos.readString(d_namelen) + pos += d_namelen + + result += dir / name + } + + result.sort() + return result + } + } + + override fun openReadOnly(file: Path): FileHandle { + val rightsBase = right_fd_filestat_get or + right_fd_read or + right_fd_seek or + right_fd_sync + val fd = pathOpen( + path = file, + oflags = 0, + rightsBase = rightsBase, + ) + return WasiFileHandle(fd, readWrite = false) + } + + override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { + val oflags = when { + mustCreate && mustExist -> { + throw IllegalArgumentException("Cannot require mustCreate and mustExist at the same time.") + } + mustCreate -> oflag_creat or oflag_excl + mustExist -> 0 + else -> oflag_creat + } + val rightsBase = right_fd_filestat_get or + right_fd_filestat_set_size or + right_fd_read or + right_fd_seek or + right_fd_sync or + right_fd_write + val fd = pathOpen( + path = file, + oflags = oflags, + rightsBase = rightsBase, + ) + return WasiFileHandle(fd, readWrite = true) + } + + override fun source(file: Path): Source { + return FileSource( + fd = pathOpen( + path = file, + oflags = 0, + rightsBase = right_fd_read, + ), + ) + } + + override fun sink(file: Path, mustCreate: Boolean): Sink { + val oflags = when { + mustCreate -> oflag_creat or oflag_excl or oflag_trunc + else -> oflag_creat or oflag_trunc + } + + return FileSink( + fd = pathOpen( + path = file, + oflags = oflags, + rightsBase = right_fd_write or right_fd_sync, + ), + ) + } + + override fun appendingSink(file: Path, mustExist: Boolean): Sink { + val oflags = when { + mustExist -> 0 + else -> oflag_creat + } + + return FileSink( + fd = pathOpen( + path = file, + oflags = oflags, + rightsBase = right_fd_write, + fdflags = fdflags_append, + ), + ) + } + + override fun createDirectory(dir: Path, mustCreate: Boolean) { + withScopedMemoryAllocator { allocator -> + val (pathAddress, pathSize) = allocator.write(dir.toString()) + + val errno = path_create_directory( + fd = preopenForPath(dir)?.fd ?: throw FileNotFoundException("no preopen: $dir"), + path = pathAddress.address.toInt(), + pathSize = pathSize, + ) + if (errno == Errno.exist.ordinal) { + if (mustCreate) throw IOException("already exists: $dir") + return + } + + if (errno != 0) throw ErrnoException(errno.toShort()) + } + } + + override fun atomicMove(source: Path, target: Path) { + withScopedMemoryAllocator { allocator -> + val (sourcePathAddress, sourcePathSize) = allocator.write(source.toString()) + val (targetPathAddress, targetPathSize) = allocator.write(target.toString()) + + val errno = path_rename( + fd = preopenForPath(source)?.fd ?: throw FileNotFoundException("no preopen: $source"), + old_path = sourcePathAddress.address.toInt(), + old_pathSize = sourcePathSize, + new_fd = preopenForPath(target)?.fd ?: throw FileNotFoundException("no preopen: $target"), + new_path = targetPathAddress.address.toInt(), + new_pathSize = targetPathSize, + ) + if (errno == Errno.noent.ordinal) { + throw FileNotFoundException("no such file: $source") + } + + if (errno != 0) throw ErrnoException(errno.toShort()) + } + } + + override fun delete(path: Path, mustExist: Boolean) { + withScopedMemoryAllocator { allocator -> + val (pathAddress, pathSize) = allocator.write(path.toString()) + val preopenFd = preopenForPath(path) ?: throw FileNotFoundException("no preopen: $path") + + var errno = path_unlink_file( + fd = preopenFd.fd, + path = pathAddress.address.toInt(), + pathSize = pathSize, + ) + // If unlink failed, try remove_directory. + when (errno) { + Errno.noent.ordinal -> { + if (mustExist) throw FileNotFoundException("no such file: $path") + return // Nothing to delete. + } + + Errno.perm.ordinal, + Errno.isdir.ordinal, + -> { + errno = path_remove_directory( + fd = preopenFd.fd, + path = pathAddress.address.toInt(), + pathSize = pathSize, + ) + } + } + if (errno != 0) throw ErrnoException(errno.toShort()) + } + } + + override fun createSymlink(source: Path, target: Path) { + withScopedMemoryAllocator { allocator -> + val sourcePreopen = preopenForPath(source) + ?: throw FileNotFoundException("no preopen: $source") + + // Always create symlinks relative to their source. Absolute symlinks are trouble because the + // absolute paths used by WASI are different from the absolute paths on the host file system. + val sourceParent = source.parent + ?: throw IOException("unexpected symlink source: $source") + val targetRelative = when { + target.isRelative -> target + else -> target.relativeTo(sourceParent) + } + + val (sourcePathAddress, sourcePathSize) = allocator.write(source.toString()) + val (targetPathAddress, targetPathSize) = allocator.write(targetRelative.toString()) + + val errno = path_symlink( + old_path = targetPathAddress.address.toInt(), + old_pathSize = targetPathSize, + fd = sourcePreopen.fd, + new_path = sourcePathAddress.address.toInt(), + new_pathSize = sourcePathSize, + ) + if (errno != 0) throw ErrnoException(errno.toShort()) + } + } + + private fun pathOpen( + path: Path, + oflags: oflags, + rightsBase: rights, + fdflags: fdflags = 0, + ): fd { + withScopedMemoryAllocator { allocator -> + val preopenFd = preopenForPath(path) ?: throw FileNotFoundException("no preopen: $path") + val (pathAddress, pathSize) = allocator.write(path.toString()) + + val returnPointer: Pointer = allocator.allocate(4) // fd is u32. + val errno = path_open( + fd = preopenFd.fd, + dirflags = 0, + path = pathAddress.address.toInt(), + pathSize = pathSize, + oflags = oflags, + fs_rights_base = rightsBase, + fs_rights_inheriting = 0, + fdflags = fdflags, + returnPointer = returnPointer.address.toInt(), + ) + if (errno == Errno.noent.ordinal) { + throw FileNotFoundException("no such file: $path") + } + if (errno != 0) throw ErrnoException(errno.toShort()) + return returnPointer.loadInt() + } + } + + /** + * Returns the preopen whose path is either an ancestor of [path], or whose path [path] is an + * ancestor of. + * + * If [path] is an ancestor of our preopen, then operating on the path will ultimately fail with a + * `notcapable` errno. + */ + private fun preopenForPath(path: Path): Preopen? { + if (path.isRelative) return relativePathPreopen + + val pathSegmentsBytes = path.segmentsBytes + + return preopens.firstOrNull { preopen -> + val commonSize = minOf(pathSegmentsBytes.size, preopen.segmentsBytes.size) + preopen.segmentsBytes.subList(0, commonSize) == pathSegmentsBytes.subList(0, commonSize) + } + } + + override fun toString() = "okio.WasiFileSystem" + + private class Preopen( + val path: Path, + val segmentsBytes: List<ByteString>, + val fd: fd, + ) +} diff --git a/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/ErrnoException.kt b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/ErrnoException.kt new file mode 100644 index 00000000..fb3aa2ac --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/ErrnoException.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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.internal + +import okio.IOException +import okio.internal.preview1.Errno +import okio.internal.preview1.errno + +class ErrnoException( + val errno: Errno, +) : IOException(errno.name) { + constructor(errno: errno) : this(Errno.entries[errno.toInt()]) +} diff --git a/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/Wasi.kt b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/Wasi.kt new file mode 100644 index 00000000..7775f616 --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/Wasi.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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.internal + +import kotlin.wasm.unsafe.MemoryAllocator +import kotlin.wasm.unsafe.Pointer +import okio.internal.preview1.fd +import okio.internal.preview1.fd_close +import okio.internal.preview1.size + +internal fun fdClose(fd: fd) { + val errno = fd_close(fd = fd) + if (errno != 0) throw ErrnoException(errno.toShort()) +} + +internal fun Pointer.readString(byteCount: Int): String { + if (byteCount == 0) return "" + + // Drop the last byte if it's 0. At least in NodeJS' implementation, strings are returned with + // a trailing NUL byte. + val lastByte = (this + byteCount - 1).loadByte() + val byteArray = when { + lastByte.toInt() == 0 -> readByteArray(byteCount - 1) + else -> readByteArray(byteCount) + } + + return byteArray.decodeToString() +} + +private fun Pointer.readByteArray(byteCount: Int): ByteArray { + val result = ByteArray(byteCount) + read(result, 0, byteCount) + return result +} + +internal fun Pointer.read( + sink: ByteArray, + offset: Int, + count: Int, +): ByteArray { + for (i in 0 until count) { + sink[offset + i] = (this + i).loadByte() + } + return sink +} + +internal fun MemoryAllocator.write( + string: String, +): Pair<Pointer, size> { + val bytes = string.encodeToByteArray() + + // Append a trailing NUL byte. This shouldn't be necessary, but it reduces crashes in practice, + // at least on NodeJS 20.0. https://github.com/WebAssembly/WASI/issues/492 + val result = allocate(bytes.size + 1) + var pos = result + for (element in bytes) { + pos.storeByte(element) + pos += 1 + } + pos.storeByte(0) + return result to bytes.size +} + +internal fun MemoryAllocator.write( + byteArray: ByteArray, + offset: Int = 0, + count: Int = byteArray.size - offset, +): Pointer { + val result = allocate(count) + var pos = result + for (b in offset until (offset + count)) { + pos.storeByte(byteArray[b]) + pos += 1 + } + return result +} diff --git a/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Errno.kt b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Errno.kt new file mode 100644 index 00000000..bc5a4555 --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Errno.kt @@ -0,0 +1,238 @@ +// Copyright 2019-2023 the Contributors to the WASI Specification +// This file is adapted from the WASI preview1 spec here: +// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md +package okio.internal.preview1 + +@Suppress("ktlint:enum-entry-name-case") +enum class Errno { + /** `success`: No error occurred. System call completed successfully. */ + success, + + /** `2big`: Argument list too long. */ + toobig, + + /** `acces`: Permission denied. */ + acces, + + /** `addrinuse`: Address in use. */ + addrinuse, + + /** `addrnotavail`: Address not available. */ + addrnotavail, + + /** `afnosupport`: Address family not supported. */ + afnosupport, + + /** `again`: Resource unavailable, or operation would block. */ + again, + + /** `already`: Connection already in progress. */ + already, + + /** `badf`: Bad file descriptor. */ + badf, + + /** `badmsg`: Bad message. */ + badmsg, + + /** `busy`: Device or resource busy. */ + busy, + + /** `canceled`: Operation canceled. */ + canceled, + + /** `child`: No child processes. */ + child, + + /** `connaborted`: Connection aborted. */ + connaborted, + + /** `connrefused`: Connection refused. */ + connrefused, + + /** `connreset`: Connection reset. */ + connreset, + + /** `deadlk`: Resource deadlock would occur. */ + deadlk, + + /** `destaddrreq`: Destination address required. */ + destaddrreq, + + /** `dom`: Mathematics argument out of domain of function. */ + dom, + + /** `dquot`: Reserved. */ + dquot, + + /** `exist`: File exists. */ + exist, + + /** `fault`: Bad address. */ + fault, + + /** `fbig`: File too large. */ + fbig, + + /** `hostunreach`: Host is unreachable. */ + hostunreach, + + /** `idrm`: Identifier removed. */ + idrm, + + /** `ilseq`: Illegal byte sequence. */ + ilseq, + + /** `inprogress`: Operation in progress. */ + inprogress, + + /** `intr`: Interrupted function. */ + intr, + + /** `inval`: Invalid argument. */ + inval, + + /** `io`: I/O error. */ + io, + + /** `isconn`: Socket is connected. */ + isconn, + + /** `isdir`: Is a directory. */ + isdir, + + /** `loop`: Too many levels of symbolic links. */ + loop, + + /** `mfile`: File descriptor value too large. */ + mfile, + + /** `mlink`: Too many links. */ + mlink, + + /** `msgsize`: Message too large. */ + msgsize, + + /** `multihop`: Reserved. */ + multihop, + + /** `nametoolong`: Filename too long. */ + nametoolong, + + /** `netdown`: Network is down. */ + netdown, + + /** `netreset`: Connection aborted by network. */ + netreset, + + /** `netunreach`: Network unreachable. */ + netunreach, + + /** `nfile`: Too many files open in system. */ + nfile, + + /** `nobufs`: No buffer space available. */ + nobufs, + + /** `nodev`: No such device. */ + nodev, + + /** `noent`: No such file or directory. */ + noent, + + /** `noexec`: Executable file format error. */ + noexec, + + /** `nolck`: No locks available. */ + nolck, + + /** `nolink`: Reserved. */ + nolink, + + /** `nomem`: Not enough space. */ + nomem, + + /** `nomsg`: No message of the desired type. */ + nomsg, + + /** `noprotoopt`: Protocol not available. */ + noprotoopt, + + /** `nospc`: No space left on device. */ + nospc, + + /** `nosys`: Function not supported. */ + nosys, + + /** `notconn`: The socket is not connected. */ + notconn, + + /** `notdir`: Not a directory or a symbolic link to a directory. */ + notdir, + + /** `notempty`: Directory not empty. */ + notempty, + + /** `notrecoverable`: State not recoverable. */ + notrecoverable, + + /** `notsock`: Not a socket. */ + notsock, + + /** `notsup`: Not supported, or operation not supported on socket. */ + notsup, + + /** `notty`: Inappropriate I/O control operation. */ + notty, + + /** `nxio`: No such device or address. */ + nxio, + + /** `overflow`: Value too large to be stored in data type. */ + overflow, + + /** `ownerdead`: Previous owner died. */ + ownerdead, + + /** `perm`: Operation not permitted. */ + perm, + + /** `pipe`: Broken pipe. */ + pipe, + + /** `proto`: Protocol error. */ + proto, + + /** `protonosupport`: Protocol not supported. */ + protonosupport, + + /** `prototype`: Protocol wrong type for socket. */ + prototype_, + + /** `range`: Result too large. */ + range, + + /** `rofs`: Read-only file system. */ + rofs, + + /** `spipe`: Invalid seek. */ + spipe, + + /** `srch`: No such process. */ + srch, + + /** `stale`: Reserved. */ + stale, + + /** `timedout`: Connection timed out. */ + timedout, + + /** `txtbsy`: Text file busy. */ + txtbsy, + + /** `xdev`: Cross-device link. */ + xdev, + + /** `notcapable`: Extension: Capabilities insufficient. */ + notcapable, +} diff --git a/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Fdflags.kt b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Fdflags.kt new file mode 100644 index 00000000..5c3bf217 --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Fdflags.kt @@ -0,0 +1,26 @@ +// Copyright 2019-2023 the Contributors to the WASI Specification +// This file is adapted from the WASI preview1 spec here: +// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md +package okio.internal.preview1 + +/** + * `fdflags: Record`. + * + * File descriptor flags. + */ +typealias fdflags = Short + +/** Data written to the file is always appended to the file's end. */ +val fdflags_append: Short = (1 shl 0).toShort() + +/** Write according to synchronized I/O data integrity completion. Only the data stored in the file is synchronized. */ +val fdflags_dsync: Short = (1 shl 1).toShort() + +/** Non-blocking mode. */ +val fdflags_nonblock: Short = (1 shl 2).toShort() + +/** Synchronized read I/O operations. */ +val fdflags_rsync: Short = (1 shl 3).toShort() + +/** Write according to synchronized I/O file integrity completion. In addition to synchronizing the data stored in the file, the implementation may also synchronously update the file's metadata. */ +val fdflags_sync: Short = (1 shl 4).toShort() diff --git a/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Filetype.kt b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Filetype.kt new file mode 100644 index 00000000..f4788b36 --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Filetype.kt @@ -0,0 +1,35 @@ +// Copyright 2019-2023 the Contributors to the WASI Specification +// This file is adapted from the WASI preview1 spec here: +// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md +package okio.internal.preview1 + +/** + * `Variant`. + * + * The type of a file descriptor or file. + */ +typealias filetype = Byte + +/** The type of the file descriptor or file is unknown or is different from any of the other types specified. */ +val filetype_unknown: filetype = 0 + +/** The file descriptor or file refers to a block device inode. */ +val filetype_block_device: filetype = 1 + +/** The file descriptor or file refers to a character device inode. */ +val filetype_character_device: filetype = 2 + +/** The file descriptor or file refers to a directory inode. */ +val filetype_directory: filetype = 3 + +/** The file descriptor or file refers to a regular file inode. */ +val filetype_regular_file: filetype = 4 + +/** The file descriptor or file refers to a datagram socket. */ +val filetype_socket_dgram: filetype = 5 + +/** The file descriptor or file refers to a byte-stream socket. */ +val filetype_socket_stream: filetype = 6 + +/** The file refers to a symbolic link inode. */ +val filetype_symbolic_link: filetype = 7 diff --git a/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/LookupFlags.kt b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/LookupFlags.kt new file mode 100644 index 00000000..5608f445 --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/LookupFlags.kt @@ -0,0 +1,17 @@ +// Copyright 2019-2023 the Contributors to the WASI Specification +// This file is adapted from the WASI preview1 spec here: +// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md +package okio.internal.preview1 + +/** + * `lookupflags: Record`. + * + * Flags determining the method of how paths are resolved. + * + * Bit0: + * symlink_follow: + */ +typealias lookupflags = Int + +/** As long as the resolved path corresponds to a symbolic link, it is expanded. */ +val lookupflags_symlink_follow = 1 shl 0 diff --git a/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/OFlags.kt b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/OFlags.kt new file mode 100644 index 00000000..3efeb764 --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/OFlags.kt @@ -0,0 +1,23 @@ +// Copyright 2019-2023 the Contributors to the WASI Specification +// This file is adapted from the WASI preview1 spec here: +// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md +package okio.internal.preview1 + +/** + * `oflags: `Record`. + * + * Open flags used by path_open. + */ +typealias oflags = Int + +/** Create file if it does not exist. */ +val oflag_creat = 1 shl 0 + +/** Fail if not a directory. */ +val oflag_directory = 1 shl 1 + +/** Fail if file already exists. */ +val oflag_excl = 1 shl 2 + +/** Truncate file to size 0. */ +val oflag_trunc = 1 shl 3 diff --git a/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Preview1.kt b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Preview1.kt new file mode 100644 index 00000000..4441d5ec --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Preview1.kt @@ -0,0 +1,336 @@ +// Copyright 2019-2023 the Contributors to the WASI Specification +// This file is adapted from the WASI preview1 spec here: +// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md +package okio.internal.preview1 + +import kotlin.wasm.WasmImport + +/** `u32`. */ +typealias size = Int + +/** + * `Handle`. + * + * A file descriptor handle. + */ +typealias fd = Int + +/** + * `u64`. + * + * A reference to the offset of a directory entry. + * + * The value 0 signifies the start of the directory. + */ +typealias dircookie = Long + +/** + * `Variant`. + * + * Error codes returned by functions. Not all of these error codes are returned by the functions + * provided by this API; some are used in higher-level library layers, and others are provided + * merely for alignment with POSIX. + */ +typealias errno = Short + +/** + * `u64`. + * + * File serial number that is unique within its file system. + */ +typealias inode = Long + +/** + * `u32`. + * + * The type for the [`dirent::d_namlen`](#dirent.d_namlen) field of [`dirent`](#dirent) struct. + */ +typealias dirnamelen = Int + +/** + * `Pointer<u8>`. + */ +typealias PointerU8 = Int + +/** + * path_create_directory(fd: fd, path: string) -> Result<(), errno> + * + * Create a directory. + * Note: This is similar to `mkdirat` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "path_create_directory") +internal external fun path_create_directory( + fd: fd, + path: PointerU8, + pathSize: size, +): Int // should be Short?? + +/** + * path_filestat_get(fd: fd, flags: lookupflags, path: string) -> Result<filestat, errno> + * + * Return the attributes of a file or directory. + * Note: This is similar to `stat` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "path_filestat_get") +internal external fun path_filestat_get( + fd: fd, + flags: lookupflags, + path: PointerU8, + pathSize: size, + returnPointer: PointerU8, +): Int // should be Short?? + +/** + * path_open(fd: fd, dirflags: lookupflags, path: string, oflags: oflags, fs_rights_base: rights, fs_rights_inheriting: rights, fdflags: fdflags) -> Result<fd, errno> + * + * Open a file or directory. + * The returned file descriptor is not guaranteed to be the lowest-numbered + * file descriptor not currently open; it is randomized to prevent + * applications from depending on making assumptions about indexes, since this + * is error-prone in multi-threaded contexts. The returned file descriptor is + * guaranteed to be less than 2**31. + * Note: This is similar to `openat` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "path_open") +internal external fun path_open( + fd: fd, + dirflags: lookupflags, + path: PointerU8, + pathSize: size, + oflags: oflags, + fs_rights_base: rights, + fs_rights_inheriting: rights, + fdflags: fdflags, + returnPointer: PointerU8, +): Int // should be Short?? + +/** + * path_readlink(fd: fd, path: string, buf: Pointer<u8>, buf_len: size) -> Result<size, errno> + * + * Read the contents of a symbolic link. + * Note: This is similar to `readlinkat` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "path_readlink") +internal external fun path_readlink( + fd: fd, + path: PointerU8, + pathSize: size, + buf: PointerU8, + buf_len: size, + returnPointer: PointerU8, +): Int // should be Short?? + +/** + * path_remove_directory(fd: fd, path: string) -> Result<(), errno> + * + * Remove a directory. + * Return [`errno::notempty`](#errno.notempty) if the directory is not empty. + * Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "path_remove_directory") +internal external fun path_remove_directory( + fd: fd, + path: PointerU8, + pathSize: size, +): Int // should be Short?? + +/** + * path_rename(fd: fd, old_path: string, new_fd: fd, new_path: string) -> Result<(), errno> + * + * Rename a file or directory. + * Note: This is similar to `renameat` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "path_rename") +internal external fun path_rename( + fd: fd, + old_path: PointerU8, + old_pathSize: size, + new_fd: fd, + new_path: PointerU8, + new_pathSize: size, +): Int // should be Short?? + +/** + * path_symlink(old_path: string, fd: fd, new_path: string) -> Result<(), errno> + * + * Create a symbolic link. + * Note: This is similar to `symlinkat` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "path_symlink") +internal external fun path_symlink( + old_path: PointerU8, + old_pathSize: size, + fd: fd, + new_path: PointerU8, + new_pathSize: size, +): Int // should be Short?? + +/** + * path_unlink_file(fd: fd, path: string) -> Result<(), errno> + * + * Unlink a file. + * Return [`errno::isdir`](#errno.isdir) if the path refers to a directory. + * Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "path_unlink_file") +internal external fun path_unlink_file( + fd: fd, + path: PointerU8, + pathSize: size, +): Int // should be Short?? + +/** + * fd_close(fd: fd) -> Result<(), errno> + * + * Close a file descriptor. + * Note: This is similar to `close` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "fd_close") +internal external fun fd_close( + fd: fd, +): Int // should be Short?? + +/** + * fd_filestat_get(fd: fd) -> Result<filestat, errno> + * + * Return the attributes of an open file. + */ +@WasmImport("wasi_snapshot_preview1", "fd_filestat_get") +internal external fun fd_filestat_get( + fd: fd, + returnPointer: PointerU8, +): Int // should be Short?? + +/** + * fd_pread(fd: fd, iovs: iovec_array, offset: filesize) -> Result<size, errno> + * + * Read from a file descriptor. + * Note: This is similar to `readv` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "fd_pread") +internal external fun fd_pread( + fd: fd, + iovs: PointerU8, + iovsSize: size, + offset: Long, + returnPointer: PointerU8, +): Int // should be Short?? + +/** + * fd_prestat_dir_name(fd: fd, path: Pointer<u8>, path_len: size) -> Result<(), errno> + * + * Return a description of the given preopened file descriptor. + */ +@WasmImport("wasi_snapshot_preview1", "fd_prestat_dir_name") +internal external fun fd_prestat_dir_name( + fd: fd, + path: PointerU8, + pathSize: size, +): Int // should be Short?? + +/** + * fd_prestat_get(fd: fd) -> Result<prestat, errno> + * + * Return a description of the given preopened file descriptor. + */ +@WasmImport("wasi_snapshot_preview1", "fd_prestat_get") +internal external fun fd_prestat_get( + fd: fd, + returnPointer: PointerU8, +): Int // should be Short?? + +/** + * fd_pwrite(fd: fd, iovs: ciovec_array, offset: filesize) -> Result<size, errno>` + * + * Write to a file descriptor, without using and updating the file descriptor's offset. + * Note: This is similar to `pwritev` in Linux (and other Unix-es). + * + * Like Linux (and other Unix-es), any calls of `pwrite` (and other + * functions to read or write) for a regular file by other threads in the + * WASI process should not be interleaved while `pwrite` is executed. + */ +@WasmImport("wasi_snapshot_preview1", "fd_pwrite") +internal external fun fd_pwrite( + fd: fd, + iovs: PointerU8, + iovsSize: size, + offset: Long, + returnPointer: PointerU8, +): Int // should be Short?? + +/** + * fd_read(fd: fd, iovs: iovec_array) -> Result<size, errno> + * + * Read from a file descriptor. + * Note: This is similar to `readv` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "fd_read") +internal external fun fd_read( + fd: fd, + iovs: PointerU8, + iovsSize: size, + returnPointer: PointerU8, +): Int // should be Short?? + +/** + * fd_readdir(fd: fd, buf: Pointer<u8>, buf_len: size, cookie: dircookie) -> Result<size, errno> + * + * Read directory entries from a directory. + * When successful, the contents of the output buffer consist of a sequence of + * directory entries. Each directory entry consists of a [`dirent`](#dirent) object, + * followed by [`dirent::d_namlen`](#dirent.d_namlen) bytes holding the name of the directory + * entry. + * This function fills the output buffer as much as possible, potentially + * truncating the last directory entry. This allows the caller to grow its + * read buffer size in case it's too small to fit a single large directory + * entry, or skip the oversized directory entry. + */ +@WasmImport("wasi_snapshot_preview1", "fd_readdir") +internal external fun fd_readdir( + fd: fd, + buf: PointerU8, + buf_len: size, + cookie: dircookie, + returnPointer: PointerU8, +): Int // should be Short?? + +/** + * fd_filestat_set_size(fd: fd, size: filesize) -> Result<(), errno> + * + * Adjust the size of an open file. If this increases the file's size, the extra bytes are filled with zeros. + * Note: This is similar to `ftruncate` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "fd_filestat_set_size") +internal external fun fd_filestat_set_size( + fd: fd, + size: Long, +): Int // should be Short?? + +/** + * fd_sync(fd: fd) -> Result<(), errno> + * + * Synchronize the data and metadata of a file to disk. + * Note: This is similar to `fsync` in POSIX. + */ +@WasmImport("wasi_snapshot_preview1", "fd_sync") +internal external fun fd_sync( + fd: fd, +): Int // should be Short?? + +/** + * fd_write(fd: fd, iovs: ciovec_array) -> Result<size, errno> + * + * Write to a file descriptor. + * Note: This is similar to `writev` in POSIX. + * + * Like POSIX, any calls of `write` (and other functions to read or write) + * for a regular file by other threads in the WASI process should not be + * interleaved while `write` is executed. + */ +@WasmImport("wasi_snapshot_preview1", "fd_write") +internal external fun fd_write( + fd: fd, + iovs: PointerU8, + iovsSize: size, + returnPointer: PointerU8, +): Int // should be Short?? diff --git a/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Rights.kt b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Rights.kt new file mode 100644 index 00000000..b72f26f4 --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/internal/preview1/Rights.kt @@ -0,0 +1,130 @@ +// Copyright 2019-2023 the Contributors to the WASI Specification +// This file is adapted from the WASI preview1 spec here: +// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md +package okio.internal.preview1 + +/** `rights: Record`. */ +typealias rights = Long + +/** + * The right to invoke [`fd_datasync`](#fd_datasync)0 + * If [`path_open`](#path_open) is set, includes the right to invoke + * [`path_open`](#path_open) with [`fdflags::dsync`](#fdflags.dsync). + */ +val right_fd_datasync = 1L shl 0 + +/** + * The right to invoke [`fd_read`](#fd_read) and [`sock_recv`](#sock_recv). + * If [`rights::fd_seek`](#rights.fd_seek) is set, includes the right to invoke [`fd_pread`](#fd_pread). + */ +val right_fd_read = 1L shl 1 + +/** The right to invoke [`fd_seek`](#fd_seek). This flag implies [`rights::fd_tell`](#rights.fd_tell). */ +val right_fd_seek = 1L shl 2 + +/** The right to invoke [`fd_fdstat_set_flags`](#fd_fdstat_set_flags). */ +val right_fd_fdstat_set_flags = 1L shl 3 + +/** + * The right to invoke [`fd_sync`](#fd_sync). + * If [`path_open`](#path_open) is set, includes the right to invoke + * [`path_open`](#path_open) with [`fdflags::rsync`](#fdflags.rsync) and [`fdflags::dsync`](#fdflags.dsync). + */ +val right_fd_sync = 1L shl 4 + +/** + * The right to invoke [`fd_seek`](#fd_seek) in such a way that the file offset + * remains unaltered (i.e., [`whence::cur`](#whence.cur) with offset zero), or to + * invoke [`fd_tell`](#fd_tell). + */ +val right_fd_tell = 1L shl 5 + +/** + * The right to invoke [`fd_write`](#fd_write) and [`sock_send`](#sock_send). + * If [`rights::fd_seek`](#rights.fd_seek) is set, includes the right to invoke [`fd_pwrite`](#fd_pwrite). + */ +val right_fd_write = 1L shl 6 + +/** The right to invoke [`fd_advise`](#fd_advise). */ +val right_fd_advise = 1L shl 7 + +/** The right to invoke [`fd_allocate`](#fd_allocate). */ +val right_fd_allocate = 1L shl 8 + +/** The right to invoke [`path_create_directory`](#path_create_directory). */ +val right_path_create_directory = 1L shl 9 + +/** If [`path_open`](#path_open) is set, the right to invoke [`path_open`](#path_open) with [`oflags::creat`](#oflags.creat). */ +val right_path_create_file = 1L shl 10 + +/** + * The right to invoke [`path_link`](#path_link) with the file descriptor as the + * source directory. + */ +val right_path_link_source = 1L shl 11 + +/** The right to invoke [`path_link`](#path_link) with the file descriptor as the target directory. + */ +val right_path_link_target = 1L shl 12 + +/** The right to invoke [`path_open`](#path_open). */ +val right_path_open = 1L shl 13 + +/** The right to invoke [`fd_readdir`](#fd_readdir). */ +val right_fd_readdir = 1L shl 14 + +/** The right to invoke [`path_readlink`](#path_readlink). */ +val right_path_readlink = 1L shl 15 + +/** The right to invoke [`path_rename`](#path_rename) with the file descriptor as the source directory. */ +val right_path_rename_source = 1L shl 16 + +/** The right to invoke [`path_rename`](#path_rename) with the file descriptor as the target directory. */ +val right_path_rename_target = 1L shl 17 + +/** The right to invoke [`path_filestat_get`](#path_filestat_get). */ +val right_path_filestat_get = 1L shl 18 + +/** + * The right to change a file's size. + * If [`path_open`](#path_open) is set, includes the right to invoke [`path_open`](#path_open) with [`oflags::trunc`](#oflags.trunc). + * Note: there is no function named `path_filestat_set_size`. This follows POSIX design, + * which only has `ftruncate` and does not provide `ftruncateat`. + * While such function would be desirable from the API design perspective, there are virtually + * no use cases for it since no code written for POSIX systems would use it. + * Moreover, implementing it would require multiple syscalls, leading to inferior performance. + */ +val right_path_filestat_set_size = 1L shl 19 + +/** The right to invoke [`path_filestat_set_times`](#path_filestat_set_times). */ +val right_path_filestat_set_times = 1L shl 20 + +/** The right to invoke [`fd_filestat_get`](#fd_filestat_get). */ +val right_fd_filestat_get = 1L shl 21 + +/** The right to invoke [`fd_filestat_set_size`](#fd_filestat_set_size). */ +val right_fd_filestat_set_size = 1L shl 22 + +/** The right to invoke [`fd_filestat_set_times`](#fd_filestat_set_times). */ +val right_fd_filestat_set_times = 1L shl 23 + +/** The right to invoke [`path_symlink`](#path_symlink). */ +val right_path_symlink = 1L shl 24 + +/** The right to invoke [`path_remove_directory`](#path_remove_directory). */ +val right_path_remove_directory = 1L shl 25 + +/** The right to invoke [`path_unlink_file`](#path_unlink_file). */ +val right_path_unlink_file = 1L shl 26 + +/** + * If [`rights::fd_read`](#rights.fd_read) is set, includes the right to invoke [`poll_oneoff`](#poll_oneoff) to subscribe to [`eventtype::fd_read`](#eventtype.fd_read). + * If [`rights::fd_write`](#rights.fd_write) is set, includes the right to invoke [`poll_oneoff`](#poll_oneoff) to subscribe to [`eventtype::fd_write`](#eventtype.fd_write). + */ +val right_poll_fd_readwrite = 1L shl 27 + +/** The right to invoke [`sock_shutdown`](#sock_shutdown). */ +val right_sock_shutdown = 1L shl 28 + +/** The right to invoke [`sock_accept`](#sock_accept). */ +val right_sock_accept = 1L shl 29 diff --git a/okio-wasifilesystem/src/wasmWasiTest/kotlin/okio/WasiFileSystemPreopensTest.kt b/okio-wasifilesystem/src/wasmWasiTest/kotlin/okio/WasiFileSystemPreopensTest.kt new file mode 100644 index 00000000..ed844fa5 --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiTest/kotlin/okio/WasiFileSystemPreopensTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNull +import okio.Path.Companion.toPath + +/** + * Confirm the [WasiFileSystem] can operate on different preopened directories independently. + * + * This tracks the `preopens` attribute in `.mjs` script in `okio-wasifilesystem/build.gradle.kts`. + */ +class WasiFileSystemPreopensTest { + private val fileSystem = WasiFileSystem + private val testId = "${this::class.simpleName}-${randomToken(16)}" + private val baseA: Path = "/a".toPath() / testId + private val baseB: Path = "/b".toPath() / testId + + @BeforeTest + fun setUp() { + fileSystem.createDirectory(baseA) + fileSystem.createDirectory(baseB) + } + + @Test + fun operateOnPreopens() { + fileSystem.write(baseA / "a.txt") { + writeUtf8("hello world a") + } + fileSystem.write(baseB / "b.txt") { + writeUtf8("bello burld") + } + assertEquals( + "hello world a".length.toLong(), + fileSystem.metadata(baseA / "a.txt").size, + ) + assertEquals( + "bello burld".length.toLong(), + fileSystem.metadata(baseB / "b.txt").size, + ) + } + + @Test + fun operateAcrossPreopens() { + fileSystem.write(baseA / "a.txt") { + writeUtf8("hello world") + } + + fileSystem.atomicMove(baseA / "a.txt", baseB / "b.txt") + + assertEquals( + "hello world", + fileSystem.read(baseB / "b.txt") { + readUtf8() + }, + ) + } + + @Test + fun cannotOperateOutsideOfPreopens() { + val noPreopen = "/c".toPath() / testId + assertFailsWith<FileNotFoundException> { + fileSystem.createDirectory(noPreopen) + } + assertFailsWith<FileNotFoundException> { + fileSystem.sink(noPreopen) + } + assertNull(fileSystem.metadataOrNull(noPreopen)) + assertFailsWith<FileNotFoundException> { + fileSystem.metadata(noPreopen) + } + assertNull(fileSystem.listOrNull(noPreopen)) + assertFailsWith<FileNotFoundException> { + fileSystem.list(noPreopen) + } + } +} diff --git a/okio-wasifilesystem/src/wasmWasiTest/kotlin/okio/WasiFileSystemTest.kt b/okio-wasifilesystem/src/wasmWasiTest/kotlin/okio/WasiFileSystemTest.kt new file mode 100644 index 00000000..b6846b45 --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiTest/kotlin/okio/WasiFileSystemTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 okio.Path.Companion.toPath + +class WasiFileSystemTest : AbstractFileSystemTest( + clock = WasiClock, + fileSystem = WasiFileSystem, + windowsLimitations = Path.DIRECTORY_SEPARATOR == "\\", + allowClobberingEmptyDirectories = Path.DIRECTORY_SEPARATOR == "\\", + allowAtomicMoveFromFileToDirectory = false, + temporaryDirectory = "/tmp".toPath(), +) diff --git a/okio-wasifilesystem/src/wasmWasiTest/kotlin/okio/WasiTest.kt b/okio-wasifilesystem/src/wasmWasiTest/kotlin/okio/WasiTest.kt new file mode 100644 index 00000000..6992cb2c --- /dev/null +++ b/okio-wasifilesystem/src/wasmWasiTest/kotlin/okio/WasiTest.kt @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue +import okio.ByteString.Companion.encodeUtf8 +import okio.Path.Companion.toPath + +class WasiTest { + private val fileSystem = WasiFileSystem + private val base: Path = "/tmp".toPath() / "${this::class.simpleName}-${randomToken(16)}" + + @BeforeTest + fun setUp() { + fileSystem.createDirectory(base) + } + + @Test + fun createDirectory() { + fileSystem.createDirectory(base / "child") + } + + @Test + fun canonicalizeAbsolutePathNoSymlinks() { + val path = base / "regular_file.txt" + fileSystem.write(path) { + writeUtf8("hello") + } + assertEquals( + path, + fileSystem.canonicalize(path), + ) + } + + @Test + fun canonicalizeAbsolutePathWithSymlinksInFiles() { + val target = base / "target" + val source = base / "source" + fileSystem.write(target) { + writeUtf8("hello") + } + fileSystem.createSymlink(source, "target".toPath()) + assertEquals( + target, + fileSystem.canonicalize(source), + ) + } + + @Test + fun canonicalizeAbsolutePathWithSymlinksInDirectories() { + val target = base / "target" + val source = base / "source" + fileSystem.createDirectory(target) + fileSystem.write(target / "file.txt") { + writeUtf8("hello") + } + fileSystem.createSymlink(source, "target".toPath()) + assertEquals( + target / "file.txt", + fileSystem.canonicalize(source / "file.txt"), + ) + } + + @Test + fun canonicalizeAbsolutePathWithSymlinkCycle() { + fileSystem.createSymlink(base / "rock", "scissors".toPath()) + fileSystem.createSymlink(base / "scissors", "paper".toPath()) + fileSystem.createSymlink(base / "paper", "rock".toPath()) + val e = assertFailsWith<IOException> { + fileSystem.canonicalize(base / "rock") + } + assertEquals("symlink cycle?", e.message) + } + + @Test + fun writeAndReadEmptyFile() { + writeAndReadFile(ByteString.EMPTY, base / "empty.txt") + } + + @Test + fun writeAndReadShortFile() { + writeAndReadFile("hello\n".encodeUtf8(), base / "hello.txt") + } + + private fun writeAndReadFile(content: ByteString, fileName: Path) { + fileSystem.write(fileName) { + write(content) + } + assertEquals( + content, + fileSystem.read(fileName) { + readByteString() + }, + ) + } + + @Test + fun writeAndReadLongFile() { + val fileName = base / "5m_bytes.txt" + fileSystem.write(fileName) { + for (i in 0L until 1_000_000L) { + writeByte(i.toInt()) + writeByte(0) + writeByte(0) + writeByte(0) + writeByte(0) + } + } + fileSystem.read(fileName) { + for (i in 0L until 1_000_000L) { + assertEquals(i.toByte(), readByte()) + assertEquals(0, readByte()) + assertEquals(0, readByte()) + assertEquals(0, readByte()) + assertEquals(0, readByte()) + } + assertTrue(exhausted()) + } + } + + @Test + fun appendToFile() { + val fileName = base / "append.txt" + fileSystem.write(fileName) { + writeUtf8("hello") + } + fileSystem.appendingSink(fileName).buffer().use { + it.writeUtf8(" world") + } + assertEquals( + "hello world", + fileSystem.read(fileName) { + readUtf8() + }, + ) + } + + @Test + fun listDirectory() { + fileSystem.write(base / "a") { + writeUtf8("this file has a 1-byte file name") + } + fileSystem.write(base / "a.txt") { + writeUtf8("this file has a 5-byte file name") + } + + assertEquals( + listOf( + base / "a", + base / "a.txt", + ), + fileSystem.list(base).sorted(), + ) + } + + @Test + fun deleteFile() { + fileSystem.write(base / "a") { + } + fileSystem.write(base / "b") { + } + fileSystem.write(base / "c") { + } + fileSystem.delete(base / "b") + + assertEquals( + listOf( + base / "a", + base / "c", + ), + fileSystem.list(base).sorted(), + ) + } + + @Test + fun deleteDirectory() { + fileSystem.createDirectory(base / "a") + fileSystem.createDirectory(base / "b") + fileSystem.createDirectory(base / "c") + fileSystem.delete(base / "b") + + assertEquals( + listOf( + base / "a", + base / "c", + ), + fileSystem.list(base).sorted(), + ) + } + + @Test + fun createSymlink() { + val targetPath = base / "target" + val sourcePath = base / "source" + fileSystem.write(targetPath) { + writeUtf8("this is the target file's contents") + } + fileSystem.createSymlink(sourcePath, "target".toPath()) + + assertEquals( + "this is the target file's contents", + fileSystem.read(sourcePath) { + readUtf8() + }, + ) + } + + @Test + fun rename() { + val targetPath = base / "target" + val sourcePath = base / "source" + fileSystem.write(sourcePath) { + writeUtf8("this is the file's contents") + } + fileSystem.atomicMove(sourcePath, targetPath) + + assertEquals( + "this is the file's contents", + fileSystem.read(targetPath) { + readUtf8() + }, + ) + assertEquals( + listOf(targetPath), + fileSystem.list(base), + ) + } + + @Test + fun fileMetadata() { + val regularFile = base / "regularFile" + val directory = base / "directory" + val symlink = base / "symlink" + fileSystem.write(regularFile) { + writeUtf8("this is a regular file") + } + fileSystem.createDirectory(directory) + fileSystem.createSymlink(symlink, "regularFile".toPath()) + + val regularFileMetadata = fileSystem.metadata(regularFile) + assertEquals(true, regularFileMetadata.isRegularFile) + assertEquals(false, regularFileMetadata.isDirectory) + assertEquals(null, regularFileMetadata.symlinkTarget) + assertEquals(22L, regularFileMetadata.size) + + val directoryMetadata = fileSystem.metadata(directory) + assertEquals(false, directoryMetadata.isRegularFile) + assertEquals(true, directoryMetadata.isDirectory) + assertEquals(null, directoryMetadata.symlinkTarget) + // Note: no assertions about directory size. + + val symlinkMetadata = fileSystem.metadata(symlink) + assertEquals(false, symlinkMetadata.isRegularFile) + assertEquals(false, symlinkMetadata.isDirectory) + assertEquals("regularFile".toPath(), symlinkMetadata.symlinkTarget) + assertEquals("regularFile".length.toLong(), symlinkMetadata.size) + } + + @Test + fun absentMetadata() { + assertEquals(null, fileSystem.metadataOrNull(base / "no-such-file")) + assertFailsWith<FileNotFoundException> { + fileSystem.metadata(base / "no-such-file") + } + } + + @Test + fun fileHandleRead() { + val path = base / "file.txt" + fileSystem.write(path) { + writeUtf8("this is a file about dogs and cats") + } + fileSystem.openReadOnly(path).use { handle -> + val sink = Buffer() + handle.read(21L, sink, 4L) + + assertEquals( + "dogs", + sink.readUtf8(), + ) + } + } + + @Test + fun fileHandleWrite() { + val path = base / "file.txt" + fileSystem.write(path) { + writeUtf8("this is a file about cats and cats") + } + fileSystem.openReadWrite(path).use { handle -> + val source = Buffer().writeUtf8("dogs") + handle.write(21L, source, 4L) + + assertEquals( + "this is a file about dogs and cats", + fileSystem.read(path) { + readUtf8() + }, + ) + } + } + + @Test + fun fileHandleGetSize() { + val path = base / "file.txt" + fileSystem.write(path) { + writeUtf8("this is a file about dogs and cats") + } + fileSystem.openReadOnly(path).use { handle -> + assertEquals( + 34L, + handle.size(), + ) + } + } + + @Test + fun fileHandleResize() { + val path = base / "file.txt" + fileSystem.write(path) { + writeUtf8("this is a file about dogs and cats") + } + fileSystem.openReadWrite(path).use { handle -> + handle.resize(25L) + + assertEquals( + "this is a file about dogs", + fileSystem.read(path) { + readUtf8() + }, + ) + } + } + + @Test + fun fileHandleFlush() { + val path = base / "file.txt" + fileSystem.openReadWrite(path).use { handle -> + handle.sink().buffer().use { + it.writeUtf8("hello") + } + handle.flush() + + assertEquals( + "hello", + fileSystem.read(path) { + readUtf8() + }, + ) + } + } + + @Test + fun fileSinkFlush() { + val path = base / "file.txt" + fileSystem.write(path) { + writeUtf8("hello") + flush() + + assertEquals( + "hello", + fileSystem.read(path) { + readUtf8() + }, + ) + } + } +} diff --git a/okio/api/okio.api b/okio/api/okio.api new file mode 100644 index 00000000..b85e1870 --- /dev/null +++ b/okio/api/okio.api @@ -0,0 +1,809 @@ +public final class okio/-DeflaterSinkExtensions { + public static final fun deflate (Lokio/Sink;Ljava/util/zip/Deflater;)Lokio/DeflaterSink; + public static synthetic fun deflate$default (Lokio/Sink;Ljava/util/zip/Deflater;ILjava/lang/Object;)Lokio/DeflaterSink; +} + +public final class okio/-DeprecatedOkio { + public static final field INSTANCE Lokio/-DeprecatedOkio; + public final fun appendingSink (Ljava/io/File;)Lokio/Sink; + public final fun blackhole ()Lokio/Sink; + public final fun buffer (Lokio/Sink;)Lokio/BufferedSink; + public final fun buffer (Lokio/Source;)Lokio/BufferedSource; + public final fun sink (Ljava/io/File;)Lokio/Sink; + public final fun sink (Ljava/io/OutputStream;)Lokio/Sink; + public final fun sink (Ljava/net/Socket;)Lokio/Sink; + public final fun sink (Ljava/nio/file/Path;[Ljava/nio/file/OpenOption;)Lokio/Sink; + public final fun source (Ljava/io/File;)Lokio/Source; + public final fun source (Ljava/io/InputStream;)Lokio/Source; + public final fun source (Ljava/net/Socket;)Lokio/Source; + public final fun source (Ljava/nio/file/Path;[Ljava/nio/file/OpenOption;)Lokio/Source; +} + +public final class okio/-DeprecatedUpgrade { + public static final fun getOkio ()Lokio/-DeprecatedOkio; + public static final fun getUtf8 ()Lokio/-DeprecatedUtf8; +} + +public final class okio/-DeprecatedUtf8 { + public static final field INSTANCE Lokio/-DeprecatedUtf8; + public final fun size (Ljava/lang/String;)J + public final fun size (Ljava/lang/String;II)J +} + +public final class okio/-GzipSinkExtensions { + public static final fun gzip (Lokio/Sink;)Lokio/GzipSink; +} + +public final class okio/-GzipSourceExtensions { + public static final fun gzip (Lokio/Source;)Lokio/GzipSource; +} + +public final class okio/-InflaterSourceExtensions { + public static final fun inflate (Lokio/Source;Ljava/util/zip/Inflater;)Lokio/InflaterSource; + public static synthetic fun inflate$default (Lokio/Source;Ljava/util/zip/Inflater;ILjava/lang/Object;)Lokio/InflaterSource; +} + +public class okio/AsyncTimeout : okio/Timeout { + public fun <init> ()V + public final fun access$newTimeoutException (Ljava/io/IOException;)Ljava/io/IOException; + public fun cancel ()V + public final fun enter ()V + public final fun exit ()Z + protected fun newTimeoutException (Ljava/io/IOException;)Ljava/io/IOException; + public final fun sink (Lokio/Sink;)Lokio/Sink; + public final fun source (Lokio/Source;)Lokio/Source; + protected fun timedOut ()V + public final fun withTimeout (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; +} + +public final class okio/Buffer : java/lang/Cloneable, java/nio/channels/ByteChannel, okio/BufferedSink, okio/BufferedSource { + public final fun -deprecated_getByte (J)B + public final fun -deprecated_size ()J + public fun <init> ()V + public fun buffer ()Lokio/Buffer; + public final fun clear ()V + public synthetic fun clone ()Ljava/lang/Object; + public fun clone ()Lokio/Buffer; + public fun close ()V + public final fun completeSegmentByteCount ()J + public final fun copy ()Lokio/Buffer; + public final fun copyTo (Ljava/io/OutputStream;)Lokio/Buffer; + public final fun copyTo (Ljava/io/OutputStream;J)Lokio/Buffer; + public final fun copyTo (Ljava/io/OutputStream;JJ)Lokio/Buffer; + public final fun copyTo (Lokio/Buffer;J)Lokio/Buffer; + public final fun copyTo (Lokio/Buffer;JJ)Lokio/Buffer; + public static synthetic fun copyTo$default (Lokio/Buffer;Ljava/io/OutputStream;JJILjava/lang/Object;)Lokio/Buffer; + public static synthetic fun copyTo$default (Lokio/Buffer;Lokio/Buffer;JILjava/lang/Object;)Lokio/Buffer; + public static synthetic fun copyTo$default (Lokio/Buffer;Lokio/Buffer;JJILjava/lang/Object;)Lokio/Buffer; + public fun emit ()Lokio/Buffer; + public synthetic fun emit ()Lokio/BufferedSink; + public fun emitCompleteSegments ()Lokio/Buffer; + public synthetic fun emitCompleteSegments ()Lokio/BufferedSink; + public fun equals (Ljava/lang/Object;)Z + public fun exhausted ()Z + public fun flush ()V + public fun getBuffer ()Lokio/Buffer; + public final fun getByte (J)B + public fun hashCode ()I + public final fun hmacSha1 (Lokio/ByteString;)Lokio/ByteString; + public final fun hmacSha256 (Lokio/ByteString;)Lokio/ByteString; + public final fun hmacSha512 (Lokio/ByteString;)Lokio/ByteString; + public fun indexOf (B)J + public fun indexOf (BJ)J + public fun indexOf (BJJ)J + public fun indexOf (Lokio/ByteString;)J + public fun indexOf (Lokio/ByteString;J)J + public fun indexOfElement (Lokio/ByteString;)J + public fun indexOfElement (Lokio/ByteString;J)J + public fun inputStream ()Ljava/io/InputStream; + public fun isOpen ()Z + public final fun md5 ()Lokio/ByteString; + public fun outputStream ()Ljava/io/OutputStream; + public fun peek ()Lokio/BufferedSource; + public fun rangeEquals (JLokio/ByteString;)Z + public fun rangeEquals (JLokio/ByteString;II)Z + public fun read (Ljava/nio/ByteBuffer;)I + public fun read (Lokio/Buffer;J)J + public fun read ([B)I + public fun read ([BII)I + public fun readAll (Lokio/Sink;)J + public final fun readAndWriteUnsafe ()Lokio/Buffer$UnsafeCursor; + public final fun readAndWriteUnsafe (Lokio/Buffer$UnsafeCursor;)Lokio/Buffer$UnsafeCursor; + public static synthetic fun readAndWriteUnsafe$default (Lokio/Buffer;Lokio/Buffer$UnsafeCursor;ILjava/lang/Object;)Lokio/Buffer$UnsafeCursor; + public fun readByte ()B + public fun readByteArray ()[B + public fun readByteArray (J)[B + public fun readByteString ()Lokio/ByteString; + public fun readByteString (J)Lokio/ByteString; + public fun readDecimalLong ()J + public final fun readFrom (Ljava/io/InputStream;)Lokio/Buffer; + public final fun readFrom (Ljava/io/InputStream;J)Lokio/Buffer; + public fun readFully (Lokio/Buffer;J)V + public fun readFully ([B)V + public fun readHexadecimalUnsignedLong ()J + public fun readInt ()I + public fun readIntLe ()I + public fun readLong ()J + public fun readLongLe ()J + public fun readShort ()S + public fun readShortLe ()S + public fun readString (JLjava/nio/charset/Charset;)Ljava/lang/String; + public fun readString (Ljava/nio/charset/Charset;)Ljava/lang/String; + public final fun readUnsafe ()Lokio/Buffer$UnsafeCursor; + public final fun readUnsafe (Lokio/Buffer$UnsafeCursor;)Lokio/Buffer$UnsafeCursor; + public static synthetic fun readUnsafe$default (Lokio/Buffer;Lokio/Buffer$UnsafeCursor;ILjava/lang/Object;)Lokio/Buffer$UnsafeCursor; + public fun readUtf8 ()Ljava/lang/String; + public fun readUtf8 (J)Ljava/lang/String; + public fun readUtf8CodePoint ()I + public fun readUtf8Line ()Ljava/lang/String; + public fun readUtf8LineStrict ()Ljava/lang/String; + public fun readUtf8LineStrict (J)Ljava/lang/String; + public fun request (J)Z + public fun require (J)V + public fun select (Lokio/Options;)I + public final fun sha1 ()Lokio/ByteString; + public final fun sha256 ()Lokio/ByteString; + public final fun sha512 ()Lokio/ByteString; + public final fun size ()J + public fun skip (J)V + public final fun snapshot ()Lokio/ByteString; + public final fun snapshot (I)Lokio/ByteString; + public fun timeout ()Lokio/Timeout; + public fun toString ()Ljava/lang/String; + public fun write (Ljava/nio/ByteBuffer;)I + public fun write (Lokio/Buffer;J)V + public fun write (Lokio/ByteString;)Lokio/Buffer; + public synthetic fun write (Lokio/ByteString;)Lokio/BufferedSink; + public fun write (Lokio/ByteString;II)Lokio/Buffer; + public synthetic fun write (Lokio/ByteString;II)Lokio/BufferedSink; + public fun write (Lokio/Source;J)Lokio/Buffer; + public synthetic fun write (Lokio/Source;J)Lokio/BufferedSink; + public fun write ([B)Lokio/Buffer; + public synthetic fun write ([B)Lokio/BufferedSink; + public fun write ([BII)Lokio/Buffer; + public synthetic fun write ([BII)Lokio/BufferedSink; + public fun writeAll (Lokio/Source;)J + public fun writeByte (I)Lokio/Buffer; + public synthetic fun writeByte (I)Lokio/BufferedSink; + public fun writeDecimalLong (J)Lokio/Buffer; + public synthetic fun writeDecimalLong (J)Lokio/BufferedSink; + public fun writeHexadecimalUnsignedLong (J)Lokio/Buffer; + public synthetic fun writeHexadecimalUnsignedLong (J)Lokio/BufferedSink; + public fun writeInt (I)Lokio/Buffer; + public synthetic fun writeInt (I)Lokio/BufferedSink; + public fun writeIntLe (I)Lokio/Buffer; + public synthetic fun writeIntLe (I)Lokio/BufferedSink; + public fun writeLong (J)Lokio/Buffer; + public synthetic fun writeLong (J)Lokio/BufferedSink; + public fun writeLongLe (J)Lokio/Buffer; + public synthetic fun writeLongLe (J)Lokio/BufferedSink; + public fun writeShort (I)Lokio/Buffer; + public synthetic fun writeShort (I)Lokio/BufferedSink; + public fun writeShortLe (I)Lokio/Buffer; + public synthetic fun writeShortLe (I)Lokio/BufferedSink; + public fun writeString (Ljava/lang/String;IILjava/nio/charset/Charset;)Lokio/Buffer; + public synthetic fun writeString (Ljava/lang/String;IILjava/nio/charset/Charset;)Lokio/BufferedSink; + public fun writeString (Ljava/lang/String;Ljava/nio/charset/Charset;)Lokio/Buffer; + public synthetic fun writeString (Ljava/lang/String;Ljava/nio/charset/Charset;)Lokio/BufferedSink; + public final fun writeTo (Ljava/io/OutputStream;)Lokio/Buffer; + public final fun writeTo (Ljava/io/OutputStream;J)Lokio/Buffer; + public static synthetic fun writeTo$default (Lokio/Buffer;Ljava/io/OutputStream;JILjava/lang/Object;)Lokio/Buffer; + public fun writeUtf8 (Ljava/lang/String;)Lokio/Buffer; + public synthetic fun writeUtf8 (Ljava/lang/String;)Lokio/BufferedSink; + public fun writeUtf8 (Ljava/lang/String;II)Lokio/Buffer; + public synthetic fun writeUtf8 (Ljava/lang/String;II)Lokio/BufferedSink; + public fun writeUtf8CodePoint (I)Lokio/Buffer; + public synthetic fun writeUtf8CodePoint (I)Lokio/BufferedSink; +} + +public final class okio/Buffer$UnsafeCursor : java/io/Closeable { + public field buffer Lokio/Buffer; + public field data [B + public field end I + public field offset J + public field readWrite Z + public field start I + public fun <init> ()V + public fun close ()V + public final fun expandBuffer (I)J + public final fun next ()I + public final fun resizeBuffer (J)J + public final fun seek (J)I +} + +public abstract interface class okio/BufferedSink : java/nio/channels/WritableByteChannel, okio/Sink { + public abstract fun buffer ()Lokio/Buffer; + public abstract fun emit ()Lokio/BufferedSink; + public abstract fun emitCompleteSegments ()Lokio/BufferedSink; + public abstract fun flush ()V + public abstract fun getBuffer ()Lokio/Buffer; + public abstract fun outputStream ()Ljava/io/OutputStream; + public abstract fun write (Lokio/ByteString;)Lokio/BufferedSink; + public abstract fun write (Lokio/ByteString;II)Lokio/BufferedSink; + public abstract fun write (Lokio/Source;J)Lokio/BufferedSink; + public abstract fun write ([B)Lokio/BufferedSink; + public abstract fun write ([BII)Lokio/BufferedSink; + public abstract fun writeAll (Lokio/Source;)J + public abstract fun writeByte (I)Lokio/BufferedSink; + public abstract fun writeDecimalLong (J)Lokio/BufferedSink; + public abstract fun writeHexadecimalUnsignedLong (J)Lokio/BufferedSink; + public abstract fun writeInt (I)Lokio/BufferedSink; + public abstract fun writeIntLe (I)Lokio/BufferedSink; + public abstract fun writeLong (J)Lokio/BufferedSink; + public abstract fun writeLongLe (J)Lokio/BufferedSink; + public abstract fun writeShort (I)Lokio/BufferedSink; + public abstract fun writeShortLe (I)Lokio/BufferedSink; + public abstract fun writeString (Ljava/lang/String;IILjava/nio/charset/Charset;)Lokio/BufferedSink; + public abstract fun writeString (Ljava/lang/String;Ljava/nio/charset/Charset;)Lokio/BufferedSink; + public abstract fun writeUtf8 (Ljava/lang/String;)Lokio/BufferedSink; + public abstract fun writeUtf8 (Ljava/lang/String;II)Lokio/BufferedSink; + public abstract fun writeUtf8CodePoint (I)Lokio/BufferedSink; +} + +public abstract interface class okio/BufferedSource : java/nio/channels/ReadableByteChannel, okio/Source { + public abstract fun buffer ()Lokio/Buffer; + public abstract fun exhausted ()Z + public abstract fun getBuffer ()Lokio/Buffer; + public abstract fun indexOf (B)J + public abstract fun indexOf (BJ)J + public abstract fun indexOf (BJJ)J + public abstract fun indexOf (Lokio/ByteString;)J + public abstract fun indexOf (Lokio/ByteString;J)J + public abstract fun indexOfElement (Lokio/ByteString;)J + public abstract fun indexOfElement (Lokio/ByteString;J)J + public abstract fun inputStream ()Ljava/io/InputStream; + public abstract fun peek ()Lokio/BufferedSource; + public abstract fun rangeEquals (JLokio/ByteString;)Z + public abstract fun rangeEquals (JLokio/ByteString;II)Z + public abstract fun read ([B)I + public abstract fun read ([BII)I + public abstract fun readAll (Lokio/Sink;)J + public abstract fun readByte ()B + public abstract fun readByteArray ()[B + public abstract fun readByteArray (J)[B + public abstract fun readByteString ()Lokio/ByteString; + public abstract fun readByteString (J)Lokio/ByteString; + public abstract fun readDecimalLong ()J + public abstract fun readFully (Lokio/Buffer;J)V + public abstract fun readFully ([B)V + public abstract fun readHexadecimalUnsignedLong ()J + public abstract fun readInt ()I + public abstract fun readIntLe ()I + public abstract fun readLong ()J + public abstract fun readLongLe ()J + public abstract fun readShort ()S + public abstract fun readShortLe ()S + public abstract fun readString (JLjava/nio/charset/Charset;)Ljava/lang/String; + public abstract fun readString (Ljava/nio/charset/Charset;)Ljava/lang/String; + public abstract fun readUtf8 ()Ljava/lang/String; + public abstract fun readUtf8 (J)Ljava/lang/String; + public abstract fun readUtf8CodePoint ()I + public abstract fun readUtf8Line ()Ljava/lang/String; + public abstract fun readUtf8LineStrict ()Ljava/lang/String; + public abstract fun readUtf8LineStrict (J)Ljava/lang/String; + public abstract fun request (J)Z + public abstract fun require (J)V + public abstract fun select (Lokio/Options;)I + public abstract fun skip (J)V +} + +public class okio/ByteString : java/io/Serializable, java/lang/Comparable { + public static final field Companion Lokio/ByteString$Companion; + public static final field EMPTY Lokio/ByteString; + public final fun -deprecated_getByte (I)B + public final fun -deprecated_size ()I + public fun asByteBuffer ()Ljava/nio/ByteBuffer; + public fun base64 ()Ljava/lang/String; + public fun base64Url ()Ljava/lang/String; + public synthetic fun compareTo (Ljava/lang/Object;)I + public fun compareTo (Lokio/ByteString;)I + public fun copyInto (I[BII)V + public static synthetic fun copyInto$default (Lokio/ByteString;I[BIIILjava/lang/Object;)V + public static final fun decodeBase64 (Ljava/lang/String;)Lokio/ByteString; + public static final fun decodeHex (Ljava/lang/String;)Lokio/ByteString; + public static final fun encodeString (Ljava/lang/String;Ljava/nio/charset/Charset;)Lokio/ByteString; + public static final fun encodeUtf8 (Ljava/lang/String;)Lokio/ByteString; + public final fun endsWith (Lokio/ByteString;)Z + public final fun endsWith ([B)Z + public fun equals (Ljava/lang/Object;)Z + public final fun getByte (I)B + public fun hashCode ()I + public fun hex ()Ljava/lang/String; + public fun hmacSha1 (Lokio/ByteString;)Lokio/ByteString; + public fun hmacSha256 (Lokio/ByteString;)Lokio/ByteString; + public fun hmacSha512 (Lokio/ByteString;)Lokio/ByteString; + public final fun indexOf (Lokio/ByteString;)I + public final fun indexOf (Lokio/ByteString;I)I + public final fun indexOf ([B)I + public fun indexOf ([BI)I + public static synthetic fun indexOf$default (Lokio/ByteString;Lokio/ByteString;IILjava/lang/Object;)I + public static synthetic fun indexOf$default (Lokio/ByteString;[BIILjava/lang/Object;)I + public final fun lastIndexOf (Lokio/ByteString;)I + public final fun lastIndexOf (Lokio/ByteString;I)I + public final fun lastIndexOf ([B)I + public fun lastIndexOf ([BI)I + public static synthetic fun lastIndexOf$default (Lokio/ByteString;Lokio/ByteString;IILjava/lang/Object;)I + public static synthetic fun lastIndexOf$default (Lokio/ByteString;[BIILjava/lang/Object;)I + public final fun md5 ()Lokio/ByteString; + public static final fun of (Ljava/nio/ByteBuffer;)Lokio/ByteString; + public static final fun of ([B)Lokio/ByteString; + public static final fun of ([BII)Lokio/ByteString; + public fun rangeEquals (ILokio/ByteString;II)Z + public fun rangeEquals (I[BII)Z + public static final fun read (Ljava/io/InputStream;I)Lokio/ByteString; + public final fun sha1 ()Lokio/ByteString; + public final fun sha256 ()Lokio/ByteString; + public final fun sha512 ()Lokio/ByteString; + public final fun size ()I + public final fun startsWith (Lokio/ByteString;)Z + public final fun startsWith ([B)Z + public fun string (Ljava/nio/charset/Charset;)Ljava/lang/String; + public final fun substring ()Lokio/ByteString; + public final fun substring (I)Lokio/ByteString; + public fun substring (II)Lokio/ByteString; + public static synthetic fun substring$default (Lokio/ByteString;IIILjava/lang/Object;)Lokio/ByteString; + public fun toAsciiLowercase ()Lokio/ByteString; + public fun toAsciiUppercase ()Lokio/ByteString; + public fun toByteArray ()[B + public fun toString ()Ljava/lang/String; + public fun utf8 ()Ljava/lang/String; + public fun write (Ljava/io/OutputStream;)V +} + +public final class okio/ByteString$Companion { + public final fun -deprecated_decodeBase64 (Ljava/lang/String;)Lokio/ByteString; + public final fun -deprecated_decodeHex (Ljava/lang/String;)Lokio/ByteString; + public final fun -deprecated_encodeString (Ljava/lang/String;Ljava/nio/charset/Charset;)Lokio/ByteString; + public final fun -deprecated_encodeUtf8 (Ljava/lang/String;)Lokio/ByteString; + public final fun -deprecated_of (Ljava/nio/ByteBuffer;)Lokio/ByteString; + public final fun -deprecated_of ([BII)Lokio/ByteString; + public final fun -deprecated_read (Ljava/io/InputStream;I)Lokio/ByteString; + public final fun decodeBase64 (Ljava/lang/String;)Lokio/ByteString; + public final fun decodeHex (Ljava/lang/String;)Lokio/ByteString; + public final fun encodeString (Ljava/lang/String;Ljava/nio/charset/Charset;)Lokio/ByteString; + public static synthetic fun encodeString$default (Lokio/ByteString$Companion;Ljava/lang/String;Ljava/nio/charset/Charset;ILjava/lang/Object;)Lokio/ByteString; + public final fun encodeUtf8 (Ljava/lang/String;)Lokio/ByteString; + public final fun of (Ljava/nio/ByteBuffer;)Lokio/ByteString; + public final fun of ([B)Lokio/ByteString; + public final fun of ([BII)Lokio/ByteString; + public static synthetic fun of$default (Lokio/ByteString$Companion;[BIIILjava/lang/Object;)Lokio/ByteString; + public final fun read (Ljava/io/InputStream;I)Lokio/ByteString; +} + +public final class okio/CipherSink : okio/Sink { + public fun <init> (Lokio/BufferedSink;Ljavax/crypto/Cipher;)V + public fun close ()V + public fun flush ()V + public final fun getCipher ()Ljavax/crypto/Cipher; + public fun timeout ()Lokio/Timeout; + public fun write (Lokio/Buffer;J)V +} + +public final class okio/CipherSource : okio/Source { + public fun <init> (Lokio/BufferedSource;Ljavax/crypto/Cipher;)V + public fun close ()V + public final fun getCipher ()Ljavax/crypto/Cipher; + public fun read (Lokio/Buffer;J)J + public fun timeout ()Lokio/Timeout; +} + +public final class okio/DeflaterSink : okio/Sink { + public fun <init> (Lokio/Sink;Ljava/util/zip/Deflater;)V + public fun close ()V + public fun flush ()V + public fun timeout ()Lokio/Timeout; + public fun toString ()Ljava/lang/String; + public fun write (Lokio/Buffer;J)V +} + +public abstract interface annotation class okio/ExperimentalFileSystem : java/lang/annotation/Annotation { +} + +public abstract class okio/FileHandle : java/io/Closeable { + public fun <init> (Z)V + public final fun appendingSink ()Lokio/Sink; + public final fun close ()V + public final fun flush ()V + public final fun getLock ()Ljava/util/concurrent/locks/ReentrantLock; + public final fun getReadWrite ()Z + public final fun position (Lokio/Sink;)J + public final fun position (Lokio/Source;)J + protected abstract fun protectedClose ()V + protected abstract fun protectedFlush ()V + protected abstract fun protectedRead (J[BII)I + protected abstract fun protectedResize (J)V + protected abstract fun protectedSize ()J + protected abstract fun protectedWrite (J[BII)V + public final fun read (JLokio/Buffer;J)J + public final fun read (J[BII)I + public final fun reposition (Lokio/Sink;J)V + public final fun reposition (Lokio/Source;J)V + public final fun resize (J)V + public final fun sink (J)Lokio/Sink; + public static synthetic fun sink$default (Lokio/FileHandle;JILjava/lang/Object;)Lokio/Sink; + public final fun size ()J + public final fun source (J)Lokio/Source; + public static synthetic fun source$default (Lokio/FileHandle;JILjava/lang/Object;)Lokio/Source; + public final fun write (JLokio/Buffer;J)V + public final fun write (J[BII)V +} + +public final class okio/FileMetadata { + public fun <init> ()V + public fun <init> (ZZLokio/Path;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/Map;)V + public synthetic fun <init> (ZZLokio/Path;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun copy (ZZLokio/Path;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/Map;)Lokio/FileMetadata; + public static synthetic fun copy$default (Lokio/FileMetadata;ZZLokio/Path;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/Map;ILjava/lang/Object;)Lokio/FileMetadata; + public final fun extra (Lkotlin/reflect/KClass;)Ljava/lang/Object; + public final fun getCreatedAtMillis ()Ljava/lang/Long; + public final fun getExtras ()Ljava/util/Map; + public final fun getLastAccessedAtMillis ()Ljava/lang/Long; + public final fun getLastModifiedAtMillis ()Ljava/lang/Long; + public final fun getSize ()Ljava/lang/Long; + public final fun getSymlinkTarget ()Lokio/Path; + public final fun isDirectory ()Z + public final fun isRegularFile ()Z + public fun toString ()Ljava/lang/String; +} + +public abstract class okio/FileSystem { + public static final field Companion Lokio/FileSystem$Companion; + public static final field RESOURCES Lokio/FileSystem; + public static final field SYSTEM Lokio/FileSystem; + public static final field SYSTEM_TEMPORARY_DIRECTORY Lokio/Path; + public final fun -read (Lokio/Path;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public final fun -write (Lokio/Path;ZLkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static synthetic fun -write$default (Lokio/FileSystem;Lokio/Path;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; + public fun <init> ()V + public final fun appendingSink (Lokio/Path;)Lokio/Sink; + public abstract fun appendingSink (Lokio/Path;Z)Lokio/Sink; + public static synthetic fun appendingSink$default (Lokio/FileSystem;Lokio/Path;ZILjava/lang/Object;)Lokio/Sink; + public abstract fun atomicMove (Lokio/Path;Lokio/Path;)V + public abstract fun canonicalize (Lokio/Path;)Lokio/Path; + public fun copy (Lokio/Path;Lokio/Path;)V + public final fun createDirectories (Lokio/Path;)V + public final fun createDirectories (Lokio/Path;Z)V + public static synthetic fun createDirectories$default (Lokio/FileSystem;Lokio/Path;ZILjava/lang/Object;)V + public final fun createDirectory (Lokio/Path;)V + public abstract fun createDirectory (Lokio/Path;Z)V + public static synthetic fun createDirectory$default (Lokio/FileSystem;Lokio/Path;ZILjava/lang/Object;)V + public abstract fun createSymlink (Lokio/Path;Lokio/Path;)V + public final fun delete (Lokio/Path;)V + public abstract fun delete (Lokio/Path;Z)V + public static synthetic fun delete$default (Lokio/FileSystem;Lokio/Path;ZILjava/lang/Object;)V + public final fun deleteRecursively (Lokio/Path;)V + public fun deleteRecursively (Lokio/Path;Z)V + public static synthetic fun deleteRecursively$default (Lokio/FileSystem;Lokio/Path;ZILjava/lang/Object;)V + public final fun exists (Lokio/Path;)Z + public static final fun get (Ljava/nio/file/FileSystem;)Lokio/FileSystem; + public abstract fun list (Lokio/Path;)Ljava/util/List; + public abstract fun listOrNull (Lokio/Path;)Ljava/util/List; + public final fun listRecursively (Lokio/Path;)Lkotlin/sequences/Sequence; + public fun listRecursively (Lokio/Path;Z)Lkotlin/sequences/Sequence; + public static synthetic fun listRecursively$default (Lokio/FileSystem;Lokio/Path;ZILjava/lang/Object;)Lkotlin/sequences/Sequence; + public final fun metadata (Lokio/Path;)Lokio/FileMetadata; + public abstract fun metadataOrNull (Lokio/Path;)Lokio/FileMetadata; + public abstract fun openReadOnly (Lokio/Path;)Lokio/FileHandle; + public final fun openReadWrite (Lokio/Path;)Lokio/FileHandle; + public abstract fun openReadWrite (Lokio/Path;ZZ)Lokio/FileHandle; + public static synthetic fun openReadWrite$default (Lokio/FileSystem;Lokio/Path;ZZILjava/lang/Object;)Lokio/FileHandle; + public final fun sink (Lokio/Path;)Lokio/Sink; + public abstract fun sink (Lokio/Path;Z)Lokio/Sink; + public static synthetic fun sink$default (Lokio/FileSystem;Lokio/Path;ZILjava/lang/Object;)Lokio/Sink; + public abstract fun source (Lokio/Path;)Lokio/Source; +} + +public final class okio/FileSystem$Companion { + public final fun get (Ljava/nio/file/FileSystem;)Lokio/FileSystem; +} + +public abstract class okio/ForwardingFileSystem : okio/FileSystem { + public fun <init> (Lokio/FileSystem;)V + public fun appendingSink (Lokio/Path;Z)Lokio/Sink; + public fun atomicMove (Lokio/Path;Lokio/Path;)V + public fun canonicalize (Lokio/Path;)Lokio/Path; + public fun createDirectory (Lokio/Path;Z)V + public fun createSymlink (Lokio/Path;Lokio/Path;)V + public final fun delegate ()Lokio/FileSystem; + public fun delete (Lokio/Path;Z)V + public fun list (Lokio/Path;)Ljava/util/List; + public fun listOrNull (Lokio/Path;)Ljava/util/List; + public fun listRecursively (Lokio/Path;Z)Lkotlin/sequences/Sequence; + public fun metadataOrNull (Lokio/Path;)Lokio/FileMetadata; + public fun onPathParameter (Lokio/Path;Ljava/lang/String;Ljava/lang/String;)Lokio/Path; + public fun onPathResult (Lokio/Path;Ljava/lang/String;)Lokio/Path; + public fun openReadOnly (Lokio/Path;)Lokio/FileHandle; + public fun openReadWrite (Lokio/Path;ZZ)Lokio/FileHandle; + public fun sink (Lokio/Path;Z)Lokio/Sink; + public fun source (Lokio/Path;)Lokio/Source; + public fun toString ()Ljava/lang/String; +} + +public abstract class okio/ForwardingSink : okio/Sink { + public final fun -deprecated_delegate ()Lokio/Sink; + public fun <init> (Lokio/Sink;)V + public fun close ()V + public final fun delegate ()Lokio/Sink; + public fun flush ()V + public fun timeout ()Lokio/Timeout; + public fun toString ()Ljava/lang/String; + public fun write (Lokio/Buffer;J)V +} + +public abstract class okio/ForwardingSource : okio/Source { + public final fun -deprecated_delegate ()Lokio/Source; + public fun <init> (Lokio/Source;)V + public fun close ()V + public final fun delegate ()Lokio/Source; + public fun read (Lokio/Buffer;J)J + public fun timeout ()Lokio/Timeout; + public fun toString ()Ljava/lang/String; +} + +public class okio/ForwardingTimeout : okio/Timeout { + public fun <init> (Lokio/Timeout;)V + public fun awaitSignal (Ljava/util/concurrent/locks/Condition;)V + public fun cancel ()V + public fun clearDeadline ()Lokio/Timeout; + public fun clearTimeout ()Lokio/Timeout; + public fun deadlineNanoTime ()J + public fun deadlineNanoTime (J)Lokio/Timeout; + public final fun delegate ()Lokio/Timeout; + public fun hasDeadline ()Z + public final fun setDelegate (Lokio/Timeout;)Lokio/ForwardingTimeout; + public final synthetic fun setDelegate (Lokio/Timeout;)V + public fun throwIfReached ()V + public fun timeout (JLjava/util/concurrent/TimeUnit;)Lokio/Timeout; + public fun timeoutNanos ()J + public fun waitUntilNotified (Ljava/lang/Object;)V +} + +public final class okio/GzipSink : okio/Sink { + public final fun -deprecated_deflater ()Ljava/util/zip/Deflater; + public fun <init> (Lokio/Sink;)V + public fun close ()V + public final fun deflater ()Ljava/util/zip/Deflater; + public fun flush ()V + public fun timeout ()Lokio/Timeout; + public fun write (Lokio/Buffer;J)V +} + +public final class okio/GzipSource : okio/Source { + public fun <init> (Lokio/Source;)V + public fun close ()V + public fun read (Lokio/Buffer;J)J + public fun timeout ()Lokio/Timeout; +} + +public final class okio/HashingSink : okio/ForwardingSink, okio/Sink { + public static final field Companion Lokio/HashingSink$Companion; + public final fun -deprecated_hash ()Lokio/ByteString; + public final fun hash ()Lokio/ByteString; + public static final fun hmacSha1 (Lokio/Sink;Lokio/ByteString;)Lokio/HashingSink; + public static final fun hmacSha256 (Lokio/Sink;Lokio/ByteString;)Lokio/HashingSink; + public static final fun hmacSha512 (Lokio/Sink;Lokio/ByteString;)Lokio/HashingSink; + public static final fun md5 (Lokio/Sink;)Lokio/HashingSink; + public static final fun sha1 (Lokio/Sink;)Lokio/HashingSink; + public static final fun sha256 (Lokio/Sink;)Lokio/HashingSink; + public static final fun sha512 (Lokio/Sink;)Lokio/HashingSink; + public fun write (Lokio/Buffer;J)V +} + +public final class okio/HashingSink$Companion { + public final fun hmacSha1 (Lokio/Sink;Lokio/ByteString;)Lokio/HashingSink; + public final fun hmacSha256 (Lokio/Sink;Lokio/ByteString;)Lokio/HashingSink; + public final fun hmacSha512 (Lokio/Sink;Lokio/ByteString;)Lokio/HashingSink; + public final fun md5 (Lokio/Sink;)Lokio/HashingSink; + public final fun sha1 (Lokio/Sink;)Lokio/HashingSink; + public final fun sha256 (Lokio/Sink;)Lokio/HashingSink; + public final fun sha512 (Lokio/Sink;)Lokio/HashingSink; +} + +public final class okio/HashingSource : okio/ForwardingSource, okio/Source { + public static final field Companion Lokio/HashingSource$Companion; + public final fun -deprecated_hash ()Lokio/ByteString; + public final fun hash ()Lokio/ByteString; + public static final fun hmacSha1 (Lokio/Source;Lokio/ByteString;)Lokio/HashingSource; + public static final fun hmacSha256 (Lokio/Source;Lokio/ByteString;)Lokio/HashingSource; + public static final fun hmacSha512 (Lokio/Source;Lokio/ByteString;)Lokio/HashingSource; + public static final fun md5 (Lokio/Source;)Lokio/HashingSource; + public fun read (Lokio/Buffer;J)J + public static final fun sha1 (Lokio/Source;)Lokio/HashingSource; + public static final fun sha256 (Lokio/Source;)Lokio/HashingSource; + public static final fun sha512 (Lokio/Source;)Lokio/HashingSource; +} + +public final class okio/HashingSource$Companion { + public final fun hmacSha1 (Lokio/Source;Lokio/ByteString;)Lokio/HashingSource; + public final fun hmacSha256 (Lokio/Source;Lokio/ByteString;)Lokio/HashingSource; + public final fun hmacSha512 (Lokio/Source;Lokio/ByteString;)Lokio/HashingSource; + public final fun md5 (Lokio/Source;)Lokio/HashingSource; + public final fun sha1 (Lokio/Source;)Lokio/HashingSource; + public final fun sha256 (Lokio/Source;)Lokio/HashingSource; + public final fun sha512 (Lokio/Source;)Lokio/HashingSource; +} + +public final class okio/InflaterSource : okio/Source { + public fun <init> (Lokio/Source;Ljava/util/zip/Inflater;)V + public fun close ()V + public fun read (Lokio/Buffer;J)J + public final fun readOrInflate (Lokio/Buffer;J)J + public final fun refill ()Z + public fun timeout ()Lokio/Timeout; +} + +public final class okio/Okio { + public static final fun appendingSink (Ljava/io/File;)Lokio/Sink; + public static final fun asResourceFileSystem (Ljava/lang/ClassLoader;)Lokio/FileSystem; + public static final fun blackhole ()Lokio/Sink; + public static final fun buffer (Lokio/Sink;)Lokio/BufferedSink; + public static final fun buffer (Lokio/Source;)Lokio/BufferedSource; + public static final fun cipherSink (Lokio/Sink;Ljavax/crypto/Cipher;)Lokio/CipherSink; + public static final fun cipherSource (Lokio/Source;Ljavax/crypto/Cipher;)Lokio/CipherSource; + public static final fun hashingSink (Lokio/Sink;Ljava/security/MessageDigest;)Lokio/HashingSink; + public static final fun hashingSink (Lokio/Sink;Ljavax/crypto/Mac;)Lokio/HashingSink; + public static final fun hashingSource (Lokio/Source;Ljava/security/MessageDigest;)Lokio/HashingSource; + public static final fun hashingSource (Lokio/Source;Ljavax/crypto/Mac;)Lokio/HashingSource; + public static final fun openZip (Lokio/FileSystem;Lokio/Path;)Lokio/FileSystem; + public static final fun sink (Ljava/io/File;)Lokio/Sink; + public static final fun sink (Ljava/io/File;Z)Lokio/Sink; + public static final fun sink (Ljava/io/OutputStream;)Lokio/Sink; + public static final fun sink (Ljava/net/Socket;)Lokio/Sink; + public static final fun sink (Ljava/nio/file/Path;[Ljava/nio/file/OpenOption;)Lokio/Sink; + public static synthetic fun sink$default (Ljava/io/File;ZILjava/lang/Object;)Lokio/Sink; + public static final fun source (Ljava/io/File;)Lokio/Source; + public static final fun source (Ljava/io/InputStream;)Lokio/Source; + public static final fun source (Ljava/net/Socket;)Lokio/Source; + public static final fun source (Ljava/nio/file/Path;[Ljava/nio/file/OpenOption;)Lokio/Source; + public static final fun use (Ljava/io/Closeable;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + +public final class okio/Options : kotlin/collections/AbstractList, java/util/RandomAccess { + public static final field Companion Lokio/Options$Companion; + public synthetic fun <init> ([Lokio/ByteString;[ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun contains (Ljava/lang/Object;)Z + public fun contains (Lokio/ByteString;)Z + public synthetic fun get (I)Ljava/lang/Object; + public fun get (I)Lokio/ByteString; + public fun getSize ()I + public final fun indexOf (Ljava/lang/Object;)I + public fun indexOf (Lokio/ByteString;)I + public final fun lastIndexOf (Ljava/lang/Object;)I + public fun lastIndexOf (Lokio/ByteString;)I + public static final fun of ([Lokio/ByteString;)Lokio/Options; +} + +public final class okio/Options$Companion { + public final fun of ([Lokio/ByteString;)Lokio/Options; +} + +public final class okio/Path : java/lang/Comparable { + public static final field Companion Lokio/Path$Companion; + public static final field DIRECTORY_SEPARATOR Ljava/lang/String; + public synthetic fun compareTo (Ljava/lang/Object;)I + public fun compareTo (Lokio/Path;)I + public fun equals (Ljava/lang/Object;)Z + public static final fun get (Ljava/io/File;)Lokio/Path; + public static final fun get (Ljava/io/File;Z)Lokio/Path; + public static final fun get (Ljava/lang/String;)Lokio/Path; + public static final fun get (Ljava/lang/String;Z)Lokio/Path; + public static final fun get (Ljava/nio/file/Path;)Lokio/Path; + public static final fun get (Ljava/nio/file/Path;Z)Lokio/Path; + public final fun getRoot ()Lokio/Path; + public final fun getSegments ()Ljava/util/List; + public final fun getSegmentsBytes ()Ljava/util/List; + public fun hashCode ()I + public final fun isAbsolute ()Z + public final fun isRelative ()Z + public final fun isRoot ()Z + public final fun name ()Ljava/lang/String; + public final fun nameBytes ()Lokio/ByteString; + public final fun normalized ()Lokio/Path; + public final fun parent ()Lokio/Path; + public final fun relativeTo (Lokio/Path;)Lokio/Path; + public final fun resolve (Ljava/lang/String;)Lokio/Path; + public final fun resolve (Ljava/lang/String;Z)Lokio/Path; + public final fun resolve (Lokio/ByteString;)Lokio/Path; + public final fun resolve (Lokio/ByteString;Z)Lokio/Path; + public final fun resolve (Lokio/Path;)Lokio/Path; + public final fun resolve (Lokio/Path;Z)Lokio/Path; + public static synthetic fun resolve$default (Lokio/Path;Ljava/lang/String;ZILjava/lang/Object;)Lokio/Path; + public static synthetic fun resolve$default (Lokio/Path;Lokio/ByteString;ZILjava/lang/Object;)Lokio/Path; + public static synthetic fun resolve$default (Lokio/Path;Lokio/Path;ZILjava/lang/Object;)Lokio/Path; + public final fun toFile ()Ljava/io/File; + public final fun toNioPath ()Ljava/nio/file/Path; + public fun toString ()Ljava/lang/String; + public final fun volumeLetter ()Ljava/lang/Character; +} + +public final class okio/Path$Companion { + public final fun get (Ljava/io/File;)Lokio/Path; + public final fun get (Ljava/io/File;Z)Lokio/Path; + public final fun get (Ljava/lang/String;)Lokio/Path; + public final fun get (Ljava/lang/String;Z)Lokio/Path; + public final fun get (Ljava/nio/file/Path;)Lokio/Path; + public final fun get (Ljava/nio/file/Path;Z)Lokio/Path; + public static synthetic fun get$default (Lokio/Path$Companion;Ljava/io/File;ZILjava/lang/Object;)Lokio/Path; + public static synthetic fun get$default (Lokio/Path$Companion;Ljava/lang/String;ZILjava/lang/Object;)Lokio/Path; + public static synthetic fun get$default (Lokio/Path$Companion;Ljava/nio/file/Path;ZILjava/lang/Object;)Lokio/Path; +} + +public final class okio/Pipe { + public final fun -deprecated_sink ()Lokio/Sink; + public final fun -deprecated_source ()Lokio/Source; + public fun <init> (J)V + public final fun cancel ()V + public final fun fold (Lokio/Sink;)V + public final fun getCondition ()Ljava/util/concurrent/locks/Condition; + public final fun getLock ()Ljava/util/concurrent/locks/ReentrantLock; + public final fun sink ()Lokio/Sink; + public final fun source ()Lokio/Source; +} + +public abstract interface class okio/Sink : java/io/Closeable, java/io/Flushable { + public abstract fun close ()V + public abstract fun flush ()V + public abstract fun timeout ()Lokio/Timeout; + public abstract fun write (Lokio/Buffer;J)V +} + +public abstract interface class okio/Source : java/io/Closeable { + public abstract fun close ()V + public abstract fun read (Lokio/Buffer;J)J + public abstract fun timeout ()Lokio/Timeout; +} + +public final class okio/Throttler { + public fun <init> ()V + public final fun bytesPerSecond (J)V + public final fun bytesPerSecond (JJ)V + public final fun bytesPerSecond (JJJ)V + public static synthetic fun bytesPerSecond$default (Lokio/Throttler;JJJILjava/lang/Object;)V + public final fun getCondition ()Ljava/util/concurrent/locks/Condition; + public final fun getLock ()Ljava/util/concurrent/locks/ReentrantLock; + public final fun sink (Lokio/Sink;)Lokio/Sink; + public final fun source (Lokio/Source;)Lokio/Source; +} + +public class okio/Timeout { + public static final field Companion Lokio/Timeout$Companion; + public static final field NONE Lokio/Timeout; + public fun <init> ()V + public fun awaitSignal (Ljava/util/concurrent/locks/Condition;)V + public fun cancel ()V + public fun clearDeadline ()Lokio/Timeout; + public fun clearTimeout ()Lokio/Timeout; + public final fun deadline (JLjava/util/concurrent/TimeUnit;)Lokio/Timeout; + public fun deadlineNanoTime ()J + public fun deadlineNanoTime (J)Lokio/Timeout; + public fun hasDeadline ()Z + public final fun intersectWith (Lokio/Timeout;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; + public fun throwIfReached ()V + public fun timeout (JLjava/util/concurrent/TimeUnit;)Lokio/Timeout; + public fun timeoutNanos ()J + public fun waitUntilNotified (Ljava/lang/Object;)V +} + +public final class okio/Timeout$Companion { + public final fun minTimeout (JJ)J + public final fun timeout (Lokio/Timeout;JLkotlin/time/DurationUnit;)Lokio/Timeout; + public final fun timeout-HG0u8IE (Lokio/Timeout;J)Lokio/Timeout; +} + +public final class okio/Utf8 { + public static final fun size (Ljava/lang/String;)J + public static final fun size (Ljava/lang/String;I)J + public static final fun size (Ljava/lang/String;II)J + public static synthetic fun size$default (Ljava/lang/String;IIILjava/lang/Object;)J +} + +public final class okio/_JvmPlatformKt { + public static final fun withLock (Ljava/util/concurrent/locks/ReentrantLock;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; +} + +public final class okio/internal/_Utf8Kt { + public static final fun commonAsUtf8ToByteArray (Ljava/lang/String;)[B + public static final fun commonToUtf8String ([BII)Ljava/lang/String; + public static synthetic fun commonToUtf8String$default ([BIIILjava/lang/Object;)Ljava/lang/String; +} + diff --git a/okio/build.gradle b/okio/build.gradle deleted file mode 100644 index 980ecb0a..00000000 --- a/okio/build.gradle +++ /dev/null @@ -1,193 +0,0 @@ -apply plugin: 'org.jetbrains.kotlin.multiplatform' - -/* - * Here's the main hierarchy of variants. Any `expect` functions in one level of the tree are - * `actual` functions in a (potentially indirect) child node. - * - * ``` - * common - * |-- jvm - * '-- nonJvm - * |-- js - * '-- native - * |- unix - * | |-- apple - * | | |-- iosArm64 - * | | |-- iosX64 - * | | |-- macosX64 - * | | |-- watchosArm32 - * | | |-- watchosArm64 - * | | '-- watchosX86 - * | '-- linux - * | '-- linuxX64 - * '-- mingw - * '-- mingwX64 - * ``` - * - * Every child of `native` also includes a source set that depends on the pointer size: - * - * * sizet32 for watchOS, including watchOS 64-bit architectures - * * sizet64 for everything else - * - * The `hashFunctions` source set builds on all platforms. It ships as a main source set on non-JVM - * platforms and as a test source set on the JVM platform. - */ -kotlin { - jvm { - withJava() - } - if (kmpJsEnabled) { - js { - configure([compilations.main, compilations.test]) { - tasks.getByName(compileKotlinTaskName).kotlinOptions { - moduleKind = "umd" - sourceMap = true - metaInfo = true - } - } - nodejs { - testTask { - useMocha { - timeout = "30s" - } - } - } - } - } - if (kmpNativeEnabled) { - iosX64() - iosArm64() - watchosArm32() - watchosArm64() - watchosX86() - // Required to generate tests tasks: https://youtrack.jetbrains.com/issue/KT-26547 - linuxX64() - macosX64() - mingwX64() - } - sourceSets { - all { - languageSettings { - useExperimentalAnnotation('kotlin.RequiresOptIn') - } - } - commonMain { - dependencies { - api deps.kotlin.stdLib.common - } - } - commonTest { - dependencies { - implementation deps.kotlin.test.common - implementation deps.kotlin.test.annotations - implementation deps.kotlin.time - } - } - nonJvmMain { - kotlin.srcDirs += 'src/hashFunctions/kotlin' - } - jvmMain { - dependencies { - api deps.kotlin.stdLib.jdk6 - compileOnly deps.animalSniffer.annotations - } - } - jvmTest { - kotlin.srcDirs += 'src/hashFunctions/kotlin' - dependencies { - implementation deps.test.junit - implementation deps.test.assertj - implementation deps.kotlin.test.jdk - } - } - jsMain { - dependsOn nonJvmMain - dependencies { - api deps.kotlin.stdLib.js - } - } - jsTest { - dependencies { - implementation deps.kotlin.test.js - } - } - - nativeMain { - dependsOn nonJvmMain - } - nativeTest { - dependsOn commonTest - } - - sizet32Main { - dependsOn nativeMain - } - sizet64Main { - dependsOn nativeMain - } - - mingwMain { - dependsOn nativeMain - } - mingwX64Main { - dependsOn sizet64Main - dependsOn mingwMain - } - mingwX64Test { - dependsOn nativeTest - } - - unixMain { - dependsOn nativeMain - } - - appleMain { - dependsOn unixMain - } - appleTest { - dependsOn nativeTest - } - configure([iosX64Main, iosArm64Main, macosX64Main]) { - dependsOn sizet64Main - dependsOn appleMain - } - configure([iosX64Test, iosArm64Test, macosX64Test]) { - dependsOn appleTest - } - configure([watchosArm32Main, watchosArm64Main, watchosX86Main]) { - // Note that size_t is 32-bit on all watchOS versions (ie. pointers are always 32-bit). - dependsOn sizet32Main - dependsOn appleMain - } - configure([watchosArm32Test, watchosArm64Test, watchosX86Test]) { - dependsOn appleTest - } - - linuxMain { - dependsOn unixMain - dependsOn nativeMain - } - linuxX64Main { - dependsOn sizet64Main - dependsOn linuxMain - } - linuxX64Test { - dependsOn nativeTest - } - } -} - -tasks.withType(JavaCompile) { - options.encoding = 'UTF-8' - targetCompatibility = JavaVersion.VERSION_1_8 -} - -// modify these lines for MANIFEST.MF properties or for specific bnd instructions -project.ext.bndManifest = ''' - Export-Package: okio - Automatic-Module-Name: okio - Bundle-SymbolicName: com.squareup.okio - ''' - -apply from: 'jvm/jvm.gradle' -apply from: "$rootDir/gradle/gradle-mvn-mpp-push.gradle" diff --git a/okio/build.gradle.kts b/okio/build.gradle.kts new file mode 100644 index 00000000..cac4a345 --- /dev/null +++ b/okio/build.gradle.kts @@ -0,0 +1,197 @@ +import aQute.bnd.gradle.BundleTaskConvention +import com.vanniktech.maven.publish.JavadocJar.Dokka +import com.vanniktech.maven.publish.KotlinMultiplatform +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import kotlinx.validation.ApiValidationExtension +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithTests +import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType +import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable + +plugins { + kotlin("multiplatform") + id("org.jetbrains.dokka") + id("com.vanniktech.maven.publish.base") + id("build-support") + id("binary-compatibility-validator") +} + +/* + * Here's the main hierarchy of variants. Any `expect` functions in one level of the tree are + * `actual` functions in a (potentially indirect) child node. + * + * ``` + * common + * |-- js + * |-- jvm + * |-- native + * | |-- mingw + * | | '-- mingwX64 + * | '-- unix + * | |-- apple + * | | |-- iosArm64 + * | | |-- iosX64 + * | | |-- macosX64 + * | | |-- tvosArm64 + * | | |-- tvosX64 + * | | |-- watchosArm32 + * | | |-- watchosArm64 + * | '-- linux + * | |-- linuxX64 + * | '-- linuxArm64 + * '-- wasm + * '-- wasmJs + * '-- wasmWasi + * ``` + * + * The `nonJvm` source set excludes that platform. + * + * The `hashFunctions` source set builds on all platforms. It ships as a main source set on non-JVM + * platforms and as a test source set on the JVM platform. + */ +kotlin { + configureOrCreateOkioPlatforms() + + sourceSets { + all { + languageSettings.apply { + // Required for CPointer etc. since Kotlin 1.9. + optIn("kotlinx.cinterop.ExperimentalForeignApi") + } + } + + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(libs.kotlin.test) + implementation(projects.okioTestingSupport) + } + } + + val hashFunctions by creating { + dependsOn(commonMain) + } + + val nonAppleMain by creating { + dependsOn(hashFunctions) + } + + val nonWasmTest by creating { + dependencies { + implementation(libs.kotlin.time) + implementation(projects.okioFakefilesystem) + } + } + + val nonJvmMain by creating { + dependsOn(hashFunctions) + dependsOn(commonMain) + } + + val nonJvmTest by creating { + dependsOn(commonTest) + } + + val jvmMain by getting { + } + val jvmTest by getting { + kotlin.srcDir("src/jvmTest/hashFunctions") + dependsOn(nonWasmTest) + dependencies { + implementation(libs.test.junit) + implementation(libs.test.assertj) + implementation(libs.test.jimfs) + } + } + + if (kmpJsEnabled) { + val jsMain by getting { + dependsOn(nonJvmMain) + dependsOn(nonAppleMain) + } + val jsTest by getting { + dependsOn(nonWasmTest) + dependsOn(nonJvmTest) + } + } + + if (kmpNativeEnabled) { + createSourceSet("nativeMain", parent = nonJvmMain) + .also { nativeMain -> + createSourceSet("mingwMain", parent = nativeMain, children = mingwTargets).also { mingwMain -> + mingwMain.dependsOn(nonAppleMain) + } + createSourceSet("unixMain", parent = nativeMain) + .also { unixMain -> + createSourceSet("linuxMain", parent = unixMain, children = linuxTargets).also { linuxMain -> + linuxMain.dependsOn(nonAppleMain) + } + createSourceSet("appleMain", parent = unixMain, children = appleTargets) + } + } + + createSourceSet("nativeTest", parent = commonTest, children = mingwTargets + linuxTargets) + .also { nativeTest -> + nativeTest.dependsOn(nonJvmTest) + nativeTest.dependsOn(nonWasmTest) + createSourceSet("appleTest", parent = nativeTest, children = appleTargets) + } + } + + if (kmpWasmEnabled) { + createSourceSet("wasmMain", parent = commonMain, children = wasmTargets) + .also { wasmMain -> + wasmMain.dependsOn(nonJvmMain) + wasmMain.dependsOn(nonAppleMain) + } + createSourceSet("wasmTest", parent = commonTest, children = wasmTargets) + .also { wasmTest -> + wasmTest.dependsOn(nonJvmTest) + } + } + } + + targets.withType<KotlinNativeTargetWithTests<*>> { + binaries { + // Configure a separate test where code runs in background + test("background", setOf(NativeBuildType.DEBUG)) { + freeCompilerArgs += "-trw" + } + } + testRuns { + val background by creating { + setExecutionSourceFrom(binaries.getByName("backgroundDebugTest") as TestExecutable) + } + } + } +} + +tasks { + val jvmJar by getting(Jar::class) { + // BundleTaskConvention() crashes unless there's a 'main' source set. + sourceSets.create(SourceSet.MAIN_SOURCE_SET_NAME) + val bndConvention = BundleTaskConvention(this) + bndConvention.setBnd( + """ + Export-Package: okio + Automatic-Module-Name: okio + Bundle-SymbolicName: com.squareup.okio + """, + ) + // Call the convention when the task has finished to modify the jar to contain OSGi metadata. + doLast { + bndConvention.buildBundle() + } + } +} + +configure<MavenPublishBaseExtension> { + configure( + KotlinMultiplatform(javadocJar = Dokka("dokkaGfm")), + ) +} + +plugins.withId("binary-compatibility-validator") { + configure<ApiValidationExtension> { + ignoredProjects += "jmh" + } +} diff --git a/okio/gradle.properties b/okio/gradle.properties deleted file mode 100644 index 775966eb..00000000 --- a/okio/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -POM_ARTIFACT_ID=okio -POM_NAME=Okio diff --git a/okio/jvm/japicmp/build.gradle b/okio/jvm/japicmp/build.gradle deleted file mode 100644 index 782c6996..00000000 --- a/okio/jvm/japicmp/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2019 Square, Inc. - * - * 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. - */ -import me.champeau.gradle.japicmp.JapicmpTask - -apply plugin: 'java-library' -apply plugin: 'me.champeau.gradle.japicmp' - -configurations { - baseline - latest -} - -dependencies { - baseline('com.squareup.okio:okio:1.14.1') { - transitive = false - force = true - } - latest project(path: ':okio', configuration: 'jvmRuntimeElements') -} - -task japicmp(type: JapicmpTask, dependsOn: 'jar') { - oldClasspath = configurations.baseline - newClasspath = configurations.latest - onlyBinaryIncompatibleModified = true - failOnModification = true - txtOutputFile = file("$buildDir/reports/japi.txt") - ignoreMissingClasses = true - includeSynthetic = true - classExcludes = [ - 'okio.ByteString', // Bytecode version changed from 51.0 to 50.0 - 'okio.RealBufferedSink', // Internal. - 'okio.RealBufferedSource', // Internal. - 'okio.SegmentedByteString', // Internal. - 'okio.SegmentPool', // Internal. - 'okio.Util', // Internal. - 'okio.Options', // Bytecode version changed from 51.0 to 50.0 - ] - methodExcludes = [ - 'okio.ByteString#getByte(int)', // Became 'final' in 1.15.0. - 'okio.ByteString#size()', // Became 'final' in 1.15.0. - ] -} -check.dependsOn japicmp diff --git a/okio/jvm/jmh/build.gradle b/okio/jvm/jmh/build.gradle deleted file mode 100644 index 54eebdd3..00000000 --- a/okio/jvm/jmh/build.gradle +++ /dev/null @@ -1,36 +0,0 @@ -import com.github.jengelman.gradle.plugins.shadow.transformers.DontIncludeResourceTransformer -import com.github.jengelman.gradle.plugins.shadow.transformers.IncludeResourceTransformer - -apply plugin: 'java-library' -apply plugin: 'org.jetbrains.kotlin.jvm' -apply plugin: 'com.github.johnrengelman.shadow' -apply plugin: 'me.champeau.gradle.jmh' - -jmhJar { - def excludeAllBenchmarkLists = new DontIncludeResourceTransformer() - excludeAllBenchmarkLists.resource = "META-INF/BenchmarkList" - transform(excludeAllBenchmarkLists) - - def includeCorrectBenchmarkList = new IncludeResourceTransformer() - includeCorrectBenchmarkList.resource = "META-INF/BenchmarkList" - includeCorrectBenchmarkList.file = new File("${project.buildDir}/jmh-generated-resources/META-INF/BenchmarkList") - transform(includeCorrectBenchmarkList) -} - -jmh { - jvmArgs = ['-Djmh.separateClasspathJAR=true'] - include = ['com\\.squareup\\.okio\\.benchmarks\\.MessageDigestBenchmark.*'] - duplicateClassesStrategy = 'warn' -} - -dependencies { - compile project(':okio') - compile deps.kotlin.stdLib.jdk6 - compile deps.jmh.core - jmh project(path: ':okio', configuration: 'jvmRuntimeElements') - jmh deps.kotlin.stdLib.jdk6 - jmh deps.jmh.core - jmh deps.jmh.generator -} - -assemble.dependsOn(jmhJar) diff --git a/okio/jvm/jmh/build.gradle.kts b/okio/jvm/jmh/build.gradle.kts new file mode 100644 index 00000000..0e9c04b7 --- /dev/null +++ b/okio/jvm/jmh/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + kotlin("jvm") + id("me.champeau.jmh") +} + +jmh { +} + +dependencies { + api(projects.okio) + api(libs.jmh.core) +} diff --git a/okio/jvm/jvm.gradle b/okio/jvm/jvm.gradle deleted file mode 100644 index cd81891e..00000000 --- a/okio/jvm/jvm.gradle +++ /dev/null @@ -1,26 +0,0 @@ -apply plugin: 'java-library' -apply plugin: 'ru.vyarus.animalsniffer' - -kotlin.targets.matching { it.platformType.name == 'jvm' }.all { target -> - target.project.sourceCompatibility = JavaVersion.VERSION_1_7 - target.project.targetCompatibility = JavaVersion.VERSION_1_7 - - tasks['jvmJar'].configure { t -> - // the bnd task convention modifies this jar task accordingly - def bndConvention = bndBundleTaskConventionClass.newInstance(t); - bndConvention.bnd = project.ext.bndManifest - // call the convention when the task has finished to modify the jar to contain OSGi metadata - t.doLast { - bndConvention.buildBundle() - } - } - - target.project.animalsniffer { - sourceSets = [target.project.sourceSets.main] - } - - target.project.dependencies { - signature 'net.sf.androidscents.signature:android-api-level-15:4.0.3_r5@signature' - signature 'org.codehaus.mojo.signature:java17:1.0@signature' - } -} diff --git a/okio/src/appleMain/kotlin/okio/ApplePosixVariant.kt b/okio/src/appleMain/kotlin/okio/ApplePosixVariant.kt new file mode 100644 index 00000000..8840180b --- /dev/null +++ b/okio/src/appleMain/kotlin/okio/ApplePosixVariant.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import platform.posix.ENOENT +import platform.posix.S_IFDIR +import platform.posix.S_IFMT +import platform.posix.S_IFREG +import platform.posix.errno +import platform.posix.lstat +import platform.posix.stat + +internal actual fun PosixFileSystem.variantMetadataOrNull(path: Path): FileMetadata? { + return memScoped { + val stat = alloc<stat>() + if (lstat(path.toString(), stat.ptr) != 0) { + if (errno == ENOENT) return null + throw errnoToIOException(errno) + } + return@memScoped FileMetadata( + isRegularFile = stat.st_mode.toInt() and S_IFMT == S_IFREG, + isDirectory = stat.st_mode.toInt() and S_IFMT == S_IFDIR, + symlinkTarget = symlinkTarget(stat, path), + size = stat.st_size, + createdAtMillis = stat.st_ctimespec.epochMillis, + lastModifiedAtMillis = stat.st_mtimespec.epochMillis, + lastAccessedAtMillis = stat.st_atimespec.epochMillis, + ) + } +} diff --git a/okio/src/appleMain/kotlin/okio/ByteString.kt b/okio/src/appleMain/kotlin/okio/ByteString.kt index eb141033..7d49ebd6 100644 --- a/okio/src/appleMain/kotlin/okio/ByteString.kt +++ b/okio/src/appleMain/kotlin/okio/ByteString.kt @@ -1,11 +1,11 @@ /* - * Copyright (C) 2020 Square, Inc. + * Copyright (C) 2018 Square, Inc. * * 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 + * 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, @@ -13,20 +13,211 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package okio +import kotlin.experimental.ExperimentalNativeApi +import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.addressOf import kotlinx.cinterop.usePinned +import okio.internal.HashFunction +import okio.internal.Hmac +import okio.internal.Md5 +import okio.internal.Sha1 +import okio.internal.Sha256 +import okio.internal.Sha512 +import okio.internal.commonBase64 +import okio.internal.commonBase64Url +import okio.internal.commonCompareTo +import okio.internal.commonCopyInto +import okio.internal.commonDecodeBase64 +import okio.internal.commonDecodeHex +import okio.internal.commonEncodeUtf8 +import okio.internal.commonEndsWith +import okio.internal.commonEquals +import okio.internal.commonGetByte +import okio.internal.commonGetSize +import okio.internal.commonHashCode +import okio.internal.commonHex +import okio.internal.commonIndexOf +import okio.internal.commonInternalArray +import okio.internal.commonLastIndexOf +import okio.internal.commonOf +import okio.internal.commonRangeEquals +import okio.internal.commonStartsWith +import okio.internal.commonSubstring +import okio.internal.commonToAsciiLowercase +import okio.internal.commonToAsciiUppercase +import okio.internal.commonToByteArray +import okio.internal.commonToByteString +import okio.internal.commonToString +import okio.internal.commonUtf8 +import okio.internal.commonWrite import platform.Foundation.NSData import platform.posix.memcpy -fun NSData.toByteString(): ByteString { - val data = this - return ByteString( - ByteArray(data.length.toInt()).apply { - usePinned { pinned -> - memcpy(pinned.addressOf(0), data.bytes, data.length) +actual open class ByteString +internal actual constructor( + internal actual val data: ByteArray, +) : Comparable<ByteString> { + @Suppress("SetterBackingFieldAssignment") + internal actual var hashCode: Int = 0 // 0 if unknown. + set(value) { + // Do nothing to avoid IllegalImmutabilityException. + } + + @Suppress("SetterBackingFieldAssignment") + internal actual var utf8: String? = null + set(value) { + // Do nothing to avoid IllegalImmutabilityException. + } + + actual open fun utf8(): String = commonUtf8() + + actual open fun base64(): String = commonBase64() + + actual open fun base64Url(): String = commonBase64Url() + + actual open fun hex(): String = commonHex() + + actual fun md5() = digest(Md5()) + + actual fun sha1() = digest(Sha1()) + + actual fun sha256() = digest(Sha256()) + + actual fun sha512() = digest(Sha512()) + + /** Returns the 160-bit SHA-1 HMAC of this byte string. */ + actual fun hmacSha1(key: ByteString) = digest(Hmac.sha1(key)) + + /** Returns the 256-bit SHA-256 HMAC of this byte string. */ + actual fun hmacSha256(key: ByteString) = digest(Hmac.sha256(key)) + + /** Returns the 512-bit SHA-512 HMAC of this byte string. */ + actual fun hmacSha512(key: ByteString) = digest(Hmac.sha512(key)) + + internal open fun digest(hashFunction: HashFunction): ByteString { + hashFunction.update(data, 0, size) + val digestBytes = hashFunction.digest() + return ByteString(digestBytes) + } + + actual open fun toAsciiLowercase(): ByteString = commonToAsciiLowercase() + + actual open fun toAsciiUppercase(): ByteString = commonToAsciiUppercase() + + actual open fun substring(beginIndex: Int, endIndex: Int): ByteString = + commonSubstring(beginIndex, endIndex) + + internal actual open fun internalGet(pos: Int): Byte { + if (pos >= size || pos < 0) throw ArrayIndexOutOfBoundsException("size=$size pos=$pos") + return commonGetByte(pos) + } + + actual operator fun get(index: Int): Byte = internalGet(index) + + actual val size + get() = getSize() + + internal actual open fun getSize() = commonGetSize() + + actual open fun toByteArray() = commonToByteArray() + + internal actual open fun internalArray() = commonInternalArray() + + internal actual open fun write(buffer: Buffer, offset: Int, byteCount: Int) = + commonWrite(buffer, offset, byteCount) + + actual open fun rangeEquals( + offset: Int, + other: ByteString, + otherOffset: Int, + byteCount: Int, + ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount) + + actual open fun rangeEquals( + offset: Int, + other: ByteArray, + otherOffset: Int, + byteCount: Int, + ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount) + + actual open fun copyInto( + offset: Int, + target: ByteArray, + targetOffset: Int, + byteCount: Int, + ) = commonCopyInto(offset, target, targetOffset, byteCount) + + actual fun startsWith(prefix: ByteString) = commonStartsWith(prefix) + + actual fun startsWith(prefix: ByteArray) = commonStartsWith(prefix) + + actual fun endsWith(suffix: ByteString) = commonEndsWith(suffix) + + actual fun endsWith(suffix: ByteArray) = commonEndsWith(suffix) + + actual fun indexOf(other: ByteString, fromIndex: Int) = indexOf(other.internalArray(), fromIndex) + + actual open fun indexOf(other: ByteArray, fromIndex: Int) = commonIndexOf(other, fromIndex) + + actual fun lastIndexOf(other: ByteString, fromIndex: Int) = commonLastIndexOf(other, fromIndex) + + actual open fun lastIndexOf(other: ByteArray, fromIndex: Int) = commonLastIndexOf(other, fromIndex) + + actual override fun equals(other: Any?) = commonEquals(other) + + actual override fun hashCode() = commonHashCode() + + actual override fun compareTo(other: ByteString) = commonCompareTo(other) + + /** + * Returns a human-readable string that describes the contents of this byte string. Typically this + * is a string like `[text=Hello]` or `[hex=0000ffff]`. + */ + actual override fun toString() = commonToString() + + actual companion object { + actual val EMPTY: ByteString = ByteString(byteArrayOf()) + + actual fun of(vararg data: Byte) = commonOf(data) + + actual fun ByteArray.toByteString(offset: Int, byteCount: Int): ByteString = + commonToByteString(offset, byteCount) + + actual fun String.encodeUtf8(): ByteString = commonEncodeUtf8() + + actual fun String.decodeBase64(): ByteString? = commonDecodeBase64() + + actual fun String.decodeHex() = commonDecodeHex() + + @OptIn(UnsafeNumber::class, ExperimentalNativeApi::class) + @CName("of") + fun NSData.toByteString(): ByteString { + val data = this + val size = data.length.toInt() + return if (size != 0) { + ByteString( + ByteArray(size).apply { + usePinned { pinned -> + memcpy(pinned.addressOf(0), data.bytes, data.length) + } + }, + ) + } else { + EMPTY } } - ) + } +} + +@Deprecated( + message = "Moved to ByteString companion object", + replaceWith = ReplaceWith("this.toByteString()", "okio.ByteString.Companion.toByteString"), +) +fun NSData.toByteString(): ByteString { + with(ByteString) { + return toByteString() + } } diff --git a/okio/src/nonJvmMain/kotlin/okio/SegmentedByteString.kt b/okio/src/appleMain/kotlin/okio/SegmentedByteString.kt index fe718901..485d834e 100644 --- a/okio/src/nonJvmMain/kotlin/okio/SegmentedByteString.kt +++ b/okio/src/appleMain/kotlin/okio/SegmentedByteString.kt @@ -16,6 +16,7 @@ package okio import okio.internal.HashFunction +import okio.internal.commonCopyInto import okio.internal.commonEquals import okio.internal.commonGetSize import okio.internal.commonHashCode @@ -28,7 +29,7 @@ import okio.internal.forEachSegment internal actual class SegmentedByteString internal actual constructor( internal actual val segments: Array<ByteArray>, - internal actual val directory: IntArray + internal actual val directory: IntArray, ) : ByteString(EMPTY.data) { override fun base64() = toByteString().base64() @@ -57,21 +58,28 @@ internal actual class SegmentedByteString internal actual constructor( offset: Int, other: ByteString, otherOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount) override fun rangeEquals( offset: Int, other: ByteArray, otherOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount) + override fun copyInto( + offset: Int, + target: ByteArray, + targetOffset: Int, + byteCount: Int, + ) = commonCopyInto(offset, target, targetOffset, byteCount) + override fun indexOf(other: ByteArray, fromIndex: Int) = toByteString().indexOf(other, fromIndex) override fun lastIndexOf(other: ByteArray, fromIndex: Int) = toByteString().lastIndexOf( other, - fromIndex + fromIndex, ) override fun digest(hashFunction: HashFunction): ByteString { diff --git a/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt b/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt index 5ff03b21..0e600e06 100644 --- a/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt @@ -15,17 +15,33 @@ */ package okio +import kotlin.test.Test +import kotlin.test.assertEquals import platform.Foundation.NSData import platform.Foundation.NSString import platform.Foundation.NSUTF8StringEncoding import platform.Foundation.dataUsingEncoding -import kotlin.test.Test -import kotlin.test.assertEquals class AppleByteStringTest { @Test fun nsDataToByteString() { val data = ("Hello" as NSString).dataUsingEncoding(NSUTF8StringEncoding) as NSData - val byteString = data.toByteString() + + @Suppress("DEPRECATION") // Ensure deprecated function continues to work. + val byteStringDeprecated = data.toByteString() + assertEquals("Hello", byteStringDeprecated.utf8()) + + val byteString = with(ByteString) { data.toByteString() } assertEquals("Hello", byteString.utf8()) } + + @Test fun emptyNsDataToByteString() { + val data = ("" as NSString).dataUsingEncoding(NSUTF8StringEncoding) as NSData + + @Suppress("DEPRECATION") // Ensure deprecated function continues to work. + val byteStringDeprecated = data.toByteString() + assertEquals(ByteString.EMPTY, byteStringDeprecated) + + val byteString = with(ByteString) { data.toByteString() } + assertEquals(ByteString.EMPTY, byteString) + } } diff --git a/okio/src/commonMain/kotlin/okio/-Base64.kt b/okio/src/commonMain/kotlin/okio/Base64.kt index 98db0b5a..150793cc 100644 --- a/okio/src/commonMain/kotlin/okio/-Base64.kt +++ b/okio/src/commonMain/kotlin/okio/Base64.kt @@ -14,17 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:JvmName("-Base64") // A leading '-' hides this class from Java. -@file:JvmName("-Base64") package okio -import okio.ByteString.Companion.encodeUtf8 import kotlin.jvm.JvmName +import kotlin.native.concurrent.SharedImmutable +import okio.ByteString.Companion.encodeUtf8 /** @author Alexander Y. Kleymenov */ +@SharedImmutable internal val BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".encodeUtf8().data + +@SharedImmutable internal val BASE64_URL_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".encodeUtf8().data @@ -53,17 +57,17 @@ internal fun String.decodeBase64ToArray(): ByteArray? { // char ASCII value // A 65 0 // Z 90 25 (ASCII - 65) - bits = c.toInt() - 65 + bits = c.code - 65 } else if (c in 'a'..'z') { // char ASCII value // a 97 26 // z 122 51 (ASCII - 71) - bits = c.toInt() - 71 + bits = c.code - 71 } else if (c in '0'..'9') { // char ASCII value // 0 48 52 // 9 57 61 (ASCII + 4) - bits = c.toInt() + 4 + bits = c.code + 4 } else if (c == '+' || c == '-') { bits = 62 } else if (c == '/' || c == '_') { @@ -132,8 +136,8 @@ internal fun ByteArray.encodeBase64(map: ByteArray = BASE64): String { val b0 = this[i].toInt() out[index++] = map[b0 and 0xff shr 2] out[index++] = map[b0 and 0x03 shl 4] - out[index++] = '='.toByte() - out[index] = '='.toByte() + out[index++] = '='.code.toByte() + out[index] = '='.code.toByte() } 2 -> { val b0 = this[i++].toInt() @@ -141,7 +145,7 @@ internal fun ByteArray.encodeBase64(map: ByteArray = BASE64): String { out[index++] = map[(b0 and 0xff shr 2)] out[index++] = map[(b0 and 0x03 shl 4) or (b1 and 0xff shr 4)] out[index++] = map[(b1 and 0x0f shl 2)] - out[index] = '='.toByte() + out[index] = '='.code.toByte() } } return out.toUtf8String() diff --git a/okio/src/commonMain/kotlin/okio/Buffer.kt b/okio/src/commonMain/kotlin/okio/Buffer.kt index 16394d2c..009ed233 100644 --- a/okio/src/commonMain/kotlin/okio/Buffer.kt +++ b/okio/src/commonMain/kotlin/okio/Buffer.kt @@ -46,7 +46,7 @@ expect class Buffer() : BufferedSource, BufferedSink { fun copyTo( out: Buffer, offset: Long = 0L, - byteCount: Long + byteCount: Long, ): Buffer /** @@ -55,7 +55,7 @@ expect class Buffer() : BufferedSource, BufferedSink { */ fun copyTo( out: Buffer, - offset: Long = 0L + offset: Long = 0L, ): Buffer /** @@ -133,7 +133,10 @@ expect class Buffer() : BufferedSource, BufferedSink { override fun writeHexadecimalUnsignedLong(v: Long): Buffer - /** Returns a deep copy of this buffer. */ + /** + * Returns a deep copy of this buffer. The returned [Buffer] initially shares the underlying + * [ByteArray]s. See [UnsafeCursor] for more details. + */ fun copy(): Buffer /** Returns an immutable copy of this buffer as a byte string. */ @@ -142,9 +145,9 @@ expect class Buffer() : BufferedSource, BufferedSink { /** Returns an immutable copy of the first `byteCount` bytes of this buffer as a byte string. */ fun snapshot(byteCount: Int): ByteString - fun readUnsafe(unsafeCursor: UnsafeCursor = UnsafeCursor()): UnsafeCursor + fun readUnsafe(unsafeCursor: UnsafeCursor = DEFAULT__new_UnsafeCursor): UnsafeCursor - fun readAndWriteUnsafe(unsafeCursor: UnsafeCursor = UnsafeCursor()): UnsafeCursor + fun readAndWriteUnsafe(unsafeCursor: UnsafeCursor = DEFAULT__new_UnsafeCursor): UnsafeCursor /** * A handle to the underlying data in a buffer. This handle is unsafe because it does not enforce @@ -240,7 +243,7 @@ expect class Buffer() : BufferedSource, BufferedSink { * // start = 0 end = 3 * ``` * - * There is an optimization in `Buffer.clone()` and other methods that allows two segments to + * There is an optimization in `Buffer.copy()` and other methods that allows two segments to * share the same underlying byte array. Clones can't write to the shared byte array; instead they * allocate a new (private) segment early. * @@ -253,7 +256,7 @@ expect class Buffer() : BufferedSource, BufferedSink { * // ^ ^ * // start = 2 end = 5000 * - * nana2 = nana.clone() + * nana2 = nana.copy() * nana2.writeUtf8("batman") * * // [ 'n', 'a', 'n', 'a', ..., 'n', 'a', 'n', 'a', '?', '?', '?', ...] @@ -337,14 +340,19 @@ expect class Buffer() : BufferedSource, BufferedSink { * You can reuse instances of this class if you like. Use the overloads of [Buffer.readUnsafe] and * [Buffer.readAndWriteUnsafe] that take a cursor and close it after use. */ - class UnsafeCursor constructor() { + class UnsafeCursor constructor() : Closeable { @JvmField var buffer: Buffer? + @JvmField var readWrite: Boolean internal var segment: Segment? + @JvmField var offset: Long + @JvmField var data: ByteArray? + @JvmField var start: Int + @JvmField var end: Int /** @@ -403,6 +411,6 @@ expect class Buffer() : BufferedSource, BufferedSink { */ fun expandBuffer(minByteCount: Int): Long - fun close() + override fun close() } } diff --git a/okio/src/commonMain/kotlin/okio/BufferedSink.kt b/okio/src/commonMain/kotlin/okio/BufferedSink.kt index 40c26585..03c8230a 100644 --- a/okio/src/commonMain/kotlin/okio/BufferedSink.kt +++ b/okio/src/commonMain/kotlin/okio/BufferedSink.kt @@ -19,7 +19,7 @@ package okio * A sink that keeps a buffer internally so that callers can do small writes without a performance * penalty. */ -expect interface BufferedSink : Sink { +expect sealed interface BufferedSink : Sink { /** This sink's internal buffer. */ val buffer: Buffer diff --git a/okio/src/commonMain/kotlin/okio/BufferedSource.kt b/okio/src/commonMain/kotlin/okio/BufferedSource.kt index 0ba4d152..86b4803a 100644 --- a/okio/src/commonMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/commonMain/kotlin/okio/BufferedSource.kt @@ -20,7 +20,7 @@ package okio * penalty. It also allows clients to read ahead, buffering as much as necessary before consuming * input. */ -expect interface BufferedSource : Source { +expect sealed interface BufferedSource : Source { /** This source's internal buffer. */ val buffer: Buffer diff --git a/okio/src/commonMain/kotlin/okio/ByteString.kt b/okio/src/commonMain/kotlin/okio/ByteString.kt index 7eb34c6a..3f4fb82f 100644 --- a/okio/src/commonMain/kotlin/okio/ByteString.kt +++ b/okio/src/commonMain/kotlin/okio/ByteString.kt @@ -42,7 +42,7 @@ internal constructor(data: ByteArray) : Comparable<ByteString> { internal var hashCode: Int internal var utf8: String? - /** Constructs a new `String` by decoding the bytes as `UTF-8`. */ + /** Constructs a new `String` by decoding the bytes as `UTF-8`. */ fun utf8(): String /** @@ -54,29 +54,38 @@ internal constructor(data: ByteArray) : Comparable<ByteString> { /** Returns this byte string encoded as [URL-safe Base64](http://www.ietf.org/rfc/rfc4648.txt). */ fun base64Url(): String - /** Returns this byte string encoded in hexadecimal. */ + /** Returns this byte string encoded in hexadecimal. */ fun hex(): String - /** Returns the 128-bit MD5 hash of this byte string. */ + /** + * Returns the 128-bit MD5 hash of this byte string. + * + * MD5 has been vulnerable to collisions since 2004. It should not be used in new code. + */ fun md5(): ByteString - /** Returns the 160-bit SHA-1 hash of this byte string. */ + /** + * Returns the 160-bit SHA-1 hash of this byte string. + * + * SHA-1 has been vulnerable to collisions since 2017. It should not be used in new code. + */ fun sha1(): ByteString - /** Returns the 256-bit SHA-256 hash of this byte string. */ + /** Returns the 256-bit SHA-256 hash of this byte string. */ fun sha256(): ByteString - /** Returns the 512-bit SHA-512 hash of this byte string. */ + /** Returns the 512-bit SHA-512 hash of this byte string. */ fun sha512(): ByteString - /** Returns the 160-bit SHA-1 HMAC of this byte string. */ + /** Returns the 160-bit SHA-1 HMAC of this byte string. */ fun hmacSha1(key: ByteString): ByteString - /** Returns the 256-bit SHA-256 HMAC of this byte string. */ + /** Returns the 256-bit SHA-256 HMAC of this byte string. */ fun hmacSha256(key: ByteString): ByteString - /** Returns the 512-bit SHA-512 HMAC of this byte string. */ + /** Returns the 512-bit SHA-512 HMAC of this byte string. */ fun hmacSha512(key: ByteString): ByteString + /** * Returns a byte string equal to this byte string, but with the bytes 'A' through 'Z' replaced * with the corresponding byte in 'a' through 'z'. Returns this byte string if it contains no @@ -89,7 +98,7 @@ internal constructor(data: ByteArray) : Comparable<ByteString> { * `beginIndex` and ends at the specified `endIndex`. Returns this byte string if `beginIndex` is * 0 and `endIndex` is the length of this byte string. */ - fun substring(beginIndex: Int = 0, endIndex: Int = size): ByteString + fun substring(beginIndex: Int = 0, endIndex: Int = DEFAULT__ByteString_size): ByteString /** * Returns a byte string equal to this byte string, but with the bytes 'a' through 'z' replaced @@ -98,16 +107,17 @@ internal constructor(data: ByteArray) : Comparable<ByteString> { */ fun toAsciiUppercase(): ByteString - /** Returns the byte at `pos`. */ + /** Returns the byte at `pos`. */ internal fun internalGet(pos: Int): Byte - /** Returns the byte at `index`. */ + /** Returns the byte at `index`. */ @JvmName("getByte") operator fun get(index: Int): Byte /** Returns the number of bytes in this ByteString. */ val size: Int - @JvmName("size") get + @JvmName("size") + get // Hack to work around Kotlin's limitation for using JvmName on open/override vals/funs internal fun getSize(): Int @@ -115,7 +125,7 @@ internal constructor(data: ByteArray) : Comparable<ByteString> { /** Returns a byte array containing a copy of the bytes in this `ByteString`. */ fun toByteArray(): ByteArray - /** Writes the contents of this byte string to `buffer`. */ + /** Writes the contents of this byte string to `buffer`. */ internal fun write(buffer: Buffer, offset: Int, byteCount: Int) /** Returns the bytes of this string without a defensive copy. Do not mutate! */ @@ -133,6 +143,14 @@ internal constructor(data: ByteArray) : Comparable<ByteString> { */ fun rangeEquals(offset: Int, other: ByteArray, otherOffset: Int, byteCount: Int): Boolean + /** + * Copies bytes of this in `[offset..offset+byteCount]` to other in + * `[targetOffset..targetOffset+byteCount]`. + * + * @throws IndexOutOfBoundsException if either range is out of bounds. + */ + fun copyInto(offset: Int = 0, target: ByteArray, targetOffset: Int = 0, byteCount: Int) + fun startsWith(prefix: ByteString): Boolean fun startsWith(prefix: ByteArray): Boolean @@ -147,9 +165,9 @@ internal constructor(data: ByteArray) : Comparable<ByteString> { @JvmOverloads fun indexOf(other: ByteArray, fromIndex: Int = 0): Int - fun lastIndexOf(other: ByteString, fromIndex: Int = size): Int + fun lastIndexOf(other: ByteString, fromIndex: Int = DEFAULT__ByteString_size): Int - fun lastIndexOf(other: ByteArray, fromIndex: Int = size): Int + fun lastIndexOf(other: ByteArray, fromIndex: Int = DEFAULT__ByteString_size): Int override fun equals(other: Any?): Boolean @@ -164,7 +182,7 @@ internal constructor(data: ByteArray) : Comparable<ByteString> { override fun toString(): String companion object { - /** A singleton empty `ByteString`. */ + /** A singleton empty `ByteString`. */ @JvmField val EMPTY: ByteString @@ -177,9 +195,9 @@ internal constructor(data: ByteArray) : Comparable<ByteString> { * starting at `offset`. */ @JvmStatic - fun ByteArray.toByteString(offset: Int = 0, byteCount: Int = size): ByteString + fun ByteArray.toByteString(offset: Int = 0, byteCount: Int = DEFAULT__ByteString_size): ByteString - /** Returns a new byte string containing the `UTF-8` bytes of this [String]. */ + /** Returns a new byte string containing the `UTF-8` bytes of this [String]. */ @JvmStatic fun String.encodeUtf8(): ByteString @@ -190,7 +208,7 @@ internal constructor(data: ByteArray) : Comparable<ByteString> { @JvmStatic fun String.decodeBase64(): ByteString? - /** Decodes the hex-encoded bytes and returns their value a byte string. */ + /** Decodes the hex-encoded bytes and returns their value a byte string. */ @JvmStatic fun String.decodeHex(): ByteString } diff --git a/okio/src/commonMain/kotlin/okio/-Platform.kt b/okio/src/commonMain/kotlin/okio/CommonPlatform.kt index 4790d3cd..e073d0b3 100644 --- a/okio/src/commonMain/kotlin/okio/-Platform.kt +++ b/okio/src/commonMain/kotlin/okio/CommonPlatform.kt @@ -13,9 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:JvmName("-CommonPlatform") // A leading '-' hides this class from Java. package okio +import kotlin.jvm.JvmName + internal expect fun ByteArray.toUtf8String(): String internal expect fun String.asUtf8ToByteArray(): ByteArray @@ -23,15 +26,24 @@ internal expect fun String.asUtf8ToByteArray(): ByteArray // TODO make internal https://youtrack.jetbrains.com/issue/KT-37316 expect class ArrayIndexOutOfBoundsException(message: String?) : IndexOutOfBoundsException -internal expect inline fun <R> synchronized(lock: Any, block: () -> R): R +expect class Lock + +expect inline fun <T> Lock.withLock(action: () -> T): T + +internal expect fun newLock(): Lock expect open class IOException(message: String?, cause: Throwable?) : Exception { - constructor(message: String? = null) + constructor(message: String?) + constructor() } -expect open class EOFException(message: String? = null) : IOException +expect class ProtocolException(message: String) : IOException + +expect open class EOFException(message: String?) : IOException { + constructor() +} -expect class FileNotFoundException(message: String? = null) : IOException +expect class FileNotFoundException(message: String?) : IOException expect interface Closeable { /** diff --git a/okio/src/commonMain/kotlin/okio/ExperimentalFileSystem.kt b/okio/src/commonMain/kotlin/okio/ExperimentalFileSystem.kt index 7fc3a4a2..c5e182eb 100644 --- a/okio/src/commonMain/kotlin/okio/ExperimentalFileSystem.kt +++ b/okio/src/commonMain/kotlin/okio/ExperimentalFileSystem.kt @@ -15,13 +15,16 @@ */ package okio -import kotlin.RequiresOptIn.Level.ERROR -import kotlin.annotation.AnnotationRetention.BINARY +import kotlin.DeprecationLevel.HIDDEN +import kotlin.annotation.AnnotationRetention.SOURCE import kotlin.annotation.AnnotationTarget.CLASS import kotlin.annotation.AnnotationTarget.FUNCTION import kotlin.annotation.AnnotationTarget.PROPERTY -@RequiresOptIn(level = ERROR, message = "okio's FileSystem is unstable and subject to change") -@Retention(BINARY) +@Deprecated( + message = "This annotation is obsolete and should be removed.", + level = HIDDEN, +) +@Retention(SOURCE) @Target(CLASS, FUNCTION, PROPERTY) annotation class ExperimentalFileSystem diff --git a/okio/src/commonMain/kotlin/okio/FileHandle.kt b/okio/src/commonMain/kotlin/okio/FileHandle.kt new file mode 100644 index 00000000..d05c3c99 --- /dev/null +++ b/okio/src/commonMain/kotlin/okio/FileHandle.kt @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + +/** + * An open file for reading and writing; using either streaming and random access. + * + * Use [read] and [write] to perform one-off random-access reads and writes. Use [source], [sink], + * and [appendingSink] for streaming reads and writes. + * + * File handles must be closed when they are no longer needed. It is an error to read, write, or + * create streams after a file handle is closed. The operating system resources held by a file + * handle will be released once the file handle **and** all of its streams are closed. + * + * Although this class offers both reading and writing APIs, file handle instances may be + * read-only or write-only. For example, a handle to a file on a read-only file system will throw an + * exception if a write is attempted. + * + * File handles may be used by multiple threads concurrently. But the individual sources and sinks + * produced by a file handle are not safe for concurrent use. + */ +abstract class FileHandle( + /** + * True if this handle supports both reading and writing. If this is false all write operations + * including [write], [sink], [resize], and [flush] will all throw [IllegalStateException] if + * called. + */ + val readWrite: Boolean, +) : Closeable { + /** + * True once the file handle is closed. Resources should be released with [protectedClose] once + * this is true and [openStreamCount] is 0. + */ + private var closed = false + + /** + * Reference count of the number of open sources and sinks on this file handle. Resources should + * be released with [protectedClose] once this is 0 and [closed] is true. + */ + private var openStreamCount = 0 + + val lock: Lock = newLock() + + /** + * Reads at least 1, and up to [byteCount] bytes from this starting at [fileOffset] and copies + * them to [array] at [arrayOffset]. Returns the number of bytes read, or -1 if [fileOffset] + * equals [size]. + */ + @Throws(IOException::class) + fun read( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ): Int { + lock.withLock { + check(!closed) { "closed" } + } + return protectedRead(fileOffset, array, arrayOffset, byteCount) + } + + /** + * Reads at least 1, and up to [byteCount] bytes from this starting at [fileOffset] and appends + * them to [sink]. Returns the number of bytes read, or -1 if [fileOffset] equals [size]. + */ + @Throws(IOException::class) + fun read(fileOffset: Long, sink: Buffer, byteCount: Long): Long { + lock.withLock { + check(!closed) { "closed" } + } + return readNoCloseCheck(fileOffset, sink, byteCount) + } + + /** + * Returns the total number of bytes in the file. This will change if the file size changes. + */ + @Throws(IOException::class) + fun size(): Long { + lock.withLock { + check(!closed) { "closed" } + } + return protectedSize() + } + + /** + * Changes the number of bytes in this file to [size]. This will remove bytes from the end if the + * new size is smaller. It will add `0` bytes to the end if it is larger. + */ + @Throws(IOException::class) + fun resize(size: Long) { + check(readWrite) { "file handle is read-only" } + lock.withLock { + check(!closed) { "closed" } + } + return protectedResize(size) + } + + /** Reads [byteCount] bytes from [array] and writes them to this at [fileOffset]. */ + fun write( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ) { + check(readWrite) { "file handle is read-only" } + lock.withLock { + check(!closed) { "closed" } + } + return protectedWrite(fileOffset, array, arrayOffset, byteCount) + } + + /** Removes [byteCount] bytes from [source] and writes them to this at [fileOffset]. */ + @Throws(IOException::class) + fun write(fileOffset: Long, source: Buffer, byteCount: Long) { + check(readWrite) { "file handle is read-only" } + lock.withLock { + check(!closed) { "closed" } + } + writeNoCloseCheck(fileOffset, source, byteCount) + } + + /** Pushes all buffered bytes to their final destination. */ + @Throws(IOException::class) + fun flush() { + check(readWrite) { "file handle is read-only" } + lock.withLock { + check(!closed) { "closed" } + } + return protectedFlush() + } + + /** + * Returns a source that reads from this starting at [fileOffset]. The returned source must be + * closed when it is no longer needed. + */ + @Throws(IOException::class) + fun source(fileOffset: Long = 0L): Source { + lock.withLock { + check(!closed) { "closed" } + openStreamCount++ + } + return FileHandleSource(this, fileOffset) + } + + /** + * Returns the position of [source] in the file. The argument [source] must be either a source + * produced by this file handle, or a [BufferedSource] that directly wraps such a source. If the + * parameter is a [BufferedSource], it adjusts for buffered bytes. + */ + @Throws(IOException::class) + fun position(source: Source): Long { + var source = source + var bufferSize = 0L + + if (source is RealBufferedSource) { + bufferSize = source.buffer.size + source = source.source + } + + require(source is FileHandleSource && source.fileHandle === this) { + "source was not created by this FileHandle" + } + check(!source.closed) { "closed" } + + return source.position - bufferSize + } + + /** + * Change the position of [source] in the file to [position]. The argument [source] must be either + * a source produced by this file handle, or a [BufferedSource] that directly wraps such a source. + * If the parameter is a [BufferedSource], it will skip or clear buffered bytes. + */ + @Throws(IOException::class) + fun reposition(source: Source, position: Long) { + if (source is RealBufferedSource) { + val fileHandleSource = source.source + require(fileHandleSource is FileHandleSource && fileHandleSource.fileHandle === this) { + "source was not created by this FileHandle" + } + check(!fileHandleSource.closed) { "closed" } + + val bufferSize = source.buffer.size + val toSkip = position - (fileHandleSource.position - bufferSize) + if (toSkip in 0L until bufferSize) { + // The new position requires only a buffer change. + source.skip(toSkip) + } else { + // The new position doesn't share data with the current buffer. + source.buffer.clear() + fileHandleSource.position = position + } + } else { + require(source is FileHandleSource && source.fileHandle === this) { + "source was not created by this FileHandle" + } + check(!source.closed) { "closed" } + source.position = position + } + } + + /** + * Returns a sink that writes to this starting at [fileOffset]. The returned sink must be closed + * when it is no longer needed. + */ + @Throws(IOException::class) + fun sink(fileOffset: Long = 0L): Sink { + check(readWrite) { "file handle is read-only" } + lock.withLock { + check(!closed) { "closed" } + openStreamCount++ + } + return FileHandleSink(this, fileOffset) + } + + /** + * Returns a sink that writes to this starting at the end. The returned sink must be closed when + * it is no longer needed. + */ + @Throws(IOException::class) + fun appendingSink(): Sink { + return sink(size()) + } + + /** + * Returns the position of [sink] in the file. The argument [sink] must be either a sink produced + * by this file handle, or a [BufferedSink] that directly wraps such a sink. If the parameter is a + * [BufferedSink], it adjusts for buffered bytes. + */ + @Throws(IOException::class) + fun position(sink: Sink): Long { + var sink = sink + var bufferSize = 0L + + if (sink is RealBufferedSink) { + bufferSize = sink.buffer.size + sink = sink.sink + } + + require(sink is FileHandleSink && sink.fileHandle === this) { + "sink was not created by this FileHandle" + } + check(!sink.closed) { "closed" } + + return sink.position + bufferSize + } + + /** + * Change the position of [sink] in the file to [position]. The argument [sink] must be either a + * sink produced by this file handle, or a [BufferedSink] that directly wraps such a sink. If the + * parameter is a [BufferedSink], it emits for buffered bytes. + */ + @Throws(IOException::class) + fun reposition(sink: Sink, position: Long) { + if (sink is RealBufferedSink) { + val fileHandleSink = sink.sink + require(fileHandleSink is FileHandleSink && fileHandleSink.fileHandle === this) { + "sink was not created by this FileHandle" + } + check(!fileHandleSink.closed) { "closed" } + + sink.emit() + fileHandleSink.position = position + } else { + require(sink is FileHandleSink && sink.fileHandle === this) { + "sink was not created by this FileHandle" + } + check(!sink.closed) { "closed" } + sink.position = position + } + } + + @Throws(IOException::class) + final override fun close() { + lock.withLock { + if (closed) return + closed = true + if (openStreamCount != 0) return + } + protectedClose() + } + + /** Like [read] but not performing any close check. */ + @Throws(IOException::class) + protected abstract fun protectedRead( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ): Int + + /** Like [write] but not performing any close check. */ + @Throws(IOException::class) + protected abstract fun protectedWrite( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ) + + /** Like [flush] but not performing any close check. */ + @Throws(IOException::class) + protected abstract fun protectedFlush() + + /** Like [resize] but not performing any close check. */ + @Throws(IOException::class) + protected abstract fun protectedResize(size: Long) + + /** Like [size] but not performing any close check. */ + @Throws(IOException::class) + protected abstract fun protectedSize(): Long + + /** + * Subclasses should implement this to release resources held by this file handle. It is invoked + * once both the file handle is closed, and also all sources and sinks produced by it are also + * closed. + */ + @Throws(IOException::class) + protected abstract fun protectedClose() + + private fun readNoCloseCheck(fileOffset: Long, sink: Buffer, byteCount: Long): Long { + require(byteCount >= 0L) { "byteCount < 0: $byteCount" } + + var currentOffset = fileOffset + val targetOffset = fileOffset + byteCount + + while (currentOffset < targetOffset) { + val tail = sink.writableSegment(1) + val readByteCount = protectedRead( + fileOffset = currentOffset, + array = tail.data, + arrayOffset = tail.limit, + byteCount = minOf(targetOffset - currentOffset, Segment.SIZE - tail.limit).toInt(), + ) + + if (readByteCount == -1) { + if (tail.pos == tail.limit) { + // We allocated a tail segment, but didn't end up needing it. Recycle! + sink.head = tail.pop() + SegmentPool.recycle(tail) + } + if (fileOffset == currentOffset) return -1L // We wanted bytes but didn't return any. + break + } + + tail.limit += readByteCount + currentOffset += readByteCount + sink.size += readByteCount + } + + return currentOffset - fileOffset + } + + private fun writeNoCloseCheck(fileOffset: Long, source: Buffer, byteCount: Long) { + checkOffsetAndCount(source.size, 0L, byteCount) + + var currentOffset = fileOffset + val targetOffset = fileOffset + byteCount + + while (currentOffset < targetOffset) { + val head = source.head!! + val toCopy = minOf(targetOffset - currentOffset, head.limit - head.pos).toInt() + protectedWrite(currentOffset, head.data, head.pos, toCopy) + + head.pos += toCopy + currentOffset += toCopy + source.size -= toCopy + + if (head.pos == head.limit) { + source.head = head.pop() + SegmentPool.recycle(head) + } + } + } + + private class FileHandleSink( + val fileHandle: FileHandle, + var position: Long, + ) : Sink { + var closed = false + + override fun write(source: Buffer, byteCount: Long) { + check(!closed) { "closed" } + fileHandle.writeNoCloseCheck(position, source, byteCount) + position += byteCount + } + + override fun flush() { + check(!closed) { "closed" } + fileHandle.protectedFlush() + } + + override fun timeout() = Timeout.NONE + + override fun close() { + if (closed) return + closed = true + fileHandle.lock.withLock { + fileHandle.openStreamCount-- + if (fileHandle.openStreamCount != 0 || !fileHandle.closed) return@close + } + fileHandle.protectedClose() + } + } + + private class FileHandleSource( + val fileHandle: FileHandle, + var position: Long, + ) : Source { + var closed = false + + override fun read(sink: Buffer, byteCount: Long): Long { + check(!closed) { "closed" } + val result = fileHandle.readNoCloseCheck(position, sink, byteCount) + if (result != -1L) position += result + return result + } + + override fun timeout() = Timeout.NONE + + override fun close() { + if (closed) return + closed = true + fileHandle.lock.withLock { + fileHandle.openStreamCount-- + if (fileHandle.openStreamCount != 0 || !fileHandle.closed) return@close + } + fileHandle.protectedClose() + } + } +} diff --git a/okio/src/commonMain/kotlin/okio/FileMetadata.kt b/okio/src/commonMain/kotlin/okio/FileMetadata.kt new file mode 100644 index 00000000..ede70e6f --- /dev/null +++ b/okio/src/commonMain/kotlin/okio/FileMetadata.kt @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlin.reflect.KClass +import kotlin.reflect.cast + +/** + * Description of a file or another object referenced by a path. + * + * In simple use a file system is a mechanism for organizing files and directories on a local + * storage device. In practice file systems are more capable and their contents more varied. For + * example, a path may refer to: + * + * * An operating system process that consumes data, produces data, or both. For example, reading + * from the `/dev/urandom` file on Linux returns a unique sequence of pseudorandom bytes to each + * reader. + * + * * A stream that connects a pair of programs together. A pipe is a special file that a producing + * program writes to and a consuming program reads from. Both programs operate concurrently. The + * size of a pipe is not well defined: the writer can write as much data as the reader is able to + * read. + * + * * A file on a remote file system. The performance and availability of remote files may be quite + * different from that of local files! + * + * * A symbolic link (symlink) to another path. When attempting to access this path the file system + * will follow the link and return data from the target path. + * + * * The same content as another path without a symlink. On UNIX file systems an inode is an + * anonymous handle to a file's content, and multiple paths may target the same inode without any + * other relationship to one another. A consequence of this design is that a directory with three + * 1 GiB files may only need 1 GiB on the storage device. + * + * This class does not attempt to model these rich file system features! It exposes a limited view + * useful for programs with only basic file system needs. Be cautious of the potential consequences + * of special files when writing programs that operate on a file system. + * + * File metadata is subject to change, and code that operates on file systems should defend against + * changes to the file that occur between reading metadata and subsequent operations. + */ +class FileMetadata( + /** True if this file is a container of bytes. If this is true, then [size] is non-null. */ + val isRegularFile: Boolean = false, + + /** + * True if the path refers to a directory that contains 0 or more child paths. + * + * Note that a path does not need to be a directory for [FileSystem.list] to return successfully. + * For example, mounted storage devices may have child files, but do not identify themselves as + * directories. + */ + val isDirectory: Boolean = false, + + /** + * The absolute or relative path that this file is a symlink to, or null if this is not a symlink. + * If this is a relative path, it is relative to the source file's parent directory. + */ + val symlinkTarget: Path? = null, + + /** + * The number of bytes readable from this file. The amount of storage resources consumed by this + * file may be larger (due to block size overhead, redundant copies for RAID, etc.), or smaller + * (due to file system compression, shared inodes, etc). + */ + val size: Long? = null, + + /** + * The system time of the host computer when this file was created, if the host file system + * supports this feature. This is typically available on Windows NTFS file systems and not + * available on UNIX or Windows FAT file systems. + */ + val createdAtMillis: Long? = null, + + /** + * The system time of the host computer when this file was most recently written. + * + * Note that the accuracy of the returned time may be much more coarse than its precision. In + * particular, this value is expressed with millisecond precision but may be accessed at + * second- or day-accuracy only. + */ + val lastModifiedAtMillis: Long? = null, + + /** + * The system time of the host computer when this file was most recently read or written. + * + * Note that the accuracy of the returned time may be much more coarse than its precision. In + * particular, this value is expressed with millisecond precision but may be accessed at + * second- or day-accuracy only. + */ + val lastAccessedAtMillis: Long? = null, + + extras: Map<KClass<*>, Any> = mapOf(), +) { + /** + * Additional file system-specific metadata organized by the class of that metadata. File systems + * may use this to include information like permissions, content-type, or linked applications. + * + * Values in this map should be instances of immutable classes. Keys should be the types of those + * classes. + */ + val extras: Map<KClass<*>, Any> = extras.toMap() + + /** Returns extra metadata of type [type], or null if no such metadata is held. */ + fun <T : Any> extra(type: KClass<out T>): T? { + val value = extras[type] ?: return null + return type.cast(value) + } + + fun copy( + isRegularFile: Boolean = this.isRegularFile, + isDirectory: Boolean = this.isDirectory, + symlinkTarget: Path? = this.symlinkTarget, + size: Long? = this.size, + createdAtMillis: Long? = this.createdAtMillis, + lastModifiedAtMillis: Long? = this.lastModifiedAtMillis, + lastAccessedAtMillis: Long? = this.lastAccessedAtMillis, + extras: Map<KClass<*>, Any> = this.extras, + ): FileMetadata { + return FileMetadata( + isRegularFile = isRegularFile, + isDirectory = isDirectory, + symlinkTarget = symlinkTarget, + size = size, + createdAtMillis = createdAtMillis, + lastAccessedAtMillis = lastAccessedAtMillis, + lastModifiedAtMillis = lastModifiedAtMillis, + extras = extras, + ) + } + + override fun toString(): String { + val fields = mutableListOf<String>() + if (isRegularFile) fields += "isRegularFile" + if (isDirectory) fields += "isDirectory" + if (size != null) fields += "byteCount=$size" + if (createdAtMillis != null) fields += "createdAt=$createdAtMillis" + if (lastModifiedAtMillis != null) fields += "lastModifiedAt=$lastModifiedAtMillis" + if (lastAccessedAtMillis != null) fields += "lastAccessedAt=$lastAccessedAtMillis" + if (extras.isNotEmpty()) fields += "extras=$extras" + return fields.joinToString(separator = ", ", prefix = "FileMetadata(", postfix = ")") + } +} diff --git a/okio/src/commonMain/kotlin/okio/FileSystem.kt b/okio/src/commonMain/kotlin/okio/FileSystem.kt new file mode 100644 index 00000000..9535880b --- /dev/null +++ b/okio/src/commonMain/kotlin/okio/FileSystem.kt @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 + +/** + * Read and write access to a hierarchical collection of files, addressed by [paths][Path]. This + * is a natural interface to the current computer's local file system. + * + * Other implementations are possible: + * + * * `FakeFileSystem` is an in-memory file system suitable for testing. Note that this class is + * included in the `okio-fakefilesystem` artifact. + * + * * [ForwardingFileSystem] is a file system decorator. Use it to apply monitoring, encryption, + * compression, or filtering to another file system. + * + * * A ZIP file system could provide access to the contents of a `.zip` file. + * + * For improved capability and testability, consider structuring your classes to dependency inject + * a `FileSystem` rather than using `FileSystem.SYSTEM` directly. + * + * Small API + * --------- + * + * This interface is deliberately limited in which features it supports. + * + * It is not suitable for high-latency or unreliable remote file systems. It lacks support for + * retries, timeouts, cancellation, and bulk operations. + * + * It cannot create special file types like hard links, pipes, or mounts. Reading or writing these + * files works as if they were regular files. + * + * It cannot read or write file access control features like the UNIX `chmod` and Windows access + * control lists. It does honor these controls and will fail with an [IOException] if privileges + * are insufficient! + * + * It cannot lock files or check which files are locked. + * + * It cannot watch the file system for changes. + * + * Applications that need rich file system features should use another API! + * + * Multiplatform + * ------------- + * + * This class supports a matrix of Kotlin platforms (JVM, Kotlin/Native, Kotlin/JS) and operating + * systems (Linux, macOS, and Windows). It attempts to balance working similarly across platforms + * with being consistent with the local operating system. + * + * This is a blocking API which limits its applicability on concurrent Node.js services. File + * operations will block the event loop (and all JavaScript execution!) until they complete. + * + * It supports the path schemes of both Windows (like `C:\Users`) and UNIX (like `/home`). Note that + * path resolution rules differ by platform. + * + * Differences vs. Java IO APIs + * ---------------------------- + * + * The `java.io.File` class is Java's original file system API. The `delete` and `renameTo` methods + * return false if the operation failed. The `list` method returns null if the file isn't a + * directory or could not be listed. This class always throws an [IOException] when an operation + * doesn't succeed. + * + * The `java.nio.file.Path` and `java.nio.file.Files` classes are the entry points of Java's new file system + * API. Each `Path` instance is scoped to a particular file system, though that is often implicit + * because the `Paths.get()` function automatically uses the default (ie. system) file system. + * In Okio's API paths are just identifiers; you must use a specific `FileSystem` object to do + * I/O with. + */ +expect abstract class FileSystem() { + + /** + * Resolves [path] against the current working directory and symlinks in this file system. The + * returned path identifies the same file as [path], but with an absolute path that does not + * include any symbolic links. + * + * This is similar to `File.getCanonicalFile()` on the JVM and `realpath` on POSIX. Unlike + * `File.getCanonicalFile()`, this throws if the file doesn't exist. + * + * @throws IOException if [path] cannot be resolved. This will occur if the file doesn't exist, + * if the current working directory doesn't exist or is inaccessible, or if another failure + * occurs while resolving the path. + */ + @Throws(IOException::class) + abstract fun canonicalize(path: Path): Path + + /** + * Returns metadata of the file, directory, or object identified by [path]. + * + * @throws IOException if [path] does not exist or its metadata cannot be read. + */ + @Throws(IOException::class) + fun metadata(path: Path): FileMetadata + + /** + * Returns metadata of the file, directory, or object identified by [path]. This returns null if + * there is no file at [path]. + * + * @throws IOException if [path] cannot be accessed due to a connectivity problem, permissions + * problem, or other issue. + */ + @Throws(IOException::class) + abstract fun metadataOrNull(path: Path): FileMetadata? + + /** + * Returns true if [path] identifies an object on this file system. + * + * @throws IOException if [path] cannot be accessed due to a connectivity problem, permissions + * problem, or other issue. + */ + @Throws(IOException::class) + fun exists(path: Path): Boolean + + /** + * Returns the children of [dir]. The returned list is sorted using natural ordering. If [dir] is + * a relative path, the returned elements will also be relative paths. If it is an absolute path, + * the returned elements will also be absolute paths. + * + * Note that a path does not need to be a [directory][FileMetadata.isDirectory] for this function + * to return successfully. For example, mounted storage devices may have child files but do not + * identify themselves as directories. + * + * @throws IOException if [dir] does not exist or cannot be listed. A path cannot be listed if the + * current process doesn't have access to [dir], or if there's a loop of symbolic links, or if + * any name is too long. + */ + @Throws(IOException::class) + abstract fun list(dir: Path): List<Path> + + /** + * Returns the children of the directory identified by [dir]. The returned list is sorted using + * natural ordering. If [dir] is a relative path, the returned elements will also be relative + * paths. If it is an absolute path, the returned elements will also be absolute paths. + * + * This returns null if [dir] does not exist or cannot be listed. A directory cannot be listed if + * the current process doesn't have access to [dir], or if there's a loop of symbolic links, or if + * any name is too long. + */ + abstract fun listOrNull(dir: Path): List<Path>? + + /** + * Returns a sequence that **lazily** traverses the children of [dir] using repeated calls to + * [list]. If none of [dir]'s children are directories this returns the same elements as [list]. + * + * The returned sequence visits the tree of files in depth-first order. Parent paths are returned + * before their children. + * + * Note that [listRecursively] does not throw exceptions but the returned sequence does. When it + * is iterated, the returned sequence throws a [FileNotFoundException] if [dir] does not exist, or + * an [IOException] if [dir] cannot be listed. + * + * @param followSymlinks true to follow symlinks while traversing the children. If [dir] itself is + * a symlink it will be followed even if this parameter is false. + */ + open fun listRecursively(dir: Path, followSymlinks: Boolean = false): Sequence<Path> + + /** + * Returns a handle to read [file]. This will fail if the file doesn't already exist. + * + * @throws IOException if [file] does not exist, is not a file, or cannot be accessed. A file + * cannot be accessed if the current process doesn't have sufficient permissions for [file], + * if there's a loop of symbolic links, or if any name is too long. + */ + @Throws(IOException::class) + abstract fun openReadOnly(file: Path): FileHandle + + /** + * Returns a handle to read and write [file]. This will create the file if it doesn't already + * exist. + * + * @param mustCreate true to throw an [IOException] instead of overwriting an existing file. + * This is equivalent to `O_EXCL` on POSIX and `CREATE_NEW` on Windows. + * @param mustExist true to throw an [IOException] instead of creating a new file. This is + * equivalent to `r+` on POSIX and `OPEN_EXISTING` on Windows. + * @throws IOException if [file] is not a file, or cannot be accessed. A file cannot be accessed + * if the current process doesn't have sufficient reading and writing permissions for [file], + * if there's a loop of symbolic links, or if any name is too long. + */ + @Throws(IOException::class) + abstract fun openReadWrite( + file: Path, + mustCreate: Boolean = false, + mustExist: Boolean = false, + ): FileHandle + + /** + * Returns a source that reads the bytes of [file] from beginning to end. + * + * @throws IOException if [file] does not exist, is not a file, or cannot be read. A file cannot + * be read if the current process doesn't have access to [file], if there's a loop of symbolic + * links, or if any name is too long. + */ + @Throws(IOException::class) + abstract fun source(file: Path): Source + + /** + * Creates a source to read [file], executes [readerAction] to read it, and then closes the + * source. This is a compact way to read the contents of a file. + */ + @Throws(IOException::class) + inline fun <T> read(file: Path, readerAction: BufferedSource.() -> T): T + + /** + * Returns a sink that writes bytes to [file] from beginning to end. If [file] already exists it + * will be replaced with the new data. + * + * @param mustCreate true to throw an [IOException] instead of overwriting an existing file. + * This is equivalent to `O_EXCL` on POSIX and `CREATE_NEW` on Windows. + * + * @throws IOException if [file] cannot be written. A file cannot be written if its enclosing + * directory does not exist, if the current process doesn't have access to [file], if there's + * a loop of symbolic links, or if any name is too long. + */ + @Throws(IOException::class) + abstract fun sink(file: Path, mustCreate: Boolean = false): Sink + + /** + * Creates a sink to write [file], executes [writerAction] to write it, and then closes the sink. + * This is a compact way to write a file. + * + * @param mustCreate true to throw an [IOException] instead of overwriting an existing file. + * This is equivalent to `O_EXCL` on POSIX and `CREATE_NEW` on Windows. + */ + @Throws(IOException::class) + inline fun <T> write( + file: Path, + mustCreate: Boolean = false, + writerAction: BufferedSink.() -> T, + ): T + + /** + * Returns a sink that appends bytes to the end of [file], creating it if it doesn't already + * exist. + * + * @param mustExist true to throw an [IOException] instead of creating a new file. This is + * equivalent to `r+` on POSIX and `OPEN_EXISTING` on Windows. + * + * @throws IOException if [file] cannot be written. A file cannot be written if its enclosing + * directory does not exist, if the current process doesn't have access to [file], if there's + * a loop of symbolic links, or if any name is too long. + */ + @Throws(IOException::class) + abstract fun appendingSink(file: Path, mustExist: Boolean = false): Sink + + /** + * Creates a directory at the path identified by [dir]. + * + * @param mustCreate true to throw an [IOException] if the directory already exists. + * @throws IOException if [dir]'s parent does not exist, is not a directory, or cannot be written. + * A directory cannot be created if the current process doesn't have access, if there's a loop + * of symbolic links, or if any name is too long. + */ + @Throws(IOException::class) + abstract fun createDirectory(dir: Path, mustCreate: Boolean = false) + + /** + * Creates a directory at the path identified by [dir], and any enclosing parent path directories, + * recursively. + * + * @param mustCreate true to throw an [IOException] instead of overwriting an existing directory. + * @throws IOException if any [metadata] or [createDirectory] operation fails. + */ + @Throws(IOException::class) + fun createDirectories(dir: Path, mustCreate: Boolean = false) + + /** + * Moves [source] to [target] in-place if the underlying file system supports it. If [target] + * exists, it is first removed. If `source == target`, this operation does nothing. This may be + * used to move a file or a directory. + * + * **Only as Atomic as the Underlying File System Supports** + * + * FAT and NTFS file systems cannot atomically move a file over an existing file. If the target + * file already exists, the move is performed into two steps: + * + * 1. Atomically delete the target file. + * 2. Atomically rename the source file to the target file. + * + * The delete step and move step are each atomic but not atomic in aggregate! If this process + * crashes, the host operating system crashes, or the hardware fails it is possible that the + * delete step will succeed and the rename will not. + * + * **Entire-file or nothing** + * + * These are the possible results of this operation: + * + * * This operation returns normally, the source file is absent, and the target file contains the + * data previously held by the source file. This is the success case. + * + * * The operation throws an [IOException] and the file system is unchanged. For example, this + * occurs if this process lacks permissions to perform the move. + * + * * This operation throws an [IOException], the target file is deleted, but the source file is + * unchanged. This is the partial failure case described above and is only possible on + * file systems like FAT and NTFS that do not support atomic file replacement. Typically in + * such cases this operation won't return at all because the process or operating system has + * also crashed. + * + * There is no failure mode where the target file holds a subset of the bytes of the source file. + * If the rename step cannot be performed atomically, this function will throw an [IOException] + * before attempting a move. Typically this occurs if the source and target files are on different + * physical volumes. + * + * **Non-Atomic Moves** + * + * If you need to move files across volumes, use [copy] followed by [delete], and change your + * application logic to recover should the copy step suffer a partial failure. + * + * @throws IOException if the move cannot be performed, or cannot be performed atomically. Moves + * fail if the source doesn't exist, if the target is not writable, if the target already + * exists and cannot be replaced, or if the move would cause physical or quota limits to be + * exceeded. This list of potential problems is not exhaustive. + */ + @Throws(IOException::class) + abstract fun atomicMove(source: Path, target: Path) + + /** + * Copies all the bytes from the file at [source] to the file at [target]. This does not copy + * file metadata like last modified time, permissions, or extended attributes. + * + * This function is not atomic; a failure may leave [target] in an inconsistent state. For + * example, [target] may be empty or contain only a prefix of [source]. + * + * @throws IOException if [source] cannot be read or if [target] cannot be written. + */ + @Throws(IOException::class) + open fun copy(source: Path, target: Path) + + /** + * Deletes the file or directory at [path]. + * + * @param mustExist true to throw an [IOException] if there is nothing at [path] to delete. + * @throws IOException if there is a file or directory but it could not be deleted. Deletes fail + * if the current process doesn't have access, if the file system is readonly, or if [path] + * is a non-empty directory. This list of potential problems is not exhaustive. + */ + @Throws(IOException::class) + abstract fun delete(path: Path, mustExist: Boolean = false) + + /** + * Recursively deletes all children of [fileOrDirectory] if it is a directory, then deletes + * [fileOrDirectory] itself. + * + * This function does not defend against race conditions. For example, if child files are created + * or deleted in [fileOrDirectory] while this function is executing, this may fail with an + * [IOException]. + * + * @param mustExist true to throw an [IOException] if there is nothing at [fileOrDirectory] to + * delete. + * @throws IOException if any [metadata], [list], or [delete] operation fails. + */ + @Throws(IOException::class) + open fun deleteRecursively(fileOrDirectory: Path, mustExist: Boolean = false) + + /** + * Creates a symbolic link at [source] that resolves to [target]. If [target] is a relative path, + * it is relative to `source.parent`. + * + * @throws IOException if [source] cannot be created. This may be because it already exists + * or because its storage doesn't support symlinks. This list of potential problems is not + * exhaustive. + */ + @Throws(IOException::class) + abstract fun createSymlink(source: Path, target: Path) + + companion object { + /** + * Returns a writable temporary directory on [SYSTEM]. + * + * This is platform-specific. + * + * * **JVM and Android**: the path in the `java.io.tmpdir` system property + * * **Linux, iOS, and macOS**: the path in the `TMPDIR` environment variable. + * * **Windows**: the first non-null of `TEMP`, `TMP`, and `USERPROFILE` environment variables. + * + * **Note that the returned directory is not generally private.** Other users or processes that + * share this file system may read data that is written to this directory, or write malicious + * data for this process to receive. + */ + val SYSTEM_TEMPORARY_DIRECTORY: Path + } +} diff --git a/okio/src/commonMain/kotlin/okio/ForwardingFileSystem.kt b/okio/src/commonMain/kotlin/okio/ForwardingFileSystem.kt new file mode 100644 index 00000000..3548b52b --- /dev/null +++ b/okio/src/commonMain/kotlin/okio/ForwardingFileSystem.kt @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlin.jvm.JvmName + +/** + * A [FileSystem] that forwards calls to another, intended for subclassing. + * + * ### Fault Injection + * + * You can use this to deterministically trigger file system failures in tests. This is useful to + * confirm that your program behaves correctly even if its file system operations fail. For example, + * this subclass fails every access of files named `unlucky.txt`: + * + * ``` + * val faultyFileSystem = object : ForwardingFileSystem(FileSystem.SYSTEM) { + * override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path { + * if (path.name == "unlucky.txt") throw IOException("synthetic failure!") + * return path + * } + * } + * ``` + * + * You can fail specific operations by overriding them directly: + * + * ``` + * val faultyFileSystem = object : ForwardingFileSystem(FileSystem.SYSTEM) { + * override fun delete(path: Path) { + * throw IOException("synthetic failure!") + * } + * } + * ``` + * + * ### Observability + * + * You can extend this to verify which files your program accesses. This is a testing file system + * that records accesses as they happen: + * + * ``` + * class LoggingFileSystem : ForwardingFileSystem(FileSystem.SYSTEM) { + * val log = mutableListOf<String>() + * + * override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path { + * log += "$functionName($parameterName=$path)" + * return path + * } + * } + * ``` + * + * This makes it easy for tests to assert exactly which files were accessed. + * + * ``` + * @Test + * fun testMergeJsonReports() { + * createSampleJsonReports() + * loggingFileSystem.log.clear() + * + * mergeJsonReports() + * + * assertThat(loggingFileSystem.log).containsExactly( + * "list(dir=json_reports)", + * "source(file=json_reports/2020-10.json)", + * "source(file=json_reports/2020-12.json)", + * "source(file=json_reports/2020-11.json)", + * "sink(file=json_reports/2020-all.json)" + * ) + * } + * ``` + * + * ### Transformations + * + * Subclasses can transform file names and content. + * + * For example, your program may be written to operate on a well-known directory like `/etc/` or + * `/System`. You can rewrite paths to make such operations safer to test. + * + * You may also transform file content to apply application-layer encryption or compression. This + * is particularly useful in situations where it's difficult or impossible to enable those features + * in the underlying file system. + * + * ### Abstract Functions Only + * + * Some file system functions like [copy] are implemented by using other features. These are the + * non-abstract functions in the [FileSystem] interface. + * + * **This class forwards only the abstract functions;** non-abstract functions delegate to the + * other functions of this class. If desired, subclasses may override non-abstract functions to + * forward them. + */ +abstract class ForwardingFileSystem( + /** [FileSystem] to which this instance is delegating. */ + @get:JvmName("delegate") + val delegate: FileSystem, +) : FileSystem() { + + /** + * Invoked each time a path is passed as a parameter to this file system. This returns the path to + * pass to [delegate], which should be [path] itself or a path on [delegate] that corresponds to + * it. + * + * Subclasses may override this to log accesses, fail on unexpected accesses, or map paths across + * file systems. + * + * The base implementation returns [path]. + * + * Note that this function will be called twice for calls to [atomicMove]; once for the source + * file and once for the target file. + * + * @param path the path passed to any of the functions of this. + * @param functionName a string like "canonicalize", "metadataOrNull", or "appendingSink". + * @param parameterName a string like "path", "file", "source", or "target". + * @return the path to pass to [delegate] for the same parameter. + */ + open fun onPathParameter(path: Path, functionName: String, parameterName: String): Path = path + + /** + * Invoked each time a path is returned by [delegate]. This returns the path to return to the + * caller, which should be [path] itself or a path on this that corresponds to it. + * + * Subclasses may override this to log accesses, fail on unexpected path accesses, or map + * directories or path names. + * + * The base implementation returns [path]. + * + * @param path the path returned by any of the functions of this. + * @param functionName a string like "canonicalize" or "list". + * @return the path to return to the caller. + */ + open fun onPathResult(path: Path, functionName: String): Path = path + + @Throws(IOException::class) + override fun canonicalize(path: Path): Path { + val path = onPathParameter(path, "canonicalize", "path") + val result = delegate.canonicalize(path) + return onPathResult(result, "canonicalize") + } + + @Throws(IOException::class) + override fun metadataOrNull(path: Path): FileMetadata? { + val path = onPathParameter(path, "metadataOrNull", "path") + val metadataOrNull = delegate.metadataOrNull(path) ?: return null + if (metadataOrNull.symlinkTarget == null) return metadataOrNull + + val symlinkTarget = onPathResult(metadataOrNull.symlinkTarget, "metadataOrNull") + return metadataOrNull.copy(symlinkTarget = symlinkTarget) + } + + @Throws(IOException::class) + override fun list(dir: Path): List<Path> { + val dir = onPathParameter(dir, "list", "dir") + val result = delegate.list(dir) + val paths = result.mapTo(mutableListOf()) { onPathResult(it, "list") } + paths.sort() + return paths + } + + override fun listOrNull(dir: Path): List<Path>? { + val dir = onPathParameter(dir, "listOrNull", "dir") + val result = delegate.listOrNull(dir) ?: return null + val paths = result.mapTo(mutableListOf()) { onPathResult(it, "listOrNull") } + paths.sort() + return paths + } + + override fun listRecursively(dir: Path, followSymlinks: Boolean): Sequence<Path> { + val dir = onPathParameter(dir, "listRecursively", "dir") + val result = delegate.listRecursively(dir, followSymlinks) + return result.map { onPathResult(it, "listRecursively") } + } + + @Throws(IOException::class) + override fun openReadOnly(file: Path): FileHandle { + val file = onPathParameter(file, "openReadOnly", "file") + return delegate.openReadOnly(file) + } + + @Throws(IOException::class) + override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { + val file = onPathParameter(file, "openReadWrite", "file") + return delegate.openReadWrite(file, mustCreate, mustExist) + } + + @Throws(IOException::class) + override fun source(file: Path): Source { + val file = onPathParameter(file, "source", "file") + return delegate.source(file) + } + + @Throws(IOException::class) + override fun sink(file: Path, mustCreate: Boolean): Sink { + val file = onPathParameter(file, "sink", "file") + return delegate.sink(file, mustCreate) + } + + @Throws(IOException::class) + override fun appendingSink(file: Path, mustExist: Boolean): Sink { + val file = onPathParameter(file, "appendingSink", "file") + return delegate.appendingSink(file, mustExist) + } + + @Throws(IOException::class) + override fun createDirectory(dir: Path, mustCreate: Boolean) { + val dir = onPathParameter(dir, "createDirectory", "dir") + delegate.createDirectory(dir, mustCreate) + } + + @Throws(IOException::class) + override fun atomicMove(source: Path, target: Path) { + val source = onPathParameter(source, "atomicMove", "source") + val target = onPathParameter(target, "atomicMove", "target") + delegate.atomicMove(source, target) + } + + @Throws(IOException::class) + override fun delete(path: Path, mustExist: Boolean) { + val path = onPathParameter(path, "delete", "path") + delegate.delete(path, mustExist) + } + + @Throws(IOException::class) + override fun createSymlink(source: Path, target: Path) { + val source = onPathParameter(source, "createSymlink", "source") + val target = onPathParameter(target, "createSymlink", "target") + delegate.createSymlink(source, target) + } + + override fun toString() = "${this::class.simpleName}($delegate)" +} diff --git a/okio/src/commonMain/kotlin/okio/ForwardingSource.kt b/okio/src/commonMain/kotlin/okio/ForwardingSource.kt new file mode 100644 index 00000000..0b3e9f6a --- /dev/null +++ b/okio/src/commonMain/kotlin/okio/ForwardingSource.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 + +/** A [Source] which forwards calls to another. Useful for subclassing. */ +expect abstract class ForwardingSource constructor( + /** [Source] to which this instance is delegating. */ + delegate: Source, +) : Source { + /** [Source] to which this instance is delegating. */ + val delegate: Source + + // TODO 'Source by delegate' once https://youtrack.jetbrains.com/issue/KT-23935 is fixed. + + @Throws(IOException::class) + override fun read(sink: Buffer, byteCount: Long): Long + + override fun timeout(): Timeout + + @Throws(IOException::class) + override fun close() + + override fun toString(): String +} diff --git a/okio/src/commonMain/kotlin/okio/HashingSink.kt b/okio/src/commonMain/kotlin/okio/HashingSink.kt index 06cb6bf0..d5b8f8db 100644 --- a/okio/src/commonMain/kotlin/okio/HashingSink.kt +++ b/okio/src/commonMain/kotlin/okio/HashingSink.kt @@ -42,10 +42,18 @@ expect class HashingSink : Sink { val hash: ByteString companion object { - /** Returns a sink that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. */ + /** + * Returns a sink that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. + * + * MD5 has been vulnerable to collisions since 2004. It should not be used in new code. + */ fun md5(sink: Sink): HashingSink - /** Returns a sink that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. */ + /** + * Returns a sink that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. + * + * SHA-1 has been vulnerable to collisions since 2017. It should not be used in new code. + */ fun sha1(sink: Sink): HashingSink /** Returns a sink that uses the SHA-256 hash algorithm to produce 256-bit hashes. */ diff --git a/okio/src/commonMain/kotlin/okio/HashingSource.kt b/okio/src/commonMain/kotlin/okio/HashingSource.kt index 9c61c995..52905ea7 100644 --- a/okio/src/commonMain/kotlin/okio/HashingSource.kt +++ b/okio/src/commonMain/kotlin/okio/HashingSource.kt @@ -43,10 +43,18 @@ expect class HashingSource : Source { val hash: ByteString companion object { - /** Returns a source that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. */ + /** + * Returns a source that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. + * + * MD5 has been vulnerable to collisions since 2004. It should not be used in new code. + */ fun md5(source: Source): HashingSource - /** Returns a source that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. */ + /** + * Returns a source that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. + * + * SHA-1 has been vulnerable to collisions since 2017. It should not be used in new code. + */ fun sha1(source: Source): HashingSource /** Returns a source that uses the SHA-256 hash algorithm to produce 256-bit hashes. */ diff --git a/okio/src/commonMain/kotlin/okio/Okio.kt b/okio/src/commonMain/kotlin/okio/Okio.kt index 116678aa..861d5112 100644 --- a/okio/src/commonMain/kotlin/okio/Okio.kt +++ b/okio/src/commonMain/kotlin/okio/Okio.kt @@ -56,13 +56,16 @@ inline fun <T : Closeable?, R> T.use(block: (T) -> R): R { result = block(this) } catch (t: Throwable) { thrown = t - } - - try { - this?.close() - } catch (t: Throwable) { - if (thrown == null) thrown = t - else thrown.addSuppressed(t) + } finally { + try { + this?.close() + } catch (t: Throwable) { + if (thrown == null) { + thrown = t + } else { + thrown.addSuppressed(t) + } + } } if (thrown != null) throw thrown diff --git a/okio/src/commonMain/kotlin/okio/Options.kt b/okio/src/commonMain/kotlin/okio/Options.kt index 9ce07391..e8dae6e1 100644 --- a/okio/src/commonMain/kotlin/okio/Options.kt +++ b/okio/src/commonMain/kotlin/okio/Options.kt @@ -20,7 +20,7 @@ import kotlin.jvm.JvmStatic /** An indexed set of values that may be read with [BufferedSource.select]. */ class Options private constructor( internal val byteStrings: Array<out ByteString>, - internal val trie: IntArray + internal val trie: IntArray, ) : AbstractList<ByteString>(), RandomAccess { override val size: Int @@ -111,7 +111,7 @@ class Options private constructor( byteStrings: List<ByteString>, fromIndex: Int = 0, toIndex: Int = byteStrings.size, - indexes: List<Int> + indexes: List<Int>, ) { require(fromIndex < toIndex) for (i in fromIndex until toIndex) { @@ -179,7 +179,7 @@ class Options private constructor( byteStrings = byteStrings, fromIndex = rangeStart, toIndex = rangeEnd, - indexes = indexes + indexes = indexes, ) } @@ -223,7 +223,7 @@ class Options private constructor( byteStrings = byteStrings, fromIndex = fromIndex, toIndex = toIndex, - indexes = indexes + indexes = indexes, ) node.writeAll(childNodes) } diff --git a/okio/src/commonMain/kotlin/okio/Path.kt b/okio/src/commonMain/kotlin/okio/Path.kt new file mode 100644 index 00000000..337abac7 --- /dev/null +++ b/okio/src/commonMain/kotlin/okio/Path.kt @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 okio.Path.Companion.toPath + +/** + * A hierarchical address on a file system. A path is an identifier only; a [FileSystem] is required + * to access the file that a path refers to, if any. + * + * UNIX and Windows Paths + * ---------------------- + * + * Paths follow different rules on UNIX vs. Windows operating systems. On UNIX operating systems + * (including Linux, Android, macOS, and iOS), the `/` slash character separates path segments. On + * Windows, the `\` backslash character separates path segments. The two platforms each have their + * own rules for path resolution. This class implements all rules on all platforms; for example you + * can model a Linux path in a native Windows application. + * + * Absolute and Relative Paths + * --------------------------- + * + * * **Absolute paths** identify a location independent of any working directory. On UNIX, absolute + * paths are prefixed with a slash, `/`. On Windows, absolute paths are one of two forms. The + * first is a volume letter, a colon, and a backslash, like `C:\`. The second is called a + * Universal Naming Convention (UNC) path, and it is prefixed by two backslashes `\\`. The term + * ‘fully-qualified path’ is a synonym of ‘absolute path’. + * + * * **Relative paths** are everything else. On their own, relative paths do not identify a + * location on a file system; they are relative to the system's current working directory. Use + * [FileSystem.canonicalize] to convert a relative path to its absolute path on a particular + * file system. + * + * There are some special cases when working with relative paths. + * + * On Windows, each volume (like `A:\` and `C:\`) has its own current working directory. A path + * prefixed with a volume letter and colon but no slash (like `A:letter.doc`) is relative to the + * working directory on the named volume. For example, if the working directory on `A:\` is + * `A:\jesse`, then the path `A:letter.doc` resolves to `A:\jesse\letter.doc`. + * + * The path string `C:\Windows` is an absolute path when following Windows rules and a relative + * path when following UNIX rules. For example, if the current working directory is + * `/Users/jesse`, then `C:\Windows` resolves to `/Users/jesse/C:/Windows`. + * + * This class decides which rules to follow by inspecting the first slash character in the path + * string. If the path contains no slash characters, it uses the host platform's rules. Or you may + * explicitly specify which rules to use by specifying the `directorySeparator` parameter in + * [toPath]. Pass `"/"` to get UNIX rules and `"\"` to get Windows rules. + * + * Path Traversal + * -------------- + * + * After the optional path root (like `/` on UNIX, like `X:\` or `\\` on Windows), the remainder of + * the path is a sequence of segments separated by `/` or `\` characters. Segments satisfy these + * rules: + * + * * Segments are always non-empty. + * * If the segment is `.`, then the full path must be `.`. + * * For normalized paths, if the segment is `..`, then the path must be relative. All `..` + * segments precede all other segments. In all cases, a segment `..` cannot be the first segment + * of an absolute path. + * + * The only path that ends with `/` is the file system root, `/`. The dot path `.` is a relative + * path that resolves to whichever path it is resolved against. + * + * The [name] is the last segment in a path. It is typically a file or directory name, like + * `README.md` or `Desktop`. The name may be another special value: + * + * * The empty string is the name of the file system root path (full path `/`). + * * `.` is the name of the identity relative path (full path `.`). + * * `..` is the name of a path consisting of only `..` segments (such as `../../..`). + * + * Comparing Paths + * --------------- + * + * Path implements [Comparable], [equals], and [hashCode]. If two paths are equal then they operate + * on the same file on the file system. + * + * Note that the converse is not true: **if two paths are non-equal, they may still resolve to the + * same file on the file system.** Here are some of the ways non-equal paths resolve to the same + * file: + * + * * **Case differences.** The default file system on macOS is case-insensitive. The paths + * `/Users/jesse/notes.txt` and `/USERS/JESSE/NOTES.TXT` are non-equal but these paths resolve to + * the same file. + * * **Mounting differences.** Volumes may be mounted at multiple paths. On macOS, + * `/Users/jesse/notes.txt` and `/Volumes/Macintosh HD/Users/jesse/notes.txt` typically resolve + * to the same file. On Windows, `C:\project\notes.txt` and `\\localhost\c$\project\notes.txt` + * typically resolve to the same file. + * * **Hard links.** UNIX file systems permit multiple paths to refer for same file. The paths may + * be wildly different, like `/Users/jesse/bruce_wayne.vcard` and + * `/Users/jesse/batman.vcard`, but changes via either path are reflected in both. + * * **Symlinks.** Symlinks permit multiple paths and directories to refer to the same file. On + * macOS `/tmp` is symlinked to `/private/tmp`, so `/tmp/notes.txt` and `/private/tmp/notes.txt` + * resolve to the same file. + * + * To test whether two paths refer to the same file, try [FileSystem.canonicalize] first. This + * follows symlinks and looks up the preserved casing for case-insensitive case-preserved paths. + * **This method does not guarantee a unique result, however.** For example, each hard link to a + * file may return its own canonical path. + * + * Paths are sorted in case-sensitive order. + * + * Sample Paths + * ------------ + * + * <table> + * <tr><th> Path <th> Parent <th> Root <th> Name <th> Notes </tr> + * <tr><td> `/` <td> null <td> `/` <td> (empty) <td> root </tr> + * <tr><td> `/home/jesse/notes.txt` <td> `/home/jesse` <td> `/` <td> `notes.txt` <td> absolute path </tr> + * <tr><td> `project/notes.txt` <td> `project` <td> null <td> `notes.txt` <td> relative path </tr> + * <tr><td> `../../project/notes.txt` <td> `../../project` <td> null <td> `notes.txt` <td> relative path with traversal </tr> + * <tr><td> `../../..` <td> null <td> null <td> `..` <td> relative path with traversal </tr> + * <tr><td> `.` <td> null <td> null <td> `.` <td> current working directory </tr> + * <tr><td> `C:\` <td> null <td> `C:\` <td> (empty) <td> volume root (Windows) </tr> + * <tr><td> `C:\Windows\notepad.exe` <td> `C:\Windows` <td> `C:\` <td> `notepad.exe` <td> volume absolute path (Windows) </tr> + * <tr><td> `\` <td> null <td> `\` <td> (empty) <td> absolute path (Windows) </tr> + * <tr><td> `\Windows\notepad.exe` <td> `\Windows` <td> `\` <td> `notepad.exe` <td> absolute path (Windows) </tr> + * <tr><td> `C:` <td> null <td> null <td> (empty) <td> volume-relative path (Windows) </tr> + * <tr><td> `C:project\notes.txt` <td> `C:project` <td> null <td> `notes.txt` <td> volume-relative path (Windows) </tr> + * <tr><td> `\\server` <td> null <td> `\\server` <td> `server` <td> UNC server (Windows) </tr> + * <tr><td> `\\server\project\notes.txt` <td> `\\server\project` <td> `\\server` <td> `notes.txt` <td> UNC absolute path (Windows) </tr> + * </table> + */ +expect class Path internal constructor(bytes: ByteString) : Comparable<Path> { + /** + * This is the root path if this is an absolute path, or null if it is a relative path. UNIX paths + * have a single root, `/`. Each volume on Windows is its own root, like `C:\` and `D:\`. The + * path to the current volume `\` is its own root. Windows UNC paths like `\\server` are also + * roots. + */ + val root: Path? + + /** + * The components of this path that are usually delimited by slashes. If the root is not null it + * precedes these segments. If this path is a root its segments list is empty. + */ + val segments: List<String> + + val segmentsBytes: List<ByteString> + + internal val bytes: ByteString + + /** This is true if [root] is not null. */ + val isAbsolute: Boolean + + /** This is true if [root] is null. */ + val isRelative: Boolean + + /** + * This is the volume letter like "C" on Windows paths that starts with a volume letter. For + * example, on the path "C:\Windows" this returns "C". This property is null if this is not a + * Windows path, or if it doesn't have a volume letter. + * + * Note that paths that start with a volume letter are not necessarily absolute paths. For + * example, the path "C:notepad.exe" is relative to whatever the current working directory is on + * the C: drive. + */ + val volumeLetter: Char? + + val nameBytes: ByteString + + val name: String + + /** + * Returns the path immediately enclosing this path. + * + * This returns null if this has no parent. That includes these paths: + * + * * The file system root (`/`) + * * The identity relative path (`.`) + * * A Windows volume root (like `C:\`) + * * A Windows Universal Naming Convention (UNC) root path (`\\server`) + * * A reference to the current working directory on a Windows volume (`C:`). + * * A series of relative paths (like `..` and `../..`). + */ + val parent: Path? + + /** Returns true if `this == this.root`. That is, this is an absolute path with no parent. */ + val isRoot: Boolean + + /** + * Returns a path that resolves [child] relative to this path. Note that the result isn't + * guaranteed to be normalized even if this and [child] are both normalized themselves. + * + * If [child] is an [absolute path][isAbsolute] or [has a volume letter][hasVolumeLetter] then + * this function is equivalent to `child.toPath()`. + */ + operator fun div(child: String): Path + + /** + * Returns a path that resolves [child] relative to this path. Note that the result isn't + * guaranteed to be normalized even if this and [child] are both normalized themselves. + * + * If [child] is an [absolute path][isAbsolute] or [has a volume letter][hasVolumeLetter] then + * this function is equivalent to `child.toPath()`. + */ + operator fun div(child: ByteString): Path + + /** + * Returns a path that resolves [child] relative to this path. Note that the result isn't + * guaranteed to be normalized even if this and [child] are both normalized themselves. + * + * If [child] is an [absolute path][isAbsolute] or [has a volume letter][hasVolumeLetter] then + * this function is equivalent to `child.toPath()`. + */ + operator fun div(child: Path): Path + + /** + * Returns a path that resolves [child] relative to this path. + * + * Set [normalize] to true to eagerly consume `..` segments on the resolved path. In all cases, + * leading `..` on absolute paths will be removed. If [normalize] is false, note that the result + * isn't guaranteed to be normalized even if this and [child] are both normalized themselves. + * + * If [child] is an [absolute path][isAbsolute] or [has a volume letter][hasVolumeLetter] then + * this function is equivalent to `child.toPath(normalize)`. + */ + fun resolve(child: String, normalize: Boolean = false): Path + + /** + * Returns a path that resolves [child] relative to this path. + * + * Set [normalize] to true to eagerly consume `..` segments on the resolved path. In all cases, + * leading `..` on absolute paths will be removed. If [normalize] is false, note that the result + * isn't guaranteed to be normalized even if this and [child] are both normalized themselves. + * + * If [child] is an [absolute path][isAbsolute] or [has a volume letter][hasVolumeLetter] then + * this function is equivalent to `child.toPath(normalize)`. + */ + fun resolve(child: ByteString, normalize: Boolean = false): Path + + /** + * Returns a path that resolves [child] relative to this path. + * + * Set [normalize] to true to eagerly consume `..` segments on the resolved path. In all cases, + * leading `..` on absolute paths will be removed. If [normalize] is false, note that the result + * isn't guaranteed to be normalized even if this and [child] are both normalized themselves. + * + * If [child] is an [absolute path][isAbsolute] or [has a volume letter][hasVolumeLetter] then + * this function is equivalent to `child.toPath(normalize)`. + */ + fun resolve(child: Path, normalize: Boolean = false): Path + + /** + * Returns this path relative to [other]. This effectively inverts the resolve operator, `/`. For + * any two paths `a` and `b` that have the same root, `a / (b.relativeTo(a))` is equal to `b`. If + * both paths don't use the same slash, the resolved path will use the slash of the [other] path. + * + * @throws IllegalArgumentException if this path and the [other] path are not both + * [absolute paths][isAbsolute] or both [relative paths][isRelative], or if they are both + * [absolute paths][isAbsolute] but of different roots (C: vs D:, or C: vs \\server, etc.). + * It will also throw if the relative path is impossible to resolve. For instance, it is + * impossible to resolve the path `../a` relative to `../../b`. + */ + @Throws(IllegalArgumentException::class) + fun relativeTo(other: Path): Path + + /** + * Returns the normalized version of this path. This has the same effect as + * `this.toString().toPath(normalize = true)`. + */ + fun normalized(): Path + + override fun compareTo(other: Path): Int + + override fun equals(other: Any?): Boolean + + override fun hashCode(): Int + + override fun toString(): String + + companion object { + /** + * Either `/` (on UNIX-like systems including Android, iOS, and Linux) or `\` (on Windows + * systems). + * + * This separator is used by `FileSystem.SYSTEM` and possibly other file systems on the host + * system. Some file system implementations may not use this separator. + */ + val DIRECTORY_SEPARATOR: String + + /** + * Returns the [Path] representation for this string. + * + * Set [normalize] to true to eagerly consume `..` segments in your path. In all cases, leading + * `..` on absolute paths will be removed. + * + * ``` + * "/Users/jesse/Documents/../notes.txt".toPath(normalize = false).toString() => "/Users/jesse/Documents/../notes.txt" + * "/Users/jesse/Documents/../notes.txt".toPath(normalize = true).toString() => "/Users/jesse/notes.txt" + * ``` + */ + fun String.toPath(normalize: Boolean = false): Path + } +} diff --git a/okio/src/commonMain/kotlin/okio/PeekSource.kt b/okio/src/commonMain/kotlin/okio/PeekSource.kt index 598d83de..70f8ef92 100644 --- a/okio/src/commonMain/kotlin/okio/PeekSource.kt +++ b/okio/src/commonMain/kotlin/okio/PeekSource.kt @@ -26,7 +26,7 @@ package okio * invalid and throw [IllegalStateException] on any future reads. */ internal class PeekSource( - private val upstream: BufferedSource + private val upstream: BufferedSource, ) : Source { private val buffer = upstream.buffer private var expectedSegment = buffer.head @@ -42,7 +42,7 @@ internal class PeekSource( // do not match the current head and head position of the upstream buffer check( expectedSegment == null || - expectedSegment === buffer.head && expectedPos == buffer.head!!.pos + expectedSegment === buffer.head && expectedPos == buffer.head!!.pos, ) { "Peek source is invalid because upstream source was used" } diff --git a/okio/src/commonMain/kotlin/okio/RealBufferedSink.kt b/okio/src/commonMain/kotlin/okio/RealBufferedSink.kt index 80e3fae4..81032153 100644 --- a/okio/src/commonMain/kotlin/okio/RealBufferedSink.kt +++ b/okio/src/commonMain/kotlin/okio/RealBufferedSink.kt @@ -17,7 +17,7 @@ package okio internal expect class RealBufferedSink( - sink: Sink + sink: Sink, ) : BufferedSink { val sink: Sink var closed: Boolean diff --git a/okio/src/commonMain/kotlin/okio/RealBufferedSource.kt b/okio/src/commonMain/kotlin/okio/RealBufferedSource.kt index b626e425..b6f7322e 100644 --- a/okio/src/commonMain/kotlin/okio/RealBufferedSource.kt +++ b/okio/src/commonMain/kotlin/okio/RealBufferedSource.kt @@ -17,7 +17,7 @@ package okio internal expect class RealBufferedSource( - source: Source + source: Source, ) : BufferedSource { val source: Source var closed: Boolean diff --git a/okio/src/commonMain/kotlin/okio/Segment.kt b/okio/src/commonMain/kotlin/okio/Segment.kt index 36879499..3a34c59f 100644 --- a/okio/src/commonMain/kotlin/okio/Segment.kt +++ b/okio/src/commonMain/kotlin/okio/Segment.kt @@ -167,8 +167,10 @@ internal class Segment { } data.copyInto( - sink.data, destinationOffset = sink.limit, startIndex = pos, - endIndex = pos + byteCount + sink.data, + destinationOffset = sink.limit, + startIndex = pos, + endIndex = pos + byteCount, ) sink.limit += byteCount pos += byteCount diff --git a/okio/src/commonMain/kotlin/okio/SegmentedByteString.kt b/okio/src/commonMain/kotlin/okio/SegmentedByteString.kt index bda4f4a3..1eb5628e 100644 --- a/okio/src/commonMain/kotlin/okio/SegmentedByteString.kt +++ b/okio/src/commonMain/kotlin/okio/SegmentedByteString.kt @@ -42,7 +42,7 @@ package okio */ internal expect class SegmentedByteString internal constructor( segments: Array<ByteArray>, - directory: IntArray + directory: IntArray, ) : ByteString { internal val segments: Array<ByteArray> diff --git a/okio/src/commonMain/kotlin/okio/Utf8.kt b/okio/src/commonMain/kotlin/okio/Utf8.kt index ca20e214..465a4abf 100644 --- a/okio/src/commonMain/kotlin/okio/Utf8.kt +++ b/okio/src/commonMain/kotlin/okio/Utf8.kt @@ -82,7 +82,7 @@ fun String.utf8Size(beginIndex: Int = 0, endIndex: Int = length): Long { var result = 0L var i = beginIndex while (i < endIndex) { - val c = this[i].toInt() + val c = this[i].code if (c < 0x80) { // A 7-bit character with 1 byte. @@ -97,7 +97,7 @@ fun String.utf8Size(beginIndex: Int = 0, endIndex: Int = length): Long { result += 3 i++ } else { - val low = if (i + 1 < endIndex) this[i + 1].toInt() else 0 + val low = if (i + 1 < endIndex) this[i + 1].code else 0 if (c > 0xdbff || low < 0xdc00 || low > 0xdfff) { // A malformed surrogate, which yields '?'. result++ @@ -113,9 +113,9 @@ fun String.utf8Size(beginIndex: Int = 0, endIndex: Int = length): Long { return result } -internal const val REPLACEMENT_BYTE: Byte = '?'.toByte() +internal const val REPLACEMENT_BYTE: Byte = '?'.code.toByte() internal const val REPLACEMENT_CHARACTER: Char = '\ufffd' -internal const val REPLACEMENT_CODE_POINT: Int = REPLACEMENT_CHARACTER.toInt() +internal const val REPLACEMENT_CODE_POINT: Int = REPLACEMENT_CHARACTER.code @Suppress("NOTHING_TO_INLINE") // Syntactic sugar. internal inline fun isIsoControl(codePoint: Int): Boolean = @@ -132,7 +132,7 @@ internal inline fun isUtf8Continuation(byte: Byte): Boolean { internal inline fun String.processUtf8Bytes( beginIndex: Int, endIndex: Int, - yield: (Byte) -> Unit + yield: (Byte) -> Unit, ) { // Transcode a UTF-16 String to UTF-8 bytes. var index = beginIndex @@ -142,20 +142,20 @@ internal inline fun String.processUtf8Bytes( when { c < '\u0080' -> { // Emit a 7-bit character with 1 byte. - yield(c.toByte()) // 0xxxxxxx + yield(c.code.toByte()) // 0xxxxxxx index++ // Assume there is going to be more ASCII while (index < endIndex && this[index] < '\u0080') { - yield(this[index++].toByte()) + yield(this[index++].code.toByte()) } } c < '\u0800' -> { // Emit a 11-bit character with 2 bytes. /* ktlint-disable no-multi-spaces */ - yield((c.toInt() shr 6 or 0xc0).toByte()) // 110xxxxx - yield((c.toInt() and 0x3f or 0x80).toByte()) // 10xxxxxx + yield((c.code shr 6 or 0xc0).toByte()) // 110xxxxx + yield((c.code and 0x3f or 0x80).toByte()) // 10xxxxxx /* ktlint-enable no-multi-spaces */ index++ } @@ -163,9 +163,9 @@ internal inline fun String.processUtf8Bytes( c !in '\ud800'..'\udfff' -> { // Emit a 16-bit character with 3 bytes. /* ktlint-disable no-multi-spaces */ - yield((c.toInt() shr 12 or 0xe0).toByte()) // 1110xxxx - yield((c.toInt() shr 6 and 0x3f or 0x80).toByte()) // 10xxxxxx - yield((c.toInt() and 0x3f or 0x80).toByte()) // 10xxxxxx + yield((c.code shr 12 or 0xe0).toByte()) // 1110xxxx + yield((c.code shr 6 and 0x3f or 0x80).toByte()) // 10xxxxxx + yield((c.code and 0x3f or 0x80).toByte()) // 10xxxxxx /* ktlint-enable no-multi-spaces */ index++ } @@ -185,7 +185,7 @@ internal inline fun String.processUtf8Bytes( // UTF-16 low surrogate: 110111yyyyyyyyyy (10 bits) // Unicode code point: 00010000000000000000 + xxxxxxxxxxyyyyyyyyyy (21 bits) val codePoint = ( - ((c.toInt() shl 10) + this[index + 1].toInt()) + + ((c.code shl 10) + this[index + 1].code) + (0x010000 - (0xd800 shl 10) - 0xdc00) ) @@ -207,7 +207,7 @@ internal inline fun String.processUtf8Bytes( internal inline fun ByteArray.processUtf8CodePoints( beginIndex: Int, endIndex: Int, - yield: (Int) -> Unit + yield: (Int) -> Unit, ) { var index = beginIndex while (index < endIndex) { @@ -247,6 +247,7 @@ internal inline fun ByteArray.processUtf8CodePoints( // Value added to the high UTF-16 surrogate after shifting internal const val HIGH_SURROGATE_HEADER = 0xd800 - (0x010000 ushr 10) + // Value added to the low UTF-16 surrogate after masking internal const val LOG_SURROGATE_HEADER = 0xdc00 @@ -254,7 +255,7 @@ internal const val LOG_SURROGATE_HEADER = 0xdc00 internal inline fun ByteArray.processUtf16Chars( beginIndex: Int, endIndex: Int, - yield: (Char) -> Unit + yield: (Char) -> Unit, ) { var index = beginIndex while (index < endIndex) { @@ -262,13 +263,13 @@ internal inline fun ByteArray.processUtf16Chars( when { b0 >= 0 -> { // 0b0xxxxxxx - yield(b0.toChar()) + yield(b0.toInt().toChar()) index++ // Assume there is going to be more ASCII // This is almost double the performance of the outer loop while (index < endIndex && this[index] >= 0) { - yield(this[index++].toChar()) + yield(this[index++].toInt().toChar()) } } b0 shr 5 == -2 -> { @@ -391,7 +392,7 @@ internal const val MASK_2BYTES = 0x0f80 internal inline fun ByteArray.process2Utf8Bytes( beginIndex: Int, endIndex: Int, - yield: (Int) -> Unit + yield: (Int) -> Unit, ): Int { if (endIndex <= beginIndex + 1) { yield(REPLACEMENT_CODE_POINT) @@ -434,7 +435,7 @@ internal const val MASK_3BYTES = -0x01e080 internal inline fun ByteArray.process3Utf8Bytes( beginIndex: Int, endIndex: Int, - yield: (Int) -> Unit + yield: (Int) -> Unit, ): Int { if (endIndex <= beginIndex + 2) { // At least 2 bytes remaining @@ -494,7 +495,7 @@ internal const val MASK_4BYTES = 0x381f80 internal inline fun ByteArray.process4Utf8Bytes( beginIndex: Int, endIndex: Int, - yield: (Int) -> Unit + yield: (Int) -> Unit, ): Int { if (endIndex <= beginIndex + 3) { // At least 3 bytes remaining diff --git a/okio/src/commonMain/kotlin/okio/-Util.kt b/okio/src/commonMain/kotlin/okio/Util.kt index 9ab36882..bfd8fec1 100644 --- a/okio/src/commonMain/kotlin/okio/-Util.kt +++ b/okio/src/commonMain/kotlin/okio/Util.kt @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -@file:JvmName("-Util") +@file:JvmName("-SegmentedByteString") // A leading '-' hides this class from Java. package okio -import okio.internal.HEX_DIGIT_CHARS import kotlin.jvm.JvmName +import kotlin.native.concurrent.SharedImmutable +import okio.internal.HEX_DIGIT_CHARS internal fun checkOffsetAndCount(size: Long, offset: Long, byteCount: Long) { if (offset or byteCount < 0 || offset > size || size - offset < byteCount) { @@ -93,7 +93,7 @@ internal fun arrayRangeEquals( aOffset: Int, b: ByteArray, bOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean { for (i in 0 until byteCount) { if (a[i + aOffset] != b[i + bOffset]) return false @@ -105,7 +105,7 @@ internal fun Byte.toHexString(): String { val result = CharArray(2) result[0] = HEX_DIGIT_CHARS[this shr 4 and 0xf] result[1] = HEX_DIGIT_CHARS[this and 0xf] // ktlint-disable no-multi-spaces - return String(result) + return result.concatToString() } internal fun Int.toHexString(): String { @@ -128,7 +128,7 @@ internal fun Int.toHexString(): String { i++ } - return String(result, i, result.size - i) + return result.concatToString(i, result.size) } internal fun Long.toHexString(): String { @@ -159,5 +159,28 @@ internal fun Long.toHexString(): String { i++ } - return String(result, i, result.size - i) + return result.concatToString(i, result.size) +} + +// Work around a problem where Kotlin/JS IR can't handle default parameters on expect functions +// that depend on the receiver. We use well-known, otherwise-impossible values here and must check +// for them in the receiving function, then swap in the true default value. +// https://youtrack.jetbrains.com/issue/KT-45542 + +@SharedImmutable +internal val DEFAULT__new_UnsafeCursor = Buffer.UnsafeCursor() +internal fun resolveDefaultParameter(unsafeCursor: Buffer.UnsafeCursor): Buffer.UnsafeCursor { + if (unsafeCursor === DEFAULT__new_UnsafeCursor) return Buffer.UnsafeCursor() + return unsafeCursor +} + +internal val DEFAULT__ByteString_size = -1234567890 +internal fun ByteString.resolveDefaultParameter(position: Int): Int { + if (position == DEFAULT__ByteString_size) return size + return position +} + +internal fun ByteArray.resolveDefaultParameter(sizeParam: Int): Int { + if (sizeParam == DEFAULT__ByteString_size) return size + return sizeParam } diff --git a/okio/src/commonMain/kotlin/okio/internal/-Utf8.kt b/okio/src/commonMain/kotlin/okio/internal/-Utf8.kt index 926f1853..865ff2c1 100644 --- a/okio/src/commonMain/kotlin/okio/internal/-Utf8.kt +++ b/okio/src/commonMain/kotlin/okio/internal/-Utf8.kt @@ -1,3 +1,4 @@ +// ktlint-disable filename /* * Copyright (C) 2018 Square, Inc. * @@ -13,7 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okio.internal import okio.ArrayIndexOutOfBoundsException @@ -35,7 +35,7 @@ fun ByteArray.commonToUtf8String(beginIndex: Int = 0, endIndex: Int = size): Str chars[length++] = c } - return String(chars, 0, length) + return chars.concatToString(0, length) } fun String.commonAsUtf8ToByteArray(): ByteArray { @@ -52,7 +52,7 @@ fun String.commonAsUtf8ToByteArray(): ByteArray { } return bytes.copyOf(size) } - bytes[index] = b0.toByte() + bytes[index] = b0.code.toByte() } return bytes.copyOf(length) diff --git a/okio/src/commonMain/kotlin/okio/internal/Buffer.kt b/okio/src/commonMain/kotlin/okio/internal/Buffer.kt index 0cb15cc4..2270fc1d 100644 --- a/okio/src/commonMain/kotlin/okio/internal/Buffer.kt +++ b/okio/src/commonMain/kotlin/okio/internal/Buffer.kt @@ -13,12 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:JvmName("-Buffer") // A leading '-' hides this class from Java. // TODO move to Buffer class: https://youtrack.jetbrains.com/issue/KT-20427 @file:Suppress("NOTHING_TO_INLINE") package okio.internal +import kotlin.jvm.JvmName +import kotlin.native.concurrent.SharedImmutable import okio.ArrayIndexOutOfBoundsException import okio.Buffer import okio.Buffer.UnsafeCursor @@ -35,8 +38,10 @@ import okio.and import okio.asUtf8ToByteArray import okio.checkOffsetAndCount import okio.minOf +import okio.resolveDefaultParameter import okio.toHexString +@SharedImmutable internal val HEX_DIGIT_BYTES = "0123456789abcdef".asUtf8ToByteArray() // Threshold determined empirically via ReadByteStringBenchmark @@ -52,7 +57,7 @@ internal fun rangeEquals( segmentPos: Int, bytes: ByteArray, bytesOffset: Int, - bytesLimit: Int + bytesLimit: Int, ): Boolean { var segment = segment var segmentPos = segmentPos @@ -81,7 +86,7 @@ internal fun rangeEquals( internal fun Buffer.readUtf8Line(newline: Long): String { return when { - newline > 0 && this[newline - 1] == '\r'.toByte() -> { + newline > 0 && this[newline - 1] == '\r'.code.toByte() -> { // Read everything until '\r\n', then skip the '\r\n'. val result = readUtf8(newline - 1L) skip(2L) @@ -102,7 +107,7 @@ internal fun Buffer.readUtf8Line(newline: Long): String { */ internal inline fun <T> Buffer.seek( fromIndex: Long, - lambda: (Segment?, Long) -> T + lambda: (Segment?, Long) -> T, ): T { var s: Segment = head ?: return lambda(null, -1L) @@ -234,7 +239,7 @@ internal fun Buffer.selectPrefix(options: Options, selectTruncated: Boolean = fa internal inline fun Buffer.commonCopyTo( out: Buffer, offset: Long, - byteCount: Long + byteCount: Long, ): Buffer { var offset = offset var byteCount = byteCount @@ -434,7 +439,7 @@ internal inline fun Buffer.commonSkip(byteCount: Long) { internal inline fun Buffer.commonWrite( byteString: ByteString, offset: Int = 0, - byteCount: Int = byteString.size + byteCount: Int = byteString.size, ): Buffer { byteString.write(this, offset, byteCount) return this @@ -444,7 +449,7 @@ internal inline fun Buffer.commonWriteDecimalLong(v: Long): Buffer { var v = v if (v == 0L) { // Both a shortcut and required since the following code can't handle zero. - return writeByte('0'.toInt()) + return writeByte('0'.code) } var negative = false @@ -458,33 +463,61 @@ internal inline fun Buffer.commonWriteDecimalLong(v: Long): Buffer { // Binary search for character width which favors matching lower numbers. var width = - if (v < 100000000L) - if (v < 10000L) - if (v < 100L) - if (v < 10L) 1 - else 2 - else if (v < 1000L) 3 - else 4 - else if (v < 1000000L) - if (v < 100000L) 5 - else 6 - else if (v < 10000000L) 7 - else 8 - else if (v < 1000000000000L) - if (v < 10000000000L) - if (v < 1000000000L) 9 - else 10 - else if (v < 100000000000L) 11 - else 12 - else if (v < 1000000000000000L) - if (v < 10000000000000L) 13 - else if (v < 100000000000000L) 14 - else 15 - else if (v < 100000000000000000L) - if (v < 10000000000000000L) 16 - else 17 - else if (v < 1000000000000000000L) 18 - else 19 + if (v < 100000000L) { + if (v < 10000L) { + if (v < 100L) { + if (v < 10L) { + 1 + } else { + 2 + } + } else if (v < 1000L) { + 3 + } else { + 4 + } + } else if (v < 1000000L) { + if (v < 100000L) { + 5 + } else { + 6 + } + } else if (v < 10000000L) { + 7 + } else { + 8 + } + } else if (v < 1000000000000L) { + if (v < 10000000000L) { + if (v < 1000000000L) { + 9 + } else { + 10 + } + } else if (v < 100000000000L) { + 11 + } else { + 12 + } + } else if (v < 1000000000000000L) { + if (v < 10000000000000L) { + 13 + } else if (v < 100000000000000L) { + 14 + } else { + 15 + } + } else if (v < 100000000000000000L) { + if (v < 10000000000000000L) { + 16 + } else { + 17 + } + } else if (v < 1000000000000000000L) { + 18 + } else { + 19 + } if (negative) { ++width } @@ -498,7 +531,7 @@ internal inline fun Buffer.commonWriteDecimalLong(v: Long): Buffer { v /= 10 } if (negative) { - data[--pos] = '-'.toByte() + data[--pos] = '-'.code.toByte() } tail.limit += width @@ -510,7 +543,7 @@ internal inline fun Buffer.commonWriteHexadecimalUnsignedLong(v: Long): Buffer { var v = v if (v == 0L) { // Both a shortcut and required since the following code can't handle zero. - return writeByte('0'.toInt()) + return writeByte('0'.code) } // Mask every bit below the most significant bit to a 1 @@ -572,7 +605,7 @@ internal inline fun Buffer.commonWrite(source: ByteArray) = write(source, 0, sou internal inline fun Buffer.commonWrite( source: ByteArray, offset: Int, - byteCount: Int + byteCount: Int, ): Buffer { var offset = offset checkOffsetAndCount(source.size.toLong(), offset.toLong(), byteCount.toLong()) @@ -586,7 +619,7 @@ internal inline fun Buffer.commonWrite( destination = tail.data, destinationOffset = tail.limit, startIndex = offset, - endIndex = offset + toCopy + endIndex = offset + toCopy, ) offset += toCopy @@ -625,7 +658,10 @@ internal inline fun Buffer.commonRead(sink: ByteArray, offset: Int, byteCount: I val s = head ?: return -1 val toCopy = minOf(byteCount, s.limit - s.pos) s.data.copyInto( - destination = sink, destinationOffset = offset, startIndex = s.pos, endIndex = s.pos + toCopy + destination = sink, + destinationOffset = offset, + startIndex = s.pos, + endIndex = s.pos + toCopy, ) s.pos += toCopy @@ -662,8 +698,8 @@ internal inline fun Buffer.commonReadDecimalLong(): Long { while (pos < limit) { val b = data[pos] - if (b >= '0'.toByte() && b <= '9'.toByte()) { - val digit = '0'.toByte() - b + if (b >= '0'.code.toByte() && b <= '9'.code.toByte()) { + val digit = '0'.code.toByte() - b // Detect when the digit would cause an overflow. if (value < OVERFLOW_ZONE || value == OVERFLOW_ZONE && digit < overflowDigit) { @@ -673,15 +709,10 @@ internal inline fun Buffer.commonReadDecimalLong(): Long { } value *= 10L value += digit.toLong() - } else if (b == '-'.toByte() && seen == 0) { + } else if (b == '-'.code.toByte() && seen == 0) { negative = true overflowDigit -= 1 } else { - if (seen == 0) { - throw NumberFormatException( - "Expected leading [0-9] or '-' character but was 0x${b.toHexString()}" - ) - } // Set a flag to stop iteration. We still need to run through segment updating below. done = true break @@ -699,6 +730,14 @@ internal inline fun Buffer.commonReadDecimalLong(): Long { } while (!done && head != null) size -= seen.toLong() + + val minimumSeen = if (negative) 2 else 1 + if (seen < minimumSeen) { + if (size == 0L) throw EOFException() + val expected = if (negative) "Expected a digit" else "Expected a digit or '-'" + throw NumberFormatException("$expected but was 0x${get(0).toHexString()}") + } + return if (negative) value else -value } @@ -720,16 +759,16 @@ internal inline fun Buffer.commonReadHexadecimalUnsignedLong(): Long { val digit: Int val b = data[pos] - if (b >= '0'.toByte() && b <= '9'.toByte()) { - digit = b - '0'.toByte() - } else if (b >= 'a'.toByte() && b <= 'f'.toByte()) { - digit = b - 'a'.toByte() + 10 - } else if (b >= 'A'.toByte() && b <= 'F'.toByte()) { - digit = b - 'A'.toByte() + 10 // We never write uppercase, but we support reading it. + if (b >= '0'.code.toByte() && b <= '9'.code.toByte()) { + digit = b - '0'.code.toByte() + } else if (b >= 'a'.code.toByte() && b <= 'f'.code.toByte()) { + digit = b - 'a'.code.toByte() + 10 + } else if (b >= 'A'.code.toByte() && b <= 'F'.code.toByte()) { + digit = b - 'A'.code.toByte() + 10 // We never write uppercase, but we support reading it. } else { if (seen == 0) { throw NumberFormatException( - "Expected leading [0-9a-fA-F] character but was 0x${b.toHexString()}" + "Expected leading [0-9a-fA-F] character but was 0x${b.toHexString()}", ) } // Set a flag to stop iteration. We still need to run through segment updating below. @@ -825,7 +864,7 @@ internal inline fun Buffer.commonReadUtf8(byteCount: Long): String { } internal inline fun Buffer.commonReadUtf8Line(): String? { - val newline = indexOf('\n'.toByte()) + val newline = indexOf('\n'.code.toByte()) return when { newline != -1L -> readUtf8Line(newline) @@ -837,11 +876,11 @@ internal inline fun Buffer.commonReadUtf8Line(): String? { internal inline fun Buffer.commonReadUtf8LineStrict(limit: Long): String { require(limit >= 0L) { "limit < 0: $limit" } val scanLength = if (limit == Long.MAX_VALUE) Long.MAX_VALUE else limit + 1L - val newline = indexOf('\n'.toByte(), 0L, scanLength) + val newline = indexOf('\n'.code.toByte(), 0L, scanLength) if (newline != -1L) return readUtf8Line(newline) if (scanLength < size && - this[scanLength - 1] == '\r'.toByte() && - this[scanLength] == '\n'.toByte() + this[scanLength - 1] == '\r'.code.toByte() && + this[scanLength] == '\n'.code.toByte() ) { return readUtf8Line(scanLength) // The line was 'limit' UTF-8 bytes followed by \r\n. } @@ -850,8 +889,8 @@ internal inline fun Buffer.commonReadUtf8LineStrict(limit: Long): String { throw EOFException( "\\n not found: limit=${minOf( size, - limit - )} content=${data.readByteString().hex()}${'…'}" + limit, + )} content=${data.readByteString().hex()}${'…'}", ) } @@ -938,7 +977,7 @@ internal inline fun Buffer.commonWriteUtf8(string: String, beginIndex: Int, endI // Transcode a UTF-16 Java String to UTF-8 bytes. var i = beginIndex while (i < endIndex) { - var c = string[i].toInt() + var c = string[i].code when { c < 0x80 -> { @@ -953,7 +992,7 @@ internal inline fun Buffer.commonWriteUtf8(string: String, beginIndex: Int, endI // Fast-path contiguous runs of ASCII characters. This is ugly, but yields a ~4x performance // improvement over independent calls to writeByte(). while (i < runLimit) { - c = string[i].toInt() + c = string[i].code if (c >= 0x80) break data[segmentOffset + i++] = c.toByte() // 0xxxxxxx } @@ -992,9 +1031,9 @@ internal inline fun Buffer.commonWriteUtf8(string: String, beginIndex: Int, endI // c is a surrogate. Make sure it is a high surrogate & that its successor is a low // surrogate. If not, the UTF-16 is invalid, in which case we emit a replacement // character. - val low = (if (i + 1 < endIndex) string[i + 1].toInt() else 0) + val low = (if (i + 1 < endIndex) string[i + 1].code else 0) if (c > 0xdbff || low !in 0xdc00..0xdfff) { - writeByte('?'.toInt()) + writeByte('?'.code) i++ } else { // UTF-16 high surrogate: 110110xxxxxxxxxx (10 bits) @@ -1039,7 +1078,7 @@ internal inline fun Buffer.commonWriteUtf8CodePoint(codePoint: Int): Buffer { } codePoint in 0xd800..0xdfff -> { // Emit a replacement character for a partial surrogate. - writeByte('?'.toInt()) + writeByte('?'.code) } codePoint < 0x10000 -> { // Emit a 16-bit code point with 3 bytes. @@ -1373,7 +1412,7 @@ internal inline fun Buffer.commonRangeEquals( offset: Long, bytes: ByteString, bytesOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean { if (offset < 0L || bytesOffset < 0 || @@ -1507,6 +1546,7 @@ internal inline fun Buffer.commonSnapshot(byteCount: Int): ByteString { } internal fun Buffer.commonReadUnsafe(unsafeCursor: UnsafeCursor): UnsafeCursor { + val unsafeCursor = resolveDefaultParameter(unsafeCursor) check(unsafeCursor.buffer == null) { "already attached to a buffer" } unsafeCursor.buffer = this @@ -1515,6 +1555,7 @@ internal fun Buffer.commonReadUnsafe(unsafeCursor: UnsafeCursor): UnsafeCursor { } internal fun Buffer.commonReadAndWriteUnsafe(unsafeCursor: UnsafeCursor): UnsafeCursor { + val unsafeCursor = resolveDefaultParameter(unsafeCursor) check(unsafeCursor.buffer == null) { "already attached to a buffer" } unsafeCursor.buffer = this diff --git a/okio/src/commonMain/kotlin/okio/internal/ByteString.kt b/okio/src/commonMain/kotlin/okio/internal/ByteString.kt index 7a1a488b..311c17e5 100644 --- a/okio/src/commonMain/kotlin/okio/internal/ByteString.kt +++ b/okio/src/commonMain/kotlin/okio/internal/ByteString.kt @@ -13,9 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:JvmName("-ByteString") // A leading '-' hides this class from Java. package okio.internal +import kotlin.jvm.JvmName +import kotlin.native.concurrent.SharedImmutable import okio.BASE64_URL_SAFE import okio.Buffer import okio.ByteString @@ -28,6 +31,7 @@ import okio.decodeBase64ToArray import okio.encodeBase64 import okio.isIsoControl import okio.processUtf8CodePoints +import okio.resolveDefaultParameter import okio.shr import okio.toUtf8String @@ -51,6 +55,7 @@ internal inline fun ByteString.commonBase64(): String = data.encodeBase64() @Suppress("NOTHING_TO_INLINE") internal inline fun ByteString.commonBase64Url() = data.encodeBase64(map = BASE64_URL_SAFE) +@SharedImmutable internal val HEX_DIGIT_CHARS = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') @@ -62,7 +67,7 @@ internal inline fun ByteString.commonHex(): String { result[c++] = HEX_DIGIT_CHARS[b shr 4 and 0xf] result[c++] = HEX_DIGIT_CHARS[b and 0xf] // ktlint-disable no-multi-spaces } - return String(result) + return result.concatToString() } @Suppress("NOTHING_TO_INLINE") @@ -71,7 +76,7 @@ internal inline fun ByteString.commonToAsciiLowercase(): ByteString { var i = 0 while (i < data.size) { var c = data[i] - if (c < 'A'.toByte() || c > 'Z'.toByte()) { + if (c < 'A'.code.toByte() || c > 'Z'.code.toByte()) { i++ continue } @@ -81,7 +86,7 @@ internal inline fun ByteString.commonToAsciiLowercase(): ByteString { lowercase[i++] = (c - ('A' - 'a')).toByte() while (i < lowercase.size) { c = lowercase[i] - if (c < 'A'.toByte() || c > 'Z'.toByte()) { + if (c < 'A'.code.toByte() || c > 'Z'.code.toByte()) { i++ continue } @@ -99,7 +104,7 @@ internal inline fun ByteString.commonToAsciiUppercase(): ByteString { var i = 0 while (i < data.size) { var c = data[i] - if (c < 'a'.toByte() || c > 'z'.toByte()) { + if (c < 'a'.code.toByte() || c > 'z'.code.toByte()) { i++ continue } @@ -109,7 +114,7 @@ internal inline fun ByteString.commonToAsciiUppercase(): ByteString { lowercase[i++] = (c - ('a' - 'A')).toByte() while (i < lowercase.size) { c = lowercase[i] - if (c < 'a'.toByte() || c > 'z'.toByte()) { + if (c < 'a'.code.toByte() || c > 'z'.code.toByte()) { i++ continue } @@ -123,6 +128,7 @@ internal inline fun ByteString.commonToAsciiUppercase(): ByteString { @Suppress("NOTHING_TO_INLINE") internal inline fun ByteString.commonSubstring(beginIndex: Int, endIndex: Int): ByteString { + val endIndex = resolveDefaultParameter(endIndex) require(beginIndex >= 0) { "beginIndex < 0" } require(endIndex <= data.size) { "endIndex > length(${data.size})" } @@ -152,7 +158,7 @@ internal inline fun ByteString.commonRangeEquals( offset: Int, other: ByteString, otherOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean = other.rangeEquals(otherOffset, this.data, offset, byteCount) @Suppress("NOTHING_TO_INLINE") @@ -160,7 +166,7 @@ internal inline fun ByteString.commonRangeEquals( offset: Int, other: ByteArray, otherOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean { return ( offset >= 0 && offset <= data.size - byteCount && @@ -170,6 +176,16 @@ internal inline fun ByteString.commonRangeEquals( } @Suppress("NOTHING_TO_INLINE") +internal inline fun ByteString.commonCopyInto( + offset: Int, + target: ByteArray, + targetOffset: Int, + byteCount: Int, +) { + data.copyInto(target, targetOffset, offset, offset + byteCount) +} + +@Suppress("NOTHING_TO_INLINE") internal inline fun ByteString.commonStartsWith(prefix: ByteString) = rangeEquals(0, prefix, 0, prefix.size) @@ -199,11 +215,12 @@ internal inline fun ByteString.commonIndexOf(other: ByteArray, fromIndex: Int): @Suppress("NOTHING_TO_INLINE") internal inline fun ByteString.commonLastIndexOf( other: ByteString, - fromIndex: Int + fromIndex: Int, ) = lastIndexOf(other.internalArray(), fromIndex) @Suppress("NOTHING_TO_INLINE") internal inline fun ByteString.commonLastIndexOf(other: ByteArray, fromIndex: Int): Int { + val fromIndex = resolveDefaultParameter(fromIndex) val limit = data.size - other.size for (i in minOf(fromIndex, limit) downTo 0) { if (arrayRangeEquals(data, i, other, 0, other.size)) { @@ -255,6 +272,7 @@ internal inline fun commonOf(data: ByteArray) = ByteString(data.copyOf()) @Suppress("NOTHING_TO_INLINE") internal inline fun ByteArray.commonToByteString(offset: Int, byteCount: Int): ByteString { + val byteCount = resolveDefaultParameter(byteCount) checkOffsetAndCount(size.toLong(), offset.toLong(), byteCount.toLong()) return ByteString(copyOfRange(offset, offset + byteCount)) } @@ -332,7 +350,7 @@ private fun codePointIndexToCharIndex(s: ByteArray, codePointCount: Int): Int { return charCount } - if ((c != '\n'.toInt() && c != '\r'.toInt() && isIsoControl(c)) || + if ((c != '\n'.code && c != '\r'.code && isIsoControl(c)) || c == REPLACEMENT_CODE_POINT ) { return -1 diff --git a/okio/src/commonMain/kotlin/okio/internal/FileSystem.kt b/okio/src/commonMain/kotlin/okio/internal/FileSystem.kt new file mode 100644 index 00000000..72c541d1 --- /dev/null +++ b/okio/src/commonMain/kotlin/okio/internal/FileSystem.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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. + */ +@file:JvmName("-FileSystem") // A leading '-' hides this class from Java. + +package okio.internal + +import kotlin.jvm.JvmName +import okio.FileMetadata +import okio.FileNotFoundException +import okio.FileSystem +import okio.IOException +import okio.Path +import okio.buffer +import okio.use + +/** + * Returns metadata of the file, directory, or object identified by [path]. + * + * @throws IOException if [path] does not exist or its metadata cannot be read. + */ +@Throws(IOException::class) +internal fun FileSystem.commonMetadata(path: Path): FileMetadata { + return metadataOrNull(path) ?: throw FileNotFoundException("no such file: $path") +} + +@Throws(IOException::class) +internal fun FileSystem.commonExists(path: Path): Boolean { + return metadataOrNull(path) != null +} + +@Throws(IOException::class) +internal fun FileSystem.commonCreateDirectories(dir: Path, mustCreate: Boolean) { + // Compute the sequence of directories to create. + val directories = ArrayDeque<Path>() + var path: Path? = dir + while (path != null && !exists(path)) { + directories.addFirst(path) + path = path.parent + } + + if (mustCreate && directories.isEmpty()) throw IOException("$dir already exists.") + + // Create them. + for (toCreate in directories) { + // We know we are creating new directories by now so we don't have to pass down `mustCreate`. + createDirectory(toCreate) + } +} + +@Throws(IOException::class) +internal fun FileSystem.commonCopy(source: Path, target: Path) { + source(source).use { bytesIn -> + sink(target).buffer().use { bytesOut -> + bytesOut.writeAll(bytesIn) + } + } +} + +@Throws(IOException::class) +internal fun FileSystem.commonDeleteRecursively(fileOrDirectory: Path, mustExist: Boolean) { + val sequence = sequence { + collectRecursively( + fileSystem = this@commonDeleteRecursively, + stack = ArrayDeque(), + path = fileOrDirectory, + followSymlinks = false, + postorder = true, + ) + } + val iterator = sequence.iterator() + while (iterator.hasNext()) { + val toDelete = iterator.next() + delete(toDelete, mustExist = mustExist && !iterator.hasNext()) + } +} + +@Throws(IOException::class) +internal fun FileSystem.commonListRecursively(dir: Path, followSymlinks: Boolean): Sequence<Path> { + return sequence { + val stack = ArrayDeque<Path>() + stack.addLast(dir) + for (child in list(dir)) { + collectRecursively( + fileSystem = this@commonListRecursively, + stack = stack, + path = child, + followSymlinks = followSymlinks, + postorder = false, + ) + } + } +} + +internal suspend fun SequenceScope<Path>.collectRecursively( + fileSystem: FileSystem, + stack: ArrayDeque<Path>, + path: Path, + followSymlinks: Boolean, + postorder: Boolean, +) { + // For listRecursively, visit enclosing directory first. + if (!postorder) { + yield(path) + } + + val children = fileSystem.listOrNull(path) ?: listOf() + if (children.isNotEmpty()) { + // Figure out if path is a symlink and detect symlink cycles. + var symlinkPath = path + var symlinkCount = 0 + while (true) { + if (followSymlinks && symlinkPath in stack) throw IOException("symlink cycle at $path") + symlinkPath = fileSystem.symlinkTarget(symlinkPath) ?: break + symlinkCount++ + } + + // Recursively visit children. + if (followSymlinks || symlinkCount == 0) { + stack.addLast(symlinkPath) + try { + for (child in children) { + collectRecursively(fileSystem, stack, child, followSymlinks, postorder) + } + } finally { + stack.removeLast() + } + } + } + + // For deleteRecursively, visit enclosing directory last. + if (postorder) { + yield(path) + } +} + +/** Returns a resolved path to the symlink target, resolving it if necessary. */ +@Throws(IOException::class) +internal fun FileSystem.symlinkTarget(path: Path): Path? { + val target = metadata(path).symlinkTarget ?: return null + return path.parent!!.div(target) +} diff --git a/okio/src/commonMain/kotlin/okio/internal/Path.kt b/okio/src/commonMain/kotlin/okio/internal/Path.kt new file mode 100644 index 00000000..6910e30b --- /dev/null +++ b/okio/src/commonMain/kotlin/okio/internal/Path.kt @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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. + */ +@file:JvmName("-Path") // A leading '-' hides this class from Java. + +package okio.internal + +import kotlin.jvm.JvmName +import kotlin.native.concurrent.SharedImmutable +import okio.Buffer +import okio.ByteString +import okio.ByteString.Companion.encodeUtf8 +import okio.Path +import okio.Path.Companion.toPath + +@SharedImmutable +private val SLASH = "/".encodeUtf8() + +@SharedImmutable +private val BACKSLASH = "\\".encodeUtf8() + +@SharedImmutable +private val ANY_SLASH = "/\\".encodeUtf8() + +@SharedImmutable +private val DOT = ".".encodeUtf8() + +@SharedImmutable +private val DOT_DOT = "..".encodeUtf8() + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonRoot(): Path? { + return when (val rootLength = rootLength()) { + -1 -> null + else -> Path(bytes.substring(0, rootLength)) + } +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonSegments(): List<String> { + return commonSegmentsBytes().map { it.utf8() } +} + +/** This function skips the root then splits on slash. */ +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonSegmentsBytes(): List<ByteString> { + val result = mutableListOf<ByteString>() + var segmentStart = rootLength() + + // segmentStart should always follow a `\`, but for UNC paths it doesn't. + if (segmentStart == -1) { + segmentStart = 0 + } else if (segmentStart < bytes.size && bytes[segmentStart] == '\\'.code.toByte()) { + segmentStart++ + } + + for (i in segmentStart until bytes.size) { + if (bytes[i] == '/'.code.toByte() || bytes[i] == '\\'.code.toByte()) { + result += bytes.substring(segmentStart, i) + segmentStart = i + 1 + } + } + + if (segmentStart < bytes.size) { + result += bytes.substring(segmentStart, bytes.size) + } + + return result +} + +/** Return the length of the prefix of this that is the root path, or -1 if it has no root. */ +private fun Path.rootLength(): Int { + if (bytes.size == 0) return -1 + if (bytes[0] == '/'.code.toByte()) return 1 + + if (bytes[0] == '\\'.code.toByte()) { + if (bytes.size > 2 && bytes[1] == '\\'.code.toByte()) { + // Look for a root like `\\localhost`. + var uncRootEnd = bytes.indexOf(BACKSLASH, fromIndex = 2) + if (uncRootEnd == -1) uncRootEnd = bytes.size + return uncRootEnd + } + + // We found a root like `\`. + return 1 + } + + // Look for a root like `C:\`. + if (bytes.size > 2 && bytes[1] == ':'.code.toByte() && bytes[2] == '\\'.code.toByte()) { + val c = bytes[0].toInt().toChar() + if (c !in 'a'..'z' && c !in 'A'..'Z') return -1 + return 3 + } + + return -1 +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonIsAbsolute(): Boolean { + return rootLength() != -1 +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonIsRelative(): Boolean { + return rootLength() == -1 +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonVolumeLetter(): Char? { + if (bytes.indexOf(SLASH) != -1) return null + if (bytes.size < 2) return null + if (bytes[1] != ':'.code.toByte()) return null + val c = bytes[0].toInt().toChar() + if (c !in 'a'..'z' && c !in 'A'..'Z') return null + return c +} + +private val Path.indexOfLastSlash: Int + get() { + val lastSlash = bytes.lastIndexOf(SLASH) + if (lastSlash != -1) return lastSlash + return bytes.lastIndexOf(BACKSLASH) + } + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonNameBytes(): ByteString { + val lastSlash = indexOfLastSlash + return when { + lastSlash != -1 -> bytes.substring(lastSlash + 1) + volumeLetter != null && bytes.size == 2 -> ByteString.EMPTY // "C:" has no name. + else -> bytes + } +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonName(): String { + return nameBytes.utf8() +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonParent(): Path? { + if (bytes == DOT || bytes == SLASH || bytes == BACKSLASH || lastSegmentIsDotDot()) { + return null // Terminal path. + } + + val lastSlash = indexOfLastSlash + when { + lastSlash == 2 && volumeLetter != null -> { + if (bytes.size == 3) return null // "C:\" has no parent. + return Path(bytes.substring(endIndex = 3)) // Keep the trailing '\' in C:\. + } + lastSlash == 1 && bytes.startsWith(BACKSLASH) -> { + return null // "\\server" is a UNC path with no parent. + } + lastSlash == -1 && volumeLetter != null -> { + if (bytes.size == 2) return null // "C:" has no parent. + return Path(bytes.substring(endIndex = 2)) // C: is volume-relative. + } + lastSlash == -1 -> { + return Path(DOT) // Parent is the current working directory. + } + lastSlash == 0 -> { + return Path(bytes.substring(endIndex = 1)) // Parent is the filesystem root '/'. + } + else -> { + return Path(bytes.substring(endIndex = lastSlash)) + } + } +} + +private fun Path.lastSegmentIsDotDot(): Boolean { + if (bytes.endsWith(DOT_DOT)) { + if (bytes.size == 2) return true // ".." is the whole string. + if (bytes.rangeEquals(bytes.size - 3, SLASH, 0, 1)) return true // Ends with "/..". + if (bytes.rangeEquals(bytes.size - 3, BACKSLASH, 0, 1)) return true // Ends with "\..". + } + return false +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonIsRoot(): Boolean { + return rootLength() == bytes.size +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonResolve(child: String, normalize: Boolean): Path { + return commonResolve(Buffer().writeUtf8(child), normalize = normalize) +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonResolve(child: ByteString, normalize: Boolean): Path { + return commonResolve(Buffer().write(child), normalize = normalize) +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonResolve(child: Buffer, normalize: Boolean): Path { + return commonResolve(child.toPath(normalize = false), normalize = normalize) +} + +internal fun Path.commonResolve(child: Path, normalize: Boolean): Path { + if (child.isAbsolute || child.volumeLetter != null) return child + + val slash = slash ?: child.slash ?: Path.DIRECTORY_SEPARATOR.toSlash() + + val buffer = Buffer() + buffer.write(bytes) + if (buffer.size > 0) { + buffer.write(slash) + } + buffer.write(child.bytes) + return buffer.toPath(normalize = normalize) +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonRelativeTo(other: Path): Path { + require(root == other.root) { + "Paths of different roots cannot be relative to each other: $this and $other" + } + + val thisSegments = this.segmentsBytes + val otherSegments = other.segmentsBytes + + // We look at the path both have in common. + var firstNewSegmentIndex = 0 + val minSegmentsSize = minOf(thisSegments.size, otherSegments.size) + while (firstNewSegmentIndex < minSegmentsSize && + thisSegments[firstNewSegmentIndex] == otherSegments[firstNewSegmentIndex] + ) { + firstNewSegmentIndex++ + } + + if (firstNewSegmentIndex == minSegmentsSize && bytes.size == other.bytes.size) { + // `this` and `other` are the same path. + return ".".toPath() + } + + require(otherSegments.subList(firstNewSegmentIndex, otherSegments.size).indexOf(DOT_DOT) == -1) { + "Impossible relative path to resolve: $this and $other" + } + + val buffer = Buffer() + val slash = other.slash ?: slash ?: Path.DIRECTORY_SEPARATOR.toSlash() + for (i in firstNewSegmentIndex until otherSegments.size) { + buffer.write(DOT_DOT) + buffer.write(slash) + } + for (i in firstNewSegmentIndex until thisSegments.size) { + buffer.write(thisSegments[i]) + buffer.write(slash) + } + return buffer.toPath(normalize = false) +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonNormalized(): Path { + return toString().toPath(normalize = true) +} + +private val Path.slash: ByteString? + get() { + return when { + bytes.indexOf(SLASH) != -1 -> SLASH + bytes.indexOf(BACKSLASH) != -1 -> BACKSLASH + else -> null + } + } + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonCompareTo(other: Path): Int { + return bytes.compareTo(other.bytes) +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonEquals(other: Any?): Boolean { + return other is Path && other.bytes == bytes +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonHashCode(): Int { + return bytes.hashCode() +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Path.commonToString(): String { + return bytes.utf8() +} + +internal fun String.commonToPath(normalize: Boolean): Path { + return Buffer().writeUtf8(this).toPath(normalize) +} + +/** Consume the buffer and return it as a path. */ +internal fun Buffer.toPath(normalize: Boolean): Path { + var slash: ByteString? = null + val result = Buffer() + + // Consume the absolute path prefix, like `/`, `\\`, `C:`, or `C:\` and write the + // canonicalized prefix to result. + var leadingSlashCount = 0 + while (rangeEquals(0L, SLASH) || rangeEquals(0L, BACKSLASH)) { + val byte = readByte() + slash = slash ?: byte.toSlash() + leadingSlashCount++ + } + val windowsUncPath = leadingSlashCount >= 2 && slash == BACKSLASH + if (windowsUncPath) { + // This is a Windows UNC path, like \\server\directory\file.txt. + result.write(slash!!) + result.write(slash) + } else if (leadingSlashCount > 0) { + // This is platform-dependent: + // * On UNIX: an absolute path like /home + // * On Windows: this is relative to the current volume, like \Windows. + result.write(slash!!) + } else { + // This path doesn't start with any slash. We must initialize the slash character to use. + val limit = indexOfElement(ANY_SLASH) + slash = slash ?: when (limit) { + -1L -> Path.DIRECTORY_SEPARATOR.toSlash() + else -> get(limit).toSlash() + } + if (startsWithVolumeLetterAndColon(slash)) { + if (limit == 2L) { + result.write(this, 3L) // Absolute on a named volume, like `C:\`. + } else { + result.write(this, 2L) // Relative to the named volume, like `C:`. + } + } + } + + val absolute = result.size > 0 + + val canonicalParts = mutableListOf<ByteString>() + while (!exhausted()) { + val limit = indexOfElement(ANY_SLASH) + + val part: ByteString + if (limit == -1L) { + part = readByteString() + } else { + part = readByteString(limit) + readByte() + } + + if (part == DOT_DOT) { + if (absolute && canonicalParts.isEmpty()) { + // Silently consume `..`. + } else if (!normalize || !absolute && (canonicalParts.isEmpty() || canonicalParts.last() == DOT_DOT)) { + canonicalParts.add(part) // '..' doesn't pop '..' for relative paths. + } else if (windowsUncPath && canonicalParts.size == 1) { + // `..` doesn't pop UNC hostnames. + } else { + canonicalParts.removeLastOrNull() + } + } else if (part != DOT && part != ByteString.EMPTY) { + canonicalParts.add(part) + } + } + + for (i in 0 until canonicalParts.size) { + if (i > 0) result.write(slash) + result.write(canonicalParts[i]) + } + if (result.size == 0L) { + result.write(DOT) + } + + return Path(result.readByteString()) +} + +private fun String.toSlash(): ByteString { + return when (this) { + "/" -> SLASH + "\\" -> BACKSLASH + else -> throw IllegalArgumentException("not a directory separator: $this") + } +} + +private fun Byte.toSlash(): ByteString { + return when (toInt()) { + '/'.code -> SLASH + '\\'.code -> BACKSLASH + else -> throw IllegalArgumentException("not a directory separator: $this") + } +} + +private fun Buffer.startsWithVolumeLetterAndColon(slash: ByteString): Boolean { + if (slash != BACKSLASH) return false + if (size < 2) return false + if (get(1) != ':'.code.toByte()) return false + val b = get(0).toInt().toChar() + return b in 'a'..'z' || b in 'A'..'Z' +} diff --git a/okio/src/commonMain/kotlin/okio/internal/RealBufferedSink.kt b/okio/src/commonMain/kotlin/okio/internal/RealBufferedSink.kt index 49b0c4d7..d03216ee 100644 --- a/okio/src/commonMain/kotlin/okio/internal/RealBufferedSink.kt +++ b/okio/src/commonMain/kotlin/okio/internal/RealBufferedSink.kt @@ -17,8 +17,11 @@ // TODO move to RealBufferedSink class: https://youtrack.jetbrains.com/issue/KT-20427 @file:Suppress("NOTHING_TO_INLINE") +@file:JvmName("-RealBufferedSink") // A leading '-' hides this class from Java. + package okio.internal +import kotlin.jvm.JvmName import okio.Buffer import okio.BufferedSink import okio.ByteString @@ -42,7 +45,7 @@ internal inline fun RealBufferedSink.commonWrite(byteString: ByteString): Buffer internal inline fun RealBufferedSink.commonWrite( byteString: ByteString, offset: Int, - byteCount: Int + byteCount: Int, ): BufferedSink { check(!closed) { "closed" } buffer.write(byteString, offset, byteCount) @@ -58,7 +61,7 @@ internal inline fun RealBufferedSink.commonWriteUtf8(string: String): BufferedSi internal inline fun RealBufferedSink.commonWriteUtf8( string: String, beginIndex: Int, - endIndex: Int + endIndex: Int, ): BufferedSink { check(!closed) { "closed" } buffer.writeUtf8(string, beginIndex, endIndex) @@ -80,7 +83,7 @@ internal inline fun RealBufferedSink.commonWrite(source: ByteArray): BufferedSin internal inline fun RealBufferedSink.commonWrite( source: ByteArray, offset: Int, - byteCount: Int + byteCount: Int, ): BufferedSink { check(!closed) { "closed" } buffer.write(source, offset, byteCount) diff --git a/okio/src/commonMain/kotlin/okio/internal/RealBufferedSource.kt b/okio/src/commonMain/kotlin/okio/internal/RealBufferedSource.kt index 4b901437..5b0d55b2 100644 --- a/okio/src/commonMain/kotlin/okio/internal/RealBufferedSource.kt +++ b/okio/src/commonMain/kotlin/okio/internal/RealBufferedSource.kt @@ -17,8 +17,11 @@ // TODO move to RealBufferedSource class: https://youtrack.jetbrains.com/issue/KT-20427 @file:Suppress("NOTHING_TO_INLINE") +@file:JvmName("-RealBufferedSource") // A leading '-' hides this class from Java. + package okio.internal +import kotlin.jvm.JvmName import okio.Buffer import okio.BufferedSource import okio.ByteString @@ -178,7 +181,7 @@ internal inline fun RealBufferedSource.commonReadUtf8(byteCount: Long): String { } internal inline fun RealBufferedSource.commonReadUtf8Line(): String? { - val newline = indexOf('\n'.toByte()) + val newline = indexOf('\n'.code.toByte()) return if (newline == -1L) { if (buffer.size != 0L) { @@ -194,11 +197,11 @@ internal inline fun RealBufferedSource.commonReadUtf8Line(): String? { internal inline fun RealBufferedSource.commonReadUtf8LineStrict(limit: Long): String { require(limit >= 0) { "limit < 0: $limit" } val scanLength = if (limit == Long.MAX_VALUE) Long.MAX_VALUE else limit + 1 - val newline = indexOf('\n'.toByte(), 0, scanLength) + val newline = indexOf('\n'.code.toByte(), 0, scanLength) if (newline != -1L) return buffer.readUtf8Line(newline) if (scanLength < Long.MAX_VALUE && - request(scanLength) && buffer[scanLength - 1] == '\r'.toByte() && - request(scanLength + 1) && buffer[scanLength] == '\n'.toByte() + request(scanLength) && buffer[scanLength - 1] == '\r'.code.toByte() && + request(scanLength + 1) && buffer[scanLength] == '\n'.code.toByte() ) { return buffer.readUtf8Line(scanLength) // The line was 'limit' UTF-8 bytes followed by \r\n. } @@ -206,7 +209,7 @@ internal inline fun RealBufferedSource.commonReadUtf8LineStrict(limit: Long): St buffer.copyTo(data, 0, okio.minOf(32, buffer.size)) throw EOFException( "\\n not found: limit=" + minOf(buffer.size, limit) + - " content=" + data.readByteString().hex() + '…'.toString() + " content=" + data.readByteString().hex() + '…'.toString(), ) } @@ -259,10 +262,10 @@ internal inline fun RealBufferedSource.commonReadDecimalLong(): Long { var pos = 0L while (request(pos + 1)) { val b = buffer[pos] - if ((b < '0'.toByte() || b > '9'.toByte()) && (pos != 0L || b != '-'.toByte())) { + if ((b < '0'.code.toByte() || b > '9'.code.toByte()) && (pos != 0L || b != '-'.code.toByte())) { // Non-digit, or non-leading negative sign. if (pos == 0L) { - throw NumberFormatException("Expected leading [0-9] or '-' character but was 0x${b.toString(16)}") + throw NumberFormatException("Expected a digit or '-' but was 0x${b.toString(16)}") } break } @@ -278,9 +281,9 @@ internal inline fun RealBufferedSource.commonReadHexadecimalUnsignedLong(): Long var pos = 0 while (request((pos + 1).toLong())) { val b = buffer[pos.toLong()] - if ((b < '0'.toByte() || b > '9'.toByte()) && - (b < 'a'.toByte() || b > 'f'.toByte()) && - (b < 'A'.toByte() || b > 'F'.toByte()) + if ((b < '0'.code.toByte() || b > '9'.code.toByte()) && + (b < 'a'.code.toByte() || b > 'f'.code.toByte()) && + (b < 'A'.code.toByte() || b > 'F'.code.toByte()) ) { // Non-digit, or non-leading negative sign. if (pos == 0) { @@ -363,7 +366,7 @@ internal inline fun RealBufferedSource.commonRangeEquals( offset: Long, bytes: ByteString, bytesOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean { check(!closed) { "closed" } diff --git a/okio/src/commonMain/kotlin/okio/internal/SegmentedByteString.kt b/okio/src/commonMain/kotlin/okio/internal/SegmentedByteString.kt index f46e1389..dc3edd5b 100644 --- a/okio/src/commonMain/kotlin/okio/internal/SegmentedByteString.kt +++ b/okio/src/commonMain/kotlin/okio/internal/SegmentedByteString.kt @@ -17,14 +17,18 @@ // TODO move to SegmentedByteString class: https://youtrack.jetbrains.com/issue/KT-20427 @file:Suppress("NOTHING_TO_INLINE") +@file:JvmName("-SegmentedByteString") // A leading '-' hides this class from Java. + package okio.internal +import kotlin.jvm.JvmName import okio.Buffer import okio.ByteString import okio.Segment import okio.SegmentedByteString import okio.arrayRangeEquals import okio.checkOffsetAndCount +import okio.resolveDefaultParameter internal fun IntArray.binarySearch(value: Int, fromIndex: Int, toIndex: Int): Int { var left = fromIndex @@ -54,7 +58,7 @@ internal fun SegmentedByteString.segment(pos: Int): Int { /** Processes all segments, invoking `action` with the ByteArray and range of valid data. */ internal inline fun SegmentedByteString.forEachSegment( - action: (data: ByteArray, offset: Int, byteCount: Int) -> Unit + action: (data: ByteArray, offset: Int, byteCount: Int) -> Unit, ) { val segmentCount = segments.size var s = 0 @@ -76,7 +80,7 @@ internal inline fun SegmentedByteString.forEachSegment( private inline fun SegmentedByteString.forEachSegment( beginIndex: Int, endIndex: Int, - action: (data: ByteArray, offset: Int, byteCount: Int) -> Unit + action: (data: ByteArray, offset: Int, byteCount: Int) -> Unit, ) { var s = segment(beginIndex) var pos = beginIndex @@ -97,6 +101,7 @@ private inline fun SegmentedByteString.forEachSegment( // have to call these functions. Remove all this nonsense when expect class allow actual code. internal inline fun SegmentedByteString.commonSubstring(beginIndex: Int, endIndex: Int): ByteString { + val endIndex = resolveDefaultParameter(endIndex) require(beginIndex >= 0) { "beginIndex=$beginIndex < 0" } require(endIndex <= size) { "endIndex=$endIndex > length($size)" } @@ -141,8 +146,10 @@ internal inline fun SegmentedByteString.commonToByteArray(): ByteArray { var resultPos = 0 forEachSegment { data, offset, byteCount -> data.copyInto( - result, destinationOffset = resultPos, startIndex = offset, - endIndex = offset + byteCount + result, + destinationOffset = resultPos, + startIndex = offset, + endIndex = offset + byteCount, ) resultPos += byteCount } @@ -167,7 +174,7 @@ internal inline fun SegmentedByteString.commonRangeEquals( offset: Int, other: ByteString, otherOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean { if (offset < 0 || offset > size - byteCount) return false // Go segment-by-segment through this, passing arrays to other's rangeEquals(). @@ -183,7 +190,7 @@ internal inline fun SegmentedByteString.commonRangeEquals( offset: Int, other: ByteArray, otherOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean { if (offset < 0 || offset > size - byteCount || otherOffset < 0 || otherOffset > other.size - byteCount @@ -199,6 +206,22 @@ internal inline fun SegmentedByteString.commonRangeEquals( return true } +internal inline fun SegmentedByteString.commonCopyInto( + offset: Int, + target: ByteArray, + targetOffset: Int, + byteCount: Int, +) { + checkOffsetAndCount(size.toLong(), offset.toLong(), byteCount.toLong()) + checkOffsetAndCount(target.size.toLong(), targetOffset.toLong(), byteCount.toLong()) + // Go segment-by-segment through this, copying ranges of arrays. + var targetOffset = targetOffset + forEachSegment(offset, offset + byteCount) { data, offset, byteCount -> + data.copyInto(target, targetOffset, offset, offset + byteCount) + targetOffset += byteCount + } +} + internal inline fun SegmentedByteString.commonEquals(other: Any?): Boolean { return when { other === this -> true diff --git a/okio/src/commonTest/kotlin/okio/AbstractBufferedSinkTest.kt b/okio/src/commonTest/kotlin/okio/AbstractBufferedSinkTest.kt index 49bff8d9..e922d5f2 100644 --- a/okio/src/commonTest/kotlin/okio/AbstractBufferedSinkTest.kt +++ b/okio/src/commonTest/kotlin/okio/AbstractBufferedSinkTest.kt @@ -16,18 +16,17 @@ package okio -import okio.ByteString.Companion.decodeHex -import okio.ByteString.Companion.encodeUtf8 -import kotlin.math.pow import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.encodeUtf8 class BufferSinkTest : AbstractBufferedSinkTest(BufferedSinkFactory.BUFFER) class RealBufferedSinkTest : AbstractBufferedSinkTest(BufferedSinkFactory.REAL_BUFFERED_SINK) abstract class AbstractBufferedSinkTest internal constructor( - factory: BufferedSinkFactory + factory: BufferedSinkFactory, ) { private val data: Buffer = Buffer() private val sink: BufferedSink = factory.create(data) @@ -217,26 +216,60 @@ abstract class AbstractBufferedSinkTest internal constructor( } @Test fun closeEmitsBufferedBytes() { - sink.writeByte('a'.toInt()) + sink.writeByte('a'.code) sink.close() - assertEquals('a', data.readByte().toChar()) + assertEquals('a', data.readByte().toInt().toChar()) } + /** + * This test hard codes the results of Long.toString() because that function rounds large values + * when using Kotlin/JS IR. https://youtrack.jetbrains.com/issue/KT-39891 + */ @Test fun longDecimalString() { - assertLongDecimalString(0) - assertLongDecimalString(Long.MIN_VALUE) - assertLongDecimalString(Long.MAX_VALUE) - - for (i in 1..19) { - val value = 10.0.pow(i).toLong() - assertLongDecimalString(value - 1) - assertLongDecimalString(value) - } + assertLongDecimalString("0", 0) + assertLongDecimalString("-9223372036854775808", Long.MIN_VALUE) + assertLongDecimalString("9223372036854775807", Long.MAX_VALUE) + assertLongDecimalString("9", 9L) + assertLongDecimalString("99", 99L) + assertLongDecimalString("999", 999L) + assertLongDecimalString("9999", 9999L) + assertLongDecimalString("99999", 99999L) + assertLongDecimalString("999999", 999999L) + assertLongDecimalString("9999999", 9999999L) + assertLongDecimalString("99999999", 99999999L) + assertLongDecimalString("999999999", 999999999L) + assertLongDecimalString("9999999999", 9999999999L) + assertLongDecimalString("99999999999", 99999999999L) + assertLongDecimalString("999999999999", 999999999999L) + assertLongDecimalString("9999999999999", 9999999999999L) + assertLongDecimalString("99999999999999", 99999999999999L) + assertLongDecimalString("999999999999999", 999999999999999L) + assertLongDecimalString("9999999999999999", 9999999999999999L) + assertLongDecimalString("99999999999999999", 99999999999999999L) + assertLongDecimalString("999999999999999999", 999999999999999999L) + assertLongDecimalString("10", 10L) + assertLongDecimalString("100", 100L) + assertLongDecimalString("1000", 1000L) + assertLongDecimalString("10000", 10000L) + assertLongDecimalString("100000", 100000L) + assertLongDecimalString("1000000", 1000000L) + assertLongDecimalString("10000000", 10000000L) + assertLongDecimalString("100000000", 100000000L) + assertLongDecimalString("1000000000", 1000000000L) + assertLongDecimalString("10000000000", 10000000000L) + assertLongDecimalString("100000000000", 100000000000L) + assertLongDecimalString("1000000000000", 1000000000000L) + assertLongDecimalString("10000000000000", 10000000000000L) + assertLongDecimalString("100000000000000", 100000000000000L) + assertLongDecimalString("1000000000000000", 1000000000000000L) + assertLongDecimalString("10000000000000000", 10000000000000000L) + assertLongDecimalString("100000000000000000", 100000000000000000L) + assertLongDecimalString("1000000000000000000", 1000000000000000000L) } - private fun assertLongDecimalString(value: Long) { + private fun assertLongDecimalString(string: String, value: Long) { sink.writeDecimalLong(value).writeUtf8("zzz").flush() - val expected = "${value}zzz" + val expected = "${string}zzz" val actual = data.readUtf8() assertEquals(expected, actual, "$value expected $expected but was $actual") } diff --git a/okio/src/commonTest/kotlin/okio/AbstractBufferedSourceTest.kt b/okio/src/commonTest/kotlin/okio/AbstractBufferedSourceTest.kt index b15d369c..70d76cb6 100644 --- a/okio/src/commonTest/kotlin/okio/AbstractBufferedSourceTest.kt +++ b/okio/src/commonTest/kotlin/okio/AbstractBufferedSourceTest.kt @@ -16,14 +16,13 @@ package okio -import okio.ByteString.Companion.decodeHex -import okio.ByteString.Companion.encodeUtf8 import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue -import kotlin.test.fail +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.encodeUtf8 class BufferSourceTest : AbstractBufferedSourceTest(BufferedSourceFactory.BUFFER) class RealBufferedSourceTest : AbstractBufferedSourceTest(BufferedSourceFactory.REAL_BUFFERED_SOURCE) @@ -33,7 +32,7 @@ class PeekBufferTest : AbstractBufferedSourceTest(BufferedSourceFactory.PEEK_BUF class PeekBufferedSourceTest : AbstractBufferedSourceTest(BufferedSourceFactory.PEEK_BUFFERED_SOURCE) abstract class AbstractBufferedSourceTest internal constructor( - private val factory: BufferedSourceFactory + private val factory: BufferedSourceFactory, ) { private val sink: BufferedSink private val source: BufferedSource @@ -111,8 +110,8 @@ abstract class AbstractBufferedSourceTest internal constructor( 0x87.toByte(), 0x65.toByte(), 0x43.toByte(), - 0x21.toByte() - ) + 0x21.toByte(), + ), ) sink.emit() assertEquals(-0x543210ff, source.readInt().toLong()) @@ -130,8 +129,8 @@ abstract class AbstractBufferedSourceTest internal constructor( 0x87.toByte(), 0x65.toByte(), 0x43.toByte(), - 0x21.toByte() - ) + 0x21.toByte(), + ), ) sink.emit() assertEquals(0x10efcdab, source.readIntLe().toLong()) @@ -184,8 +183,8 @@ abstract class AbstractBufferedSourceTest internal constructor( 0x12.toByte(), 0x23.toByte(), 0x34.toByte(), - 0x45.toByte() - ) + 0x45.toByte(), + ), ) sink.emit() assertEquals(-0x543210ef789abcdfL, source.readLong()) @@ -211,8 +210,8 @@ abstract class AbstractBufferedSourceTest internal constructor( 0x12.toByte(), 0x23.toByte(), 0x34.toByte(), - 0x45.toByte() - ) + 0x45.toByte(), + ), ) sink.emit() assertEquals(0x2143658710efcdabL, source.readLongLe()) @@ -231,8 +230,8 @@ abstract class AbstractBufferedSourceTest internal constructor( 0x87.toByte(), 0x65.toByte(), 0x43.toByte(), - 0x21.toByte() - ) + 0x21.toByte(), + ), ) sink.emit() source.skip((Segment.SIZE - 7).toLong()) @@ -341,14 +340,14 @@ abstract class AbstractBufferedSourceTest internal constructor( // Verify we read all that we could from the source. assertArrayEquals( byteArrayOf( - 'H'.toByte(), - 'e'.toByte(), - 'l'.toByte(), - 'l'.toByte(), - 'o'.toByte(), - 0 + 'H'.code.toByte(), + 'e'.code.toByte(), + 'l'.code.toByte(), + 'l'.code.toByte(), + 'o'.code.toByte(), + 0, ), - array + array, ) } @@ -360,11 +359,11 @@ abstract class AbstractBufferedSourceTest internal constructor( val read = source.read(sink) if (factory.isOneByteAtATime) { assertEquals(1, read.toLong()) - val expected = byteArrayOf('a'.toByte(), 0, 0) + val expected = byteArrayOf('a'.code.toByte(), 0, 0) assertArrayEquals(expected, sink) } else { assertEquals(3, read.toLong()) - val expected = byteArrayOf('a'.toByte(), 'b'.toByte(), 'c'.toByte()) + val expected = byteArrayOf('a'.code.toByte(), 'b'.code.toByte(), 'c'.code.toByte()) assertArrayEquals(expected, sink) } } @@ -377,11 +376,12 @@ abstract class AbstractBufferedSourceTest internal constructor( val read = source.read(sink) if (factory.isOneByteAtATime) { assertEquals(1, read.toLong()) - val expected = byteArrayOf('a'.toByte(), 0, 0, 0, 0) + val expected = byteArrayOf('a'.code.toByte(), 0, 0, 0, 0) assertArrayEquals(expected, sink) } else { assertEquals(4, read.toLong()) - val expected = byteArrayOf('a'.toByte(), 'b'.toByte(), 'c'.toByte(), 'd'.toByte(), 0) + val expected = + byteArrayOf('a'.code.toByte(), 'b'.code.toByte(), 'c'.code.toByte(), 'd'.code.toByte(), 0) assertArrayEquals(expected, sink) } } @@ -394,11 +394,12 @@ abstract class AbstractBufferedSourceTest internal constructor( val read = source.read(sink, 2, 3) if (factory.isOneByteAtATime) { assertEquals(1, read.toLong()) - val expected = byteArrayOf(0, 0, 'a'.toByte(), 0, 0, 0, 0) + val expected = byteArrayOf(0, 0, 'a'.code.toByte(), 0, 0, 0, 0) assertArrayEquals(expected, sink) } else { assertEquals(3, read.toLong()) - val expected = byteArrayOf(0, 0, 'a'.toByte(), 'b'.toByte(), 'c'.toByte(), 0, 0) + val expected = + byteArrayOf(0, 0, 'a'.code.toByte(), 'b'.code.toByte(), 'c'.code.toByte(), 0, 0) assertArrayEquals(expected, sink) } } @@ -491,9 +492,9 @@ abstract class AbstractBufferedSourceTest internal constructor( sink.writeUtf8("c") sink.emit() source.skip(1) - assertEquals('b'.toLong(), (source.readByte() and 0xff).toLong()) + assertEquals('b'.code.toLong(), (source.readByte() and 0xff).toLong()) source.skip((Segment.SIZE - 2).toLong()) - assertEquals('b'.toLong(), (source.readByte() and 0xff).toLong()) + assertEquals('b'.code.toLong(), (source.readByte() and 0xff).toLong()) source.skip(1) assertTrue(source.exhausted()) } @@ -501,7 +502,6 @@ abstract class AbstractBufferedSourceTest internal constructor( @Test fun skipInsufficientData() { sink.writeUtf8("a") sink.emit() - assertFailsWith<EOFException> { source.skip(2) } @@ -509,61 +509,61 @@ abstract class AbstractBufferedSourceTest internal constructor( @Test fun indexOf() { // The segment is empty. - assertEquals(-1, source.indexOf('a'.toByte())) + assertEquals(-1, source.indexOf('a'.code.toByte())) // The segment has one value. sink.writeUtf8("a") // a sink.emit() - assertEquals(0, source.indexOf('a'.toByte())) - assertEquals(-1, source.indexOf('b'.toByte())) + assertEquals(0, source.indexOf('a'.code.toByte())) + assertEquals(-1, source.indexOf('b'.code.toByte())) // The segment has lots of data. sink.writeUtf8("b".repeat(Segment.SIZE - 2)) // ab...b sink.emit() - assertEquals(0, source.indexOf('a'.toByte())) - assertEquals(1, source.indexOf('b'.toByte())) - assertEquals(-1, source.indexOf('c'.toByte())) + assertEquals(0, source.indexOf('a'.code.toByte())) + assertEquals(1, source.indexOf('b'.code.toByte())) + assertEquals(-1, source.indexOf('c'.code.toByte())) // The segment doesn't start at 0, it starts at 2. source.skip(2) // b...b - assertEquals(-1, source.indexOf('a'.toByte())) - assertEquals(0, source.indexOf('b'.toByte())) - assertEquals(-1, source.indexOf('c'.toByte())) + assertEquals(-1, source.indexOf('a'.code.toByte())) + assertEquals(0, source.indexOf('b'.code.toByte())) + assertEquals(-1, source.indexOf('c'.code.toByte())) // The segment is full. sink.writeUtf8("c") // b...bc sink.emit() - assertEquals(-1, source.indexOf('a'.toByte())) - assertEquals(0, source.indexOf('b'.toByte())) - assertEquals((Segment.SIZE - 3).toLong(), source.indexOf('c'.toByte())) + assertEquals(-1, source.indexOf('a'.code.toByte())) + assertEquals(0, source.indexOf('b'.code.toByte())) + assertEquals((Segment.SIZE - 3).toLong(), source.indexOf('c'.code.toByte())) // The segment doesn't start at 2, it starts at 4. source.skip(2) // b...bc - assertEquals(-1, source.indexOf('a'.toByte())) - assertEquals(0, source.indexOf('b'.toByte())) - assertEquals((Segment.SIZE - 5).toLong(), source.indexOf('c'.toByte())) + assertEquals(-1, source.indexOf('a'.code.toByte())) + assertEquals(0, source.indexOf('b'.code.toByte())) + assertEquals((Segment.SIZE - 5).toLong(), source.indexOf('c'.code.toByte())) // Two segments. sink.writeUtf8("d") // b...bcd, d is in the 2nd segment. sink.emit() - assertEquals((Segment.SIZE - 4).toLong(), source.indexOf('d'.toByte())) - assertEquals(-1, source.indexOf('e'.toByte())) + assertEquals((Segment.SIZE - 4).toLong(), source.indexOf('d'.code.toByte())) + assertEquals(-1, source.indexOf('e'.code.toByte())) } @Test fun indexOfByteWithStartOffset() { sink.writeUtf8("a").writeUtf8("b".repeat(Segment.SIZE)).writeUtf8("c") sink.emit() - assertEquals(-1, source.indexOf('a'.toByte(), 1)) - assertEquals(15, source.indexOf('b'.toByte(), 15)) + assertEquals(-1, source.indexOf('a'.code.toByte(), 1)) + assertEquals(15, source.indexOf('b'.code.toByte(), 15)) } @Test fun indexOfByteWithBothOffsets() { if (factory.isOneByteAtATime) { - // When run on Travis this causes out-of-memory errors. + // When run on CI this causes out-of-memory errors. return } - val a = 'a'.toByte() - val c = 'c'.toByte() + val a = 'a'.code.toByte() + val c = 'c'.code.toByte() val size = Segment.SIZE * 5 val bytes = ByteArray(size) { a } @@ -585,7 +585,7 @@ abstract class AbstractBufferedSourceTest internal constructor( size - Segment.SIZE + 1, size - 3, size - 2, - size - 1 + size - 1, ) // In each iteration, we write c to the known point and then search for it using different @@ -615,17 +615,11 @@ abstract class AbstractBufferedSourceTest internal constructor( @Test fun indexOfByteInvalidBoundsThrows() { sink.writeUtf8("abc") sink.emit() - - try { - source.indexOf('a'.toByte(), -1) - fail("Expected failure: fromIndex < 0") - } catch (expected: IllegalArgumentException) { + assertFailsWith<IllegalArgumentException>("Expected failure: fromIndex < 0") { + source.indexOf('a'.code.toByte(), -1) } - - try { - source.indexOf('a'.toByte(), 10, 0) - fail("Expected failure: fromIndex > toIndex") - } catch (expected: IllegalArgumentException) { + assertFailsWith<IllegalArgumentException>("Expected failure: fromIndex > toIndex") { + source.indexOf('a'.code.toByte(), 10, 0) } } @@ -649,55 +643,55 @@ abstract class AbstractBufferedSourceTest internal constructor( sink.emit() assertEquals( (Segment.SIZE - 3).toLong(), - source.indexOf("aabc".encodeUtf8(), (Segment.SIZE - 4).toLong()) + source.indexOf("aabc".encodeUtf8(), (Segment.SIZE - 4).toLong()), ) assertEquals( (Segment.SIZE - 3).toLong(), - source.indexOf("aabc".encodeUtf8(), (Segment.SIZE - 3).toLong()) + source.indexOf("aabc".encodeUtf8(), (Segment.SIZE - 3).toLong()), ) assertEquals( (Segment.SIZE - 2).toLong(), - source.indexOf("abcd".encodeUtf8(), (Segment.SIZE - 2).toLong()) + source.indexOf("abcd".encodeUtf8(), (Segment.SIZE - 2).toLong()), ) assertEquals( (Segment.SIZE - 2).toLong(), - source.indexOf("abc".encodeUtf8(), (Segment.SIZE - 2).toLong()) + source.indexOf("abc".encodeUtf8(), (Segment.SIZE - 2).toLong()), ) assertEquals( (Segment.SIZE - 2).toLong(), - source.indexOf("abc".encodeUtf8(), (Segment.SIZE - 2).toLong()) + source.indexOf("abc".encodeUtf8(), (Segment.SIZE - 2).toLong()), ) assertEquals( (Segment.SIZE - 2).toLong(), - source.indexOf("ab".encodeUtf8(), (Segment.SIZE - 2).toLong()) + source.indexOf("ab".encodeUtf8(), (Segment.SIZE - 2).toLong()), ) assertEquals( (Segment.SIZE - 2).toLong(), - source.indexOf("a".encodeUtf8(), (Segment.SIZE - 2).toLong()) + source.indexOf("a".encodeUtf8(), (Segment.SIZE - 2).toLong()), ) assertEquals( (Segment.SIZE - 1).toLong(), - source.indexOf("bc".encodeUtf8(), (Segment.SIZE - 2).toLong()) + source.indexOf("bc".encodeUtf8(), (Segment.SIZE - 2).toLong()), ) assertEquals( (Segment.SIZE - 1).toLong(), - source.indexOf("b".encodeUtf8(), (Segment.SIZE - 2).toLong()) + source.indexOf("b".encodeUtf8(), (Segment.SIZE - 2).toLong()), ) assertEquals( Segment.SIZE.toLong(), - source.indexOf("c".encodeUtf8(), (Segment.SIZE - 2).toLong()) + source.indexOf("c".encodeUtf8(), (Segment.SIZE - 2).toLong()), ) assertEquals( Segment.SIZE.toLong(), - source.indexOf("c".encodeUtf8(), Segment.SIZE.toLong()) + source.indexOf("c".encodeUtf8(), Segment.SIZE.toLong()), ) assertEquals( (Segment.SIZE + 1).toLong(), - source.indexOf("d".encodeUtf8(), (Segment.SIZE - 2).toLong()) + source.indexOf("d".encodeUtf8(), (Segment.SIZE - 2).toLong()), ) assertEquals( (Segment.SIZE + 1).toLong(), - source.indexOf("d".encodeUtf8(), (Segment.SIZE + 1).toLong()) + source.indexOf("d".encodeUtf8(), (Segment.SIZE + 1).toLong()), ) } @@ -723,19 +717,15 @@ abstract class AbstractBufferedSourceTest internal constructor( } @Test fun indexOfByteStringInvalidArgumentsThrows() { - try { + var e = assertFailsWith<IllegalArgumentException> { source.indexOf(ByteString.of()) - fail() - } catch (e: IllegalArgumentException) { - assertEquals("bytes is empty", e.message) } + assertEquals("bytes is empty", e.message) - try { + e = assertFailsWith<IllegalArgumentException> { source.indexOf("hi".encodeUtf8(), -1) - fail() - } catch (e: IllegalArgumentException) { - assertEquals("fromIndex < 0: -1", e.message) } + assertEquals("fromIndex < 0: -1", e.message) } /** @@ -781,10 +771,10 @@ abstract class AbstractBufferedSourceTest internal constructor( @Test fun indexOfByteWithFromIndex() { sink.writeUtf8("aaa") sink.emit() - assertEquals(0, source.indexOf('a'.toByte())) - assertEquals(0, source.indexOf('a'.toByte(), 0)) - assertEquals(1, source.indexOf('a'.toByte(), 1)) - assertEquals(2, source.indexOf('a'.toByte(), 2)) + assertEquals(0, source.indexOf('a'.code.toByte())) + assertEquals(0, source.indexOf('a'.code.toByte(), 0)) + assertEquals(1, source.indexOf('a'.code.toByte(), 1)) + assertEquals(2, source.indexOf('a'.code.toByte(), 2)) } @Test fun indexOfByteStringWithFromIndex() { @@ -857,35 +847,29 @@ abstract class AbstractBufferedSourceTest internal constructor( } @Test fun longHexStringTooLongThrows() { - try { - sink.writeUtf8("fffffffffffffffff") - sink.emit() + sink.writeUtf8("fffffffffffffffff") + sink.emit() + + val e = assertFailsWith<NumberFormatException> { source.readHexadecimalUnsignedLong() - fail() - } catch (e: NumberFormatException) { - assertEquals("Number too large: fffffffffffffffff", e.message) } + assertEquals("Number too large: fffffffffffffffff", e.message) } @Test fun longHexStringTooShortThrows() { - try { - sink.writeUtf8(" ") - sink.emit() + sink.writeUtf8(" ") + sink.emit() + + val e = assertFailsWith<NumberFormatException> { source.readHexadecimalUnsignedLong() - fail() - } catch (e: NumberFormatException) { - assertEquals("Expected leading [0-9a-fA-F] character but was 0x20", e.message) } + assertEquals("Expected leading [0-9a-fA-F] character but was 0x20", e.message) } @Test fun longHexEmptySourceThrows() { - try { - sink.writeUtf8("") - sink.emit() - source.readHexadecimalUnsignedLong() - fail() - } catch (expected: EOFException) { - } + sink.writeUtf8("") + sink.emit() + assertFailsWith<EOFException> { source.readHexadecimalUnsignedLong() } } @Test fun longDecimalString() { @@ -918,60 +902,74 @@ abstract class AbstractBufferedSourceTest internal constructor( } @Test fun longDecimalStringTooLongThrows() { - try { - sink.writeUtf8("12345678901234567890") // Too many digits. - sink.emit() + sink.writeUtf8("12345678901234567890") // Too many digits. + sink.emit() + + val e = assertFailsWith<NumberFormatException> { source.readDecimalLong() - fail() - } catch (e: NumberFormatException) { - assertEquals("Number too large: 12345678901234567890", e.message) } + assertEquals("Number too large: 12345678901234567890", e.message) } @Test fun longDecimalStringTooHighThrows() { - try { - sink.writeUtf8("9223372036854775808") // Right size but cannot fit. - sink.emit() + sink.writeUtf8("9223372036854775808") // Right size but cannot fit. + sink.emit() + + val e = assertFailsWith<NumberFormatException> { source.readDecimalLong() - fail() - } catch (e: NumberFormatException) { - assertEquals("Number too large: 9223372036854775808", e.message) } + assertEquals("Number too large: 9223372036854775808", e.message) } @Test fun longDecimalStringTooLowThrows() { - try { - sink.writeUtf8("-9223372036854775809") // Right size but cannot fit. - sink.emit() + sink.writeUtf8("-9223372036854775809") // Right size but cannot fit. + sink.emit() + + val e = assertFailsWith<NumberFormatException> { source.readDecimalLong() - fail() - } catch (e: NumberFormatException) { - assertEquals("Number too large: -9223372036854775809", e.message) } + assertEquals("Number too large: -9223372036854775809", e.message) } @Test fun longDecimalStringTooShortThrows() { - try { - sink.writeUtf8(" ") - sink.emit() + sink.writeUtf8(" ") + sink.emit() + + val e = assertFailsWith<NumberFormatException> { source.readDecimalLong() - fail() - } catch (e: NumberFormatException) { - assertEquals("Expected leading [0-9] or '-' character but was 0x20", e.message) } + assertEquals("Expected a digit or '-' but was 0x20", e.message) } @Test fun longDecimalEmptyThrows() { - try { - sink.writeUtf8("") - sink.emit() + sink.writeUtf8("") + sink.emit() + assertFailsWith<EOFException> { + source.readDecimalLong() + } + } + + @Test fun longDecimalLoneDashThrows() { + sink.writeUtf8("-") + sink.emit() + assertFailsWith<EOFException> { + source.readDecimalLong() + } + } + + @Test fun longDecimalDashFollowedByNonDigitThrows() { + sink.writeUtf8("- ") + sink.emit() + assertFailsWith<NumberFormatException> { source.readDecimalLong() - fail() - } catch (expected: EOFException) { } } @Test fun codePoints() { + // TODO: remove this suppression once this issue is fixed. + // https://youtrack.jetbrains.com/issue/KT-60212 + if (isWasm()) return + sink.write("7f".decodeHex()) sink.emit() assertEquals(0x7f, source.readUtf8CodePoint().toLong()) @@ -1000,21 +998,24 @@ abstract class AbstractBufferedSourceTest internal constructor( val options = Options.of( "ROCK".encodeUtf8(), "SCISSORS".encodeUtf8(), - "PAPER".encodeUtf8() + "PAPER".encodeUtf8(), ) sink.writeUtf8("PAPER,SCISSORS,ROCK") sink.emit() assertEquals(2, source.select(options).toLong()) - assertEquals(','.toLong(), source.readByte().toLong()) + assertEquals(','.code.toLong(), source.readByte().toLong()) assertEquals(1, source.select(options).toLong()) - assertEquals(','.toLong(), source.readByte().toLong()) + assertEquals(','.code.toLong(), source.readByte().toLong()) assertEquals(0, source.select(options).toLong()) assertTrue(source.exhausted()) } /** Note that this test crashes the VM on Android. */ @Test fun selectSpanningMultipleSegments() { + if (factory.isOneByteAtATime && isBrowser()) { + return // This test times out on browsers. + } val commonPrefix = randomBytes(Segment.SIZE + 10) val a = Buffer().write(commonPrefix).writeUtf8("a").readByteString() val bc = Buffer().write(commonPrefix).writeUtf8("bc").readByteString() @@ -1036,7 +1037,7 @@ abstract class AbstractBufferedSourceTest internal constructor( val options = Options.of( "ROCK".encodeUtf8(), "SCISSORS".encodeUtf8(), - "PAPER".encodeUtf8() + "PAPER".encodeUtf8(), ) sink.writeUtf8("SPOCK") @@ -1049,7 +1050,7 @@ abstract class AbstractBufferedSourceTest internal constructor( val options = Options.of( "abcd".encodeUtf8(), "abce".encodeUtf8(), - "abcc".encodeUtf8() + "abcc".encodeUtf8(), ) sink.writeUtf8("abcc").writeUtf8("abcd").writeUtf8("abce") @@ -1063,7 +1064,7 @@ abstract class AbstractBufferedSourceTest internal constructor( val options = Options.of( "abcd".encodeUtf8(), "abce".encodeUtf8(), - "abcc".encodeUtf8() + "abcc".encodeUtf8(), ) sink.writeUtf8("abc") sink.emit() @@ -1075,7 +1076,7 @@ abstract class AbstractBufferedSourceTest internal constructor( val options = Options.of( "abcd".encodeUtf8(), "abc".encodeUtf8(), - "abcde".encodeUtf8() + "abcde".encodeUtf8(), ) sink.writeUtf8("abcdef") sink.emit() @@ -1086,7 +1087,7 @@ abstract class AbstractBufferedSourceTest internal constructor( @Test fun selectFromEmptySource() { val options = Options.of( "abc".encodeUtf8(), - "def".encodeUtf8() + "def".encodeUtf8(), ) assertEquals(-1, source.select(options).toLong()) } @@ -1132,6 +1133,10 @@ abstract class AbstractBufferedSourceTest internal constructor( } @Test fun peekLarge() { + if (factory.isOneByteAtATime) { + // When run on CI this causes out-of-memory errors. + return + } sink.writeUtf8("abcdef") sink.writeUtf8("g".repeat(2 * Segment.SIZE)) sink.writeUtf8("hij") @@ -1163,12 +1168,10 @@ abstract class AbstractBufferedSourceTest internal constructor( assertEquals("def", source.readUtf8(3)) - try { + val e = assertFailsWith<IllegalStateException> { peek.readUtf8() - fail() - } catch (e: IllegalStateException) { - assertEquals("Peek source is invalid because upstream source was used", e.message) } + assertEquals("Peek source is invalid because upstream source was used", e.message) } @Test fun peekSegmentThenInvalid() { @@ -1186,12 +1189,10 @@ abstract class AbstractBufferedSourceTest internal constructor( // Skip the rest of the buffered data peek.skip(peek.buffer.size) - try { + val e = assertFailsWith<IllegalStateException> { peek.readByte() - fail() - } catch (e: IllegalStateException) { - assertEquals("Peek source is invalid because upstream source was used", e.message) } + assertEquals("Peek source is invalid because upstream source was used", e.message) } @Test fun peekDoesntReadTooMuch() { diff --git a/okio/src/commonTest/kotlin/okio/BufferCommonTest.kt b/okio/src/commonTest/kotlin/okio/BufferCommonTest.kt index 842faffe..cc946883 100644 --- a/okio/src/commonTest/kotlin/okio/BufferCommonTest.kt +++ b/okio/src/commonTest/kotlin/okio/BufferCommonTest.kt @@ -15,9 +15,9 @@ */ package okio -import okio.ByteString.Companion.encodeUtf8 import kotlin.test.Test import kotlin.test.assertEquals +import okio.ByteString.Companion.encodeUtf8 class BufferCommonTest { diff --git a/okio/src/commonTest/kotlin/okio/BufferedSourceFactory.kt b/okio/src/commonTest/kotlin/okio/BufferedSourceFactory.kt index b9836202..173bb841 100644 --- a/okio/src/commonTest/kotlin/okio/BufferedSourceFactory.kt +++ b/okio/src/commonTest/kotlin/okio/BufferedSourceFactory.kt @@ -19,7 +19,7 @@ package okio interface BufferedSourceFactory { class Pipe( var sink: BufferedSink, - var source: BufferedSource + var source: BufferedSource, ) val isOneByteAtATime: Boolean @@ -36,7 +36,7 @@ interface BufferedSourceFactory { val buffer = Buffer() return Pipe( buffer, - buffer + buffer, ) } } @@ -51,7 +51,7 @@ interface BufferedSourceFactory { val buffer = Buffer() return Pipe( buffer, - (buffer as Source).buffer() + (buffer as Source).buffer(), ) } } @@ -79,7 +79,7 @@ interface BufferedSourceFactory { if (result > 0L) sink.write(box.copy(), result) return result } - }.buffer() + }.buffer(), ) } } @@ -104,7 +104,7 @@ interface BufferedSourceFactory { } } }.buffer(), - buffer + buffer, ) } } @@ -118,7 +118,7 @@ interface BufferedSourceFactory { val buffer = Buffer() return Pipe( buffer, - buffer.peek() + buffer.peek(), ) } } @@ -133,7 +133,7 @@ interface BufferedSourceFactory { val buffer = Buffer() return Pipe( buffer, - (buffer as Source).buffer().peek() + (buffer as Source).buffer().peek(), ) } } @@ -144,7 +144,7 @@ interface BufferedSourceFactory { arrayOf(ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE), arrayOf(ONE_BYTE_AT_A_TIME_BUFFER), arrayOf(PEEK_BUFFER), - arrayOf(PEEK_BUFFERED_SOURCE) + arrayOf(PEEK_BUFFERED_SOURCE), ) } } diff --git a/okio/src/commonTest/kotlin/okio/ByteStringMoreTests.kt b/okio/src/commonTest/kotlin/okio/ByteStringMoreTests.kt new file mode 100644 index 00000000..d9721d16 --- /dev/null +++ b/okio/src/commonTest/kotlin/okio/ByteStringMoreTests.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 Square, Inc. + * + * 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 kotlin.test.Test +import kotlin.test.assertEquals +import okio.ByteString.Companion.toByteString + +class ByteStringMoreTests { + @Test fun arrayToByteString() { + val actual = byteArrayOf(1, 2, 3, 4).toByteString() + val expected = ByteString.of(1, 2, 3, 4) + assertEquals(actual, expected) + } +} diff --git a/okio/src/commonTest/kotlin/okio/ByteStringTest.kt b/okio/src/commonTest/kotlin/okio/ByteStringTest.kt index c75c4581..9866e14b 100644 --- a/okio/src/commonTest/kotlin/okio/ByteStringTest.kt +++ b/okio/src/commonTest/kotlin/okio/ByteStringTest.kt @@ -16,10 +16,6 @@ package okio -import okio.ByteString.Companion.decodeBase64 -import okio.ByteString.Companion.decodeHex -import okio.ByteString.Companion.encodeUtf8 -import okio.internal.commonAsUtf8ToByteArray import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals @@ -29,6 +25,11 @@ import kotlin.test.assertNotEquals import kotlin.test.assertSame import kotlin.test.assertTrue import kotlin.test.fail +import okio.ByteString.Companion.decodeBase64 +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.encodeUtf8 +import okio.ByteString.Companion.toByteString +import okio.internal.commonAsUtf8ToByteArray class ByteStringTest : AbstractByteStringTest(ByteStringFactory.BYTE_STRING) class SegmentedByteStringTest : AbstractByteStringTest(ByteStringFactory.SEGMENTED_BYTE_STRING) @@ -36,14 +37,14 @@ class ByteStringOneBytePerSegmentTest : AbstractByteStringTest(ByteStringFactory class OkioEncoderTest : AbstractByteStringTest(ByteStringFactory.OKIO_ENCODER) abstract class AbstractByteStringTest internal constructor( - private val factory: ByteStringFactory + private val factory: ByteStringFactory, ) { @Test fun get() { val actual = factory.encodeUtf8("abc") assertEquals(3, actual.size) - assertEquals(actual[0], 'a'.toByte()) - assertEquals(actual[1], 'b'.toByte()) - assertEquals(actual[2], 'c'.toByte()) + assertEquals(actual[0], 'a'.code.toByte()) + assertEquals(actual[1], 'b'.code.toByte()) + assertEquals(actual[2], 'c'.code.toByte()) try { actual[-1] fail("no index out of bounds: -1") @@ -214,7 +215,7 @@ abstract class AbstractByteStringTest internal constructor( ( "d09dd0b020d0b1d0b5d180d0b5d0b3d18320d0bfd183d181" + "d182d18bd0bdd0bdd18bd18520d0b2d0bed0bbd0bd" - ).decodeHex() + ).decodeHex(), ) assertEquals(byteString.utf8(), bronzeHorseman) } @@ -277,7 +278,7 @@ abstract class AbstractByteStringTest internal constructor( assertEquals("AAAA", factory.encodeUtf8("\u0000\u0000\u0000").base64()) assertEquals( "SG93IG1hbnkgbGluZXMgb2YgY29kZSBhcmUgdGhlcmU/ICdib3V0IDIgbWlsbGlvbi4=", - factory.encodeUtf8("How many lines of code are there? 'bout 2 million.").base64() + factory.encodeUtf8("How many lines of code are there? 'bout 2 million.").base64(), ) } @@ -288,7 +289,7 @@ abstract class AbstractByteStringTest internal constructor( assertEquals("AAAA", factory.encodeUtf8("\u0000\u0000\u0000").base64Url()) assertEquals( "SG93IG1hbnkgbGluZXMgb2YgY29kZSBhcmUgdGhlcmU_ICdib3V0IDIgbWlsbGlvbi4=", - factory.encodeUtf8("How many lines of code are there? 'bout 2 million.").base64Url() + factory.encodeUtf8("How many lines of code are there? 'bout 2 million.").base64Url(), ) } @@ -313,7 +314,7 @@ abstract class AbstractByteStringTest internal constructor( ( "V2hhdCdzIHRvIGJlIHNjYXJlZCBhYm91dD8gSXQncyBqdXN0IGEgbGl0dGxlIGhpY2" + "N1cCBpbiB0aGUgcG93ZXIuLi4=" - ).decodeBase64()!!.utf8() + ).decodeBase64()!!.utf8(), ) // Uses two encoding styles. Malformed, but supported as a side-effect. assertEquals("ffffff".decodeHex(), "__//".decodeBase64()) @@ -358,11 +359,11 @@ abstract class AbstractByteStringTest internal constructor( @Test fun toStringOnShortText() { assertEquals( "[text=Tyrannosaur]", - factory.encodeUtf8("Tyrannosaur").toString() + factory.encodeUtf8("Tyrannosaur").toString(), ) assertEquals( "[text=təˈranəˌsôr]", - factory.decodeHex("74c999cb8872616ec999cb8c73c3b472").toString() + factory.decodeHex("74c999cb8872616ec999cb8c73c3b472").toString(), ) } @@ -379,7 +380,7 @@ abstract class AbstractByteStringTest internal constructor( assertEquals( "[size=517 text=Um, I'll tell you the problem with the scientific power that " + "you…]", - factory.encodeUtf8(raw).toString() + factory.encodeUtf8(raw).toString(), ) val war = ( "Սm, I'll 𝓽𝖾ll ᶌօ𝘂 ᴛℎ℮ 𝜚𝕣०bl𝖾m wі𝕥𝒽 𝘵𝘩𝐞 𝓼𝙘𝐢𝔢𝓷𝗍𝜄𝚏𝑖c 𝛠𝝾w𝚎𝑟 𝕥h⍺𝞃 𝛄𝓸𝘂'𝒓𝗲 υ𝖘𝓲𝗇ɡ 𝕙𝚎𝑟e, " + @@ -392,7 +393,7 @@ abstract class AbstractByteStringTest internal constructor( assertEquals( "[size=1496 text=Սm, I'll 𝓽𝖾ll ᶌօ𝘂 ᴛℎ℮ 𝜚𝕣०bl𝖾m wі𝕥𝒽 𝘵𝘩𝐞 𝓼𝙘𝐢𝔢𝓷𝗍𝜄𝚏𝑖c 𝛠𝝾w𝚎𝑟 𝕥h⍺𝞃 " + "𝛄𝓸𝘂…]", - factory.encodeUtf8(war).toString() + factory.encodeUtf8(war).toString(), ) } @@ -400,7 +401,7 @@ abstract class AbstractByteStringTest internal constructor( // Instead of emitting a literal newline in the toString(), these are escaped as "\n". assertEquals( "[text=a\\r\\nb\\nc\\rd\\\\e]", - factory.encodeUtf8("a\r\nb\nc\rd\\e").toString() + factory.encodeUtf8("a\r\nb\nc\rd\\e").toString(), ) } @@ -408,13 +409,13 @@ abstract class AbstractByteStringTest internal constructor( val byteString = factory.decodeHex( "" + "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55" + - "4bf0b54023c29b624de9ef9c2f931efc580f9afb" + "4bf0b54023c29b624de9ef9c2f931efc580f9afb", ) assertEquals( "[hex=" + "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55" + "4bf0b54023c29b624de9ef9c2f931efc580f9afb]", - byteString.toString() + byteString.toString(), ) } @@ -422,13 +423,13 @@ abstract class AbstractByteStringTest internal constructor( val byteString = factory.decodeHex( "" + "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55" + - "4bf0b54023c29b624de9ef9c2f931efc580f9afba1" + "4bf0b54023c29b624de9ef9c2f931efc580f9afba1", ) assertEquals( "[size=65 hex=" + "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55" + "4bf0b54023c29b624de9ef9c2f931efc580f9afb…]", - byteString.toString() + byteString.toString(), ) } @@ -441,7 +442,7 @@ abstract class AbstractByteStringTest internal constructor( factory.decodeHex("80"), factory.decodeHex("81"), factory.decodeHex("fe"), - factory.decodeHex("ff") + factory.decodeHex("ff"), ) val sortedByteStrings = originalByteStrings.toMutableList() @@ -479,7 +480,7 @@ abstract class AbstractByteStringTest internal constructor( factory.decodeHex("010101"), factory.decodeHex("7f0000"), factory.decodeHex("7f0000ffff"), - factory.decodeHex("ffffff") + factory.decodeHex("ffffff"), ) val sortedByteStrings = originalByteStrings.toMutableList() @@ -496,4 +497,101 @@ abstract class AbstractByteStringTest internal constructor( assertEquals("0e4dd66217fc8d2e298b78c8cd9392870dcd065d0ff675d0edff5bcd227837e9", sha256().hex()) assertEquals("483676b93c4417198b465083d196ec6a9fab8d004515874b8ff47e041f5f56303cc08179625030b8b5b721c09149a18f0f59e64e7ae099518cea78d3d83167e1", sha512().hex()) } + + @Test fun copyInto() { + val byteString = factory.encodeUtf8("abcdefgh") + val byteArray = "WwwwXxxxYyyyZzzz".encodeToByteArray() + byteString.copyInto(target = byteArray, byteCount = 5) + assertEquals("abcdexxxYyyyZzzz", byteArray.decodeToString()) + } + + @Test fun copyIntoFullRange() { + val byteString = factory.encodeUtf8("abcdefghijklmnop") + val byteArray = "WwwwXxxxYyyyZzzz".encodeToByteArray() + byteString.copyInto(target = byteArray, byteCount = 16) + assertEquals("abcdefghijklmnop", byteArray.decodeToString()) + } + + @Test fun copyIntoWithTargetOffset() { + val byteString = factory.encodeUtf8("abcdefgh") + val byteArray = "WwwwXxxxYyyyZzzz".encodeToByteArray() + byteString.copyInto(target = byteArray, targetOffset = 11, byteCount = 5) + assertEquals("WwwwXxxxYyyabcde", byteArray.decodeToString()) + } + + @Test fun copyIntoWithSourceOffset() { + val byteString = factory.encodeUtf8("abcdefgh") + val byteArray = "WwwwXxxxYyyyZzzz".encodeToByteArray() + byteString.copyInto(offset = 3, target = byteArray, byteCount = 5) + assertEquals("defghxxxYyyyZzzz", byteArray.decodeToString()) + } + + @Test fun copyIntoWithAllParameters() { + val byteString = factory.encodeUtf8("abcdefgh") + val byteArray = "WwwwXxxxYyyyZzzz".encodeToByteArray() + byteString.copyInto(offset = 3, target = byteArray, targetOffset = 11, byteCount = 5) + assertEquals("WwwwXxxxYyydefgh", byteArray.decodeToString()) + } + + @Test fun copyIntoBoundsChecks() { + val byteString = factory.encodeUtf8("abcdefgh") + val byteArray = "WwwwXxxxYyyyZzzz".encodeToByteArray() + assertFailsWith<IndexOutOfBoundsException> { + byteString.copyInto(offset = -1, target = byteArray, targetOffset = 1, byteCount = 1) + } + assertFailsWith<IndexOutOfBoundsException> { + byteString.copyInto(offset = 9, target = byteArray, targetOffset = 0, byteCount = 0) + } + assertFailsWith<IndexOutOfBoundsException> { + byteString.copyInto(offset = 1, target = byteArray, targetOffset = -1, byteCount = 1) + } + assertFailsWith<IndexOutOfBoundsException> { + byteString.copyInto(offset = 1, target = byteArray, targetOffset = 17, byteCount = 1) + } + assertFailsWith<IndexOutOfBoundsException> { + byteString.copyInto(offset = 7, target = byteArray, targetOffset = 1, byteCount = 2) + } + assertFailsWith<IndexOutOfBoundsException> { + byteString.copyInto(offset = 1, target = byteArray, targetOffset = 15, byteCount = 2) + } + } + + @Test fun copyEmptyAtBounds() { + val byteString = factory.encodeUtf8("abcdefgh") + val byteArray = "WwwwXxxxYyyyZzzz".encodeToByteArray() + byteString.copyInto(offset = 0, target = byteArray, targetOffset = 0, byteCount = 0) + assertEquals("WwwwXxxxYyyyZzzz", byteArray.decodeToString()) + byteString.copyInto(offset = 0, target = byteArray, targetOffset = 16, byteCount = 0) + assertEquals("WwwwXxxxYyyyZzzz", byteArray.decodeToString()) + byteString.copyInto(offset = 8, target = byteArray, targetOffset = 0, byteCount = 0) + assertEquals("WwwwXxxxYyyyZzzz", byteArray.decodeToString()) + } + + @Test + fun ofCopy() { + val bytes = "Hello, World!".encodeToByteArray() + val byteString = ByteString.of(*bytes) + // Verify that the bytes were copied out. + bytes[4] = 'a'.code.toByte() + assertEquals("Hello, World!", byteString.utf8()) + } + + @Test + fun ofCopyRange() { + val bytes = "Hello, World!".encodeToByteArray() + val byteString: ByteString = bytes.toByteString(2, 9) + // Verify that the bytes were copied out. + bytes[4] = 'a'.code.toByte() + assertEquals("llo, Worl", byteString.utf8()) + } + + @Test + fun getByteOutOfBounds() { + val byteString = factory.decodeHex("ab12") + try { + byteString[2] + fail() + } catch (expected: IndexOutOfBoundsException) { + } + } } diff --git a/okio/src/commonTest/kotlin/okio/CommonBufferTest.kt b/okio/src/commonTest/kotlin/okio/CommonBufferTest.kt index 292e3c5c..0dbaf021 100644 --- a/okio/src/commonTest/kotlin/okio/CommonBufferTest.kt +++ b/okio/src/commonTest/kotlin/okio/CommonBufferTest.kt @@ -15,13 +15,13 @@ */ package okio -import okio.ByteString.Companion.decodeHex import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue +import okio.ByteString.Companion.decodeHex /** * Tests solely for the behavior of Buffer's implementation. For generic BufferedSink or @@ -48,22 +48,22 @@ class CommonBufferTest { assertEquals("[size=0]", Buffer().toString()) assertEquals( "[text=a\\r\\nb\\nc\\rd\\\\e]", - Buffer().writeUtf8("a\r\nb\nc\rd\\e").toString() + Buffer().writeUtf8("a\r\nb\nc\rd\\e").toString(), ) assertEquals( "[text=Tyrannosaur]", - Buffer().writeUtf8("Tyrannosaur").toString() + Buffer().writeUtf8("Tyrannosaur").toString(), ) assertEquals( "[text=təˈranəˌsôr]", Buffer() .write("74c999cb8872616ec999cb8c73c3b472".decodeHex()) - .toString() + .toString(), ) assertEquals( "[hex=0000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000]", - Buffer().write(ByteArray(64)).toString() + Buffer().write(ByteArray(64)).toString(), ) } @@ -127,10 +127,16 @@ class CommonBufferTest { val segmentSizes = moveBytesBetweenBuffers('a'.repeat(size), 'b'.repeat(size)) assertEquals( listOf( - Segment.SIZE, Segment.SIZE, Segment.SIZE, 1, - Segment.SIZE, Segment.SIZE, Segment.SIZE, 1 + Segment.SIZE, + Segment.SIZE, + Segment.SIZE, + 1, + Segment.SIZE, + Segment.SIZE, + Segment.SIZE, + 1, ), - segmentSizes + segmentSizes, ) } @@ -240,14 +246,14 @@ class CommonBufferTest { buffer.writeUtf8('b'.repeat(halfSegment)) buffer.writeUtf8('c'.repeat(halfSegment)) buffer.writeUtf8('d'.repeat(halfSegment)) - assertEquals(0, buffer.indexOf('a'.toByte(), 0)) - assertEquals((halfSegment - 1).toLong(), buffer.indexOf('a'.toByte(), (halfSegment - 1).toLong())) - assertEquals(halfSegment.toLong(), buffer.indexOf('b'.toByte(), (halfSegment - 1).toLong())) - assertEquals((halfSegment * 2).toLong(), buffer.indexOf('c'.toByte(), (halfSegment - 1).toLong())) - assertEquals((halfSegment * 3).toLong(), buffer.indexOf('d'.toByte(), (halfSegment - 1).toLong())) - assertEquals((halfSegment * 3).toLong(), buffer.indexOf('d'.toByte(), (halfSegment * 2).toLong())) - assertEquals((halfSegment * 3).toLong(), buffer.indexOf('d'.toByte(), (halfSegment * 3).toLong())) - assertEquals((halfSegment * 4 - 1).toLong(), buffer.indexOf('d'.toByte(), (halfSegment * 4 - 1).toLong())) + assertEquals(0, buffer.indexOf('a'.code.toByte(), 0)) + assertEquals((halfSegment - 1).toLong(), buffer.indexOf('a'.code.toByte(), (halfSegment - 1).toLong())) + assertEquals(halfSegment.toLong(), buffer.indexOf('b'.code.toByte(), (halfSegment - 1).toLong())) + assertEquals((halfSegment * 2).toLong(), buffer.indexOf('c'.code.toByte(), (halfSegment - 1).toLong())) + assertEquals((halfSegment * 3).toLong(), buffer.indexOf('d'.code.toByte(), (halfSegment - 1).toLong())) + assertEquals((halfSegment * 3).toLong(), buffer.indexOf('d'.code.toByte(), (halfSegment * 2).toLong())) + assertEquals((halfSegment * 3).toLong(), buffer.indexOf('d'.code.toByte(), (halfSegment * 3).toLong())) + assertEquals((halfSegment * 4 - 1).toLong(), buffer.indexOf('d'.code.toByte(), (halfSegment * 4 - 1).toLong())) } @Test fun byteAt() { @@ -255,11 +261,11 @@ class CommonBufferTest { buffer.writeUtf8("a") buffer.writeUtf8('b'.repeat(Segment.SIZE)) buffer.writeUtf8("c") - assertEquals('a'.toLong(), buffer[0].toLong()) - assertEquals('a'.toLong(), buffer[0].toLong()) // getByte doesn't mutate! - assertEquals('c'.toLong(), buffer[buffer.size - 1].toLong()) - assertEquals('b'.toLong(), buffer[buffer.size - 2].toLong()) - assertEquals('b'.toLong(), buffer[buffer.size - 3].toLong()) + assertEquals('a'.code.toLong(), buffer[0].toLong()) + assertEquals('a'.code.toLong(), buffer[0].toLong()) // getByte doesn't mutate! + assertEquals('c'.code.toLong(), buffer[buffer.size - 1].toLong()) + assertEquals('b'.code.toLong(), buffer[buffer.size - 2].toLong()) + assertEquals('b'.code.toLong(), buffer[buffer.size - 3].toLong()) } @Test fun getByteOfEmptyBuffer() { @@ -279,7 +285,8 @@ class CommonBufferTest { } @Suppress("ReplaceAssertBooleanWithAssertEquality") - @Test fun equalsAndHashCodeEmpty() { + @Test + fun equalsAndHashCodeEmpty() { val a = Buffer() val b = Buffer() assertTrue(a == b) @@ -287,7 +294,8 @@ class CommonBufferTest { } @Suppress("ReplaceAssertBooleanWithAssertEquality") - @Test fun equalsAndHashCode() { + @Test + fun equalsAndHashCode() { val a = Buffer().writeUtf8("dog") val b = Buffer().writeUtf8("hotdog") assertFalse(a == b) @@ -299,7 +307,8 @@ class CommonBufferTest { } @Suppress("ReplaceAssertBooleanWithAssertEquality") - @Test fun equalsAndHashCodeSpanningSegments() { + @Test + fun equalsAndHashCodeSpanningSegments() { val data = ByteArray(1024 * 1024) val dice = Random(0) dice.nextBytes(data) @@ -323,13 +332,13 @@ class CommonBufferTest { val write1 = Buffer().writeUtf8( 'a'.repeat(Segment.SIZE) + 'b'.repeat(Segment.SIZE) + - 'c'.repeat(Segment.SIZE) + 'c'.repeat(Segment.SIZE), ) val source = Buffer().writeUtf8( 'a'.repeat(Segment.SIZE) + 'b'.repeat(Segment.SIZE) + - 'c'.repeat(Segment.SIZE) + 'c'.repeat(Segment.SIZE), ) val mockSink = MockSink() diff --git a/okio/src/commonTest/kotlin/okio/CommonOptionsTest.kt b/okio/src/commonTest/kotlin/okio/CommonOptionsTest.kt index bb45321d..ae8156d6 100644 --- a/okio/src/commonTest/kotlin/okio/CommonOptionsTest.kt +++ b/okio/src/commonTest/kotlin/okio/CommonOptionsTest.kt @@ -15,11 +15,11 @@ */ package okio -import okio.ByteString.Companion.encodeUtf8 import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.fail +import okio.ByteString.Companion.encodeUtf8 class CommonOptionsTest { /** Confirm that options prefers the first-listed option, not the longest or shortest one. */ @@ -32,12 +32,13 @@ class CommonOptionsTest { assertEquals( utf8Options("hotdog", "hoth", "hot").trieString(), """ - |hot - | -> 2 - | d - | og -> 0 - | h -> 1 - |""".trimMargin() + |hot + | -> 2 + | d + | og -> 0 + | h -> 1 + | + """.trimMargin(), ) } @@ -70,81 +71,82 @@ class CommonOptionsTest { "connectTimeout", "readTimeout", "writeTimeout", - "pingInterval" + "pingInterval", ) assertEquals( options.trieString(), """ - |a - | uthenticator -> 17 - |c - | a - | che -> 9 - | e - | rtificate - | C - | hainCleaner -> 13 - | P - | inner -> 15 - | o - | n - | nect - | T - | imeout -> 23 - | i - | on - | P - | ool -> 18 - | S - | pecs -> 3 - | o - | kieJar -> 8 - |d - | i - | spatcher -> 0 - | n - | s -> 19 - |e - | ventListenerFactory -> 6 - |f - | ollow - | R - | edirects -> 21 - | S - | slRedirects -> 20 - |h - | ostnameVerifier -> 14 - |i - | nter - | c - | eptors -> 4 - | n - | alCache -> 10 - |n - | etworkInterceptors -> 5 - |p - | i - | ngInterval -> 26 - | r - | o - | t - | ocols -> 2 - | x - | y -> 1 - |r - | e - | a - | dTimeout -> 24 - | t - | ryOnConnectionFailure -> 22 - |s - | o - | cketFactory -> 11 - | s - | lSocketFactory -> 12 - |w - | riteTimeout -> 25 - |""".trimMargin() + |a + | uthenticator -> 17 + |c + | a + | che -> 9 + | e + | rtificate + | C + | hainCleaner -> 13 + | P + | inner -> 15 + | o + | n + | nect + | T + | imeout -> 23 + | i + | on + | P + | ool -> 18 + | S + | pecs -> 3 + | o + | kieJar -> 8 + |d + | i + | spatcher -> 0 + | n + | s -> 19 + |e + | ventListenerFactory -> 6 + |f + | ollow + | R + | edirects -> 21 + | S + | slRedirects -> 20 + |h + | ostnameVerifier -> 14 + |i + | nter + | c + | eptors -> 4 + | n + | alCache -> 10 + |n + | etworkInterceptors -> 5 + |p + | i + | ngInterval -> 26 + | r + | o + | t + | ocols -> 2 + | x + | y -> 1 + |r + | e + | a + | dTimeout -> 24 + | t + | ryOnConnectionFailure -> 22 + |s + | o + | cketFactory -> 11 + | s + | lSocketFactory -> 12 + |w + | riteTimeout -> 25 + | + """.trimMargin(), ) assertSelect("", -1, options) assertSelect("a", -1, options) @@ -212,10 +214,11 @@ class CommonOptionsTest { assertEquals( options.trieString(), """ - |abc - | -> 1 - | A -> 0 - |""".trimMargin() + |abc + | -> 1 + | A -> 0 + | + """.trimMargin(), ) assertSelect("abc", 1, options) assertSelect("abcA", 0, options) @@ -228,35 +231,39 @@ class CommonOptionsTest { assertEquals( utf8Options("a", "ab", "abc", "abcd", "abcde").trieString(), """ - |a -> 0 - |""".trimMargin() + |a -> 0 + | + """.trimMargin(), ) assertEquals( utf8Options("abc", "a", "ab", "abe", "abcd", "abcf").trieString(), """ - |a - | -> 1 - | bc -> 0 - |""".trimMargin() + |a + | -> 1 + | bc -> 0 + | + """.trimMargin(), ) assertEquals( utf8Options("abc", "ab", "a").trieString(), """ - |a - | -> 2 - | b - | -> 1 - | c -> 0 - |""".trimMargin() + |a + | -> 2 + | b + | -> 1 + | c -> 0 + | + """.trimMargin(), ) assertEquals( utf8Options("abcd", "abce", "abc", "abcf", "abcg").trieString(), """ - |abc - | -> 2 - | d -> 0 - | e -> 1 - |""".trimMargin() + |abc + | -> 2 + | d -> 0 + | e -> 1 + | + """.trimMargin(), ) } diff --git a/okio/src/commonTest/kotlin/okio/CommonRealBufferedSinkTest.kt b/okio/src/commonTest/kotlin/okio/CommonRealBufferedSinkTest.kt index abbbae8b..eec7607a 100644 --- a/okio/src/commonTest/kotlin/okio/CommonRealBufferedSinkTest.kt +++ b/okio/src/commonTest/kotlin/okio/CommonRealBufferedSinkTest.kt @@ -47,7 +47,7 @@ class CommonRealBufferedSinkTest { @Test fun bufferedSinkFlush() { val sink = Buffer() val bufferedSink = (sink as Sink).buffer() - bufferedSink.writeByte('a'.toInt()) + bufferedSink.writeByte('a'.code) assertEquals(0, sink.size) bufferedSink.flush() assertEquals(0, bufferedSink.buffer.size) @@ -93,9 +93,9 @@ class CommonRealBufferedSinkTest { @Test fun closeWithExceptionWhenWriting() { val mockSink = MockSink() - mockSink.scheduleThrow(0, IOException()) + mockSink.scheduleThrow(0, IOException("boom")) val bufferedSink = mockSink.buffer() - bufferedSink.writeByte('a'.toInt()) + bufferedSink.writeByte('a'.code) assertFailsWith<IOException> { bufferedSink.close() } @@ -105,9 +105,9 @@ class CommonRealBufferedSinkTest { @Test fun closeWithExceptionWhenClosing() { val mockSink = MockSink() - mockSink.scheduleThrow(1, IOException()) + mockSink.scheduleThrow(1, IOException("boom")) val bufferedSink = mockSink.buffer() - bufferedSink.writeByte('a'.toInt()) + bufferedSink.writeByte('a'.code) assertFailsWith<IOException> { bufferedSink.close() } @@ -120,7 +120,7 @@ class CommonRealBufferedSinkTest { mockSink.scheduleThrow(0, IOException("first")) mockSink.scheduleThrow(1, IOException("second")) val bufferedSink = mockSink.buffer() - bufferedSink.writeByte('a'.toInt()) + bufferedSink.writeByte('a'.code) try { bufferedSink.close() fail() @@ -134,12 +134,12 @@ class CommonRealBufferedSinkTest { @Test fun operationsAfterClose() { val mockSink = MockSink() val bufferedSink = mockSink.buffer() - bufferedSink.writeByte('a'.toInt()) + bufferedSink.writeByte('a'.code) bufferedSink.close() // Test a sample set of methods. assertFailsWith<IllegalStateException> { - bufferedSink.writeByte('a'.toInt()) + bufferedSink.writeByte('a'.code) } assertFailsWith<IllegalStateException> { @@ -186,7 +186,7 @@ class CommonRealBufferedSinkTest { val write3 = Buffer().writeUtf8("c".repeat(Segment.SIZE)) val source = Buffer().writeUtf8( - "${"a".repeat(Segment.SIZE)}${"b".repeat(Segment.SIZE)}${"c".repeat(Segment.SIZE)}" + "${"a".repeat(Segment.SIZE)}${"b".repeat(Segment.SIZE)}${"c".repeat(Segment.SIZE)}", ) val mockSink = MockSink() @@ -196,7 +196,7 @@ class CommonRealBufferedSinkTest { mockSink.assertLog( "write($write1, ${write1.size})", "write($write2, ${write2.size})", - "write($write3, ${write3.size})" + "write($write3, ${write3.size})", ) } } diff --git a/okio/src/commonTest/kotlin/okio/CommonRealBufferedSourceTest.kt b/okio/src/commonTest/kotlin/okio/CommonRealBufferedSourceTest.kt index 4663919a..6756c5a5 100644 --- a/okio/src/commonTest/kotlin/okio/CommonRealBufferedSourceTest.kt +++ b/okio/src/commonTest/kotlin/okio/CommonRealBufferedSourceTest.kt @@ -36,7 +36,7 @@ class CommonRealBufferedSourceTest { ).buffer() assertEquals(6, buffer.size) - assertEquals(-1, bufferedSource.indexOf('e'.toByte(), 0, 4)) + assertEquals(-1, bufferedSource.indexOf('e'.code.toByte(), 0, 4)) assertEquals(2, buffer.size) } @@ -141,7 +141,7 @@ class CommonRealBufferedSourceTest { val write3 = Buffer().writeUtf8("c".repeat(Segment.SIZE)) val source = Buffer().writeUtf8( - "${"a".repeat(Segment.SIZE)}${"b".repeat(Segment.SIZE)}${"c".repeat(Segment.SIZE)}" + "${"a".repeat(Segment.SIZE)}${"b".repeat(Segment.SIZE)}${"c".repeat(Segment.SIZE)}", ) val mockSink = MockSink() @@ -150,7 +150,7 @@ class CommonRealBufferedSourceTest { mockSink.assertLog( "write($write1, ${write1.size})", "write($write2, ${write2.size})", - "write($write3, ${write3.size})" + "write($write3, ${write3.size})", ) } } diff --git a/okio/src/commonTest/kotlin/okio/ForwardingSourceTest.kt b/okio/src/commonTest/kotlin/okio/ForwardingSourceTest.kt new file mode 100644 index 00000000..70c3fa45 --- /dev/null +++ b/okio/src/commonTest/kotlin/okio/ForwardingSourceTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 Square, Inc. + * + * 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 kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ForwardingSourceTest { + val source = Buffer().writeUtf8("Delegate") + + @Test + fun testForwardingSourceOverrides() { + val forwarder = "Forwarder" + val newSource = Buffer().writeUtf8(forwarder) + val forwardingSource = object : ForwardingSource(source) { + override fun read(sink: Buffer, byteCount: Long): Long { + return newSource.read(sink, byteCount) + } + } + + assertEquals("Forwarder", forwardingSource.buffer().readUtf8()) + } + + @Test + fun testForwardingSourceDelegates() { + val forwardingSource = object : ForwardingSource(source) { + } + + assertEquals("Delegate", forwardingSource.buffer().readUtf8()) + } + + @Test + fun testToString() { + val forwardingSource = object : ForwardingSource(source) { + } + + assertTrue(forwardingSource.toString().endsWith("([text=Delegate])")) + } +} diff --git a/okio/src/commonTest/kotlin/okio/HashingSinkTest.kt b/okio/src/commonTest/kotlin/okio/HashingSinkTest.kt index db1aeeec..bb88be7a 100644 --- a/okio/src/commonTest/kotlin/okio/HashingSinkTest.kt +++ b/okio/src/commonTest/kotlin/okio/HashingSinkTest.kt @@ -15,14 +15,14 @@ */ package okio +import kotlin.test.Test +import kotlin.test.assertEquals import okio.HashingSink.Companion.hmacSha1 import okio.HashingSink.Companion.hmacSha256 import okio.HashingSink.Companion.hmacSha512 import okio.HashingSink.Companion.sha1 import okio.HashingSink.Companion.sha256 import okio.HashingSink.Companion.sha512 -import kotlin.test.Test -import kotlin.test.assertEquals class HashingSinkTest { private val source = Buffer() diff --git a/okio/src/commonTest/kotlin/okio/HashingSourceTest.kt b/okio/src/commonTest/kotlin/okio/HashingSourceTest.kt index 83e2e264..7111edf4 100644 --- a/okio/src/commonTest/kotlin/okio/HashingSourceTest.kt +++ b/okio/src/commonTest/kotlin/okio/HashingSourceTest.kt @@ -15,15 +15,15 @@ */ package okio +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail import okio.HashingSource.Companion.hmacSha1 import okio.HashingSource.Companion.hmacSha256 import okio.HashingSource.Companion.hmacSha512 import okio.HashingSource.Companion.md5 import okio.HashingSource.Companion.sha1 import okio.HashingSource.Companion.sha256 -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.fail class HashingSourceTest { private val source = Buffer() @@ -82,11 +82,11 @@ class HashingSourceTest { val hashingSource = sha256(source) val bufferedSource = hashingSource.buffer() source.writeUtf8("a") - assertEquals('a'.toLong(), bufferedSource.readUtf8CodePoint().toLong()) + assertEquals('a'.code.toLong(), bufferedSource.readUtf8CodePoint().toLong()) source.writeUtf8("b") - assertEquals('b'.toLong(), bufferedSource.readUtf8CodePoint().toLong()) + assertEquals('b'.code.toLong(), bufferedSource.readUtf8CodePoint().toLong()) source.writeUtf8("c") - assertEquals('c'.toLong(), bufferedSource.readUtf8CodePoint().toLong()) + assertEquals('c'.code.toLong(), bufferedSource.readUtf8CodePoint().toLong()) assertEquals(HashingTest.SHA256_abc, hashingSource.hash) } diff --git a/okio/src/commonTest/kotlin/okio/HashingTest.kt b/okio/src/commonTest/kotlin/okio/HashingTest.kt index 1cae58d0..d290dc0c 100644 --- a/okio/src/commonTest/kotlin/okio/HashingTest.kt +++ b/okio/src/commonTest/kotlin/okio/HashingTest.kt @@ -15,10 +15,10 @@ */ package okio -import okio.ByteString.Companion.decodeHex -import okio.ByteString.Companion.encodeUtf8 import kotlin.test.Test import kotlin.test.assertEquals +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.encodeUtf8 class HashingTest { @Test fun byteStringMd5() { diff --git a/okio/src/commonTest/kotlin/okio/util.kt b/okio/src/commonTest/kotlin/okio/OkioTesting.kt index 953eef88..8dbdd2a1 100644 --- a/okio/src/commonTest/kotlin/okio/util.kt +++ b/okio/src/commonTest/kotlin/okio/OkioTesting.kt @@ -16,11 +16,6 @@ package okio import kotlin.random.Random -import kotlin.test.assertEquals - -fun Char.repeat(count: Int): String { - return toString().repeat(count) -} fun segmentSizes(buffer: Buffer): List<Int> { var segment = buffer.head ?: return emptyList() @@ -34,17 +29,6 @@ fun segmentSizes(buffer: Buffer): List<Int> { return sizes } -fun assertArrayEquals(a: ByteArray, b: ByteArray) { - assertEquals(a.contentToString(), b.contentToString()) -} - -fun randomBytes(length: Int): ByteString { - val random = Random(0) - val randomBytes = ByteArray(length) - random.nextBytes(randomBytes) - return ByteString.of(*randomBytes) -} - fun bufferWithRandomSegmentLayout(dice: Random, data: ByteArray): Buffer { val result = Buffer() @@ -92,3 +76,24 @@ fun makeSegments(source: ByteString): ByteString { } return buffer.snapshot() } + +/** + * Returns a string with all '\' slashes replaced with '/' slashes. This is useful for test + * assertions that intend to ignore slashes. + */ +fun Path.withUnixSlashes(): String { + return toString().replace('\\', '/') +} + +expect fun assertRelativeTo( + a: Path, + b: Path, + bRelativeToA: Path, + sameAsNio: Boolean = true, +) + +expect fun assertRelativeToFails( + a: Path, + b: Path, + sameAsNio: Boolean = true, +): IllegalArgumentException diff --git a/okio/src/commonTest/kotlin/okio/PathTest.kt b/okio/src/commonTest/kotlin/okio/PathTest.kt new file mode 100644 index 00000000..cb2920d7 --- /dev/null +++ b/okio/src/commonTest/kotlin/okio/PathTest.kt @@ -0,0 +1,777 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue +import okio.Path.Companion.toPath + +class PathTest { + @Test + fun unixRoot() { + val path = "/".toPath() + assertEquals(path, path.normalized()) + assertEquals(path, path.root) + assertEquals(listOf(), path.segments) + assertEquals("/", path.toString()) + assertNull(path.parent) + assertNull(path.volumeLetter) + assertEquals("", path.name) + assertTrue(path.isAbsolute) + assertTrue(path.isRoot) + } + + @Test + fun unixAbsolutePath() { + val path = "/home/jesse/todo.txt".toPath() + assertEquals(path, path.normalized()) + assertEquals("/".toPath(), path.root) + assertEquals(listOf("home", "jesse", "todo.txt"), path.segments) + assertEquals("/home/jesse/todo.txt", path.toString()) + assertEquals("/home/jesse".toPath(), path.parent) + assertNull(path.volumeLetter) + assertEquals("todo.txt", path.name) + assertTrue(path.isAbsolute) + assertFalse(path.isRoot) + } + + @Test + fun unixRelativePath() { + val path = "project/todo.txt".toPath() + assertEquals(path, path.normalized()) + assertNull(path.root) + assertEquals(listOf("project", "todo.txt"), path.segments) + assertEquals("project/todo.txt", path.toString()) + assertEquals("project".toPath(), path.parent) + assertNull(path.volumeLetter) + assertEquals("todo.txt", path.name) + assertFalse(path.isAbsolute) + assertFalse(path.isRoot) + } + + @Test + fun unixRelativePathWithDots() { + val path = "../../project/todo.txt".toPath() + assertEquals(path, path.normalized()) + assertNull(path.root) + assertEquals(listOf("..", "..", "project", "todo.txt"), path.segments) + assertEquals("../../project/todo.txt", path.toString()) + assertEquals("../../project".toPath(), path.parent) + assertNull(path.volumeLetter) + assertEquals("todo.txt", path.name) + assertFalse(path.isAbsolute) + assertFalse(path.isRoot) + } + + @Test + fun unixRelativeSeriesOfDotDots() { + val path = "../../..".toPath() + assertEquals(path, path.normalized()) + assertNull(path.root) + assertEquals(listOf("..", "..", ".."), path.segments) + assertEquals("../../..", path.toString()) + assertNull(path.parent) + assertNull(path.volumeLetter) + assertEquals("..", path.name) + assertFalse(path.isAbsolute) + assertFalse(path.isRoot) + } + + @Test + fun unixAbsoluteSeriesOfDotDots() { + val path = "/../../..".toPath() + assertEquals(path, path.normalized()) + assertEquals("/".toPath(), path.root) + assertEquals(listOf(), path.segments) + assertEquals("/", path.toString()) + assertNull(path.parent) + assertNull(path.volumeLetter) + assertEquals("", path.name) + assertTrue(path.isAbsolute) + assertTrue(path.isRoot) + } + + @Test + fun unixAbsoluteSingleDot() { + val path = "/.".toPath() + assertEquals(path, path.normalized()) + assertEquals("/".toPath(), path.root) + assertEquals(listOf(), path.segments) + assertEquals("/", path.toString()) + assertNull(path.parent) + assertNull(path.volumeLetter) + assertEquals("", path.name) + assertTrue(path.isAbsolute) + assertTrue(path.isRoot) + } + + @Test + fun unixRelativeDoubleDots() { + val path = "..".toPath() + assertEquals(path, path.normalized()) + assertNull(path.root) + assertEquals(listOf(".."), path.segments) + assertEquals("..", path.toString()) + assertNull(path.parent) + assertNull(path.volumeLetter) + assertEquals("..", path.name) + assertFalse(path.isAbsolute) + assertFalse(path.isRoot) + } + + @Test + fun unixRelativeSingleDot() { + val path = ".".toPath() + assertEquals(path, path.normalized()) + assertNull(path.root) + assertEquals(listOf("."), path.segments) + assertEquals(".", path.toString()) + assertNull(path.parent) + assertNull(path.volumeLetter) + assertEquals(".", path.name) + assertFalse(path.isAbsolute) + assertFalse(path.isRoot) + } + + @Test + fun windowsVolumeLetter() { + val path = "C:\\".toPath() + assertEquals(path, path.normalized()) + assertEquals("C:\\".toPath(), path.root) + assertEquals(listOf(), path.segments) + assertEquals("C:\\", path.toString()) + assertNull(path.parent) + assertEquals('C', path.volumeLetter) + assertEquals("", path.name) + assertTrue(path.isAbsolute) + assertTrue(path.isRoot) + } + + @Test + fun windowsAbsolutePathWithVolumeLetter() { + val path = "C:\\Windows\\notepad.exe".toPath() + assertEquals(path, path.normalized()) + assertEquals("C:\\".toPath(), path.root) + assertEquals(listOf("Windows", "notepad.exe"), path.segments) + assertEquals("C:\\Windows\\notepad.exe", path.toString()) + assertEquals("C:\\Windows".toPath(), path.parent) + assertEquals('C', path.volumeLetter) + assertEquals("notepad.exe", path.name) + assertTrue(path.isAbsolute) + assertFalse(path.isRoot) + } + + @Test + fun windowsAbsolutePath() { + val path = "\\".toPath() + assertEquals(path, path.normalized()) + assertEquals("\\".toPath(), path.root) + assertEquals(listOf(), path.segments) + assertEquals("\\", path.toString()) + assertEquals(null, path.parent) + assertNull(path.volumeLetter) + assertEquals("", path.name) + assertTrue(path.isAbsolute) + assertTrue(path.isRoot) + } + + @Test + fun windowsAbsolutePathWithoutVolumeLetter() { + val path = "\\Windows\\notepad.exe".toPath() + assertEquals(path, path.normalized()) + assertEquals("\\".toPath(), path.root) + assertEquals(listOf("Windows", "notepad.exe"), path.segments) + assertEquals("\\Windows\\notepad.exe", path.toString()) + assertEquals("\\Windows".toPath(), path.parent) + assertNull(path.volumeLetter) + assertEquals("notepad.exe", path.name) + assertTrue(path.isAbsolute) + assertFalse(path.isRoot) + } + + @Test + fun windowsRelativePathWithVolumeLetter() { + val path = "C:Windows\\notepad.exe".toPath() + assertEquals(path, path.normalized()) + assertNull(path.root) + assertEquals(listOf("C:Windows", "notepad.exe"), path.segments) + assertEquals("C:Windows\\notepad.exe", path.toString()) + assertEquals("C:Windows".toPath(), path.parent) + assertEquals('C', path.volumeLetter) + assertEquals("notepad.exe", path.name) + assertFalse(path.isAbsolute) + assertFalse(path.isRoot) + } + + @Test + fun windowsVolumeLetterRelative() { + val path = "C:".toPath() + assertEquals(path, path.normalized()) + assertNull(path.root) + assertEquals(listOf("C:"), path.segments) + assertEquals("C:", path.toString()) + assertNull(path.parent) + assertEquals('C', path.volumeLetter) + assertEquals("", path.name) + assertFalse(path.isAbsolute) + assertFalse(path.isRoot) + } + + @Test + fun windowsRelativePath() { + val path = "Windows\\notepad.exe".toPath() + assertEquals(path, path.normalized()) + assertNull(path.root) + assertEquals(listOf("Windows", "notepad.exe"), path.segments) + assertEquals("Windows\\notepad.exe", path.toString()) + assertEquals("Windows".toPath(), path.parent) + assertNull(path.volumeLetter) + assertEquals("notepad.exe", path.name) + assertFalse(path.isAbsolute) + assertFalse(path.isRoot) + } + + @Test + fun windowsUncServer() { + val path = "\\\\server".toPath() + assertEquals(path, path.normalized()) + assertEquals("\\\\server".toPath(), path.root) + assertEquals(listOf(), path.segments) + assertEquals("\\\\server", path.toString()) + assertNull(path.parent) + assertNull(path.volumeLetter) + assertEquals("server", path.name) + assertTrue(path.isAbsolute) + assertTrue(path.isRoot) + } + + @Test + fun windowsUncAbsolutePath() { + val path = "\\\\server\\project\\notes.txt".toPath() + assertEquals(path, path.normalized()) + assertEquals("\\\\server".toPath(), path.root) + assertEquals(listOf("project", "notes.txt"), path.segments) + assertEquals("\\\\server\\project\\notes.txt", path.toString()) + assertEquals("\\\\server\\project".toPath(), path.parent) + assertNull(path.volumeLetter) + assertEquals("notes.txt", path.name) + assertTrue(path.isAbsolute) + assertFalse(path.isRoot) + } + + @Test + fun absolutePathTraversalWithDivOperator() { + val root = "/".toPath() + assertEquals("/home".toPath(), root / "home") + assertEquals("/home/jesse".toPath(), root / "home" / "jesse") + assertEquals("/home/jesse/..".toPath(), root / "home" / "jesse" / "..") + assertEquals("/home/jesse/../jake".toPath(), root / "home" / "jesse" / ".." / "jake") + } + + @Test + fun relativePathTraversalWithDivOperator() { + val slash = Path.DIRECTORY_SEPARATOR + val cwd = ".".toPath() + assertEquals("home".toPath(), cwd / "home") + assertEquals("home${slash}jesse".toPath(), cwd / "home" / "jesse") + assertEquals("home${slash}jesse$slash..".toPath(), cwd / "home" / "jesse" / "..") + assertEquals( + "home${slash}jesse$slash..${slash}jake".toPath(), + cwd / "home" / "jesse" / ".." / "jake", + ) + } + + @Test + fun relativePathTraversalWithDots() { + val slash = Path.DIRECTORY_SEPARATOR + val cwd = ".".toPath() + assertEquals("..".toPath(), cwd / "..") + assertEquals("..$slash..".toPath(), cwd / ".." / "..") + assertEquals("..$slash..${slash}etc".toPath(), cwd / ".." / ".." / "etc") + assertEquals( + "..$slash..${slash}etc${slash}passwd".toPath(), + cwd / ".." / ".." / "etc" / "passwd", + ) + } + + @Test + fun pathTraversalBaseIgnoredIfChildIsAnAbsolutePath() { + assertEquals("/home".toPath(), "".toPath() / "/home") + assertEquals("/home".toPath(), "relative".toPath() / "/home") + assertEquals("/home".toPath(), "/base".toPath() / "/home") + assertEquals("/home".toPath(), "/".toPath() / "/home") + } + + @Test + fun stringToAbsolutePath() { + assertEquals("/", "/".toPath().toString()) + assertEquals("/a", "/a".toPath().toString()) + assertEquals("/a", "/a/".toPath().toString()) + assertEquals("/a/b/c", "/a/b/c".toPath().toString()) + assertEquals("/a/b/c", "/a/b/c/".toPath().toString()) + assertEquals("/", "/".toPath(normalize = true).toString()) + assertEquals("/a", "/a".toPath(normalize = true).toString()) + assertEquals("/a", "/a/".toPath(normalize = true).toString()) + assertEquals("/a/b/c", "/a/b/c".toPath(normalize = true).toString()) + assertEquals("/a/b/c", "/a/b/c/".toPath(normalize = true).toString()) + } + + @Test + fun stringToAbsolutePathWithTraversal() { + assertEquals("/", "/..".toPath().toString()) + assertEquals("/", "/../".toPath().toString()) + assertEquals("/", "/../..".toPath().toString()) + assertEquals("/", "/../../".toPath().toString()) + assertEquals("/", "/..".toPath(normalize = true).toString()) + assertEquals("/", "/../".toPath(normalize = true).toString()) + assertEquals("/", "/../..".toPath(normalize = true).toString()) + assertEquals("/", "/../../".toPath(normalize = true).toString()) + } + + @Test + fun stringToAbsolutePathWithEmptySegments() { + assertEquals("/", "//".toPath().toString()) + assertEquals("/a", "//a".toPath().toString()) + assertEquals("/a", "/a//".toPath().toString()) + assertEquals("/a", "//a//".toPath().toString()) + assertEquals("/a/b", "/a/b//".toPath().toString()) + assertEquals("/", "//".toPath(normalize = true).toString()) + assertEquals("/a", "//a".toPath(normalize = true).toString()) + assertEquals("/a", "/a//".toPath(normalize = true).toString()) + assertEquals("/a", "//a//".toPath(normalize = true).toString()) + assertEquals("/a/b", "/a/b//".toPath(normalize = true).toString()) + } + + @Test + fun stringToAbsolutePathWithDots() { + assertEquals("/", "/./".toPath().toString()) + assertEquals("/a", "/./a".toPath().toString()) + assertEquals("/a", "/a/./".toPath().toString()) + assertEquals("/a", "/a//.".toPath().toString()) + assertEquals("/a", "/./a//".toPath().toString()) + assertEquals("/a", "/a/.".toPath().toString()) + assertEquals("/a", "//a/./".toPath().toString()) + assertEquals("/a", "//a/./.".toPath().toString()) + assertEquals("/a/b", "/a/./b/".toPath().toString()) + assertEquals("/", "/./".toPath(normalize = true).toString()) + assertEquals("/a", "/./a".toPath(normalize = true).toString()) + assertEquals("/a", "/a/./".toPath(normalize = true).toString()) + assertEquals("/a", "/a//.".toPath(normalize = true).toString()) + assertEquals("/a", "/./a//".toPath(normalize = true).toString()) + assertEquals("/a", "/a/.".toPath(normalize = true).toString()) + assertEquals("/a", "//a/./".toPath(normalize = true).toString()) + assertEquals("/a", "//a/./.".toPath(normalize = true).toString()) + assertEquals("/a/b", "/a/./b/".toPath(normalize = true).toString()) + } + + @Test + fun stringToRelativePath() { + assertEquals(".", "".toPath().toString()) + assertEquals(".", ".".toPath().toString()) + assertEquals("a", "a/".toPath().toString()) + assertEquals("a/b", "a/b".toPath().toString()) + assertEquals("a/b", "a/b/".toPath().toString()) + assertEquals("a/b/c/d", "a/b/c/d".toPath().toString()) + assertEquals("a/b/c/d", "a/b/c/d/".toPath().toString()) + assertEquals(".", "".toPath(normalize = true).toString()) + assertEquals(".", ".".toPath(normalize = true).toString()) + assertEquals("a", "a/".toPath(normalize = true).toString()) + assertEquals("a/b", "a/b".toPath(normalize = true).toString()) + assertEquals("a/b", "a/b/".toPath(normalize = true).toString()) + assertEquals("a/b/c/d", "a/b/c/d".toPath(normalize = true).toString()) + assertEquals("a/b/c/d", "a/b/c/d/".toPath(normalize = true).toString()) + } + + @Test + fun stringToRelativePathWithTraversal() { + assertEquals("..", "..".toPath().toString()) + assertEquals("..", "../".toPath().toString()) + assertEquals("a/..", "a/..".toPath().toString()) + assertEquals("a/..", "a/../".toPath().toString()) + assertEquals("a/../..", "a/../..".toPath().toString()) + assertEquals("a/../..", "a/../../".toPath().toString()) + assertEquals("a/../../..", "a/../../..".toPath().toString()) + assertEquals("../../b", "../../b".toPath().toString()) + assertEquals("a/../../../b", "a/../../../b".toPath().toString()) + assertEquals("a/../../../b/../c", "a/../../../b/../c".toPath().toString()) + assertEquals("..", "..".toPath(normalize = true).toString()) + assertEquals("..", "../".toPath(normalize = true).toString()) + assertEquals(".", "a/..".toPath(normalize = true).toString()) + assertEquals(".", "a/../".toPath(normalize = true).toString()) + assertEquals("..", "a/../..".toPath(normalize = true).toString()) + assertEquals("..", "a/../../".toPath(normalize = true).toString()) + assertEquals("../..", "a/../../..".toPath(normalize = true).toString()) + assertEquals("../../b", "../../b".toPath(normalize = true).toString()) + assertEquals("../../b", "a/../../../b".toPath(normalize = true).toString()) + assertEquals("../../c", "a/../../../b/../c".toPath(normalize = true).toString()) + } + + @Test + fun stringToRelativePathWithEmptySegments() { + assertEquals("a", "a//".toPath().toString()) + assertEquals("a/b", "a//b".toPath().toString()) + assertEquals("a/b", "a/b//".toPath().toString()) + assertEquals("a/b", "a//b//".toPath().toString()) + assertEquals("a/b/c", "a/b/c//".toPath().toString()) + assertEquals("a", "a//".toPath(normalize = true).toString()) + assertEquals("a/b", "a//b".toPath(normalize = true).toString()) + assertEquals("a/b", "a/b//".toPath(normalize = true).toString()) + assertEquals("a/b", "a//b//".toPath(normalize = true).toString()) + assertEquals("a/b/c", "a/b/c//".toPath(normalize = true).toString()) + } + + @Test + fun stringToRelativePathWithDots() { + assertEquals(".", ".".toPath().toString()) + assertEquals(".", "./".toPath().toString()) + assertEquals(".", "././".toPath().toString()) + assertEquals("a/..", "././a/..".toPath().toString()) + assertEquals("a", "a/./".toPath().toString()) + assertEquals("a/b", "a/./b".toPath().toString()) + assertEquals("a/b", "a/b/./".toPath().toString()) + assertEquals("a/b", "a/b//.".toPath().toString()) + assertEquals("a/b", "a/./b//".toPath().toString()) + assertEquals("a/b", "a/b/.".toPath().toString()) + assertEquals("a/b", "a//b/./".toPath().toString()) + assertEquals("a/b", "a//b/./.".toPath().toString()) + assertEquals("a/b/c", "a/b/./c/".toPath().toString()) + assertEquals(".", ".".toPath(normalize = true).toString()) + assertEquals(".", "./".toPath(normalize = true).toString()) + assertEquals(".", "././".toPath(normalize = true).toString()) + assertEquals(".", "././a/..".toPath(normalize = true).toString()) + assertEquals("a", "a/./".toPath(normalize = true).toString()) + assertEquals("a/b", "a/./b".toPath(normalize = true).toString()) + assertEquals("a/b", "a/b/./".toPath(normalize = true).toString()) + assertEquals("a/b", "a/b//.".toPath(normalize = true).toString()) + assertEquals("a/b", "a/./b//".toPath(normalize = true).toString()) + assertEquals("a/b", "a/b/.".toPath(normalize = true).toString()) + assertEquals("a/b", "a//b/./".toPath(normalize = true).toString()) + assertEquals("a/b", "a//b/./.".toPath(normalize = true).toString()) + assertEquals("a/b/c", "a/b/./c/".toPath(normalize = true).toString()) + } + + @Test + fun composingWindowsPath() { + assertEquals("C:\\Windows\\notepad.exe".toPath(), "C:\\".toPath() / "Windows" / "notepad.exe") + } + + @Test + fun windowsResolveAbsolutePath() { + assertEquals("\\Users".toPath(), "C:\\Windows".toPath() / "\\Users") + } + + @Test + fun windowsPathTraversalUp() { + assertEquals("C:\\x\\y\\..\\..\\..\\z".toPath(), "C:\\x\\y\\..\\..\\..\\z".toPath()) + assertEquals("C:x\\y\\..\\..\\..\\z".toPath(), "C:x\\y\\..\\..\\..\\z".toPath()) + assertEquals("C:\\z".toPath(), "C:\\x\\y\\..\\..\\..\\z".toPath(normalize = true)) + assertEquals("C:..\\z".toPath(), "C:x\\y\\..\\..\\..\\z".toPath(normalize = true)) + } + + @Test + fun samePathDifferentSlashesAreNotEqual() { + assertNotEquals("/a".toPath(), "\\b".toPath()) + assertNotEquals("a/b".toPath(), "a\\b".toPath()) + } + + @Test + fun samePathNoSlashesAreEqual() { + assertEquals("a".toPath().parent!!, "a".toPath().parent!!) + assertEquals("a/b".toPath().parent!!, "a\\b".toPath().parent!!) + } + + @Test + fun relativeToWindowsPaths() { + val a = "C:\\Windows\\notepad.exe".toPath() + val b = "C:\\".toPath() + assertRelativeTo(a, b, "..\\..".toPath(), sameAsNio = false) + assertRelativeTo(b, a, "Windows\\notepad.exe".toPath(), sameAsNio = false) + + val c = "C:\\Windows\\".toPath() + val d = "C:\\Windows".toPath() + assertRelativeTo(c, d, ".".toPath()) + assertRelativeTo(d, c, ".".toPath()) + + val e = "C:\\Windows\\Downloads\\".toPath() + val f = "C:\\Windows\\Documents\\Hello.txt".toPath() + assertRelativeTo(e, f, "..\\Documents\\Hello.txt".toPath(), sameAsNio = false) + assertRelativeTo(f, e, "..\\..\\Downloads".toPath(), sameAsNio = false) + + val g = "C:\\Windows\\".toPath() + val h = "D:\\Windows\\".toPath() + assertRelativeToFails(g, h, sameAsNio = false) + assertRelativeToFails(h, g, sameAsNio = false) + } + + @Test + fun relativeToWindowsUncPaths() { + val a = "\\\\localhost\\c$\\development\\schema.proto".toPath() + val b = "\\\\localhost\\c$\\project\\notes.txt".toPath() + assertRelativeTo(a, b, "..\\..\\project\\notes.txt".toPath(), sameAsNio = false) + assertRelativeTo(b, a, "..\\..\\development\\schema.proto".toPath(), sameAsNio = false) + + val c = "C:\\Windows\\".toPath() + val d = "\\\\localhost\\c$\\project\\notes.txt".toPath() + assertRelativeToFails(c, d, sameAsNio = false) + assertRelativeToFails(d, c, sameAsNio = false) + } + + @Test + fun absoluteUnixRoot() { + val a = "/Users/jesse/hello.txt".toPath() + val b = "/".toPath() + assertRelativeTo(a, b, "../../..".toPath()) + assertRelativeTo(b, a, "Users/jesse/hello.txt".toPath()) + + val c = "/Users/jesse/hello.txt".toPath() + val d = "/Admin/Secret".toPath() + assertRelativeTo(c, d, "../../../Admin/Secret".toPath()) + assertRelativeTo(d, c, "../../Users/jesse/hello.txt".toPath()) + + val e = "/Users/".toPath() + val f = "/Users".toPath() + assertRelativeTo(e, f, ".".toPath()) + assertRelativeTo(f, e, ".".toPath()) + } + + // Note that we handle the normalized version of the paths when computing relative paths. + @Test + fun relativeToUnnormalizedPath() { + val a = "Users/../a".toPath() // `a` if normalized. + val b = "Users/b/../c".toPath() // `Users/c` if normalized. + assertRelativeToFails(a, b, sameAsNio = false) + assertRelativeToFails(b, a, sameAsNio = false) + assertRelativeTo(a.normalized(), b.normalized(), "../Users/c".toPath()) + assertRelativeTo(b.normalized(), a.normalized(), "../../a".toPath()) + } + + @Test + fun relativeToNormalizedPath() { + val a = "Users/../a".toPath(normalize = true) // results to `a`. + val b = "Users/b/../c".toPath(normalize = true) // results to `Users/c`. + assertRelativeTo(a, b, "../Users/c".toPath()) + assertRelativeTo(b, a, "../../a".toPath()) + } + + @Test + fun absoluteToRelative() { + val a = "/Users/jesse/hello.txt".toPath() + val b = "Desktop/goodbye.txt".toPath() + + var exception = assertRelativeToFails(a, b) + assertEquals( + "Paths of different roots cannot be relative to each other: " + + "Desktop/goodbye.txt and /Users/jesse/hello.txt", + exception.message, + ) + + exception = assertRelativeToFails(b, a) + assertEquals( + "Paths of different roots cannot be relative to each other: " + + "/Users/jesse/hello.txt and Desktop/goodbye.txt", + exception.message, + ) + } + + @Test + fun absoluteToAbsolute() { + val a = "/Users/jesse/hello.txt".toPath() + val b = "/Users/benoit/Desktop/goodbye.txt".toPath() + assertRelativeTo(a, b, "../../benoit/Desktop/goodbye.txt".toPath()) + assertRelativeTo(b, a, "../../../jesse/hello.txt".toPath()) + } + + @Test + fun absoluteToSelf() { + val a = "/Users/jesse/hello.txt".toPath() + assertRelativeTo(a, a, ".".toPath()) + + val b = "/Users/benoit/../jesse/hello.txt".toPath() + // NIO normalizes. + assertRelativeTo(a, b, "../../benoit/../jesse/hello.txt".toPath(), sameAsNio = false) + assertRelativeToFails(b, a, sameAsNio = false) + assertRelativeTo(b.normalized(), a, ".".toPath()) + assertRelativeTo(a, b.normalized(), ".".toPath()) + } + + @Test + fun relativeToSelf() { + val a = "Desktop/hello.txt".toPath() + assertRelativeTo(a, a, ".".toPath()) + + val b = "Documents/../Desktop/hello.txt".toPath() + // NIO normalizes. + assertRelativeTo(a, b, "../../Documents/../Desktop/hello.txt".toPath(), sameAsNio = false) + assertRelativeToFails(b, a, sameAsNio = false) + assertRelativeTo(a, b.normalized(), ".".toPath()) + assertRelativeTo(b.normalized(), a, ".".toPath()) + } + + @Test + fun relativeToRelative() { + val a = "Desktop/documents/resume.txt".toPath() + val b = "Desktop/documents/2021/taxes.txt".toPath() + assertRelativeTo(a, b, "../2021/taxes.txt".toPath(), sameAsNio = false) + assertRelativeTo(b, a, "../../resume.txt".toPath(), sameAsNio = false) + + val c = "documents/resume.txt".toPath() + val d = "downloads/2021/taxes.txt".toPath() + assertRelativeTo(c, d, "../../downloads/2021/taxes.txt".toPath(), sameAsNio = false) + assertRelativeTo(d, c, "../../../documents/resume.txt".toPath(), sameAsNio = false) + } + + @Test + fun relativeToRelativeWithMiddleDots() { + val a = "Desktop/documents/a...n".toPath() + val b = "Desktop/documents/m...z".toPath() + assertRelativeTo(a, b, "../m...z".toPath()) + assertRelativeTo(b, a, "../a...n".toPath()) + } + + @Test + fun relativeToRelativeWithMiddleDotsInCommonPrefix() { + val a = "Desktop/documents/a...n/red".toPath() + val b = "Desktop/documents/a...m/blue".toPath() + assertRelativeTo(a, b, "../../a...m/blue".toPath()) + assertRelativeTo(b, a, "../../a...n/red".toPath()) + } + + @Test + fun relativeToRelativeWithUpNavigationPrefix() { + // We can't navigate from 'taxes' to 'resumes' because we don't know the name of 'Documents'. + // /Users/jwilson/Documents/2021/Current + // /Users/jwilson/Documents/resumes + // /Users/jwilson/taxes + val a = "../../resumes".toPath() + val b = "../../../taxes".toPath() + assertRelativeTo(a, b, "../../taxes".toPath()) + assertRelativeToFails(b, a, sameAsNio = false) + } + + @Test + fun relativeToRelativeDifferentSlashes() { + val a = "Desktop/documents/resume.txt".toPath() + val b = "Desktop\\documents\\2021\\taxes.txt".toPath() + assertRelativeTo(a, b, "../2021/taxes.txt".toPath(), sameAsNio = false) + assertRelativeTo(b, a, "..\\..\\resume.txt".toPath(), sameAsNio = false) + + val c = "documents/resume.txt".toPath() + val d = "downloads\\2021\\taxes.txt".toPath() + assertRelativeTo(c, d, "../../downloads/2021/taxes.txt".toPath(), sameAsNio = false) + assertRelativeTo(d, c, "..\\..\\..\\documents\\resume.txt".toPath(), sameAsNio = false) + } + + @Test + fun windowsUncPathsDoNotDotDot() { + assertEquals( + """\\localhost\c$\Windows""", + """\\localhost\c$\Windows""".toPath().toString(), + ) + assertEquals( + """\\127.0.0.1\c$\Windows""", + """\\127.0.0.1\c$\Windows""".toPath().toString(), + ) + assertEquals( + """\\127.0.0.1\c$\Windows\..\Windows""", + """\\127.0.0.1\c$\Windows\..\Windows""".toPath().toString(), + ) + assertEquals( + """\\127.0.0.1\..\localhost\c$\Windows""", + """\\127.0.0.1\..\localhost\c$\Windows""".toPath().toString(), + ) + assertEquals( + """\\127.0.0.1\c$\..\d$""", + """\\127.0.0.1\c$\..\d$""".toPath().toString(), + ) + + assertEquals( + """\\localhost\c$\Windows""", + """\\localhost\c$\Windows""".toPath(normalize = true).toString(), + ) + assertEquals( + """\\127.0.0.1\c$\Windows""", + """\\127.0.0.1\c$\Windows""".toPath(normalize = true).toString(), + ) + assertEquals( + """\\127.0.0.1\c$\Windows""", + """\\127.0.0.1\c$\Windows\..\Windows""".toPath(normalize = true).toString(), + ) + assertEquals( + """\\127.0.0.1\localhost\c$\Windows""", + """\\127.0.0.1\..\localhost\c$\Windows""".toPath(normalize = true).toString(), + ) + assertEquals( + """\\127.0.0.1\d$""", + """\\127.0.0.1\c$\..\d$""".toPath(normalize = true).toString(), + ) + assertEquals( + """\\127.0.0.1\c$""", + """\\..\127.0.0.1\..\c$""".toPath(normalize = true).toString(), + ) + } + + @Test fun normalizeAbsolute() { + assertEquals("/", "/.".toPath(normalize = true).toString()) + assertEquals("/", "/.".toPath(normalize = false).toString()) + assertEquals("/", "/..".toPath(normalize = true).toString()) + assertEquals("/", "/..".toPath(normalize = false).toString()) + assertEquals("/", "/../..".toPath(normalize = true).toString()) + assertEquals("/", "/../..".toPath(normalize = false).toString()) + + assertEquals("/a/b", "/a/./b".toPath(normalize = true).toString()) + assertEquals("/a/b", "/a/./b".toPath(normalize = false).toString()) + assertEquals("/a/.../b", "/a/..././b".toPath(normalize = true).toString()) + assertEquals("/a/.../b", "/a/..././b".toPath(normalize = false).toString()) + assertEquals("/", "/a/..".toPath(normalize = true).toString()) + assertEquals("/a/..", "/a/..".toPath(normalize = false).toString()) + assertEquals("/b", "/../a/../b".toPath(normalize = true).toString()) + assertEquals("/a/../b", "/../a/../b".toPath(normalize = false).toString()) + } + + @Test fun normalizeRelative() { + assertEquals(".", ".".toPath(normalize = true).toString()) + assertEquals(".", ".".toPath(normalize = false).toString()) + assertEquals("..", "..".toPath(normalize = true).toString()) + assertEquals("..", "..".toPath(normalize = false).toString()) + assertEquals("../..", "../..".toPath(normalize = true).toString()) + assertEquals("../..", "../..".toPath(normalize = false).toString()) + + assertEquals("a/b", "a/./b".toPath(normalize = true).toString()) + assertEquals("a/b", "a/./b".toPath(normalize = false).toString()) + assertEquals("a/.../b", "a/..././b".toPath(normalize = true).toString()) + assertEquals("a/.../b", "a/..././b".toPath(normalize = false).toString()) + assertEquals(".", "a/..".toPath(normalize = true).toString()) + assertEquals("a/..", "a/..".toPath(normalize = false).toString()) + assertEquals("../b", "../a/../b".toPath(normalize = true).toString()) + assertEquals("../a/../b", "../a/../b".toPath(normalize = false).toString()) + } + + @Test fun normalized() { + val normalizedRoot = "/".toPath() + assertEquals(normalizedRoot, "/a/..".toPath(normalize = true)) + assertEquals(normalizedRoot, "/a/..".toPath(normalize = false).normalized()) + assertEquals(normalizedRoot, "/a/..".toPath(normalize = true).normalized()) + + val normalizedRelative = "../b".toPath() + assertEquals(normalizedRelative, "../a/../b".toPath(normalize = true)) + assertEquals(normalizedRelative, "../a/../b".toPath(normalize = false).normalized()) + assertEquals(normalizedRelative, "../a/../b".toPath(normalize = true).normalized()) + } +} diff --git a/okio/src/commonTest/kotlin/okio/UnsafeCursorTest.kt b/okio/src/commonTest/kotlin/okio/UnsafeCursorTest.kt index 680ebdc2..10290eac 100644 --- a/okio/src/commonTest/kotlin/okio/UnsafeCursorTest.kt +++ b/okio/src/commonTest/kotlin/okio/UnsafeCursorTest.kt @@ -17,6 +17,7 @@ package okio import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue class UnsafeCursorTest { @Test fun acquireForRead() { @@ -43,7 +44,7 @@ class UnsafeCursorTest { val cursor = buffer.readAndWriteUnsafe() try { while (cursor.next() != -1) { - cursor.data!!.fill('z'.toByte(), cursor.start, cursor.end) + cursor.data!!.fill('z'.code.toByte(), cursor.start, cursor.end) } } finally { cursor.close() @@ -58,7 +59,7 @@ class UnsafeCursorTest { val cursor = buffer.readAndWriteUnsafe() try { cursor.expandBuffer(100) - cursor.data!!.fill('z'.toByte(), cursor.start, cursor.start + 100) + cursor.data!!.fill('z'.code.toByte(), cursor.start, cursor.start + 100) cursor.resizeBuffer(100L) } finally { cursor.close() @@ -77,11 +78,15 @@ class UnsafeCursorTest { val cursor = buffer.readAndWriteUnsafe() try { cursor.resizeBuffer(100L) - cursor.data!!.fill('z'.toByte(), cursor.start, cursor.end) + cursor.data!!.fill('z'.code.toByte(), cursor.start, cursor.end) } finally { cursor.close() } assertEquals("z".repeat(100), buffer.readUtf8()) } + + @Test fun testUnsafeCursorIsClosable() { + assertTrue(Closeable::class.isInstance(Buffer.UnsafeCursor())) + } } diff --git a/okio/src/commonTest/kotlin/okio/Utf8KotlinTest.kt b/okio/src/commonTest/kotlin/okio/Utf8KotlinTest.kt index 1ade4179..337fe83a 100644 --- a/okio/src/commonTest/kotlin/okio/Utf8KotlinTest.kt +++ b/okio/src/commonTest/kotlin/okio/Utf8KotlinTest.kt @@ -16,17 +16,17 @@ package okio -import okio.ByteString.Companion.decodeHex -import okio.internal.commonAsUtf8ToByteArray import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import okio.ByteString.Companion.decodeHex +import okio.internal.commonAsUtf8ToByteArray class Utf8KotlinTest { @Test fun oneByteCharacters() { assertEncoded("00", 0x00) // Smallest 1-byte character. - assertEncoded("20", ' '.toInt()) - assertEncoded("7e", '~'.toInt()) + assertEncoded("20", ' '.code) + assertEncoded("7e", '~'.code) assertEncoded("7f", 0x7f) // Largest 1-byte character. } @@ -90,10 +90,10 @@ class Utf8KotlinTest { @Test fun highSurrogateFollowedByNonSurrogate() { assertStringEncoded("3fee8080", "\ud800\ue000") // "?\ue000": Following character is too high. - assertCodePointDecoded("f090ee8080", REPLACEMENT_CODE_POINT, '\ue000'.toInt()) + assertCodePointDecoded("f090ee8080", REPLACEMENT_CODE_POINT, '\ue000'.code) assertStringEncoded("3f61", "\ud800\u0061") // "?a": Following character is too low. - assertCodePointDecoded("f09061", REPLACEMENT_CODE_POINT, 'a'.toInt()) + assertCodePointDecoded("f09061", REPLACEMENT_CODE_POINT, 'a'.code) } @Test fun doubleLowSurrogate() { @@ -113,7 +113,7 @@ class Utf8KotlinTest { @Test fun writeSurrogateCodePoint() { assertStringEncoded("ed9fbf", "\ud7ff") // Below lowest surrogate is okay. - assertCodePointDecoded("ed9fbf", '\ud7ff'.toInt()) + assertCodePointDecoded("ed9fbf", '\ud7ff'.code) assertStringEncoded("3f", "\ud800") // Lowest surrogate gets '?'. assertCodePointDecoded("eda080", REPLACEMENT_CODE_POINT) @@ -122,7 +122,7 @@ class Utf8KotlinTest { assertCodePointDecoded("edbfbf", REPLACEMENT_CODE_POINT) assertStringEncoded("ee8080", "\ue000") // Above highest surrogate is okay. - assertCodePointDecoded("ee8080", '\ue000'.toInt()) + assertCodePointDecoded("ee8080", '\ue000'.code) } @Test fun size() { diff --git a/okio/src/hashFunctions/kotlin/okio/internal/HashFunction.kt b/okio/src/hashFunctions/kotlin/okio/internal/HashFunction.kt index 5948ab0f..870d0a68 100644 --- a/okio/src/hashFunctions/kotlin/okio/internal/HashFunction.kt +++ b/okio/src/hashFunctions/kotlin/okio/internal/HashFunction.kt @@ -20,7 +20,7 @@ internal interface HashFunction { fun update( input: ByteArray, offset: Int = 0, - byteCount: Int = input.size + byteCount: Int = input.size, ) fun digest(): ByteArray diff --git a/okio/src/hashFunctions/kotlin/okio/internal/Hmac.kt b/okio/src/hashFunctions/kotlin/okio/internal/Hmac.kt index 95e3c5dd..b8c7231b 100644 --- a/okio/src/hashFunctions/kotlin/okio/internal/Hmac.kt +++ b/okio/src/hashFunctions/kotlin/okio/internal/Hmac.kt @@ -20,7 +20,7 @@ import okio.xor internal class Hmac private constructor( private val hashFunction: HashFunction, - private val outerKey: ByteArray + private val outerKey: ByteArray, ) : HashFunction { override fun update(input: ByteArray, offset: Int, byteCount: Int) { hashFunction.update(input, offset, byteCount) @@ -51,7 +51,7 @@ internal class Hmac private constructor( private fun create( key: ByteString, hashFunction: HashFunction, - blockLength: Int + blockLength: Int, ): Hmac { val keySize = key.size val paddedKey = when { @@ -68,7 +68,7 @@ internal class Hmac private constructor( return Hmac( hashFunction, - outerKey + outerKey, ) } } diff --git a/okio/src/hashFunctions/kotlin/okio/internal/Md5.kt b/okio/src/hashFunctions/kotlin/okio/internal/Md5.kt index e43e4476..678643d7 100644 --- a/okio/src/hashFunctions/kotlin/okio/internal/Md5.kt +++ b/okio/src/hashFunctions/kotlin/okio/internal/Md5.kt @@ -31,7 +31,7 @@ internal class Md5 : HashFunction { override fun update( input: ByteArray, offset: Int, - byteCount: Int + byteCount: Int, ) { messageLength += byteCount var pos = offset @@ -188,7 +188,7 @@ internal class Md5 : HashFunction { private val s = intArrayOf( 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, - 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21 + 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, ) private val k = intArrayOf( @@ -200,7 +200,7 @@ internal class Md5 : HashFunction { -155497632, -1094730640, 681279174, -358537222, -722521979, 76029189, -640364487, -421815835, 530742520, -995338651, -198630844, 1126891415, -1416354905, -57434055, 1700485571, -1894986606, -1051523, -2054922799, 1873313359, -30611744, -1560198380, 1309151649, - -145523070, -1120210379, 718787259, -343485551 + -145523070, -1120210379, 718787259, -343485551, ) } } diff --git a/okio/src/hashFunctions/kotlin/okio/internal/Sha1.kt b/okio/src/hashFunctions/kotlin/okio/internal/Sha1.kt index e9a8de16..b1fc5b7d 100644 --- a/okio/src/hashFunctions/kotlin/okio/internal/Sha1.kt +++ b/okio/src/hashFunctions/kotlin/okio/internal/Sha1.kt @@ -32,7 +32,7 @@ internal class Sha1 : HashFunction { override fun update( input: ByteArray, offset: Int, - byteCount: Int + byteCount: Int, ) { messageLength += byteCount var pos = offset diff --git a/okio/src/hashFunctions/kotlin/okio/internal/Sha256.kt b/okio/src/hashFunctions/kotlin/okio/internal/Sha256.kt index aa0d24d0..550ab2d8 100644 --- a/okio/src/hashFunctions/kotlin/okio/internal/Sha256.kt +++ b/okio/src/hashFunctions/kotlin/okio/internal/Sha256.kt @@ -35,7 +35,7 @@ internal class Sha256 : HashFunction { override fun update( input: ByteArray, offset: Int, - byteCount: Int + byteCount: Int, ) { messageLength += byteCount var pos = offset @@ -100,7 +100,7 @@ internal class Sha256 : HashFunction { } private fun hash( - words: IntArray + words: IntArray, ) { val localK = k var a = h0 @@ -247,7 +247,7 @@ internal class Sha256 : HashFunction { 1986661051, -2117940946, -1838011259, -1564481375, -1474664885, -1035236496, -949202525, -778901479, -694614492, -200395387, 275423344, 430227734, 506948616, 659060556, 883997877, 958139571, 1322822218, 1537002063, 1747873779, 1955562222, 2024104815, -2067236844, - -1933114872, -1866530822, -1538233109, -1090935817, -965641998 + -1933114872, -1866530822, -1538233109, -1090935817, -965641998, ) } } diff --git a/okio/src/hashFunctions/kotlin/okio/internal/Sha512.kt b/okio/src/hashFunctions/kotlin/okio/internal/Sha512.kt index 390a50a1..86a314b2 100644 --- a/okio/src/hashFunctions/kotlin/okio/internal/Sha512.kt +++ b/okio/src/hashFunctions/kotlin/okio/internal/Sha512.kt @@ -35,7 +35,7 @@ internal class Sha512 : HashFunction { override fun update( input: ByteArray, offset: Int, - byteCount: Int + byteCount: Int, ) { messageLength += byteCount var pos = offset @@ -285,7 +285,7 @@ internal class Sha512 : HashFunction { -3880063495543823972L, -3348786107499101689L, -1523767162380948706L, -757361751448694408L, 500013540394364858L, 748580250866718886L, 1242879168328830382L, 1977374033974150939L, 2944078676154940804L, 3659926193048069267L, 4368137639120453308L, 4836135668995329356L, - 5532061633213252278L, 6448918945643986474L, 6902733635092675308L, 7801388544844847127L + 5532061633213252278L, 6448918945643986474L, 6902733635092675308L, 7801388544844847127L, ) } } diff --git a/okio/src/jsMain/kotlin/okio/FileSystem.kt b/okio/src/jsMain/kotlin/okio/FileSystem.kt new file mode 100644 index 00000000..ea1bb4bf --- /dev/null +++ b/okio/src/jsMain/kotlin/okio/FileSystem.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 okio.Path.Companion.toPath +import okio.internal.commonCopy +import okio.internal.commonCreateDirectories +import okio.internal.commonDeleteRecursively +import okio.internal.commonExists +import okio.internal.commonListRecursively +import okio.internal.commonMetadata + +actual abstract class FileSystem { + actual abstract fun canonicalize(path: Path): Path + + actual fun metadata(path: Path): FileMetadata = commonMetadata(path) + + actual abstract fun metadataOrNull(path: Path): FileMetadata? + + actual fun exists(path: Path): Boolean = commonExists(path) + + actual abstract fun list(dir: Path): List<Path> + + actual abstract fun listOrNull(dir: Path): List<Path>? + + actual open fun listRecursively(dir: Path, followSymlinks: Boolean): Sequence<Path> = + commonListRecursively(dir, followSymlinks) + + actual abstract fun openReadOnly(file: Path): FileHandle + + actual abstract fun openReadWrite( + file: Path, + mustCreate: Boolean, + mustExist: Boolean, + ): FileHandle + + actual abstract fun source(file: Path): Source + + actual inline fun <T> read(file: Path, readerAction: BufferedSource.() -> T): T { + return source(file).buffer().use { + it.readerAction() + } + } + + actual abstract fun sink(file: Path, mustCreate: Boolean): Sink + + actual inline fun <T> write( + file: Path, + mustCreate: Boolean, + writerAction: BufferedSink.() -> T, + ): T { + return sink(file, mustCreate).buffer().use { + it.writerAction() + } + } + + actual abstract fun appendingSink(file: Path, mustExist: Boolean): Sink + + actual abstract fun createDirectory(dir: Path, mustCreate: Boolean) + + actual fun createDirectories(dir: Path, mustCreate: Boolean): Unit = commonCreateDirectories(dir, mustCreate) + + actual abstract fun atomicMove(source: Path, target: Path) + + actual open fun copy(source: Path, target: Path): Unit = commonCopy(source, target) + + actual abstract fun delete(path: Path, mustExist: Boolean) + + actual open fun deleteRecursively(fileOrDirectory: Path, mustExist: Boolean): Unit = + commonDeleteRecursively(fileOrDirectory, mustExist) + + actual abstract fun createSymlink(source: Path, target: Path) + + actual companion object { + actual val SYSTEM_TEMPORARY_DIRECTORY: Path = tmpdir.toPath() + } +} diff --git a/okio/src/jsMain/kotlin/okio/JsPlatform.kt b/okio/src/jsMain/kotlin/okio/JsPlatform.kt new file mode 100644 index 00000000..04350d15 --- /dev/null +++ b/okio/src/jsMain/kotlin/okio/JsPlatform.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 + +/* + * This file exposes Node.js `os` and `path` APIs to Kotlin/JS, using reasonable default behaviors + * if those symbols aren't available. + * + * This was originally implemented using a Kotlin/JS [JsModule], but that broke browser builds + * because modules weren't available to browsers. + * + * https://nodejs.org/api/os.html + * https://github.com/browserify/path-browserify/blob/master/index.js + * https://github.com/CoderPuppy/os-browserify/blob/master/browser.js + */ + +internal actual val PLATFORM_DIRECTORY_SEPARATOR: String + get() = (path?.sep) as? String ?: "/" + +private val os: dynamic + get() { + return try { + js("require('os')") + } catch (t: Throwable) { + null + } + } + +private val path: dynamic + get() { + return try { + js("require('path')") + } catch (t: Throwable) { + null + } + } + +internal val tmpdir: String + get() = os?.tmpdir() as? String ?: "/tmp" diff --git a/okio/src/jvmMain/kotlin/okio/-DeprecatedOkio.kt b/okio/src/jvmMain/kotlin/okio/-DeprecatedOkio.kt index 7b6835e1..db94f869 100644 --- a/okio/src/jvmMain/kotlin/okio/-DeprecatedOkio.kt +++ b/okio/src/jvmMain/kotlin/okio/-DeprecatedOkio.kt @@ -21,7 +21,7 @@ import java.io.InputStream import java.io.OutputStream import java.net.Socket import java.nio.file.OpenOption -import java.nio.file.Path +import java.nio.file.Path as NioPath @Deprecated(message = "changed in Okio 2.x") object `-DeprecatedOkio` { @@ -29,9 +29,9 @@ object `-DeprecatedOkio` { message = "moved to extension function", replaceWith = ReplaceWith( expression = "file.appendingSink()", - imports = ["okio.appendingSink"] + imports = ["okio.appendingSink"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun appendingSink(file: File) = file.appendingSink() @@ -39,9 +39,9 @@ object `-DeprecatedOkio` { message = "moved to extension function", replaceWith = ReplaceWith( expression = "sink.buffer()", - imports = ["okio.buffer"] + imports = ["okio.buffer"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun buffer(sink: Sink) = sink.buffer() @@ -49,9 +49,9 @@ object `-DeprecatedOkio` { message = "moved to extension function", replaceWith = ReplaceWith( expression = "source.buffer()", - imports = ["okio.buffer"] + imports = ["okio.buffer"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun buffer(source: Source) = source.buffer() @@ -59,9 +59,9 @@ object `-DeprecatedOkio` { message = "moved to extension function", replaceWith = ReplaceWith( expression = "file.sink()", - imports = ["okio.sink"] + imports = ["okio.sink"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun sink(file: File) = file.sink() @@ -69,9 +69,9 @@ object `-DeprecatedOkio` { message = "moved to extension function", replaceWith = ReplaceWith( expression = "outputStream.sink()", - imports = ["okio.sink"] + imports = ["okio.sink"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun sink(outputStream: OutputStream) = outputStream.sink() @@ -79,19 +79,19 @@ object `-DeprecatedOkio` { message = "moved to extension function", replaceWith = ReplaceWith( expression = "path.sink(*options)", - imports = ["okio.sink"] + imports = ["okio.sink"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) - fun sink(path: Path, vararg options: OpenOption) = path.sink(*options) + fun sink(path: NioPath, vararg options: OpenOption) = path.sink(*options) @Deprecated( message = "moved to extension function", replaceWith = ReplaceWith( expression = "socket.sink()", - imports = ["okio.sink"] + imports = ["okio.sink"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun sink(socket: Socket) = socket.sink() @@ -99,9 +99,9 @@ object `-DeprecatedOkio` { message = "moved to extension function", replaceWith = ReplaceWith( expression = "file.source()", - imports = ["okio.source"] + imports = ["okio.source"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun source(file: File) = file.source() @@ -109,9 +109,9 @@ object `-DeprecatedOkio` { message = "moved to extension function", replaceWith = ReplaceWith( expression = "inputStream.source()", - imports = ["okio.source"] + imports = ["okio.source"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun source(inputStream: InputStream) = inputStream.source() @@ -119,19 +119,19 @@ object `-DeprecatedOkio` { message = "moved to extension function", replaceWith = ReplaceWith( expression = "path.source(*options)", - imports = ["okio.source"] + imports = ["okio.source"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) - fun source(path: Path, vararg options: OpenOption) = path.source(*options) + fun source(path: NioPath, vararg options: OpenOption) = path.source(*options) @Deprecated( message = "moved to extension function", replaceWith = ReplaceWith( expression = "socket.source()", - imports = ["okio.source"] + imports = ["okio.source"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun source(socket: Socket) = socket.source() @@ -139,9 +139,9 @@ object `-DeprecatedOkio` { message = "moved to extension function", replaceWith = ReplaceWith( expression = "blackholeSink()", - imports = ["okio.blackholeSink"] + imports = ["okio.blackholeSink"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun blackhole() = blackholeSink() } diff --git a/okio/src/jvmMain/kotlin/okio/-DeprecatedUtf8.kt b/okio/src/jvmMain/kotlin/okio/-DeprecatedUtf8.kt index b4bc7574..3f123c8e 100644 --- a/okio/src/jvmMain/kotlin/okio/-DeprecatedUtf8.kt +++ b/okio/src/jvmMain/kotlin/okio/-DeprecatedUtf8.kt @@ -22,9 +22,9 @@ object `-DeprecatedUtf8` { message = "moved to extension function", replaceWith = ReplaceWith( expression = "string.utf8Size()", - imports = ["okio.utf8Size"] + imports = ["okio.utf8Size"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun size(string: String) = string.utf8Size() @@ -32,9 +32,9 @@ object `-DeprecatedUtf8` { message = "moved to extension function", replaceWith = ReplaceWith( expression = "string.utf8Size(beginIndex, endIndex)", - imports = ["okio.utf8Size"] + imports = ["okio.utf8Size"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun size(string: String, beginIndex: Int, endIndex: Int) = string.utf8Size(beginIndex, endIndex) } diff --git a/okio/src/jvmMain/kotlin/okio/-Platform.kt b/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt index 4edb3ce0..cf263c45 100644 --- a/okio/src/jvmMain/kotlin/okio/-Platform.kt +++ b/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt @@ -1,3 +1,4 @@ +// ktlint-disable filename /* * Copyright (C) 2018 Square, Inc. * @@ -13,10 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -@file:JvmName("-Platform") package okio +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock as jvmWithLock + internal actual fun ByteArray.toUtf8String(): String = String(this, Charsets.UTF_8) internal actual fun String.asUtf8ToByteArray(): ByteArray = toByteArray(Charsets.UTF_8) @@ -24,12 +26,16 @@ internal actual fun String.asUtf8ToByteArray(): ByteArray = toByteArray(Charsets // TODO remove if https://youtrack.jetbrains.com/issue/KT-20641 provides a better solution actual typealias ArrayIndexOutOfBoundsException = java.lang.ArrayIndexOutOfBoundsException -internal actual inline fun <R> synchronized(lock: Any, block: () -> R): R { - return kotlin.synchronized(lock, block) -} +actual typealias Lock = ReentrantLock + +internal actual fun newLock(): Lock = ReentrantLock() + +actual inline fun <T> Lock.withLock(action: () -> T): T = jvmWithLock(action) actual typealias IOException = java.io.IOException +actual typealias ProtocolException = java.net.ProtocolException + actual typealias EOFException = java.io.EOFException actual typealias FileNotFoundException = java.io.FileNotFoundException diff --git a/okio/src/jvmMain/kotlin/okio/AsyncTimeout.kt b/okio/src/jvmMain/kotlin/okio/AsyncTimeout.kt index 20778327..a9af3ba8 100644 --- a/okio/src/jvmMain/kotlin/okio/AsyncTimeout.kt +++ b/okio/src/jvmMain/kotlin/okio/AsyncTimeout.kt @@ -18,6 +18,9 @@ package okio import java.io.IOException import java.io.InterruptedIOException import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.Condition +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock /** * This timeout uses a background thread to take action exactly when the timeout occurs. Use this to @@ -25,19 +28,18 @@ import java.util.concurrent.TimeUnit * writing. * * Subclasses should override [timedOut] to take action when a timeout occurs. This method will be - * invoked by the shared watchdog thread so it should not do any long-running operations. Otherwise + * invoked by the shared watchdog thread so it should not do any long-running operations. Otherwise, * we risk starving other timeouts from being triggered. * * Use [sink] and [source] to apply this timeout to a stream. The returned value will apply the * timeout to each operation on the wrapped stream. * - * Callers should call [enter] before doing work that is subject to timeouts, and [exit] afterwards. + * Callers should call [enter] before doing work that is subject to timeouts, and [exit] afterward. * The return value of [exit] indicates whether a timeout was triggered. Note that the call to * [timedOut] is asynchronous, and may be called after [exit]. */ open class AsyncTimeout : Timeout() { - /** True if this node is currently in the queue. */ - private var inQueue = false + private var state = STATE_IDLE /** The next node in the linked list. */ private var next: AsyncTimeout? = null @@ -51,12 +53,38 @@ open class AsyncTimeout : Timeout() { if (timeoutNanos == 0L && !hasDeadline) { return // No timeout and no deadline? Don't bother with the queue. } - scheduleTimeout(this, timeoutNanos, hasDeadline) + + lock.withLock { + check(state == STATE_IDLE) { "Unbalanced enter/exit" } + state = STATE_IN_QUEUE + insertIntoQueue(this, timeoutNanos, hasDeadline) + } } /** Returns true if the timeout occurred. */ fun exit(): Boolean { - return cancelScheduledTimeout(this) + lock.withLock { + val oldState = this.state + state = STATE_IDLE + + if (oldState == STATE_IN_QUEUE) { + removeFromQueue(this) + return false + } else { + return oldState == STATE_TIMED_OUT + } + } + } + + override fun cancel() { + super.cancel() + + lock.withLock { + if (state == STATE_IN_QUEUE) { + removeFromQueue(this) + state = STATE_CANCELED + } + } } /** @@ -170,7 +198,7 @@ open class AsyncTimeout : Timeout() { return e } - private class Watchdog internal constructor() : Thread("Okio Watchdog") { + private class Watchdog : Thread("Okio Watchdog") { init { isDaemon = true } @@ -178,8 +206,8 @@ open class AsyncTimeout : Timeout() { override fun run() { while (true) { try { - var timedOut: AsyncTimeout? = null - synchronized(AsyncTimeout::class.java) { + var timedOut: AsyncTimeout? + lock.withLock { timedOut = awaitTimeout() // The queue is completely empty. Let this thread exit and let another watchdog thread @@ -198,7 +226,10 @@ open class AsyncTimeout : Timeout() { } } - companion object { + private companion object { + val lock: ReentrantLock = ReentrantLock() + val condition: Condition = lock.newCondition() + /** * Don't write more than 64 KiB of data at a time, give or take a segment. Otherwise slow * connections may suffer timeouts even when they're making (slow) progress. Without this, @@ -210,6 +241,43 @@ open class AsyncTimeout : Timeout() { private val IDLE_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(60) private val IDLE_TIMEOUT_NANOS = TimeUnit.MILLISECONDS.toNanos(IDLE_TIMEOUT_MILLIS) + /* + * .-------------. + * | | + * .------------ exit() ------| CANCELED | + * | | | + * | '-------------' + * | ^ + * | | cancel() + * v | + * .-------------. .-------------. + * | |---- enter() ----->| | + * | IDLE | | IN QUEUE | + * | |<---- exit() ------| | + * '-------------' '-------------' + * ^ | + * | | time out + * | v + * | .-------------. + * | | | + * '------------ exit() ------| TIMED OUT | + * | | + * '-------------' + * + * Notes: + * * enter() crashes if called from a state other than IDLE. + * * If there's no timeout (ie. wait forever), then enter() is a no-op. There's no state to + * track entered but not in the queue. + * * exit() is a no-op from IDLE. This is probably too lenient, but it made it simpler for + * early implementations to support cases where enter() as a no-op. + * * cancel() is a no-op from every state but IN QUEUE. + */ + + private const val STATE_IDLE = 0 + private const val STATE_IN_QUEUE = 1 + private const val STATE_TIMED_OUT = 2 + private const val STATE_CANCELED = 3 + /** * The watchdog thread processes a linked list of pending timeouts, sorted in the order to be * triggered. This class synchronizes on AsyncTimeout.class. This lock guards the queue. @@ -220,86 +288,74 @@ open class AsyncTimeout : Timeout() { */ private var head: AsyncTimeout? = null - private fun scheduleTimeout(node: AsyncTimeout, timeoutNanos: Long, hasDeadline: Boolean) { - synchronized(AsyncTimeout::class.java) { - check(!node.inQueue) { "Unbalanced enter/exit" } - node.inQueue = true - - // Start the watchdog thread and create the head node when the first timeout is scheduled. - if (head == null) { - head = AsyncTimeout() - Watchdog().start() - } + private fun insertIntoQueue(node: AsyncTimeout, timeoutNanos: Long, hasDeadline: Boolean) { + // Start the watchdog thread and create the head node when the first timeout is scheduled. + if (head == null) { + head = AsyncTimeout() + Watchdog().start() + } - val now = System.nanoTime() - if (timeoutNanos != 0L && hasDeadline) { - // Compute the earliest event; either timeout or deadline. Because nanoTime can wrap - // around, minOf() is undefined for absolute values, but meaningful for relative ones. - node.timeoutAt = now + minOf(timeoutNanos, node.deadlineNanoTime() - now) - } else if (timeoutNanos != 0L) { - node.timeoutAt = now + timeoutNanos - } else if (hasDeadline) { - node.timeoutAt = node.deadlineNanoTime() - } else { - throw AssertionError() - } + val now = System.nanoTime() + if (timeoutNanos != 0L && hasDeadline) { + // Compute the earliest event; either timeout or deadline. Because nanoTime can wrap + // around, minOf() is undefined for absolute values, but meaningful for relative ones. + node.timeoutAt = now + minOf(timeoutNanos, node.deadlineNanoTime() - now) + } else if (timeoutNanos != 0L) { + node.timeoutAt = now + timeoutNanos + } else if (hasDeadline) { + node.timeoutAt = node.deadlineNanoTime() + } else { + throw AssertionError() + } - // Insert the node in sorted order. - val remainingNanos = node.remainingNanos(now) - var prev = head!! - while (true) { - if (prev.next == null || remainingNanos < prev.next!!.remainingNanos(now)) { - node.next = prev.next - prev.next = node - if (prev === head) { - // Wake up the watchdog when inserting at the front. - (AsyncTimeout::class.java as Object).notify() - } - break + // Insert the node in sorted order. + val remainingNanos = node.remainingNanos(now) + var prev = head!! + while (true) { + if (prev.next == null || remainingNanos < prev.next!!.remainingNanos(now)) { + node.next = prev.next + prev.next = node + if (prev === head) { + // Wake up the watchdog when inserting at the front. + condition.signal() } - prev = prev.next!! + break } + prev = prev.next!! } } /** Returns true if the timeout occurred. */ - private fun cancelScheduledTimeout(node: AsyncTimeout): Boolean { - synchronized(AsyncTimeout::class.java) { - if (!node.inQueue) return false - node.inQueue = false - - // Remove the node from the linked list. - var prev = head - while (prev != null) { - if (prev.next === node) { - prev.next = node.next - node.next = null - return false - } - prev = prev.next + private fun removeFromQueue(node: AsyncTimeout) { + var prev = head + while (prev != null) { + if (prev.next === node) { + prev.next = node.next + node.next = null + return } - - // The node wasn't found in the linked list: it must have timed out! - return true + prev = prev.next } + + error("node was not found in the queue") } /** * Removes and returns the node at the head of the list, waiting for it to time out if * necessary. This returns [head] if there was no node at the head of the list when starting, * and there continues to be no node after waiting [IDLE_TIMEOUT_NANOS]. It returns null if a - * new node was inserted while waiting. Otherwise this returns the node being waited on that has - * been removed. + * new node was inserted while waiting. Otherwise, this returns the node being waited on that + * has been removed. */ @Throws(InterruptedException::class) - internal fun awaitTimeout(): AsyncTimeout? { + fun awaitTimeout(): AsyncTimeout? { // Get the next eligible node. val node = head!!.next // The queue is empty. Wait until either something is enqueued or the idle timeout elapses. if (node == null) { val startNanos = System.nanoTime() - (AsyncTimeout::class.java as Object).wait(IDLE_TIMEOUT_MILLIS) + condition.await(IDLE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) return if (head!!.next == null && System.nanoTime() - startNanos >= IDLE_TIMEOUT_NANOS) { head // The idle timeout elapsed. } else { @@ -307,21 +363,18 @@ open class AsyncTimeout : Timeout() { } } - var waitNanos = node.remainingNanos(System.nanoTime()) + val waitNanos = node.remainingNanos(System.nanoTime()) // The head of the queue hasn't timed out yet. Await that. if (waitNanos > 0) { - // Waiting is made complicated by the fact that we work in nanoseconds, - // but the API wants (millis, nanos) in two arguments. - val waitMillis = waitNanos / 1000000L - waitNanos -= waitMillis * 1000000L - (AsyncTimeout::class.java as Object).wait(waitMillis, waitNanos.toInt()) + condition.await(waitNanos, TimeUnit.NANOSECONDS) return null } // The head of the queue has timed out. Remove it. head!!.next = node.next node.next = null + node.state = STATE_TIMED_OUT return node } } diff --git a/okio/src/jvmMain/kotlin/okio/Buffer.kt b/okio/src/jvmMain/kotlin/okio/Buffer.kt index 857c983e..88dfaba7 100644 --- a/okio/src/jvmMain/kotlin/okio/Buffer.kt +++ b/okio/src/jvmMain/kotlin/okio/Buffer.kt @@ -15,6 +15,18 @@ */ package okio +import java.io.Closeable +import java.io.EOFException +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer +import java.nio.channels.ByteChannel +import java.nio.charset.Charset +import java.security.InvalidKeyException +import java.security.MessageDigest +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec import okio.internal.commonClear import okio.internal.commonClose import okio.internal.commonCompleteSegmentByteCount @@ -60,18 +72,6 @@ import okio.internal.commonWriteLong import okio.internal.commonWriteShort import okio.internal.commonWriteUtf8 import okio.internal.commonWriteUtf8CodePoint -import java.io.Closeable -import java.io.EOFException -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.nio.ByteBuffer -import java.nio.channels.ByteChannel -import java.nio.charset.Charset -import java.security.InvalidKeyException -import java.security.MessageDigest -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel { @JvmField internal actual var head: Segment? = null @@ -147,7 +147,7 @@ actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel { fun copyTo( out: OutputStream, offset: Long = 0L, - byteCount: Long = size - offset + byteCount: Long = size - offset, ): Buffer { var offset = offset var byteCount = byteCount @@ -177,15 +177,15 @@ actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel { actual fun copyTo( out: Buffer, offset: Long, - byteCount: Long + byteCount: Long, ): Buffer = commonCopyTo(out, offset, byteCount) actual fun copyTo( out: Buffer, - offset: Long + offset: Long, ): Buffer = copyTo(out, offset, size - offset) - /** Write `byteCount` bytes from this to `out`. */ + /** Write `byteCount` bytes from this to `out`. */ @Throws(IOException::class) @JvmOverloads fun writeTo(out: OutputStream, byteCount: Long = size): Buffer { @@ -212,14 +212,14 @@ actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel { return this } - /** Read and exhaust bytes from `input` into this. */ + /** Read and exhaust bytes from `input` into this. */ @Throws(IOException::class) fun readFrom(input: InputStream): Buffer { readFrom(input, Long.MAX_VALUE, true) return this } - /** Read `byteCount` bytes from `input` into this. */ + /** Read `byteCount` bytes from `input` into this. */ @Throws(IOException::class) fun readFrom(input: InputStream, byteCount: Long): Buffer { require(byteCount >= 0L) { "byteCount < 0: $byteCount" } @@ -387,15 +387,17 @@ actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel { commonWriteUtf8CodePoint(codePoint) override fun writeString(string: String, charset: Charset) = writeString( - string, 0, string.length, - charset + string, + 0, + string.length, + charset, ) override fun writeString( string: String, beginIndex: Int, endIndex: Int, - charset: Charset + charset: Charset, ): Buffer { require(beginIndex >= 0) { "beginIndex < 0: $beginIndex" } require(endIndex >= beginIndex) { "endIndex < beginIndex: $endIndex < $beginIndex" } @@ -410,7 +412,7 @@ actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel { actual override fun write( source: ByteArray, offset: Int, - byteCount: Int + byteCount: Int, ): Buffer = commonWrite(source, offset, byteCount) @Throws(IOException::class) @@ -492,7 +494,7 @@ actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel { offset: Long, bytes: ByteString, bytesOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean = commonRangeEquals(offset, bytes, bytesOffset, byteCount) override fun flush() {} @@ -503,16 +505,24 @@ actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel { override fun timeout() = Timeout.NONE - /** Returns the 128-bit MD5 hash of this buffer. */ + /** + * Returns the 128-bit MD5 hash of this buffer. + * + * MD5 has been vulnerable to collisions since 2004. It should not be used in new code. + */ actual fun md5() = digest("MD5") - /** Returns the 160-bit SHA-1 hash of this buffer. */ + /** + * Returns the 160-bit SHA-1 hash of this buffer. + * + * SHA-1 has been vulnerable to collisions since 2017. It should not be used in new code. + */ actual fun sha1() = digest("SHA-1") - /** Returns the 256-bit SHA-256 hash of this buffer. */ + /** Returns the 256-bit SHA-256 hash of this buffer. */ actual fun sha256() = digest("SHA-256") - /** Returns the 512-bit SHA-512 hash of this buffer. */ + /** Returns the 512-bit SHA-512 hash of this buffer. */ actual fun sha512() = digest("SHA-512") private fun digest(algorithm: String): ByteString { @@ -528,13 +538,13 @@ actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel { return ByteString(messageDigest.digest()) } - /** Returns the 160-bit SHA-1 HMAC of this buffer. */ + /** Returns the 160-bit SHA-1 HMAC of this buffer. */ actual fun hmacSha1(key: ByteString) = hmac("HmacSHA1", key) - /** Returns the 256-bit SHA-256 HMAC of this buffer. */ + /** Returns the 256-bit SHA-256 HMAC of this buffer. */ actual fun hmacSha256(key: ByteString) = hmac("HmacSHA256", key) - /** Returns the 512-bit SHA-512 HMAC of this buffer. */ + /** Returns the 512-bit SHA-512 HMAC of this buffer. */ actual fun hmacSha512(key: ByteString) = hmac("HmacSHA512", key) private fun hmac(algorithm: String, key: ByteString): ByteString { @@ -567,7 +577,10 @@ actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel { actual fun copy(): Buffer = commonCopy() - /** Returns a deep copy of this buffer. */ + /** + * Returns a deep copy of this buffer. This is the same as [copy] but allows [Buffer] to implement + * the [Cloneable] interface on the JVM. + */ public override fun clone(): Buffer = copy() actual fun snapshot(): ByteString = commonSnapshot() @@ -585,7 +598,7 @@ actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel { @Deprecated( message = "moved to operator function", replaceWith = ReplaceWith(expression = "this[index]"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun getByte(index: Long) = this[index] @@ -593,18 +606,23 @@ actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel { @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "size"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun size() = size actual class UnsafeCursor : Closeable { @JvmField actual var buffer: Buffer? = null + @JvmField actual var readWrite: Boolean = false internal actual var segment: Segment? = null + @JvmField actual var offset = -1L + @JvmField actual var data: ByteArray? = null + @JvmField actual var start = -1 + @JvmField actual var end = -1 actual fun next(): Int = commonNext() diff --git a/okio/src/jvmMain/kotlin/okio/BufferedSink.kt b/okio/src/jvmMain/kotlin/okio/BufferedSink.kt index 4aa1bb06..fd124a4e 100644 --- a/okio/src/jvmMain/kotlin/okio/BufferedSink.kt +++ b/okio/src/jvmMain/kotlin/okio/BufferedSink.kt @@ -20,12 +20,12 @@ import java.io.OutputStream import java.nio.channels.WritableByteChannel import java.nio.charset.Charset -actual interface BufferedSink : Sink, WritableByteChannel { +actual sealed interface BufferedSink : Sink, WritableByteChannel { /** Returns this sink's internal buffer. */ @Deprecated( message = "moved to val: use getBuffer() instead", replaceWith = ReplaceWith(expression = "buffer"), - level = DeprecationLevel.WARNING + level = DeprecationLevel.WARNING, ) fun buffer(): Buffer diff --git a/okio/src/jvmMain/kotlin/okio/BufferedSource.kt b/okio/src/jvmMain/kotlin/okio/BufferedSource.kt index b312d569..ca6b94bf 100644 --- a/okio/src/jvmMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/jvmMain/kotlin/okio/BufferedSource.kt @@ -20,12 +20,12 @@ import java.io.InputStream import java.nio.channels.ReadableByteChannel import java.nio.charset.Charset -actual interface BufferedSource : Source, ReadableByteChannel { +actual sealed interface BufferedSource : Source, ReadableByteChannel { /** Returns this source's internal buffer. */ @Deprecated( message = "moved to val: use getBuffer() instead", replaceWith = ReplaceWith(expression = "buffer"), - level = DeprecationLevel.WARNING + level = DeprecationLevel.WARNING, ) fun buffer(): Buffer diff --git a/okio/src/jvmMain/kotlin/okio/ByteString.kt b/okio/src/jvmMain/kotlin/okio/ByteString.kt index 0edad9cb..37922adf 100644 --- a/okio/src/jvmMain/kotlin/okio/ByteString.kt +++ b/okio/src/jvmMain/kotlin/okio/ByteString.kt @@ -15,9 +15,23 @@ */ package okio +import java.io.EOFException +import java.io.IOException +import java.io.InputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.OutputStream +import java.io.Serializable +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.security.InvalidKeyException +import java.security.MessageDigest +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec import okio.internal.commonBase64 import okio.internal.commonBase64Url import okio.internal.commonCompareTo +import okio.internal.commonCopyInto import okio.internal.commonDecodeBase64 import okio.internal.commonDecodeHex import okio.internal.commonEncodeUtf8 @@ -41,23 +55,10 @@ import okio.internal.commonToByteString import okio.internal.commonToString import okio.internal.commonUtf8 import okio.internal.commonWrite -import java.io.EOFException -import java.io.IOException -import java.io.InputStream -import java.io.ObjectInputStream -import java.io.ObjectOutputStream -import java.io.OutputStream -import java.io.Serializable -import java.nio.ByteBuffer -import java.nio.charset.Charset -import java.security.InvalidKeyException -import java.security.MessageDigest -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec actual open class ByteString internal actual constructor( - internal actual val data: ByteArray + internal actual val data: ByteArray, ) : Serializable, Comparable<ByteString> { @Transient internal actual var hashCode: Int = 0 // Lazily computed; 0 if unknown. @Transient internal actual var utf8: String? = null // Lazily computed. @@ -122,7 +123,8 @@ internal actual constructor( actual operator fun get(index: Int): Byte = internalGet(index) actual val size - @JvmName("size") get() = getSize() + @JvmName("size") + get() = getSize() internal actual open fun getSize() = commonGetSize() @@ -146,16 +148,23 @@ internal actual constructor( offset: Int, other: ByteString, otherOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount) actual open fun rangeEquals( offset: Int, other: ByteArray, otherOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount) + actual open fun copyInto( + offset: Int, + target: ByteArray, + targetOffset: Int, + byteCount: Int, + ) = commonCopyInto(offset, target, targetOffset, byteCount) + actual fun startsWith(prefix: ByteString) = commonStartsWith(prefix) actual fun startsWith(prefix: ByteArray) = commonStartsWith(prefix) @@ -203,7 +212,7 @@ internal actual constructor( @Deprecated( message = "moved to operator function", replaceWith = ReplaceWith(expression = "this[index]"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun getByte(index: Int) = this[index] @@ -211,7 +220,7 @@ internal actual constructor( @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "size"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun size() = size @@ -279,9 +288,9 @@ internal actual constructor( message = "moved to extension function", replaceWith = ReplaceWith( expression = "string.decodeBase64()", - imports = ["okio.ByteString.Companion.decodeBase64"] + imports = ["okio.ByteString.Companion.decodeBase64"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun decodeBase64(string: String) = string.decodeBase64() @@ -290,9 +299,9 @@ internal actual constructor( message = "moved to extension function", replaceWith = ReplaceWith( expression = "string.decodeHex()", - imports = ["okio.ByteString.Companion.decodeHex"] + imports = ["okio.ByteString.Companion.decodeHex"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun decodeHex(string: String) = string.decodeHex() @@ -301,9 +310,9 @@ internal actual constructor( message = "moved to extension function", replaceWith = ReplaceWith( expression = "string.encode(charset)", - imports = ["okio.ByteString.Companion.encode"] + imports = ["okio.ByteString.Companion.encode"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun encodeString(string: String, charset: Charset) = string.encode(charset) @@ -312,9 +321,9 @@ internal actual constructor( message = "moved to extension function", replaceWith = ReplaceWith( expression = "string.encodeUtf8()", - imports = ["okio.ByteString.Companion.encodeUtf8"] + imports = ["okio.ByteString.Companion.encodeUtf8"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun encodeUtf8(string: String) = string.encodeUtf8() @@ -323,9 +332,9 @@ internal actual constructor( message = "moved to extension function", replaceWith = ReplaceWith( expression = "buffer.toByteString()", - imports = ["okio.ByteString.Companion.toByteString"] + imports = ["okio.ByteString.Companion.toByteString"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun of(buffer: ByteBuffer) = buffer.toByteString() @@ -334,9 +343,9 @@ internal actual constructor( message = "moved to extension function", replaceWith = ReplaceWith( expression = "array.toByteString(offset, byteCount)", - imports = ["okio.ByteString.Companion.toByteString"] + imports = ["okio.ByteString.Companion.toByteString"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun of(array: ByteArray, offset: Int, byteCount: Int) = array.toByteString(offset, byteCount) @@ -345,9 +354,9 @@ internal actual constructor( message = "moved to extension function", replaceWith = ReplaceWith( expression = "inputstream.readByteString(byteCount)", - imports = ["okio.ByteString.Companion.readByteString"] + imports = ["okio.ByteString.Companion.readByteString"], ), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun read(inputstream: InputStream, byteCount: Int) = inputstream.readByteString(byteCount) } diff --git a/okio/src/jvmMain/kotlin/okio/CipherSink.kt b/okio/src/jvmMain/kotlin/okio/CipherSink.kt index aa482842..9b5c7a92 100644 --- a/okio/src/jvmMain/kotlin/okio/CipherSink.kt +++ b/okio/src/jvmMain/kotlin/okio/CipherSink.kt @@ -20,7 +20,7 @@ import javax.crypto.Cipher class CipherSink( private val sink: BufferedSink, - val cipher: Cipher + val cipher: Cipher, ) : Sink { private val blockSize = cipher.blockSize private var closed = false @@ -50,7 +50,13 @@ class CipherSink( // Shorten input until output is guaranteed to fit within a segment var outputSize = cipher.getOutputSize(size) while (outputSize > Segment.SIZE) { - check(size > blockSize) { "Unexpected output size $outputSize for input size $size" } + if (size <= blockSize) { + // Bug: For AES-GCM on Android `update` method never outputs any data + // As a consequence, `getOutputSize` just keeps increasing indefinitely after each update + // When that happens, the fallback is to perform the update operation without using a pre-allocated segment + sink.write(cipher.update(source.readByteArray(remaining))) + return remaining.toInt() + } size -= blockSize outputSize = cipher.getOutputSize(size) } @@ -104,6 +110,18 @@ class CipherSink( val outputSize = cipher.getOutputSize(0) if (outputSize == 0) return null + if (outputSize > Segment.SIZE) { + // Bug: For AES-GCM on Android `update` method never outputs any data + // As a consequence, `doFinal` returns the fully encrypted data, which may be arbitrarily large + // When that happens, the fallback is to perform the `doFinal` operation without using a pre-allocated segment + try { + sink.write(cipher.doFinal()) + } catch (t: Throwable) { + return t + } + return null + } + var thrown: Throwable? = null val buffer = sink.buffer diff --git a/okio/src/jvmMain/kotlin/okio/CipherSource.kt b/okio/src/jvmMain/kotlin/okio/CipherSource.kt index 154371f3..aeb49e66 100644 --- a/okio/src/jvmMain/kotlin/okio/CipherSource.kt +++ b/okio/src/jvmMain/kotlin/okio/CipherSource.kt @@ -20,7 +20,7 @@ import javax.crypto.Cipher class CipherSource( private val source: BufferedSource, - val cipher: Cipher + val cipher: Cipher, ) : Source { private val blockSize = cipher.blockSize private val buffer = Buffer() @@ -37,7 +37,6 @@ class CipherSource( require(byteCount >= 0L) { "byteCount < 0: $byteCount" } check(!closed) { "closed" } if (byteCount == 0L) return 0L - if (final) return buffer.read(sink, byteCount) refill() @@ -45,7 +44,7 @@ class CipherSource( } private fun refill() { - while (buffer.size == 0L) { + while (buffer.size == 0L && !final) { if (source.exhausted()) { final = true doFinal() @@ -63,7 +62,14 @@ class CipherSource( // Shorten input until output is guaranteed to fit within a segment var outputSize = cipher.getOutputSize(size) while (outputSize > Segment.SIZE) { - check(size > blockSize) { "Unexpected output size $outputSize for input size $size" } + if (size <= blockSize) { + // Bug: For AES-GCM on Android `update` method never outputs any data + // As a consequence, `getOutputSize` just keeps increasing indefinitely after each update + // When that happens, the fallback is to break the streaming implementation and just cipher the rest of the data all at once + final = true + buffer.write(cipher.doFinal(source.readByteArray())) + return + } size -= blockSize outputSize = cipher.getOutputSize(size) } diff --git a/okio/src/jvmMain/kotlin/okio/DeflaterSink.kt b/okio/src/jvmMain/kotlin/okio/DeflaterSink.kt index e71cdff8..6fcf7d83 100644 --- a/okio/src/jvmMain/kotlin/okio/DeflaterSink.kt +++ b/okio/src/jvmMain/kotlin/okio/DeflaterSink.kt @@ -19,8 +19,6 @@ package okio -import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -import java.io.IOException import java.util.zip.Deflater /** @@ -75,7 +73,6 @@ internal constructor(private val sink: BufferedSink, private val deflater: Defla } } - @IgnoreJRERequirement private fun deflate(syncFlush: Boolean) { val buffer = sink.buffer while (true) { @@ -85,10 +82,14 @@ internal constructor(private val sink: BufferedSink, private val deflater: Defla // Java 1.7, and is public (although with @hide) on Android since 2.3. // The @hide tag means that this code won't compile against the Android // 2.3 SDK, but it will run fine there. - val deflated = if (syncFlush) { - deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit, Deflater.SYNC_FLUSH) - } else { - deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit) + val deflated = try { + if (syncFlush) { + deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit, Deflater.SYNC_FLUSH) + } else { + deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit) + } + } catch (npe: NullPointerException) { + throw IOException("Deflater already closed", npe) } if (deflated > 0) { diff --git a/okio/src/jvmMain/kotlin/okio/-DeprecatedUpgrade.kt b/okio/src/jvmMain/kotlin/okio/DeprecatedUpgrade.kt index 5f955470..199b22fa 100644 --- a/okio/src/jvmMain/kotlin/okio/-DeprecatedUpgrade.kt +++ b/okio/src/jvmMain/kotlin/okio/DeprecatedUpgrade.kt @@ -14,6 +14,7 @@ * limitations under the License. */ @file:JvmName("-DeprecatedUpgrade") + package okio val Okio = `-DeprecatedOkio` diff --git a/okio/src/jvmMain/kotlin/okio/FileSystem.kt b/okio/src/jvmMain/kotlin/okio/FileSystem.kt new file mode 100644 index 00000000..7a552cc5 --- /dev/null +++ b/okio/src/jvmMain/kotlin/okio/FileSystem.kt @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 java.nio.file.FileSystem as JavaNioFileSystem +import okio.Path.Companion.toPath +import okio.internal.ResourceFileSystem +import okio.internal.commonCopy +import okio.internal.commonCreateDirectories +import okio.internal.commonDeleteRecursively +import okio.internal.commonExists +import okio.internal.commonListRecursively +import okio.internal.commonMetadata + +actual abstract class FileSystem { + @Throws(IOException::class) + actual abstract fun canonicalize(path: Path): Path + + @Throws(IOException::class) + actual fun metadata(path: Path): FileMetadata = commonMetadata(path) + + @Throws(IOException::class) + actual abstract fun metadataOrNull(path: Path): FileMetadata? + + @Throws(IOException::class) + actual fun exists(path: Path): Boolean = commonExists(path) + + @Throws(IOException::class) + actual abstract fun list(dir: Path): List<Path> + + actual abstract fun listOrNull(dir: Path): List<Path>? + + actual open fun listRecursively(dir: Path, followSymlinks: Boolean): Sequence<Path> = + commonListRecursively(dir, followSymlinks) + + fun listRecursively(dir: Path): Sequence<Path> = listRecursively(dir, followSymlinks = false) + + @Throws(IOException::class) + actual abstract fun openReadOnly(file: Path): FileHandle + + @Throws(IOException::class) + actual abstract fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle + + @Throws(IOException::class) + fun openReadWrite(file: Path): FileHandle = + openReadWrite(file, mustCreate = false, mustExist = false) + + @Throws(IOException::class) + actual abstract fun source(file: Path): Source + + @Throws(IOException::class) + @JvmName("-read") + actual inline fun <T> read(file: Path, readerAction: BufferedSource.() -> T): T { + return source(file).buffer().use { + it.readerAction() + } + } + + @Throws(IOException::class) + actual abstract fun sink(file: Path, mustCreate: Boolean): Sink + + @Throws(IOException::class) + fun sink(file: Path): Sink = sink(file, mustCreate = false) + + @Throws(IOException::class) + @JvmName("-write") + actual inline fun <T> write(file: Path, mustCreate: Boolean, writerAction: BufferedSink.() -> T): T { + return sink(file, mustCreate = mustCreate).buffer().use { + it.writerAction() + } + } + + @Throws(IOException::class) + actual abstract fun appendingSink(file: Path, mustExist: Boolean): Sink + + @Throws(IOException::class) + fun appendingSink(file: Path): Sink = appendingSink(file, mustExist = false) + + @Throws(IOException::class) + actual abstract fun createDirectory(dir: Path, mustCreate: Boolean) + + @Throws(IOException::class) + fun createDirectory(dir: Path) = createDirectory(dir, mustCreate = false) + + @Throws(IOException::class) + actual fun createDirectories(dir: Path, mustCreate: Boolean): Unit = + commonCreateDirectories(dir, mustCreate) + + @Throws(IOException::class) + fun createDirectories(dir: Path): Unit = createDirectories(dir, mustCreate = false) + + @Throws(IOException::class) + actual abstract fun atomicMove(source: Path, target: Path) + + @Throws(IOException::class) + actual open fun copy(source: Path, target: Path): Unit = commonCopy(source, target) + + @Throws(IOException::class) + actual abstract fun delete(path: Path, mustExist: Boolean) + + @Throws(IOException::class) + fun delete(path: Path) = delete(path, mustExist = false) + + @Throws(IOException::class) + actual open fun deleteRecursively(fileOrDirectory: Path, mustExist: Boolean): Unit = + commonDeleteRecursively(fileOrDirectory, mustExist) + + @Throws(IOException::class) + fun deleteRecursively(fileOrDirectory: Path): Unit = + deleteRecursively(fileOrDirectory, mustExist = false) + + @Throws(IOException::class) + actual abstract fun createSymlink(source: Path, target: Path) + + actual companion object { + /** + * The current process's host file system. Use this instance directly, or dependency inject a + * [FileSystem] to make code testable. + */ + @JvmField + val SYSTEM: FileSystem = run { + try { + Class.forName("java.nio.file.Files") + return@run NioSystemFileSystem() + } catch (e: ClassNotFoundException) { + return@run JvmSystemFileSystem() + } + } + + @JvmField + actual val SYSTEM_TEMPORARY_DIRECTORY: Path = System.getProperty("java.io.tmpdir").toPath() + + /** + * A read-only file system holding the classpath resources of the current process. If a resource + * is available with [ClassLoader.getResource], it is also available via this file system. + * + * In applications that compose multiple class loaders, this holds only the resources of + * whichever class loader includes Okio classes. Use [ClassLoader.asResourceFileSystem] for the + * resources of a specific class loader. + */ + @JvmField + val RESOURCES: FileSystem = ResourceFileSystem( + classLoader = ResourceFileSystem::class.java.classLoader, + indexEagerly = false, + ) + + @JvmName("get") + @JvmStatic + fun JavaNioFileSystem.asOkioFileSystem(): FileSystem = NioFileSystemWrappingFileSystem(this) + } +} diff --git a/okio/src/jvmMain/kotlin/okio/ForwardingSink.kt b/okio/src/jvmMain/kotlin/okio/ForwardingSink.kt index 8f0eb2f1..060aa4ce 100644 --- a/okio/src/jvmMain/kotlin/okio/ForwardingSink.kt +++ b/okio/src/jvmMain/kotlin/okio/ForwardingSink.kt @@ -21,7 +21,7 @@ import java.io.IOException abstract class ForwardingSink( /** [Sink] to which this instance is delegating. */ @get:JvmName("delegate") - val delegate: Sink + val delegate: Sink, ) : Sink { // TODO 'Sink by delegate' once https://youtrack.jetbrains.com/issue/KT-23935 is fixed. @@ -42,7 +42,7 @@ abstract class ForwardingSink( @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "delegate"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun delegate() = delegate } diff --git a/okio/src/jvmMain/kotlin/okio/ForwardingSource.kt b/okio/src/jvmMain/kotlin/okio/ForwardingSource.kt index 30a47f6c..5be29d32 100644 --- a/okio/src/jvmMain/kotlin/okio/ForwardingSource.kt +++ b/okio/src/jvmMain/kotlin/okio/ForwardingSource.kt @@ -17,29 +17,27 @@ package okio import java.io.IOException -/** A [Source] which forwards calls to another. Useful for subclassing. */ -abstract class ForwardingSource( - /** [Source] to which this instance is delegating. */ +actual abstract class ForwardingSource actual constructor( @get:JvmName("delegate") - val delegate: Source + actual val delegate: Source, ) : Source { // TODO 'Source by delegate' once https://youtrack.jetbrains.com/issue/KT-23935 is fixed. @Throws(IOException::class) - override fun read(sink: Buffer, byteCount: Long): Long = delegate.read(sink, byteCount) + actual override fun read(sink: Buffer, byteCount: Long): Long = delegate.read(sink, byteCount) - override fun timeout() = delegate.timeout() + actual override fun timeout() = delegate.timeout() @Throws(IOException::class) - override fun close() = delegate.close() + actual override fun close() = delegate.close() - override fun toString() = "${javaClass.simpleName}($delegate)" + actual override fun toString() = "${javaClass.simpleName}($delegate)" @JvmName("-deprecated_delegate") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "delegate"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun delegate() = delegate } diff --git a/okio/src/jvmMain/kotlin/okio/ForwardingTimeout.kt b/okio/src/jvmMain/kotlin/okio/ForwardingTimeout.kt index 23d83aab..4516a51d 100644 --- a/okio/src/jvmMain/kotlin/okio/ForwardingTimeout.kt +++ b/okio/src/jvmMain/kotlin/okio/ForwardingTimeout.kt @@ -17,12 +17,13 @@ package okio import java.io.IOException import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.Condition /** A [Timeout] which forwards calls to another. Useful for subclassing. */ open class ForwardingTimeout( @get:JvmName("delegate") @set:JvmSynthetic // So .java callers get the setter that returns this. - var delegate: Timeout + var delegate: Timeout, ) : Timeout() { // For backwards compatibility with Okio 1.x, this exists so it can return `ForwardingTimeout`. @@ -40,7 +41,7 @@ open class ForwardingTimeout( override fun deadlineNanoTime() = delegate.deadlineNanoTime() override fun deadlineNanoTime(deadlineNanoTime: Long) = delegate.deadlineNanoTime( - deadlineNanoTime + deadlineNanoTime, ) override fun clearTimeout() = delegate.clearTimeout() @@ -49,4 +50,10 @@ open class ForwardingTimeout( @Throws(IOException::class) override fun throwIfReached() = delegate.throwIfReached() + + override fun cancel() = delegate.cancel() + + override fun awaitSignal(condition: Condition) = delegate.awaitSignal(condition) + + override fun waitUntilNotified(monitor: Any) = delegate.waitUntilNotified(monitor) } diff --git a/okio/src/jvmMain/kotlin/okio/GzipSink.kt b/okio/src/jvmMain/kotlin/okio/GzipSink.kt index db87dafa..1b5cbc63 100644 --- a/okio/src/jvmMain/kotlin/okio/GzipSink.kt +++ b/okio/src/jvmMain/kotlin/okio/GzipSink.kt @@ -139,7 +139,7 @@ class GzipSink(sink: Sink) : Sink { @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "deflater"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun deflater() = deflater } diff --git a/okio/src/jvmMain/kotlin/okio/GzipSource.kt b/okio/src/jvmMain/kotlin/okio/GzipSource.kt index ff1e3d32..1cc4172a 100644 --- a/okio/src/jvmMain/kotlin/okio/GzipSource.kt +++ b/okio/src/jvmMain/kotlin/okio/GzipSource.kt @@ -117,7 +117,7 @@ class GzipSource(source: Source) : Source { if (flags.getBit(FEXTRA)) { source.require(2) if (fhcrc) updateCrc(source.buffer, 0, 2) - val xlen = source.buffer.readShortLe().toLong() + val xlen = (source.buffer.readShortLe().toInt() and 0xffff).toLong() source.require(xlen) if (fhcrc) updateCrc(source.buffer, 0, xlen) source.skip(xlen) diff --git a/okio/src/jvmMain/kotlin/okio/HashingSink.kt b/okio/src/jvmMain/kotlin/okio/HashingSink.kt index 36bfd2b8..0c097d20 100644 --- a/okio/src/jvmMain/kotlin/okio/HashingSink.kt +++ b/okio/src/jvmMain/kotlin/okio/HashingSink.kt @@ -61,7 +61,7 @@ actual class HashingSink : ForwardingSink, Sink { // Need to explicitly declare } } catch (e: InvalidKeyException) { throw IllegalArgumentException(e) - } + }, ) @Throws(IOException::class) @@ -103,16 +103,24 @@ actual class HashingSink : ForwardingSink, Sink { // Need to explicitly declare @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "hash"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun hash() = hash actual companion object { - /** Returns a sink that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. */ + /** + * Returns a sink that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. + * + * MD5 has been vulnerable to collisions since 2004. It should not be used in new code. + */ @JvmStatic actual fun md5(sink: Sink) = HashingSink(sink, "MD5") - /** Returns a sink that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. */ + /** + * Returns a sink that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. + * + * SHA-1 has been vulnerable to collisions since 2017. It should not be used in new code. + */ @JvmStatic actual fun sha1(sink: Sink) = HashingSink(sink, "SHA-1") diff --git a/okio/src/jvmMain/kotlin/okio/HashingSource.kt b/okio/src/jvmMain/kotlin/okio/HashingSource.kt index 25b695d7..e3d9191b 100644 --- a/okio/src/jvmMain/kotlin/okio/HashingSource.kt +++ b/okio/src/jvmMain/kotlin/okio/HashingSource.kt @@ -62,7 +62,7 @@ actual class HashingSource : ForwardingSource, Source { // Need to explicitly de } } catch (e: InvalidKeyException) { throw IllegalArgumentException(e) - } + }, ) @Throws(IOException::class) @@ -114,16 +114,24 @@ actual class HashingSource : ForwardingSource, Source { // Need to explicitly de @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "hash"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun hash() = hash actual companion object { - /** Returns a source that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. */ + /** + * Returns a source that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. + * + * MD5 has been vulnerable to collisions since 2004. It should not be used in new code. + */ @JvmStatic actual fun md5(source: Source) = HashingSource(source, "MD5") - /** Returns a source that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. */ + /** + * Returns a source that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. + * + * SHA-1 has been vulnerable to collisions since 2017. It should not be used in new code. + */ @JvmStatic actual fun sha1(source: Source) = HashingSource(source, "SHA-1") diff --git a/okio/src/jvmMain/kotlin/okio/InflaterSource.kt b/okio/src/jvmMain/kotlin/okio/InflaterSource.kt index 6fe1feb6..1d72d9d4 100644 --- a/okio/src/jvmMain/kotlin/okio/InflaterSource.kt +++ b/okio/src/jvmMain/kotlin/okio/InflaterSource.kt @@ -88,7 +88,7 @@ internal constructor(private val source: BufferedSource, private val inflater: I return bytesInflated.toLong() } - // We allocated a tail segment but didn't end up needing it. Recycle! + // We allocated a tail segment, but didn't end up needing it. Recycle! if (tail.pos == tail.limit) { sink.head = tail.pop() SegmentPool.recycle(tail) diff --git a/okio/src/jvmMain/kotlin/okio/JvmFileHandle.kt b/okio/src/jvmMain/kotlin/okio/JvmFileHandle.kt new file mode 100644 index 00000000..5a6c3b73 --- /dev/null +++ b/okio/src/jvmMain/kotlin/okio/JvmFileHandle.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 java.io.RandomAccessFile + +internal class JvmFileHandle( + readWrite: Boolean, + private val randomAccessFile: RandomAccessFile, +) : FileHandle(readWrite) { + + @Synchronized + override fun protectedResize(size: Long) { + val currentSize = size() + val delta = size - currentSize + if (delta > 0) { + protectedWrite(currentSize, ByteArray(delta.toInt()), 0, delta.toInt()) + } else { + randomAccessFile.setLength(size) + } + } + + @Synchronized + override fun protectedSize(): Long { + return randomAccessFile.length() + } + + @Synchronized + override fun protectedRead( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ): Int { + randomAccessFile.seek(fileOffset) + var bytesRead = 0 + while (bytesRead < byteCount) { + val readResult = randomAccessFile.read(array, arrayOffset, byteCount - bytesRead) + if (readResult == -1) { + if (bytesRead == 0) return -1 + break + } + bytesRead += readResult + } + return bytesRead + } + + @Synchronized + override fun protectedWrite( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ) { + randomAccessFile.seek(fileOffset) + randomAccessFile.write(array, arrayOffset, byteCount) + } + + @Synchronized + override fun protectedFlush() { + randomAccessFile.fd.sync() + } + + @Synchronized + override fun protectedClose() { + randomAccessFile.close() + } +} diff --git a/okio/src/jvmMain/kotlin/okio/JvmOkio.kt b/okio/src/jvmMain/kotlin/okio/JvmOkio.kt index 25d55166..614c48b9 100644 --- a/okio/src/jvmMain/kotlin/okio/JvmOkio.kt +++ b/okio/src/jvmMain/kotlin/okio/JvmOkio.kt @@ -20,7 +20,6 @@ package okio -import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream @@ -31,19 +30,20 @@ import java.net.Socket import java.net.SocketTimeoutException import java.nio.file.Files import java.nio.file.OpenOption -import java.nio.file.Path +import java.nio.file.Path as NioPath import java.security.MessageDigest import java.util.logging.Level import java.util.logging.Logger import javax.crypto.Cipher import javax.crypto.Mac +import okio.internal.ResourceFileSystem /** Returns a sink that writes to `out`. */ fun OutputStream.sink(): Sink = OutputStreamSink(this, Timeout()) private class OutputStreamSink( private val out: OutputStream, - private val timeout: Timeout + private val timeout: Timeout, ) : Sink { override fun write(source: Buffer, byteCount: Long) { @@ -78,9 +78,9 @@ private class OutputStreamSink( /** Returns a source that reads from `in`. */ fun InputStream.source(): Source = InputStreamSource(this, Timeout()) -private class InputStreamSource( +private open class InputStreamSource( private val input: InputStream, - private val timeout: Timeout + private val timeout: Timeout, ) : Source { override fun read(sink: Buffer, byteCount: Long): Long { @@ -178,18 +178,16 @@ fun File.appendingSink(): Sink = FileOutputStream(this, true).sink() /** Returns a source that reads from `file`. */ @Throws(FileNotFoundException::class) -fun File.source(): Source = inputStream().source() +fun File.source(): Source = InputStreamSource(inputStream(), Timeout.NONE) -/** Returns a source that reads from `path`. */ +/** Returns a sink that writes to `path`. */ @Throws(IOException::class) -@IgnoreJRERequirement // Can only be invoked on Java 7+. -fun Path.sink(vararg options: OpenOption): Sink = +fun NioPath.sink(vararg options: OpenOption): Sink = Files.newOutputStream(this, *options).sink() -/** Returns a sink that writes to `path`. */ +/** Returns a source that reads from `path`. */ @Throws(IOException::class) -@IgnoreJRERequirement // Can only be invoked on Java 7+. -fun Path.source(vararg options: OpenOption): Source = +fun NioPath.source(vararg options: OpenOption): Source = Files.newInputStream(this, *options).source() /** @@ -226,6 +224,11 @@ fun Sink.hashingSink(digest: MessageDigest): HashingSink = HashingSink(this, dig */ fun Source.hashingSource(digest: MessageDigest): HashingSource = HashingSource(this, digest) +@Throws(IOException::class) +fun FileSystem.openZip(zipPath: Path): FileSystem = okio.internal.openZip(zipPath, this) + +fun ClassLoader.asResourceFileSystem(): FileSystem = ResourceFileSystem(this, indexEagerly = true) + /** * Returns true if this error is due to a firmware bug fixed after Android 4.2.2. * https://code.google.com/p/android/issues/detail?id=54072 diff --git a/okio/src/jvmMain/kotlin/okio/JvmSystemFileSystem.kt b/okio/src/jvmMain/kotlin/okio/JvmSystemFileSystem.kt new file mode 100644 index 00000000..8584dfb5 --- /dev/null +++ b/okio/src/jvmMain/kotlin/okio/JvmSystemFileSystem.kt @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 java.io.InterruptedIOException +import java.io.RandomAccessFile +import okio.Path.Companion.toOkioPath + +/** + * A file system that adapts `java.io`. + * + * This base class is used on Android API levels 15 (our minimum supported API) through 26 + * (the first release that includes java.nio.file). + */ +internal open class JvmSystemFileSystem : FileSystem() { + override fun canonicalize(path: Path): Path { + val canonicalFile = path.toFile().canonicalFile + if (!canonicalFile.exists()) throw FileNotFoundException("no such file") + return canonicalFile.toOkioPath() + } + + override fun metadataOrNull(path: Path): FileMetadata? { + val file = path.toFile() + val isRegularFile = file.isFile + val isDirectory = file.isDirectory + val lastModifiedAtMillis = file.lastModified() + val size = file.length() + + if (!isRegularFile && + !isDirectory && + lastModifiedAtMillis == 0L && + size == 0L && + !file.exists() + ) { + return null + } + + return FileMetadata( + isRegularFile = isRegularFile, + isDirectory = isDirectory, + symlinkTarget = null, + size = size, + createdAtMillis = null, + lastModifiedAtMillis = lastModifiedAtMillis, + lastAccessedAtMillis = null, + ) + } + + override fun list(dir: Path): List<Path> = list(dir, throwOnFailure = true)!! + + override fun listOrNull(dir: Path): List<Path>? = list(dir, throwOnFailure = false) + + private fun list(dir: Path, throwOnFailure: Boolean): List<Path>? { + val file = dir.toFile() + val entries = file.list() + if (entries == null) { + if (throwOnFailure) { + if (!file.exists()) throw FileNotFoundException("no such file: $dir") + throw IOException("failed to list $dir") + } else { + return null + } + } + val result = entries.mapTo(mutableListOf()) { dir / it } + result.sort() + return result + } + + override fun openReadOnly(file: Path): FileHandle { + return JvmFileHandle(readWrite = false, randomAccessFile = RandomAccessFile(file.toFile(), "r")) + } + + override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { + require(!mustCreate || !mustExist) { + "Cannot require mustCreate and mustExist at the same time." + } + if (mustCreate) file.requireCreate() + if (mustExist) file.requireExist() + return JvmFileHandle(readWrite = true, randomAccessFile = RandomAccessFile(file.toFile(), "rw")) + } + + override fun source(file: Path): Source { + return file.toFile().source() + } + + override fun sink(file: Path, mustCreate: Boolean): Sink { + if (mustCreate) file.requireCreate() + return file.toFile().sink() + } + + override fun appendingSink(file: Path, mustExist: Boolean): Sink { + if (mustExist) file.requireExist() + return file.toFile().sink(append = true) + } + + override fun createDirectory(dir: Path, mustCreate: Boolean) { + if (!dir.toFile().mkdir()) { + val alreadyExist = metadataOrNull(dir)?.isDirectory == true + if (alreadyExist) { + if (mustCreate) { + throw IOException("$dir already exists.") + } else { + return + } + } + throw IOException("failed to create directory: $dir") + } + } + + override fun atomicMove(source: Path, target: Path) { + // Note that on Windows, this will fail if [target] already exists. + val renamed = source.toFile().renameTo(target.toFile()) + if (!renamed) throw IOException("failed to move $source to $target") + } + + override fun delete(path: Path, mustExist: Boolean) { + if (Thread.interrupted()) { + // If the current thread has been interrupted. + throw InterruptedIOException("interrupted") + } + val file = path.toFile() + val deleted = file.delete() + if (!deleted) { + if (file.exists()) throw IOException("failed to delete $path") + if (mustExist) throw FileNotFoundException("no such file: $path") + } + } + + override fun createSymlink(source: Path, target: Path) { + throw IOException("unsupported") + } + + override fun toString() = "JvmSystemFileSystem" + + // We have to implement existence verification non-atomically on the JVM because there's no API + // to do so. + private fun Path.requireExist() { + if (!exists(this)) throw IOException("$this doesn't exist.") + } + + private fun Path.requireCreate() { + if (exists(this)) throw IOException("$this already exists.") + } +} diff --git a/okio/src/jvmMain/kotlin/okio/NioFileSystemFileHandle.kt b/okio/src/jvmMain/kotlin/okio/NioFileSystemFileHandle.kt new file mode 100644 index 00000000..480f41a4 --- /dev/null +++ b/okio/src/jvmMain/kotlin/okio/NioFileSystemFileHandle.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 java.nio.ByteBuffer +import java.nio.channels.FileChannel + +internal class NioFileSystemFileHandle( + readWrite: Boolean, + private val fileChannel: FileChannel, +) : FileHandle(readWrite) { + + @Synchronized + override fun protectedResize(size: Long) { + val currentSize = size() + val delta = size - currentSize + if (delta > 0) { + protectedWrite(currentSize, ByteArray(delta.toInt()), 0, delta.toInt()) + } else { + fileChannel.truncate(size) + } + } + + @Synchronized + override fun protectedSize(): Long { + return fileChannel.size() + } + + @Synchronized + override fun protectedRead( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ): Int { + fileChannel.position(fileOffset) + val byteBuffer = ByteBuffer.wrap(array, arrayOffset, byteCount) + var bytesRead = 0 + while (bytesRead < byteCount) { + val readResult = fileChannel.read(byteBuffer) + if (readResult == -1) { + if (bytesRead == 0) return -1 + break + } + bytesRead += readResult + } + return bytesRead + } + + @Synchronized + override fun protectedWrite( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ) { + fileChannel.position(fileOffset) + val byteBuffer = ByteBuffer.wrap(array, arrayOffset, byteCount) + fileChannel.write(byteBuffer) + } + + @Synchronized + override fun protectedFlush() { + fileChannel.force(true) + } + + @Synchronized + override fun protectedClose() { + fileChannel.close() + } +} diff --git a/okio/src/jvmMain/kotlin/okio/NioFileSystemWrappingFileSystem.kt b/okio/src/jvmMain/kotlin/okio/NioFileSystemWrappingFileSystem.kt new file mode 100644 index 00000000..ddf11d81 --- /dev/null +++ b/okio/src/jvmMain/kotlin/okio/NioFileSystemWrappingFileSystem.kt @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 java.io.InterruptedIOException +import java.nio.channels.FileChannel +import java.nio.file.FileSystem as NioFileSystem +import java.nio.file.NoSuchFileException +import java.nio.file.Path as NioPath +import java.nio.file.StandardCopyOption +import java.nio.file.StandardOpenOption +import kotlin.io.path.createDirectory +import kotlin.io.path.createSymbolicLinkPointingTo +import kotlin.io.path.deleteExisting +import kotlin.io.path.exists +import kotlin.io.path.inputStream +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.moveTo +import kotlin.io.path.outputStream +import okio.Path.Companion.toOkioPath + +/** + * A file system that wraps a `java.nio.file.FileSystem` and executes all operations in the context of the wrapped file + * system. + */ +internal class NioFileSystemWrappingFileSystem(private val nioFileSystem: NioFileSystem) : NioSystemFileSystem() { + /** + * On a [java.nio.file.FileSystem], paths are stateful and hold a reference to the file system they got provided from. + * Using [getPath][NioFileSystem.getPath], we ask [nioFileSystem] to wrap the [Path]'s value in order to set itself as + * its provider which is needed for operations on the nio file system to work properly. + */ + private fun Path.resolve(): NioPath { + return nioFileSystem.getPath(toString()) + } + + override fun canonicalize(path: Path): Path { + try { + return path.resolve().toRealPath().toOkioPath() + } catch (e: NoSuchFileException) { + throw FileNotFoundException("no such file: $path") + } + } + + override fun metadataOrNull(path: Path): FileMetadata? { + return metadataOrNull(path.resolve()) + } + + override fun list(dir: Path): List<Path> = list(dir, throwOnFailure = true)!! + + override fun listOrNull(dir: Path): List<Path>? = list(dir, throwOnFailure = false) + + private fun list(dir: Path, throwOnFailure: Boolean): List<Path>? { + val nioDir = dir.resolve() + val entries = try { + nioDir.listDirectoryEntries() + } catch (e: Exception) { + if (throwOnFailure) { + if (!nioDir.exists()) throw FileNotFoundException("no such file: $dir") + throw IOException("failed to list $dir") + } else { + return null + } + } + val result = entries.mapTo(mutableListOf()) { entry -> entry.toOkioPath() } + result.sort() + return result + } + + override fun openReadOnly(file: Path): FileHandle { + val channel = try { + FileChannel.open(file.resolve(), StandardOpenOption.READ) + } catch (e: NoSuchFileException) { + throw FileNotFoundException("no such file: $file") + } + return NioFileSystemFileHandle(readWrite = false, fileChannel = channel) + } + + override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { + require(!mustCreate || !mustExist) { "Cannot require mustCreate and mustExist at the same time." } + val openOptions = buildList { + add(StandardOpenOption.READ) + add(StandardOpenOption.WRITE) + if (mustCreate) { + add(StandardOpenOption.CREATE_NEW) + } else if (!mustExist) { + add(StandardOpenOption.CREATE) + } + } + + val channel = try { + FileChannel.open(file.resolve(), *openOptions.toTypedArray()) + } catch (e: NoSuchFileException) { + throw FileNotFoundException("no such file: $file") + } + return NioFileSystemFileHandle(readWrite = true, fileChannel = channel) + } + + override fun source(file: Path): Source { + try { + return file.resolve().inputStream().source() + } catch (e: NoSuchFileException) { + throw FileNotFoundException("no such file: $file") + } + } + + override fun sink(file: Path, mustCreate: Boolean): Sink { + val openOptions = buildList { + if (mustCreate) add(StandardOpenOption.CREATE_NEW) + } + try { + return file.resolve() + .outputStream(*openOptions.toTypedArray()) + .sink() + } catch (e: NoSuchFileException) { + throw FileNotFoundException("no such file: $file") + } + } + + override fun appendingSink(file: Path, mustExist: Boolean): Sink { + val openOptions = buildList { + add(StandardOpenOption.APPEND) + if (!mustExist) add(StandardOpenOption.CREATE) + } + return file.resolve() + .outputStream(*openOptions.toTypedArray()) + .sink() + } + + override fun createDirectory(dir: Path, mustCreate: Boolean) { + val alreadyExist = metadataOrNull(dir)?.isDirectory == true + if (alreadyExist && mustCreate) { + throw IOException("$dir already exists.") + } + + try { + dir.resolve().createDirectory() + } catch (e: IOException) { + if (alreadyExist) return + throw IOException("failed to create directory: $dir", e) + } + } + + // Note that `java.nio.file.FileSystem` allows atomic moves of a file even if the target is an existing directory. + override fun atomicMove(source: Path, target: Path) { + try { + source.resolve().moveTo( + target.resolve(), + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING, + ) + } catch (e: NoSuchFileException) { + throw FileNotFoundException(e.message) + } catch (e: UnsupportedOperationException) { + throw IOException("atomic move not supported") + } + } + + override fun delete(path: Path, mustExist: Boolean) { + if (Thread.interrupted()) { + // If the current thread has been interrupted. + throw InterruptedIOException("interrupted") + } + val nioPath = path.resolve() + try { + nioPath.deleteExisting() + } catch (e: NoSuchFileException) { + if (mustExist) throw FileNotFoundException("no such file: $path") + } catch (e: IOException) { + if (nioPath.exists()) throw IOException("failed to delete $path") + } + } + + override fun createSymlink(source: Path, target: Path) { + source.resolve().createSymbolicLinkPointingTo(target.resolve()) + } + + override fun toString() = nioFileSystem::class.simpleName!! +} diff --git a/okio/src/jvmMain/kotlin/okio/NioSystemFileSystem.kt b/okio/src/jvmMain/kotlin/okio/NioSystemFileSystem.kt new file mode 100644 index 00000000..f4ebbced --- /dev/null +++ b/okio/src/jvmMain/kotlin/okio/NioSystemFileSystem.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 java.nio.file.FileSystemException +import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.NoSuchFileException +import java.nio.file.Path as NioPath +import java.nio.file.StandardCopyOption.ATOMIC_MOVE +import java.nio.file.StandardCopyOption.REPLACE_EXISTING +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.attribute.FileTime +import okio.Path.Companion.toOkioPath + +/** + * Extends [JvmSystemFileSystem] for platforms that support `java.nio.file` first introduced in + * Java 7 and Android 8.0 (API level 26). + */ +internal open class NioSystemFileSystem : JvmSystemFileSystem() { + override fun metadataOrNull(path: Path): FileMetadata? { + return metadataOrNull(path.toNioPath()) + } + + protected fun metadataOrNull(nioPath: NioPath): FileMetadata? { + val attributes = try { + Files.readAttributes( + nioPath, + BasicFileAttributes::class.java, + LinkOption.NOFOLLOW_LINKS, + ) + } catch (_: NoSuchFileException) { + return null + } catch (_: FileSystemException) { + return null + } + + val symlinkTarget: NioPath? = if (attributes.isSymbolicLink) { + Files.readSymbolicLink(nioPath) + } else { + null + } + + return FileMetadata( + isRegularFile = attributes.isRegularFile, + isDirectory = attributes.isDirectory, + symlinkTarget = symlinkTarget?.toOkioPath(), + size = attributes.size(), + createdAtMillis = attributes.creationTime()?.zeroToNull(), + lastModifiedAtMillis = attributes.lastModifiedTime()?.zeroToNull(), + lastAccessedAtMillis = attributes.lastAccessTime()?.zeroToNull(), + ) + } + + /** + * Returns this time as an epoch millis. If this is 0L this returns null, because epoch time 0L is + * a special value that indicates the requested time was not available. + */ + private fun FileTime.zeroToNull(): Long? { + return toMillis().takeIf { it != 0L } + } + + override fun atomicMove(source: Path, target: Path) { + try { + Files.move(source.toNioPath(), target.toNioPath(), ATOMIC_MOVE, REPLACE_EXISTING) + } catch (e: NoSuchFileException) { + throw FileNotFoundException(e.message) + } catch (e: UnsupportedOperationException) { + throw IOException("atomic move not supported") + } + } + + override fun createSymlink(source: Path, target: Path) { + Files.createSymbolicLink(source.toNioPath(), target.toNioPath()) + } + + override fun toString() = "NioSystemFileSystem" +} diff --git a/okio/src/jvmMain/kotlin/okio/Path.kt b/okio/src/jvmMain/kotlin/okio/Path.kt new file mode 100644 index 00000000..12456969 --- /dev/null +++ b/okio/src/jvmMain/kotlin/okio/Path.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 java.io.File +import java.nio.file.Path as NioPath +import java.nio.file.Paths +import okio.internal.commonCompareTo +import okio.internal.commonEquals +import okio.internal.commonHashCode +import okio.internal.commonIsAbsolute +import okio.internal.commonIsRelative +import okio.internal.commonIsRoot +import okio.internal.commonName +import okio.internal.commonNameBytes +import okio.internal.commonNormalized +import okio.internal.commonParent +import okio.internal.commonRelativeTo +import okio.internal.commonResolve +import okio.internal.commonRoot +import okio.internal.commonSegments +import okio.internal.commonSegmentsBytes +import okio.internal.commonToPath +import okio.internal.commonToString +import okio.internal.commonVolumeLetter + +actual class Path internal actual constructor( + internal actual val bytes: ByteString, +) : Comparable<Path> { + actual val root: Path? + get() = commonRoot() + + actual val segments: List<String> + get() = commonSegments() + + actual val segmentsBytes: List<ByteString> + get() = commonSegmentsBytes() + + actual val isAbsolute: Boolean + get() = commonIsAbsolute() + + actual val isRelative: Boolean + get() = commonIsRelative() + + @get:JvmName("volumeLetter") + actual val volumeLetter: Char? + get() = commonVolumeLetter() + + @get:JvmName("nameBytes") + actual val nameBytes: ByteString + get() = commonNameBytes() + + @get:JvmName("name") + actual val name: String + get() = commonName() + + @get:JvmName("parent") + actual val parent: Path? + get() = commonParent() + + actual val isRoot: Boolean + get() = commonIsRoot() + + @JvmName("resolve") + actual operator fun div(child: String): Path = commonResolve(child, normalize = false) + + @JvmName("resolve") + actual operator fun div(child: ByteString): Path = commonResolve(child, normalize = false) + + @JvmName("resolve") + actual operator fun div(child: Path): Path = commonResolve(child, normalize = false) + + actual fun resolve(child: String, normalize: Boolean): Path = + commonResolve(child, normalize = normalize) + + actual fun resolve(child: ByteString, normalize: Boolean): Path = + commonResolve(child, normalize = normalize) + + actual fun resolve(child: Path, normalize: Boolean): Path = + commonResolve(child = child, normalize = normalize) + + actual fun relativeTo(other: Path): Path = commonRelativeTo(other) + + actual fun normalized(): Path = commonNormalized() + + fun toFile(): File = File(toString()) + + // Can only be invoked on platforms that have java.nio.file. + fun toNioPath(): NioPath = Paths.get(toString()) + + actual override fun compareTo(other: Path): Int = commonCompareTo(other) + + actual override fun equals(other: Any?): Boolean = commonEquals(other) + + actual override fun hashCode() = commonHashCode() + + actual override fun toString() = commonToString() + + actual companion object { + @JvmField + actual val DIRECTORY_SEPARATOR: String = File.separator + + @JvmName("get") + @JvmStatic + @JvmOverloads + actual fun String.toPath(normalize: Boolean): Path = commonToPath(normalize) + + @JvmName("get") + @JvmStatic + @JvmOverloads + fun File.toOkioPath(normalize: Boolean = false): Path = toString().toPath(normalize) + + @JvmName("get") + @JvmStatic + @JvmOverloads + fun NioPath.toOkioPath(normalize: Boolean = false): Path = toString().toPath(normalize) + } +} diff --git a/okio/src/jvmMain/kotlin/okio/Pipe.kt b/okio/src/jvmMain/kotlin/okio/Pipe.kt index 43c23bfd..0fae4e03 100644 --- a/okio/src/jvmMain/kotlin/okio/Pipe.kt +++ b/okio/src/jvmMain/kotlin/okio/Pipe.kt @@ -15,6 +15,10 @@ */ package okio +import java.util.concurrent.locks.Condition +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + /** * A source and a sink that are attached. The sink's output is the source's input. Typically each * is accessed by its own thread: a producer thread writes data to the sink and a consumer thread @@ -40,6 +44,9 @@ class Pipe(internal val maxBufferSize: Long) { internal var sourceClosed = false internal var foldedSink: Sink? = null + val lock: ReentrantLock = ReentrantLock() + val condition: Condition = lock.newCondition() + init { require(maxBufferSize >= 1L) { "maxBufferSize < 1: $maxBufferSize" } } @@ -51,21 +58,21 @@ class Pipe(internal val maxBufferSize: Long) { override fun write(source: Buffer, byteCount: Long) { var byteCount = byteCount var delegate: Sink? = null - synchronized(buffer) { + lock.withLock { check(!sinkClosed) { "closed" } if (canceled) throw IOException("canceled") while (byteCount > 0) { foldedSink?.let { delegate = it - return@synchronized + return@withLock } if (sourceClosed) throw IOException("source is closed") val bufferSpaceAvailable = maxBufferSize - buffer.size if (bufferSpaceAvailable == 0L) { - timeout.waitUntilNotified(buffer) // Wait until the source drains the buffer. + timeout.awaitSignal(condition) // Wait until the source drains the buffer. if (canceled) throw IOException("canceled") continue } @@ -73,7 +80,7 @@ class Pipe(internal val maxBufferSize: Long) { val bytesToWrite = minOf(bufferSpaceAvailable, byteCount) buffer.write(source, bytesToWrite) byteCount -= bytesToWrite - (buffer as Object).notifyAll() // Notify the source that it can resume reading. + condition.signalAll() // Notify the source that it can resume reading. } } @@ -82,13 +89,13 @@ class Pipe(internal val maxBufferSize: Long) { override fun flush() { var delegate: Sink? = null - synchronized(buffer) { + lock.withLock { check(!sinkClosed) { "closed" } if (canceled) throw IOException("canceled") foldedSink?.let { delegate = it - return@synchronized + return@withLock } if (sourceClosed && buffer.size > 0L) { @@ -101,17 +108,17 @@ class Pipe(internal val maxBufferSize: Long) { override fun close() { var delegate: Sink? = null - synchronized(buffer) { + lock.withLock { if (sinkClosed) return foldedSink?.let { delegate = it - return@synchronized + return@withLock } if (sourceClosed && buffer.size > 0L) throw IOException("source is closed") sinkClosed = true - (buffer as Object).notifyAll() // Notify the source that no more bytes are coming. + condition.signalAll() // Notify the source that no more bytes are coming. } delegate?.forward { close() } @@ -125,26 +132,26 @@ class Pipe(internal val maxBufferSize: Long) { private val timeout = Timeout() override fun read(sink: Buffer, byteCount: Long): Long { - synchronized(buffer) { + lock.withLock { check(!sourceClosed) { "closed" } if (canceled) throw IOException("canceled") while (buffer.size == 0L) { if (sinkClosed) return -1L - timeout.waitUntilNotified(buffer) // Wait until the sink fills the buffer. + timeout.awaitSignal(condition) // Wait until the sink fills the buffer. if (canceled) throw IOException("canceled") } val result = buffer.read(sink, byteCount) - (buffer as Object).notifyAll() // Notify the sink that it can resume writing. + condition.signalAll() // Notify the sink that it can resume writing. return result } } override fun close() { - synchronized(buffer) { + lock.withLock { sourceClosed = true - (buffer as Object).notifyAll() // Notify the sink that no more bytes are desired. + condition.signalAll() // Notify the sink that no more bytes are desired. } } @@ -166,7 +173,7 @@ class Pipe(internal val maxBufferSize: Long) { // must copy it to sink without holding any locks, then try it all again. var closed = false lateinit var sinkBuffer: Buffer - synchronized(buffer) { + lock.withLock { check(foldedSink == null) { "sink already folded" } if (canceled) { @@ -183,7 +190,7 @@ class Pipe(internal val maxBufferSize: Long) { closed = sinkClosed sinkBuffer = Buffer() sinkBuffer.write(buffer, buffer.size) - (buffer as Object).notifyAll() // Notify the sink that it can resume writing. + condition.signalAll() // Notify the sink that it can resume writing. } var success = false @@ -197,9 +204,9 @@ class Pipe(internal val maxBufferSize: Long) { success = true } finally { if (!success) { - synchronized(buffer) { + lock.withLock { sourceClosed = true - (buffer as Object).notifyAll() // Notify the sink that it can resume writing. + condition.signalAll() // Notify the sink that it can resume writing. } } } @@ -214,7 +221,7 @@ class Pipe(internal val maxBufferSize: Long) { @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "sink"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun sink() = sink @@ -222,7 +229,7 @@ class Pipe(internal val maxBufferSize: Long) { @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "source"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.ERROR, ) fun source() = source @@ -240,10 +247,10 @@ class Pipe(internal val maxBufferSize: Long) { * operating on the source or the sink. */ fun cancel() { - synchronized(buffer) { + lock.withLock { canceled = true buffer.clear() - (buffer as Object).notifyAll() // Notify the source and sink that they're canceled. + condition.signalAll() // Notify the source and sink that they're canceled. } } } diff --git a/okio/src/jvmMain/kotlin/okio/RealBufferedSink.kt b/okio/src/jvmMain/kotlin/okio/RealBufferedSink.kt index 7df3f937..dece38d6 100644 --- a/okio/src/jvmMain/kotlin/okio/RealBufferedSink.kt +++ b/okio/src/jvmMain/kotlin/okio/RealBufferedSink.kt @@ -15,6 +15,10 @@ */ package okio +import java.io.IOException +import java.io.OutputStream +import java.nio.ByteBuffer +import java.nio.charset.Charset import okio.internal.commonClose import okio.internal.commonEmit import okio.internal.commonEmitCompleteSegments @@ -34,15 +38,12 @@ import okio.internal.commonWriteShort import okio.internal.commonWriteShortLe import okio.internal.commonWriteUtf8 import okio.internal.commonWriteUtf8CodePoint -import java.io.IOException -import java.io.OutputStream -import java.nio.ByteBuffer -import java.nio.charset.Charset internal actual class RealBufferedSink actual constructor( - @JvmField actual val sink: Sink + @JvmField actual val sink: Sink, ) : BufferedSink { @JvmField val bufferField = Buffer() + @JvmField actual var closed: Boolean = false @Suppress("OVERRIDE_BY_INLINE") // Prevent internal code from calling the getter. @@ -71,7 +72,7 @@ internal actual class RealBufferedSink actual constructor( string: String, beginIndex: Int, endIndex: Int, - charset: Charset + charset: Charset, ): BufferedSink { check(!closed) { "closed" } buffer.writeString(string, beginIndex, endIndex, charset) diff --git a/okio/src/jvmMain/kotlin/okio/RealBufferedSource.kt b/okio/src/jvmMain/kotlin/okio/RealBufferedSource.kt index 109ef140..35f6c353 100644 --- a/okio/src/jvmMain/kotlin/okio/RealBufferedSource.kt +++ b/okio/src/jvmMain/kotlin/okio/RealBufferedSource.kt @@ -15,6 +15,10 @@ */ package okio +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.charset.Charset import okio.internal.commonClose import okio.internal.commonExhausted import okio.internal.commonIndexOf @@ -45,15 +49,12 @@ import okio.internal.commonSelect import okio.internal.commonSkip import okio.internal.commonTimeout import okio.internal.commonToString -import java.io.IOException -import java.io.InputStream -import java.nio.ByteBuffer -import java.nio.charset.Charset internal actual class RealBufferedSource actual constructor( - @JvmField actual val source: Source + @JvmField actual val source: Source, ) : BufferedSource { @JvmField val bufferField = Buffer() + @JvmField actual var closed: Boolean = false @Suppress("OVERRIDE_BY_INLINE") // Prevent internal code from calling the getter. @@ -126,15 +127,17 @@ internal actual class RealBufferedSource actual constructor( commonIndexOfElement(targetBytes, fromIndex) override fun rangeEquals(offset: Long, bytes: ByteString) = rangeEquals( - offset, bytes, 0, - bytes.size + offset, + bytes, + 0, + bytes.size, ) override fun rangeEquals( offset: Long, bytes: ByteString, bytesOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean = commonRangeEquals(offset, bytes, bytesOffset, byteCount) override fun peek(): BufferedSource = commonPeek() diff --git a/okio/src/jvmMain/kotlin/okio/SegmentPool.kt b/okio/src/jvmMain/kotlin/okio/SegmentPool.kt index 7a7b0492..4a05478c 100644 --- a/okio/src/jvmMain/kotlin/okio/SegmentPool.kt +++ b/okio/src/jvmMain/kotlin/okio/SegmentPool.kt @@ -15,10 +15,10 @@ */ package okio +import java.util.concurrent.atomic.AtomicReference import okio.SegmentPool.LOCK import okio.SegmentPool.recycle import okio.SegmentPool.take -import java.util.concurrent.atomic.AtomicReference /** * This class pools segments in a lock-free singly-linked stack. Though this code is lock-free it @@ -28,7 +28,9 @@ import java.util.concurrent.atomic.AtomicReference * On [take], a caller swaps the stack's next pointer with the [LOCK] sentinel. If the stack was * not already locked, the caller replaces the head node with its successor. * - * On [recycle], a caller swaps the head with a new node whose successor is the replaced head. + * On [recycle], a caller swaps the stack's next pointer with the [LOCK] sentinel. If the stack was + * not already locked, the caller replaces the head node with a new node whose successor is the + * replaced head. * * On conflict, operations succeed, but segments are not pushed into the stack. For example, a * [take] that loses a race allocates a new segment regardless of the pool size. A [recycle] call @@ -104,19 +106,19 @@ internal actual object SegmentPool { val firstRef = firstRef() - val first = firstRef.get() - if (first === LOCK) return // A take() is currently in progress. + val first = firstRef.getAndSet(LOCK) + if (first === LOCK) return // A take() or recycle() is currently in progress. val firstLimit = first?.limit ?: 0 - if (firstLimit >= MAX_SIZE) return // Pool is full. + if (firstLimit >= MAX_SIZE) { + firstRef.set(first) // Pool is full. + return + } segment.next = first segment.pos = 0 segment.limit = firstLimit + Segment.SIZE - // If we lost a race with another operation, don't recycle this segment. - if (!firstRef.compareAndSet(first, segment)) { - segment.next = null // Don't leak a reference in the pool either! - } + firstRef.set(segment) } private fun firstRef(): AtomicReference<Segment?> { diff --git a/okio/src/jvmMain/kotlin/okio/SegmentedByteString.kt b/okio/src/jvmMain/kotlin/okio/SegmentedByteString.kt index bce9d5a9..fe64afdb 100644 --- a/okio/src/jvmMain/kotlin/okio/SegmentedByteString.kt +++ b/okio/src/jvmMain/kotlin/okio/SegmentedByteString.kt @@ -15,6 +15,15 @@ */ package okio +import java.io.IOException +import java.io.OutputStream +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.security.InvalidKeyException +import java.security.MessageDigest +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import okio.internal.commonCopyInto import okio.internal.commonEquals import okio.internal.commonGetSize import okio.internal.commonHashCode @@ -24,18 +33,10 @@ import okio.internal.commonSubstring import okio.internal.commonToByteArray import okio.internal.commonWrite import okio.internal.forEachSegment -import java.io.IOException -import java.io.OutputStream -import java.nio.ByteBuffer -import java.nio.charset.Charset -import java.security.InvalidKeyException -import java.security.MessageDigest -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec internal actual class SegmentedByteString internal actual constructor( @Transient internal actual val segments: Array<ByteArray>, - @Transient internal actual val directory: IntArray + @Transient internal actual val directory: IntArray, ) : ByteString(EMPTY.data) { override fun string(charset: Charset) = toByteString().string(charset) @@ -98,21 +99,28 @@ internal actual class SegmentedByteString internal actual constructor( offset: Int, other: ByteString, otherOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount) override fun rangeEquals( offset: Int, other: ByteArray, otherOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount) + override fun copyInto( + offset: Int, + target: ByteArray, + targetOffset: Int, + byteCount: Int, + ) = commonCopyInto(offset, target, targetOffset, byteCount) + override fun indexOf(other: ByteArray, fromIndex: Int) = toByteString().indexOf(other, fromIndex) override fun lastIndexOf(other: ByteArray, fromIndex: Int) = toByteString().lastIndexOf( other, - fromIndex + fromIndex, ) /** Returns a copy as a non-segmented byte string. */ diff --git a/okio/src/jvmMain/kotlin/okio/Throttler.kt b/okio/src/jvmMain/kotlin/okio/Throttler.kt index dbb83fe3..859fb716 100644 --- a/okio/src/jvmMain/kotlin/okio/Throttler.kt +++ b/okio/src/jvmMain/kotlin/okio/Throttler.kt @@ -17,6 +17,9 @@ package okio import java.io.IOException import java.io.InterruptedIOException +import java.util.concurrent.locks.Condition +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock /** * Enables limiting of Source and Sink throughput. Attach to this throttler via [source] and [sink] @@ -40,12 +43,15 @@ class Throttler internal constructor( * The nanoTime that we've consumed all bytes through. This is never greater than the current * nanoTime plus nanosForMaxByteCount. */ - private var allocatedUntil: Long + private var allocatedUntil: Long, ) { private var bytesPerSecond: Long = 0L private var waitByteCount: Long = 8 * 1024 // 8 KiB. private var maxByteCount: Long = 256 * 1024 // 256 KiB. + val lock: ReentrantLock = ReentrantLock() + val condition: Condition = lock.newCondition() + constructor() : this(allocatedUntil = System.nanoTime()) /** Sets the rate at which bytes will be allocated. Use 0 for no limit. */ @@ -53,9 +59,9 @@ class Throttler internal constructor( fun bytesPerSecond( bytesPerSecond: Long, waitByteCount: Long = this.waitByteCount, - maxByteCount: Long = this.maxByteCount + maxByteCount: Long = this.maxByteCount, ) { - synchronized(this) { + lock.withLock { require(bytesPerSecond >= 0) require(waitByteCount > 0) require(maxByteCount >= waitByteCount) @@ -63,7 +69,7 @@ class Throttler internal constructor( this.bytesPerSecond = bytesPerSecond this.waitByteCount = waitByteCount this.maxByteCount = maxByteCount - (this as Object).notifyAll() + condition.signalAll() } } @@ -74,15 +80,14 @@ class Throttler internal constructor( internal fun take(byteCount: Long): Long { require(byteCount > 0) - synchronized(this) { + lock.withLock { while (true) { val now = System.nanoTime() val byteCountOrWaitNanos = byteCountOrWaitNanos(now, byteCount) if (byteCountOrWaitNanos >= 0) return byteCountOrWaitNanos - waitNanos(-byteCountOrWaitNanos) + condition.awaitNanos(-byteCountOrWaitNanos) } } - throw AssertionError() // Unreachable, but synchronized() doesn't know that. } /** @@ -125,12 +130,6 @@ class Throttler internal constructor( private fun Long.bytesToNanos() = this * 1_000_000_000L / bytesPerSecond - private fun waitNanos(nanosToWait: Long) { - val millisToWait = nanosToWait / 1_000_000L - val remainderNanos = nanosToWait - (millisToWait * 1_000_000L) - (this as Object).wait(millisToWait, remainderNanos.toInt()) - } - /** Create a Source which honors this Throttler. */ fun source(source: Source): Source { return object : ForwardingSource(source) { diff --git a/okio/src/jvmMain/kotlin/okio/Timeout.kt b/okio/src/jvmMain/kotlin/okio/Timeout.kt index c522380f..b5f5f359 100644 --- a/okio/src/jvmMain/kotlin/okio/Timeout.kt +++ b/okio/src/jvmMain/kotlin/okio/Timeout.kt @@ -18,6 +18,11 @@ package okio import java.io.IOException import java.io.InterruptedIOException import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.Condition +import kotlin.concurrent.Volatile +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toTimeUnit actual open class Timeout { /** @@ -29,6 +34,12 @@ actual open class Timeout { private var timeoutNanos = 0L /** + * A sentinel that is updated to a new object on each call to [cancel]. Sample this property + * before and after an operation to test if the timeout was canceled during the operation. + */ + @Volatile private var cancelMark: Any? = null + + /** * Wait at most `timeout` time before aborting an operation. Using a per-operation timeout means * that as long as forward progress is being made, no sequence of operations will fail. * @@ -104,6 +115,104 @@ actual open class Timeout { } /** + * Prevent all current applications of this timeout from firing. Use this when a time-limited + * operation should no longer be time-limited because the nature of the operation has changed. + * + * This function does not mutate the [deadlineNanoTime] or [timeoutNanos] properties of this + * timeout. It only applies to active operations that are limited by this timeout, and applies by + * allowing those operations to run indefinitely. + * + * Subclasses that override this method must call `super.cancel()`. + */ + open fun cancel() { + cancelMark = Any() + } + + /** + * Waits on `monitor` until it is signaled. Throws [InterruptedIOException] if either the thread + * is interrupted or if this timeout elapses before `monitor` is signaled. + * The caller must hold the lock that monitor is bound to. + * + * Here's a sample class that uses `awaitSignal()` to await a specific state. Note that the + * call is made within a loop to avoid unnecessary waiting and to mitigate spurious notifications. + * ``` + * class Dice { + * Random random = new Random(); + * int latestTotal; + * + * ReentrantLock lock = new ReentrantLock(); + * Condition condition = lock.newCondition(); + * + * public void roll() { + * lock.withLock { + * latestTotal = 2 + random.nextInt(6) + random.nextInt(6); + * System.out.println("Rolled " + latestTotal); + * condition.signalAll(); + * } + * } + * + * public void rollAtFixedRate(int period, TimeUnit timeUnit) { + * Executors.newScheduledThreadPool(0).scheduleAtFixedRate(new Runnable() { + * public void run() { + * roll(); + * } + * }, 0, period, timeUnit); + * } + * + * public void awaitTotal(Timeout timeout, int total) + * throws InterruptedIOException { + * lock.withLock { + * while (latestTotal != total) { + * timeout.awaitSignal(this); + * } + * } + * } + * } + * ``` + */ + @Throws(InterruptedIOException::class) + open fun awaitSignal(condition: Condition) { + try { + val hasDeadline = hasDeadline() + val timeoutNanos = timeoutNanos() + + if (!hasDeadline && timeoutNanos == 0L) { + condition.await() // There is no timeout: wait forever. + return + } + + // Compute how long we'll wait. + val waitNanos = if (hasDeadline && timeoutNanos != 0L) { + val deadlineNanos = deadlineNanoTime() - System.nanoTime() + minOf(timeoutNanos, deadlineNanos) + } else if (hasDeadline) { + deadlineNanoTime() - System.nanoTime() + } else { + timeoutNanos + } + + if (waitNanos <= 0) throw InterruptedIOException("timeout") + + val cancelMarkBefore = cancelMark + + // Attempt to wait that long. This will return early if the monitor is notified. + val nanosRemaining = condition.awaitNanos(waitNanos) + + // If there's time remaining, we probably got the call we were waiting for. + if (nanosRemaining > 0) return + + // Return without throwing if this timeout was canceled while we were waiting. Note that this + // return is a 'spurious wakeup' because Condition.signal() was not called. + if (cancelMark !== cancelMarkBefore) return + + throw InterruptedIOException("timeout") + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() // Retain interrupted status. + throw InterruptedIOException("interrupted") + } + } + + /** * Waits on `monitor` until it is notified. Throws [InterruptedIOException] if either the thread * is interrupted or if this timeout elapses before `monitor` is notified. The caller must be * synchronized on `monitor`. @@ -139,7 +248,7 @@ actual open class Timeout { * ``` */ @Throws(InterruptedIOException::class) - fun waitUntilNotified(monitor: Any) { + open fun waitUntilNotified(monitor: Any) { try { val hasDeadline = hasDeadline() val timeoutNanos = timeoutNanos() @@ -160,18 +269,23 @@ actual open class Timeout { timeoutNanos } - // Attempt to wait that long. This will break out early if the monitor is notified. - var elapsedNanos = 0L - if (waitNanos > 0L) { - val waitMillis = waitNanos / 1000000L - (monitor as Object).wait(waitMillis, (waitNanos - waitMillis * 1000000L).toInt()) - elapsedNanos = System.nanoTime() - start - } + if (waitNanos <= 0) throw InterruptedIOException("timeout") - // Throw if the timeout elapsed before the monitor was notified. - if (elapsedNanos >= waitNanos) { - throw InterruptedIOException("timeout") - } + val cancelMarkBefore = cancelMark + + // Attempt to wait that long. This will return early if the monitor is notified. + val waitMillis = waitNanos / 1000000L + (monitor as Object).wait(waitMillis, (waitNanos - waitMillis * 1000000L).toInt()) + val elapsedNanos = System.nanoTime() - start + + // If there's time remaining, we probably got the call we were waiting for. + if (elapsedNanos < waitNanos) return + + // Return without throwing if this timeout was canceled while we were waiting. Note that this + // return is a 'spurious wakeup' because Object.notify() was not called. + if (cancelMark !== cancelMarkBefore) return + + throw InterruptedIOException("timeout") } catch (e: InterruptedException) { Thread.currentThread().interrupt() // Retain interrupted status. throw InterruptedIOException("interrupted") @@ -182,7 +296,7 @@ actual open class Timeout { * Applies the minimum intersection between this timeout and `other`, run `block`, then finally * rollback this timeout's values. */ - inline fun intersectWith(other: Timeout, block: () -> Unit) { + inline fun <T> intersectWith(other: Timeout, block: () -> T): T { val originalTimeout = this.timeoutNanos() this.timeout(minTimeout(other.timeoutNanos(), this.timeoutNanos()), TimeUnit.NANOSECONDS) @@ -192,7 +306,7 @@ actual open class Timeout { this.deadlineNanoTime(Math.min(this.deadlineNanoTime(), other.deadlineNanoTime())) } try { - block() + return block() } finally { this.timeout(originalTimeout, TimeUnit.NANOSECONDS) if (other.hasDeadline()) { @@ -204,7 +318,7 @@ actual open class Timeout { this.deadlineNanoTime(other.deadlineNanoTime()) } try { - block() + return block() } finally { this.timeout(originalTimeout, TimeUnit.NANOSECONDS) if (other.hasDeadline()) { @@ -223,6 +337,14 @@ actual open class Timeout { override fun throwIfReached() {} } + fun Timeout.timeout(timeout: Long, unit: DurationUnit): Timeout { + return timeout(timeout, unit.toTimeUnit()) + } + + fun Timeout.timeout(duration: Duration): Timeout { + return timeout(duration.inWholeNanoseconds, TimeUnit.NANOSECONDS) + } + fun minTimeout(aNanos: Long, bNanos: Long) = when { aNanos == 0L -> bNanos bNanos == 0L -> aNanos diff --git a/okio/src/jvmMain/kotlin/okio/ZipFileSystem.kt b/okio/src/jvmMain/kotlin/okio/ZipFileSystem.kt new file mode 100644 index 00000000..2c8d9ea5 --- /dev/null +++ b/okio/src/jvmMain/kotlin/okio/ZipFileSystem.kt @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 java.io.FileNotFoundException +import java.util.zip.Inflater +import okio.Path.Companion.toPath +import okio.internal.COMPRESSION_METHOD_STORED +import okio.internal.FixedLengthSource +import okio.internal.ZipEntry +import okio.internal.readLocalHeader +import okio.internal.skipLocalHeader + +/** + * Read only access to a [zip file][zip_format] and common [extra fields][extra_fields]. + * + * Zip Timestamps + * -------------- + * + * The base zip format tracks the [last modified timestamp][FileMetadata.lastModifiedAtMillis]. It + * does not track [created timestamps][FileMetadata.createdAtMillis] or [last accessed + * timestamps][FileMetadata.lastAccessedAtMillis]. This format has limitations: + * + * * Timestamps are 16-bit values stored with 2-second precision. Some zip encoders (WinZip, PKZIP) + * round up to the nearest 2 seconds; other encoders (Java) round down. + * + * * Timestamps before 1980-01-01 cannot be represented. They cannot represent dates after + * 2107-12-31. + * + * * Timestamps are stored in local time with no time zone offset. If the time zone offset changes + * – due to daylight savings time or the zip file being sent to another time zone – file times + * will be incorrect. The file time will be shifted by the difference in time zone offsets + * between the encoder and decoder. + * + * The zip format has optional extensions for timestamps. + * + * * UNIX timestamps (0x000d) support both last-access time and last modification time. These + * timestamps are stored with 1-second precision using UTC. + * + * * NTFS timestamps (0x000a) support creation time, last access time, and last modified time. + * These timestamps are stored with 100-millisecond precision using UTC. + * + * * Extended timestamps (0x5455) are stored as signed 32-bit timestamps with 1-second precision. + * These cannot express dates beyond 2038-01-19. + * + * This class currently supports base timestamps and extended timestamps. + * + * [zip_format]: https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE_6.2.0.txt + * [extra_fields]: https://opensource.apple.com/source/zip/zip-6/unzip/unzip/proginfo/extra.fld + */ +internal class ZipFileSystem internal constructor( + private val zipPath: Path, + private val fileSystem: FileSystem, + private val entries: Map<Path, ZipEntry>, + private val comment: String?, +) : FileSystem() { + override fun canonicalize(path: Path): Path { + val canonical = canonicalizeInternal(path) + if (canonical !in entries) { + throw FileNotFoundException("$path") + } + return canonical + } + + /** Don't throw [FileNotFoundException] if the path doesn't identify a file. */ + private fun canonicalizeInternal(path: Path): Path { + return ROOT.resolve(path, normalize = true) + } + + override fun metadataOrNull(path: Path): FileMetadata? { + val canonicalPath = canonicalizeInternal(path) + val entry = entries[canonicalPath] ?: return null + + val basicMetadata = FileMetadata( + isRegularFile = !entry.isDirectory, + isDirectory = entry.isDirectory, + symlinkTarget = null, + size = if (entry.isDirectory) null else entry.size, + createdAtMillis = null, + lastModifiedAtMillis = entry.lastModifiedAtMillis, + lastAccessedAtMillis = null, + ) + + if (entry.offset == -1L) { + return basicMetadata + } + + return fileSystem.openReadOnly(zipPath).use { fileHandle -> + return@use fileHandle.source(entry.offset).buffer().use { source -> + source.readLocalHeader(basicMetadata) + } + } + } + + override fun openReadOnly(file: Path): FileHandle { + throw UnsupportedOperationException("not implemented yet!") + } + + override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { + throw IOException("zip entries are not writable") + } + + override fun list(dir: Path): List<Path> = list(dir, throwOnFailure = true)!! + + override fun listOrNull(dir: Path): List<Path>? = list(dir, throwOnFailure = false) + + private fun list(dir: Path, throwOnFailure: Boolean): List<Path>? { + val canonicalDir = canonicalizeInternal(dir) + val entry = entries[canonicalDir] + ?: if (throwOnFailure) throw IOException("not a directory: $dir") else return null + return entry.children.toList() + } + + @Throws(IOException::class) + override fun source(file: Path): Source { + val canonicalPath = canonicalizeInternal(file) + val entry = entries[canonicalPath] ?: throw FileNotFoundException("no such file: $file") + val source = fileSystem.openReadOnly(zipPath).use { fileHandle -> + fileHandle.source(entry.offset).buffer() + } + source.skipLocalHeader() + + return when (entry.compressionMethod) { + COMPRESSION_METHOD_STORED -> { + FixedLengthSource(source, entry.size, truncate = true) + } + else -> { + val inflaterSource = InflaterSource( + FixedLengthSource(source, entry.compressedSize, truncate = true), + Inflater(true), + ) + FixedLengthSource(inflaterSource, entry.size, truncate = false) + } + } + } + + override fun sink(file: Path, mustCreate: Boolean): Sink { + throw IOException("zip file systems are read-only") + } + + override fun appendingSink(file: Path, mustExist: Boolean): Sink { + throw IOException("zip file systems are read-only") + } + + override fun createDirectory(dir: Path, mustCreate: Boolean): Unit = + throw IOException("zip file systems are read-only") + + override fun atomicMove(source: Path, target: Path): Unit = + throw IOException("zip file systems are read-only") + + override fun delete(path: Path, mustExist: Boolean): Unit = + throw IOException("zip file systems are read-only") + + override fun createSymlink(source: Path, target: Path): Unit = + throw IOException("zip file systems are read-only") + + private companion object { + val ROOT = "/".toPath() + } +} diff --git a/okio/src/jvmMain/kotlin/okio/internal/FixedLengthSource.kt b/okio/src/jvmMain/kotlin/okio/internal/FixedLengthSource.kt new file mode 100644 index 00000000..bc2c7d0b --- /dev/null +++ b/okio/src/jvmMain/kotlin/okio/internal/FixedLengthSource.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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.internal + +import okio.Buffer +import okio.ForwardingSource +import okio.IOException +import okio.Source + +/** + * A source that returns [size] bytes of [delegate]. + * + * This throws an [IOException] if the delegate returns fewer than [size] bytes. + * + * If [truncate] is true, this truncates to [size] bytes. Otherwise this requires that [delegate] + * will return exactly [size] bytes, and will throw an [IOException] if it doesn't. + */ +internal class FixedLengthSource( + delegate: Source, + private val size: Long, + private val truncate: Boolean, +) : ForwardingSource(delegate) { + private var bytesReceived = 0L + + override fun read(sink: Buffer, byteCount: Long): Long { + // Figure out how many bytes to attempt to read. + // + // If we're truncating, we never attempt to read more than what's remaining. + // + // Otherwise we expect the underlying source to be exactly the promised size. Read as much as + // possible and throw an exception if too many bytes are returned. + val toRead = when { + bytesReceived > size -> 0L // Already read more than the promised size. + truncate -> { + val remaining = size - bytesReceived + if (remaining == 0L) return -1L // Already read exactly the promised size. + minOf(byteCount, remaining) + } + else -> byteCount + } + + val result = super.read(sink, toRead) + + if (result != -1L) bytesReceived += result + + // Throw an exception if we received too few bytes or too many. + if ((bytesReceived < size && result == -1L) || bytesReceived > size) { + if (result > 0L && bytesReceived > size) { + // If we received bytes beyond the limit, don't return them to the caller. + sink.truncateToSize(sink.size - (bytesReceived - size)) + } + throw IOException("expected $size bytes but got $bytesReceived") + } + + return result + } + + private fun Buffer.truncateToSize(newSize: Long) { + val scratch = Buffer() + scratch.writeAll(this) + write(scratch, newSize) + scratch.clear() + } +} diff --git a/okio/src/jvmMain/kotlin/okio/internal/ResourceFileSystem.kt b/okio/src/jvmMain/kotlin/okio/internal/ResourceFileSystem.kt new file mode 100644 index 00000000..efe76801 --- /dev/null +++ b/okio/src/jvmMain/kotlin/okio/internal/ResourceFileSystem.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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.internal + +import java.io.File +import java.io.IOException +import java.net.URI +import java.net.URL +import okio.FileHandle +import okio.FileMetadata +import okio.FileNotFoundException +import okio.FileSystem +import okio.Path +import okio.Path.Companion.toOkioPath +import okio.Path.Companion.toPath +import okio.Sink +import okio.Source +import okio.source + +/** + * A file system exposing Java classpath resources. It is equivalent to the files returned by + * [ClassLoader.getResource] but supports extra features like [metadataOrNull] and [list]. + * + * If `.jar` files overlap, this returns an arbitrary element. For overlapping directories it unions + * their contents. + * + * ResourceFileSystem excludes `.class` files. + * + * This file system is read-only. + */ +internal class ResourceFileSystem internal constructor( + private val classLoader: ClassLoader, + indexEagerly: Boolean, + private val systemFileSystem: FileSystem = SYSTEM, +) : FileSystem() { + private val roots: List<Pair<FileSystem, Path>> by lazy { classLoader.toClasspathRoots() } + + init { + if (indexEagerly) { + roots.size + } + } + + override fun canonicalize(path: Path): Path { + // TODO(jwilson): throw FileNotFoundException if the canonical file doesn't exist. + return canonicalizeInternal(path) + } + + /** Don't throw [FileNotFoundException] if the path doesn't identify a file. */ + private fun canonicalizeInternal(path: Path): Path { + return ROOT.resolve(path, normalize = true) + } + + override fun list(dir: Path): List<Path> { + val relativePath = dir.toRelativePath() + val result = mutableSetOf<Path>() + var foundAny = false + for ((fileSystem, base) in roots) { + try { + result += fileSystem.list(base / relativePath) + .filter { keepPath(it) } + .map { it.removeBase(base) } + foundAny = true + } catch (_: IOException) { + } + } + if (!foundAny) throw FileNotFoundException("file not found: $dir") + return result.toList() + } + + override fun listOrNull(dir: Path): List<Path>? { + val relativePath = dir.toRelativePath() + val result = mutableSetOf<Path>() + var foundAny = false + for ((fileSystem, base) in roots) { + val baseResult = fileSystem.listOrNull(base / relativePath) + ?.filter { keepPath(it) } + ?.map { it.removeBase(base) } + if (baseResult != null) { + result += baseResult + foundAny = true + } + } + return if (foundAny) result.toList() else null + } + + override fun openReadOnly(file: Path): FileHandle { + if (!keepPath(file)) throw FileNotFoundException("file not found: $file") + val relativePath = file.toRelativePath() + for ((fileSystem, base) in roots) { + try { + return fileSystem.openReadOnly(base / relativePath) + } catch (_: FileNotFoundException) { + } + } + throw FileNotFoundException("file not found: $file") + } + + override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { + throw IOException("resources are not writable") + } + + override fun metadataOrNull(path: Path): FileMetadata? { + if (!keepPath(path)) return null + val relativePath = path.toRelativePath() + for ((fileSystem, base) in roots) { + return fileSystem.metadataOrNull(base / relativePath) ?: continue + } + return null + } + + override fun source(file: Path): Source { + if (!keepPath(file)) throw FileNotFoundException("file not found: $file") + // Make sure we have a path that doesn't start with '/'. + val relativePath = ROOT.resolve(file).relativeTo(ROOT) + return classLoader.getResourceAsStream(relativePath.toString())?.source() + ?: throw FileNotFoundException("file not found: $file") + } + + override fun sink(file: Path, mustCreate: Boolean): Sink { + throw IOException("$this is read-only") + } + + override fun appendingSink(file: Path, mustExist: Boolean): Sink { + throw IOException("$this is read-only") + } + + override fun createDirectory(dir: Path, mustCreate: Boolean): Unit = + throw IOException("$this is read-only") + + override fun atomicMove(source: Path, target: Path): Unit = + throw IOException("$this is read-only") + + override fun delete(path: Path, mustExist: Boolean): Unit = + throw IOException("$this is read-only") + + override fun createSymlink(source: Path, target: Path): Unit = + throw IOException("$this is read-only") + + private fun Path.toRelativePath(): String { + val canonicalThis = canonicalizeInternal(this) + return canonicalThis.relativeTo(ROOT).toString() + } + + /** + * Returns a search path of classpath roots. Each element contains a file system to use, and + * the base directory of that file system to search from. + */ + private fun ClassLoader.toClasspathRoots(): List<Pair<FileSystem, Path>> { + // We'd like to build this upon an API like ClassLoader.getURLs() but unfortunately that + // API exists only on URLClassLoader (and that isn't the default class loader implementation). + // + // The closest we have is `ClassLoader.getResources("")`. It returns all classpath roots that + // are directories but none that are .jar files. To mitigate that we also search for all + // `META-INF/MANIFEST.MF` files, hastily assuming that every .jar file will have such an + // entry. + // + // Classpath entries that aren't directories and don't have a META-INF/MANIFEST.MF file will + // not be visible in this file system. + return getResources("").toList().mapNotNull { it.toFileRoot() } + + getResources("META-INF/MANIFEST.MF").toList().mapNotNull { it.toJarRoot() } + } + + private fun URL.toFileRoot(): Pair<FileSystem, Path>? { + if (protocol != "file") return null // Ignore unexpected URLs. + return systemFileSystem to File(toURI()).toOkioPath() + } + + private fun URL.toJarRoot(): Pair<FileSystem, Path>? { + val urlString = toString() + if (!urlString.startsWith("jar:file:")) return null // Ignore unexpected URLs. + + // Given a URL like `jar:file:/tmp/foo.jar!/META-INF/MANIFEST.MF`, get the path to the archive + // file, like `/tmp/foo.jar`. + val suffixStart = urlString.lastIndexOf("!") + if (suffixStart == -1) return null + val path = File(URI.create(urlString.substring("jar:".length, suffixStart))).toOkioPath() + val zip = openZip( + zipPath = path, + fileSystem = systemFileSystem, + predicate = { entry -> keepPath(entry.canonicalPath) }, + ) + return zip to ROOT + } + + private companion object { + val ROOT = "/".toPath() + + fun Path.removeBase(base: Path): Path { + val prefix = base.toString() + return ROOT / (toString().removePrefix(prefix).replace('\\', '/')) + } + + private fun keepPath(path: Path) = !path.name.endsWith(".class", ignoreCase = true) + } +} diff --git a/okio/src/jvmMain/kotlin/okio/internal/ZipEntry.kt b/okio/src/jvmMain/kotlin/okio/internal/ZipEntry.kt new file mode 100644 index 00000000..a370aca8 --- /dev/null +++ b/okio/src/jvmMain/kotlin/okio/internal/ZipEntry.kt @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.internal + +import okio.Path + +internal class ZipEntry( + /** + * Absolute path of this entry. If the raw name on disk contains relative paths like `..`, they + * are not present in this path. + */ + val canonicalPath: Path, + + /** True if this entry is a directory. When encoded directory entries' names end with `/`. */ + val isDirectory: Boolean = false, + + /** The comment on this entry. Empty if there is no comment. */ + val comment: String = "", + + /** The CRC32 of the uncompressed data, or -1 if not set. */ + val crc: Long = -1L, + + /** The compressed size in bytes, or -1 if unknown. */ + val compressedSize: Long = -1L, + + /** The uncompressed size in bytes, or -1 if unknown. */ + val size: Long = -1L, + + /** Either [COMPRESSION_METHOD_DEFLATED] or [COMPRESSION_METHOD_STORED]. */ + val compressionMethod: Int = -1, + + val lastModifiedAtMillis: Long? = null, + + val offset: Long = -1L, +) { + val children = mutableListOf<Path>() +} diff --git a/okio/src/jvmMain/kotlin/okio/internal/ZipFiles.kt b/okio/src/jvmMain/kotlin/okio/internal/ZipFiles.kt new file mode 100644 index 00000000..02b6a848 --- /dev/null +++ b/okio/src/jvmMain/kotlin/okio/internal/ZipFiles.kt @@ -0,0 +1,458 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.internal + +import java.util.Calendar +import java.util.GregorianCalendar +import okio.BufferedSource +import okio.FileMetadata +import okio.FileSystem +import okio.IOException +import okio.Path +import okio.Path.Companion.toPath +import okio.ZipFileSystem +import okio.buffer + +private const val LOCAL_FILE_HEADER_SIGNATURE = 0x4034b50 +private const val CENTRAL_FILE_HEADER_SIGNATURE = 0x2014b50 +private const val END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x6054b50 +private const val ZIP64_LOCATOR_SIGNATURE = 0x07064b50 +private const val ZIP64_EOCD_RECORD_SIGNATURE = 0x06064b50 + +internal const val COMPRESSION_METHOD_DEFLATED = 8 +internal const val COMPRESSION_METHOD_STORED = 0 + +/** General Purpose Bit Flags, Bit 0. Set if the file is encrypted. */ +private const val BIT_FLAG_ENCRYPTED = 1 shl 0 + +/** + * General purpose bit flags that this implementation handles. Strict enforcement of additional + * flags may break legitimate use cases. + */ +private const val BIT_FLAG_UNSUPPORTED_MASK = BIT_FLAG_ENCRYPTED + +/** Max size of entries and archives without zip64. */ +private const val MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE = 0xffffffffL + +private const val HEADER_ID_ZIP64_EXTENDED_INFO = 0x1 +private const val HEADER_ID_EXTENDED_TIMESTAMP = 0x5455 + +/** + * Opens the file at [zipPath] for use as a file system. This uses UTF-8 to comments and names in + * the zip file. + * + * @param predicate a function that returns false for entries that should be omitted from the file + * system. + */ +@Throws(IOException::class) +internal fun openZip( + zipPath: Path, + fileSystem: FileSystem, + predicate: (ZipEntry) -> Boolean = { true }, +): ZipFileSystem { + fileSystem.openReadOnly(zipPath).use { fileHandle -> + // Scan backwards from the end of the file looking for the END_OF_CENTRAL_DIRECTORY_SIGNATURE. + // If this file has no comment we'll see it on the first attempt; otherwise we have to go + // backwards byte-by-byte until we reach it. (The number of bytes scanned will equal the comment + // size). + var scanOffset = fileHandle.size() - 22 // end of central directory record size is 22 bytes. + if (scanOffset < 0L) { + throw IOException("not a zip: size=${fileHandle.size()}") + } + val stopOffset = maxOf(scanOffset - 65_536L, 0L) + val eocdOffset: Long + var record: EocdRecord + val comment: String + while (true) { + val source = fileHandle.source(scanOffset).buffer() + try { + if (source.readIntLe() == END_OF_CENTRAL_DIRECTORY_SIGNATURE) { + eocdOffset = scanOffset + record = source.readEocdRecord() + comment = source.readUtf8(record.commentByteCount.toLong()) + break + } + } finally { + source.close() + } + + scanOffset-- + if (scanOffset < stopOffset) { + throw IOException("not a zip: end of central directory signature not found") + } + } + + // If this is a zip64, read a zip64 central directory record. + val zip64LocatorOffset = eocdOffset - 20 // zip64 end of central directory locator is 20 bytes. + if (zip64LocatorOffset > 0L) { + fileHandle.source(zip64LocatorOffset).buffer().use { zip64LocatorSource -> + if (zip64LocatorSource.readIntLe() == ZIP64_LOCATOR_SIGNATURE) { + val diskWithCentralDir = zip64LocatorSource.readIntLe() + val zip64EocdRecordOffset = zip64LocatorSource.readLongLe() + val numDisks = zip64LocatorSource.readIntLe() + if (numDisks != 1 || diskWithCentralDir != 0) { + throw IOException("unsupported zip: spanned") + } + fileHandle.source(zip64EocdRecordOffset).buffer().use { zip64EocdSource -> + val zip64EocdSignature = zip64EocdSource.readIntLe() + if (zip64EocdSignature != ZIP64_EOCD_RECORD_SIGNATURE) { + throw IOException( + "bad zip: expected ${ZIP64_EOCD_RECORD_SIGNATURE.hex} " + + "but was ${zip64EocdSignature.hex}", + ) + } + record = zip64EocdSource.readZip64EocdRecord(record) + } + } + } + } + + // Seek to the first central directory entry and read all of the entries. + val entries = mutableListOf<ZipEntry>() + fileHandle.source(record.centralDirectoryOffset).buffer().use { source -> + for (i in 0 until record.entryCount) { + val entry = source.readEntry() + if (entry.offset >= record.centralDirectoryOffset) { + throw IOException("bad zip: local file header offset >= central directory offset") + } + if (predicate(entry)) { + entries += entry + } + } + } + + // Organize the entries into a tree. + val index = buildIndex(entries) + + return ZipFileSystem(zipPath, fileSystem, index, comment) + } +} + +/** + * Returns a map containing all of [entries], plus parent entries required so that all entries + * (other than the file system root `/`) have a parent. + */ +private fun buildIndex(entries: List<ZipEntry>): Map<Path, ZipEntry> { + val root = "/".toPath() + val result = mutableMapOf( + root to ZipEntry(canonicalPath = root, isDirectory = true), + ) + + // Iterate in sorted order so each path is preceded by its parent. + for (entry in entries.sortedBy { it.canonicalPath }) { + // Note that this may clobber an existing element in the map. For consistency with java.util.zip + // and java.nio.file.FileSystem, this prefers the last-encountered element. + val replaced = result.put(entry.canonicalPath, entry) + if (replaced != null) continue + + // Make sure this parent directories exist all the way up to the file system root. + var child = entry + while (true) { + val parentPath = child.canonicalPath.parent ?: break // child is '/'. + var parentEntry = result[parentPath] + + // We've found a parent that already exists! Add the child; we're done. + if (parentEntry != null) { + parentEntry.children += child.canonicalPath + break + } + + // A parent is missing! Synthesize one. + parentEntry = ZipEntry( + canonicalPath = parentPath, + isDirectory = true, + ) + result[parentPath] = parentEntry + parentEntry.children += child.canonicalPath + child = parentEntry + } + } + + return result +} + +/** When this returns, [this] will be positioned at the start of the next entry. */ +@Throws(IOException::class) +internal fun BufferedSource.readEntry(): ZipEntry { + val signature = readIntLe() + if (signature != CENTRAL_FILE_HEADER_SIGNATURE) { + throw IOException( + "bad zip: expected ${CENTRAL_FILE_HEADER_SIGNATURE.hex} but was ${signature.hex}", + ) + } + + skip(4) // version made by (2) + version to extract (2). + val bitFlag = readShortLe().toInt() and 0xffff + if (bitFlag and BIT_FLAG_UNSUPPORTED_MASK != 0) { + throw IOException("unsupported zip: general purpose bit flag=${bitFlag.hex}") + } + + val compressionMethod = readShortLe().toInt() and 0xffff + val time = readShortLe().toInt() and 0xffff + val date = readShortLe().toInt() and 0xffff + // TODO(jwilson): decode NTFS and UNIX extra metadata to return better timestamps. + val lastModifiedAtMillis = dosDateTimeToEpochMillis(date, time) + + // These are 32-bit values in the file, but 64-bit fields in this object. + val crc = readIntLe().toLong() and 0xffffffffL + var compressedSize = readIntLe().toLong() and 0xffffffffL + var size = readIntLe().toLong() and 0xffffffffL + val nameSize = readShortLe().toInt() and 0xffff + val extraSize = readShortLe().toInt() and 0xffff + val commentByteCount = readShortLe().toInt() and 0xffff + + skip(8) // disk number start (2) + internal file attributes (2) + external file attributes (4). + var offset = readIntLe().toLong() and 0xffffffffL + val name = readUtf8(nameSize.toLong()) + if ('\u0000' in name) throw IOException("bad zip: filename contains 0x00") + + val requiredZip64ExtraSize = run { + var result = 0L + if (size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) result += 8 + if (compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) result += 8 + if (offset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) result += 8 + return@run result + } + + var hasZip64Extra = false + readExtra(extraSize) { headerId, dataSize -> + when (headerId) { + HEADER_ID_ZIP64_EXTENDED_INFO -> { + if (hasZip64Extra) { + throw IOException("bad zip: zip64 extra repeated") + } + hasZip64Extra = true + + if (dataSize < requiredZip64ExtraSize) { + throw IOException("bad zip: zip64 extra too short") + } + + // Read each field if it has a sentinel value in the regular header. + size = if (size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) readLongLe() else size + compressedSize = if (compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) readLongLe() else 0L + offset = if (offset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) readLongLe() else 0L + } + } + } + + if (requiredZip64ExtraSize > 0L && !hasZip64Extra) { + throw IOException("bad zip: zip64 extra required but absent") + } + + val comment = readUtf8(commentByteCount.toLong()) + val canonicalPath = "/".toPath() / name + val isDirectory = name.endsWith("/") + + return ZipEntry( + canonicalPath = canonicalPath, + isDirectory = isDirectory, + comment = comment, + crc = crc, + compressedSize = compressedSize, + size = size, + compressionMethod = compressionMethod, + lastModifiedAtMillis = lastModifiedAtMillis, + offset = offset, + ) +} + +@Throws(IOException::class) +private fun BufferedSource.readEocdRecord(): EocdRecord { + val diskNumber = readShortLe().toInt() and 0xffff + val diskWithCentralDir = readShortLe().toInt() and 0xffff + val entryCount = (readShortLe().toInt() and 0xffff).toLong() + val totalEntryCount = (readShortLe().toInt() and 0xffff).toLong() + if (entryCount != totalEntryCount || diskNumber != 0 || diskWithCentralDir != 0) { + throw IOException("unsupported zip: spanned") + } + skip(4) // central directory size. + val centralDirectoryOffset = readIntLe().toLong() and 0xffffffffL + val commentByteCount = readShortLe().toInt() and 0xffff + + return EocdRecord( + entryCount = entryCount, + centralDirectoryOffset = centralDirectoryOffset, + commentByteCount = commentByteCount, + ) +} + +@Throws(IOException::class) +private fun BufferedSource.readZip64EocdRecord(regularRecord: EocdRecord): EocdRecord { + skip(12) // size of central directory record (8) + version made by (2) + version to extract (2). + val diskNumber = readIntLe() + val diskWithCentralDirStart = readIntLe() + val entryCount = readLongLe() + val totalEntryCount = readLongLe() + if (entryCount != totalEntryCount || diskNumber != 0 || diskWithCentralDirStart != 0) { + throw IOException("unsupported zip: spanned") + } + skip(8) // central directory size. + val centralDirectoryOffset = readLongLe() + + return EocdRecord( + entryCount = entryCount, + centralDirectoryOffset = centralDirectoryOffset, + commentByteCount = regularRecord.commentByteCount, + ) +} + +/** + * Read a sequence of 0 or more extra fields. Each field has this structure: + * + * * 2-byte header ID + * * 2-byte data size + * * variable-byte data value + * + * This reads each extra field and calls [block] for each. The parameters are the header ID and + * data size. It is an error for [block] to process more bytes than the data size. + */ +private fun BufferedSource.readExtra(extraSize: Int, block: (Int, Long) -> Unit) { + var remaining = extraSize.toLong() + while (remaining != 0L) { + if (remaining < 4) { + throw IOException("bad zip: truncated header in extra field") + } + val headerId = readShortLe().toInt() and 0xffff + val dataSize = readShortLe().toLong() and 0xffff + remaining -= 4 + if (remaining < dataSize) { + throw IOException("bad zip: truncated value in extra field") + } + require(dataSize) + val sizeBefore = buffer.size + block(headerId, dataSize) + val fieldRemaining = dataSize + buffer.size - sizeBefore + when { + fieldRemaining < 0 -> { + throw IOException("unsupported zip: too many bytes processed for $headerId") + } + fieldRemaining > 0 -> { + buffer.skip(fieldRemaining) + } + } + remaining -= dataSize + } +} + +internal fun BufferedSource.skipLocalHeader() { + readOrSkipLocalHeader(null) +} + +internal fun BufferedSource.readLocalHeader(basicMetadata: FileMetadata): FileMetadata { + return readOrSkipLocalHeader(basicMetadata)!! +} + +/** + * If [basicMetadata] is null this will return null. Otherwise it will return a new header which + * updates [basicMetadata] with information from the local header. + */ +private fun BufferedSource.readOrSkipLocalHeader(basicMetadata: FileMetadata?): FileMetadata? { + var lastModifiedAtMillis = basicMetadata?.lastModifiedAtMillis + var lastAccessedAtMillis: Long? = null + var createdAtMillis: Long? = null + + val signature = readIntLe() + if (signature != LOCAL_FILE_HEADER_SIGNATURE) { + throw IOException( + "bad zip: expected ${LOCAL_FILE_HEADER_SIGNATURE.hex} but was ${signature.hex}", + ) + } + skip(2) // version to extract. + val bitFlag = readShortLe().toInt() and 0xffff + if (bitFlag and BIT_FLAG_UNSUPPORTED_MASK != 0) { + throw IOException("unsupported zip: general purpose bit flag=${bitFlag.hex}") + } + skip(18) // compression method (2) + time+date (4) + crc32 (4) + compressed size (4) + size (4). + val fileNameLength = readShortLe().toLong() and 0xffff + val extraSize = readShortLe().toInt() and 0xffff + skip(fileNameLength) + + if (basicMetadata == null) { + skip(extraSize.toLong()) + return null + } + + readExtra(extraSize) { headerId, dataSize -> + when (headerId) { + HEADER_ID_EXTENDED_TIMESTAMP -> { + if (dataSize < 1) { + throw IOException("bad zip: extended timestamp extra too short") + } + val flags = readByte().toInt() and 0xff + + val hasLastModifiedAtMillis = (flags and 0x1) == 0x1 + val hasLastAccessedAtMillis = (flags and 0x2) == 0x2 + val hasCreatedAtMillis = (flags and 0x4) == 0x4 + val requiredSize = run { + var result = 1L + if (hasLastModifiedAtMillis) result += 4L + if (hasLastAccessedAtMillis) result += 4L + if (hasCreatedAtMillis) result += 4L + return@run result + } + if (dataSize < requiredSize) { + throw IOException("bad zip: extended timestamp extra too short") + } + + if (hasLastModifiedAtMillis) lastModifiedAtMillis = readIntLe() * 1000L + if (hasLastAccessedAtMillis) lastAccessedAtMillis = readIntLe() * 1000L + if (hasCreatedAtMillis) createdAtMillis = readIntLe() * 1000L + } + } + } + + return FileMetadata( + isRegularFile = basicMetadata.isRegularFile, + isDirectory = basicMetadata.isDirectory, + symlinkTarget = null, + size = basicMetadata.size, + createdAtMillis = createdAtMillis, + lastModifiedAtMillis = lastModifiedAtMillis, + lastAccessedAtMillis = lastAccessedAtMillis, + ) +} + +/** + * Converts a 32-bit DOS date+time to milliseconds since epoch. Note that this function interprets + * a value with no time zone as a value with the local time zone. + */ +private fun dosDateTimeToEpochMillis(date: Int, time: Int): Long? { + if (time == -1) { + return null + } + + // Note that this inherits the local time zone. + val cal = GregorianCalendar() + cal.set(Calendar.MILLISECOND, 0) + val year = 1980 + (date shr 9 and 0x7f) + val month = date shr 5 and 0xf + val day = date and 0x1f + val hour = time shr 11 and 0x1f + val minute = time shr 5 and 0x3f + val second = time and 0x1f shl 1 + cal.set(year, month - 1, day, hour, minute, second) + return cal.time.time +} + +private class EocdRecord( + val entryCount: Long, + val centralDirectoryOffset: Long, + val commentByteCount: Int, +) + +private val Int.hex: String + get() = "0x${this.toString(16)}" diff --git a/okio/src/jvmMain/resources/META-INF/proguard/okio.pro b/okio/src/jvmMain/resources/META-INF/proguard/okio.pro deleted file mode 100644 index 2b698343..00000000 --- a/okio/src/jvmMain/resources/META-INF/proguard/okio.pro +++ /dev/null @@ -1,2 +0,0 @@ -# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. --dontwarn org.codehaus.mojo.animal_sniffer.* diff --git a/okio/src/jvmTest/hashFunctions b/okio/src/jvmTest/hashFunctions new file mode 120000 index 00000000..1634c79e --- /dev/null +++ b/okio/src/jvmTest/hashFunctions @@ -0,0 +1 @@ +../hashFunctions/kotlin
\ No newline at end of file diff --git a/okio/src/jvmTest/java/okio/AsyncTimeoutTest.java b/okio/src/jvmTest/java/okio/AsyncTimeoutTest.java deleted file mode 100644 index 974218b1..00000000 --- a/okio/src/jvmTest/java/okio/AsyncTimeoutTest.java +++ /dev/null @@ -1,392 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; - -import java.io.IOException; -import java.io.InterruptedIOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Random; -import java.util.concurrent.BlockingDeque; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.TimeUnit; - -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; -import static okio.TestUtil.bufferWithRandomSegmentLayout; -import static org.junit.Assert.*; - -/** - * This test uses four timeouts of varying durations: 250ms, 500ms, 750ms and - * 1000ms, named 'a', 'b', 'c' and 'd'. - */ -public final class AsyncTimeoutTest { - private final BlockingDeque<AsyncTimeout> timedOut = new LinkedBlockingDeque<>(); - private final AsyncTimeout a = new RecordingAsyncTimeout(); - private final AsyncTimeout b = new RecordingAsyncTimeout(); - private final AsyncTimeout c = new RecordingAsyncTimeout(); - private final AsyncTimeout d = new RecordingAsyncTimeout(); - - @Before public void setUp() throws Exception { - a.timeout( 250, TimeUnit.MILLISECONDS); - b.timeout( 500, TimeUnit.MILLISECONDS); - c.timeout( 750, TimeUnit.MILLISECONDS); - d.timeout(1000, TimeUnit.MILLISECONDS); - } - - @Test public void zeroTimeoutIsNoTimeout() throws Exception { - AsyncTimeout timeout = new RecordingAsyncTimeout(); - timeout.timeout(0, TimeUnit.MILLISECONDS); - timeout.enter(); - Thread.sleep(250); - assertFalse(timeout.exit()); - assertTimedOut(); - } - - @Test public void singleInstanceTimedOut() throws Exception { - a.enter(); - Thread.sleep(500); - assertTrue(a.exit()); - assertTimedOut(a); - } - - @Test public void singleInstanceNotTimedOut() throws Exception { - b.enter(); - Thread.sleep(250); - b.exit(); - assertFalse(b.exit()); - assertTimedOut(); - } - - @Test public void instancesAddedAtEnd() throws Exception { - a.enter(); - b.enter(); - c.enter(); - d.enter(); - Thread.sleep(1250); - assertTrue(a.exit()); - assertTrue(b.exit()); - assertTrue(c.exit()); - assertTrue(d.exit()); - assertTimedOut(a, b, c, d); - } - - @Test public void instancesAddedAtFront() throws Exception { - d.enter(); - c.enter(); - b.enter(); - a.enter(); - Thread.sleep(1250); - assertTrue(d.exit()); - assertTrue(c.exit()); - assertTrue(b.exit()); - assertTrue(a.exit()); - assertTimedOut(a, b, c, d); - } - - @Test public void instancesRemovedAtFront() throws Exception { - a.enter(); - b.enter(); - c.enter(); - d.enter(); - assertFalse(a.exit()); - assertFalse(b.exit()); - assertFalse(c.exit()); - assertFalse(d.exit()); - assertTimedOut(); - } - - @Test public void instancesRemovedAtEnd() throws Exception { - a.enter(); - b.enter(); - c.enter(); - d.enter(); - assertFalse(d.exit()); - assertFalse(c.exit()); - assertFalse(b.exit()); - assertFalse(a.exit()); - assertTimedOut(); - } - - @Test public void doubleEnter() throws Exception { - a.enter(); - try { - a.enter(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test public void reEnter() throws Exception { - a.timeout(10, SECONDS); - a.enter(); - assertFalse(a.exit()); - a.enter(); - assertFalse(a.exit()); - } - - @Test public void reEnterAfterTimeout() throws Exception { - a.timeout(1, MILLISECONDS); - a.enter(); - assertSame(a, timedOut.take()); - assertTrue(a.exit()); - a.enter(); - assertFalse(a.exit()); - } - - @Test public void deadlineOnly() throws Exception { - RecordingAsyncTimeout timeout = new RecordingAsyncTimeout(); - timeout.deadline(250, TimeUnit.MILLISECONDS); - timeout.enter(); - Thread.sleep(500); - assertTrue(timeout.exit()); - assertTimedOut(timeout); - } - - @Test public void deadlineBeforeTimeout() throws Exception { - RecordingAsyncTimeout timeout = new RecordingAsyncTimeout(); - timeout.deadline(250, TimeUnit.MILLISECONDS); - timeout.timeout(750, TimeUnit.MILLISECONDS); - timeout.enter(); - Thread.sleep(500); - assertTrue(timeout.exit()); - assertTimedOut(timeout); - } - - @Test public void deadlineAfterTimeout() throws Exception { - RecordingAsyncTimeout timeout = new RecordingAsyncTimeout(); - timeout.timeout(250, TimeUnit.MILLISECONDS); - timeout.deadline(750, TimeUnit.MILLISECONDS); - timeout.enter(); - Thread.sleep(500); - assertTrue(timeout.exit()); - assertTimedOut(timeout); - } - - @Test public void deadlineStartsBeforeEnter() throws Exception { - RecordingAsyncTimeout timeout = new RecordingAsyncTimeout(); - timeout.deadline(500, TimeUnit.MILLISECONDS); - Thread.sleep(500); - timeout.enter(); - Thread.sleep(250); - assertTrue(timeout.exit()); - assertTimedOut(timeout); - } - - @Test public void deadlineInThePast() throws Exception { - RecordingAsyncTimeout timeout = new RecordingAsyncTimeout(); - timeout.deadlineNanoTime(System.nanoTime() - 1); - timeout.enter(); - Thread.sleep(250); - assertTrue(timeout.exit()); - assertTimedOut(timeout); - } - - @Test public void wrappedSinkTimesOut() throws Exception { - Sink sink = new ForwardingSink(new Buffer()) { - @Override public void write(Buffer source, long byteCount) throws IOException { - try { - Thread.sleep(500); - } catch (InterruptedException e) { - throw new AssertionError(); - } - } - }; - AsyncTimeout timeout = new AsyncTimeout(); - timeout.timeout(250, TimeUnit.MILLISECONDS); - Sink timeoutSink = timeout.sink(sink); - Buffer data = new Buffer().writeUtf8("a"); - try { - timeoutSink.write(data, 1); - fail(); - } catch (InterruptedIOException expected) { - } - } - - @Test public void wrappedSinkFlushTimesOut() throws Exception { - Sink sink = new ForwardingSink(new Buffer()) { - @Override public void flush() throws IOException { - try { - Thread.sleep(500); - } catch (InterruptedException e) { - throw new AssertionError(); - } - } - }; - AsyncTimeout timeout = new AsyncTimeout(); - timeout.timeout(250, TimeUnit.MILLISECONDS); - Sink timeoutSink = timeout.sink(sink); - try { - timeoutSink.flush(); - fail(); - } catch (InterruptedIOException expected) { - } - } - - @Test public void wrappedSinkCloseTimesOut() throws Exception { - Sink sink = new ForwardingSink(new Buffer()) { - @Override public void close() throws IOException { - try { - Thread.sleep(500); - } catch (InterruptedException e) { - throw new AssertionError(); - } - } - }; - AsyncTimeout timeout = new AsyncTimeout(); - timeout.timeout(250, TimeUnit.MILLISECONDS); - Sink timeoutSink = timeout.sink(sink); - try { - timeoutSink.close(); - fail(); - } catch (InterruptedIOException expected) { - } - } - - @Test public void wrappedSourceTimesOut() throws Exception { - Source source = new ForwardingSource(new Buffer()) { - @Override public long read(Buffer sink, long byteCount) throws IOException { - try { - Thread.sleep(500); - return -1; - } catch (InterruptedException e) { - throw new AssertionError(); - } - } - }; - AsyncTimeout timeout = new AsyncTimeout(); - timeout.timeout(250, TimeUnit.MILLISECONDS); - Source timeoutSource = timeout.source(source); - try { - timeoutSource.read(new Buffer(), 0); - fail(); - } catch (InterruptedIOException expected) { - } - } - - @Test public void wrappedSourceCloseTimesOut() throws Exception { - Source source = new ForwardingSource(new Buffer()) { - @Override public void close() throws IOException { - try { - Thread.sleep(500); - } catch (InterruptedException e) { - throw new AssertionError(); - } - } - }; - AsyncTimeout timeout = new AsyncTimeout(); - timeout.timeout(250, TimeUnit.MILLISECONDS); - Source timeoutSource = timeout.source(source); - try { - timeoutSource.close(); - fail(); - } catch (InterruptedIOException expected) { - } - } - - @Test public void wrappedThrowsWithTimeout() throws Exception { - Sink sink = new ForwardingSink(new Buffer()) { - @Override public void write(Buffer source, long byteCount) throws IOException { - try { - Thread.sleep(500); - throw new IOException("exception and timeout"); - } catch (InterruptedException e) { - throw new AssertionError(); - } - } - }; - AsyncTimeout timeout = new AsyncTimeout(); - timeout.timeout(250, TimeUnit.MILLISECONDS); - Sink timeoutSink = timeout.sink(sink); - Buffer data = new Buffer().writeUtf8("a"); - try { - timeoutSink.write(data, 1); - fail(); - } catch (InterruptedIOException expected) { - assertEquals("timeout", expected.getMessage()); - assertEquals("exception and timeout", expected.getCause().getMessage()); - } - } - - @Test public void wrappedThrowsWithoutTimeout() throws Exception { - Sink sink = new ForwardingSink(new Buffer()) { - @Override public void write(Buffer source, long byteCount) throws IOException { - throw new IOException("no timeout occurred"); - } - }; - AsyncTimeout timeout = new AsyncTimeout(); - timeout.timeout(250, TimeUnit.MILLISECONDS); - Sink timeoutSink = timeout.sink(sink); - Buffer data = new Buffer().writeUtf8("a"); - try { - timeoutSink.write(data, 1); - fail(); - } catch (IOException expected) { - assertEquals("no timeout occurred", expected.getMessage()); - } - } - - /** - * We had a bug where writing a very large buffer would fail with an - * unexpected timeout because although the sink was making steady forward - * progress, doing it all as a single write caused a timeout. - */ - @Ignore("Flaky") - @Test public void sinkSplitsLargeWrites() throws Exception { - byte[] data = new byte[512 * 1024]; - Random dice = new Random(0); - dice.nextBytes(data); - final Buffer source = bufferWithRandomSegmentLayout(dice, data); - final Buffer target = new Buffer(); - - Sink sink = new ForwardingSink(new Buffer()) { - @Override public void write(Buffer source, long byteCount) throws IOException { - try { - Thread.sleep(byteCount / 500); // ~500 KiB/s. - target.write(source, byteCount); - } catch (InterruptedException e) { - throw new AssertionError(); - } - } - }; - - // Timeout after 250 ms of inactivity. - AsyncTimeout timeout = new AsyncTimeout(); - timeout.timeout(250, TimeUnit.MILLISECONDS); - Sink timeoutSink = timeout.sink(sink); - - // Transmit 500 KiB of data, which should take ~1 second. But expect no timeout! - timeoutSink.write(source, source.size()); - - // The data should all have arrived. - assertEquals(ByteString.of(data), target.readByteString()); - } - - /** Asserts which timeouts fired, and in which order. */ - private void assertTimedOut(Timeout... expected) { - assertEquals(Arrays.asList(expected), new ArrayList<Timeout>(timedOut)); - } - - class RecordingAsyncTimeout extends AsyncTimeout { - @Override protected void timedOut() { - timedOut.add(this); - } - } -} diff --git a/okio/src/jvmTest/java/okio/BufferCursorTest.java b/okio/src/jvmTest/java/okio/BufferCursorTest.java deleted file mode 100644 index 4cad858a..00000000 --- a/okio/src/jvmTest/java/okio/BufferCursorTest.java +++ /dev/null @@ -1,468 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; - -import static kotlin.text.StringsKt.repeat; -import static okio.Buffer.UnsafeCursor; -import static okio.TestUtil.SEGMENT_SIZE; -import static okio.TestUtil.deepCopy; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.junit.Assume.assumeTrue; - -@RunWith(Parameterized.class) -public final class BufferCursorTest { - - @Parameters(name = "{0}") - public static List<Object[]> parameters() throws Exception { - List<Object[]> result = new ArrayList<>(); - for (BufferFactory bufferFactory : BufferFactory.values()) { - result.add(new Object[] {bufferFactory}); - } - return result; - } - - @Parameter public BufferFactory bufferFactory; - - @Test public void apiExample() throws Exception { - Buffer buffer = new Buffer(); - - try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) { - cursor.resizeBuffer(1000_000); - - do { - Arrays.fill(cursor.data, cursor.start, cursor.end, (byte) 'x'); - } while (cursor.next() != -1); - - cursor.seek(3); - cursor.data[cursor.start] = 'o'; - - cursor.seek(1); - cursor.data[cursor.start] = 'o'; - - cursor.resizeBuffer(4); - } - - assertEquals(new Buffer().writeUtf8("xoxo"), buffer); - } - - @Test public void accessSegmentBySegment() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - try (UnsafeCursor cursor = buffer.readUnsafe()) { - Buffer actual = new Buffer(); - while (cursor.next() != -1L) { - actual.write(cursor.data, cursor.start, cursor.end - cursor.start); - } - assertEquals(buffer, actual); - } - } - - @Test public void seekToNegativeOneSeeksBeforeFirstSegment() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - try (UnsafeCursor cursor = buffer.readUnsafe()) { - cursor.seek(-1L); - assertEquals(-1, cursor.offset); - assertEquals(null, cursor.data); - assertEquals(-1, cursor.start); - assertEquals(-1, cursor.end); - - cursor.next(); - assertEquals(0, cursor.offset); - } - } - - @Test public void accessByteByByte() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - try (UnsafeCursor cursor = buffer.readUnsafe()) { - byte[] actual = new byte[(int) buffer.size()]; - for (int i = 0; i < buffer.size(); i++) { - cursor.seek(i); - actual[i] = cursor.data[cursor.start]; - } - assertEquals(ByteString.of(actual), buffer.snapshot()); - } - } - - @Test public void accessByteByByteReverse() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - try (UnsafeCursor cursor = buffer.readUnsafe()) { - byte[] actual = new byte[(int) buffer.size()]; - for (int i = (int) (buffer.size() - 1); i >= 0; i--) { - cursor.seek(i); - actual[i] = cursor.data[cursor.start]; - } - assertEquals(ByteString.of(actual), buffer.snapshot()); - } - } - - @Test public void accessByteByByteAlwaysResettingToZero() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - try (UnsafeCursor cursor = buffer.readUnsafe()) { - byte[] actual = new byte[(int) buffer.size()]; - for (int i = 0; i < buffer.size(); i++) { - cursor.seek(i); - actual[i] = cursor.data[cursor.start]; - cursor.seek(0L); - } - assertEquals(ByteString.of(actual), buffer.snapshot()); - } - } - - @Test public void segmentBySegmentNavigation() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - UnsafeCursor cursor = buffer.readUnsafe(); - assertEquals(-1, cursor.offset); - try { - long lastOffset = cursor.offset; - while (cursor.next() != -1L) { - assertTrue(cursor.offset > lastOffset); - lastOffset = cursor.offset; - } - assertEquals(buffer.size(), cursor.offset); - assertNull(cursor.data); - assertEquals(-1, cursor.start); - assertEquals(-1, cursor.end); - } finally { - cursor.close(); - } - } - - @Test public void seekWithinSegment() throws Exception { - assumeTrue(bufferFactory == BufferFactory.SMALL_SEGMENTED_BUFFER); - Buffer buffer = bufferFactory.newBuffer(); - assertEquals("abcdefghijkl", buffer.clone().readUtf8()); - - // Seek to the 'f' in the "defg" segment. - try (UnsafeCursor cursor = buffer.readUnsafe()) { - assertEquals(2, cursor.seek(5)); // 2 for 2 bytes left in the segment: "fg". - assertEquals(5, cursor.offset); - assertEquals(2, cursor.end - cursor.start); - assertEquals('d', (char) cursor.data[cursor.start - 2]); // Out of bounds! - assertEquals('e', (char) cursor.data[cursor.start - 1]); // Out of bounds! - assertEquals('f', (char) cursor.data[cursor.start]); - assertEquals('g', (char) cursor.data[cursor.start + 1]); - } - } - - @Test public void acquireAndRelease() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - UnsafeCursor cursor = new UnsafeCursor(); - - // Nothing initialized before acquire. - assertEquals(-1, cursor.offset); - assertNull(cursor.data); - assertEquals(-1, cursor.start); - assertEquals(-1, cursor.end); - - buffer.readUnsafe(cursor); - cursor.close(); - - // Nothing initialized after close. - assertEquals(-1, cursor.offset); - assertNull(cursor.data); - assertEquals(-1, cursor.start); - assertEquals(-1, cursor.end); - } - - @Test public void doubleAcquire() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - try (UnsafeCursor cursor = buffer.readUnsafe()) { - buffer.readUnsafe(cursor); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test public void releaseWithoutAcquire() throws Exception { - UnsafeCursor cursor = new UnsafeCursor(); - try { - cursor.close(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test public void releaseAfterRelease() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - UnsafeCursor cursor = buffer.readUnsafe(); - cursor.close(); - try { - cursor.close(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test public void enlarge() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - long originalSize = buffer.size(); - - Buffer expected = deepCopy(buffer); - expected.writeUtf8("abc"); - - try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) { - assertEquals(originalSize, cursor.resizeBuffer(originalSize + 3)); - cursor.seek(originalSize); - cursor.data[cursor.start] = 'a'; - cursor.seek(originalSize + 1); - cursor.data[cursor.start] = 'b'; - cursor.seek(originalSize + 2); - cursor.data[cursor.start] = 'c'; - } - - assertEquals(expected, buffer); - } - - @Test public void enlargeByManySegments() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - long originalSize = buffer.size(); - - Buffer expected = deepCopy(buffer); - expected.writeUtf8(repeat("x", 1_000_000)); - - try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) { - cursor.resizeBuffer(originalSize + 1_000_000); - cursor.seek(originalSize); - do { - Arrays.fill(cursor.data, cursor.start, cursor.end, (byte) 'x'); - } while (cursor.next() != -1); - } - - assertEquals(expected, buffer); - } - - @Test public void resizeNotAcquired() throws Exception { - UnsafeCursor cursor = new UnsafeCursor(); - try { - cursor.resizeBuffer(10); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test public void expandNotAcquired() throws Exception { - UnsafeCursor cursor = new UnsafeCursor(); - try { - cursor.expandBuffer(10); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test public void resizeAcquiredReadOnly() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - - try (UnsafeCursor cursor = buffer.readUnsafe()) { - cursor.resizeBuffer(10); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test public void expandAcquiredReadOnly() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - - try (UnsafeCursor cursor = buffer.readUnsafe()) { - cursor.expandBuffer(10); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test public void shrink() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - assumeTrue(buffer.size() > 3); - long originalSize = buffer.size(); - - Buffer expected = new Buffer(); - deepCopy(buffer).copyTo(expected, 0, originalSize - 3); - - try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) { - assertEquals(originalSize, cursor.resizeBuffer(originalSize - 3)); - } - - assertEquals(expected, buffer); - } - - @Test public void shrinkByManySegments() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - assumeTrue(buffer.size() <= 1_000_000); - long originalSize = buffer.size(); - - Buffer toShrink = new Buffer(); - toShrink.writeUtf8(repeat("x", 1_000_000)); - deepCopy(buffer).copyTo(toShrink, 0, originalSize); - - UnsafeCursor cursor = new UnsafeCursor(); - toShrink.readAndWriteUnsafe(cursor); - try { - cursor.resizeBuffer(originalSize); - } finally { - cursor.close(); - } - - Buffer expected = new Buffer(); - expected.writeUtf8(repeat("x", (int) originalSize)); - assertEquals(expected, toShrink); - } - - @Test public void shrinkAdjustOffset() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - assumeTrue(buffer.size() > 4); - - try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) { - cursor.seek(buffer.size() - 1); - cursor.resizeBuffer(3); - assertEquals(3, cursor.offset); - assertEquals(null, cursor.data); - assertEquals(-1, cursor.start); - assertEquals(-1, cursor.end); - } - } - - @Test public void resizeToSameSizeSeeksToEnd() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - long originalSize = buffer.size(); - - try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) { - cursor.seek(buffer.size() / 2); - assertEquals(originalSize, buffer.size()); - cursor.resizeBuffer(originalSize); - assertEquals(originalSize, buffer.size()); - assertEquals(originalSize, cursor.offset); - assertNull(cursor.data); - assertEquals(-1, cursor.start); - assertEquals(-1, cursor.end); - } - } - - @Test public void resizeEnlargeMovesCursorToOldSize() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - long originalSize = buffer.size(); - - Buffer expected = deepCopy(buffer); - expected.writeUtf8("a"); - - try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) { - cursor.seek(buffer.size() / 2); - assertEquals(originalSize, buffer.size()); - cursor.resizeBuffer(originalSize + 1); - assertEquals(originalSize, cursor.offset); - assertNotNull(cursor.data); - assertNotEquals(-1, cursor.start); - assertEquals(cursor.start + 1, cursor.end); - cursor.data[cursor.start] = 'a'; - } - - assertEquals(expected, buffer); - } - - @Test public void resizeShrinkMovesCursorToEnd() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - assumeTrue(buffer.size() > 0); - long originalSize = buffer.size(); - - try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) { - cursor.seek(buffer.size() / 2); - assertEquals(originalSize, buffer.size()); - cursor.resizeBuffer(originalSize - 1); - assertEquals(originalSize - 1, cursor.offset); - assertNull(cursor.data); - assertEquals(-1, cursor.start); - assertEquals(-1, cursor.end); - } - } - - @Test public void expand() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - long originalSize = buffer.size(); - - Buffer expected = deepCopy(buffer); - expected.writeUtf8("abcde"); - - try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) { - cursor.expandBuffer(5); - - for (int i = 0; i < 5; i++) { - cursor.data[cursor.start + i] = (byte) ('a' + i); - } - - cursor.resizeBuffer(originalSize + 5); - } - - assertEquals(expected, buffer); - } - - @Test public void expandSameSegment() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - long originalSize = buffer.size(); - assumeTrue(originalSize > 0); - - try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) { - cursor.seek(originalSize - 1); - int originalEnd = cursor.end; - assumeTrue(originalEnd < SEGMENT_SIZE); - - long addedByteCount = cursor.expandBuffer(1); - assertEquals(SEGMENT_SIZE - originalEnd, addedByteCount); - - assertEquals(originalSize + addedByteCount, buffer.size()); - assertEquals(originalSize, cursor.offset); - assertEquals(originalEnd, cursor.start); - assertEquals(SEGMENT_SIZE, cursor.end); - } - } - - @Test public void expandNewSegment() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - long originalSize = buffer.size(); - - try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) { - long addedByteCount = cursor.expandBuffer(SEGMENT_SIZE); - assertEquals(SEGMENT_SIZE, addedByteCount); - - assertEquals(originalSize, cursor.offset); - assertEquals(0, cursor.start); - assertEquals(SEGMENT_SIZE, cursor.end); - } - } - - @Test public void expandMovesOffsetToOldSize() throws Exception { - Buffer buffer = bufferFactory.newBuffer(); - long originalSize = buffer.size(); - - try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) { - cursor.seek(buffer.size() / 2); - assertEquals(originalSize, buffer.size()); - long addedByteCount = cursor.expandBuffer(5); - assertEquals(originalSize + addedByteCount, buffer.size()); - assertEquals(originalSize, cursor.offset); - } - } -} diff --git a/okio/src/jvmTest/java/okio/BufferTest.java b/okio/src/jvmTest/java/okio/BufferTest.java deleted file mode 100644 index f5586d40..00000000 --- a/okio/src/jvmTest/java/okio/BufferTest.java +++ /dev/null @@ -1,580 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.List; -import java.util.Random; -import org.junit.Test; - -import static java.util.Arrays.asList; -import static kotlin.text.Charsets.UTF_8; -import static kotlin.text.StringsKt.repeat; -import static okio.TestUtil.SEGMENT_POOL_MAX_SIZE; -import static okio.TestUtil.SEGMENT_SIZE; -import static okio.TestUtil.assertNoEmptySegments; -import static okio.TestUtil.bufferWithRandomSegmentLayout; -import static okio.TestUtil.segmentPoolByteCount; -import static okio.TestUtil.segmentSizes; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -/** - * Tests solely for the behavior of Buffer's implementation. For generic BufferedSink or - * BufferedSource behavior use BufferedSinkTest or BufferedSourceTest, respectively. - */ -public final class BufferTest { - @Test public void readAndWriteUtf8() throws Exception { - Buffer buffer = new Buffer(); - buffer.writeUtf8("ab"); - assertEquals(2, buffer.size()); - buffer.writeUtf8("cdef"); - assertEquals(6, buffer.size()); - assertEquals("abcd", buffer.readUtf8(4)); - assertEquals(2, buffer.size()); - assertEquals("ef", buffer.readUtf8(2)); - assertEquals(0, buffer.size()); - try { - buffer.readUtf8(1); - fail(); - } catch (EOFException expected) { - } - } - - /** Buffer's toString is the same as ByteString's. */ - @Test public void bufferToString() { - assertEquals("[size=0]", new Buffer().toString()); - assertEquals("[text=a\\r\\nb\\nc\\rd\\\\e]", - new Buffer().writeUtf8("a\r\nb\nc\rd\\e").toString()); - assertEquals("[text=Tyrannosaur]", - new Buffer().writeUtf8("Tyrannosaur").toString()); - assertEquals("[text=təˈranəˌsôr]", new Buffer() - .write(ByteString.decodeHex("74c999cb8872616ec999cb8c73c3b472")) - .toString()); - assertEquals("[hex=0000000000000000000000000000000000000000000000000000000000000000000000000000" - + "0000000000000000000000000000000000000000000000000000]", - new Buffer().write(new byte[64]).toString()); - } - - @Test public void multipleSegmentBuffers() throws Exception { - Buffer buffer = new Buffer(); - buffer.writeUtf8(repeat("a", 1000)); - buffer.writeUtf8(repeat("b", 2500)); - buffer.writeUtf8(repeat("c", 5000)); - buffer.writeUtf8(repeat("d", 10000)); - buffer.writeUtf8(repeat("e", 25000)); - buffer.writeUtf8(repeat("f", 50000)); - - assertEquals(repeat("a", 999), buffer.readUtf8(999)); // a...a - assertEquals("a" + repeat("b", 2500) + "c", buffer.readUtf8(2502)); // ab...bc - assertEquals(repeat("c", 4998), buffer.readUtf8(4998)); // c...c - assertEquals("c" + repeat("d", 10000) + "e", buffer.readUtf8(10002)); // cd...de - assertEquals(repeat("e", 24998), buffer.readUtf8(24998)); // e...e - assertEquals("e" + repeat("f", 50000), buffer.readUtf8(50001)); // ef...f - assertEquals(0, buffer.size()); - } - - @Test public void fillAndDrainPool() throws Exception { - Buffer buffer = new Buffer(); - - // Take 2 * MAX_SIZE segments. This will drain the pool, even if other tests filled it. - buffer.write(new byte[(int) SEGMENT_POOL_MAX_SIZE]); - buffer.write(new byte[(int) SEGMENT_POOL_MAX_SIZE]); - assertEquals(0, segmentPoolByteCount()); - - // Recycle MAX_SIZE segments. They're all in the pool. - buffer.skip(SEGMENT_POOL_MAX_SIZE); - assertEquals(SEGMENT_POOL_MAX_SIZE, segmentPoolByteCount()); - - // Recycle MAX_SIZE more segments. The pool is full so they get garbage collected. - buffer.skip(SEGMENT_POOL_MAX_SIZE); - assertEquals(SEGMENT_POOL_MAX_SIZE, segmentPoolByteCount()); - - // Take MAX_SIZE segments to drain the pool. - buffer.write(new byte[(int) SEGMENT_POOL_MAX_SIZE]); - assertEquals(0, segmentPoolByteCount()); - - // Take MAX_SIZE more segments. The pool is drained so these will need to be allocated. - buffer.write(new byte[(int) SEGMENT_POOL_MAX_SIZE]); - assertEquals(0, segmentPoolByteCount()); - } - - @Test public void moveBytesBetweenBuffersShareSegment() throws Exception { - int size = (SEGMENT_SIZE / 2) - 1; - List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat("a", size), repeat("b", size)); - assertEquals(asList(size * 2), segmentSizes); - } - - @Test public void moveBytesBetweenBuffersReassignSegment() throws Exception { - int size = (SEGMENT_SIZE / 2) + 1; - List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat("a", size), repeat("b", size)); - assertEquals(asList(size, size), segmentSizes); - } - - @Test public void moveBytesBetweenBuffersMultipleSegments() throws Exception { - int size = 3 * SEGMENT_SIZE + 1; - List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat("a", size), repeat("b", size)); - assertEquals(asList(SEGMENT_SIZE, SEGMENT_SIZE, SEGMENT_SIZE, 1, - SEGMENT_SIZE, SEGMENT_SIZE, SEGMENT_SIZE, 1), segmentSizes); - } - - private List<Integer> moveBytesBetweenBuffers(String... contents) throws IOException { - StringBuilder expected = new StringBuilder(); - Buffer buffer = new Buffer(); - for (String s : contents) { - Buffer source = new Buffer(); - source.writeUtf8(s); - buffer.writeAll(source); - expected.append(s); - } - List<Integer> segmentSizes = segmentSizes(buffer); - assertEquals(expected.toString(), buffer.readUtf8(expected.length())); - return segmentSizes; - } - - /** The big part of source's first segment is being moved. */ - @Test public void writeSplitSourceBufferLeft() { - int writeSize = SEGMENT_SIZE / 2 + 1; - - Buffer sink = new Buffer(); - sink.writeUtf8(repeat("b", SEGMENT_SIZE - 10)); - - Buffer source = new Buffer(); - source.writeUtf8(repeat("a", SEGMENT_SIZE * 2)); - sink.write(source, writeSize); - - assertEquals(asList(SEGMENT_SIZE - 10, writeSize), segmentSizes(sink)); - assertEquals(asList(SEGMENT_SIZE - writeSize, SEGMENT_SIZE), segmentSizes(source)); - } - - /** The big part of source's first segment is staying put. */ - @Test public void writeSplitSourceBufferRight() { - int writeSize = SEGMENT_SIZE / 2 - 1; - - Buffer sink = new Buffer(); - sink.writeUtf8(repeat("b", SEGMENT_SIZE - 10)); - - Buffer source = new Buffer(); - source.writeUtf8(repeat("a", SEGMENT_SIZE * 2)); - sink.write(source, writeSize); - - assertEquals(asList(SEGMENT_SIZE - 10, writeSize), segmentSizes(sink)); - assertEquals(asList(SEGMENT_SIZE - writeSize, SEGMENT_SIZE), segmentSizes(source)); - } - - @Test public void writePrefixDoesntSplit() { - Buffer sink = new Buffer(); - sink.writeUtf8(repeat("b", 10)); - - Buffer source = new Buffer(); - source.writeUtf8(repeat("a", SEGMENT_SIZE * 2)); - sink.write(source, 20); - - assertEquals(asList(30), segmentSizes(sink)); - assertEquals(asList(SEGMENT_SIZE - 20, SEGMENT_SIZE), segmentSizes(source)); - assertEquals(30, sink.size()); - assertEquals(SEGMENT_SIZE * 2 - 20, source.size()); - } - - @Test public void writePrefixDoesntSplitButRequiresCompact() throws Exception { - Buffer sink = new Buffer(); - sink.writeUtf8(repeat("b", SEGMENT_SIZE - 10)); // limit = size - 10 - sink.readUtf8(SEGMENT_SIZE - 20); // pos = size = 20 - - Buffer source = new Buffer(); - source.writeUtf8(repeat("a", SEGMENT_SIZE * 2)); - sink.write(source, 20); - - assertEquals(asList(30), segmentSizes(sink)); - assertEquals(asList(SEGMENT_SIZE - 20, SEGMENT_SIZE), segmentSizes(source)); - assertEquals(30, sink.size()); - assertEquals(SEGMENT_SIZE * 2 - 20, source.size()); - } - - @Test public void copyToSpanningSegments() throws Exception { - Buffer source = new Buffer(); - source.writeUtf8(repeat("a", SEGMENT_SIZE * 2)); - source.writeUtf8(repeat("b", SEGMENT_SIZE * 2)); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - source.copyTo(out, 10, SEGMENT_SIZE * 3); - - assertEquals(repeat("a", SEGMENT_SIZE * 2 - 10) + repeat("b", SEGMENT_SIZE + 10), - out.toString()); - assertEquals(repeat("a", SEGMENT_SIZE * 2) + repeat("b", SEGMENT_SIZE * 2), - source.readUtf8(SEGMENT_SIZE * 4)); - } - - @Test public void copyToStream() throws Exception { - Buffer buffer = new Buffer().writeUtf8("hello, world!"); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - buffer.copyTo(out); - String outString = new String(out.toByteArray(), UTF_8); - assertEquals("hello, world!", outString); - assertEquals("hello, world!", buffer.readUtf8()); - } - - @Test public void writeToSpanningSegments() throws Exception { - Buffer buffer = new Buffer(); - buffer.writeUtf8(repeat("a", SEGMENT_SIZE * 2)); - buffer.writeUtf8(repeat("b", SEGMENT_SIZE * 2)); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - buffer.skip(10); - buffer.writeTo(out, SEGMENT_SIZE * 3); - - assertEquals(repeat("a", SEGMENT_SIZE * 2 - 10) + repeat("b", SEGMENT_SIZE + 10), - out.toString()); - assertEquals(repeat("b", SEGMENT_SIZE - 10), buffer.readUtf8(buffer.size())); - } - - @Test public void writeToStream() throws Exception { - Buffer buffer = new Buffer().writeUtf8("hello, world!"); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - buffer.writeTo(out); - String outString = new String(out.toByteArray(), UTF_8); - assertEquals("hello, world!", outString); - assertEquals(0, buffer.size()); - } - - @Test public void readFromStream() throws Exception { - InputStream in = new ByteArrayInputStream("hello, world!".getBytes(UTF_8)); - Buffer buffer = new Buffer(); - buffer.readFrom(in); - String out = buffer.readUtf8(); - assertEquals("hello, world!", out); - } - - @Test public void readFromSpanningSegments() throws Exception { - InputStream in = new ByteArrayInputStream("hello, world!".getBytes(UTF_8)); - Buffer buffer = new Buffer().writeUtf8(repeat("a", SEGMENT_SIZE - 10)); - buffer.readFrom(in); - String out = buffer.readUtf8(); - assertEquals(repeat("a", SEGMENT_SIZE - 10) + "hello, world!", out); - } - - @Test public void readFromStreamWithCount() throws Exception { - InputStream in = new ByteArrayInputStream("hello, world!".getBytes(UTF_8)); - Buffer buffer = new Buffer(); - buffer.readFrom(in, 10); - String out = buffer.readUtf8(); - assertEquals("hello, wor", out); - } - - @Test public void readFromDoesNotLeaveEmptyTailSegment() throws IOException { - Buffer buffer = new Buffer(); - buffer.readFrom(new ByteArrayInputStream(new byte[SEGMENT_SIZE])); - assertNoEmptySegments(buffer); - } - - @Test public void moveAllRequestedBytesWithRead() throws Exception { - Buffer sink = new Buffer(); - sink.writeUtf8(repeat("a", 10)); - - Buffer source = new Buffer(); - source.writeUtf8(repeat("b", 15)); - - assertEquals(10, source.read(sink, 10)); - assertEquals(20, sink.size()); - assertEquals(5, source.size()); - assertEquals(repeat("a", 10) + repeat("b", 10), sink.readUtf8(20)); - } - - @Test public void moveFewerThanRequestedBytesWithRead() throws Exception { - Buffer sink = new Buffer(); - sink.writeUtf8(repeat("a", 10)); - - Buffer source = new Buffer(); - source.writeUtf8(repeat("b", 20)); - - assertEquals(20, source.read(sink, 25)); - assertEquals(30, sink.size()); - assertEquals(0, source.size()); - assertEquals(repeat("a", 10) + repeat("b", 20), sink.readUtf8(30)); - } - - @Test public void indexOfWithOffset() { - Buffer buffer = new Buffer(); - int halfSegment = SEGMENT_SIZE / 2; - buffer.writeUtf8(repeat("a", halfSegment)); - buffer.writeUtf8(repeat("b", halfSegment)); - buffer.writeUtf8(repeat("c", halfSegment)); - buffer.writeUtf8(repeat("d", halfSegment)); - assertEquals(0, buffer.indexOf((byte) 'a', 0)); - assertEquals(halfSegment - 1, buffer.indexOf((byte) 'a', halfSegment - 1)); - assertEquals(halfSegment, buffer.indexOf((byte) 'b', halfSegment - 1)); - assertEquals(halfSegment * 2, buffer.indexOf((byte) 'c', halfSegment - 1)); - assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment - 1)); - assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment * 2)); - assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment * 3)); - assertEquals(halfSegment * 4 - 1, buffer.indexOf((byte) 'd', halfSegment * 4 - 1)); - } - - @Test public void byteAt() { - Buffer buffer = new Buffer(); - buffer.writeUtf8("a"); - buffer.writeUtf8(repeat("b", SEGMENT_SIZE)); - buffer.writeUtf8("c"); - assertEquals('a', buffer.getByte(0)); - assertEquals('a', buffer.getByte(0)); // getByte doesn't mutate! - assertEquals('c', buffer.getByte(buffer.size() - 1)); - assertEquals('b', buffer.getByte(buffer.size() - 2)); - assertEquals('b', buffer.getByte(buffer.size() - 3)); - } - - @Test public void getByteOfEmptyBuffer() { - Buffer buffer = new Buffer(); - try { - buffer.getByte(0); - fail(); - } catch (IndexOutOfBoundsException expected) { - } - } - - @Test public void writePrefixToEmptyBuffer() throws IOException { - Buffer sink = new Buffer(); - Buffer source = new Buffer(); - source.writeUtf8("abcd"); - sink.write(source, 2); - assertEquals("ab", sink.readUtf8(2)); - } - - @Test public void cloneDoesNotObserveWritesToOriginal() { - Buffer original = new Buffer(); - Buffer clone = original.clone(); - original.writeUtf8("abc"); - assertEquals(0, clone.size()); - } - - @Test public void cloneDoesNotObserveReadsFromOriginal() throws Exception { - Buffer original = new Buffer(); - original.writeUtf8("abc"); - Buffer clone = original.clone(); - assertEquals("abc", original.readUtf8(3)); - assertEquals(3, clone.size()); - assertEquals("ab", clone.readUtf8(2)); - } - - @Test public void originalDoesNotObserveWritesToClone() { - Buffer original = new Buffer(); - Buffer clone = original.clone(); - clone.writeUtf8("abc"); - assertEquals(0, original.size()); - } - - @Test public void originalDoesNotObserveReadsFromClone() throws Exception { - Buffer original = new Buffer(); - original.writeUtf8("abc"); - Buffer clone = original.clone(); - assertEquals("abc", clone.readUtf8(3)); - assertEquals(3, original.size()); - assertEquals("ab", original.readUtf8(2)); - } - - @Test public void cloneMultipleSegments() throws Exception { - Buffer original = new Buffer(); - original.writeUtf8(repeat("a", SEGMENT_SIZE * 3)); - Buffer clone = original.clone(); - original.writeUtf8(repeat("b", SEGMENT_SIZE * 3)); - clone.writeUtf8(repeat("c", SEGMENT_SIZE * 3)); - - assertEquals(repeat("a", SEGMENT_SIZE * 3) + repeat("b", SEGMENT_SIZE * 3), - original.readUtf8(SEGMENT_SIZE * 6)); - assertEquals(repeat("a", SEGMENT_SIZE * 3) + repeat("c", SEGMENT_SIZE * 3), - clone.readUtf8(SEGMENT_SIZE * 6)); - } - - @Test public void equalsAndHashCodeEmpty() { - Buffer a = new Buffer(); - Buffer b = new Buffer(); - assertTrue(a.equals(b)); - assertTrue(a.hashCode() == b.hashCode()); - } - - @Test public void equalsAndHashCode() throws Exception { - Buffer a = new Buffer().writeUtf8("dog"); - Buffer b = new Buffer().writeUtf8("hotdog"); - assertFalse(a.equals(b)); - assertFalse(a.hashCode() == b.hashCode()); - - b.readUtf8(3); // Leaves b containing 'dog'. - assertTrue(a.equals(b)); - assertTrue(a.hashCode() == b.hashCode()); - } - - @Test public void equalsAndHashCodeSpanningSegments() throws Exception { - byte[] data = new byte[1024 * 1024]; - Random dice = new Random(0); - dice.nextBytes(data); - - Buffer a = bufferWithRandomSegmentLayout(dice, data); - Buffer b = bufferWithRandomSegmentLayout(dice, data); - assertTrue(a.equals(b)); - assertTrue(a.hashCode() == b.hashCode()); - - data[data.length / 2]++; // Change a single byte. - Buffer c = bufferWithRandomSegmentLayout(dice, data); - assertFalse(a.equals(c)); - assertFalse(a.hashCode() == c.hashCode()); - } - - @Test public void bufferInputStreamByteByByte() throws Exception { - Buffer source = new Buffer(); - source.writeUtf8("abc"); - - InputStream in = source.inputStream(); - assertEquals(3, in.available()); - assertEquals('a', in.read()); - assertEquals('b', in.read()); - assertEquals('c', in.read()); - assertEquals(-1, in.read()); - assertEquals(0, in.available()); - } - - @Test public void bufferInputStreamBulkReads() throws Exception { - Buffer source = new Buffer(); - source.writeUtf8("abc"); - - byte[] byteArray = new byte[4]; - - Arrays.fill(byteArray, (byte) -5); - InputStream in = source.inputStream(); - assertEquals(3, in.read(byteArray)); - assertEquals("[97, 98, 99, -5]", Arrays.toString(byteArray)); - - Arrays.fill(byteArray, (byte) -7); - assertEquals(-1, in.read(byteArray)); - assertEquals("[-7, -7, -7, -7]", Arrays.toString(byteArray)); - } - - /** - * When writing data that's already buffered, there's no reason to page the - * data by segment. - */ - @Test public void readAllWritesAllSegmentsAtOnce() throws Exception { - Buffer write1 = new Buffer().writeUtf8("" - + repeat("a", SEGMENT_SIZE) - + repeat("b", SEGMENT_SIZE) - + repeat("c", SEGMENT_SIZE)); - - Buffer source = new Buffer().writeUtf8("" - + repeat("a", SEGMENT_SIZE) - + repeat("b", SEGMENT_SIZE) - + repeat("c", SEGMENT_SIZE)); - - MockSink mockSink = new MockSink(); - - assertEquals(SEGMENT_SIZE * 3, source.readAll(mockSink)); - assertEquals(0, source.size()); - mockSink.assertLog("write(" + write1 + ", " + write1.size() + ")"); - } - - @Test public void writeAllMultipleSegments() throws Exception { - Buffer source = new Buffer().writeUtf8(repeat("a", SEGMENT_SIZE * 3)); - Buffer sink = new Buffer(); - - assertEquals(SEGMENT_SIZE * 3, sink.writeAll(source)); - assertEquals(0, source.size()); - assertEquals(repeat("a", SEGMENT_SIZE * 3), sink.readUtf8()); - } - - @Test public void copyTo() { - Buffer source = new Buffer(); - source.writeUtf8("party"); - - Buffer target = new Buffer(); - source.copyTo(target, 1, 3); - - assertEquals("art", target.readUtf8()); - assertEquals("party", source.readUtf8()); - } - - @Test public void copyToOnSegmentBoundary() { - String as = repeat("a", SEGMENT_SIZE); - String bs = repeat("b", SEGMENT_SIZE); - String cs = repeat("c", SEGMENT_SIZE); - String ds = repeat("d", SEGMENT_SIZE); - - Buffer source = new Buffer(); - source.writeUtf8(as); - source.writeUtf8(bs); - source.writeUtf8(cs); - - Buffer target = new Buffer(); - target.writeUtf8(ds); - - source.copyTo(target, as.length(), bs.length() + cs.length()); - assertEquals(ds + bs + cs, target.readUtf8()); - } - - @Test public void copyToOffSegmentBoundary() { - String as = repeat("a", SEGMENT_SIZE - 1); - String bs = repeat("b", SEGMENT_SIZE + 2); - String cs = repeat("c", SEGMENT_SIZE - 4); - String ds = repeat("d", SEGMENT_SIZE + 8); - - Buffer source = new Buffer(); - source.writeUtf8(as); - source.writeUtf8(bs); - source.writeUtf8(cs); - - Buffer target = new Buffer(); - target.writeUtf8(ds); - - source.copyTo(target, as.length(), bs.length() + cs.length()); - assertEquals(ds + bs + cs, target.readUtf8()); - } - - @Test public void copyToSourceAndTargetCanBeTheSame() { - String as = repeat("a", SEGMENT_SIZE); - String bs = repeat("b", SEGMENT_SIZE); - - Buffer source = new Buffer(); - source.writeUtf8(as); - source.writeUtf8(bs); - - source.copyTo(source, 0, source.size()); - assertEquals(as + bs + as + bs, source.readUtf8()); - } - - @Test public void copyToEmptySource() { - Buffer source = new Buffer(); - Buffer target = new Buffer().writeUtf8("aaa"); - source.copyTo(target, 0L, 0L); - assertEquals("", source.readUtf8()); - assertEquals("aaa", target.readUtf8()); - } - - @Test public void copyToEmptyTarget() { - Buffer source = new Buffer().writeUtf8("aaa"); - Buffer target = new Buffer(); - source.copyTo(target, 0L, 3L); - assertEquals("aaa", source.readUtf8()); - assertEquals("aaa", target.readUtf8()); - } - - @Test public void snapshotReportsAccurateSize() { - Buffer buf = new Buffer().write(new byte[] { 0, 1, 2, 3 }); - assertEquals(1, buf.snapshot(1).size()); - } -} diff --git a/okio/src/jvmTest/java/okio/BufferedSinkJavaTest.java b/okio/src/jvmTest/java/okio/BufferedSinkJavaTest.java deleted file mode 100644 index 357b992a..00000000 --- a/okio/src/jvmTest/java/okio/BufferedSinkJavaTest.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 java.io.IOException; -import java.io.OutputStream; -import org.junit.Test; - -import static kotlin.text.StringsKt.repeat; -import static okio.TestUtil.SEGMENT_SIZE; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * Tests solely for the behavior of RealBufferedSink's implementation. For generic - * BufferedSink behavior use BufferedSinkTest. - */ -public final class BufferedSinkJavaTest { - @Test public void inputStreamCloses() throws Exception { - BufferedSink sink = Okio.buffer((Sink) new Buffer()); - OutputStream out = sink.outputStream(); - out.close(); - try { - sink.writeUtf8("Hi!"); - fail(); - } catch (IllegalStateException e) { - assertEquals("closed", e.getMessage()); - } - } - - @Test public void bufferedSinkEmitsTailWhenItIsComplete() throws IOException { - Buffer sink = new Buffer(); - BufferedSink bufferedSink = Okio.buffer((Sink) sink); - bufferedSink.writeUtf8(repeat("a", SEGMENT_SIZE - 1)); - assertEquals(0, sink.size()); - bufferedSink.writeByte(0); - assertEquals(SEGMENT_SIZE, sink.size()); - assertEquals(0, bufferedSink.getBuffer().size()); - } - - @Test public void bufferedSinkEmitMultipleSegments() throws IOException { - Buffer sink = new Buffer(); - BufferedSink bufferedSink = Okio.buffer((Sink) sink); - bufferedSink.writeUtf8(repeat("a", SEGMENT_SIZE * 4 - 1)); - assertEquals(SEGMENT_SIZE * 3, sink.size()); - assertEquals(SEGMENT_SIZE - 1, bufferedSink.getBuffer().size()); - } - - @Test public void bufferedSinkFlush() throws IOException { - Buffer sink = new Buffer(); - BufferedSink bufferedSink = Okio.buffer((Sink) sink); - bufferedSink.writeByte('a'); - assertEquals(0, sink.size()); - bufferedSink.flush(); - assertEquals(0, bufferedSink.getBuffer().size()); - assertEquals(1, sink.size()); - } - - @Test public void bytesEmittedToSinkWithFlush() throws Exception { - Buffer sink = new Buffer(); - BufferedSink bufferedSink = Okio.buffer((Sink) sink); - bufferedSink.writeUtf8("abc"); - bufferedSink.flush(); - assertEquals(3, sink.size()); - } - - @Test public void bytesNotEmittedToSinkWithoutFlush() throws Exception { - Buffer sink = new Buffer(); - BufferedSink bufferedSink = Okio.buffer((Sink) sink); - bufferedSink.writeUtf8("abc"); - assertEquals(0, sink.size()); - } - - @Test public void bytesEmittedToSinkWithEmit() throws Exception { - Buffer sink = new Buffer(); - BufferedSink bufferedSink = Okio.buffer((Sink) sink); - bufferedSink.writeUtf8("abc"); - bufferedSink.emit(); - assertEquals(3, sink.size()); - } - - @Test public void completeSegmentsEmitted() throws Exception { - Buffer sink = new Buffer(); - BufferedSink bufferedSink = Okio.buffer((Sink) sink); - bufferedSink.writeUtf8(repeat("a", SEGMENT_SIZE * 3)); - assertEquals(SEGMENT_SIZE * 3, sink.size()); - } - - @Test public void incompleteSegmentsNotEmitted() throws Exception { - Buffer sink = new Buffer(); - BufferedSink bufferedSink = Okio.buffer((Sink) sink); - bufferedSink.writeUtf8(repeat("a", SEGMENT_SIZE * 3 - 1)); - assertEquals(SEGMENT_SIZE * 2, sink.size()); - } - - @Test public void closeWithExceptionWhenWriting() throws IOException { - MockSink mockSink = new MockSink(); - mockSink.scheduleThrow(0, new IOException()); - BufferedSink bufferedSink = Okio.buffer(mockSink); - bufferedSink.writeByte('a'); - try { - bufferedSink.close(); - fail(); - } catch (IOException expected) { - } - mockSink.assertLog("write([text=a], 1)", "close()"); - } - - @Test public void closeWithExceptionWhenClosing() throws IOException { - MockSink mockSink = new MockSink(); - mockSink.scheduleThrow(1, new IOException()); - BufferedSink bufferedSink = Okio.buffer(mockSink); - bufferedSink.writeByte('a'); - try { - bufferedSink.close(); - fail(); - } catch (IOException expected) { - } - mockSink.assertLog("write([text=a], 1)", "close()"); - } - - @Test public void closeWithExceptionWhenWritingAndClosing() throws IOException { - MockSink mockSink = new MockSink(); - mockSink.scheduleThrow(0, new IOException("first")); - mockSink.scheduleThrow(1, new IOException("second")); - BufferedSink bufferedSink = Okio.buffer(mockSink); - bufferedSink.writeByte('a'); - try { - bufferedSink.close(); - fail(); - } catch (IOException expected) { - assertEquals("first", expected.getMessage()); - } - mockSink.assertLog("write([text=a], 1)", "close()"); - } - - @Test public void operationsAfterClose() throws IOException { - MockSink mockSink = new MockSink(); - BufferedSink bufferedSink = Okio.buffer(mockSink); - bufferedSink.writeByte('a'); - bufferedSink.close(); - - // Test a sample set of methods. - try { - bufferedSink.writeByte('a'); - fail(); - } catch (IllegalStateException expected) { - } - - try { - bufferedSink.write(new byte[10]); - fail(); - } catch (IllegalStateException expected) { - } - - try { - bufferedSink.emitCompleteSegments(); - fail(); - } catch (IllegalStateException expected) { - } - - try { - bufferedSink.emit(); - fail(); - } catch (IllegalStateException expected) { - } - - try { - bufferedSink.flush(); - fail(); - } catch (IllegalStateException expected) { - } - - // Test a sample set of methods on the OutputStream. - OutputStream os = bufferedSink.outputStream(); - try { - os.write('a'); - fail(); - } catch (IOException expected) { - } - - try { - os.write(new byte[10]); - fail(); - } catch (IOException expected) { - } - - // Permitted - os.flush(); - } - - @Test public void writeAll() throws IOException { - MockSink mockSink = new MockSink(); - BufferedSink bufferedSink = Okio.buffer(mockSink); - - bufferedSink.getBuffer().writeUtf8("abc"); - assertEquals(3, bufferedSink.writeAll(new Buffer().writeUtf8("def"))); - - assertEquals(6, bufferedSink.getBuffer().size()); - assertEquals("abcdef", bufferedSink.getBuffer().readUtf8(6)); - mockSink.assertLog(); // No writes. - } - - @Test public void writeAllExhausted() throws IOException { - MockSink mockSink = new MockSink(); - BufferedSink bufferedSink = Okio.buffer(mockSink); - - assertEquals(0, bufferedSink.writeAll(new Buffer())); - assertEquals(0, bufferedSink.getBuffer().size()); - mockSink.assertLog(); // No writes. - } - - @Test public void writeAllWritesOneSegmentAtATime() throws IOException { - Buffer write1 = new Buffer().writeUtf8(repeat("a", SEGMENT_SIZE)); - Buffer write2 = new Buffer().writeUtf8(repeat("b", SEGMENT_SIZE)); - Buffer write3 = new Buffer().writeUtf8(repeat("c", SEGMENT_SIZE)); - - Buffer source = new Buffer().writeUtf8("" - + repeat("a", SEGMENT_SIZE) - + repeat("b", SEGMENT_SIZE) - + repeat("c", SEGMENT_SIZE)); - - MockSink mockSink = new MockSink(); - BufferedSink bufferedSink = Okio.buffer(mockSink); - assertEquals(SEGMENT_SIZE * 3, bufferedSink.writeAll(source)); - - mockSink.assertLog( - "write(" + write1 + ", " + write1.size() + ")", - "write(" + write2 + ", " + write2.size() + ")", - "write(" + write3 + ", " + write3.size() + ")"); - } -} diff --git a/okio/src/jvmTest/java/okio/BufferedSinkTest.java b/okio/src/jvmTest/java/okio/BufferedSinkTest.java deleted file mode 100644 index e0132846..00000000 --- a/okio/src/jvmTest/java/okio/BufferedSinkTest.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 java.io.EOFException; -import java.io.IOException; -import java.io.OutputStream; -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.List; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; - -import static java.util.Arrays.asList; -import static kotlin.text.Charsets.UTF_8; -import static kotlin.text.StringsKt.repeat; -import static okio.TestUtil.SEGMENT_SIZE; -import static okio.TestUtil.segmentSizes; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -@RunWith(Parameterized.class) -public final class BufferedSinkTest { - private interface Factory { - Factory BUFFER = new Factory() { - @Override public BufferedSink create(Buffer data) { - return data; - } - - @Override public String toString() { - return "Buffer"; - } - }; - - Factory REAL_BUFFERED_SINK = new Factory() { - @Override public BufferedSink create(Buffer data) { - return Okio.buffer((Sink) data); - } - - @Override public String toString() { - return "RealBufferedSink"; - } - }; - - BufferedSink create(Buffer data); - } - - @Parameters(name = "{0}") - public static List<Object[]> parameters() { - return Arrays.asList( - new Object[] {Factory.BUFFER}, - new Object[] {Factory.REAL_BUFFERED_SINK}); - } - - @Parameter public Factory factory; - private Buffer data; - private BufferedSink sink; - - @Before public void setUp() { - data = new Buffer(); - sink = factory.create(data); - } - - @Test public void writeNothing() throws IOException { - sink.writeUtf8(""); - sink.flush(); - assertEquals(0, data.size()); - } - - @Test public void writeBytes() throws Exception { - sink.writeByte(0xab); - sink.writeByte(0xcd); - sink.flush(); - assertEquals("[hex=abcd]", data.toString()); - } - - @Test public void writeLastByteInSegment() throws Exception { - sink.writeUtf8(repeat("a", SEGMENT_SIZE - 1)); - sink.writeByte(0x20); - sink.writeByte(0x21); - sink.flush(); - assertEquals(asList(SEGMENT_SIZE, 1), segmentSizes(data)); - assertEquals(repeat("a", SEGMENT_SIZE - 1), data.readUtf8(SEGMENT_SIZE - 1)); - assertEquals("[text= !]", data.toString()); - } - - @Test public void writeShort() throws Exception { - sink.writeShort(0xabcd); - sink.writeShort(0x4321); - sink.flush(); - assertEquals("[hex=abcd4321]", data.toString()); - } - - @Test public void writeShortLe() throws Exception { - sink.writeShortLe(0xcdab); - sink.writeShortLe(0x2143); - sink.flush(); - assertEquals("[hex=abcd4321]", data.toString()); - } - - @Test public void writeInt() throws Exception { - sink.writeInt(0xabcdef01); - sink.writeInt(0x87654321); - sink.flush(); - assertEquals("[hex=abcdef0187654321]", data.toString()); - } - - @Test public void writeLastIntegerInSegment() throws Exception { - sink.writeUtf8(repeat("a", SEGMENT_SIZE - 4)); - sink.writeInt(0xabcdef01); - sink.writeInt(0x87654321); - sink.flush(); - assertEquals(asList(SEGMENT_SIZE, 4), segmentSizes(data)); - assertEquals(repeat("a", SEGMENT_SIZE - 4), data.readUtf8(SEGMENT_SIZE - 4)); - assertEquals("[hex=abcdef0187654321]", data.toString()); - } - - @Test public void writeIntegerDoesNotQuiteFitInSegment() throws Exception { - sink.writeUtf8(repeat("a", SEGMENT_SIZE - 3)); - sink.writeInt(0xabcdef01); - sink.writeInt(0x87654321); - sink.flush(); - assertEquals(asList(SEGMENT_SIZE - 3, 8), segmentSizes(data)); - assertEquals(repeat("a", SEGMENT_SIZE - 3), data.readUtf8(SEGMENT_SIZE - 3)); - assertEquals("[hex=abcdef0187654321]", data.toString()); - } - - @Test public void writeIntLe() throws Exception { - sink.writeIntLe(0xabcdef01); - sink.writeIntLe(0x87654321); - sink.flush(); - assertEquals("[hex=01efcdab21436587]", data.toString()); - } - - @Test public void writeLong() throws Exception { - sink.writeLong(0xabcdef0187654321L); - sink.writeLong(0xcafebabeb0b15c00L); - sink.flush(); - assertEquals("[hex=abcdef0187654321cafebabeb0b15c00]", data.toString()); - } - - @Test public void writeLongLe() throws Exception { - sink.writeLongLe(0xabcdef0187654321L); - sink.writeLongLe(0xcafebabeb0b15c00L); - sink.flush(); - assertEquals("[hex=2143658701efcdab005cb1b0bebafeca]", data.toString()); - } - - @Test public void writeByteString() throws IOException { - sink.write(ByteString.encodeUtf8("təˈranəˌsôr")); - sink.flush(); - assertEquals(ByteString.decodeHex("74c999cb8872616ec999cb8c73c3b472"), data.readByteString()); - } - - @Test public void writeByteStringOffset() throws IOException { - sink.write(ByteString.encodeUtf8("təˈranəˌsôr"), 5, 5); - sink.flush(); - assertEquals(ByteString.decodeHex("72616ec999"), data.readByteString()); - } - - @Test public void writeSegmentedByteString() throws IOException { - sink.write(new Buffer().write(ByteString.encodeUtf8("təˈranəˌsôr")).snapshot()); - sink.flush(); - assertEquals(ByteString.decodeHex("74c999cb8872616ec999cb8c73c3b472"), data.readByteString()); - } - - @Test public void writeSegmentedByteStringOffset() throws IOException { - sink.write(new Buffer().write(ByteString.encodeUtf8("təˈranəˌsôr")).snapshot(), 5, 5); - sink.flush(); - assertEquals(ByteString.decodeHex("72616ec999"), data.readByteString()); - } - - @Test public void writeStringUtf8() throws IOException { - sink.writeUtf8("təˈranəˌsôr"); - sink.flush(); - assertEquals(ByteString.decodeHex("74c999cb8872616ec999cb8c73c3b472"), data.readByteString()); - } - - @Test public void writeSubstringUtf8() throws IOException { - sink.writeUtf8("təˈranəˌsôr", 3, 7); - sink.flush(); - assertEquals(ByteString.decodeHex("72616ec999"), data.readByteString()); - } - - @Test public void writeStringWithCharset() throws IOException { - sink.writeString("təˈranəˌsôr", Charset.forName("utf-32be")); - sink.flush(); - assertEquals(ByteString.decodeHex("0000007400000259000002c800000072000000610000006e00000259" - + "000002cc00000073000000f400000072"), data.readByteString()); - } - - @Test public void writeSubstringWithCharset() throws IOException { - sink.writeString("təˈranəˌsôr", 3, 7, Charset.forName("utf-32be")); - sink.flush(); - assertEquals(ByteString.decodeHex("00000072000000610000006e00000259"), data.readByteString()); - } - - @Test public void writeUtf8SubstringWithCharset() throws IOException { - sink.writeString("təˈranəˌsôr", 3, 7, Charset.forName("utf-8")); - sink.flush(); - assertEquals(ByteString.encodeUtf8("ranə"), data.readByteString()); - } - - @Test public void writeAll() throws Exception { - Buffer source = new Buffer().writeUtf8("abcdef"); - - assertEquals(6, sink.writeAll(source)); - assertEquals(0, source.size()); - sink.flush(); - assertEquals("abcdef", data.readUtf8()); - } - - @Test public void writeSource() throws Exception { - Buffer source = new Buffer().writeUtf8("abcdef"); - - // Force resolution of the Source method overload. - sink.write((Source) source, 4); - sink.flush(); - assertEquals("abcd", data.readUtf8()); - assertEquals("ef", source.readUtf8()); - } - - @Test public void writeSourceReadsFully() throws Exception { - Source source = new ForwardingSource(new Buffer()) { - @Override public long read(Buffer sink, long byteCount) throws IOException { - sink.writeUtf8("abcd"); - return 4; - } - }; - - sink.write(source, 8); - sink.flush(); - assertEquals("abcdabcd", data.readUtf8()); - } - - @Test public void writeSourcePropagatesEof() throws IOException { - Source source = new Buffer().writeUtf8("abcd"); - - try { - sink.write(source, 8); - fail(); - } catch (EOFException expected) { - } - - // Ensure that whatever was available was correctly written. - sink.flush(); - assertEquals("abcd", data.readUtf8()); - } - - @Test public void writeSourceWithZeroIsNoOp() throws IOException { - // This test ensures that a zero byte count never calls through to read the source. It may be - // tied to something like a socket which will potentially block trying to read a segment when - // ultimately we don't want any data. - Source source = new ForwardingSource(new Buffer()) { - @Override public long read(Buffer sink, long byteCount) throws IOException { - throw new AssertionError(); - } - }; - sink.write(source, 0); - assertEquals(0, data.size()); - } - - @Test public void writeAllExhausted() throws Exception { - Buffer source = new Buffer(); - assertEquals(0, sink.writeAll(source)); - assertEquals(0, source.size()); - } - - @Test public void closeEmitsBufferedBytes() throws IOException { - sink.writeByte('a'); - sink.close(); - assertEquals('a', data.readByte()); - } - - @Test public void outputStream() throws Exception { - OutputStream out = sink.outputStream(); - out.write('a'); - out.write(repeat("b", 9998).getBytes(UTF_8)); - out.write('c'); - out.flush(); - assertEquals("a" + repeat("b", 9998) + "c", data.readUtf8()); - } - - @Test public void outputStreamBounds() throws Exception { - OutputStream out = sink.outputStream(); - try { - out.write(new byte[100], 50, 51); - fail(); - } catch (ArrayIndexOutOfBoundsException expected) { - } - } - - @Test public void longDecimalString() throws IOException { - assertLongDecimalString(0); - assertLongDecimalString(Long.MIN_VALUE); - assertLongDecimalString(Long.MAX_VALUE); - - for (int i = 1; i < 20; i++) { - long value = BigInteger.valueOf(10L).pow(i).longValue(); - assertLongDecimalString(value - 1); - assertLongDecimalString(value); - } - } - - private void assertLongDecimalString(long value) throws IOException { - sink.writeDecimalLong(value).writeUtf8("zzz").flush(); - String expected = Long.toString(value) + "zzz"; - String actual = data.readUtf8(); - assertEquals(value + " expected " + expected + " but was " + actual, actual, expected); - } - - @Test public void longHexString() throws IOException { - assertLongHexString(0); - assertLongHexString(Long.MIN_VALUE); - assertLongHexString(Long.MAX_VALUE); - - for (int i = 0; i < 63; i++) { - assertLongHexString((1L << i) - 1); - assertLongHexString(1L << i); - } - } - - @Test public void writeNioBuffer() throws Exception { - String expected = "abcdefg"; - - ByteBuffer nioByteBuffer = ByteBuffer.allocate(1024); - nioByteBuffer.put("abcdefg".getBytes(UTF_8)); - nioByteBuffer.flip(); - - int byteCount = sink.write(nioByteBuffer); - assertEquals(expected.length(), byteCount); - assertEquals(expected.length(), nioByteBuffer.position()); - assertEquals(expected.length(), nioByteBuffer.limit()); - - sink.flush(); - assertEquals(expected, data.readUtf8()); - } - - @Test public void writeLargeNioBufferWritesAllData() throws Exception { - String expected = repeat("a", SEGMENT_SIZE * 3); - - ByteBuffer nioByteBuffer = ByteBuffer.allocate(SEGMENT_SIZE * 4); - nioByteBuffer.put(repeat("a", SEGMENT_SIZE * 3).getBytes(UTF_8)); - nioByteBuffer.flip(); - - int byteCount = sink.write(nioByteBuffer); - assertEquals(expected.length(), byteCount); - assertEquals(expected.length(), nioByteBuffer.position()); - assertEquals(expected.length(), nioByteBuffer.limit()); - - sink.flush(); - assertEquals(expected, data.readUtf8()); - } - - private void assertLongHexString(long value) throws IOException { - sink.writeHexadecimalUnsignedLong(value).writeUtf8("zzz").flush(); - String expected = String.format("%x", value) + "zzz"; - String actual = data.readUtf8(); - assertEquals(value + " expected " + expected + " but was " + actual, actual, expected); - } -} diff --git a/okio/src/jvmTest/java/okio/BufferedSourceJavaTest.java b/okio/src/jvmTest/java/okio/BufferedSourceJavaTest.java deleted file mode 100644 index 470a2ddc..00000000 --- a/okio/src/jvmTest/java/okio/BufferedSourceJavaTest.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import org.junit.Test; - -import static kotlin.text.Charsets.UTF_8; -import static kotlin.text.StringsKt.repeat; -import static okio.TestUtil.SEGMENT_SIZE; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * Tests solely for the behavior of RealBufferedSource's implementation. For generic - * BufferedSource behavior use BufferedSourceTest. - */ -public final class BufferedSourceJavaTest { - @Test public void inputStreamTracksSegments() throws Exception { - Buffer source = new Buffer(); - source.writeUtf8("a"); - source.writeUtf8(repeat("b", SEGMENT_SIZE)); - source.writeUtf8("c"); - - InputStream in = Okio.buffer((Source) source).inputStream(); - assertEquals(0, in.available()); - assertEquals(SEGMENT_SIZE + 2, source.size()); - - // Reading one byte buffers a full segment. - assertEquals('a', in.read()); - assertEquals(SEGMENT_SIZE - 1, in.available()); - assertEquals(2, source.size()); - - // Reading as much as possible reads the rest of that buffered segment. - byte[] data = new byte[SEGMENT_SIZE * 2]; - assertEquals(SEGMENT_SIZE - 1, in.read(data, 0, data.length)); - assertEquals(repeat("b", SEGMENT_SIZE - 1), new String(data, 0, SEGMENT_SIZE - 1, UTF_8)); - assertEquals(2, source.size()); - - // Continuing to read buffers the next segment. - assertEquals('b', in.read()); - assertEquals(1, in.available()); - assertEquals(0, source.size()); - - // Continuing to read reads from the buffer. - assertEquals('c', in.read()); - assertEquals(0, in.available()); - assertEquals(0, source.size()); - - // Once we've exhausted the source, we're done. - assertEquals(-1, in.read()); - assertEquals(0, source.size()); - } - - @Test public void inputStreamCloses() throws Exception { - BufferedSource source = Okio.buffer((Source) new Buffer()); - InputStream in = source.inputStream(); - in.close(); - try { - source.require(1); - fail(); - } catch (IllegalStateException e) { - assertEquals("closed", e.getMessage()); - } - } - - @Test public void indexOfStopsReadingAtLimit() throws Exception { - Buffer buffer = new Buffer().writeUtf8("abcdef"); - BufferedSource bufferedSource = Okio.buffer(new ForwardingSource(buffer) { - @Override public long read(Buffer sink, long byteCount) throws IOException { - return super.read(sink, Math.min(1, byteCount)); - } - }); - - assertEquals(6, buffer.size()); - assertEquals(-1, bufferedSource.indexOf((byte) 'e', 0, 4)); - assertEquals(2, buffer.size()); - } - - @Test public void requireTracksBufferFirst() throws Exception { - Buffer source = new Buffer(); - source.writeUtf8("bb"); - - BufferedSource bufferedSource = Okio.buffer((Source) source); - bufferedSource.getBuffer().writeUtf8("aa"); - - bufferedSource.require(2); - assertEquals(2, bufferedSource.getBuffer().size()); - assertEquals(2, source.size()); - } - - @Test public void requireIncludesBufferBytes() throws Exception { - Buffer source = new Buffer(); - source.writeUtf8("b"); - - BufferedSource bufferedSource = Okio.buffer((Source) source); - bufferedSource.getBuffer().writeUtf8("a"); - - bufferedSource.require(2); - assertEquals("ab", bufferedSource.getBuffer().readUtf8(2)); - } - - @Test public void requireInsufficientData() throws Exception { - Buffer source = new Buffer(); - source.writeUtf8("a"); - - BufferedSource bufferedSource = Okio.buffer((Source) source); - - try { - bufferedSource.require(2); - fail(); - } catch (EOFException expected) { - } - } - - @Test public void requireReadsOneSegmentAtATime() throws Exception { - Buffer source = new Buffer(); - source.writeUtf8(repeat("a", SEGMENT_SIZE)); - source.writeUtf8(repeat("b", SEGMENT_SIZE)); - - BufferedSource bufferedSource = Okio.buffer((Source) source); - - bufferedSource.require(2); - assertEquals(SEGMENT_SIZE, source.size()); - assertEquals(SEGMENT_SIZE, bufferedSource.getBuffer().size()); - } - - @Test public void skipReadsOneSegmentAtATime() throws Exception { - Buffer source = new Buffer(); - source.writeUtf8(repeat("a", SEGMENT_SIZE)); - source.writeUtf8(repeat("b", SEGMENT_SIZE)); - BufferedSource bufferedSource = Okio.buffer((Source) source); - bufferedSource.skip(2); - assertEquals(SEGMENT_SIZE, source.size()); - assertEquals(SEGMENT_SIZE - 2, bufferedSource.getBuffer().size()); - } - - @Test public void skipTracksBufferFirst() throws Exception { - Buffer source = new Buffer(); - source.writeUtf8("bb"); - - BufferedSource bufferedSource = Okio.buffer((Source) source); - bufferedSource.getBuffer().writeUtf8("aa"); - - bufferedSource.skip(2); - assertEquals(0, bufferedSource.getBuffer().size()); - assertEquals(2, source.size()); - } - - @Test public void operationsAfterClose() throws IOException { - Buffer source = new Buffer(); - BufferedSource bufferedSource = Okio.buffer((Source) source); - bufferedSource.close(); - - // Test a sample set of methods. - try { - bufferedSource.indexOf((byte) 1); - fail(); - } catch (IllegalStateException expected) { - } - - try { - bufferedSource.skip(1); - fail(); - } catch (IllegalStateException expected) { - } - - try { - bufferedSource.readByte(); - fail(); - } catch (IllegalStateException expected) { - } - - try { - bufferedSource.readByteString(10); - fail(); - } catch (IllegalStateException expected) { - } - - // Test a sample set of methods on the InputStream. - InputStream is = bufferedSource.inputStream(); - try { - is.read(); - fail(); - } catch (IOException expected) { - } - - try { - is.read(new byte[10]); - fail(); - } catch (IOException expected) { - } - } - - /** - * We don't want readAll to buffer an unbounded amount of data. Instead it - * should buffer a segment, write it, and repeat. - */ - @Test public void readAllReadsOneSegmentAtATime() throws IOException { - Buffer write1 = new Buffer().writeUtf8(repeat("a", SEGMENT_SIZE)); - Buffer write2 = new Buffer().writeUtf8(repeat("b", SEGMENT_SIZE)); - Buffer write3 = new Buffer().writeUtf8(repeat("c", SEGMENT_SIZE)); - - Buffer source = new Buffer().writeUtf8("" - + repeat("a", SEGMENT_SIZE) - + repeat("b", SEGMENT_SIZE) - + repeat("c", SEGMENT_SIZE)); - - MockSink mockSink = new MockSink(); - BufferedSource bufferedSource = Okio.buffer((Source) source); - assertEquals(SEGMENT_SIZE * 3, bufferedSource.readAll(mockSink)); - mockSink.assertLog( - "write(" + write1 + ", " + write1.size() + ")", - "write(" + write2 + ", " + write2.size() + ")", - "write(" + write3 + ", " + write3.size() + ")"); - } -} diff --git a/okio/src/jvmTest/java/okio/BufferedSourceTest.java b/okio/src/jvmTest/java/okio/BufferedSourceTest.java deleted file mode 100644 index 149ae64a..00000000 --- a/okio/src/jvmTest/java/okio/BufferedSourceTest.java +++ /dev/null @@ -1,1492 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; - -import static kotlin.text.Charsets.US_ASCII; -import static kotlin.text.Charsets.UTF_8; -import static kotlin.text.StringsKt.repeat; -import static okio.TestUtil.SEGMENT_SIZE; -import static okio.TestUtil.assertByteArrayEquals; -import static okio.TestUtil.assertByteArraysEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.junit.Assume.assumeTrue; - -@RunWith(Parameterized.class) -public final class BufferedSourceTest { - interface Factory { - Factory BUFFER = new Factory() { - @Override public Pipe pipe() { - Buffer buffer = new Buffer(); - Pipe result = new Pipe(); - result.sink = buffer; - result.source = buffer; - return result; - } - - @Override public boolean isOneByteAtATime() { - return false; - } - - @Override public String toString() { - return "Buffer"; - } - }; - - Factory REAL_BUFFERED_SOURCE = new Factory() { - @Override public Pipe pipe() { - Buffer buffer = new Buffer(); - Pipe result = new Pipe(); - result.sink = buffer; - result.source = Okio.buffer((Source) buffer); - return result; - } - - @Override public boolean isOneByteAtATime() { - return false; - } - - @Override public String toString() { - return "RealBufferedSource"; - } - }; - - /** - * A factory deliberately written to create buffers whose internal segments are always 1 byte - * long. We like testing with these segments because are likely to trigger bugs! - */ - Factory ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE = new Factory() { - @Override public Pipe pipe() { - Buffer buffer = new Buffer(); - Pipe result = new Pipe(); - result.sink = buffer; - result.source = Okio.buffer(new ForwardingSource(buffer) { - @Override public long read(Buffer sink, long byteCount) throws IOException { - // Read one byte into a new buffer, then clone it so that the segment is shared. - // Shared segments cannot be compacted so we'll get a long chain of short segments. - Buffer box = new Buffer(); - long result = super.read(box, Math.min(byteCount, 1L)); - if (result > 0L) sink.write(box.clone(), result); - return result; - } - }); - return result; - } - - @Override public boolean isOneByteAtATime() { - return true; - } - - @Override public String toString() { - return "OneByteAtATimeBufferedSource"; - } - }; - - Factory ONE_BYTE_AT_A_TIME_BUFFER = new Factory() { - @Override public Pipe pipe() { - Buffer buffer = new Buffer(); - Pipe result = new Pipe(); - result.source = buffer; - result.sink = Okio.buffer(new ForwardingSink(buffer) { - @Override public void write(Buffer source, long byteCount) throws IOException { - // Write each byte into a new buffer, then clone it so that the segments are shared. - // Shared segments cannot be compacted so we'll get a long chain of short segments. - for (int i = 0; i < byteCount; i++) { - Buffer box = new Buffer(); - box.write(source, 1); - super.write(box.clone(), 1); - } - } - }); - return result; - } - - @Override public boolean isOneByteAtATime() { - return true; - } - - @Override public String toString() { - return "OneByteAtATimeBuffer"; - } - }; - - Factory PEEK_BUFFER = new Factory() { - @Override public Pipe pipe() { - Buffer buffer = new Buffer(); - Pipe result = new Pipe(); - result.sink = buffer; - result.source = buffer.peek(); - return result; - } - - @Override public boolean isOneByteAtATime() { - return false; - } - - @Override public String toString() { - return "PeekBuffer"; - } - }; - - Factory PEEK_BUFFERED_SOURCE = new Factory() { - @Override public Pipe pipe() { - Buffer buffer = new Buffer(); - Pipe result = new Pipe(); - result.sink = buffer; - result.source = Okio.buffer((Source) buffer).peek(); - return result; - } - - @Override public boolean isOneByteAtATime() { - return false; - } - - @Override public String toString() { - return "PeekBufferedSource"; - } - }; - - Pipe pipe(); - - boolean isOneByteAtATime(); - } - - private static class Pipe { - BufferedSink sink; - BufferedSource source; - } - - @Parameters(name = "{0}") - public static List<Object[]> parameters() { - return Arrays.asList( - new Object[] { Factory.BUFFER }, - new Object[] { Factory.REAL_BUFFERED_SOURCE }, - new Object[] { Factory.ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE }, - new Object[] { Factory.ONE_BYTE_AT_A_TIME_BUFFER }, - new Object[] { Factory.PEEK_BUFFER }, - new Object[] { Factory.PEEK_BUFFERED_SOURCE }); - } - - @Parameter public Factory factory; - private BufferedSink sink; - private BufferedSource source; - - @Before public void setUp() { - Pipe pipe = factory.pipe(); - sink = pipe.sink; - source = pipe.source; - } - - @Test public void readBytes() throws Exception { - sink.write(new byte[] { (byte) 0xab, (byte) 0xcd }); - sink.emit(); - assertEquals(0xab, source.readByte() & 0xff); - assertEquals(0xcd, source.readByte() & 0xff); - assertTrue(source.exhausted()); - } - - @Test public void readByteTooShortThrows() throws IOException { - try { - source.readByte(); - fail(); - } catch (EOFException expected) { - } - } - - @Test public void readShort() throws Exception { - sink.write(new byte[] { - (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01 - }); - sink.emit(); - assertEquals((short) 0xabcd, source.readShort()); - assertEquals((short) 0xef01, source.readShort()); - assertTrue(source.exhausted()); - } - - @Test public void readShortLe() throws Exception { - sink.write(new byte[] { - (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10 - }); - sink.emit(); - assertEquals((short) 0xcdab, source.readShortLe()); - assertEquals((short) 0x10ef, source.readShortLe()); - assertTrue(source.exhausted()); - } - - @Test public void readShortSplitAcrossMultipleSegments() throws Exception { - sink.writeUtf8(repeat("a", SEGMENT_SIZE - 1)); - sink.write(new byte[] { (byte) 0xab, (byte) 0xcd }); - sink.emit(); - source.skip(SEGMENT_SIZE - 1); - assertEquals((short) 0xabcd, source.readShort()); - assertTrue(source.exhausted()); - } - - @Test public void readShortTooShortThrows() throws IOException { - sink.writeShort(Short.MAX_VALUE); - sink.emit(); - source.readByte(); - try { - source.readShort(); - fail(); - } catch (EOFException expected) { - } - } - - @Test public void readShortLeTooShortThrows() throws IOException { - sink.writeShortLe(Short.MAX_VALUE); - sink.emit(); - source.readByte(); - try { - source.readShortLe(); - fail(); - } catch (EOFException expected) { - } - } - - @Test public void readInt() throws Exception { - sink.write(new byte[] { - (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01, (byte) 0x87, (byte) 0x65, (byte) 0x43, - (byte) 0x21 - }); - sink.emit(); - assertEquals(0xabcdef01, source.readInt()); - assertEquals(0x87654321, source.readInt()); - assertTrue(source.exhausted()); - } - - @Test public void readIntLe() throws Exception { - sink.write(new byte[] { - (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10, (byte) 0x87, (byte) 0x65, (byte) 0x43, - (byte) 0x21 - }); - sink.emit(); - assertEquals(0x10efcdab, source.readIntLe()); - assertEquals(0x21436587, source.readIntLe()); - assertTrue(source.exhausted()); - } - - @Test public void readIntSplitAcrossMultipleSegments() throws Exception { - sink.writeUtf8(repeat("a", SEGMENT_SIZE - 3)); - sink.write(new byte[] { - (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01 - }); - sink.emit(); - source.skip(SEGMENT_SIZE - 3); - assertEquals(0xabcdef01, source.readInt()); - assertTrue(source.exhausted()); - } - - @Test public void readIntTooShortThrows() throws IOException { - sink.writeInt(Integer.MAX_VALUE); - sink.emit(); - source.readByte(); - try { - source.readInt(); - fail(); - } catch (EOFException expected) { - } - } - - @Test public void readIntLeTooShortThrows() throws IOException { - sink.writeIntLe(Integer.MAX_VALUE); - sink.emit(); - source.readByte(); - try { - source.readIntLe(); - fail(); - } catch (EOFException expected) { - } - } - - @Test public void readLong() throws Exception { - sink.write(new byte[] { - (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10, (byte) 0x87, (byte) 0x65, (byte) 0x43, - (byte) 0x21, (byte) 0x36, (byte) 0x47, (byte) 0x58, (byte) 0x69, (byte) 0x12, (byte) 0x23, - (byte) 0x34, (byte) 0x45 - }); - sink.emit(); - assertEquals(0xabcdef1087654321L, source.readLong()); - assertEquals(0x3647586912233445L, source.readLong()); - assertTrue(source.exhausted()); - } - - @Test public void readLongLe() throws Exception { - sink.write(new byte[] { - (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10, (byte) 0x87, (byte) 0x65, (byte) 0x43, - (byte) 0x21, (byte) 0x36, (byte) 0x47, (byte) 0x58, (byte) 0x69, (byte) 0x12, (byte) 0x23, - (byte) 0x34, (byte) 0x45 - }); - sink.emit(); - assertEquals(0x2143658710efcdabL, source.readLongLe()); - assertEquals(0x4534231269584736L, source.readLongLe()); - assertTrue(source.exhausted()); - } - - @Test public void readLongSplitAcrossMultipleSegments() throws Exception { - sink.writeUtf8(repeat("a", SEGMENT_SIZE - 7)); - sink.write(new byte[] { - (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01, (byte) 0x87, (byte) 0x65, (byte) 0x43, - (byte) 0x21, - }); - sink.emit(); - source.skip(SEGMENT_SIZE - 7); - assertEquals(0xabcdef0187654321L, source.readLong()); - assertTrue(source.exhausted()); - } - - @Test public void readLongTooShortThrows() throws IOException { - sink.writeLong(Long.MAX_VALUE); - sink.emit(); - source.readByte(); - try { - source.readLong(); - fail(); - } catch (EOFException expected) { - } - } - - @Test public void readLongLeTooShortThrows() throws IOException { - sink.writeLongLe(Long.MAX_VALUE); - sink.emit(); - source.readByte(); - try { - source.readLongLe(); - fail(); - } catch (EOFException expected) { - } - } - - @Test public void readAll() throws IOException { - source.getBuffer().writeUtf8("abc"); - sink.writeUtf8("def"); - sink.emit(); - - Buffer sink = new Buffer(); - assertEquals(6, source.readAll(sink)); - assertEquals("abcdef", sink.readUtf8()); - assertTrue(source.exhausted()); - } - - @Test public void readAllExhausted() throws IOException { - MockSink mockSink = new MockSink(); - assertEquals(0, source.readAll(mockSink)); - assertTrue(source.exhausted()); - mockSink.assertLog(); - } - - @Test public void readExhaustedSource() throws Exception { - Buffer sink = new Buffer(); - sink.writeUtf8(repeat("a", 10)); - assertEquals(-1, source.read(sink, 10)); - assertEquals(10, sink.size()); - assertTrue(source.exhausted()); - } - - @Test public void readZeroBytesFromSource() throws Exception { - Buffer sink = new Buffer(); - sink.writeUtf8(repeat("a", 10)); - - // Either 0 or -1 is reasonable here. For consistency with Android's - // ByteArrayInputStream we return 0. - assertEquals(-1, source.read(sink, 0)); - assertEquals(10, sink.size()); - assertTrue(source.exhausted()); - } - - @Test public void readFully() throws Exception { - sink.writeUtf8(repeat("a", 10000)); - sink.emit(); - Buffer sink = new Buffer(); - source.readFully(sink, 9999); - assertEquals(repeat("a", 9999), sink.readUtf8()); - assertEquals("a", source.readUtf8()); - } - - @Test public void readFullyTooShortThrows() throws IOException { - sink.writeUtf8("Hi"); - sink.emit(); - Buffer sink = new Buffer(); - try { - source.readFully(sink, 5); - fail(); - } catch (EOFException ignored) { - } - - // Verify we read all that we could from the source. - assertEquals("Hi", sink.readUtf8()); - } - - @Test public void readFullyByteArray() throws IOException { - Buffer data = new Buffer(); - data.writeUtf8("Hello").writeUtf8(repeat("e", SEGMENT_SIZE)); - - byte[] expected = data.clone().readByteArray(); - sink.write(data, data.size()); - sink.emit(); - - byte[] sink = new byte[SEGMENT_SIZE + 5]; - source.readFully(sink); - assertByteArraysEquals(expected, sink); - } - - @Test public void readFullyByteArrayTooShortThrows() throws IOException { - sink.writeUtf8("Hello"); - sink.emit(); - - byte[] array = new byte[6]; - try { - source.readFully(array); - fail(); - } catch (EOFException ignored) { - } - - // Verify we read all that we could from the source. - assertByteArraysEquals(new byte[] { 'H', 'e', 'l', 'l', 'o', 0 }, array); - } - - @Test public void readIntoByteArray() throws IOException { - sink.writeUtf8("abcd"); - sink.emit(); - - byte[] sink = new byte[3]; - int read = source.read(sink); - if (factory.isOneByteAtATime()) { - assertEquals(1, read); - byte[] expected = { 'a', 0, 0 }; - assertByteArraysEquals(expected, sink); - } else { - assertEquals(3, read); - byte[] expected = { 'a', 'b', 'c' }; - assertByteArraysEquals(expected, sink); - } - } - - @Test public void readIntoByteArrayNotEnough() throws IOException { - sink.writeUtf8("abcd"); - sink.emit(); - - byte[] sink = new byte[5]; - int read = source.read(sink); - if (factory.isOneByteAtATime()) { - assertEquals(1, read); - byte[] expected = { 'a', 0, 0, 0, 0 }; - assertByteArraysEquals(expected, sink); - } else { - assertEquals(4, read); - byte[] expected = { 'a', 'b', 'c', 'd', 0 }; - assertByteArraysEquals(expected, sink); - } - } - - @Test public void readIntoByteArrayOffsetAndCount() throws IOException { - sink.writeUtf8("abcd"); - sink.emit(); - - byte[] sink = new byte[7]; - int read = source.read(sink, 2, 3); - if (factory.isOneByteAtATime()) { - assertEquals(1, read); - byte[] expected = { 0, 0, 'a', 0, 0, 0, 0 }; - assertByteArraysEquals(expected, sink); - } else { - assertEquals(3, read); - byte[] expected = { 0, 0, 'a', 'b', 'c', 0, 0 }; - assertByteArraysEquals(expected, sink); - } - } - - @Test public void readByteArray() throws IOException { - String string = "abcd" + repeat("e", SEGMENT_SIZE); - sink.writeUtf8(string); - sink.emit(); - assertByteArraysEquals(string.getBytes(UTF_8), source.readByteArray()); - } - - @Test public void readByteArrayPartial() throws IOException { - sink.writeUtf8("abcd"); - sink.emit(); - assertEquals("[97, 98, 99]", Arrays.toString(source.readByteArray(3))); - assertEquals("d", source.readUtf8(1)); - } - - @Test public void readByteArrayTooShortThrows() throws IOException { - sink.writeUtf8("abc"); - sink.emit(); - try { - source.readByteArray(4); - fail(); - } catch (EOFException expected) { - } - assertEquals("abc", source.readUtf8()); // The read shouldn't consume any data. - } - - @Test public void readByteString() throws IOException { - sink.writeUtf8("abcd").writeUtf8(repeat("e", SEGMENT_SIZE)); - sink.emit(); - assertEquals("abcd" + repeat("e", SEGMENT_SIZE), source.readByteString().utf8()); - } - - @Test public void readByteStringPartial() throws IOException { - sink.writeUtf8("abcd").writeUtf8(repeat("e", SEGMENT_SIZE)); - sink.emit(); - assertEquals("abc", source.readByteString(3).utf8()); - assertEquals("d", source.readUtf8(1)); - } - - @Test public void readByteStringTooShortThrows() throws IOException { - sink.writeUtf8("abc"); - sink.emit(); - try { - source.readByteString(4); - fail(); - } catch (EOFException expected) { - } - assertEquals("abc", source.readUtf8()); // The read shouldn't consume any data. - } - - @Test public void readSpecificCharsetPartial() throws Exception { - sink.write(ByteString.decodeHex("0000007600000259000002c80000006c000000e40000007300000259" - + "000002cc000000720000006100000070000000740000025900000072")); - sink.emit(); - assertEquals("vəˈläsə", source.readString(7 * 4, Charset.forName("utf-32"))); - } - - @Test public void readSpecificCharset() throws Exception { - sink.write(ByteString.decodeHex("0000007600000259000002c80000006c000000e40000007300000259" - + "000002cc000000720000006100000070000000740000025900000072")); - sink.emit(); - assertEquals("vəˈläsəˌraptər", source.readString(Charset.forName("utf-32"))); - } - - @Test public void readStringTooShortThrows() throws IOException { - sink.writeString("abc", US_ASCII); - sink.emit(); - try { - source.readString(4, US_ASCII); - fail(); - } catch (EOFException expected) { - } - assertEquals("abc", source.readUtf8()); // The read shouldn't consume any data. - } - - @Test public void readUtf8SpansSegments() throws Exception { - sink.writeUtf8(repeat("a", SEGMENT_SIZE * 2)); - sink.emit(); - source.skip(SEGMENT_SIZE - 1); - assertEquals("aa", source.readUtf8(2)); - } - - @Test public void readUtf8Segment() throws Exception { - sink.writeUtf8(repeat("a", SEGMENT_SIZE)); - sink.emit(); - assertEquals(repeat("a", SEGMENT_SIZE), source.readUtf8(SEGMENT_SIZE)); - } - - @Test public void readUtf8PartialBuffer() throws Exception { - sink.writeUtf8(repeat("a", SEGMENT_SIZE + 20)); - sink.emit(); - assertEquals(repeat("a", SEGMENT_SIZE + 10), source.readUtf8(SEGMENT_SIZE + 10)); - } - - @Test public void readUtf8EntireBuffer() throws Exception { - sink.writeUtf8(repeat("a", SEGMENT_SIZE * 2)); - sink.emit(); - assertEquals(repeat("a", SEGMENT_SIZE * 2), source.readUtf8()); - } - - @Test public void readUtf8TooShortThrows() throws IOException { - sink.writeUtf8("abc"); - sink.emit(); - try { - source.readUtf8(4L); - fail(); - } catch (EOFException expected) { - } - assertEquals("abc", source.readUtf8()); // The read shouldn't consume any data. - } - - @Test public void skip() throws Exception { - sink.writeUtf8("a"); - sink.writeUtf8(repeat("b", SEGMENT_SIZE)); - sink.writeUtf8("c"); - sink.emit(); - source.skip(1); - assertEquals('b', source.readByte() & 0xff); - source.skip(SEGMENT_SIZE - 2); - assertEquals('b', source.readByte() & 0xff); - source.skip(1); - assertTrue(source.exhausted()); - } - - @Test public void skipInsufficientData() throws Exception { - sink.writeUtf8("a"); - sink.emit(); - - try { - source.skip(2); - fail(); - } catch (EOFException ignored) { - } - } - - @Test public void indexOf() throws Exception { - // The segment is empty. - assertEquals(-1, source.indexOf((byte) 'a')); - - // The segment has one value. - sink.writeUtf8("a"); // a - sink.emit(); - assertEquals(0, source.indexOf((byte) 'a')); - assertEquals(-1, source.indexOf((byte) 'b')); - - // The segment has lots of data. - sink.writeUtf8(repeat("b", SEGMENT_SIZE - 2)); // ab...b - sink.emit(); - assertEquals(0, source.indexOf((byte) 'a')); - assertEquals(1, source.indexOf((byte) 'b')); - assertEquals(-1, source.indexOf((byte) 'c')); - - // The segment doesn't start at 0, it starts at 2. - source.skip(2); // b...b - assertEquals(-1, source.indexOf((byte) 'a')); - assertEquals(0, source.indexOf((byte) 'b')); - assertEquals(-1, source.indexOf((byte) 'c')); - - // The segment is full. - sink.writeUtf8("c"); // b...bc - sink.emit(); - assertEquals(-1, source.indexOf((byte) 'a')); - assertEquals(0, source.indexOf((byte) 'b')); - assertEquals(SEGMENT_SIZE - 3, source.indexOf((byte) 'c')); - - // The segment doesn't start at 2, it starts at 4. - source.skip(2); // b...bc - assertEquals(-1, source.indexOf((byte) 'a')); - assertEquals(0, source.indexOf((byte) 'b')); - assertEquals(SEGMENT_SIZE - 5, source.indexOf((byte) 'c')); - - // Two segments. - sink.writeUtf8("d"); // b...bcd, d is in the 2nd segment. - sink.emit(); - assertEquals(SEGMENT_SIZE - 4, source.indexOf((byte) 'd')); - assertEquals(-1, source.indexOf((byte) 'e')); - } - - @Test public void indexOfByteWithStartOffset() throws IOException { - sink.writeUtf8("a").writeUtf8(repeat("b", SEGMENT_SIZE)).writeUtf8("c"); - sink.emit(); - assertEquals(-1, source.indexOf((byte) 'a', 1)); - assertEquals(15, source.indexOf((byte) 'b', 15)); - } - - @Test public void indexOfByteWithBothOffsets() throws IOException { - if (factory.isOneByteAtATime()) { - // When run on Travis this causes out-of-memory errors. - return; - } - byte a = (byte) 'a'; - byte c = (byte) 'c'; - - int size = SEGMENT_SIZE * 5; - byte[] bytes = new byte[size]; - Arrays.fill(bytes, a); - - // These are tricky places where the buffer - // starts, ends, or segments come together. - int[] points = { - 0, 1, 2, - SEGMENT_SIZE - 1, SEGMENT_SIZE, SEGMENT_SIZE + 1, - size / 2 - 1, size / 2, size / 2 + 1, - size - SEGMENT_SIZE - 1, size - SEGMENT_SIZE, size - SEGMENT_SIZE + 1, - size - 3, size - 2, size - 1 - }; - - // In each iteration, we write c to the known point and then search for it using different - // windows. Some of the windows don't overlap with c's position, and therefore a match shouldn't - // be found. - for (int p : points) { - bytes[p] = c; - sink.write(bytes); - sink.emit(); - - assertEquals( p, source.indexOf(c, 0, size )); - assertEquals( p, source.indexOf(c, 0, p + 1 )); - assertEquals( p, source.indexOf(c, p, size )); - assertEquals( p, source.indexOf(c, p, p + 1 )); - assertEquals( p, source.indexOf(c, p / 2, p * 2 + 1)); - assertEquals(-1, source.indexOf(c, 0, p / 2 )); - assertEquals(-1, source.indexOf(c, 0, p )); - assertEquals(-1, source.indexOf(c, 0, 0 )); - assertEquals(-1, source.indexOf(c, p, p )); - - // Reset. - source.readUtf8(); - bytes[p] = a; - } - } - - @Test public void indexOfByteInvalidBoundsThrows() throws IOException { - sink.writeUtf8("abc"); - sink.emit(); - - try { - source.indexOf((byte) 'a', -1); - fail("Expected failure: fromIndex < 0"); - } catch (IllegalArgumentException expected) { - } - - try { - source.indexOf((byte) 'a', 10, 0); - fail("Expected failure: fromIndex > toIndex"); - } catch (IllegalArgumentException expected) { - } - } - - @Test public void indexOfByteString() throws IOException { - assertEquals(-1, source.indexOf(ByteString.encodeUtf8("flop"))); - - sink.writeUtf8("flip flop"); - sink.emit(); - assertEquals(5, source.indexOf(ByteString.encodeUtf8("flop"))); - source.readUtf8(); // Clear stream. - - // Make sure we backtrack and resume searching after partial match. - sink.writeUtf8("hi hi hi hey"); - sink.emit(); - assertEquals(3, source.indexOf(ByteString.encodeUtf8("hi hi hey"))); - } - - @Test public void indexOfByteStringAtSegmentBoundary() throws IOException { - sink.writeUtf8(repeat("a", SEGMENT_SIZE - 1)); - sink.writeUtf8("bcd"); - sink.emit(); - assertEquals(SEGMENT_SIZE - 3, source.indexOf(ByteString.encodeUtf8("aabc"), SEGMENT_SIZE - 4)); - assertEquals(SEGMENT_SIZE - 3, source.indexOf(ByteString.encodeUtf8("aabc"), SEGMENT_SIZE - 3)); - assertEquals(SEGMENT_SIZE - 2, source.indexOf(ByteString.encodeUtf8("abcd"), SEGMENT_SIZE - 2)); - assertEquals(SEGMENT_SIZE - 2, source.indexOf(ByteString.encodeUtf8("abc"), SEGMENT_SIZE - 2)); - assertEquals(SEGMENT_SIZE - 2, source.indexOf(ByteString.encodeUtf8("abc"), SEGMENT_SIZE - 2)); - assertEquals(SEGMENT_SIZE - 2, source.indexOf(ByteString.encodeUtf8("ab"), SEGMENT_SIZE - 2)); - assertEquals(SEGMENT_SIZE - 2, source.indexOf(ByteString.encodeUtf8("a"), SEGMENT_SIZE - 2)); - assertEquals(SEGMENT_SIZE - 1, source.indexOf(ByteString.encodeUtf8("bc"), SEGMENT_SIZE - 2)); - assertEquals(SEGMENT_SIZE - 1, source.indexOf(ByteString.encodeUtf8("b"), SEGMENT_SIZE - 2)); - assertEquals(SEGMENT_SIZE, source.indexOf(ByteString.encodeUtf8("c"), SEGMENT_SIZE - 2)); - assertEquals(SEGMENT_SIZE, source.indexOf(ByteString.encodeUtf8("c"), SEGMENT_SIZE )); - assertEquals(SEGMENT_SIZE + 1, source.indexOf(ByteString.encodeUtf8("d"), SEGMENT_SIZE - 2)); - assertEquals(SEGMENT_SIZE + 1, source.indexOf(ByteString.encodeUtf8("d"), SEGMENT_SIZE + 1)); - } - - @Test public void indexOfDoesNotWrapAround() throws IOException { - sink.writeUtf8(repeat("a", SEGMENT_SIZE - 1)); - sink.writeUtf8("bcd"); - sink.emit(); - assertEquals(-1, source.indexOf(ByteString.encodeUtf8("abcda"), SEGMENT_SIZE - 3)); - } - - @Test public void indexOfByteStringWithOffset() throws IOException { - assertEquals(-1, source.indexOf(ByteString.encodeUtf8("flop"), 1)); - - sink.writeUtf8("flop flip flop"); - sink.emit(); - assertEquals(10, source.indexOf(ByteString.encodeUtf8("flop"), 1)); - source.readUtf8(); // Clear stream - - // Make sure we backtrack and resume searching after partial match. - sink.writeUtf8("hi hi hi hi hey"); - sink.emit(); - assertEquals(6, source.indexOf(ByteString.encodeUtf8("hi hi hey"), 1)); - } - - @Test public void indexOfByteStringInvalidArgumentsThrows() throws IOException { - try { - source.indexOf(ByteString.of()); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("bytes is empty", e.getMessage()); - } - try { - source.indexOf(ByteString.encodeUtf8("hi"), -1); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("fromIndex < 0: -1", e.getMessage()); - } - } - - /** - * With {@link Factory#ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE}, this code was extremely slow. - * https://github.com/square/okio/issues/171 - */ - @Test public void indexOfByteStringAcrossSegmentBoundaries() throws IOException { - sink.writeUtf8(repeat("a", SEGMENT_SIZE * 2 - 3)); - sink.writeUtf8("bcdefg"); - sink.emit(); - assertEquals(SEGMENT_SIZE * 2 - 4, source.indexOf(ByteString.encodeUtf8("ab"))); - assertEquals(SEGMENT_SIZE * 2 - 4, source.indexOf(ByteString.encodeUtf8("abc"))); - assertEquals(SEGMENT_SIZE * 2 - 4, source.indexOf(ByteString.encodeUtf8("abcd"))); - assertEquals(SEGMENT_SIZE * 2 - 4, source.indexOf(ByteString.encodeUtf8("abcde"))); - assertEquals(SEGMENT_SIZE * 2 - 4, source.indexOf(ByteString.encodeUtf8("abcdef"))); - assertEquals(SEGMENT_SIZE * 2 - 4, source.indexOf(ByteString.encodeUtf8("abcdefg"))); - assertEquals(SEGMENT_SIZE * 2 - 3, source.indexOf(ByteString.encodeUtf8("bcdefg"))); - assertEquals(SEGMENT_SIZE * 2 - 2, source.indexOf(ByteString.encodeUtf8("cdefg"))); - assertEquals(SEGMENT_SIZE * 2 - 1, source.indexOf(ByteString.encodeUtf8("defg"))); - assertEquals(SEGMENT_SIZE * 2, source.indexOf(ByteString.encodeUtf8("efg"))); - assertEquals(SEGMENT_SIZE * 2 + 1, source.indexOf(ByteString.encodeUtf8("fg"))); - assertEquals(SEGMENT_SIZE * 2 + 2, source.indexOf(ByteString.encodeUtf8("g"))); - } - - @Test public void indexOfElement() throws IOException { - sink.writeUtf8("a").writeUtf8(repeat("b", SEGMENT_SIZE)).writeUtf8("c"); - sink.emit(); - assertEquals(0, source.indexOfElement(ByteString.encodeUtf8("DEFGaHIJK"))); - assertEquals(1, source.indexOfElement(ByteString.encodeUtf8("DEFGHIJKb"))); - assertEquals(SEGMENT_SIZE + 1, source.indexOfElement(ByteString.encodeUtf8("cDEFGHIJK"))); - assertEquals(1, source.indexOfElement(ByteString.encodeUtf8("DEFbGHIc"))); - assertEquals(-1L, source.indexOfElement(ByteString.encodeUtf8("DEFGHIJK"))); - assertEquals(-1L, source.indexOfElement(ByteString.encodeUtf8(""))); - } - - @Test public void indexOfElementWithOffset() throws IOException { - sink.writeUtf8("a").writeUtf8(repeat("b", SEGMENT_SIZE)).writeUtf8("c"); - sink.emit(); - assertEquals(-1, source.indexOfElement(ByteString.encodeUtf8("DEFGaHIJK"), 1)); - assertEquals(15, source.indexOfElement(ByteString.encodeUtf8("DEFGHIJKb"), 15)); - } - - @Test public void indexOfByteWithFromIndex() throws Exception { - sink.writeUtf8("aaa"); - sink.emit(); - assertEquals(0, source.indexOf((byte) 'a')); - assertEquals(0, source.indexOf((byte) 'a', 0)); - assertEquals(1, source.indexOf((byte) 'a', 1)); - assertEquals(2, source.indexOf((byte) 'a', 2)); - } - - @Test public void indexOfByteStringWithFromIndex() throws Exception { - sink.writeUtf8("aaa"); - sink.emit(); - assertEquals(0, source.indexOf(ByteString.encodeUtf8("a"))); - assertEquals(0, source.indexOf(ByteString.encodeUtf8("a"), 0)); - assertEquals(1, source.indexOf(ByteString.encodeUtf8("a"), 1)); - assertEquals(2, source.indexOf(ByteString.encodeUtf8("a"), 2)); - } - - @Test public void indexOfElementWithFromIndex() throws Exception { - sink.writeUtf8("aaa"); - sink.emit(); - assertEquals(0, source.indexOfElement(ByteString.encodeUtf8("a"))); - assertEquals(0, source.indexOfElement(ByteString.encodeUtf8("a"), 0)); - assertEquals(1, source.indexOfElement(ByteString.encodeUtf8("a"), 1)); - assertEquals(2, source.indexOfElement(ByteString.encodeUtf8("a"), 2)); - } - - @Test public void request() throws IOException { - sink.writeUtf8("a").writeUtf8(repeat("b", SEGMENT_SIZE)).writeUtf8("c"); - sink.emit(); - assertTrue(source.request(SEGMENT_SIZE + 2)); - assertFalse(source.request(SEGMENT_SIZE + 3)); - } - - @Test public void require() throws IOException { - sink.writeUtf8("a").writeUtf8(repeat("b", SEGMENT_SIZE)).writeUtf8("c"); - sink.emit(); - source.require(SEGMENT_SIZE + 2); - try { - source.require(SEGMENT_SIZE + 3); - fail(); - } catch (EOFException expected) { - } - } - - @Test public void inputStream() throws Exception { - sink.writeUtf8("abc"); - sink.emit(); - InputStream in = source.inputStream(); - byte[] bytes = { 'z', 'z', 'z' }; - int read = in.read(bytes); - if (factory.isOneByteAtATime()) { - assertEquals(1, read); - assertByteArrayEquals("azz", bytes); - - read = in.read(bytes); - assertEquals(1, read); - assertByteArrayEquals("bzz", bytes); - - read = in.read(bytes); - assertEquals(1, read); - assertByteArrayEquals("czz", bytes); - } else { - assertEquals(3, read); - assertByteArrayEquals("abc", bytes); - } - - assertEquals(-1, in.read()); - } - - @Test public void inputStreamOffsetCount() throws Exception { - sink.writeUtf8("abcde"); - sink.emit(); - InputStream in = source.inputStream(); - byte[] bytes = { 'z', 'z', 'z', 'z', 'z' }; - int read = in.read(bytes, 1, 3); - if (factory.isOneByteAtATime()) { - assertEquals(1, read); - assertByteArrayEquals("zazzz", bytes); - } else { - assertEquals(3, read); - assertByteArrayEquals("zabcz", bytes); - } - } - - @Test public void inputStreamSkip() throws Exception { - sink.writeUtf8("abcde"); - sink.emit(); - InputStream in = source.inputStream(); - assertEquals(4, in.skip(4)); - assertEquals('e', in.read()); - - sink.writeUtf8("abcde"); - sink.emit(); - assertEquals(5, in.skip(10)); // Try to skip too much. - assertEquals(0, in.skip(1)); // Try to skip when exhausted. - } - - @Test public void inputStreamCharByChar() throws Exception { - sink.writeUtf8("abc"); - sink.emit(); - InputStream in = source.inputStream(); - assertEquals('a', in.read()); - assertEquals('b', in.read()); - assertEquals('c', in.read()); - assertEquals(-1, in.read()); - } - - @Test public void inputStreamBounds() throws IOException { - sink.writeUtf8(repeat("a", 100)); - sink.emit(); - InputStream in = source.inputStream(); - try { - in.read(new byte[100], 50, 51); - fail(); - } catch (ArrayIndexOutOfBoundsException expected) { - } - } - - @Test public void longHexString() throws IOException { - assertLongHexString("8000000000000000", 0x8000000000000000L); - assertLongHexString("fffffffffffffffe", 0xFFFFFFFFFFFFFFFEL); - assertLongHexString("FFFFFFFFFFFFFFFe", 0xFFFFFFFFFFFFFFFEL); - assertLongHexString("ffffffffffffffff", 0xffffffffffffffffL); - assertLongHexString("FFFFFFFFFFFFFFFF", 0xFFFFFFFFFFFFFFFFL); - assertLongHexString("0000000000000000", 0x0); - assertLongHexString("0000000000000001", 0x1); - assertLongHexString("7999999999999999", 0x7999999999999999L); - - assertLongHexString("FF", 0xFF); - assertLongHexString("0000000000000001", 0x1); - } - - @Test public void hexStringWithManyLeadingZeros() throws IOException { - assertLongHexString("00000000000000001", 0x1); - assertLongHexString("0000000000000000ffffffffffffffff", 0xffffffffffffffffL); - assertLongHexString("00000000000000007fffffffffffffff", 0x7fffffffffffffffL); - assertLongHexString(repeat("0", SEGMENT_SIZE + 1) + "1", 0x1); - } - - private void assertLongHexString(String s, long expected) throws IOException { - sink.writeUtf8(s); - sink.emit(); - long actual = source.readHexadecimalUnsignedLong(); - assertEquals(s + " --> " + expected, expected, actual); - } - - @Test public void longHexStringAcrossSegment() throws IOException { - sink.writeUtf8(repeat("a", SEGMENT_SIZE - 8)).writeUtf8("FFFFFFFFFFFFFFFF"); - sink.emit(); - source.skip(SEGMENT_SIZE - 8); - assertEquals(-1, source.readHexadecimalUnsignedLong()); - } - - @Test public void longHexStringTooLongThrows() throws IOException { - try { - sink.writeUtf8("fffffffffffffffff"); - sink.emit(); - source.readHexadecimalUnsignedLong(); - fail(); - } catch (NumberFormatException e) { - assertEquals("Number too large: fffffffffffffffff", e.getMessage()); - } - } - - @Test public void longHexStringTooShortThrows() throws IOException { - try { - sink.writeUtf8(" "); - sink.emit(); - source.readHexadecimalUnsignedLong(); - fail(); - } catch (NumberFormatException e) { - assertEquals("Expected leading [0-9a-fA-F] character but was 0x20", e.getMessage()); - } - } - - @Test public void longHexEmptySourceThrows() throws IOException { - try { - sink.writeUtf8(""); - sink.emit(); - source.readHexadecimalUnsignedLong(); - fail(); - } catch (EOFException expected) { - } - } - - @Test public void longDecimalString() throws IOException { - assertLongDecimalString("-9223372036854775808", -9223372036854775808L); - assertLongDecimalString("-1", -1L); - assertLongDecimalString("0", 0L); - assertLongDecimalString("1", 1L); - assertLongDecimalString("9223372036854775807", 9223372036854775807L); - - assertLongDecimalString("00000001", 1L); - assertLongDecimalString("-000001", -1L); - } - - private void assertLongDecimalString(String s, long expected) throws IOException { - sink.writeUtf8(s); - sink.writeUtf8("zzz"); - sink.emit(); - long actual = source.readDecimalLong(); - assertEquals(s + " --> " + expected, expected, actual); - assertEquals("zzz", source.readUtf8()); - } - - @Test public void longDecimalStringAcrossSegment() throws IOException { - sink.writeUtf8(repeat("a", SEGMENT_SIZE - 8)).writeUtf8("1234567890123456"); - sink.writeUtf8("zzz"); - sink.emit(); - source.skip(SEGMENT_SIZE - 8); - assertEquals(1234567890123456L, source.readDecimalLong()); - assertEquals("zzz", source.readUtf8()); - } - - @Test public void longDecimalStringTooLongThrows() throws IOException { - try { - sink.writeUtf8("12345678901234567890"); // Too many digits. - sink.emit(); - source.readDecimalLong(); - fail(); - } catch (NumberFormatException e) { - assertEquals("Number too large: 12345678901234567890", e.getMessage()); - } - } - - @Test public void longDecimalStringTooHighThrows() throws IOException { - try { - sink.writeUtf8("9223372036854775808"); // Right size but cannot fit. - sink.emit(); - source.readDecimalLong(); - fail(); - } catch (NumberFormatException e) { - assertEquals("Number too large: 9223372036854775808", e.getMessage()); - } - } - - @Test public void longDecimalStringTooLowThrows() throws IOException { - try { - sink.writeUtf8("-9223372036854775809"); // Right size but cannot fit. - sink.emit(); - source.readDecimalLong(); - fail(); - } catch (NumberFormatException e) { - assertEquals("Number too large: -9223372036854775809", e.getMessage()); - } - } - - @Test public void longDecimalStringTooShortThrows() throws IOException { - try { - sink.writeUtf8(" "); - sink.emit(); - source.readDecimalLong(); - fail(); - } catch (NumberFormatException e) { - assertEquals("Expected leading [0-9] or '-' character but was 0x20", e.getMessage()); - } - } - - @Test public void longDecimalEmptyThrows() throws IOException { - try { - sink.writeUtf8(""); - sink.emit(); - source.readDecimalLong(); - fail(); - } catch (EOFException expected) { - } - } - - @Test public void codePoints() throws IOException { - sink.write(ByteString.decodeHex("7f")); - sink.emit(); - assertEquals(0x7f, source.readUtf8CodePoint()); - - sink.write(ByteString.decodeHex("dfbf")); - sink.emit(); - assertEquals(0x07ff, source.readUtf8CodePoint()); - - sink.write(ByteString.decodeHex("efbfbf")); - sink.emit(); - assertEquals(0xffff, source.readUtf8CodePoint()); - - sink.write(ByteString.decodeHex("f48fbfbf")); - sink.emit(); - assertEquals(0x10ffff, source.readUtf8CodePoint()); - } - - @Test public void decimalStringWithManyLeadingZeros() throws IOException { - assertLongDecimalString("00000000000000001", 1); - assertLongDecimalString("00000000000000009223372036854775807", 9223372036854775807L); - assertLongDecimalString("-00000000000000009223372036854775808", -9223372036854775808L); - assertLongDecimalString(repeat("0", SEGMENT_SIZE + 1) + "1", 1); - } - - @Test public void select() throws IOException { - Options options = Options.Companion.of( - ByteString.encodeUtf8("ROCK"), - ByteString.encodeUtf8("SCISSORS"), - ByteString.encodeUtf8("PAPER")); - - sink.writeUtf8("PAPER,SCISSORS,ROCK"); - sink.emit(); - assertEquals(2, source.select(options)); - assertEquals(',', source.readByte()); - assertEquals(1, source.select(options)); - assertEquals(',', source.readByte()); - assertEquals(0, source.select(options)); - assertTrue(source.exhausted()); - } - - /** Note that this test crashes the VM on Android. */ - @Test public void selectSpanningMultipleSegments() throws IOException { - ByteString commonPrefix = TestUtil.randomBytes(SEGMENT_SIZE + 10); - ByteString a = new Buffer().write(commonPrefix).writeUtf8("a").readByteString(); - ByteString bc = new Buffer().write(commonPrefix).writeUtf8("bc").readByteString(); - ByteString bd = new Buffer().write(commonPrefix).writeUtf8("bd").readByteString(); - Options options = Options.Companion.of(a, bc, bd); - - sink.write(bd); - sink.write(a); - sink.write(bc); - sink.emit(); - - assertEquals(2, source.select(options)); - assertEquals(0, source.select(options)); - assertEquals(1, source.select(options)); - assertTrue(source.exhausted()); - } - - @Test public void selectNotFound() throws IOException { - Options options = Options.Companion.of( - ByteString.encodeUtf8("ROCK"), - ByteString.encodeUtf8("SCISSORS"), - ByteString.encodeUtf8("PAPER")); - - sink.writeUtf8("SPOCK"); - sink.emit(); - assertEquals(-1, source.select(options)); - assertEquals("SPOCK", source.readUtf8()); - } - - @Test public void selectValuesHaveCommonPrefix() throws IOException { - Options options = Options.Companion.of( - ByteString.encodeUtf8("abcd"), - ByteString.encodeUtf8("abce"), - ByteString.encodeUtf8("abcc")); - - sink.writeUtf8("abcc").writeUtf8("abcd").writeUtf8("abce"); - sink.emit(); - assertEquals(2, source.select(options)); - assertEquals(0, source.select(options)); - assertEquals(1, source.select(options)); - } - - @Test public void selectLongerThanSource() throws IOException { - Options options = Options.Companion.of( - ByteString.encodeUtf8("abcd"), - ByteString.encodeUtf8("abce"), - ByteString.encodeUtf8("abcc")); - sink.writeUtf8("abc"); - sink.emit(); - assertEquals(-1, source.select(options)); - assertEquals("abc", source.readUtf8()); - } - - @Test public void selectReturnsFirstByteStringThatMatches() throws IOException { - Options options = Options.Companion.of( - ByteString.encodeUtf8("abcd"), - ByteString.encodeUtf8("abc"), - ByteString.encodeUtf8("abcde")); - sink.writeUtf8("abcdef"); - sink.emit(); - assertEquals(0, source.select(options)); - assertEquals("ef", source.readUtf8()); - } - - @Test public void selectFromEmptySource() throws IOException { - Options options = Options.Companion.of( - ByteString.encodeUtf8("abc"), - ByteString.encodeUtf8("def")); - assertEquals(-1, source.select(options)); - } - - @Test public void selectNoByteStringsFromEmptySource() throws IOException { - Options options = Options.of(); - assertEquals(-1, source.select(options)); - } - - @Test public void peek() throws IOException { - sink.writeUtf8("abcdefghi"); - sink.emit(); - - assertEquals("abc", source.readUtf8(3)); - - BufferedSource peek = source.peek(); - assertEquals("def", peek.readUtf8(3)); - assertEquals("ghi", peek.readUtf8(3)); - assertFalse(peek.request(1)); - - assertEquals("def", source.readUtf8(3)); - } - - @Test public void peekMultiple() throws IOException { - sink.writeUtf8("abcdefghi"); - sink.emit(); - - assertEquals("abc", source.readUtf8(3)); - - BufferedSource peek1 = source.peek(); - BufferedSource peek2 = source.peek(); - - assertEquals("def", peek1.readUtf8(3)); - - assertEquals("def", peek2.readUtf8(3)); - assertEquals("ghi", peek2.readUtf8(3)); - assertFalse(peek2.request(1)); - - assertEquals("ghi", peek1.readUtf8(3)); - assertFalse(peek1.request(1)); - - assertEquals("def", source.readUtf8(3)); - } - - @Test public void peekLarge() throws IOException { - sink.writeUtf8("abcdef"); - sink.writeUtf8(repeat("g", 2 * SEGMENT_SIZE)); - sink.writeUtf8("hij"); - sink.emit(); - - assertEquals("abc", source.readUtf8(3)); - - BufferedSource peek = source.peek(); - assertEquals("def", peek.readUtf8(3)); - peek.skip(2 * SEGMENT_SIZE); - assertEquals("hij", peek.readUtf8(3)); - assertFalse(peek.request(1)); - - assertEquals("def", source.readUtf8(3)); - source.skip(2 * SEGMENT_SIZE); - assertEquals("hij", source.readUtf8(3)); - } - - @Test public void peekInvalid() throws IOException { - sink.writeUtf8("abcdefghi"); - sink.emit(); - - assertEquals("abc", source.readUtf8(3)); - - BufferedSource peek = source.peek(); - assertEquals("def", peek.readUtf8(3)); - assertEquals("ghi", peek.readUtf8(3)); - assertFalse(peek.request(1)); - - assertEquals("def", source.readUtf8(3)); - - try { - peek.readUtf8(); - fail(); - } catch (IllegalStateException e) { - assertEquals("Peek source is invalid because upstream source was used", e.getMessage()); - } - } - - @Test public void peekSegmentThenInvalid() throws IOException { - sink.writeUtf8("abc"); - sink.writeUtf8(repeat("d", 2 * SEGMENT_SIZE)); - sink.emit(); - - assertEquals("abc", source.readUtf8(3)); - - // Peek a little data and skip the rest of the upstream source - BufferedSource peek = source.peek(); - assertEquals("ddd", peek.readUtf8(3)); - source.readAll(Okio.blackhole()); - - // Skip the rest of the buffered data - peek.skip(peek.getBuffer().size()); - - try { - peek.readByte(); - fail(); - } catch (IllegalStateException e) { - assertEquals("Peek source is invalid because upstream source was used", e.getMessage()); - } - } - - @Test public void peekDoesntReadTooMuch() throws IOException { - // 6 bytes in source's buffer plus 3 bytes upstream. - sink.writeUtf8("abcdef"); - sink.emit(); - source.require(6L); - sink.writeUtf8("ghi"); - sink.emit(); - - BufferedSource peek = source.peek(); - - // Read 3 bytes. This reads some of the buffered data. - assertTrue(peek.request(3)); - if (!(source instanceof Buffer)) { - assertEquals(6, source.getBuffer().size()); - assertEquals(6, peek.getBuffer().size()); - } - assertEquals("abc", peek.readUtf8(3L)); - - // Read 3 more bytes. This exhausts the buffered data. - assertTrue(peek.request(3)); - if (!(source instanceof Buffer)) { - assertEquals(6, source.getBuffer().size()); - assertEquals(3, peek.getBuffer().size()); - } - assertEquals("def", peek.readUtf8(3L)); - - // Read 3 more bytes. This draws new bytes. - assertTrue(peek.request(3)); - assertEquals(9, source.getBuffer().size()); - assertEquals(3, peek.getBuffer().size()); - assertEquals("ghi", peek.readUtf8(3L)); - } - - @Test public void rangeEquals() throws IOException { - sink.writeUtf8("A man, a plan, a canal. Panama."); - sink.emit(); - assertTrue(source.rangeEquals(7 , ByteString.encodeUtf8("a plan"))); - assertTrue(source.rangeEquals(0 , ByteString.encodeUtf8("A man"))); - assertTrue(source.rangeEquals(24, ByteString.encodeUtf8("Panama"))); - assertFalse(source.rangeEquals(24, ByteString.encodeUtf8("Panama. Panama. Panama."))); - } - - @Test public void rangeEqualsWithOffsetAndCount() throws IOException { - sink.writeUtf8("A man, a plan, a canal. Panama."); - sink.emit(); - assertTrue(source.rangeEquals(7 , ByteString.encodeUtf8("aaa plannn"), 2, 6)); - assertTrue(source.rangeEquals(0 , ByteString.encodeUtf8("AAA mannn"), 2, 5)); - assertTrue(source.rangeEquals(24, ByteString.encodeUtf8("PPPanamaaa"), 2, 6)); - } - - @Test public void rangeEqualsOnlyReadsUntilMismatch() throws IOException { - assumeTrue(factory == Factory.ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE); // Other sources read in chunks anyway. - - sink.writeUtf8("A man, a plan, a canal. Panama."); - sink.emit(); - assertFalse(source.rangeEquals(0, ByteString.encodeUtf8("A man."))); - assertEquals("A man,", source.getBuffer().readUtf8()); - } - - @Test public void rangeEqualsArgumentValidation() throws IOException { - // Negative source offset. - assertFalse(source.rangeEquals(-1, ByteString.encodeUtf8("A"))); - // Negative bytes offset. - assertFalse(source.rangeEquals(0, ByteString.encodeUtf8("A"), -1, 1)); - // Bytes offset longer than bytes length. - assertFalse(source.rangeEquals(0, ByteString.encodeUtf8("A"), 2, 1)); - // Negative byte count. - assertFalse(source.rangeEquals(0, ByteString.encodeUtf8("A"), 0, -1)); - // Byte count longer than bytes length. - assertFalse(source.rangeEquals(0, ByteString.encodeUtf8("A"), 0, 2)); - // Bytes offset plus byte count longer than bytes length. - assertFalse(source.rangeEquals(0, ByteString.encodeUtf8("A"), 1, 1)); - } - - @Test public void readNioBuffer() throws Exception { - String expected = factory.isOneByteAtATime() ? "a" : "abcdefg"; - sink.writeUtf8("abcdefg"); - sink.emit(); - - ByteBuffer nioByteBuffer = ByteBuffer.allocate(1024); - int byteCount = source.read(nioByteBuffer); - assertEquals(expected.length(), byteCount); - assertEquals(expected.length(), nioByteBuffer.position()); - assertEquals(nioByteBuffer.capacity(), nioByteBuffer.limit()); - - nioByteBuffer.flip(); - byte[] data = new byte[expected.length()]; - nioByteBuffer.get(data); - assertEquals(expected, new String(data)); - } - - /** Note that this test crashes the VM on Android. */ - @Test public void readLargeNioBufferOnlyReadsOneSegment() throws Exception { - String expected = factory.isOneByteAtATime() - ? "a" - : repeat("a", SEGMENT_SIZE); - sink.writeUtf8(repeat("a", SEGMENT_SIZE * 4)); - sink.emit(); - - ByteBuffer nioByteBuffer = ByteBuffer.allocate(SEGMENT_SIZE * 3); - int byteCount = source.read(nioByteBuffer); - assertEquals(expected.length(), byteCount); - assertEquals(expected.length(), nioByteBuffer.position()); - assertEquals(nioByteBuffer.capacity(), nioByteBuffer.limit()); - - nioByteBuffer.flip(); - byte[] data = new byte[expected.length()]; - nioByteBuffer.get(data); - assertEquals(expected, new String(data)); - } - - @Test public void factorySegmentSizes() throws Exception { - sink.writeUtf8("abc"); - sink.emit(); - source.require(3); - if (factory.isOneByteAtATime()) { - assertEquals(Arrays.asList(1, 1, 1), TestUtil.segmentSizes(source.getBuffer())); - } else { - assertEquals(Collections.singletonList(3), TestUtil.segmentSizes(source.getBuffer())); - } - } -} diff --git a/okio/src/jvmTest/java/okio/ByteStringJavaTest.java b/okio/src/jvmTest/java/okio/ByteStringJavaTest.java deleted file mode 100644 index f1b6624e..00000000 --- a/okio/src/jvmTest/java/okio/ByteStringJavaTest.java +++ /dev/null @@ -1,633 +0,0 @@ -/* - * Copyright 2014 Square Inc. - * - * 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 java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Random; -import kotlin.text.Charsets; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; - -import static okio.TestUtil.assertByteArraysEquals; -import static okio.TestUtil.assertEquivalent; -import static okio.TestUtil.makeSegments; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -@RunWith(Parameterized.class) -public final class ByteStringJavaTest { - interface Factory { - Factory BYTE_STRING = new Factory() { - @Override public ByteString decodeHex(String hex) { - return ByteString.decodeHex(hex); - } - - @Override public ByteString encodeUtf8(String s) { - return ByteString.encodeUtf8(s); - } - }; - - Factory SEGMENTED_BYTE_STRING = new Factory() { - @Override public ByteString decodeHex(String hex) { - Buffer buffer = new Buffer(); - buffer.write(ByteString.decodeHex(hex)); - return buffer.snapshot(); - } - - @Override public ByteString encodeUtf8(String s) { - Buffer buffer = new Buffer(); - buffer.writeUtf8(s); - return buffer.snapshot(); - } - }; - - Factory ONE_BYTE_PER_SEGMENT = new Factory() { - @Override public ByteString decodeHex(String hex) { - return makeSegments(ByteString.decodeHex(hex)); - } - - @Override public ByteString encodeUtf8(String s) { - return makeSegments(ByteString.encodeUtf8(s)); - } - }; - - ByteString decodeHex(String hex); - ByteString encodeUtf8(String s); - } - - @Parameters(name = "{1}") - public static List<Object[]> parameters() { - return Arrays.asList( - new Object[] { Factory.BYTE_STRING, "ByteString" }, - new Object[] { Factory.SEGMENTED_BYTE_STRING, "SegmentedByteString" }, - new Object[] { Factory.ONE_BYTE_PER_SEGMENT, "SegmentedByteString (one-at-a-time)" }); - } - - @Parameter(0) public Factory factory; - @Parameter(1) public String name; - - @Test public void ofCopy() { - byte[] bytes = "Hello, World!".getBytes(Charsets.UTF_8); - ByteString byteString = ByteString.of(bytes); - // Verify that the bytes were copied out. - bytes[4] = (byte) 'a'; - assertEquals("Hello, World!", byteString.utf8()); - } - - @Test public void ofCopyRange() { - byte[] bytes = "Hello, World!".getBytes(Charsets.UTF_8); - ByteString byteString = ByteString.of(bytes, 2, 9); - // Verify that the bytes were copied out. - bytes[4] = (byte) 'a'; - assertEquals("llo, Worl", byteString.utf8()); - } - - @Test public void ofByteBuffer() { - byte[] bytes = "Hello, World!".getBytes(Charsets.UTF_8); - ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); - byteBuffer.position(2).limit(11); - ByteString byteString = ByteString.of(byteBuffer); - // Verify that the bytes were copied out. - byteBuffer.put(4, (byte) 'a'); - assertEquals("llo, Worl", byteString.utf8()); - } - - @Test public void getByte() throws Exception { - ByteString byteString = factory.decodeHex("ab12"); - assertEquals(-85, byteString.getByte(0)); - assertEquals(18, byteString.getByte(1)); - } - - @Test public void getByteOutOfBounds() throws Exception { - ByteString byteString = factory.decodeHex("ab12"); - try { - byteString.getByte(2); - fail(); - } catch (IndexOutOfBoundsException expected) { - } - } - - @Test public void startsWithByteString() throws Exception { - ByteString byteString = factory.decodeHex("112233"); - assertTrue(byteString.startsWith(ByteString.decodeHex(""))); - assertTrue(byteString.startsWith(ByteString.decodeHex("11"))); - assertTrue(byteString.startsWith(ByteString.decodeHex("1122"))); - assertTrue(byteString.startsWith(ByteString.decodeHex("112233"))); - assertFalse(byteString.startsWith(ByteString.decodeHex("2233"))); - assertFalse(byteString.startsWith(ByteString.decodeHex("11223344"))); - assertFalse(byteString.startsWith(ByteString.decodeHex("112244"))); - } - - @Test public void endsWithByteString() throws Exception { - ByteString byteString = factory.decodeHex("112233"); - assertTrue(byteString.endsWith(ByteString.decodeHex(""))); - assertTrue(byteString.endsWith(ByteString.decodeHex("33"))); - assertTrue(byteString.endsWith(ByteString.decodeHex("2233"))); - assertTrue(byteString.endsWith(ByteString.decodeHex("112233"))); - assertFalse(byteString.endsWith(ByteString.decodeHex("1122"))); - assertFalse(byteString.endsWith(ByteString.decodeHex("00112233"))); - assertFalse(byteString.endsWith(ByteString.decodeHex("002233"))); - } - - @Test public void startsWithByteArray() throws Exception { - ByteString byteString = factory.decodeHex("112233"); - assertTrue(byteString.startsWith(ByteString.decodeHex("").toByteArray())); - assertTrue(byteString.startsWith(ByteString.decodeHex("11").toByteArray())); - assertTrue(byteString.startsWith(ByteString.decodeHex("1122").toByteArray())); - assertTrue(byteString.startsWith(ByteString.decodeHex("112233").toByteArray())); - assertFalse(byteString.startsWith(ByteString.decodeHex("2233").toByteArray())); - assertFalse(byteString.startsWith(ByteString.decodeHex("11223344").toByteArray())); - assertFalse(byteString.startsWith(ByteString.decodeHex("112244").toByteArray())); - } - - @Test public void endsWithByteArray() throws Exception { - ByteString byteString = factory.decodeHex("112233"); - assertTrue(byteString.endsWith(ByteString.decodeHex("").toByteArray())); - assertTrue(byteString.endsWith(ByteString.decodeHex("33").toByteArray())); - assertTrue(byteString.endsWith(ByteString.decodeHex("2233").toByteArray())); - assertTrue(byteString.endsWith(ByteString.decodeHex("112233").toByteArray())); - assertFalse(byteString.endsWith(ByteString.decodeHex("1122").toByteArray())); - assertFalse(byteString.endsWith(ByteString.decodeHex("00112233").toByteArray())); - assertFalse(byteString.endsWith(ByteString.decodeHex("002233").toByteArray())); - } - - @Test public void indexOfByteString() throws Exception { - ByteString byteString = factory.decodeHex("112233"); - assertEquals(0, byteString.indexOf(ByteString.decodeHex("112233"))); - assertEquals(0, byteString.indexOf(ByteString.decodeHex("1122"))); - assertEquals(0, byteString.indexOf(ByteString.decodeHex("11"))); - assertEquals(0, byteString.indexOf(ByteString.decodeHex("11"), 0)); - assertEquals(0, byteString.indexOf(ByteString.decodeHex(""))); - assertEquals(0, byteString.indexOf(ByteString.decodeHex(""), 0)); - assertEquals(1, byteString.indexOf(ByteString.decodeHex("2233"))); - assertEquals(1, byteString.indexOf(ByteString.decodeHex("22"))); - assertEquals(1, byteString.indexOf(ByteString.decodeHex("22"), 1)); - assertEquals(1, byteString.indexOf(ByteString.decodeHex(""), 1)); - assertEquals(2, byteString.indexOf(ByteString.decodeHex("33"))); - assertEquals(2, byteString.indexOf(ByteString.decodeHex("33"), 2)); - assertEquals(2, byteString.indexOf(ByteString.decodeHex(""), 2)); - assertEquals(3, byteString.indexOf(ByteString.decodeHex(""), 3)); - assertEquals(-1, byteString.indexOf(ByteString.decodeHex("112233"), 1)); - assertEquals(-1, byteString.indexOf(ByteString.decodeHex("44"))); - assertEquals(-1, byteString.indexOf(ByteString.decodeHex("11223344"))); - assertEquals(-1, byteString.indexOf(ByteString.decodeHex("112244"))); - assertEquals(-1, byteString.indexOf(ByteString.decodeHex("112233"), 1)); - assertEquals(-1, byteString.indexOf(ByteString.decodeHex("2233"), 2)); - assertEquals(-1, byteString.indexOf(ByteString.decodeHex("33"), 3)); - assertEquals(-1, byteString.indexOf(ByteString.decodeHex(""), 4)); - } - - @Test public void indexOfWithOffset() throws Exception { - ByteString byteString = factory.decodeHex("112233112233"); - assertEquals(0, byteString.indexOf(ByteString.decodeHex("112233"), -1)); - assertEquals(0, byteString.indexOf(ByteString.decodeHex("112233"), 0)); - assertEquals(0, byteString.indexOf(ByteString.decodeHex("112233"))); - assertEquals(3, byteString.indexOf(ByteString.decodeHex("112233"), 1)); - assertEquals(3, byteString.indexOf(ByteString.decodeHex("112233"), 2)); - assertEquals(3, byteString.indexOf(ByteString.decodeHex("112233"), 3)); - assertEquals(-1, byteString.indexOf(ByteString.decodeHex("112233"), 4)); - } - - @Test public void indexOfByteArray() throws Exception { - ByteString byteString = factory.decodeHex("112233"); - assertEquals(0, byteString.indexOf(ByteString.decodeHex("112233").toByteArray())); - assertEquals(1, byteString.indexOf(ByteString.decodeHex("2233").toByteArray())); - assertEquals(2, byteString.indexOf(ByteString.decodeHex("33").toByteArray())); - assertEquals(-1, byteString.indexOf(ByteString.decodeHex("112244").toByteArray())); - } - - @Test public void lastIndexOfByteString() throws Exception { - ByteString byteString = factory.decodeHex("112233"); - assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex("112233"))); - assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex("1122"))); - assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex("11"))); - assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex("11"), 3)); - assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex("11"), 0)); - assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex(""), 0)); - assertEquals(1, byteString.lastIndexOf(ByteString.decodeHex("2233"))); - assertEquals(1, byteString.lastIndexOf(ByteString.decodeHex("22"))); - assertEquals(1, byteString.lastIndexOf(ByteString.decodeHex("22"), 3)); - assertEquals(1, byteString.lastIndexOf(ByteString.decodeHex("22"), 1)); - assertEquals(1, byteString.lastIndexOf(ByteString.decodeHex(""), 1)); - assertEquals(2, byteString.lastIndexOf(ByteString.decodeHex("33"))); - assertEquals(2, byteString.lastIndexOf(ByteString.decodeHex("33"), 3)); - assertEquals(2, byteString.lastIndexOf(ByteString.decodeHex("33"), 2)); - assertEquals(2, byteString.lastIndexOf(ByteString.decodeHex(""), 2)); - assertEquals(3, byteString.lastIndexOf(ByteString.decodeHex(""), 3)); - assertEquals(3, byteString.lastIndexOf(ByteString.decodeHex(""))); - assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("112233"), -1)); - assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("112233"), -2)); - assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("44"))); - assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("11223344"))); - assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("112244"))); - assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("2233"), 0)); - assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("33"), 1)); - assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex(""), -1)); - } - - @Test public void lastIndexOfByteArray() throws Exception { - ByteString byteString = factory.decodeHex("112233"); - assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex("112233").toByteArray())); - assertEquals(1, byteString.lastIndexOf(ByteString.decodeHex("2233").toByteArray())); - assertEquals(2, byteString.lastIndexOf(ByteString.decodeHex("33").toByteArray())); - assertEquals(3, byteString.lastIndexOf(ByteString.decodeHex("").toByteArray())); - } - - @SuppressWarnings("SelfEquals") - @Test public void equals() throws Exception { - ByteString byteString = factory.decodeHex("000102"); - assertTrue(byteString.equals(byteString)); - assertTrue(byteString.equals(ByteString.decodeHex("000102"))); - assertTrue(factory.decodeHex("").equals(ByteString.EMPTY)); - assertTrue(factory.decodeHex("").equals(ByteString.of())); - assertTrue(ByteString.EMPTY.equals(factory.decodeHex(""))); - assertTrue(ByteString.of().equals(factory.decodeHex(""))); - assertFalse(byteString.equals(new Object())); - assertFalse(byteString.equals(ByteString.decodeHex("000201"))); - } - - private final String bronzeHorseman = "На берегу пустынных волн"; - - @Test public void utf8() throws Exception { - ByteString byteString = factory.encodeUtf8(bronzeHorseman); - assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.getBytes(Charsets.UTF_8)); - assertTrue(byteString.equals(ByteString.of(bronzeHorseman.getBytes(Charsets.UTF_8)))); - assertEquals(byteString.utf8(), bronzeHorseman); - } - - @Test public void encodeNullCharset() throws Exception { - try { - ByteString.encodeString("hello", null); - fail(); - } catch (NullPointerException expected) { - } - } - - @Test public void encodeNullString() throws Exception { - try { - ByteString.encodeString(null, Charset.forName("UTF-8")); - fail(); - } catch (NullPointerException expected) { - } - } - - @Test public void decodeNullCharset() throws Exception { - try { - ByteString.of().string(null); - fail(); - } catch (NullPointerException expected) { - } - } - - @Test public void encodeDecodeStringUtf8() throws Exception { - Charset utf8 = Charset.forName("UTF-8"); - ByteString byteString = ByteString.encodeString(bronzeHorseman, utf8); - assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.getBytes(utf8)); - assertEquals(byteString, ByteString.decodeHex("d09dd0b020d0b1d0b5d180d0b5d0b3d18320d0bfd183d181" - + "d182d18bd0bdd0bdd18bd18520d0b2d0bed0bbd0bd")); - assertEquals(bronzeHorseman, byteString.string(utf8)); - } - - @Test public void encodeDecodeStringUtf16be() throws Exception { - Charset utf16be = Charset.forName("UTF-16BE"); - ByteString byteString = ByteString.encodeString(bronzeHorseman, utf16be); - assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.getBytes(utf16be)); - assertEquals(byteString, ByteString.decodeHex("041d043000200431043504400435043304430020043f0443" - + "04410442044b043d043d044b044500200432043e043b043d")); - assertEquals(bronzeHorseman, byteString.string(utf16be)); - } - - @Test public void encodeDecodeStringUtf32be() throws Exception { - Charset utf32be = Charset.forName("UTF-32BE"); - ByteString byteString = ByteString.encodeString(bronzeHorseman, utf32be); - assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.getBytes(utf32be)); - assertEquals(byteString, ByteString.decodeHex("0000041d0000043000000020000004310000043500000440" - + "000004350000043300000443000000200000043f0000044300000441000004420000044b0000043d0000043d" - + "0000044b0000044500000020000004320000043e0000043b0000043d")); - assertEquals(bronzeHorseman, byteString.string(utf32be)); - } - - @Test public void encodeDecodeStringAsciiIsLossy() throws Exception { - Charset ascii = Charset.forName("US-ASCII"); - ByteString byteString = ByteString.encodeString(bronzeHorseman, ascii); - assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.getBytes(ascii)); - assertEquals(byteString, - ByteString.decodeHex("3f3f203f3f3f3f3f3f203f3f3f3f3f3f3f3f3f203f3f3f3f")); - assertEquals("?? ?????? ????????? ????", byteString.string(ascii)); - } - - @Test public void decodeMalformedStringReturnsReplacementCharacter() throws Exception { - Charset utf16be = Charset.forName("UTF-16BE"); - String string = ByteString.decodeHex("04").string(utf16be); - assertEquals("\ufffd", string); - } - - @Test public void testHashCode() throws Exception { - ByteString byteString = factory.decodeHex("0102"); - assertEquals(byteString.hashCode(), byteString.hashCode()); - assertEquals(byteString.hashCode(), ByteString.decodeHex("0102").hashCode()); - } - - @Test public void read() throws Exception { - InputStream in = new ByteArrayInputStream("abc".getBytes(Charsets.UTF_8)); - assertEquals(ByteString.decodeHex("6162"), ByteString.read(in, 2)); - assertEquals(ByteString.decodeHex("63"), ByteString.read(in, 1)); - assertEquals(ByteString.of(), ByteString.read(in, 0)); - } - - @Test public void readAndToLowercase() throws Exception { - InputStream in = new ByteArrayInputStream("ABC".getBytes(Charsets.UTF_8)); - assertEquals(ByteString.encodeUtf8("ab"), ByteString.read(in, 2).toAsciiLowercase()); - assertEquals(ByteString.encodeUtf8("c"), ByteString.read(in, 1).toAsciiLowercase()); - assertEquals(ByteString.EMPTY, ByteString.read(in, 0).toAsciiLowercase()); - } - - @Test public void toAsciiLowerCaseNoUppercase() throws Exception { - ByteString s = factory.encodeUtf8("a1_+"); - assertEquals(s, s.toAsciiLowercase()); - if (factory == Factory.BYTE_STRING) { - assertSame(s, s.toAsciiLowercase()); - } - } - - @Test public void toAsciiAllUppercase() throws Exception { - assertEquals(ByteString.encodeUtf8("ab"), factory.encodeUtf8("AB").toAsciiLowercase()); - } - - @Test public void toAsciiStartsLowercaseEndsUppercase() throws Exception { - assertEquals(ByteString.encodeUtf8("abcd"), factory.encodeUtf8("abCD").toAsciiLowercase()); - } - - @Test public void readAndToUppercase() throws Exception { - InputStream in = new ByteArrayInputStream("abc".getBytes(Charsets.UTF_8)); - assertEquals(ByteString.encodeUtf8("AB"), ByteString.read(in, 2).toAsciiUppercase()); - assertEquals(ByteString.encodeUtf8("C"), ByteString.read(in, 1).toAsciiUppercase()); - assertEquals(ByteString.EMPTY, ByteString.read(in, 0).toAsciiUppercase()); - } - - @Test public void toAsciiStartsUppercaseEndsLowercase() throws Exception { - assertEquals(ByteString.encodeUtf8("ABCD"), factory.encodeUtf8("ABcd").toAsciiUppercase()); - } - - @Test public void substring() throws Exception { - ByteString byteString = factory.encodeUtf8("Hello, World!"); - - assertEquals(byteString.substring(0), byteString); - assertEquals(byteString.substring(0, 5), ByteString.encodeUtf8("Hello")); - assertEquals(byteString.substring(7), ByteString.encodeUtf8("World!")); - assertEquals(byteString.substring(6, 6), ByteString.encodeUtf8("")); - } - - @Test public void substringWithInvalidBounds() throws Exception { - ByteString byteString = factory.encodeUtf8("Hello, World!"); - - try { - byteString.substring(-1); - fail(); - } catch (IllegalArgumentException expected) { - } - - try { - byteString.substring(0, 14); - fail(); - } catch (IllegalArgumentException expected) { - } - - try { - byteString.substring(8, 7); - fail(); - } catch (IllegalArgumentException expected) { - } - } - - @Test public void write() throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - factory.decodeHex("616263").write(out); - assertByteArraysEquals(new byte[] { 0x61, 0x62, 0x63 }, out.toByteArray()); - } - - @Test public void encodeBase64() { - assertEquals("", factory.encodeUtf8("").base64()); - assertEquals("AA==", factory.encodeUtf8("\u0000").base64()); - assertEquals("AAA=", factory.encodeUtf8("\u0000\u0000").base64()); - assertEquals("AAAA", factory.encodeUtf8("\u0000\u0000\u0000").base64()); - assertEquals("SG93IG1hbnkgbGluZXMgb2YgY29kZSBhcmUgdGhlcmU/ICdib3V0IDIgbWlsbGlvbi4=", - factory.encodeUtf8("How many lines of code are there? 'bout 2 million.").base64()); - } - - @Test public void encodeBase64Url() { - assertEquals("", factory.encodeUtf8("").base64Url()); - assertEquals("AA==", factory.encodeUtf8("\u0000").base64Url()); - assertEquals("AAA=", factory.encodeUtf8("\u0000\u0000").base64Url()); - assertEquals("AAAA", factory.encodeUtf8("\u0000\u0000\u0000").base64Url()); - assertEquals("SG93IG1hbnkgbGluZXMgb2YgY29kZSBhcmUgdGhlcmU_ICdib3V0IDIgbWlsbGlvbi4=", - factory.encodeUtf8("How many lines of code are there? 'bout 2 million.").base64Url()); - } - - @Test public void ignoreUnnecessaryPadding() { - assertEquals("", ByteString.decodeBase64("====").utf8()); - assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64("AAAA====").utf8()); - } - - @Test public void decodeBase64() { - assertEquals("", ByteString.decodeBase64("").utf8()); - assertEquals(null, ByteString.decodeBase64("/===")); // Can't do anything with 6 bits! - assertEquals(ByteString.decodeHex("ff"), ByteString.decodeBase64("//==")); - assertEquals(ByteString.decodeHex("ff"), ByteString.decodeBase64("__==")); - assertEquals(ByteString.decodeHex("ffff"), ByteString.decodeBase64("///=")); - assertEquals(ByteString.decodeHex("ffff"), ByteString.decodeBase64("___=")); - assertEquals(ByteString.decodeHex("ffffff"), ByteString.decodeBase64("////")); - assertEquals(ByteString.decodeHex("ffffff"), ByteString.decodeBase64("____")); - assertEquals(ByteString.decodeHex("ffffffffffff"), ByteString.decodeBase64("////////")); - assertEquals(ByteString.decodeHex("ffffffffffff"), ByteString.decodeBase64("________")); - assertEquals("What's to be scared about? It's just a little hiccup in the power...", - ByteString.decodeBase64("V2hhdCdzIHRvIGJlIHNjYXJlZCBhYm91dD8gSXQncyBqdXN0IGEgbGl0dGxlIGhpY2" - + "N1cCBpbiB0aGUgcG93ZXIuLi4=").utf8()); - // Uses two encoding styles. Malformed, but supported as a side-effect. - assertEquals(ByteString.decodeHex("ffffff"), ByteString.decodeBase64("__//")); - } - - @Test public void decodeBase64WithWhitespace() { - assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64(" AA AA ").utf8()); - assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64(" AA A\r\nA ").utf8()); - assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64("AA AA").utf8()); - assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64(" AA AA ").utf8()); - assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64(" AA A\r\nA ").utf8()); - assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64("A AAA").utf8()); - assertEquals("", ByteString.decodeBase64(" ").utf8()); - } - - @Test public void encodeHex() throws Exception { - assertEquals("000102", ByteString.of((byte) 0x0, (byte) 0x1, (byte) 0x2).hex()); - } - - @Test public void decodeHex() throws Exception { - assertEquals(ByteString.of((byte) 0x0, (byte) 0x1, (byte) 0x2), ByteString.decodeHex("000102")); - } - - @Test public void decodeHexOddNumberOfChars() throws Exception { - try { - ByteString.decodeHex("aaa"); - fail(); - } catch (IllegalArgumentException expected) { - } - } - - @Test public void decodeHexInvalidChar() throws Exception { - try { - ByteString.decodeHex("a\u0000"); - fail(); - } catch (IllegalArgumentException expected) { - } - } - - @Test public void toStringOnEmpty() { - assertEquals("[size=0]", factory.decodeHex("").toString()); - } - - @Test public void toStringOnShortText() { - assertEquals("[text=Tyrannosaur]", - factory.encodeUtf8("Tyrannosaur").toString()); - assertEquals("[text=təˈranəˌsôr]", - factory.decodeHex("74c999cb8872616ec999cb8c73c3b472").toString()); - } - - @Test public void toStringOnLongTextIsTruncated() { - String raw = "Um, I'll tell you the problem with the scientific power that you're using here, " - + "it didn't require any discipline to attain it. You read what others had done and you " - + "took the next step. You didn't earn the knowledge for yourselves, so you don't take any " - + "responsibility for it. You stood on the shoulders of geniuses to accomplish something " - + "as fast as you could, and before you even knew what you had, you patented it, and " - + "packaged it, and slapped it on a plastic lunchbox, and now you're selling it, you wanna " - + "sell it."; - assertEquals("[size=517 text=Um, I'll tell you the problem with the scientific power that " - + "you…]", factory.encodeUtf8(raw).toString()); - String war = "Սm, I'll 𝓽𝖾ll ᶌօ𝘂 ᴛℎ℮ 𝜚𝕣०bl𝖾m wі𝕥𝒽 𝘵𝘩𝐞 𝓼𝙘𝐢𝔢𝓷𝗍𝜄𝚏𝑖c 𝛠𝝾w𝚎𝑟 𝕥h⍺𝞃 𝛄𝓸𝘂'𝒓𝗲 υ𝖘𝓲𝗇ɡ 𝕙𝚎𝑟e, " - + "𝛊𝓽 ⅆ𝕚𝐝𝝿'𝗍 𝔯𝙚𝙦ᴜ𝜾𝒓𝘦 𝔞𝘯𝐲 ԁ𝜄𝑠𝚌ι𝘱lι𝒏e 𝑡𝜎 𝕒𝚝𝖙𝓪і𝞹 𝔦𝚝. 𝒀ο𝗎 𝔯𝑒⍺𝖉 w𝐡𝝰𝔱 𝞂𝞽һ𝓮𝓇ƽ հ𝖺𝖉 ⅾ𝛐𝝅ⅇ 𝝰πԁ 𝔂ᴑᴜ 𝓉ﮨ၀𝚔 " - + "т𝒽𝑒 𝗇𝕖ⅹ𝚝 𝔰𝒕е𝓅. 𝘠ⲟ𝖚 𝖉ⅰԁ𝝕'τ 𝙚𝚊r𝞹 𝘵Ꮒ𝖾 𝝒𝐧هwl𝑒𝖉ƍ𝙚 𝓯૦r 𝔂𝞼𝒖𝕣𝑠𝕖l𝙫𝖊𝓼, 𐑈о y𝘰𝒖 ⅆە𝗇't 𝜏α𝒌𝕖 𝛂𝟉ℽ " - + "𝐫ⅇ𝗌ⲣ๐ϖ𝖘ꙇᖯ𝓲l𝓲𝒕𝘆 𝐟𝞼𝘳 𝚤𝑡. 𝛶𝛔𝔲 s𝕥σσ𝐝 ﮩ𝕟 𝒕𝗁𝔢 𝘴𝐡𝜎ᴜlⅾ𝓮𝔯𝚜 𝛐𝙛 ᶃ𝚎ᴨᎥս𝚜𝘦𝓈 𝓽𝞸 a𝒄𝚌𝞸mρl𝛊ꜱ𝐡 𝓈𝚘m𝚎𝞃𝔥⍳𝞹𝔤 𝐚𝗌 𝖋a𝐬𝒕 " - + "αs γ𝛐𝕦 𝔠ﻫ𝛖lԁ, 𝚊π𝑑 Ь𝑒𝙛૦𝓇𝘦 𝓎٥𝖚 ⅇvℯ𝝅 𝜅ո𝒆w w𝗵𝒂𝘁 ᶌ੦𝗎 h𝐚𝗱, 𝜸ﮨ𝒖 𝓹𝝰𝔱𝖾𝗇𝓽𝔢ⅆ і𝕥, 𝚊𝜛𝓭 𝓹𝖺ⅽϰ𝘢ℊеᏧ 𝑖𝞃, " - + "𝐚𝛑ꓒ 𝙨l𝔞р𝘱𝔢𝓭 ɩ𝗍 ہ𝛑 𝕒 pl𝛂ѕᴛ𝗂𝐜 l𝞄ℼ𝔠𝒽𝑏ﮪ⨯, 𝔞ϖ𝒹 n𝛔w 𝛾𝐨𝞄'𝗿𝔢 ꜱ℮ll𝙞nɡ ɩ𝘁, 𝙮𝕠𝛖 w𝑎ℼ𝚗𝛂 𝕤𝓮ll 𝙞𝓉."; - assertEquals( "[size=1496 text=Սm, I'll 𝓽𝖾ll ᶌօ𝘂 ᴛℎ℮ 𝜚𝕣०bl𝖾m wі𝕥𝒽 𝘵𝘩𝐞 𝓼𝙘𝐢𝔢𝓷𝗍𝜄𝚏𝑖c 𝛠𝝾w𝚎𝑟 𝕥h⍺𝞃 " - + "𝛄𝓸𝘂…]", factory.encodeUtf8(war).toString()); - } - - @Test public void toStringOnTextWithNewlines() { - // Instead of emitting a literal newline in the toString(), these are escaped as "\n". - assertEquals("[text=a\\r\\nb\\nc\\rd\\\\e]", - factory.encodeUtf8("a\r\nb\nc\rd\\e").toString()); - } - - @Test public void toStringOnData() { - ByteString byteString = factory.decodeHex("" - + "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55" - + "4bf0b54023c29b624de9ef9c2f931efc580f9afb"); - assertEquals("[hex=" - + "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55" - + "4bf0b54023c29b624de9ef9c2f931efc580f9afb]", byteString.toString()); - } - - @Test public void toStringOnLongDataIsTruncated() { - ByteString byteString = factory.decodeHex("" - + "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55" - + "4bf0b54023c29b624de9ef9c2f931efc580f9afba1"); - assertEquals("[size=65 hex=" - + "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55" - + "4bf0b54023c29b624de9ef9c2f931efc580f9afb…]", byteString.toString()); - } - - @Test public void javaSerializationTestNonEmpty() throws Exception { - ByteString byteString = factory.encodeUtf8(bronzeHorseman); - assertEquivalent(byteString, TestUtil.<ByteString>reserialize(byteString)); - } - - @Test public void javaSerializationTestEmpty() throws Exception { - ByteString byteString = factory.decodeHex(""); - assertEquivalent(byteString, TestUtil.<ByteString>reserialize(byteString)); - } - - @Test public void compareToSingleBytes() throws Exception { - List<ByteString> originalByteStrings = Arrays.asList( - factory.decodeHex("00"), - factory.decodeHex("01"), - factory.decodeHex("7e"), - factory.decodeHex("7f"), - factory.decodeHex("80"), - factory.decodeHex("81"), - factory.decodeHex("fe"), - factory.decodeHex("ff")); - - List<ByteString> sortedByteStrings = new ArrayList<>(originalByteStrings); - Collections.shuffle(sortedByteStrings, new Random(0)); - Collections.sort(sortedByteStrings); - - assertEquals(originalByteStrings, sortedByteStrings); - } - - @Test public void compareToMultipleBytes() throws Exception { - List<ByteString> originalByteStrings = Arrays.asList( - factory.decodeHex(""), - factory.decodeHex("00"), - factory.decodeHex("0000"), - factory.decodeHex("000000"), - factory.decodeHex("00000000"), - factory.decodeHex("0000000000"), - factory.decodeHex("0000000001"), - factory.decodeHex("000001"), - factory.decodeHex("00007f"), - factory.decodeHex("0000ff"), - factory.decodeHex("000100"), - factory.decodeHex("000101"), - factory.decodeHex("007f00"), - factory.decodeHex("00ff00"), - factory.decodeHex("010000"), - factory.decodeHex("010001"), - factory.decodeHex("01007f"), - factory.decodeHex("0100ff"), - factory.decodeHex("010100"), - factory.decodeHex("01010000"), - factory.decodeHex("0101000000"), - factory.decodeHex("0101000001"), - factory.decodeHex("010101"), - factory.decodeHex("7f0000"), - factory.decodeHex("7f0000ffff"), - factory.decodeHex("ffffff")); - - List<ByteString> sortedByteStrings = new ArrayList<>(originalByteStrings); - Collections.shuffle(sortedByteStrings, new Random(0)); - Collections.sort(sortedByteStrings); - - assertEquals(originalByteStrings, sortedByteStrings); - } - - @Test public void asByteBuffer() { - assertEquals(0x42, ByteString.of((byte) 0x41, (byte) 0x42, (byte) 0x43).asByteBuffer().get(1)); - } -} diff --git a/okio/src/jvmTest/java/okio/DeflaterSinkTest.java b/okio/src/jvmTest/java/okio/DeflaterSinkTest.java deleted file mode 100644 index f0a31f00..00000000 --- a/okio/src/jvmTest/java/okio/DeflaterSinkTest.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 java.io.IOException; -import java.io.InputStream; -import java.util.zip.Deflater; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; -import org.junit.Test; - -import static kotlin.text.StringsKt.repeat; -import static okio.TestUtil.SEGMENT_SIZE; -import static okio.TestUtil.randomBytes; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public final class DeflaterSinkTest { - @Test public void deflateWithClose() throws Exception { - Buffer data = new Buffer(); - String original = "They're moving in herds. They do move in herds."; - data.writeUtf8(original); - Buffer sink = new Buffer(); - DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater()); - deflaterSink.write(data, data.size()); - deflaterSink.close(); - Buffer inflated = inflate(sink); - assertEquals(original, inflated.readUtf8()); - } - - @Test public void deflateWithSyncFlush() throws Exception { - String original = "Yes, yes, yes. That's why we're taking extreme precautions."; - Buffer data = new Buffer(); - data.writeUtf8(original); - Buffer sink = new Buffer(); - DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater()); - deflaterSink.write(data, data.size()); - deflaterSink.flush(); - Buffer inflated = inflate(sink); - assertEquals(original, inflated.readUtf8()); - } - - @Test public void deflateWellCompressed() throws IOException { - String original = repeat("a", 1024 * 1024); - Buffer data = new Buffer(); - data.writeUtf8(original); - Buffer sink = new Buffer(); - DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater()); - deflaterSink.write(data, data.size()); - deflaterSink.close(); - Buffer inflated = inflate(sink); - assertEquals(original, inflated.readUtf8()); - } - - @Test public void deflatePoorlyCompressed() throws IOException { - ByteString original = randomBytes(1024 * 1024); - Buffer data = new Buffer(); - data.write(original); - Buffer sink = new Buffer(); - DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater()); - deflaterSink.write(data, data.size()); - deflaterSink.close(); - Buffer inflated = inflate(sink); - assertEquals(original, inflated.readByteString()); - } - - @Test public void multipleSegmentsWithoutCompression() throws IOException { - Buffer buffer = new Buffer(); - Deflater deflater = new Deflater(); - deflater.setLevel(Deflater.NO_COMPRESSION); - DeflaterSink deflaterSink = new DeflaterSink(buffer, deflater); - int byteCount = SEGMENT_SIZE * 4; - deflaterSink.write(new Buffer().writeUtf8(repeat("a", byteCount)), byteCount); - deflaterSink.close(); - assertEquals(repeat("a", byteCount), inflate(buffer).readUtf8(byteCount)); - } - - @Test public void deflateIntoNonemptySink() throws Exception { - String original = "They're moving in herds. They do move in herds."; - - // Exercise all possible offsets for the outgoing segment. - for (int i = 0; i < SEGMENT_SIZE; i++) { - Buffer data = new Buffer().writeUtf8(original); - Buffer sink = new Buffer().writeUtf8(repeat("a", i)); - - DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater()); - deflaterSink.write(data, data.size()); - deflaterSink.close(); - - sink.skip(i); - Buffer inflated = inflate(sink); - assertEquals(original, inflated.readUtf8()); - } - } - - /** - * This test deflates a single segment of without compression because that's - * the easiest way to force close() to emit a large amount of data to the - * underlying sink. - */ - @Test public void closeWithExceptionWhenWritingAndClosing() throws IOException { - MockSink mockSink = new MockSink(); - mockSink.scheduleThrow(0, new IOException("first")); - mockSink.scheduleThrow(1, new IOException("second")); - Deflater deflater = new Deflater(); - deflater.setLevel(Deflater.NO_COMPRESSION); - DeflaterSink deflaterSink = new DeflaterSink(mockSink, deflater); - deflaterSink.write(new Buffer().writeUtf8(repeat("a", SEGMENT_SIZE)), SEGMENT_SIZE); - try { - deflaterSink.close(); - fail(); - } catch (IOException expected) { - assertEquals("first", expected.getMessage()); - } - mockSink.assertLogContains("close()"); - } - - /** - * Uses streaming decompression to inflate {@code deflated}. The input must - * either be finished or have a trailing sync flush. - */ - private Buffer inflate(Buffer deflated) throws IOException { - InputStream deflatedIn = deflated.inputStream(); - Inflater inflater = new Inflater(); - InputStream inflatedIn = new InflaterInputStream(deflatedIn, inflater); - Buffer result = new Buffer(); - byte[] buffer = new byte[8192]; - while (!inflater.needsInput() || deflated.size() > 0 || deflatedIn.available() > 0) { - int count = inflatedIn.read(buffer, 0, buffer.length); - if (count != -1) { - result.write(buffer, 0, count); - } - } - return result; - } -} diff --git a/okio/src/jvmTest/java/okio/ForwardingTimeoutTest.java b/okio/src/jvmTest/java/okio/ForwardingTimeoutTest.java deleted file mode 100644 index 45536fc0..00000000 --- a/okio/src/jvmTest/java/okio/ForwardingTimeoutTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 java.util.concurrent.TimeUnit; -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class ForwardingTimeoutTest { - @Test public void getAndSetDelegate() { - Timeout timeout1 = new Timeout(); - Timeout timeout2 = new Timeout(); - - ForwardingTimeout forwardingTimeout = new ForwardingTimeout(timeout1); - forwardingTimeout.timeout(5, TimeUnit.SECONDS); - assertThat(timeout1.timeoutNanos()).isNotEqualTo(0L); - assertThat(timeout2.timeoutNanos()).isEqualTo(0L); - forwardingTimeout.clearTimeout(); - assertThat(timeout1.timeoutNanos()).isEqualTo(0L); - assertThat(timeout2.timeoutNanos()).isEqualTo(0L); - assertThat(forwardingTimeout.delegate()).isEqualTo(timeout1); - - assertThat(forwardingTimeout.setDelegate(timeout2)).isEqualTo(forwardingTimeout); - forwardingTimeout.timeout(5, TimeUnit.SECONDS); - assertThat(timeout1.timeoutNanos()).isEqualTo(0L); - assertThat(timeout2.timeoutNanos()).isNotEqualTo(0L); - forwardingTimeout.clearTimeout(); - assertThat(timeout1.timeoutNanos()).isEqualTo(0L); - assertThat(timeout2.timeoutNanos()).isEqualTo(0L); - assertThat(forwardingTimeout.delegate()).isEqualTo(timeout2); - } -} diff --git a/okio/src/jvmTest/java/okio/GzipSinkTest.java b/okio/src/jvmTest/java/okio/GzipSinkTest.java deleted file mode 100644 index 848ff02c..00000000 --- a/okio/src/jvmTest/java/okio/GzipSinkTest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 java.io.IOException; -import org.junit.Test; - -import static kotlin.text.StringsKt.repeat; -import static okio.TestUtil.SEGMENT_SIZE; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public final class GzipSinkTest { - @Test public void gzipGunzip() throws Exception { - Buffer data = new Buffer(); - String original = "It's a UNIX system! I know this!"; - data.writeUtf8(original); - Buffer sink = new Buffer(); - GzipSink gzipSink = new GzipSink(sink); - gzipSink.write(data, data.size()); - gzipSink.close(); - Buffer inflated = gunzip(sink); - assertEquals(original, inflated.readUtf8()); - } - - @Test public void closeWithExceptionWhenWritingAndClosing() throws IOException { - MockSink mockSink = new MockSink(); - mockSink.scheduleThrow(0, new IOException("first")); - mockSink.scheduleThrow(1, new IOException("second")); - GzipSink gzipSink = new GzipSink(mockSink); - gzipSink.write(new Buffer().writeUtf8(repeat("a", SEGMENT_SIZE)), SEGMENT_SIZE); - try { - gzipSink.close(); - fail(); - } catch (IOException expected) { - assertEquals("first", expected.getMessage()); - } - mockSink.assertLogContains("close()"); - } - - private Buffer gunzip(Buffer gzipped) throws IOException { - Buffer result = new Buffer(); - GzipSource source = new GzipSource(gzipped); - while (source.read(result, Integer.MAX_VALUE) != -1) { - } - return result; - } -} diff --git a/okio/src/jvmTest/java/okio/GzipSourceTest.java b/okio/src/jvmTest/java/okio/GzipSourceTest.java deleted file mode 100644 index 69b81e3f..00000000 --- a/okio/src/jvmTest/java/okio/GzipSourceTest.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 java.io.IOException; -import java.util.zip.CRC32; -import org.junit.Test; - -import static kotlin.text.Charsets.UTF_8; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public final class GzipSourceTest { - - @Test public void gunzip() throws Exception { - Buffer gzipped = new Buffer(); - gzipped.write(gzipHeader); - gzipped.write(deflated); - gzipped.write(gzipTrailer); - assertGzipped(gzipped); - } - - @Test public void gunzip_withHCRC() throws Exception { - CRC32 hcrc = new CRC32(); - ByteString gzipHeader = gzipHeaderWithFlags((byte) 0x02); - hcrc.update(gzipHeader.toByteArray()); - - Buffer gzipped = new Buffer(); - gzipped.write(gzipHeader); - gzipped.writeShort(TestUtil.reverseBytes((short) hcrc.getValue())); // little endian - gzipped.write(deflated); - gzipped.write(gzipTrailer); - assertGzipped(gzipped); - } - - @Test public void gunzip_withExtra() throws Exception { - Buffer gzipped = new Buffer(); - gzipped.write(gzipHeaderWithFlags((byte) 0x04)); - gzipped.writeShort(TestUtil.reverseBytes((short) 7)); // little endian extra length - gzipped.write("blubber".getBytes(UTF_8), 0, 7); - gzipped.write(deflated); - gzipped.write(gzipTrailer); - assertGzipped(gzipped); - } - - @Test public void gunzip_withName() throws Exception { - Buffer gzipped = new Buffer(); - gzipped.write(gzipHeaderWithFlags((byte) 0x08)); - gzipped.write("foo.txt".getBytes(UTF_8), 0, 7); - gzipped.writeByte(0); // zero-terminated - gzipped.write(deflated); - gzipped.write(gzipTrailer); - assertGzipped(gzipped); - } - - @Test public void gunzip_withComment() throws Exception { - Buffer gzipped = new Buffer(); - gzipped.write(gzipHeaderWithFlags((byte) 0x10)); - gzipped.write("rubbish".getBytes(UTF_8), 0, 7); - gzipped.writeByte(0); // zero-terminated - gzipped.write(deflated); - gzipped.write(gzipTrailer); - assertGzipped(gzipped); - } - - /** - * For portability, it is a good idea to export the gzipped bytes and try running gzip. Ex. - * {@code echo gzipped | base64 --decode | gzip -l -v} - */ - @Test public void gunzip_withAll() throws Exception { - Buffer gzipped = new Buffer(); - gzipped.write(gzipHeaderWithFlags((byte) 0x1c)); - gzipped.writeShort(TestUtil.reverseBytes((short) 7)); // little endian extra length - gzipped.write("blubber".getBytes(UTF_8), 0, 7); - gzipped.write("foo.txt".getBytes(UTF_8), 0, 7); - gzipped.writeByte(0); // zero-terminated - gzipped.write("rubbish".getBytes(UTF_8), 0, 7); - gzipped.writeByte(0); // zero-terminated - gzipped.write(deflated); - gzipped.write(gzipTrailer); - assertGzipped(gzipped); - } - - private void assertGzipped(Buffer gzipped) throws IOException { - Buffer gunzipped = gunzip(gzipped); - assertEquals("It's a UNIX system! I know this!", gunzipped.readUtf8()); - } - - /** - * Note that you cannot test this with old versions of gzip, as they interpret flag bit 1 as - * CONTINUATION, not HCRC. For example, this is the case with the default gzip on osx. - */ - @Test public void gunzipWhenHeaderCRCIncorrect() { - Buffer gzipped = new Buffer(); - gzipped.write(gzipHeaderWithFlags((byte) 0x02)); - gzipped.writeShort((short) 0); // wrong HCRC! - gzipped.write(deflated); - gzipped.write(gzipTrailer); - - try { - gunzip(gzipped); - fail(); - } catch (IOException e) { - assertEquals("FHCRC: actual 0x0000261d != expected 0x00000000", e.getMessage()); - } - } - - @Test public void gunzipWhenCRCIncorrect() { - Buffer gzipped = new Buffer(); - gzipped.write(gzipHeader); - gzipped.write(deflated); - gzipped.writeInt(TestUtil.reverseBytes(0x1234567)); // wrong CRC - gzipped.write(gzipTrailer.toByteArray(), 3, 4); - - try { - gunzip(gzipped); - fail(); - } catch (IOException e) { - assertEquals("CRC: actual 0x37ad8f8d != expected 0x01234567", e.getMessage()); - } - } - - @Test public void gunzipWhenLengthIncorrect() { - Buffer gzipped = new Buffer(); - gzipped.write(gzipHeader); - gzipped.write(deflated); - gzipped.write(gzipTrailer.toByteArray(), 0, 4); - gzipped.writeInt(TestUtil.reverseBytes(0x123456)); // wrong length - - try { - gunzip(gzipped); - fail(); - } catch (IOException e) { - assertEquals("ISIZE: actual 0x00000020 != expected 0x00123456", e.getMessage()); - } - } - - @Test public void gunzipExhaustsSource() throws Exception { - Buffer gzippedSource = new Buffer() - .write(ByteString.decodeHex("1f8b08000000000000004b4c4a0600c241243503000000")); // 'abc' - - ExhaustableSource exhaustableSource = new ExhaustableSource(gzippedSource); - BufferedSource gunzippedSource = Okio.buffer(new GzipSource(exhaustableSource)); - - assertEquals('a', gunzippedSource.readByte()); - assertEquals('b', gunzippedSource.readByte()); - assertEquals('c', gunzippedSource.readByte()); - assertFalse(exhaustableSource.exhausted); - assertEquals(-1, gunzippedSource.read(new Buffer(), 1)); - assertTrue(exhaustableSource.exhausted); - } - - @Test public void gunzipThrowsIfSourceIsNotExhausted() throws Exception { - Buffer gzippedSource = new Buffer() - .write(ByteString.decodeHex("1f8b08000000000000004b4c4a0600c241243503000000")); // 'abc' - gzippedSource.writeByte('d'); // This byte shouldn't be here! - - BufferedSource gunzippedSource = Okio.buffer(new GzipSource(gzippedSource)); - - assertEquals('a', gunzippedSource.readByte()); - assertEquals('b', gunzippedSource.readByte()); - assertEquals('c', gunzippedSource.readByte()); - try { - gunzippedSource.readByte(); - fail(); - } catch (IOException expected) { - } - } - - private ByteString gzipHeaderWithFlags(byte flags) { - byte[] result = gzipHeader.toByteArray(); - result[3] = flags; - return ByteString.of(result); - } - - private final ByteString gzipHeader = ByteString.decodeHex("1f8b0800000000000000"); - - // Deflated "It's a UNIX system! I know this!" - private final ByteString deflated = ByteString.decodeHex( - "f32c512f56485408f5f38c5028ae2c2e49cd5554f054c8cecb2f5728c9c82c560400"); - - private final ByteString gzipTrailer = ByteString.decodeHex("" - + "8d8fad37" // Checksum of deflated. - + "20000000" // 32 in little endian. - ); - - private Buffer gunzip(Buffer gzipped) throws IOException { - Buffer result = new Buffer(); - GzipSource source = new GzipSource(gzipped); - while (source.read(result, Integer.MAX_VALUE) != -1) { - } - return result; - } - - /** This source keeps track of whether its read has returned -1. */ - static class ExhaustableSource implements Source { - private final Source source; - private boolean exhausted; - - ExhaustableSource(Source source) { - this.source = source; - } - - @Override public long read(Buffer sink, long byteCount) throws IOException { - long result = source.read(sink, byteCount); - if (result == -1) exhausted = true; - return result; - } - - @Override public Timeout timeout() { - return source.timeout(); - } - - @Override public void close() throws IOException { - source.close(); - } - } -} diff --git a/okio/src/jvmTest/java/okio/InflaterSourceTest.java b/okio/src/jvmTest/java/okio/InflaterSourceTest.java deleted file mode 100644 index 0486638d..00000000 --- a/okio/src/jvmTest/java/okio/InflaterSourceTest.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 java.io.EOFException; -import java.io.IOException; -import java.util.List; -import java.util.zip.DeflaterOutputStream; -import java.util.zip.Inflater; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - -import static kotlin.text.StringsKt.repeat; -import static okio.TestUtil.SEGMENT_SIZE; -import static okio.TestUtil.randomBytes; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.junit.Assume.assumeFalse; - -@RunWith(Parameterized.class) -public final class InflaterSourceTest { - /** - * Use a parameterized test to control how many bytes the InflaterSource gets with each request - * for more bytes. - */ - @Parameters(name = "{0}") - public static List<Object[]> parameters() { - return BufferedSourceFactory.Companion.getPARAMETERIZED_TEST_VALUES(); - } - - public final BufferedSourceFactory bufferFactory; - public BufferedSink deflatedSink; - public BufferedSource deflatedSource; - - public InflaterSourceTest(BufferedSourceFactory bufferFactory) { - this.bufferFactory = bufferFactory; - resetDeflatedSourceAndSink(); - } - - private void resetDeflatedSourceAndSink() { - BufferedSourceFactory.Pipe pipe = bufferFactory.pipe(); - this.deflatedSink = pipe.getSink(); - this.deflatedSource = pipe.getSource(); - } - - @Test public void inflate() throws Exception { - decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tKtYDAF6CD5s="); - Buffer inflated = inflate(deflatedSource); - assertEquals("God help us, we're in the hands of engineers.", inflated.readUtf8()); - } - - @Test public void inflateTruncated() throws Exception { - decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tKtYDAF6CDw=="); - try { - inflate(deflatedSource); - fail(); - } catch (EOFException expected) { - } - } - - @Test public void inflateWellCompressed() throws Exception { - decodeBase64("eJztwTEBAAAAwqCs61/CEL5AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8BtFeWvE="); - String original = repeat("a", 1024 * 1024); - deflate(ByteString.encodeUtf8(original)); - Buffer inflated = inflate(deflatedSource); - assertEquals(original, inflated.readUtf8()); - } - - @Test public void inflatePoorlyCompressed() throws Exception { - assumeFalse(bufferFactory.isOneByteAtATime()); // 8 GiB for 1 byte per segment! - - ByteString original = randomBytes(1024 * 1024); - deflate(original); - Buffer inflated = inflate(deflatedSource); - assertEquals(original, inflated.readByteString()); - } - - @Test public void inflateIntoNonemptySink() throws Exception { - for (int i = 0; i < SEGMENT_SIZE; i++) { - resetDeflatedSourceAndSink(); - Buffer inflated = new Buffer().writeUtf8(repeat("a", i)); - deflate(ByteString.encodeUtf8("God help us, we're in the hands of engineers.")); - InflaterSource source = new InflaterSource(deflatedSource, new Inflater()); - while (source.read(inflated, Integer.MAX_VALUE) != -1) { - } - inflated.skip(i); - assertEquals("God help us, we're in the hands of engineers.", inflated.readUtf8()); - } - } - - @Test public void inflateSingleByte() throws Exception { - Buffer inflated = new Buffer(); - decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tKtYDAF6CD5s="); - InflaterSource source = new InflaterSource(deflatedSource, new Inflater()); - source.read(inflated, 1); - source.close(); - assertEquals("G", inflated.readUtf8()); - assertEquals(0, inflated.size()); - } - - @Test public void inflateByteCount() throws Exception { - assumeFalse(bufferFactory.isOneByteAtATime()); // This test assumes one step. - - Buffer inflated = new Buffer(); - decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tKtYDAF6CD5s="); - InflaterSource source = new InflaterSource(deflatedSource, new Inflater()); - source.read(inflated, 11); - source.close(); - assertEquals("God help us", inflated.readUtf8()); - assertEquals(0, inflated.size()); - } - - @Test public void sourceExhaustedPrematurelyOnRead() throws Exception { - // Deflate 0 bytes of data that lacks the in-stream terminator. - decodeBase64("eJwAAAD//w=="); - - Buffer inflated = new Buffer(); - Inflater inflater = new Inflater(); - InflaterSource source = new InflaterSource(deflatedSource, inflater); - assertThat(deflatedSource.exhausted()).isFalse(); - try { - source.read(inflated, Long.MAX_VALUE); - fail(); - } catch (EOFException expected) { - assertThat(expected).hasMessage("source exhausted prematurely"); - } - - // Despite the exception, the read() call made forward progress on the underlying stream! - assertThat(deflatedSource.exhausted()).isTrue(); - } - - /** - * Confirm that {@link InflaterSource#readOrInflate} consumes a byte on each call even if it - * doesn't produce a byte on every call. - */ - @Test public void readOrInflateMakesByteByByteProgress() throws Exception { - // Deflate 0 bytes of data that lacks the in-stream terminator. - decodeBase64("eJwAAAD//w=="); - int deflatedByteCount = 7; - - Buffer inflated = new Buffer(); - Inflater inflater = new Inflater(); - InflaterSource source = new InflaterSource(deflatedSource, inflater); - assertThat(deflatedSource.exhausted()).isFalse(); - - if (bufferFactory.isOneByteAtATime()) { - for (int i = 0; i < deflatedByteCount; i++) { - assertThat(inflater.getBytesRead()).isEqualTo(i); - assertThat(source.readOrInflate(inflated, Long.MAX_VALUE)).isEqualTo(0L); - } - } else { - assertThat(source.readOrInflate(inflated, Long.MAX_VALUE)).isEqualTo(0L); - } - - assertThat(inflater.getBytesRead()).isEqualTo(deflatedByteCount); - assertThat(deflatedSource.exhausted()); - } - - private void decodeBase64(String s) throws IOException { - deflatedSink.write(ByteString.decodeBase64(s)); - deflatedSink.flush(); - } - - /** Use DeflaterOutputStream to deflate source. */ - private void deflate(ByteString source) throws IOException { - Sink sink = Okio.sink(new DeflaterOutputStream(deflatedSink.outputStream())); - sink.write(new Buffer().write(source), source.size()); - sink.close(); - } - - /** Returns a new buffer containing the inflated contents of {@code deflated}. */ - private Buffer inflate(BufferedSource deflated) throws IOException { - Buffer result = new Buffer(); - InflaterSource source = new InflaterSource(deflated, new Inflater()); - while (source.read(result, Integer.MAX_VALUE) != -1) { - } - return result; - } -} diff --git a/okio/src/jvmTest/java/okio/LargeStreamsTest.java b/okio/src/jvmTest/java/okio/LargeStreamsTest.java deleted file mode 100644 index b9be1a2e..00000000 --- a/okio/src/jvmTest/java/okio/LargeStreamsTest.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2016 Square, Inc. - * - * 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 java.io.IOException; -import java.io.OutputStream; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.zip.Deflater; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; -import org.junit.Test; - -import static okio.TestUtil.SEGMENT_SIZE; -import static okio.TestUtil.randomSource; -import static org.junit.Assert.assertEquals; - -/** Slow running tests that run a large amount of data through a stream. */ -public final class LargeStreamsTest { - /** 4 GiB plus 1 byte. This is greater than what can be expressed in an unsigned int. */ - public static final long FOUR_GIB_PLUS_ONE = 0x100000001L; - - /** SHA-256 of {@code TestUtil.randomSource(FOUR_GIB_PLUS_ONE)}. */ - public static final ByteString SHA256_RANDOM_FOUR_GIB_PLUS_1 = ByteString.decodeHex( - "9654947a655c5efc445502fd1bf11117d894b7812b7974fde8ca4a02c5066315"); - - @Test public void test() throws Exception { - Pipe pipe = new Pipe(1024 * 1024); - - Future<Long> future = readAllAndCloseAsync(randomSource(FOUR_GIB_PLUS_ONE), pipe.sink()); - - HashingSink hashingSink = HashingSink.sha256(Okio.blackhole()); - readAllAndClose(pipe.source(), hashingSink); - - assertEquals(FOUR_GIB_PLUS_ONE, (long) future.get()); - assertEquals(SHA256_RANDOM_FOUR_GIB_PLUS_1, hashingSink.hash()); - } - - /** Note that this test hangs on Android. */ - @Test public void gzipSource() throws Exception { - Pipe pipe = new Pipe(1024 * 1024); - - OutputStream gzipOut = new GZIPOutputStream(Okio.buffer(pipe.sink()).outputStream()) { - { - // Disable compression to speed up a slow test. Improved from 141s to 33s on one machine. - def.setLevel(Deflater.NO_COMPRESSION); - } - }; - Future<Long> future = readAllAndCloseAsync( - randomSource(FOUR_GIB_PLUS_ONE), Okio.sink(gzipOut)); - - HashingSink hashingSink = HashingSink.sha256(Okio.blackhole()); - GzipSource gzipSource = new GzipSource(pipe.source()); - readAllAndClose(gzipSource, hashingSink); - - assertEquals(FOUR_GIB_PLUS_ONE, (long) future.get()); - assertEquals(SHA256_RANDOM_FOUR_GIB_PLUS_1, hashingSink.hash()); - } - - /** Note that this test hangs on Android. */ - @Test public void gzipSink() throws Exception { - Pipe pipe = new Pipe(1024 * 1024); - - GzipSink gzipSink = new GzipSink(pipe.sink()); - - // Disable compression to speed up a slow test. Improved from 141s to 35s on one machine. - gzipSink.deflater().setLevel(Deflater.NO_COMPRESSION); - Future<Long> future = readAllAndCloseAsync(randomSource(FOUR_GIB_PLUS_ONE), gzipSink); - - HashingSink hashingSink = HashingSink.sha256(Okio.blackhole()); - GZIPInputStream gzipIn = new GZIPInputStream(Okio.buffer(pipe.source()).inputStream()); - readAllAndClose(Okio.source(gzipIn), hashingSink); - - assertEquals(FOUR_GIB_PLUS_ONE, (long) future.get()); - assertEquals(SHA256_RANDOM_FOUR_GIB_PLUS_1, hashingSink.hash()); - } - - /** Reads all bytes from {@code source} and writes them to {@code sink}. */ - private Long readAllAndClose(Source source, Sink sink) throws IOException { - long result = 0L; - Buffer buffer = new Buffer(); - for (long count; (count = source.read(buffer, SEGMENT_SIZE)) != -1L; result += count) { - sink.write(buffer, count); - } - source.close(); - sink.close(); - return result; - } - - /** Calls {@link #readAllAndClose} on a background thread. */ - private Future<Long> readAllAndCloseAsync(final Source source, final Sink sink) { - ExecutorService executor = Executors.newSingleThreadExecutor(); - try { - return executor.submit(new Callable<Long>() { - @Override public Long call() throws Exception { - return readAllAndClose(source, sink); - } - }); - } finally { - executor.shutdown(); - } - } -} diff --git a/okio/src/jvmTest/java/okio/NioTest.java b/okio/src/jvmTest/java/okio/NioTest.java deleted file mode 100644 index aec17733..00000000 --- a/okio/src/jvmTest/java/okio/NioTest.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 java.io.File; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; -import java.nio.file.StandardOpenOption; -import kotlin.text.Charsets; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -import static junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertFalse; -import static junit.framework.TestCase.assertTrue; - -/** Test interop between our beloved Okio and java.nio. */ -public final class NioTest { - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test public void sourceIsOpen() throws Exception { - BufferedSource source = Okio.buffer((Source) new Buffer()); - assertTrue(source.isOpen()); - source.close(); - assertFalse(source.isOpen()); - } - - @Test public void sinkIsOpen() throws Exception { - BufferedSink sink = Okio.buffer((Sink) new Buffer()); - assertTrue(sink.isOpen()); - sink.close(); - assertFalse(sink.isOpen()); - } - - @Test public void writableChannelNioFile() throws Exception { - File file = temporaryFolder.newFile(); - FileChannel fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.WRITE); - testWritableByteChannel(fileChannel); - - BufferedSource emitted = Okio.buffer(Okio.source(file)); - assertEquals("defghijklmnopqrstuvw", emitted.readUtf8()); - emitted.close(); - } - - @Test public void writableChannelBuffer() throws Exception { - Buffer buffer = new Buffer(); - testWritableByteChannel(buffer); - assertEquals("defghijklmnopqrstuvw", buffer.readUtf8()); - } - - @Test public void writableChannelBufferedSink() throws Exception { - Buffer buffer = new Buffer(); - BufferedSink bufferedSink = Okio.buffer((Sink) buffer); - testWritableByteChannel(bufferedSink); - assertEquals("defghijklmnopqrstuvw", buffer.readUtf8()); - } - - @Test public void readableChannelNioFile() throws Exception { - File file = temporaryFolder.newFile(); - - BufferedSink initialData = Okio.buffer(Okio.sink(file)); - initialData.writeUtf8("abcdefghijklmnopqrstuvwxyz"); - initialData.close(); - - FileChannel fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.READ); - testReadableByteChannel(fileChannel); - } - - @Test public void readableChannelBuffer() throws Exception { - Buffer buffer = new Buffer(); - buffer.writeUtf8("abcdefghijklmnopqrstuvwxyz"); - - testReadableByteChannel(buffer); - } - - @Test public void readableChannelBufferedSource() throws Exception { - Buffer buffer = new Buffer(); - BufferedSource bufferedSource = Okio.buffer((Source) buffer); - buffer.writeUtf8("abcdefghijklmnopqrstuvwxyz"); - - testReadableByteChannel(bufferedSource); - } - - /** - * Does some basic writes to {@code channel}. We execute this against both Okio's channels and - * also a standard implementation from the JDK to confirm that their behavior is consistent. - */ - private void testWritableByteChannel(WritableByteChannel channel) throws Exception { - assertTrue(channel.isOpen()); - - ByteBuffer byteBuffer = ByteBuffer.allocate(1024); - byteBuffer.put("abcdefghijklmnopqrstuvwxyz".getBytes(Charsets.UTF_8)); - byteBuffer.flip(); - byteBuffer.position(3); - byteBuffer.limit(23); - - int byteCount = channel.write(byteBuffer); - assertEquals(20, byteCount); - assertEquals(23, byteBuffer.position()); - assertEquals(23, byteBuffer.limit()); - - channel.close(); - assertEquals(channel instanceof Buffer, channel.isOpen()); // Buffer.close() does nothing. - } - - /** - * Does some basic reads from {@code channel}. We execute this against both Okio's channels and - * also a standard implementation from the JDK to confirm that their behavior is consistent. - */ - private void testReadableByteChannel(ReadableByteChannel channel) throws Exception { - assertTrue(channel.isOpen()); - - ByteBuffer byteBuffer = ByteBuffer.allocate(1024); - byteBuffer.position(3); - byteBuffer.limit(23); - - int byteCount = channel.read(byteBuffer); - assertEquals(20, byteCount); - assertEquals(23, byteBuffer.position()); - assertEquals(23, byteBuffer.limit()); - - channel.close(); - assertEquals(channel instanceof Buffer, channel.isOpen()); // Buffer.close() does nothing. - - byteBuffer.flip(); - byteBuffer.position(3); - byte[] data = new byte[byteBuffer.remaining()]; - byteBuffer.get(data); - assertEquals("abcdefghijklmnopqrst", new String(data, Charsets.UTF_8)); - } -} diff --git a/okio/src/jvmTest/java/okio/OkioTest.java b/okio/src/jvmTest/java/okio/OkioTest.java deleted file mode 100644 index 71a44470..00000000 --- a/okio/src/jvmTest/java/okio/OkioTest.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -import static kotlin.text.Charsets.UTF_8; -import static kotlin.text.StringsKt.repeat; -import static okio.TestUtil.SEGMENT_SIZE; -import static okio.TestUtil.assertNoEmptySegments; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public final class OkioTest { - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test public void readWriteFile() throws Exception { - File file = temporaryFolder.newFile(); - - BufferedSink sink = Okio.buffer(Okio.sink(file)); - sink.writeUtf8("Hello, java.io file!"); - sink.close(); - assertTrue(file.exists()); - assertEquals(20, file.length()); - - BufferedSource source = Okio.buffer(Okio.source(file)); - assertEquals("Hello, java.io file!", source.readUtf8()); - source.close(); - } - - @Test public void appendFile() throws Exception { - File file = temporaryFolder.newFile(); - - BufferedSink sink = Okio.buffer(Okio.appendingSink(file)); - sink.writeUtf8("Hello, "); - sink.close(); - assertTrue(file.exists()); - assertEquals(7, file.length()); - - sink = Okio.buffer(Okio.appendingSink(file)); - sink.writeUtf8("java.io file!"); - sink.close(); - assertEquals(20, file.length()); - - BufferedSource source = Okio.buffer(Okio.source(file)); - assertEquals("Hello, java.io file!", source.readUtf8()); - source.close(); - } - - @Test public void readWritePath() throws Exception { - Path path = temporaryFolder.newFile().toPath(); - - BufferedSink sink = Okio.buffer(Okio.sink(path)); - sink.writeUtf8("Hello, java.nio file!"); - sink.close(); - assertTrue(Files.exists(path)); - assertEquals(21, Files.size(path)); - - BufferedSource source = Okio.buffer(Okio.source(path)); - assertEquals("Hello, java.nio file!", source.readUtf8()); - source.close(); - } - - @Test public void sinkFromOutputStream() throws Exception { - Buffer data = new Buffer(); - data.writeUtf8("a"); - data.writeUtf8(repeat("b", 9998)); - data.writeUtf8("c"); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Sink sink = Okio.sink(out); - sink.write(data, 3); - assertEquals("abb", out.toString("UTF-8")); - sink.write(data, data.size()); - assertEquals("a" + repeat("b", 9998) + "c", out.toString("UTF-8")); - } - - @Test public void sourceFromInputStream() throws Exception { - InputStream in = new ByteArrayInputStream( - ("a" + repeat("b", SEGMENT_SIZE * 2) + "c").getBytes(UTF_8)); - - // Source: ab...bc - Source source = Okio.source(in); - Buffer sink = new Buffer(); - - // Source: b...bc. Sink: abb. - assertEquals(3, source.read(sink, 3)); - assertEquals("abb", sink.readUtf8(3)); - - // Source: b...bc. Sink: b...b. - assertEquals(SEGMENT_SIZE, source.read(sink, 20000)); - assertEquals(repeat("b", SEGMENT_SIZE), sink.readUtf8()); - - // Source: b...bc. Sink: b...bc. - assertEquals(SEGMENT_SIZE - 1, source.read(sink, 20000)); - assertEquals(repeat("b", SEGMENT_SIZE - 2) + "c", sink.readUtf8()); - - // Source and sink are empty. - assertEquals(-1, source.read(sink, 1)); - } - - @Test public void sourceFromInputStreamWithSegmentSize() throws Exception { - InputStream in = new ByteArrayInputStream(new byte[SEGMENT_SIZE]); - Source source = Okio.source(in); - Buffer sink = new Buffer(); - - assertEquals(SEGMENT_SIZE, source.read(sink, SEGMENT_SIZE)); - assertEquals(-1, source.read(sink, SEGMENT_SIZE)); - - assertNoEmptySegments(sink); - } - - @Test public void sourceFromInputStreamBounds() throws Exception { - Source source = Okio.source(new ByteArrayInputStream(new byte[100])); - try { - source.read(new Buffer(), -1); - fail(); - } catch (IllegalArgumentException expected) { - } - } - - @Test public void bufferSinkThrowsOnNull() { - try { - Okio.buffer((Sink) null); - fail(); - } catch (NullPointerException expected) { - } - } - - @Test public void bufferSourceThrowsOnNull() { - try { - Okio.buffer((Source) null); - fail(); - } catch (NullPointerException expected) { - } - } - - @Test public void blackhole() throws Exception { - Buffer data = new Buffer(); - data.writeUtf8("blackhole"); - - Sink blackhole = Okio.blackhole(); - blackhole.write(data, 5); - - assertEquals("hole", data.readUtf8()); - } -} diff --git a/okio/src/jvmTest/java/okio/PipeTest.java b/okio/src/jvmTest/java/okio/PipeTest.java deleted file mode 100644 index 030e6ba0..00000000 --- a/okio/src/jvmTest/java/okio/PipeTest.java +++ /dev/null @@ -1,376 +0,0 @@ -/* - * Copyright (C) 2016 Square, Inc. - * - * 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 java.io.IOException; -import java.io.InterruptedIOException; -import java.util.Random; -import java.util.concurrent.Callable; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import org.junit.After; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public final class PipeTest { - final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2); - - @After public void tearDown() throws Exception { - executorService.shutdown(); - } - - @Test public void test() throws Exception { - Pipe pipe = new Pipe(6); - pipe.sink().write(new Buffer().writeUtf8("abc"), 3L); - - Source source = pipe.source(); - Buffer readBuffer = new Buffer(); - assertEquals(3L, source.read(readBuffer, 6L)); - assertEquals("abc", readBuffer.readUtf8()); - - pipe.sink().close(); - assertEquals(-1L, source.read(readBuffer, 6L)); - - source.close(); - } - - /** - * A producer writes the first 16 MiB of bytes generated by {@code new Random(0)} to a sink, and a - * consumer consumes them. Both compute hashes of their data to confirm that they're as expected. - */ - @Test public void largeDataset() throws Exception { - final Pipe pipe = new Pipe(1000L); // An awkward size to force producer/consumer exchange. - final long totalBytes = 16L * 1024L * 1024L; - ByteString expectedHash = ByteString.decodeHex("7c3b224bea749086babe079360cf29f98d88262d"); - - // Write data to the sink. - Future<ByteString> sinkHash = executorService.submit(new Callable<ByteString>() { - @Override public ByteString call() throws Exception { - HashingSink hashingSink = HashingSink.sha1(pipe.sink()); - Random random = new Random(0); - byte[] data = new byte[8192]; - - Buffer buffer = new Buffer(); - for (long i = 0L; i < totalBytes; i += data.length) { - random.nextBytes(data); - buffer.write(data); - hashingSink.write(buffer, buffer.size()); - } - - hashingSink.close(); - return hashingSink.hash(); - } - }); - - // Read data from the source. - Future<ByteString> sourceHash = executorService.submit(new Callable<ByteString>() { - @Override public ByteString call() throws Exception { - Buffer blackhole = new Buffer(); - HashingSink hashingSink = HashingSink.sha1(blackhole); - - Buffer buffer = new Buffer(); - while (pipe.source().read(buffer, Long.MAX_VALUE) != -1) { - hashingSink.write(buffer, buffer.size()); - blackhole.clear(); - } - - pipe.source().close(); - return hashingSink.hash(); - } - }); - - assertEquals(expectedHash, sinkHash.get()); - assertEquals(expectedHash, sourceHash.get()); - } - - @Test public void sinkTimeout() throws Exception { - TestUtil.INSTANCE.assumeNotWindows(); - - Pipe pipe = new Pipe(3); - pipe.sink().timeout().timeout(1000, TimeUnit.MILLISECONDS); - pipe.sink().write(new Buffer().writeUtf8("abc"), 3L); - double start = now(); - try { - pipe.sink().write(new Buffer().writeUtf8("def"), 3L); - fail(); - } catch (InterruptedIOException expected) { - assertEquals("timeout", expected.getMessage()); - } - assertElapsed(1000.0, start); - - Buffer readBuffer = new Buffer(); - assertEquals(3L, pipe.source().read(readBuffer, 6L)); - assertEquals("abc", readBuffer.readUtf8()); - } - - @Test public void sourceTimeout() throws Exception { - TestUtil.INSTANCE.assumeNotWindows(); - - Pipe pipe = new Pipe(3L); - pipe.source().timeout().timeout(1000, TimeUnit.MILLISECONDS); - double start = now(); - Buffer readBuffer = new Buffer(); - try { - pipe.source().read(readBuffer, 6L); - fail(); - } catch (InterruptedIOException expected) { - assertEquals("timeout", expected.getMessage()); - } - assertElapsed(1000.0, start); - assertEquals(0, readBuffer.size()); - } - - /** - * The writer is writing 12 bytes as fast as it can to a 3 byte buffer. The reader alternates - * sleeping 1000 ms, then reading 3 bytes. That should make for an approximate timeline like - * this: - * - * 0: writer writes 'abc', blocks 0: reader sleeps until 1000 - * 1000: reader reads 'abc', sleeps until 2000 - * 1000: writer writes 'def', blocks - * 2000: reader reads 'def', sleeps until 3000 - * 2000: writer writes 'ghi', blocks - * 3000: reader reads 'ghi', sleeps until 4000 - * 3000: writer writes 'jkl', returns - * 4000: reader reads 'jkl', returns - * - * Because the writer is writing to a buffer, it finishes before the reader does. - */ - @Test public void sinkBlocksOnSlowReader() throws Exception { - final Pipe pipe = new Pipe(3L); - executorService.execute(new Runnable() { - @Override public void run() { - try { - Buffer buffer = new Buffer(); - Thread.sleep(1000L); - assertEquals(3, pipe.source().read(buffer, Long.MAX_VALUE)); - assertEquals("abc", buffer.readUtf8()); - Thread.sleep(1000L); - assertEquals(3, pipe.source().read(buffer, Long.MAX_VALUE)); - assertEquals("def", buffer.readUtf8()); - Thread.sleep(1000L); - assertEquals(3, pipe.source().read(buffer, Long.MAX_VALUE)); - assertEquals("ghi", buffer.readUtf8()); - Thread.sleep(1000L); - assertEquals(3, pipe.source().read(buffer, Long.MAX_VALUE)); - assertEquals("jkl", buffer.readUtf8()); - } catch (IOException | InterruptedException e) { - throw new AssertionError(); - } - } - }); - - double start = now(); - pipe.sink().write(new Buffer().writeUtf8("abcdefghijkl"), 12); - assertElapsed(3000.0, start); - } - - @Test public void sinkWriteFailsByClosedReader() throws Exception { - final Pipe pipe = new Pipe(3L); - executorService.schedule(new Runnable() { - @Override public void run() { - try { - pipe.source().close(); - } catch (IOException e) { - throw new AssertionError(); - } - } - }, 1000, TimeUnit.MILLISECONDS); - - double start = now(); - try { - pipe.sink().write(new Buffer().writeUtf8("abcdef"), 6); - fail(); - } catch (IOException expected) { - assertEquals("source is closed", expected.getMessage()); - assertElapsed(1000.0, start); - } - } - - @Test public void sinkFlushDoesntWaitForReader() throws Exception { - Pipe pipe = new Pipe(100L); - pipe.sink().write(new Buffer().writeUtf8("abc"), 3); - pipe.sink().flush(); - - BufferedSource bufferedSource = Okio.buffer(pipe.source()); - assertEquals("abc", bufferedSource.readUtf8(3)); - } - - @Test public void sinkFlushFailsIfReaderIsClosedBeforeAllDataIsRead() throws Exception { - Pipe pipe = new Pipe(100L); - pipe.sink().write(new Buffer().writeUtf8("abc"), 3); - pipe.source().close(); - try { - pipe.sink().flush(); - fail(); - } catch (IOException expected) { - assertEquals("source is closed", expected.getMessage()); - } - } - - @Test public void sinkCloseFailsIfReaderIsClosedBeforeAllDataIsRead() throws Exception { - Pipe pipe = new Pipe(100L); - pipe.sink().write(new Buffer().writeUtf8("abc"), 3); - pipe.source().close(); - try { - pipe.sink().close(); - fail(); - } catch (IOException expected) { - assertEquals("source is closed", expected.getMessage()); - } - } - - @Test public void sinkClose() throws Exception { - Pipe pipe = new Pipe(100L); - pipe.sink().close(); - try { - pipe.sink().write(new Buffer().writeUtf8("abc"), 3); - fail(); - } catch (IllegalStateException expected) { - assertEquals("closed", expected.getMessage()); - } - try { - pipe.sink().flush(); - fail(); - } catch (IllegalStateException expected) { - assertEquals("closed", expected.getMessage()); - } - } - - @Test public void sinkMultipleClose() throws Exception { - Pipe pipe = new Pipe(100L); - pipe.sink().close(); - pipe.sink().close(); - } - - @Test public void sinkCloseDoesntWaitForSourceRead() throws Exception { - Pipe pipe = new Pipe(100L); - pipe.sink().write(new Buffer().writeUtf8("abc"), 3); - pipe.sink().close(); - - BufferedSource bufferedSource = Okio.buffer(pipe.source()); - assertEquals("abc", bufferedSource.readUtf8()); - assertTrue(bufferedSource.exhausted()); - } - - @Test public void sourceClose() throws Exception { - Pipe pipe = new Pipe(100L); - pipe.source().close(); - try { - pipe.source().read(new Buffer(), 3); - fail(); - } catch (IllegalStateException expected) { - assertEquals("closed", expected.getMessage()); - } - } - - @Test public void sourceMultipleClose() throws Exception { - Pipe pipe = new Pipe(100L); - pipe.source().close(); - pipe.source().close(); - } - - @Test public void sourceReadUnblockedByClosedSink() throws Exception { - final Pipe pipe = new Pipe(3L); - executorService.schedule(new Runnable() { - @Override public void run() { - try { - pipe.sink().close(); - } catch (IOException e) { - throw new AssertionError(); - } - } - }, 1000, TimeUnit.MILLISECONDS); - - double start = now(); - Buffer readBuffer = new Buffer(); - assertEquals(-1, pipe.source().read(readBuffer, Long.MAX_VALUE)); - assertEquals(0, readBuffer.size()); - assertElapsed(1000.0, start); - } - - /** - * The writer has 12 bytes to write. It alternates sleeping 1000 ms, then writing 3 bytes. The - * reader is reading as fast as it can. That should make for an approximate timeline like this: - * - * 0: writer sleeps until 1000 - * 0: reader blocks - * 1000: writer writes 'abc', sleeps until 2000 - * 1000: reader reads 'abc' - * 2000: writer writes 'def', sleeps until 3000 - * 2000: reader reads 'def' - * 3000: writer writes 'ghi', sleeps until 4000 - * 3000: reader reads 'ghi' - * 4000: writer writes 'jkl', returns - * 4000: reader reads 'jkl', returns - */ - @Test public void sourceBlocksOnSlowWriter() throws Exception { - final Pipe pipe = new Pipe(100L); - executorService.execute(new Runnable() { - @Override public void run() { - try { - Thread.sleep(1000L); - pipe.sink().write(new Buffer().writeUtf8("abc"), 3); - Thread.sleep(1000L); - pipe.sink().write(new Buffer().writeUtf8("def"), 3); - Thread.sleep(1000L); - pipe.sink().write(new Buffer().writeUtf8("ghi"), 3); - Thread.sleep(1000L); - pipe.sink().write(new Buffer().writeUtf8("jkl"), 3); - } catch (IOException | InterruptedException e) { - throw new AssertionError(); - } - } - }); - - double start = now(); - Buffer readBuffer = new Buffer(); - - assertEquals(3, pipe.source().read(readBuffer, Long.MAX_VALUE)); - assertEquals("abc", readBuffer.readUtf8()); - assertElapsed(1000.0, start); - - assertEquals(3, pipe.source().read(readBuffer, Long.MAX_VALUE)); - assertEquals("def", readBuffer.readUtf8()); - assertElapsed(2000.0, start); - - assertEquals(3, pipe.source().read(readBuffer, Long.MAX_VALUE)); - assertEquals("ghi", readBuffer.readUtf8()); - assertElapsed(3000.0, start); - - assertEquals(3, pipe.source().read(readBuffer, Long.MAX_VALUE)); - assertEquals("jkl", readBuffer.readUtf8()); - assertElapsed(4000.0, start); - } - - /** Returns the nanotime in milliseconds as a double for measuring timeouts. */ - private double now() { - return System.nanoTime() / 1000000.0d; - } - - /** - * Fails the test unless the time from start until now is duration, accepting differences in - * -50..+450 milliseconds. - */ - private void assertElapsed(double duration, double start) { - assertEquals(duration, now() - start - 200d, 250.0); - } -} diff --git a/okio/src/jvmTest/java/okio/ReadUtf8LineTest.java b/okio/src/jvmTest/java/okio/ReadUtf8LineTest.java deleted file mode 100644 index 9cc177f1..00000000 --- a/okio/src/jvmTest/java/okio/ReadUtf8LineTest.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 java.io.EOFException; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import static kotlin.text.StringsKt.repeat; -import static okio.TestUtil.SEGMENT_SIZE; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -@RunWith(Parameterized.class) -public final class ReadUtf8LineTest { - private interface Factory { - BufferedSource create(Buffer data); - } - - @Parameterized.Parameters(name = "{0}") - public static List<Object[]> parameters() { - return Arrays.asList( - new Object[] { new Factory() { - @Override public BufferedSource create(Buffer data) { - return data; - } - - @Override public String toString() { - return "Buffer"; - } - }}, - new Object[] { new Factory() { - @Override public BufferedSource create(Buffer data) { - return new RealBufferedSource(data); - } - - @Override public String toString() { - return "RealBufferedSource"; - } - }}, - new Object[] { new Factory() { - @Override public BufferedSource create(Buffer data) { - return new RealBufferedSource(new ForwardingSource(data) { - @Override public long read(Buffer sink, long byteCount) throws IOException { - return super.read(sink, Math.min(1, byteCount)); - } - }); - } - - @Override public String toString() { - return "Slow RealBufferedSource"; - } - }} - ); - } - - @Parameterized.Parameter - public Factory factory; - - private Buffer data; - private BufferedSource source; - - @Before public void setUp() { - data = new Buffer(); - source = factory.create(data); - } - - @Test public void readLines() throws IOException { - data.writeUtf8("abc\ndef\n"); - assertEquals("abc", source.readUtf8LineStrict()); - assertEquals("def", source.readUtf8LineStrict()); - try { - source.readUtf8LineStrict(); - fail(); - } catch (EOFException expected) { - assertEquals("\\n not found: limit=0 content=…", expected.getMessage()); - } - } - - @Test public void readUtf8LineStrictWithLimits() throws IOException { - int[] lens = {1, SEGMENT_SIZE - 2, SEGMENT_SIZE - 1, SEGMENT_SIZE, SEGMENT_SIZE * 10}; - for (int len : lens) { - data.writeUtf8(repeat("a", len)).writeUtf8("\n"); - assertEquals(len, source.readUtf8LineStrict(len).length()); - source.readUtf8(); - - data.writeUtf8(repeat("a", len)).writeUtf8("\n").writeUtf8(repeat("a", len)); - assertEquals(len, source.readUtf8LineStrict(len).length()); - source.readUtf8(); - - data.writeUtf8(repeat("a", len)).writeUtf8("\r\n"); - assertEquals(len, source.readUtf8LineStrict(len).length()); - source.readUtf8(); - - data.writeUtf8(repeat("a", len)).writeUtf8("\r\n").writeUtf8(repeat("a", len)); - assertEquals(len, source.readUtf8LineStrict(len).length()); - source.readUtf8(); - } - } - - @Test public void readUtf8LineStrictNoBytesConsumedOnFailure() throws IOException { - data.writeUtf8("abc\n"); - try { - source.readUtf8LineStrict(2); - fail(); - } catch (EOFException expected) { - assertTrue(expected.getMessage().startsWith("\\n not found: limit=2 content=61626")); - } - assertEquals("abc", source.readUtf8LineStrict(3)); - } - - @Test public void readUtf8LineStrictEmptyString() throws IOException { - data.writeUtf8("\r\nabc"); - assertEquals("", source.readUtf8LineStrict(0)); - assertEquals("abc", source.readUtf8()); - } - - @Test public void readUtf8LineStrictNonPositive() throws IOException { - data.writeUtf8("\r\n"); - try { - source.readUtf8LineStrict(-1); - fail("Expected failure: limit must be greater than 0"); - } catch (IllegalArgumentException expected) { - } - } - - @Test public void eofExceptionProvidesLimitedContent() throws IOException { - data.writeUtf8("aaaaaaaabbbbbbbbccccccccdddddddde"); - try { - source.readUtf8LineStrict(); - fail(); - } catch (EOFException expected) { - assertEquals("\\n not found: limit=33 content=616161616161616162626262626262626363636363636363" - + "6464646464646464…", expected.getMessage()); - } - } - - @Test public void newlineAtEnd() throws IOException { - data.writeUtf8("abc\n"); - assertEquals("abc", source.readUtf8LineStrict(3)); - assertTrue(source.exhausted()); - - data.writeUtf8("abc\r\n"); - assertEquals("abc", source.readUtf8LineStrict(3)); - assertTrue(source.exhausted()); - - data.writeUtf8("abc\r"); - try { - source.readUtf8LineStrict(3); - fail(); - } catch (EOFException expected) { - assertEquals("\\n not found: limit=3 content=6162630d…", expected.getMessage()); - } - source.readUtf8(); - - data.writeUtf8("abc"); - try { - source.readUtf8LineStrict(3); - fail(); - } catch (EOFException expected) { - assertEquals("\\n not found: limit=3 content=616263…", expected.getMessage()); - } - } - - @Test public void emptyLines() throws IOException { - data.writeUtf8("\n\n\n"); - assertEquals("", source.readUtf8LineStrict()); - assertEquals("", source.readUtf8LineStrict()); - assertEquals("", source.readUtf8LineStrict()); - assertTrue(source.exhausted()); - } - - @Test public void crDroppedPrecedingLf() throws IOException { - data.writeUtf8("abc\r\ndef\r\nghi\rjkl\r\n"); - assertEquals("abc", source.readUtf8LineStrict()); - assertEquals("def", source.readUtf8LineStrict()); - assertEquals("ghi\rjkl", source.readUtf8LineStrict()); - } - - @Test public void bufferedReaderCompatible() throws IOException { - data.writeUtf8("abc\ndef"); - assertEquals("abc", source.readUtf8Line()); - assertEquals("def", source.readUtf8Line()); - assertEquals(null, source.readUtf8Line()); - } - - @Test public void bufferedReaderCompatibleWithTrailingNewline() throws IOException { - data.writeUtf8("abc\ndef\n"); - assertEquals("abc", source.readUtf8Line()); - assertEquals("def", source.readUtf8Line()); - assertEquals(null, source.readUtf8Line()); - } -} diff --git a/okio/src/jvmTest/java/okio/SocketTimeoutTest.java b/okio/src/jvmTest/java/okio/SocketTimeoutTest.java deleted file mode 100644 index 6a6aadcd..00000000 --- a/okio/src/jvmTest/java/okio/SocketTimeoutTest.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketTimeoutException; -import java.util.concurrent.TimeUnit; -import org.junit.Test; - -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public final class SocketTimeoutTest { - - // The size of the socket buffers to use. Less than half the data transferred during tests to - // ensure send and receive buffers are flooded and any necessary blocking behavior takes place. - private static final int SOCKET_BUFFER_SIZE = 256 * 1024; - private static final int ONE_MB = 1024 * 1024; - - @Test public void readWithoutTimeout() throws Exception { - Socket socket = socket(ONE_MB, 0); - BufferedSource source = Okio.buffer(Okio.source(socket)); - source.timeout().timeout(5000, TimeUnit.MILLISECONDS); - source.require(ONE_MB); - socket.close(); - } - - @Test public void readWithTimeout() throws Exception { - Socket socket = socket(0, 0); - BufferedSource source = Okio.buffer(Okio.source(socket)); - source.timeout().timeout(250, TimeUnit.MILLISECONDS); - try { - source.require(ONE_MB); - fail(); - } catch (SocketTimeoutException expected) { - } - socket.close(); - } - - @Test public void writeWithoutTimeout() throws Exception { - Socket socket = socket(0, ONE_MB); - Sink sink = Okio.buffer(Okio.sink(socket)); - sink.timeout().timeout(500, TimeUnit.MILLISECONDS); - byte[] data = new byte[ONE_MB]; - sink.write(new Buffer().write(data), data.length); - sink.flush(); - socket.close(); - } - - @Test public void writeWithTimeout() throws Exception { - Socket socket = socket(0, 0); - Sink sink = Okio.sink(socket); - sink.timeout().timeout(500, TimeUnit.MILLISECONDS); - byte[] data = new byte[ONE_MB]; - long start = System.nanoTime(); - try { - sink.write(new Buffer().write(data), data.length); - sink.flush(); - fail(); - } catch (SocketTimeoutException expected) { - } - long elapsed = System.nanoTime() - start; - socket.close(); - - assertTrue("elapsed: " + elapsed, TimeUnit.NANOSECONDS.toMillis(elapsed) >= 500); - assertTrue("elapsed: " + elapsed, TimeUnit.NANOSECONDS.toMillis(elapsed) <= 750); - } - - /** - * Returns a socket that can read {@code readableByteCount} incoming bytes and - * will accept {@code writableByteCount} written bytes. The socket will idle - * for 5 seconds when the required data has been read and written. - */ - static Socket socket(final int readableByteCount, final int writableByteCount) throws IOException { - final ServerSocket serverSocket = new ServerSocket(0); - serverSocket.setReuseAddress(true); - serverSocket.setReceiveBufferSize(SOCKET_BUFFER_SIZE); - - Thread peer = new Thread("peer") { - @Override public void run() { - Socket socket = null; - try { - socket = serverSocket.accept(); - socket.setSendBufferSize(SOCKET_BUFFER_SIZE); - writeFully(socket.getOutputStream(), readableByteCount); - readFully(socket.getInputStream(), writableByteCount); - Thread.sleep(5000); // Sleep 5 seconds so the peer can close the connection. - } catch (Exception ignored) { - } finally { - try { - if (socket != null) socket.close(); - } catch (IOException ignored) { - } - } - } - }; - peer.start(); - - Socket socket = new Socket(serverSocket.getInetAddress(), serverSocket.getLocalPort()); - socket.setReceiveBufferSize(SOCKET_BUFFER_SIZE); - socket.setSendBufferSize(SOCKET_BUFFER_SIZE); - return socket; - } - - private static void writeFully(OutputStream out, int byteCount) throws IOException { - out.write(new byte[byteCount]); - out.flush(); - } - - private static byte[] readFully(InputStream in, int byteCount) throws IOException { - int count = 0; - byte[] result = new byte[byteCount]; - while (count < byteCount) { - int read = in.read(result, count, result.length - count); - if (read == -1) throw new EOFException(); - count += read; - } - return result; - } -} diff --git a/okio/src/jvmTest/java/okio/Utf8Test.java b/okio/src/jvmTest/java/okio/Utf8Test.java deleted file mode 100644 index 63e0b7d4..00000000 --- a/okio/src/jvmTest/java/okio/Utf8Test.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 java.io.EOFException; -import kotlin.text.Charsets; -import org.junit.Test; - -import static kotlin.text.StringsKt.repeat; -import static okio.TestUtil.REPLACEMENT_CODE_POINT; -import static okio.TestUtil.SEGMENT_SIZE; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public final class Utf8Test { - @Test public void oneByteCharacters() throws Exception { - assertEncoded("00", 0x00); // Smallest 1-byte character. - assertEncoded("20", ' '); - assertEncoded("7e", '~'); - assertEncoded("7f", 0x7f); // Largest 1-byte character. - } - - @Test public void twoByteCharacters() throws Exception { - assertEncoded("c280", 0x0080); // Smallest 2-byte character. - assertEncoded("c3bf", 0x00ff); - assertEncoded("c480", 0x0100); - assertEncoded("dfbf", 0x07ff); // Largest 2-byte character. - } - - @Test public void threeByteCharacters() throws Exception { - assertEncoded("e0a080", 0x0800); // Smallest 3-byte character. - assertEncoded("e0bfbf", 0x0fff); - assertEncoded("e18080", 0x1000); - assertEncoded("e1bfbf", 0x1fff); - assertEncoded("ed8080", 0xd000); - assertEncoded("ed9fbf", 0xd7ff); // Largest character lower than the min surrogate. - assertEncoded("ee8080", 0xe000); // Smallest character greater than the max surrogate. - assertEncoded("eebfbf", 0xefff); - assertEncoded("ef8080", 0xf000); - assertEncoded("efbfbf", 0xffff); // Largest 3-byte character. - } - - @Test public void fourByteCharacters() throws Exception { - assertEncoded("f0908080", 0x010000); // Smallest surrogate pair. - assertEncoded("f48fbfbf", 0x10ffff); // Largest code point expressible by UTF-16. - } - - @Test public void danglingHighSurrogate() throws Exception { - assertStringEncoded("3f", "\ud800"); // "?" - } - - @Test public void lowSurrogateWithoutHighSurrogate() throws Exception { - assertStringEncoded("3f", "\udc00"); // "?" - } - - @Test public void highSurrogateFollowedByNonSurrogate() throws Exception { - assertStringEncoded("3f61", "\ud800\u0061"); // "?a": Following character is too low. - assertStringEncoded("3fee8080", "\ud800\ue000"); // "?\ue000": Following character is too high. - } - - @Test public void doubleLowSurrogate() throws Exception { - assertStringEncoded("3f3f", "\udc00\udc00"); // "??" - } - - @Test public void doubleHighSurrogate() throws Exception { - assertStringEncoded("3f3f", "\ud800\ud800"); // "??" - } - - @Test public void highSurrogateLowSurrogate() throws Exception { - assertStringEncoded("3f3f", "\udc00\ud800"); // "??" - } - - @Test public void multipleSegmentString() throws Exception { - String a = repeat("a", SEGMENT_SIZE + SEGMENT_SIZE + 1); - Buffer encoded = new Buffer().writeUtf8(a); - Buffer expected = new Buffer().write(a.getBytes(Charsets.UTF_8)); - assertEquals(expected, encoded); - } - - @Test public void stringSpansSegments() throws Exception { - Buffer buffer = new Buffer(); - String a = repeat("a", SEGMENT_SIZE - 1); - String b = "bb"; - String c = repeat("c", SEGMENT_SIZE - 1); - buffer.writeUtf8(a); - buffer.writeUtf8(b); - buffer.writeUtf8(c); - assertEquals(a + b + c, buffer.readUtf8()); - } - - @Test public void readEmptyBufferThrowsEofException() throws Exception { - Buffer buffer = new Buffer(); - try { - buffer.readUtf8CodePoint(); - fail(); - } catch (EOFException expected) { - } - } - - @Test public void readLeadingContinuationByteReturnsReplacementCharacter() throws Exception { - Buffer buffer = new Buffer(); - buffer.writeByte(0xbf); - assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint()); - assertTrue(buffer.exhausted()); - } - - @Test public void readMissingContinuationBytesThrowsEofException() throws Exception { - Buffer buffer = new Buffer(); - buffer.writeByte(0xdf); - try { - buffer.readUtf8CodePoint(); - fail(); - } catch (EOFException expected) { - } - assertFalse(buffer.exhausted()); // Prefix byte wasn't consumed. - } - - @Test public void readTooLargeCodepointReturnsReplacementCharacter() throws Exception { - // 5-byte and 6-byte code points are not supported. - Buffer buffer = new Buffer(); - buffer.write(ByteString.decodeHex("f888808080")); - assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint()); - assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint()); - assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint()); - assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint()); - assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint()); - assertTrue(buffer.exhausted()); - } - - @Test public void readNonContinuationBytesReturnsReplacementCharacter() throws Exception { - // Use a non-continuation byte where a continuation byte is expected. - Buffer buffer = new Buffer(); - buffer.write(ByteString.decodeHex("df20")); - assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint()); - assertEquals(0x20, buffer.readUtf8CodePoint()); // Non-continuation character not consumed. - assertTrue(buffer.exhausted()); - } - - @Test public void readCodePointBeyondUnicodeMaximum() throws Exception { - // A 4-byte encoding with data above the U+10ffff Unicode maximum. - Buffer buffer = new Buffer(); - buffer.write(ByteString.decodeHex("f4908080")); - assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint()); - assertTrue(buffer.exhausted()); - } - - @Test public void readSurrogateCodePoint() throws Exception { - Buffer buffer = new Buffer(); - buffer.write(ByteString.decodeHex("eda080")); - assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint()); - assertTrue(buffer.exhausted()); - buffer.write(ByteString.decodeHex("edbfbf")); - assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint()); - assertTrue(buffer.exhausted()); - } - - @Test public void readOverlongCodePoint() throws Exception { - // Use 2 bytes to encode data that only needs 1 byte. - Buffer buffer = new Buffer(); - buffer.write(ByteString.decodeHex("c080")); - assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint()); - assertTrue(buffer.exhausted()); - } - - @Test public void writeSurrogateCodePoint() throws Exception { - assertStringEncoded("ed9fbf", "\ud7ff"); // Below lowest surrogate is okay. - assertStringEncoded("3f", "\ud800"); // Lowest surrogate gets '?'. - assertStringEncoded("3f", "\udfff"); // Highest surrogate gets '?'. - assertStringEncoded("ee8080", "\ue000"); // Above highest surrogate is okay. - } - - @Test public void writeCodePointBeyondUnicodeMaximum() throws Exception { - Buffer buffer = new Buffer(); - try { - buffer.writeUtf8CodePoint(0x110000); - fail(); - } catch (IllegalArgumentException expected) { - assertEquals("Unexpected code point: 0x110000", expected.getMessage()); - } - } - - @Test public void size() throws Exception { - assertEquals(0, Utf8.size("")); - assertEquals(3, Utf8.size("abc")); - assertEquals(16, Utf8.size("təˈranəˌsôr")); - } - - @Test public void sizeWithBounds() throws Exception { - assertEquals(0, Utf8.size("", 0, 0)); - assertEquals(0, Utf8.size("abc", 0, 0)); - assertEquals(1, Utf8.size("abc", 1, 2)); - assertEquals(2, Utf8.size("abc", 0, 2)); - assertEquals(3, Utf8.size("abc", 0, 3)); - assertEquals(16, Utf8.size("təˈranəˌsôr", 0, 11)); - assertEquals(5, Utf8.size("təˈranəˌsôr", 3, 7)); - } - - @Test public void sizeBoundsCheck() throws Exception { - try { - Utf8.size(null, 0, 0); - fail(); - } catch (NullPointerException expected) { - } - try { - Utf8.size("abc", -1, 2); - fail(); - } catch (IllegalArgumentException expected) { - } - try { - Utf8.size("abc", 2, 1); - fail(); - } catch (IllegalArgumentException expected) { - } - try { - Utf8.size("abc", 1, 4); - fail(); - } catch (IllegalArgumentException expected) { - } - } - - private void assertEncoded(String hex, int... codePoints) throws Exception { - assertCodePointEncoded(hex, codePoints); - assertCodePointDecoded(hex, codePoints); - assertStringEncoded(hex, new String(codePoints, 0, codePoints.length)); - } - - private void assertCodePointEncoded(String hex, int... codePoints) throws Exception { - Buffer buffer = new Buffer(); - for (int codePoint : codePoints) { - buffer.writeUtf8CodePoint(codePoint); - } - assertEquals(buffer.readByteString(), ByteString.decodeHex(hex)); - } - - private void assertCodePointDecoded(String hex, int... codePoints) throws Exception { - Buffer buffer = new Buffer().write(ByteString.decodeHex(hex)); - for (int codePoint : codePoints) { - assertEquals(codePoint, buffer.readUtf8CodePoint()); - } - assertTrue(buffer.exhausted()); - } - - private void assertStringEncoded(String hex, String string) throws Exception { - ByteString expectedUtf8 = ByteString.decodeHex(hex); - - // Confirm our expectations are consistent with the platform. - ByteString platformUtf8 = ByteString.of(string.getBytes("UTF-8")); - assertEquals(expectedUtf8, platformUtf8); - - // Confirm our implementation matches those expectations. - ByteString actualUtf8 = new Buffer().writeUtf8(string).readByteString(); - assertEquals(expectedUtf8, actualUtf8); - - // Confirm we are consistent when writing one code point at a time. - Buffer bufferUtf8 = new Buffer(); - for (int i = 0; i < string.length(); ) { - int c = string.codePointAt(i); - bufferUtf8.writeUtf8CodePoint(c); - i += Character.charCount(c); - } - assertEquals(expectedUtf8, bufferUtf8.readByteString()); - - // Confirm we are consistent when measuring lengths. - assertEquals(expectedUtf8.size(), Utf8.size(string)); - assertEquals(expectedUtf8.size(), Utf8.size(string, 0, string.length())); - } -} diff --git a/okio/src/jvmTest/java/okio/WaitUntilNotifiedTest.java b/okio/src/jvmTest/java/okio/WaitUntilNotifiedTest.java deleted file mode 100644 index e4405289..00000000 --- a/okio/src/jvmTest/java/okio/WaitUntilNotifiedTest.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (C) 2016 Square, Inc. - * - * 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 java.io.InterruptedIOException; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import org.junit.After; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public final class WaitUntilNotifiedTest { - final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(0); - - @After public void tearDown() { - executorService.shutdown(); - } - - @Test public synchronized void notified() throws InterruptedIOException { - Timeout timeout = new Timeout(); - timeout.timeout(5000, TimeUnit.MILLISECONDS); - - double start = now(); - executorService.schedule(new Runnable() { - @Override public void run() { - synchronized (WaitUntilNotifiedTest.this) { - WaitUntilNotifiedTest.this.notify(); - } - } - }, 1000, TimeUnit.MILLISECONDS); - - timeout.waitUntilNotified(this); - assertElapsed(1000.0, start); - } - - @Test public synchronized void timeout() { - TestUtil.INSTANCE.assumeNotWindows(); - - Timeout timeout = new Timeout(); - timeout.timeout(1000, TimeUnit.MILLISECONDS); - double start = now(); - try { - timeout.waitUntilNotified(this); - fail(); - } catch (InterruptedIOException expected) { - assertEquals("timeout", expected.getMessage()); - } - assertElapsed(1000.0, start); - } - - @Test public synchronized void deadline() { - TestUtil.INSTANCE.assumeNotWindows(); - - Timeout timeout = new Timeout(); - timeout.deadline(1000, TimeUnit.MILLISECONDS); - double start = now(); - try { - timeout.waitUntilNotified(this); - fail(); - } catch (InterruptedIOException expected) { - assertEquals("timeout", expected.getMessage()); - } - assertElapsed(1000.0, start); - } - - @Test public synchronized void deadlineBeforeTimeout() { - TestUtil.INSTANCE.assumeNotWindows(); - - Timeout timeout = new Timeout(); - timeout.timeout(5000, TimeUnit.MILLISECONDS); - timeout.deadline(1000, TimeUnit.MILLISECONDS); - double start = now(); - try { - timeout.waitUntilNotified(this); - fail(); - } catch (InterruptedIOException expected) { - assertEquals("timeout", expected.getMessage()); - } - assertElapsed(1000.0, start); - } - - @Test public synchronized void timeoutBeforeDeadline() { - TestUtil.INSTANCE.assumeNotWindows(); - - Timeout timeout = new Timeout(); - timeout.timeout(1000, TimeUnit.MILLISECONDS); - timeout.deadline(5000, TimeUnit.MILLISECONDS); - double start = now(); - try { - timeout.waitUntilNotified(this); - fail(); - } catch (InterruptedIOException expected) { - assertEquals("timeout", expected.getMessage()); - } - assertElapsed(1000.0, start); - } - - @Test public synchronized void deadlineAlreadyReached() { - TestUtil.INSTANCE.assumeNotWindows(); - - Timeout timeout = new Timeout(); - timeout.deadlineNanoTime(System.nanoTime()); - double start = now(); - try { - timeout.waitUntilNotified(this); - fail(); - } catch (InterruptedIOException expected) { - assertEquals("timeout", expected.getMessage()); - } - assertElapsed(0.0, start); - } - - @Test public synchronized void threadInterrupted() { - TestUtil.INSTANCE.assumeNotWindows(); - - Timeout timeout = new Timeout(); - double start = now(); - Thread.currentThread().interrupt(); - try { - timeout.waitUntilNotified(this); - fail(); - } catch (InterruptedIOException expected) { - assertEquals("interrupted", expected.getMessage()); - assertTrue(Thread.interrupted()); - } - assertElapsed(0.0, start); - } - - @Test public synchronized void threadInterruptedOnThrowIfReached() throws Exception { - TestUtil.INSTANCE.assumeNotWindows(); - - Timeout timeout = new Timeout(); - Thread.currentThread().interrupt(); - try { - timeout.throwIfReached(); - fail(); - } catch (InterruptedIOException expected) { - assertEquals("interrupted", expected.getMessage()); - assertTrue(Thread.interrupted()); - } - } - - /** Returns the nanotime in milliseconds as a double for measuring timeouts. */ - private double now() { - return System.nanoTime() / 1000000.0d; - } - - /** - * Fails the test unless the time from start until now is duration, accepting differences in - * -50..+450 milliseconds. - */ - private void assertElapsed(double duration, double start) { - assertEquals(duration, now() - start - 200d, 250.0); - } -} diff --git a/okio/src/jvmTest/kotlin/okio/AsyncTimeoutTest.kt b/okio/src/jvmTest/kotlin/okio/AsyncTimeoutTest.kt new file mode 100644 index 00000000..f3a90a2b --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/AsyncTimeoutTest.kt @@ -0,0 +1,500 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.io.IOException +import java.io.InterruptedIOException +import java.util.Random +import java.util.concurrent.LinkedBlockingDeque +import java.util.concurrent.TimeUnit +import okio.ByteString.Companion.of +import okio.TestUtil.bufferWithRandomSegmentLayout +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + +/** + * This test uses four timeouts of varying durations: 250ms, 500ms, 750ms and + * 1000ms, named 'a', 'b', 'c' and 'd'. + */ +class AsyncTimeoutTest { + private val timedOut = LinkedBlockingDeque<AsyncTimeout>() + private val a = RecordingAsyncTimeout() + private val b = RecordingAsyncTimeout() + private val c = RecordingAsyncTimeout() + private val d = RecordingAsyncTimeout() + + @Before + fun setUp() { + a.timeout(250, TimeUnit.MILLISECONDS) + b.timeout(500, TimeUnit.MILLISECONDS) + c.timeout(750, TimeUnit.MILLISECONDS) + d.timeout(1000, TimeUnit.MILLISECONDS) + } + + @Test + fun zeroTimeoutIsNoTimeout() { + val timeout = RecordingAsyncTimeout() + timeout.timeout(0, TimeUnit.MILLISECONDS) + timeout.enter() + Thread.sleep(250) + assertFalse(timeout.exit()) + assertTimedOut() + } + + @Test + fun singleInstanceTimedOut() { + a.enter() + Thread.sleep(500) + assertTrue(a.exit()) + assertTimedOut(a) + } + + @Test + fun singleInstanceNotTimedOut() { + b.enter() + Thread.sleep(250) + b.exit() + assertFalse(b.exit()) + assertTimedOut() + } + + @Test + fun instancesAddedAtEnd() { + a.enter() + b.enter() + c.enter() + d.enter() + Thread.sleep(1250) + assertTrue(a.exit()) + assertTrue(b.exit()) + assertTrue(c.exit()) + assertTrue(d.exit()) + assertTimedOut(a, b, c, d) + } + + @Test + fun instancesAddedAtFront() { + d.enter() + c.enter() + b.enter() + a.enter() + Thread.sleep(1250) + assertTrue(d.exit()) + assertTrue(c.exit()) + assertTrue(b.exit()) + assertTrue(a.exit()) + assertTimedOut(a, b, c, d) + } + + @Test + fun instancesRemovedAtFront() { + a.enter() + b.enter() + c.enter() + d.enter() + assertFalse(a.exit()) + assertFalse(b.exit()) + assertFalse(c.exit()) + assertFalse(d.exit()) + assertTimedOut() + } + + @Test + fun instancesRemovedAtEnd() { + a.enter() + b.enter() + c.enter() + d.enter() + assertFalse(d.exit()) + assertFalse(c.exit()) + assertFalse(b.exit()) + assertFalse(a.exit()) + assertTimedOut() + } + + @Test + fun doubleEnter() { + a.enter() + try { + a.enter() + fail() + } catch (expected: IllegalStateException) { + } + } + + @Test + fun reEnter() { + a.timeout(10, TimeUnit.SECONDS) + a.enter() + assertFalse(a.exit()) + a.enter() + assertFalse(a.exit()) + } + + @Test + fun reEnterAfterTimeout() { + a.timeout(1, TimeUnit.MILLISECONDS) + a.enter() + Assert.assertSame(a, timedOut.take()) + assertTrue(a.exit()) + a.enter() + assertFalse(a.exit()) + } + + @Test + fun deadlineOnly() { + val timeout = RecordingAsyncTimeout() + timeout.deadline(250, TimeUnit.MILLISECONDS) + timeout.enter() + Thread.sleep(500) + assertTrue(timeout.exit()) + assertTimedOut(timeout) + } + + @Test + fun deadlineBeforeTimeout() { + val timeout = RecordingAsyncTimeout() + timeout.deadline(250, TimeUnit.MILLISECONDS) + timeout.timeout(750, TimeUnit.MILLISECONDS) + timeout.enter() + Thread.sleep(500) + assertTrue(timeout.exit()) + assertTimedOut(timeout) + } + + @Test + fun deadlineAfterTimeout() { + val timeout = RecordingAsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + timeout.deadline(750, TimeUnit.MILLISECONDS) + timeout.enter() + Thread.sleep(500) + assertTrue(timeout.exit()) + assertTimedOut(timeout) + } + + @Test + fun deadlineStartsBeforeEnter() { + val timeout = RecordingAsyncTimeout() + timeout.deadline(500, TimeUnit.MILLISECONDS) + Thread.sleep(500) + timeout.enter() + Thread.sleep(250) + assertTrue(timeout.exit()) + assertTimedOut(timeout) + } + + @Test + fun deadlineInThePast() { + val timeout = RecordingAsyncTimeout() + timeout.deadlineNanoTime(System.nanoTime() - 1) + timeout.enter() + Thread.sleep(250) + assertTrue(timeout.exit()) + assertTimedOut(timeout) + } + + @Test + fun wrappedSinkTimesOut() { + val sink: Sink = object : ForwardingSink(Buffer()) { + override fun write(source: Buffer, byteCount: Long) { + Thread.sleep(500) + } + } + val timeout = AsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + val timeoutSink = timeout.sink(sink) + val data = Buffer().writeUtf8("a") + try { + timeoutSink.write(data, 1) + fail() + } catch (expected: InterruptedIOException) { + } + } + + @Test + fun wrappedSinkFlushTimesOut() { + val sink: Sink = object : ForwardingSink(Buffer()) { + override fun flush() { + Thread.sleep(500) + } + } + val timeout = AsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + val timeoutSink = timeout.sink(sink) + try { + timeoutSink.flush() + fail() + } catch (expected: InterruptedIOException) { + } + } + + @Test + fun wrappedSinkCloseTimesOut() { + val sink: Sink = object : ForwardingSink(Buffer()) { + override fun close() { + Thread.sleep(500) + } + } + val timeout = AsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + val timeoutSink = timeout.sink(sink) + try { + timeoutSink.close() + fail() + } catch (expected: InterruptedIOException) { + } + } + + @Test + fun wrappedSourceTimesOut() { + val source: Source = object : ForwardingSource(Buffer()) { + override fun read(sink: Buffer, byteCount: Long): Long { + Thread.sleep(500) + return -1 + } + } + val timeout = AsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + val timeoutSource = timeout.source(source) + try { + timeoutSource.read(Buffer(), 0) + fail() + } catch (expected: InterruptedIOException) { + } + } + + @Test + fun wrappedSourceCloseTimesOut() { + val source: Source = object : ForwardingSource(Buffer()) { + override fun close() { + Thread.sleep(500) + } + } + val timeout = AsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + val timeoutSource = timeout.source(source) + try { + timeoutSource.close() + fail() + } catch (expected: InterruptedIOException) { + } + } + + @Test + fun wrappedThrowsWithTimeout() { + val sink: Sink = object : ForwardingSink(Buffer()) { + override fun write(source: Buffer, byteCount: Long) { + Thread.sleep(500) + throw IOException("exception and timeout") + } + } + val timeout = AsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + val timeoutSink = timeout.sink(sink) + val data = Buffer().writeUtf8("a") + try { + timeoutSink.write(data, 1) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + assertEquals("exception and timeout", expected.cause!!.message) + } + } + + @Test + fun wrappedThrowsWithoutTimeout() { + val sink: Sink = object : ForwardingSink(Buffer()) { + override fun write(source: Buffer, byteCount: Long) { + throw IOException("no timeout occurred") + } + } + val timeout = AsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + val timeoutSink = timeout.sink(sink) + val data = Buffer().writeUtf8("a") + try { + timeoutSink.write(data, 1) + fail() + } catch (expected: IOException) { + assertEquals("no timeout occurred", expected.message) + } + } + + /** + * We had a bug where writing a very large buffer would fail with an + * unexpected timeout because although the sink was making steady forward + * progress, doing it all as a single write caused a timeout. + */ + @Ignore("Flaky") + @Test + fun sinkSplitsLargeWrites() { + val data = ByteArray(512 * 1024) + val dice = Random(0) + dice.nextBytes(data) + val source = bufferWithRandomSegmentLayout(dice, data) + val target = Buffer() + val sink: Sink = object : ForwardingSink(Buffer()) { + override fun write(source: Buffer, byteCount: Long) { + Thread.sleep(byteCount / 500) // ~500 KiB/s. + target.write(source, byteCount) + } + } + + // Timeout after 250 ms of inactivity. + val timeout = AsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + val timeoutSink = timeout.sink(sink) + + // Transmit 500 KiB of data, which should take ~1 second. But expect no timeout! + timeoutSink.write(source, source.size) + + // The data should all have arrived. + assertEquals(of(*data), target.readByteString()) + } + + @Test + fun enterCancelSleepExit() { + val timeout = RecordingAsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + timeout.enter() + timeout.cancel() + Thread.sleep(500) + + // Call didn't time out because the timeout was canceled. + assertFalse(timeout.exit()) + assertTimedOut() + } + + @Test + fun enterCancelCancelSleepExit() { + val timeout = RecordingAsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + timeout.enter() + timeout.cancel() + timeout.cancel() + Thread.sleep(500) + + // Call didn't time out because the timeout was canceled. + assertFalse(timeout.exit()) + assertTimedOut() + } + + @Test + fun enterSleepCancelExit() { + val timeout = RecordingAsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + timeout.enter() + Thread.sleep(500) + timeout.cancel() + + // Call timed out because the cancel was too late. + assertTrue(timeout.exit()) + assertTimedOut(timeout) + } + + @Test + fun enterSleepCancelCancelExit() { + val timeout = RecordingAsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + timeout.enter() + Thread.sleep(500) + timeout.cancel() + timeout.cancel() + + // Call timed out because both cancels were too late. + assertTrue(timeout.exit()) + assertTimedOut(timeout) + } + + @Test + fun enterCancelSleepCancelExit() { + val timeout = RecordingAsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + timeout.enter() + timeout.cancel() + Thread.sleep(500) + timeout.cancel() + + // Call didn't time out because the timeout was canceled. + assertFalse(timeout.exit()) + assertTimedOut() + } + + @Test + fun cancelEnterSleepExit() { + val timeout = RecordingAsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + timeout.cancel() + timeout.enter() + Thread.sleep(500) + + // Call timed out because the cancel was too early. + assertTrue(timeout.exit()) + assertTimedOut(timeout) + } + + @Test + fun cancelEnterCancelSleepExit() { + val timeout = RecordingAsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + timeout.cancel() + timeout.enter() + timeout.cancel() + Thread.sleep(500) + + // Call didn't time out because the timeout was canceled. + assertFalse(timeout.exit()) + assertTimedOut() + } + + @Test + fun enterCancelSleepExitEnterSleepExit() { + val timeout = RecordingAsyncTimeout() + timeout.timeout(250, TimeUnit.MILLISECONDS) + + // First call doesn't time out because we cancel it. + timeout.enter() + timeout.cancel() + Thread.sleep(500) + assertFalse(timeout.exit()) + assertTimedOut() + + // Second call does time out because it isn't canceled a second time. + timeout.enter() + Thread.sleep(500) + assertTrue(timeout.exit()) + assertTimedOut(timeout) + } + + /** Asserts which timeouts fired, and in which order. */ + private fun assertTimedOut(vararg expected: Timeout) { + assertEquals(expected.toList(), timedOut.toList()) + timedOut.clear() + } + + internal inner class RecordingAsyncTimeout : AsyncTimeout() { + override fun timedOut() { + timedOut.add(this) + } + } +} diff --git a/okio/src/jvmTest/kotlin/okio/AwaitSignalTest.kt b/okio/src/jvmTest/kotlin/okio/AwaitSignalTest.kt new file mode 100644 index 00000000..a04e8cfe --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/AwaitSignalTest.kt @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2023 Block Inc. + * + * 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 java.io.InterruptedIOException +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.Condition +import java.util.concurrent.locks.ReentrantLock +import okio.TestUtil.assumeNotWindows +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@RunWith(Parameterized::class) +class AwaitSignalTest( + factory: TimeoutFactory, +) { + private val timeout = factory.newTimeout() + val executorService = TestingExecutors.newScheduledExecutorService(0) + + val lock: ReentrantLock = ReentrantLock() + val condition: Condition = lock.newCondition() + + @After + fun tearDown() { + executorService.shutdown() + } + + @Test + fun signaled() = lock.withLock { + timeout.timeout(5000, TimeUnit.MILLISECONDS) + val start = now() + executorService.schedule( + { lock.withLock { condition.signal() } }, + 1000, + TimeUnit.MILLISECONDS, + ) + timeout.awaitSignal(condition) + assertElapsed(1000.0, start) + } + + @Test + fun timeout() = lock.withLock { + assumeNotWindows() + timeout.timeout(1000, TimeUnit.MILLISECONDS) + val start = now() + try { + timeout.awaitSignal(condition) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(1000.0, start) + } + + @Test + fun deadline() = lock.withLock { + assumeNotWindows() + timeout.deadline(1000, TimeUnit.MILLISECONDS) + val start = now() + try { + timeout.awaitSignal(condition) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(1000.0, start) + } + + @Test + fun deadlineBeforeTimeout() = lock.withLock { + assumeNotWindows() + timeout.timeout(5000, TimeUnit.MILLISECONDS) + timeout.deadline(1000, TimeUnit.MILLISECONDS) + val start = now() + try { + timeout.awaitSignal(condition) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(1000.0, start) + } + + @Test + fun timeoutBeforeDeadline() = lock.withLock { + assumeNotWindows() + timeout.timeout(1000, TimeUnit.MILLISECONDS) + timeout.deadline(5000, TimeUnit.MILLISECONDS) + val start = now() + try { + timeout.awaitSignal(condition) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(1000.0, start) + } + + @Test + fun deadlineAlreadyReached() = lock.withLock { + assumeNotWindows() + timeout.deadlineNanoTime(System.nanoTime()) + val start = now() + try { + timeout.awaitSignal(condition) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(0.0, start) + } + + @Test + fun threadInterrupted() = lock.withLock { + assumeNotWindows() + val start = now() + Thread.currentThread().interrupt() + try { + timeout.awaitSignal(condition) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("interrupted", expected.message) + assertTrue(Thread.interrupted()) + } + assertElapsed(0.0, start) + } + + @Test + fun threadInterruptedOnThrowIfReached() = lock.withLock { + assumeNotWindows() + Thread.currentThread().interrupt() + try { + timeout.throwIfReached() + fail() + } catch (expected: InterruptedIOException) { + assertEquals("interrupted", expected.message) + assertTrue(Thread.interrupted()) + } + } + + @Test + fun cancelBeforeWaitDoesNothing() = lock.withLock { + timeout.timeout(1000, TimeUnit.MILLISECONDS) + timeout.cancel() + val start = now() + try { + timeout.awaitSignal(condition) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(1000.0, start) + } + + @Test + fun canceledTimeoutDoesNotThrowWhenNotNotifiedOnTime() = lock.withLock { + assumeNotWindows() + timeout.timeout(1000, TimeUnit.MILLISECONDS) + timeout.cancelLater(500) + + val start = now() + timeout.awaitSignal(condition) // Returns early but doesn't throw. + assertElapsed(1000.0, start) + } + + @Test + @Synchronized + fun multipleCancelsAreIdempotent() = lock.withLock { + timeout.timeout(1000, TimeUnit.MILLISECONDS) + timeout.cancelLater(250) + timeout.cancelLater(500) + timeout.cancelLater(750) + + val start = now() + timeout.awaitSignal(condition) // Returns early but doesn't throw. + assertElapsed(1000.0, start) + } + + /** Returns the nanotime in milliseconds as a double for measuring timeouts. */ + private fun now(): Double { + return System.nanoTime() / 1000000.0 + } + + /** + * Fails the test unless the time from start until now is duration, accepting differences in + * -50..+450 milliseconds. + */ + private fun assertElapsed(duration: Double, start: Double) { + assertEquals(duration, now() - start - 200.0, 250.0) + } + + private fun Timeout.cancelLater(delay: Long) { + executorService.schedule( + { cancel() }, + delay, + TimeUnit.MILLISECONDS, + ) + } + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun parameters(): List<Array<out Any?>> = TimeoutFactory.entries.map { arrayOf(it) } + } +} diff --git a/okio/src/jvmTest/kotlin/okio/BufferCursorKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/BufferCursorKotlinTest.kt index ddbc4c53..0b50b005 100644 --- a/okio/src/jvmTest/kotlin/okio/BufferCursorKotlinTest.kt +++ b/okio/src/jvmTest/kotlin/okio/BufferCursorKotlinTest.kt @@ -15,6 +15,11 @@ */ package okio +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotSame +import kotlin.test.assertSame +import kotlin.test.assertTrue import okio.Buffer.UnsafeCursor import okio.TestUtil.deepCopy import org.junit.Assume.assumeTrue @@ -23,11 +28,6 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameter import org.junit.runners.Parameterized.Parameters -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotSame -import kotlin.test.assertSame -import kotlin.test.assertTrue @RunWith(Parameterized::class) class BufferCursorKotlinTest { @@ -97,7 +97,7 @@ class BufferCursorKotlinTest { buffer.readAndWriteUnsafe().use { cursor -> while (cursor.next() != -1) { - cursor.data!!.fill('x'.toByte(), cursor.start, cursor.end) + cursor.data!!.fill('x'.code.toByte(), cursor.start, cursor.end) } } diff --git a/okio/src/jvmTest/kotlin/okio/BufferCursorTest.kt b/okio/src/jvmTest/kotlin/okio/BufferCursorTest.kt new file mode 100644 index 00000000..54873286 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/BufferCursorTest.kt @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 java.util.Arrays +import okio.ByteString.Companion.of +import okio.TestUtil.SEGMENT_SIZE +import okio.TestUtil.deepCopy +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.fail +import org.junit.Assume.assumeTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@RunWith(Parameterized::class) +class BufferCursorTest( + private var bufferFactory: BufferFactory, +) { + + @Test + fun apiExample() { + val buffer = Buffer() + buffer.readAndWriteUnsafe().use { cursor -> + cursor.resizeBuffer(1000000) + do { + Arrays.fill(cursor.data, cursor.start, cursor.end, 'x'.code.toByte()) + } while (cursor.next() != -1) + cursor.seek(3) + cursor.data!![cursor.start] = 'o'.code.toByte() + cursor.seek(1) + cursor.data!![cursor.start] = 'o'.code.toByte() + cursor.resizeBuffer(4) + } + assertEquals(Buffer().writeUtf8("xoxo"), buffer) + } + + @Test + fun accessSegmentBySegment() { + val buffer = bufferFactory.newBuffer() + buffer.readUnsafe().use { cursor -> + val actual = Buffer() + while (cursor.next().toLong() != -1L) { + actual.write(cursor.data!!, cursor.start, cursor.end - cursor.start) + } + assertEquals(buffer, actual) + } + } + + @Test + fun seekToNegativeOneSeeksBeforeFirstSegment() { + val buffer = bufferFactory.newBuffer() + buffer.readUnsafe().use { cursor -> + cursor.seek(-1L) + assertEquals(-1, cursor.offset) + assertNull(cursor.data) + assertEquals(-1, cursor.start.toLong()) + assertEquals(-1, cursor.end.toLong()) + cursor.next() + assertEquals(0, cursor.offset) + } + } + + @Test + fun accessByteByByte() { + val buffer = bufferFactory.newBuffer() + buffer.readUnsafe().use { cursor -> + val actual = ByteArray(buffer.size.toInt()) + for (i in 0 until buffer.size) { + cursor.seek(i) + actual[i.toInt()] = cursor.data!![cursor.start] + } + assertEquals(of(*actual), buffer.snapshot()) + } + } + + @Test + fun accessByteByByteReverse() { + val buffer = bufferFactory.newBuffer() + buffer.readUnsafe().use { cursor -> + val actual = ByteArray(buffer.size.toInt()) + for (i in (buffer.size - 1).toInt() downTo 0) { + cursor.seek(i.toLong()) + actual[i] = cursor.data!![cursor.start] + } + assertEquals(of(*actual), buffer.snapshot()) + } + } + + @Test + fun accessByteByByteAlwaysResettingToZero() { + val buffer = bufferFactory.newBuffer() + buffer.readUnsafe().use { cursor -> + val actual = ByteArray(buffer.size.toInt()) + for (i in 0 until buffer.size) { + cursor.seek(i) + actual[i.toInt()] = cursor.data!![cursor.start] + cursor.seek(0L) + } + assertEquals(of(*actual), buffer.snapshot()) + } + } + + @Test + fun segmentBySegmentNavigation() { + val buffer = bufferFactory.newBuffer() + val cursor = buffer.readUnsafe() + assertEquals(-1, cursor.offset) + try { + var lastOffset = cursor.offset + while (cursor.next().toLong() != -1L) { + Assert.assertTrue(cursor.offset > lastOffset) + lastOffset = cursor.offset + } + assertEquals(buffer.size, cursor.offset) + assertNull(cursor.data) + assertEquals(-1, cursor.start.toLong()) + assertEquals(-1, cursor.end.toLong()) + } finally { + cursor.close() + } + } + + @Test + fun seekWithinSegment() { + assumeTrue(bufferFactory === BufferFactory.SMALL_SEGMENTED_BUFFER) + val buffer = bufferFactory.newBuffer() + assertEquals("abcdefghijkl", buffer.clone().readUtf8()) + buffer.readUnsafe().use { cursor -> + assertEquals(2, cursor.seek(5).toLong()) // 2 for 2 bytes left in the segment: "fg". + assertEquals(5, cursor.offset) + assertEquals(2, (cursor.end - cursor.start).toLong()) + assertEquals('d'.code.toLong(), Char(cursor.data!![cursor.start - 2].toUShort()).code.toLong()) // Out of bounds! + assertEquals('e'.code.toLong(), Char(cursor.data!![cursor.start - 1].toUShort()).code.toLong()) // Out of bounds! + assertEquals('f'.code.toLong(), Char(cursor.data!![cursor.start].toUShort()).code.toLong()) + assertEquals('g'.code.toLong(), Char(cursor.data!![cursor.start + 1].toUShort()).code.toLong()) + } + } + + @Test + fun acquireAndRelease() { + val buffer = bufferFactory.newBuffer() + val cursor = Buffer.UnsafeCursor() + + // Nothing initialized before acquire. + assertEquals(-1, cursor.offset) + assertNull(cursor.data) + assertEquals(-1, cursor.start.toLong()) + assertEquals(-1, cursor.end.toLong()) + buffer.readUnsafe(cursor) + cursor.close() + + // Nothing initialized after close. + assertEquals(-1, cursor.offset) + assertNull(cursor.data) + assertEquals(-1, cursor.start.toLong()) + assertEquals(-1, cursor.end.toLong()) + } + + @Test + fun doubleAcquire() { + val buffer = bufferFactory.newBuffer() + try { + buffer.readUnsafe().use { cursor -> + buffer.readUnsafe(cursor) + fail() + } + } catch (expected: IllegalStateException) { + } + } + + @Test + fun releaseWithoutAcquire() { + val cursor = Buffer.UnsafeCursor() + try { + cursor.close() + fail() + } catch (expected: IllegalStateException) { + } + } + + @Test + fun releaseAfterRelease() { + val buffer = bufferFactory.newBuffer() + val cursor = buffer.readUnsafe() + cursor.close() + try { + cursor.close() + fail() + } catch (expected: IllegalStateException) { + } + } + + @Test + fun enlarge() { + val buffer = bufferFactory.newBuffer() + val originalSize = buffer.size + val expected = deepCopy(buffer) + expected.writeUtf8("abc") + buffer.readAndWriteUnsafe().use { cursor -> + assertEquals(originalSize, cursor.resizeBuffer(originalSize + 3)) + cursor.seek(originalSize) + cursor.data!![cursor.start] = 'a'.code.toByte() + cursor.seek(originalSize + 1) + cursor.data!![cursor.start] = 'b'.code.toByte() + cursor.seek(originalSize + 2) + cursor.data!![cursor.start] = 'c'.code.toByte() + } + assertEquals(expected, buffer) + } + + @Test + fun enlargeByManySegments() { + val buffer = bufferFactory.newBuffer() + val originalSize = buffer.size + val expected = deepCopy(buffer) + expected.writeUtf8("x".repeat(1000000)) + buffer.readAndWriteUnsafe().use { cursor -> + cursor.resizeBuffer(originalSize + 1000000) + cursor.seek(originalSize) + do { + Arrays.fill(cursor.data, cursor.start, cursor.end, 'x'.code.toByte()) + } while (cursor.next() != -1) + } + assertEquals(expected, buffer) + } + + @Test + fun resizeNotAcquired() { + val cursor = Buffer.UnsafeCursor() + try { + cursor.resizeBuffer(10) + fail() + } catch (expected: IllegalStateException) { + } + } + + @Test + fun expandNotAcquired() { + val cursor = Buffer.UnsafeCursor() + try { + cursor.expandBuffer(10) + fail() + } catch (expected: IllegalStateException) { + } + } + + @Test + fun resizeAcquiredReadOnly() { + val buffer = bufferFactory.newBuffer() + try { + buffer.readUnsafe().use { cursor -> + cursor.resizeBuffer(10) + fail() + } + } catch (expected: IllegalStateException) { + } + } + + @Test + fun expandAcquiredReadOnly() { + val buffer = bufferFactory.newBuffer() + try { + buffer.readUnsafe().use { cursor -> + cursor.expandBuffer(10) + fail() + } + } catch (expected: IllegalStateException) { + } + } + + @Test + fun shrink() { + val buffer = bufferFactory.newBuffer() + assumeTrue(buffer.size > 3) + val originalSize = buffer.size + val expected = Buffer() + deepCopy(buffer).copyTo(expected, 0, originalSize - 3) + buffer.readAndWriteUnsafe().use { cursor -> + assertEquals(originalSize, cursor.resizeBuffer(originalSize - 3)) + } + assertEquals(expected, buffer) + } + + @Test + fun shrinkByManySegments() { + val buffer = bufferFactory.newBuffer() + assumeTrue(buffer.size <= 1000000) + val originalSize = buffer.size + val toShrink = Buffer() + toShrink.writeUtf8("x".repeat(1000000)) + deepCopy(buffer).copyTo(toShrink, 0, originalSize) + val cursor = Buffer.UnsafeCursor() + toShrink.readAndWriteUnsafe(cursor) + try { + cursor.resizeBuffer(originalSize) + } finally { + cursor.close() + } + val expected = Buffer() + expected.writeUtf8("x".repeat(originalSize.toInt())) + assertEquals(expected, toShrink) + } + + @Test + fun shrinkAdjustOffset() { + val buffer = bufferFactory.newBuffer() + assumeTrue(buffer.size > 4) + buffer.readAndWriteUnsafe().use { cursor -> + cursor.seek(buffer.size - 1) + cursor.resizeBuffer(3) + assertEquals(3, cursor.offset) + assertNull(cursor.data) + assertEquals(-1, cursor.start.toLong()) + assertEquals(-1, cursor.end.toLong()) + } + } + + @Test + fun resizeToSameSizeSeeksToEnd() { + val buffer = bufferFactory.newBuffer() + val originalSize = buffer.size + buffer.readAndWriteUnsafe().use { cursor -> + cursor.seek(buffer.size / 2) + assertEquals(originalSize, buffer.size) + cursor.resizeBuffer(originalSize) + assertEquals(originalSize, buffer.size) + assertEquals(originalSize, cursor.offset) + assertNull(cursor.data) + assertEquals(-1, cursor.start.toLong()) + assertEquals(-1, cursor.end.toLong()) + } + } + + @Test + fun resizeEnlargeMovesCursorToOldSize() { + val buffer = bufferFactory.newBuffer() + val originalSize = buffer.size + val expected = deepCopy(buffer) + expected.writeUtf8("a") + buffer.readAndWriteUnsafe().use { cursor -> + cursor.seek(buffer.size / 2) + assertEquals(originalSize, buffer.size) + cursor.resizeBuffer(originalSize + 1) + assertEquals(originalSize, cursor.offset) + assertNotNull(cursor.data) + assertNotEquals(-1, cursor.start.toLong()) + assertEquals((cursor.start + 1).toLong(), cursor.end.toLong()) + cursor.data!![cursor.start] = 'a'.code.toByte() + } + assertEquals(expected, buffer) + } + + @Test + fun resizeShrinkMovesCursorToEnd() { + val buffer = bufferFactory.newBuffer() + assumeTrue(buffer.size > 0) + val originalSize = buffer.size + buffer.readAndWriteUnsafe().use { cursor -> + cursor.seek(buffer.size / 2) + assertEquals(originalSize, buffer.size) + cursor.resizeBuffer(originalSize - 1) + assertEquals(originalSize - 1, cursor.offset) + assertNull(cursor.data) + assertEquals(-1, cursor.start.toLong()) + assertEquals(-1, cursor.end.toLong()) + } + } + + @Test + fun expand() { + val buffer = bufferFactory.newBuffer() + val originalSize = buffer.size + val expected = deepCopy(buffer) + expected.writeUtf8("abcde") + buffer.readAndWriteUnsafe().use { cursor -> + cursor.expandBuffer(5) + for (i in 0..4) { + cursor.data!![cursor.start + i] = ('a'.code + i).toByte() + } + cursor.resizeBuffer(originalSize + 5) + } + assertEquals(expected, buffer) + } + + @Test + fun expandSameSegment() { + val buffer = bufferFactory.newBuffer() + val originalSize = buffer.size + assumeTrue(originalSize > 0) + buffer.readAndWriteUnsafe().use { cursor -> + cursor.seek(originalSize - 1) + val originalEnd = cursor.end + assumeTrue(originalEnd < SEGMENT_SIZE) + val addedByteCount = cursor.expandBuffer(1) + assertEquals((SEGMENT_SIZE - originalEnd).toLong(), addedByteCount) + assertEquals(originalSize + addedByteCount, buffer.size) + assertEquals(originalSize, cursor.offset) + assertEquals(originalEnd.toLong(), cursor.start.toLong()) + assertEquals(SEGMENT_SIZE.toLong(), cursor.end.toLong()) + } + } + + @Test + fun expandNewSegment() { + val buffer = bufferFactory.newBuffer() + val originalSize = buffer.size + buffer.readAndWriteUnsafe().use { cursor -> + val addedByteCount = cursor.expandBuffer(SEGMENT_SIZE) + assertEquals(SEGMENT_SIZE.toLong(), addedByteCount) + assertEquals(originalSize, cursor.offset) + assertEquals(0, cursor.start.toLong()) + assertEquals(SEGMENT_SIZE.toLong(), cursor.end.toLong()) + } + } + + @Test + fun expandMovesOffsetToOldSize() { + val buffer = bufferFactory.newBuffer() + val originalSize = buffer.size + buffer.readAndWriteUnsafe().use { cursor -> + cursor.seek(buffer.size / 2) + assertEquals(originalSize, buffer.size) + val addedByteCount = cursor.expandBuffer(5) + assertEquals(originalSize + addedByteCount, buffer.size) + assertEquals(originalSize, cursor.offset) + } + } + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun parameters(): List<Array<Any>> { + val result = mutableListOf<Array<Any>>() + for (bufferFactory in BufferFactory.values()) { + result += arrayOf(bufferFactory) + } + return result + } + } +} diff --git a/okio/src/jvmTest/kotlin/okio/BufferFactory.kt b/okio/src/jvmTest/kotlin/okio/BufferFactory.kt index e1533d23..0e6ce906 100644 --- a/okio/src/jvmTest/kotlin/okio/BufferFactory.kt +++ b/okio/src/jvmTest/kotlin/okio/BufferFactory.kt @@ -15,9 +15,9 @@ */ package okio +import java.util.Random import okio.TestUtil.bufferWithRandomSegmentLayout import okio.TestUtil.bufferWithSegments -import java.util.Random enum class BufferFactory { EMPTY { @@ -59,7 +59,8 @@ enum class BufferFactory { return bufferWithRandomSegmentLayout(dice, largeByteArray) } - }; + }, + ; @Throws(Exception::class) abstract fun newBuffer(): Buffer diff --git a/okio/src/jvmTest/kotlin/okio/BufferKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/BufferKotlinTest.kt index eda7989d..3bc8193c 100644 --- a/okio/src/jvmTest/kotlin/okio/BufferKotlinTest.kt +++ b/okio/src/jvmTest/kotlin/okio/BufferKotlinTest.kt @@ -15,16 +15,16 @@ */ package okio +import kotlin.test.assertFailsWith import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import kotlin.test.assertFailsWith class BufferKotlinTest { @Test fun get() { val actual = Buffer().writeUtf8("abc") - assertThat(actual[0]).isEqualTo('a'.toByte()) - assertThat(actual[1]).isEqualTo('b'.toByte()) - assertThat(actual[2]).isEqualTo('c'.toByte()) + assertThat(actual[0]).isEqualTo('a'.code.toByte()) + assertThat(actual[1]).isEqualTo('b'.code.toByte()) + assertThat(actual[2]).isEqualTo('c'.code.toByte()) assertFailsWith<IndexOutOfBoundsException> { actual[-1] } diff --git a/okio/src/jvmTest/kotlin/okio/BufferTest.kt b/okio/src/jvmTest/kotlin/okio/BufferTest.kt new file mode 100644 index 00000000..6f3501bd --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/BufferTest.kt @@ -0,0 +1,612 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.EOFException +import java.io.InputStream +import java.util.Arrays +import java.util.Random +import kotlin.text.Charsets.UTF_8 +import okio.ByteString.Companion.decodeHex +import okio.TestUtil.SEGMENT_POOL_MAX_SIZE +import okio.TestUtil.SEGMENT_SIZE +import okio.TestUtil.assertNoEmptySegments +import okio.TestUtil.bufferWithRandomSegmentLayout +import okio.TestUtil.segmentPoolByteCount +import okio.TestUtil.segmentSizes +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Test + +/** + * Tests solely for the behavior of Buffer's implementation. For generic BufferedSink or + * BufferedSource behavior use BufferedSinkTest or BufferedSourceTest, respectively. + */ +class BufferTest { + @Test + fun readAndWriteUtf8() { + val buffer = Buffer() + buffer.writeUtf8("ab") + assertEquals(2, buffer.size) + buffer.writeUtf8("cdef") + assertEquals(6, buffer.size) + assertEquals("abcd", buffer.readUtf8(4)) + assertEquals(2, buffer.size) + assertEquals("ef", buffer.readUtf8(2)) + assertEquals(0, buffer.size) + try { + buffer.readUtf8(1) + fail() + } catch (expected: EOFException) { + } + } + + /** Buffer's toString is the same as ByteString's. */ + @Test + fun bufferToString() { + assertEquals("[size=0]", Buffer().toString()) + assertEquals( + "[text=a\\r\\nb\\nc\\rd\\\\e]", + Buffer().writeUtf8("a\r\nb\nc\rd\\e").toString(), + ) + assertEquals( + "[text=Tyrannosaur]", + Buffer().writeUtf8("Tyrannosaur").toString(), + ) + assertEquals( + "[text=təˈranəˌsôr]", + Buffer() + .write("74c999cb8872616ec999cb8c73c3b472".decodeHex()) + .toString(), + ) + assertEquals( + "[hex=0000000000000000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000]", + Buffer().write(ByteArray(64)).toString(), + ) + } + + @Test + fun multipleSegmentBuffers() { + val buffer = Buffer() + buffer.writeUtf8("a".repeat(1000)) + buffer.writeUtf8("b".repeat(2500)) + buffer.writeUtf8("c".repeat(5000)) + buffer.writeUtf8("d".repeat(10000)) + buffer.writeUtf8("e".repeat(25000)) + buffer.writeUtf8("f".repeat(50000)) + assertEquals("a".repeat(999), buffer.readUtf8(999)) // a...a + assertEquals("a" + "b".repeat(2500) + "c", buffer.readUtf8(2502)) // ab...bc + assertEquals("c".repeat(4998), buffer.readUtf8(4998)) // c...c + assertEquals("c" + "d".repeat(10000) + "e", buffer.readUtf8(10002)) // cd...de + assertEquals("e".repeat(24998), buffer.readUtf8(24998)) // e...e + assertEquals("e" + "f".repeat(50000), buffer.readUtf8(50001)) // ef...f + assertEquals(0, buffer.size) + } + + @Test + fun fillAndDrainPool() { + val buffer = Buffer() + + // Take 2 * MAX_SIZE segments. This will drain the pool, even if other tests filled it. + buffer.write(ByteArray(SEGMENT_POOL_MAX_SIZE)) + buffer.write(ByteArray(SEGMENT_POOL_MAX_SIZE)) + assertEquals(0, segmentPoolByteCount().toLong()) + + // Recycle MAX_SIZE segments. They're all in the pool. + buffer.skip(SEGMENT_POOL_MAX_SIZE.toLong()) + assertEquals(SEGMENT_POOL_MAX_SIZE.toLong(), segmentPoolByteCount().toLong()) + + // Recycle MAX_SIZE more segments. The pool is full so they get garbage collected. + buffer.skip(SEGMENT_POOL_MAX_SIZE.toLong()) + assertEquals(SEGMENT_POOL_MAX_SIZE.toLong(), segmentPoolByteCount().toLong()) + + // Take MAX_SIZE segments to drain the pool. + buffer.write(ByteArray(SEGMENT_POOL_MAX_SIZE)) + assertEquals(0, segmentPoolByteCount().toLong()) + + // Take MAX_SIZE more segments. The pool is drained so these will need to be allocated. + buffer.write(ByteArray(SEGMENT_POOL_MAX_SIZE)) + assertEquals(0, segmentPoolByteCount().toLong()) + } + + @Test + fun moveBytesBetweenBuffersShareSegment() { + val size: Int = SEGMENT_SIZE / 2 - 1 + val segmentSizes = moveBytesBetweenBuffers("a".repeat(size), "b".repeat(size)) + assertEquals(Arrays.asList(size * 2), segmentSizes) + } + + @Test + fun moveBytesBetweenBuffersReassignSegment() { + val size: Int = SEGMENT_SIZE / 2 + 1 + val segmentSizes = moveBytesBetweenBuffers("a".repeat(size), "b".repeat(size)) + assertEquals(Arrays.asList(size, size), segmentSizes) + } + + @Test + fun moveBytesBetweenBuffersMultipleSegments() { + val size: Int = 3 * SEGMENT_SIZE + 1 + val segmentSizes = moveBytesBetweenBuffers("a".repeat(size), "b".repeat(size)) + assertEquals( + listOf( + SEGMENT_SIZE, + SEGMENT_SIZE, + SEGMENT_SIZE, + 1, + SEGMENT_SIZE, + SEGMENT_SIZE, + SEGMENT_SIZE, + 1, + ), + segmentSizes, + ) + } + + private fun moveBytesBetweenBuffers(vararg contents: String): List<Int> { + val expected = StringBuilder() + val buffer = Buffer() + for (s in contents) { + val source = Buffer() + source.writeUtf8(s) + buffer.writeAll(source) + expected.append(s) + } + val segmentSizes = segmentSizes(buffer) + assertEquals(expected.toString(), buffer.readUtf8(expected.length.toLong())) + return segmentSizes + } + + /** The big part of source's first segment is being moved. */ + @Test + fun writeSplitSourceBufferLeft() { + val writeSize: Int = SEGMENT_SIZE / 2 + 1 + val sink = Buffer() + sink.writeUtf8("b".repeat(SEGMENT_SIZE - 10)) + val source = Buffer() + source.writeUtf8("a".repeat(SEGMENT_SIZE * 2)) + sink.write(source, writeSize.toLong()) + assertEquals(Arrays.asList<Int>(SEGMENT_SIZE - 10, writeSize), segmentSizes(sink)) + assertEquals(Arrays.asList<Int>(SEGMENT_SIZE - writeSize, SEGMENT_SIZE), segmentSizes(source)) + } + + /** The big part of source's first segment is staying put. */ + @Test + fun writeSplitSourceBufferRight() { + val writeSize: Int = SEGMENT_SIZE / 2 - 1 + val sink = Buffer() + sink.writeUtf8("b".repeat(SEGMENT_SIZE - 10)) + val source = Buffer() + source.writeUtf8("a".repeat(SEGMENT_SIZE * 2)) + sink.write(source, writeSize.toLong()) + assertEquals(Arrays.asList<Int>(SEGMENT_SIZE - 10, writeSize), segmentSizes(sink)) + assertEquals(Arrays.asList<Int>(SEGMENT_SIZE - writeSize, SEGMENT_SIZE), segmentSizes(source)) + } + + @Test + fun writePrefixDoesntSplit() { + val sink = Buffer() + sink.writeUtf8("b".repeat(10)) + val source = Buffer() + source.writeUtf8("a".repeat(SEGMENT_SIZE * 2)) + sink.write(source, 20) + assertEquals(mutableListOf(30), segmentSizes(sink)) + assertEquals(Arrays.asList<Int>(SEGMENT_SIZE - 20, SEGMENT_SIZE), segmentSizes(source)) + assertEquals(30, sink.size) + assertEquals((SEGMENT_SIZE * 2 - 20).toLong(), source.size) + } + + @Test + fun writePrefixDoesntSplitButRequiresCompact() { + val sink = Buffer() + sink.writeUtf8("b".repeat(SEGMENT_SIZE - 10)) // limit = size - 10 + sink.readUtf8((SEGMENT_SIZE - 20).toLong()) // pos = size = 20 + val source = Buffer() + source.writeUtf8("a".repeat(SEGMENT_SIZE * 2)) + sink.write(source, 20) + assertEquals(mutableListOf(30), segmentSizes(sink)) + assertEquals(Arrays.asList<Int>(SEGMENT_SIZE - 20, SEGMENT_SIZE), segmentSizes(source)) + assertEquals(30, sink.size) + assertEquals((SEGMENT_SIZE * 2 - 20).toLong(), source.size) + } + + @Test + fun copyToSpanningSegments() { + val source = Buffer() + source.writeUtf8("a".repeat(SEGMENT_SIZE * 2)) + source.writeUtf8("b".repeat(SEGMENT_SIZE * 2)) + val out = ByteArrayOutputStream() + source.copyTo(out, 10, (SEGMENT_SIZE * 3).toLong()) + assertEquals( + "a".repeat(SEGMENT_SIZE * 2 - 10) + "b".repeat(SEGMENT_SIZE + 10), + out.toString(), + ) + assertEquals( + "a".repeat(SEGMENT_SIZE * 2) + "b".repeat(SEGMENT_SIZE * 2), + source.readUtf8((SEGMENT_SIZE * 4).toLong()), + ) + } + + @Test + fun copyToStream() { + val buffer = Buffer().writeUtf8("hello, world!") + val out = ByteArrayOutputStream() + buffer.copyTo(out) + val outString = out.toByteArray().toString(UTF_8) + assertEquals("hello, world!", outString) + assertEquals("hello, world!", buffer.readUtf8()) + } + + @Test + fun writeToSpanningSegments() { + val buffer = Buffer() + buffer.writeUtf8("a".repeat(SEGMENT_SIZE * 2)) + buffer.writeUtf8("b".repeat(SEGMENT_SIZE * 2)) + val out = ByteArrayOutputStream() + buffer.skip(10) + buffer.writeTo(out, (SEGMENT_SIZE * 3).toLong()) + assertEquals( + "a".repeat(SEGMENT_SIZE * 2 - 10) + "b".repeat(SEGMENT_SIZE + 10), + out.toString(), + ) + assertEquals("b".repeat(SEGMENT_SIZE - 10), buffer.readUtf8(buffer.size)) + } + + @Test + fun writeToStream() { + val buffer = Buffer().writeUtf8("hello, world!") + val out = ByteArrayOutputStream() + buffer.writeTo(out) + val outString = out.toByteArray().toString(UTF_8) + assertEquals("hello, world!", outString) + assertEquals(0, buffer.size) + } + + @Test + fun readFromStream() { + val `in`: InputStream = ByteArrayInputStream("hello, world!".toByteArray(UTF_8)) + val buffer = Buffer() + buffer.readFrom(`in`) + val out = buffer.readUtf8() + assertEquals("hello, world!", out) + } + + @Test + fun readFromSpanningSegments() { + val `in`: InputStream = ByteArrayInputStream("hello, world!".toByteArray(UTF_8)) + val buffer = Buffer().writeUtf8("a".repeat(SEGMENT_SIZE - 10)) + buffer.readFrom(`in`) + val out = buffer.readUtf8() + assertEquals("a".repeat(SEGMENT_SIZE - 10) + "hello, world!", out) + } + + @Test + fun readFromStreamWithCount() { + val `in`: InputStream = ByteArrayInputStream("hello, world!".toByteArray(UTF_8)) + val buffer = Buffer() + buffer.readFrom(`in`, 10) + val out = buffer.readUtf8() + assertEquals("hello, wor", out) + } + + @Test + fun readFromDoesNotLeaveEmptyTailSegment() { + val buffer = Buffer() + buffer.readFrom(ByteArrayInputStream(ByteArray(SEGMENT_SIZE))) + assertNoEmptySegments(buffer) + } + + @Test + fun moveAllRequestedBytesWithRead() { + val sink = Buffer() + sink.writeUtf8("a".repeat(10)) + val source = Buffer() + source.writeUtf8("b".repeat(15)) + assertEquals(10, source.read(sink, 10)) + assertEquals(20, sink.size) + assertEquals(5, source.size) + assertEquals("a".repeat(10) + "b".repeat(10), sink.readUtf8(20)) + } + + @Test + fun moveFewerThanRequestedBytesWithRead() { + val sink = Buffer() + sink.writeUtf8("a".repeat(10)) + val source = Buffer() + source.writeUtf8("b".repeat(20)) + assertEquals(20, source.read(sink, 25)) + assertEquals(30, sink.size) + assertEquals(0, source.size) + assertEquals("a".repeat(10) + "b".repeat(20), sink.readUtf8(30)) + } + + @Test + fun indexOfWithOffset() { + val buffer = Buffer() + val halfSegment: Int = SEGMENT_SIZE / 2 + buffer.writeUtf8("a".repeat(halfSegment)) + buffer.writeUtf8("b".repeat(halfSegment)) + buffer.writeUtf8("c".repeat(halfSegment)) + buffer.writeUtf8("d".repeat(halfSegment)) + assertEquals(0, buffer.indexOf('a'.code.toByte(), 0)) + assertEquals((halfSegment - 1).toLong(), buffer.indexOf('a'.code.toByte(), (halfSegment - 1).toLong())) + assertEquals(halfSegment.toLong(), buffer.indexOf('b'.code.toByte(), (halfSegment - 1).toLong())) + assertEquals((halfSegment * 2).toLong(), buffer.indexOf('c'.code.toByte(), (halfSegment - 1).toLong())) + assertEquals((halfSegment * 3).toLong(), buffer.indexOf('d'.code.toByte(), (halfSegment - 1).toLong())) + assertEquals((halfSegment * 3).toLong(), buffer.indexOf('d'.code.toByte(), (halfSegment * 2).toLong())) + assertEquals((halfSegment * 3).toLong(), buffer.indexOf('d'.code.toByte(), (halfSegment * 3).toLong())) + assertEquals((halfSegment * 4 - 1).toLong(), buffer.indexOf('d'.code.toByte(), (halfSegment * 4 - 1).toLong())) + } + + @Test + fun byteAt() { + val buffer = Buffer() + buffer.writeUtf8("a") + buffer.writeUtf8("b".repeat(SEGMENT_SIZE)) + buffer.writeUtf8("c") + assertEquals('a'.code.toLong(), buffer[0].toLong()) + assertEquals('a'.code.toLong(), buffer[0].toLong()) // getByte doesn't mutate! + assertEquals('c'.code.toLong(), buffer[buffer.size - 1].toLong()) + assertEquals('b'.code.toLong(), buffer[buffer.size - 2].toLong()) + assertEquals('b'.code.toLong(), buffer[buffer.size - 3].toLong()) + } + + @Test + fun getByteOfEmptyBuffer() { + val buffer = Buffer() + try { + buffer[0] + fail() + } catch (expected: IndexOutOfBoundsException) { + } + } + + @Test + fun writePrefixToEmptyBuffer() { + val sink = Buffer() + val source = Buffer() + source.writeUtf8("abcd") + sink.write(source, 2) + assertEquals("ab", sink.readUtf8(2)) + } + + @Test + fun cloneDoesNotObserveWritesToOriginal() { + val original = Buffer() + val clone = original.clone() + original.writeUtf8("abc") + assertEquals(0, clone.size) + } + + @Test + fun cloneDoesNotObserveReadsFromOriginal() { + val original = Buffer() + original.writeUtf8("abc") + val clone = original.clone() + assertEquals("abc", original.readUtf8(3)) + assertEquals(3, clone.size) + assertEquals("ab", clone.readUtf8(2)) + } + + @Test + fun originalDoesNotObserveWritesToClone() { + val original = Buffer() + val clone = original.clone() + clone.writeUtf8("abc") + assertEquals(0, original.size) + } + + @Test + fun originalDoesNotObserveReadsFromClone() { + val original = Buffer() + original.writeUtf8("abc") + val clone = original.clone() + assertEquals("abc", clone.readUtf8(3)) + assertEquals(3, original.size) + assertEquals("ab", original.readUtf8(2)) + } + + @Test + fun cloneMultipleSegments() { + val original = Buffer() + original.writeUtf8("a".repeat(SEGMENT_SIZE * 3)) + val clone = original.clone() + original.writeUtf8("b".repeat(SEGMENT_SIZE * 3)) + clone.writeUtf8("c".repeat(SEGMENT_SIZE * 3)) + assertEquals( + "a".repeat(SEGMENT_SIZE * 3) + "b".repeat(SEGMENT_SIZE * 3), + original.readUtf8((SEGMENT_SIZE * 6).toLong()), + ) + assertEquals( + "a".repeat(SEGMENT_SIZE * 3) + "c".repeat(SEGMENT_SIZE * 3), + clone.readUtf8((SEGMENT_SIZE * 6).toLong()), + ) + } + + @Test + fun equalsAndHashCodeEmpty() { + val a = Buffer() + val b = Buffer() + assertEquals(a, b) + assertEquals(a.hashCode().toLong(), b.hashCode().toLong()) + } + + @Test + fun equalsAndHashCode() { + val a = Buffer().writeUtf8("dog") + val b = Buffer().writeUtf8("hotdog") + Assert.assertNotEquals(a, b) + Assert.assertNotEquals(a.hashCode().toLong(), b.hashCode().toLong()) + b.readUtf8(3) // Leaves b containing 'dog'. + assertEquals(a, b) + assertEquals(a.hashCode().toLong(), b.hashCode().toLong()) + } + + @Test + fun equalsAndHashCodeSpanningSegments() { + val data = ByteArray(1024 * 1024) + val dice = Random(0) + dice.nextBytes(data) + val a = bufferWithRandomSegmentLayout(dice, data) + val b = bufferWithRandomSegmentLayout(dice, data) + assertEquals(a, b) + assertEquals(a.hashCode().toLong(), b.hashCode().toLong()) + data[data.size / 2]++ // Change a single byte. + val c = bufferWithRandomSegmentLayout(dice, data) + Assert.assertNotEquals(a, c) + Assert.assertNotEquals(a.hashCode().toLong(), c.hashCode().toLong()) + } + + @Test + fun bufferInputStreamByteByByte() { + val source = Buffer() + source.writeUtf8("abc") + val `in` = source.inputStream() + assertEquals(3, `in`.available().toLong()) + assertEquals('a'.code.toLong(), `in`.read().toLong()) + assertEquals('b'.code.toLong(), `in`.read().toLong()) + assertEquals('c'.code.toLong(), `in`.read().toLong()) + assertEquals(-1, `in`.read().toLong()) + assertEquals(0, `in`.available().toLong()) + } + + @Test + fun bufferInputStreamBulkReads() { + val source = Buffer() + source.writeUtf8("abc") + val byteArray = ByteArray(4) + byteArray.fill(-5) + val `in` = source.inputStream() + assertEquals(3, `in`.read(byteArray).toLong()) + assertEquals("[97, 98, 99, -5]", Arrays.toString(byteArray)) + byteArray.fill(-7) + assertEquals(-1, `in`.read(byteArray).toLong()) + assertEquals("[-7, -7, -7, -7]", Arrays.toString(byteArray)) + } + + /** + * When writing data that's already buffered, there's no reason to page the + * data by segment. + */ + @Test + fun readAllWritesAllSegmentsAtOnce() { + val write1 = Buffer().writeUtf8( + "" + + "a".repeat(SEGMENT_SIZE) + + "b".repeat(SEGMENT_SIZE) + + "c".repeat(SEGMENT_SIZE), + ) + val source = Buffer().writeUtf8( + "" + + "a".repeat(SEGMENT_SIZE) + + "b".repeat(SEGMENT_SIZE) + + "c".repeat(SEGMENT_SIZE), + ) + val mockSink = MockSink() + assertEquals((SEGMENT_SIZE * 3).toLong(), source.readAll(mockSink)) + assertEquals(0, source.size) + mockSink.assertLog("write(" + write1 + ", " + write1.size + ")") + } + + @Test + fun writeAllMultipleSegments() { + val source = Buffer().writeUtf8("a".repeat(SEGMENT_SIZE * 3)) + val sink = Buffer() + assertEquals((SEGMENT_SIZE * 3).toLong(), sink.writeAll(source)) + assertEquals(0, source.size) + assertEquals("a".repeat(SEGMENT_SIZE * 3), sink.readUtf8()) + } + + @Test + fun copyTo() { + val source = Buffer() + source.writeUtf8("party") + val target = Buffer() + source.copyTo(target, 1, 3) + assertEquals("art", target.readUtf8()) + assertEquals("party", source.readUtf8()) + } + + @Test + fun copyToOnSegmentBoundary() { + val `as` = "a".repeat(SEGMENT_SIZE) + val bs = "b".repeat(SEGMENT_SIZE) + val cs = "c".repeat(SEGMENT_SIZE) + val ds = "d".repeat(SEGMENT_SIZE) + val source = Buffer() + source.writeUtf8(`as`) + source.writeUtf8(bs) + source.writeUtf8(cs) + val target = Buffer() + target.writeUtf8(ds) + source.copyTo(target, `as`.length.toLong(), (bs.length + cs.length).toLong()) + assertEquals(ds + bs + cs, target.readUtf8()) + } + + @Test + fun copyToOffSegmentBoundary() { + val `as` = "a".repeat(SEGMENT_SIZE - 1) + val bs = "b".repeat(SEGMENT_SIZE + 2) + val cs = "c".repeat(SEGMENT_SIZE - 4) + val ds = "d".repeat(SEGMENT_SIZE + 8) + val source = Buffer() + source.writeUtf8(`as`) + source.writeUtf8(bs) + source.writeUtf8(cs) + val target = Buffer() + target.writeUtf8(ds) + source.copyTo(target, `as`.length.toLong(), (bs.length + cs.length).toLong()) + assertEquals(ds + bs + cs, target.readUtf8()) + } + + @Test + fun copyToSourceAndTargetCanBeTheSame() { + val `as` = "a".repeat(SEGMENT_SIZE) + val bs = "b".repeat(SEGMENT_SIZE) + val source = Buffer() + source.writeUtf8(`as`) + source.writeUtf8(bs) + source.copyTo(source, 0, source.size) + assertEquals(`as` + bs + `as` + bs, source.readUtf8()) + } + + @Test + fun copyToEmptySource() { + val source = Buffer() + val target = Buffer().writeUtf8("aaa") + source.copyTo(target, 0L, 0L) + assertEquals("", source.readUtf8()) + assertEquals("aaa", target.readUtf8()) + } + + @Test + fun copyToEmptyTarget() { + val source = Buffer().writeUtf8("aaa") + val target = Buffer() + source.copyTo(target, 0L, 3L) + assertEquals("aaa", source.readUtf8()) + assertEquals("aaa", target.readUtf8()) + } + + @Test + fun snapshotReportsAccurateSize() { + val buf = Buffer().write(byteArrayOf(0, 1, 2, 3)) + assertEquals(1, buf.snapshot(1).size.toLong()) + } +} diff --git a/okio/src/jvmTest/kotlin/okio/BufferedSinkJavaTest.kt b/okio/src/jvmTest/kotlin/okio/BufferedSinkJavaTest.kt new file mode 100644 index 00000000..5faf7605 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/BufferedSinkJavaTest.kt @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.io.IOException +import okio.TestUtil.SEGMENT_SIZE +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Test + +/** + * Tests solely for the behavior of RealBufferedSink's implementation. For generic + * BufferedSink behavior use BufferedSinkTest. + */ +class BufferedSinkJavaTest { + @Test + fun inputStreamCloses() { + val sink = (Buffer() as Sink).buffer() + val out = sink.outputStream() + out.close() + try { + sink.writeUtf8("Hi!") + fail() + } catch (e: IllegalStateException) { + assertEquals("closed", e.message) + } + } + + @Test + fun bufferedSinkEmitsTailWhenItIsComplete() { + val sink = Buffer() + val bufferedSink = (sink as Sink).buffer() + bufferedSink.writeUtf8("a".repeat(SEGMENT_SIZE - 1)) + assertEquals(0, sink.size) + bufferedSink.writeByte(0) + assertEquals(SEGMENT_SIZE.toLong(), sink.size) + assertEquals(0, bufferedSink.buffer.size) + } + + @Test + fun bufferedSinkEmitMultipleSegments() { + val sink = Buffer() + val bufferedSink = (sink as Sink).buffer() + bufferedSink.writeUtf8("a".repeat(SEGMENT_SIZE * 4 - 1)) + assertEquals((SEGMENT_SIZE * 3).toLong(), sink.size) + assertEquals((SEGMENT_SIZE - 1).toLong(), bufferedSink.buffer.size) + } + + @Test + fun bufferedSinkFlush() { + val sink = Buffer() + val bufferedSink = (sink as Sink).buffer() + bufferedSink.writeByte('a'.code) + assertEquals(0, sink.size) + bufferedSink.flush() + assertEquals(0, bufferedSink.buffer.size) + assertEquals(1, sink.size) + } + + @Test + fun bytesEmittedToSinkWithFlush() { + val sink = Buffer() + val bufferedSink = (sink as Sink).buffer() + bufferedSink.writeUtf8("abc") + bufferedSink.flush() + assertEquals(3, sink.size) + } + + @Test + fun bytesNotEmittedToSinkWithoutFlush() { + val sink = Buffer() + val bufferedSink = (sink as Sink).buffer() + bufferedSink.writeUtf8("abc") + assertEquals(0, sink.size) + } + + @Test + fun bytesEmittedToSinkWithEmit() { + val sink = Buffer() + val bufferedSink = (sink as Sink).buffer() + bufferedSink.writeUtf8("abc") + bufferedSink.emit() + assertEquals(3, sink.size) + } + + @Test + fun completeSegmentsEmitted() { + val sink = Buffer() + val bufferedSink = (sink as Sink).buffer() + bufferedSink.writeUtf8("a".repeat(SEGMENT_SIZE * 3)) + assertEquals((SEGMENT_SIZE * 3).toLong(), sink.size) + } + + @Test + fun incompleteSegmentsNotEmitted() { + val sink = Buffer() + val bufferedSink = (sink as Sink).buffer() + bufferedSink.writeUtf8("a".repeat(SEGMENT_SIZE * 3 - 1)) + assertEquals((SEGMENT_SIZE * 2).toLong(), sink.size) + } + + @Test + fun closeWithExceptionWhenWriting() { + val mockSink = MockSink() + mockSink.scheduleThrow(0, IOException()) + val bufferedSink = mockSink.buffer() + bufferedSink.writeByte('a'.code) + try { + bufferedSink.close() + fail() + } catch (expected: IOException) { + } + mockSink.assertLog("write([text=a], 1)", "close()") + } + + @Test + fun closeWithExceptionWhenClosing() { + val mockSink = MockSink() + mockSink.scheduleThrow(1, IOException()) + val bufferedSink = mockSink.buffer() + bufferedSink.writeByte('a'.code) + try { + bufferedSink.close() + fail() + } catch (expected: IOException) { + } + mockSink.assertLog("write([text=a], 1)", "close()") + } + + @Test + fun closeWithExceptionWhenWritingAndClosing() { + val mockSink = MockSink() + mockSink.scheduleThrow(0, IOException("first")) + mockSink.scheduleThrow(1, IOException("second")) + val bufferedSink = mockSink.buffer() + bufferedSink.writeByte('a'.code) + try { + bufferedSink.close() + fail() + } catch (expected: IOException) { + assertEquals("first", expected.message) + } + mockSink.assertLog("write([text=a], 1)", "close()") + } + + @Test + fun operationsAfterClose() { + val mockSink = MockSink() + val bufferedSink = mockSink.buffer() + bufferedSink.writeByte('a'.code) + bufferedSink.close() + + // Test a sample set of methods. + try { + bufferedSink.writeByte('a'.code) + fail() + } catch (expected: IllegalStateException) { + } + try { + bufferedSink.write(ByteArray(10)) + fail() + } catch (expected: IllegalStateException) { + } + try { + bufferedSink.emitCompleteSegments() + fail() + } catch (expected: IllegalStateException) { + } + try { + bufferedSink.emit() + fail() + } catch (expected: IllegalStateException) { + } + try { + bufferedSink.flush() + fail() + } catch (expected: IllegalStateException) { + } + + // Test a sample set of methods on the OutputStream. + val os = bufferedSink.outputStream() + try { + os.write('a'.code) + fail() + } catch (expected: IOException) { + } + try { + os.write(ByteArray(10)) + fail() + } catch (expected: IOException) { + } + + // Permitted + os.flush() + } + + @Test + fun writeAll() { + val mockSink = MockSink() + val bufferedSink = mockSink.buffer() + bufferedSink.buffer.writeUtf8("abc") + assertEquals(3, bufferedSink.writeAll(Buffer().writeUtf8("def"))) + assertEquals(6, bufferedSink.buffer.size) + assertEquals("abcdef", bufferedSink.buffer.readUtf8(6)) + mockSink.assertLog() // No writes. + } + + @Test + fun writeAllExhausted() { + val mockSink = MockSink() + val bufferedSink = mockSink.buffer() + assertEquals(0, bufferedSink.writeAll(Buffer())) + assertEquals(0, bufferedSink.buffer.size) + mockSink.assertLog() // No writes. + } + + @Test + fun writeAllWritesOneSegmentAtATime() { + val write1 = Buffer().writeUtf8("a".repeat(SEGMENT_SIZE)) + val write2 = Buffer().writeUtf8("b".repeat(SEGMENT_SIZE)) + val write3 = Buffer().writeUtf8("c".repeat(SEGMENT_SIZE)) + val source = Buffer().writeUtf8( + "" + + "a".repeat(SEGMENT_SIZE) + + "b".repeat(SEGMENT_SIZE) + + "c".repeat(SEGMENT_SIZE), + ) + val mockSink = MockSink() + val bufferedSink = mockSink.buffer() + assertEquals((SEGMENT_SIZE * 3).toLong(), bufferedSink.writeAll(source)) + mockSink.assertLog( + "write(" + write1 + ", " + write1.size + ")", + "write(" + write2 + ", " + write2.size + ")", + "write(" + write3 + ", " + write3.size + ")", + ) + } +} diff --git a/okio/src/jvmTest/kotlin/okio/BufferedSinkTest.kt b/okio/src/jvmTest/kotlin/okio/BufferedSinkTest.kt new file mode 100644 index 00000000..c9b3d187 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/BufferedSinkTest.kt @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.io.EOFException +import java.math.BigInteger +import java.nio.ByteBuffer +import java.nio.charset.Charset +import kotlin.text.Charsets.UTF_8 +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.encodeUtf8 +import okio.TestUtil.SEGMENT_SIZE +import okio.TestUtil.segmentSizes +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@RunWith(Parameterized::class) +class BufferedSinkTest( + factory: Factory, +) { + interface Factory { + fun create(data: Buffer): BufferedSink + + companion object { + val BUFFER: Factory = object : Factory { + override fun create(data: Buffer) = data + override fun toString() = "Buffer" + } + val REAL_BUFFERED_SINK: Factory = object : Factory { + override fun create(data: Buffer) = (data as Sink).buffer() + override fun toString() = "RealBufferedSink" + } + } + } + + private val data: Buffer = Buffer() + private val sink: BufferedSink = factory.create(data) + + @Test + fun writeNothing() { + sink.writeUtf8("") + sink.flush() + assertEquals(0, data.size) + } + + @Test + fun writeBytes() { + sink.writeByte(0xab) + sink.writeByte(0xcd) + sink.flush() + assertEquals("[hex=abcd]", data.toString()) + } + + @Test + fun writeLastByteInSegment() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE - 1)) + sink.writeByte(0x20) + sink.writeByte(0x21) + sink.flush() + assertEquals(listOf(SEGMENT_SIZE, 1), segmentSizes(data)) + assertEquals("a".repeat(SEGMENT_SIZE - 1), data.readUtf8((SEGMENT_SIZE - 1).toLong())) + assertEquals("[text= !]", data.toString()) + } + + @Test + fun writeShort() { + sink.writeShort(0xabcd) + sink.writeShort(0x4321) + sink.flush() + assertEquals("[hex=abcd4321]", data.toString()) + } + + @Test + fun writeShortLe() { + sink.writeShortLe(0xcdab) + sink.writeShortLe(0x2143) + sink.flush() + assertEquals("[hex=abcd4321]", data.toString()) + } + + @Test + fun writeInt() { + sink.writeInt(-0x543210ff) + sink.writeInt(-0x789abcdf) + sink.flush() + assertEquals("[hex=abcdef0187654321]", data.toString()) + } + + @Test + fun writeLastIntegerInSegment() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE - 4)) + sink.writeInt(-0x543210ff) + sink.writeInt(-0x789abcdf) + sink.flush() + assertEquals(listOf(SEGMENT_SIZE, 4), segmentSizes(data)) + assertEquals("a".repeat(SEGMENT_SIZE - 4), data.readUtf8((SEGMENT_SIZE - 4).toLong())) + assertEquals("[hex=abcdef0187654321]", data.toString()) + } + + @Test + fun writeIntegerDoesNotQuiteFitInSegment() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE - 3)) + sink.writeInt(-0x543210ff) + sink.writeInt(-0x789abcdf) + sink.flush() + assertEquals(listOf(SEGMENT_SIZE - 3, 8), segmentSizes(data)) + assertEquals("a".repeat(SEGMENT_SIZE - 3), data.readUtf8((SEGMENT_SIZE - 3).toLong())) + assertEquals("[hex=abcdef0187654321]", data.toString()) + } + + @Test + fun writeIntLe() { + sink.writeIntLe(-0x543210ff) + sink.writeIntLe(-0x789abcdf) + sink.flush() + assertEquals("[hex=01efcdab21436587]", data.toString()) + } + + @Test + fun writeLong() { + sink.writeLong(-0x543210fe789abcdfL) + sink.writeLong(-0x350145414f4ea400L) + sink.flush() + assertEquals("[hex=abcdef0187654321cafebabeb0b15c00]", data.toString()) + } + + @Test + fun writeLongLe() { + sink.writeLongLe(-0x543210fe789abcdfL) + sink.writeLongLe(-0x350145414f4ea400L) + sink.flush() + assertEquals("[hex=2143658701efcdab005cb1b0bebafeca]", data.toString()) + } + + @Test + fun writeByteString() { + sink.write("təˈranəˌsôr".encodeUtf8()) + sink.flush() + assertEquals("74c999cb8872616ec999cb8c73c3b472".decodeHex(), data.readByteString()) + } + + @Test + fun writeByteStringOffset() { + sink.write("təˈranəˌsôr".encodeUtf8(), 5, 5) + sink.flush() + assertEquals("72616ec999".decodeHex(), data.readByteString()) + } + + @Test + fun writeSegmentedByteString() { + sink.write(Buffer().write("təˈranəˌsôr".encodeUtf8()).snapshot()) + sink.flush() + assertEquals("74c999cb8872616ec999cb8c73c3b472".decodeHex(), data.readByteString()) + } + + @Test + fun writeSegmentedByteStringOffset() { + sink.write(Buffer().write("təˈranəˌsôr".encodeUtf8()).snapshot(), 5, 5) + sink.flush() + assertEquals("72616ec999".decodeHex(), data.readByteString()) + } + + @Test + fun writeStringUtf8() { + sink.writeUtf8("təˈranəˌsôr") + sink.flush() + assertEquals("74c999cb8872616ec999cb8c73c3b472".decodeHex(), data.readByteString()) + } + + @Test + fun writeSubstringUtf8() { + sink.writeUtf8("təˈranəˌsôr", 3, 7) + sink.flush() + assertEquals("72616ec999".decodeHex(), data.readByteString()) + } + + @Test + fun writeStringWithCharset() { + sink.writeString("təˈranəˌsôr", Charset.forName("utf-32be")) + sink.flush() + assertEquals( + ( + "0000007400000259000002c800000072000000610000006e00000259" + + "000002cc00000073000000f400000072" + ).decodeHex(), + data.readByteString(), + ) + } + + @Test + fun writeSubstringWithCharset() { + sink.writeString("təˈranəˌsôr", 3, 7, Charset.forName("utf-32be")) + sink.flush() + assertEquals("00000072000000610000006e00000259".decodeHex(), data.readByteString()) + } + + @Test + fun writeUtf8SubstringWithCharset() { + sink.writeString("təˈranəˌsôr", 3, 7, Charset.forName("utf-8")) + sink.flush() + assertEquals("ranə".encodeUtf8(), data.readByteString()) + } + + @Test + fun writeAll() { + val source = Buffer().writeUtf8("abcdef") + assertEquals(6, sink.writeAll(source)) + assertEquals(0, source.size) + sink.flush() + assertEquals("abcdef", data.readUtf8()) + } + + @Test + fun writeSource() { + val source = Buffer().writeUtf8("abcdef") + + // Force resolution of the Source method overload. + sink.write((source as Source), 4) + sink.flush() + assertEquals("abcd", data.readUtf8()) + assertEquals("ef", source.readUtf8()) + } + + @Test + fun writeSourceReadsFully() { + val source: Source = object : ForwardingSource(Buffer()) { + override fun read(sink: Buffer, byteCount: Long): Long { + sink.writeUtf8("abcd") + return 4 + } + } + sink.write(source, 8) + sink.flush() + assertEquals("abcdabcd", data.readUtf8()) + } + + @Test + fun writeSourcePropagatesEof() { + val source: Source = Buffer().writeUtf8("abcd") + try { + sink.write(source, 8) + fail() + } catch (expected: EOFException) { + } + + // Ensure that whatever was available was correctly written. + sink.flush() + assertEquals("abcd", data.readUtf8()) + } + + @Test + fun writeSourceWithZeroIsNoOp() { + // This test ensures that a zero byte count never calls through to read the source. It may be + // tied to something like a socket which will potentially block trying to read a segment when + // ultimately we don't want any data. + val source: Source = object : ForwardingSource(Buffer()) { + override fun read(sink: Buffer, byteCount: Long): Long { + throw AssertionError() + } + } + sink.write(source, 0) + assertEquals(0, data.size) + } + + @Test + fun writeAllExhausted() { + val source = Buffer() + assertEquals(0, sink.writeAll(source)) + assertEquals(0, source.size) + } + + @Test + fun closeEmitsBufferedBytes() { + sink.writeByte('a'.code) + sink.close() + assertEquals('a'.code.toLong(), data.readByte().toLong()) + } + + @Test + fun outputStream() { + val out = sink.outputStream() + out.write('a'.code) + out.write("b".repeat(9998).toByteArray(UTF_8)) + out.write('c'.code) + out.flush() + assertEquals("a" + "b".repeat(9998) + "c", data.readUtf8()) + } + + @Test + fun outputStreamBounds() { + val out = sink.outputStream() + try { + out.write(ByteArray(100), 50, 51) + fail() + } catch (expected: ArrayIndexOutOfBoundsException) { + } + } + + @Test + fun longDecimalString() { + assertLongDecimalString(0) + assertLongDecimalString(Long.MIN_VALUE) + assertLongDecimalString(Long.MAX_VALUE) + for (i in 1..19) { + val value = BigInteger.valueOf(10L).pow(i).toLong() + assertLongDecimalString(value - 1) + assertLongDecimalString(value) + } + } + + private fun assertLongDecimalString(value: Long) { + sink.writeDecimalLong(value).writeUtf8("zzz").flush() + val expected = java.lang.Long.toString(value) + "zzz" + val actual = data.readUtf8() + assertEquals("$value expected $expected but was $actual", actual, expected) + } + + @Test + fun longHexString() { + assertLongHexString(0) + assertLongHexString(Long.MIN_VALUE) + assertLongHexString(Long.MAX_VALUE) + for (i in 0..62) { + assertLongHexString((1L shl i) - 1) + assertLongHexString(1L shl i) + } + } + + @Test + fun writeNioBuffer() { + val expected = "abcdefg" + val nioByteBuffer = ByteBuffer.allocate(1024) + nioByteBuffer.put("abcdefg".toByteArray(UTF_8)) + (nioByteBuffer as java.nio.Buffer).flip() // Cast necessary for Java 8. + val byteCount = sink.write(nioByteBuffer) + assertEquals(expected.length.toLong(), byteCount.toLong()) + assertEquals(expected.length.toLong(), nioByteBuffer.position().toLong()) + assertEquals(expected.length.toLong(), nioByteBuffer.limit().toLong()) + sink.flush() + assertEquals(expected, data.readUtf8()) + } + + @Test + fun writeLargeNioBufferWritesAllData() { + val expected = "a".repeat(SEGMENT_SIZE * 3) + val nioByteBuffer = ByteBuffer.allocate(SEGMENT_SIZE * 4) + nioByteBuffer.put("a".repeat(SEGMENT_SIZE * 3).toByteArray(UTF_8)) + (nioByteBuffer as java.nio.Buffer).flip() // Cast necessary for Java 8. + val byteCount = sink.write(nioByteBuffer) + assertEquals(expected.length.toLong(), byteCount.toLong()) + assertEquals(expected.length.toLong(), nioByteBuffer.position().toLong()) + assertEquals(expected.length.toLong(), nioByteBuffer.limit().toLong()) + sink.flush() + assertEquals(expected, data.readUtf8()) + } + + private fun assertLongHexString(value: Long) { + sink.writeHexadecimalUnsignedLong(value).writeUtf8("zzz").flush() + val expected = String.format("%x", value) + "zzz" + val actual = data.readUtf8() + assertEquals("$value expected $expected but was $actual", actual, expected) + } + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun parameters(): List<Array<Any>> = listOf( + arrayOf(Factory.BUFFER), + arrayOf(Factory.REAL_BUFFERED_SINK), + ) + } +} diff --git a/okio/src/jvmTest/kotlin/okio/BufferedSourceJavaTest.kt b/okio/src/jvmTest/kotlin/okio/BufferedSourceJavaTest.kt new file mode 100644 index 00000000..cd7f15ea --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/BufferedSourceJavaTest.kt @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.io.EOFException +import java.io.IOException +import kotlin.text.Charsets.UTF_8 +import okio.TestUtil.SEGMENT_SIZE +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Test + +/** + * Tests solely for the behavior of RealBufferedSource's implementation. For generic + * BufferedSource behavior use BufferedSourceTest. + */ +class BufferedSourceJavaTest { + @Test + fun inputStreamTracksSegments() { + val source = Buffer() + source.writeUtf8("a") + source.writeUtf8("b".repeat(SEGMENT_SIZE)) + source.writeUtf8("c") + val `in` = (source as Source).buffer().inputStream() + assertEquals(0, `in`.available().toLong()) + assertEquals((SEGMENT_SIZE + 2).toLong(), source.size) + + // Reading one byte buffers a full segment. + assertEquals('a'.code.toLong(), `in`.read().toLong()) + assertEquals((SEGMENT_SIZE - 1).toLong(), `in`.available().toLong()) + assertEquals(2, source.size) + + // Reading as much as possible reads the rest of that buffered segment. + val data = ByteArray(SEGMENT_SIZE * 2) + assertEquals((SEGMENT_SIZE - 1).toLong(), `in`.read(data, 0, data.size).toLong()) + assertEquals("b".repeat(SEGMENT_SIZE - 1), String(data, 0, SEGMENT_SIZE - 1, UTF_8)) + assertEquals(2, source.size) + + // Continuing to read buffers the next segment. + assertEquals('b'.code.toLong(), `in`.read().toLong()) + assertEquals(1, `in`.available().toLong()) + assertEquals(0, source.size) + + // Continuing to read reads from the buffer. + assertEquals('c'.code.toLong(), `in`.read().toLong()) + assertEquals(0, `in`.available().toLong()) + assertEquals(0, source.size) + + // Once we've exhausted the source, we're done. + assertEquals(-1, `in`.read().toLong()) + assertEquals(0, source.size) + } + + @Test + fun inputStreamCloses() { + val source = (Buffer() as Source).buffer() + val inputStream = source.inputStream() + inputStream.close() + try { + source.require(1) + fail() + } catch (e: IllegalStateException) { + assertEquals("closed", e.message) + } + } + + @Test + fun indexOfStopsReadingAtLimit() { + val buffer = Buffer().writeUtf8("abcdef") + val bufferedSource = object : ForwardingSource(buffer) { + override fun read(sink: Buffer, byteCount: Long): Long { + return super.read(sink, Math.min(1, byteCount)) + } + }.buffer() + assertEquals(6, buffer.size) + assertEquals(-1, bufferedSource.indexOf('e'.code.toByte(), 0, 4)) + assertEquals(2, buffer.size) + } + + @Test + fun requireTracksBufferFirst() { + val source = Buffer() + source.writeUtf8("bb") + val bufferedSource = (source as Source).buffer() + bufferedSource.buffer.writeUtf8("aa") + bufferedSource.require(2) + assertEquals(2, bufferedSource.buffer.size) + assertEquals(2, source.size) + } + + @Test + fun requireIncludesBufferBytes() { + val source = Buffer() + source.writeUtf8("b") + val bufferedSource = (source as Source).buffer() + bufferedSource.buffer.writeUtf8("a") + bufferedSource.require(2) + assertEquals("ab", bufferedSource.buffer.readUtf8(2)) + } + + @Test + fun requireInsufficientData() { + val source = Buffer() + source.writeUtf8("a") + val bufferedSource = (source as Source).buffer() + try { + bufferedSource.require(2) + fail() + } catch (expected: EOFException) { + } + } + + @Test + fun requireReadsOneSegmentAtATime() { + val source = Buffer() + source.writeUtf8("a".repeat(SEGMENT_SIZE)) + source.writeUtf8("b".repeat(SEGMENT_SIZE)) + val bufferedSource = (source as Source).buffer() + bufferedSource.require(2) + assertEquals(SEGMENT_SIZE.toLong(), source.size) + assertEquals(SEGMENT_SIZE.toLong(), bufferedSource.buffer.size) + } + + @Test + fun skipReadsOneSegmentAtATime() { + val source = Buffer() + source.writeUtf8("a".repeat(SEGMENT_SIZE)) + source.writeUtf8("b".repeat(SEGMENT_SIZE)) + val bufferedSource = (source as Source).buffer() + bufferedSource.skip(2) + assertEquals(SEGMENT_SIZE.toLong(), source.size) + assertEquals((SEGMENT_SIZE - 2).toLong(), bufferedSource.buffer.size) + } + + @Test + fun skipTracksBufferFirst() { + val source = Buffer() + source.writeUtf8("bb") + val bufferedSource = (source as Source).buffer() + bufferedSource.buffer.writeUtf8("aa") + bufferedSource.skip(2) + assertEquals(0, bufferedSource.buffer.size) + assertEquals(2, source.size) + } + + @Test + fun operationsAfterClose() { + val source = Buffer() + val bufferedSource = (source as Source).buffer() + bufferedSource.close() + + // Test a sample set of methods. + try { + bufferedSource.indexOf(1.toByte()) + fail() + } catch (expected: IllegalStateException) { + } + try { + bufferedSource.skip(1) + fail() + } catch (expected: IllegalStateException) { + } + try { + bufferedSource.readByte() + fail() + } catch (expected: IllegalStateException) { + } + try { + bufferedSource.readByteString(10) + fail() + } catch (expected: IllegalStateException) { + } + + // Test a sample set of methods on the InputStream. + val inputStream = bufferedSource.inputStream() + try { + inputStream.read() + fail() + } catch (expected: IOException) { + } + try { + inputStream.read(ByteArray(10)) + fail() + } catch (expected: IOException) { + } + } + + /** + * We don't want readAll to buffer an unbounded amount of data. Instead it + * should buffer a segment, write it, and repeat. + */ + @Test + fun readAllReadsOneSegmentAtATime() { + val write1 = Buffer().writeUtf8("a".repeat(SEGMENT_SIZE)) + val write2 = Buffer().writeUtf8("b".repeat(SEGMENT_SIZE)) + val write3 = Buffer().writeUtf8("c".repeat(SEGMENT_SIZE)) + val source = Buffer().writeUtf8( + "" + + "a".repeat(SEGMENT_SIZE) + + "b".repeat(SEGMENT_SIZE) + + "c".repeat(SEGMENT_SIZE), + ) + val mockSink = MockSink() + val bufferedSource = (source as Source).buffer() + assertEquals((SEGMENT_SIZE * 3).toLong(), bufferedSource.readAll(mockSink)) + mockSink.assertLog( + "write(" + write1 + ", " + write1.size + ")", + "write(" + write2 + ", " + write2.size + ")", + "write(" + write3 + ", " + write3.size + ")", + ) + } +} diff --git a/okio/src/jvmTest/kotlin/okio/BufferedSourceTest.kt b/okio/src/jvmTest/kotlin/okio/BufferedSourceTest.kt new file mode 100644 index 00000000..b30944b7 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/BufferedSourceTest.kt @@ -0,0 +1,1503 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.io.EOFException +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.util.Arrays +import kotlin.text.Charsets.US_ASCII +import kotlin.text.Charsets.UTF_8 +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.encodeUtf8 +import okio.Options.Companion.of +import okio.TestUtil.SEGMENT_SIZE +import okio.TestUtil.assertByteArrayEquals +import okio.TestUtil.assertByteArraysEquals +import okio.TestUtil.randomBytes +import okio.TestUtil.segmentSizes +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Assume +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@RunWith(Parameterized::class) +class BufferedSourceTest( + private val factory: Factory, +) { + interface Factory { + fun pipe(): Pipe + val isOneByteAtATime: Boolean + + companion object { + val BUFFER: Factory = object : Factory { + override fun pipe(): Pipe { + val buffer = Buffer() + return Pipe(buffer, buffer) + } + + override val isOneByteAtATime: Boolean get() = false + + override fun toString() = "Buffer" + } + + val REAL_BUFFERED_SOURCE: Factory = object : Factory { + override fun pipe(): Pipe { + val buffer = Buffer() + return Pipe( + sink = buffer, + source = (buffer as Source).buffer(), + ) + } + + override val isOneByteAtATime: Boolean get() = false + + override fun toString() = "RealBufferedSource" + } + + /** + * A factory deliberately written to create buffers whose internal segments are always 1 byte + * long. We like testing with these segments because are likely to trigger bugs! + */ + val ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE: Factory = object : Factory { + override fun pipe(): Pipe { + val buffer = Buffer() + return Pipe( + sink = buffer, + source = object : ForwardingSource(buffer) { + override fun read(sink: Buffer, byteCount: Long): Long { + // Read one byte into a new buffer, then clone it so that the segment is shared. + // Shared segments cannot be compacted so we'll get a long chain of short segments. + val box = Buffer() + val result = super.read(box, Math.min(byteCount, 1L)) + if (result > 0L) sink.write(box.clone(), result) + return result + } + }.buffer(), + ) + } + + override val isOneByteAtATime: Boolean get() = true + + override fun toString() = "OneByteAtATimeBufferedSource" + } + + val ONE_BYTE_AT_A_TIME_BUFFER: Factory = object : Factory { + override fun pipe(): Pipe { + val buffer = Buffer() + val sink = object : ForwardingSink(buffer) { + override fun write(source: Buffer, byteCount: Long) { + // Write each byte into a new buffer, then clone it so that the segments are shared. + // Shared segments cannot be compacted so we'll get a long chain of short segments. + for (i in 0 until byteCount) { + val box = Buffer() + box.write(source, 1) + super.write(box.clone(), 1) + } + } + }.buffer() + return Pipe( + sink = sink, + source = buffer, + ) + } + + override val isOneByteAtATime: Boolean get() = true + + override fun toString() = "OneByteAtATimeBuffer" + } + + val PEEK_BUFFER: Factory = object : Factory { + override fun pipe(): Pipe { + val buffer = Buffer() + return Pipe( + sink = buffer, + source = buffer.peek(), + ) + } + + override val isOneByteAtATime: Boolean get() = false + + override fun toString() = "PeekBuffer" + } + + val PEEK_BUFFERED_SOURCE: Factory = object : Factory { + override fun pipe(): Pipe { + val buffer = Buffer() + return Pipe( + sink = buffer, + source = (buffer as Source).buffer().peek(), + ) + } + + override val isOneByteAtATime: Boolean get() = false + + override fun toString() = "PeekBufferedSource" + } + } + } + + class Pipe( + var sink: BufferedSink, + var source: BufferedSource, + ) + + private val pipe = factory.pipe() + private val sink: BufferedSink = pipe.sink + private val source: BufferedSource = pipe.source + + @Test + fun readBytes() { + sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte())) + sink.emit() + assertEquals(0xab, (source.readByte().toInt() and 0xff).toLong()) + assertEquals(0xcd, (source.readByte().toInt() and 0xff).toLong()) + assertTrue(source.exhausted()) + } + + @Test + fun readByteTooShortThrows() { + try { + source.readByte() + fail() + } catch (expected: EOFException) { + } + } + + @Test + fun readShort() { + sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte(), 0xef.toByte(), 0x01.toByte())) + sink.emit() + assertEquals(0xabcd.toShort().toLong(), source.readShort().toLong()) + assertEquals(0xef01.toShort().toLong(), source.readShort().toLong()) + assertTrue(source.exhausted()) + } + + @Test + fun readShortLe() { + sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte(), 0xef.toByte(), 0x10.toByte())) + sink.emit() + assertEquals(0xcdab.toShort().toLong(), source.readShortLe().toLong()) + assertEquals(0x10ef.toShort().toLong(), source.readShortLe().toLong()) + assertTrue(source.exhausted()) + } + + @Test + fun readShortSplitAcrossMultipleSegments() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE - 1)) + sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte())) + sink.emit() + source.skip((SEGMENT_SIZE - 1).toLong()) + assertEquals(0xabcd.toShort().toLong(), source.readShort().toLong()) + assertTrue(source.exhausted()) + } + + @Test + fun readShortTooShortThrows() { + sink.writeShort(Short.MAX_VALUE.toInt()) + sink.emit() + source.readByte() + try { + source.readShort() + fail() + } catch (expected: EOFException) { + } + } + + @Test + fun readShortLeTooShortThrows() { + sink.writeShortLe(Short.MAX_VALUE.toInt()) + sink.emit() + source.readByte() + try { + source.readShortLe() + fail() + } catch (expected: EOFException) { + } + } + + @Test + fun readInt() { + sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte(), 0xef.toByte(), 0x01.toByte(), 0x87.toByte(), 0x65.toByte(), 0x43.toByte(), 0x21.toByte())) + sink.emit() + assertEquals(-0x543210ff, source.readInt().toLong()) + assertEquals(-0x789abcdf, source.readInt().toLong()) + assertTrue(source.exhausted()) + } + + @Test + fun readIntLe() { + sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte(), 0xef.toByte(), 0x10.toByte(), 0x87.toByte(), 0x65.toByte(), 0x43.toByte(), 0x21.toByte())) + sink.emit() + assertEquals(0x10efcdab, source.readIntLe().toLong()) + assertEquals(0x21436587, source.readIntLe().toLong()) + assertTrue(source.exhausted()) + } + + @Test + fun readIntSplitAcrossMultipleSegments() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE - 3)) + sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte(), 0xef.toByte(), 0x01.toByte())) + sink.emit() + source.skip((SEGMENT_SIZE - 3).toLong()) + assertEquals(-0x543210ff, source.readInt().toLong()) + assertTrue(source.exhausted()) + } + + @Test + fun readIntTooShortThrows() { + sink.writeInt(Int.MAX_VALUE) + sink.emit() + source.readByte() + try { + source.readInt() + fail() + } catch (expected: EOFException) { + } + } + + @Test + fun readIntLeTooShortThrows() { + sink.writeIntLe(Int.MAX_VALUE) + sink.emit() + source.readByte() + try { + source.readIntLe() + fail() + } catch (expected: EOFException) { + } + } + + @Test + fun readLong() { + sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte(), 0xef.toByte(), 0x10.toByte(), 0x87.toByte(), 0x65.toByte(), 0x43.toByte(), 0x21.toByte(), 0x36.toByte(), 0x47.toByte(), 0x58.toByte(), 0x69.toByte(), 0x12.toByte(), 0x23.toByte(), 0x34.toByte(), 0x45.toByte())) + sink.emit() + assertEquals(-0x543210ef789abcdfL, source.readLong()) + assertEquals(0x3647586912233445L, source.readLong()) + assertTrue(source.exhausted()) + } + + @Test + fun readLongLe() { + sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte(), 0xef.toByte(), 0x10.toByte(), 0x87.toByte(), 0x65.toByte(), 0x43.toByte(), 0x21.toByte(), 0x36.toByte(), 0x47.toByte(), 0x58.toByte(), 0x69.toByte(), 0x12.toByte(), 0x23.toByte(), 0x34.toByte(), 0x45.toByte())) + sink.emit() + assertEquals(0x2143658710efcdabL, source.readLongLe()) + assertEquals(0x4534231269584736L, source.readLongLe()) + assertTrue(source.exhausted()) + } + + @Test + fun readLongSplitAcrossMultipleSegments() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE - 7)) + sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte(), 0xef.toByte(), 0x01.toByte(), 0x87.toByte(), 0x65.toByte(), 0x43.toByte(), 0x21.toByte())) + sink.emit() + source.skip((SEGMENT_SIZE - 7).toLong()) + assertEquals(-0x543210fe789abcdfL, source.readLong()) + assertTrue(source.exhausted()) + } + + @Test + fun readLongTooShortThrows() { + sink.writeLong(Long.MAX_VALUE) + sink.emit() + source.readByte() + try { + source.readLong() + fail() + } catch (expected: EOFException) { + } + } + + @Test + fun readLongLeTooShortThrows() { + sink.writeLongLe(Long.MAX_VALUE) + sink.emit() + source.readByte() + try { + source.readLongLe() + fail() + } catch (expected: EOFException) { + } + } + + @Test + fun readAll() { + source.buffer.writeUtf8("abc") + sink.writeUtf8("def") + sink.emit() + val sink = Buffer() + assertEquals(6, source.readAll(sink)) + assertEquals("abcdef", sink.readUtf8()) + assertTrue(source.exhausted()) + } + + @Test + fun readAllExhausted() { + val mockSink = MockSink() + assertEquals(0, source.readAll(mockSink)) + assertTrue(source.exhausted()) + mockSink.assertLog() + } + + @Test + fun readExhaustedSource() { + val sink = Buffer() + sink.writeUtf8("a".repeat(10)) + assertEquals(-1, source.read(sink, 10)) + assertEquals(10, sink.size) + assertTrue(source.exhausted()) + } + + @Test + fun readZeroBytesFromSource() { + val sink = Buffer() + sink.writeUtf8("a".repeat(10)) + + // Either 0 or -1 is reasonable here. For consistency with Android's + // ByteArrayInputStream we return 0. + assertEquals(-1, source.read(sink, 0)) + assertEquals(10, sink.size) + assertTrue(source.exhausted()) + } + + @Test + fun readFully() { + sink.writeUtf8("a".repeat(10000)) + sink.emit() + val sink = Buffer() + source.readFully(sink, 9999) + assertEquals("a".repeat(9999), sink.readUtf8()) + assertEquals("a", source.readUtf8()) + } + + @Test + fun readFullyTooShortThrows() { + sink.writeUtf8("Hi") + sink.emit() + val sink = Buffer() + try { + source.readFully(sink, 5) + fail() + } catch (ignored: EOFException) { + } + + // Verify we read all that we could from the source. + assertEquals("Hi", sink.readUtf8()) + } + + @Test + fun readFullyByteArray() { + val data = Buffer() + data.writeUtf8("Hello").writeUtf8("e".repeat(SEGMENT_SIZE)) + val expected = data.clone().readByteArray() + sink.write(data, data.size) + sink.emit() + val sink = ByteArray(SEGMENT_SIZE + 5) + source.readFully(sink) + assertByteArraysEquals(expected, sink) + } + + @Test + fun readFullyByteArrayTooShortThrows() { + sink.writeUtf8("Hello") + sink.emit() + val array = ByteArray(6) + try { + source.readFully(array) + fail() + } catch (ignored: EOFException) { + } + + // Verify we read all that we could from the source. + assertByteArraysEquals(byteArrayOf('H'.code.toByte(), 'e'.code.toByte(), 'l'.code.toByte(), 'l'.code.toByte(), 'o'.code.toByte(), 0), array) + } + + @Test + fun readIntoByteArray() { + sink.writeUtf8("abcd") + sink.emit() + val sink = ByteArray(3) + val read = source.read(sink) + if (factory.isOneByteAtATime) { + assertEquals(1, read.toLong()) + val expected = byteArrayOf('a'.code.toByte(), 0, 0) + assertByteArraysEquals(expected, sink) + } else { + assertEquals(3, read.toLong()) + val expected = byteArrayOf('a'.code.toByte(), 'b'.code.toByte(), 'c'.code.toByte()) + assertByteArraysEquals(expected, sink) + } + } + + @Test + fun readIntoByteArrayNotEnough() { + sink.writeUtf8("abcd") + sink.emit() + val sink = ByteArray(5) + val read = source.read(sink) + if (factory.isOneByteAtATime) { + assertEquals(1, read.toLong()) + val expected = byteArrayOf('a'.code.toByte(), 0, 0, 0, 0) + assertByteArraysEquals(expected, sink) + } else { + assertEquals(4, read.toLong()) + val expected = byteArrayOf('a'.code.toByte(), 'b'.code.toByte(), 'c'.code.toByte(), 'd'.code.toByte(), 0) + assertByteArraysEquals(expected, sink) + } + } + + @Test + fun readIntoByteArrayOffsetAndCount() { + sink.writeUtf8("abcd") + sink.emit() + val sink = ByteArray(7) + val read = source.read(sink, 2, 3) + if (factory.isOneByteAtATime) { + assertEquals(1, read.toLong()) + val expected = byteArrayOf(0, 0, 'a'.code.toByte(), 0, 0, 0, 0) + assertByteArraysEquals(expected, sink) + } else { + assertEquals(3, read.toLong()) + val expected = byteArrayOf(0, 0, 'a'.code.toByte(), 'b'.code.toByte(), 'c'.code.toByte(), 0, 0) + assertByteArraysEquals(expected, sink) + } + } + + @Test + fun readByteArray() { + val string = "abcd" + "e".repeat(SEGMENT_SIZE) + sink.writeUtf8(string) + sink.emit() + assertByteArraysEquals(string.toByteArray(UTF_8), source.readByteArray()) + } + + @Test + fun readByteArrayPartial() { + sink.writeUtf8("abcd") + sink.emit() + assertEquals("[97, 98, 99]", Arrays.toString(source.readByteArray(3))) + assertEquals("d", source.readUtf8(1)) + } + + @Test + fun readByteArrayTooShortThrows() { + sink.writeUtf8("abc") + sink.emit() + try { + source.readByteArray(4) + fail() + } catch (expected: EOFException) { + } + assertEquals("abc", source.readUtf8()) // The read shouldn't consume any data. + } + + @Test + fun readByteString() { + sink.writeUtf8("abcd").writeUtf8("e".repeat(SEGMENT_SIZE)) + sink.emit() + assertEquals("abcd" + "e".repeat(SEGMENT_SIZE), source.readByteString().utf8()) + } + + @Test + fun readByteStringPartial() { + sink.writeUtf8("abcd").writeUtf8("e".repeat(SEGMENT_SIZE)) + sink.emit() + assertEquals("abc", source.readByteString(3).utf8()) + assertEquals("d", source.readUtf8(1)) + } + + @Test + fun readByteStringTooShortThrows() { + sink.writeUtf8("abc") + sink.emit() + try { + source.readByteString(4) + fail() + } catch (expected: EOFException) { + } + assertEquals("abc", source.readUtf8()) // The read shouldn't consume any data. + } + + @Test + fun readSpecificCharsetPartial() { + sink.write( + ( + "0000007600000259000002c80000006c000000e40000007300000259" + + "000002cc000000720000006100000070000000740000025900000072" + ).decodeHex(), + ) + sink.emit() + assertEquals("vəˈläsə", source.readString((7 * 4).toLong(), Charset.forName("utf-32"))) + } + + @Test + fun readSpecificCharset() { + sink.write( + ( + "0000007600000259000002c80000006c000000e40000007300000259" + + "000002cc000000720000006100000070000000740000025900000072" + ).decodeHex(), + ) + sink.emit() + assertEquals("vəˈläsəˌraptər", source.readString(Charset.forName("utf-32"))) + } + + @Test + fun readStringTooShortThrows() { + sink.writeString("abc", US_ASCII) + sink.emit() + try { + source.readString(4, US_ASCII) + fail() + } catch (expected: EOFException) { + } + assertEquals("abc", source.readUtf8()) // The read shouldn't consume any data. + } + + @Test + fun readUtf8SpansSegments() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE * 2)) + sink.emit() + source.skip((SEGMENT_SIZE - 1).toLong()) + assertEquals("aa", source.readUtf8(2)) + } + + @Test + fun readUtf8Segment() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE)) + sink.emit() + assertEquals("a".repeat(SEGMENT_SIZE), source.readUtf8(SEGMENT_SIZE.toLong())) + } + + @Test + fun readUtf8PartialBuffer() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE + 20)) + sink.emit() + assertEquals("a".repeat(SEGMENT_SIZE + 10), source.readUtf8((SEGMENT_SIZE + 10).toLong())) + } + + @Test + fun readUtf8EntireBuffer() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE * 2)) + sink.emit() + assertEquals("a".repeat(SEGMENT_SIZE * 2), source.readUtf8()) + } + + @Test + fun readUtf8TooShortThrows() { + sink.writeUtf8("abc") + sink.emit() + try { + source.readUtf8(4L) + fail() + } catch (expected: EOFException) { + } + assertEquals("abc", source.readUtf8()) // The read shouldn't consume any data. + } + + @Test + fun skip() { + sink.writeUtf8("a") + sink.writeUtf8("b".repeat(SEGMENT_SIZE)) + sink.writeUtf8("c") + sink.emit() + source.skip(1) + assertEquals('b'.code.toLong(), (source.readByte().toInt() and 0xff).toLong()) + source.skip((SEGMENT_SIZE - 2).toLong()) + assertEquals('b'.code.toLong(), (source.readByte().toInt() and 0xff).toLong()) + source.skip(1) + assertTrue(source.exhausted()) + } + + @Test + fun skipInsufficientData() { + sink.writeUtf8("a") + sink.emit() + try { + source.skip(2) + fail() + } catch (ignored: EOFException) { + } + } + + @Test + fun indexOf() { + // The segment is empty. + assertEquals(-1, source.indexOf('a'.code.toByte())) + + // The segment has one value. + sink.writeUtf8("a") // a + sink.emit() + assertEquals(0, source.indexOf('a'.code.toByte())) + assertEquals(-1, source.indexOf('b'.code.toByte())) + + // The segment has lots of data. + sink.writeUtf8("b".repeat(SEGMENT_SIZE - 2)) // ab...b + sink.emit() + assertEquals(0, source.indexOf('a'.code.toByte())) + assertEquals(1, source.indexOf('b'.code.toByte())) + assertEquals(-1, source.indexOf('c'.code.toByte())) + + // The segment doesn't start at 0, it starts at 2. + source.skip(2) // b...b + assertEquals(-1, source.indexOf('a'.code.toByte())) + assertEquals(0, source.indexOf('b'.code.toByte())) + assertEquals(-1, source.indexOf('c'.code.toByte())) + + // The segment is full. + sink.writeUtf8("c") // b...bc + sink.emit() + assertEquals(-1, source.indexOf('a'.code.toByte())) + assertEquals(0, source.indexOf('b'.code.toByte())) + assertEquals((SEGMENT_SIZE - 3).toLong(), source.indexOf('c'.code.toByte())) + + // The segment doesn't start at 2, it starts at 4. + source.skip(2) // b...bc + assertEquals(-1, source.indexOf('a'.code.toByte())) + assertEquals(0, source.indexOf('b'.code.toByte())) + assertEquals((SEGMENT_SIZE - 5).toLong(), source.indexOf('c'.code.toByte())) + + // Two segments. + sink.writeUtf8("d") // b...bcd, d is in the 2nd segment. + sink.emit() + assertEquals((SEGMENT_SIZE - 4).toLong(), source.indexOf('d'.code.toByte())) + assertEquals(-1, source.indexOf('e'.code.toByte())) + } + + @Test + fun indexOfByteWithStartOffset() { + sink.writeUtf8("a").writeUtf8("b".repeat(SEGMENT_SIZE)).writeUtf8("c") + sink.emit() + assertEquals(-1, source.indexOf('a'.code.toByte(), 1)) + assertEquals(15, source.indexOf('b'.code.toByte(), 15)) + } + + @Test + fun indexOfByteWithBothOffsets() { + if (factory.isOneByteAtATime) { + // When run on Travis this causes out-of-memory errors. + return + } + val a = 'a'.code.toByte() + val c = 'c'.code.toByte() + val size: Int = SEGMENT_SIZE * 5 + val bytes = ByteArray(size) + Arrays.fill(bytes, a) + + // These are tricky places where the buffer + // starts, ends, or segments come together. + val points = intArrayOf( + 0, 1, 2, + SEGMENT_SIZE - 1, SEGMENT_SIZE, SEGMENT_SIZE + 1, + size / 2 - 1, size / 2, size / 2 + 1, + size - SEGMENT_SIZE - 1, size - SEGMENT_SIZE, size - SEGMENT_SIZE + 1, + size - 3, size - 2, size - 1, + ) + + // In each iteration, we write c to the known point and then search for it using different + // windows. Some of the windows don't overlap with c's position, and therefore a match shouldn't + // be found. + for (p in points) { + bytes[p] = c + sink.write(bytes) + sink.emit() + assertEquals(p.toLong(), source.indexOf(c, 0, size.toLong())) + assertEquals(p.toLong(), source.indexOf(c, 0, (p + 1).toLong())) + assertEquals(p.toLong(), source.indexOf(c, p.toLong(), size.toLong())) + assertEquals(p.toLong(), source.indexOf(c, p.toLong(), (p + 1).toLong())) + assertEquals(p.toLong(), source.indexOf(c, (p / 2).toLong(), (p * 2 + 1).toLong())) + assertEquals(-1, source.indexOf(c, 0, (p / 2).toLong())) + assertEquals(-1, source.indexOf(c, 0, p.toLong())) + assertEquals(-1, source.indexOf(c, 0, 0)) + assertEquals(-1, source.indexOf(c, p.toLong(), p.toLong())) + + // Reset. + source.readUtf8() + bytes[p] = a + } + } + + @Test + fun indexOfByteInvalidBoundsThrows() { + sink.writeUtf8("abc") + sink.emit() + try { + source.indexOf('a'.code.toByte(), -1) + fail("Expected failure: fromIndex < 0") + } catch (expected: IllegalArgumentException) { + } + try { + source.indexOf('a'.code.toByte(), 10, 0) + fail("Expected failure: fromIndex > toIndex") + } catch (expected: IllegalArgumentException) { + } + } + + @Test + fun indexOfByteString() { + assertEquals(-1, source.indexOf("flop".encodeUtf8())) + sink.writeUtf8("flip flop") + sink.emit() + assertEquals(5, source.indexOf("flop".encodeUtf8())) + source.readUtf8() // Clear stream. + + // Make sure we backtrack and resume searching after partial match. + sink.writeUtf8("hi hi hi hey") + sink.emit() + assertEquals(3, source.indexOf("hi hi hey".encodeUtf8())) + } + + @Test + fun indexOfByteStringAtSegmentBoundary() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE - 1)) + sink.writeUtf8("bcd") + sink.emit() + assertEquals((SEGMENT_SIZE - 3).toLong(), source.indexOf("aabc".encodeUtf8(), (SEGMENT_SIZE - 4).toLong())) + assertEquals((SEGMENT_SIZE - 3).toLong(), source.indexOf("aabc".encodeUtf8(), (SEGMENT_SIZE - 3).toLong())) + assertEquals((SEGMENT_SIZE - 2).toLong(), source.indexOf("abcd".encodeUtf8(), (SEGMENT_SIZE - 2).toLong())) + assertEquals((SEGMENT_SIZE - 2).toLong(), source.indexOf("abc".encodeUtf8(), (SEGMENT_SIZE - 2).toLong())) + assertEquals((SEGMENT_SIZE - 2).toLong(), source.indexOf("abc".encodeUtf8(), (SEGMENT_SIZE - 2).toLong())) + assertEquals((SEGMENT_SIZE - 2).toLong(), source.indexOf("ab".encodeUtf8(), (SEGMENT_SIZE - 2).toLong())) + assertEquals((SEGMENT_SIZE - 2).toLong(), source.indexOf("a".encodeUtf8(), (SEGMENT_SIZE - 2).toLong())) + assertEquals((SEGMENT_SIZE - 1).toLong(), source.indexOf("bc".encodeUtf8(), (SEGMENT_SIZE - 2).toLong())) + assertEquals((SEGMENT_SIZE - 1).toLong(), source.indexOf("b".encodeUtf8(), (SEGMENT_SIZE - 2).toLong())) + assertEquals(SEGMENT_SIZE.toLong(), source.indexOf("c".encodeUtf8(), (SEGMENT_SIZE - 2).toLong())) + assertEquals(SEGMENT_SIZE.toLong(), source.indexOf("c".encodeUtf8(), SEGMENT_SIZE.toLong())) + assertEquals((SEGMENT_SIZE + 1).toLong(), source.indexOf("d".encodeUtf8(), (SEGMENT_SIZE - 2).toLong())) + assertEquals((SEGMENT_SIZE + 1).toLong(), source.indexOf("d".encodeUtf8(), (SEGMENT_SIZE + 1).toLong())) + } + + @Test + fun indexOfDoesNotWrapAround() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE - 1)) + sink.writeUtf8("bcd") + sink.emit() + assertEquals(-1, source.indexOf("abcda".encodeUtf8(), (SEGMENT_SIZE - 3).toLong())) + } + + @Test + fun indexOfByteStringWithOffset() { + assertEquals(-1, source.indexOf("flop".encodeUtf8(), 1)) + sink.writeUtf8("flop flip flop") + sink.emit() + assertEquals(10, source.indexOf("flop".encodeUtf8(), 1)) + source.readUtf8() // Clear stream + + // Make sure we backtrack and resume searching after partial match. + sink.writeUtf8("hi hi hi hi hey") + sink.emit() + assertEquals(6, source.indexOf("hi hi hey".encodeUtf8(), 1)) + } + + @Test + fun indexOfByteStringInvalidArgumentsThrows() { + try { + source.indexOf(ByteString.of()) + fail() + } catch (e: IllegalArgumentException) { + assertEquals("bytes is empty", e.message) + } + try { + source.indexOf("hi".encodeUtf8(), -1) + fail() + } catch (e: IllegalArgumentException) { + assertEquals("fromIndex < 0: -1", e.message) + } + } + + /** + * With [Factory.ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE], this code was extremely slow. + * https://github.com/square/okio/issues/171 + */ + @Test + fun indexOfByteStringAcrossSegmentBoundaries() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE * 2 - 3)) + sink.writeUtf8("bcdefg") + sink.emit() + assertEquals((SEGMENT_SIZE * 2 - 4).toLong(), source.indexOf("ab".encodeUtf8())) + assertEquals((SEGMENT_SIZE * 2 - 4).toLong(), source.indexOf("abc".encodeUtf8())) + assertEquals((SEGMENT_SIZE * 2 - 4).toLong(), source.indexOf("abcd".encodeUtf8())) + assertEquals((SEGMENT_SIZE * 2 - 4).toLong(), source.indexOf("abcde".encodeUtf8())) + assertEquals((SEGMENT_SIZE * 2 - 4).toLong(), source.indexOf("abcdef".encodeUtf8())) + assertEquals((SEGMENT_SIZE * 2 - 4).toLong(), source.indexOf("abcdefg".encodeUtf8())) + assertEquals((SEGMENT_SIZE * 2 - 3).toLong(), source.indexOf("bcdefg".encodeUtf8())) + assertEquals((SEGMENT_SIZE * 2 - 2).toLong(), source.indexOf("cdefg".encodeUtf8())) + assertEquals((SEGMENT_SIZE * 2 - 1).toLong(), source.indexOf("defg".encodeUtf8())) + assertEquals((SEGMENT_SIZE * 2).toLong(), source.indexOf("efg".encodeUtf8())) + assertEquals((SEGMENT_SIZE * 2 + 1).toLong(), source.indexOf("fg".encodeUtf8())) + assertEquals((SEGMENT_SIZE * 2 + 2).toLong(), source.indexOf("g".encodeUtf8())) + } + + @Test + fun indexOfElement() { + sink.writeUtf8("a").writeUtf8("b".repeat(SEGMENT_SIZE)).writeUtf8("c") + sink.emit() + assertEquals(0, source.indexOfElement("DEFGaHIJK".encodeUtf8())) + assertEquals(1, source.indexOfElement("DEFGHIJKb".encodeUtf8())) + assertEquals((SEGMENT_SIZE + 1).toLong(), source.indexOfElement("cDEFGHIJK".encodeUtf8())) + assertEquals(1, source.indexOfElement("DEFbGHIc".encodeUtf8())) + assertEquals(-1L, source.indexOfElement("DEFGHIJK".encodeUtf8())) + assertEquals(-1L, source.indexOfElement("".encodeUtf8())) + } + + @Test + fun indexOfElementWithOffset() { + sink.writeUtf8("a").writeUtf8("b".repeat(SEGMENT_SIZE)).writeUtf8("c") + sink.emit() + assertEquals(-1, source.indexOfElement("DEFGaHIJK".encodeUtf8(), 1)) + assertEquals(15, source.indexOfElement("DEFGHIJKb".encodeUtf8(), 15)) + } + + @Test + fun indexOfByteWithFromIndex() { + sink.writeUtf8("aaa") + sink.emit() + assertEquals(0, source.indexOf('a'.code.toByte())) + assertEquals(0, source.indexOf('a'.code.toByte(), 0)) + assertEquals(1, source.indexOf('a'.code.toByte(), 1)) + assertEquals(2, source.indexOf('a'.code.toByte(), 2)) + } + + @Test + fun indexOfByteStringWithFromIndex() { + sink.writeUtf8("aaa") + sink.emit() + assertEquals(0, source.indexOf("a".encodeUtf8())) + assertEquals(0, source.indexOf("a".encodeUtf8(), 0)) + assertEquals(1, source.indexOf("a".encodeUtf8(), 1)) + assertEquals(2, source.indexOf("a".encodeUtf8(), 2)) + } + + @Test + fun indexOfElementWithFromIndex() { + sink.writeUtf8("aaa") + sink.emit() + assertEquals(0, source.indexOfElement("a".encodeUtf8())) + assertEquals(0, source.indexOfElement("a".encodeUtf8(), 0)) + assertEquals(1, source.indexOfElement("a".encodeUtf8(), 1)) + assertEquals(2, source.indexOfElement("a".encodeUtf8(), 2)) + } + + @Test + fun request() { + sink.writeUtf8("a").writeUtf8("b".repeat(SEGMENT_SIZE)).writeUtf8("c") + sink.emit() + assertTrue(source.request((SEGMENT_SIZE + 2).toLong())) + assertFalse(source.request((SEGMENT_SIZE + 3).toLong())) + } + + @Test + fun require() { + sink.writeUtf8("a").writeUtf8("b".repeat(SEGMENT_SIZE)).writeUtf8("c") + sink.emit() + source.require((SEGMENT_SIZE + 2).toLong()) + try { + source.require((SEGMENT_SIZE + 3).toLong()) + fail() + } catch (expected: EOFException) { + } + } + + @Test + fun inputStream() { + sink.writeUtf8("abc") + sink.emit() + val `in` = source.inputStream() + val bytes = byteArrayOf('z'.code.toByte(), 'z'.code.toByte(), 'z'.code.toByte()) + var read = `in`.read(bytes) + if (factory.isOneByteAtATime) { + assertEquals(1, read.toLong()) + assertByteArrayEquals("azz", bytes) + read = `in`.read(bytes) + assertEquals(1, read.toLong()) + assertByteArrayEquals("bzz", bytes) + read = `in`.read(bytes) + assertEquals(1, read.toLong()) + assertByteArrayEquals("czz", bytes) + } else { + assertEquals(3, read.toLong()) + assertByteArrayEquals("abc", bytes) + } + assertEquals(-1, `in`.read().toLong()) + } + + @Test + fun inputStreamOffsetCount() { + sink.writeUtf8("abcde") + sink.emit() + val `in` = source.inputStream() + val bytes = byteArrayOf('z'.code.toByte(), 'z'.code.toByte(), 'z'.code.toByte(), 'z'.code.toByte(), 'z'.code.toByte()) + val read = `in`.read(bytes, 1, 3) + if (factory.isOneByteAtATime) { + assertEquals(1, read.toLong()) + assertByteArrayEquals("zazzz", bytes) + } else { + assertEquals(3, read.toLong()) + assertByteArrayEquals("zabcz", bytes) + } + } + + @Test + fun inputStreamSkip() { + sink.writeUtf8("abcde") + sink.emit() + val `in` = source.inputStream() + assertEquals(4, `in`.skip(4)) + assertEquals('e'.code.toLong(), `in`.read().toLong()) + sink.writeUtf8("abcde") + sink.emit() + assertEquals(5, `in`.skip(10)) // Try to skip too much. + assertEquals(0, `in`.skip(1)) // Try to skip when exhausted. + } + + @Test + fun inputStreamCharByChar() { + sink.writeUtf8("abc") + sink.emit() + val `in` = source.inputStream() + assertEquals('a'.code.toLong(), `in`.read().toLong()) + assertEquals('b'.code.toLong(), `in`.read().toLong()) + assertEquals('c'.code.toLong(), `in`.read().toLong()) + assertEquals(-1, `in`.read().toLong()) + } + + @Test + fun inputStreamBounds() { + sink.writeUtf8("a".repeat(100)) + sink.emit() + val `in` = source.inputStream() + try { + `in`.read(ByteArray(100), 50, 51) + fail() + } catch (expected: java.lang.ArrayIndexOutOfBoundsException) { + } + } + + @Test + fun longHexString() { + assertLongHexString("8000000000000000", -0x7fffffffffffffffL - 1L) + assertLongHexString("fffffffffffffffe", -0x2L) + assertLongHexString("FFFFFFFFFFFFFFFe", -0x2L) + assertLongHexString("ffffffffffffffff", -0x1L) + assertLongHexString("FFFFFFFFFFFFFFFF", -0x1L) + assertLongHexString("0000000000000000", 0x0) + assertLongHexString("0000000000000001", 0x1) + assertLongHexString("7999999999999999", 0x7999999999999999L) + assertLongHexString("FF", 0xFF) + assertLongHexString("0000000000000001", 0x1) + } + + @Test + fun hexStringWithManyLeadingZeros() { + assertLongHexString("00000000000000001", 0x1) + assertLongHexString("0000000000000000ffffffffffffffff", -0x1L) + assertLongHexString("00000000000000007fffffffffffffff", 0x7fffffffffffffffL) + assertLongHexString("0".repeat(SEGMENT_SIZE + 1) + "1", 0x1) + } + + private fun assertLongHexString(s: String, expected: Long) { + sink.writeUtf8(s) + sink.emit() + val actual = source.readHexadecimalUnsignedLong() + assertEquals("$s --> $expected", expected, actual) + } + + @Test + fun longHexStringAcrossSegment() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE - 8)).writeUtf8("FFFFFFFFFFFFFFFF") + sink.emit() + source.skip((SEGMENT_SIZE - 8).toLong()) + assertEquals(-1, source.readHexadecimalUnsignedLong()) + } + + @Test + fun longHexStringTooLongThrows() { + try { + sink.writeUtf8("fffffffffffffffff") + sink.emit() + source.readHexadecimalUnsignedLong() + fail() + } catch (e: NumberFormatException) { + assertEquals("Number too large: fffffffffffffffff", e.message) + } + } + + @Test + fun longHexStringTooShortThrows() { + try { + sink.writeUtf8(" ") + sink.emit() + source.readHexadecimalUnsignedLong() + fail() + } catch (e: NumberFormatException) { + assertEquals("Expected leading [0-9a-fA-F] character but was 0x20", e.message) + } + } + + @Test + fun longHexEmptySourceThrows() { + try { + sink.writeUtf8("") + sink.emit() + source.readHexadecimalUnsignedLong() + fail() + } catch (expected: EOFException) { + } + } + + @Test + fun longDecimalString() { + assertLongDecimalString("-9223372036854775808", -9223372036854775807L - 1L) + assertLongDecimalString("-1", -1L) + assertLongDecimalString("0", 0L) + assertLongDecimalString("1", 1L) + assertLongDecimalString("9223372036854775807", 9223372036854775807L) + assertLongDecimalString("00000001", 1L) + assertLongDecimalString("-000001", -1L) + } + + private fun assertLongDecimalString(s: String, expected: Long) { + sink.writeUtf8(s) + sink.writeUtf8("zzz") + sink.emit() + val actual = source.readDecimalLong() + assertEquals("$s --> $expected", expected, actual) + assertEquals("zzz", source.readUtf8()) + } + + @Test + fun longDecimalStringAcrossSegment() { + sink.writeUtf8("a".repeat(SEGMENT_SIZE - 8)).writeUtf8("1234567890123456") + sink.writeUtf8("zzz") + sink.emit() + source.skip((SEGMENT_SIZE - 8).toLong()) + assertEquals(1234567890123456L, source.readDecimalLong()) + assertEquals("zzz", source.readUtf8()) + } + + @Test + fun longDecimalStringTooLongThrows() { + try { + sink.writeUtf8("12345678901234567890") // Too many digits. + sink.emit() + source.readDecimalLong() + fail() + } catch (e: NumberFormatException) { + assertEquals("Number too large: 12345678901234567890", e.message) + } + } + + @Test + fun longDecimalStringTooHighThrows() { + try { + sink.writeUtf8("9223372036854775808") // Right size but cannot fit. + sink.emit() + source.readDecimalLong() + fail() + } catch (e: NumberFormatException) { + assertEquals("Number too large: 9223372036854775808", e.message) + } + } + + @Test + fun longDecimalStringTooLowThrows() { + try { + sink.writeUtf8("-9223372036854775809") // Right size but cannot fit. + sink.emit() + source.readDecimalLong() + fail() + } catch (e: NumberFormatException) { + assertEquals("Number too large: -9223372036854775809", e.message) + } + } + + @Test + fun longDecimalStringTooShortThrows() { + try { + sink.writeUtf8(" ") + sink.emit() + source.readDecimalLong() + fail() + } catch (e: NumberFormatException) { + assertEquals("Expected a digit or '-' but was 0x20", e.message) + } + } + + @Test + fun longDecimalEmptyThrows() { + try { + sink.writeUtf8("") + sink.emit() + source.readDecimalLong() + fail() + } catch (expected: EOFException) { + } + } + + @Test + fun codePoints() { + sink.write("7f".decodeHex()) + sink.emit() + assertEquals(0x7f, source.readUtf8CodePoint().toLong()) + sink.write("dfbf".decodeHex()) + sink.emit() + assertEquals(0x07ff, source.readUtf8CodePoint().toLong()) + sink.write("efbfbf".decodeHex()) + sink.emit() + assertEquals(0xffff, source.readUtf8CodePoint().toLong()) + sink.write("f48fbfbf".decodeHex()) + sink.emit() + assertEquals(0x10ffff, source.readUtf8CodePoint().toLong()) + } + + @Test + fun decimalStringWithManyLeadingZeros() { + assertLongDecimalString("00000000000000001", 1) + assertLongDecimalString("00000000000000009223372036854775807", 9223372036854775807L) + assertLongDecimalString("-00000000000000009223372036854775808", -9223372036854775807L - 1L) + assertLongDecimalString("0".repeat(SEGMENT_SIZE + 1) + "1", 1) + } + + @Test + fun select() { + val options = of( + "ROCK".encodeUtf8(), + "SCISSORS".encodeUtf8(), + "PAPER".encodeUtf8(), + ) + sink.writeUtf8("PAPER,SCISSORS,ROCK") + sink.emit() + assertEquals(2, source.select(options).toLong()) + assertEquals(','.code.toLong(), source.readByte().toLong()) + assertEquals(1, source.select(options).toLong()) + assertEquals(','.code.toLong(), source.readByte().toLong()) + assertEquals(0, source.select(options).toLong()) + assertTrue(source.exhausted()) + } + + /** Note that this test crashes the VM on Android. */ + @Test + fun selectSpanningMultipleSegments() { + val commonPrefix = randomBytes(SEGMENT_SIZE + 10) + val a = Buffer().write(commonPrefix).writeUtf8("a").readByteString() + val bc = Buffer().write(commonPrefix).writeUtf8("bc").readByteString() + val bd = Buffer().write(commonPrefix).writeUtf8("bd").readByteString() + val options = of(a, bc, bd) + sink.write(bd) + sink.write(a) + sink.write(bc) + sink.emit() + assertEquals(2, source.select(options).toLong()) + assertEquals(0, source.select(options).toLong()) + assertEquals(1, source.select(options).toLong()) + assertTrue(source.exhausted()) + } + + @Test + fun selectNotFound() { + val options = of( + "ROCK".encodeUtf8(), + "SCISSORS".encodeUtf8(), + "PAPER".encodeUtf8(), + ) + sink.writeUtf8("SPOCK") + sink.emit() + assertEquals(-1, source.select(options).toLong()) + assertEquals("SPOCK", source.readUtf8()) + } + + @Test + fun selectValuesHaveCommonPrefix() { + val options = of( + "abcd".encodeUtf8(), + "abce".encodeUtf8(), + "abcc".encodeUtf8(), + ) + sink.writeUtf8("abcc").writeUtf8("abcd").writeUtf8("abce") + sink.emit() + assertEquals(2, source.select(options).toLong()) + assertEquals(0, source.select(options).toLong()) + assertEquals(1, source.select(options).toLong()) + } + + @Test + fun selectLongerThanSource() { + val options = of( + "abcd".encodeUtf8(), + "abce".encodeUtf8(), + "abcc".encodeUtf8(), + ) + sink.writeUtf8("abc") + sink.emit() + assertEquals(-1, source.select(options).toLong()) + assertEquals("abc", source.readUtf8()) + } + + @Test + fun selectReturnsFirstByteStringThatMatches() { + val options = of( + "abcd".encodeUtf8(), + "abc".encodeUtf8(), + "abcde".encodeUtf8(), + ) + sink.writeUtf8("abcdef") + sink.emit() + assertEquals(0, source.select(options).toLong()) + assertEquals("ef", source.readUtf8()) + } + + @Test + fun selectFromEmptySource() { + val options = of( + "abc".encodeUtf8(), + "def".encodeUtf8(), + ) + assertEquals(-1, source.select(options).toLong()) + } + + @Test + fun selectNoByteStringsFromEmptySource() { + val options = Options.of() + assertEquals(-1, source.select(options).toLong()) + } + + @Test + fun peek() { + sink.writeUtf8("abcdefghi") + sink.emit() + assertEquals("abc", source.readUtf8(3)) + val peek = source.peek() + assertEquals("def", peek.readUtf8(3)) + assertEquals("ghi", peek.readUtf8(3)) + assertFalse(peek.request(1)) + assertEquals("def", source.readUtf8(3)) + } + + @Test + fun peekMultiple() { + sink.writeUtf8("abcdefghi") + sink.emit() + assertEquals("abc", source.readUtf8(3)) + val peek1 = source.peek() + val peek2 = source.peek() + assertEquals("def", peek1.readUtf8(3)) + assertEquals("def", peek2.readUtf8(3)) + assertEquals("ghi", peek2.readUtf8(3)) + assertFalse(peek2.request(1)) + assertEquals("ghi", peek1.readUtf8(3)) + assertFalse(peek1.request(1)) + assertEquals("def", source.readUtf8(3)) + } + + @Test + fun peekLarge() { + sink.writeUtf8("abcdef") + sink.writeUtf8("g".repeat(2 * SEGMENT_SIZE)) + sink.writeUtf8("hij") + sink.emit() + assertEquals("abc", source.readUtf8(3)) + val peek = source.peek() + assertEquals("def", peek.readUtf8(3)) + peek.skip((2 * SEGMENT_SIZE).toLong()) + assertEquals("hij", peek.readUtf8(3)) + assertFalse(peek.request(1)) + assertEquals("def", source.readUtf8(3)) + source.skip((2 * SEGMENT_SIZE).toLong()) + assertEquals("hij", source.readUtf8(3)) + } + + @Test + fun peekInvalid() { + sink.writeUtf8("abcdefghi") + sink.emit() + assertEquals("abc", source.readUtf8(3)) + val peek = source.peek() + assertEquals("def", peek.readUtf8(3)) + assertEquals("ghi", peek.readUtf8(3)) + assertFalse(peek.request(1)) + assertEquals("def", source.readUtf8(3)) + try { + peek.readUtf8() + fail() + } catch (e: IllegalStateException) { + assertEquals("Peek source is invalid because upstream source was used", e.message) + } + } + + @Test + fun peekSegmentThenInvalid() { + sink.writeUtf8("abc") + sink.writeUtf8("d".repeat(2 * SEGMENT_SIZE)) + sink.emit() + assertEquals("abc", source.readUtf8(3)) + + // Peek a little data and skip the rest of the upstream source + val peek = source.peek() + assertEquals("ddd", peek.readUtf8(3)) + source.readAll(blackholeSink()) + + // Skip the rest of the buffered data + peek.skip(peek.buffer.size) + try { + peek.readByte() + fail() + } catch (e: IllegalStateException) { + assertEquals("Peek source is invalid because upstream source was used", e.message) + } + } + + @Test + fun peekDoesntReadTooMuch() { + // 6 bytes in source's buffer plus 3 bytes upstream. + sink.writeUtf8("abcdef") + sink.emit() + source.require(6L) + sink.writeUtf8("ghi") + sink.emit() + val peek = source.peek() + + // Read 3 bytes. This reads some of the buffered data. + assertTrue(peek.request(3)) + if (source !is Buffer) { + assertEquals(6, source.buffer.size) + assertEquals(6, peek.buffer.size) + } + assertEquals("abc", peek.readUtf8(3L)) + + // Read 3 more bytes. This exhausts the buffered data. + assertTrue(peek.request(3)) + if (source !is Buffer) { + assertEquals(6, source.buffer.size) + assertEquals(3, peek.buffer.size) + } + assertEquals("def", peek.readUtf8(3L)) + + // Read 3 more bytes. This draws new bytes. + assertTrue(peek.request(3)) + assertEquals(9, source.buffer.size) + assertEquals(3, peek.buffer.size) + assertEquals("ghi", peek.readUtf8(3L)) + } + + @Test + fun rangeEquals() { + sink.writeUtf8("A man, a plan, a canal. Panama.") + sink.emit() + assertTrue(source.rangeEquals(7, "a plan".encodeUtf8())) + assertTrue(source.rangeEquals(0, "A man".encodeUtf8())) + assertTrue(source.rangeEquals(24, "Panama".encodeUtf8())) + assertFalse(source.rangeEquals(24, "Panama. Panama. Panama.".encodeUtf8())) + } + + @Test + fun rangeEqualsWithOffsetAndCount() { + sink.writeUtf8("A man, a plan, a canal. Panama.") + sink.emit() + assertTrue(source.rangeEquals(7, "aaa plannn".encodeUtf8(), 2, 6)) + assertTrue(source.rangeEquals(0, "AAA mannn".encodeUtf8(), 2, 5)) + assertTrue(source.rangeEquals(24, "PPPanamaaa".encodeUtf8(), 2, 6)) + } + + @Test + fun rangeEqualsOnlyReadsUntilMismatch() { + Assume.assumeTrue(factory === Factory.ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE) // Other sources read in chunks anyway. + sink.writeUtf8("A man, a plan, a canal. Panama.") + sink.emit() + assertFalse(source.rangeEquals(0, "A man.".encodeUtf8())) + assertEquals("A man,", source.buffer.readUtf8()) + } + + @Test + fun rangeEqualsArgumentValidation() { + // Negative source offset. + assertFalse(source.rangeEquals(-1, "A".encodeUtf8())) + // Negative bytes offset. + assertFalse(source.rangeEquals(0, "A".encodeUtf8(), -1, 1)) + // Bytes offset longer than bytes length. + assertFalse(source.rangeEquals(0, "A".encodeUtf8(), 2, 1)) + // Negative byte count. + assertFalse(source.rangeEquals(0, "A".encodeUtf8(), 0, -1)) + // Byte count longer than bytes length. + assertFalse(source.rangeEquals(0, "A".encodeUtf8(), 0, 2)) + // Bytes offset plus byte count longer than bytes length. + assertFalse(source.rangeEquals(0, "A".encodeUtf8(), 1, 1)) + } + + @Test + fun readNioBuffer() { + val expected = if (factory.isOneByteAtATime) "a" else "abcdefg" + sink.writeUtf8("abcdefg") + sink.emit() + val nioByteBuffer = ByteBuffer.allocate(1024) + val byteCount = source.read(nioByteBuffer) + assertEquals(expected.length.toLong(), byteCount.toLong()) + assertEquals(expected.length.toLong(), nioByteBuffer.position().toLong()) + assertEquals(nioByteBuffer.capacity().toLong(), nioByteBuffer.limit().toLong()) + (nioByteBuffer as java.nio.Buffer).flip() // Cast necessary for Java 8. + val data = ByteArray(expected.length) + nioByteBuffer[data] + assertEquals(expected, data.decodeToString()) + } + + /** Note that this test crashes the VM on Android. */ + @Test + fun readLargeNioBufferOnlyReadsOneSegment() { + val expected = if (factory.isOneByteAtATime) "a" else "a".repeat(SEGMENT_SIZE) + sink.writeUtf8("a".repeat(SEGMENT_SIZE * 4)) + sink.emit() + val nioByteBuffer = ByteBuffer.allocate(SEGMENT_SIZE * 3) + val byteCount = source.read(nioByteBuffer) + assertEquals(expected.length.toLong(), byteCount.toLong()) + assertEquals(expected.length.toLong(), nioByteBuffer.position().toLong()) + assertEquals(nioByteBuffer.capacity().toLong(), nioByteBuffer.limit().toLong()) + (nioByteBuffer as java.nio.Buffer).flip() // Cast necessary for Java 8. + val data = ByteArray(expected.length) + nioByteBuffer[data] + assertEquals(expected, data.decodeToString()) + } + + @Test + fun factorySegmentSizes() { + sink.writeUtf8("abc") + sink.emit() + source.require(3) + if (factory.isOneByteAtATime) { + assertEquals(mutableListOf(1, 1, 1), segmentSizes(source.buffer)) + } else { + assertEquals(listOf(3), segmentSizes(source.buffer)) + } + } + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun parameters(): List<Array<Any>> { + return listOf( + arrayOf(Factory.BUFFER), + arrayOf(Factory.REAL_BUFFERED_SOURCE), + arrayOf(Factory.ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE), + arrayOf(Factory.ONE_BYTE_AT_A_TIME_BUFFER), + arrayOf(Factory.PEEK_BUFFER), + arrayOf(Factory.PEEK_BUFFERED_SOURCE), + ) + } + } +} diff --git a/okio/src/jvmTest/kotlin/okio/ByteStringJavaTest.kt b/okio/src/jvmTest/kotlin/okio/ByteStringJavaTest.kt new file mode 100644 index 00000000..490eb425 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/ByteStringJavaTest.kt @@ -0,0 +1,278 @@ +/* + * Copyright 2014 Square Inc. + * + * 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 java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +import java.util.Random +import kotlin.text.Charsets.US_ASCII +import kotlin.text.Charsets.UTF_16BE +import kotlin.text.Charsets.UTF_32BE +import kotlin.text.Charsets.UTF_8 +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.encode +import okio.ByteString.Companion.encodeUtf8 +import okio.ByteString.Companion.readByteString +import okio.ByteString.Companion.toByteString +import okio.TestUtil.assertByteArraysEquals +import okio.TestUtil.assertEquivalent +import okio.TestUtil.makeSegments +import okio.TestUtil.reserialize +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameter +import org.junit.runners.Parameterized.Parameters + +@RunWith(Parameterized::class) +class ByteStringJavaTest { + interface Factory { + fun decodeHex(hex: String): ByteString + fun encodeUtf8(s: String): ByteString + + companion object { + val BYTE_STRING: Factory = object : Factory { + override fun decodeHex(hex: String): ByteString { + return hex.decodeHex() + } + + override fun encodeUtf8(s: String): ByteString { + return s.encodeUtf8() + } + } + val SEGMENTED_BYTE_STRING: Factory = object : Factory { + override fun decodeHex(hex: String): ByteString { + val buffer = Buffer() + buffer.write(hex.decodeHex()) + return buffer.snapshot() + } + + override fun encodeUtf8(s: String): ByteString { + val buffer = Buffer() + buffer.writeUtf8(s) + return buffer.snapshot() + } + } + val ONE_BYTE_PER_SEGMENT: Factory = object : Factory { + override fun decodeHex(hex: String): ByteString { + return makeSegments(hex.decodeHex()) + } + + override fun encodeUtf8(s: String): ByteString { + return makeSegments(s.encodeUtf8()) + } + } + } + } + + @Parameter(0) + lateinit var factory: Factory + + @Parameter(1) + lateinit var name: String + + @Test + fun ofByteBuffer() { + val bytes = "Hello, World!".toByteArray(UTF_8) + val byteBuffer = ByteBuffer.wrap(bytes) + (byteBuffer as java.nio.Buffer).position(2).limit(11) // Cast necessary for Java 8. + val byteString: ByteString = byteBuffer.toByteString() + // Verify that the bytes were copied out. + byteBuffer.put(4, 'a'.code.toByte()) + assertEquals("llo, Worl", byteString.utf8()) + } + + @Test + fun read() { + val inputStream = ByteArrayInputStream("abc".toByteArray(UTF_8)) + assertEquals("6162".decodeHex(), inputStream.readByteString(2)) + assertEquals("63".decodeHex(), inputStream.readByteString(1)) + assertEquals(ByteString.of(), inputStream.readByteString(0)) + } + + @Test + fun readAndToLowercase() { + val inputStream = ByteArrayInputStream("ABC".toByteArray(UTF_8)) + assertEquals("ab".encodeUtf8(), inputStream.readByteString(2).toAsciiLowercase()) + assertEquals("c".encodeUtf8(), inputStream.readByteString(1).toAsciiLowercase()) + assertEquals(ByteString.EMPTY, inputStream.readByteString(0).toAsciiLowercase()) + } + + @Test + fun readAndToUppercase() { + val inputStream = ByteArrayInputStream("abc".toByteArray(UTF_8)) + assertEquals("AB".encodeUtf8(), inputStream.readByteString(2).toAsciiUppercase()) + assertEquals("C".encodeUtf8(), inputStream.readByteString(1).toAsciiUppercase()) + assertEquals(ByteString.EMPTY, inputStream.readByteString(0).toAsciiUppercase()) + } + + @Test + fun write() { + val out = ByteArrayOutputStream() + factory.decodeHex("616263").write(out) + assertByteArraysEquals(byteArrayOf(0x61, 0x62, 0x63), out.toByteArray()) + } + + @Test + fun compareToSingleBytes() { + val originalByteStrings = listOf( + factory.decodeHex("00"), + factory.decodeHex("01"), + factory.decodeHex("7e"), + factory.decodeHex("7f"), + factory.decodeHex("80"), + factory.decodeHex("81"), + factory.decodeHex("fe"), + factory.decodeHex("ff"), + ) + val sortedByteStrings = originalByteStrings.toMutableList() + sortedByteStrings.shuffle(Random(0)) + sortedByteStrings.sort() + assertEquals(originalByteStrings, sortedByteStrings) + } + + @Test + fun compareToMultipleBytes() { + val originalByteStrings = listOf( + factory.decodeHex(""), + factory.decodeHex("00"), + factory.decodeHex("0000"), + factory.decodeHex("000000"), + factory.decodeHex("00000000"), + factory.decodeHex("0000000000"), + factory.decodeHex("0000000001"), + factory.decodeHex("000001"), + factory.decodeHex("00007f"), + factory.decodeHex("0000ff"), + factory.decodeHex("000100"), + factory.decodeHex("000101"), + factory.decodeHex("007f00"), + factory.decodeHex("00ff00"), + factory.decodeHex("010000"), + factory.decodeHex("010001"), + factory.decodeHex("01007f"), + factory.decodeHex("0100ff"), + factory.decodeHex("010100"), + factory.decodeHex("01010000"), + factory.decodeHex("0101000000"), + factory.decodeHex("0101000001"), + factory.decodeHex("010101"), + factory.decodeHex("7f0000"), + factory.decodeHex("7f0000ffff"), + factory.decodeHex("ffffff"), + ) + val sortedByteStrings = originalByteStrings.toMutableList() + sortedByteStrings.shuffle(Random(0)) + sortedByteStrings.sort() + assertEquals(originalByteStrings, sortedByteStrings) + } + + @Test + fun javaSerializationTestNonEmpty() { + val byteString = factory.encodeUtf8(bronzeHorseman) + assertEquivalent(byteString, reserialize(byteString)) + } + + @Test + fun javaSerializationTestEmpty() { + val byteString = factory.decodeHex("") + assertEquivalent(byteString, reserialize(byteString)) + } + + @Test + fun asByteBuffer() { + assertEquals( + 0x42, + ByteString.of(0x41.toByte(), 0x42.toByte(), 0x43.toByte()).asByteBuffer()[1].toLong(), + ) + } + + @Test + fun encodeDecodeStringUtf8() { + val byteString = bronzeHorseman.encode(UTF_8) + assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.toByteArray(UTF_8)) + assertEquals( + byteString, + ( + "d09dd0b020d0b1d0b5d180d0b5d0b3d18320d0bfd183d181" + + "d182d18bd0bdd0bdd18bd18520d0b2d0bed0bbd0bd" + ).decodeHex(), + ) + assertEquals(bronzeHorseman, byteString.string(UTF_8)) + } + + @Test + fun encodeDecodeStringUtf16be() { + val byteString = bronzeHorseman.encode(UTF_16BE) + assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.toByteArray(UTF_16BE)) + assertEquals( + byteString, + ( + "041d043000200431043504400435043304430020043f0443" + + "04410442044b043d043d044b044500200432043e043b043d" + ).decodeHex(), + ) + assertEquals(bronzeHorseman, byteString.string(UTF_16BE)) + } + + @Test + fun encodeDecodeStringUtf32be() { + val byteString: ByteString = bronzeHorseman.encode(UTF_32BE) + assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.toByteArray(UTF_32BE)) + assertEquals( + byteString, + ( + "0000041d0000043000000020000004310000043500000440" + + "000004350000043300000443000000200000043f0000044300000441000004420000044b0000043d0000043d" + + "0000044b0000044500000020000004320000043e0000043b0000043d" + ).decodeHex(), + ) + assertEquals(bronzeHorseman, byteString.string(UTF_32BE)) + } + + @Test + fun encodeDecodeStringAsciiIsLossy() { + val byteString: ByteString = bronzeHorseman.encode(US_ASCII) + assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.toByteArray(US_ASCII)) + assertEquals( + byteString, + "3f3f203f3f3f3f3f3f203f3f3f3f3f3f3f3f3f203f3f3f3f".decodeHex(), + ) + assertEquals("?? ?????? ????????? ????", byteString.string(US_ASCII)) + } + + @Test + fun decodeMalformedStringReturnsReplacementCharacter() { + val string = "04".decodeHex().string(UTF_16BE) + assertEquals("\ufffd", string) + } + + companion object { + private val bronzeHorseman = "На берегу пустынных волн" + + @JvmStatic + @Parameters(name = "{1}") + fun parameters(): List<Array<Any>> { + return listOf( + arrayOf(Factory.BYTE_STRING, "ByteString"), + arrayOf(Factory.SEGMENTED_BYTE_STRING, "SegmentedByteString"), + arrayOf(Factory.ONE_BYTE_PER_SEGMENT, "SegmentedByteString (one-at-a-time)"), + ) + } + } +} diff --git a/okio/src/jvmTest/kotlin/okio/ByteStringKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/ByteStringKotlinTest.kt index c554251f..ba91338d 100644 --- a/okio/src/jvmTest/kotlin/okio/ByteStringKotlinTest.kt +++ b/okio/src/jvmTest/kotlin/okio/ByteStringKotlinTest.kt @@ -15,14 +15,14 @@ */ package okio -import okio.ByteString.Companion.encode -import okio.ByteString.Companion.encodeUtf8 -import okio.ByteString.Companion.readByteString -import okio.ByteString.Companion.toByteString import java.io.ByteArrayInputStream import java.nio.ByteBuffer import kotlin.test.Test import kotlin.test.assertEquals +import okio.ByteString.Companion.encode +import okio.ByteString.Companion.encodeUtf8 +import okio.ByteString.Companion.readByteString +import okio.ByteString.Companion.toByteString class ByteStringKotlinTest { @Test fun arrayToByteString() { diff --git a/okio/src/jvmTest/kotlin/okio/CipherAlgorithm.kt b/okio/src/jvmTest/kotlin/okio/CipherAlgorithm.kt index f9f42b03..69415241 100644 --- a/okio/src/jvmTest/kotlin/okio/CipherAlgorithm.kt +++ b/okio/src/jvmTest/kotlin/okio/CipherAlgorithm.kt @@ -23,7 +23,7 @@ data class CipherAlgorithm( val transformation: String, val padding: Boolean, val keyLength: Int, - val ivLength: Int? = null + val ivLength: Int? = null, ) { fun createCipherFactory(random: Random): CipherFactory { val key = random.nextBytes(keyLength) @@ -57,7 +57,7 @@ data class CipherAlgorithm( CipherAlgorithm("DESede/CBC/NoPadding", false, 24, 8), CipherAlgorithm("DESede/CBC/PKCS5Padding", true, 24, 8), CipherAlgorithm("DESede/ECB/NoPadding", false, 24), - CipherAlgorithm("DESede/ECB/PKCS5Padding", true, 24) + CipherAlgorithm("DESede/ECB/PKCS5Padding", true, 24), ) } } diff --git a/okio/src/jvmTest/kotlin/okio/CipherFactory.kt b/okio/src/jvmTest/kotlin/okio/CipherFactory.kt index 7b93c652..bb4d3cf3 100644 --- a/okio/src/jvmTest/kotlin/okio/CipherFactory.kt +++ b/okio/src/jvmTest/kotlin/okio/CipherFactory.kt @@ -19,7 +19,7 @@ import javax.crypto.Cipher class CipherFactory( private val transformation: String, - private val init: Cipher.(mode: Int) -> Unit + private val init: Cipher.(mode: Int) -> Unit, ) { val blockSize get() = newCipher().blockSize diff --git a/okio/src/jvmTest/kotlin/okio/CipherSinkTest.kt b/okio/src/jvmTest/kotlin/okio/CipherSinkTest.kt index f273971d..84d27d06 100644 --- a/okio/src/jvmTest/kotlin/okio/CipherSinkTest.kt +++ b/okio/src/jvmTest/kotlin/okio/CipherSinkTest.kt @@ -15,10 +15,10 @@ */ package okio +import kotlin.random.Random import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized -import kotlin.random.Random @RunWith(Parameterized::class) class CipherSinkTest(private val cipherAlgorithm: CipherAlgorithm) { @@ -84,7 +84,7 @@ class CipherSinkTest(private val cipherAlgorithm: CipherAlgorithm) { val cipherSink = buffer.cipherSink(cipherFactory.encrypt) cipherSink.buffer().use { data.forEach { - byte -> + byte -> it.writeByte(byte.toInt()) } } diff --git a/okio/src/jvmTest/kotlin/okio/CipherSourceTest.kt b/okio/src/jvmTest/kotlin/okio/CipherSourceTest.kt index f97258e2..16775350 100644 --- a/okio/src/jvmTest/kotlin/okio/CipherSourceTest.kt +++ b/okio/src/jvmTest/kotlin/okio/CipherSourceTest.kt @@ -15,10 +15,10 @@ */ package okio +import kotlin.random.Random import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized -import kotlin.random.Random @RunWith(Parameterized::class) class CipherSourceTest(private val cipherAlgorithm: CipherAlgorithm) { diff --git a/okio/src/jvmTest/kotlin/okio/DeflateKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/DeflateKotlinTest.kt index 7a1744ad..5174871b 100644 --- a/okio/src/jvmTest/kotlin/okio/DeflateKotlinTest.kt +++ b/okio/src/jvmTest/kotlin/okio/DeflateKotlinTest.kt @@ -16,11 +16,11 @@ package okio -import okio.ByteString.Companion.decodeHex -import org.junit.Test import java.util.zip.Deflater import java.util.zip.Inflater import kotlin.test.assertEquals +import okio.ByteString.Companion.decodeHex +import org.junit.Test class DeflateKotlinTest { @Test fun deflate() { diff --git a/okio/src/jvmTest/kotlin/okio/DeflaterSinkTest.kt b/okio/src/jvmTest/kotlin/okio/DeflaterSinkTest.kt new file mode 100644 index 00000000..57a8586d --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/DeflaterSinkTest.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.util.zip.Deflater +import java.util.zip.Inflater +import java.util.zip.InflaterInputStream +import okio.TestUtil.SEGMENT_SIZE +import okio.TestUtil.randomBytes +import org.junit.Assert +import org.junit.Test + +class DeflaterSinkTest { + @Test + fun deflateWithClose() { + val data = Buffer() + val original = "They're moving in herds. They do move in herds." + data.writeUtf8(original) + val sink = Buffer() + val deflaterSink = DeflaterSink(sink, Deflater()) + deflaterSink.write(data, data.size) + deflaterSink.close() + val inflated = inflate(sink) + Assert.assertEquals(original, inflated.readUtf8()) + } + + @Test + fun deflateWithSyncFlush() { + val original = "Yes, yes, yes. That's why we're taking extreme precautions." + val data = Buffer() + data.writeUtf8(original) + val sink = Buffer() + val deflaterSink = DeflaterSink(sink, Deflater()) + deflaterSink.write(data, data.size) + deflaterSink.flush() + val inflated = inflate(sink) + Assert.assertEquals(original, inflated.readUtf8()) + } + + @Test + fun deflateWellCompressed() { + val original = "a".repeat(1024 * 1024) + val data = Buffer() + data.writeUtf8(original) + val sink = Buffer() + val deflaterSink = DeflaterSink(sink, Deflater()) + deflaterSink.write(data, data.size) + deflaterSink.close() + val inflated = inflate(sink) + Assert.assertEquals(original, inflated.readUtf8()) + } + + @Test + fun deflatePoorlyCompressed() { + val original = randomBytes(1024 * 1024) + val data = Buffer() + data.write(original) + val sink = Buffer() + val deflaterSink = DeflaterSink(sink, Deflater()) + deflaterSink.write(data, data.size) + deflaterSink.close() + val inflated = inflate(sink) + Assert.assertEquals(original, inflated.readByteString()) + } + + @Test + fun multipleSegmentsWithoutCompression() { + val buffer = Buffer() + val deflater = Deflater() + deflater.setLevel(Deflater.NO_COMPRESSION) + val deflaterSink = DeflaterSink(buffer, deflater) + val byteCount = SEGMENT_SIZE * 4 + deflaterSink.write(Buffer().writeUtf8("a".repeat(byteCount)), byteCount.toLong()) + deflaterSink.close() + Assert.assertEquals("a".repeat(byteCount), inflate(buffer).readUtf8(byteCount.toLong())) + } + + @Test + fun deflateIntoNonemptySink() { + val original = "They're moving in herds. They do move in herds." + + // Exercise all possible offsets for the outgoing segment. + for (i in 0 until SEGMENT_SIZE) { + val data = Buffer().writeUtf8(original) + val sink = Buffer().writeUtf8("a".repeat(i)) + val deflaterSink = DeflaterSink(sink, Deflater()) + deflaterSink.write(data, data.size) + deflaterSink.close() + sink.skip(i.toLong()) + val inflated = inflate(sink) + Assert.assertEquals(original, inflated.readUtf8()) + } + } + + /** + * This test deflates a single segment of without compression because that's + * the easiest way to force close() to emit a large amount of data to the + * underlying sink. + */ + @Test + fun closeWithExceptionWhenWritingAndClosing() { + val mockSink = MockSink() + mockSink.scheduleThrow(0, IOException("first")) + mockSink.scheduleThrow(1, IOException("second")) + val deflater = Deflater() + deflater.setLevel(Deflater.NO_COMPRESSION) + val deflaterSink = DeflaterSink(mockSink, deflater) + deflaterSink.write(Buffer().writeUtf8("a".repeat(SEGMENT_SIZE)), SEGMENT_SIZE.toLong()) + try { + deflaterSink.close() + Assert.fail() + } catch (expected: IOException) { + Assert.assertEquals("first", expected.message) + } + mockSink.assertLogContains("close()") + } + + /** + * This test confirms that we swallow NullPointerException from Deflater and + * rethrow as an IOException. + */ + @Test + fun rethrowNullPointerAsIOException() { + val deflater = Deflater() + // Close to cause a NullPointerException + deflater.end() + + val data = Buffer().apply { + writeUtf8("They're moving in herds. They do move in herds.") + } + val deflaterSink = DeflaterSink(Buffer(), deflater) + + val ioe = Assert.assertThrows("", IOException::class.java) { + deflaterSink.write(data, data.size) + } + + Assert.assertTrue(ioe.cause is NullPointerException) + } + + /** + * Uses streaming decompression to inflate `deflated`. The input must + * either be finished or have a trailing sync flush. + */ + private fun inflate(deflated: Buffer): Buffer { + val deflatedIn = deflated.inputStream() + val inflater = Inflater() + val inflatedIn = InflaterInputStream(deflatedIn, inflater) + val result = Buffer() + val buffer = ByteArray(8192) + while (!inflater.needsInput() || deflated.size > 0 || deflatedIn.available() > 0) { + val count = inflatedIn.read(buffer, 0, buffer.size) + if (count != -1) { + result.write(buffer, 0, count) + } + } + return result + } +} diff --git a/okio/src/jvmTest/kotlin/okio/FileHandleFileSystemTest.kt b/okio/src/jvmTest/kotlin/okio/FileHandleFileSystemTest.kt new file mode 100644 index 00000000..021ffb4c --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/FileHandleFileSystemTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 com.google.common.jimfs.Configuration +import com.google.common.jimfs.Jimfs +import java.nio.file.FileSystems +import kotlinx.datetime.Clock +import okio.FileHandleFileSystemTest.FileHandleTestingFileSystem +import okio.FileSystem.Companion.asOkioFileSystem + +/** + * Run a regular file system test, but use [FileHandle] for more file system operations than usual. + * This is intended to increase test coverage for [FileHandle]. + */ +class FileHandleFileSystemTest : AbstractFileSystemTest( + clock = Clock.System, + fileSystem = FileHandleTestingFileSystem(FileSystem.SYSTEM), + windowsLimitations = Path.DIRECTORY_SEPARATOR == "\\", + allowClobberingEmptyDirectories = Path.DIRECTORY_SEPARATOR == "\\", + allowAtomicMoveFromFileToDirectory = false, + temporaryDirectory = FileSystem.SYSTEM_TEMPORARY_DIRECTORY, +) { + /** + * A testing-only file system that implements all reading and writing operations with + * [FileHandle]. This is intended to increase test coverage for [FileHandle]. + */ + class FileHandleTestingFileSystem(delegate: FileSystem) : ForwardingFileSystem(delegate) { + override fun source(file: Path): Source { + val fileHandle = openReadOnly(file) + return fileHandle.source() + .also { fileHandle.close() } + } + + override fun sink(file: Path, mustCreate: Boolean): Sink { + val fileHandle = openReadWrite(file, mustCreate = mustCreate, mustExist = false) + fileHandle.resize(0L) // If the file already has data, get rid of it. + return fileHandle.sink() + .also { fileHandle.close() } + } + + override fun appendingSink(file: Path, mustExist: Boolean): Sink { + val fileHandle = openReadWrite(file, mustCreate = false, mustExist = mustExist) + return fileHandle.appendingSink() + .also { fileHandle.close() } + } + } +} + +class FileHandleNioJimFileSystemWrapperFileSystemTest : AbstractFileSystemTest( + clock = Clock.System, + fileSystem = FileHandleTestingFileSystem( + Jimfs + .newFileSystem( + when (Path.DIRECTORY_SEPARATOR == "\\") { + true -> Configuration.windows() + false -> Configuration.unix() + }, + ).asOkioFileSystem(), + ), + windowsLimitations = false, + allowClobberingEmptyDirectories = true, + allowAtomicMoveFromFileToDirectory = true, + temporaryDirectory = FileSystem.SYSTEM_TEMPORARY_DIRECTORY, +) + +class FileHandleNioDefaultFileSystemWrapperFileSystemTest : AbstractFileSystemTest( + clock = Clock.System, + fileSystem = FileHandleTestingFileSystem( + FileSystems.getDefault().asOkioFileSystem(), + ), + windowsLimitations = false, + allowClobberingEmptyDirectories = Path.DIRECTORY_SEPARATOR == "\\", + allowAtomicMoveFromFileToDirectory = false, + allowRenameWhenTargetIsOpen = Path.DIRECTORY_SEPARATOR != "\\", + temporaryDirectory = FileSystem.SYSTEM_TEMPORARY_DIRECTORY, +) diff --git a/okio/src/jvmTest/kotlin/okio/FileLeakTest.kt b/okio/src/jvmTest/kotlin/okio/FileLeakTest.kt new file mode 100644 index 00000000..5fd18a6b --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/FileLeakTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2023 Square, Inc. and others. + * + * 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 java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import okio.Path.Companion.toPath +import okio.fakefilesystem.FakeFileSystem +import org.junit.After +import org.junit.Before +import org.junit.Test + +class FileLeakTest { + + private lateinit var fakeFileSystem: FakeFileSystem + private val fakeZip = "/test.zip".toPath() + private val fakeEntry = "some.file".toPath() + private val fakeDirectory = "/another/".toPath() + private val fakeEntry2 = fakeDirectory / "another.file" + + @Before + fun setup() { + fakeFileSystem = FakeFileSystem() + with(fakeFileSystem) { + write(fakeZip) { + writeZip { + putEntry(fakeEntry.name) { + writeUtf8("FooBar") + } + try { + putNextEntry(ZipEntry(fakeDirectory.name).apply { time = 0L }) + } finally { + closeEntry() + } + putEntry(fakeEntry2.toString()) { + writeUtf8("SomethingElse") + } + } + } + } + } + + @After + fun tearDown() { + fakeFileSystem.checkNoOpenFiles() + } + + @Test + fun zipFileSystemExistsTest() { + val zipFileSystem = fakeFileSystem.openZip(fakeZip) + assertTrue(zipFileSystem.exists(fakeEntry)) + } + + @Test + fun zipFileSystemMetadataTest() { + val zipFileSystem = fakeFileSystem.openZip(fakeZip) + assertNotNull(zipFileSystem.metadataOrNull(fakeEntry)) + } + + @Test + fun zipFileSystemSourceTest() { + val zipFileSystem = fakeFileSystem.openZip(fakeZip) + zipFileSystem.source(fakeEntry).use { source -> + assertEquals("FooBar", source.buffer().readUtf8()) + } + } + + @Test + fun zipFileSystemListRecursiveTest() { + val zipFileSystem = fakeFileSystem.openZip(fakeZip) + zipFileSystem.listRecursively("/".toPath()).toList() + fakeFileSystem.delete(fakeZip) + } +} + +/** + * Writes a ZIP file to a [BufferedSink]. + */ +private inline fun <R> BufferedSink.writeZip(action: ZipOutputStream.() -> R): R { + return ZipOutputStream(outputStream()).use(action) +} + +/** + * Adds a new ZIP entry named [name], populates it with [action], and closes the entry. + */ +private inline fun <R> ZipOutputStream.putEntry(name: String, action: BufferedSink.() -> R): R { + putNextEntry(ZipEntry(name).apply { time = 0L }) + val sink = sink().buffer() + return try { + sink.action() + } finally { + sink.flush() + closeEntry() + } +} diff --git a/okio/src/jvmTest/kotlin/okio/FileSystemJavaTest.kt b/okio/src/jvmTest/kotlin/okio/FileSystemJavaTest.kt new file mode 100644 index 00000000..699db3ef --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/FileSystemJavaTest.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 java.io.File +import java.nio.file.Paths +import okio.ByteString.Companion.encodeUtf8 +import okio.Path.Companion.toOkioPath +import okio.Path.Companion.toPath +import okio.fakefilesystem.FakeFileSystem +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class FileSystemJavaTest { + @Test + fun pathApi() { + val path = "/home/jesse/todo.txt".toPath(false) + assertThat("/home/jesse".toPath(false).div("todo.txt")).isEqualTo(path) + assertThat("/home/jesse/todo.txt".toPath(false)).isEqualTo(path) + assertThat(path.isAbsolute).isTrue() + assertThat(path.isRelative).isFalse() + assertThat(path.isRoot).isFalse() + assertThat(path.name).isEqualTo("todo.txt") + assertThat(path.nameBytes).isEqualTo("todo.txt".encodeUtf8()) + assertThat(path.parent).isEqualTo("/home/jesse".toPath(false)) + assertThat(path.volumeLetter).isNull() + } + + @Test + fun directorySeparator() { + assertThat(Path.DIRECTORY_SEPARATOR).isIn("/", "\\") + } + + /** Like the same test in JvmTest, but this is using the Java APIs. */ + @Test + fun javaIoFileToOkioPath() { + val string = "/foo/bar/baz".replace("/", Path.DIRECTORY_SEPARATOR) + val javaIoFile = File(string) + val okioPath: Path = string.toPath(false) + assertThat(javaIoFile.toOkioPath(false)).isEqualTo(okioPath) + assertThat(okioPath.toFile()).isEqualTo(javaIoFile) + } + + /** Like the same test in JvmTest, but this is using the Java APIs. */ + @Test + fun nioPathToOkioPath() { + val string = "/foo/bar/baz".replace("/", Path.DIRECTORY_SEPARATOR) + val nioPath = Paths.get(string) + val okioPath: Path = string.toPath(false) + assertThat(nioPath.toOkioPath(false)).isEqualTo(okioPath) + assertThat(okioPath.toNioPath() as Any).isEqualTo(nioPath) + } + + // Just confirm these APIs exist; don't invoke them + @Suppress("unused") + fun fileSystemApi() { + val fileSystem = FileSystem.SYSTEM + val pathA: Path = "a.txt".toPath() + val pathB: Path = "b.txt".toPath() + fileSystem.canonicalize(pathA) + fileSystem.metadata(pathA) + fileSystem.metadataOrNull(pathA) + fileSystem.exists(pathA) + fileSystem.list(pathA) + fileSystem.listOrNull(pathA) + fileSystem.listRecursively(pathA, false) + fileSystem.listRecursively(pathA) + fileSystem.openReadOnly(pathA) + fileSystem.openReadWrite(pathA, mustCreate = false, mustExist = false) + fileSystem.openReadWrite(pathA) + fileSystem.source(pathA) + // Note that FileSystem.read() isn't available to Java callers. + fileSystem.sink(pathA, false) + fileSystem.sink(pathA) + // Note that FileSystem.write() isn't available to Java callers. + fileSystem.appendingSink(pathA, false) + fileSystem.appendingSink(pathA) + fileSystem.createDirectory(pathA) + fileSystem.createDirectories(pathA) + fileSystem.atomicMove(pathA, pathB) + fileSystem.copy(pathA, pathB) + fileSystem.delete(pathA) + fileSystem.deleteRecursively(pathA) + fileSystem.createSymlink(pathA, pathB) + } + + @Test + fun fakeFileSystemApi() { + val fakeFileSystem = FakeFileSystem() + assertThat(fakeFileSystem.clock).isNotNull() + assertThat(fakeFileSystem.allPaths).isEmpty() + assertThat(fakeFileSystem.openPaths).isEmpty() + fakeFileSystem.checkNoOpenFiles() + } + + @Test + fun forwardingFileSystemApi() { + val fakeFileSystem = FakeFileSystem() + val log: MutableList<String> = ArrayList() + val forwardingFileSystem: ForwardingFileSystem = object : ForwardingFileSystem(fakeFileSystem) { + override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path { + log.add("$functionName($parameterName=$path)") + return path + } + } + forwardingFileSystem.metadataOrNull("/".toPath(false)) + assertThat(log).containsExactly("metadataOrNull(path=/)") + } +} diff --git a/okio/src/jvmTest/kotlin/okio/FixedLengthSourceTest.kt b/okio/src/jvmTest/kotlin/okio/FixedLengthSourceTest.kt new file mode 100644 index 00000000..79184177 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/FixedLengthSourceTest.kt @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 kotlin.test.fail +import okio.internal.FixedLengthSource +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +internal class FixedLengthSourceTest { + @Test + fun happyPathWithTruncate() { + val delegate = Buffer().writeUtf8("abcdefghijklmnop") + val fixedLengthSource = FixedLengthSource(delegate, 16, truncate = true) + val buffer = Buffer() + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(10L) + assertThat(buffer.readUtf8()).isEqualTo("abcdefghij") + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(6L) + assertThat(buffer.readUtf8()).isEqualTo("klmnop") + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(-1L) + assertThat(buffer.readUtf8()).isEqualTo("") + } + + @Test + fun happyPathNoTruncate() { + val delegate = Buffer().writeUtf8("abcdefghijklmnop") + val fixedLengthSource = FixedLengthSource(delegate, 16, truncate = false) + val buffer = Buffer() + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(10L) + assertThat(buffer.readUtf8()).isEqualTo("abcdefghij") + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(6L) + assertThat(buffer.readUtf8()).isEqualTo("klmnop") + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(-1L) + assertThat(buffer.readUtf8()).isEqualTo("") + } + + @Test + fun delegateTooLongWithTruncate() { + val delegate = Buffer().writeUtf8("abcdefghijklmnopqr") + val fixedLengthSource = FixedLengthSource(delegate, 16, truncate = true) + val buffer = Buffer() + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(10L) + assertThat(buffer.readUtf8()).isEqualTo("abcdefghij") + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(6L) + assertThat(buffer.readUtf8()).isEqualTo("klmnop") + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(-1L) + assertThat(buffer.readUtf8()).isEqualTo("") + } + + @Test + fun delegateTooLongWithTruncateFencepost() { + val delegate = Buffer().writeUtf8("abcdefghijklmnop") + val fixedLengthSource = FixedLengthSource(delegate, 10, truncate = true) + val buffer = Buffer() + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(10L) + assertThat(buffer.readUtf8()).isEqualTo("abcdefghij") + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(-1L) + assertThat(buffer.readUtf8()).isEmpty() + } + + @Test + fun delegateTooLongNoTruncate() { + val delegate = Buffer().writeUtf8("abcdefghijklmnopqr") + val fixedLengthSource = FixedLengthSource(delegate, 16, truncate = false) + val buffer = Buffer() + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(10L) + assertThat(buffer.readUtf8()).isEqualTo("abcdefghij") + try { + fixedLengthSource.read(buffer, 10L) + fail() + } catch (e: IOException) { + assertThat(e).hasMessage("expected 16 bytes but got 18") + assertThat(buffer.readUtf8()).isEqualTo("klmnop") // Doesn't produce too many bytes! + } + try { + fixedLengthSource.read(buffer, 10L) + fail() + } catch (e: IOException) { + assertThat(e).hasMessage("expected 16 bytes but got 18") + assertThat(buffer.readUtf8()).isEmpty() // Doesn't produce any bytes! + } + } + + @Test + fun delegateTooLongNoTruncateFencepost() { + val delegate = Buffer().writeUtf8("abcdefghijklmnop") + val fixedLengthSource = FixedLengthSource(delegate, 10, truncate = false) + val buffer = Buffer() + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(10L) + assertThat(buffer.readUtf8()).isEqualTo("abcdefghij") + try { + fixedLengthSource.read(buffer, 10L) + fail() + } catch (e: IOException) { + assertThat(e).hasMessage("expected 10 bytes but got 16") + assertThat(buffer.readUtf8()).isEmpty() // Doesn't produce too many bytes! + } + try { + fixedLengthSource.read(buffer, 10L) + fail() + } catch (e: IOException) { + assertThat(e).hasMessage("expected 10 bytes but got 16") + assertThat(buffer.readUtf8()).isEmpty() // Doesn't produce any bytes! + } + } + + @Test + fun delegateTooShortWithTruncate() { + val delegate = Buffer().writeUtf8("abcdefghijklmn") + val fixedLengthSource = FixedLengthSource(delegate, 16, truncate = true) + val buffer = Buffer() + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(10L) + assertThat(buffer.readUtf8()).isEqualTo("abcdefghij") + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(4L) + assertThat(buffer.readUtf8()).isEqualTo("klmn") + try { + fixedLengthSource.read(buffer, 10L) + fail() + } catch (e: IOException) { + assertThat(e).hasMessage("expected 16 bytes but got 14") + } + try { + fixedLengthSource.read(buffer, 10L) + fail() + } catch (e: IOException) { + assertThat(e).hasMessage("expected 16 bytes but got 14") + } + } + + @Test + fun delegateTooShortNoTruncate() { + val delegate = Buffer().writeUtf8("abcdefghijklmn") + val fixedLengthSource = FixedLengthSource(delegate, 16, truncate = false) + val buffer = Buffer() + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(10L) + assertThat(buffer.readUtf8()).isEqualTo("abcdefghij") + assertThat(fixedLengthSource.read(buffer, 10L)).isEqualTo(4L) + assertThat(buffer.readUtf8()).isEqualTo("klmn") + try { + fixedLengthSource.read(buffer, 10L) + fail() + } catch (e: IOException) { + assertThat(e).hasMessage("expected 16 bytes but got 14") + } + try { + fixedLengthSource.read(buffer, 10L) + fail() + } catch (e: IOException) { + assertThat(e).hasMessage("expected 16 bytes but got 14") + } + } +} diff --git a/okio/src/jvmTest/kotlin/okio/ForwardingTimeoutKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/ForwardingTimeoutKotlinTest.kt index 4db02c50..f2c19c6e 100644 --- a/okio/src/jvmTest/kotlin/okio/ForwardingTimeoutKotlinTest.kt +++ b/okio/src/jvmTest/kotlin/okio/ForwardingTimeoutKotlinTest.kt @@ -15,9 +15,9 @@ */ package okio +import java.util.concurrent.TimeUnit import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import java.util.concurrent.TimeUnit class ForwardingTimeoutKotlinTest { @Test fun getAndSetDelegate() { diff --git a/okio/src/jvmTest/kotlin/okio/ForwardingTimeoutTest.kt b/okio/src/jvmTest/kotlin/okio/ForwardingTimeoutTest.kt new file mode 100644 index 00000000..637cb976 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/ForwardingTimeoutTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 java.util.concurrent.TimeUnit +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class ForwardingTimeoutTest { + @Test + fun getAndSetDelegate() { + val timeout1 = Timeout() + val timeout2 = Timeout() + val forwardingTimeout = ForwardingTimeout(timeout1) + forwardingTimeout.timeout(5, TimeUnit.SECONDS) + assertThat(timeout1.timeoutNanos()).isNotEqualTo(0L) + assertThat(timeout2.timeoutNanos()).isEqualTo(0L) + forwardingTimeout.clearTimeout() + assertThat(timeout1.timeoutNanos()).isEqualTo(0L) + assertThat(timeout2.timeoutNanos()).isEqualTo(0L) + assertThat(forwardingTimeout.delegate).isEqualTo(timeout1) + assertThat(forwardingTimeout.setDelegate(timeout2)).isEqualTo(forwardingTimeout) + forwardingTimeout.timeout(5, TimeUnit.SECONDS) + assertThat(timeout1.timeoutNanos()).isEqualTo(0L) + assertThat(timeout2.timeoutNanos()).isNotEqualTo(0L) + forwardingTimeout.clearTimeout() + assertThat(timeout1.timeoutNanos()).isEqualTo(0L) + assertThat(timeout2.timeoutNanos()).isEqualTo(0L) + assertThat(forwardingTimeout.delegate).isEqualTo(timeout2) + } +} diff --git a/okio/src/jvmTest/kotlin/okio/GzipKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/GzipKotlinTest.kt index 6bbe9b51..dfe7182a 100644 --- a/okio/src/jvmTest/kotlin/okio/GzipKotlinTest.kt +++ b/okio/src/jvmTest/kotlin/okio/GzipKotlinTest.kt @@ -16,21 +16,35 @@ package okio +import kotlin.test.assertEquals import okio.ByteString.Companion.decodeHex import org.junit.Test -import kotlin.test.assertEquals class GzipKotlinTest { @Test fun sink() { val data = Buffer() - val gzip = (data as Sink).gzip() - gzip.buffer().writeUtf8("Hi!").close() + (data as Sink).gzip().buffer().use { gzip -> + gzip.writeUtf8("Hi!") + } assertEquals("1f8b0800000000000000f3c8540400dac59e7903000000", data.readByteString().hex()) } @Test fun source() { val buffer = Buffer().write("1f8b0800000000000000f3c8540400dac59e7903000000".decodeHex()) - val gzip = (buffer as Source).gzip() - assertEquals("Hi!", gzip.buffer().readUtf8()) + (buffer as Source).gzip().buffer().use { gzip -> + assertEquals("Hi!", gzip.readUtf8()) + } + } + + @Test fun extraLongXlen() { + val xlen = 0xffff + val buffer = Buffer() + .write("1f8b0804000000000000".decodeHex()) + .writeShort(xlen) + .write(ByteArray(xlen)) + .write("f3c8540400dac59e7903000000".decodeHex()) + (buffer as Source).gzip().buffer().use { gzip -> + assertEquals("Hi!", gzip.readUtf8()) + } } } diff --git a/okio/src/jvmTest/kotlin/okio/GzipSinkTest.kt b/okio/src/jvmTest/kotlin/okio/GzipSinkTest.kt new file mode 100644 index 00000000..b3fe171a --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/GzipSinkTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.io.IOException +import okio.TestUtil.SEGMENT_SIZE +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Test + +class GzipSinkTest { + @Test + fun gzipGunzip() { + val data = Buffer() + val original = "It's a UNIX system! I know this!" + data.writeUtf8(original) + val sink = Buffer() + val gzipSink = GzipSink(sink) + gzipSink.write(data, data.size) + gzipSink.close() + val inflated = gunzip(sink) + assertEquals(original, inflated.readUtf8()) + } + + @Test + fun closeWithExceptionWhenWritingAndClosing() { + val mockSink = MockSink() + mockSink.scheduleThrow(0, IOException("first")) + mockSink.scheduleThrow(1, IOException("second")) + val gzipSink = GzipSink(mockSink) + gzipSink.write(Buffer().writeUtf8("a".repeat(SEGMENT_SIZE)), SEGMENT_SIZE.toLong()) + try { + gzipSink.close() + fail() + } catch (expected: IOException) { + assertEquals("first", expected.message) + } + mockSink.assertLogContains("close()") + } + + private fun gunzip(gzipped: Buffer): Buffer { + val result = Buffer() + val source = GzipSource(gzipped) + while (source.read(result, Int.MAX_VALUE.toLong()) != -1L) { + } + return result + } +} diff --git a/okio/src/jvmTest/kotlin/okio/GzipSourceTest.kt b/okio/src/jvmTest/kotlin/okio/GzipSourceTest.kt new file mode 100644 index 00000000..812aab14 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/GzipSourceTest.kt @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.io.IOException +import java.util.zip.CRC32 +import kotlin.text.Charsets.UTF_8 +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.of +import okio.TestUtil.reverseBytes +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test + +class GzipSourceTest { + @Test + fun gunzip() { + val gzipped = Buffer() + gzipped.write(gzipHeader) + gzipped.write(deflated) + gzipped.write(gzipTrailer) + assertGzipped(gzipped) + } + + @Test + fun gunzip_withHCRC() { + val hcrc = CRC32() + val gzipHeader = gzipHeaderWithFlags(0x02.toByte()) + hcrc.update(gzipHeader.toByteArray()) + val gzipped = Buffer() + gzipped.write(gzipHeader) + gzipped.writeShort(hcrc.value.toShort().reverseBytes().toInt()) // little endian + gzipped.write(deflated) + gzipped.write(gzipTrailer) + assertGzipped(gzipped) + } + + @Test + fun gunzip_withExtra() { + val gzipped = Buffer() + gzipped.write(gzipHeaderWithFlags(0x04.toByte())) + gzipped.writeShort(7.toShort().reverseBytes().toInt()) // little endian extra length + gzipped.write("blubber".toByteArray(UTF_8), 0, 7) + gzipped.write(deflated) + gzipped.write(gzipTrailer) + assertGzipped(gzipped) + } + + @Test + fun gunzip_withName() { + val gzipped = Buffer() + gzipped.write(gzipHeaderWithFlags(0x08.toByte())) + gzipped.write("foo.txt".toByteArray(UTF_8), 0, 7) + gzipped.writeByte(0) // zero-terminated + gzipped.write(deflated) + gzipped.write(gzipTrailer) + assertGzipped(gzipped) + } + + @Test + fun gunzip_withComment() { + val gzipped = Buffer() + gzipped.write(gzipHeaderWithFlags(0x10.toByte())) + gzipped.write("rubbish".toByteArray(UTF_8), 0, 7) + gzipped.writeByte(0) // zero-terminated + gzipped.write(deflated) + gzipped.write(gzipTrailer) + assertGzipped(gzipped) + } + + /** + * For portability, it is a good idea to export the gzipped bytes and try running gzip. Ex. + * `echo gzipped | base64 --decode | gzip -l -v` + */ + @Test + fun gunzip_withAll() { + val gzipped = Buffer() + gzipped.write(gzipHeaderWithFlags(0x1c.toByte())) + gzipped.writeShort(7.toShort().reverseBytes().toInt()) // little endian extra length + gzipped.write("blubber".toByteArray(UTF_8), 0, 7) + gzipped.write("foo.txt".toByteArray(UTF_8), 0, 7) + gzipped.writeByte(0) // zero-terminated + gzipped.write("rubbish".toByteArray(UTF_8), 0, 7) + gzipped.writeByte(0) // zero-terminated + gzipped.write(deflated) + gzipped.write(gzipTrailer) + assertGzipped(gzipped) + } + + private fun assertGzipped(gzipped: Buffer) { + val gunzipped = gunzip(gzipped) + assertEquals("It's a UNIX system! I know this!", gunzipped.readUtf8()) + } + + /** + * Note that you cannot test this with old versions of gzip, as they interpret flag bit 1 as + * CONTINUATION, not HCRC. For example, this is the case with the default gzip on osx. + */ + @Test + fun gunzipWhenHeaderCRCIncorrect() { + val gzipped = Buffer() + gzipped.write(gzipHeaderWithFlags(0x02.toByte())) + gzipped.writeShort(0.toShort().toInt()) // wrong HCRC! + gzipped.write(deflated) + gzipped.write(gzipTrailer) + try { + gunzip(gzipped) + fail() + } catch (e: IOException) { + assertEquals("FHCRC: actual 0x0000261d != expected 0x00000000", e.message) + } + } + + @Test + fun gunzipWhenCRCIncorrect() { + val gzipped = Buffer() + gzipped.write(gzipHeader) + gzipped.write(deflated) + gzipped.writeInt(0x1234567.reverseBytes()) // wrong CRC + gzipped.write(gzipTrailer.toByteArray(), 3, 4) + try { + gunzip(gzipped) + fail() + } catch (e: IOException) { + assertEquals("CRC: actual 0x37ad8f8d != expected 0x01234567", e.message) + } + } + + @Test + fun gunzipWhenLengthIncorrect() { + val gzipped = Buffer() + gzipped.write(gzipHeader) + gzipped.write(deflated) + gzipped.write(gzipTrailer.toByteArray(), 0, 4) + gzipped.writeInt(0x123456.reverseBytes()) // wrong length + try { + gunzip(gzipped) + fail() + } catch (e: IOException) { + assertEquals("ISIZE: actual 0x00000020 != expected 0x00123456", e.message) + } + } + + @Test + fun gunzipExhaustsSource() { + val gzippedSource = Buffer() + .write("1f8b08000000000000004b4c4a0600c241243503000000".decodeHex()) // 'abc' + val exhaustableSource = ExhaustableSource(gzippedSource) + val gunzippedSource = GzipSource(exhaustableSource).buffer() + assertEquals('a'.code.toLong(), gunzippedSource.readByte().toLong()) + assertEquals('b'.code.toLong(), gunzippedSource.readByte().toLong()) + assertEquals('c'.code.toLong(), gunzippedSource.readByte().toLong()) + assertFalse(exhaustableSource.exhausted) + assertEquals(-1, gunzippedSource.read(Buffer(), 1)) + assertTrue(exhaustableSource.exhausted) + } + + @Test + fun gunzipThrowsIfSourceIsNotExhausted() { + val gzippedSource = Buffer() + .write("1f8b08000000000000004b4c4a0600c241243503000000".decodeHex()) // 'abc' + gzippedSource.writeByte('d'.code) // This byte shouldn't be here! + val gunzippedSource = GzipSource(gzippedSource).buffer() + assertEquals('a'.code.toLong(), gunzippedSource.readByte().toLong()) + assertEquals('b'.code.toLong(), gunzippedSource.readByte().toLong()) + assertEquals('c'.code.toLong(), gunzippedSource.readByte().toLong()) + try { + gunzippedSource.readByte() + fail() + } catch (expected: IOException) { + } + } + + private fun gzipHeaderWithFlags(flags: Byte): ByteString { + val result = gzipHeader.toByteArray() + result[3] = flags + return of(*result) + } + + private val gzipHeader = "1f8b0800000000000000".decodeHex() + + // Deflated "It's a UNIX system! I know this!" + private val deflated = "f32c512f56485408f5f38c5028ae2c2e49cd5554f054c8cecb2f5728c9c82c560400".decodeHex() + private val gzipTrailer = ( + "" + + "8d8fad37" + // Checksum of deflated. + "20000000" + ) // 32 in little endian. + .decodeHex() + + private fun gunzip(gzipped: Buffer): Buffer { + val result = Buffer() + val source = GzipSource(gzipped) + while (source.read(result, Int.MAX_VALUE.toLong()) != -1L) { + } + return result + } + + /** This source keeps track of whether its read has returned -1. */ + internal class ExhaustableSource(private val source: Source) : Source { + var exhausted = false + + override fun read(sink: Buffer, byteCount: Long): Long { + val result = source.read(sink, byteCount) + if (result == -1L) exhausted = true + return result + } + + override fun timeout(): Timeout { + return source.timeout() + } + + override fun close() { + source.close() + } + } +} diff --git a/okio/src/jvmTest/kotlin/okio/InflaterSourceTest.kt b/okio/src/jvmTest/kotlin/okio/InflaterSourceTest.kt new file mode 100644 index 00000000..53273776 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/InflaterSourceTest.kt @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.io.EOFException +import java.util.zip.DeflaterOutputStream +import java.util.zip.Inflater +import okio.BufferedSourceFactory.Companion.PARAMETERIZED_TEST_VALUES +import okio.ByteString.Companion.decodeBase64 +import okio.ByteString.Companion.encodeUtf8 +import okio.TestUtil.SEGMENT_SIZE +import okio.TestUtil.randomBytes +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Assume.assumeFalse +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@RunWith(Parameterized::class) +class InflaterSourceTest( + private val bufferFactory: BufferedSourceFactory, +) { + private lateinit var deflatedSink: BufferedSink + private lateinit var deflatedSource: BufferedSource + + init { + resetDeflatedSourceAndSink() + } + + private fun resetDeflatedSourceAndSink() { + val pipe = bufferFactory.pipe() + deflatedSink = pipe.sink + deflatedSource = pipe.source + } + + @Test + fun inflate() { + decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tKtYDAF6CD5s=") + val inflated = inflate(deflatedSource) + assertEquals("God help us, we're in the hands of engineers.", inflated.readUtf8()) + } + + @Test + fun inflateTruncated() { + decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tKtYDAF6CDw==") + try { + inflate(deflatedSource) + fail() + } catch (expected: EOFException) { + } + } + + @Test + fun inflateWellCompressed() { + decodeBase64( + "eJztwTEBAAAAwqCs61/CEL5AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8BtFeWvE=", + ) + val original = "a".repeat(1024 * 1024) + deflate(original.encodeUtf8()) + val inflated = inflate(deflatedSource) + assertEquals(original, inflated.readUtf8()) + } + + @Test + fun inflatePoorlyCompressed() { + assumeFalse(bufferFactory.isOneByteAtATime) // 8 GiB for 1 byte per segment! + val original = randomBytes(1024 * 1024) + deflate(original) + val inflated = inflate(deflatedSource) + assertEquals(original, inflated.readByteString()) + } + + @Test + fun inflateIntoNonemptySink() { + for (i in 0 until SEGMENT_SIZE) { + resetDeflatedSourceAndSink() + val inflated = Buffer().writeUtf8("a".repeat(i)) + deflate("God help us, we're in the hands of engineers.".encodeUtf8()) + val source = InflaterSource(deflatedSource, Inflater()) + while (source.read(inflated, Int.MAX_VALUE.toLong()) != -1L) { + } + inflated.skip(i.toLong()) + assertEquals("God help us, we're in the hands of engineers.", inflated.readUtf8()) + } + } + + @Test + fun inflateSingleByte() { + val inflated = Buffer() + decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tKtYDAF6CD5s=") + val source = InflaterSource(deflatedSource, Inflater()) + source.read(inflated, 1) + source.close() + assertEquals("G", inflated.readUtf8()) + assertEquals(0, inflated.size) + } + + @Test + fun inflateByteCount() { + assumeFalse(bufferFactory.isOneByteAtATime) // This test assumes one step. + val inflated = Buffer() + decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tKtYDAF6CD5s=") + val source = InflaterSource(deflatedSource, Inflater()) + source.read(inflated, 11) + source.close() + assertEquals("God help us", inflated.readUtf8()) + assertEquals(0, inflated.size) + } + + @Test + fun sourceExhaustedPrematurelyOnRead() { + // Deflate 0 bytes of data that lacks the in-stream terminator. + decodeBase64("eJwAAAD//w==") + val inflated = Buffer() + val inflater = Inflater() + val source = InflaterSource(deflatedSource, inflater) + assertThat(deflatedSource.exhausted()).isFalse + try { + source.read(inflated, Long.MAX_VALUE) + fail() + } catch (expected: EOFException) { + assertThat(expected).hasMessage("source exhausted prematurely") + } + + // Despite the exception, the read() call made forward progress on the underlying stream! + assertThat(deflatedSource.exhausted()).isTrue + } + + /** + * Confirm that [InflaterSource.readOrInflate] consumes a byte on each call even if it + * doesn't produce a byte on every call. + */ + @Test + fun readOrInflateMakesByteByByteProgress() { + // Deflate 0 bytes of data that lacks the in-stream terminator. + decodeBase64("eJwAAAD//w==") + val deflatedByteCount = 7 + val inflated = Buffer() + val inflater = Inflater() + val source = InflaterSource(deflatedSource, inflater) + assertThat(deflatedSource.exhausted()).isFalse + if (bufferFactory.isOneByteAtATime) { + for (i in 0 until deflatedByteCount) { + assertThat(inflater.bytesRead).isEqualTo(i.toLong()) + assertThat(source.readOrInflate(inflated, Long.MAX_VALUE)).isEqualTo(0L) + } + } else { + assertThat(source.readOrInflate(inflated, Long.MAX_VALUE)).isEqualTo(0L) + } + assertThat(inflater.bytesRead).isEqualTo(deflatedByteCount.toLong()) + assertThat(deflatedSource.exhausted()).isTrue() + } + + private fun decodeBase64(s: String) { + deflatedSink.write(s.decodeBase64()!!) + deflatedSink.flush() + } + + /** Use DeflaterOutputStream to deflate source. */ + private fun deflate(source: ByteString) { + val sink = DeflaterOutputStream(deflatedSink.outputStream()).sink() + sink.write(Buffer().write(source), source.size.toLong()) + sink.close() + } + + /** Returns a new buffer containing the inflated contents of `deflated`. */ + private fun inflate(deflated: BufferedSource?): Buffer { + val result = Buffer() + val source = InflaterSource(deflated!!, Inflater()) + while (source.read(result, Int.MAX_VALUE.toLong()) != -1L) { + } + return result + } + + companion object { + /** + * Use a parameterized test to control how many bytes the InflaterSource gets with each request + * for more bytes. + */ + @JvmStatic + @Parameters(name = "{0}") + fun parameters(): List<Array<Any>> = PARAMETERIZED_TEST_VALUES + } +} diff --git a/okio/src/jvmTest/kotlin/okio/JimfsOkioRoundTripTest.kt b/okio/src/jvmTest/kotlin/okio/JimfsOkioRoundTripTest.kt new file mode 100644 index 00000000..d4e2239b --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/JimfsOkioRoundTripTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 com.google.common.jimfs.Configuration +import com.google.common.jimfs.Jimfs +import java.nio.file.StandardOpenOption +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.test.BeforeTest +import kotlin.test.assertEquals +import okio.FileSystem.Companion.asOkioFileSystem +import org.junit.Test + +class JimfsOkioRoundTripTest { + private val temporaryDirectory = FileSystem.SYSTEM_TEMPORARY_DIRECTORY + private val jimFs = Jimfs.newFileSystem( + when (Path.DIRECTORY_SEPARATOR == "\\") { + true -> Configuration.windows() + false -> Configuration.unix() + }.toBuilder() + .setWorkingDirectory(temporaryDirectory.toString()) + .build(), + ) + private val jimFsRoot = jimFs.rootDirectories.first() + private val okioFs = jimFs.asOkioFileSystem() + private val base: Path = temporaryDirectory / "${this::class.simpleName}-${randomToken(16)}" + + @BeforeTest + fun setUp() { + okioFs.createDirectory(base) + } + + @Test + fun writeOkioReadJim() { + val path = base / "file-handle-write-okio-and-read-jim" + + okioFs.write(path) { + writeUtf8("abcdefghijklmnop") + } + + assertEquals("abcdefghijklmnop", jimFsRoot.resolve(path.toString()).readText(Charsets.UTF_8)) + } + + @Test + fun writeJimReadOkio() { + val path = base / "file-handle-write-jim-and-read-okio" + jimFsRoot.resolve(path.toString()).writeText("abcdefghijklmnop", Charsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.WRITE) + + okioFs.openReadWrite(path).use { handle -> + handle.source().buffer().use { source -> + assertEquals("abcde", source.readUtf8(5)) + assertEquals("fghijklmnop", source.readUtf8()) + } + } + } +} diff --git a/okio/src/jvmTest/kotlin/okio/JvmSystemFileSystemTest.kt b/okio/src/jvmTest/kotlin/okio/JvmSystemFileSystemTest.kt new file mode 100644 index 00000000..fc4b7c09 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/JvmSystemFileSystemTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 com.google.common.jimfs.Configuration +import com.google.common.jimfs.Jimfs +import java.io.InterruptedIOException +import java.nio.file.FileSystems +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.fail +import kotlinx.datetime.Clock +import okio.FileSystem.Companion.asOkioFileSystem +import org.junit.Test + +/** + * This test will run using [NioSystemFileSystem] by default. If [java.nio.file.Files] is not found + * on the classpath, [JvmSystemFileSystem] will be use instead. + */ +class NioSystemFileSystemTest : AbstractFileSystemTest( + clock = Clock.System, + fileSystem = FileSystem.SYSTEM, + windowsLimitations = Path.DIRECTORY_SEPARATOR == "\\", + allowClobberingEmptyDirectories = Path.DIRECTORY_SEPARATOR == "\\", + allowAtomicMoveFromFileToDirectory = false, + temporaryDirectory = FileSystem.SYSTEM_TEMPORARY_DIRECTORY, +) + +class JvmSystemFileSystemTest : AbstractFileSystemTest( + clock = Clock.System, + fileSystem = JvmSystemFileSystem(), + windowsLimitations = Path.DIRECTORY_SEPARATOR == "\\", + allowClobberingEmptyDirectories = Path.DIRECTORY_SEPARATOR == "\\", + allowAtomicMoveFromFileToDirectory = false, + temporaryDirectory = FileSystem.SYSTEM_TEMPORARY_DIRECTORY, +) { + + @Test fun checkInterruptedBeforeDeleting() { + Thread.currentThread().interrupt() + try { + fileSystem.delete(base) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("interrupted", expected.message) + assertFalse(Thread.interrupted()) + } + } +} + +class NioJimFileSystemWrappingFileSystemTest : AbstractFileSystemTest( + clock = Clock.System, + fileSystem = Jimfs + .newFileSystem( + when (Path.DIRECTORY_SEPARATOR == "\\") { + true -> Configuration.windows() + false -> Configuration.unix() + }, + ).asOkioFileSystem(), + windowsLimitations = false, + allowClobberingEmptyDirectories = true, + allowAtomicMoveFromFileToDirectory = true, + temporaryDirectory = FileSystem.SYSTEM_TEMPORARY_DIRECTORY, +) + +class NioDefaultFileSystemWrappingFileSystemTest : AbstractFileSystemTest( + clock = Clock.System, + fileSystem = FileSystems.getDefault().asOkioFileSystem(), + windowsLimitations = false, + allowClobberingEmptyDirectories = Path.DIRECTORY_SEPARATOR == "\\", + allowAtomicMoveFromFileToDirectory = false, + allowRenameWhenTargetIsOpen = Path.DIRECTORY_SEPARATOR != "\\", + temporaryDirectory = FileSystem.SYSTEM_TEMPORARY_DIRECTORY, +) diff --git a/okio/src/jvmTest/kotlin/okio/JvmTest.kt b/okio/src/jvmTest/kotlin/okio/JvmTest.kt new file mode 100644 index 00000000..690e64a3 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/JvmTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 java.io.File +import java.nio.file.Paths +import kotlin.test.Test +import okio.Path.Companion.toOkioPath +import okio.Path.Companion.toPath +import org.assertj.core.api.Assertions.assertThat + +class JvmTest { + @Test + fun baseDirectoryConsistentWithJavaIoFile() { + assertThat(FileSystem.SYSTEM.canonicalize(".".toPath()).toString()) + .isEqualTo(File("").canonicalFile.toString()) + } + + @Test + fun javaIoFileToOkioPath() { + val string = "/foo/bar/baz".replace("/", Path.DIRECTORY_SEPARATOR) + val javaIoFile = File(string) + val okioPath = string.toPath() + assertThat(javaIoFile.toOkioPath()).isEqualTo(okioPath) + assertThat(okioPath.toFile()).isEqualTo(javaIoFile) + } + + @Test + fun nioPathToOkioPath() { + val string = "/foo/bar/baz".replace("/", Path.DIRECTORY_SEPARATOR) + val nioPath = Paths.get(string) + val okioPath = string.toPath() + assertThat(nioPath.toOkioPath()).isEqualTo(okioPath) + assertThat(okioPath.toNioPath() as Any).isEqualTo(nioPath) + } +} diff --git a/okio/src/jvmTest/kotlin/okio/JvmTesting.kt b/okio/src/jvmTest/kotlin/okio/JvmTesting.kt new file mode 100644 index 00000000..e6b091d7 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/JvmTesting.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import okio.Path.Companion.toOkioPath +import okio.Path.Companion.toPath + +actual fun assertRelativeTo( + a: Path, + b: Path, + bRelativeToA: Path, + sameAsNio: Boolean, +) { + val actual = b.relativeTo(a) + assertEquals(bRelativeToA, actual) + assertEquals(b.normalized().withUnixSlashes(), (a / actual).normalized().withUnixSlashes()) + // Also confirm our behavior is consistent with java.nio. + if (sameAsNio) { + // On Windows, java.nio will modify slashes to backslashes for relative paths, so we force it. + val nioPath = a.toNioPath().relativize(b.toNioPath()) + .toOkioPath(normalize = true).withUnixSlashes().toPath() + assertEquals(bRelativeToA, nioPath) + } +} + +actual fun assertRelativeToFails( + a: Path, + b: Path, + sameAsNio: Boolean, +): IllegalArgumentException { + // Check java.nio first. + if (sameAsNio) { + assertFailsWith<IllegalArgumentException> { + a.toNioPath().relativize(b.toNioPath()) + } + } + // Return okio. + return assertFailsWith { b.relativeTo(a) } +} diff --git a/okio/src/jvmTest/kotlin/okio/LargeStreamsTest.kt b/okio/src/jvmTest/kotlin/okio/LargeStreamsTest.kt new file mode 100644 index 00000000..47d387fe --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/LargeStreamsTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2016 Square, Inc. + * + * 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 java.util.concurrent.Future +import java.util.zip.Deflater +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream +import okio.ByteString.Companion.decodeHex +import okio.HashingSink.Companion.sha256 +import okio.TestUtil.SEGMENT_SIZE +import okio.TestUtil.randomSource +import okio.TestingExecutors.newExecutorService +import org.junit.Assert.assertEquals +import org.junit.Test + +/** Slow running tests that run a large amount of data through a stream. */ +class LargeStreamsTest { + @Test + fun test() { + val pipe = Pipe((1024 * 1024).toLong()) + val future = readAllAndCloseAsync(randomSource(FOUR_GIB_PLUS_ONE), pipe.sink) + val hashingSink = sha256(blackholeSink()) + readAllAndClose(pipe.source, hashingSink) + assertEquals(FOUR_GIB_PLUS_ONE, future.get() as Long) + assertEquals(SHA256_RANDOM_FOUR_GIB_PLUS_1, hashingSink.hash) + } + + /** Note that this test hangs on Android. */ + @Test + fun gzipSource() { + val pipe = Pipe(1024L * 1024) + val gzipOut = object : GZIPOutputStream(pipe.sink.buffer().outputStream()) { + init { + // Disable compression to speed up a slow test. Improved from 141s to 33s on one machine. + def.setLevel(Deflater.NO_COMPRESSION) + } + } + val future = readAllAndCloseAsync( + randomSource(FOUR_GIB_PLUS_ONE), + gzipOut.sink(), + ) + val hashingSink = sha256(blackholeSink()) + val gzipSource = GzipSource(pipe.source) + readAllAndClose(gzipSource, hashingSink) + assertEquals(FOUR_GIB_PLUS_ONE, future.get() as Long) + assertEquals(SHA256_RANDOM_FOUR_GIB_PLUS_1, hashingSink.hash) + } + + /** Note that this test hangs on Android. */ + @Test + fun gzipSink() { + val pipe = Pipe(1024L * 1024) + val gzipSink = GzipSink(pipe.sink) + + // Disable compression to speed up a slow test. Improved from 141s to 35s on one machine. + gzipSink.deflater.setLevel(Deflater.NO_COMPRESSION) + val future = readAllAndCloseAsync(randomSource(FOUR_GIB_PLUS_ONE), gzipSink) + val hashingSink = sha256(blackholeSink()) + val gzipIn = GZIPInputStream(pipe.source.buffer().inputStream()) + readAllAndClose(gzipIn.source(), hashingSink) + assertEquals(FOUR_GIB_PLUS_ONE, future.get() as Long) + assertEquals(SHA256_RANDOM_FOUR_GIB_PLUS_1, hashingSink.hash) + } + + /** Reads all bytes from `source` and writes them to `sink`. */ + private fun readAllAndClose(source: Source, sink: Sink): Long { + var result = 0L + val buffer = Buffer() + while (true) { + val count = source.read(buffer, SEGMENT_SIZE.toLong()) + if (count == -1L) break + sink.write(buffer, count) + result += count + } + source.close() + sink.close() + return result + } + + /** Calls [readAllAndClose] on a background thread. */ + private fun readAllAndCloseAsync(source: Source, sink: Sink): Future<Long> { + val executor = newExecutorService(0) + return try { + executor.submit<Long> { readAllAndClose(source, sink) } + } finally { + executor.shutdown() + } + } + + companion object { + /** 4 GiB plus 1 byte. This is greater than what can be expressed in an unsigned int. */ + const val FOUR_GIB_PLUS_ONE = 0x100000001L + + /** SHA-256 of `TestUtil.randomSource(FOUR_GIB_PLUS_ONE)`. */ + val SHA256_RANDOM_FOUR_GIB_PLUS_1 = + "9654947a655c5efc445502fd1bf11117d894b7812b7974fde8ca4a02c5066315".decodeHex() + } +} diff --git a/okio/src/jvmTest/java/okio/MessageDigestConsistencyTest.kt b/okio/src/jvmTest/kotlin/okio/MessageDigestConsistencyTest.kt index 962d0119..dac72988 100644 --- a/okio/src/jvmTest/java/okio/MessageDigestConsistencyTest.kt +++ b/okio/src/jvmTest/kotlin/okio/MessageDigestConsistencyTest.kt @@ -15,6 +15,9 @@ */ package okio +import java.security.MessageDigest +import java.util.Random +import kotlin.test.Test import okio.ByteString.Companion.toByteString import okio.internal.HashFunction import okio.internal.Md5 @@ -22,9 +25,6 @@ import okio.internal.Sha1 import okio.internal.Sha256 import okio.internal.Sha512 import org.assertj.core.api.Assertions.assertThat -import java.security.MessageDigest -import java.util.Random -import kotlin.test.Test /** * Confirm Okio is consistent with the JDK's MessageDigest algorithms for various sizes and slices. @@ -55,7 +55,7 @@ class MessageDigestConsistencyTest { algorithm = algorithm, hashFunction = newHashFunction(), seed = seed, - updateCount = updateCount + updateCount = updateCount, ) } } @@ -65,7 +65,7 @@ class MessageDigestConsistencyTest { algorithm: String, hashFunction: HashFunction, seed: Long, - updateCount: Int + updateCount: Int, ) { val data = Buffer() @@ -79,13 +79,13 @@ class MessageDigestConsistencyTest { hashFunction.update( input = byteArray, offset = offset, - byteCount = byteCount + byteCount = byteCount, ) data.write( source = byteArray, offset = offset, - byteCount = byteCount + byteCount = byteCount, ) } diff --git a/okio/src/jvmTest/kotlin/okio/NioTest.kt b/okio/src/jvmTest/kotlin/okio/NioTest.kt new file mode 100644 index 00000000..35e11ceb --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/NioTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 java.nio.ByteBuffer +import java.nio.channels.FileChannel +import java.nio.channels.ReadableByteChannel +import java.nio.channels.WritableByteChannel +import java.nio.file.StandardOpenOption +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.text.Charsets.UTF_8 +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +/** Test interop between our beloved Okio and java.nio. */ +class NioTest { + @JvmField @Rule + var temporaryFolder = TemporaryFolder() + + @Test + fun sourceIsOpen() { + val source = (Buffer() as Source).buffer() + assertTrue(source.isOpen()) + source.close() + assertFalse(source.isOpen()) + } + + @Test + fun sinkIsOpen() { + val sink = (Buffer() as Sink).buffer() + assertTrue(sink.isOpen) + sink.close() + assertFalse(sink.isOpen) + } + + @Test + fun writableChannelNioFile() { + val file = temporaryFolder.newFile() + val fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.WRITE) + testWritableByteChannel(fileChannel) + val emitted = file.source().buffer() + assertEquals("defghijklmnopqrstuvw", emitted.readUtf8()) + emitted.close() + } + + @Test + fun writableChannelBuffer() { + val buffer = Buffer() + testWritableByteChannel(buffer) + assertEquals("defghijklmnopqrstuvw", buffer.readUtf8()) + } + + @Test + fun writableChannelBufferedSink() { + val buffer = Buffer() + val bufferedSink = (buffer as Sink).buffer() + testWritableByteChannel(bufferedSink) + assertEquals("defghijklmnopqrstuvw", buffer.readUtf8()) + } + + @Test + fun readableChannelNioFile() { + val file = temporaryFolder.newFile() + val initialData = file.sink().buffer() + initialData.writeUtf8("abcdefghijklmnopqrstuvwxyz") + initialData.close() + val fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.READ) + testReadableByteChannel(fileChannel) + } + + @Test + fun readableChannelBuffer() { + val buffer = Buffer() + buffer.writeUtf8("abcdefghijklmnopqrstuvwxyz") + testReadableByteChannel(buffer) + } + + @Test + fun readableChannelBufferedSource() { + val buffer = Buffer() + val bufferedSource = (buffer as Source).buffer() + buffer.writeUtf8("abcdefghijklmnopqrstuvwxyz") + testReadableByteChannel(bufferedSource) + } + + /** + * Does some basic writes to `channel`. We execute this against both Okio's channels and + * also a standard implementation from the JDK to confirm that their behavior is consistent. + */ + private fun testWritableByteChannel(channel: WritableByteChannel) { + assertTrue(channel.isOpen) + val byteBuffer = ByteBuffer.allocate(1024) + byteBuffer.put("abcdefghijklmnopqrstuvwxyz".toByteArray(UTF_8)) + (byteBuffer as java.nio.Buffer).flip() // Cast necessary for Java 8. + (byteBuffer as java.nio.Buffer).position(3) // Cast necessary for Java 8. + (byteBuffer as java.nio.Buffer).limit(23) // Cast necessary for Java 8. + val byteCount = channel.write(byteBuffer) + assertEquals(20, byteCount) + assertEquals(23, byteBuffer.position()) + assertEquals(23, byteBuffer.limit()) + channel.close() + assertEquals(channel is Buffer, channel.isOpen) // Buffer.close() does nothing. + } + + /** + * Does some basic reads from `channel`. We execute this against both Okio's channels and + * also a standard implementation from the JDK to confirm that their behavior is consistent. + */ + private fun testReadableByteChannel(channel: ReadableByteChannel) { + assertTrue(channel.isOpen) + val byteBuffer = ByteBuffer.allocate(1024) + (byteBuffer as java.nio.Buffer).position(3) // Cast necessary for Java 8. + (byteBuffer as java.nio.Buffer).limit(23) // Cast necessary for Java 8. + val byteCount = channel.read(byteBuffer) + assertEquals(20, byteCount) + assertEquals(23, byteBuffer.position()) + assertEquals(23, byteBuffer.limit()) + channel.close() + assertEquals(channel is Buffer, channel.isOpen) // Buffer.close() does nothing. + (byteBuffer as java.nio.Buffer).flip() // Cast necessary for Java 8. + (byteBuffer as java.nio.Buffer).position(3) // Cast necessary for Java 8. + val data = ByteArray(byteBuffer.remaining()) + byteBuffer[data] + assertEquals("abcdefghijklmnopqrst", String(data, UTF_8)) + } +} diff --git a/okio/src/jvmTest/kotlin/okio/OkioKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/OkioKotlinTest.kt index 58c6bcad..86270d1e 100644 --- a/okio/src/jvmTest/kotlin/okio/OkioKotlinTest.kt +++ b/okio/src/jvmTest/kotlin/okio/OkioKotlinTest.kt @@ -16,17 +16,17 @@ package okio -import org.assertj.core.api.Assertions.assertThat -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File import java.net.Socket import java.nio.file.StandardOpenOption import java.nio.file.StandardOpenOption.APPEND +import org.assertj.core.api.Assertions.assertThat +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder class OkioKotlinTest { @get:Rule val temp = TemporaryFolder() @@ -96,7 +96,8 @@ class OkioKotlinTest { } @Ignore("Not sure how to test this") - @Test fun pathSourceWithOptions() { + @Test + fun pathSourceWithOptions() { val folder = temp.newFolder() val file = File(folder, "new.txt") file.toPath().source(StandardOpenOption.CREATE_NEW) diff --git a/okio/src/jvmTest/kotlin/okio/OkioTest.kt b/okio/src/jvmTest/kotlin/okio/OkioTest.kt new file mode 100644 index 00000000..9514ddd2 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/OkioTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.nio.file.Files +import kotlin.text.Charsets.UTF_8 +import okio.TestUtil.SEGMENT_SIZE +import okio.TestUtil.assertNoEmptySegments +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class OkioTest { + @JvmField + @Rule + var temporaryFolder = TemporaryFolder() + + @Test + fun readWriteFile() { + val file = temporaryFolder.newFile() + val sink = file.sink().buffer() + sink.writeUtf8("Hello, java.io file!") + sink.close() + assertTrue(file.exists()) + assertEquals(20, file.length()) + val source = file.source().buffer() + assertEquals("Hello, java.io file!", source.readUtf8()) + source.close() + } + + @Test + fun appendFile() { + val file = temporaryFolder.newFile() + var sink = file.appendingSink().buffer() + sink.writeUtf8("Hello, ") + sink.close() + assertTrue(file.exists()) + assertEquals(7, file.length()) + sink = file.appendingSink().buffer() + sink.writeUtf8("java.io file!") + sink.close() + assertEquals(20, file.length()) + val source = file.source().buffer() + assertEquals("Hello, java.io file!", source.readUtf8()) + source.close() + } + + @Test + fun readWritePath() { + val path = temporaryFolder.newFile().toPath() + val sink = path.sink().buffer() + sink.writeUtf8("Hello, java.nio file!") + sink.close() + assertTrue(Files.exists(path)) + assertEquals(21, Files.size(path)) + val source = path.source().buffer() + assertEquals("Hello, java.nio file!", source.readUtf8()) + source.close() + } + + @Test + fun sinkFromOutputStream() { + val data = Buffer() + data.writeUtf8("a") + data.writeUtf8("b".repeat(9998)) + data.writeUtf8("c") + val out = ByteArrayOutputStream() + val sink = out.sink() + sink.write(data, 3) + assertEquals("abb", out.toString("UTF-8")) + sink.write(data, data.size) + assertEquals("a" + "b".repeat(9998) + "c", out.toString("UTF-8")) + } + + @Test + fun sourceFromInputStream() { + val inputStream = ByteArrayInputStream( + ("a" + "b".repeat(SEGMENT_SIZE * 2) + "c").toByteArray(UTF_8), + ) + + // Source: ab...bc + val source = inputStream.source() + val sink = Buffer() + + // Source: b...bc. Sink: abb. + assertEquals(3, source.read(sink, 3)) + assertEquals("abb", sink.readUtf8(3)) + + // Source: b...bc. Sink: b...b. + assertEquals(SEGMENT_SIZE.toLong(), source.read(sink, 20000)) + assertEquals("b".repeat(SEGMENT_SIZE), sink.readUtf8()) + + // Source: b...bc. Sink: b...bc. + assertEquals((SEGMENT_SIZE - 1).toLong(), source.read(sink, 20000)) + assertEquals("b".repeat(SEGMENT_SIZE - 2) + "c", sink.readUtf8()) + + // Source and sink are empty. + assertEquals(-1, source.read(sink, 1)) + } + + @Test + fun sourceFromInputStreamWithSegmentSize() { + val inputStream = ByteArrayInputStream(ByteArray(SEGMENT_SIZE)) + val source = inputStream.source() + val sink = Buffer() + assertEquals(SEGMENT_SIZE.toLong(), source.read(sink, SEGMENT_SIZE.toLong())) + assertEquals(-1, source.read(sink, SEGMENT_SIZE.toLong())) + assertNoEmptySegments(sink) + } + + @Test + fun sourceFromInputStreamBounds() { + val source = ByteArrayInputStream(ByteArray(100)).source() + try { + source.read(Buffer(), -1) + fail() + } catch (expected: IllegalArgumentException) { + } + } + + @Test + fun blackhole() { + val data = Buffer() + data.writeUtf8("blackhole") + val blackhole = blackholeSink() + blackhole.write(data, 5) + assertEquals("hole", data.readUtf8()) + } +} diff --git a/okio/src/jvmTest/kotlin/okio/PipeKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/PipeKotlinTest.kt index 3a41e742..ac50f3a0 100644 --- a/okio/src/jvmTest/kotlin/okio/PipeKotlinTest.kt +++ b/okio/src/jvmTest/kotlin/okio/PipeKotlinTest.kt @@ -15,6 +15,10 @@ */ package okio +import java.io.IOException +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.test.assertFailsWith import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -22,19 +26,16 @@ import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Rule import org.junit.Test -import java.io.IOException -import java.util.concurrent.CountDownLatch -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import kotlin.test.assertFailsWith import org.junit.rules.Timeout as JUnitTimeout class PipeKotlinTest { - @JvmField @Rule val timeout = JUnitTimeout(5, TimeUnit.SECONDS) + @JvmField @Rule + val timeout = JUnitTimeout(5, TimeUnit.SECONDS) - private val executorService = Executors.newScheduledThreadPool(1) + private val executorService = TestingExecutors.newScheduledExecutorService(1) - @After @Throws(Exception::class) + @After + @Throws(Exception::class) fun tearDown() { executorService.shutdown() } @@ -108,7 +109,8 @@ class PipeKotlinTest { pipe.fold(foldSink) latch.countDown() }, - 500, TimeUnit.MILLISECONDS + 500, + TimeUnit.MILLISECONDS, ) val sink = pipe.sink.buffer() @@ -536,7 +538,8 @@ class PipeKotlinTest { } assertEquals("boom", foldFailure.message) }, - 500, TimeUnit.MILLISECONDS + 500, + TimeUnit.MILLISECONDS, ) val writeFailure = assertFailsWith<IOException> { @@ -656,7 +659,8 @@ class PipeKotlinTest { { pipe.cancel() }, - smallerTimeoutNanos, TimeUnit.NANOSECONDS + smallerTimeoutNanos, + TimeUnit.NANOSECONDS, ) val pipeSink = pipe.sink.buffer() @@ -699,7 +703,8 @@ class PipeKotlinTest { { pipe.cancel() }, - smallerTimeoutNanos, TimeUnit.NANOSECONDS + smallerTimeoutNanos, + TimeUnit.NANOSECONDS, ) val pipeSource = pipe.source.buffer() @@ -783,8 +788,9 @@ class PipeKotlinTest { val elapsed = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis() - start) assertEquals( - expected.toDouble(), elapsed.toDouble(), - TimeUnit.MILLISECONDS.toNanos(200).toDouble() + expected.toDouble(), + elapsed.toDouble(), + TimeUnit.MILLISECONDS.toNanos(200).toDouble(), ) } diff --git a/okio/src/jvmTest/kotlin/okio/PipeTest.kt b/okio/src/jvmTest/kotlin/okio/PipeTest.kt new file mode 100644 index 00000000..74b61290 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/PipeTest.kt @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2016 Square, Inc. + * + * 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 java.io.IOException +import java.io.InterruptedIOException +import java.util.Random +import java.util.concurrent.TimeUnit +import okio.ByteString.Companion.decodeHex +import okio.HashingSink.Companion.sha1 +import okio.TestUtil.assumeNotWindows +import okio.TestingExecutors.newScheduledExecutorService +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test + +class PipeTest { + private val executorService = newScheduledExecutorService(2) + + @After + fun tearDown() { + executorService.shutdown() + } + + @Test + fun test() { + val pipe = Pipe(6) + pipe.sink.write(Buffer().writeUtf8("abc"), 3L) + val source = pipe.source + val readBuffer = Buffer() + assertEquals(3L, source.read(readBuffer, 6L)) + assertEquals("abc", readBuffer.readUtf8()) + pipe.sink.close() + assertEquals(-1L, source.read(readBuffer, 6L)) + source.close() + } + + /** + * A producer writes the first 16 MiB of bytes generated by `new Random(0)` to a sink, and a + * consumer consumes them. Both compute hashes of their data to confirm that they're as expected. + */ + @Test + fun largeDataset() { + val pipe = Pipe(1000L) // An awkward size to force producer/consumer exchange. + val totalBytes = 16L * 1024L * 1024L + val expectedHash = "7c3b224bea749086babe079360cf29f98d88262d".decodeHex() + + // Write data to the sink. + val sinkHash = executorService.submit<ByteString> { + val hashingSink = sha1(pipe.sink) + val random = Random(0) + val data = ByteArray(8192) + val buffer = Buffer() + var i = 0L + while (i < totalBytes) { + random.nextBytes(data) + buffer.write(data) + hashingSink.write(buffer, buffer.size) + i += data.size.toLong() + } + hashingSink.close() + hashingSink.hash + } + + // Read data from the source. + val sourceHash = executorService.submit<ByteString> { + val blackhole = Buffer() + val hashingSink = sha1(blackhole) + val buffer = Buffer() + while (pipe.source.read(buffer, Long.MAX_VALUE) != -1L) { + hashingSink.write(buffer, buffer.size) + blackhole.clear() + } + pipe.source.close() + hashingSink.hash + } + assertEquals(expectedHash, sinkHash.get()) + assertEquals(expectedHash, sourceHash.get()) + } + + @Test + fun sinkTimeout() { + assumeNotWindows() + val pipe = Pipe(3) + pipe.sink.timeout().timeout(1000, TimeUnit.MILLISECONDS) + pipe.sink.write(Buffer().writeUtf8("abc"), 3L) + val start = now() + try { + pipe.sink.write(Buffer().writeUtf8("def"), 3L) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(1000.0, start) + val readBuffer = Buffer() + assertEquals(3L, pipe.source.read(readBuffer, 6L)) + assertEquals("abc", readBuffer.readUtf8()) + } + + @Test + fun sourceTimeout() { + assumeNotWindows() + val pipe = Pipe(3L) + pipe.source.timeout().timeout(1000, TimeUnit.MILLISECONDS) + val start = now() + val readBuffer = Buffer() + try { + pipe.source.read(readBuffer, 6L) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(1000.0, start) + assertEquals(0, readBuffer.size) + } + + /** + * The writer is writing 12 bytes as fast as it can to a 3 byte buffer. The reader alternates + * sleeping 1000 ms, then reading 3 bytes. That should make for an approximate timeline like + * this: + * + * ``` + * 0: writer writes 'abc', blocks 0: reader sleeps until 1000 + * 1000: reader reads 'abc', sleeps until 2000 + * 1000: writer writes 'def', blocks + * 2000: reader reads 'def', sleeps until 3000 + * 2000: writer writes 'ghi', blocks + * 3000: reader reads 'ghi', sleeps until 4000 + * 3000: writer writes 'jkl', returns + * 4000: reader reads 'jkl', returns + * ``` + * + * + * Because the writer is writing to a buffer, it finishes before the reader does. + */ + @Test + fun sinkBlocksOnSlowReader() { + val pipe = Pipe(3L) + executorService.execute { + val buffer = Buffer() + Thread.sleep(1000L) + assertEquals(3, pipe.source.read(buffer, Long.MAX_VALUE)) + assertEquals("abc", buffer.readUtf8()) + Thread.sleep(1000L) + assertEquals(3, pipe.source.read(buffer, Long.MAX_VALUE)) + assertEquals("def", buffer.readUtf8()) + Thread.sleep(1000L) + assertEquals(3, pipe.source.read(buffer, Long.MAX_VALUE)) + assertEquals("ghi", buffer.readUtf8()) + Thread.sleep(1000L) + assertEquals(3, pipe.source.read(buffer, Long.MAX_VALUE)) + assertEquals("jkl", buffer.readUtf8()) + } + val start = now() + pipe.sink.write(Buffer().writeUtf8("abcdefghijkl"), 12) + assertElapsed(3000.0, start) + } + + @Test + fun sinkWriteFailsByClosedReader() { + val pipe = Pipe(3L) + executorService.schedule( + { + pipe.source.close() + }, + 1000, + TimeUnit.MILLISECONDS, + ) + val start = now() + try { + pipe.sink.write(Buffer().writeUtf8("abcdef"), 6) + fail() + } catch (expected: IOException) { + assertEquals("source is closed", expected.message) + assertElapsed(1000.0, start) + } + } + + @Test + fun sinkFlushDoesntWaitForReader() { + val pipe = Pipe(100L) + pipe.sink.write(Buffer().writeUtf8("abc"), 3) + pipe.sink.flush() + val bufferedSource = pipe.source.buffer() + assertEquals("abc", bufferedSource.readUtf8(3)) + } + + @Test + fun sinkFlushFailsIfReaderIsClosedBeforeAllDataIsRead() { + val pipe = Pipe(100L) + pipe.sink.write(Buffer().writeUtf8("abc"), 3) + pipe.source.close() + try { + pipe.sink.flush() + fail() + } catch (expected: IOException) { + assertEquals("source is closed", expected.message) + } + } + + @Test + fun sinkCloseFailsIfReaderIsClosedBeforeAllDataIsRead() { + val pipe = Pipe(100L) + pipe.sink.write(Buffer().writeUtf8("abc"), 3) + pipe.source.close() + try { + pipe.sink.close() + fail() + } catch (expected: IOException) { + assertEquals("source is closed", expected.message) + } + } + + @Test + fun sinkClose() { + val pipe = Pipe(100L) + pipe.sink.close() + try { + pipe.sink.write(Buffer().writeUtf8("abc"), 3) + fail() + } catch (expected: IllegalStateException) { + assertEquals("closed", expected.message) + } + try { + pipe.sink.flush() + fail() + } catch (expected: IllegalStateException) { + assertEquals("closed", expected.message) + } + } + + @Test + fun sinkMultipleClose() { + val pipe = Pipe(100L) + pipe.sink.close() + pipe.sink.close() + } + + @Test + fun sinkCloseDoesntWaitForSourceRead() { + val pipe = Pipe(100L) + pipe.sink.write(Buffer().writeUtf8("abc"), 3) + pipe.sink.close() + val bufferedSource = pipe.source.buffer() + assertEquals("abc", bufferedSource.readUtf8()) + assertTrue(bufferedSource.exhausted()) + } + + @Test + fun sourceClose() { + val pipe = Pipe(100L) + pipe.source.close() + try { + pipe.source.read(Buffer(), 3) + fail() + } catch (expected: IllegalStateException) { + assertEquals("closed", expected.message) + } + } + + @Test + fun sourceMultipleClose() { + val pipe = Pipe(100L) + pipe.source.close() + pipe.source.close() + } + + @Test + fun sourceReadUnblockedByClosedSink() { + val pipe = Pipe(3L) + executorService.schedule( + { + pipe.sink.close() + }, + 1000, + TimeUnit.MILLISECONDS, + ) + val start = now() + val readBuffer = Buffer() + assertEquals(-1, pipe.source.read(readBuffer, Long.MAX_VALUE)) + assertEquals(0, readBuffer.size) + assertElapsed(1000.0, start) + } + + /** + * The writer has 12 bytes to write. It alternates sleeping 1000 ms, then writing 3 bytes. The + * reader is reading as fast as it can. That should make for an approximate timeline like this: + * + * ``` + * 0: writer sleeps until 1000 + * 0: reader blocks + * 1000: writer writes 'abc', sleeps until 2000 + * 1000: reader reads 'abc' + * 2000: writer writes 'def', sleeps until 3000 + * 2000: reader reads 'def' + * 3000: writer writes 'ghi', sleeps until 4000 + * 3000: reader reads 'ghi' + * 4000: writer writes 'jkl', returns + * 4000: reader reads 'jkl', returns + * ``` + */ + @Test + fun sourceBlocksOnSlowWriter() { + val pipe = Pipe(100L) + executorService.execute { + Thread.sleep(1000L) + pipe.sink.write(Buffer().writeUtf8("abc"), 3) + Thread.sleep(1000L) + pipe.sink.write(Buffer().writeUtf8("def"), 3) + Thread.sleep(1000L) + pipe.sink.write(Buffer().writeUtf8("ghi"), 3) + Thread.sleep(1000L) + pipe.sink.write(Buffer().writeUtf8("jkl"), 3) + } + val start = now() + val readBuffer = Buffer() + assertEquals(3, pipe.source.read(readBuffer, Long.MAX_VALUE)) + assertEquals("abc", readBuffer.readUtf8()) + assertElapsed(1000.0, start) + assertEquals(3, pipe.source.read(readBuffer, Long.MAX_VALUE)) + assertEquals("def", readBuffer.readUtf8()) + assertElapsed(2000.0, start) + assertEquals(3, pipe.source.read(readBuffer, Long.MAX_VALUE)) + assertEquals("ghi", readBuffer.readUtf8()) + assertElapsed(3000.0, start) + assertEquals(3, pipe.source.read(readBuffer, Long.MAX_VALUE)) + assertEquals("jkl", readBuffer.readUtf8()) + assertElapsed(4000.0, start) + } + + /** Returns the nanotime in milliseconds as a double for measuring timeouts. */ + private fun now(): Double { + return System.nanoTime() / 1000000.0 + } + + /** + * Fails the test unless the time from start until now is duration, accepting differences in + * -50..+450 milliseconds. + */ + private fun assertElapsed(duration: Double, start: Double) { + assertEquals(duration, now() - start - 200.0, 250.0) + } +} diff --git a/okio/src/jvmTest/kotlin/okio/ReadUtf8LineTest.kt b/okio/src/jvmTest/kotlin/okio/ReadUtf8LineTest.kt new file mode 100644 index 00000000..6f64355b --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/ReadUtf8LineTest.kt @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.io.EOFException +import okio.TestUtil.SEGMENT_SIZE +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameter +import org.junit.runners.Parameterized.Parameters + +@RunWith(Parameterized::class) +class ReadUtf8LineTest { + interface Factory { + fun create(data: Buffer): BufferedSource + } + + @Parameter + lateinit var factory: Factory + private lateinit var data: Buffer + private lateinit var source: BufferedSource + + @Before + fun setUp() { + data = Buffer() + source = factory.create(data) + } + + @Test + fun readLines() { + data.writeUtf8("abc\ndef\n") + assertEquals("abc", source.readUtf8LineStrict()) + assertEquals("def", source.readUtf8LineStrict()) + try { + source.readUtf8LineStrict() + fail() + } catch (expected: EOFException) { + assertEquals("\\n not found: limit=0 content=…", expected.message) + } + } + + @Test + fun readUtf8LineStrictWithLimits() { + val lens = intArrayOf(1, SEGMENT_SIZE - 2, SEGMENT_SIZE - 1, SEGMENT_SIZE, SEGMENT_SIZE * 10) + for (len in lens) { + data.writeUtf8("a".repeat(len)).writeUtf8("\n") + assertEquals(len.toLong(), source.readUtf8LineStrict(len.toLong()).length.toLong()) + source.readUtf8() + data.writeUtf8("a".repeat(len)).writeUtf8("\n").writeUtf8("a".repeat(len)) + assertEquals(len.toLong(), source.readUtf8LineStrict(len.toLong()).length.toLong()) + source.readUtf8() + data.writeUtf8("a".repeat(len)).writeUtf8("\r\n") + assertEquals(len.toLong(), source.readUtf8LineStrict(len.toLong()).length.toLong()) + source.readUtf8() + data.writeUtf8("a".repeat(len)).writeUtf8("\r\n").writeUtf8("a".repeat(len)) + assertEquals(len.toLong(), source.readUtf8LineStrict(len.toLong()).length.toLong()) + source.readUtf8() + } + } + + @Test + fun readUtf8LineStrictNoBytesConsumedOnFailure() { + data.writeUtf8("abc\n") + try { + source.readUtf8LineStrict(2) + fail() + } catch (expected: EOFException) { + assertTrue(expected.message!!.startsWith("\\n not found: limit=2 content=61626")) + } + assertEquals("abc", source.readUtf8LineStrict(3)) + } + + @Test + fun readUtf8LineStrictEmptyString() { + data.writeUtf8("\r\nabc") + assertEquals("", source.readUtf8LineStrict(0)) + assertEquals("abc", source.readUtf8()) + } + + @Test + fun readUtf8LineStrictNonPositive() { + data.writeUtf8("\r\n") + try { + source.readUtf8LineStrict(-1) + fail("Expected failure: limit must be greater than 0") + } catch (expected: IllegalArgumentException) { + } + } + + @Test + fun eofExceptionProvidesLimitedContent() { + data.writeUtf8("aaaaaaaabbbbbbbbccccccccdddddddde") + try { + source.readUtf8LineStrict() + fail() + } catch (expected: EOFException) { + assertEquals( + "\\n not found: limit=33 content=616161616161616162626262626262626363636363636363" + + "6464646464646464…", + expected.message, + ) + } + } + + @Test + fun newlineAtEnd() { + data.writeUtf8("abc\n") + assertEquals("abc", source.readUtf8LineStrict(3)) + assertTrue(source.exhausted()) + data.writeUtf8("abc\r\n") + assertEquals("abc", source.readUtf8LineStrict(3)) + assertTrue(source.exhausted()) + data.writeUtf8("abc\r") + try { + source.readUtf8LineStrict(3) + fail() + } catch (expected: EOFException) { + assertEquals("\\n not found: limit=3 content=6162630d…", expected.message) + } + source.readUtf8() + data.writeUtf8("abc") + try { + source.readUtf8LineStrict(3) + fail() + } catch (expected: EOFException) { + assertEquals("\\n not found: limit=3 content=616263…", expected.message) + } + } + + @Test + fun emptyLines() { + data.writeUtf8("\n\n\n") + assertEquals("", source.readUtf8LineStrict()) + assertEquals("", source.readUtf8LineStrict()) + assertEquals("", source.readUtf8LineStrict()) + assertTrue(source.exhausted()) + } + + @Test + fun crDroppedPrecedingLf() { + data.writeUtf8("abc\r\ndef\r\nghi\rjkl\r\n") + assertEquals("abc", source.readUtf8LineStrict()) + assertEquals("def", source.readUtf8LineStrict()) + assertEquals("ghi\rjkl", source.readUtf8LineStrict()) + } + + @Test + fun bufferedReaderCompatible() { + data.writeUtf8("abc\ndef") + assertEquals("abc", source.readUtf8Line()) + assertEquals("def", source.readUtf8Line()) + assertNull(source.readUtf8Line()) + } + + @Test + fun bufferedReaderCompatibleWithTrailingNewline() { + data.writeUtf8("abc\ndef\n") + assertEquals("abc", source.readUtf8Line()) + assertEquals("def", source.readUtf8Line()) + assertNull(source.readUtf8Line()) + } + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun parameters(): List<Array<Any>> { + return listOf( + arrayOf( + object : Factory { + override fun create(data: Buffer) = data + override fun toString() = "Buffer" + }, + ), + arrayOf( + object : Factory { + override fun create(data: Buffer) = RealBufferedSource(data) + override fun toString() = "RealBufferedSource" + }, + ), + arrayOf( + object : Factory { + override fun create(data: Buffer): BufferedSource { + return RealBufferedSource( + object : ForwardingSource(data) { + override fun read(sink: Buffer, byteCount: Long): Long { + return super.read(sink, 1L.coerceAtMost(byteCount)) + } + }, + ) + } + + override fun toString() = "Slow RealBufferedSource" + }, + ), + ) + } + } +} diff --git a/okio/src/jvmTest/kotlin/okio/SegmentSharingTest.kt b/okio/src/jvmTest/kotlin/okio/SegmentSharingTest.kt index 73e74d99..df4a9d5e 100644 --- a/okio/src/jvmTest/kotlin/okio/SegmentSharingTest.kt +++ b/okio/src/jvmTest/kotlin/okio/SegmentSharingTest.kt @@ -15,14 +15,14 @@ */ package okio +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue import okio.ByteString.Companion.encodeUtf8 import okio.TestUtil.assertEquivalent import okio.TestUtil.bufferWithSegments import okio.TestUtil.takeAllPoolSegments import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertTrue /** Tests behavior optimized by sharing segments between buffers and byte strings. */ class SegmentSharingTest { @@ -40,12 +40,12 @@ class SegmentSharingTest { @Test fun snapshotGetByte() { val byteString = bufferWithSegments(xs, ys, zs).snapshot() - assertEquals('x', byteString[0].toChar()) - assertEquals('x', byteString[xs.length - 1].toChar()) - assertEquals('y', byteString[xs.length].toChar()) - assertEquals('y', byteString[xs.length + ys.length - 1].toChar()) - assertEquals('z', byteString[xs.length + ys.length].toChar()) - assertEquals('z', byteString[xs.length + ys.length + zs.length - 1].toChar()) + assertEquals('x', byteString[0].toInt().toChar()) + assertEquals('x', byteString[xs.length - 1].toInt().toChar()) + assertEquals('y', byteString[xs.length].toInt().toChar()) + assertEquals('y', byteString[xs.length + ys.length - 1].toInt().toChar()) + assertEquals('z', byteString[xs.length + ys.length].toInt().toChar()) + assertEquals('z', byteString[xs.length + ys.length + zs.length - 1].toInt().toChar()) assertFailsWith<IndexOutOfBoundsException> { byteString[-1] } diff --git a/okio/src/jvmTest/kotlin/okio/SocketTimeoutTest.kt b/okio/src/jvmTest/kotlin/okio/SocketTimeoutTest.kt new file mode 100644 index 00000000..f38796f7 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/SocketTimeoutTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.io.EOFException +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.InetAddress +import java.net.ServerSocket +import java.net.Socket +import java.net.SocketTimeoutException +import java.util.concurrent.TimeUnit +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test + +class SocketTimeoutTest { + @Test + fun readWithoutTimeout() { + val socket = socket(ONE_MB, 0) + val source = socket.source().buffer() + source.timeout().timeout(5000, TimeUnit.MILLISECONDS) + source.require(ONE_MB.toLong()) + socket.close() + } + + @Test + fun readWithTimeout() { + val socket = socket(0, 0) + val source = socket.source().buffer() + source.timeout().timeout(250, TimeUnit.MILLISECONDS) + try { + source.require(ONE_MB.toLong()) + fail() + } catch (expected: SocketTimeoutException) { + } + socket.close() + } + + @Test + fun writeWithoutTimeout() { + val socket = socket(0, ONE_MB) + val sink: Sink = socket.sink().buffer() + sink.timeout().timeout(500, TimeUnit.MILLISECONDS) + val data = ByteArray(ONE_MB) + sink.write(Buffer().write(data), data.size.toLong()) + sink.flush() + socket.close() + } + + @Test + fun writeWithTimeout() { + val socket = socket(0, 0) + val sink = socket.sink() + sink.timeout().timeout(500, TimeUnit.MILLISECONDS) + val data = ByteArray(ONE_MB) + val start = System.nanoTime() + try { + sink.write(Buffer().write(data), data.size.toLong()) + sink.flush() + fail() + } catch (expected: SocketTimeoutException) { + } + val elapsed = System.nanoTime() - start + socket.close() + assertTrue("elapsed: $elapsed", TimeUnit.NANOSECONDS.toMillis(elapsed) >= 500) + assertTrue("elapsed: $elapsed", TimeUnit.NANOSECONDS.toMillis(elapsed) <= 750) + } + + companion object { + // The size of the socket buffers to use. Less than half the data transferred during tests to + // ensure send and receive buffers are flooded and any necessary blocking behavior takes place. + private const val SOCKET_BUFFER_SIZE = 256 * 1024 + private const val ONE_MB = 1024 * 1024 + + /** + * Returns a socket that can read `readableByteCount` incoming bytes and + * will accept `writableByteCount` written bytes. The socket will idle + * for 5 seconds when the required data has been read and written. + */ + fun socket(readableByteCount: Int, writableByteCount: Int): Socket { + val inetAddress = InetAddress.getByName("localhost") + val serverSocket = ServerSocket(0, 50, inetAddress) + serverSocket.reuseAddress = true + serverSocket.receiveBufferSize = SOCKET_BUFFER_SIZE + val peer: Thread = object : Thread("peer") { + override fun run() { + var socket: Socket? = null + try { + socket = serverSocket.accept() + socket.sendBufferSize = SOCKET_BUFFER_SIZE + writeFully(socket.getOutputStream(), readableByteCount) + readFully(socket.getInputStream(), writableByteCount) + sleep(5000) // Sleep 5 seconds so the peer can close the connection. + } catch (ignored: Exception) { + } finally { + try { + socket?.close() + } catch (ignored: IOException) { + } + } + } + } + peer.start() + val socket = Socket(serverSocket.inetAddress, serverSocket.localPort) + socket.receiveBufferSize = SOCKET_BUFFER_SIZE + socket.sendBufferSize = SOCKET_BUFFER_SIZE + return socket + } + + private fun writeFully(out: OutputStream, byteCount: Int) { + out.write(ByteArray(byteCount)) + out.flush() + } + + private fun readFully(`in`: InputStream, byteCount: Int): ByteArray { + var count = 0 + val result = ByteArray(byteCount) + while (count < byteCount) { + val read = `in`.read(result, count, result.size - count) + if (read == -1) throw EOFException() + count += read + } + return result + } + } +} diff --git a/okio/src/jvmTest/kotlin/okio/TestUtil.kt b/okio/src/jvmTest/kotlin/okio/TestUtil.kt index f9f61e62..703986e7 100644 --- a/okio/src/jvmTest/kotlin/okio/TestUtil.kt +++ b/okio/src/jvmTest/kotlin/okio/TestUtil.kt @@ -15,16 +15,17 @@ */ package okio -import okio.ByteString.Companion.encodeUtf8 -import org.junit.Assume import java.io.IOException import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.io.Serializable +import java.util.Locale import java.util.Random import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue +import okio.ByteString.Companion.encodeUtf8 +import org.junit.Assume object TestUtil { // Necessary to make an internal member visible to Java. @@ -175,10 +176,10 @@ object TestUtil { } /** Serializes original to bytes, then deserializes those bytes and returns the result. */ + // Assume serialization doesn't change types. @Suppress("UNCHECKED_CAST") @Throws(Exception::class) @JvmStatic - // Assume serialization doesn't change types. fun <T : Serializable> reserialize(original: T): T { val buffer = Buffer() val out = ObjectOutputStream(buffer.outputStream()) @@ -298,5 +299,5 @@ object TestUtil { return reversed.toShort() } - fun assumeNotWindows() = Assume.assumeFalse(System.getProperty("os.name").toLowerCase().contains("win")) + fun assumeNotWindows() = Assume.assumeFalse(System.getProperty("os.name").lowercase(Locale.getDefault()).contains("win")) } diff --git a/okio/src/jvmTest/kotlin/okio/ThrottlerTakeTest.kt b/okio/src/jvmTest/kotlin/okio/ThrottlerTakeTest.kt index aab94b30..5349dd3d 100644 --- a/okio/src/jvmTest/kotlin/okio/ThrottlerTakeTest.kt +++ b/okio/src/jvmTest/kotlin/okio/ThrottlerTakeTest.kt @@ -15,9 +15,9 @@ */ package okio +import java.util.concurrent.TimeUnit import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import java.util.concurrent.TimeUnit class ThrottlerTakeTest { private var nowNanos = 0L diff --git a/okio/src/jvmTest/kotlin/okio/ThrottlerTest.kt b/okio/src/jvmTest/kotlin/okio/ThrottlerTest.kt index 0b151794..baa487c1 100644 --- a/okio/src/jvmTest/kotlin/okio/ThrottlerTest.kt +++ b/okio/src/jvmTest/kotlin/okio/ThrottlerTest.kt @@ -15,12 +15,11 @@ */ package okio +import kotlin.test.Ignore import okio.TestUtil.randomSource import org.junit.After import org.junit.Before import org.junit.Test -import java.util.concurrent.Executors -import kotlin.test.Ignore @Ignore("These tests are flaky and fail on slower hardware, need to be improved") class ThrottlerTest { @@ -31,7 +30,7 @@ class ThrottlerTest { private val throttlerSlow = Throttler() private val threads = 4 - private val executorService = Executors.newFixedThreadPool(threads) + private val executorService = TestingExecutors.newExecutorService(threads) private var stopwatch = Stopwatch() @Before fun setup() { diff --git a/okio/src/jvmTest/kotlin/okio/TimeoutFactory.kt b/okio/src/jvmTest/kotlin/okio/TimeoutFactory.kt new file mode 100644 index 00000000..3a9cac7c --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/TimeoutFactory.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 + +enum class TimeoutFactory { + BASE { + override fun newTimeout() = Timeout() + }, + + FORWARDING { + override fun newTimeout() = ForwardingTimeout(BASE.newTimeout()) + }, + + ASYNC { + override fun newTimeout() = AsyncTimeout() + }, + ; + + abstract fun newTimeout(): Timeout +} diff --git a/okio/src/jvmTest/kotlin/okio/TimeoutTest.kt b/okio/src/jvmTest/kotlin/okio/TimeoutTest.kt new file mode 100644 index 00000000..2d5c7989 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/TimeoutTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 java.util.concurrent.TimeUnit +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Rule +import org.junit.Test +import org.junit.rules.Timeout as JUnitTimeout + +class TimeoutTest { + @JvmField @Rule + val timeout = JUnitTimeout(5, TimeUnit.SECONDS) + + private val executorService = TestingExecutors.newExecutorService(1) + + @After + @Throws(Exception::class) + fun tearDown() { + executorService.shutdown() + } + + @Test fun intersectWithReturnsAValue() { + val timeoutA = Timeout() + val timeoutB = Timeout() + + val s = timeoutA.intersectWith(timeoutB) { "hello" } + assertEquals("hello", s) + } + + @Test fun intersectWithPrefersSmallerTimeout() { + val timeoutA = Timeout() + timeoutA.timeout(smallerTimeoutNanos, TimeUnit.NANOSECONDS) + + val timeoutB = Timeout() + timeoutB.timeout(biggerTimeoutNanos, TimeUnit.NANOSECONDS) + + timeoutA.intersectWith(timeoutB) { + assertEquals(smallerTimeoutNanos, timeoutA.timeoutNanos()) + assertEquals(biggerTimeoutNanos, timeoutB.timeoutNanos()) + } + timeoutB.intersectWith(timeoutA) { + assertEquals(smallerTimeoutNanos, timeoutA.timeoutNanos()) + assertEquals(smallerTimeoutNanos, timeoutB.timeoutNanos()) + } + assertEquals(smallerTimeoutNanos, timeoutA.timeoutNanos()) + assertEquals(biggerTimeoutNanos, timeoutB.timeoutNanos()) + } + + @Test fun intersectWithPrefersNonZeroTimeout() { + val timeoutA = Timeout() + + val timeoutB = Timeout() + timeoutB.timeout(biggerTimeoutNanos, TimeUnit.NANOSECONDS) + + timeoutA.intersectWith(timeoutB) { + assertEquals(biggerTimeoutNanos, timeoutA.timeoutNanos()) + assertEquals(biggerTimeoutNanos, timeoutB.timeoutNanos()) + } + timeoutB.intersectWith(timeoutA) { + assertEquals(0L, timeoutA.timeoutNanos()) + assertEquals(biggerTimeoutNanos, timeoutB.timeoutNanos()) + } + assertEquals(0L, timeoutA.timeoutNanos()) + assertEquals(biggerTimeoutNanos, timeoutB.timeoutNanos()) + } + + @Test fun intersectWithPrefersSmallerDeadline() { + val timeoutA = Timeout() + timeoutA.deadlineNanoTime(smallerDeadlineNanos) + + val timeoutB = Timeout() + timeoutB.deadlineNanoTime(biggerDeadlineNanos) + + timeoutA.intersectWith(timeoutB) { + assertEquals(smallerDeadlineNanos, timeoutA.deadlineNanoTime()) + assertEquals(biggerDeadlineNanos, timeoutB.deadlineNanoTime()) + } + timeoutB.intersectWith(timeoutA) { + assertEquals(smallerDeadlineNanos, timeoutA.deadlineNanoTime()) + assertEquals(smallerDeadlineNanos, timeoutB.deadlineNanoTime()) + } + assertEquals(smallerDeadlineNanos, timeoutA.deadlineNanoTime()) + assertEquals(biggerDeadlineNanos, timeoutB.deadlineNanoTime()) + } + + @Test fun intersectWithPrefersNonZeroDeadline() { + val timeoutA = Timeout() + + val timeoutB = Timeout() + timeoutB.deadlineNanoTime(biggerDeadlineNanos) + + timeoutA.intersectWith(timeoutB) { + assertEquals(biggerDeadlineNanos, timeoutA.deadlineNanoTime()) + assertEquals(biggerDeadlineNanos, timeoutB.deadlineNanoTime()) + } + timeoutB.intersectWith(timeoutA) { + assertFalse(timeoutA.hasDeadline()) + assertEquals(biggerDeadlineNanos, timeoutB.deadlineNanoTime()) + } + assertFalse(timeoutA.hasDeadline()) + assertEquals(biggerDeadlineNanos, timeoutB.deadlineNanoTime()) + } + + companion object { + val smallerTimeoutNanos = TimeUnit.MILLISECONDS.toNanos(500L) + val biggerTimeoutNanos = TimeUnit.MILLISECONDS.toNanos(1500L) + + val smallerDeadlineNanos = TimeUnit.MILLISECONDS.toNanos(500L) + val biggerDeadlineNanos = TimeUnit.MILLISECONDS.toNanos(1500L) + } +} diff --git a/okio/src/jvmTest/kotlin/okio/Utf8Test.kt b/okio/src/jvmTest/kotlin/okio/Utf8Test.kt new file mode 100644 index 00000000..3657f136 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/Utf8Test.kt @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 java.io.EOFException +import kotlin.text.Charsets.UTF_8 +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.of +import okio.TestUtil.SEGMENT_SIZE +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test + +class Utf8Test { + @Test + fun oneByteCharacters() { + assertEncoded("00", 0x00) // Smallest 1-byte character. + assertEncoded("20", ' '.code) + assertEncoded("7e", '~'.code) + assertEncoded("7f", 0x7f) // Largest 1-byte character. + } + + @Test + fun twoByteCharacters() { + assertEncoded("c280", 0x0080) // Smallest 2-byte character. + assertEncoded("c3bf", 0x00ff) + assertEncoded("c480", 0x0100) + assertEncoded("dfbf", 0x07ff) // Largest 2-byte character. + } + + @Test + fun threeByteCharacters() { + assertEncoded("e0a080", 0x0800) // Smallest 3-byte character. + assertEncoded("e0bfbf", 0x0fff) + assertEncoded("e18080", 0x1000) + assertEncoded("e1bfbf", 0x1fff) + assertEncoded("ed8080", 0xd000) + assertEncoded("ed9fbf", 0xd7ff) // Largest character lower than the min surrogate. + assertEncoded("ee8080", 0xe000) // Smallest character greater than the max surrogate. + assertEncoded("eebfbf", 0xefff) + assertEncoded("ef8080", 0xf000) + assertEncoded("efbfbf", 0xffff) // Largest 3-byte character. + } + + @Test + fun fourByteCharacters() { + assertEncoded("f0908080", 0x010000) // Smallest surrogate pair. + assertEncoded("f48fbfbf", 0x10ffff) // Largest code point expressible by UTF-16. + } + + @Test + fun danglingHighSurrogate() { + assertStringEncoded("3f", "\ud800") // "?" + } + + @Test + fun lowSurrogateWithoutHighSurrogate() { + assertStringEncoded("3f", "\udc00") // "?" + } + + @Test + fun highSurrogateFollowedByNonSurrogate() { + assertStringEncoded("3f61", "\ud800\u0061") // "?a": Following character is too low. + assertStringEncoded("3fee8080", "\ud800\ue000") // "?\ue000": Following character is too high. + } + + @Test + fun doubleLowSurrogate() { + assertStringEncoded("3f3f", "\udc00\udc00") // "??" + } + + @Test + fun doubleHighSurrogate() { + assertStringEncoded("3f3f", "\ud800\ud800") // "??" + } + + @Test + fun highSurrogateLowSurrogate() { + assertStringEncoded("3f3f", "\udc00\ud800") // "??" + } + + @Test + fun multipleSegmentString() { + val a = "a".repeat(SEGMENT_SIZE + SEGMENT_SIZE + 1) + val encoded = Buffer().writeUtf8(a) + val expected = Buffer().write(a.toByteArray(UTF_8)) + assertEquals(expected, encoded) + } + + @Test + fun stringSpansSegments() { + val buffer = Buffer() + val a = "a".repeat(SEGMENT_SIZE - 1) + val b = "bb" + val c = "c".repeat(SEGMENT_SIZE - 1) + buffer.writeUtf8(a) + buffer.writeUtf8(b) + buffer.writeUtf8(c) + assertEquals(a + b + c, buffer.readUtf8()) + } + + @Test + fun readEmptyBufferThrowsEofException() { + val buffer = Buffer() + try { + buffer.readUtf8CodePoint() + fail() + } catch (expected: EOFException) { + } + } + + @Test + fun readLeadingContinuationByteReturnsReplacementCharacter() { + val buffer = Buffer() + buffer.writeByte(0xbf) + assertEquals(REPLACEMENT_CODE_POINT.toLong(), buffer.readUtf8CodePoint().toLong()) + assertTrue(buffer.exhausted()) + } + + @Test + fun readMissingContinuationBytesThrowsEofException() { + val buffer = Buffer() + buffer.writeByte(0xdf) + try { + buffer.readUtf8CodePoint() + fail() + } catch (expected: EOFException) { + } + assertFalse(buffer.exhausted()) // Prefix byte wasn't consumed. + } + + @Test + fun readTooLargeCodepointReturnsReplacementCharacter() { + // 5-byte and 6-byte code points are not supported. + val buffer = Buffer() + buffer.write("f888808080".decodeHex()) + assertEquals(REPLACEMENT_CODE_POINT.toLong(), buffer.readUtf8CodePoint().toLong()) + assertEquals(REPLACEMENT_CODE_POINT.toLong(), buffer.readUtf8CodePoint().toLong()) + assertEquals(REPLACEMENT_CODE_POINT.toLong(), buffer.readUtf8CodePoint().toLong()) + assertEquals(REPLACEMENT_CODE_POINT.toLong(), buffer.readUtf8CodePoint().toLong()) + assertEquals(REPLACEMENT_CODE_POINT.toLong(), buffer.readUtf8CodePoint().toLong()) + assertTrue(buffer.exhausted()) + } + + @Test + fun readNonContinuationBytesReturnsReplacementCharacter() { + // Use a non-continuation byte where a continuation byte is expected. + val buffer = Buffer() + buffer.write("df20".decodeHex()) + assertEquals(REPLACEMENT_CODE_POINT.toLong(), buffer.readUtf8CodePoint().toLong()) + assertEquals(0x20, buffer.readUtf8CodePoint().toLong()) // Non-continuation character not consumed. + assertTrue(buffer.exhausted()) + } + + @Test + fun readCodePointBeyondUnicodeMaximum() { + // A 4-byte encoding with data above the U+10ffff Unicode maximum. + val buffer = Buffer() + buffer.write("f4908080".decodeHex()) + assertEquals(REPLACEMENT_CODE_POINT.toLong(), buffer.readUtf8CodePoint().toLong()) + assertTrue(buffer.exhausted()) + } + + @Test + fun readSurrogateCodePoint() { + val buffer = Buffer() + buffer.write("eda080".decodeHex()) + assertEquals(REPLACEMENT_CODE_POINT.toLong(), buffer.readUtf8CodePoint().toLong()) + assertTrue(buffer.exhausted()) + buffer.write("edbfbf".decodeHex()) + assertEquals(REPLACEMENT_CODE_POINT.toLong(), buffer.readUtf8CodePoint().toLong()) + assertTrue(buffer.exhausted()) + } + + @Test + fun readOverlongCodePoint() { + // Use 2 bytes to encode data that only needs 1 byte. + val buffer = Buffer() + buffer.write("c080".decodeHex()) + assertEquals(REPLACEMENT_CODE_POINT.toLong(), buffer.readUtf8CodePoint().toLong()) + assertTrue(buffer.exhausted()) + } + + @Test + fun writeSurrogateCodePoint() { + assertStringEncoded("ed9fbf", "\ud7ff") // Below lowest surrogate is okay. + assertStringEncoded("3f", "\ud800") // Lowest surrogate gets '?'. + assertStringEncoded("3f", "\udfff") // Highest surrogate gets '?'. + assertStringEncoded("ee8080", "\ue000") // Above highest surrogate is okay. + } + + @Test + fun writeCodePointBeyondUnicodeMaximum() { + val buffer = Buffer() + try { + buffer.writeUtf8CodePoint(0x110000) + fail() + } catch (expected: IllegalArgumentException) { + assertEquals("Unexpected code point: 0x110000", expected.message) + } + } + + @Test + fun size() { + assertEquals(0, "".utf8Size()) + assertEquals(3, "abc".utf8Size()) + assertEquals(16, "təˈranəˌsôr".utf8Size()) + } + + @Test + fun sizeWithBounds() { + assertEquals(0, "".utf8Size(0, 0)) + assertEquals(0, "abc".utf8Size(0, 0)) + assertEquals(1, "abc".utf8Size(1, 2)) + assertEquals(2, "abc".utf8Size(0, 2)) + assertEquals(3, "abc".utf8Size(0, 3)) + assertEquals(16, "təˈranəˌsôr".utf8Size(0, 11)) + assertEquals(5, "təˈranəˌsôr".utf8Size(3, 7)) + } + + @Test + fun sizeBoundsCheck() { + try { + null!!.utf8Size(0, 0) + fail() + } catch (expected: NullPointerException) { + } + try { + "abc".utf8Size(-1, 2) + fail() + } catch (expected: IllegalArgumentException) { + } + try { + "abc".utf8Size(2, 1) + fail() + } catch (expected: IllegalArgumentException) { + } + try { + "abc".utf8Size(1, 4) + fail() + } catch (expected: IllegalArgumentException) { + } + } + + private fun assertEncoded(hex: String, vararg codePoints: Int) { + assertCodePointEncoded(hex, *codePoints) + assertCodePointDecoded(hex, *codePoints) + assertStringEncoded(hex, String(codePoints, 0, codePoints.size)) + } + + private fun assertCodePointEncoded(hex: String, vararg codePoints: Int) { + val buffer = Buffer() + for (codePoint in codePoints) { + buffer.writeUtf8CodePoint(codePoint) + } + assertEquals(buffer.readByteString(), hex.decodeHex()) + } + + private fun assertCodePointDecoded(hex: String, vararg codePoints: Int) { + val buffer = Buffer().write(hex.decodeHex()) + for (codePoint in codePoints) { + assertEquals(codePoint.toLong(), buffer.readUtf8CodePoint().toLong()) + } + assertTrue(buffer.exhausted()) + } + + private fun assertStringEncoded(hex: String, string: String) { + val expectedUtf8 = hex.decodeHex() + + // Confirm our expectations are consistent with the platform. + val platformUtf8 = of(*string.toByteArray(charset("UTF-8"))) + assertEquals(expectedUtf8, platformUtf8) + + // Confirm our implementation matches those expectations. + val actualUtf8 = Buffer().writeUtf8(string).readByteString() + assertEquals(expectedUtf8, actualUtf8) + + // Confirm we are consistent when writing one code point at a time. + val bufferUtf8 = Buffer() + var i = 0 + while (i < string.length) { + val c = string.codePointAt(i) + bufferUtf8.writeUtf8CodePoint(c) + i += Character.charCount(c) + } + assertEquals(expectedUtf8, bufferUtf8.readByteString()) + + // Confirm we are consistent when measuring lengths. + assertEquals(expectedUtf8.size.toLong(), string.utf8Size()) + assertEquals(expectedUtf8.size.toLong(), string.utf8Size(0, string.length)) + } +} diff --git a/okio/src/jvmTest/kotlin/okio/WaitUntilNotifiedTest.kt b/okio/src/jvmTest/kotlin/okio/WaitUntilNotifiedTest.kt new file mode 100644 index 00000000..44c6bbfd --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/WaitUntilNotifiedTest.kt @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2016 Square, Inc. + * + * 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 java.io.InterruptedIOException +import java.util.concurrent.TimeUnit +import okio.TestUtil.assumeNotWindows +import okio.TestingExecutors.newScheduledExecutorService +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@RunWith(Parameterized::class) +class WaitUntilNotifiedTest( + factory: TimeoutFactory, +) { + private val timeout = factory.newTimeout() + private val executorService = newScheduledExecutorService(0) + + @After + fun tearDown() { + executorService.shutdown() + } + + @Test + @Synchronized + fun notified() { + timeout.timeout(5000, TimeUnit.MILLISECONDS) + val start = now() + executorService.schedule( + { + synchronized(this@WaitUntilNotifiedTest) { + (this as Object).notify() + } + }, + 1000, + TimeUnit.MILLISECONDS, + ) + timeout.waitUntilNotified(this) + assertElapsed(1000.0, start) + } + + @Test + @Synchronized + fun timeout() { + assumeNotWindows() + timeout.timeout(1000, TimeUnit.MILLISECONDS) + val start = now() + try { + timeout.waitUntilNotified(this) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(1000.0, start) + } + + @Test + @Synchronized + fun deadline() { + assumeNotWindows() + timeout.deadline(1000, TimeUnit.MILLISECONDS) + val start = now() + try { + timeout.waitUntilNotified(this) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(1000.0, start) + } + + @Test + @Synchronized + fun deadlineBeforeTimeout() { + assumeNotWindows() + timeout.timeout(5000, TimeUnit.MILLISECONDS) + timeout.deadline(1000, TimeUnit.MILLISECONDS) + val start = now() + try { + timeout.waitUntilNotified(this) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(1000.0, start) + } + + @Test + @Synchronized + fun timeoutBeforeDeadline() { + assumeNotWindows() + timeout.timeout(1000, TimeUnit.MILLISECONDS) + timeout.deadline(5000, TimeUnit.MILLISECONDS) + val start = now() + try { + timeout.waitUntilNotified(this) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(1000.0, start) + } + + @Test + @Synchronized + fun deadlineAlreadyReached() { + assumeNotWindows() + timeout.deadlineNanoTime(System.nanoTime()) + val start = now() + try { + timeout.waitUntilNotified(this) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(0.0, start) + } + + @Test + @Synchronized + fun threadInterrupted() { + assumeNotWindows() + val start = now() + Thread.currentThread().interrupt() + try { + timeout.waitUntilNotified(this) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("interrupted", expected.message) + assertTrue(Thread.interrupted()) + } + assertElapsed(0.0, start) + } + + @Test + @Synchronized + fun threadInterruptedOnThrowIfReached() { + assumeNotWindows() + Thread.currentThread().interrupt() + try { + timeout.throwIfReached() + fail() + } catch (expected: InterruptedIOException) { + assertEquals("interrupted", expected.message) + assertTrue(Thread.interrupted()) + } + } + + @Test + @Synchronized + fun cancelBeforeWaitDoesNothing() { + assumeNotWindows() + timeout.timeout(1000, TimeUnit.MILLISECONDS) + timeout.cancel() + val start = now() + try { + timeout.waitUntilNotified(this) + fail() + } catch (expected: InterruptedIOException) { + assertEquals("timeout", expected.message) + } + assertElapsed(1000.0, start) + } + + @Test + @Synchronized + fun canceledTimeoutDoesNotThrowWhenNotNotifiedOnTime() { + timeout.timeout(1000, TimeUnit.MILLISECONDS) + timeout.cancelLater(500) + + val start = now() + timeout.waitUntilNotified(this) // Returns early but doesn't throw. + assertElapsed(1000.0, start) + } + + @Test + @Synchronized + fun multipleCancelsAreIdempotent() { + timeout.timeout(1000, TimeUnit.MILLISECONDS) + timeout.cancelLater(250) + timeout.cancelLater(500) + timeout.cancelLater(750) + + val start = now() + timeout.waitUntilNotified(this) // Returns early but doesn't throw. + assertElapsed(1000.0, start) + } + + /** Returns the nanotime in milliseconds as a double for measuring timeouts. */ + private fun now(): Double { + return System.nanoTime() / 1000000.0 + } + + /** + * Fails the test unless the time from start until now is duration, accepting differences in + * -50..+450 milliseconds. + */ + private fun assertElapsed(duration: Double, start: Double) { + assertEquals(duration, now() - start - 200.0, 250.0) + } + + private fun Timeout.cancelLater(delay: Long) { + executorService.schedule( + { + cancel() + }, + delay, + TimeUnit.MILLISECONDS, + ) + } + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun parameters(): List<Array<out Any?>> = TimeoutFactory.entries.map { arrayOf(it) } + } +} diff --git a/okio/src/jvmTest/kotlin/okio/ZipBuilder.kt b/okio/src/jvmTest/kotlin/okio/ZipBuilder.kt new file mode 100644 index 00000000..1abcc031 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/ZipBuilder.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 org.junit.Assume.assumeTrue + +/** + * Execute the `zip` command line program to create reference zip files for testing. + * + * Unfortunately the `zip` command line is limited in its ability to create exactly the zip files + * we want for testing. In particular, the only way it will create zip64 archives is if the input + * file is received from a UNIX pipe. (In such cases the file length is unknown in advance, and so + * the tool uses zip64 for the possibility of a very large file.) Files received from a pipe are + * always named `-`. + */ +class ZipBuilder( + private val directory: Path, +) { + private val fileSystem = FileSystem.SYSTEM + + private val entries = mutableListOf<Entry>() + private val options = mutableListOf<String>() + private var archiveComment: String = "" + + @JvmOverloads + fun addEntry( + path: String, + content: String? = null, + directory: Boolean = false, + comment: String = "", + modifiedAt: String? = null, + accessedAt: String? = null, + zip64: Boolean = false, + ) = apply { + entries += Entry(path, content, directory, comment, modifiedAt, accessedAt, zip64) + } + + fun addOption(option: String) = apply { options += option } + + fun archiveComment(archiveComment: String) = apply { this.archiveComment = archiveComment } + + fun build(): Path { + assumeTrue("ZipBuilder doesn't work on Windows", Path.DIRECTORY_SEPARATOR == "/") + + val archive = directory / "${randomToken(16)}.zip" + val anyZip64 = entries.any { it.zip64 } + + require(!anyZip64 || entries.size == 1) { + "ZipBuilder permits at most one zip64 entry" + } + require(!anyZip64 || archiveComment.isEmpty()) { + "Cannot combine archiveComment with zip64" + } + + val promptForComments = entries.any { it.comment.isNotEmpty() } + if (promptForComments) { + options += "--entry-comments" + } + if (archiveComment.isNotEmpty()) { + options += "--archive-comment" + } + + val command = mutableListOf<String>() + command += "zip" + command += options + command += archive.toString() + + for (entry in entries) { + if (!entry.zip64) { + val absolutePath = directory / entry.path + fileSystem.createDirectories(absolutePath.parent!!) + + if (entry.directory) { + fileSystem.createDirectories(absolutePath) + } else { + fileSystem.write(absolutePath) { + writeUtf8(entry.content!!) + } + } + + if (entry.modifiedAt != null) { + touch("-m", absolutePath, entry.modifiedAt) + } + if (entry.accessedAt != null) { + touch("-a", absolutePath, entry.accessedAt) + } + } + command += entry.path + } + + val process = ProcessBuilder() + .command(command) + .directory(directory.toFile()) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() + + process.outputStream.sink().buffer().use { sink -> + if (anyZip64) { + sink.writeUtf8(entries.single().content!!) + } + if (promptForComments) { + for (entry in entries) { + sink.writeUtf8(entry.comment) + sink.writeUtf8("\n") + sink.flush() + } + } + sink.writeUtf8(archiveComment) + } + + val result = process.waitFor() + require(result == 0) { "process failed: $command" } + + return archive + } + + private fun touch(option: String, absolutePath: Path, date: String) { + val exitCode = ProcessBuilder() + .command("touch", option, "-t", date, absolutePath.toString()) + .apply { environment()["TZ"] = "UTC" } + .start() + .waitFor() + require(exitCode == 0) + } + + private class Entry( + val path: String, + val content: String?, + val directory: Boolean, + val comment: String, + val modifiedAt: String?, + val accessedAt: String?, + val zip64: Boolean, + ) { + init { + require(directory != (content != null)) { "must be a directory or have content" } + if (zip64) { + require(path == "-") { "zip64 file name must be '-'" } + require(comment == "") { "zip64 must not have comments" } + require(modifiedAt == null) { "zip64 must not have modifiedAt" } + } + } + } +} diff --git a/okio/src/jvmTest/kotlin/okio/ZipFileSystemJavaTest.kt b/okio/src/jvmTest/kotlin/okio/ZipFileSystemJavaTest.kt new file mode 100644 index 00000000..6d636145 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/ZipFileSystemJavaTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 okio.Path.Companion.toPath +import org.assertj.core.api.Assertions +import org.junit.Before +import org.junit.Test + +class ZipFileSystemJavaTest { + private val fileSystem = FileSystem.SYSTEM + private val base = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.div(randomToken(16)) + + @Before + fun setUp() { + fileSystem.createDirectory(base) + } + + @Test + fun zipFileSystemApi() { + val zipPath = ZipBuilder(base) + .addEntry("hello.txt", "Hello World") + .build() + val zipFileSystem = fileSystem.openZip(zipPath) + zipFileSystem.source("hello.txt".toPath(false)).buffer().use { source -> + val content = source.readUtf8() + Assertions.assertThat(content).isEqualTo("Hello World") + } + } +} diff --git a/okio/src/jvmTest/kotlin/okio/ZipFileSystemTest.kt b/okio/src/jvmTest/kotlin/okio/ZipFileSystemTest.kt new file mode 100644 index 00000000..fd7aa46b --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/ZipFileSystemTest.kt @@ -0,0 +1,502 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 kotlin.test.assertFailsWith +import kotlinx.datetime.Instant +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.encodeUtf8 +import okio.Path.Companion.toPath +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test + +class ZipFileSystemTest { + private val fileSystem = FileSystem.SYSTEM + private var base = FileSystem.SYSTEM_TEMPORARY_DIRECTORY / randomToken(16) + + @Before + fun setUp() { + fileSystem.createDirectory(base) + } + + @Test + fun emptyZip() { + // ZipBuilder cannot write empty zips. + val zipPath = base / "empty.zip" + fileSystem.write(zipPath) { + write("504b0506000000000000000000000000000000000000".decodeHex()) + } + + val zipFileSystem = fileSystem.openZip(zipPath) + assertThat(zipFileSystem.list("/".toPath())).isEmpty() + } + + @Test + fun emptyZipWithPrependedData() { + // ZipBuilder cannot write empty zips. + val zipPath = base / "empty.zip" + fileSystem.write(zipPath) { + writeUtf8("Hello I'm junk data prepended to the ZIP!") + write("504b0506000000000000000000000000000000000000".decodeHex()) + } + + val zipFileSystem = fileSystem.openZip(zipPath) + assertThat(zipFileSystem.list("/".toPath())).isEmpty() + } + + @Test + fun zipWithFiles() { + val zipPath = ZipBuilder(base) + .addEntry("hello.txt", "Hello World") + .addEntry("directory/subdirectory/child.txt", "Another file!") + .build() + val zipFileSystem = fileSystem.openZip(zipPath) + + assertThat(zipFileSystem.read("hello.txt".toPath()) { readUtf8() }) + .isEqualTo("Hello World") + + assertThat(zipFileSystem.read("directory/subdirectory/child.txt".toPath()) { readUtf8() }) + .isEqualTo("Another file!") + + assertThat(zipFileSystem.list("/".toPath())) + .hasSameElementsAs(listOf("/hello.txt".toPath(), "/directory".toPath())) + assertThat(zipFileSystem.list("/directory".toPath())) + .containsExactly("/directory/subdirectory".toPath()) + assertThat(zipFileSystem.list("/directory/subdirectory".toPath())) + .containsExactly("/directory/subdirectory/child.txt".toPath()) + } + + /** + * Note that the zip tool does not compress files that don't benefit from it. Examples above like + * 'Hello World' are stored, not deflated. + */ + @Test + fun zipWithDeflate() { + val content = "Android\n".repeat(1000) + val zipPath = ZipBuilder(base) + .addEntry("a.txt", content) + .addOption("--compression-method") + .addOption("deflate") + .build() + assertThat(fileSystem.metadata(zipPath).size).isLessThan(content.length.toLong()) + val zipFileSystem = fileSystem.openZip(zipPath) + + assertThat(zipFileSystem.read("a.txt".toPath()) { readUtf8() }) + .isEqualTo(content) + } + + @Test + fun zipWithStore() { + val content = "Android\n".repeat(1000) + val zipPath = ZipBuilder(base) + .addEntry("a.txt", content) + .addOption("--compression-method") + .addOption("store") + .build() + assertThat(fileSystem.metadata(zipPath).size).isGreaterThan(content.length.toLong()) + val zipFileSystem = fileSystem.openZip(zipPath) + + assertThat(zipFileSystem.read("a.txt".toPath()) { readUtf8() }) + .isEqualTo(content) + } + + /** + * Confirm we can read zip files that have file comments, even if these comments are not exposed + * in the public API. + */ + @Test + fun zipWithFileComments() { + val zipPath = ZipBuilder(base) + .addEntry("a.txt", "Android", comment = "A is for Android") + .addEntry("b.txt", "Banana", comment = "B or not to Be") + .build() + val zipFileSystem = fileSystem.openZip(zipPath) + + assertThat(zipFileSystem.read("a.txt".toPath()) { readUtf8() }) + .isEqualTo("Android") + + assertThat(zipFileSystem.read("b.txt".toPath()) { readUtf8() }) + .isEqualTo("Banana") + } + + @Test + fun zipWithFileModifiedDate() { + val zipPath = ZipBuilder(base) + .addEntry( + path = "a.txt", + content = "Android", + modifiedAt = "200102030405.06", + accessedAt = "200102030405.07", + ) + .addEntry( + path = "b.txt", + content = "Banana", + modifiedAt = "200908070605.04", + accessedAt = "200908070605.03", + ) + .build() + val zipFileSystem = fileSystem.openZip(zipPath) + + zipFileSystem.metadata("a.txt".toPath()) + .apply { + assertThat(isRegularFile).isTrue() + assertThat(isDirectory).isFalse() + assertThat(size).isEqualTo(7L) + assertThat(createdAtMillis).isNull() + assertThat(lastModifiedAtMillis).isEqualTo("2001-02-03T04:05:06Z".toEpochMillis()) + assertThat(lastAccessedAtMillis).isEqualTo("2001-02-03T04:05:07Z".toEpochMillis()) + } + + zipFileSystem.metadata("b.txt".toPath()) + .apply { + assertThat(isRegularFile).isTrue() + assertThat(isDirectory).isFalse() + assertThat(size).isEqualTo(6L) + assertThat(createdAtMillis).isNull() + assertThat(lastModifiedAtMillis).isEqualTo("2009-08-07T06:05:04Z".toEpochMillis()) + assertThat(lastAccessedAtMillis).isEqualTo("2009-08-07T06:05:03Z".toEpochMillis()) + } + } + + /** Confirm we suffer UNIX limitations on our date format. */ + @Test + fun zipWithFileOutOfBoundsModifiedDate() { + val zipPath = ZipBuilder(base) + .addEntry( + path = "a.txt", + content = "Android", + modifiedAt = "196912310000.00", + accessedAt = "196912300000.00", + ) + .addEntry( + path = "b.txt", + content = "Banana", + modifiedAt = "203801190314.07", // Last UNIX date representable in 31 bits. + accessedAt = "203801190314.08", // Overflows! + ) + .build() + val zipFileSystem = fileSystem.openZip(zipPath) + + println(Instant.fromEpochMilliseconds(-2147483648000L)) + + zipFileSystem.metadata("a.txt".toPath()) + .apply { + assertThat(isRegularFile).isTrue() + assertThat(isDirectory).isFalse() + assertThat(size).isEqualTo(7L) + assertThat(createdAtMillis).isNull() + assertThat(lastModifiedAtMillis).isEqualTo("1969-12-31T00:00:00Z".toEpochMillis()) + assertThat(lastAccessedAtMillis).isEqualTo("1969-12-30T00:00:00Z".toEpochMillis()) + } + + // Greater than the upper bound wraps around. + zipFileSystem.metadata("b.txt".toPath()) + .apply { + assertThat(isRegularFile).isTrue() + assertThat(isDirectory).isFalse() + assertThat(size).isEqualTo(6L) + assertThat(createdAtMillis).isNull() + assertThat(lastModifiedAtMillis).isEqualTo("2038-01-19T03:14:07Z".toEpochMillis()) + assertThat(lastAccessedAtMillis).isEqualTo("1901-12-13T20:45:52Z".toEpochMillis()) + } + } + + /** + * Directories are optional in the zip file. But if we want metadata on them they must be stored. + * Note that this test adds the directories last; otherwise adding child files to them will cause + * their modified at times to change. + */ + @Test + fun zipWithDirectoryModifiedDate() { + val zipPath = ZipBuilder(base) + .addEntry("a/a.txt", "Android") + .addEntry( + path = "a", + directory = true, + modifiedAt = "200102030405.06", + accessedAt = "200102030405.07", + ) + .addEntry("b/b.txt", "Android") + .addEntry( + path = "b", + directory = true, + modifiedAt = "200908070605.04", + accessedAt = "200908070605.03", + ) + .build() + val zipFileSystem = fileSystem.openZip(zipPath) + + zipFileSystem.metadata("a".toPath()) + .apply { + assertThat(isRegularFile).isFalse() + assertThat(isDirectory).isTrue() + assertThat(size).isNull() + assertThat(createdAtMillis).isNull() + assertThat(lastModifiedAtMillis).isEqualTo("2001-02-03T04:05:06Z".toEpochMillis()) + assertThat(lastAccessedAtMillis).isEqualTo("2001-02-03T04:05:07Z".toEpochMillis()) + } + assertThat(zipFileSystem.list("a".toPath())).containsExactly("/a/a.txt".toPath()) + + zipFileSystem.metadata("b".toPath()) + .apply { + assertThat(isRegularFile).isFalse() + assertThat(isDirectory).isTrue() + assertThat(size).isNull() + assertThat(createdAtMillis).isNull() + assertThat(lastModifiedAtMillis).isEqualTo("2009-08-07T06:05:04Z".toEpochMillis()) + assertThat(lastAccessedAtMillis).isEqualTo("2009-08-07T06:05:03Z".toEpochMillis()) + } + assertThat(zipFileSystem.list("b".toPath())).containsExactly("/b/b.txt".toPath()) + } + + @Test + fun zipWithModifiedDate() { + val zipPath = ZipBuilder(base) + .addEntry( + "a/a.txt", + modifiedAt = "197001010001.00", + accessedAt = "197001010002.00", + content = "Android", + ) + .build() + val zipFileSystem = fileSystem.openZip(zipPath) + + zipFileSystem.metadata("a/a.txt".toPath()) + .apply { + assertThat(createdAtMillis).isNull() + assertThat(lastModifiedAtMillis).isEqualTo("1970-01-01T00:01:00Z".toEpochMillis()) + assertThat(lastAccessedAtMillis).isEqualTo("1970-01-01T00:02:00Z".toEpochMillis()) + } + } + + /** Build a very small zip file with just a single empty directory. */ + @Test + fun zipWithEmptyDirectory() { + val zipPath = ZipBuilder(base) + .addEntry( + path = "a", + directory = true, + modifiedAt = "200102030405.06", + accessedAt = "200102030405.07", + ) + .build() + val zipFileSystem = fileSystem.openZip(zipPath) + + zipFileSystem.metadata("a".toPath()) + .apply { + assertThat(isRegularFile).isFalse() + assertThat(isDirectory).isTrue() + assertThat(size).isNull() + assertThat(createdAtMillis).isNull() + assertThat(lastModifiedAtMillis).isEqualTo("2001-02-03T04:05:06Z".toEpochMillis()) + assertThat(lastAccessedAtMillis).isEqualTo("2001-02-03T04:05:07Z".toEpochMillis()) + } + assertThat(zipFileSystem.list("a".toPath())).isEmpty() + } + + /** + * The `--no-dir-entries` option causes the zip file to omit the directories from the encoded + * file. Our implementation synthesizes these missing directories automatically. + */ + @Test + fun zipWithSyntheticDirectory() { + val zipPath = ZipBuilder(base) + .addEntry("a/a.txt", "Android") + .addEntry("a", directory = true) + .addEntry("b/b.txt", "Android") + .addEntry("b", directory = true) + .addOption("--no-dir-entries") + .build() + val zipFileSystem = fileSystem.openZip(zipPath) + + zipFileSystem.metadata("a".toPath()) + .apply { + assertThat(isRegularFile).isFalse() + assertThat(isDirectory).isTrue() + assertThat(size).isNull() + assertThat(createdAtMillis).isNull() + assertThat(lastModifiedAtMillis).isNull() + assertThat(lastAccessedAtMillis).isNull() + } + assertThat(zipFileSystem.list("a".toPath())).containsExactly("/a/a.txt".toPath()) + + zipFileSystem.metadata("b".toPath()) + .apply { + assertThat(isRegularFile).isFalse() + assertThat(isDirectory).isTrue() + assertThat(size).isNull() + assertThat(createdAtMillis).isNull() + assertThat(lastModifiedAtMillis).isNull() + assertThat(lastAccessedAtMillis).isNull() + } + assertThat(zipFileSystem.list("b".toPath())).containsExactly("/b/b.txt".toPath()) + } + + /** + * Force a file to be encoded with zip64 metadata. We use a pipe to force the zip command to + * create a zip64 archive; otherwise we'd need to add a very large file to get this format. + */ + @Test + fun zip64() { + val zipPath = ZipBuilder(base) + .addEntry("-", "Android", zip64 = true) + .build() + val zipFileSystem = fileSystem.openZip(zipPath) + + assertThat(zipFileSystem.read("-".toPath()) { readUtf8() }) + .isEqualTo("Android") + } + + /** + * Confirm we can read zip files with a full-archive comment, even if this comment is not surfaced + * in our API. + */ + @Test + fun zipWithArchiveComment() { + val zipPath = ZipBuilder(base) + .addEntry("a.txt", "Android") + .archiveComment("this comment applies to the entire archive") + .build() + val zipFileSystem = fileSystem.openZip(zipPath) + + assertThat(zipFileSystem.read("a.txt".toPath()) { readUtf8() }) + .isEqualTo("Android") + } + + @Test + fun cannotReadZipWithSpanning() { + // Spanned archives must be at least 64 KiB. + val largeFile = randomToken(length = 128 * 1024) + val zipPath = ZipBuilder(base) + .addEntry("large_file.txt", largeFile) + .addOption("--split-size") + .addOption("64k") + .build() + assertFailsWith<IOException> { + fileSystem.openZip(zipPath) + } + } + + @Test + fun cannotReadZipWithEncryption() { + val zipPath = ZipBuilder(base) + .addEntry("a.txt", "Android") + .addOption("--password") + .addOption("secret") + .build() + assertFailsWith<IOException> { + fileSystem.openZip(zipPath) + } + } + + @Test + fun zipTooShort() { + val zipPath = ZipBuilder(base) + .addEntry("a.txt", "Android") + .build() + + val prefix = fileSystem.read(zipPath) { readByteString(20) } + fileSystem.write(zipPath) { write(prefix) } + + assertFailsWith<IOException> { + fileSystem.openZip(zipPath) + } + } + + /** + * The zip format permits multiple files with the same names. For example, + * `kotlin-gradle-plugin-1.5.20.jar` contains two copies of + * `META-INF/kotlin-gradle-statistics.kotlin_module`. + * + * We used to crash on duplicates, but they are common in practice so now we prefer the last + * entry. This behavior is consistent with both [java.util.zip.ZipFile] and + * [java.nio.file.FileSystem]. + */ + @Test + fun filesOverlap() { + val zipPath = ZipBuilder(base) + .addEntry("hello.txt", "This is the first hello.txt") + .addEntry("xxxxx.xxx", "This is the second hello.txt") + .build() + val original = fileSystem.read(zipPath) { readByteString() } + val rewritten = original.replaceAll("xxxxx.xxx".encodeUtf8(), "hello.txt".encodeUtf8()) + fileSystem.write(zipPath) { write(rewritten) } + + val zipFileSystem = fileSystem.openZip(zipPath) + assertThat(zipFileSystem.read("hello.txt".toPath()) { readUtf8() }) + .isEqualTo("This is the second hello.txt") + assertThat(zipFileSystem.list("/".toPath())) + .containsExactly("/hello.txt".toPath()) + } + + @Test + fun canonicalizationValid() { + val zipPath = ZipBuilder(base) + .addEntry("hello.txt", "Hello World") + .addEntry("directory/child.txt", "Another file!") + .build() + val zipFileSystem = fileSystem.openZip(zipPath) + + assertThat(zipFileSystem.canonicalize("/".toPath())).isEqualTo("/".toPath()) + assertThat(zipFileSystem.canonicalize(".".toPath())).isEqualTo("/".toPath()) + assertThat(zipFileSystem.canonicalize("not/a/path/../../..".toPath())).isEqualTo("/".toPath()) + assertThat(zipFileSystem.canonicalize("hello.txt".toPath())).isEqualTo("/hello.txt".toPath()) + assertThat(zipFileSystem.canonicalize("stuff/../hello.txt".toPath())).isEqualTo("/hello.txt".toPath()) + assertThat(zipFileSystem.canonicalize("directory".toPath())).isEqualTo("/directory".toPath()) + assertThat(zipFileSystem.canonicalize("directory/whevs/..".toPath())).isEqualTo("/directory".toPath()) + assertThat(zipFileSystem.canonicalize("directory/child.txt".toPath())).isEqualTo("/directory/child.txt".toPath()) + assertThat(zipFileSystem.canonicalize("directory/whevs/../child.txt".toPath())).isEqualTo("/directory/child.txt".toPath()) + } + + @Test + fun canonicalizationInvalidThrows() { + val zipPath = ZipBuilder(base) + .addEntry("hello.txt", "Hello World") + .addEntry("directory/child.txt", "Another file!") + .build() + val zipFileSystem = fileSystem.openZip(zipPath) + + assertFailsWith<FileNotFoundException> { + zipFileSystem.canonicalize("not/a/path".toPath()) + } + } +} + +private fun ByteString.replaceAll(a: ByteString, b: ByteString): ByteString { + val buffer = Buffer() + buffer.write(this) + buffer.replace(a, b) + return buffer.readByteString() +} + +private fun Buffer.replace(a: ByteString, b: ByteString) { + val result = Buffer() + while (!exhausted()) { + val index = indexOf(a) + if (index == -1L) { + result.writeAll(this) + } else { + result.write(this, index) + result.write(b) + skip(a.size.toLong()) + } + } + writeAll(result) +} + +/** Decodes this ISO8601 time string. */ +fun String.toEpochMillis() = Instant.parse(this).toEpochMilliseconds() diff --git a/okio/src/jvmTest/java/okio/internal/HmacTest.kt b/okio/src/jvmTest/kotlin/okio/internal/HmacTest.kt index 0c5a7f43..01666d17 100644 --- a/okio/src/jvmTest/java/okio/internal/HmacTest.kt +++ b/okio/src/jvmTest/kotlin/okio/internal/HmacTest.kt @@ -15,14 +15,14 @@ */ package okio.internal +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.random.Random import okio.ByteString -import org.junit.Assert.assertArrayEquals +import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec -import kotlin.random.Random /** * Check the [Hmac] implementation against the reference [Mac] JVM implementation. @@ -44,7 +44,7 @@ class HmacTest(val parameters: Parameters) { Parameters( algorithm, keySize, - dataSize + dataSize, ) } } @@ -72,7 +72,7 @@ class HmacTest(val parameters: Parameters) { mac.update(bytes) val hmacValue = mac.digest() - assertArrayEquals(expected, hmacValue) + Assert.assertArrayEquals(expected, hmacValue) } @Test @@ -82,13 +82,13 @@ class HmacTest(val parameters: Parameters) { } val hmacValue = mac.digest() - assertArrayEquals(expected, hmacValue) + Assert.assertArrayEquals(expected, hmacValue) } data class Parameters( val algorithm: Algorithm, val keySize: Int, - val dataSize: Int + val dataSize: Int, ) { val algorithmName get() = algorithm.algorithmName @@ -98,7 +98,7 @@ class HmacTest(val parameters: Parameters) { enum class Algorithm( val algorithmName: String, - internal val HmacFactory: (key: ByteString) -> Hmac + internal val HmacFactory: (key: ByteString) -> Hmac, ) { SHA_1("HmacSha1", Hmac.Companion::sha1), SHA_256("HmacSha256", Hmac.Companion::sha256), diff --git a/okio/src/jvmTest/kotlin/okio/internal/ResourceFileSystemTest.kt b/okio/src/jvmTest/kotlin/okio/internal/ResourceFileSystemTest.kt new file mode 100644 index 00000000..13c63158 --- /dev/null +++ b/okio/src/jvmTest/kotlin/okio/internal/ResourceFileSystemTest.kt @@ -0,0 +1,505 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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.internal + +import java.net.URL +import java.net.URLClassLoader +import java.util.Enumeration +import kotlin.reflect.KClass +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.fail +import okio.BufferedSource +import okio.ByteString +import okio.FileNotFoundException +import okio.FileSystem +import okio.ForwardingFileSystem +import okio.IOException +import okio.Path +import okio.Path.Companion.toPath +import okio.ZipBuilder +import okio.randomToken +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class ResourceFileSystemTest { + private val fileSystem = FileSystem.RESOURCES as ResourceFileSystem + private var base = FileSystem.SYSTEM_TEMPORARY_DIRECTORY / randomToken(16) + + @Test + fun testResourceA() { + val path = "okio/resourcefilesystem/a.txt".toPath() + + val metadata = fileSystem.metadataOrNull(path)!! + + assertThat(metadata.size).isEqualTo(1L) + assertThat(metadata.isRegularFile).isTrue() + assertThat(metadata.isDirectory).isFalse() + + val content = fileSystem.read(path) { readUtf8() } + assertThat(fileSystem.metadata(path).isRegularFile).isTrue() + + assertThat(content).isEqualTo("a") + } + + @Test + fun testResourceB() { + val path = "okio/resourcefilesystem/b/b.txt".toPath() + + val metadata = fileSystem.metadataOrNull(path)!! + + assertThat(metadata.size).isEqualTo(3L) + assertThat(metadata.isRegularFile).isTrue() + assertThat(metadata.isDirectory).isFalse() + + val content = fileSystem.read(path) { readUtf8() } + assertThat(fileSystem.metadata(path).isRegularFile).isTrue() + + assertThat(content).isEqualTo("b/b") + } + + @Test + fun testSingleArchive() { + val zipPath = ZipBuilder(base) + .addEntry("hello.txt", "Hello World") + .addEntry("directory/subdirectory/child.txt", "Another file!") + .addEntry("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n") + .build() + val resourceFileSystem = ResourceFileSystem( + classLoader = URLClassLoader(arrayOf(zipPath.toFile().toURI().toURL()), null), + indexEagerly = false, + ) + + assertThat(resourceFileSystem.read("hello.txt".toPath()) { readUtf8() }) + .isEqualTo("Hello World") + assertThat( + resourceFileSystem.metadata("hello.txt".toPath()).isRegularFile, + ).isTrue() + + assertThat(resourceFileSystem.read("directory/subdirectory/child.txt".toPath()) { readUtf8() }) + .isEqualTo("Another file!") + assertThat( + resourceFileSystem.metadata("directory/subdirectory/child.txt".toPath()).isRegularFile, + ).isTrue() + + assertThat(resourceFileSystem.list("/".toPath())) + .hasSameElementsAs(listOf("/META-INF".toPath(), "/hello.txt".toPath(), "/directory".toPath())) + assertThat(resourceFileSystem.list("/directory".toPath())) + .containsExactly("/directory/subdirectory".toPath()) + assertThat(resourceFileSystem.list("/directory/subdirectory".toPath())) + .containsExactly("/directory/subdirectory/child.txt".toPath()) + + val metadata = resourceFileSystem.metadata(".".toPath()) + assertThat(metadata.isDirectory).isTrue() + } + + @Test + fun testDirectoryAndJarOverlap() { + val filesAPath = base / "filesA" + FileSystem.SYSTEM.createDirectories(filesAPath / "colors") + FileSystem.SYSTEM.write(filesAPath / "colors" / "red.txt") { writeUtf8("Apples are red") } + FileSystem.SYSTEM.write(filesAPath / "colors" / "green.txt") { writeUtf8("Grass is green") } + val zipBPath = ZipBuilder(base) + .addEntry("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n") + .addEntry("colors/blue.txt", "The sky is blue") + .addEntry("colors/green.txt", "Limes are green") + .build() + + val resourceFileSystem = ResourceFileSystem( + classLoader = URLClassLoader( + arrayOf( + filesAPath.toFile().toURI().toURL(), + zipBPath.toFile().toURI().toURL(), + ), + null, + ), + indexEagerly = false, + ) + + assertThat(resourceFileSystem.read("/colors/red.txt".toPath()) { readUtf8() }) + .isEqualTo("Apples are red") + assertThat(resourceFileSystem.read("/colors/green.txt".toPath()) { readUtf8() }) + .isEqualTo("Grass is green") + assertThat(resourceFileSystem.read("/colors/blue.txt".toPath()) { readUtf8() }) + .isEqualTo("The sky is blue") + + assertThat(resourceFileSystem.metadata("/colors/red.txt".toPath()).isRegularFile).isTrue() + assertThat(resourceFileSystem.metadata("/colors/green.txt".toPath()).isRegularFile).isTrue() + assertThat(resourceFileSystem.metadata("/colors/blue.txt".toPath()).isRegularFile).isTrue() + + assertThat(resourceFileSystem.list("/".toPath())) + .hasSameElementsAs(listOf("/META-INF".toPath(), "/colors".toPath())) + assertThat(resourceFileSystem.list("/colors".toPath())).hasSameElementsAs( + listOf( + "/colors/red.txt".toPath(), + "/colors/green.txt".toPath(), + "/colors/blue.txt".toPath(), + ), + ) + + assertThat(resourceFileSystem.metadata("/".toPath()).isDirectory).isTrue() + assertThat(resourceFileSystem.metadata("/colors".toPath()).isDirectory).isTrue() + } + + @Test + fun testDirectoryAndDirectoryOverlap() { + val filesAPath = base / "filesA" + FileSystem.SYSTEM.createDirectories(filesAPath / "colors") + FileSystem.SYSTEM.write(filesAPath / "colors" / "red.txt") { writeUtf8("Apples are red") } + FileSystem.SYSTEM.write(filesAPath / "colors" / "green.txt") { writeUtf8("Grass is green") } + val filesBPath = base / "filesB" + FileSystem.SYSTEM.createDirectories(filesBPath / "colors") + FileSystem.SYSTEM.write(filesBPath / "colors" / "blue.txt") { writeUtf8("The sky is blue") } + FileSystem.SYSTEM.write(filesBPath / "colors" / "green.txt") { writeUtf8("Limes are green") } + + val resourceFileSystem = ResourceFileSystem( + classLoader = URLClassLoader( + arrayOf( + filesAPath.toFile().toURI().toURL(), + filesBPath.toFile().toURI().toURL(), + ), + null, + ), + indexEagerly = false, + ) + + assertThat(resourceFileSystem.read("/colors/red.txt".toPath()) { readUtf8() }) + .isEqualTo("Apples are red") + assertThat(resourceFileSystem.read("/colors/green.txt".toPath()) { readUtf8() }) + .isEqualTo("Grass is green") + assertThat(resourceFileSystem.read("/colors/blue.txt".toPath()) { readUtf8() }) + .isEqualTo("The sky is blue") + + assertThat(resourceFileSystem.metadata("/colors/red.txt".toPath()).isRegularFile).isTrue() + assertThat(resourceFileSystem.metadata("/colors/green.txt".toPath()).isRegularFile).isTrue() + assertThat(resourceFileSystem.metadata("/colors/blue.txt".toPath()).isRegularFile).isTrue() + + assertThat(resourceFileSystem.list("/".toPath())) + .hasSameElementsAs(listOf("/colors".toPath())) + assertThat(resourceFileSystem.list("/colors".toPath())).hasSameElementsAs( + listOf( + "/colors/red.txt".toPath(), + "/colors/green.txt".toPath(), + "/colors/blue.txt".toPath(), + ), + ) + + assertThat(resourceFileSystem.metadata("/".toPath()).isDirectory).isTrue() + assertThat(resourceFileSystem.metadata("/colors".toPath()).isDirectory).isTrue() + } + + @Test + fun testJarAndJarOverlap() { + val zipAPath = ZipBuilder(base) + .addEntry("colors/red.txt", "Apples are red") + .addEntry("colors/green.txt", "Grass is green") + .addEntry("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n") + .build() + val zipBPath = ZipBuilder(base) + .addEntry("colors/blue.txt", "The sky is blue") + .addEntry("colors/green.txt", "Limes are green") + .addEntry("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n") + .build() + val resourceFileSystem = ResourceFileSystem( + classLoader = URLClassLoader( + arrayOf( + zipAPath.toFile().toURI().toURL(), + zipBPath.toFile().toURI().toURL(), + ), + null, + ), + indexEagerly = false, + ) + + assertThat(resourceFileSystem.read("/colors/red.txt".toPath()) { readUtf8() }) + .isEqualTo("Apples are red") + assertThat(resourceFileSystem.read("/colors/green.txt".toPath()) { readUtf8() }) + .isEqualTo("Grass is green") + assertThat(resourceFileSystem.read("/colors/blue.txt".toPath()) { readUtf8() }) + .isEqualTo("The sky is blue") + + assertThat(resourceFileSystem.metadata("/colors/red.txt".toPath()).isRegularFile).isTrue() + assertThat(resourceFileSystem.metadata("/colors/green.txt".toPath()).isRegularFile).isTrue() + assertThat(resourceFileSystem.metadata("/colors/blue.txt".toPath()).isRegularFile).isTrue() + + assertThat(resourceFileSystem.list("/".toPath())) + .hasSameElementsAs(listOf("/META-INF".toPath(), "/colors".toPath())) + assertThat(resourceFileSystem.list("/colors".toPath())).hasSameElementsAs( + listOf( + "/colors/red.txt".toPath(), + "/colors/green.txt".toPath(), + "/colors/blue.txt".toPath(), + ), + ) + + assertThat(resourceFileSystem.metadata("/".toPath()).isDirectory).isTrue() + assertThat(resourceFileSystem.metadata("/colors".toPath()).isDirectory).isTrue() + } + + @Test + fun testResourceMissing() { + val path = "okio/resourcefilesystem/b/c.txt".toPath() + + assertThat(fileSystem.metadataOrNull(path)).isNull() + + try { + fileSystem.read(path) { readUtf8() } + fail() + } catch (ioe: IOException) { + assertThat(ioe.message).isEqualTo("file not found: okio/resourcefilesystem/b/c.txt") + } + } + + @Test + fun testProjectIsListable() { + val path = "okio/resourcefilesystem/b/".toPath() + + val metadata = fileSystem.metadataOrNull(path)!! + + assertThat(metadata.isDirectory).isTrue() + assertThat(metadata.createdAtMillis).isGreaterThan(1L) + + assertThat(fileSystem.list(path).map { it.name }).containsExactly("b.txt") + } + + @Test + fun testResourceFromJar() { + val path = "LICENSE-junit.txt".toPath() + + val metadata = fileSystem.metadataOrNull(path)!! + + assertThat(metadata.size).isGreaterThan(10000L) + assertThat(metadata.isRegularFile).isTrue() + assertThat(metadata.isDirectory).isFalse() + + val content = fileSystem.read(path) { readUtf8Line() } + + assertThat(content).isEqualTo("JUnit") + } + + @Test + fun testClassFilesOmittedFromJar() { + assertThat(fileSystem.list("/org/junit/rules".toPath())).isEmpty() + assertThat(fileSystem.metadataOrNull("/org/junit/Test.class".toPath())).isNull() + } + + @Test + fun testClassFilesOmittedFromDirectory() { + val filesPath = base / "files" + val packagePath = filesPath / "com" / "example" / "project" + FileSystem.SYSTEM.createDirectories(packagePath) + FileSystem.SYSTEM.write(packagePath / "Hello.class") { writeUtf8("cafebabe") } + + val resourceFileSystem = ResourceFileSystem( + classLoader = URLClassLoader( + arrayOf(filesPath.toFile().toURI().toURL()), + null, + ), + indexEagerly = false, + ) + + assertThat(resourceFileSystem.list("/com/example/project".toPath())).isEmpty() + assertThat(resourceFileSystem.metadataOrNull("/com/example/project/Hello.class".toPath())) + .isNull() + assertFailsWith<FileNotFoundException> { + resourceFileSystem.source("/com/example/project/Hello.class".toPath()) + } + } + + @Test + fun testDirectoryFromJar() { + val path = "org/junit/".toPath() + + val metadata = fileSystem.metadataOrNull(path) + assertThat(metadata?.isDirectory).isTrue() + + val files = fileSystem.list(path).map { it.name } + assertThat(files).contains("matchers", "rules") + assertThat(files.filter { it.endsWith(".class") }).isEmpty() + } + + @Test + fun packagePath() { + val path = ByteString::class.java.`package`.toPath() + + assertThat((path / "a.txt").toString()) + .isEqualTo("okio${Path.DIRECTORY_SEPARATOR}a.txt") + } + + @Test + fun classResource() { + val path = ByteString::class.packagePath!! + + assertThat((path / "a.txt").toString()) + .isEqualTo("okio${Path.DIRECTORY_SEPARATOR}a.txt") + } + + /** + * Confirm that class loaders aren't accessed until the file system is used. This should save + * resources so that zip files aren't decoded if they're unused. + */ + @Test + fun testIndexLazily() { + val classLoader = object : ClassLoader() { + override fun findResources(name: String?): Enumeration<URL> { + throw Exception("finding a resource") + } + } + + val resourceFileSystem = ResourceFileSystem( + classLoader = classLoader, + indexEagerly = false, + ) + + assertThat( + assertFailsWith<Exception> { + resourceFileSystem.list("/".toPath()) + }, + ).hasMessage("finding a resource") + } + + @Test + fun testIndexEagerly() { + val classLoader = object : ClassLoader() { + override fun findResources(name: String?): Enumeration<URL> { + throw Exception("finding a resource") + } + } + + assertThat( + assertFailsWith<Exception> { + ResourceFileSystem( + classLoader = classLoader, + indexEagerly = true, + ) + }, + ).hasMessage("finding a resource") + } + + /** Confirm we can read individual files without triggering indexing. */ + @Test + fun testSourceDoesntTriggerIndexing() { + val processedPaths = mutableSetOf<Path>() + val recordingFileSystem = object : ForwardingFileSystem(FileSystem.SYSTEM) { + override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path { + processedPaths += path + return super.onPathParameter(path, functionName, parameterName) + } + } + + val zipAPath = ZipBuilder(base) + .addEntry("colors/red.txt", "Apples are red") + .addEntry("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n") + .build() + val zipBPath = ZipBuilder(base) + .addEntry("colors/blue.txt", "The sky is blue") + .addEntry("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n") + .build() + val resourceFileSystem = ResourceFileSystem( + classLoader = URLClassLoader( + arrayOf( + zipAPath.toFile().toURI().toURL(), + zipBPath.toFile().toURI().toURL(), + ), + null, + ), + indexEagerly = false, + systemFileSystem = recordingFileSystem, + ) + + // Reading paths with source() or read() doesn't index zips. + assertThat( + resourceFileSystem.read("/colors/red.txt".toPath()) { readUtf8() }, + ).isEqualTo("Apples are red") + assertThat( + resourceFileSystem.read("/colors/blue.txt".toPath()) { readUtf8() }, + ).isEqualTo("The sky is blue") + assertThat(processedPaths).isEmpty() + + // Calling list() does though. + assertThat(resourceFileSystem.list("/colors".toPath())).containsExactlyInAnyOrder( + "/colors/red.txt".toPath(), + "/colors/blue.txt".toPath(), + ) + assertThat(processedPaths).containsExactlyInAnyOrder( + zipAPath, + zipBPath, + ) + } + + /** + * Our resource file system uses [URLClassLoader] internally, which means we need to go back and + * forth between [File], [URL], and [URI] models for component paths. This is a big hazard for + * escaping special characters and it's likely that some file paths won't survive the round trip! + */ + @Test + fun fileNameWithSpaceInPath() { + val zipPath = ZipBuilder(base / "space in directory name") + .addEntry("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n") + .addEntry("hello.txt", "Hello World") + .build() + val resourceFileSystem = ResourceFileSystem( + classLoader = URLClassLoader(arrayOf(zipPath.toFile().toURI().toURL()), null), + indexEagerly = false, + ) + + assertThat(resourceFileSystem.read("hello.txt".toPath()) { readUtf8() }) + .isEqualTo("Hello World") + assertThat(resourceFileSystem.metadata("hello.txt".toPath())).isNotNull() + } + + @Test + fun missingResourceSilentlyIgnored() { + val zipAPath = ZipBuilder(base) + .addEntry("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n") + .addEntry("hello.txt", "Hello World") + .build() + val resourceFileSystem = ResourceFileSystem( + classLoader = URLClassLoader( + arrayOf( + zipAPath.toFile().toURI().toURL(), + (base / "missing.zip").toFile().toURI().toURL(), + ), + null, + ), + indexEagerly = false, + ) + + assertThat(resourceFileSystem.read("hello.txt".toPath()) { readUtf8() }) + .isEqualTo("Hello World") + } + + @Test + fun listSpecialCharacterNamedFiles() { + val path = "okio/resourcefilesystem/non-ascii".toPath() + + assertThat(fileSystem.listRecursively(path).toList()).containsExactly( + "/okio/resourcefilesystem/non-ascii/ギリシア神話".toPath(), + "/okio/resourcefilesystem/non-ascii/ギリシア神話/Ἰλιάς".toPath(), + ) + + val content = fileSystem.read( + "/okio/resourcefilesystem/non-ascii/ギリシア神話/Ἰλιάς".toPath(), + BufferedSource::readUtf8, + ) + assertEquals("Chante, ô déesse, le courroux du Péléide Achille,\n", content) + } + + private fun Package.toPath(): Path = name.replace(".", "/").toPath() + + private val KClass<*>.packagePath: Path? + get() = qualifiedName?.replace(".", "/")?.toPath()?.parent +} diff --git a/okio/src/jvmTest/resources/okio/resourcefilesystem/a.txt b/okio/src/jvmTest/resources/okio/resourcefilesystem/a.txt new file mode 100644 index 00000000..2e65efe2 --- /dev/null +++ b/okio/src/jvmTest/resources/okio/resourcefilesystem/a.txt @@ -0,0 +1 @@ +a
\ No newline at end of file diff --git a/okio/src/jvmTest/resources/okio/resourcefilesystem/b/b.txt b/okio/src/jvmTest/resources/okio/resourcefilesystem/b/b.txt new file mode 100644 index 00000000..09038752 --- /dev/null +++ b/okio/src/jvmTest/resources/okio/resourcefilesystem/b/b.txt @@ -0,0 +1 @@ +b/b
\ No newline at end of file diff --git a/okio/src/jvmTest/resources/okio/resourcefilesystem/non-ascii/ギリシア神話/Ἰλιάς b/okio/src/jvmTest/resources/okio/resourcefilesystem/non-ascii/ギリシア神話/Ἰλιάς new file mode 100644 index 00000000..3c4ecbae --- /dev/null +++ b/okio/src/jvmTest/resources/okio/resourcefilesystem/non-ascii/ギリシア神話/Ἰλιάς @@ -0,0 +1 @@ +Chante, ô déesse, le courroux du Péléide Achille, diff --git a/okio/src/linuxMain/kotlin/okio/LinuxPosixVariant.kt b/okio/src/linuxMain/kotlin/okio/LinuxPosixVariant.kt new file mode 100644 index 00000000..168d0ddc --- /dev/null +++ b/okio/src/linuxMain/kotlin/okio/LinuxPosixVariant.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import platform.posix.ENOENT +import platform.posix.S_IFDIR +import platform.posix.S_IFMT +import platform.posix.S_IFREG +import platform.posix.errno +import platform.posix.lstat +import platform.posix.stat + +internal actual fun PosixFileSystem.variantMetadataOrNull(path: Path): FileMetadata? { + return memScoped { + val stat = alloc<stat>() + if (lstat(path.toString(), stat.ptr) != 0) { + if (errno == ENOENT) return null + throw errnoToIOException(errno) + } + return@memScoped FileMetadata( + isRegularFile = stat.st_mode.toInt() and S_IFMT == S_IFREG, + isDirectory = stat.st_mode.toInt() and S_IFMT == S_IFDIR, + symlinkTarget = symlinkTarget(stat, path), + size = stat.st_size, + createdAtMillis = stat.st_ctim.epochMillis, + lastModifiedAtMillis = stat.st_mtim.epochMillis, + lastAccessedAtMillis = stat.st_atim.epochMillis, + ) + } +} diff --git a/okio/src/mingwX64Main/kotlin/okio/Windows.kt b/okio/src/mingwX64Main/kotlin/okio/Windows.kt new file mode 100644 index 00000000..18370b9d --- /dev/null +++ b/okio/src/mingwX64Main/kotlin/okio/Windows.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlinx.cinterop.ByteVarOf +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.memScoped +import platform.windows.DWORD +import platform.windows.ERROR_FILE_NOT_FOUND +import platform.windows.ERROR_PATH_NOT_FOUND +import platform.windows.FORMAT_MESSAGE_FROM_SYSTEM +import platform.windows.FORMAT_MESSAGE_IGNORE_INSERTS +import platform.windows.FormatMessageA +import platform.windows.GetLastError +import platform.windows.LANG_NEUTRAL +import platform.windows.SUBLANG_DEFAULT + +internal fun lastErrorToIOException(): IOException { + val lastError = GetLastError() + return when (lastError.toInt()) { + ERROR_FILE_NOT_FOUND, ERROR_PATH_NOT_FOUND -> FileNotFoundException(lastErrorString(lastError)) + else -> IOException(lastErrorString(lastError)) + } +} + +internal fun lastErrorString(lastError: DWORD): String { + memScoped { + val messageMaxSize = 2048 + val message = allocArray<ByteVarOf<Byte>>(messageMaxSize) + FormatMessageA( + dwFlags = (FORMAT_MESSAGE_FROM_SYSTEM or FORMAT_MESSAGE_IGNORE_INSERTS).toUInt(), + lpSource = null, + dwMessageId = lastError, + dwLanguageId = (SUBLANG_DEFAULT * 1024 + LANG_NEUTRAL).toUInt(), // MAKELANGID macro. + lpBuffer = message, + nSize = messageMaxSize.toUInt(), + Arguments = null, + ) + return Buffer().writeNullTerminated(message).readUtf8().trim() + } +} diff --git a/okio/src/mingwX64Main/kotlin/okio/WindowsFileHandle.kt b/okio/src/mingwX64Main/kotlin/okio/WindowsFileHandle.kt new file mode 100644 index 00000000..79cfb7f9 --- /dev/null +++ b/okio/src/mingwX64Main/kotlin/okio/WindowsFileHandle.kt @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 kotlinx.cinterop.CValuesRef +import kotlinx.cinterop.IntVar +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.usePinned +import kotlinx.cinterop.value +import platform.windows.CloseHandle +import platform.windows.ERROR_HANDLE_EOF +import platform.windows.FILE_BEGIN +import platform.windows.FlushFileBuffers +import platform.windows.GetFileSizeEx +import platform.windows.GetLastError +import platform.windows.HANDLE +import platform.windows.LARGE_INTEGER +import platform.windows.ReadFile +import platform.windows.SetEndOfFile +import platform.windows.SetFilePointer +import platform.windows.WriteFile +import platform.windows._OVERLAPPED + +internal class WindowsFileHandle( + readWrite: Boolean, + private val file: HANDLE?, +) : FileHandle(readWrite) { + override fun protectedSize(): Long { + memScoped { + val result = alloc<LARGE_INTEGER>() + if (GetFileSizeEx(file, result.ptr) == 0) { + throw lastErrorToIOException() + } + return result.toLong() + } + } + + override fun protectedRead( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ): Int { + val bytesRead = if (array.isNotEmpty()) { + array.usePinned { pinned -> + variantPread(pinned.addressOf(arrayOffset), byteCount, fileOffset) + } + } else { + 0 + } + if (bytesRead == 0) return -1 + return bytesRead + } + + fun variantPread( + target: CValuesRef<*>, + byteCount: Int, + offset: Long, + ): Int { + memScoped { + val overlapped = alloc<_OVERLAPPED>() + overlapped.Offset = offset.toUInt() + overlapped.OffsetHigh = (offset ushr 32).toUInt() + val readFileResult = ReadFile( + hFile = file, + lpBuffer = target.getPointer(this), + nNumberOfBytesToRead = byteCount.toUInt(), + lpNumberOfBytesRead = null, + lpOverlapped = overlapped.ptr, + ) + if (readFileResult == 0 && GetLastError().toInt() != ERROR_HANDLE_EOF) { + throw lastErrorToIOException() + } + return overlapped.InternalHigh.toInt() + } + } + + override fun protectedWrite( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ) { + val bytesWritten = if (array.isNotEmpty()) { + array.usePinned { pinned -> + variantPwrite(pinned.addressOf(arrayOffset), byteCount, fileOffset) + } + } else { + 0 + } + if (bytesWritten != byteCount) throw IOException("bytesWritten=$bytesWritten") + } + + fun variantPwrite( + source: CValuesRef<*>, + byteCount: Int, + offset: Long, + ): Int { + memScoped { + val overlapped = alloc<_OVERLAPPED>() + overlapped.Offset = offset.toUInt() + overlapped.OffsetHigh = (offset ushr 32).toUInt() + val writeFileResult = WriteFile( + hFile = file, + lpBuffer = source.getPointer(this), + nNumberOfBytesToWrite = byteCount.toUInt(), + lpNumberOfBytesWritten = null, + lpOverlapped = overlapped.ptr, + ) + if (writeFileResult == 0) { + throw lastErrorToIOException() + } + return overlapped.InternalHigh.toInt() + } + } + + override fun protectedFlush() { + if (FlushFileBuffers(file) == 0) { + throw lastErrorToIOException() + } + } + + override fun protectedResize(size: Long) { + memScoped { + val distanceToMoveHigh = alloc<IntVar>() + distanceToMoveHigh.value = (size ushr 32).toInt() + val movePointerResult = SetFilePointer( + hFile = file, + lDistanceToMove = size.toInt(), + lpDistanceToMoveHigh = distanceToMoveHigh.ptr, + dwMoveMethod = FILE_BEGIN.toUInt(), + ) + if (movePointerResult == 0U) { + throw lastErrorToIOException() + } + if (SetEndOfFile(file) == 0) { + throw lastErrorToIOException() + } + } + } + + override fun protectedClose() { + if (CloseHandle(file) == 0) { + throw lastErrorToIOException() + } + } + + private fun LARGE_INTEGER.toLong(): Long { + return (HighPart.toLong() shl 32) + (LowPart.toLong() and 0xffffffffL) + } +} diff --git a/okio/src/mingwX64Main/kotlin/okio/WindowsPosixVariant.kt b/okio/src/mingwX64Main/kotlin/okio/WindowsPosixVariant.kt new file mode 100644 index 00000000..21dd41a0 --- /dev/null +++ b/okio/src/mingwX64Main/kotlin/okio/WindowsPosixVariant.kt @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlinx.cinterop.CPointer +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.toKString +import okio.Path.Companion.toPath +import platform.posix.EACCES +import platform.posix.ENOENT +import platform.posix.FILE +import platform.posix.PATH_MAX +import platform.posix.S_IFDIR +import platform.posix.S_IFMT +import platform.posix.S_IFREG +import platform.posix._fullpath +import platform.posix._stat64 +import platform.posix.errno +import platform.posix.fopen +import platform.posix.free +import platform.posix.getenv +import platform.posix.mkdir +import platform.posix.remove +import platform.posix.rmdir +import platform.windows.CREATE_NEW +import platform.windows.CreateFileA +import platform.windows.FILE_ATTRIBUTE_NORMAL +import platform.windows.FILE_SHARE_WRITE +import platform.windows.GENERIC_READ +import platform.windows.GENERIC_WRITE +import platform.windows.INVALID_HANDLE_VALUE +import platform.windows.MOVEFILE_REPLACE_EXISTING +import platform.windows.MoveFileExA +import platform.windows.OPEN_ALWAYS +import platform.windows.OPEN_EXISTING + +internal actual val PLATFORM_TEMPORARY_DIRECTORY: Path + get() { + // Windows' built-in APIs check the TEMP, TMP, and USERPROFILE environment variables in order. + // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppatha?redirectedfrom=MSDN + val temp = getenv("TEMP") + if (temp != null) return temp.toKString().toPath() + + val tmp = getenv("TMP") + if (tmp != null) return tmp.toKString().toPath() + + val userProfile = getenv("USERPROFILE") + if (userProfile != null) return userProfile.toKString().toPath() + + return "\\Windows\\TEMP".toPath() + } + +internal actual val PLATFORM_DIRECTORY_SEPARATOR = "\\" + +internal actual fun PosixFileSystem.variantDelete(path: Path, mustExist: Boolean) { + val pathString = path.toString() + + if (remove(pathString) == 0) return + + // If remove failed with EACCES, it might be a directory. Try that. + if (errno == EACCES) { + if (rmdir(pathString) == 0) return + } + if (errno == ENOENT) { + if (mustExist) { + throw FileNotFoundException("no such file: $path") + } else { + return + } + } + + throw errnoToIOException(EACCES) +} + +internal actual fun PosixFileSystem.variantMkdir(dir: Path): Int { + return mkdir(dir.toString()) +} + +internal actual fun PosixFileSystem.variantCanonicalize(path: Path): Path { + // Note that _fullpath() returns normally if the file doesn't exist. + val fullpath = _fullpath(null, path.toString(), PATH_MAX.toULong()) + ?: throw errnoToIOException(errno) + try { + val pathString = Buffer().writeNullTerminated(fullpath).readUtf8() + if (platform.posix.access(pathString, 0) != 0 && errno == ENOENT) { + throw FileNotFoundException("no such file") + } + return pathString.toPath() + } finally { + free(fullpath) + } +} + +internal actual fun PosixFileSystem.variantMetadataOrNull(path: Path): FileMetadata? { + return memScoped { + val stat = alloc<_stat64>() + if (_stat64(path.toString(), stat.ptr) != 0) { + if (errno == ENOENT) return null + throw errnoToIOException(errno) + } + return@memScoped FileMetadata( + isRegularFile = stat.st_mode.toInt() and S_IFMT == S_IFREG, + isDirectory = stat.st_mode.toInt() and S_IFMT == S_IFDIR, + symlinkTarget = null, + size = stat.st_size, + createdAtMillis = stat.st_ctime * 1000L, + lastModifiedAtMillis = stat.st_mtime * 1000L, + lastAccessedAtMillis = stat.st_atime * 1000L, + ) + } +} + +internal actual fun PosixFileSystem.variantMove(source: Path, target: Path) { + if (MoveFileExA(source.toString(), target.toString(), MOVEFILE_REPLACE_EXISTING.toUInt()) == 0) { + throw lastErrorToIOException() + } +} + +internal actual fun PosixFileSystem.variantSource(file: Path): Source { + val openFile: CPointer<FILE> = fopen(file.toString(), "rb") + ?: throw errnoToIOException(errno) + return FileSource(openFile) +} + +internal actual fun PosixFileSystem.variantSink(file: Path, mustCreate: Boolean): Sink { + // We're non-atomically checking file existence because Windows errors if we use the `x` flag along with `w`. + if (mustCreate && exists(file)) throw IOException("$file already exists.") + val openFile: CPointer<FILE> = fopen(file.toString(), "wb") + ?: throw errnoToIOException(errno) + return FileSink(openFile) +} + +internal actual fun PosixFileSystem.variantAppendingSink(file: Path, mustExist: Boolean): Sink { + // There is a `r+` flag which we could have used to force existence of [file] but this flag + // doesn't allow opening for appending, and we don't currently have a way to move the cursor to + // the end of the file. We are then forcing existence non-atomically. + if (mustExist && !exists(file)) throw IOException("$file doesn't exist.") + val openFile: CPointer<FILE> = fopen(file.toString(), "ab") + ?: throw errnoToIOException(errno) + return FileSink(openFile) +} + +internal actual fun PosixFileSystem.variantOpenReadOnly(file: Path): FileHandle { + val openFile = CreateFileA( + lpFileName = file.toString(), + dwDesiredAccess = GENERIC_READ, + dwShareMode = FILE_SHARE_WRITE.toUInt(), + lpSecurityAttributes = null, + dwCreationDisposition = OPEN_EXISTING.toUInt(), + dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL.toUInt(), + hTemplateFile = null, + ) + if (openFile == INVALID_HANDLE_VALUE) { + throw lastErrorToIOException() + } + return WindowsFileHandle(false, openFile) +} + +internal actual fun PosixFileSystem.variantOpenReadWrite( + file: Path, + mustCreate: Boolean, + mustExist: Boolean, +): FileHandle { + require(!mustCreate || !mustExist) { + "Cannot require mustCreate and mustExist at the same time." + } + + val creationDisposition = when { + mustCreate -> CREATE_NEW.toUInt() + mustExist -> OPEN_EXISTING.toUInt() + else -> OPEN_ALWAYS.toUInt() + } + + val openFile = CreateFileA( + lpFileName = file.toString(), + dwDesiredAccess = GENERIC_READ or GENERIC_WRITE.toUInt(), + dwShareMode = FILE_SHARE_WRITE.toUInt(), + lpSecurityAttributes = null, + dwCreationDisposition = creationDisposition, + dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL.toUInt(), + hTemplateFile = null, + ) + if (openFile == INVALID_HANDLE_VALUE) { + throw lastErrorToIOException() + } + return WindowsFileHandle(true, openFile) +} + +internal actual fun PosixFileSystem.variantCreateSymlink(source: Path, target: Path) { + throw IOException("Not supported") +} diff --git a/okio/src/nativeMain/kotlin/okio/Cinterop.kt b/okio/src/nativeMain/kotlin/okio/Cinterop.kt new file mode 100644 index 00000000..a9f3e74f --- /dev/null +++ b/okio/src/nativeMain/kotlin/okio/Cinterop.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlinx.cinterop.ByteVarOf +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.get +import kotlinx.cinterop.set +import platform.posix.ENOENT +import platform.posix.strerror + +internal fun Buffer.writeNullTerminated(bytes: CPointer<ByteVarOf<Byte>>): Buffer = apply { + var pos = 0 + while (true) { + val byte = bytes[pos++].toInt() + if (byte == 0) { + break + } else { + writeByte(byte) + } + } +} + +internal fun Buffer.write( + source: CPointer<ByteVarOf<Byte>>, + offset: Int = 0, + byteCount: Int, +): Buffer = apply { + for (i in offset until offset + byteCount) { + writeByte(source[i].toInt()) + } +} + +internal fun Buffer.read( + sink: CPointer<ByteVarOf<Byte>>, + offset: Int = 0, + byteCount: Int, +): Buffer = apply { + for (i in offset until offset + byteCount) { + sink[i] = readByte() + } +} + +internal fun errnoToIOException(errno: Int): IOException { + val message = strerror(errno) + val messageString = if (message != null) { + Buffer().writeNullTerminated(message).readUtf8() + } else { + "errno: $errno" + } + return when (errno) { + ENOENT -> FileNotFoundException(messageString) + else -> IOException(messageString) + } +} diff --git a/okio/src/nativeMain/kotlin/okio/FileSink.kt b/okio/src/nativeMain/kotlin/okio/FileSink.kt new file mode 100644 index 00000000..4b514267 --- /dev/null +++ b/okio/src/nativeMain/kotlin/okio/FileSink.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlinx.cinterop.CPointer +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.usePinned +import okio.Buffer.UnsafeCursor +import platform.posix.FILE +import platform.posix.errno +import platform.posix.fclose +import platform.posix.fflush + +/** Writes bytes to a file as a sink. */ +internal class FileSink( + private val file: CPointer<FILE>, +) : Sink { + private val unsafeCursor = UnsafeCursor() + private var closed = false + + override fun write( + source: Buffer, + byteCount: Long, + ) { + require(byteCount >= 0L) { "byteCount < 0: $byteCount" } + require(source.size >= byteCount) { "source.size=${source.size} < byteCount=$byteCount" } + check(!closed) { "closed" } + + var byteCount = byteCount + while (byteCount > 0) { + // Get the first segment, which we will read a contiguous range of bytes from. + val cursor = source.readUnsafe(unsafeCursor) + val segmentReadableByteCount = cursor.next() + val attemptCount = minOf(byteCount, segmentReadableByteCount.toLong()).toInt() + + // Copy bytes from that segment into the file. + val bytesWritten = cursor.data!!.usePinned { pinned -> + variantFwrite(pinned.addressOf(cursor.start), attemptCount.toUInt(), file).toLong() + } + + // Consume the bytes from the segment. + cursor.close() + source.skip(bytesWritten) + byteCount -= bytesWritten + + // If the write was shorter than expected, some I/O failed. + if (bytesWritten < attemptCount) { + throw errnoToIOException(errno) + } + } + } + + override fun flush() { + if (fflush(file) != 0) { + throw errnoToIOException(errno) + } + } + + override fun timeout(): Timeout = Timeout.NONE + + override fun close() { + if (closed) return + closed = true + if (fclose(file) != 0) { + throw errnoToIOException(errno) + } + } +} diff --git a/okio/src/nativeMain/kotlin/okio/FileSource.kt b/okio/src/nativeMain/kotlin/okio/FileSource.kt new file mode 100644 index 00000000..6e9965f4 --- /dev/null +++ b/okio/src/nativeMain/kotlin/okio/FileSource.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlinx.cinterop.CPointer +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.usePinned +import okio.Buffer.UnsafeCursor +import platform.posix.FILE +import platform.posix.errno +import platform.posix.fclose +import platform.posix.feof +import platform.posix.ferror + +/** Reads the bytes of a file as a source. */ +internal class FileSource( + private val file: CPointer<FILE>, +) : Source { + private val unsafeCursor = UnsafeCursor() + private var closed = false + + override fun read( + sink: Buffer, + byteCount: Long, + ): Long { + require(byteCount >= 0L) { "byteCount < 0: $byteCount" } + check(!closed) { "closed" } + val sinkInitialSize = sink.size + + // Request a writable segment in `sink`. We request at least 1024 bytes, unless the request is + // for smaller than that, in which case we request only that many bytes. + val cursor = sink.readAndWriteUnsafe(unsafeCursor) + val addedCapacityCount = cursor.expandBuffer(minByteCount = minOf(byteCount, 1024L).toInt()) + + // Now that we have a writable segment, figure out how many bytes to read. This is the smaller + // of the user's requested byte count, and the segment's writable capacity. + val attemptCount = minOf(byteCount, addedCapacityCount) + + // Copy bytes from the file to the segment. + val bytesRead = cursor.data!!.usePinned { pinned -> + variantFread(pinned.addressOf(cursor.start), attemptCount.toUInt(), file).toLong() + } + + // Remove new capacity that was added but not used. + cursor.resizeBuffer(sinkInitialSize + bytesRead) + cursor.close() + + return when { + bytesRead == attemptCount -> bytesRead + feof(file) != 0 -> if (bytesRead == 0L) -1L else bytesRead + ferror(file) != 0 -> throw errnoToIOException(errno) + else -> bytesRead + } + } + + override fun timeout(): Timeout = Timeout.NONE + + override fun close() { + if (closed) return + closed = true + fclose(file) + } +} diff --git a/okio/src/nativeMain/kotlin/okio/FileSystem.kt b/okio/src/nativeMain/kotlin/okio/FileSystem.kt new file mode 100644 index 00000000..eb1e1653 --- /dev/null +++ b/okio/src/nativeMain/kotlin/okio/FileSystem.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 okio.internal.commonCopy +import okio.internal.commonCreateDirectories +import okio.internal.commonDeleteRecursively +import okio.internal.commonExists +import okio.internal.commonListRecursively +import okio.internal.commonMetadata + +actual abstract class FileSystem { + @Throws(IOException::class) + actual abstract fun canonicalize(path: Path): Path + + @Throws(IOException::class) + actual fun metadata(path: Path): FileMetadata = commonMetadata(path) + + @Throws(IOException::class) + actual abstract fun metadataOrNull(path: Path): FileMetadata? + + @Throws(IOException::class) + actual fun exists(path: Path): Boolean = commonExists(path) + + @Throws(IOException::class) + actual abstract fun list(dir: Path): List<Path> + + actual abstract fun listOrNull(dir: Path): List<Path>? + + actual open fun listRecursively(dir: Path, followSymlinks: Boolean): Sequence<Path> = + commonListRecursively(dir, followSymlinks) + + @Throws(IOException::class) + actual abstract fun openReadOnly(file: Path): FileHandle + + @Throws(IOException::class) + actual abstract fun openReadWrite( + file: Path, + mustCreate: Boolean, + mustExist: Boolean, + ): FileHandle + + @Throws(IOException::class) + actual abstract fun source(file: Path): Source + + @Throws(IOException::class) + actual inline fun <T> read(file: Path, readerAction: BufferedSource.() -> T): T { + return source(file).buffer().use { + it.readerAction() + } + } + + @Throws(IOException::class) + actual abstract fun sink(file: Path, mustCreate: Boolean): Sink + + @Throws(IOException::class) + actual inline fun <T> write( + file: Path, + mustCreate: Boolean, + writerAction: BufferedSink.() -> T, + ): T { + return sink(file, mustCreate).buffer().use { + it.writerAction() + } + } + + @Throws(IOException::class) + actual abstract fun appendingSink(file: Path, mustExist: Boolean): Sink + + @Throws(IOException::class) + actual abstract fun createDirectory(dir: Path, mustCreate: Boolean) + + @Throws(IOException::class) + actual fun createDirectories(dir: Path, mustCreate: Boolean): Unit = commonCreateDirectories(dir, mustCreate) + + @Throws(IOException::class) + actual abstract fun atomicMove(source: Path, target: Path) + + @Throws(IOException::class) + actual open fun copy(source: Path, target: Path): Unit = commonCopy(source, target) + + @Throws(IOException::class) + actual abstract fun delete(path: Path, mustExist: Boolean) + + @Throws(IOException::class) + actual open fun deleteRecursively(fileOrDirectory: Path, mustExist: Boolean): Unit = + commonDeleteRecursively(fileOrDirectory, mustExist) + + @Throws(IOException::class) + actual abstract fun createSymlink(source: Path, target: Path) + + actual companion object { + /** + * The current process's host file system. Use this instance directly, or dependency inject a + * [FileSystem] to make code testable. + */ + val SYSTEM: FileSystem = PosixFileSystem + + actual val SYSTEM_TEMPORARY_DIRECTORY: Path = PLATFORM_TEMPORARY_DIRECTORY + } +} diff --git a/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt b/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt new file mode 100644 index 00000000..8958d99b --- /dev/null +++ b/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlinx.cinterop.CPointer +import kotlinx.cinterop.get +import okio.Path.Companion.toPath +import okio.internal.toPath +import platform.posix.EEXIST +import platform.posix.closedir +import platform.posix.dirent +import platform.posix.errno +import platform.posix.opendir +import platform.posix.readdir +import platform.posix.set_posix_errno + +internal object PosixFileSystem : FileSystem() { + private val SELF_DIRECTORY_ENTRY = ".".toPath() + private val PARENT_DIRECTORY_ENTRY = "..".toPath() + + override fun canonicalize(path: Path) = variantCanonicalize(path) + + override fun metadataOrNull(path: Path) = variantMetadataOrNull(path) + + override fun list(dir: Path): List<Path> = list(dir, throwOnFailure = true)!! + + override fun listOrNull(dir: Path): List<Path>? = list(dir, throwOnFailure = false) + + private fun list(dir: Path, throwOnFailure: Boolean): List<Path>? { + val opendir = opendir(dir.toString()) + ?: if (throwOnFailure) throw errnoToIOException(errno) else return null + + try { + val result = mutableListOf<Path>() + val buffer = Buffer() + + set_posix_errno(0) // If readdir() returns null it's either the end or an error. + while (true) { + val dirent: CPointer<dirent> = readdir(opendir) ?: break + val childPath = buffer.writeNullTerminated( + bytes = dirent[0].d_name, + ).toPath(normalize = true) + + if (childPath == SELF_DIRECTORY_ENTRY || childPath == PARENT_DIRECTORY_ENTRY) { + continue // exclude '.' and '..' from the results. + } + + result += dir / childPath + } + + if (errno != 0) { + if (throwOnFailure) { + throw errnoToIOException(errno) + } else { + return null + } + } + + result.sort() + return result + } finally { + closedir(opendir) // Ignore errno from closedir. + } + } + + override fun openReadOnly(file: Path) = variantOpenReadOnly(file) + + override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { + return variantOpenReadWrite(file, mustCreate = mustCreate, mustExist = mustExist) + } + + override fun source(file: Path) = variantSource(file) + + override fun sink(file: Path, mustCreate: Boolean) = variantSink(file, mustCreate) + + override fun appendingSink(file: Path, mustExist: Boolean) = variantAppendingSink(file, mustExist) + + override fun createDirectory(dir: Path, mustCreate: Boolean) { + val result = variantMkdir(dir) + if (result != 0) { + if (errno == EEXIST) { + if (mustCreate) { + errnoToIOException(errno) + } else { + return + } + } + throw errnoToIOException(errno) + } + } + + override fun atomicMove( + source: Path, + target: Path, + ) { + variantMove(source, target) + } + + override fun delete(path: Path, mustExist: Boolean) { + variantDelete(path, mustExist) + } + + override fun createSymlink(source: Path, target: Path) = variantCreateSymlink(source, target) + + override fun toString() = "PosixSystemFileSystem" +} diff --git a/okio/src/nativeMain/kotlin/okio/PosixVariant.kt b/okio/src/nativeMain/kotlin/okio/PosixVariant.kt new file mode 100644 index 00000000..d2c4ee3e --- /dev/null +++ b/okio/src/nativeMain/kotlin/okio/PosixVariant.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 + +internal expect val PLATFORM_TEMPORARY_DIRECTORY: Path + +internal expect fun PosixFileSystem.variantDelete(path: Path, mustExist: Boolean) + +internal expect fun PosixFileSystem.variantMkdir(dir: Path): Int + +internal expect fun PosixFileSystem.variantCanonicalize(path: Path): Path + +internal expect fun PosixFileSystem.variantMetadataOrNull(path: Path): FileMetadata? + +internal expect fun PosixFileSystem.variantMove(source: Path, target: Path) + +internal expect fun PosixFileSystem.variantSource(file: Path): Source + +internal expect fun PosixFileSystem.variantSink(file: Path, mustCreate: Boolean): Sink + +internal expect fun PosixFileSystem.variantAppendingSink(file: Path, mustExist: Boolean): Sink + +internal expect fun PosixFileSystem.variantOpenReadOnly(file: Path): FileHandle + +internal expect fun PosixFileSystem.variantOpenReadWrite( + file: Path, + mustCreate: Boolean, + mustExist: Boolean, +): FileHandle + +internal expect fun PosixFileSystem.variantCreateSymlink(source: Path, target: Path) diff --git a/okio/src/nativeMain/kotlin/okio/SizetVariant.kt b/okio/src/nativeMain/kotlin/okio/SizetVariant.kt new file mode 100644 index 00000000..46f5bca4 --- /dev/null +++ b/okio/src/nativeMain/kotlin/okio/SizetVariant.kt @@ -0,0 +1,39 @@ +/* +* Copyright (C) 2020 Square, Inc. +* +* 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 kotlinx.cinterop.ByteVar +import kotlinx.cinterop.ByteVarOf +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.convert +import platform.posix.FILE +import platform.posix.fread +import platform.posix.fwrite + +@OptIn(UnsafeNumber::class) +internal fun variantFread( + target: CPointer<ByteVarOf<Byte>>, + byteCount: UInt, + file: CPointer<FILE>, +): UInt = fread(target, 1u, byteCount.convert(), file).convert() + +@OptIn(UnsafeNumber::class) +internal fun variantFwrite( + source: CPointer<ByteVar>, + byteCount: UInt, + file: CPointer<FILE>, +): UInt = fwrite(source, 1u, byteCount.convert(), file).convert() diff --git a/okio/src/nativeTest/kotlin/okio/NativeSystemFileSystemTest.kt b/okio/src/nativeTest/kotlin/okio/NativeSystemFileSystemTest.kt new file mode 100644 index 00000000..e4db5eff --- /dev/null +++ b/okio/src/nativeTest/kotlin/okio/NativeSystemFileSystemTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlinx.datetime.Clock + +class NativeSystemFileSystemTest : AbstractFileSystemTest( + clock = Clock.System, + fileSystem = FileSystem.SYSTEM, + windowsLimitations = Path.DIRECTORY_SEPARATOR == "\\", + allowClobberingEmptyDirectories = Path.DIRECTORY_SEPARATOR == "\\", + allowAtomicMoveFromFileToDirectory = false, + temporaryDirectory = FileSystem.SYSTEM_TEMPORARY_DIRECTORY, +) diff --git a/okio/src/nonJvmMain/kotlin/okio/ByteString.kt b/okio/src/nonAppleMain/kotlin/okio/ByteString.kt index f0f038b6..e1b8cc51 100644 --- a/okio/src/nonJvmMain/kotlin/okio/ByteString.kt +++ b/okio/src/nonAppleMain/kotlin/okio/ByteString.kt @@ -25,6 +25,7 @@ import okio.internal.Sha512 import okio.internal.commonBase64 import okio.internal.commonBase64Url import okio.internal.commonCompareTo +import okio.internal.commonCopyInto import okio.internal.commonDecodeBase64 import okio.internal.commonDecodeHex import okio.internal.commonEncodeUtf8 @@ -51,13 +52,14 @@ import okio.internal.commonWrite actual open class ByteString internal actual constructor( - internal actual val data: ByteArray + internal actual val data: ByteArray, ) : Comparable<ByteString> { @Suppress("SetterBackingFieldAssignment") internal actual var hashCode: Int = 0 // 0 if unknown. set(value) { // Do nothing to avoid IllegalImmutabilityException. } + @Suppress("SetterBackingFieldAssignment") internal actual var utf8: String? = null set(value) { @@ -125,16 +127,23 @@ internal actual constructor( offset: Int, other: ByteString, otherOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount) actual open fun rangeEquals( offset: Int, other: ByteArray, otherOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount) + actual open fun copyInto( + offset: Int, + target: ByteArray, + targetOffset: Int, + byteCount: Int, + ) = commonCopyInto(offset, target, targetOffset, byteCount) + actual fun startsWith(prefix: ByteString) = commonStartsWith(prefix) actual fun startsWith(prefix: ByteArray) = commonStartsWith(prefix) diff --git a/okio/src/nonAppleMain/kotlin/okio/SegmentedByteString.kt b/okio/src/nonAppleMain/kotlin/okio/SegmentedByteString.kt new file mode 100644 index 00000000..485d834e --- /dev/null +++ b/okio/src/nonAppleMain/kotlin/okio/SegmentedByteString.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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 okio.internal.HashFunction +import okio.internal.commonCopyInto +import okio.internal.commonEquals +import okio.internal.commonGetSize +import okio.internal.commonHashCode +import okio.internal.commonInternalGet +import okio.internal.commonRangeEquals +import okio.internal.commonSubstring +import okio.internal.commonToByteArray +import okio.internal.commonWrite +import okio.internal.forEachSegment + +internal actual class SegmentedByteString internal actual constructor( + internal actual val segments: Array<ByteArray>, + internal actual val directory: IntArray, +) : ByteString(EMPTY.data) { + + override fun base64() = toByteString().base64() + + override fun hex() = toByteString().hex() + + override fun toAsciiLowercase() = toByteString().toAsciiLowercase() + + override fun toAsciiUppercase() = toByteString().toAsciiUppercase() + + override fun base64Url() = toByteString().base64Url() + + override fun substring(beginIndex: Int, endIndex: Int): ByteString = + commonSubstring(beginIndex, endIndex) + + override fun internalGet(pos: Int): Byte = commonInternalGet(pos) + + override fun getSize() = commonGetSize() + + override fun toByteArray(): ByteArray = commonToByteArray() + + override fun write(buffer: Buffer, offset: Int, byteCount: Int): Unit = + commonWrite(buffer, offset, byteCount) + + override fun rangeEquals( + offset: Int, + other: ByteString, + otherOffset: Int, + byteCount: Int, + ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount) + + override fun rangeEquals( + offset: Int, + other: ByteArray, + otherOffset: Int, + byteCount: Int, + ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount) + + override fun copyInto( + offset: Int, + target: ByteArray, + targetOffset: Int, + byteCount: Int, + ) = commonCopyInto(offset, target, targetOffset, byteCount) + + override fun indexOf(other: ByteArray, fromIndex: Int) = toByteString().indexOf(other, fromIndex) + + override fun lastIndexOf(other: ByteArray, fromIndex: Int) = toByteString().lastIndexOf( + other, + fromIndex, + ) + + override fun digest(hashFunction: HashFunction): ByteString { + forEachSegment { data, offset, byteCount -> + hashFunction.update(data, offset, byteCount) + } + val digestBytes = hashFunction.digest() + return ByteString(digestBytes) + } + + /** Returns a copy as a non-segmented byte string. */ + private fun toByteString() = ByteString(toByteArray()) + + override fun internalArray() = toByteArray() + + override fun equals(other: Any?): Boolean = commonEquals(other) + + override fun hashCode(): Int = commonHashCode() + + override fun toString() = toByteString().toString() +} diff --git a/okio/src/nonJvmMain/kotlin/okio/Buffer.kt b/okio/src/nonJvmMain/kotlin/okio/Buffer.kt index ec28f63f..8dfb5622 100644 --- a/okio/src/nonJvmMain/kotlin/okio/Buffer.kt +++ b/okio/src/nonJvmMain/kotlin/okio/Buffer.kt @@ -93,12 +93,12 @@ actual class Buffer : BufferedSource, BufferedSink { actual fun copyTo( out: Buffer, offset: Long, - byteCount: Long + byteCount: Long, ): Buffer = commonCopyTo(out, offset, byteCount) actual fun copyTo( out: Buffer, - offset: Long + offset: Long, ): Buffer = copyTo(out, offset, size - offset) actual operator fun get(pos: Long): Byte = commonGet(pos) @@ -232,7 +232,7 @@ actual class Buffer : BufferedSource, BufferedSink { offset: Long, bytes: ByteString, bytesOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean = commonRangeEquals(offset, bytes, bytesOffset, byteCount) override fun flush() = Unit @@ -297,7 +297,7 @@ actual class Buffer : BufferedSource, BufferedSink { actual fun readAndWriteUnsafe(unsafeCursor: UnsafeCursor): UnsafeCursor = commonReadAndWriteUnsafe(unsafeCursor) - actual class UnsafeCursor { + actual class UnsafeCursor : Closeable { actual var buffer: Buffer? = null actual var readWrite: Boolean = false @@ -315,7 +315,7 @@ actual class Buffer : BufferedSource, BufferedSink { actual fun expandBuffer(minByteCount: Int): Long = commonExpandBuffer(minByteCount) - actual fun close() { + actual override fun close() { commonClose() } } diff --git a/okio/src/nonJvmMain/kotlin/okio/BufferedSink.kt b/okio/src/nonJvmMain/kotlin/okio/BufferedSink.kt index 65d717c6..cdb767a2 100644 --- a/okio/src/nonJvmMain/kotlin/okio/BufferedSink.kt +++ b/okio/src/nonJvmMain/kotlin/okio/BufferedSink.kt @@ -15,7 +15,7 @@ */ package okio -actual interface BufferedSink : Sink { +actual sealed interface BufferedSink : Sink { actual val buffer: Buffer actual fun write(byteString: ByteString): BufferedSink diff --git a/okio/src/nonJvmMain/kotlin/okio/BufferedSource.kt b/okio/src/nonJvmMain/kotlin/okio/BufferedSource.kt index 98b7718a..369a3e63 100644 --- a/okio/src/nonJvmMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/nonJvmMain/kotlin/okio/BufferedSource.kt @@ -15,7 +15,7 @@ */ package okio -actual interface BufferedSource : Source { +actual sealed interface BufferedSource : Source { actual val buffer: Buffer actual fun exhausted(): Boolean diff --git a/okio/src/nonJvmMain/kotlin/okio/ForwardingSource.kt b/okio/src/nonJvmMain/kotlin/okio/ForwardingSource.kt new file mode 100644 index 00000000..1b75eba5 --- /dev/null +++ b/okio/src/nonJvmMain/kotlin/okio/ForwardingSource.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 + +actual abstract class ForwardingSource actual constructor( + actual val delegate: Source, +) : Source { + // TODO 'Source by delegate' once https://youtrack.jetbrains.com/issue/KT-23935 is fixed. + + @Throws(IOException::class) + actual override fun read(sink: Buffer, byteCount: Long): Long = delegate.read(sink, byteCount) + + actual override fun timeout() = delegate.timeout() + + @Throws(IOException::class) + actual override fun close() = delegate.close() + + actual override fun toString() = "${this::class.simpleName}($delegate)" +} diff --git a/okio/src/nonJvmMain/kotlin/okio/HashingSink.kt b/okio/src/nonJvmMain/kotlin/okio/HashingSink.kt index cbd14a26..fd86acf9 100644 --- a/okio/src/nonJvmMain/kotlin/okio/HashingSink.kt +++ b/okio/src/nonJvmMain/kotlin/okio/HashingSink.kt @@ -24,7 +24,7 @@ import okio.internal.Sha512 actual class HashingSink internal constructor( private val sink: Sink, - private val hashFunction: HashFunction + private val hashFunction: HashFunction, ) : Sink { override fun write(source: Buffer, byteCount: Long) { diff --git a/okio/src/nonJvmMain/kotlin/okio/HashingSource.kt b/okio/src/nonJvmMain/kotlin/okio/HashingSource.kt index 62bfd608..40e4a628 100644 --- a/okio/src/nonJvmMain/kotlin/okio/HashingSource.kt +++ b/okio/src/nonJvmMain/kotlin/okio/HashingSource.kt @@ -24,7 +24,7 @@ import okio.internal.Sha512 actual class HashingSource internal constructor( private val source: Source, - private val hashFunction: HashFunction + private val hashFunction: HashFunction, ) : Source { override fun read(sink: Buffer, byteCount: Long): Long { diff --git a/okio/src/nonJvmMain/kotlin/okio/-Platform.kt b/okio/src/nonJvmMain/kotlin/okio/NonJvmPlatform.kt index 90fcd36d..6e9ac870 100644 --- a/okio/src/nonJvmMain/kotlin/okio/-Platform.kt +++ b/okio/src/nonJvmMain/kotlin/okio/NonJvmPlatform.kt @@ -19,24 +19,39 @@ package okio import okio.internal.commonAsUtf8ToByteArray import okio.internal.commonToUtf8String +internal expect val PLATFORM_DIRECTORY_SEPARATOR: String + internal actual fun ByteArray.toUtf8String(): String = commonToUtf8String() internal actual fun String.asUtf8ToByteArray(): ByteArray = commonAsUtf8ToByteArray() actual open class ArrayIndexOutOfBoundsException actual constructor( - message: String? + message: String?, ) : IndexOutOfBoundsException(message) -internal actual inline fun <R> synchronized(lock: Any, block: () -> R): R = block() +actual class Lock { + companion object { + val instance = Lock() + } +} + +internal actual fun newLock(): Lock = Lock.instance + +actual inline fun <T> Lock.withLock(action: () -> T): T = action() actual open class IOException actual constructor( message: String?, - cause: Throwable? + cause: Throwable?, ) : Exception(message, cause) { actual constructor(message: String?) : this(message, null) + actual constructor() : this(null, null) } -actual open class EOFException actual constructor(message: String?) : IOException(message) +actual class ProtocolException actual constructor(message: String) : IOException(message) + +actual open class EOFException actual constructor(message: String?) : IOException(message) { + actual constructor() : this(null) +} actual open class FileNotFoundException actual constructor(message: String?) : IOException(message) diff --git a/okio/src/nonJvmMain/kotlin/okio/Path.kt b/okio/src/nonJvmMain/kotlin/okio/Path.kt new file mode 100644 index 00000000..16328afa --- /dev/null +++ b/okio/src/nonJvmMain/kotlin/okio/Path.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 okio.internal.commonCompareTo +import okio.internal.commonEquals +import okio.internal.commonHashCode +import okio.internal.commonIsAbsolute +import okio.internal.commonIsRelative +import okio.internal.commonIsRoot +import okio.internal.commonName +import okio.internal.commonNameBytes +import okio.internal.commonNormalized +import okio.internal.commonParent +import okio.internal.commonRelativeTo +import okio.internal.commonResolve +import okio.internal.commonRoot +import okio.internal.commonSegments +import okio.internal.commonSegmentsBytes +import okio.internal.commonToPath +import okio.internal.commonToString +import okio.internal.commonVolumeLetter + +actual class Path internal actual constructor( + internal actual val bytes: ByteString, +) : Comparable<Path> { + actual val root: Path? + get() = commonRoot() + + actual val segments: List<String> + get() = commonSegments() + + actual val segmentsBytes: List<ByteString> + get() = commonSegmentsBytes() + + actual val isAbsolute: Boolean + get() = commonIsAbsolute() + + actual val isRelative: Boolean + get() = commonIsRelative() + + actual val volumeLetter: Char? + get() = commonVolumeLetter() + + actual val nameBytes: ByteString + get() = commonNameBytes() + + actual val name: String + get() = commonName() + + actual val parent: Path? + get() = commonParent() + + actual val isRoot: Boolean + get() = commonIsRoot() + + actual operator fun div(child: String): Path = commonResolve(child, normalize = false) + + actual operator fun div(child: ByteString): Path = commonResolve(child, normalize = false) + + actual operator fun div(child: Path): Path = commonResolve(child, normalize = false) + + actual fun resolve(child: String, normalize: Boolean): Path = + commonResolve(child, normalize = normalize) + + actual fun resolve(child: ByteString, normalize: Boolean): Path = + commonResolve(child, normalize = normalize) + + actual fun resolve(child: Path, normalize: Boolean): Path = + commonResolve(child = child, normalize = normalize) + + actual fun relativeTo(other: Path): Path = commonRelativeTo(other) + + actual fun normalized(): Path = commonNormalized() + + actual override fun compareTo(other: Path): Int = commonCompareTo(other) + + actual override fun equals(other: Any?): Boolean = commonEquals(other) + + actual override fun hashCode() = commonHashCode() + + actual override fun toString() = commonToString() + + actual companion object { + actual val DIRECTORY_SEPARATOR: String = PLATFORM_DIRECTORY_SEPARATOR + + actual fun String.toPath(normalize: Boolean): Path = commonToPath(normalize) + } +} diff --git a/okio/src/nonJvmMain/kotlin/okio/RealBufferedSink.kt b/okio/src/nonJvmMain/kotlin/okio/RealBufferedSink.kt index ed03094e..8b09c7f4 100644 --- a/okio/src/nonJvmMain/kotlin/okio/RealBufferedSink.kt +++ b/okio/src/nonJvmMain/kotlin/okio/RealBufferedSink.kt @@ -37,7 +37,7 @@ import okio.internal.commonWriteUtf8 import okio.internal.commonWriteUtf8CodePoint internal actual class RealBufferedSink actual constructor( - actual val sink: Sink + actual val sink: Sink, ) : BufferedSink { actual var closed: Boolean = false override val buffer = Buffer() diff --git a/okio/src/nonJvmMain/kotlin/okio/RealBufferedSource.kt b/okio/src/nonJvmMain/kotlin/okio/RealBufferedSource.kt index d6f4b942..93ad10f0 100644 --- a/okio/src/nonJvmMain/kotlin/okio/RealBufferedSource.kt +++ b/okio/src/nonJvmMain/kotlin/okio/RealBufferedSource.kt @@ -47,7 +47,7 @@ import okio.internal.commonTimeout import okio.internal.commonToString internal actual class RealBufferedSource actual constructor( - actual val source: Source + actual val source: Source, ) : BufferedSource { actual var closed: Boolean = false override val buffer: Buffer = Buffer() @@ -96,15 +96,17 @@ internal actual class RealBufferedSource actual constructor( commonIndexOfElement(targetBytes, fromIndex) override fun rangeEquals(offset: Long, bytes: ByteString) = rangeEquals( - offset, bytes, 0, - bytes.size + offset, + bytes, + 0, + bytes.size, ) override fun rangeEquals( offset: Long, bytes: ByteString, bytesOffset: Int, - byteCount: Int + byteCount: Int, ): Boolean = commonRangeEquals(offset, bytes, bytesOffset, byteCount) override fun peek(): BufferedSource = commonPeek() diff --git a/okio/src/nonJvmTest/kotlin/okio/NonJvmTesting.kt b/okio/src/nonJvmTest/kotlin/okio/NonJvmTesting.kt new file mode 100644 index 00000000..a5e9a780 --- /dev/null +++ b/okio/src/nonJvmTest/kotlin/okio/NonJvmTesting.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +actual fun assertRelativeTo( + a: Path, + b: Path, + bRelativeToA: Path, + sameAsNio: Boolean, +) { + val actual = b.relativeTo(a) + assertEquals(bRelativeToA, actual) + assertEquals(b.normalized().withUnixSlashes(), (a / actual).normalized().withUnixSlashes()) +} + +actual fun assertRelativeToFails( + a: Path, + b: Path, + sameAsNio: Boolean, +): IllegalArgumentException { + return assertFailsWith { b.relativeTo(a) } +} diff --git a/okio/src/nonWasmTest/kotlin/okio/FakeFileSystemTest.kt b/okio/src/nonWasmTest/kotlin/okio/FakeFileSystemTest.kt new file mode 100644 index 00000000..07dfad8d --- /dev/null +++ b/okio/src/nonWasmTest/kotlin/okio/FakeFileSystemTest.kt @@ -0,0 +1,495 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlin.reflect.KClass +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.minutes +import okio.Path.Companion.toPath +import okio.fakefilesystem.FakeFileSystem + +class FakeWindowsFileSystemTest : FakeFileSystemTest( + FakeFileSystem(clock = FakeClock()).also { it.emulateWindows() }, + temporaryDirectory = "C:\\".toPath(), +) + +class FakeUnixFileSystemTest : FakeFileSystemTest( + FakeFileSystem(clock = FakeClock()).also { it.emulateUnix() }, + temporaryDirectory = "/".toPath(), +) + +class StrictFakeFileSystemTest : FakeFileSystemTest( + FakeFileSystem(clock = FakeClock()), + temporaryDirectory = "/".toPath(), +) + +abstract class FakeFileSystemTest internal constructor( + private val fakeFileSystem: FakeFileSystem, + temporaryDirectory: Path, +) : AbstractFileSystemTest( + clock = fakeFileSystem.clock, + fileSystem = fakeFileSystem, + windowsLimitations = !fakeFileSystem.allowMovingOpenFiles, + allowClobberingEmptyDirectories = fakeFileSystem.allowClobberingEmptyDirectories, + allowAtomicMoveFromFileToDirectory = false, + temporaryDirectory = temporaryDirectory, +) { + private val fakeClock: FakeClock = fakeFileSystem.clock as FakeClock + + @Test + fun openPathsIncludesOpenSink() { + val openPath = base / "open-file" + val sink = fileSystem.sink(openPath) + assertEquals(openPath, fakeFileSystem.openPaths.single()) + sink.close() + assertTrue(fakeFileSystem.openPaths.isEmpty()) + } + + @Test + fun openPathsIncludesOpenSource() { + val openPath = base / "open-file" + openPath.writeUtf8("hello, world!") + assertTrue(fakeFileSystem.openPaths.isEmpty()) + val source = fileSystem.source(openPath) + assertEquals(openPath, fakeFileSystem.openPaths.single()) + source.close() + assertTrue(fakeFileSystem.openPaths.isEmpty()) + } + + @Test + fun openPathsIsOpenOrder() { + if (!fakeFileSystem.allowWritesWhileWriting) return + + val fileA = base / "a" + val fileB = base / "b" + val fileC = base / "c" + val fileD = base / "d" + + assertEquals(fakeFileSystem.openPaths, listOf()) + val sinkD = fileSystem.sink(fileD) + assertEquals(fakeFileSystem.openPaths, listOf(fileD)) + val sinkB = fileSystem.sink(fileB) + assertEquals(fakeFileSystem.openPaths, listOf(fileD, fileB)) + val sinkC = fileSystem.sink(fileC) + assertEquals(fakeFileSystem.openPaths, listOf(fileD, fileB, fileC)) + val sinkA = fileSystem.sink(fileA) + assertEquals(fakeFileSystem.openPaths, listOf(fileD, fileB, fileC, fileA)) + val sinkB2 = fileSystem.sink(fileB) + assertEquals(fakeFileSystem.openPaths, listOf(fileD, fileB, fileC, fileA, fileB)) + sinkD.close() + assertEquals(fakeFileSystem.openPaths, listOf(fileB, fileC, fileA, fileB)) + sinkB2.close() + assertEquals(fakeFileSystem.openPaths, listOf(fileB, fileC, fileA)) + sinkB.close() + assertEquals(fakeFileSystem.openPaths, listOf(fileC, fileA)) + sinkC.close() + assertEquals(fakeFileSystem.openPaths, listOf(fileA)) + sinkA.close() + assertEquals(fakeFileSystem.openPaths, listOf()) + } + + @Test + fun allPathsIncludesFile() { + val file = base / "all-files-includes-file" + file.writeUtf8("hello, world!") + assertEquals(setOf(base, file), fakeFileSystem.allPaths) + } + + @Test + fun allPathsIsSorted() { + val fileA = base / "a" + val fileB = base / "b" + val fileC = base / "c" + val fileD = base / "d" + + // Create files in a different order than the sorted order, so a file system that returns files + // in creation-order or reverse-creation order won't pass by accident. + fileD.writeUtf8("fileD") + fileB.writeUtf8("fileB") + fileC.writeUtf8("fileC") + fileA.writeUtf8("fileA") + + assertEquals(listOf(base, fileA, fileB, fileC, fileD), fakeFileSystem.allPaths.toList()) + } + + @Test + fun allPathsIncludesDirectory() { + val dir = base / "all-files-includes-directory" + fileSystem.createDirectory(dir) + assertEquals(setOf(base, dir), fakeFileSystem.allPaths) + } + + @Test + fun allPathsDoesNotIncludeDeletedFile() { + val file = base / "all-files-does-not-include-deleted-file" + file.writeUtf8("hello, world!") + fileSystem.delete(file) + assertEquals(setOf(base), fakeFileSystem.allPaths) + } + + @Test + fun allPathsDoesNotIncludeDeletedOpenFile() { + if (windowsLimitations) return // Can't delete open files with Windows' limitations. + + val file = base / "all-files-does-not-include-deleted-open-file" + val sink = fileSystem.sink(file) + assertEquals(setOf(base, file), fakeFileSystem.allPaths) + fileSystem.delete(file) + assertEquals(setOf(base), fakeFileSystem.allPaths) + sink.close() + } + + @Test + fun fileLastAccessedTime() { + val path = base / "file-last-accessed-time" + + fakeClock.sleep(1.minutes) + path.writeUtf8("hello, world!") + val createdAt = clock.now() + + fakeClock.sleep(1.minutes) + path.writeUtf8("hello again!") + val modifiedAt = clock.now() + + fakeClock.sleep(1.minutes) + path.readUtf8() + val accessedAt = clock.now() + + val metadata = fileSystem.metadata(path) + assertEquals(createdAt, metadata.createdAt) + assertEquals(modifiedAt, metadata.lastModifiedAt) + assertEquals(accessedAt, metadata.lastAccessedAt) + } + + @Test + fun directoryLastAccessedTime() { + val path = base / "directory-last-accessed-time" + + fakeClock.sleep(1.minutes) + fileSystem.createDirectory(path) + val createdAt = clock.now() + + fakeClock.sleep(1.minutes) + (path / "child").writeUtf8("hello world!") + val modifiedAt = clock.now() + + fakeClock.sleep(1.minutes) + fileSystem.list(path) + val accessedAt = clock.now() + + val metadata = fileSystem.metadata(path) + assertEquals(createdAt, metadata.createdAt) + assertEquals(modifiedAt, metadata.lastModifiedAt) + assertEquals(accessedAt, metadata.lastAccessedAt) + } + + @Test + fun checkNoOpenFilesThrowsOnOpenSource() { + val path = base / "check-no-open-files-open-source" + path.writeUtf8("hello, world!") + val exception = fileSystem.source(path).use { source -> + assertFailsWith<IllegalStateException> { + fakeFileSystem.checkNoOpenFiles() + } + } + + assertEquals( + """ + |expected 0 open files, but found: + | $path + """.trimMargin(), + exception.message, + ) + assertEquals("file opened for READ here", exception.cause?.message) + + // Now that the source is closed this is safe. + fakeFileSystem.checkNoOpenFiles() + } + + @Test + fun checkNoOpenFilesThrowsOnOpenSink() { + val path = base / "check-no-open-files-open-sink" + val exception = fileSystem.sink(path).use { source -> + assertFailsWith<IllegalStateException> { + fakeFileSystem.checkNoOpenFiles() + } + } + + assertEquals( + """ + |expected 0 open files, but found: + | $path + """.trimMargin(), + exception.message, + ) + assertEquals("file opened for WRITE here", exception.cause?.message) + + // Now that the source is closed this is safe. + fakeFileSystem.checkNoOpenFiles() + } + + @Test + fun createDirectoriesForVolumeLetterRoot() { + val path = "X:\\".toPath() + fileSystem.createDirectories(path) + assertTrue(fileSystem.metadata(path).isDirectory) + } + + @Test + fun createDirectoriesForChildOfVolumeLetterRoot() { + val path = "X:\\path".toPath() + fileSystem.createDirectories(path) + assertTrue(fileSystem.metadata(path).isDirectory) + } + + @Test + fun createDirectoriesForUnixRoot() { + val path = "/".toPath() + fileSystem.createDirectories(path) + assertTrue(fileSystem.metadata(path).isDirectory) + } + + @Test + fun createDirectoriesForChildOfUnixRoot() { + val path = "/path".toPath() + fileSystem.createDirectories(path) + assertTrue(fileSystem.metadata(path).isDirectory) + } + + @Test + fun createDirectoriesForUncRoot() { + val path = "\\\\server".toPath() + fileSystem.createDirectories(path) + assertTrue(fileSystem.metadata(path).isDirectory) + } + + @Test + fun createDirectoriesForChildOfUncRoot() { + val path = "\\\\server\\project".toPath() + fileSystem.createDirectories(path) + assertTrue(fileSystem.metadata(path).isDirectory) + } + + @Test + fun workingDirectoryMustBeAbsolute() { + val exception = assertFailsWith<IllegalArgumentException> { + fakeFileSystem.workingDirectory = "some/relative/path".toPath() + } + assertEquals("expected an absolute path but was some/relative/path", exception.message) + } + + @Test + fun metadataForRootsGeneratedOnDemand() { + assertTrue(fileSystem.metadata("X:\\".toPath()).isDirectory) + assertTrue(fileSystem.metadata("/".toPath()).isDirectory) + assertTrue(fileSystem.metadata("\\\\server".toPath()).isDirectory) + } + + @Test + fun startWriteWhileWritingNotAllowedWhenStrict() { + val path = base / "write-write" + path.writeUtf8("hello world!") + fileSystem.sink(path).use { + try { + fileSystem.sink(path).use { + } + assertTrue(fakeFileSystem.allowWritesWhileWriting) + } catch (_: IOException) { + assertFalse(fakeFileSystem.allowWritesWhileWriting) + } + } + } + + @Test + fun startReadWhileWritingNotAllowedWhenStrict() { + val path = base / "write-read" + path.writeUtf8("hello world!") + fileSystem.sink(path).use { + try { + fileSystem.source(path).use { + } + assertTrue(fakeFileSystem.allowReadsWhileWriting) + } catch (_: IOException) { + assertFalse(fakeFileSystem.allowReadsWhileWriting) + } + } + } + + @Test + fun startWriteWhileReadingNotAllowedWhenStrict() { + val path = base / "read-write" + path.writeUtf8("hello world!") + fileSystem.source(path).use { + try { + fileSystem.sink(path).use { + } + assertTrue(fakeFileSystem.allowReadsWhileWriting) + } catch (_: IOException) { + assertFalse(fakeFileSystem.allowReadsWhileWriting) + } + } + } + + @Test + fun startReadWhileReadingAllowedWhenStrict() { + val path = base / "read-read" + path.writeUtf8("hello world!") + fileSystem.source(path).use { + fileSystem.source(path).use { + } + } + } + + @Test + fun symlinkCanBeUsedAfterSettingAllowSymlinksToFalse() { + if (!supportsSymlink()) return + + val target = base / "symlink-target" + val source = base / "symlink-source" + fileSystem.createSymlink(source, target) + fakeFileSystem.allowSymlinks = false + target.writeUtf8("I am the target file") + assertEquals("I am the target file", source.readUtf8()) + } + + @Test + fun symlinkCannotBeCreatedAfterSettingAllowSymlinksToFalse() { + fakeFileSystem.allowSymlinks = false + val target = base / "symlink-target" + val source = base / "symlink-source" + assertFailsWith<IOException> { + fileSystem.createSymlink(source, target) + } + } + + @Test + fun fileExtras() { + val path = base / "a.txt" + path.writeUtf8("hello") + fakeFileSystem.setExtra(path, ContentTypeExtra::class, ContentTypeExtra("text/plain")) + val metadata = fileSystem.metadata(path) + assertEquals(ContentTypeExtra("text/plain"), metadata.extra(ContentTypeExtra::class)) + } + + @Test + fun directoryExtras() { + val path = base / "a.txt" + fileSystem.createDirectory(path) + fakeFileSystem.setExtra(path, ContentTypeExtra::class, ContentTypeExtra("text/plain")) + val metadata = fileSystem.metadata(path) + assertEquals(ContentTypeExtra("text/plain"), metadata.extra(ContentTypeExtra::class)) + } + + @Test + fun symlinkExtras() { + if (!supportsSymlink()) return + + val pathA = base / "a.txt" + val pathB = base / "b.txt" + fileSystem.createSymlink(pathA, pathB) + fakeFileSystem.setExtra(pathA, ContentTypeExtra::class, ContentTypeExtra("text/plain")) + val metadata = fileSystem.metadata(pathA) + assertEquals(ContentTypeExtra("text/plain"), metadata.extra(ContentTypeExtra::class)) + } + + @Test + fun deleteExtra() { + val path = base / "a.txt" + path.writeUtf8("hello") + fakeFileSystem.setExtra(path, ContentTypeExtra::class, ContentTypeExtra("text/plain")) + fakeFileSystem.setExtra(path, ContentTypeExtra::class, null) + val metadata = fileSystem.metadata(path) + assertNull(metadata.extra(ContentTypeExtra::class)) + assertEquals(mapOf(), metadata.extras) + } + + @Test + fun extraIsNotCopiedByFileCopy() { + val pathA = base / "a.txt" + val pathB = base / "b.txt" + pathA.writeUtf8("hello") + fakeFileSystem.setExtra(pathA, ContentTypeExtra::class, ContentTypeExtra("text/plain")) + fileSystem.copy(pathA, pathB) + val metadata = fileSystem.metadata(pathB) + assertNull(metadata.extra(ContentTypeExtra::class)) + } + + @Test + fun extraIsMovedByAtomicMove() { + val pathA = base / "a.txt" + val pathB = base / "b.txt" + pathA.writeUtf8("hello") + fakeFileSystem.setExtra(pathA, ContentTypeExtra::class, ContentTypeExtra("text/plain")) + fileSystem.atomicMove(pathA, pathB) + val metadata = fileSystem.metadata(pathB) + assertEquals(ContentTypeExtra("text/plain"), metadata.extra(ContentTypeExtra::class)) + } + + @Test + fun extrasHappyPath() { + val metadata = FileMetadata( + isRegularFile = true, + size = 10L, + extras = mapOf(ContentTypeExtra::class to ContentTypeExtra("text/plain")), + ) + assertEquals(ContentTypeExtra("text/plain"), metadata.extra(ContentTypeExtra::class)) + } + + @Test + fun createExtrasDefensiveCopy() { + val extras = mutableMapOf<KClass<*>, Any>( + ContentTypeExtra::class to ContentTypeExtra("text/plain"), + ) + val metadata = FileMetadata( + isRegularFile = true, + size = 10L, + extras = extras, + ) + extras.clear() + assertEquals(ContentTypeExtra("text/plain"), metadata.extra(ContentTypeExtra::class)) + } + + @Test + fun getExtraAbsent() { + val metadata = FileMetadata( + isRegularFile = true, + size = 10L, + extras = mapOf(), + ) + assertNull(metadata.extra(ContentTypeExtra::class)) + } + + @Test + fun getExtraWrongType() { + val metadata = FileMetadata( + isRegularFile = true, + size = 10L, + extras = mapOf(ContentTypeExtra::class to "hello"), + ) + assertFailsWith<ClassCastException> { + metadata.extra(ContentTypeExtra::class) + } + } + + internal data class ContentTypeExtra( + val contentType: String, + ) +} diff --git a/okio/src/nonWasmTest/kotlin/okio/ForwardingFileSystemTest.kt b/okio/src/nonWasmTest/kotlin/okio/ForwardingFileSystemTest.kt new file mode 100644 index 00000000..6b7e089b --- /dev/null +++ b/okio/src/nonWasmTest/kotlin/okio/ForwardingFileSystemTest.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue +import kotlinx.datetime.Clock +import okio.Path.Companion.toPath +import okio.fakefilesystem.FakeFileSystem + +class ForwardingFileSystemTest : AbstractFileSystemTest( + clock = Clock.System, + fileSystem = object : ForwardingFileSystem(FakeFileSystem().apply { emulateUnix() }) {}, + windowsLimitations = false, + allowClobberingEmptyDirectories = false, + allowAtomicMoveFromFileToDirectory = false, + temporaryDirectory = "/".toPath(), +) { + @Test + fun pathBlocking() { + val forwardingFileSystem = object : ForwardingFileSystem(fileSystem) { + override fun delete(path: Path, mustExist: Boolean) { + throw IOException("synthetic failure!") + } + + override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path { + if (path.name.contains("blocked")) throw IOException("blocked path!") + return path + } + } + + forwardingFileSystem.createDirectory(base / "okay") + assertFailsWith<IOException> { + forwardingFileSystem.createDirectory(base / "blocked") + } + } + + @Test + fun operationBlocking() { + val forwardingFileSystem = object : ForwardingFileSystem(fileSystem) { + override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path { + if (functionName == "delete") throw IOException("blocked operation!") + return path + } + } + + forwardingFileSystem.createDirectory(base / "operation-blocking") + assertFailsWith<IOException> { + forwardingFileSystem.delete(base / "operation-blocking") + } + } + + @Test + fun pathMapping() { + val prefix = "/mapped" + val source = base / "source" + val mappedSource = (prefix + source).toPath() + val target = base / "target" + val mappedTarget = (prefix + target).toPath() + + source.writeUtf8("hello, world!") + + val forwardingFileSystem = object : ForwardingFileSystem(fileSystem) { + override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path { + return path.toString().removePrefix(prefix).toPath() + } + + override fun onPathResult(path: Path, functionName: String): Path { + return (prefix + path).toPath() + } + } + + forwardingFileSystem.copy(mappedSource, mappedTarget) + assertTrue(target in fileSystem.list(base)) + assertTrue(mappedTarget in forwardingFileSystem.list(base)) + assertEquals("hello, world!", source.readUtf8()) + assertEquals("hello, world!", target.readUtf8()) + } + + /** + * Path mapping might impact the sort order. Confirm that list() returns elements in sorted order + * even if that order is different in the delegate file system. + */ + @Test + fun pathMappingImpactedBySorting() { + val az = base / "az" + val by = base / "by" + val cx = base / "cx" + az.writeUtf8("az") + by.writeUtf8("by") + cx.writeUtf8("cx") + + val forwardingFileSystem = object : ForwardingFileSystem(fileSystem) { + override fun onPathResult(path: Path, functionName: String): Path { + return path.parent!! / path.name.reversed() + } + } + + assertEquals(listOf(base / "az", base / "by", base / "cx"), fileSystem.list(base)) + assertEquals(listOf(base / "xc", base / "yb", base / "za"), forwardingFileSystem.list(base)) + } + + @Test + fun copyIsNotForwarded() { + val log = mutableListOf<String>() + + val delegate = object : ForwardingFileSystem(fileSystem) { + override fun copy(source: Path, target: Path) { + throw AssertionError("unexpected call to copy()") + } + } + + val forwardingFileSystem = object : ForwardingFileSystem(delegate) { + override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path { + log += "$functionName($parameterName=$path)" + return path + } + } + + val source = base / "source" + source.writeUtf8("hello, world!") + val target = base / "target" + forwardingFileSystem.copy(source, target) + assertTrue(target in fileSystem.list(base)) + assertEquals("hello, world!", source.readUtf8()) + assertEquals("hello, world!", target.readUtf8()) + + assertEquals(listOf("source(file=$source)", "sink(file=$target)"), log) + } + + @Test + fun metadataForwardsParameterAndSymlinkTarget() { + val log = mutableListOf<String>() + + val forwardingFileSystem = object : ForwardingFileSystem(fileSystem) { + override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path { + log += "$functionName($parameterName=$path)" + return path + } + + override fun onPathResult(path: Path, functionName: String): Path { + log += "$functionName($path)" + return path + } + } + + val target = base / "symlink-target" + val source = base / "symlink-source" + + fileSystem.createSymlink(source, target) + + val sourceMetadata = forwardingFileSystem.metadata(source) + assertEquals(target, sourceMetadata.symlinkTarget) + + assertEquals(listOf("metadataOrNull(path=$source)", "metadataOrNull($target)"), log) + } +} diff --git a/okio/src/nonWasmTest/kotlin/okio/UseTest.kt b/okio/src/nonWasmTest/kotlin/okio/UseTest.kt new file mode 100644 index 00000000..a36cf068 --- /dev/null +++ b/okio/src/nonWasmTest/kotlin/okio/UseTest.kt @@ -0,0 +1,28 @@ +package okio + +import kotlin.test.Test +import okio.Path.Companion.toPath +import okio.fakefilesystem.FakeFileSystem + +class UseTest { + val fakeFileSystem = FakeFileSystem(clock = FakeClock()).also { it.emulateUnix() } + + val base = "/cache".toPath().also { + fakeFileSystem.createDirectories(it) + } + + @Test + fun closesWithUseBlock() { + fun testMethodWithUse() { + val sink = fakeFileSystem.sink(base / "all-files-includes-file") + + sink.use { + return@testMethodWithUse + } + } + + testMethodWithUse() + + fakeFileSystem.checkNoOpenFiles() + } +} diff --git a/okio/src/unixMain/kotlin/okio/UnixFileHandle.kt b/okio/src/unixMain/kotlin/okio/UnixFileHandle.kt new file mode 100644 index 00000000..62e7849f --- /dev/null +++ b/okio/src/unixMain/kotlin/okio/UnixFileHandle.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 kotlinx.cinterop.CPointer +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.usePinned +import platform.posix.FILE +import platform.posix.errno +import platform.posix.fclose +import platform.posix.fflush +import platform.posix.fileno +import platform.posix.fstat +import platform.posix.ftruncate +import platform.posix.stat + +internal class UnixFileHandle( + readWrite: Boolean, + private val file: CPointer<FILE>, +) : FileHandle(readWrite) { + override fun protectedSize(): Long { + memScoped { + val stat = alloc<stat>() + if (fstat(fileno(file), stat.ptr) != 0) { + throw errnoToIOException(errno) + } + return stat.st_size + } + } + + override fun protectedRead( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ): Int { + val bytesRead = if (array.isNotEmpty()) { + array.usePinned { pinned -> + variantPread(file, pinned.addressOf(arrayOffset), byteCount, fileOffset) + } + } else { + 0 + } + if (bytesRead == -1) throw errnoToIOException(errno) + if (bytesRead == 0) return -1 + return bytesRead + } + + override fun protectedWrite( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int, + ) { + val bytesWritten = if (array.isNotEmpty()) { + array.usePinned { pinned -> + variantPwrite(file, pinned.addressOf(arrayOffset), byteCount, fileOffset) + } + } else { + 0 + } + if (bytesWritten != byteCount) throw errnoToIOException(errno) + } + + override fun protectedFlush() { + if (fflush(file) != 0) { + throw errnoToIOException(errno) + } + } + + override fun protectedResize(size: Long) { + if (ftruncate(fileno(file), size) == -1) { + throw errnoToIOException(errno) + } + } + + override fun protectedClose() { + if (fclose(file) != 0) { + throw errnoToIOException(errno) + } + } +} diff --git a/okio/src/unixMain/kotlin/okio/UnixPosixVariant.kt b/okio/src/unixMain/kotlin/okio/UnixPosixVariant.kt new file mode 100644 index 00000000..27630d67 --- /dev/null +++ b/okio/src/unixMain/kotlin/okio/UnixPosixVariant.kt @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 kotlinx.cinterop.ByteVar +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.CValuesRef +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.toKString +import okio.Path.Companion.toPath +import okio.internal.toPath +import platform.posix.DEFFILEMODE +import platform.posix.ENOENT +import platform.posix.FILE +import platform.posix.O_CREAT +import platform.posix.O_EXCL +import platform.posix.O_RDWR +import platform.posix.PATH_MAX +import platform.posix.S_IFLNK +import platform.posix.S_IFMT +import platform.posix.errno +import platform.posix.fdopen +import platform.posix.fileno +import platform.posix.fopen +import platform.posix.free +import platform.posix.getenv +import platform.posix.mkdir +import platform.posix.open +import platform.posix.pread +import platform.posix.pwrite +import platform.posix.readlink +import platform.posix.realpath +import platform.posix.remove +import platform.posix.rename +import platform.posix.stat +import platform.posix.symlink +import platform.posix.timespec + +internal actual val PLATFORM_TEMPORARY_DIRECTORY: Path + get() { + val tmpdir = getenv("TMPDIR") + if (tmpdir != null) return tmpdir.toKString().toPath() + + return "/tmp".toPath() + } + +internal actual val PLATFORM_DIRECTORY_SEPARATOR = "/" + +internal actual fun PosixFileSystem.variantDelete(path: Path, mustExist: Boolean) { + val result = remove(path.toString()) + if (result != 0) { + if (errno == ENOENT) { + if (mustExist) { + throw FileNotFoundException("no such file: $path") + } else { + return + } + } + throw errnoToIOException(errno) + } +} + +@OptIn(UnsafeNumber::class) +internal actual fun PosixFileSystem.variantMkdir(dir: Path): Int { + return mkdir(dir.toString(), 0b111111111u.convert() /* octal 777 */) +} + +internal actual fun PosixFileSystem.variantCanonicalize(path: Path): Path { + // Note that realpath() fails if the file doesn't exist. + val fullpath = realpath(path.toString(), null) + ?: throw errnoToIOException(errno) + try { + return Buffer().writeNullTerminated(fullpath).toPath(normalize = true) + } finally { + free(fullpath) + } +} + +internal actual fun PosixFileSystem.variantMove( + source: Path, + target: Path, +) { + val result = rename(source.toString(), target.toString()) + if (result != 0) { + throw errnoToIOException(errno) + } +} + +internal actual fun PosixFileSystem.variantSource(file: Path): Source { + val openFile: CPointer<FILE> = fopen(file.toString(), "r") + ?: throw errnoToIOException(errno) + return FileSource(openFile) +} + +internal actual fun PosixFileSystem.variantSink(file: Path, mustCreate: Boolean): Sink { + val openFile: CPointer<FILE> = fopen(file.toString(), if (mustCreate) "wx" else "w") + ?: throw errnoToIOException(errno) + return FileSink(openFile) +} + +internal actual fun PosixFileSystem.variantAppendingSink(file: Path, mustExist: Boolean): Sink { + // There is a `r+` flag which we could have used to force existence of [file] but this flag + // doesn't allow opening for appending, and we don't currently have a way to move the cursor to + // the end of the file. We are then forcing existence non-atomically. + if (mustExist && !exists(file)) throw IOException("$file doesn't exist.") + val openFile: CPointer<FILE> = fopen(file.toString(), "a") + ?: throw errnoToIOException(errno) + return FileSink(openFile) +} + +internal actual fun PosixFileSystem.variantOpenReadOnly(file: Path): FileHandle { + val openFile: CPointer<FILE> = fopen(file.toString(), "r") + ?: throw errnoToIOException(errno) + return UnixFileHandle(false, openFile) +} + +internal actual fun PosixFileSystem.variantOpenReadWrite( + file: Path, + mustCreate: Boolean, + mustExist: Boolean, +): FileHandle { + // Note that we're using open() followed by fdopen() rather than fopen() because this way we + // can pass exactly the flags we want. Note that there's no string mode that opens for reading + // and writing that creates if necessary. ("a+" has features but can't do random access). + val flags = when { + mustCreate && mustExist -> + throw IllegalArgumentException("Cannot require mustCreate and mustExist at the same time.") + mustCreate -> O_RDWR or O_CREAT or O_EXCL + mustExist -> O_RDWR + else -> O_RDWR or O_CREAT + } + + val fid = open(file.toString(), flags, DEFFILEMODE) + if (fid == -1) throw errnoToIOException(errno) + + // Use 'r+' to get reading and writing on the FILE, which is all we need. + val openFile = fdopen(fid, "r+") ?: throw errnoToIOException(errno) + + return UnixFileHandle(true, openFile) +} + +internal actual fun PosixFileSystem.variantCreateSymlink(source: Path, target: Path) { + if (source.parent == null || !exists(source.parent!!)) { + throw IOException("parent directory does not exist: ${source.parent}") + } + + if (exists(source)) { + throw IOException("already exists: $source") + } + + val result = symlink(target.toString(), source.toString()) + if (result != 0) { + throw errnoToIOException(errno) + } +} + +@OptIn(UnsafeNumber::class) +internal fun variantPread( + file: CPointer<FILE>, + target: CValuesRef<*>, + byteCount: Int, + offset: Long, +): Int = pread(fileno(file), target, byteCount.convert(), offset).convert() + +@OptIn(UnsafeNumber::class) +internal fun variantPwrite( + file: CPointer<FILE>, + source: CValuesRef<*>, + byteCount: Int, + offset: Long, +): Int = pwrite(fileno(file), source, byteCount.convert(), offset).convert() + +@OptIn(UnsafeNumber::class) +internal val timespec.epochMillis: Long + get() = tv_sec * 1000L + tv_sec / 1_000_000L + +@OptIn(UnsafeNumber::class) +internal fun symlinkTarget(stat: stat, path: Path): Path? { + if (stat.st_mode.toInt() and S_IFMT != S_IFLNK) return null + + // `path` is a symlink, let's resolve its target. + memScoped { + val buffer = allocArray<ByteVar>(PATH_MAX) + val byteCount = readlink(path.toString(), buffer, PATH_MAX.convert()) + if (byteCount.convert<Int>() == -1) { + throw errnoToIOException(errno) + } + return buffer.toKString().toPath() + } +} diff --git a/okio/src/wasmMain/kotlin/okio/FileSystem.kt b/okio/src/wasmMain/kotlin/okio/FileSystem.kt new file mode 100644 index 00000000..2152a91b --- /dev/null +++ b/okio/src/wasmMain/kotlin/okio/FileSystem.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 okio.Path.Companion.toPath +import okio.internal.commonCopy +import okio.internal.commonCreateDirectories +import okio.internal.commonDeleteRecursively +import okio.internal.commonExists +import okio.internal.commonListRecursively +import okio.internal.commonMetadata + +actual abstract class FileSystem { + actual abstract fun canonicalize(path: Path): Path + + actual fun metadata(path: Path): FileMetadata = commonMetadata(path) + + actual abstract fun metadataOrNull(path: Path): FileMetadata? + + actual fun exists(path: Path): Boolean = commonExists(path) + + actual abstract fun list(dir: Path): List<Path> + + actual abstract fun listOrNull(dir: Path): List<Path>? + + actual open fun listRecursively(dir: Path, followSymlinks: Boolean): Sequence<Path> = + commonListRecursively(dir, followSymlinks) + + actual abstract fun openReadOnly(file: Path): FileHandle + + actual abstract fun openReadWrite( + file: Path, + mustCreate: Boolean, + mustExist: Boolean, + ): FileHandle + + actual abstract fun source(file: Path): Source + + actual inline fun <T> read(file: Path, readerAction: BufferedSource.() -> T): T { + return source(file).buffer().use { + it.readerAction() + } + } + + actual abstract fun sink(file: Path, mustCreate: Boolean): Sink + + actual inline fun <T> write( + file: Path, + mustCreate: Boolean, + writerAction: BufferedSink.() -> T, + ): T { + return sink(file, mustCreate).buffer().use { + it.writerAction() + } + } + + actual abstract fun appendingSink(file: Path, mustExist: Boolean): Sink + + actual abstract fun createDirectory(dir: Path, mustCreate: Boolean) + + actual fun createDirectories(dir: Path, mustCreate: Boolean): Unit = commonCreateDirectories(dir, mustCreate) + + actual abstract fun atomicMove(source: Path, target: Path) + + actual open fun copy(source: Path, target: Path): Unit = commonCopy(source, target) + + actual abstract fun delete(path: Path, mustExist: Boolean) + + actual open fun deleteRecursively(fileOrDirectory: Path, mustExist: Boolean): Unit = + commonDeleteRecursively(fileOrDirectory, mustExist) + + actual abstract fun createSymlink(source: Path, target: Path) + + actual companion object { + actual val SYSTEM_TEMPORARY_DIRECTORY: Path = "/tmp".toPath() + } +} diff --git a/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt b/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt new file mode 100644 index 00000000..4a874ec8 --- /dev/null +++ b/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 + +internal actual val PLATFORM_DIRECTORY_SEPARATOR: String + get() = "/" diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..1a2efbb4 --- /dev/null +++ b/renovate.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ], + "semanticCommits": "disabled" +} diff --git a/samples/build.gradle b/samples/build.gradle deleted file mode 100644 index efaef19e..00000000 --- a/samples/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -apply plugin: 'org.jetbrains.kotlin.multiplatform' -apply plugin: 'application' - -mainClassName = System.getProperty("mainClass") - -kotlin { - jvm { - withJava() - } - sourceSets { - commonMain { - dependencies { - implementation project(':okio') - } - } - jvmTest { - dependencies { - implementation deps.test.junit - implementation deps.test.assertj - } - } - } -} diff --git a/samples/build.gradle.kts b/samples/build.gradle.kts new file mode 100644 index 00000000..8fb0952e --- /dev/null +++ b/samples/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + kotlin("multiplatform") + application +} + +application { + mainClass.set(System.getProperty("mainClass")) +} + +kotlin { + jvm { + withJava() + } + sourceSets { + commonMain { + dependencies { + implementation(projects.okio) + } + } + val jvmTest by getting { + dependencies { + implementation(libs.test.junit) + implementation(libs.test.assertj) + } + } + } +} diff --git a/samples/src/jvmMain/java/okio/samples/BitmapEncoder.java b/samples/src/jvmMain/java/okio/samples/BitmapEncoder.java index a505f6dc..8642170e 100644 --- a/samples/src/jvmMain/java/okio/samples/BitmapEncoder.java +++ b/samples/src/jvmMain/java/okio/samples/BitmapEncoder.java @@ -15,10 +15,11 @@ */ package okio.samples; -import java.io.File; import java.io.IOException; import okio.BufferedSink; +import okio.FileSystem; import okio.Okio; +import okio.Path; public final class BitmapEncoder { static final class Bitmap { @@ -66,8 +67,8 @@ public final class BitmapEncoder { return new Bitmap(pixels); } - void encode(Bitmap bitmap, File file) throws IOException { - try (BufferedSink sink = Okio.buffer(Okio.sink(file))) { + void encode(Bitmap bitmap, FileSystem fileSystem, Path path) throws IOException { + try (BufferedSink sink = Okio.buffer(fileSystem.sink(path))) { encode(bitmap, sink); } } @@ -122,6 +123,6 @@ public final class BitmapEncoder { public static void main(String[] args) throws Exception { BitmapEncoder encoder = new BitmapEncoder(); Bitmap bitmap = encoder.generateGradient(); - encoder.encode(bitmap, new File("gradient.bmp")); + encoder.encode(bitmap, FileSystem.SYSTEM, Path.get("gradient.bmp")); } } diff --git a/samples/src/jvmMain/java/okio/samples/Hashing.java b/samples/src/jvmMain/java/okio/samples/Hashing.java index 0f2b4474..66d99ba6 100644 --- a/samples/src/jvmMain/java/okio/samples/Hashing.java +++ b/samples/src/jvmMain/java/okio/samples/Hashing.java @@ -15,23 +15,24 @@ */ package okio.samples; -import java.io.File; import java.io.IOException; import okio.Buffer; import okio.BufferedSink; import okio.BufferedSource; import okio.ByteString; +import okio.FileSystem; import okio.HashingSink; import okio.HashingSource; import okio.Okio; +import okio.Path; import okio.Source; public final class Hashing { public void run() throws Exception { - File file = new File("../README.md"); + Path path = Path.get("../README.md"); System.out.println("ByteString"); - ByteString byteString = readByteString(file); + ByteString byteString = readByteString(path); System.out.println(" md5: " + byteString.md5().hex()); System.out.println(" sha1: " + byteString.sha1().hex()); System.out.println(" sha256: " + byteString.sha256().hex()); @@ -39,7 +40,7 @@ public final class Hashing { System.out.println(); System.out.println("Buffer"); - Buffer buffer = readBuffer(file); + Buffer buffer = readBuffer(path); System.out.println(" md5: " + buffer.md5().hex()); System.out.println(" sha1: " + buffer.sha1().hex()); System.out.println(" sha256: " + buffer.sha256().hex()); @@ -47,7 +48,7 @@ public final class Hashing { System.out.println(); System.out.println("HashingSource"); - try (HashingSource hashingSource = HashingSource.sha256(Okio.source(file)); + try (HashingSource hashingSource = HashingSource.sha256(FileSystem.SYSTEM.source(path)); BufferedSource source = Okio.buffer(hashingSource)) { source.readAll(Okio.blackhole()); System.out.println(" sha256: " + hashingSource.hash().hex()); @@ -57,7 +58,7 @@ public final class Hashing { System.out.println("HashingSink"); try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole()); BufferedSink sink = Okio.buffer(hashingSink); - Source source = Okio.source(file)) { + Source source = FileSystem.SYSTEM.source(path)) { sink.writeAll(source); sink.close(); // Emit anything buffered. System.out.println(" sha256: " + hashingSink.hash().hex()); @@ -70,14 +71,14 @@ public final class Hashing { System.out.println(); } - public ByteString readByteString(File file) throws IOException { - try (BufferedSource source = Okio.buffer(Okio.source(file))) { + public ByteString readByteString(Path path) throws IOException { + try (BufferedSource source = Okio.buffer(FileSystem.SYSTEM.source(path))) { return source.readByteString(); } } - public Buffer readBuffer(File file) throws IOException { - try (Source source = Okio.source(file)) { + public Buffer readBuffer(Path path) throws IOException { + try (Source source = FileSystem.SYSTEM.source(path)) { Buffer buffer = new Buffer(); buffer.writeAll(source); return buffer; diff --git a/samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.java b/samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.java index b195a513..010319ef 100644 --- a/samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.java +++ b/samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.java @@ -15,19 +15,20 @@ */ package okio.samples; -import java.io.File; import java.io.IOException; import okio.BufferedSource; +import okio.FileSystem; import okio.Okio; +import okio.Path; import okio.Source; public final class ReadFileLineByLine { public void run() throws Exception { - readLines(new File("../README.md")); + readLines(Path.get("../README.md")); } - public void readLines(File file) throws IOException { - try (Source fileSource = Okio.source(file); + public void readLines(Path path) throws IOException { + try (Source fileSource = FileSystem.SYSTEM.source(path); BufferedSource bufferedFileSource = Okio.buffer(fileSource)) { while (true) { diff --git a/samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.kt b/samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.kt new file mode 100644 index 00000000..8dd573a6 --- /dev/null +++ b/samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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.samples + +import okio.FileSystem +import okio.IOException +import okio.Path +import okio.Path.Companion.toPath +import okio.buffer + +@Throws(IOException::class) +fun readLines(path: Path) { + FileSystem.SYSTEM.source(path).use { fileSource -> + fileSource.buffer().use { bufferedFileSource -> + while (true) { + val line = bufferedFileSource.readUtf8Line() ?: break + if ("square" in line) { + println(line) + } + } + } + } +} + +fun main() { + readLines("../README.md".toPath()) +} diff --git a/samples/src/jvmMain/java/okio/samples/ReadJavaIoFileLineByLine.java b/samples/src/jvmMain/java/okio/samples/ReadJavaIoFileLineByLine.java new file mode 100644 index 00000000..6479524d --- /dev/null +++ b/samples/src/jvmMain/java/okio/samples/ReadJavaIoFileLineByLine.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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.samples; + +import java.io.File; +import java.io.IOException; +import okio.BufferedSource; +import okio.Okio; +import okio.Source; + +public final class ReadJavaIoFileLineByLine { + public void run() throws Exception { + readLines(new File("../README.md")); + } + + public void readLines(File file) throws IOException { + try (Source fileSource = Okio.source(file); + BufferedSource bufferedFileSource = Okio.buffer(fileSource)) { + + while (true) { + String line = bufferedFileSource.readUtf8Line(); + if (line == null) break; + + if (line.contains("square")) { + System.out.println(line); + } + } + + } + } + + public static void main(String... args) throws Exception { + new ReadJavaIoFileLineByLine().run(); + } +} diff --git a/samples/src/jvmMain/java/okio/samples/WriteFile.java b/samples/src/jvmMain/java/okio/samples/WriteFile.java index e613abe5..c71e912a 100644 --- a/samples/src/jvmMain/java/okio/samples/WriteFile.java +++ b/samples/src/jvmMain/java/okio/samples/WriteFile.java @@ -15,20 +15,21 @@ */ package okio.samples; -import java.io.File; import java.io.IOException; import java.util.Map; import okio.BufferedSink; +import okio.FileSystem; import okio.Okio; +import okio.Path; import okio.Sink; public final class WriteFile { public void run() throws Exception { - writeEnv(new File("env.txt")); + writeEnv(Path.get("env.txt")); } - public void writeEnv(File file) throws IOException { - try (Sink fileSink = Okio.sink(file); + public void writeEnv(Path path) throws IOException { + try (Sink fileSink = FileSystem.SYSTEM.sink(path); BufferedSink bufferedSink = Okio.buffer(fileSink)) { for (Map.Entry<String, String> entry : System.getenv().entrySet()) { diff --git a/samples/src/jvmMain/java/okio/samples/WriteJavaIoFile.java b/samples/src/jvmMain/java/okio/samples/WriteJavaIoFile.java new file mode 100644 index 00000000..d2b0502a --- /dev/null +++ b/samples/src/jvmMain/java/okio/samples/WriteJavaIoFile.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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.samples; + +import java.io.File; +import java.io.IOException; +import java.util.Map; +import okio.BufferedSink; +import okio.Okio; +import okio.Sink; + +public final class WriteJavaIoFile { + public void run() throws Exception { + writeEnv(new File("env.txt")); + } + + public void writeEnv(File file) throws IOException { + try (Sink fileSink = Okio.sink(file); + BufferedSink bufferedSink = Okio.buffer(fileSink)) { + + for (Map.Entry<String, String> entry : System.getenv().entrySet()) { + bufferedSink.writeUtf8(entry.getKey()); + bufferedSink.writeUtf8("="); + bufferedSink.writeUtf8(entry.getValue()); + bufferedSink.writeUtf8("\n"); + } + + } + } + + public static void main(String... args) throws Exception { + new WriteFile().run(); + } +} diff --git a/samples/src/jvmMain/kotlin/okio/samples/BitmapEncoder.kt b/samples/src/jvmMain/kotlin/okio/samples/BitmapEncoder.kt index 86422832..e23b3261 100644 --- a/samples/src/jvmMain/kotlin/okio/samples/BitmapEncoder.kt +++ b/samples/src/jvmMain/kotlin/okio/samples/BitmapEncoder.kt @@ -15,16 +15,16 @@ */ package okio.samples -import okio.BufferedSink -import okio.buffer -import okio.sink -import java.io.File -import java.io.IOException import kotlin.math.hypot +import okio.BufferedSink +import okio.FileSystem +import okio.IOException +import okio.Path +import okio.Path.Companion.toPath class KotlinBitmapEncoder { class Bitmap( - private val pixels: Array<IntArray> + private val pixels: Array<IntArray>, ) { val width: Int = pixels[0].size val height: Int = pixels.size @@ -54,8 +54,8 @@ class KotlinBitmapEncoder { } @Throws(IOException::class) - fun encode(bitmap: Bitmap, file: File) { - file.sink().buffer().use { sink -> encode(bitmap, sink) } + fun encode(bitmap: Bitmap, fileSystem: FileSystem, path: Path) { + fileSystem.write(path) { encode(bitmap, this) } } /** https://en.wikipedia.org/wiki/BMP_file_format */ @@ -109,5 +109,5 @@ class KotlinBitmapEncoder { fun main() { val encoder = KotlinBitmapEncoder() val bitmap = encoder.generateGradient() - encoder.encode(bitmap, File("gradient.bmp")) + encoder.encode(bitmap, FileSystem.SYSTEM, "gradient.bmp".toPath()) } diff --git a/samples/src/jvmMain/kotlin/okio/samples/ExploreCharsets.kt b/samples/src/jvmMain/kotlin/okio/samples/ExploreCharsets.kt index 824a01ed..18744831 100644 --- a/samples/src/jvmMain/kotlin/okio/samples/ExploreCharsets.kt +++ b/samples/src/jvmMain/kotlin/okio/samples/ExploreCharsets.kt @@ -15,9 +15,9 @@ */ package okio.samples +import java.io.IOException import okio.ByteString.Companion.encodeUtf8 import okio.utf8Size -import java.io.IOException @Throws(IOException::class) fun dumpStringData(s: String) { diff --git a/samples/src/jvmMain/kotlin/okio/samples/GoldenValue.kt b/samples/src/jvmMain/kotlin/okio/samples/GoldenValue.kt index 2e86ff74..aeb5aeca 100644 --- a/samples/src/jvmMain/kotlin/okio/samples/GoldenValue.kt +++ b/samples/src/jvmMain/kotlin/okio/samples/GoldenValue.kt @@ -15,13 +15,13 @@ */ package okio.samples -import okio.Buffer -import okio.ByteString -import okio.ByteString.Companion.decodeBase64 import java.io.IOException import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.io.Serializable +import okio.Buffer +import okio.ByteString +import okio.ByteString.Companion.decodeBase64 class KotlinGoldenValue { fun run() { @@ -60,7 +60,7 @@ class KotlinGoldenValue { private fun assertEquals( a: Point, - b: Point + b: Point, ) { if (a.x != b.x || a.y != b.y) throw AssertionError() } diff --git a/samples/src/jvmMain/kotlin/okio/samples/Hashing.kt b/samples/src/jvmMain/kotlin/okio/samples/Hashing.kt index 3169d4e1..1584d8b4 100644 --- a/samples/src/jvmMain/kotlin/okio/samples/Hashing.kt +++ b/samples/src/jvmMain/kotlin/okio/samples/Hashing.kt @@ -15,23 +15,24 @@ */ package okio.samples +import java.io.IOException import okio.Buffer import okio.ByteString import okio.ByteString.Companion.decodeHex +import okio.FileSystem import okio.HashingSink.Companion.sha256 import okio.HashingSource.Companion.sha256 +import okio.Path +import okio.Path.Companion.toPath import okio.blackholeSink import okio.buffer -import okio.source -import java.io.File -import java.io.IOException class KotlinHashing { fun run() { - val file = File("../README.md") + val path = "../README.md".toPath() println("ByteString") - val byteString = readByteString(file) + val byteString = readByteString(path) println(" md5: " + byteString.md5().hex()) println(" sha1: " + byteString.sha1().hex()) println(" sha256: " + byteString.sha256().hex()) @@ -39,7 +40,7 @@ class KotlinHashing { println() println("Buffer") - val buffer = readBuffer(file) + val buffer = readBuffer(path) println(" md5: " + buffer.md5().hex()) println(" sha1: " + buffer.sha1().hex()) println(" sha256: " + buffer.sha256().hex()) @@ -47,7 +48,7 @@ class KotlinHashing { println() println("HashingSource") - sha256(file.source()).use { hashingSource -> + sha256(FileSystem.SYSTEM.source(path)).use { hashingSource -> hashingSource.buffer().use { source -> source.readAll(blackholeSink()) println(" sha256: " + hashingSource.hash.hex()) @@ -58,7 +59,7 @@ class KotlinHashing { println("HashingSink") sha256(blackholeSink()).use { hashingSink -> hashingSink.buffer().use { sink -> - file.source().use { source -> + FileSystem.SYSTEM.source(path).use { source -> sink.writeAll(source) sink.close() // Emit anything buffered. println(" sha256: " + hashingSink.hash.hex()) @@ -74,14 +75,16 @@ class KotlinHashing { } @Throws(IOException::class) - fun readByteString(file: File): ByteString { - return file.source().buffer().use { it.readByteString() } + fun readByteString(path: Path): ByteString { + return FileSystem.SYSTEM.read(path) { readByteString() } } @Throws(IOException::class) - fun readBuffer(file: File): Buffer { - return file.source().use { source -> - Buffer().also { it.writeAll(source) } + fun readBuffer(path: Path): Buffer { + FileSystem.SYSTEM.read(path) { + val result = Buffer() + readAll(result) + return result } } } diff --git a/samples/src/jvmMain/kotlin/okio/samples/ReadFileLineByLine.kt b/samples/src/jvmMain/kotlin/okio/samples/ReadJavaIoFileLineByLine.kt index b3fa31ba..05cdd6d3 100644 --- a/samples/src/jvmMain/kotlin/okio/samples/ReadFileLineByLine.kt +++ b/samples/src/jvmMain/kotlin/okio/samples/ReadJavaIoFileLineByLine.kt @@ -15,10 +15,10 @@ */ package okio.samples -import okio.buffer -import okio.source import java.io.File import java.io.IOException +import okio.buffer +import okio.source @Throws(IOException::class) fun readLines(file: File) { diff --git a/samples/src/jvmMain/kotlin/okio/samples/SocksProxyServer.kt b/samples/src/jvmMain/kotlin/okio/samples/SocksProxyServer.kt index d3b786a1..f68811b9 100644 --- a/samples/src/jvmMain/kotlin/okio/samples/SocksProxyServer.kt +++ b/samples/src/jvmMain/kotlin/okio/samples/SocksProxyServer.kt @@ -15,13 +15,6 @@ */ package okio.samples -import okio.Buffer -import okio.BufferedSink -import okio.Sink -import okio.Source -import okio.buffer -import okio.sink -import okio.source import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress @@ -33,6 +26,13 @@ import java.net.URL import java.util.Collections import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors +import okio.Buffer +import okio.BufferedSink +import okio.Sink +import okio.Source +import okio.buffer +import okio.sink +import okio.source private const val VERSION_5 = 5 private const val METHOD_NO_AUTHENTICATION_REQUIRED = 0 @@ -64,7 +64,7 @@ class KotlinSocksProxyServer { fun proxy(): Proxy = Proxy( Proxy.Type.SOCKS, - InetSocketAddress.createUnresolved("localhost", serverSocket.localPort) + InetSocketAddress.createUnresolved("localhost", serverSocket.localPort), ) private fun acceptSockets() { diff --git a/samples/src/jvmMain/kotlin/okio/samples/WriteFile.kt b/samples/src/jvmMain/kotlin/okio/samples/WriteFile.kt index 56726932..516e9f67 100644 --- a/samples/src/jvmMain/kotlin/okio/samples/WriteFile.kt +++ b/samples/src/jvmMain/kotlin/okio/samples/WriteFile.kt @@ -15,14 +15,15 @@ */ package okio.samples +import okio.FileSystem +import okio.IOException +import okio.Path +import okio.Path.Companion.toPath import okio.buffer -import okio.sink -import java.io.File -import java.io.IOException @Throws(IOException::class) -fun writeEnv(file: File) { - file.sink().buffer().use { sink -> +fun writeEnv(path: Path) { + FileSystem.SYSTEM.sink(path).buffer().use { sink -> for ((key, value) in System.getenv()) { sink.writeUtf8(key) sink.writeUtf8("=") @@ -33,5 +34,5 @@ fun writeEnv(file: File) { } fun main() { - writeEnv(File("env.txt")) + writeEnv("env.txt".toPath()) } diff --git a/samples/src/jvmMain/kotlin/okio/samples/WriteJavaIoFile.kt b/samples/src/jvmMain/kotlin/okio/samples/WriteJavaIoFile.kt new file mode 100644 index 00000000..9981462a --- /dev/null +++ b/samples/src/jvmMain/kotlin/okio/samples/WriteJavaIoFile.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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.samples + +import java.io.File +import java.io.IOException +import okio.buffer +import okio.sink + +@Throws(IOException::class) +fun writeEnv(file: File) { + file.sink().buffer().use { sink -> + for ((key, value) in System.getenv()) { + sink.writeUtf8(key) + sink.writeUtf8("=") + sink.writeUtf8(value) + sink.writeUtf8("\n") + } + } +} + +fun main() { + writeEnv(File("env.txt")) +} diff --git a/samples/src/jvmTest/java/okio/samples/ChannelsTest.java b/samples/src/jvmTest/java/okio/samples/ChannelsTest.java index 8c497475..6b3042b0 100644 --- a/samples/src/jvmTest/java/okio/samples/ChannelsTest.java +++ b/samples/src/jvmTest/java/okio/samples/ChannelsTest.java @@ -18,7 +18,6 @@ package okio.samples; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.EnumSet; import java.util.Set; @@ -84,7 +83,7 @@ public final class ChannelsTest { } @Test public void testReadWriteFile() throws Exception { - Path path = temporaryFolder.newFile().toPath(); + java.nio.file.Path path = temporaryFolder.newFile().toPath(); Sink sink = new FileChannelSink(FileChannel.open(path, w), Timeout.NONE); sink.write(new Buffer().writeUtf8(quote), 317); @@ -105,7 +104,7 @@ public final class ChannelsTest { } @Test public void testAppend() throws Exception { - Path path = temporaryFolder.newFile().toPath(); + java.nio.file.Path path = temporaryFolder.newFile().toPath(); Buffer buffer = new Buffer().writeUtf8(quote); Sink sink; diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 97f9d9e9..00000000 --- a/settings.gradle +++ /dev/null @@ -1,14 +0,0 @@ -rootProject.name = 'okio-parent' - -include ':okio' -include ':okio:jvm:japicmp' -include ':okio:jvm:jmh' -include ':samples' - -enableFeaturePreview("GRADLE_METADATA") - -// The Android test module doesn't work in IntelliJ. Use Android Studio or the command line. -if (properties.containsKey('android.injected.invoked.from.ide') || - System.getenv('ANDROID_SDK_ROOT') != null) { - include ':android-test' -} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..b07a4b81 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,25 @@ +rootProject.name = "okio-parent" + +includeBuild("build-support") + +include(":okio") +include(":okio-assetfilesystem") +include(":okio-bom") +include(":okio-fakefilesystem") +if (System.getProperty("kjs", "true").toBoolean()) { + include(":okio-nodefilesystem") +} +include(":okio-testing-support") +include(":okio:jvm:jmh") +if (System.getProperty("kwasm", "true").toBoolean()) { + include(":okio-wasifilesystem") +} +include(":samples") + +// The Android test module doesn't work in IntelliJ. Use Android Studio or the command line. +if (System.getProperties().containsKey("android.injected.invoked.from.ide") || + System.getenv("ANDROID_SDK_ROOT") != null) { + include(":android-test") +} + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") |