2025-12-24 17:23:41 +08:00
|
|
|
package aviation
|
2024-07-20 18:26:17 +08:00
|
|
|
|
|
|
|
|
import (
|
2025-11-17 11:47:23 +08:00
|
|
|
"caatsm/internal/adapter/dto"
|
2025-11-15 09:01:24 +08:00
|
|
|
"errors"
|
2024-07-20 22:21:12 +08:00
|
|
|
"fmt"
|
2024-07-20 22:33:22 +08:00
|
|
|
"regexp"
|
2024-07-20 18:26:17 +08:00
|
|
|
"strings"
|
2024-07-20 22:25:28 +08:00
|
|
|
"time"
|
2024-08-14 14:29:24 +08:00
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
2024-07-20 18:26:17 +08:00
|
|
|
)
|
|
|
|
|
|
2024-07-22 08:12:18 +08:00
|
|
|
var (
|
2024-08-06 22:46:01 +08:00
|
|
|
otherPatterns = []*regexp.Regexp{navPattern,
|
|
|
|
|
remarkPattern,
|
|
|
|
|
selPattern,
|
|
|
|
|
pbnPattern,
|
|
|
|
|
eetPattern,
|
|
|
|
|
performancePattern,
|
2024-08-07 17:05:42 +08:00
|
|
|
regPattern,
|
2024-08-06 22:46:01 +08:00
|
|
|
reroutePattern}
|
2025-11-15 09:01:24 +08:00
|
|
|
// ErrHeaderParse indicates an invalid header section.
|
|
|
|
|
ErrHeaderParse = errors.New("invalid telegram header")
|
|
|
|
|
// ErrBodyParse indicates a failure matching the telegram body.
|
|
|
|
|
ErrBodyParse = errors.New("invalid telegram body")
|
2024-07-22 08:12:18 +08:00
|
|
|
)
|
2024-07-22 00:01:27 +08:00
|
|
|
|
2024-07-20 22:21:12 +08:00
|
|
|
type BodyParser struct {
|
2024-07-24 16:03:45 +08:00
|
|
|
body string
|
2024-08-06 12:44:32 +08:00
|
|
|
bodyPatterns map[string]BodyConfig
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-24 16:03:45 +08:00
|
|
|
func NewBodyParser(body string) *BodyParser {
|
2024-07-24 16:53:30 +08:00
|
|
|
return &BodyParser{
|
2024-08-06 12:44:32 +08:00
|
|
|
bodyPatterns: bodyPatterns,
|
2024-07-24 16:53:30 +08:00
|
|
|
body: body,
|
|
|
|
|
}
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-26 17:55:25 +08:00
|
|
|
// GetBodyPatterns returns the body patterns map.
|
|
|
|
|
// The bodyPatterns map is a reference to the package-level bodyPatterns,
|
|
|
|
|
// which is initialized once at startup and never modified, making it safe
|
|
|
|
|
// for concurrent reads without synchronization.
|
2024-08-07 17:05:42 +08:00
|
|
|
func (parser *BodyParser) GetBodyPatterns() map[string]BodyConfig {
|
2025-12-26 17:55:25 +08:00
|
|
|
return parser.bodyPatterns
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
|
|
|
|
|
2024-08-07 17:05:42 +08:00
|
|
|
func (parser *BodyParser) Parse() (string, interface{}, error) {
|
|
|
|
|
parser.body = strings.TrimSpace(parser.body)
|
|
|
|
|
category := findCategory(parser.body)
|
2024-07-22 00:01:27 +08:00
|
|
|
if category == "" {
|
2024-07-23 16:45:03 +08:00
|
|
|
return "", nil, fmt.Errorf("no category found in body text")
|
2024-07-22 00:01:27 +08:00
|
|
|
}
|
2024-07-23 16:45:03 +08:00
|
|
|
|
2025-12-24 17:40:52 +08:00
|
|
|
patternConfig, exists := parser.bodyPatterns[category]
|
|
|
|
|
if !exists || patternConfig.Patterns == nil {
|
2025-12-26 17:55:25 +08:00
|
|
|
return "", nil, fmt.Errorf("no matching pattern found for category: %s", category)
|
2025-12-24 17:40:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx := ParseContext{
|
|
|
|
|
Body: parser.body,
|
|
|
|
|
Tokens: Tokenizer{}.Tokenize(parser.body),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, p := range patternConfig.Patterns {
|
|
|
|
|
if data := extract(parser.body, p.Expression); data != nil {
|
|
|
|
|
parsed, err := parseCategory(category, ctx, data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", nil, err
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
2025-12-24 17:40:52 +08:00
|
|
|
return category, parsed, nil
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-24 17:40:52 +08:00
|
|
|
|
2024-08-07 17:05:42 +08:00
|
|
|
return "", nil, fmt.Errorf("no matching pattern found for body: %s", parser.body)
|
2024-07-22 00:01:27 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func findCategory(body string) string {
|
2024-07-24 10:33:12 +08:00
|
|
|
if match := categoryRegex.FindStringSubmatch(body); match != nil {
|
|
|
|
|
for i, name := range categoryRegex.SubexpNames() {
|
2024-07-22 12:42:30 +08:00
|
|
|
if i != 0 && name == "category" {
|
|
|
|
|
return match[i]
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-07-22 00:01:27 +08:00
|
|
|
}
|
|
|
|
|
return ""
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-31 16:04:52 +08:00
|
|
|
func extract(data string, exp *regexp.Regexp) map[string]string {
|
2025-12-26 17:55:25 +08:00
|
|
|
// Use timeout protection to prevent ReDoS attacks
|
|
|
|
|
match, err := MatchWithTimeout(exp, data, DefaultRegexTimeout)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// Timeout occurred - return nil to indicate no match
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2024-07-29 17:54:52 +08:00
|
|
|
if len(match) > 0 {
|
|
|
|
|
return extractData(match, exp)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-20 22:33:22 +08:00
|
|
|
func extractData(match []string, re *regexp.Regexp) map[string]string {
|
|
|
|
|
data := make(map[string]string)
|
|
|
|
|
for i, name := range re.SubexpNames() {
|
|
|
|
|
if i != 0 && name != "" {
|
2024-07-24 15:20:00 +08:00
|
|
|
data[name] = strings.TrimSpace(match[i])
|
2024-07-20 22:33:22 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return data
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 13:07:09 +08:00
|
|
|
func headerToParsedTelegram(header Header) dto.ParsedTelegram {
|
|
|
|
|
return dto.ParsedTelegram{
|
|
|
|
|
MessageID: header.MessageID,
|
|
|
|
|
DateTime: header.DateTime,
|
|
|
|
|
PriorityIndicator: header.PriorityIndicator,
|
|
|
|
|
PrimaryAddress: header.PrimaryAddress,
|
|
|
|
|
SecondaryAddresses: header.SecondaryAddresses,
|
|
|
|
|
Originator: header.Originator,
|
|
|
|
|
OriginatorDateTime: header.OriginatorDateTime,
|
|
|
|
|
Category: header.Category,
|
|
|
|
|
Body: header.Body,
|
|
|
|
|
Content: header.Content,
|
|
|
|
|
ReceivedAt: header.ReceivedAt,
|
|
|
|
|
ParsedAt: header.ParsedAt,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 17:55:25 +08:00
|
|
|
// Parse parses a raw ICAO aviation telegram and returns a ParsedTelegram with parsing status.
|
|
|
|
|
//
|
|
|
|
|
// IMPORTANT ERROR HANDLING PATTERN:
|
|
|
|
|
// This function intentionally returns both a non-nil ParsedTelegram AND an error when parsing fails.
|
|
|
|
|
// This design decision allows the caller to persist failed parse attempts with error details to the
|
|
|
|
|
// database for audit and compliance purposes. This pattern is specific to the aviation parser's
|
|
|
|
|
// error handling strategy where parser failures are permanent (ACK'd, not retried) and must be
|
|
|
|
|
// stored for regulatory compliance and troubleshooting.
|
|
|
|
|
//
|
|
|
|
|
// Error Handling Strategy:
|
|
|
|
|
// - Input validation failure: Returns ParsedTelegram with MessageStatusHeaderError + ErrHeaderParse
|
|
|
|
|
// - Header parse failure: Returns ParsedTelegram with MessageStatusHeaderError + ErrHeaderParse
|
|
|
|
|
// - Body parse failure: Returns ParsedTelegram with MessageStatusBodyError + ErrBodyParse
|
|
|
|
|
// - Success: Returns ParsedTelegram with MessageStatusParsed + nil error
|
|
|
|
|
//
|
|
|
|
|
// The returned ParsedTelegram is ALWAYS non-nil and safe to use, even when error is non-nil.
|
|
|
|
|
// Callers should check both the error and the ParsedTelegram.Status field to determine the outcome.
|
|
|
|
|
//
|
|
|
|
|
// Security:
|
|
|
|
|
// - Input size validation prevents DoS attacks (max 1800 chars per AFTN standard)
|
|
|
|
|
// - Regex timeout protection prevents ReDoS attacks (100ms timeout)
|
|
|
|
|
//
|
|
|
|
|
// Example usage:
|
|
|
|
|
//
|
|
|
|
|
// parsed, err := Parse(rawTelegram)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// // Parse failed, but parsed contains error details for storage
|
|
|
|
|
// repository.InsertRaw(parsed) // Store for audit
|
|
|
|
|
// return Permanent(err) // Don't retry
|
|
|
|
|
// }
|
|
|
|
|
// // Parse succeeded
|
|
|
|
|
// repository.Insert(parsed)
|
|
|
|
|
// publisher.Publish(parsed.BodyData)
|
2025-11-17 11:47:23 +08:00
|
|
|
func Parse(rawText string) (*dto.ParsedTelegram, error) {
|
2025-12-26 17:55:25 +08:00
|
|
|
// Validate input size to prevent DoS attacks
|
|
|
|
|
if err := ValidateInputSize(rawText); err != nil {
|
|
|
|
|
msg := dto.NewParsedTelegram()
|
|
|
|
|
msg.Content = rawText
|
|
|
|
|
msg.Comments = err.Error()
|
|
|
|
|
msg.ErrorReason = err.Error()
|
|
|
|
|
msg.Status = dto.MessageStatusHeaderError
|
|
|
|
|
return msg, fmt.Errorf("%w: %w", ErrHeaderParse, err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 09:58:49 +08:00
|
|
|
header, err := ParseHeader(rawText)
|
2024-07-20 22:21:12 +08:00
|
|
|
if err != nil {
|
2025-11-17 11:47:23 +08:00
|
|
|
msg := dto.NewParsedTelegram()
|
2024-08-09 11:07:07 +08:00
|
|
|
msg.Content = rawText
|
2025-11-15 09:01:24 +08:00
|
|
|
msg.Comments = err.Error()
|
|
|
|
|
msg.ErrorReason = err.Error()
|
2025-11-17 11:47:23 +08:00
|
|
|
msg.Status = dto.MessageStatusHeaderError
|
2025-11-15 09:01:24 +08:00
|
|
|
return msg, fmt.Errorf("%w: %w", ErrHeaderParse, err)
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
2024-07-22 16:23:15 +08:00
|
|
|
|
2025-11-16 09:58:49 +08:00
|
|
|
bodyParser := NewBodyParser(header.Body)
|
2025-11-15 09:01:24 +08:00
|
|
|
category, bodyData, bodyErr := bodyParser.Parse()
|
2025-11-16 09:58:49 +08:00
|
|
|
header.Category = category
|
|
|
|
|
header.ParsedAt = time.Now()
|
2024-07-23 16:45:03 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
if bodyErr != nil {
|
2025-11-19 13:07:09 +08:00
|
|
|
parsed := headerToParsedTelegram(header)
|
|
|
|
|
parsed.Parsed = false
|
|
|
|
|
parsed.Comments = bodyErr.Error()
|
|
|
|
|
parsed.Status = dto.MessageStatusBodyError
|
|
|
|
|
parsed.ErrorReason = bodyErr.Error()
|
|
|
|
|
return &parsed, fmt.Errorf("%w: %w", ErrBodyParse, bodyErr)
|
2025-11-16 09:58:49 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-19 13:07:09 +08:00
|
|
|
parsed := headerToParsedTelegram(header)
|
|
|
|
|
parsed.BodyData = bodyData
|
|
|
|
|
parsed.Parsed = true
|
|
|
|
|
parsed.Status = dto.MessageStatusParsed
|
2025-11-16 09:58:49 +08:00
|
|
|
parsed.Uuid = uuid.New().String()
|
2025-11-19 13:07:09 +08:00
|
|
|
return &parsed, nil
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-24 14:38:32 +08:00
|
|
|
func cleanMessage(text string) string {
|
2024-07-22 08:49:21 +08:00
|
|
|
cleanedText := emptyLineRemove.ReplaceAllString(text, "")
|
|
|
|
|
cleanText := strings.ReplaceAll(cleanedText, "\n\n", "\n")
|
2024-07-24 10:33:12 +08:00
|
|
|
if match := bodyOnly.FindStringSubmatch(cleanText); len(match) > 1 {
|
|
|
|
|
bodyContent := match[2]
|
|
|
|
|
if bodyContent[len(bodyContent)-1] == '\n' {
|
|
|
|
|
return bodyContent[:len(bodyContent)-1]
|
2024-07-22 08:49:21 +08:00
|
|
|
}
|
2024-07-24 10:33:12 +08:00
|
|
|
return bodyContent
|
2024-07-22 08:12:18 +08:00
|
|
|
}
|
|
|
|
|
return ""
|
2024-07-22 00:01:27 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-16 09:58:49 +08:00
|
|
|
// ParseHeader parses only the header portion of the message and returns a lightweight struct
|
|
|
|
|
// with header fields and body content. It is used internally by the aviation parser.
|
|
|
|
|
type Header struct {
|
|
|
|
|
MessageID string
|
|
|
|
|
DateTime string
|
|
|
|
|
PriorityIndicator string
|
|
|
|
|
PrimaryAddress string
|
|
|
|
|
SecondaryAddresses string
|
|
|
|
|
Originator string
|
|
|
|
|
OriginatorDateTime string
|
|
|
|
|
Category string
|
|
|
|
|
Content string
|
|
|
|
|
Body string
|
|
|
|
|
ReceivedAt time.Time
|
|
|
|
|
ParsedAt time.Time
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 17:55:25 +08:00
|
|
|
// ParseHeader parses the header portion of an ICAO telegram.
|
|
|
|
|
// It extracts message metadata (ID, datetime, addresses, originator) and separates
|
|
|
|
|
// the body content for subsequent parsing.
|
|
|
|
|
//
|
|
|
|
|
// Returns Header struct with parsed fields and the raw body content.
|
|
|
|
|
// On error, returns Header with Content field populated for audit purposes.
|
2025-11-16 09:58:49 +08:00
|
|
|
func ParseHeader(fullMessage string) (Header, error) {
|
2024-07-24 16:38:22 +08:00
|
|
|
cleaned := cleanMessage(fullMessage)
|
|
|
|
|
lines := strings.Split(cleaned, "\n")
|
2024-07-20 18:26:17 +08:00
|
|
|
|
2025-12-26 17:55:25 +08:00
|
|
|
if len(lines) < MinHeaderLines {
|
|
|
|
|
return Header{Content: fullMessage}, fmt.Errorf("invalid message format: expected at least %d lines, got %d", MinHeaderLines, len(lines))
|
2024-07-24 14:21:02 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-22 16:23:15 +08:00
|
|
|
_, messageID, dateTime, err := parseStartIndicator(lines[0])
|
2024-07-20 18:55:28 +08:00
|
|
|
if err != nil {
|
2025-11-16 09:58:49 +08:00
|
|
|
return Header{Content: fullMessage}, err
|
2024-07-20 18:26:17 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-22 00:01:27 +08:00
|
|
|
priorityIndicator, primaryAddress := parsePriorityAndPrimary(lines[1])
|
2024-07-24 16:38:22 +08:00
|
|
|
secondaryAddresses, originator, originatorDateTime, body := parseRemainingLines(lines[2:])
|
2024-07-20 18:55:28 +08:00
|
|
|
|
2025-11-16 09:58:49 +08:00
|
|
|
return Header{
|
2024-07-20 18:26:17 +08:00
|
|
|
MessageID: messageID,
|
|
|
|
|
DateTime: dateTime,
|
|
|
|
|
PriorityIndicator: priorityIndicator,
|
|
|
|
|
PrimaryAddress: primaryAddress,
|
|
|
|
|
SecondaryAddresses: secondaryAddresses,
|
|
|
|
|
Originator: originator,
|
|
|
|
|
OriginatorDateTime: originatorDateTime,
|
2024-08-09 11:07:07 +08:00
|
|
|
Content: fullMessage,
|
2024-07-24 16:38:22 +08:00
|
|
|
Body: body,
|
2024-07-20 22:25:28 +08:00
|
|
|
ReceivedAt: time.Now(),
|
2024-07-20 18:55:28 +08:00
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseStartIndicator(line string) (string, string, string, error) {
|
2024-07-20 22:33:22 +08:00
|
|
|
parts := strings.Fields(line)
|
2025-12-26 17:55:25 +08:00
|
|
|
if len(parts) >= MinStartIndicatorParts && strings.HasPrefix(parts[0], StartIndicatorPrefix) {
|
2024-07-20 22:33:22 +08:00
|
|
|
return parts[0], parts[1], parts[2], nil
|
2024-07-20 18:26:17 +08:00
|
|
|
}
|
2024-07-20 22:33:22 +08:00
|
|
|
return "", "", "", fmt.Errorf("invalid start indicator line format: %s", line)
|
2024-07-20 18:55:28 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-22 00:01:27 +08:00
|
|
|
func parsePriorityAndPrimary(line string) (string, string) {
|
2024-07-20 18:55:28 +08:00
|
|
|
parts := strings.Fields(line)
|
2025-12-26 17:55:25 +08:00
|
|
|
if len(parts) >= MinPriorityLineParts {
|
2024-07-22 00:01:27 +08:00
|
|
|
return parts[0], parts[1]
|
2024-07-20 18:55:28 +08:00
|
|
|
}
|
2024-07-22 00:01:27 +08:00
|
|
|
return "", ""
|
2024-07-20 18:55:28 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-26 17:55:25 +08:00
|
|
|
// isOriginatorLine checks if a dot-prefixed line matches the originator format (.CODE DATETIME).
|
|
|
|
|
// Returns the originator code, datetime, and whether it's a valid match.
|
|
|
|
|
func isOriginatorLine(line string) (originator, dateTime string, isMatch bool) {
|
|
|
|
|
if !strings.HasPrefix(line, ".") {
|
|
|
|
|
return "", "", false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parts := strings.Fields(line[1:])
|
|
|
|
|
if len(parts) < MinOriginatorParts {
|
|
|
|
|
return "", "", false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isAllUppercaseLetters(parts[0]) && isAllDigits(parts[1]) {
|
|
|
|
|
return parts[0], parts[1], true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", "", false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isBodyStartLine checks if a line indicates the start of the message body.
|
|
|
|
|
func isBodyStartLine(line string) bool {
|
|
|
|
|
return strings.HasPrefix(line, BeginPartMarker) || strings.HasPrefix(line, "(")
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-09 11:07:07 +08:00
|
|
|
func parseRemainingLines(lines []string) (string, string, string, string) {
|
2024-07-20 18:55:28 +08:00
|
|
|
var (
|
2025-12-26 17:55:25 +08:00
|
|
|
secondaryAddresses strings.Builder
|
2024-07-20 18:55:28 +08:00
|
|
|
originator string
|
|
|
|
|
originatorDateTime string
|
2025-12-26 17:55:25 +08:00
|
|
|
body strings.Builder
|
|
|
|
|
inBody bool
|
2024-07-20 18:55:28 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for _, line := range lines {
|
|
|
|
|
line = strings.TrimSpace(line)
|
2025-12-26 17:55:25 +08:00
|
|
|
|
|
|
|
|
// Skip empty lines and single dots
|
|
|
|
|
if line == "" || line == EndHeaderMarker {
|
|
|
|
|
continue
|
2024-07-20 18:55:28 +08:00
|
|
|
}
|
2025-12-26 17:55:25 +08:00
|
|
|
|
|
|
|
|
// Once in body, collect all remaining lines
|
|
|
|
|
if inBody {
|
|
|
|
|
body.WriteString(line + "\n")
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for originator line (.CODE DATETIME)
|
|
|
|
|
if orig, dt, isOrig := isOriginatorLine(line); isOrig {
|
|
|
|
|
originator = orig
|
|
|
|
|
originatorDateTime = dt
|
|
|
|
|
inBody = true
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for body start markers
|
|
|
|
|
if isBodyStartLine(line) {
|
|
|
|
|
inBody = true
|
|
|
|
|
// Skip lines containing NNNN (end marker)
|
|
|
|
|
if !strings.Contains(line, "NNNN") {
|
|
|
|
|
body.WriteString(line + "\n")
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to parse as originator using regex (fallback)
|
|
|
|
|
if dt, orig := getOriginator(line); orig != "" {
|
|
|
|
|
originatorDateTime = dt
|
|
|
|
|
originator = orig
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Otherwise, treat as secondary address
|
|
|
|
|
secondaryAddresses.WriteString(" " + line)
|
2024-07-20 18:55:28 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-26 17:55:25 +08:00
|
|
|
return secondaryAddresses.String(), originator, originatorDateTime, body.String()
|
2024-07-20 18:26:17 +08:00
|
|
|
}
|
2024-07-23 16:45:03 +08:00
|
|
|
|
|
|
|
|
func getOriginator(line string) (string, string) {
|
|
|
|
|
match := originator.FindStringSubmatch(line)
|
2025-12-26 17:55:25 +08:00
|
|
|
if len(match) >= MinOriginatorMatchGroups {
|
2024-07-23 16:45:03 +08:00
|
|
|
return match[1], match[2]
|
|
|
|
|
}
|
|
|
|
|
return "", ""
|
|
|
|
|
}
|
2024-07-23 19:12:08 +08:00
|
|
|
|
2025-11-17 17:53:24 +08:00
|
|
|
// isAllUppercaseLetters checks if a string contains only uppercase letters
|
|
|
|
|
func isAllUppercaseLetters(s string) bool {
|
|
|
|
|
if len(s) == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, r := range s {
|
|
|
|
|
if r < 'A' || r > 'Z' {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isAllDigits checks if a string contains only digits
|
|
|
|
|
func isAllDigits(s string) bool {
|
|
|
|
|
if len(s) == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, r := range s {
|
|
|
|
|
if r < '0' || r > '9' {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-23 19:12:08 +08:00
|
|
|
func parseOther(text string) map[string]string {
|
|
|
|
|
data := make(map[string]string)
|
|
|
|
|
for _, re := range otherPatterns {
|
2024-07-24 10:33:12 +08:00
|
|
|
if match := re.FindStringSubmatch(text); len(match) > 0 {
|
2024-07-23 19:12:08 +08:00
|
|
|
for i, name := range re.SubexpNames() {
|
|
|
|
|
if i != 0 && name != "" {
|
2024-07-23 19:36:11 +08:00
|
|
|
data[name] = strings.TrimSpace(match[i])
|
2024-07-23 19:12:08 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-07-24 10:33:12 +08:00
|
|
|
return data
|
2024-07-23 19:12:08 +08:00
|
|
|
}
|