///|
/// Validation profile for station and protocol admission checks.
pub struct ValidationProfile {
max_points : Int
max_history : Int
require_time_tag_for_events : Bool
reject_unknown_types : Bool
require_non_global_station : Bool
} derive(Eq, Debug)
///|
pub fn ValidationProfile::default() -> ValidationProfile {
{
max_points: 4096,
max_history: 4096,
require_time_tag_for_events: false,
reject_unknown_types: true,
require_non_global_station: true,
}
}
///|
pub fn ValidationProfile::new(
max_points? : Int = 4096,
max_history? : Int = 4096,
require_time_tag_for_events? : Bool = false,
reject_unknown_types? : Bool = true,
require_non_global_station? : Bool = true,
) -> Result[ValidationProfile, String] {
if max_points < 1 || max_history < 1 {
Err("validation limits must be positive")
} else {
Ok({
max_points,
max_history,
require_time_tag_for_events,
reject_unknown_types,
require_non_global_station,
})
}
}
///|
pub fn ValidationProfile::max_points(self : ValidationProfile) -> Int {
self.max_points
}
///|
pub fn ValidationProfile::max_history(self : ValidationProfile) -> Int {
self.max_history
}
///|
pub fn ValidationProfile::require_time_tag_for_events(
self : ValidationProfile,
) -> Bool {
self.require_time_tag_for_events
}
///|
pub fn ValidationProfile::reject_unknown_types(
self : ValidationProfile,
) -> Bool {
self.reject_unknown_types
}
///|
pub fn ValidationProfile::require_non_global_station(
self : ValidationProfile,
) -> Bool {
self.require_non_global_station
}
///|
pub enum ValidationIssueKind {
ValidationMissing
ValidationOutOfRange
ValidationType
ValidationCapacity
ValidationQuality
ValidationPolicy
} derive(Eq, Debug)
///|
pub struct ValidationIssue {
kind : ValidationIssueKind
path : String
message : String
address : InformationAddress?
fatal : Bool
} derive(Eq, Debug)
///|
pub fn ValidationIssue::kind(self : ValidationIssue) -> ValidationIssueKind {
self.kind
}
///|
pub fn ValidationIssue::path(self : ValidationIssue) -> String {
self.path
}
///|
pub fn ValidationIssue::message(self : ValidationIssue) -> String {
self.message
}
///|
pub fn ValidationIssue::address(self : ValidationIssue) -> InformationAddress? {
self.address
}
///|
pub fn ValidationIssue::fatal(self : ValidationIssue) -> Bool {
self.fatal
}
///|
pub struct ValidationReport {
subject : String
valid : Bool
issues : Array[ValidationIssue]
checked : Int
} derive(Debug)
///|
pub fn ValidationReport::subject(self : ValidationReport) -> String {
self.subject
}
///|
pub fn ValidationReport::valid(self : ValidationReport) -> Bool {
self.valid
}
///|
pub fn ValidationReport::issues(
self : ValidationReport,
) -> Array[ValidationIssue] {
self.issues.copy()
}
///|
pub fn ValidationReport::checked(self : ValidationReport) -> Int {
self.checked
}
///|
fn validation_issue(
kind : ValidationIssueKind,
path : String,
message : String,
address : InformationAddress?,
fatal : Bool,
) -> ValidationIssue {
{ kind, path, message, address, fatal }
}
///|
pub fn validate_application_object(
object : ApplicationObject,
profile : ValidationProfile,
) -> ValidationReport {
let issues : Array[ValidationIssue] = []
match object.validate() {
Ok(_) => ()
Err(error) =>
issues.push(
validation_issue(
ValidationType,
"object",
error.message(),
Some(object.address()),
true,
),
)
}
if profile.reject_unknown_types() && object.type_id() is UnknownType(_) {
issues.push(
validation_issue(
ValidationType,
"type-id",
"unknown application type",
Some(object.address()),
true,
),
)
}
{
subject: "application-object",
valid: issues.length() == 0,
issues,
checked: 1,
}
}
///|
pub fn validate_station_config(
config : StationConfig,
profile : ValidationProfile,
) -> ValidationReport {
let issues : Array[ValidationIssue] = []
if profile.require_non_global_station() && config.common_address().is_global() {
issues.push(
validation_issue(
ValidationPolicy,
"common-address",
"global address is not a station identity",
None,
true,
),
)
}
if config.point_capacity() > profile.max_points() {
issues.push(
validation_issue(
ValidationCapacity,
"point-capacity",
"point capacity exceeds validation profile",
None,
false,
),
)
}
if config.history_limit() > profile.max_history() {
issues.push(
validation_issue(
ValidationCapacity,
"history-limit",
"history limit exceeds validation profile",
None,
false,
),
)
}
{ subject: config.name(), valid: issues.length() == 0, issues, checked: 1 }
}
///|
pub fn validate_point_store(
store : PointStore,
profile : ValidationProfile,
) -> ValidationReport {
let issues : Array[ValidationIssue] = []
let points = store.query(PointFilter::all())
if points.length() > profile.max_points() {
issues.push(
validation_issue(
ValidationCapacity,
"points",
"point store exceeds configured capacity",
None,
true,
),
)
}
if store.history_len() > profile.max_history() {
issues.push(
validation_issue(
ValidationCapacity,
"history",
"point history exceeds configured capacity",
None,
false,
),
)
}
let mut checked = 0
for record in points {
checked += 1
if !record.is_valid() {
issues.push(
validation_issue(
ValidationQuality,
"quality",
"point quality is unusable",
Some(record.address()),
false,
),
)
}
}
{ subject: "point-store", valid: issues.length() == 0, issues, checked }
}
///|
pub fn validate_transport_frame_admission(
data : Bytes,
limits : ResourceLimits,
) -> ValidationReport {
let issues : Array[ValidationIssue] = []
if data.length() > limits.max_apdu() {
issues.push(
validation_issue(
ValidationCapacity,
"apdu",
"APDU exceeds resource limit",
None,
true,
),
)
}
match decode_frame(data) {
Ok(frame) =>
if frame.kind == Information && frame.payload.length() > limits.max_asdu() {
issues.push(
validation_issue(
ValidationCapacity,
"asdu",
"ASDU exceeds resource limit",
None,
true,
),
)
}
Err(error) =>
issues.push(validation_issue(ValidationType, "frame", error, None, true))
}
{
subject: "transport-frame",
valid: issues.length() == 0,
issues,
checked: 1,
}
}
///|
pub fn validation_issue_kind_examples() -> Array[ValidationIssueKind] {
[
ValidationMissing,
ValidationOutOfRange,
ValidationType,
ValidationCapacity,
ValidationQuality,
ValidationPolicy,
]
}