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
47 changes: 40 additions & 7 deletions src/Helpers/Enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,48 @@ export const CONDUCTING_EQUIPMENT_TYPES = new Set([
]);

// Valid LNClass enum values
// System LN Group + Domain LN Groups from OCL Helpers/Enums.ocl
export const LN_CLASS_ENUM = new Set([
'LLN0', 'LPHD', 'LNOD', 'TVTR', 'TCTR', 'TCLK', 'TRES', 'TWPI', 'TWPP',
'TPOS', 'TSPT', 'TPAS', 'TMPS', 'TAMV', 'TAMP', 'TVLV', 'TWQI', 'TWQP',
// System LN Group
'LLN0', 'LPHD', 'LCCH', 'LGOS', 'LSVS', 'LTIM', 'LTMS', 'LTRK',
// Domain A
'ANCR', 'ARCO', 'ARIS', 'ATCC', 'AVCO',
// Domain C
'CALH', 'CCGR', 'CILO', 'CPOW', 'CSWI', 'CSYN',
// Domain F
'FCNT', 'FCSD', 'FFIL', 'FLIM', 'FPID', 'FRMP', 'FSPT', 'FXOT', 'FXUT',
// Domain G
'GAPC', 'GGIO', 'GLOG', 'GSAL',
// Domain I
'IARC', 'IHMI', 'ISAF', 'ITCI', 'ITMI', 'ITPC',
// Domain K
'KFAN', 'KFIL', 'KPMP', 'KTNK', 'KVLV',
// Domain M
'MHAI', 'MHAN', 'MHYD', 'MMDC', 'MMET', 'MMTN', 'MMTR', 'MMXN', 'MMXU', 'MSQI', 'MSTA',
// Domain P
'PDIF', 'PDIR', 'PDIS', 'PDOP', 'PDUP', 'PFRC', 'PHAR', 'PHIZ', 'PIOC', 'PMRI', 'PMSS', 'POPF', 'PPAM', 'PRTR', 'PSCH', 'PSDE', 'PTEF', 'PTHF', 'PTOC', 'PTOF', 'PTOV', 'PTRC', 'PTTR', 'PTUC', 'PTUF', 'PTUV', 'PUPF',
// Domain Q
'QFVR', 'QITR', 'QIUB', 'QVTR', 'QVUB', 'QVVR',
// Domain R
'RADR', 'RBDR', 'RBRF', 'RDIR', 'RDRE', 'RDRS', 'RFLO', 'RMXU', 'RPSB', 'RREC', 'RSYN',
// Domain S
'SARC', 'SCBR', 'SIMG', 'SIML', 'SLTC', 'SOPM', 'SPDC', 'SPTR', 'SSWI', 'STMP', 'SVBR',
// Domain T
'TANG', 'TAXD', 'TDST', 'TFLW', 'TGSN', 'THUM', 'TLVL', 'TMGF', 'TMVM', 'TPRS', 'TRTN', 'TSND', 'TTMP', 'TTNS', 'TVBR', 'TVTR', 'TWPH',
// Domain X
'XCBR', 'XSWI',
// Domain Y
'YEFN', 'YLTC', 'YPSH', 'YPTR',
// Domain Z
'ZAXN', 'ZBAT', 'ZBSH', 'ZCAB', 'ZCAP', 'ZCON', 'ZGEN', 'ZGIL', 'ZLIN', 'ZMOT', 'ZREA', 'ZRES', 'ZRRC', 'ZSAR', 'ZSCR', 'ZSMC', 'ZTCF', 'ZTCR',
// Existing values from original set (not in OCL domain groups but used in practice)
'LNOD', 'TCTR', 'TCLK', 'TRES', 'TWPI', 'TWPP', 'TPOS', 'TSPT', 'TPAS', 'TMPS', 'TAMV', 'TAMP', 'TVLV', 'TWQI', 'TWQP',
'TFLV', 'TFLP', 'TFLQ', 'TFFQ', 'TFRQ', 'TITV', 'TITQ', 'TITP', 'TITF',
'BB00', 'BBO1', 'BBO2', 'BBO3', 'BRCB', 'BBRB', 'DIR', 'DOP', 'DOI', 'DOC',
'CSWI', 'XCBR', 'XSWI', 'CSWI2', 'XCBR2', 'XSWI2', 'CSWI3', 'XCBR3', 'XSWI3',
'CSWI2', 'XCBR2', 'XSWI2', 'CSWI3', 'XCBR3', 'XSWI3',
'SGCB', 'XSGC', 'CSGR', 'XSGR', 'MXGR', 'XMXG', 'AGCS', 'XGCS', 'ACSE',
'CSAC', 'XSAC', 'ATCC', 'XATC', 'TCTR', 'TVTR', 'DRCS', 'XDRC', 'RDRE',
'XDRE', 'RDRS', 'XRDR', 'RREC', 'XRRC', 'CILO', 'XCIL', 'SPC', 'XSPC',
'CSAC', 'XSAC', 'XATC', 'DRCS', 'XDRC',
'RDRE', 'XDRE', 'RDRS', 'XRDR',
'RSPC', 'XRPC', 'SEL', 'XSEL', 'SRGC', 'XSGC', 'SICF', 'XSIC', 'SICC',
'XSIC2', 'SICC2', 'XSIC3', 'SICC3', 'SIMG', 'XSIM', 'SIMC', 'XSIC4', 'SICC4'
]);
Expand Down Expand Up @@ -81,8 +114,8 @@ export function validSclLNClassEnum(lnClass: string): boolean {
return LN_CLASS_ENUM.has(lnClass);
}

// Valid prefix pattern: alphanumeric and special characters
export const PREFIX_PATTERN = /^[a-zA-Z0-9_\-]{0,4}$/;
// Valid prefix pattern from OCL BaseSimpleTypes.ocl: empty string OR [A-Za-z][0-9A-Za-z_]{0,10}
export const PREFIX_PATTERN = /^(|[A-Za-z][0-9A-Za-z_]{0,10})$/;

// Validate prefix
export function validSclPrefix(prefix: string): boolean {
Expand Down
90 changes: 90 additions & 0 deletions src/RequiredAttributes/AnyLN.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ValidationIssue } from '../ValidationIssue';
import { LN_CLASS_ENUM } from '../Helpers/Enums';

const selectorAnyLN = [
':root > IED > AccessPoint > LN',
':root > IED > AccessPoint > Server > LDevice > LN0',
':root > IED > AccessPoint > Server > LDevice > LN',
];

// Rule: LN_lnClass_required
export function ln_lnClass_required(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(selectorAnyLN.join(', ')))
.filter((element) => !element.getAttribute('lnClass'))
.map((element) => ({
severity: 'ERROR',
id: 'LN_lnClass_required',
filename: docName,
message: 'lnClass attribute shall be present in AnyLN',
element: element,
}));
}

// Rule: LN_lnClass_valid
export function ln_lnClass_valid(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(selectorAnyLN.join(', ')))
.filter((element) => {
const lnClass = element.getAttribute('lnClass');
return lnClass !== null && !isValidSclLNClass(lnClass);
})
.map((element) => ({
severity: 'ERROR',
id: 'LN_lnClass_valid',
filename: docName,
message: 'lnClass attribute shall be valid in AnyLN',
element: element,
}));
}

// Rule: LN_inst_required
export function ln_inst_required(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(selectorAnyLN.join(', ')))
.filter((element) => element.getAttribute('inst') === null)
.map((element) => ({
severity: 'ERROR',
id: 'LN_inst_required',
filename: docName,
message: 'inst attribute shall be present in AnyLN',
element: element,
}));
}

// Rule: AnyLn_lnType_required
export function anyLn_lnType_required(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(selectorAnyLN.join(', ')))
.filter((element) => element.getAttribute('lnType') === null)
.map((element) => ({
severity: 'ERROR',
id: 'AnyLN_lnType_required',
filename: docName,
message: 'lnType attribute shall be present in AnyLN',
element: element,
}));
}

// Rule: AnyLn_lnType_valid
export function anyLn_lnType_valid(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(selectorAnyLN.join(', ')))
.filter((element) => {
const lnType = element.getAttribute('lnType');
return lnType !== null && !isValidSclName(lnType);
})
.map((element) => ({
severity: 'ERROR',
id: 'AnyLN_lnType_valid',
filename: docName,
message: 'lnType attribute shall be valid in AnyLN',
element: element,
}));
}

function isValidSclLNClass(lnClass: string): boolean {
// OCL: validSclLNClassEnum(s) = validSclPredefinedLNClassEnum(s) OR validSclExtensionLNClassEnum(s)
// Predefined: check against LN_CLASS_ENUM
// Extension: exactly 4 uppercase letters
return LN_CLASS_ENUM.has(lnClass) || /^[A-Z]{4}$/.test(lnClass);
}

function isValidSclName(name: string): boolean {
return name.length > 0 && name.length <= 128;
}
186 changes: 186 additions & 0 deletions src/RequiredAttributes/Association.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { LN_CLASS_ENUM, validSclPrefix } from '../Helpers/Enums';
import { ValidationIssue } from '../ValidationIssue';

// Rule: association_kind_required
export function association_kind_required(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(':root > IED > AccessPoint > Server > Association'))
.filter((association) => !association.getAttribute('kind'))
.map((association) => ({
severity: 'ERROR',
id: 'Association_kind_required',
filename: docName,
message: 'kind attribute shall be present in Association',
element: association,
}));
}

// Rule: association_associationID_valid
export function association_associationID_valid(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(':root > IED > AccessPoint > Server > Association'))
.filter((association) => {
const associationID = association.getAttribute('associationID');
return associationID !== null && !isValidSclAssociationID(associationID);
})
.map((association) => ({
severity: 'ERROR',
id: 'Association_associationID_valid',
filename: docName,
message: 'associationID attribute shall be valid in Association',
element: association,
}));
}

// Rule: association_prefix_valid
export function association_prefix_valid(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(':root > IED > AccessPoint > Server > Association'))
.filter((association) => {
const prefix = association.getAttribute('prefix');
return prefix !== null && !validSclPrefix(prefix);
})
.map((association) => ({
severity: 'ERROR',
id: 'Association_prefix_valid',
filename: docName,
message: 'prefix attribute shall be valid in Association',
element: association,
}));
}

// Rule: association_lnClass_required
export function association_lnClass_required(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(':root > IED > AccessPoint > Server > Association'))
.filter((association) => !association.getAttribute('lnClass'))
.map((association) => ({
severity: 'ERROR',
id: 'Association_lnClass_required',
filename: docName,
message: 'lnClass attribute shall be present in Association',
element: association,
}));
}

// Rule: association_lnClass_valid
export function association_lnClass_valid(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(':root > IED > AccessPoint > Server > Association'))
.filter((association) => {
const lnClass = association.getAttribute('lnClass');
return lnClass !== null && !isValidSclLNClass(lnClass);
})
.map((association) => ({
severity: 'ERROR',
id: 'Association_lnClass_valid',
filename: docName,
message: 'lnClass attribute shall be valid in Association',
element: association,
}));
}

// Rule: association_lnInst_required
export function association_lnInst_required(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(':root > IED > AccessPoint > Server > Association'))
.filter((association) => !association.getAttribute('lnInst'))
.map((association) => ({
severity: 'ERROR',
id: 'Association_lnInst_required',
filename: docName,
message: 'lnInst attribute shall be present in Association',
element: association,
}));
}

// Rule: association_lnInst_valid
export function association_lnInst_valid(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(':root > IED > AccessPoint > Server > Association'))
.filter((association) => {
const lnInst = association.getAttribute('lnInst');
return lnInst !== null && !isValidSclLNInstOrEmpty(lnInst);
})
.map((association) => ({
severity: 'ERROR',
id: 'Association_lnInst_valid',
filename: docName,
message: 'lnInst attribute shall be valid in Association',
element: association,
}));
}

// Rule: association_iedName_required
export function association_iedName_required(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(':root > IED > AccessPoint > Server > Association'))
.filter((association) => !association.getAttribute('iedName'))
.map((association) => ({
severity: 'ERROR',
id: 'Association_iedName_required',
filename: docName,
message: 'iedName attribute shall be present in Association',
element: association,
}));
}

// Rule: association_iedName_valid
export function association_iedName_valid(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(':root > IED > AccessPoint > Server > Association'))
.filter((association) => {
const iedName = association.getAttribute('iedName');
return iedName !== null && !isValidSclIEDName(iedName);
})
.map((association) => ({
severity: 'ERROR',
id: 'Association_iedName_valid',
filename: docName,
message: 'iedName attribute shall be valid in Association',
element: association,
}));
}

// Rule: association_ldInst_required
export function association_ldInst_required(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(':root > IED > AccessPoint > Server > Association'))
.filter((association) => !association.getAttribute('ldInst'))
.map((association) => ({
severity: 'ERROR',
id: 'Association_ldInst_required',
filename: docName,
message: 'ldInst attribute shall be present in Association',
element: association,
}));
}

// Rule: association_ldInst_valid
export function association_ldInst_valid(scl: XMLDocument, docName: string): ValidationIssue[] {
return Array.from(scl.querySelectorAll(':root > IED > AccessPoint > Server > Association'))
.filter((association) => {
const ldInst = association.getAttribute('ldInst');
return ldInst !== null && !isValidSclLDInst(ldInst);
})
.map((association) => ({
severity: 'ERROR',
id: 'Association_ldInst_valid',
filename: docName,
message: 'ldInst attribute shall be valid in Association',
element: association,
}));
}

function isValidSclAssociationID(id: string): boolean {
return id.length >= 1 && id.length <= 128;
}

function isValidSclLNClass(lnClass: string): boolean {
// OCL: validSclLNClassEnum(s) = validSclPredefinedLNClassEnum(s) OR validSclExtensionLNClassEnum(s)
// Predefined: check against LN_CLASS_ENUM
// Extension: exactly 4 uppercase letters
return LN_CLASS_ENUM.has(lnClass) || /^[A-Z]{4}$/.test(lnClass);
}

function isValidSclLNInstOrEmpty(lnInst: string): boolean {
return lnInst.length === 0 || /^[a-zA-Z0-9_]+$/.test(lnInst);
}

function isValidSclIEDName(iedName: string): boolean {
return /^[\x20-\x7E]+$/.test(iedName) && iedName.length >= 1 && iedName.length <= 129;
}

function isValidSclLDInst(ldInst: string): boolean {
return /^[A-Za-z0-9][0-9A-Za-z_]{0,63}$/.test(ldInst);
}
Loading
Loading