diff --git a/classad/reader.go b/classad/reader.go index 04b56f1..10b836a 100644 --- a/classad/reader.go +++ b/classad/reader.go @@ -2,30 +2,52 @@ package classad import ( "bufio" + "fmt" "io" "iter" "strings" + "unicode/utf8" +) + +const ( + // maxBufferSize limits the buffer size to prevent unbounded memory growth + // when processing malformed or very large inputs + maxBufferSize = 10 * 1024 * 1024 // 10MB + // readChunkSize is the size of chunks read from the io.Reader + readChunkSize = 4096 // 4KB ) // Reader provides an iterator for parsing multiple ClassAds from an io.Reader. // It supports both new-style (bracketed) and old-style (newline-delimited) formats. type Reader struct { - scanner *bufio.Scanner - oldStyle bool - err error - current *ClassAd + // For new-style (bracketed) ClassAds + bufReader *bufio.Reader + buffer strings.Builder + oldStyle bool + scanner *bufio.Scanner + err error + current *ClassAd } // NewReader creates a new Reader for parsing new-style ClassAds (with brackets). // Each ClassAd should be on its own, separated by whitespace or comments. +// This function natively supports concatenated ClassAds (e.g., "][") through +// grammar-level parsing. // Example format: // // [Foo = 1; Bar = 2] // [Baz = 3; Qux = 4] +// +// Also supports concatenated format: +// +// [Foo = 1; Bar = 2][Baz = 3; Qux = 4] +// +// This implementation streams data from the io.Reader, processing ClassAds +// one at a time without buffering the entire input in memory. func NewReader(r io.Reader) *Reader { return &Reader{ - scanner: bufio.NewScanner(r), - oldStyle: false, + bufReader: bufio.NewReader(r), + oldStyle: false, } } @@ -59,71 +81,239 @@ func (r *Reader) Next() bool { return r.nextNew() } -// nextNew reads the next new-style ClassAd (with brackets) +// nextNew reads the next new-style ClassAd by streaming from the reader. +// It tracks bracket depth to detect complete ClassAds, handling strings +// and comments properly. func (r *Reader) nextNew() bool { - var lines []string - inClassAd := false - bracketDepth := 0 + // Read data incrementally until we have a complete ClassAd + for { + // Check buffer size limit before expensive scan to prevent unbounded growth + if r.buffer.Len() > maxBufferSize { + r.err = fmt.Errorf("buffer exceeded maximum size (%d bytes): input may be malformed or too large", maxBufferSize) + return false + } - for r.scanner.Scan() { - line := strings.TrimSpace(r.scanner.Text()) + // Check if we already have a complete ClassAd in the buffer + adStr, remaining, found := r.findCompleteClassAd() + if found { + // Parse the complete ClassAd + ad, err := Parse(adStr) + if err != nil { + r.err = err + return false + } + r.current = ad + // Update buffer with remaining data + r.buffer.Reset() + r.buffer.WriteString(remaining) + return true + } - // Skip empty lines and comments outside of ClassAds - if !inClassAd && (line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "/*")) { - continue + // Need more data - read a chunk + chunk := make([]byte, readChunkSize) + n, err := r.bufReader.Read(chunk) + if n > 0 { + r.buffer.Write(chunk[:n]) + // Check buffer size after writing to catch cases where a single chunk exceeds limit + if r.buffer.Len() > maxBufferSize { + r.err = fmt.Errorf("buffer exceeded maximum size (%d bytes): input may be malformed or too large", maxBufferSize) + return false + } + } + if err == io.EOF { + return r.handleEOF() + } + if err != nil { + r.err = err + return false } + } +} - // Check if this line starts a ClassAd - if !inClassAd && strings.HasPrefix(line, "[") { - inClassAd = true +// handleEOF processes remaining data when EOF is reached. +// It attempts to parse any complete ClassAd or remaining data in the buffer. +func (r *Reader) handleEOF() bool { + // Check if we have a complete ClassAd in buffer + adStr, remaining, found := r.findCompleteClassAd() + if found { + ad, parseErr := Parse(adStr) + if parseErr != nil { + r.err = parseErr + return false + } + r.current = ad + r.buffer.Reset() + r.buffer.WriteString(remaining) + return true + } + // Check if there's any remaining data that might be a ClassAd + remainingStr := strings.TrimSpace(r.buffer.String()) + if remainingStr != "" { + // Try to parse what's left + ad, parseErr := Parse(remainingStr) + if parseErr != nil { + r.err = parseErr + return false } + r.current = ad + r.buffer.Reset() + return true + } + return false +} - if inClassAd { - lines = append(lines, line) +// findCompleteClassAd scans the buffer to find a complete ClassAd (balanced brackets). +// It returns the ClassAd string, any remaining data, and whether a complete ClassAd was found. +// This handles strings and comments properly so brackets inside them don't affect depth. +// The function uses byte-level iteration for efficiency, properly handling UTF-8 sequences. +func (r *Reader) findCompleteClassAd() (classAdStr, remaining string, found bool) { + bufStr := r.buffer.String() + if bufStr == "" { + return "", "", false + } - // Count brackets to handle nested ClassAds - for _, ch := range line { - switch ch { - case '[': - bracketDepth++ - case ']': - bracketDepth-- + // Track bracket depth, handling strings and comments + depth := 0 + inString := false + inLineComment := false + inBlockComment := false + escapeNext := false + startPos := -1 + + // Skip leading whitespace and comments to find the start of a ClassAd + skipWhitespace := true + + // Use byte-level iteration for efficiency (brackets are ASCII, single-byte) + // But properly handle UTF-8 sequences when advancing + for i := 0; i < len(bufStr); { + ch := bufStr[i] + + // Skip whitespace before finding the first bracket + if skipWhitespace { + if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' { + i++ + continue + } + // Check for line comment + if i+1 < len(bufStr) && ch == '/' && bufStr[i+1] == '/' { + // Skip to end of line + for i < len(bufStr) && bufStr[i] != '\n' { + i++ } + continue } + // Check for block comment + if i+1 < len(bufStr) && ch == '/' && bufStr[i+1] == '*' { + // Skip block comment + i += 2 + for i+1 < len(bufStr) { + if bufStr[i] == '*' && bufStr[i+1] == '/' { + i += 2 + break + } + // Advance by rune to handle UTF-8 in comments + _, size := utf8.DecodeRuneInString(bufStr[i:]) + if size == 0 { + break + } + i += size + } + continue + } + skipWhitespace = false + } - // If we've closed all brackets, we have a complete ClassAd - if bracketDepth == 0 { - classAdStr := strings.Join(lines, "\n") - ad, err := Parse(classAdStr) - if err != nil { - r.err = err - return false + // Handle escape sequences in strings + if escapeNext { + escapeNext = false + // Advance by rune to handle UTF-8 escape sequences properly + _, size := utf8.DecodeRuneInString(bufStr[i:]) + if size == 0 { + break + } + i += size + continue + } + + // Handle escape sequences in strings + if inString && ch == '\\' { + escapeNext = true + i++ + continue + } + + // Handle strings + if !inLineComment && !inBlockComment { + if ch == '"' { + inString = !inString + i++ + continue + } + } + + // Only process brackets when not in string or comment + // Brackets are ASCII (single-byte), so byte-level comparison is safe + if !inString && !inLineComment && !inBlockComment { + switch ch { + case '[': + if depth == 0 { + startPos = i } - r.current = ad - return true + depth++ + i++ + case ']': + depth-- + if depth == 0 && startPos >= 0 { + // Found complete ClassAd + classAdStr = bufStr[startPos : i+1] + remaining = strings.TrimSpace(bufStr[i+1:]) + return classAdStr, remaining, true + } + i++ + default: + // Not a bracket - advance by rune for UTF-8 handling + _, size := utf8.DecodeRuneInString(bufStr[i:]) + if size == 0 { + break + } + i += size } + continue } - } - // Check for scanner errors - if err := r.scanner.Err(); err != nil { - r.err = err - return false - } + // Handle comments (only when not in string) + if !inString { + if !inBlockComment && i+1 < len(bufStr) && ch == '/' && bufStr[i+1] == '/' { + inLineComment = true + i += 2 + continue + } + if inLineComment && ch == '\n' { + inLineComment = false + i++ + continue + } + if !inLineComment && !inBlockComment && i+1 < len(bufStr) && ch == '/' && bufStr[i+1] == '*' { + inBlockComment = true + i += 2 + continue + } + if inBlockComment && i+1 < len(bufStr) && ch == '*' && bufStr[i+1] == '/' { + inBlockComment = false + i += 2 + continue + } + } - // If we have accumulated lines but hit EOF, try to parse them - if len(lines) > 0 { - classAdStr := strings.Join(lines, "\n") - ad, err := Parse(classAdStr) - if err != nil { - r.err = err - return false + // Advance by rune to handle UTF-8 properly in strings and comments + _, size := utf8.DecodeRuneInString(bufStr[i:]) + if size == 0 { + break } - r.current = ad - return true + i += size } - return false + return "", "", false } // nextOld reads the next old-style ClassAd (newline-delimited, separated by blank lines) diff --git a/classad/reader_test.go b/classad/reader_test.go index 3e0ff48..2fc3038 100644 --- a/classad/reader_test.go +++ b/classad/reader_test.go @@ -1,6 +1,7 @@ package classad import ( + "fmt" "strings" "testing" ) @@ -566,3 +567,215 @@ Name = "third"` t.Errorf("Expected 3 ClassAds, got %d", count) } } + +// TestNewReader_ConcatenatedClassAds tests parsing ClassAds that are concatenated +// without whitespace between them (e.g., "]["). This format is used by HTCondor +// when writing ClassAds to plugin input files. +func TestNewReader_ConcatenatedClassAds(t *testing.T) { + // Test concatenated format without newlines: ][ + input := `[Url = "pelican://example.com/file1"; LocalFileName = "/tmp/file1"][Url = "pelican://example.com/file2"; LocalFileName = "/tmp/file2"][Url = "pelican://example.com/file3"; LocalFileName = "/tmp/file3"]` + reader := NewReader(strings.NewReader(input)) + + count := 0 + urls := []string{} + + for reader.Next() { + ad := reader.ClassAd() + url, ok := ad.EvaluateAttrString("Url") + if !ok { + t.Error("Expected Url attribute") + } + urls = append(urls, url) + count++ + } + + if reader.Err() != nil { + t.Errorf("Unexpected error: %v", reader.Err()) + } + + if count != 3 { + t.Errorf("Expected 3 ClassAds, got %d", count) + } + + expectedUrls := []string{ + "pelican://example.com/file1", + "pelican://example.com/file2", + "pelican://example.com/file3", + } + for i, url := range urls { + if url != expectedUrls[i] { + t.Errorf("Expected Url=%s, got %s", expectedUrls[i], url) + } + } +} + +// TestAll_ConcatenatedClassAds tests the iterator pattern with concatenated ClassAds +func TestAll_ConcatenatedClassAds(t *testing.T) { + // Test concatenated format without newlines: ][ + input := `[ID = 1; Name = "first"][ID = 2; Name = "second"][ID = 3; Name = "third"]` + + count := 0 + ids := []int64{} + + All(strings.NewReader(input))(func(ad *ClassAd) bool { + count++ + id, ok := ad.EvaluateAttrInt("ID") + if !ok { + t.Error("Expected ID attribute") + } + ids = append(ids, id) + return true + }) + + if count != 3 { + t.Errorf("Expected 3 ClassAds, got %d", count) + } + + expectedIds := []int64{1, 2, 3} + for i, id := range ids { + if id != expectedIds[i] { + t.Errorf("Expected ID=%d, got %d", expectedIds[i], id) + } + } +} + +// TestNewReader_UTF8Characters tests handling of UTF-8 characters in ClassAds +func TestNewReader_UTF8Characters(t *testing.T) { + // Test with UTF-8 characters in attribute values + input := `[Name = "José"; City = "São Paulo"; Emoji = "🚀"]` + reader := NewReader(strings.NewReader(input)) + + if !reader.Next() { + t.Fatalf("Expected ClassAd, got error: %v", reader.Err()) + } + + ad := reader.ClassAd() + name, _ := ad.EvaluateAttrString("Name") + if name != "José" { + t.Errorf("Expected Name=José, got %s", name) + } + + city, _ := ad.EvaluateAttrString("City") + if city != "São Paulo" { + t.Errorf("Expected City=São Paulo, got %s", city) + } + + emoji, _ := ad.EvaluateAttrString("Emoji") + if emoji != "🚀" { + t.Errorf("Expected Emoji=🚀, got %s", emoji) + } +} + +// TestNewReader_UTF8InStrings tests UTF-8 characters inside strings with brackets +func TestNewReader_UTF8InStrings(t *testing.T) { + // Test that brackets inside UTF-8 strings don't break parsing + input := `[Message = "Hello [world] 🌍"; Value = 42]` + reader := NewReader(strings.NewReader(input)) + + if !reader.Next() { + t.Fatalf("Expected ClassAd, got error: %v", reader.Err()) + } + + ad := reader.ClassAd() + message, _ := ad.EvaluateAttrString("Message") + if message != "Hello [world] 🌍" { + t.Errorf("Expected Message='Hello [world] 🌍', got %s", message) + } + + value, _ := ad.EvaluateAttrInt("Value") + if value != 42 { + t.Errorf("Expected Value=42, got %d", value) + } +} + +// TestNewReader_EmptyBrackets tests empty ClassAd brackets +func TestNewReader_EmptyBrackets(t *testing.T) { + input := `[]` + reader := NewReader(strings.NewReader(input)) + + if !reader.Next() { + t.Fatalf("Expected ClassAd, got error: %v", reader.Err()) + } + + ad := reader.ClassAd() + if ad == nil { + t.Fatal("Expected ClassAd, got nil") + } + + // Empty ClassAd should be valid + if reader.Next() { + t.Error("Expected no more ClassAds") + } +} + +// TestNewReader_BufferSizeLimit tests that buffer size limit is enforced +func TestNewReader_BufferSizeLimit(t *testing.T) { + // Create input that will cause the buffer to exceed the limit + // We'll create a stream that grows the buffer past the limit + // Use a size that's just over maxBufferSize to trigger the check quickly + // We need enough data to fill multiple chunks and exceed the limit + largeValueSize := maxBufferSize + 1 // Just one byte over the limit + largeValue := strings.Repeat("x", largeValueSize) + input := fmt.Sprintf(`[LargeAttr = %q]`, largeValue) + reader := NewReader(strings.NewReader(input)) + + // Should fail due to buffer size limit + // The reader will read chunks until the buffer exceeds the limit + // We check the buffer size before the expensive scan, so this should be fast + if reader.Next() { + t.Error("Expected Next() to return false due to buffer size limit") + } + + if reader.Err() == nil { + t.Error("Expected error for buffer size limit exceeded") + return + } + + errMsg := reader.Err().Error() + if !strings.Contains(errMsg, "buffer exceeded maximum size") { + t.Errorf("Expected buffer size error, got: %v", reader.Err()) + } +} + +// TestNewReader_UnclosedBrackets tests handling of unclosed brackets +func TestNewReader_UnclosedBrackets(t *testing.T) { + // Test with unclosed bracket (should eventually fail or timeout) + input := `[Foo = 1; Bar = 2` + reader := NewReader(strings.NewReader(input)) + + // Should not find a complete ClassAd + if reader.Next() { + t.Error("Expected Next() to return false for unclosed bracket") + } + + // Should have an error when trying to parse incomplete data + if reader.Err() == nil { + t.Error("Expected error for unclosed bracket") + } +} + +// TestNewReader_MultipleUTF8ClassAds tests multiple ClassAds with UTF-8 +func TestNewReader_MultipleUTF8ClassAds(t *testing.T) { + input := `[Name = "José"; ID = 1][Name = "François"; ID = 2][Name = "北京"; ID = 3]` + reader := NewReader(strings.NewReader(input)) + + expectedNames := []string{"José", "François", "北京"} + count := 0 + + for reader.Next() { + ad := reader.ClassAd() + name, _ := ad.EvaluateAttrString("Name") + if count < len(expectedNames) && name != expectedNames[count] { + t.Errorf("Expected Name=%s, got %s", expectedNames[count], name) + } + count++ + } + + if reader.Err() != nil { + t.Errorf("Unexpected error: %v", reader.Err()) + } + + if count != 3 { + t.Errorf("Expected 3 ClassAds, got %d", count) + } +} diff --git a/parser/classad.y b/parser/classad.y index b3463a4..2e6ad7e 100644 --- a/parser/classad.y +++ b/parser/classad.y @@ -11,6 +11,7 @@ import ( node ast.Node expr ast.Expr classad *ast.ClassAd + classads []*ast.ClassAd attr *ast.AttributeAssignment attrs []*ast.AttributeAssignment exprlist []ast.Expr @@ -42,6 +43,7 @@ import ( %left '.' '[' '(' %type classad record_literal +%type classad_list %type attr_list %type attr_assign %type expr literal primary_expr postfix_expr unary_expr @@ -53,14 +55,24 @@ import ( %% start - : classad + : classad_list { - if lex, ok := yylex.(interface{ SetResult(ast.Node) }); ok { - lex.SetResult($1) + if lex, ok := yylex.(interface{ SetResultList([]*ast.ClassAd) }); ok { + lex.SetResultList($1) + } else if lex, ok := yylex.(interface{ SetResult(ast.Node) }); ok && len($1) == 1 { + // For backward compatibility: if only one ClassAd, set as single result + lex.SetResult($1[0]) } } ; +classad_list + : classad + { $$ = []*ast.ClassAd{$1} } + | classad_list classad + { $$ = append($1, $2) } + ; + classad : '[' attr_list ']' { $$ = &ast.ClassAd{Attributes: $2} } diff --git a/parser/lexer.go b/parser/lexer.go index 3f1ce3b..bbb98bc 100644 --- a/parser/lexer.go +++ b/parser/lexer.go @@ -20,17 +20,21 @@ type Token struct { // Lexer represents a lexical scanner for ClassAd expressions. type Lexer struct { - input string - pos int - result ast.Node - err error + input string + pos int + result ast.Node + resultList []*ast.ClassAd + err error } // NewLexer creates a new lexer for the given input. func NewLexer(input string) *Lexer { return &Lexer{ - input: input, - pos: 0, + input: input, + pos: 0, + result: nil, + resultList: nil, + err: nil, } } @@ -210,11 +214,21 @@ func (l *Lexer) Result() (ast.Node, error) { return l.result, l.err } +// ResultList returns the parsed list of ClassAds and any error. +func (l *Lexer) ResultList() ([]*ast.ClassAd, error) { + return l.resultList, l.err +} + // SetResult sets the parse result. func (l *Lexer) SetResult(node ast.Node) { l.result = node } +// SetResultList sets the parse result as a list of ClassAds. +func (l *Lexer) SetResultList(classads []*ast.ClassAd) { + l.resultList = classads +} + func (l *Lexer) peek() rune { if l.pos >= len(l.input) { return 0 diff --git a/parser/parser.go b/parser/parser.go index f432cd6..ec7478d 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -9,9 +9,20 @@ import ( ) // Parse parses a ClassAd expression string and returns the AST. +// For backward compatibility, if the input contains a single ClassAd, +// it returns that ClassAd. If it contains multiple ClassAds, it returns +// the first one. func Parse(input string) (ast.Node, error) { lex := NewLexer(input) yyParse(lex) + + // Check if we got a list of ClassAds + if list, err := lex.ResultList(); err == nil && len(list) > 0 { + // For backward compatibility, return the first ClassAd + return list[0], nil + } + + // Fall back to single result (for backward compatibility) return lex.Result() } diff --git a/parser/y.go b/parser/y.go index 5402020..ce265b2 100644 --- a/parser/y.go +++ b/parser/y.go @@ -1,13 +1,15 @@ -// Code generated by goyacc -v y.output -o parser/y.go -p yy parser/classad.y. DO NOT EDIT. +// Code generated by goyacc -o parser/y.go -p yy parser/classad.y. DO NOT EDIT. //line parser/classad.y:2 package parser -import ( - __yyfmt__ "fmt" +import __yyfmt__ "fmt" + +//line parser/classad.y:2 +import ( "github.com/PelicanPlatform/classad/ast" -) //line parser/classad.y:2 +) //line parser/classad.y:10 type yySymType struct { @@ -15,6 +17,7 @@ type yySymType struct { node ast.Node expr ast.Expr classad *ast.ClassAd + classads []*ast.ClassAd attr *ast.AttributeAssignment attrs []*ast.AttributeAssignment exprlist []ast.Expr @@ -99,7 +102,7 @@ const yyEofCode = 1 const yyErrCode = 2 const yyInitialStackSize = 16 -//line parser/classad.y:267 +//line parser/classad.y:279 //line yacctab:1 var yyExca = [...]int8{ @@ -110,93 +113,96 @@ var yyExca = [...]int8{ const yyPrivate = 57344 -const yyLast = 130 +const yyLast = 132 var yyAct = [...]int8{ - 75, 13, 73, 23, 21, 18, 19, 106, 24, 17, - 22, 12, 105, 111, 31, 38, 36, 37, 39, 40, - 41, 15, 79, 104, 10, 20, 107, 9, 8, 9, - 110, 71, 3, 16, 72, 67, 68, 69, 70, 27, - 26, 65, 66, 48, 78, 28, 29, 4, 42, 33, - 62, 63, 64, 83, 34, 84, 82, 7, 89, 90, - 91, 92, 7, 47, 96, 97, 80, 102, 93, 94, - 95, 98, 99, 100, 103, 85, 86, 87, 88, 81, - 46, 109, 108, 31, 38, 36, 37, 39, 40, 41, - 76, 60, 61, 77, 57, 58, 59, 45, 5, 53, - 54, 55, 56, 49, 50, 51, 52, 112, 27, 26, - 113, 43, 6, 44, 28, 29, 101, 42, 33, 7, - 1, 74, 11, 34, 14, 25, 30, 32, 35, 2, + 77, 15, 75, 25, 23, 20, 21, 17, 26, 19, + 24, 18, 108, 14, 107, 113, 33, 40, 38, 39, + 41, 42, 43, 6, 81, 22, 106, 109, 11, 9, + 12, 10, 11, 112, 73, 4, 74, 69, 70, 71, + 72, 29, 28, 67, 68, 9, 80, 30, 31, 50, + 44, 35, 62, 63, 82, 85, 36, 86, 84, 83, + 91, 92, 93, 94, 49, 79, 98, 99, 78, 104, + 95, 96, 97, 100, 101, 102, 105, 87, 88, 89, + 90, 7, 48, 111, 47, 33, 40, 38, 39, 41, + 42, 43, 64, 65, 66, 59, 60, 61, 55, 56, + 57, 58, 51, 52, 53, 54, 45, 103, 46, 114, + 29, 28, 115, 110, 8, 9, 30, 31, 3, 44, + 35, 5, 1, 76, 16, 36, 13, 27, 32, 34, + 2, 37, } var yyPact = [...]int16{ - -6, -1000, -1000, 58, -12, -1000, -1000, -18, -1000, 115, - 79, -1000, -1000, -1000, 100, 83, 65, 47, 26, 85, - 77, 68, 62, 19, -1000, 4, 79, 79, 79, 79, - -1000, -8, -1000, 79, 79, -1000, -1000, -1000, -1000, -1000, - -1000, -1000, 53, 10, 79, 79, 79, 79, 79, 79, - 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, - 79, 79, 79, 79, 79, 112, 79, -1000, -1000, -1000, - -1000, 79, -20, -33, -39, -1000, -14, -1000, 70, 79, - 83, 65, 47, 26, 85, 77, 77, 77, 77, 68, - 68, 68, 68, 62, 62, 62, 19, 19, -1000, -1000, - -1000, -1000, -10, -30, -1000, -1000, 79, -1000, 79, -1000, - -1000, -1000, -1000, -1000, + -3, -1000, -3, -1000, 41, -1000, -9, -1000, -1000, -12, + -1000, 111, 81, -1000, -1000, -1000, 95, 70, 67, 48, + 32, 84, 76, 69, 23, 61, -1000, 6, 81, 81, + 81, 81, -1000, -5, -1000, 81, 81, -1000, -1000, -1000, + -1000, -1000, -1000, -1000, 25, 12, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 103, 81, -1000, + -1000, -1000, -1000, 81, -17, -31, -34, -1000, -13, -1000, + 101, 81, 70, 67, 48, 32, 84, 76, 76, 76, + 76, 69, 69, 69, 69, 23, 23, 23, 61, 61, + -1000, -1000, -1000, -1000, -7, -28, -1000, -1000, 81, -1000, + 81, -1000, -1000, -1000, -1000, -1000, } var yyPgo = [...]uint8{ - 0, 129, 128, 47, 112, 0, 127, 126, 125, 8, - 3, 10, 4, 25, 6, 5, 9, 33, 21, 124, - 1, 121, 2, 120, + 0, 118, 131, 130, 23, 114, 0, 129, 128, 127, + 8, 3, 10, 4, 25, 6, 5, 9, 11, 7, + 124, 1, 123, 2, 122, } var yyR1 = [...]int8{ - 0, 23, 1, 1, 2, 2, 3, 3, 3, 4, - 5, 20, 20, 20, 19, 19, 18, 18, 17, 17, - 16, 16, 15, 15, 14, 14, 14, 14, 14, 13, - 13, 13, 13, 13, 12, 12, 12, 12, 11, 11, - 11, 10, 10, 10, 10, 9, 9, 9, 9, 9, - 8, 8, 8, 8, 7, 7, 7, 7, 7, 6, - 6, 6, 6, 6, 6, 22, 22, 21, 21, + 0, 24, 3, 3, 1, 1, 2, 2, 4, 4, + 4, 5, 6, 21, 21, 21, 20, 20, 19, 19, + 18, 18, 17, 17, 16, 16, 15, 15, 15, 15, + 15, 14, 14, 14, 14, 14, 13, 13, 13, 13, + 12, 12, 12, 11, 11, 11, 11, 10, 10, 10, + 10, 10, 9, 9, 9, 9, 8, 8, 8, 8, + 8, 7, 7, 7, 7, 7, 7, 23, 23, 22, + 22, } var yyR2 = [...]int8{ - 0, 1, 3, 2, 3, 2, 1, 3, 2, 3, - 1, 1, 5, 4, 1, 3, 1, 3, 1, 3, - 1, 3, 1, 3, 1, 3, 3, 3, 3, 1, - 3, 3, 3, 3, 1, 3, 3, 3, 1, 3, - 3, 1, 3, 3, 3, 1, 2, 2, 2, 2, - 1, 3, 4, 4, 1, 1, 3, 3, 1, 1, - 1, 1, 1, 1, 1, 0, 1, 1, 3, + 0, 1, 1, 2, 3, 2, 3, 2, 1, 3, + 2, 3, 1, 1, 5, 4, 1, 3, 1, 3, + 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, + 3, 1, 3, 3, 3, 3, 1, 3, 3, 3, + 1, 3, 3, 1, 3, 3, 3, 1, 2, 2, + 2, 2, 1, 3, 4, 4, 1, 1, 3, 3, + 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, + 3, } var yyChk = [...]int16{ - -1000, -23, -1, 38, -3, 40, -4, 4, 40, 41, - 42, -4, -5, -20, -19, -18, -17, -16, -15, -14, - -13, -12, -11, -10, -9, -8, 30, 29, 35, 36, - -7, 4, -6, 39, 44, -2, 6, 7, 5, 8, - 9, 10, 38, 11, 13, 14, 15, 16, 17, 18, - 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, - 29, 30, 31, 32, 33, 37, 38, -9, -9, -9, - -9, 39, -5, -22, -21, -5, -3, 40, -5, 12, - -18, -17, -16, -15, -14, -13, -13, -13, -13, -12, - -12, -12, -12, -11, -11, -11, -10, -10, -9, -9, - -9, 4, -5, -22, 43, 45, 46, 40, 12, -20, - 40, 43, -5, -20, + -1000, -24, -3, -1, 38, -1, -4, 40, -5, 4, + 40, 41, 42, -5, -6, -21, -20, -19, -18, -17, + -16, -15, -14, -13, -12, -11, -10, -9, 30, 29, + 35, 36, -8, 4, -7, 39, 44, -2, 6, 7, + 5, 8, 9, 10, 38, 11, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 37, 38, -10, + -10, -10, -10, 39, -6, -23, -22, -6, -4, 40, + -6, 12, -19, -18, -17, -16, -15, -14, -14, -14, + -14, -13, -13, -13, -13, -12, -12, -12, -11, -11, + -10, -10, -10, 4, -6, -23, 43, 45, 46, 40, + 12, -21, 40, 43, -6, -21, } var yyDef = [...]int8{ - 0, -2, 1, 0, 0, 3, 6, 0, 2, 8, - 0, 7, 9, 10, 11, 14, 16, 18, 20, 22, - 24, 29, 34, 38, 41, 45, 0, 0, 0, 0, - 50, 55, 54, 0, 65, 58, 59, 60, 61, 62, - 63, 64, 0, 0, 0, 0, 0, 0, 0, 0, + 0, -2, 1, 2, 0, 3, 0, 5, 8, 0, + 4, 10, 0, 9, 11, 12, 13, 16, 18, 20, + 22, 24, 26, 31, 36, 40, 43, 47, 0, 0, + 0, 0, 52, 57, 56, 0, 67, 60, 61, 62, + 63, 64, 65, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 46, 47, 48, - 49, 65, 0, 0, 66, 67, 0, 5, 0, 0, - 15, 17, 19, 21, 23, 25, 26, 27, 28, 30, - 31, 32, 33, 35, 36, 37, 39, 40, 42, 43, - 44, 51, 0, 0, 56, 57, 0, 4, 0, 13, - 52, 53, 68, 12, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, + 49, 50, 51, 67, 0, 0, 68, 69, 0, 7, + 0, 0, 17, 19, 21, 23, 25, 27, 28, 29, + 30, 32, 33, 34, 35, 37, 38, 39, 41, 42, + 44, 45, 46, 53, 0, 0, 58, 59, 0, 6, + 0, 15, 54, 55, 70, 14, } var yyTok1 = [...]int8{ @@ -564,412 +570,427 @@ yydefault: case 1: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:57 +//line parser/classad.y:59 { - if lex, ok := yylex.(interface{ SetResult(ast.Node) }); ok { - lex.SetResult(yyDollar[1].classad) + if lex, ok := yylex.(interface{ SetResultList([]*ast.ClassAd) }); ok { + lex.SetResultList(yyDollar[1].classads) + } else if lex, ok := yylex.(interface{ SetResult(ast.Node) }); ok && len(yyDollar[1].classads) == 1 { + // For backward compatibility: if only one ClassAd, set as single result + lex.SetResult(yyDollar[1].classads[0]) } } case 2: - yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:66 + yyDollar = yyS[yypt-1 : yypt+1] +//line parser/classad.y:71 { - yyVAL.classad = &ast.ClassAd{Attributes: yyDollar[2].attrs} + yyVAL.classads = []*ast.ClassAd{yyDollar[1].classad} } case 3: yyDollar = yyS[yypt-2 : yypt+1] -//line parser/classad.y:68 +//line parser/classad.y:73 { - yyVAL.classad = &ast.ClassAd{Attributes: []*ast.AttributeAssignment{}} + yyVAL.classads = append(yyDollar[1].classads, yyDollar[2].classad) } case 4: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:73 +//line parser/classad.y:78 { yyVAL.classad = &ast.ClassAd{Attributes: yyDollar[2].attrs} } case 5: yyDollar = yyS[yypt-2 : yypt+1] -//line parser/classad.y:75 +//line parser/classad.y:80 { yyVAL.classad = &ast.ClassAd{Attributes: []*ast.AttributeAssignment{}} } case 6: + yyDollar = yyS[yypt-3 : yypt+1] +//line parser/classad.y:85 + { + yyVAL.classad = &ast.ClassAd{Attributes: yyDollar[2].attrs} + } + case 7: + yyDollar = yyS[yypt-2 : yypt+1] +//line parser/classad.y:87 + { + yyVAL.classad = &ast.ClassAd{Attributes: []*ast.AttributeAssignment{}} + } + case 8: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:80 +//line parser/classad.y:92 { yyVAL.attrs = []*ast.AttributeAssignment{yyDollar[1].attr} } - case 7: + case 9: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:82 +//line parser/classad.y:94 { yyVAL.attrs = append(yyDollar[1].attrs, yyDollar[3].attr) } - case 8: + case 10: yyDollar = yyS[yypt-2 : yypt+1] -//line parser/classad.y:84 +//line parser/classad.y:96 { yyVAL.attrs = yyDollar[1].attrs } - case 9: + case 11: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:89 +//line parser/classad.y:101 { yyVAL.attr = &ast.AttributeAssignment{Name: yyDollar[1].str, Value: yyDollar[3].expr} } - case 10: + case 12: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:94 +//line parser/classad.y:106 { yyVAL.expr = yyDollar[1].expr } - case 11: + case 13: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:99 +//line parser/classad.y:111 { yyVAL.expr = yyDollar[1].expr } - case 12: + case 14: yyDollar = yyS[yypt-5 : yypt+1] -//line parser/classad.y:101 +//line parser/classad.y:113 { yyVAL.expr = &ast.ConditionalExpr{Condition: yyDollar[1].expr, TrueExpr: yyDollar[3].expr, FalseExpr: yyDollar[5].expr} } - case 13: + case 15: yyDollar = yyS[yypt-4 : yypt+1] -//line parser/classad.y:103 +//line parser/classad.y:115 { yyVAL.expr = &ast.ElvisExpr{Left: yyDollar[1].expr, Right: yyDollar[4].expr} } - case 14: + case 16: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:108 +//line parser/classad.y:120 { yyVAL.expr = yyDollar[1].expr } - case 15: + case 17: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:110 +//line parser/classad.y:122 { yyVAL.expr = &ast.BinaryOp{Op: "||", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 16: + case 18: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:115 +//line parser/classad.y:127 { yyVAL.expr = yyDollar[1].expr } - case 17: + case 19: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:117 +//line parser/classad.y:129 { yyVAL.expr = &ast.BinaryOp{Op: "&&", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 18: + case 20: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:122 +//line parser/classad.y:134 { yyVAL.expr = yyDollar[1].expr } - case 19: + case 21: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:124 +//line parser/classad.y:136 { yyVAL.expr = &ast.BinaryOp{Op: "|", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 20: + case 22: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:129 +//line parser/classad.y:141 { yyVAL.expr = yyDollar[1].expr } - case 21: + case 23: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:131 +//line parser/classad.y:143 { yyVAL.expr = &ast.BinaryOp{Op: "^", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 22: + case 24: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:136 +//line parser/classad.y:148 { yyVAL.expr = yyDollar[1].expr } - case 23: + case 25: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:138 +//line parser/classad.y:150 { yyVAL.expr = &ast.BinaryOp{Op: "&", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 24: + case 26: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:143 +//line parser/classad.y:155 { yyVAL.expr = yyDollar[1].expr } - case 25: + case 27: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:145 +//line parser/classad.y:157 { yyVAL.expr = &ast.BinaryOp{Op: "==", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 26: + case 28: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:147 +//line parser/classad.y:159 { yyVAL.expr = &ast.BinaryOp{Op: "!=", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 27: + case 29: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:149 +//line parser/classad.y:161 { yyVAL.expr = &ast.BinaryOp{Op: "is", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 28: + case 30: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:151 +//line parser/classad.y:163 { yyVAL.expr = &ast.BinaryOp{Op: "isnt", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 29: + case 31: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:156 +//line parser/classad.y:168 { yyVAL.expr = yyDollar[1].expr } - case 30: + case 32: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:158 +//line parser/classad.y:170 { yyVAL.expr = &ast.BinaryOp{Op: "<", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 31: + case 33: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:160 +//line parser/classad.y:172 { yyVAL.expr = &ast.BinaryOp{Op: ">", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 32: + case 34: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:162 +//line parser/classad.y:174 { yyVAL.expr = &ast.BinaryOp{Op: "<=", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 33: + case 35: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:164 +//line parser/classad.y:176 { yyVAL.expr = &ast.BinaryOp{Op: ">=", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 34: + case 36: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:169 +//line parser/classad.y:181 { yyVAL.expr = yyDollar[1].expr } - case 35: + case 37: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:171 +//line parser/classad.y:183 { yyVAL.expr = &ast.BinaryOp{Op: "<<", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 36: + case 38: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:173 +//line parser/classad.y:185 { yyVAL.expr = &ast.BinaryOp{Op: ">>", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 37: + case 39: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:175 +//line parser/classad.y:187 { yyVAL.expr = &ast.BinaryOp{Op: ">>>", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 38: + case 40: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:180 +//line parser/classad.y:192 { yyVAL.expr = yyDollar[1].expr } - case 39: + case 41: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:182 +//line parser/classad.y:194 { yyVAL.expr = &ast.BinaryOp{Op: "+", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 40: + case 42: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:184 +//line parser/classad.y:196 { yyVAL.expr = &ast.BinaryOp{Op: "-", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 41: + case 43: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:189 +//line parser/classad.y:201 { yyVAL.expr = yyDollar[1].expr } - case 42: + case 44: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:191 +//line parser/classad.y:203 { yyVAL.expr = &ast.BinaryOp{Op: "*", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 43: + case 45: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:193 +//line parser/classad.y:205 { yyVAL.expr = &ast.BinaryOp{Op: "/", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 44: + case 46: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:195 +//line parser/classad.y:207 { yyVAL.expr = &ast.BinaryOp{Op: "%", Left: yyDollar[1].expr, Right: yyDollar[3].expr} } - case 45: + case 47: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:200 +//line parser/classad.y:212 { yyVAL.expr = yyDollar[1].expr } - case 46: + case 48: yyDollar = yyS[yypt-2 : yypt+1] -//line parser/classad.y:202 +//line parser/classad.y:214 { yyVAL.expr = &ast.UnaryOp{Op: "-", Expr: yyDollar[2].expr} } - case 47: + case 49: yyDollar = yyS[yypt-2 : yypt+1] -//line parser/classad.y:204 +//line parser/classad.y:216 { yyVAL.expr = &ast.UnaryOp{Op: "+", Expr: yyDollar[2].expr} } - case 48: + case 50: yyDollar = yyS[yypt-2 : yypt+1] -//line parser/classad.y:206 +//line parser/classad.y:218 { yyVAL.expr = &ast.UnaryOp{Op: "!", Expr: yyDollar[2].expr} } - case 49: + case 51: yyDollar = yyS[yypt-2 : yypt+1] -//line parser/classad.y:208 +//line parser/classad.y:220 { yyVAL.expr = &ast.UnaryOp{Op: "~", Expr: yyDollar[2].expr} } - case 50: + case 52: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:213 +//line parser/classad.y:225 { yyVAL.expr = yyDollar[1].expr } - case 51: + case 53: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:215 +//line parser/classad.y:227 { yyVAL.expr = &ast.SelectExpr{Record: yyDollar[1].expr, Attr: yyDollar[3].str} } - case 52: + case 54: yyDollar = yyS[yypt-4 : yypt+1] -//line parser/classad.y:217 +//line parser/classad.y:229 { yyVAL.expr = &ast.SubscriptExpr{Container: yyDollar[1].expr, Index: yyDollar[3].expr} } - case 53: + case 55: yyDollar = yyS[yypt-4 : yypt+1] -//line parser/classad.y:219 +//line parser/classad.y:231 { yyVAL.expr = &ast.FunctionCall{Name: yyDollar[1].str, Args: yyDollar[3].exprlist} } - case 54: + case 56: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:224 +//line parser/classad.y:236 { yyVAL.expr = yyDollar[1].expr } - case 55: + case 57: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:226 +//line parser/classad.y:238 { name, scope := ParseScopedIdentifier(yyDollar[1].str) yyVAL.expr = &ast.AttributeReference{Name: name, Scope: scope} } - case 56: + case 58: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:231 +//line parser/classad.y:243 { yyVAL.expr = yyDollar[2].expr } - case 57: + case 59: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:233 +//line parser/classad.y:245 { yyVAL.expr = &ast.ListLiteral{Elements: yyDollar[2].exprlist} } - case 58: + case 60: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:235 +//line parser/classad.y:247 { yyVAL.expr = &ast.RecordLiteral{ClassAd: yyDollar[1].classad} } - case 59: + case 61: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:240 +//line parser/classad.y:252 { yyVAL.expr = &ast.IntegerLiteral{Value: yyDollar[1].integer} } - case 60: + case 62: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:242 +//line parser/classad.y:254 { yyVAL.expr = &ast.RealLiteral{Value: yyDollar[1].real} } - case 61: + case 63: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:244 +//line parser/classad.y:256 { yyVAL.expr = &ast.StringLiteral{Value: yyDollar[1].str} } - case 62: + case 64: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:246 +//line parser/classad.y:258 { yyVAL.expr = &ast.BooleanLiteral{Value: yyDollar[1].boolean} } - case 63: + case 65: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:248 +//line parser/classad.y:260 { yyVAL.expr = &ast.UndefinedLiteral{} } - case 64: + case 66: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:250 +//line parser/classad.y:262 { yyVAL.expr = &ast.ErrorLiteral{} } - case 65: + case 67: yyDollar = yyS[yypt-0 : yypt+1] -//line parser/classad.y:255 +//line parser/classad.y:267 { yyVAL.exprlist = []ast.Expr{} } - case 66: + case 68: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:257 +//line parser/classad.y:269 { yyVAL.exprlist = yyDollar[1].exprlist } - case 67: + case 69: yyDollar = yyS[yypt-1 : yypt+1] -//line parser/classad.y:262 +//line parser/classad.y:274 { yyVAL.exprlist = []ast.Expr{yyDollar[1].expr} } - case 68: + case 70: yyDollar = yyS[yypt-3 : yypt+1] -//line parser/classad.y:264 +//line parser/classad.y:276 { yyVAL.exprlist = append(yyDollar[1].exprlist, yyDollar[3].expr) }