feat(Chmod Calculator): octal input and special flags

Handle input of octal rights values and special flags
Fix #670 and #527
This commit is contained in:
sharevb 2024-04-03 22:34:00 +02:00 committed by ShareVB
parent d3b32cc14e
commit 3ead4d37ad
4 changed files with 314 additions and 10 deletions

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from './chmod-calculator.service';
import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation, computePermissionsFromChmodOctalRepresentation } from './chmod-calculator.service';
describe('chmod-calculator', () => {
describe('computeChmodOctalRepresentation', () => {
@ -10,6 +10,7 @@ describe('chmod-calculator', () => {
owner: { read: true, write: true, execute: true },
group: { read: true, write: true, execute: true },
public: { read: true, write: true, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
},
}),
).to.eql('777');
@ -20,6 +21,7 @@ describe('chmod-calculator', () => {
owner: { read: false, write: false, execute: false },
group: { read: false, write: false, execute: false },
public: { read: false, write: false, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
},
}),
).to.eql('000');
@ -30,6 +32,7 @@ describe('chmod-calculator', () => {
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: true },
public: { read: true, write: false, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
},
}),
).to.eql('235');
@ -40,6 +43,7 @@ describe('chmod-calculator', () => {
owner: { read: true, write: false, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: false, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
},
}),
).to.eql('421');
@ -50,6 +54,7 @@ describe('chmod-calculator', () => {
owner: { read: false, write: false, execute: true },
group: { read: false, write: true, execute: false },
public: { read: true, write: false, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
},
}),
).to.eql('124');
@ -60,11 +65,57 @@ describe('chmod-calculator', () => {
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
},
}),
).to.eql('222');
});
expect(
computeChmodOctalRepresentation({
permissions: {
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false },
flags: { setuid: true, setgid: true, stickybit: true },
},
}),
).to.eql('7222');
expect(
computeChmodOctalRepresentation({
permissions: {
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false },
flags: { setuid: true, setgid: false, stickybit: false },
},
}),
).to.eql('4222');
expect(
computeChmodOctalRepresentation({
permissions: {
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false },
flags: { setuid: false, setgid: true, stickybit: false },
},
}),
).to.eql('2222');
expect(
computeChmodOctalRepresentation({
permissions: {
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false },
flags: { setuid: false, setgid: false, stickybit: true },
},
}),
).to.eql('1222');
});
});
describe('computeChmodSymbolicRepresentation', () => {
it('get the symbolic representation from permissions', () => {
expect(
computeChmodSymbolicRepresentation({
@ -72,6 +123,7 @@ describe('chmod-calculator', () => {
owner: { read: true, write: true, execute: true },
group: { read: true, write: true, execute: true },
public: { read: true, write: true, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
},
}),
).to.eql('rwxrwxrwx');
@ -82,6 +134,7 @@ describe('chmod-calculator', () => {
owner: { read: false, write: false, execute: false },
group: { read: false, write: false, execute: false },
public: { read: false, write: false, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
},
}),
).to.eql('---------');
@ -92,6 +145,7 @@ describe('chmod-calculator', () => {
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: true },
public: { read: true, write: false, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
},
}),
).to.eql('-w--wxr-x');
@ -102,6 +156,7 @@ describe('chmod-calculator', () => {
owner: { read: true, write: false, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: false, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
},
}),
).to.eql('r---w---x');
@ -112,6 +167,7 @@ describe('chmod-calculator', () => {
owner: { read: false, write: false, execute: true },
group: { read: false, write: true, execute: false },
public: { read: true, write: false, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
},
}),
).to.eql('--x-w-r--');
@ -122,9 +178,165 @@ describe('chmod-calculator', () => {
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
},
}),
).to.eql('-w--w--w-');
expect(
computeChmodSymbolicRepresentation({
permissions: {
owner: { read: false, write: false, execute: true },
group: { read: false, write: true, execute: false },
public: { read: true, write: false, execute: false },
flags: { setuid: false, setgid: false, stickybit: true },
},
}),
).to.eql('--x-w-r-t');
expect(
computeChmodSymbolicRepresentation({
permissions: {
owner: { read: false, write: false, execute: true },
group: { read: false, write: true, execute: false },
public: { read: true, write: false, execute: false },
flags: { setuid: false, setgid: true, stickybit: true },
},
}),
).to.eql('--x-wsr-t');
expect(
computeChmodSymbolicRepresentation({
permissions: {
owner: { read: false, write: false, execute: true },
group: { read: false, write: true, execute: false },
public: { read: true, write: false, execute: false },
flags: { setuid: true, setgid: true, stickybit: true },
},
}),
).to.eql('--s-wsr-t');
expect(
computeChmodSymbolicRepresentation({
permissions: {
owner: { read: true, write: false, execute: true },
group: { read: true, write: true, execute: false },
public: { read: true, write: false, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
},
}),
).to.eql('r-xrw-r--');
expect(
computeChmodSymbolicRepresentation({
permissions: {
owner: { read: true, write: true, execute: true },
group: { read: true, write: true, execute: true },
public: { read: true, write: true, execute: true },
flags: { setuid: true, setgid: true, stickybit: true },
},
}),
).to.eql('rwsrwsrwt');
});
});
describe('computePermissionsFromChmodOctalRepresentation', () => {
it('throws on invalid octal values', () => {
expect(() => computePermissionsFromChmodOctalRepresentation('12')).to.throw();
expect(() => computePermissionsFromChmodOctalRepresentation('12345')).to.throw();
expect(() => computePermissionsFromChmodOctalRepresentation('999')).to.throw();
expect(() => computePermissionsFromChmodOctalRepresentation('9999')).to.throw();
});
it('get permissions from octal representation', () => {
expect(
computePermissionsFromChmodOctalRepresentation('777'),
).to.eql({
owner: { read: true, write: true, execute: true },
group: { read: true, write: true, execute: true },
public: { read: true, write: true, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
});
expect(
computePermissionsFromChmodOctalRepresentation('000'),
).to.eql({
owner: { read: false, write: false, execute: false },
group: { read: false, write: false, execute: false },
public: { read: false, write: false, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
});
expect(
computePermissionsFromChmodOctalRepresentation('235'),
).to.eql({
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: true },
public: { read: true, write: false, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
});
expect(
computePermissionsFromChmodOctalRepresentation('421'),
).to.eql({
owner: { read: true, write: false, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: false, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
});
expect(
computePermissionsFromChmodOctalRepresentation('124'),
).to.eql({
owner: { read: false, write: false, execute: true },
group: { read: false, write: true, execute: false },
public: { read: true, write: false, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
});
expect(
computePermissionsFromChmodOctalRepresentation('222'),
).to.eql({
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
});
expect(
computePermissionsFromChmodOctalRepresentation('7222'),
).to.eql({
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false },
flags: { setuid: true, setgid: true, stickybit: true },
});
expect(
computePermissionsFromChmodOctalRepresentation('4222'),
).to.eql({
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false },
flags: { setuid: true, setgid: false, stickybit: false },
});
expect(
computePermissionsFromChmodOctalRepresentation('2222'),
).to.eql({
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false },
flags: { setuid: false, setgid: true, stickybit: false },
});
expect(
computePermissionsFromChmodOctalRepresentation('1222'),
).to.eql({
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false },
flags: { setuid: false, setgid: false, stickybit: true },
});
});
});
});

View file

@ -1,15 +1,21 @@
import _ from 'lodash';
import type { GroupPermissions, Permissions } from './chmod-calculator.types';
import type { GroupPermissions, Permissions, SpecialPermissions } from './chmod-calculator.types';
export { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation };
export { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation, computePermissionsFromChmodOctalRepresentation };
function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string {
const permissionValue = { read: 4, write: 2, execute: 1 };
const specialPermissionValue = { setuid: 4, setgid: 2, stickybit: 1 };
const getGroupPermissionValue = (permission: GroupPermissions) =>
_.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, 0) : 0), 0);
const getSpecialPermissionValue = (permission: SpecialPermissions) => {
const octalValue = _.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(specialPermissionValue, key, 0) : 0), 0);
return octalValue > 0 ? octalValue.toString() : '';
};
return [
getSpecialPermissionValue(permissions.flags || { setuid: false, setgid: false, stickybit: false }),
getGroupPermissionValue(permissions.owner),
getGroupPermissionValue(permissions.group),
getGroupPermissionValue(permissions.public),
@ -18,13 +24,40 @@ function computeChmodOctalRepresentation({ permissions }: { permissions: Permiss
function computeChmodSymbolicRepresentation({ permissions }: { permissions: Permissions }): string {
const permissionValue = { read: 'r', write: 'w', execute: 'x' };
const specialFlagPermission = 'execute';
const getGroupPermissionValue = (permission: GroupPermissions) =>
_.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, '') : '-'), '');
const getGroupPermissionValue = (permission: GroupPermissions, specialFlag: null | 's' | 't') =>
_.reduce(permission, (acc, isPermSet, key) => acc + ((key === specialFlagPermission ? specialFlag : undefined)
|| (isPermSet ? _.get(permissionValue, key, '') : '-')), '');
return [
getGroupPermissionValue(permissions.owner),
getGroupPermissionValue(permissions.group),
getGroupPermissionValue(permissions.public),
getGroupPermissionValue(permissions.owner, permissions.flags?.setuid ? 's' : null),
getGroupPermissionValue(permissions.group, permissions.flags?.setgid ? 's' : null),
getGroupPermissionValue(permissions.public, permissions.flags?.stickybit ? 't' : null),
].join('');
}
function computePermissionsFromChmodOctalRepresentation(octalPermissions: string): Permissions {
const permissionValue = { read: 4, write: 2, execute: 1 };
const specialPermissionValue = { setuid: 4, setgid: 2, stickybit: 1 };
if (!octalPermissions || !octalPermissions.match(/^[0-7]{3,4}$/)) {
throw new Error(`Invalid octal permissions (must be 3 or 4 octal digits): ${octalPermissions}`);
}
const fullOctalPermissions = octalPermissions.length === 3 ? `0${octalPermissions}` : octalPermissions;
const hasSet = (position: number, flagValue: number) => (Number(fullOctalPermissions[position]) & flagValue) === flagValue;
function computePermissionObject<T>(permissionSet: object, position: number): T {
return _.reduce(permissionSet, (acc, flag, key) => ({ ...acc, [key]: hasSet(position, flag) }), {}) as T;
}
const flagsPosition = 0;
const ownerPosition = 1;
const groupPosition = 2;
const publicPosition = 3;
return {
owner: computePermissionObject(permissionValue, ownerPosition),
group: computePermissionObject(permissionValue, groupPosition),
public: computePermissionObject(permissionValue, publicPosition),
flags: computePermissionObject(specialPermissionValue, flagsPosition),
};
}

View file

@ -1,10 +1,17 @@
export type Scope = 'read' | 'write' | 'execute';
export type Group = 'owner' | 'group' | 'public';
export type SpecialFlags = 'setuid' | 'setgid' | 'stickybit';
export type GroupPermissions = {
[k in Scope]: boolean;
};
export type SpecialPermissions = {
[k in SpecialFlags]: boolean;
};
export type Permissions = {
[k in Group]: GroupPermissions;
} & {
flags: SpecialPermissions
};

View file

@ -2,9 +2,10 @@
import { useThemeVars } from 'naive-ui';
import InputCopyable from '../../components/InputCopyable.vue';
import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from './chmod-calculator.service';
import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation, computePermissionsFromChmodOctalRepresentation } from './chmod-calculator.service';
import type { Group, Scope } from './chmod-calculator.types';
import { useValidation } from '@/composable/validation';
const themeVars = useThemeVars();
@ -19,14 +20,51 @@ const permissions = ref({
owner: { read: false, write: false, execute: false },
group: { read: false, write: false, execute: false },
public: { read: false, write: false, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
});
const permissionsInput = ref('000');
const permissionsInputValidation = useValidation({
source: permissionsInput,
rules: [
{
message: 'Invalid octal permission string',
validator: (value) => {
try {
computePermissionsFromChmodOctalRepresentation(value.trim());
return true;
}
catch {
return false;
}
},
},
],
});
watch(
permissionsInput,
(newPermissions) => {
if (!permissionsInputValidation.isValid) {
return;
}
permissions.value = computePermissionsFromChmodOctalRepresentation(newPermissions.trim());
},
);
const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value }));
const symbolic = computed(() => computeChmodSymbolicRepresentation({ permissions: permissions.value }));
</script>
<template>
<div>
<c-input-text
v-model:value="permissionsInput"
placeholder="Put your octal permissions here..."
label="Copy your octal permissions"
:validation="permissionsInputValidation"
mb-2
/>
<n-divider />
<n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table">
<thead>
<tr>
@ -52,6 +90,20 @@ const symbolic = computed(() => computeChmodSymbolicRepresentation({ permissions
<n-checkbox v-model:checked="permissions[group][scope]" size="large" />
</td>
</tr>
<tr>
<td class="line-header">
Flags
</td>
<td class="text-center">
<n-checkbox v-model:checked="permissions.flags.setuid" size="large" />
</td>
<td class="text-center">
<n-checkbox v-model:checked="permissions.flags.setgid" size="large" />
</td>
<td class="text-center">
<n-checkbox v-model:checked="permissions.flags.stickybit" size="large" />
</td>
</tr>
</tbody>
</n-table>