diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 6793009..49774bd 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -20,11 +20,12 @@ diff --git a/api/src/commonMain/kotlin/app/hakurei/planterette/api/HakureiConfig.kt b/api/src/commonMain/kotlin/app/hakurei/planterette/api/HakureiConfig.kt deleted file mode 100644 index 500efa0..0000000 --- a/api/src/commonMain/kotlin/app/hakurei/planterette/api/HakureiConfig.kt +++ /dev/null @@ -1,126 +0,0 @@ -package app.hakurei.planterette.api - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class HakureiConfig( - val id: String, - - val path: String? = null, - val args: List, - - val enablements: Byte, - - @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: String? = null, - val data: String, - val dir: String, - @SerialName("extra_perms") val extraPerms: List?, - - val identity: Int, - val groups: List, - - val container: ContainerConfig -) { - @Serializable - data class DbusConfig( - val see: List? = null, - val talk: List? = null, - val own: List? = null, - val call: Map? = null, - val broadcast: Map? = null, - val log: Boolean? = null, - val filter: Boolean? = null - ) { - } - - @Serializable - data class ExtraPermConfig( - val ensure: Boolean? = null, - val path: String, - @SerialName("r") val read: Boolean? = null, - @SerialName("w") val write: Boolean? = null, - @SerialName("x") val execute: Boolean? = null - ) - @Serializable - data class ContainerConfig( - val hostname: String? = null, - @SerialName("wait_delay") val delay: Int = -1, - @SerialName("seccomp_flags") val seccompFlags: Int, - @SerialName("seccomp_presets") val seccompPresets: Int, - @SerialName("seccomp_compat") val seccompCompat: Boolean? = null, - - val devel: Boolean? = null, - val userns: Boolean? = null, - val net: Boolean? = null, - val tty: Boolean? = null, - val multiarch: Boolean? = null, - - val env: Map, - @SerialName("map_real_uid") val mapRealUid: Boolean, - - val device: Boolean? = null, - val filesystem: List, - @SerialName("symlink") val link: List, - - val etc: String? = null, - @SerialName("auto_etc") val autoEtc: Boolean, - val cover: List - ) - - @Serializable - data class FilesystemConfig( - val dst: String? = null, - val src: String, - val write: Boolean? = null, - @SerialName("dev") val device: Boolean? = null, - @SerialName("require") val must: Boolean? = null - ) - - @Serializable - enum class Enablement(val value: Int) { - Wayland(1 shl 0), - X11(1 shl 1), - DBus(1 shl 2), - Pulse(1 shl 3); - - companion object { - fun enablements(vararg enablements: Enablement): Byte { - return enablements.sumOf(Enablement::value).toByte() - } - } - } - - @Serializable - enum class SeccompFilterPreset(val value: Int) { - Ext(1 shl 0), - DenyNS(1 shl 1), - DenyTTY(1 shl 2), - DenyDevel(1 shl 3), - Linux32(1 shl 4); - - companion object { - fun filterPresets(vararg filterPresets: SeccompFilterPreset): Int { - return filterPresets.sumOf(SeccompFilterPreset::value) - } - } - } - - @Serializable - enum class HakureiExportFlag(val value: Int) { - Multiarch(1 shl 0), - CAN(1 shl 1), - Bluetooth(1 shl 2); - - companion object { - fun exportFlags(vararg exportFlags: HakureiExportFlag): Int { - return exportFlags.sumOf(HakureiExportFlag::value) - } - } - } -} diff --git a/api/src/commonTest/kotlin/app/hakurei/planterette/api/HakureiConfigTest.kt b/api/src/commonTest/kotlin/app/hakurei/planterette/api/HakureiConfigTest.kt deleted file mode 100644 index f1158ca..0000000 --- a/api/src/commonTest/kotlin/app/hakurei/planterette/api/HakureiConfigTest.kt +++ /dev/null @@ -1,97 +0,0 @@ -package app.hakurei.planterette.api - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import kotlinx.serialization.serializer -import kotlin.test.Test -import kotlin.test.assertEquals - -class HakureiConfigTest { - @OptIn(ExperimentalSerializationApi::class) - val format = Json { - explicitNulls = false - prettyPrint = true - } - - @Test - fun testHakureiConfigSerialization() { - val testConfig = HakureiConfig( - id = "org.chromium.Chromium", - path = "/run/current-system/sw/bin/chromium", - args = listOf( - "chromium", - "--ignore-gpu-blocklist", - "--disable-smooth-scrolling", - "--enable-features=UseOzonePlatform", - "--ozone-platform=wayland" - ), - enablements = HakureiConfig.Enablement.enablements(HakureiConfig.Enablement.Wayland, HakureiConfig.Enablement.DBus, - HakureiConfig.Enablement.Pulse), - sessionBus = HakureiConfig.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 = HakureiConfig.DbusConfig( - talk = listOf("org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"), - filter = true - ), - - username = "chronos", - shell = "/run/current-system/sw/bin/zsh", - data = "/var/lib/hakurei/u0/org.chromium.Chromium", - dir = "/data/data/org.chromium.Chromium", - extraPerms = listOf( - HakureiConfig.ExtraPermConfig(path = "/var/lib/hakurei/u0", ensure = true, execute = true), - HakureiConfig.ExtraPermConfig(path = "/var/lib/hakurei/u0/org.chromium.Chromium", read = true, write = true, execute = true) - ), - identity = 9, - groups = listOf("video", "dialout", "plugdev"), - container = HakureiConfig.ContainerConfig( - hostname = "localhost", - devel = true, - userns = true, - net = true, - device = true, - seccompFlags = HakureiConfig.HakureiExportFlag.Multiarch.value, - seccompPresets = HakureiConfig.SeccompFilterPreset.Ext.value, - tty = true, - multiarch = true, - mapRealUid = true, - env = mapOf( - "GOOGLE_API_KEY" to "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", - "GOOGLE_DEFAULT_CLIENT_ID" to "77185425430.apps.googleusercontent.com", - "GOOGLE_DEFAULT_CLIENT_SECRET" to "OTJgUOQcT7lO7GsGZq2G4IlT", - ), - filesystem = listOf( - HakureiConfig.FilesystemConfig(src = "/nix/store"), - HakureiConfig.FilesystemConfig(src = "/run/current-system"), - HakureiConfig.FilesystemConfig(src = "/run/opengl-driver"), - HakureiConfig.FilesystemConfig(src = "/var/db/nix-channels"), - HakureiConfig.FilesystemConfig( - src = "/var/lib/hakurei/u0/org.chromium.Chromium", - dst = "/data/data/org.chromium.Chromium", - write = true, - must = true - ), - HakureiConfig.FilesystemConfig(src = "/dev/dri", device = true) - ), - link = listOf("/run/user/65534", "/run/user/150"), - etc = "/etc", - autoEtc = true, - cover = listOf("/var/run/nscd"), - delay = -1, - seccompCompat = true - ) - ) - val json = format.encodeToString(serializer(), testConfig) - val deserializeJson: HakureiConfig = format.decodeFromString(json) - val templateJson: HakureiConfig = format.decodeFromString(Util.readFileToString("/home/lilly/Documents/Projects/Rosa/planterette/api/src/commonTest/resources/template.json")) - assertEquals(templateJson, deserializeJson, "") - } -} \ No newline at end of file diff --git a/api/src/commonTest/resources/template.json b/api/src/commonTest/resources/template.json deleted file mode 100644 index 543b572..0000000 --- a/api/src/commonTest/resources/template.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "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": 13, - "session_bus": { - "see": null, - "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": { - "see": null, - "talk": [ - "org.bluez", - "org.freedesktop.Avahi", - "org.freedesktop.UPower" - ], - "own": null, - "call": null, - "broadcast": null, - "filter": true - }, - "username": "chronos", - "shell": "/run/current-system/sw/bin/zsh", - "data": "/var/lib/hakurei/u0/org.chromium.Chromium", - "dir": "/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_flags": 1, - "seccomp_presets": 1, - "seccomp_compat": true, - "devel": true, - "userns": true, - "net": 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": [ - { - "src": "/nix/store" - }, - { - "src": "/run/current-system" - }, - { - "src": "/run/opengl-driver" - }, - { - "src": "/var/db/nix-channels" - }, - { - "dst": "/data/data/org.chromium.Chromium", - "src": "/var/lib/hakurei/u0/org.chromium.Chromium", - "write": true, - "require": true - }, - { - "src": "/dev/dri", - "dev": true - } - ], - "symlink": [ - "/run/user/65534", - "/run/user/150" - ], - "etc": "/etc", - "auto_etc": true, - "cover": [ - "/var/run/nscd" - ] - } -} diff --git a/api/src/jvmMain/kotlin/app/hakurei/planterette/api/dsl/HakureiDSL.kt b/api/src/jvmMain/kotlin/app/hakurei/planterette/api/dsl/HakureiDSL.kt deleted file mode 100644 index 8ecaee7..0000000 --- a/api/src/jvmMain/kotlin/app/hakurei/planterette/api/dsl/HakureiDSL.kt +++ /dev/null @@ -1,2 +0,0 @@ -package app.hakurei.planterette.api.dsl - diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts deleted file mode 100644 index 840fe91..0000000 --- a/cli/build.gradle.kts +++ /dev/null @@ -1,19 +0,0 @@ -plugins { - kotlin("jvm") -} - -repositories { - mavenCentral() -} - -dependencies { - implementation(project(":api")) - testImplementation(kotlin("test")) -} - -tasks.test { - useJUnitPlatform() -} -kotlin { - jvmToolchain(21) -} \ No newline at end of file diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts deleted file mode 100644 index 8eca01e..0000000 --- a/daemon/build.gradle.kts +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id("buildsrc.convention.kotlin-jvm") - kotlin("jvm") - application -} - -dependencies { - implementation(project(":api")) -} - -application { - mainClass = "app.hakurei.planterette.PlanteretteKt" -} diff --git a/daemon/src/main/kotlin/app/hakurei/planterette/Planterette.kt b/daemon/src/main/kotlin/app/hakurei/planterette/Planterette.kt deleted file mode 100644 index 735a8cd..0000000 --- a/daemon/src/main/kotlin/app/hakurei/planterette/Planterette.kt +++ /dev/null @@ -1,5 +0,0 @@ -package app.hakurei.planterette - -fun main() { - -} \ No newline at end of file diff --git a/daemon/src/test/resources/debian.json b/daemon/src/test/resources/debian.json deleted file mode 100644 index 2f7684e..0000000 --- a/daemon/src/test/resources/debian.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "id": "org.debian", - "path": "/bin/bash", - "args": [ - "bash" - ], - "enablements": 13, - "session_bus": { - "see": null, - "talk": [ - "org.freedesktop.Notifications", - "org.freedesktop.FileManager1", - "org.freedesktop.ScreenSaver" - ], - "own": null, - "call": null, - "broadcast": null, - "filter": true - }, - "system_bus": { - "see": null, - "talk": null, - "own": null, - "call": null, - "broadcast": null, - "filter": true - }, - "username": "chronos", - "shell": "/bin/bash", - "data": "/tmp/chronos", - "dir": "/.hakurei/home", - "identity": 9, - "groups": null, - "container": { - "hostname": "debian", - "seccomp_flags": 0, - "seccomp_presets": 0, - "devel": true, - "userns": true, - "net": true, - "tty": true, - "multiarch": true, - "env": null, - "map_real_uid": true, - "filesystem": [ - { - "dst": "/boot", - "src": "/mnt/debian/boot" - }, - { - "dst": "/home", - "src": "/mnt/debian/home" - }, - { - "dst": "/media", - "src": "/mnt/debian/media" - }, - { - "dst": "/mnt", - "src": "/mnt/debian/mnt" - }, - { - "dst": "/opt", - "src": "/mnt/debian/opt" - }, - { - "dst": "/root", - "src": "/mnt/debian/root" - }, - { - "dst": "/srv", - "src": "/mnt/debian/srv" - }, - { - "dst": "/usr", - "src": "/mnt/debian/usr" - }, - { - "dst": "/var", - "src": "/mnt/debian/var" - }, - { - "src": "/sys/block" - }, - { - "src": "/sys/bus" - }, - { - "src": "/sys/class" - }, - { - "src": "/sys/dev" - }, - { - "src": "/sys/devices" - }, - { - "src": "/dev/dri", - "dev": true - } - ], - "symlink": [ - [ - "usr/bin", - "/bin" - ], - [ - "usr/lib", - "/lib" - ], - [ - "usr/lib64", - "/lib64" - ], - [ - "usr/sbin", - "/sbin" - ] - ], - "etc": "/mnt/debian/etc", - "auto_etc": false, - "cover": null - } -} diff --git a/gui/build.gradle.kts b/gui/build.gradle.kts deleted file mode 100644 index ae280b1..0000000 --- a/gui/build.gradle.kts +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - kotlin("jvm") -} - -repositories { - mavenCentral() -} - -dependencies { - implementation(project(":api")) - implementation(project(":cli")) - testImplementation(kotlin("test")) -} - -tasks.test { - useJUnitPlatform() -} -kotlin { - jvmToolchain(21) -} \ No newline at end of file diff --git a/api/build.gradle.kts b/libplt/build.gradle.kts similarity index 83% rename from api/build.gradle.kts rename to libplt/build.gradle.kts index 096d052..bb4ddb7 100644 --- a/api/build.gradle.kts +++ b/libplt/build.gradle.kts @@ -11,7 +11,7 @@ kotlin { } nativeTarget.binaries { sharedLib { - baseName = "planterette" + baseName = "libplt" } } sourceSets { @@ -22,11 +22,5 @@ kotlin { implementation(libs.bundles.kotlinxEcosystem) implementation(kotlin("test")) } - jvmMain.dependencies { - - } - jvmTest.dependencies { - - } } } diff --git a/libplt/src/commonMain/kotlin/app/hakurei/planterette/api/AbsolutePath.kt b/libplt/src/commonMain/kotlin/app/hakurei/planterette/api/AbsolutePath.kt new file mode 100644 index 0000000..f787b76 --- /dev/null +++ b/libplt/src/commonMain/kotlin/app/hakurei/planterette/api/AbsolutePath.kt @@ -0,0 +1,51 @@ +package app.hakurei.planterette.api + +import app.hakurei.planterette.api.AbsolutePath.Companion.isAbsolute +import kotlinx.io.files.Path +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.Transient +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +/** + * 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(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(pathname).isAbsolute + } + } +} + +object AbsolutePathSerializer : KSerializer { + 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) + } +} + +/** + * AbsolutePathException is returned by @see AbsolutePath() and holds the invalid pathname. + */ +data class AbsolutePathException(val pathname: String) : IllegalArgumentException("Path $pathname is not absolute") \ No newline at end of file diff --git a/libplt/src/commonMain/kotlin/app/hakurei/planterette/api/HakureiConfig.kt b/libplt/src/commonMain/kotlin/app/hakurei/planterette/api/HakureiConfig.kt new file mode 100644 index 0000000..81c2493 --- /dev/null +++ b/libplt/src/commonMain/kotlin/app/hakurei/planterette/api/HakureiConfig.kt @@ -0,0 +1,209 @@ +package app.hakurei.planterette.api + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * HakureiConfig is used to seal an app implementation. + * @param id reverse-DNS style arbitrary identifier string from config; Passed to wayland security-context-v1 as application ID and used as part of defaults in dbus session proxy + * + * @param path absolute path to executable file + * @param args final args passed to container init + * + * @param enablements system services to make available in the container + * + * @param sessionBus session D-Bus proxy configuration; null makes session bus proxy assume built-in defaults + * @param systemBus system D-Bus proxy configuration; null disables system bus proxy + * @param directWayland direct access to the wayland socket; when this gets set no attempt is made to attach security-context-v1 and the bare socket is mounted to the sandbox + * + * @param username passwd username in container, defaults to passwd name of target uid or chronos + * @param shell absolute path to shell + * @param data absolute path to home directory in the init mount namespace + * @param dir directory to ender and use as home in the container mount namespace, null for Data + * @param extraPerms extra ACL ops, dispatches before container init + * + * @param identity numerical application id, used for init user namespace credentials + * @param groups list of supplementary groups inherited by container processes + * + * @param container abstract container configuration baseline + */ +@Serializable +data class HakureiConfig( + val id: String, + + val path: AbsolutePath? = null, + val args: List, + + val enablements: Byte, + + @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, + val data: AbsolutePath, + val dir: AbsolutePath, + @SerialName("extra_perms") val extraPerms: List? = null, + + val identity: Int, + val groups: List, + + val container: ContainerConfig +) { + @Serializable + data class DbusConfig( + val see: List? = null, + val talk: List? = null, + val own: List? = null, + val call: Map? = null, + val broadcast: Map? = null, + + val log: Boolean? = null, + val filter: Boolean + ) + + /** + * ExtraPermConfig describes an ACL update op. + */ + @Serializable + data class ExtraPermConfig( + val ensure: Boolean? = null, + val path: AbsolutePath, + @SerialName("r") val read: Boolean? = null, + @SerialName("w") val write: Boolean? = null, + @SerialName("x") val execute: Boolean? = null + ) + + /** + * ContainerConfig describes the container configuration baseline to which the app implementation adds upon. + * @param hostname container hostname + * @param waitDelay duration to wait for after interrupting a container's initial process in nanoseconds; a negative value causes the container to be terminated immediately on cancellation + * @param seccompFlags extra seccomp flags + * @param seccompPresets extra seccomp presets + * @param seccompCompat disable project-specific filter extensions + * @param devel allow ptrace and friends + * @param userns allow userns creation in container + * @param net share host net namespace + * @param tty allow dangerous terminal I/O + * @param multiarch allow multiarch + * + * @param env initial process environment variables + * @param mapRealUid map target user uid to privileged user uid in the user namespace + * + * @param device pass through all devices + * @param filesystem container host filesystem bind mounts + * @param link create symlinks inside container filesystem + * + * @param autoRoot automatically bind mount top-level directories to container root; the zero value disables this behaviour + * @param rootFlags extra flags for AutoRoot + * + * @param etc read-only /etc directory + * @param autoEtc automatically set up /etc symlinks + */ + @Serializable + data class ContainerConfig( + val hostname: String?, + @SerialName("wait_delay") val waitDelay: Int? = null, + @SerialName("seccomp_flags") val seccompFlags: Int, + @SerialName("seccomp_presets") val seccompPresets: Int, + @SerialName("seccomp_compat") val seccompCompat: Boolean? = null, + + val devel: Boolean? = null, + val userns: Boolean? = null, + val net: Boolean? = null, + val tty: Boolean? = null, + val multiarch: Boolean? = null, + + val env: Map, + @SerialName("map_real_uid") val mapRealUid: Boolean, + + val device: Boolean?, + val filesystem: List, + @SerialName("symlink") val link: List, + + @SerialName("auto_root") val autoRoot: AbsolutePath, + @SerialName("root_flags") val rootFlags: Int, + + val etc: AbsolutePath?, + + @SerialName("auto_etc") val autoEtc: Boolean, + ) + + /** + * FilesystemConfig is an abstract representation of a bind mount. + * @param dst mount point in container, same as src if empty + * @param src host filesystem path to make available to the container + * @param write do not mount filesystem read-only + * @param device do not disable device files + * @param must fail if the bind mount cannot be established for any reason + */ + @Serializable + data class FilesystemConfig( + val dst: AbsolutePath? = null, + val src: AbsolutePath, + val write: Boolean? = null, + @SerialName("dev") val device: Boolean? = null, + @SerialName("require") val must: Boolean? = null + ) + + /** + * @param target symlink target in container + * @param linkname linkname the symlink points to; prepend '*' to dereference an absolute pathname on host + */ + @Serializable + data class LinkConfig( + val target: AbsolutePath, + val linkname: String + ) + enum class Enablement(val value: Int) { + Wayland(1 shl 0), + X11(1 shl 1), + DBus(1 shl 2), + Pulse(1 shl 3); + + companion object { + fun enablements(vararg enablements: Enablement): Byte { + return enablements.orOf(Enablement::value).toByte() + } + } + } + + enum class SeccompFilterPreset(val value: Int) { + Ext(1 shl 0), + DenyNS(1 shl 1), + DenyTTY(1 shl 2), + DenyDevel(1 shl 3), + Linux32(1 shl 4); + + companion object { + fun filterPresets(vararg filterPresets: SeccompFilterPreset): Int { + return filterPresets.orOf(SeccompFilterPreset::value) + } + } + } + + enum class HakureiExportFlag(val value: Int) { + Multiarch(1 shl 0), + CAN(1 shl 1), + Bluetooth(1 shl 2); + + companion object { + fun exportFlags(vararg exportFlags: HakureiExportFlag): Int { + return exportFlags.orOf(HakureiExportFlag::value) + } + } + } + + enum class RootFlag(val value: Int) { + Optional(1 shl 0), + Writable(1 shl 1), + Device(1 shl 2); + + companion object { + fun rootFlags(vararg rootFlags: RootFlag): Int { + return rootFlags.orOf(RootFlag::value) + } + } + } +} diff --git a/api/src/commonMain/kotlin/app/hakurei/planterette/api/PackageManifest.kt b/libplt/src/commonMain/kotlin/app/hakurei/planterette/api/PackageManifest.kt similarity index 57% rename from api/src/commonMain/kotlin/app/hakurei/planterette/api/PackageManifest.kt rename to libplt/src/commonMain/kotlin/app/hakurei/planterette/api/PackageManifest.kt index ddf4b1b..bdb2d42 100644 --- a/api/src/commonMain/kotlin/app/hakurei/planterette/api/PackageManifest.kt +++ b/libplt/src/commonMain/kotlin/app/hakurei/planterette/api/PackageManifest.kt @@ -4,13 +4,13 @@ import kotlinx.serialization.Serializable @Serializable data class PackageManifest( - val hakureiConfig: HakureiConfig, - val id: String, - val version: Version, - val name: String, - val description: String, - val architecture: List, - val baseImage: BaseImage + var hakureiConfig: HakureiConfig, + var id: String, + var version: Version, + var name: String, + var description: String, + var architecture: MutableList, + var baseImage: BaseImage ) { @Serializable data class Version(val version: String, val canonicalVersion: UInt) @@ -21,10 +21,12 @@ data class PackageManifest( ARM64, } @Serializable - enum class BaseImage { - DEBIAN, - CHIMERA, - + data class BaseImage(val type: Type) { + @Serializable + enum class Type { + DEBIAN, + CHIMERA, + } } companion object { val fileList = listOf("planterette.json", "icon.png", "image.tar") diff --git a/libplt/src/commonMain/kotlin/app/hakurei/planterette/api/Paths.kt b/libplt/src/commonMain/kotlin/app/hakurei/planterette/api/Paths.kt new file mode 100644 index 0000000..ee6dad8 --- /dev/null +++ b/libplt/src/commonMain/kotlin/app/hakurei/planterette/api/Paths.kt @@ -0,0 +1,47 @@ +package app.hakurei.planterette.api + +object Paths { + const val FHSRoot = "/" + const val FHSEtc = "/etc/" + const val FHSTmp = "/tmp/" + + const val FHSRun = "/run/" + const val FHSRunUser = FHSRun + "user/" + + const val FHSUsr = "/usr/" + const val FHSUsrBin = FHSUsr + "bin/" + + const val FHSVar = "/var/" + const val FHSVarLib = FHSVar + "lib/" + const val FHSVarEmpty = FHSVar + "empty/" + + const val FHSDev = "/dev/" + const val FHSProc = "/proc/" + const val FHSProcSys = FHSProc + "sys/" + const val FHSSys = "/sys/" + + const val Nonexistent = FHSProc + "nonexistent" + const val sysrootDir = "sysroot" + const val hostDir = "host" + const val hostPath = FHSRoot + hostDir + const val sysrootPath = FHSRoot + sysrootDir + + val AbsFHSRoot = AbsolutePath(FHSRoot) + val AbsFHSEtc = AbsolutePath(FHSEtc) + val AbsFHSTmp = AbsolutePath(FHSTmp) + + val AbsFHSRun = AbsolutePath(FHSRun) + val AbsFHSRunUser = AbsolutePath(FHSRunUser) + + val AbsFHSUsrBin = AbsolutePath(FHSUsrBin) + + val AbsFHSVar = AbsolutePath(FHSVar) + val AbsFHSVarLib = AbsolutePath(FHSVarLib) + + val AbsFHSDev = AbsolutePath(FHSDev) + val AbsFHSProc = AbsolutePath(FHSProc) + val AbsFHSSys = AbsolutePath(FHSSys) + + val AbsNonexistent = AbsolutePath(Nonexistent) +} + diff --git a/api/src/commonMain/kotlin/app/hakurei/planterette/api/Util.kt b/libplt/src/commonMain/kotlin/app/hakurei/planterette/api/Util.kt similarity index 71% rename from api/src/commonMain/kotlin/app/hakurei/planterette/api/Util.kt rename to libplt/src/commonMain/kotlin/app/hakurei/planterette/api/Util.kt index cfd3ad3..b5c7bd5 100644 --- a/api/src/commonMain/kotlin/app/hakurei/planterette/api/Util.kt +++ b/libplt/src/commonMain/kotlin/app/hakurei/planterette/api/Util.kt @@ -13,4 +13,12 @@ object Util { source.buffered().transferTo(buffer) return buffer.readString() } +} + +inline fun Array.orOf(selector: (T) -> Int): Int { + var sum = 0 + for (element in this) { + sum = sum or selector(element) + } + return sum } \ No newline at end of file diff --git a/libplt/src/commonTest/kotlin/app/hakurei/planterette/api/HakureiConfigTest.kt b/libplt/src/commonTest/kotlin/app/hakurei/planterette/api/HakureiConfigTest.kt new file mode 100644 index 0000000..d5264ed --- /dev/null +++ b/libplt/src/commonTest/kotlin/app/hakurei/planterette/api/HakureiConfigTest.kt @@ -0,0 +1,230 @@ +package app.hakurei.planterette.api + +import app.hakurei.planterette.api.HakureiConfig.Enablement.* +import app.hakurei.planterette.api.HakureiConfig.RootFlag +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer +import kotlin.test.Test +import kotlin.test.assertEquals + +class HakureiConfigTest { + @OptIn(ExperimentalSerializationApi::class) + val format = Json { + explicitNulls = false + prettyPrint = true + } + val want: String = """ + { + "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": 13, + "session_bus": { + "see": null, + "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": { + "see": null, + "talk": [ + "org.bluez", + "org.freedesktop.Avahi", + "org.freedesktop.UPower" + ], + "own": null, + "call": null, + "broadcast": null, + "filter": true + }, + "username": "chronos", + "shell": "/run/current-system/sw/bin/zsh", + "data": "/var/lib/hakurei/u0/org.chromium.Chromium", + "dir": "/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_flags": 1, + "seccomp_presets": 1, + "seccomp_compat": true, + "devel": true, + "userns": true, + "net": 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": [ + { + "dst": "/tmp/", + "src": "/proc/nonexistent", + "write": true + }, + { + "src": "/nix/store" + }, + { + "src": "/run/current-system" + }, + { + "src": "/run/opengl-driver" + }, + { + "src": "/var/db/nix-channels" + }, + { + "dst": "/data/data/org.chromium.Chromium", + "src": "/var/lib/hakurei/u0/org.chromium.Chromium", + "write": true, + "require": true + }, + { + "src": "/dev/dri", + "dev": true + } + ], + "symlink": [ + { + "target": "/run/user/65534", + "linkname": "/run/user/150" + } + ], + "auto_root": "/var/lib/hakurei/base/org.debian", + "root_flags": 2, + "etc": "/etc/", + "auto_etc": true + } + } + """.trimIndent() + @Test + fun testHakureiConfigSerialization() { + val testConfig = HakureiConfig( + id = "org.chromium.Chromium", + path = Paths.AbsFHSRun + "current-system/sw/bin/chromium", + args = listOf( + "chromium", + "--ignore-gpu-blocklist", + "--disable-smooth-scrolling", + "--enable-features=UseOzonePlatform", + "--ozone-platform=wayland", + ), + enablements = HakureiConfig.Enablement.enablements(Wayland, DBus, Pulse), + sessionBus = HakureiConfig.DbusConfig( + see = null, + talk = listOf("org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver"), + 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/*"), + log = false, + filter = true, + ), + systemBus = HakureiConfig.DbusConfig( + see = null, + talk = listOf("org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"), + own = null, + call = null, + broadcast = null, + log = false, + filter = true, + ), + directWayland = false, + + username = "chronos", + shell = Paths.AbsFHSRun + "current-system/sw/bin/zsh", + data = Paths.AbsFHSVarLib + "hakurei/u0/org.chromium.Chromium", + dir = AbsolutePath("/data/data/org.chromium.Chromium"), + extraPerms = listOf( + HakureiConfig.ExtraPermConfig(path = Paths.AbsFHSVarLib + "hakurei/u0", ensure = true, execute = true), + HakureiConfig.ExtraPermConfig(path = Paths.AbsFHSVarLib + "hakurei/u0/org.chromium.Chromium", read = true, write = true, execute = true) + ), + identity = 9, + groups = listOf("video", "dialout", "plugdev"), + container = HakureiConfig.ContainerConfig( + hostname = "localhost", + devel = true, + userns = true, + net = true, + device = true, + waitDelay = -1, + seccompFlags = HakureiConfig.HakureiExportFlag.exportFlags(HakureiConfig.HakureiExportFlag.Multiarch), + seccompPresets = HakureiConfig.SeccompFilterPreset.filterPresets(HakureiConfig.SeccompFilterPreset.Ext), + seccompCompat = true, + tty = true, + multiarch = true, + mapRealUid = true, + env = mapOf( + "GOOGLE_API_KEY" to "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", + "GOOGLE_DEFAULT_CLIENT_ID" to "77185425430.apps.googleusercontent.com", + "GOOGLE_DEFAULT_CLIENT_SECRET" to "OTJgUOQcT7lO7GsGZq2G4IlT" + ), + filesystem = listOf( + HakureiConfig.FilesystemConfig(dst = Paths.AbsFHSTmp, src = Paths.AbsNonexistent, write = true), + HakureiConfig.FilesystemConfig(src = AbsolutePath("/nix/store")), + HakureiConfig.FilesystemConfig(src = Paths.AbsFHSRun + "current-system"), + HakureiConfig.FilesystemConfig(src = Paths.AbsFHSRun + "opengl-driver"), + HakureiConfig.FilesystemConfig(src = Paths.AbsFHSVar + "db/nix-channels"), + HakureiConfig.FilesystemConfig(src = Paths.AbsFHSVarLib + "hakurei/u0/org.chromium.Chromium", dst = AbsolutePath("/data/data/org.chromium.Chromium"), write = true, must = true), + HakureiConfig.FilesystemConfig(src = Paths.AbsFHSDev + "dri", device = true) + ), + link = listOf(HakureiConfig.LinkConfig(Paths.AbsFHSRunUser + "65534", Paths.FHSRunUser + "150")), + autoRoot = Paths.AbsFHSVarLib + "hakurei/base/org.debian", + rootFlags = RootFlag.rootFlags(RootFlag.Writable), + etc = Paths.AbsFHSEtc, + autoEtc = true + ) + ) + println("printing for now until a more proper test can be written") + println(format.encodeToString(testConfig)) + } +} \ No newline at end of file diff --git a/plt-build/build.gradle.kts b/plt-build/build.gradle.kts new file mode 100644 index 0000000..1442968 --- /dev/null +++ b/plt-build/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + kotlin("multiplatform") +} +kotlin { + jvm() + val nativeTarget = if(System.getProperty("os.arch") == "aarch64") { + linuxArm64("native") + } else { + linuxX64("native") + } + nativeTarget.binaries { + executable() + } + sourceSets { + nativeMain.dependencies { + implementation(project(":libplt")) + } + nativeTest.dependencies { + implementation(project(":libplt")) + implementation(kotlin("test")) + } + jvmMain.dependencies { + implementation(project(":libplt")) + } + jvmTest.dependencies { + implementation(project(":libplt")) + implementation(kotlin("test")) + } + } +} \ No newline at end of file diff --git a/plt-build/src/jvmMain/kotlin/app/hakurei/planterette/api/dsl/Task.kt b/plt-build/src/jvmMain/kotlin/app/hakurei/planterette/api/dsl/Task.kt new file mode 100644 index 0000000..ea8a76f --- /dev/null +++ b/plt-build/src/jvmMain/kotlin/app/hakurei/planterette/api/dsl/Task.kt @@ -0,0 +1,16 @@ +package app.hakurei.planterette.api + +interface Task { + val execute: String +} +class AptInstallTask(val packages: List) : Task { + private fun getPackageList(): String { + var string = "" + packages.forEach { p -> + string += "$p " + } + return string + } + override val execute: String + get() = "sudo apt install ${getPackageList()}" +} \ No newline at end of file diff --git a/plt-fetch/build.gradle.kts b/plt-fetch/build.gradle.kts new file mode 100644 index 0000000..b641d4f --- /dev/null +++ b/plt-fetch/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + kotlin("multiplatform") +} +kotlin { + val nativeTarget = if(System.getProperty("os.arch") == "aarch64") { + linuxArm64("native") + } else { + linuxX64("native") + } + nativeTarget.binaries { + executable() + } + sourceSets { + nativeMain.dependencies { + implementation(project(":libplt")) + } + nativeTest.dependencies { + implementation(project(":libplt")) + implementation(kotlin("test")) + } + } +} \ No newline at end of file diff --git a/plt-pkg/build.gradle.kts b/plt-pkg/build.gradle.kts new file mode 100644 index 0000000..b641d4f --- /dev/null +++ b/plt-pkg/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + kotlin("multiplatform") +} +kotlin { + val nativeTarget = if(System.getProperty("os.arch") == "aarch64") { + linuxArm64("native") + } else { + linuxX64("native") + } + nativeTarget.binaries { + executable() + } + sourceSets { + nativeMain.dependencies { + implementation(project(":libplt")) + } + nativeTest.dependencies { + implementation(project(":libplt")) + implementation(kotlin("test")) + } + } +} \ No newline at end of file diff --git a/plt/build.gradle.kts b/plt/build.gradle.kts new file mode 100644 index 0000000..8cd057f --- /dev/null +++ b/plt/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + kotlin("multiplatform") +} +kotlin { + val nativeTarget = if(System.getProperty("os.arch") == "aarch64") { + linuxArm64("native") + } else { + linuxX64("native") + } + nativeTarget.binaries { + executable() + } + sourceSets { + nativeMain.dependencies { + implementation(project(":libplt")) + } + nativeTest.dependencies { + implementation(project(":libplt")) + implementation(kotlin("test")) + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 2ba66ff..edcb46b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,9 +9,10 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } -include(":daemon") -include(":api") -include(":cli") -include(":gui") +rootProject.name = "planterette" -rootProject.name = "planterette" \ No newline at end of file +include("plt") +include("libplt") +include("plt-build") +include("plt-fetch") +include("plt-pkg") \ No newline at end of file