diff --git a/lib/permissions/minimally-encoded-thread-permissions.js b/lib/permissions/minimally-encoded-thread-permissions.js index 3bd0f8085..bcdf94dd0 100644 --- a/lib/permissions/minimally-encoded-thread-permissions.js +++ b/lib/permissions/minimally-encoded-thread-permissions.js @@ -1,174 +1,171 @@ // @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'; +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 = new Map( - Object.entries(baseRolePermissionEncoding).map(([key, value]) => [ - value, - key, - ]), +const inverseBaseRolePermissionEncoding = invertObjectToMap( + baseRolePermissionEncoding, ); // $FlowIssue bigint-unsupported -const inversePropagationPrefixes: Map = new Map( - Object.entries(propagationPrefixes).map(([key, value]) => [value, key]), -); +const inversePropagationPrefixes: Map = + invertObjectToMap(propagationPrefixes); + // $FlowIssue bigint-unsupported -const inverseFilterPrefixes: Map = new Map( - Object.entries(filterPrefixes).map(([key, value]) => [value, key]), -); +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}`; }; export { permissionsToBitmaskHex, hasPermission, rolePermissionToBitmaskHex, decodeRolePermissionBitmask, }; diff --git a/lib/utils/objects.js b/lib/utils/objects.js index 4bc05d788..0a96ab18f 100644 --- a/lib/utils/objects.js +++ b/lib/utils/objects.js @@ -1,140 +1,149 @@ // @flow import stableStringify from 'fast-json-stable-stringify'; import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual.js'; import _isPlainObject from 'lodash/fp/isPlainObject.js'; import stringHash from 'string-hash'; type ObjectMap = { +[key: K]: T }; type NestedObjectMap = { +[key: K]: T | NestedObjectMap }; function findMaximumDepth(obj: Object): ?{ path: string, depth: number } { let longestPath = null; let longestDepth = null; for (const key in obj) { const value = obj[key]; if (typeof value !== 'object' || !value) { if (!longestDepth) { longestPath = key; longestDepth = 1; } continue; } const childResult = findMaximumDepth(obj[key]); if (!childResult) { continue; } const { path, depth } = childResult; const ourDepth = depth + 1; if (longestDepth === null || ourDepth > longestDepth) { longestPath = `${key}.${path}`; longestDepth = ourDepth; } } if (!longestPath || !longestDepth) { return null; } return { path: longestPath, depth: longestDepth }; } function values(map: ObjectMap): T[] { return Object.values ? // https://github.com/facebook/flow/issues/2221 // $FlowFixMe - Object.values currently does not have good flow support Object.values(map) : Object.keys(map).map((key: K): T => map[key]); } function keys(map: ObjectMap): K[] { return Object.keys(map); } function entries(map: ObjectMap): [K, T][] { // $FlowFixMe - flow treats the values as mixed, but we know that they are T return Object.entries(map); } function assignValueWithKey( obj: NestedObjectMap, key: K, value: T | NestedObjectMap, ): NestedObjectMap { return { ...obj, ...Object.fromEntries([[key, value]]), }; } function hash(obj: ?Object): number { if (!obj) { return -1; } return stringHash(stableStringify(obj)); } // This function doesn't look at the order of the hashes inside of the array // e.g `combineUnorderedHashes([1,2,3]) === combineUnorderedHashes([3,1,2])` // so it should only be used if the hashes include their ordering in them // somehow (e.g. `RawThreadInfo` contains `id`) function combineUnorderedHashes(hashes: $ReadOnlyArray): number { return hashes.reduce((a, v) => a ^ v, 0); } // returns an object with properties from obj1 not included in obj2 function deepDiff( obj1: NestedObjectMap, obj2: NestedObjectMap, ): NestedObjectMap { let diff: NestedObjectMap = {}; keys(obj1).forEach((key: K) => { if (_isEqual(obj1[key], obj2[key])) { return; } if (!_isPlainObject(obj1[key]) || !_isPlainObject(obj2[key])) { diff = assignValueWithKey(diff, key, obj1[key]); return; } const nestedObj1: ObjectMap = (obj1[key]: any); const nestedObj2: ObjectMap = (obj2[key]: any); const nestedDiff = deepDiff(nestedObj1, nestedObj2); if (Object.keys(nestedDiff).length > 0) { diff = assignValueWithKey(diff, key, nestedDiff); } }); return diff; } function assertObjectsAreEqual( processedObject: ObjectMap, expectedObject: ObjectMap, message: string, ) { if (_isEqual(processedObject)(expectedObject)) { return; } const dataProcessedButNotExpected = deepDiff(processedObject, expectedObject); const dataExpectedButNotProcessed = deepDiff(expectedObject, processedObject); invariant( false, `${message}: Objects should be equal.` + ` Data processed but not expected:` + ` ${JSON.stringify(dataProcessedButNotExpected)}` + ` Data expected but not processed:` + ` ${JSON.stringify(dataExpectedButNotProcessed)}`, ); } +function invertObjectToMap(obj: { +[K]: V }): Map { + const invertedMap = new Map(); + for (const key of Object.keys(obj)) { + invertedMap.set(obj[key], key); + } + return invertedMap; +} + export { findMaximumDepth, values, hash, combineUnorderedHashes, assertObjectsAreEqual, deepDiff, entries, + invertObjectToMap, };