From 18c7b68354d00bed602902cfd6f35e67436d99aa Mon Sep 17 00:00:00 2001 From: Ido Zak Date: Mon, 2 Sep 2024 21:36:32 +0300 Subject: [PATCH] SALTO-6517: fix static file path collision --- packages/adapter-utils/src/nacl_case_utils.ts | 3 +++ .../test/nacl_case_utils.test.ts | 21 ++++++++++++++++++- .../jira-adapter/src/filters/icon_utils.ts | 4 ++-- .../filters/assets/object_type_icon.test.ts | 5 +++-- .../test/filters/issue_type.test.ts | 14 ++++++++----- packages/okta-adapter/src/logo.ts | 4 ++-- 6 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/adapter-utils/src/nacl_case_utils.ts b/packages/adapter-utils/src/nacl_case_utils.ts index 142fb96edb4..4cb5fcd32bf 100644 --- a/packages/adapter-utils/src/nacl_case_utils.ts +++ b/packages/adapter-utils/src/nacl_case_utils.ts @@ -25,6 +25,9 @@ const allCapsCamelCaseRegex = /[A-Z]([A-Z][a-z])/g export const pathNaclCase = (name?: string): string => (name ? name.split(NACL_ESCAPING_SUFFIX_SEPARATOR)[0] : '').slice(0, MAX_PATH_LENGTH) +// Converts a nacl case to a file system safe name. Use it (instead of pathNaclCase) if the file name must be unique +export const fileNameNaclCase = (name: string): string => name.replace('@', '.') + // Trim part of a file name to comply with filesystem restrictions // This assumes the filesystem does not allow path parts to be over // MAX_PATH_LENGTH long in byte length diff --git a/packages/adapter-utils/test/nacl_case_utils.test.ts b/packages/adapter-utils/test/nacl_case_utils.test.ts index b8f25313c28..da043c0f466 100644 --- a/packages/adapter-utils/test/nacl_case_utils.test.ts +++ b/packages/adapter-utils/test/nacl_case_utils.test.ts @@ -6,7 +6,14 @@ * CERTAIN THIRD PARTY SOFTWARE MAY BE CONTAINED IN PORTIONS OF THE SOFTWARE. See NOTICE FILE AT https://github.com/salto-io/salto/blob/main/NOTICES */ import _ from 'lodash' -import { invertNaclCase, naclCase, normalizeFilePathPart, pathNaclCase, prettifyName } from '../src/nacl_case_utils' +import { + invertNaclCase, + naclCase, + fileNameNaclCase, + normalizeFilePathPart, + pathNaclCase, + prettifyName, +} from '../src/nacl_case_utils' describe('naclCase utils', () => { const generateRandomChar = (): string => String.fromCharCode(Math.random() * 65535) @@ -137,6 +144,18 @@ describe('naclCase utils', () => { }) }) + describe('fileNameNaclCase func', () => { + it('should return empty string for empty input', () => { + expect(fileNameNaclCase('')).toEqual('') + }) + it('should replace @ with . at the end of the input', () => { + expect(fileNameNaclCase('name@')).toEqual('name.') + }) + it('should replace @ with . in the middle of the input', () => { + expect(fileNameNaclCase('name@name')).toEqual('name.name') + }) + }) + describe('normalizeStaticResourcePath func', () => { describe('With a short path', () => { const shortPaths = ['lalala.txt', 'aבגדe.טקסט', 'noExtension'] diff --git a/packages/jira-adapter/src/filters/icon_utils.ts b/packages/jira-adapter/src/filters/icon_utils.ts index 58ab219f8df..16aa7d1acbf 100644 --- a/packages/jira-adapter/src/filters/icon_utils.ts +++ b/packages/jira-adapter/src/filters/icon_utils.ts @@ -17,7 +17,7 @@ import { import _ from 'lodash' import { client as clientUtils } from '@salto-io/adapter-components' import Joi from 'joi' -import { createSchemeGuard, naclCase, pathNaclCase } from '@salto-io/adapter-utils' +import { createSchemeGuard, fileNameNaclCase } from '@salto-io/adapter-utils' import { JIRA } from '../constants' import JiraClient from '../client/client' @@ -75,7 +75,7 @@ export const setIconContent = async ({ }): Promise => { const iconContent = await getIconContent(link, client) instance.value[fieldName] = new StaticFile({ - filepath: `${JIRA}/${instance.elemID.typeName}/${pathNaclCase(naclCase(instance.value.name))}.png`, + filepath: `${JIRA}/${instance.elemID.typeName}/${fileNameNaclCase(instance.elemID.name)}.png`, content: iconContent, }) } diff --git a/packages/jira-adapter/test/filters/assets/object_type_icon.test.ts b/packages/jira-adapter/test/filters/assets/object_type_icon.test.ts index ebb99c66948..e7f726f74d4 100644 --- a/packages/jira-adapter/test/filters/assets/object_type_icon.test.ts +++ b/packages/jira-adapter/test/filters/assets/object_type_icon.test.ts @@ -8,6 +8,7 @@ import { filterUtils, client as clientUtils } from '@salto-io/adapter-components' import { InstanceElement, Element, StaticFile, Values } from '@salto-io/adapter-api' import _ from 'lodash' +import { naclCase } from '@salto-io/adapter-utils' import { createEmptyType, getFilterParams, mockClient } from '../../utils' import JiraClient from '../../../src/client/client' import objectTypeIconFilter from '../../../src/filters/assets/object_type_icon' @@ -43,7 +44,7 @@ describe('object type icon filter', () => { }) describe('on fetch', () => { beforeEach(async () => { - objectTypeIconInstance = new InstanceElement('objectType1', objectTypeIconType, { + objectTypeIconInstance = new InstanceElement(naclCase('object_type:1'), objectTypeIconType, { name: 'objectTypeIconName', id: 12, }) @@ -78,7 +79,7 @@ describe('object type icon filter', () => { name: 'objectTypeIconName', id: 12, icon: new StaticFile({ - filepath: 'jira/ObjectTypeIcon/objectTypeIconName.png', + filepath: 'jira/ObjectTypeIcon/object_type_1.uf.png', encoding: 'binary', content, }), diff --git a/packages/jira-adapter/test/filters/issue_type.test.ts b/packages/jira-adapter/test/filters/issue_type.test.ts index 48b7f55c712..d9b12fdff93 100644 --- a/packages/jira-adapter/test/filters/issue_type.test.ts +++ b/packages/jira-adapter/test/filters/issue_type.test.ts @@ -7,6 +7,7 @@ */ import { filterUtils, client as clientUtils } from '@salto-io/adapter-components' import { BuiltinTypes, ElemID, InstanceElement, ObjectType, StaticFile, toChange } from '@salto-io/adapter-api' +import { naclCase } from '@salto-io/adapter-utils' import { ISSUE_TYPE_NAME, JIRA } from '../../src/constants' import { getFilterParams, mockClient } from '../utils' import issueTypeFilter from '../../src/filters/issue_type' @@ -105,8 +106,11 @@ describe('issueTypeFilter', () => { mockGet.mockClear() }) it('should set icon content', async () => { - const anotherInstance = instance.clone() - anotherInstance.value.name = 'anotherInstance' + const anotherInstance = new InstanceElement(naclCase('another instance'), issueType, { + name: 'anotherInstance', + description: 'anotherInstanceDescription', + avatarId: 1, + }) mockGet.mockImplementation(params => { if (params.url === '/rest/api/3/universal_avatar/view/type/issuetype/avatar/1') { return { @@ -120,7 +124,7 @@ describe('issueTypeFilter', () => { expect(instance.value.avatar).toBeDefined() expect(instance.value.avatar).toEqual( new StaticFile({ - filepath: 'jira/IssueType/instanceName.png', + filepath: 'jira/IssueType/instance.png', encoding: 'binary', content, }), @@ -128,7 +132,7 @@ describe('issueTypeFilter', () => { expect(anotherInstance.value.avatar).toBeDefined() expect(anotherInstance.value.avatar).toEqual( new StaticFile({ - filepath: 'jira/IssueType/anotherInstance.png', + filepath: 'jira/IssueType/another_instance.s.png', encoding: 'binary', content, }), @@ -149,7 +153,7 @@ describe('issueTypeFilter', () => { expect(instance.value.avatar).toBeDefined() expect(instance.value.avatar).toEqual( new StaticFile({ - filepath: 'jira/IssueType/instanceName.png', + filepath: 'jira/IssueType/instance.png', encoding: 'binary', content: Buffer.from('a string, not a buffer.'), }), diff --git a/packages/okta-adapter/src/logo.ts b/packages/okta-adapter/src/logo.ts index b6a5cbf62dd..b2943cbd2b6 100644 --- a/packages/okta-adapter/src/logo.ts +++ b/packages/okta-adapter/src/logo.ts @@ -23,7 +23,7 @@ import { } from '@salto-io/adapter-api' import FormData from 'form-data' import { elements as elementsUtils } from '@salto-io/adapter-components' -import { getParent, getParents, normalizeFilePathPart, pathNaclCase } from '@salto-io/adapter-utils' +import { getParent, getParents, fileNameNaclCase, normalizeFilePathPart, pathNaclCase } from '@salto-io/adapter-utils' import OktaClient from './client/client' import { getOktaError } from './deprecated_deployment' import { APP_LOGO_TYPE_NAME, BRAND_LOGO_TYPE_NAME, FAV_ICON_TYPE_NAME, OKTA } from './constants' @@ -159,7 +159,7 @@ export const getLogo = async ({ } // Use the full NaCL name (including suffix) to avoid naming collisions, but replace '@' with '.' to ensure file // names are valid across all operating systems. - const resourcePathName = `${normalizeFilePathPart(logoName.replace('@', '.'))}.${contentType}` + const resourcePathName = `${normalizeFilePathPart(fileNameNaclCase(logoName))}.${contentType}` const logoId = extractIdFromUrl(link) const refParents = parents.map(parent => new ReferenceExpression(parent.elemID, parent)) const logo = new InstanceElement(