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 { describe, expect, it } from 'vitest';
import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from './chmod-calculator.service'; import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation, computePermissionsFromChmodOctalRepresentation } from './chmod-calculator.service';
describe('chmod-calculator', () => { describe('chmod-calculator', () => {
describe('computeChmodOctalRepresentation', () => { describe('computeChmodOctalRepresentation', () => {
@ -10,6 +10,7 @@ describe('chmod-calculator', () => {
owner: { read: true, write: true, execute: true }, owner: { read: true, write: true, execute: true },
group: { read: true, write: true, execute: true }, group: { read: true, write: true, execute: true },
public: { read: true, write: true, execute: true }, public: { read: true, write: true, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
}, },
}), }),
).to.eql('777'); ).to.eql('777');
@ -20,6 +21,7 @@ describe('chmod-calculator', () => {
owner: { read: false, write: false, execute: false }, owner: { read: false, write: false, execute: false },
group: { read: false, write: false, execute: false }, group: { read: false, write: false, execute: false },
public: { read: false, write: false, execute: false }, public: { read: false, write: false, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
}, },
}), }),
).to.eql('000'); ).to.eql('000');
@ -30,6 +32,7 @@ describe('chmod-calculator', () => {
owner: { read: false, write: true, execute: false }, owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: true }, group: { read: false, write: true, execute: true },
public: { read: true, write: false, execute: true }, public: { read: true, write: false, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
}, },
}), }),
).to.eql('235'); ).to.eql('235');
@ -40,6 +43,7 @@ describe('chmod-calculator', () => {
owner: { read: true, write: false, execute: false }, owner: { read: true, write: false, execute: false },
group: { read: false, write: true, execute: false }, group: { read: false, write: true, execute: false },
public: { read: false, write: false, execute: true }, public: { read: false, write: false, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
}, },
}), }),
).to.eql('421'); ).to.eql('421');
@ -50,6 +54,7 @@ describe('chmod-calculator', () => {
owner: { read: false, write: false, execute: true }, owner: { read: false, write: false, execute: true },
group: { read: false, write: true, execute: false }, group: { read: false, write: true, execute: false },
public: { read: true, write: false, execute: false }, public: { read: true, write: false, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
}, },
}), }),
).to.eql('124'); ).to.eql('124');
@ -60,11 +65,57 @@ describe('chmod-calculator', () => {
owner: { read: false, write: true, execute: false }, owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false }, group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false }, public: { read: false, write: true, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
}, },
}), }),
).to.eql('222'); ).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', () => { it('get the symbolic representation from permissions', () => {
expect( expect(
computeChmodSymbolicRepresentation({ computeChmodSymbolicRepresentation({
@ -72,6 +123,7 @@ describe('chmod-calculator', () => {
owner: { read: true, write: true, execute: true }, owner: { read: true, write: true, execute: true },
group: { read: true, write: true, execute: true }, group: { read: true, write: true, execute: true },
public: { read: true, write: true, execute: true }, public: { read: true, write: true, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
}, },
}), }),
).to.eql('rwxrwxrwx'); ).to.eql('rwxrwxrwx');
@ -82,6 +134,7 @@ describe('chmod-calculator', () => {
owner: { read: false, write: false, execute: false }, owner: { read: false, write: false, execute: false },
group: { read: false, write: false, execute: false }, group: { read: false, write: false, execute: false },
public: { read: false, write: false, execute: false }, public: { read: false, write: false, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
}, },
}), }),
).to.eql('---------'); ).to.eql('---------');
@ -92,6 +145,7 @@ describe('chmod-calculator', () => {
owner: { read: false, write: true, execute: false }, owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: true }, group: { read: false, write: true, execute: true },
public: { read: true, write: false, execute: true }, public: { read: true, write: false, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
}, },
}), }),
).to.eql('-w--wxr-x'); ).to.eql('-w--wxr-x');
@ -102,6 +156,7 @@ describe('chmod-calculator', () => {
owner: { read: true, write: false, execute: false }, owner: { read: true, write: false, execute: false },
group: { read: false, write: true, execute: false }, group: { read: false, write: true, execute: false },
public: { read: false, write: false, execute: true }, public: { read: false, write: false, execute: true },
flags: { setuid: false, setgid: false, stickybit: false },
}, },
}), }),
).to.eql('r---w---x'); ).to.eql('r---w---x');
@ -112,6 +167,7 @@ describe('chmod-calculator', () => {
owner: { read: false, write: false, execute: true }, owner: { read: false, write: false, execute: true },
group: { read: false, write: true, execute: false }, group: { read: false, write: true, execute: false },
public: { read: true, write: false, execute: false }, public: { read: true, write: false, execute: false },
flags: { setuid: false, setgid: false, stickybit: false },
}, },
}), }),
).to.eql('--x-w-r--'); ).to.eql('--x-w-r--');
@ -122,9 +178,165 @@ describe('chmod-calculator', () => {
owner: { read: false, write: true, execute: false }, owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false }, group: { read: false, write: true, execute: false },
public: { 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-'); ).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 _ 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 { function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string {
const permissionValue = { read: 4, write: 2, execute: 1 }; const permissionValue = { read: 4, write: 2, execute: 1 };
const specialPermissionValue = { setuid: 4, setgid: 2, stickybit: 1 };
const getGroupPermissionValue = (permission: GroupPermissions) => const getGroupPermissionValue = (permission: GroupPermissions) =>
_.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, 0) : 0), 0); _.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 [ return [
getSpecialPermissionValue(permissions.flags || { setuid: false, setgid: false, stickybit: false }),
getGroupPermissionValue(permissions.owner), getGroupPermissionValue(permissions.owner),
getGroupPermissionValue(permissions.group), getGroupPermissionValue(permissions.group),
getGroupPermissionValue(permissions.public), getGroupPermissionValue(permissions.public),
@ -18,13 +24,40 @@ function computeChmodOctalRepresentation({ permissions }: { permissions: Permiss
function computeChmodSymbolicRepresentation({ permissions }: { permissions: Permissions }): string { function computeChmodSymbolicRepresentation({ permissions }: { permissions: Permissions }): string {
const permissionValue = { read: 'r', write: 'w', execute: 'x' }; const permissionValue = { read: 'r', write: 'w', execute: 'x' };
const specialFlagPermission = 'execute';
const getGroupPermissionValue = (permission: GroupPermissions) => const getGroupPermissionValue = (permission: GroupPermissions, specialFlag: null | 's' | 't') =>
_.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, '') : '-'), ''); _.reduce(permission, (acc, isPermSet, key) => acc + ((key === specialFlagPermission ? specialFlag : undefined)
|| (isPermSet ? _.get(permissionValue, key, '') : '-')), '');
return [ return [
getGroupPermissionValue(permissions.owner), getGroupPermissionValue(permissions.owner, permissions.flags?.setuid ? 's' : null),
getGroupPermissionValue(permissions.group), getGroupPermissionValue(permissions.group, permissions.flags?.setgid ? 's' : null),
getGroupPermissionValue(permissions.public), getGroupPermissionValue(permissions.public, permissions.flags?.stickybit ? 't' : null),
].join(''); ].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 Scope = 'read' | 'write' | 'execute';
export type Group = 'owner' | 'group' | 'public'; export type Group = 'owner' | 'group' | 'public';
export type SpecialFlags = 'setuid' | 'setgid' | 'stickybit';
export type GroupPermissions = { export type GroupPermissions = {
[k in Scope]: boolean; [k in Scope]: boolean;
}; };
export type SpecialPermissions = {
[k in SpecialFlags]: boolean;
};
export type Permissions = { export type Permissions = {
[k in Group]: GroupPermissions; [k in Group]: GroupPermissions;
} & {
flags: SpecialPermissions
}; };

View file

@ -2,9 +2,10 @@
import { useThemeVars } from 'naive-ui'; import { useThemeVars } from 'naive-ui';
import InputCopyable from '../../components/InputCopyable.vue'; 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 type { Group, Scope } from './chmod-calculator.types';
import { useValidation } from '@/composable/validation';
const themeVars = useThemeVars(); const themeVars = useThemeVars();
@ -19,14 +20,51 @@ const permissions = ref({
owner: { read: false, write: false, execute: false }, owner: { read: false, write: false, execute: false },
group: { read: false, write: false, execute: false }, group: { read: false, write: false, execute: false },
public: { 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 octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value }));
const symbolic = computed(() => computeChmodSymbolicRepresentation({ permissions: permissions.value })); const symbolic = computed(() => computeChmodSymbolicRepresentation({ permissions: permissions.value }));
</script> </script>
<template> <template>
<div> <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"> <n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table">
<thead> <thead>
<tr> <tr>
@ -52,6 +90,20 @@ const symbolic = computed(() => computeChmodSymbolicRepresentation({ permissions
<n-checkbox v-model:checked="permissions[group][scope]" size="large" /> <n-checkbox v-model:checked="permissions[group][scope]" size="large" />
</td> </td>
</tr> </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> </tbody>
</n-table> </n-table>