VisioPackageValidator.java
package com.taxonomy.export;
import com.taxonomy.visio.VisioConnect;
import com.taxonomy.visio.VisioDocument;
import com.taxonomy.visio.VisioPage;
import com.taxonomy.visio.VisioShape;
import com.taxonomy.visio.VisioProperty;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Validates the bounded in-memory model before any VSDX bytes are emitted. */
final class VisioPackageValidator {
static final long MAX_VISIO_ID = 0xffff_ffffL;
private VisioPackageValidator() {
}
static void validate(VisioDocument document) {
if (document == null) {
throw invalid("Visio document must not be null");
}
List<VisioPage> pages = document.getPages();
if (pages == null) {
throw invalid("Visio page collection must not be null");
}
if (pages.isEmpty() || pages.size() > 32) throw invalid("Visio profile requires 1 to 32 pages");
validateProperties(document.getProperties());
for (var loss : document.getLosses()) {
if (loss == null) throw invalid("Missing loss entry");
for (String text : List.of(loss.scope(), loss.id(), loss.field(), loss.kind(), loss.rationale())) validateXmlText(text, "Loss report");
}
java.util.Set<Long> pageIds = new java.util.HashSet<>();
long shapeCount = 0, connectorCount = 0;
for (int pageIndex = 0; pageIndex < pages.size(); pageIndex++) {
VisioPage page = pages.get(pageIndex);
if (page == null) {
throw invalid("Page " + pageIndex + " must not be null");
}
if (page.getId() == null || !page.getId().matches("0|[1-9][0-9]*")) throw invalid("Page ID must be a canonical unsigned integer");
long pageId;
try { pageId = Long.parseLong(page.getId()); } catch (NumberFormatException e) { throw invalid("Page ID out of range", e); }
if (pageId > MAX_VISIO_ID || !pageIds.add(pageId)) throw invalid("Duplicate or out-of-range page ID");
validateXmlText(page.getName(), "Page " + pageIndex + " name");
List<VisioShape> shapes = page.getShapes();
if (shapes == null) {
throw invalid("Shape collection on page " + pageIndex + " must not be null");
}
List<VisioConnect> connects = page.getConnects();
if (connects == null) {
throw invalid("Connect collection on page " + pageIndex + " must not be null");
}
shapeCount += shapes.size(); connectorCount += connects.size();
if (shapeCount > 20_000 || connectorCount > 60_000) throw invalid("Visio profile shape/connector limit exceeded");
Map<Long, VisioShape> shapesById = new HashMap<>();
long maximumShapeId = 0;
for (int shapeIndex = 0; shapeIndex < shapes.size(); shapeIndex++) {
VisioShape shape = shapes.get(shapeIndex);
if (shape == null) {
throw invalid("Shape " + shapeIndex + " on page " + pageIndex
+ " must not be null");
}
long shapeId = parseShapeId(
shape.getId(), "Shape " + shapeIndex + " on page " + pageIndex);
if (shapesById.putIfAbsent(shapeId, shape) != null) {
throw invalid("Duplicate Visio shape ID " + shapeId
+ " on page " + pageIndex);
}
maximumShapeId = Math.max(maximumShapeId, shapeId);
validateProperties(shape.getProperties());
validateXmlText(shape.getText(), "Text of shape " + shapeId);
if (shape.getType() != null) {
validateXmlText(shape.getType(), "Type of shape " + shapeId);
}
validateCoordinate(shape.getX(), "PinX of shape " + shapeId, true);
validateCoordinate(shape.getY(), "PinY of shape " + shapeId, true);
validateCoordinate(shape.getWidth(), "Width of shape " + shapeId, false);
validateCoordinate(shape.getHeight(), "Height of shape " + shapeId, false);
validateCoordinate(
shape.getX() + shape.getWidth() / 2.0,
"Right page extent of shape " + shapeId,
false);
validateCoordinate(
shape.getY() + shape.getHeight() / 2.0,
"Top page extent of shape " + shapeId,
false);
}
if (maximumShapeId > MAX_VISIO_ID - connects.size()) {
throw invalid("Connector IDs overflow the Visio unsigned-int range on page "
+ pageIndex);
}
for (int connectIndex = 0; connectIndex < connects.size(); connectIndex++) {
VisioConnect connect = connects.get(connectIndex);
if (connect == null) {
throw invalid("Connector " + connectIndex + " on page " + pageIndex
+ " must not be null");
}
validateProperties(connect.getProperties());
long fromId = parseShapeId(
connect.getFromShape(), "Source of connector " + connectIndex);
long toId = parseShapeId(
connect.getToShape(), "Target of connector " + connectIndex);
VisioShape source = shapesById.get(fromId);
if (source == null) {
throw invalid("Connector " + connectIndex
+ " references missing source shape " + fromId);
}
VisioShape target = shapesById.get(toId);
if (target == null) {
throw invalid("Connector " + connectIndex
+ " references missing target shape " + toId);
}
validateCoordinate(
Math.hypot(
target.getX() - source.getX(),
target.getY() - source.getY()),
"Straight-line length of connector " + connectIndex,
false);
validateXmlText(
connect.getRelationType(), "Label of connector " + connectIndex);
}
}
}
static long parseShapeId(String value, String field) {
if (value == null || !value.matches("[1-9][0-9]*")) {
throw invalid(field + " must contain a positive numeric Visio ID");
}
try {
long id = Long.parseLong(value);
if (id <= 0 || id > MAX_VISIO_ID) {
throw invalid(field + " must be between 1 and " + MAX_VISIO_ID
+ ", but was " + value);
}
return id;
} catch (NumberFormatException exception) {
throw invalid(field + " must contain a positive numeric Visio ID, but was "
+ value, exception);
}
}
private static void validateCoordinate(double value, String field, boolean allowZero) {
boolean inRange = Double.isFinite(value) && (allowZero ? value >= 0 : value > 0);
if (!inRange) {
throw invalid(field + " must be "
+ (allowZero ? "finite and non-negative" : "finite and positive")
+ ", but was " + value);
}
}
static void validateXmlText(String value, String field) {
if (value == null) {
throw invalid(field + " must not be null");
}
if (value.length() > 32767) throw invalid(field + " exceeds 32767 characters");
for (int offset = 0; offset < value.length();) {
int codePoint = value.codePointAt(offset);
if (!isXml10CodePoint(codePoint)) {
throw invalid(field + " contains an XML 1.0 control or surrogate character: U+"
+ Integer.toHexString(codePoint).toUpperCase());
}
offset += Character.charCount(codePoint);
}
}
static void validateProperties(Map<String, VisioProperty> properties) {
if (properties.size() > 64) throw invalid("Too many Visio properties");
for (var entry : properties.entrySet()) {
if (entry.getKey() == null || !entry.getKey().matches("taxonomy\\.[A-Za-z][A-Za-z0-9]{0,63}") || entry.getValue() == null) {
throw invalid("Unsafe Visio property key/value");
}
validateXmlText(entry.getValue().value(), entry.getKey());
}
}
private static boolean isXml10CodePoint(int codePoint) {
return codePoint == 0x9
|| codePoint == 0xA
|| codePoint == 0xD
|| (codePoint >= 0x20 && codePoint <= 0xD7FF)
|| (codePoint >= 0xE000 && codePoint <= 0xFFFD)
|| (codePoint >= 0x10000 && codePoint <= 0x10FFFF);
}
private static IllegalArgumentException invalid(String message) {
return new IllegalArgumentException(message);
}
private static IllegalArgumentException invalid(String message, Exception cause) {
return new IllegalArgumentException(message, cause);
}
}