Skip to content

Instantly share code, notes, and snippets.

@jaredrummler
Created February 9, 2025 16:49
Show Gist options
  • Save jaredrummler/da1a8e1213e7cbb1b0b97d006676370e to your computer and use it in GitHub Desktop.
Save jaredrummler/da1a8e1213e7cbb1b0b97d006676370e to your computer and use it in GitHub Desktop.
/*
* Copyright 2024 GoatBytes.IO
*
* 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 io.goatbytes.unix
data class FilePermissions(
val userPermission: PermissionGroup.User,
val groupPermission: PermissionGroup.Group,
val otherPermission: PermissionGroup.Other,
override val setUID: Boolean,
override val setGID: Boolean,
override val sticky: Boolean,
) : SpecialPermission {
companion object {
// file mode:
const val S_IRGRP = 32 // read permission, group
const val S_IROTH = 4 // read permission, others
const val S_IRUSR = 256 // read permission, owner
const val S_IRWXG = 56 // read, write, execute/search by group
const val S_IRWXO = 7 // read, write, execute/search by others
const val S_IRWXU = 448 // read, write, execute/search by owner
const val S_ISGID = 1024 // set-group-ID on execution
const val S_ISUID = 2048 // set-user-ID on execution
const val S_ISVTX = 512 // on directories, restricted deletion flag
const val S_IWGRP = 16 // write permission, group
const val S_IWOTH = 2 // write permission, others
const val S_IWUSR = 128 // write permission, owner
const val S_IXGRP = 8 // execute/search permission, group
const val S_IXOTH = 1 // execute/search permission, others
const val S_IXUSR = 64 // execute/search permission, owner
// file type bits:
const val S_IFMT = 61440 // type of file
const val S_IFBLK = 24576 // block special
const val S_IFCHR = 8192 // character special
const val S_IFDIR = 16384 // directory
const val S_IFIFO = 4096 // FIFO special
const val S_IFLNK = 40960 // symbolic link
const val S_IFREG = 32768 // regular
const val S_IFSOCK = 49152 // socket special
const val S_IFWHT = 57344 // whiteout special
// file types:
const val TYPE_BLOCK_SPECIAL = 'b'
const val TYPE_CHARACTER_SPECIAL = 'c'
const val TYPE_DIRECTORY = 'd'
const val TYPE_FIFO = 'p'
const val TYPE_SYMBOLIC_LINK = 'l'
const val TYPE_REGULAR = '-'
const val TYPE_SOCKET = 's'
const val TYPE_WHITEOUT = 'w'
const val TYPE_UNKNOWN = '?'
private const val REGEX_NUMERIC_NOTATION = "^[0-7]{3,4}\$"
private const val REGEX_SYMBOLIC_NOTATION = "^(?<type>[bcdpl\\-sw?])?" +
"(?<owner>[Ssr\\-][Ssw\\-][Ttx\\-])" +
"(?<group>[Ssr\\-][Ssw\\-][Ttx\\-])" +
"(?<other>[Ssr\\-][Ssw\\-][Ttx\\-])$"
private val regexSymbolicNotation by lazy { REGEX_SYMBOLIC_NOTATION.toRegex() }
private val regexNumericNotation by lazy { REGEX_NUMERIC_NOTATION.toRegex() }
fun fromStMode(mode: Int): FilePermissions {
return fromSymbolicNotation(toSymbolicNotation(mode))
}
infix fun ofMode(mode: String): FilePermissions {
require(regexNumericNotation.matches(mode)) {
"Invalid permissions. Expected permission mode, got '$mode'."
}
return fromSymbolicNotation(convertNumericToSymbolicNotation(mode))
}
fun fromSymbolicNotation(permissions: String): FilePermissions {
requireNotNull(regexSymbolicNotation.matchEntire(permissions)) {
"Invalid permissions. Expected symbolic permission notation, got '$permissions'"
}.let { match ->
operator fun MatchResult.get(name: String) = groups[name]!!.value
Triple(match["owner"], match["group"], match["other"])
}.let { (owner, group, other) ->
return FilePermissions(
userPermission = PermissionGroup.User(
canRead = owner[0] != '-',
canWrite = owner[1] != '-',
canExecute = owner[2] != '-'
),
groupPermission = PermissionGroup.Group(
canRead = group[0] != '-',
canWrite = group[1] != '-',
canExecute = group[2] != '-'
),
otherPermission = PermissionGroup.Other(
canRead = other[0] != '-',
canWrite = other[1] != '-',
canExecute = other[2] != '-'
),
setUID = owner[2] == 's' || owner[2] == 'S',
setGID = group[2] == 's' || group[2] == 'S',
sticky = other[2] == 't' || other[2] == 'T'
)
}
}
/**
* Converts the file permissions mode to the numeric notation.
*
* @param st_mode
* Mode (permissions) of file. Corresponds to C's `struct stat` from `<stat.h>`.
* See [android.system.StructStat.st_mode]
* @return The permission represented as a numeric notation.
*/
fun toNumericNotation(st_mode: Int): String {
var i = 0
/* owner */
i += if (((st_mode and S_IRUSR) != 0)) S_IRUSR else 0
i += if (((st_mode and S_IWUSR) != 0)) S_IWUSR else 0
when (st_mode and (S_IXUSR or S_ISUID)) {
S_IXUSR -> i += S_IXUSR
S_ISUID -> i += S_ISUID
S_IXUSR or S_ISUID -> i += S_IXUSR + S_ISUID
}
/* group */
i += if (((st_mode and S_IRGRP) != 0)) S_IRGRP else 0
i += if (((st_mode and S_IWGRP) != 0)) S_IWGRP else 0
when (st_mode and (S_IXGRP or S_ISGID)) {
S_IXGRP -> i += S_IXGRP
S_ISGID -> i += S_ISGID
S_IXGRP or S_ISGID -> i += S_IXGRP + S_ISGID
}
/* other */
i += if ((st_mode and S_IROTH) != 0) S_IROTH else 0
i += if ((st_mode and S_IWOTH) != 0) S_IWOTH else 0
when (st_mode and (S_IXOTH or S_ISVTX)) {
S_IXOTH -> i += S_IXOTH
S_ISVTX -> i += S_ISVTX
S_IXOTH or S_ISVTX -> i += S_IXOTH + S_ISVTX
}
return i.toUInt().toString(3) // TODO("Integer.toOctalString(i)")
}
/**
* Converts the file permissions mode to the symbolic notation.
*
* @param st_mode
* Mode (permissions) of file. Corresponds to C's `struct stat` from `<stat.h>`.
* See [android.system.StructStat.st_mode]
* @return The permission represented as a symbolic notation.
*/
fun toSymbolicNotation(st_mode: Int): String {
var p = ""
p += when (st_mode and S_IFMT) {
S_IFDIR -> TYPE_DIRECTORY
S_IFCHR -> TYPE_CHARACTER_SPECIAL
S_IFBLK -> TYPE_BLOCK_SPECIAL
S_IFREG -> TYPE_REGULAR
S_IFLNK -> TYPE_SYMBOLIC_LINK
S_IFSOCK -> TYPE_SOCKET
S_IFIFO -> TYPE_FIFO
S_IFWHT -> TYPE_WHITEOUT
else -> TYPE_UNKNOWN
}
/* owner */
p += if (((st_mode and S_IRUSR) != 0)) 'r' else '-'
p += if (((st_mode and S_IWUSR) != 0)) 'w' else '-'
when (st_mode and (S_IXUSR or S_ISUID)) {
0 -> p += '-'
S_IXUSR -> p += 'x'
S_ISUID -> p += 'S'
S_IXUSR or S_ISUID -> p += 's'
}
/* group */
p += if (((st_mode and S_IRGRP) != 0)) 'r' else '-'
p += if (((st_mode and S_IWGRP) != 0)) 'w' else '-'
when (st_mode and (S_IXGRP or S_ISGID)) {
0 -> p += '-'
S_IXGRP -> p += 'x'
S_ISGID -> p += 'S'
S_IXGRP or S_ISGID -> p += 's'
}
/* other */
p += if ((st_mode and S_IROTH) != 0) 'r' else '-'
p += if ((st_mode and S_IWOTH) != 0) 'w' else '-'
when (st_mode and (S_IXOTH or S_ISVTX)) {
0 -> p += '-'
S_IXOTH -> p += 'x'
S_ISVTX -> p += 'T'
S_IXOTH or S_ISVTX -> p += 't'
}
return p
}
/**
*
* Converts the numeric to the symbolic permission notation.
*
* Example: `convertNumericToSymbolicNotation("644")` would return "rw-r--r--"
*
* @param mode
* An octal (base-8) notation as shown by `stat -c %a`. This notation consists of at
* least three digits. Each of the three rightmost digits represents a different component of
* the permissions: owner, group, and others.
* @return the symbolic notation of the permission.
*/
fun convertNumericToSymbolicNotation(mode: String): String {
require(mode.matches("[0-7]+".toRegex()))
var arr = mode.toCharArray()
require(arr.size in 3..4) { "Invalid length" }
val length = arr.size
val special: String
if (length == 4) {
special = parseSpecialFromChar(arr[0])
arr = mode.substring(1).toCharArray()
} else {
special = "---"
}
var permissions = ""
for (i in 0..2) {
val s = special[i]
when (arr[i]) {
'0' -> permissions += if (s == '-') "---" else "--" + s.uppercaseChar()
'1' -> permissions += if (s == '-') "--x" else "--$s"
'2' -> permissions += "-w-"
'3' -> permissions += if (s == '-') "-wx" else "-w$s"
'4' -> permissions += if (s == '-') "r--" else "r-" + s.uppercaseChar()
'5' -> permissions += if (s == '-') "r-x" else "r-$s"
'6' -> permissions += "rw-"
'7' -> permissions += if (s == '-') "rwx" else "rw$s"
}
}
return permissions
}
/**
*
* Converts the symbolic to the numeric permission notation.
*
* Example: `convertSymbolicToNumericNotation("rwxr-xr-x")` would return "755"
*
* @param permissions
* The first character (optional) indicates the file type and is not related to permissions.
* The remaining nine characters are in three sets, each representing a class of
* permissions as three characters. The first set represents the user class. The second set
* represents the group class. The third set represents the others class. Examples:
* "-rwxr-xr-x", "rw-r--r--", "drwxr-xr-x"
* @return the mode
*/
fun convertSymbolicToNumericNotation(permissions: String): String {
var permissions = permissions
require(permissions.matches("[rwxSTstdcb\\-lp?]+".toRegex()))
val length = permissions.length
if (length == 10) {
// If the value has the character defining the file type, remove that character.
permissions = permissions.substring(1)
}
require(permissions.length == 9)
val special: Int = parseSpecialPermissions(permissions)
val user = parsePermissions(permissions.substring(0, 3))
val group = parsePermissions(permissions.substring(3, 6))
val other = parsePermissions(permissions.substring(6, 9))
return "$special$user$group$other"
}
private fun parsePermissions(str: String): Int {
require(str.matches("[rwxSTst\\-]+".toRegex())) { "Invalid characters in $str" }
require(str.length == 3) { "Invalid length. Expected 3, got ${str.length}" }
val permission = str.lowercase()
var mode = 0
if (permission[0] == 'r') {
mode += 4
}
if (permission[1] == 'w') {
mode += 2
}
if ((permission[2] == 'x') || (permission[2] == 's') || (permission[2] == 't')) {
mode += 1
}
return mode
}
private fun parseSpecialFromChar(c: Char): String = when (c) {
'0' -> "---"
'1' -> "--t"
'2' -> "-s-"
'3' -> "-st"
'4' -> "s--"
'5' -> "s-t"
'6' -> "ss-"
'7' -> "sst"
else -> "---"
}
private fun parseSpecialPermissions(str: String): Int {
var str = str
require(str.matches("[rwxSTstdcb\\-lp?]+".toRegex())) { "Invalid permission: '$str'" }
val length = str.length
if (length == 10) {
// If the value has the character defining the file type, remove that character.
str = str.substring(1)
}
require(length == 9)
val permission = str.lowercase()
var mode = 0
if (permission[2] == 's') {
mode += 4
}
if (permission[5] == 's') {
mode += 2
}
if (permission[8] == 't') {
mode += 1
}
return mode
}
}
}
/*
* Copyright 2024 GoatBytes.IO
*
* 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 io.goatbytes.unix
interface Permission {
val canRead: Boolean
val canWrite: Boolean
val canExecute: Boolean
}
/*
* Copyright 2024 GoatBytes.IO
*
* 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 io.goatbytes.unix
sealed interface PermissionGroup : Permission {
data class User(
override val canRead: Boolean,
override val canWrite: Boolean,
override val canExecute: Boolean
) : PermissionGroup
data class Group(
override val canRead: Boolean,
override val canWrite: Boolean,
override val canExecute: Boolean
) : PermissionGroup
data class Other(
override val canRead: Boolean,
override val canWrite: Boolean,
override val canExecute: Boolean
) : PermissionGroup
}
/*
* Copyright 2024 GoatBytes.IO
*
* 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 io.goatbytes.unix
interface SpecialPermission {
val setUID: Boolean
val setGID: Boolean
val sticky: Boolean
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment