Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions cpp/src/main/cpp/com/sandflow/smpte/regxml/FragmentBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ namespace rxml {
const UL FragmentBuilder::GenerationID_UL = "urn:smpte:ul:060e2b34.01010102.05200701.01000000";
const UL FragmentBuilder::ApplicationProductID_UL = "urn:smpte:ul:060e2b34.01010102.05200701.07000000";

const int FragmentBuilder::MAX_TIME_FRACTION = 249;

const std::string FragmentBuilder::BYTEORDER_BE = "BigEndian";
const std::string FragmentBuilder::BYTEORDER_LE = "LittleEndian";

Expand Down Expand Up @@ -1147,6 +1149,26 @@ namespace rxml {
return sb.str();
}

void FragmentBuilder::checkTimeFraction(xercesc::DOMElement * element, int fraction, const RecordTypeDefinition * definition) {

if (fraction > MAX_TIME_FRACTION) {

InvalidTimeFractionError err(fraction,
rxml::fmt(
"Record {} at Element {}",
definition->symbol,
DOMHelper::toUTF8(element->getLocalName()).c_str()
)
);

evthandler->error(err);

addInformativeComment(element, err.getReason());

}

}

void FragmentBuilder::applyRule5_8(xercesc::DOMElement * element, MXFInputStream & value, const RecordTypeDefinition * definition) {

if (definition->identification.equals(AUID_UL)) {
Expand Down Expand Up @@ -1189,7 +1211,9 @@ namespace rxml {
int second = value.readUnsignedByte();
int fraction = value.readUnsignedByte();

element->setTextContent(DOMHelper::fromUTF8(generateISO8601Time(hour, minute, second, 4 * fraction)));
element->setTextContent(DOMHelper::fromUTF8(generateISO8601Time(hour, minute, second, 4 * std::min(fraction, MAX_TIME_FRACTION))));

checkTimeFraction(element, fraction, definition);

} else if (definition->identification.equals(TimeStamp_UL)) {

Expand All @@ -1204,10 +1228,12 @@ namespace rxml {
element->setTextContent(
DOMHelper::fromUTF8(
generateISO8601Date(year, month, day) + "T" +
generateISO8601Time(hour, minute, second, 4 * fraction)
generateISO8601Time(hour, minute, second, 4 * std::min(fraction, MAX_TIME_FRACTION))
)
);

checkTimeFraction(element, fraction, definition);

} else if (definition->identification.equals(VersionType_UL)) {

/* EXCEPTION: registers used Int8 but MXF specifies UInt8 */
Expand Down
17 changes: 17 additions & 0 deletions cpp/src/main/cpp/com/sandflow/smpte/regxml/FragmentBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,19 @@ namespace rxml {
) {}
};

class InvalidTimeFractionError : public Event {
public:
InvalidTimeFractionError(int value, const std::string &where) :
Event(
"FragmentBuilder::InvalidTimeFractionError",
rxml::fmt(
"Time fraction {} exceeds {}",
rxml::to_string(value),
rxml::to_string(MAX_TIME_FRACTION)),
where
) {}
};

class IOError : public Event {
public:
IOError(const std::ios::failure &failure, const std::string &where) :
Expand Down Expand Up @@ -364,6 +377,8 @@ namespace rxml {
static const std::string BYTEORDER_BE;
static const std::string BYTEORDER_LE;

static const int MAX_TIME_FRACTION;


static const std::string REGXML_NS;
static const std::string XMLNS_NS;
Expand All @@ -389,6 +404,8 @@ namespace rxml {

static void addInformativeComment(xercesc::DOMElement * element, std::string comment);

void checkTimeFraction(xercesc::DOMElement *element, int fraction, const RecordTypeDefinition *definition);

void appendCommentWithAUIDName(AUID auid, xercesc::DOMElement * elem);

void applyRule3(xercesc::DOMNode *node, const Group &group);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.sandflow.smpte.mxf.adapters;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Test;

import com.sandflow.smpte.klv.Set;
import com.sandflow.smpte.mxf.MXFDataInput;
import com.sandflow.smpte.mxf.MXFEvent;
import com.sandflow.smpte.mxf.MXFInputContext;
import com.sandflow.smpte.util.UUID;
import com.sandflow.util.events.Event;

public class TimeFractionTest {
private final List<Event> events = new ArrayList<>();

private final MXFInputContext ctx = new MXFInputContext() {
@Override
public Set getSet(UUID uuid) {
throw new UnsupportedOperationException("Unimplemented method 'getSet'");
}

@Override
public void handleEvent(Event evt) {
events.add(evt);
}
};

private static MXFDataInput stream(int... bytes) {
byte[] b = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++) {
b[i] = (byte) bytes[i];
}
return new MXFDataInput(new ByteArrayInputStream(b));
}

@Test
void testTimeAtMaximum() throws IOException {
assertEquals(LocalTime.of(1, 2, 3, 996_000_000), LocalTimeAdapter.fromStream(stream(1, 2, 3, 249), ctx));
assertEquals(0, events.size());
}

@Test
void testTimeCapped() throws IOException {
assertEquals(LocalTime.of(1, 2, 3, 996_000_000), LocalTimeAdapter.fromStream(stream(1, 2, 3, 250), ctx));
assertEquals(1, events.size());
assertEquals(MXFEvent.EventCodes.INVALID_TIME_FRACTION, events.get(0).getCode());
}

@Test
void testTimeStampCapped() throws IOException {
assertEquals(LocalDateTime.of(2026, 10, 31, 1, 2, 3, 996_000_000),
LocalDateTimeAdapter.fromStream(stream(0x07, 0xEA, 10, 31, 1, 2, 3, 255), ctx));
assertEquals(1, events.size());
assertEquals(MXFEvent.EventCodes.INVALID_TIME_FRACTION, events.get(0).getCode());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@ public static enum EventCodes {
/**
* Inconsistent stream identifications
*/
INCONSISTENT_STREAM_ID(Event.Severity.ERROR);
INCONSISTENT_STREAM_ID(Event.Severity.ERROR),
/**
* Time fraction exceeds its maximum
*/
INVALID_TIME_FRACTION(Event.Severity.ERROR);


public final Event.Severity severity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static LocalDateTime fromStream(MXFDataInput is, MXFInputContext ctx) thr
int hour = is.readUnsignedByte();
int minute = is.readUnsignedByte();
int second = is.readUnsignedByte();
int fraction = is.readUnsignedByte();
int fraction = LocalTimeAdapter.capFraction(is.readUnsignedByte(), ctx);

/* fraction is in units of 1/250 s (4 ms) */
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@

import com.sandflow.smpte.mxf.MXFInputContext;
import com.sandflow.smpte.mxf.MXFDataInput;
import com.sandflow.smpte.mxf.MXFEvent;
import com.sandflow.smpte.mxf.MXFException;
import com.sandflow.smpte.mxf.MXFOutputContext;
import com.sandflow.smpte.mxf.MXFDataOutput;

public class LocalTimeAdapter {
public static final Integer ITEM_LENGTH = 4;
private static final int NANOS_PER_FRACTION = 4_000_000;
private static final int MAX_FRACTION = 249;

public static LocalTime fromStream(MXFDataInput is, MXFInputContext ctx) throws IOException {

Expand All @@ -48,7 +51,7 @@ public static LocalTime fromStream(MXFDataInput is, MXFInputContext ctx) throws
int hour = is.readUnsignedByte();
int minute = is.readUnsignedByte();
int second = is.readUnsignedByte();
int fraction = is.readUnsignedByte();
int fraction = capFraction(is.readUnsignedByte(), ctx);

try {
return LocalTime.of(hour, minute, second, fraction * NANOS_PER_FRACTION);
Expand All @@ -57,6 +60,22 @@ public static LocalTime fromStream(MXFDataInput is, MXFInputContext ctx) throws
}
}

static int capFraction(int fraction, MXFInputContext ctx) throws IOException {
if (fraction <= MAX_FRACTION) {
return fraction;
}

try {
ctx.handleEvent(new MXFEvent(
MXFEvent.EventCodes.INVALID_TIME_FRACTION,
"Time fraction " + fraction + " exceeds " + MAX_FRACTION));
} catch (MXFException e) {
throw new IOException(e);
}

return MAX_FRACTION;
}

public static void toStream(LocalTime t, MXFDataOutput os, MXFOutputContext ctx) throws IOException {
os.writeUnsignedByte((byte) t.getHour());
os.writeUnsignedByte((byte) t.getMinute());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ public static enum EventCodes {
* Raised when an enum value is not supported.
*/
INVALID_INTEGER_VALUE(Event.Severity.ERROR),
/**
* Raised when the fraction of a time value exceeds its maximum.
*/
INVALID_TIME_FRACTION(Event.Severity.ERROR),
/**
* Raised when the type of a String definition is not supported.
*/
Expand Down Expand Up @@ -237,6 +241,8 @@ public String getWhere() {
private static final String REGXML_NS = "http://sandflow.com/ns/SMPTEST2001-1/baseline";
private final static String XMLNS_NS = "http://www.w3.org/2000/xmlns/";

private static final int MAX_TIME_FRACTION = 249;

private static final String BYTEORDER_BE = "BigEndian";
private static final String BYTEORDER_LE = "LittleEndian";
private static final String UID_ATTR = "uid";
Expand Down Expand Up @@ -1377,6 +1383,32 @@ String generateISO8601Date(int year, int month, int day) {
return String.format("%04d-%02d-%02d", year, month, day);
}

void checkTimeFraction(Element element, int fraction, RecordTypeDefinition definition) throws RuleException {

if (fraction > MAX_TIME_FRACTION) {

FragmentEvent evt = new FragmentEvent(
EventCodes.INVALID_TIME_FRACTION,
String.format(
"Time fraction %d exceeds %d",
fraction,
MAX_TIME_FRACTION
),
String.format(
"Record %s at Element %s",
definition.getSymbol(),
element.getLocalName()
)
);

handleEvent(evt);

addInformativeComment(element, evt.getReason());

}

}

void applyRule5_8(Element element, MXFDataInput value, RecordTypeDefinition definition) throws RuleException, IOException {

if (definition.getIdentification().equals(AUID_UL)) {
Expand Down Expand Up @@ -1417,7 +1449,9 @@ void applyRule5_8(Element element, MXFDataInput value, RecordTypeDefinition defi
int second = value.readUnsignedByte();
int fraction = value.readUnsignedByte();

element.setTextContent(generateISO8601Time(hour, minute, second, 4 * fraction));
element.setTextContent(generateISO8601Time(hour, minute, second, 4 * Math.min(fraction, MAX_TIME_FRACTION)));

checkTimeFraction(element, fraction, definition);

} else if (definition.getIdentification().equals(TimeStamp_UL)) {

Expand All @@ -1429,7 +1463,9 @@ void applyRule5_8(Element element, MXFDataInput value, RecordTypeDefinition defi
int second = value.readUnsignedByte();
int fraction = value.readUnsignedByte();

element.setTextContent(generateISO8601Date(year, month, day) + "T" + generateISO8601Time(hour, minute, second, 4 * fraction));
element.setTextContent(generateISO8601Date(year, month, day) + "T" + generateISO8601Time(hour, minute, second, 4 * Math.min(fraction, MAX_TIME_FRACTION)));

checkTimeFraction(element, fraction, definition);

} else if (definition.getIdentification().equals(VersionType_UL)) {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.sandflow.smpte.regxml;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.xml.parsers.DocumentBuilderFactory;

import org.junit.jupiter.api.Test;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.sandflow.smpte.klv.Group;
import com.sandflow.smpte.mxf.MXFDataInput;
import com.sandflow.smpte.regxml.dict.MetaDictionaryCollection;
import com.sandflow.smpte.regxml.dict.definitions.RecordTypeDefinition;
import com.sandflow.smpte.util.AUID;
import com.sandflow.smpte.util.UL;
import com.sandflow.smpte.util.UUID;
import com.sandflow.util.events.Event;

public class TimeFractionTest {
private static final String TIMESTRUCT_URN = "urn:smpte:ul:060e2b34.01040101.03010600.00000000";
private static final String TIMESTAMP_URN = "urn:smpte:ul:060e2b34.01040101.03010700.00000000";

private final List<Event> events = new ArrayList<>();

private Element apply(String urn, byte... value) throws Exception {
FragmentBuilder fb = new FragmentBuilder(new MetaDictionaryCollection(), new HashMap<UUID, Group>(), null, evt -> {
events.add(evt);
return true;
});

RecordTypeDefinition def = new RecordTypeDefinition();
def.setIdentification(new AUID(UL.fromURN(urn)));
def.setSymbol("TestRecord");

Element element = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument().createElement("Test");

fb.applyRule5_8(element, new MXFDataInput(new ByteArrayInputStream(value)), def);

return element;
}

private static boolean hasComment(Element element) {
for (Node n = element.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n.getNodeType() == Node.COMMENT_NODE) {
return true;
}
}
return false;
}

@Test
void testTimeAtMaximum() throws Exception {
Element e = apply(TIMESTRUCT_URN, (byte) 1, (byte) 2, (byte) 3, (byte) 249);

assertEquals("01:02:03.996Z", e.getTextContent());
assertEquals(0, events.size());
}

@Test
void testTimeCapped() throws Exception {
Element e = apply(TIMESTRUCT_URN, (byte) 1, (byte) 2, (byte) 3, (byte) 250);

assertEquals("01:02:03.996Z", e.getTextContent());
assertEquals(1, events.size());
assertEquals(FragmentBuilder.EventCodes.INVALID_TIME_FRACTION, events.get(0).getCode());
assertTrue(hasComment(e));
}

@Test
void testTimeStampCapped() throws Exception {
Element e = apply(TIMESTAMP_URN, (byte) 0x07, (byte) 0xEA, (byte) 10, (byte) 31, (byte) 1, (byte) 2, (byte) 3, (byte) 255);

assertEquals("2026-10-31T01:02:03.996Z", e.getTextContent());
assertEquals(1, events.size());
assertEquals(FragmentBuilder.EventCodes.INVALID_TIME_FRACTION, events.get(0).getCode());
assertTrue(hasComment(e));
}
}
Loading