From e55294faf2223872dc842d44fc55afe4837d2a08 Mon Sep 17 00:00:00 2001 From: Maxime Gervais Date: Fri, 28 Aug 2026 00:06:54 +0200 Subject: [PATCH 1/6] Fix lost identifiers and devices across multiple save Signed-off-by: Maxime Gervais --- .../embarc/parser/mxf/DeviceSetHelper.java | 2 +- .../parser/mxf/IdentifierSetHelper.java | 2 +- .../embarc/parser/mxf/MXFServiceImpl.java | 98 ++++++++++++++++++- 3 files changed, 97 insertions(+), 5 deletions(-) diff --git a/src/main/com/portalmedia/embarc/parser/mxf/DeviceSetHelper.java b/src/main/com/portalmedia/embarc/parser/mxf/DeviceSetHelper.java index 6800a5c..b0822ff 100644 --- a/src/main/com/portalmedia/embarc/parser/mxf/DeviceSetHelper.java +++ b/src/main/com/portalmedia/embarc/parser/mxf/DeviceSetHelper.java @@ -15,7 +15,7 @@ public ArrayList createDeviceListFromString(String ArrayList devices = new ArrayList(); String[] valList = values.split(slash); for (String v : valList) { - if (v != "") devices.add(createDeviceFromString(v)); + if (!v.isEmpty()) devices.add(createDeviceFromString(v)); } return devices; } diff --git a/src/main/com/portalmedia/embarc/parser/mxf/IdentifierSetHelper.java b/src/main/com/portalmedia/embarc/parser/mxf/IdentifierSetHelper.java index dbb230f..c1870a5 100644 --- a/src/main/com/portalmedia/embarc/parser/mxf/IdentifierSetHelper.java +++ b/src/main/com/portalmedia/embarc/parser/mxf/IdentifierSetHelper.java @@ -14,7 +14,7 @@ public ArrayList createIdentifierListFromString(String ArrayList idents = new ArrayList(); String[] valList = values.split(slash); for (String v : valList) { - if (v != "") idents.add(createIdentifierFromString(v)); + if (!v.isEmpty()) idents.add(createIdentifierFromString(v)); } return idents; } diff --git a/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java b/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java index 7c599ba..5c9ec70 100644 --- a/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java +++ b/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java @@ -17,8 +17,10 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.logging.Level; @@ -405,7 +407,8 @@ private static AUID findInstanceUID(byte[] data, int bodyStart, int bodyEnd) { return null; } - private static byte[] encodeCoreDMSLocalSet(AS07CoreDMSFramework framework, AUID instanceID, PrimerPack primerPack) + private static byte[] encodeCoreDMSLocalSet(AS07CoreDMSFramework framework, AUID instanceID, PrimerPack primerPack, + Map preservedRawProperties) throws IOException, tv.amwa.maj.exception.InsufficientSpaceException { ClassDefinition classDef = Warehouse.lookForClass(AS07CoreDMSFrameworkImpl.class); SortedMap properties = classDef.getProperties(framework); @@ -422,8 +425,15 @@ private static byte[] encodeCoreDMSLocalSet(AS07CoreDMSFramework framework, AUID bodyOut.write(shortToBytes((short) 16)); bodyOut.write(instanceID.getAUIDValue()); + Set written = new HashSet(); for (PropertyDefinition property : properties.keySet()) { if (property.getAUID().equals(CommonConstants.ObjectClassID)) continue; + byte[] preserved = preservedRawProperties.get(property.getName()); + if (preserved != null) { + bodyOut.write(preserved); + written.add(property.getName()); + continue; + } PropertyValue value = properties.get(property); Short localTag = primerPack.lookupLocalTag(property.getAUID()); long predictedLength = value.getType().lengthAsBytes(value); @@ -434,6 +444,11 @@ private static byte[] encodeCoreDMSLocalSet(AS07CoreDMSFramework framework, AUID bodyOut.write(shortToBytes((short) actualLength)); bodyOut.write(valueBuffer.array(), 0, actualLength); } + // A preserved strong reference vector may not have round-tripped into `properties` + // (its getter throws internally for an empty-but-present list), so write it directly. + for (Map.Entry entry : preservedRawProperties.entrySet()) { + if (!written.contains(entry.getKey())) bodyOut.write(entry.getValue()); + } byte[] bodyBytes = bodyOut.toByteArray(); ByteArrayOutputStream out = new ByteArrayOutputStream(bodyBytes.length + 24); @@ -455,6 +470,69 @@ private static short findFreeLocalTag(PrimerPack primerPack) throws IOException throw new IOException("Could not find a free local tag to add a new Core DMS property."); } + // Strong reference vector properties: the generic writer below mints a fresh random UID for + // each entry instead of the sub-object's real one, so unchanged values must reuse the original TLV bytes. + private static final String[] STRONG_REFERENCE_VECTOR_PROPERTIES = { "Identifiers", "Devices" }; + + private static String safeIdentifiersString(AS07CoreDMSFramework framework) { + try { + return new IdentifierSetHelper().identifiersToString(framework.getIdentifiers()); + } catch (Exception ex) { + return ""; + } + } + + private static String safeDevicesString(AS07CoreDMSFramework framework) { + try { + return new DeviceSetHelper().devicesToString(framework.getDevices()); + } catch (Exception ex) { + return ""; + } + } + + private static PropertyDefinition findPropertyDefinition(ClassDefinition classDef, String name) { + for (PropertyDefinition property : classDef.getAllPropertyDefinitions()) { + if (property.getName().equals(name)) return property; + } + return null; + } + + private static byte[] extractTLVBytes(byte[] data, int start, int end, short targetTag) { + int position = start; + while (position + 4 <= end) { + int tag = ((data[position] & 0xFF) << 8) | (data[position + 1] & 0xFF); + int length = ((data[position + 2] & 0xFF) << 8) | (data[position + 3] & 0xFF); + int valueEnd = position + 4 + length; + if (valueEnd > end) return null; + if (tag == (targetTag & 0xFFFF)) { + byte[] tlv = new byte[valueEnd - position]; + System.arraycopy(data, position, tlv, 0, tlv.length); + return tlv; + } + position = valueEnd; + } + return null; + } + + private static Map capturePreservedStrongReferenceVectors(byte[] originalMetadataBytes, + int frameworkBodyStart, int frameworkBodyEnd, PrimerPack primerPack, ClassDefinition classDef, + String identifiersBeforeEdit, String devicesBeforeEdit, AS07CoreDMSFramework effectiveFramework) { + Map preserved = new HashMap(); + for (String propertyName : STRONG_REFERENCE_VECTOR_PROPERTIES) { + String beforeValue = propertyName.equals("Identifiers") ? identifiersBeforeEdit : devicesBeforeEdit; + String afterValue = propertyName.equals("Identifiers") + ? safeIdentifiersString(effectiveFramework) : safeDevicesString(effectiveFramework); + if (!beforeValue.equals(afterValue)) continue; + PropertyDefinition property = findPropertyDefinition(classDef, propertyName); + if (property == null) continue; + Short tag = primerPack.lookupLocalTag(property.getAUID()); + if (tag == null) continue; + byte[] raw = extractTLVBytes(originalMetadataBytes, frameworkBodyStart, frameworkBodyEnd, tag); + if (raw != null) preserved.put(propertyName, raw); + } + return preserved; + } + private PartitionEditPlan planPartitionEdit(Partition partition, CoreDMSEditor editor, RandomAccessFile source) throws IOException, tv.amwa.maj.exception.InsufficientSpaceException { HeaderMetadata headerMetadata = partition.readHeaderMetadata(); @@ -464,6 +542,10 @@ private PartitionEditPlan planPartitionEdit(Partition partition, CoreDMSEditor e if (marker == null) return null; AS07CoreDMSFramework existingFramework = (AS07CoreDMSFramework) marker.getDescriptiveFrameworkObject(); + // existingFramework is typically mutated in place by editor.apply(), so its strong + // reference vector properties must be snapshotted as strings before the edit is applied. + String identifiersBeforeEdit = safeIdentifiersString(existingFramework); + String devicesBeforeEdit = safeDevicesString(existingFramework); editor.apply(marker, existingFramework); DescriptiveFramework effectiveDf = marker.getDescriptiveFrameworkObject(); @@ -489,7 +571,11 @@ private PartitionEditPlan planPartitionEdit(Partition partition, CoreDMSEditor e PrimerPack primerPack = headerMetadata.getPrimerPack().clone(); int tagCountBefore = primerPack.countLocalTagEntries(); - byte[] newLocalSetBytes = encodeCoreDMSLocalSet(effectiveFramework, instanceID, primerPack); + ClassDefinition classDef = Warehouse.lookForClass(AS07CoreDMSFrameworkImpl.class); + Map preservedRawProperties = capturePreservedStrongReferenceVectors(originalMetadataBytes, + frameworkKlv[1], frameworkKlv[2], primerPack, classDef, + identifiersBeforeEdit, devicesBeforeEdit, effectiveFramework); + byte[] newLocalSetBytes = encodeCoreDMSLocalSet(effectiveFramework, instanceID, primerPack, preservedRawProperties); boolean primerPackGrew = primerPack.countLocalTagEntries() != tagCountBefore; int frameworkStart = frameworkKlv[0]; @@ -1371,7 +1457,13 @@ public FileInformation getMetadata(){ core = new AS07CoreDMSFrameworkImpl(); } IdentifierSetHelper idSetHelper = new IdentifierSetHelper(); - String identifiers = idSetHelper.identifiersToString(core.getIdentifiers()); + String identifiers = ""; + try { + // getIdentifiers() throws NullPointerException, not PropertyNotPresentException, when empty. + identifiers = idSetHelper.identifiersToString(core.getIdentifiers()); + } catch (Exception ex) { + LOGGER.log(Level.INFO, "AS_07_Core_DMS_Identifiers Property Not Present"); + } String devices = ""; try { From 01991fce806c555235389b3788a632db15dc103a Mon Sep 17 00:00:00 2001 From: Maxime Gervais Date: Fri, 28 Aug 2026 00:37:12 +0200 Subject: [PATCH 2/6] Fix edition of identifiers and devices Signed-off-by: Maxime Gervais --- .../embarc/parser/mxf/MXFServiceImpl.java | 139 +++++++++++++++--- 1 file changed, 116 insertions(+), 23 deletions(-) diff --git a/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java b/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java index 5c9ec70..3b9db73 100644 --- a/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java +++ b/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java @@ -35,6 +35,7 @@ import tv.amwa.maj.constant.CommonConstants; import tv.amwa.maj.exception.PropertyNotPresentException; +import tv.amwa.maj.industry.MetadataObject; import tv.amwa.maj.industry.PropertyValue; import tv.amwa.maj.industry.Warehouse; import tv.amwa.maj.io.mxf.BodyPartition; @@ -231,6 +232,11 @@ private static class PartitionEditPlan { byte[] contentBytes; } + private static class StrongReferenceVectorEncoding { + Map rawPropertyOverrides = new HashMap(); + List extraTopLevelKlvBlocks = new ArrayList(); + } + private static final long MIN_FILL_SIZE = 20L; /** @@ -408,10 +414,23 @@ private static AUID findInstanceUID(byte[] data, int bodyStart, int bodyEnd) { } private static byte[] encodeCoreDMSLocalSet(AS07CoreDMSFramework framework, AUID instanceID, PrimerPack primerPack, - Map preservedRawProperties) + Map rawPropertyOverrides) throws IOException, tv.amwa.maj.exception.InsufficientSpaceException { - ClassDefinition classDef = Warehouse.lookForClass(AS07CoreDMSFrameworkImpl.class); - SortedMap properties = classDef.getProperties(framework); + return encodeLocalSet(AS07CoreDMSFrameworkImpl.class, framework, instanceID, primerPack, rawPropertyOverrides); + } + + // Used for the sub-objects a strong reference vector points to (e.g. an AS07DMSIdentifierSetImpl + // entry of Identifiers): plain properties only, no overrides needed. + private static byte[] encodeSubObjectLocalSet(Class implClass, MetadataObject obj, AUID instanceID, + PrimerPack primerPack) throws IOException, tv.amwa.maj.exception.InsufficientSpaceException { + return encodeLocalSet(implClass, obj, instanceID, primerPack, java.util.Collections.emptyMap()); + } + + private static byte[] encodeLocalSet(Class implClass, MetadataObject obj, AUID instanceID, PrimerPack primerPack, + Map rawPropertyOverrides) + throws IOException, tv.amwa.maj.exception.InsufficientSpaceException { + ClassDefinition classDef = Warehouse.lookForClass(implClass); + SortedMap properties = classDef.getProperties(obj); for (PropertyDefinition property : properties.keySet()) { if (property.getAUID().equals(CommonConstants.ObjectClassID)) continue; @@ -428,9 +447,9 @@ private static byte[] encodeCoreDMSLocalSet(AS07CoreDMSFramework framework, AUID Set written = new HashSet(); for (PropertyDefinition property : properties.keySet()) { if (property.getAUID().equals(CommonConstants.ObjectClassID)) continue; - byte[] preserved = preservedRawProperties.get(property.getName()); - if (preserved != null) { - bodyOut.write(preserved); + byte[] override = rawPropertyOverrides.get(property.getName()); + if (override != null) { + bodyOut.write(override); written.add(property.getName()); continue; } @@ -444,9 +463,9 @@ private static byte[] encodeCoreDMSLocalSet(AS07CoreDMSFramework framework, AUID bodyOut.write(shortToBytes((short) actualLength)); bodyOut.write(valueBuffer.array(), 0, actualLength); } - // A preserved strong reference vector may not have round-tripped into `properties` + // An overridden strong reference vector may not have round-tripped into `properties` // (its getter throws internally for an empty-but-present list), so write it directly. - for (Map.Entry entry : preservedRawProperties.entrySet()) { + for (Map.Entry entry : rawPropertyOverrides.entrySet()) { if (!written.contains(entry.getKey())) bodyOut.write(entry.getValue()); } @@ -458,6 +477,19 @@ private static byte[] encodeCoreDMSLocalSet(AS07CoreDMSFramework framework, AUID return out.toByteArray(); } + private static byte[] buildStrongReferenceVectorBatchValue(List uids) { + ByteArrayOutputStream out = new ByteArrayOutputStream(8 + uids.size() * 16); + byte[] header = new byte[8]; + header[0] = (byte) ((uids.size() >> 24) & 0xFF); + header[1] = (byte) ((uids.size() >> 16) & 0xFF); + header[2] = (byte) ((uids.size() >> 8) & 0xFF); + header[3] = (byte) (uids.size() & 0xFF); + header[4] = 0; header[5] = 0; header[6] = 0; header[7] = 16; + out.write(header, 0, 8); + for (AUID uid : uids) out.write(uid.getAUIDValue(), 0, 16); + return out.toByteArray(); + } + private static byte[] shortToBytes(short value) { return new byte[] { (byte) ((value >> 8) & 0xFF), (byte) (value & 0xFF) }; } @@ -514,26 +546,78 @@ private static byte[] extractTLVBytes(byte[] data, int start, int end, short tar return null; } - private static Map capturePreservedStrongReferenceVectors(byte[] originalMetadataBytes, + private static Class subObjectImplClass(String propertyName) { + return propertyName.equals("Identifiers") ? AS07DMSIdentifierSetImpl.class : AS07CoreDMSDeviceObjectsImpl.class; + } + + private static List currentSubObjects(String propertyName, AS07CoreDMSFramework framework) { + try { + return propertyName.equals("Identifiers") ? framework.getIdentifiers() : framework.getDevices(); + } catch (Exception ex) { + return java.util.Collections.emptyList(); + } + } + + // Handles both cases for Identifiers/Devices: when unchanged, the original TLV bytes are + // reused verbatim (see STRONG_REFERENCE_VECTOR_PROPERTIES); when genuinely edited, fresh + // sub-object KLV blocks are built (each with its own generated instance UID) and a matching + // strong reference vector value is written to point at them. + private static StrongReferenceVectorEncoding planStrongReferenceVectorEncoding(byte[] originalMetadataBytes, int frameworkBodyStart, int frameworkBodyEnd, PrimerPack primerPack, ClassDefinition classDef, - String identifiersBeforeEdit, String devicesBeforeEdit, AS07CoreDMSFramework effectiveFramework) { - Map preserved = new HashMap(); + String identifiersBeforeEdit, String devicesBeforeEdit, AS07CoreDMSFramework effectiveFramework, + Map> sharedSubObjectUIDs) + throws IOException, tv.amwa.maj.exception.InsufficientSpaceException { + StrongReferenceVectorEncoding encoding = new StrongReferenceVectorEncoding(); for (String propertyName : STRONG_REFERENCE_VECTOR_PROPERTIES) { String beforeValue = propertyName.equals("Identifiers") ? identifiersBeforeEdit : devicesBeforeEdit; String afterValue = propertyName.equals("Identifiers") ? safeIdentifiersString(effectiveFramework) : safeDevicesString(effectiveFramework); - if (!beforeValue.equals(afterValue)) continue; PropertyDefinition property = findPropertyDefinition(classDef, propertyName); if (property == null) continue; + + if (beforeValue.equals(afterValue)) { + Short tag = primerPack.lookupLocalTag(property.getAUID()); + if (tag == null) continue; + byte[] raw = extractTLVBytes(originalMetadataBytes, frameworkBodyStart, frameworkBodyEnd, tag); + if (raw != null) encoding.rawPropertyOverrides.put(propertyName, raw); + continue; + } + + List newItems = currentSubObjects(propertyName, effectiveFramework); + if (newItems.isEmpty()) continue; // property becomes absent; nothing to write or add + + Class implClass = subObjectImplClass(propertyName); + // Header and footer metadata are independently re-planned copies of the same edit, so + // the same conceptual new entry must reuse one UID across both instead of getting a + // fresh random one from each - otherwise the two partitions silently diverge. + List reusableUIDs = sharedSubObjectUIDs.get(propertyName); + boolean canReuse = reusableUIDs != null && reusableUIDs.size() == newItems.size(); + List subInstanceIDs = new ArrayList(); + for (int i = 0; i < newItems.size(); i++) { + AUID subInstanceID = canReuse ? reusableUIDs.get(i) : new AUIDImpl(); + encoding.extraTopLevelKlvBlocks.add( + encodeSubObjectLocalSet(implClass, newItems.get(i), subInstanceID, primerPack)); + subInstanceIDs.add(subInstanceID); + } + if (!canReuse) sharedSubObjectUIDs.put(propertyName, subInstanceIDs); + Short tag = primerPack.lookupLocalTag(property.getAUID()); - if (tag == null) continue; - byte[] raw = extractTLVBytes(originalMetadataBytes, frameworkBodyStart, frameworkBodyEnd, tag); - if (raw != null) preserved.put(propertyName, raw); - } - return preserved; + if (tag == null) { + tag = findFreeLocalTag(primerPack); + primerPack.addLocalTagEntry(tag, property.getAUID()); + } + byte[] batchValue = buildStrongReferenceVectorBatchValue(subInstanceIDs); + ByteArrayOutputStream tlv = new ByteArrayOutputStream(4 + batchValue.length); + tlv.write(shortToBytes(tag)); + tlv.write(shortToBytes((short) batchValue.length)); + tlv.write(batchValue); + encoding.rawPropertyOverrides.put(propertyName, tlv.toByteArray()); + } + return encoding; } - private PartitionEditPlan planPartitionEdit(Partition partition, CoreDMSEditor editor, RandomAccessFile source) + private PartitionEditPlan planPartitionEdit(Partition partition, CoreDMSEditor editor, RandomAccessFile source, + Map> sharedSubObjectUIDs) throws IOException, tv.amwa.maj.exception.InsufficientSpaceException { HeaderMetadata headerMetadata = partition.readHeaderMetadata(); if (headerMetadata == null) return null; @@ -572,10 +656,11 @@ private PartitionEditPlan planPartitionEdit(Partition partition, CoreDMSEditor e PrimerPack primerPack = headerMetadata.getPrimerPack().clone(); int tagCountBefore = primerPack.countLocalTagEntries(); ClassDefinition classDef = Warehouse.lookForClass(AS07CoreDMSFrameworkImpl.class); - Map preservedRawProperties = capturePreservedStrongReferenceVectors(originalMetadataBytes, + StrongReferenceVectorEncoding strongRefEncoding = planStrongReferenceVectorEncoding(originalMetadataBytes, frameworkKlv[1], frameworkKlv[2], primerPack, classDef, - identifiersBeforeEdit, devicesBeforeEdit, effectiveFramework); - byte[] newLocalSetBytes = encodeCoreDMSLocalSet(effectiveFramework, instanceID, primerPack, preservedRawProperties); + identifiersBeforeEdit, devicesBeforeEdit, effectiveFramework, sharedSubObjectUIDs); + byte[] newLocalSetBytes = encodeCoreDMSLocalSet(effectiveFramework, instanceID, primerPack, + strongRefEncoding.rawPropertyOverrides); boolean primerPackGrew = primerPack.countLocalTagEntries() != tagCountBefore; int frameworkStart = frameworkKlv[0]; @@ -600,6 +685,9 @@ private PartitionEditPlan planPartitionEdit(Partition partition, CoreDMSEditor e contentStream.write(originalMetadataBytes, 0, frameworkStart); } contentStream.write(newLocalSetBytes); + for (byte[] extraKlvBlock : strongRefEncoding.extraTopLevelKlvBlocks) { + contentStream.write(extraKlvBlock); + } contentStream.write(originalMetadataBytes, frameworkEnd, realMetadataEnd - frameworkEnd); byte[] contentBytes = contentStream.toByteArray(); @@ -622,12 +710,17 @@ private MXFFileWriteResult applyCoreDMSEdit(String outputFilePath, CoreDMSEditor if (mxfFile.getRunInSize() != 0) throw new IOException("Files with a run-in are not supported by the safe Core DMS rewrite path."); + // Header and footer metadata are duplicate copies of each other, so a freshly created + // sub-object (e.g. a new Identifiers entry) must reuse the same instance UID in both + // partitions rather than getting a fresh random one from each independent plan. + Map> sharedSubObjectUIDs = new HashMap>(); PartitionEditPlan headerPlan; PartitionEditPlan footerPlan; try (RandomAccessFile source = new RandomAccessFile(filePath, "r")) { - headerPlan = planPartitionEdit(mxfFile.getHeaderPartition(), editor, source); + headerPlan = planPartitionEdit(mxfFile.getHeaderPartition(), editor, source, sharedSubObjectUIDs); FooterPartition footerPartition = mxfFile.getFooterPartition(); - footerPlan = footerPartition != null ? planPartitionEdit(footerPartition, editor, source) : null; + footerPlan = footerPartition != null + ? planPartitionEdit(footerPartition, editor, source, sharedSubObjectUIDs) : null; } if (headerPlan == null && footerPlan == null) { From 1d5eb03d7cd81210f531d4accd274f199d67f40b Mon Sep 17 00:00:00 2001 From: Maxime Gervais Date: Fri, 28 Aug 2026 01:29:02 +0200 Subject: [PATCH 3/6] Treat empty fields as absent Signed-off-by: Maxime Gervais --- src/main/com/portalmedia/embarc/cli/Main.java | 2 +- .../embarc/parser/mxf/MXFServiceImpl.java | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/com/portalmedia/embarc/cli/Main.java b/src/main/com/portalmedia/embarc/cli/Main.java index 148dc1f..16789cf 100644 --- a/src/main/com/portalmedia/embarc/cli/Main.java +++ b/src/main/com/portalmedia/embarc/cli/Main.java @@ -538,7 +538,7 @@ private static void printAS07CoreDMS(MXFMetadata data) { private static void printCoreProperty(HashMap coreData, MXFColumn col, String label) { String value = ""; - if (coreData.containsKey(col)) { + if (coreData.containsKey(col) && coreData.get(col).getCurrentValue() != null) { value = coreData.get(col).getCurrentValue(); } System.out.format("%-35s%-1s\n", label, value); diff --git a/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java b/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java index 3b9db73..2ef17ef 100644 --- a/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java +++ b/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java @@ -416,18 +416,21 @@ private static AUID findInstanceUID(byte[] data, int bodyStart, int bodyEnd) { private static byte[] encodeCoreDMSLocalSet(AS07CoreDMSFramework framework, AUID instanceID, PrimerPack primerPack, Map rawPropertyOverrides) throws IOException, tv.amwa.maj.exception.InsufficientSpaceException { - return encodeLocalSet(AS07CoreDMSFrameworkImpl.class, framework, instanceID, primerPack, rawPropertyOverrides); + return encodeLocalSet(AS07CoreDMSFrameworkImpl.class, framework, instanceID, primerPack, rawPropertyOverrides, true); } // Used for the sub-objects a strong reference vector points to (e.g. an AS07DMSIdentifierSetImpl - // entry of Identifiers): plain properties only, no overrides needed. + // entry of Identifiers): plain properties only, no overrides needed. Empty string sub-fields are + // still written (omitEmptyStrings=false): IdentifierSetHelper/DeviceSetHelper encode a sub-object + // as a fixed number of comma-separated positions, and dropping one would shift every field after + // it when the record is parsed back on read. private static byte[] encodeSubObjectLocalSet(Class implClass, MetadataObject obj, AUID instanceID, PrimerPack primerPack) throws IOException, tv.amwa.maj.exception.InsufficientSpaceException { - return encodeLocalSet(implClass, obj, instanceID, primerPack, java.util.Collections.emptyMap()); + return encodeLocalSet(implClass, obj, instanceID, primerPack, java.util.Collections.emptyMap(), false); } private static byte[] encodeLocalSet(Class implClass, MetadataObject obj, AUID instanceID, PrimerPack primerPack, - Map rawPropertyOverrides) + Map rawPropertyOverrides, boolean omitEmptyStrings) throws IOException, tv.amwa.maj.exception.InsufficientSpaceException { ClassDefinition classDef = Warehouse.lookForClass(implClass); SortedMap properties = classDef.getProperties(obj); @@ -454,6 +457,11 @@ private static byte[] encodeLocalSet(Class implClass, MetadataObject obj, AUI continue; } PropertyValue value = properties.get(property); + // A field the user (or a stale default, e.g. IntendedAFD) has left as an empty string + // is treated as absent rather than serialized as present-but-empty: some MAJ getters + // default to "" instead of null and would otherwise resurrect a property that was + // never actually set in the file, on every unrelated edit. + if (omitEmptyStrings && "".equals(value.getValue())) continue; Short localTag = primerPack.lookupLocalTag(property.getAUID()); long predictedLength = value.getType().lengthAsBytes(value); ByteBuffer valueBuffer = ByteBuffer.allocate((int) predictedLength + 64); From ccf62c4e3804fdf070db2e3f39d2b3beff20e871 Mon Sep 17 00:00:00 2001 From: Maxime Gervais Date: Fri, 28 Aug 2026 01:57:00 +0200 Subject: [PATCH 4/6] Avoid mxf corruption when writting four slashes or comma Signed-off-by: Maxime Gervais --- .../embarc/parser/mxf/DelimitedListCodec.java | 83 +++++++++++++++++++ .../embarc/parser/mxf/DeviceSetHelper.java | 18 ++-- .../parser/mxf/IdentifierSetHelper.java | 25 +++--- 3 files changed, 105 insertions(+), 21 deletions(-) create mode 100644 src/main/com/portalmedia/embarc/parser/mxf/DelimitedListCodec.java diff --git a/src/main/com/portalmedia/embarc/parser/mxf/DelimitedListCodec.java b/src/main/com/portalmedia/embarc/parser/mxf/DelimitedListCodec.java new file mode 100644 index 0000000..1e6110d --- /dev/null +++ b/src/main/com/portalmedia/embarc/parser/mxf/DelimitedListCodec.java @@ -0,0 +1,83 @@ +package com.portalmedia.embarc.parser.mxf; + +import java.util.ArrayList; +import java.util.List; + +/** + * Shared escaping/splitting logic for IdentifierSetHelper and DeviceSetHelper, which both encode a + * list of records as fields joined by a run of 4 commas, records joined by a run of 4 slashes. A + * field value containing a run of 4 (or more) of the delimiter character used to be indistinguishable + * from the delimiter itself, corrupting the split. Every backslash, comma, and slash in a raw field + * value is now backslash-escaped before joining, so an unescaped run of the delimiter character can + * only be a real delimiter -- single, unescaped commas/slashes (i.e. anything shorter than the 4-char + * run) are left untouched on read for compatibility with files written before this escaping existed. + */ +final class DelimitedListCodec { + private DelimitedListCodec() {} + + static String escapeField(String value) { + if (value == null) return ""; + StringBuilder sb = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '\\' || c == ',' || c == '/') sb.append('\\'); + sb.append(c); + } + return sb.toString(); + } + + private static boolean isEscapable(char c) { + return c == '\\' || c == ',' || c == '/'; + } + + static String unescapeField(String value) { + StringBuilder sb = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '\\' && i + 1 < value.length() && isEscapable(value.charAt(i + 1))) { + sb.append(value.charAt(++i)); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * Splits on runs of exactly `runLength` (or more) unescaped occurrences of `delimiter`. + * Backslash-escaped characters are skipped over (not unescaped here) so this can be applied at + * the record level (slash) and then again at the field level (comma) on each resulting piece, + * with a single final {@link #unescapeField} pass on each leaf field. A backslash NOT followed + * by one of the escapable characters is not an escape sequence (e.g. a raw "C:\path" typed + * directly into a CSV cell): leave it as an ordinary character rather than swallowing it, so + * pre-existing, un-escaped text with incidental backslashes doesn't silently lose them. + */ + static List splitOnDelimiterRun(String value, char delimiter, int runLength) { + List parts = new ArrayList(); + int start = 0; + int i = 0; + int length = value.length(); + while (i < length) { + char c = value.charAt(i); + if (c == '\\' && i + 1 < length && isEscapable(value.charAt(i + 1))) { + i += 2; + continue; + } + if (c == delimiter) { + int runEnd = i; + while (runEnd < length && value.charAt(runEnd) == delimiter) runEnd++; + if (runEnd - i >= runLength) { + parts.add(value.substring(start, i)); + i += runLength; + start = i; + continue; + } + i = runEnd; + continue; + } + i++; + } + parts.add(value.substring(start)); + return parts; + } +} diff --git a/src/main/com/portalmedia/embarc/parser/mxf/DeviceSetHelper.java b/src/main/com/portalmedia/embarc/parser/mxf/DeviceSetHelper.java index b0822ff..48924bc 100644 --- a/src/main/com/portalmedia/embarc/parser/mxf/DeviceSetHelper.java +++ b/src/main/com/portalmedia/embarc/parser/mxf/DeviceSetHelper.java @@ -13,7 +13,7 @@ public class DeviceSetHelper { public ArrayList createDeviceListFromString(String values) { ArrayList devices = new ArrayList(); - String[] valList = values.split(slash); + List valList = DelimitedListCodec.splitOnDelimiterRun(values, slash.charAt(0), slash.length()); for (String v : valList) { if (!v.isEmpty()) devices.add(createDeviceFromString(v)); } @@ -21,13 +21,13 @@ public ArrayList createDeviceListFromString(String } public AS07CoreDMSDeviceObjectsImpl createDeviceFromString(String values) { - String[] valList = values.split(comma); + List valList = DelimitedListCodec.splitOnDelimiterRun(values, comma.charAt(0), comma.length()); AS07CoreDMSDeviceObjectsImpl device = new AS07CoreDMSDeviceObjectsImpl(); - if(valList.length>0) device.setDeviceType(valList[0]); - if(valList.length>1) device.setManufacturer(valList[1]); - if(valList.length>2) device.setModel(valList[2]); - if(valList.length>3) device.setSerialNumber(valList[3]); - if(valList.length>4) device.setUsageDescription(valList[4]); + if(valList.size()>0) device.setDeviceType(DelimitedListCodec.unescapeField(valList.get(0))); + if(valList.size()>1) device.setManufacturer(DelimitedListCodec.unescapeField(valList.get(1))); + if(valList.size()>2) device.setModel(DelimitedListCodec.unescapeField(valList.get(2))); + if(valList.size()>3) device.setSerialNumber(DelimitedListCodec.unescapeField(valList.get(3))); + if(valList.size()>4) device.setUsageDescription(DelimitedListCodec.unescapeField(valList.get(4))); return device; } @@ -69,6 +69,8 @@ public String deviceToString(AS07CoreDMSDeviceObjectsImpl device) { usage = device.getUsageDescription(); } catch(PropertyNotPresentException pex) {} - return type + comma + manu + comma + model + comma + serial + comma + usage; + return DelimitedListCodec.escapeField(type) + comma + DelimitedListCodec.escapeField(manu) + comma + + DelimitedListCodec.escapeField(model) + comma + DelimitedListCodec.escapeField(serial) + comma + + DelimitedListCodec.escapeField(usage); } } diff --git a/src/main/com/portalmedia/embarc/parser/mxf/IdentifierSetHelper.java b/src/main/com/portalmedia/embarc/parser/mxf/IdentifierSetHelper.java index c1870a5..785b18f 100644 --- a/src/main/com/portalmedia/embarc/parser/mxf/IdentifierSetHelper.java +++ b/src/main/com/portalmedia/embarc/parser/mxf/IdentifierSetHelper.java @@ -12,7 +12,7 @@ public class IdentifierSetHelper { public ArrayList createIdentifierListFromString(String values) { ArrayList idents = new ArrayList(); - String[] valList = values.split(slash); + List valList = DelimitedListCodec.splitOnDelimiterRun(values, slash.charAt(0), slash.length()); for (String v : valList) { if (!v.isEmpty()) idents.add(createIdentifierFromString(v)); } @@ -20,12 +20,12 @@ public ArrayList createIdentifierListFromString(String } public AS07DMSIdentifierSetImpl createIdentifierFromString(String values) { - String[] valList = values.split(comma); + List valList = DelimitedListCodec.splitOnDelimiterRun(values, comma.charAt(0), comma.length()); AS07DMSIdentifierSetImpl ident = new AS07DMSIdentifierSetImpl(); - if (valList.length > 0) ident.setIdentifierValue(valList[0]); - if (valList.length > 1) ident.setIdentifierRole(valList[1]); - if (valList.length > 2) ident.setIdentifierType(valList[2]); - if (valList.length > 3) ident.setIdentifierComment(valList[3]); + if (valList.size() > 0) ident.setIdentifierValue(DelimitedListCodec.unescapeField(valList.get(0))); + if (valList.size() > 1) ident.setIdentifierRole(DelimitedListCodec.unescapeField(valList.get(1))); + if (valList.size() > 2) ident.setIdentifierType(DelimitedListCodec.unescapeField(valList.get(2))); + if (valList.size() > 3) ident.setIdentifierComment(DelimitedListCodec.unescapeField(valList.get(3))); return ident; } @@ -47,13 +47,12 @@ public String identifierToString(AS07DMSIdentifierSetImpl id) { String type = null; String comm = null; - try { - val = id.getIdentifierValue(); - role = id.getIdentifierRole(); - type = id.getIdentifierType(); - comm = id.getIdentifierComment(); - } catch (PropertyNotPresentException ex) {} + try { val = id.getIdentifierValue(); } catch (PropertyNotPresentException ex) {} + try { role = id.getIdentifierRole(); } catch (PropertyNotPresentException ex) {} + try { type = id.getIdentifierType(); } catch (PropertyNotPresentException ex) {} + try { comm = id.getIdentifierComment(); } catch (PropertyNotPresentException ex) {} - return val + comma + role + comma + type + comma + comm; + return DelimitedListCodec.escapeField(val) + comma + DelimitedListCodec.escapeField(role) + comma + + DelimitedListCodec.escapeField(type) + comma + DelimitedListCodec.escapeField(comm); } } From a902a95da5c7049fde0a22a338fa5ed8d15c2436 Mon Sep 17 00:00:00 2001 From: Maxime Gervais Date: Fri, 28 Aug 2026 12:04:19 +0200 Subject: [PATCH 5/6] Fix CI Signed-off-by: Maxime Gervais --- .github/workflows/embARC_Checks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/embARC_Checks.yml b/.github/workflows/embARC_Checks.yml index cd2470b..1463841 100644 --- a/.github/workflows/embARC_Checks.yml +++ b/.github/workflows/embARC_Checks.yml @@ -56,8 +56,8 @@ jobs: - name: embARC-maj run: | # Don't use git clone because it's fail on Windows due to invalid path (.metadata/.plugins/org.eclipse.core.runtime/.settings/org.springframework.ide.eclipse.boot.dash:Cloud Foundry.prefs') - Invoke-WebRequest -Uri "https://github.com/PortalMedia/embARC-maj/archive/refs/heads/master.zip" -OutFile "embARC-maj.zip" - Expand-Archive -Path "embARC-maj.zip" -DestinationPath "." + Invoke-WebRequest -Uri "https://github.com/MediaArea/embARC-maj/archive/refs/heads/master.zip" -OutFile "embARC-maj.zip" + & 7z.exe x embARC-maj.zip Rename-Item -Path "embARC-maj-master" -NewName "embARC-maj" - name: Compile run: | From 9d6eb0728f8682f225958c47829466468f01dfeb Mon Sep 17 00:00:00 2001 From: Maxime Gervais Date: Fri, 28 Aug 2026 13:08:55 +0200 Subject: [PATCH 6/6] Show error icon when required mxf Identifiers field is empty Signed-off-by: Maxime Gervais --- .../com/portalmedia/embarc/gui/mxf/CoreMXFController.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/com/portalmedia/embarc/gui/mxf/CoreMXFController.java b/src/main/com/portalmedia/embarc/gui/mxf/CoreMXFController.java index 33ee19b..7ab2d28 100644 --- a/src/main/com/portalmedia/embarc/gui/mxf/CoreMXFController.java +++ b/src/main/com/portalmedia/embarc/gui/mxf/CoreMXFController.java @@ -423,6 +423,11 @@ private void createIdentifiersDisplay(MXFSelectedFilesSummary summary, MXFColumn }); iconHbox.setAccessibleRole(AccessibleRole.BUTTON); iconHbox.setAccessibleText("Open modal with Identifiers specification."); + if (col.isRequired() && identifiers.size() == 0) { + final FontIcon warningIcon = new FontIcon(FontAwesomeSolid.EXCLAMATION_CIRCLE); + warningIcon.getStyleClass().add("fadgi-sr-warning"); + labelIconHbox.getChildren().add(warningIcon); + } hbox.getChildren().addAll(labelIconHbox); labelIconHbox.setPrefWidth(285.0);