Skip to content

Commit 0d4e6ca

Browse files
committed
Add support for special bits in permission calculations
1 parent 014a6f7 commit 0d4e6ca

4 files changed

Lines changed: 243 additions & 40 deletions

File tree

src/mako/file/Permission.php

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,48 +18,58 @@ enum Permission: int
1818
{
1919
// No permissions
2020

21-
case NONE = 0o000;
21+
case NONE = 0o0000;
2222

2323
// Owner permissions
2424

25-
case OWNER_EXECUTE = 0o100;
26-
case OWNER_WRITE = 0o200;
27-
case OWNER_EXECUTE_WRITE = 0o300;
28-
case OWNER_READ = 0o400;
29-
case OWNER_EXECUTE_READ = 0o500;
30-
case OWNER_WRITE_READ = 0o600;
31-
case OWNER_FULL = 0o700;
25+
case OWNER_EXECUTE = 0o0100;
26+
case OWNER_WRITE = 0o0200;
27+
case OWNER_EXECUTE_WRITE = 0o0300;
28+
case OWNER_READ = 0o0400;
29+
case OWNER_EXECUTE_READ = 0o0500;
30+
case OWNER_WRITE_READ = 0o0600;
31+
case OWNER_FULL = 0o0700;
3232

3333
// Group permissions
3434

35-
case GROUP_EXECUTE = 0o010;
36-
case GROUP_WRITE = 0o020;
37-
case GROUP_EXECUTE_WRITE = 0o030;
38-
case GROUP_READ = 0o040;
39-
case GROUP_EXECUTE_READ = 0o050;
40-
case GROUP_WRITE_READ = 0o060;
41-
case GROUP_FULL = 0o070;
35+
case GROUP_EXECUTE = 0o0010;
36+
case GROUP_WRITE = 0o0020;
37+
case GROUP_EXECUTE_WRITE = 0o0030;
38+
case GROUP_READ = 0o0040;
39+
case GROUP_EXECUTE_READ = 0o0050;
40+
case GROUP_WRITE_READ = 0o0060;
41+
case GROUP_FULL = 0o0070;
4242

4343
// Public permissions
4444

45-
case PUBLIC_EXECUTE = 0o001;
46-
case PUBLIC_WRITE = 0o002;
47-
case PUBLIC_EXECUTE_WRITE = 0o003;
48-
case PUBLIC_READ = 0o004;
49-
case PUBLIC_EXECUTE_READ = 0o005;
50-
case PUBLIC_WRITE_READ = 0o006;
51-
case PUBLIC_FULL = 0o007;
45+
case PUBLIC_EXECUTE = 0o0001;
46+
case PUBLIC_WRITE = 0o0002;
47+
case PUBLIC_EXECUTE_WRITE = 0o0003;
48+
case PUBLIC_READ = 0o0004;
49+
case PUBLIC_EXECUTE_READ = 0o0005;
50+
case PUBLIC_WRITE_READ = 0o0006;
51+
case PUBLIC_FULL = 0o0007;
5252

5353
// Full permissions (owner, group, and public)
5454

55-
case FULL = 0o777;
55+
case FULL = 0o0777;
56+
57+
// Special bits
58+
59+
case SPECIAL_STICKY = 0o1000;
60+
case SPECIAL_SETGID = 0o2000;
61+
case SPECIAL_SETUID = 0o4000;
62+
63+
// Full permissions (owner, group, and public) with all special bits
64+
65+
case FULL_WITH_ALL_SPECIAL = 0o7777;
5666

5767
/**
5868
* Calculates sum of the specified permissions.
5969
*/
6070
public static function calculate(Permission ...$permission): int
6171
{
62-
$permissions = 0o000;
72+
$permissions = 0o0000;
6373

6474
foreach ($permission as $_permission) {
6575
$permissions |= $_permission->value;
@@ -73,14 +83,14 @@ public static function calculate(Permission ...$permission): int
7383
*/
7484
public static function hasPermissions(int $permissions, Permission ...$permission): bool
7585
{
76-
if ($permissions < 0o000 || $permissions > 0o777) {
77-
throw new InvalidArgumentException(sprintf('The integer [ %s ] does not represent a valid octal between 0o000 and 0o777.', $permissions));
86+
if ($permissions < 0o0000 || $permissions > 0o7777) {
87+
throw new InvalidArgumentException(sprintf('The integer [ %s ] does not represent a valid octal between 0o0000 and 0o7777.', $permissions));
7888
}
7989

80-
$permission = empty($permission) ? 0o000 : self::calculate(...$permission);
90+
$permission = empty($permission) ? 0o0000 : self::calculate(...$permission);
8191

82-
if ($permission === 0o000) {
83-
return $permissions === 0o000;
92+
if ($permission === 0o0000) {
93+
return $permissions === 0o0000;
8494
}
8595

8696
return ($permissions & $permission) === $permission;

src/mako/file/Permissions.php

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use function decoct;
1313
use function sprintf;
1414
use function str_pad;
15+
use function strtoupper;
1516

1617
/**
1718
* File permission collection.
@@ -38,8 +39,8 @@ final public function __construct(Permission ...$permissions)
3839
*/
3940
public static function fromInt(int $permissions): static
4041
{
41-
if ($permissions < Permission::NONE->value || $permissions > Permission::FULL->value) {
42-
throw new InvalidArgumentException(sprintf('The integer [ %s ] does not represent a valid octal between 0o000 and 0o777.', $permissions));
42+
if ($permissions < Permission::NONE->value || $permissions > (Permission::FULL_WITH_ALL_SPECIAL->value)) {
43+
throw new InvalidArgumentException(sprintf('The integer [ %s ] does not represent a valid octal between 0o0000 and 0o7777.', $permissions));
4344
}
4445

4546
if ($permissions === Permission::NONE->value) {
@@ -56,6 +57,9 @@ public static function fromInt(int $permissions): static
5657
Permission::PUBLIC_READ,
5758
Permission::PUBLIC_WRITE,
5859
Permission::PUBLIC_EXECUTE,
60+
Permission::SPECIAL_SETUID,
61+
Permission::SPECIAL_SETGID,
62+
Permission::SPECIAL_STICKY,
5963
];
6064

6165
$permission = [];
@@ -106,13 +110,30 @@ public function toOctalString(): string
106110
/**
107111
* Returns a rwx string representation of a permission group.
108112
*/
109-
protected function getGroupAsRwxString(int $permissions, Permission $read, Permission $write, Permission $execute): string
113+
protected function getGroupAsRwxString(int $permissions, Permission $read, Permission $write, Permission $execute, string $group): string
110114
{
111115
$rwx = '';
112116

117+
// Read & Write
118+
113119
$rwx .= ($permissions & $read->value) ? 'r' : '-';
114120
$rwx .= ($permissions & $write->value) ? 'w' : '-';
115-
$rwx .= ($permissions & $execute->value) ? 'x' : '-';
121+
122+
// Determine special bit
123+
124+
$specialChar = match ($group) {
125+
'owner' => ($permissions & Permission::SPECIAL_SETUID->value) ? 's' : null,
126+
'group' => ($permissions & Permission::SPECIAL_SETGID->value) ? 's' : null,
127+
'public' => ($permissions & Permission::SPECIAL_STICKY->value) ? 't' : null,
128+
};
129+
130+
// Execute + special bit handling
131+
132+
$hasExecute = ($permissions & $execute->value) !== 0;
133+
134+
$rwx .= $specialChar !== null ? ($hasExecute ? $specialChar : strtoupper($specialChar)) : ($hasExecute ? 'x' : '-');
135+
136+
// Return rwx string
116137

117138
return $rwx;
118139
}
@@ -124,9 +145,9 @@ public function toRwxString(): string
124145
{
125146
$permissions = Permission::calculate(...$this->permissions);
126147

127-
$owner = $this->getGroupAsRwxString($permissions, Permission::OWNER_READ, Permission::OWNER_WRITE, Permission::OWNER_EXECUTE);
128-
$group = $this->getGroupAsRwxString($permissions, Permission::GROUP_READ, Permission::GROUP_WRITE, Permission::GROUP_EXECUTE);
129-
$public = $this->getGroupAsRwxString($permissions, Permission::PUBLIC_READ, Permission::PUBLIC_WRITE, Permission::PUBLIC_EXECUTE);
148+
$owner = $this->getGroupAsRwxString($permissions, Permission::OWNER_READ, Permission::OWNER_WRITE, Permission::OWNER_EXECUTE, 'owner');
149+
$group = $this->getGroupAsRwxString($permissions, Permission::GROUP_READ, Permission::GROUP_WRITE, Permission::GROUP_EXECUTE, 'group');
150+
$public = $this->getGroupAsRwxString($permissions, Permission::PUBLIC_READ, Permission::PUBLIC_WRITE, Permission::PUBLIC_EXECUTE, 'public');
130151

131152
return "{$owner}{$group}{$public}";
132153
}

tests/unit/file/PermissionTest.php

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ public function testCalculate(): void
7474

7575
$this->assertSame(0o007, Permission::calculate(Permission::PUBLIC_FULL));
7676

77+
//
78+
79+
$this->assertSame(0o1000, Permission::calculate(Permission::SPECIAL_STICKY));
80+
81+
$this->assertSame(0o2000, Permission::calculate(Permission::SPECIAL_SETGID));
82+
83+
$this->assertSame(0o4000, Permission::calculate(Permission::SPECIAL_SETUID));
84+
7785
// Test combinations
7886

7987
$this->assertSame(0o666, Permission::calculate(
@@ -101,6 +109,25 @@ public function testCalculate(): void
101109
Permission::PUBLIC_READ,
102110
Permission::PUBLIC_EXECUTE)
103111
);
112+
113+
$this->assertSame(0o1755, Permission::calculate(
114+
Permission::SPECIAL_STICKY,
115+
Permission::OWNER_FULL,
116+
Permission::GROUP_READ,
117+
Permission::GROUP_EXECUTE,
118+
Permission::PUBLIC_READ,
119+
Permission::PUBLIC_EXECUTE)
120+
);
121+
122+
$this->assertSame(0o3755, Permission::calculate(
123+
Permission::SPECIAL_STICKY,
124+
Permission::SPECIAL_SETGID,
125+
Permission::OWNER_FULL,
126+
Permission::GROUP_READ,
127+
Permission::GROUP_EXECUTE,
128+
Permission::PUBLIC_READ,
129+
Permission::PUBLIC_EXECUTE)
130+
);
104131
}
105132

106133
/**
@@ -110,9 +137,9 @@ public function testHasPermissionsWithInvalidPermissions(): void
110137
{
111138
$this->expectException(InvalidArgumentException::class);
112139

113-
$this->expectExceptionMessage('The integer [ 1337 ] does not represent a valid octal between 0o000 and 0o777.');
140+
$this->expectExceptionMessage('The integer [ 13337 ] does not represent a valid octal between 0o0000 and 0o7777.');
114141

115-
Permission::hasPermissions(1337, Permission::NONE);
142+
Permission::hasPermissions(13337, Permission::NONE);
116143
}
117144

118145
/**
@@ -130,6 +157,12 @@ public function testHasPermissions(): void
130157

131158
$this->assertTrue(Permission::hasPermissions(0o755, Permission::OWNER_FULL));
132159

160+
$this->assertTrue(Permission::hasPermissions(0o1000, Permission::SPECIAL_STICKY));
161+
162+
$this->assertTrue(Permission::hasPermissions(0o3000, Permission::SPECIAL_STICKY, Permission::SPECIAL_SETGID));
163+
164+
$this->assertTrue(Permission::hasPermissions(0o7000, Permission::SPECIAL_STICKY, Permission::SPECIAL_SETGID, Permission::SPECIAL_SETUID));
165+
133166
$this->assertFalse(Permission::hasPermissions(0o755, Permission::GROUP_WRITE));
134167

135168
$this->assertFalse(Permission::hasPermissions(0o755, Permission::PUBLIC_WRITE));

0 commit comments

Comments
 (0)