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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ body:
description: Provide a minimal C code example that demonstrates the issue
render: c
placeholder: |
#include "J2735_macros.h"
#include "J2735_api.h"

int main(void) {
uint8_t buf[] = { 0x15, 0xBD, ... };
Expand Down
2 changes: 1 addition & 1 deletion .github/instructions/j2735_design.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The library splits the concept of a "Message" into two layers to map high-level
- Use "Container Structs" (byte arrays) only for pointer sizing, not for member access.
- **Anti-Pattern**: Do not define standard C structs with members for data access.

### 2. The Access Layer (`J2735_macros.h`)
### 2. The Access Layer (`J2735_internal_common.h`)

- **Concept**: Acts as the optical lens to read the data.
- **Usage**:
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ on:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: write
pull-requests: write
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/pr-compliance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ on:
- reopened
- synchronize

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

# Security: Minimal permissions - read PR, write status checks
permissions:
pull-requests: read
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ on:
- 'tools/**'
- '.github/workflows/python.yml'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: write
pull-requests: write
Expand Down
38 changes: 38 additions & 0 deletions tools/j2735_asn1_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2026 Yogev Neumann
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2026 Yogev Neumann
"""
ASN.1 language constants shared across J2735 parsing modules.

Central repository for ASN.1 syntax tokens and patterns
used throughout the J2735 toolchain.
"""

from re import DOTALL, MULTILINE, Pattern
from re import compile as re_compile
from typing import Final

# ASN.1 syntax tokens
ASN1_COMMENT_PREFIX: Final[str] = "--"
ASN1_EXTENSION_MARKER: Final[str] = "..."
ASN1_FIELD_SEPARATOR: Final[str] = ","
ASN1_OPTIONAL_KEYWORD: Final[str] = "OPTIONAL"
ASN1_SEQUENCE_KEYWORD: Final[str] = "SEQUENCE"

# ASN.1 type definition patterns
# Note: [\w-]+ allows hyphens in type names (e.g., Offset-B10, OffsetLL-B12)
ASN1_TYPE_DEF_PATTERN: Final[Pattern[str]] = re_compile(r"^([\w-]+)\s*::=\s*(.+)$", MULTILINE)
ASN1_TYPE_DEF_DOTALL_PATTERN: Final[Pattern[str]] = re_compile(r"([\w-]+)\s*::=\s*(.+)", DOTALL)
12 changes: 2 additions & 10 deletions tools/j2735_c_generator_bitwidth_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"""

from .j2735_c_generator_jinja import create_jinja_env, get_template
from .j2735_spec_parser import ASN1TypeDefinition, J2735Specification
from .j2735_spec_parser import J2735Specification

_BITWIDTH_TEMPLATE_NAME = "bitwidth_constants.j2"

Expand Down Expand Up @@ -58,15 +58,7 @@ def generate_bitwidth_constants(spec: J2735Specification) -> str:
>>> "7U" in code
True
"""
# Collect types with fixed bit-widths
fixed_types: list[ASN1TypeDefinition] = []
variable_count = 0

for _, typedef in sorted(spec.type_registry.items()):
if typedef.uper_bit_width is not None:
fixed_types.append(typedef)
else:
variable_count += 1
fixed_types, variable_count = spec.collect_fixed_width_types()

env = create_jinja_env()
template = get_template(env, _BITWIDTH_TEMPLATE_NAME)
Expand Down
9 changes: 2 additions & 7 deletions tools/j2735_c_generator_size_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"""

from .j2735_c_generator_jinja import create_jinja_env, get_template
from .j2735_spec_parser import ASN1TypeDefinition, J2735Specification
from .j2735_spec_parser import J2735Specification

_TEMPLATE_NAME = "size_constants.j2"

Expand All @@ -57,12 +57,7 @@ def generate_size_constants(spec: J2735Specification) -> str:
>>> "37U" in code # 290 bits -> 37 bytes
True
"""
# Collect types with fixed bit-widths (same as bitwidth generator)
fixed_types: list[ASN1TypeDefinition] = []

for _, typedef in sorted(spec.type_registry.items()):
if typedef.uper_bit_width is not None:
fixed_types.append(typedef)
fixed_types, _ = spec.collect_fixed_width_types()

env = create_jinja_env()
template = get_template(env, _TEMPLATE_NAME)
Expand Down
40 changes: 20 additions & 20 deletions tools/j2735_spec_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,19 @@
from re import compile as re_compile
from typing import Annotated, ClassVar, Final, Self

from .j2735_asn1_constants import (
ASN1_COMMENT_PREFIX,
ASN1_EXTENSION_MARKER,
ASN1_FIELD_SEPARATOR,
ASN1_OPTIONAL_KEYWORD,
ASN1_SEQUENCE_KEYWORD,
)

# These type aliases document constraints that are validated at runtime
# via __post_init__. They help communicate intent to readers and tools.
_PositiveInt = Annotated[int, "Value must be >= 1"]
_NonNegativeInt = Annotated[int, "Value must be >= 0"]

# ASN.1 parsing constants
_ASN1_COMMENT_PREFIX: Final[str] = "--"
_ASN1_EXTENSION_MARKER: Final[str] = "..."
_ASN1_FIELD_SEPARATOR: Final[str] = ","
_ASN1_OPTIONAL_KEYWORD: Final[str] = "OPTIONAL"
_ASN1_SEQUENCE_KEYWORD: Final[str] = "SEQUENCE"
_BITS_PER_BYTE: Final[int] = 8 # 8 bits per byte/octet in OCTET STRING

# J2735-specific constants
Expand Down Expand Up @@ -560,7 +562,7 @@ def from_asn1(cls, raw_def: str) -> Self | None:
return None

body = match.group(1)
is_extensible = _ASN1_EXTENSION_MARKER in body
is_extensible = ASN1_EXTENSION_MARKER in body

# Parse individual alternatives
alternatives: dict[str, str] = {}
Expand All @@ -569,9 +571,7 @@ def from_asn1(cls, raw_def: str) -> Self | None:
type_ref = alt_match.group(2).strip()
# Skip J2735 regional extension fields (SEQUENCE OF RegionalExtension)
# These are variable-length and would break fixed bit-width calculation
if name != _J2735_REGIONAL_FIELD_NAME or not type_ref.startswith(
_ASN1_SEQUENCE_KEYWORD
):
if name != _J2735_REGIONAL_FIELD_NAME or not type_ref.startswith(ASN1_SEQUENCE_KEYWORD):
alternatives[name] = type_ref

if not alternatives:
Expand Down Expand Up @@ -836,7 +836,7 @@ def from_asn1(cls, raw_def: str) -> Self | None:
# Assign values in document order
values = cls._assign_values(all_items)

return cls(values=values, is_extensible=_ASN1_EXTENSION_MARKER in body)
return cls(values=values, is_extensible=ASN1_EXTENSION_MARKER in body)


@dataclass(frozen=True, kw_only=True, slots=True)
Expand Down Expand Up @@ -1293,36 +1293,36 @@ def from_asn1(cls, raw_def: str) -> tuple[Self, ...]:
continue

# Check if this is a standalone comment line (starts with --)
if line.startswith(_ASN1_COMMENT_PREFIX):
if line.startswith(ASN1_COMMENT_PREFIX):
pending_section_comment = line[
len(_ASN1_COMMENT_PREFIX) : # noqa: E203 # Black VS flake8
len(ASN1_COMMENT_PREFIX) : # noqa: E203 # Black VS flake8
].strip()
continue

# Extract inline comment from end of line
line_inline_comment: str = ""
comment_pos = line.find(_ASN1_COMMENT_PREFIX)
comment_pos = line.find(ASN1_COMMENT_PREFIX)
if comment_pos != -1:
line_inline_comment = line[
comment_pos + len(_ASN1_COMMENT_PREFIX) : # noqa: E203 # Black VS flake8
comment_pos + len(ASN1_COMMENT_PREFIX) : # noqa: E203 # Black VS flake8
].strip()
line = line[:comment_pos].strip()

# Split line by field separator to get individual fields
parts = [p.strip() for p in line.split(_ASN1_FIELD_SEPARATOR) if p.strip()]
parts = [p.strip() for p in line.split(ASN1_FIELD_SEPARATOR) if p.strip()]

# Process each field part
for index, part in enumerate(parts):
# Skip extension markers
if part == _ASN1_EXTENSION_MARKER:
if part == ASN1_EXTENSION_MARKER:
continue

# Parse "fieldName TypeName OPTIONAL" or "fieldName TypeName"
tokens = part.split()
if len(tokens) < 2:
continue

is_optional = _ASN1_OPTIONAL_KEYWORD in tokens[2:] if len(tokens) > 2 else False
is_optional = ASN1_OPTIONAL_KEYWORD in tokens[2:] if len(tokens) > 2 else False

# Inline comment only applies to the LAST field on the line
inline_comment = line_inline_comment if index == len(parts) - 1 else ""
Expand Down Expand Up @@ -1685,12 +1685,12 @@ def from_asn1(cls, raw_def: str) -> Self | None:
True
"""
# Must start with SEQUENCE keyword (not CHOICE, etc.)
if not raw_def.strip().startswith(_ASN1_SEQUENCE_KEYWORD):
if not raw_def.strip().startswith(ASN1_SEQUENCE_KEYWORD):
return None
fields = SequenceField.from_asn1(raw_def)
if not fields:
return None
is_extensible = _ASN1_EXTENSION_MARKER in raw_def
is_extensible = ASN1_EXTENSION_MARKER in raw_def
return cls(fields=fields, is_extensible=is_extensible)


Expand Down
Loading