diff --git a/lib/permissions/minimally-encoded-thread-permissions.js b/lib/permissions/minimally-encoded-thread-permissions.js index a7ee37c86..b3dee9d7f 100644 --- a/lib/permissions/minimally-encoded-thread-permissions.js +++ b/lib/permissions/minimally-encoded-thread-permissions.js @@ -1,77 +1,129 @@ // @flow import invariant from 'invariant'; +import { parseThreadPermissionString } from './prefixes.js'; import type { ThreadPermission, ThreadPermissionsInfo, } from '../types/thread-permission-types.js'; import { entries } from '../utils/objects.js'; -const minimallyEncodedThreadPermissions = Object.freeze({ +// `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(1) << BigInt(0), - visible: BigInt(1) << BigInt(1), - voiced: BigInt(1) << BigInt(2), - edit_entries: BigInt(1) << BigInt(3), - edit_thread: BigInt(1) << BigInt(4), // EDIT_THREAD_NAME - edit_thread_description: BigInt(1) << BigInt(5), - edit_thread_color: BigInt(1) << BigInt(6), - delete_thread: BigInt(1) << BigInt(7), - create_subthreads: BigInt(1) << BigInt(8), // CREATE_SUBCHANNELS - create_sidebars: BigInt(1) << BigInt(9), - join_thread: BigInt(1) << BigInt(10), - edit_permissions: BigInt(1) << BigInt(11), - add_members: BigInt(1) << BigInt(12), - remove_members: BigInt(1) << BigInt(13), - change_role: BigInt(1) << BigInt(14), - leave_thread: BigInt(1) << BigInt(15), - react_to_message: BigInt(1) << BigInt(16), - edit_message: BigInt(1) << BigInt(17), - edit_thread_avatar: BigInt(1) << BigInt(18), - manage_pins: BigInt(1) << BigInt(19), - manage_invite_links: BigInt(1) << BigInt(20), + 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); }; -export { - minimallyEncodedThreadPermissions, - permissionsToBitmaskHex, - hasPermission, +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'); }; + +export { permissionsToBitmaskHex, hasPermission, rolePermissionToBitmaskHex }; diff --git a/lib/permissions/minimally-encoded-thread-permissions.test.js b/lib/permissions/minimally-encoded-thread-permissions.test.js index 539e4dc2c..090c5c125 100644 --- a/lib/permissions/minimally-encoded-thread-permissions.test.js +++ b/lib/permissions/minimally-encoded-thread-permissions.test.js @@ -1,53 +1,90 @@ // @flow import { hasPermission, permissionsToBitmaskHex, + rolePermissionToBitmaskHex, } from './minimally-encoded-thread-permissions.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'); + }); +});