From eea5943fe2df4af721bb89005c01cbf62f13abf6 Mon Sep 17 00:00:00 2001 From: Ed Date: Sat, 3 Oct 2026 17:09:28 +0300 Subject: [PATCH] fix: import bare Picture filenames as literal paths --- src/shared/vfp/importForm.ts | 9 +++- tests/shared/vfpPictureImport.test.ts | 59 +++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/shared/vfpPictureImport.test.ts diff --git a/src/shared/vfp/importForm.ts b/src/shared/vfp/importForm.ts index 9e14a59..498474e 100644 --- a/src/shared/vfp/importForm.ts +++ b/src/shared/vfp/importForm.ts @@ -1368,10 +1368,17 @@ function applyProperties( const meta = descriptor.properties.find((p) => p.name === declared)!; // a value that is worked out is remembered as the expression it is, and the property is // left at its default until the form loads and it can be worked out - if (entry.expression) { + // The designer also stores bare filenames for Picture. Restrict this exception to + // picture properties and filename-shaped values; variables and calls still evaluate. + const pictureFile = declared === 'Picture' && meta.editor === 'picture' && + !/^(?:m|this|thisform|thisformset|_screen|_vfp)\./i.test(entry.raw) && + /^[^"'()[\]\r\n+*?=<>|&]+\.(?:bmp|dib|jpg|jpeg|gif|png|ico|cur|emf|wmf|tif|tiff)$/i.test(entry.raw); + if (entry.expression && !pictureFile) { expressions[`${objectPath}.${declared}`] = entry.raw; continue; } + // A literal override must also replace an inherited/deferred expression. + if (declared === 'Picture') delete expressions[`${objectPath}.${declared}`]; // A property at its default is left out, which is what keeps an imported form from // carrying every property VFP has. But once a line has set one, a later line saying // "back to the default" has to be written down, or a class that inherits `PageCount = 1` diff --git a/tests/shared/vfpPictureImport.test.ts b/tests/shared/vfpPictureImport.test.ts new file mode 100644 index 0000000..98e4ada --- /dev/null +++ b/tests/shared/vfpPictureImport.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; +import { importFormTable } from '@shared/vfp/importForm'; +import type { DbfTableData } from '@shared/vfp/dbfTypes'; +import { Desktop } from '@shared/runtime/objectModel'; + +// Synthetic decoded SCX records; no application files or images are needed. +function importPicture(value: string, property = 'Picture') { + const names = ['PLATFORM', 'CLASS', 'BASECLASS', 'OBJNAME', 'PARENT', 'PROPERTIES']; + const table: DbfTableData = { + ok: true, version: 0x30, codepage: 1252, + fields: names.map(name => ({ name, kind: 'M', length: 4, decimals: 0 })), + records: [ + { deleted: false, values: ['WINDOWS', 'form', 'form', 'Form1', '', 'Name = "Form1"'] }, + { deleted: false, values: ['WINDOWS', 'image', 'image', 'Image1', 'Form1', `${property} = ${value}\r\nName = "Image1"`] }, + ], + }; + return importFormTable(table, 'sample'); +} + +describe('SCX image filenames', () => { + it.each(['sample.bmp', '..\\images\\sample.bmp', 'C:\\Sample Images\\logo.PNG', 'images/logo.jpg'])('imports %s as a literal Picture', (path) => { + const { doc, warnings } = importPicture(path); + expect(warnings).toEqual([]); + expect(doc.form.children[0]!.props.Picture).toBe(path); + expect(doc.meta?.vfp?.expressions?.['Form1.Image1.Picture']).toBeUndefined(); + }); + + it.each(['"sample.bmp"', "'sample.bmp'", '[sample.bmp]'])('preserves quoted literal %s', (value) => { + expect(importPicture(value).doc.form.children[0]!.props.Picture).toBe('sample.bmp'); + }); + + it.each(['(HOME() + "graphics\\sample.bmp")', 'm.imagePath', 'THISFORM.imagePath', 'THISFORM.logo.bmp', '&folder.sample.bmp', 'GETPICT()', '"images/" + m.imageName'])('keeps expression %s', (value) => { + const { doc } = importPicture(value); + expect(doc.meta?.vfp?.expressions?.['Form1.Image1.Picture']).toBe(value); + expect(doc.form.children[0]!.props.Picture).toBeUndefined(); + }); + + it('lets a later literal replace a deferred Picture expression', () => { + const { doc } = importPicture('(GETPICT())\r\nPicture = sample.bmp'); + expect(doc.form.children[0]!.props.Picture).toBe('sample.bmp'); + expect(doc.meta?.vfp?.expressions?.['Form1.Image1.Picture']).toBeUndefined(); + }); + + it('does not reinterpret an unrelated property with filename-like text', () => { + const { doc } = importPicture('sample.bmp', 'ToolTipText'); + expect(doc.meta?.vfp?.expressions?.['Form1.Image1.ToolTipText']).toBe('sample.bmp'); + }); + + it('makes the imported filename available at runtime without evaluating it', async () => { + const { doc } = importPicture('sample.bmp'); + const desktop = new Desktop(); + const instance = desktop.instantiate(doc.form, -1); + const evaluated: string[] = []; + desktop.evaluate = async (source) => { evaluated.push(source); throw new Error('Not an expression'); }; + await desktop.runFormLifecycle(instance, { expressions: doc.meta?.vfp?.expressions }); + expect(evaluated).toEqual([]); + expect(instance.child('Image1')!.get('Picture')).toBe('sample.bmp'); + }); +});