finish up Hakurei.kt, add HakureiTest.kt

This commit is contained in:
mae 2025-09-28 20:08:11 -05:00
parent bc1c7172d4
commit d1ccb1c762
Signed by: maemachinebroke
GPG Key ID: B54591A4805E9CC8
7 changed files with 456 additions and 98 deletions

3
.idea/gradle.xml generated
View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
@ -8,8 +9,6 @@
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/definition" />
<option value="$PROJECT_DIR$/host" />
</set>
</option>
</GradleProjectSettings>

View File

@ -13,6 +13,7 @@ repositories {
dependencies {
testImplementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation(kotlin("reflect"))
}
tasks.test {

View File

@ -1,6 +1,4 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "planterette"
include("definition")
include("host")
rootProject.name = "planterette"

View File

@ -1,62 +1,85 @@
package moe.rosa.planterette.hakurei
import kotlinx.serialization.Serializable
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
@Serializable
data class AbsolutePath(private val path: String) {
override fun toString(): String {
return path
import java.nio.file.Path
/**
* AbsolutePath holds a pathname checked to be absolute.
* @constructor checks pathname and returns a new AbsolutePath if pathname is absolute.
*/
@Serializable(with = AbsolutePathSerializer::class)
data class AbsolutePath(val pathname: String, @Transient val path: Path = Path.of(pathname)) {
init {
if(!isAbsolute(pathname)) {
throw AbsolutePathException(pathname)
}
}
operator fun plus(other: String): AbsolutePath {
return AbsolutePath(pathname + other)
}
companion object {
fun isAbsolute(pathname: String): Boolean {
return Path.of(pathname).isAbsolute
}
}
}
interface FSType {
val type: String
object AbsolutePathSerializer : KSerializer<AbsolutePath> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(this::class.qualifiedName!!, PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: AbsolutePath) {
encoder.encodeString(value.pathname)
}
override fun deserialize(decoder: Decoder): AbsolutePath {
val path = decoder.decodeString()
return AbsolutePath(path)
}
}
data class ApplyState(val autoEtcPrefix: String)
/**
* AbsolutePathException is returned by @see AbsolutePath() and holds the invalid pathname.
*/
data class AbsolutePathException(val pathname: String) : IllegalArgumentException("Path $pathname is not absolute")
@Serializable sealed interface FilesystemConfig
@Serializable
@SerialName("bind")
data class FSBind(
val target: AbsolutePath,
val source: AbsolutePath,
val write: Boolean,
val device: Boolean,
val ensure: Boolean,
val optional: Boolean,
val special: Boolean,
override val type: String = "bind",
) : FSType
@SerialName("dst") val target: AbsolutePath? = null,
@SerialName("src") val source: AbsolutePath,
val write: Boolean? = null,
@SerialName("dev") val device: Boolean? = null,
val ensure: Boolean? = null,
val optional: Boolean? = null,
val special: Boolean? = null,
) : FilesystemConfig
@Serializable
@SerialName("ephemeral")
data class FSEphemeral(
val target: AbsolutePath,
@SerialName("dst") val target: AbsolutePath,
val write: Boolean,
val size: Int,
val size: Int? = null,
val perm: Int,
override val type: String = "ephemeral"
) : FSType
) : FilesystemConfig
@Serializable
@SerialName("link")
data class FSLink(
val target: AbsolutePath,
@SerialName("dst") val target: AbsolutePath,
val linkname: String,
val dereference: Boolean,
override val type: String = "link"
) : FSType
) : FilesystemConfig
@Serializable
@SerialName("overlay")
data class FSOverlay(
val target: AbsolutePath,
@SerialName("dst") val target: AbsolutePath,
val lower: List<AbsolutePath>,
val upper: AbsolutePath,
val work: AbsolutePath,
override val type: String = "overlay"
) : FSType
@Serializable
data class FilesystemConfig(
val config: FSType
)
) : FilesystemConfig

View File

@ -1,93 +1,91 @@
package moe.rosa.planterette.hakurei
import kotlinx.serialization.Serializable
import kotlinx.serialization.*
@Serializable
data class HakureiConfig(
val id: String,
val path: AbsolutePath,
val args: List<String>,
val enablements: Enablements,
val sessionBus: DBusConfig,
val systemBus: DBusConfig,
val directWayland: Boolean,
val username: String,
val shell: AbsolutePath,
val home: AbsolutePath,
val id: String? = null,
val path: AbsolutePath? = null,
val args: List<String>? = null,
val enablements: Enablements? = null,
@SerialName("session_bus") val sessionBus: DBusConfig? = null,
@SerialName("system_bus") val systemBus: DBusConfig? = null,
@SerialName("direct_wayland") val directWayland: Boolean? = null,
val username: String? = null,
val shell: AbsolutePath? = null,
val home: AbsolutePath? = null,
val extraPerms: ExtraPermsConfig,
val identity: Int,
val groups: List<String>,
@SerialName("extra_perms") val extraPerms: List<ExtraPermsConfig>? = null,
val identity: Int? = null,
val groups: List<String>? = null,
val container: ContainerConfig,
)
@Serializable
data class DBusConfig(
val see: List<String>,
val talk: List<String>,
val own: List<String>,
val call: Map<String, String>,
val broadcast: Map<String, String>,
val log: Boolean,
val filter: Boolean,
)
@Serializable
data class Enablements(
val wayland: Boolean,
val x11: Boolean,
val dbus: Boolean,
val pulse: Boolean,
val container: ContainerConfig? = null,
)
@Serializable
data class ContainerConfig(
val hostname: String,
val waitDelay: Long,
val seccompFlags: Int,
val seccompPresets: Int,
val seccompCompat: Boolean,
val devel: Boolean,
val userns: Boolean,
val hostNet: Boolean,
val hostAbstract: Boolean,
val tty: Boolean,
val multiarch: Boolean,
val hostname: String? = null,
@SerialName("wait_delay") val waitDelay: Long? = null,
@SerialName("seccomp_compat") val seccompCompat: Boolean? = null,
val devel: Boolean? = null,
val userns: Boolean? = null,
@SerialName("host_net") val hostNet: Boolean? = null,
@SerialName("host_abstract") val hostAbstract: Boolean? = null,
val tty: Boolean? = null,
val multiarch: Boolean? = null,
val env: Map<String, String>,
val env: Map<String, String>? = null,
val mapRealUid: Boolean,
val device: Boolean,
@SerialName("map_real_uid") val mapRealUid: Boolean? = null,
val device: Boolean? = null,
val filesystem: List<FilesystemConfig>,
val filesystem: List<FilesystemConfig>? = null,
)
@Serializable
data class ExtraPermsConfig(
val ensure: Boolean,
val ensure: Boolean? = null,
val path: AbsolutePath,
val read: Boolean,
val write: Boolean,
val execute: Boolean,
@SerialName("r") val read: Boolean? = null,
@SerialName("w") val write: Boolean? = null,
@SerialName("x") val execute: Boolean? = null,
) {
override fun toString(): String {
val buffer = StringBuffer(5 + path.toString().length)
buffer.append("---")
if(ensure) {
if(ensure == true) {
buffer.append("+")
}
buffer.append(":")
buffer.append(path.toString())
if(read) {
if(read == true) {
buffer.setCharAt(0, 'r')
}
if(write) {
if(write == true) {
buffer.setCharAt(1, 'w')
}
if(execute) {
if(execute == true) {
buffer.setCharAt(2, 'x')
}
return buffer.toString()
}
}
@Serializable
data class DBusConfig(
val see: List<String>? = null,
val talk: List<String>? = null,
val own: List<String>? = null,
val call: Map<String, String>? = null,
val broadcast: Map<String, String>? = null,
val log: Boolean? = null,
val filter: Boolean? = null,
)
@Serializable
data class Enablements(
val wayland: Boolean? = null,
val x11: Boolean? = null,
val dbus: Boolean? = null,
val pulse: Boolean? = null,
)

View File

@ -0,0 +1,193 @@
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.io.File
import kotlin.test.Test
import kotlin.test.assertEquals
import moe.rosa.planterette.hakurei.*
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertFails
import kotlin.test.assertFailsWith
import kotlin.test.assertIs
class HakureiTest {
companion object {
val CHROMIUM_TEMPLATE = HakureiConfig(
id = "org.chromium.Chromium",
path = AbsolutePath("/run/current-system/sw/bin/chromium"),
args = listOf(
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
),
enablements = Enablements(
wayland = true,
dbus = true,
pulse = true
),
sessionBus = DBusConfig(
see = null,
talk = listOf(
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
),
own = listOf(
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
),
call = mapOf(
"org.freedesktop.portal.*" to "*"
),
broadcast = mapOf(
"org.freedesktop.portal.*" to "@/org/freedesktop/portal/*"
),
filter = true
),
systemBus = DBusConfig(
see = null,
talk = listOf(
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
),
own = null,
call = null,
broadcast = null,
filter = true
),
username = "chronos",
shell = AbsolutePath("/run/current-system/sw/bin/zsh"),
home = AbsolutePath("/data/data/org.chromium.Chromium"),
extraPerms = listOf(
ExtraPermsConfig(
ensure = true,
path = AbsolutePath("/var/lib/hakurei/u0"),
read = null,
write = null,
execute = true,
),
ExtraPermsConfig(
ensure = null,
path = AbsolutePath("/var/lib/hakurei/u0/org.chromium.Chromium"),
read = true,
write = true,
execute = true,
),
),
identity = 9,
groups = listOf(
"video",
"dialout",
"plugdev"
),
container = ContainerConfig(
hostname = "localhost",
waitDelay = -1,
seccompCompat = true,
devel = true,
userns = true,
hostNet = true,
hostAbstract = true,
tty = true,
multiarch = true,
env = mapOf(
"GOOGLE_API_KEY" to "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID" to "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET" to "OTJgUOQcT7lO7GsGZq2G4IlT"
),
mapRealUid = true,
device = true,
filesystem = listOf(
FSBind(
target = AbsolutePath("/"),
source = AbsolutePath("/var/lib/hakurei/base/org.debian"),
write = true,
special = true,
),
FSBind(
target = AbsolutePath("/etc/"),
source = AbsolutePath("/etc/"),
special = true,
),
FSEphemeral(
target = AbsolutePath("/tmp/"),
write = true,
perm = 493
),
FSOverlay(
target = AbsolutePath("/nix/store"),
lower = listOf(
AbsolutePath("/mnt-root/nix/.ro-store")
),
upper = AbsolutePath("/mnt-root/nix/.rw-store/upper"),
work = AbsolutePath("/mnt-root/nix/.rw-store/work")
),
FSBind(
source = AbsolutePath("/nix/store")
),
FSLink(
target = AbsolutePath("/run/current-system"),
linkname = "/run/current-system",
dereference = true
),
FSLink(
target = AbsolutePath("/run/opengl-driver"),
linkname = "/run/opengl-driver",
dereference = true
),
FSBind(
target = AbsolutePath("/data/data/org.chromium.Chromium"),
source = AbsolutePath("/var/lib/hakurei/u0/org.chromium.Chromium"),
write = true,
ensure = true,
),
FSBind(
source = AbsolutePath("/dev/dri"),
device = true,
optional = true
)
)
)
)
val format = Json { prettyPrint = true }
}
@OptIn(ExperimentalSerializationApi::class)
@Test
fun deserializeTest() {
println(System.getProperty("user.dir"))
val want = format.decodeFromStream<HakureiConfig>(File("/home/mae/Documents/Projects/Rosa/planterette/src/test/resources/hakurei-chromium.json").inputStream())
assertEquals(CHROMIUM_TEMPLATE, want)
}
@OptIn(ExperimentalSerializationApi::class)
@Test
fun serializeTest() {
val encoded = format.encodeToString(CHROMIUM_TEMPLATE)
val decoded = format.decodeFromString<HakureiConfig>(encoded)
assertEquals(CHROMIUM_TEMPLATE, decoded)
}
@Test
fun absolutePathTest() {
assertDoesNotThrow {
AbsolutePath("/test/absolutepath")
}
assertFailsWith(AbsolutePathException::class) {
AbsolutePath("./../../../../")
}
assertEquals(AbsolutePath("/test/absolutepath"), AbsolutePath("/test/") + "absolutepath")
}
@Test
fun extraPermsTest() {
assertIs<String>(CHROMIUM_TEMPLATE.extraPerms.toString())
}
}

View File

@ -0,0 +1,146 @@
{
"id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": {
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": {
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"system_bus": {
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/hakurei/u0",
"x": true
},
{
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"groups": [
"video",
"dialout",
"plugdev"
],
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"device": true,
"filesystem": [
{
"type": "bind",
"dst": "/",
"src": "/var/lib/hakurei/base/org.debian",
"write": true,
"special": true
},
{
"type": "bind",
"dst": "/etc/",
"src": "/etc/",
"special": true
},
{
"type": "ephemeral",
"dst": "/tmp/",
"write": true,
"perm": 493
},
{
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
},
{
"type": "link",
"dst": "/run/current-system",
"linkname": "/run/current-system",
"dereference": true
},
{
"type": "link",
"dst": "/run/opengl-driver",
"linkname": "/run/opengl-driver",
"dereference": true
},
{
"type": "bind",
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true,
"ensure": true
},
{
"type": "bind",
"src": "/dev/dri",
"dev": true,
"optional": true
}
]
}
}