diff --git a/lib/permissions/minimally-encoded-thread-permissions.js b/lib/permissions/minimally-encoded-thread-permissions.js index 3d27a0c77..5040d761d 100644 --- a/lib/permissions/minimally-encoded-thread-permissions.js +++ b/lib/permissions/minimally-encoded-thread-permissions.js @@ -1,228 +1,220 @@ // @flow import invariant from 'invariant'; import { parseThreadPermissionString } from './prefixes.js'; import type { ThreadPermission, ThreadPermissionInfo, ThreadPermissionsInfo, ThreadRolePermissionsBlob, } from '../types/thread-permission-types.js'; import { threadPermissions } from '../types/thread-permission-types.js'; import { entries, invertObjectToMap } from '../utils/objects.js'; import type { TRegex } from '../utils/validation-utils.js'; import { tRegex } from '../utils/validation-utils.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), voiced_in_announcement_channels: BigInt(21), }); // `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 tHexEncodedRolePermission: TRegex = tRegex(/^[0-9a-fA-F]{3,}$/); const threadPermissionsFromBitmaskHex = ( permissionsBitmaskHex: string, ): ThreadPermissionsInfo => { invariant( tHexEncodedRolePermission.is(permissionsBitmaskHex), 'permissionsBitmaskHex must be valid hex string.', ); const permissionsBitmask = BigInt(`0x${permissionsBitmaskHex}`); const permissions: { [permission: ThreadPermission]: ThreadPermissionInfo } = {}; for (const [key, permissionBitmask] of entries( minimallyEncodedThreadPermissions, )) { if ((permissionsBitmask & permissionBitmask) !== BigInt(0)) { permissions[key] = { value: true, source: 'null' }; } else { permissions[key] = { value: false, source: null }; } } return permissions; }; const hasPermission = ( permissionsBitmaskHex: string, permission: ThreadPermission, ): boolean => { const permissionsBitmask = BigInt(`0x${permissionsBitmaskHex}`); if (!(permission in minimallyEncodedThreadPermissions)) { return false; } const permissionBitmask = minimallyEncodedThreadPermissions[permission]; const knowOfBitmask = minimallyEncodedThreadPermissions[threadPermissions.KNOW_OF]; 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) && (permissionsBitmask & knowOfBitmask) !== 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 tHexEncodedPermissionsBitmask: TRegex = tRegex(/^[0-9a-fA-F]+$/); 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, threadPermissionsFromBitmaskHex, hasPermission, rolePermissionToBitmaskHex, decodeRolePermissionBitmask, threadRolePermissionsBlobToBitmaskArray, decodeThreadRolePermissionsBitmaskArray, tHexEncodedRolePermission, tHexEncodedPermissionsBitmask, }; diff --git a/lib/utils/objects.test.js b/lib/utils/objects.test.js index 90eed8dce..5c45597a4 100644 --- a/lib/utils/objects.test.js +++ b/lib/utils/objects.test.js @@ -1,155 +1,151 @@ // @flow import { deepDiff, invertObjectToMap } from './objects.js'; describe('deepDiff tests', () => { it('should return an empty object if the objects are identical', () => { const obj1 = { key1: 'value1', key2: { foo: 'bar' }, }; const obj2 = { key1: 'value1', key2: { foo: 'bar' }, }; const diff = deepDiff(obj1, obj2); expect(diff).toEqual({}); }); it('should return the differences between two objects', () => { const obj1 = { key1: 'value1', key2: { prop: 'a' }, }; const obj2 = { key1: 'value2', key2: { prop: 'b' }, }; const diff = deepDiff(obj1, obj2); expect(diff).toEqual({ key1: 'value1', key2: { prop: 'a', }, }); }); it('should handle objects nested in objects', () => { const obj1 = { key1: 'value1', key2: { prop: 'a', nested: { xyz: 123 } }, }; const obj2 = { key1: 'value1', key2: { prop: 'a', nested: { xyz: 124 } }, }; const diff = deepDiff(obj1, obj2); expect(diff).toEqual({ key2: { nested: { xyz: 123, }, }, }); }); it('should handle nested objects with null and undefined values', () => { const obj1 = { key1: null, key2: { prop: undefined }, }; const obj2 = { key1: undefined, key2: { prop: null }, }; const diff = deepDiff(obj1, obj2); expect(diff).toEqual({ key1: null, key2: { prop: undefined, }, }); }); it('should handle objects with different value types', () => { const obj1 = { key1: 'value1', key2: 123, }; const obj2 = { key1: 'value1', key2: '123', }; const diff = deepDiff(obj1, obj2); expect(diff).toEqual({ key2: 123, }); }); it('should handle objects with array value types', () => { const obj1 = { key1: ['value1'], key2: ['a', 1], }; const obj2 = { key1: ['value1'], key2: ['a', 2], }; const diff = deepDiff(obj1, obj2); expect(diff).toEqual({ key2: ['a', 1], }); }); }); // NOTE: `invertObjectToMap` unit tests were generated by GitHub Copilot. describe('invertObjectToMap', () => { it('should invert an object to a map', () => { const obj = { key1: 'value1', key2: 'value2', }; const map = new Map(); map.set('value1', 'key1'); map.set('value2', 'key2'); expect(invertObjectToMap(obj)).toEqual(map); }); it('should invert an object with non-string keys to a map', () => { const obj = { key1: 1, key2: 2, }; const map = new Map(); map.set(1, 'key1'); map.set(2, 'key2'); expect(invertObjectToMap(obj)).toEqual(map); }); it('should invert an object with BigInt values to map with BigInt keys', () => { const obj = { - // $FlowIssue bigint-unsupported key1: 1n, - // $FlowIssue bigint-unsupported key2: 2n, }; const map = new Map(); - // $FlowIssue bigint-unsupported map.set(1n, 'key1'); - // $FlowIssue bigint-unsupported map.set(2n, 'key2'); expect(invertObjectToMap(obj)).toEqual(map); }); it('should invert an object with null values to map with null keys', () => { const obj = { key1: null, key2: null, }; const map = new Map(); map.set(null, 'key2'); expect(() => invertObjectToMap(obj)).toThrowError( 'invertObjectToMap: obj[key2] is already in invertedMap', ); }); });