All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nativeMain.kotbase.Blob.native.kt Maven / Gradle / Ivy

There is a newer version: 3.1.3-1.1.0
Show newest version
/*
 * Copyright 2022-2023 Jeff Lockhart
 *
 * 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 kotbase

import cnames.structs.CBLBlob
import cnames.structs.CBLBlobReadStream
import cnames.structs.CBLBlobWriteStream
import kotbase.internal.DbContext
import kotbase.internal.fleece.*
import kotbase.internal.wrapCBLError
import kotbase.util.identityHashCodeHex
import kotlinx.cinterop.*
import kotlinx.io.Buffer
import kotlinx.io.IOException
import kotlinx.io.RawSource
import kotlinx.io.Source
import kotlinx.io.buffered
import kotlinx.io.files.FileNotFoundException
import kotlinx.io.files.Path
import kotlinx.io.files.SystemFileSystem
import kotlinx.io.readByteArray
import libcblite.*
import platform.posix.EINVAL
import platform.posix.R_OK
import platform.posix.access
import platform.posix.errno
import kotlin.experimental.ExperimentalNativeApi
import kotlin.native.ref.createCleaner

private const val MIME_UNKNOWN = "application/octet-stream"

public actual class Blob
private constructor(
    actual: CPointer?,
    dbContext: DbContext? = null,
    private val dict: Dictionary? = null
) {

    init {
        CBLBlob_Retain(actual)
    }

    private val memory = object {
        var actual: CPointer? = actual
    }

    @OptIn(ExperimentalNativeApi::class)
    @Suppress("unused")
    private val cleaner = createCleaner(memory) {
        CBLBlob_Release(it.actual)
    }

    public actual constructor(contentType: String, content: ByteArray) : this(
        contentType.toFLString(),
        content.toFLSlice()
    ) {
        blobContent = content
    }

    internal constructor(content: ByteArray) : this(MIME_UNKNOWN, content)

    internal constructor(
        contentType: CValue = MIME_UNKNOWN.toFLString(),
        content: CValue
    ) : this(CBLBlob_CreateWithData(contentType, content)!!) {
        CBLBlob_Release(actual)
    }

    internal constructor(actual: CPointer?, dbContext: DbContext? = null) :
            this(actual, dbContext, null)

    internal constructor(dict: Dictionary?) : this(actual = null, dict = dict)

    public actual constructor(contentType: String, stream: Source) : this(actual = null) {
        blobContentType = contentType
        blobContentStream = stream
    }

    @Throws(IOException::class)
    public actual constructor(contentType: String, fileURL: String) : this(
        contentType,
        fileURL.toFileSource()
    )

    internal val actual: CPointer?
        get() = memory.actual

    private var dbContext: DbContext? = dbContext
        set(value) {
            field = value
            val db = value?.database
            if (db != null && actual == null) {
                saveToDb(db)
            } else {
                value?.addStreamBlob(this)
            }
        }

    internal fun saveToDb(db: Database) {
        if (actual == null) {
            memory.actual = CBLBlob_CreateWithStream(
                blobContentType.toFLString(),
                blobContentStream!!.blobWriteStream(db)
            )
        }
    }

    private var blobContent: ByteArray? = null

    @OptIn(ExperimentalStdlibApi::class)
    public actual val content: ByteArray?
        get() {
            if (blobContent == null) {
                if (blobContentStream != null) {
                    blobContentStream!!.use {
                        blobContent = it.readByteArray()
                    }
                    blobContentStream = null
                    memory.actual = CBLBlob_CreateWithData(
                        blobContentType.toFLString(),
                        blobContent!!.toFLSlice()
                    )
                } else if (actual != null) {
                    dbContext?.database?.mustBeOpen()
                    blobContent = wrapCBLError { error ->
                        CBLBlob_Content(actual, error).toByteArray()
                    }
                }
            }
            return blobContent?.copyOf()
        }

    private var blobContentStream: Source? = null

    public actual val contentStream: Source?
        get() {
            if (blobContent != null) {
                return Buffer().apply {
                    write(blobContent!!)
                }
            }
            actual ?: return null
            return wrapCBLError { error ->
                CBLBlob_OpenContentStream(actual, error)?.asSource()?.buffered()
            }
        }

    private var blobContentType: String? = null

    public actual val contentType: String
        get() {
            return if (actual != null) {
                CBLBlob_ContentType(actual).toKString()
            } else {
                dict?.getString(PROP_CONTENT_TYPE)
            } ?: blobContentType ?: MIME_UNKNOWN
        }

    public actual fun toJSON(): String {
        if (digest == null) {
            throw IllegalStateException("A Blob may be encoded as JSON only after it has been saved in a database")
        }
        return if (actual != null) {
            FLValue_ToJSON(CBLBlob_Properties(actual)?.reinterpret()).toKString()!!
        } else {
            dict!!.toJSON()
        }
    }

    public actual val length: Long
        get() {
            return if (actual != null) CBLBlob_Length(actual).toLong()
            else dict?.getLong(PROP_LENGTH) ?: 0
        }

    public actual val digest: String?
        get() {
            // Java SDK sets digest only after installed in database
            return if (dbContext?.database == null) null
            else if (actual != null) CBLBlob_Digest(actual).toKString()
            else dict?.getString(PROP_DIGEST)
        }

    public actual val properties: Map
        get() = CBLBlob_Properties(actual)!!.toMap(null)

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Blob) return false
        return if (actual != null) CBLBlob_Equals(actual, other.actual)
        else if (blobContentStream != null) blobContentStream == other.blobContentStream
        else dict == other.dict
    }

    override fun hashCode(): Int =
        content.contentHashCode()

    override fun toString(): String =
        "Blob{${identityHashCodeHex()}: $digest($contentType, $length)}"

    internal fun checkSetDb(dbContext: DbContext?) {
        if (this.dbContext == null) {
            this.dbContext = dbContext
        } else if (dbContext?.database == null && this.dbContext?.database != null) {
            dbContext?.database = this.dbContext?.database
        }
    }

    public actual companion object {

        private const val META_PROP_TYPE = "@type"
        private const val TYPE_BLOB = "blob"

        private const val PROP_DIGEST = "digest"
        private const val PROP_LENGTH = "length"
        private const val PROP_CONTENT_TYPE = "content_type"

        public actual fun isBlob(props: Map?): Boolean {
            props ?: return false

            // Java SDK check is stricter than C SDK, which only checks @type = blob
            //return FLDict_IsBlob(MutableDictionary(props).actual)

            if (props[PROP_DIGEST] !is String) return false
            if (TYPE_BLOB != props[META_PROP_TYPE]) return false
            var nProps = 2

            if (props.containsKey(PROP_CONTENT_TYPE)) {
                if (props[PROP_CONTENT_TYPE] !is String) return false
                nProps++
            }

            val len = props[PROP_LENGTH]
            if (len != null) {
                if (len !is Int && len !is Long) return false
                nProps++
            }

            return nProps == props.size
        }
    }
}

internal fun CPointer.asBlob(ctxt: DbContext?) = Blob(this, ctxt)

private fun CPointer.asSource(): RawSource =
    BlobReadStreamSource(this)

private class BlobReadStreamSource(val actual: CPointer) : RawSource {

    private val memory = object {
        var closeCalled = false
        val actual = [email protected]
    }

    @OptIn(ExperimentalNativeApi::class)
    @Suppress("unused")
    private val cleaner = createCleaner(memory) {
        if (!it.closeCalled) {
            CBLBlobReader_Close(it.actual)
        }
    }

    override fun close() {
        memory.closeCalled = true
        CBLBlobReader_Close(actual)
    }

    override fun readAtMostTo(sink: Buffer, byteCount: Long): Long {
        if (byteCount == 0L) return 0L
        require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
        val bytes = ByteArray(byteCount.toInt())
        val bytesRead = wrapCBLError { error ->
            CBLBlobReader_Read(actual, bytes.refTo(0), byteCount.convert(), error)
        }
        if (bytesRead < 0) throw IOException()
        if (bytesRead == 0) return -1
        sink.write(bytes, 0, bytesRead)
        return bytesRead.toLong()
    }
}

@OptIn(ExperimentalStdlibApi::class)
private fun Source.blobWriteStream(db: Database): CPointer {
    val writer = wrapCBLError { error ->
        CBLBlobWriter_Create(db.actual, error)
    }!!
    try {
        val bufferSize = 8 * 1024
        val buffer = ByteArray(bufferSize)
        use { source ->
            while (!source.exhausted()) {
                val read = source.readAtMostTo(buffer)
                wrapCBLError { error ->
                    CBLBlobWriter_Write(writer, buffer.refTo(0), read.convert(), error)
                }
            }
        }
    } catch (e: Exception) {
        CBLBlobWriter_Close(writer)
        throw e
    }
    return writer
}

private fun String.toFileSource(): Source {
    val path = toFilePath()
    val fs = SystemFileSystem
    if (!fs.exists(path)) {
        throw FileNotFoundException("$this: open failed: ENOENT (No such file or directory)")
    }
    return fs.source(path).buffered()
}

@OptIn(ExperimentalNativeApi::class)
private fun String.toFilePath(): Path {
    val match = """^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?.+$""".toRegex()
        .matchEntire(this)
    match?.groups?.get(1)?.let { scheme ->
        // Windows drive letters are ok
        if (Platform.osFamily != OsFamily.WINDOWS || scheme.value.length != 1) {
            if (!scheme.value.equals("file", ignoreCase = true)) {
                throw IllegalArgumentException("$this must be a file-based URL.")
            }
        }
    }
    if (access(this, R_OK) == -1 && errno == EINVAL) {
        throw IllegalArgumentException("$this must be a valid file path.")
    }
    return Path(this)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy