From 3ead4d37ad3df8b67e841acde730d890f34a931f Mon Sep 17 00:00:00 2001 From: sharevb Date: Wed, 3 Apr 2024 22:34:00 +0200 Subject: [PATCH 1/4] feat(Chmod Calculator): octal input and special flags Handle input of octal rights values and special flags Fix #670 and #527 --- .../chmod-calculator.service.test.ts | 216 +++++++++++++++++- .../chmod-calculator.service.ts | 47 +++- .../chmod-calculator.types.ts | 7 + .../chmod-calculator/chmod-calculator.vue | 54 ++++- 4 files changed, 314 insertions(+), 10 deletions(-) diff --git a/src/tools/chmod-calculator/chmod-calculator.service.test.ts b/src/tools/chmod-calculator/chmod-calculator.service.test.ts index 426c4a56c..5264734c5 100644 --- a/src/tools/chmod-calculator/chmod-calculator.service.test.ts +++ b/src/tools/chmod-calculator/chmod-calculator.service.test.ts @@ -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 }, + }); }); }); }); diff --git a/src/tools/chmod-calculator/chmod-calculator.service.ts b/src/tools/chmod-calculator/chmod-calculator.service.ts index e00467179..511745f9f 100644 --- a/src/tools/chmod-calculator/chmod-calculator.service.ts +++ b/src/tools/chmod-calculator/chmod-calculator.service.ts @@ -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(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), + }; +} diff --git a/src/tools/chmod-calculator/chmod-calculator.types.ts b/src/tools/chmod-calculator/chmod-calculator.types.ts index d3dc915f9..30a7ad010 100644 --- a/src/tools/chmod-calculator/chmod-calculator.types.ts +++ b/src/tools/chmod-calculator/chmod-calculator.types.ts @@ -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 }; diff --git a/src/tools/chmod-calculator/chmod-calculator.vue b/src/tools/chmod-calculator/chmod-calculator.vue index ba6f4498e..93f9f3ae4 100644 --- a/src/tools/chmod-calculator/chmod-calculator.vue +++ b/src/tools/chmod-calculator/chmod-calculator.vue @@ -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 }));