Skip to content

Commit 73762dc

Browse files
committed
Working DBM prototype via QDBM
1 parent 08d4f66 commit 73762dc

5 files changed

Lines changed: 310 additions & 0 deletions

File tree

azure-pipelines.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ jobs:
2020
mkdir -p ./jdk/binaries/
2121
curl https://cdn.azul.com/zulu/bin/zulu8.42.0.23-ca-fx-jdk8.0.232-linux_x64.tar.gz --output ./jdk/zulu8.42.0.23-ca-fx-jdk8.0.232-linux_x64.tar.gz
2222
23+
- task: Bash@3
24+
inputs:
25+
targetType: 'inline'
26+
script: |
27+
sudo apt-get install libqdbm-dev
28+
2329
- task: JavaToolInstaller@0
2430
inputs:
2531
jdkFile: ./jdk/zulu8.42.0.23-ca-fx-jdk8.0.232-linux_x64.tar.gz

multiplatform-settings/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import com.russhwolf.settings.build.standardConfiguration
18+
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
1819

1920
plugins {
2021
id("com.android.library")
@@ -28,6 +29,9 @@ apply(from = "../gradle/publish.gradle")
2829
standardConfiguration()
2930

3031
kotlin {
32+
targets.getByName<KotlinNativeTarget>("linuxX64") {
33+
compilations["main"].cinterops.create("qdbm")
34+
}
3135
sourceSets {
3236
commonMain {
3337
dependencies {
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright 2020 Russell Wolf
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import com.russhwolf.settings.Settings
18+
import com.russhwolf.settings.cinterop.qdbm.DBM
19+
import com.russhwolf.settings.cinterop.qdbm.DBM_REPLACE
20+
import com.russhwolf.settings.cinterop.qdbm.datum
21+
import com.russhwolf.settings.cinterop.qdbm.dbm_clearerr
22+
import com.russhwolf.settings.cinterop.qdbm.dbm_close
23+
import com.russhwolf.settings.cinterop.qdbm.dbm_delete
24+
import com.russhwolf.settings.cinterop.qdbm.dbm_error
25+
import com.russhwolf.settings.cinterop.qdbm.dbm_fetch
26+
import com.russhwolf.settings.cinterop.qdbm.dbm_firstkey
27+
import com.russhwolf.settings.cinterop.qdbm.dbm_nextkey
28+
import com.russhwolf.settings.cinterop.qdbm.dbm_open
29+
import com.russhwolf.settings.cinterop.qdbm.dbm_store
30+
import kotlinx.cinterop.ByteVar
31+
import kotlinx.cinterop.CPointer
32+
import kotlinx.cinterop.CValue
33+
import kotlinx.cinterop.MemScope
34+
import kotlinx.cinterop.cValue
35+
import kotlinx.cinterop.cstr
36+
import kotlinx.cinterop.memScoped
37+
import kotlinx.cinterop.plus
38+
import kotlinx.cinterop.pointed
39+
import kotlinx.cinterop.reinterpret
40+
import kotlinx.cinterop.toCValues
41+
import kotlinx.cinterop.useContents
42+
import kotlinx.cinterop.value
43+
import platform.posix.O_CREAT
44+
import platform.posix.O_RDWR
45+
import platform.posix.S_IRGRP
46+
import platform.posix.S_IROTH
47+
import platform.posix.S_IRUSR
48+
import platform.posix.S_IWUSR
49+
import platform.posix.errno
50+
51+
// TODO clean up error checking?
52+
// TODO allow specifying directory
53+
@OptIn(ExperimentalUnsignedTypes::class)
54+
public class DbmSettings(private val path: String) : Settings {
55+
56+
override val keys: Set<String>
57+
get() = dbmOperation { dbm ->
58+
dbm.foldKeys(mutableListOf<String>()) { list, key -> list.apply { add(key.toKString()!!) } }.toSet()
59+
}
60+
61+
override val size: Int get() = dbmOperation { dbm -> dbm.foldKeys(0) { size, _ -> size + 1 } }
62+
63+
public override fun clear(): Unit = dbmOperation { dbm -> dbm.forEachKey { dbm_delete(dbm, it) } }
64+
public override fun remove(key: String): Unit = dbmOperation { dbm -> dbm_delete(dbm, datumOf(key)) }
65+
public override fun hasKey(key: String): Boolean = dbmOperation { dbm ->
66+
dbm.foldKeys(false) { out, thisKey -> if (key == thisKey.toKString()) true else out }
67+
}
68+
69+
public override fun putInt(key: String, value: Int): Unit = saveBytes(key, value.toByteArray())
70+
public override fun getInt(key: String, defaultValue: Int): Int = getIntOrNull(key) ?: defaultValue
71+
public override fun getIntOrNull(key: String): Int? = loadBytes(key)?.toInt()
72+
73+
public override fun putLong(key: String, value: Long): Unit = saveBytes(key, value.toByteArray())
74+
public override fun getLong(key: String, defaultValue: Long): Long = getLongOrNull(key) ?: defaultValue
75+
public override fun getLongOrNull(key: String): Long? = loadBytes(key)?.toLong()
76+
77+
public override fun putString(key: String, value: String): Unit = saveBytes(key, value.encodeToByteArray())
78+
public override fun getString(key: String, defaultValue: String): String = getStringOrNull(key) ?: defaultValue
79+
public override fun getStringOrNull(key: String): String? = loadBytes(key)?.decodeToString()
80+
81+
public override fun putFloat(key: String, value: Float): Unit = saveBytes(key, value.toRawBits().toByteArray())
82+
public override fun getFloat(key: String, defaultValue: Float): Float = getFloatOrNull(key) ?: defaultValue
83+
public override fun getFloatOrNull(key: String): Float? = loadBytes(key)?.toInt()?.let { Float.fromBits(it) }
84+
85+
public override fun putDouble(key: String, value: Double): Unit = saveBytes(key, value.toRawBits().toByteArray())
86+
public override fun getDouble(key: String, defaultValue: Double): Double = getDoubleOrNull(key) ?: defaultValue
87+
public override fun getDoubleOrNull(key: String): Double? = loadBytes(key)?.toLong()?.let { Double.fromBits(it) }
88+
89+
public override fun putBoolean(key: String, value: Boolean): Unit = saveBytes(key, byteArrayOf(if (value) 1 else 0))
90+
public override fun getBoolean(key: String, defaultValue: Boolean): Boolean = getBooleanOrNull(key) ?: defaultValue
91+
public override fun getBooleanOrNull(key: String): Boolean? = loadBytes(key)?.get(0)?.equals(0)?.not()
92+
93+
private inline fun <T> dbmOperation(action: MemScope.(dbm: CPointer<DBM>) -> T): T = memScoped {
94+
val dbm = dbm_open(path.cstr, O_RDWR or O_CREAT, S_IRUSR or S_IWUSR or S_IRGRP or S_IROTH)
95+
?: error("Error on dbm_open: $errno")
96+
val out = action(dbm)
97+
val error = dbm_error(dbm)
98+
if (error != 0) {
99+
try {
100+
error("error: $error")
101+
} finally {
102+
dbm_clearerr(dbm)
103+
}
104+
}
105+
dbm_close(dbm)
106+
out
107+
}
108+
109+
private inline fun saveBytes(key: String, bytes: ByteArray): Unit = dbmOperation { dbm ->
110+
dbm_store(dbm, datumOf(key), datumOf(bytes), DBM_REPLACE.toInt())
111+
}
112+
113+
private inline fun loadBytes(key: String): ByteArray? = dbmOperation { dbm ->
114+
val datum = dbm_fetch(dbm, datumOf(key))
115+
datum.toByteArray()
116+
}
117+
118+
private inline fun ByteArray.toLong(): Long = foldIndexed(0) { index, total: Long, byte: Byte ->
119+
((0xff.toLong() and byte.toLong()) shl index * Byte.SIZE_BITS) or total
120+
}
121+
122+
private inline fun ByteArray.toInt(): Int = foldIndexed(0) { index, total: Int, byte: Byte ->
123+
((0xff and byte.toInt()) shl index * Byte.SIZE_BITS) or total
124+
}
125+
126+
private inline fun Long.toByteArray(): ByteArray = ByteArray(Long.SIZE_BYTES) { index ->
127+
((this shr (Byte.SIZE_BITS * index)) and 0xff).toByte()
128+
}
129+
130+
private inline fun Int.toByteArray(): ByteArray = ByteArray(Int.SIZE_BYTES) { index ->
131+
((this shr (Byte.SIZE_BITS * index)) and 0xff).toByte()
132+
}
133+
134+
private inline fun CPointer<DBM>.forEachKey(block: (key: CValue<datum>) -> Unit) {
135+
val dbm = this
136+
var nextKey = dbm_firstkey(dbm)
137+
while (nextKey.useContents { dptr != null }) {
138+
block(nextKey)
139+
nextKey = dbm_nextkey(dbm)
140+
}
141+
}
142+
143+
private inline fun <A> CPointer<DBM>.foldKeys(initial: A, block: (accumulator: A, key: CValue<datum>) -> A): A {
144+
var accumulator = initial
145+
forEachKey { accumulator = block(accumulator, it) }
146+
return accumulator
147+
}
148+
149+
private inline fun CValue<datum>.toKString(): String? = toByteArray()?.decodeToString()
150+
private inline fun CValue<datum>.toByteArray(): ByteArray? = useContents {
151+
val size = dsize.toInt()
152+
val firstPtr: CPointer<ByteVar> = dptr?.reinterpret()
153+
?: return null
154+
return ByteArray(size) {
155+
val pointedValue = firstPtr.plus(it)?.pointed?.value
156+
pointedValue ?: 0
157+
}
158+
}
159+
160+
private inline fun MemScope.datumOf(string: String): CValue<datum> = datumOf(string.encodeToByteArray())
161+
private inline fun MemScope.datumOf(bytes: ByteArray): CValue<datum> = cValue {
162+
val cValues = bytes.toCValues()
163+
dptr = cValues.ptr
164+
dsize = cValues.size.toULong()
165+
}
166+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2020 Russell Wolf
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.russhwolf.settings
18+
19+
import DbmSettings
20+
import com.russhwolf.settings.cinterop.qdbm.DBM
21+
import com.russhwolf.settings.cinterop.qdbm.DBM_REPLACE
22+
import com.russhwolf.settings.cinterop.qdbm.datum
23+
import com.russhwolf.settings.cinterop.qdbm.dbm_close
24+
import com.russhwolf.settings.cinterop.qdbm.dbm_error
25+
import com.russhwolf.settings.cinterop.qdbm.dbm_open
26+
import com.russhwolf.settings.cinterop.qdbm.dbm_store
27+
import kotlinx.cinterop.CValuesRef
28+
import kotlinx.cinterop.cValue
29+
import kotlinx.cinterop.cstr
30+
import kotlinx.cinterop.memScoped
31+
import kotlinx.cinterop.pointed
32+
import kotlinx.cinterop.toCValues
33+
import kotlinx.cinterop.toKString
34+
import platform.posix.O_CREAT
35+
import platform.posix.O_RDWR
36+
import platform.posix.S_IRGRP
37+
import platform.posix.S_IROTH
38+
import platform.posix.S_IRUSR
39+
import platform.posix.S_IWUSR
40+
import platform.posix.errno
41+
import platform.posix.opendir
42+
import platform.posix.readdir
43+
import platform.posix.remove
44+
import kotlin.test.AfterTest
45+
import kotlin.test.BeforeTest
46+
import kotlin.test.Test
47+
import kotlin.test.assertEquals
48+
49+
@OptIn(ExperimentalUnsignedTypes::class)
50+
class DbmSettingsTest : BaseSettingsTest(
51+
platformFactory = object : Settings.Factory {
52+
override fun create(name: String?): Settings = DbmSettings(name ?: "dbm")
53+
},
54+
hasListeners = false
55+
) {
56+
57+
private lateinit var initialDbFiles: List<String>
58+
59+
@BeforeTest
60+
fun getInitialDbFiles() {
61+
initialDbFiles = getDbFiles()
62+
}
63+
64+
@AfterTest
65+
fun cleanup() {
66+
val newDbFiles = getDbFiles() - initialDbFiles
67+
newDbFiles.forEach {
68+
remove(it)
69+
}
70+
}
71+
72+
private fun getDbFiles(): List<String> {
73+
val directory = opendir("./") ?: return emptyList()
74+
75+
val out = mutableListOf<String>()
76+
while (true) {
77+
val entry = readdir(directory) ?: break
78+
79+
val filename = entry.pointed.d_name.toKString()
80+
if (filename.run { endsWith(".db") || endsWith(".dir") || endsWith(".pag") }) {
81+
out.add(filename)
82+
}
83+
}
84+
return out
85+
}
86+
87+
@Test
88+
fun constructor_filename() {
89+
val filename = "test_dbm"
90+
val settings = DbmSettings(filename)
91+
92+
memScoped {
93+
val dbm = dbm_open(
94+
filename.cstr,
95+
O_RDWR or O_CREAT,
96+
S_IRUSR or S_IWUSR or S_IRGRP or S_IROTH
97+
)
98+
?: error("error: $errno")
99+
dbm.checkError()
100+
101+
val key = cValue<datum> {
102+
val cValues = "key".encodeToByteArray().toCValues()
103+
dptr = cValues.ptr
104+
dsize = cValues.size.toULong()
105+
}
106+
val value = cValue<datum> {
107+
val cValues = "value".encodeToByteArray().toCValues()
108+
dptr = cValues.ptr
109+
dsize = cValues.size.toULong()
110+
}
111+
112+
if (dbm_store(
113+
dbm,
114+
key,
115+
value,
116+
DBM_REPLACE.toInt()
117+
) == -1) {
118+
dbm.checkError()
119+
}
120+
121+
dbm_close(dbm)
122+
}
123+
124+
assertEquals("value", settings["key", ""])
125+
}
126+
}
127+
128+
private fun CValuesRef<DBM>.checkError() {
129+
assertEquals(0, dbm_error(this))
130+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
headers = relic.h
2+
package = com.russhwolf.settings.cinterop.qdbm
3+
compilerOpts = -I/usr/include/qdbm
4+
linkerOpts = -lqdbm -L/usr/lib

0 commit comments

Comments
 (0)