diff --git a/lib/permissions/minimally-encoded-thread-permissions.js b/lib/permissions/minimally-encoded-thread-permissions.js index 85d03bd9b..b414d9162 100644 --- a/lib/permissions/minimally-encoded-thread-permissions.js +++ b/lib/permissions/minimally-encoded-thread-permissions.js @@ -1,178 +1,189 @@ // @flow import invariant from 'invariant'; import { parseThreadPermissionString } from './prefixes.js'; import type { ThreadPermission, ThreadPermissionsInfo, ThreadRolePermissionsBlob, } from '../types/thread-permission-types.js'; import { entries, invertObjectToMap } from '../utils/objects.js'; // `baseRolePermissionEncoding` maps permission names to indices. // These indices represent the 6-bit basePermission part of the 10-bit role // permission encoding created by `rolePermissionToBitmaskHex`. // The 6-bit basePermission allows for up to 2^6 = 64 different permissions. // If more than 64 permissions are needed, the encoding in // `rolePermissionToBitmaskHex` will need to be updated to accommodate this. const baseRolePermissionEncoding = Object.freeze({ // TODO (atul): Update flow to `194.0.0` for bigint support // $FlowIssue bigint-unsupported know_of: BigInt(0), visible: BigInt(1), voiced: BigInt(2), edit_entries: BigInt(3), edit_thread: BigInt(4), // EDIT_THREAD_NAME edit_thread_description: BigInt(5), edit_thread_color: BigInt(6), delete_thread: BigInt(7), create_subthreads: BigInt(8), // CREATE_SUBCHANNELS create_sidebars: BigInt(9), join_thread: BigInt(10), edit_permissions: BigInt(11), add_members: BigInt(12), remove_members: BigInt(13), change_role: BigInt(14), leave_thread: BigInt(15), react_to_message: BigInt(16), edit_message: BigInt(17), edit_thread_avatar: BigInt(18), manage_pins: BigInt(19), manage_invite_links: BigInt(20), }); // `minimallyEncodedThreadPermissions` is used to map each permission // to its respective bitmask where the index from `baseRolePermissionEncoding` // is used to set a specific bit in the bitmask. This is used in the // `permissionsToBitmaskHex` function where each permission is represented as a // single bit and the final bitmask is the union of all granted permissions. const minimallyEncodedThreadPermissions = Object.fromEntries( Object.keys(baseRolePermissionEncoding).map((key, idx) => [ key, BigInt(1) << BigInt(idx), ]), ); // This function converts a set of permissions to a hex-encoded bitmask. // Each permission is represented as a single bit in the bitmask. const permissionsToBitmaskHex = ( permissions: ThreadPermissionsInfo, ): string => { let bitmask = BigInt(0); for (const [key, permission] of entries(permissions)) { if (permission.value && key in minimallyEncodedThreadPermissions) { invariant( // TODO (atul): Update flow to `194.0.0` for bigint support // $FlowIssue illegal-typeof typeof minimallyEncodedThreadPermissions[key] === 'bigint', 'must be bigint', ); bitmask |= minimallyEncodedThreadPermissions[key]; } } return bitmask.toString(16); }; const hasPermission = ( permissionsBitmaskHex: string, permission: ThreadPermission, ): boolean => { const permissionsBitmask = BigInt(`0x${permissionsBitmaskHex}`); if (!(permission in minimallyEncodedThreadPermissions)) { return false; } const permissionBitmask = minimallyEncodedThreadPermissions[permission]; invariant( // TODO (atul): Update flow to `194.0.0` for bigint support // $FlowIssue illegal-typeof typeof permissionBitmask === 'bigint', 'permissionBitmask must be of type bigint', ); return (permissionsBitmask & permissionBitmask) !== BigInt(0); }; const propagationPrefixes = Object.freeze({ '': BigInt(0), 'descendant_': BigInt(1), 'child_': BigInt(2), }); const filterPrefixes = Object.freeze({ '': BigInt(0), 'open_': BigInt(1), 'toplevel_': BigInt(2), 'opentoplevel_': BigInt(3), }); // Role Permission Bitmask Structure // [9 8 7 6 5 4 3 2 1 0] - bit positions // [b b b b b b p p f f] - symbol representation // b = basePermission (6 bits) // p = propagationPrefix (2 bits) // f = filterPrefix (2 bits) const rolePermissionToBitmaskHex = (threadRolePermission: string): string => { const parsed = parseThreadPermissionString(threadRolePermission); const basePermissionBits = baseRolePermissionEncoding[parsed.permission] & BigInt(63); const propagationPrefixBits = propagationPrefixes[parsed.propagationPrefix ?? ''] & BigInt(3); const filterPrefixBits = filterPrefixes[parsed.filterPrefix ?? ''] & BigInt(3); const bitmask = (basePermissionBits << BigInt(4)) | (propagationPrefixBits << BigInt(2)) | filterPrefixBits; return bitmask.toString(16).padStart(3, '0'); }; const inverseBaseRolePermissionEncoding = invertObjectToMap( baseRolePermissionEncoding, ); // $FlowIssue bigint-unsupported const inversePropagationPrefixes: Map = invertObjectToMap(propagationPrefixes); // $FlowIssue bigint-unsupported const inverseFilterPrefixes: Map = invertObjectToMap(filterPrefixes); const decodeRolePermissionBitmask = (bitmask: string): string => { const bitmaskInt = BigInt(`0x${bitmask}`); const basePermission = (bitmaskInt >> BigInt(4)) & BigInt(63); const propagationPrefix = (bitmaskInt >> BigInt(2)) & BigInt(3); const filterPrefix = bitmaskInt & BigInt(3); const basePermissionString = inverseBaseRolePermissionEncoding.get(basePermission); const propagationPrefixString = inversePropagationPrefixes.get(propagationPrefix) ?? ''; const filterPrefixString = inverseFilterPrefixes.get(filterPrefix) ?? ''; invariant( basePermissionString !== null && basePermissionString !== undefined && propagationPrefixString !== null && propagationPrefixString !== undefined && filterPrefixString !== null && filterPrefixString !== undefined, 'invalid bitmask', ); return `${propagationPrefixString}${filterPrefixString}${basePermissionString}`; }; const threadRolePermissionsBlobToBitmaskArray = ( threadRolePermissionsBlob: ThreadRolePermissionsBlob, ): $ReadOnlyArray => Object.keys(threadRolePermissionsBlob).map(rolePermissionToBitmaskHex); +const decodeThreadRolePermissionsBitmaskArray = ( + threadRolePermissionsBitmaskArray: $ReadOnlyArray, +): ThreadRolePermissionsBlob => + Object.fromEntries( + threadRolePermissionsBitmaskArray.map(bitmask => [ + decodeRolePermissionBitmask(bitmask), + true, + ]), + ); + export { permissionsToBitmaskHex, hasPermission, rolePermissionToBitmaskHex, decodeRolePermissionBitmask, threadRolePermissionsBlobToBitmaskArray, + decodeThreadRolePermissionsBitmaskArray, }; diff --git a/lib/permissions/minimally-encoded-thread-permissions.test.js b/lib/permissions/minimally-encoded-thread-permissions.test.js index 18ed63720..4eabf77cc 100644 --- a/lib/permissions/minimally-encoded-thread-permissions.test.js +++ b/lib/permissions/minimally-encoded-thread-permissions.test.js @@ -1,209 +1,223 @@ // @flow import { decodeRolePermissionBitmask, + decodeThreadRolePermissionsBitmaskArray, hasPermission, permissionsToBitmaskHex, rolePermissionToBitmaskHex, threadRolePermissionsBlobToBitmaskArray, } from './minimally-encoded-thread-permissions.js'; import type { ThreadRolePermissionsBlob } from '../types/thread-permission-types.js'; describe('minimallyEncodedThreadPermissions', () => { const permissions = { know_of: { value: true, source: '1' }, visible: { value: true, source: '1' }, voiced: { value: true, source: '1' }, edit_entries: { value: true, source: '1' }, edit_thread: { value: true, source: '1' }, edit_thread_description: { value: true, source: '1' }, edit_thread_color: { value: true, source: '1' }, delete_thread: { value: true, source: '1' }, create_subthreads: { value: true, source: '1' }, create_sidebars: { value: true, source: '1' }, join_thread: { value: false, source: null }, edit_permissions: { value: false, source: null }, add_members: { value: true, source: '1' }, remove_members: { value: true, source: '1' }, change_role: { value: true, source: '1' }, leave_thread: { value: false, source: null }, react_to_message: { value: true, source: '1' }, edit_message: { value: true, source: '1' }, }; it('should encode ThreadPermissionsInfo as bitmask', () => { const permissionsBitmask = permissionsToBitmaskHex(permissions); expect(permissionsBitmask).toBe('373ff'); expect(hasPermission(permissionsBitmask, 'know_of')).toBe(true); expect(hasPermission(permissionsBitmask, 'visible')).toBe(true); expect(hasPermission(permissionsBitmask, 'voiced')).toBe(true); expect(hasPermission(permissionsBitmask, 'edit_entries')).toBe(true); expect(hasPermission(permissionsBitmask, 'edit_thread')).toBe(true); expect(hasPermission(permissionsBitmask, 'edit_thread_description')).toBe( true, ); expect(hasPermission(permissionsBitmask, 'edit_thread_color')).toBe(true); expect(hasPermission(permissionsBitmask, 'delete_thread')).toBe(true); expect(hasPermission(permissionsBitmask, 'create_subthreads')).toBe(true); expect(hasPermission(permissionsBitmask, 'create_sidebars')).toBe(true); expect(hasPermission(permissionsBitmask, 'join_thread')).toBe(false); expect(hasPermission(permissionsBitmask, 'edit_permissions')).toBe(false); expect(hasPermission(permissionsBitmask, 'remove_members')).toBe(true); expect(hasPermission(permissionsBitmask, 'change_role')).toBe(true); expect(hasPermission(permissionsBitmask, 'leave_thread')).toBe(false); expect(hasPermission(permissionsBitmask, 'react_to_message')).toBe(true); expect(hasPermission(permissionsBitmask, 'edit_message')).toBe(true); }); }); describe('rolePermissionToBitmaskHex', () => { it('should encode `child_opentoplevel_visible` successfully', () => { expect(rolePermissionToBitmaskHex('child_opentoplevel_visible')).toBe( '01b', ); }); it('should encode `child_opentoplevel_know_of` successfully', () => { expect(rolePermissionToBitmaskHex('child_opentoplevel_know_of')).toBe( '00b', ); }); it('should encode `child_toplevel_visible` successfully', () => { expect(rolePermissionToBitmaskHex('child_toplevel_visible')).toBe('01a'); }); it('should encode `child_toplevel_know_of` successfully', () => { expect(rolePermissionToBitmaskHex('child_toplevel_know_of')).toBe('00a'); }); it('should encode `child_opentoplevel_join_thread` successfully', () => { expect(rolePermissionToBitmaskHex('child_opentoplevel_join_thread')).toBe( '0ab', ); }); it('should encode `child_visible` successfully', () => { expect(rolePermissionToBitmaskHex('child_visible')).toBe('018'); }); it('should encode `child_know_of` successfully', () => { expect(rolePermissionToBitmaskHex('child_know_of')).toBe('008'); }); }); describe('decodeRolePermissionBitmask', () => { it('should decode `01b` to `child_opentoplevel_visible` successfully', () => { expect(decodeRolePermissionBitmask('01b')).toBe( 'child_opentoplevel_visible', ); }); it('should decode `00b` to `child_opentoplevel_know_of` successfully', () => { expect(decodeRolePermissionBitmask('00b')).toBe( 'child_opentoplevel_know_of', ); }); it('should decode `01a` to `child_toplevel_visible` successfully', () => { expect(decodeRolePermissionBitmask('01a')).toBe('child_toplevel_visible'); }); it('should decode `00a` to `child_toplevel_know_of` successfully', () => { expect(decodeRolePermissionBitmask('00a')).toBe('child_toplevel_know_of'); }); it('should decode `0ab` to `child_opentoplevel_join_thread` successfully', () => { expect(decodeRolePermissionBitmask('0ab')).toBe( 'child_opentoplevel_join_thread', ); }); it('should decode `018` to `child_visible` successfully', () => { expect(decodeRolePermissionBitmask('018')).toBe('child_visible'); }); it('should decode `008` to `child_know_of` successfully', () => { expect(decodeRolePermissionBitmask('008')).toBe('child_know_of'); }); }); -describe('threadRolePermissionsBlobToBitmaskArray', () => { - const permissions: ThreadRolePermissionsBlob = { - add_members: true, - child_open_join_thread: true, - create_sidebars: true, - create_subthreads: true, - descendant_open_know_of: true, - descendant_open_visible: true, - descendant_opentoplevel_join_thread: true, - edit_entries: true, - edit_message: true, - edit_permissions: true, - edit_thread: true, - edit_thread_avatar: true, - edit_thread_color: true, - edit_thread_description: true, - know_of: true, - leave_thread: true, - react_to_message: true, - remove_members: true, - visible: true, - voiced: true, - open_know_of: true, - open_visible: true, - opentoplevel_join_thread: true, - toplevel_know_of: true, - toplevel_visible: true, - opentoplevel_know_of: true, - opentoplevel_visible: true, - child_know_of: true, - child_visible: true, - child_opentoplevel_join_thread: true, - child_toplevel_know_of: true, - child_toplevel_visible: true, - child_opentoplevel_know_of: true, - child_opentoplevel_visible: true, - }; +const threadRolePermissionsBlob: ThreadRolePermissionsBlob = { + add_members: true, + child_open_join_thread: true, + create_sidebars: true, + create_subthreads: true, + descendant_open_know_of: true, + descendant_open_visible: true, + descendant_opentoplevel_join_thread: true, + edit_entries: true, + edit_message: true, + edit_permissions: true, + edit_thread: true, + edit_thread_avatar: true, + edit_thread_color: true, + edit_thread_description: true, + know_of: true, + leave_thread: true, + react_to_message: true, + remove_members: true, + visible: true, + voiced: true, + open_know_of: true, + open_visible: true, + opentoplevel_join_thread: true, + toplevel_know_of: true, + toplevel_visible: true, + opentoplevel_know_of: true, + opentoplevel_visible: true, + child_know_of: true, + child_visible: true, + child_opentoplevel_join_thread: true, + child_toplevel_know_of: true, + child_toplevel_visible: true, + child_opentoplevel_know_of: true, + child_opentoplevel_visible: true, +}; + +const threadRolePermissionsBitmaskArray = [ + '0c0', + '0a9', + '090', + '080', + '005', + '015', + '0a7', + '030', + '110', + '0b0', + '040', + '120', + '060', + '050', + '000', + '0f0', + '100', + '0d0', + '010', + '020', + '001', + '011', + '0a3', + '002', + '012', + '003', + '013', + '008', + '018', + '0ab', + '00a', + '01a', + '00b', + '01b', +]; +describe('threadRolePermissionsBlobToBitmaskArray', () => { it('should encode threadRolePermissionsBlob as bitmask array', () => { - const threadRolePermissionsBitmaskArray = - threadRolePermissionsBlobToBitmaskArray(permissions); - expect(threadRolePermissionsBitmaskArray).toEqual([ - '0c0', - '0a9', - '090', - '080', - '005', - '015', - '0a7', - '030', - '110', - '0b0', - '040', - '120', - '060', - '050', - '000', - '0f0', - '100', - '0d0', - '010', - '020', - '001', - '011', - '0a3', - '002', - '012', - '003', - '013', - '008', - '018', - '0ab', - '00a', - '01a', - '00b', - '01b', - ]); + const arr = threadRolePermissionsBlobToBitmaskArray( + threadRolePermissionsBlob, + ); + expect(arr).toEqual(threadRolePermissionsBitmaskArray); + }); +}); + +describe('decodeThreadRolePermissionsBitmaskArray', () => { + it('should decode threadRolePermissionsBitmaskArray', () => { + expect( + decodeThreadRolePermissionsBitmaskArray( + threadRolePermissionsBitmaskArray, + ), + ).toEqual(threadRolePermissionsBlob); }); });